Session and work-claim model
Schemas and ownership rules for harness sessions and work claims. Read timestamp discipline for timestamp conventions.
Table: harness_sessions
yoke universe prune --dry-run previews disposable completed detail and
settled agent-only message graphs; it preserves session identity, unresolved
work and every human-involved message. See retention.
Tracks active harness sessions and their registered identity. Sessions with ended_at IS NULL are active. A persisted active work claim protects its live holder regardless of heartbeat age, park mode, process presence, or relay restart. The scheduler reports that claim as claimed_by_other_live; stale-session reclaim refuse to release it with reason=active_work_claim. Explicit release, completion, cancellation, or authorized termination settles ownership. Parking records a delivery wait for wake routing but does not decide retention. Derived liveness (sessions.list, yoke_core.domain.session_staleness.session_liveness) is active, waiting, stale, or ended: a live work-claim holder never reads stale; mode parked or turn posture waiting declares an intentional pause shown as parked (waiting on the wire). Wake and message routing key on the activity-only activity_liveness instead. Each roster row carries reclaimable, exactly equivalent to liveness=stale. The shared session_automatic_reclaim.automatic_reclaim decision applies holdings retention, in-flight and episode-progress rules to roster, preview and execution; execution rechecks and explains newly protected skips. Activity age is a separate diagnostic.
Stale-session thresholds (canonical reference). The reclaim windows are config-tunable, not code literals. The sweep first selects an occupancy tier:
session_stale_ttl_minutes(default20) — the short tier for a session with no active work claim, no session-owned strategy-document claim, and no session-owned coordination lease. One base applies on every harness; transient stop signals attempt only a non-destructive empty-session end.session_stale_ttl_with_holdings_minutes(default1440) — the minimum tier for session-owned strategy-document locks. Active work claims have no inactivity bound. A steering seat frees another session's claim without ending it:yoke claims work release --claim-id N --reason R.yoke sessions terminate SESSION-ID --reason Rrequires the caller to hold a live steering seat on any project in the org; the target may be on any project and may itself hold steering seats. Termination releases every claim the target holds, including its steering seats and their paired strategy-document locks. Without a seat it refusesTERMINATION_AUTHORITY_REQUIRED; acquire one withyoke claims steering acquire --project P --reason TEXTor route viayoke say --steering. Desktop surfaces refuseTERMINATION_OPERATOR_SURFACEunchanged, listing claim ids to release. It refuses a session a wake is resuming asTERMINATION_RESUME_IN_FLIGHT— awake_relayattempt started at or after the recorded native exit that is still open or resumed a process not yet reported exited, within the resume-custody quiet window — because a headless worker's process exits between turns and a message resumes it;--allow-resume-in-flightoverrides for a deliberate restaff or evidence it cannot resume. Logical stop and physical exit are separate: one bounded TERM/KILL attempt (2-second TERM grace, then up to 2 seconds to verify exit; Claude job-stop command timeout 20 seconds) retains custody on missing/denied/unverified results. Inspect custody and permissions, then repeat the explicit termination command to retry a failed physical reap; pending and verified successful requests are idempotent, with prior failed evidence retained in the existing reap row. Resolver:yoke_core.domain.sessions_analytics_coreowns both source thresholds. The sessions-card stale-eligible time is absent while an active work claim protects its holder.
Long commands and sparse tool boundaries. Registered Command cases and watcher-backed suites refresh the owning session and claims while the child runs. The persisted work claim protects a silent or crashed holder until explicit release or terminal action.
session_id TEXT PRIMARY KEY -- globally unique session ID (from contract)
executor TEXT NOT NULL -- executor identity (e.g., claude-code, codex)
provider TEXT NOT NULL -- model provider (e.g., anthropic, openai)
model TEXT -- provider-attested served model; NULL until attested and landed
reasoning_effort TEXT -- served effort, distinct from requested_reasoning_effort
context_window_tokens INTEGER -- served window, distinct from requested_context_window_tokens
usage_totals TEXT -- measured usage document; parse through session_usage_facts
execution_level TEXT NOT NULL DEFAULT 'primary' -- session grouping identity stamped by session-routing settings
capabilities TEXT DEFAULT '[]' -- JSON array of capability tags
project_id INTEGER REFERENCES projects(id) -- registered project; NULL outside any project
org_id INTEGER REFERENCES organizations(id) -- authority org; set only outside any project
workspace TEXT NOT NULL -- absolute path to working directory
mode TEXT DEFAULT 'wait' -- wait (not parked) or parked; scheduling posture only, never authority
offered_at TEXT NOT NULL -- ISO 8601 when session was registered
last_heartbeat TEXT NOT NULL -- ISO 8601 of last heartbeat
native_process_gone_at TEXT -- when the death was first seen, not when last reported
native_process_gone_evidence TEXT -- bounded JSON evidence from local records
ended_at TEXT -- NULL while active; set when session ends
offer_envelope TEXT -- checkpoint and skip-memory telemetry JSON (optional)
actor_id INTEGER -- registered acting identity; legacy/additive storage permits NULL
Session scope. CHECK harness_sessions_scope_exactly_one requires exactly one of project_id and org_id. session_message_recipients, session_termination_reaps and session_evidence_fetches carry the same pair and exactly-one check, so a recipient, reap or evidence fetch for a session outside any project is owned by its org. Registration resolves the org from the authenticated authority and actor membership, never from the working directory, and the stored scope never changes; existing project rows keep org_id NULL.
Actor binding. A session a person opened binds that person's identity. A session another session LAUNCHED binds the launching actor, transitively — the launch's requester_actor_id, read at registration through the authenticated launch side channel, outranks both the machine's OS login and a relay's bearer-token actor, and an unreadable launch refuses registration rather than falling back to either. Every action one session takes on another (message, wake, keep-alive, terminate, launch) writes a SessionActionPerformed row into the TARGET session's history carrying the ACTING actor, and is role-checked against the target project (for a session outside any project, its own actor or that org's role) by yoke_core.domain.session_action_authority: project membership for messaging, waking, holding alive, and terminating a launched worker; project owner or org admin for terminating another actor's interactive session. Contract: docs/archive/decisions/session-actor-follows-the-person.md.
The process-gone columns record machine evidence without ending a claim holder. The reporting machine keeps its record until the control plane ends the session, so a retained session is reported again every poll; the stamp is the native's own exit time where the machine read one (correcting an earlier report's guess), else the time this session's first report about that same process earned, and a report about a different, older process is dropped whole rather than overwriting newer evidence, so later activity can still supersede it. sessions.list.native_process exposes the observation until a later heartbeat, tool call, or episode start supersedes it, and drops it when the evidence measured exit_code 0 under a session that declared a wait — parked, turn_posture waiting whose turn_posture_at is present and not before the current episode_started_at, or holding an item armed in the merge queue — because a finished headless command is that wait rather than a disappearance; a non-zero exit and an exit nobody measured still read as gone. sessions.list.native_process_exited_at keeps that unsuperseded exit's time even when a declared wait explains it, so a reader asking whether any process is running still sees a parked session's clean exit. The offer_envelope column retains checkpoint and skip-memory telemetry.
Turn ends and project refusals. A relayed Stop or SessionEnd still records waiting posture for an existing session owned by the authenticated actor when checkout project authorization denies the request. The refusal retains its project restrictions and performs no registration or cleanup. A policy Stop hold asking the agent to continue does not stamp waiting. Codex CLI's relay also reads the newest clean or failed task_complete from the rollout tail; later prompts or tool calls invalidate it, and an older observation cannot overwrite a newer running posture. Waiting routes the next message through native resume. If a posture write fails, the hook refusal names the failure and recovery alongside the project refusal; restore checkout authorization and retry through yoke hook evaluate.
Chain checkpoint: The checkpoint API persists handler outcome and work identity for recovery and telemetry. Terminal closeout consumes a matching checkpoint. Checkpoint budget does not block session ending; idle cleanup preserves active claims, document locks, keepalive holds, and pending launch or wake delivery. Read through yoke sessions checkpoint-read.
Probe sessions are audit rows. A probe has ended within PROBE_MAX_LIFETIME_SECONDS (30) of offered_at, has tool_call_count = 0, and has no first_user_prompt_at stamp. The stamp is written on the session row at the prompt boundary. Probe rows remain in harness_sessions for audit; operator-facing session lists and steering counts exclude them through the shared session_probe predicate. Live sessions are never classified as probes.
Indexes: idx_harness_sessions_level(execution_level), idx_harness_sessions_heartbeat(last_heartbeat).
Shell access: the Python harness-session CLI (begin|touch|end|get|list|stale|reclaim). API: /v1/sessions endpoints.
Launch binding. The opening hook binds its exact session with its launch-specific
attestation. Folder and time proximity never establish ownership. Relay reports
preserve that binding and delivery receipt. Item-assigned launches also require
the worker to claim that exact item before the deadline. Missing entry fails as
launch_work_claim_missing, and the failed launch alone no longer staffs the item.
Read yoke session-control launch get LAUNCH-ID --json for binding, receipt and
recovery evidence before retrying. Itemless launches need acknowledgement only.
Table: work_claims
Tracks active harness-session occupancy through one canonical target pair: target_kind names the target vocabulary and scope stores the exact kind-specific JSON object. Claims with released_at IS NULL are active.
- Item (
target_kind='item'):scope={"item_id":N}. - Epic task (
target_kind='epic_task'):scope={"epic_id":N,"task_num":N}. - Process (
target_kind='process'):scope={"process_key":K,"conflict_group":G}. STRATEGIZE and FEED sharestrategy-control-plane:<project>and therefore conflict. - Steering (
target_kind='steering'):scope={"project_id":N}or{"project_id":N,"document":SLUG}; N identifies the document's owning project for a document seat. Distinct non-overlapping document seats may coexist. Paired locks remain instrategy_doc_claimsthroughsteering_claim_id.
Domain validation requires each kind's keys and permits steering's optional document key. Storage has no specialized target, typed-owner, or registration-provenance columns.
id INTEGER PRIMARY KEY
session_id TEXT NOT NULL -- FK to harness_sessions.session_id
target_kind TEXT NOT NULL -- item|epic_task|process|steering|migration_serialization|qa_admission|route_qualification (deploy_serialization: retired, release by id)
scope TEXT NOT NULL -- canonical JSON object; exact shape is validated by target_kind
claim_type TEXT NOT NULL DEFAULT 'exclusive' CHECK(claim_type='exclusive')
claimed_at TEXT NOT NULL
last_heartbeat TEXT NOT NULL
released_at TEXT
release_reason TEXT -- completed, released, reclaimed, handed_off, expired, session_ended
reason TEXT -- verbatim acquisition rationale
reason_intent TEXT -- canonical acquisition intent
release_reason_intent TEXT -- caller's release intent
Indexes: idx_work_claims_session(session_id), idx_work_claims_session_released(session_id, released_at), and idx_work_claims_heartbeat(last_heartbeat).
Active-claim exclusivity invariants — these partial unique indexes, each scoped to released_at IS NULL so historical released overlap rows remain queryable evidence:
idx_work_claims_active_item ON work_claims(scope) WHERE released_at IS NULL AND target_kind='item'.idx_work_claims_active_epic_task ON work_claims(scope) WHERE released_at IS NULL AND target_kind='epic_task'.idx_work_claims_active_process_conflictindexesscope.conflict_groupwhere the process claim is active.idx_work_claims_active_steering ON work_claims(scope) WHERE released_at IS NULL AND target_kind='steering'.
Item and epic-task partial unique indexes prevent concurrent connections from leaving two unreleased claims on one work unit. claim_work retains WHERE NOT EXISTS for readable holder lookups. A losing writer raises SessionError("ALREADY_CLAIMED"), naming the winning session id.
A steering seat covers a scope, not a project. Its scope is {"project_id": N} for a whole project, or {"project_id": N, "document": "SLUG"} for one strategy document owned by that project. Two seats coexist unless their scopes overlap: two documents in one project are two seats, while a project seat overlaps only CURRENT-PLAN document steering of that project. A refusal names the holder's actor, machine, and session, and points at both yoke claims steering list --project P --active-only and taking a seat on a different document. Steering is therefore the one kind whose exclusivity is not an index — two overlapping scopes are different JSON objects, so no unique index on scope can reject the pair. Its storage-level layer is the SELECT ... FOR UPDATE on the project row that acquire takes before it evaluates overlap, which serializes every steering acquire in one project; the index on scope remains as the narrower guarantee that one exact scope has one live row. A project seat covers its unlinked items and CURRENT-PLAN members, excluding other document-linked items even without a live document seat; a document seat covers items linked to that exact owning-project/document identity in item_strategy_docs, including items executing in other projects — the link strategy.execution.link writes, and the one items.create writes when intake names a strategy_doc. Membership is read live, so a link written after a message was sent still decides which seat the message is now the business of. The fleet report for a document seat lists only its items; delivery-plane and machine facts (unregistered launches, launchable surfaces, plan limits) stay project-wide because delivery and machine capacity are shared by every seat. Steering acquisition locks the project row, then creates the seat and, for a document seat, that document's lock in one transaction. A document conflict rolls back the seat. A project-wide seat may lock a standing plan with --plan-doc; without that flag it locks no document. Releasing one seat releases only its paired document lock, even when the session holds another seat in the same project. Steering release and stale-session reclamation release each pair together, while direct release of the paired document is refused until the seat leaves.
Shell access: item/process targets use yoke claims work; steering uses yoke claims steering acquire --project P [--doc SLUG | --plan-doc SLUG] [--reason TEXT] (--doc narrows the seat to that document's linked items; --plan-doc locks the standing plan while the seat covers the whole project; neither flag covers the project and locks nothing), list [--project P] [--active-only] (which names each seat's scope, holder actor, and machine), and release CLAIM_ID --reason TEXT. All dispatch through /v1/functions/call. claims.steering.list answers for one project, so a caller asking which seats exist across every project it can see reads sessions.steering_groups.list (yoke sessions steering-groups list): no inputs, one row per live seat, carrying the seat's session id under the same steering_group_session_id name the session roster spells it on every row that seat covers. Visibility is deliberately the OR the roster applies, because a session steers a project it did not start in — a seat is visible when the project it steers is visible, or when the session holding it lives in a visible project. yoke_core.domain.sessions_steering_groups_read.live_steering_group_session_ids is the read, over the same steering_scope_coverage.live_steering_claims rule the roster's own projection uses, so a released claim and an ended holder are seats in neither. The app's card tint ranks groups by sorted seat id; this read avoids fetching the complete roster twice per mount.
Steering fleet report
What a steering session cannot see from inside its own turn: available work, quiet claim holders, and four failures that arrive as silence, composed server-side and appended to the messages that session already receives. See Steering report.
Live claim-holder lookup
The canonical recipe for "which session currently holds the work claim on PREFIX-N?" is the registered read (function id claims.work.holder_get):
yoke claims work holder-get PREFIX-N
It returns the active work_claims row (released_at IS NULL) — claim_id, holder session_id, target_kind, scope, claimed_at, and last_heartbeat — in one call. Item lookup matches target_kind='item' plus canonical scope={"item_id":N}; do not query removed specialized or owner columns. The same recipe is the canonical example in the generated agent context packet (yoke_core.domain.schema_api_context, topic claims).
Inside the Yoke source repo only, the in-tree python3 -m yoke_core.hooks.sessions_cli who-claims <item-id> helper additionally joins the owning harness_sessions row (surfacing executor and mode) and accepts --current-episode. That module is not importable from an installed Yoke, so it is an operator/debug recipe for this repo, never a portable one.
work_claims is the active session occupancy primitive — including which session currently steers a project or strategy-doc scope. It is NOT path/file ownership truth (that lives in path_claims) and NOT a dangerous shared-operation lock (those are the shared-operation coordination claims). Process path claims attribute back to their owning process work-claim through path_claims.owner_work_claim_id.
Related
Unregistered failures leave the report after item completion, a later registered launch, or operator settlement. Exit evidence renders as native_exited; see launch recovery.