Skip to main content
This page covers the per-section files the session_agent_harness job type writes under the profile’s own jobs/. For the generic layout (identity.yaml, the shared credentials file, instance.yaml, config.yaml, logs/{instance_id}/worker.jsonl) see Filesystem Layout.

Tree

The agent runs with cwd = workspace.work_folder; the worker keeps no separate agent-session directory on disk. Session resume is SDK-managed and keyed by agent_session_id (recorded in thread.yaml). job_id is deterministic: session_agent_harness-{session_id}, always keyed on the resolved session id — even when config.yaml names the session by alias (see Configuration). This guarantees stable paths across restarts, reinstalls, and config edits — and namespaces this job type away from any future ones.

Nested thread state for path aliases

A thread’s alias can itself contain /, which is what an application with a path-structured namespace hands off, e.g. projects/<pid>/items/<iid>/runs/<n>/thread. Each /-separated segment becomes one nested directory, so that alias lands at:
A flat alias like my-feature-x (no /) lands exactly where it always has: threads/my-feature-x/thread.yaml. Both shapes coexist under the same threads/ root without collision — directory and file names are just alias segments, so the state for alias a and the directory for alias a/b don’t conflict (threads/a/thread.yaml vs. threads/a/b/thread.yaml).

File-by-file

section.yaml — per-section runtime state

Persisted by the worker. Recovery on next start re-reads this.
session.session_id here is recorded exactly as configured — a uuid, or an alias if that’s how config.yaml names it. It is not necessarily the resolved id used to derive job_id (the directory name one level up): if you configure an alias, this field keeps showing that alias while the enclosing directory is always named after the resolved uuid.

not_mine.yaml — classification cache

An attach-time optimisation: the set of aliases under this section’s watched prefix that were downloaded once and found not to be the worker’s own work (a graph, a record, a published artifact — anything that isn’t a thread-typed object carrying an agent block or an instance.state). A later attach skips the download for every alias already in this set.
This is purely a cache and is never consulted in steady state: once attached, the section re-classifies from the type each session_object_modified event carries, so a stale entry here can delay picking up a newly-relevant alias by at most one attach, and can never suppress it permanently. Every artifact alias the worker publishes itself (see Job Session Object → Artifacts) is added here as it’s uploaded — artifacts are the dominant volume in a busy namespace, so this is what keeps a cold attach to a long-lived instance bounded. See Concepts → Namespace scoping & discovery for the full discovery model this cache supports.

logs/section.log

Section-scoped JSON-line log: poll cadence, event dispatch, thread discovery.

The local outbox — inside work_folder, not under jobs/

When a thread’s envelope carries an artifacts block, the worker creates <work_folder>/<artifacts.outbox> (with parent directories) the moment that thread activates — so the folder exists before the agent’s first turn looks for it. Unlike everything else on this page, the outbox lives inside the repository checkout (work_folder), not under ~/.blobhub-worker/profiles/{profile}/jobs/. That is a constraint, not a layout preference: under approval or guarded, Codex runs its shell and file-edit tools inside a workspace-write OS sandbox scoped to work_folder, so a write outside it doesn’t happen silently — it escalates into a fileChange approval request first, the same request an out-of-workspace write anywhere else would trigger. An unattended publish step can’t depend on someone answering that request every turn, so the worker sidesteps the question entirely by keeping the outbox inside the work_folder the sandbox already grants. The worker makes the folder it creates self-ignoring: it writes a .gitignore containing a single * into the outbox root. work_folder is typically a git checkout, so without this, scratch output would show up in git status and could be swept into an agent’s own git add -A. A single * ignores the directory’s entire contents — including the .gitignore file itself — needs no cooperation from the repository’s own ignore rules, and works no matter what artifacts.outbox turns out to be. Writing it is idempotent and confined to the outbox root the worker itself created; the worker never deletes, moves, or rewrites a file already there. See Job Session Object → Artifacts for the publish contract and what happens to the files once they’re in this folder.

The local inbox — also inside work_folder, not under jobs/

The mirror image of the outbox above: the worker resolves an inbox directory for every thread at activation (three-tier — thread, then worker config, then the codebase default .blobhub/inbox; see Job Session Object → Attachments and Configuration → attachments) and creates it before that thread’s first turn runs. Incoming file attachments land at:
— one subdirectory per thread item, so files from different items never collide even when they share a name. ensure_inbox is the same containment-checking, self-.gitignore-writing routine artifacts uses for the outbox (a single * written into the inbox root), for the same reasons: the inbox lives inside a git checkout the agent can git add -A, and an escaping path is refused rather than resolved. Because both folders can live inside the same work_folder, the outbox walk that artifacts publishes from excludes the inbox subtree — files the worker itself downloaded for the agent to read are not the agent’s own output, and republishing them back as artifacts would be a report about the worker’s own machinery, not the agent’s work. The on-disk inbox is the durable copy, not necessarily how the model first sees a file. When the item’s block order puts an image between two runs of text, the worker also hands that image to the harness natively, in place — Claude Code and Codex receive it at its composed position; Antigravity’s machine input takes text only, so there every file goes by path — so the path below is the fallback and the record, reached for a non-image file or whenever native delivery isn’t taken. See Job Session Object → Attachments. <safe-name> is never the attachment’s name verbatim — a client-supplied name arrives over the wire with no length limit and no guarantee it’s even a valid path segment, so it’s sanitized before it ever touches disk:
  1. Backslashes normalize to forward slashes, then only the last non-empty path segment survives — a name like ../../etc/passwd or sub/dir/file.txt contributes only passwd or file.txt.
  2. Every character outside [A-Za-z0-9._@-] becomes _, and any leading dots are stripped (no hidden-file, no ..).
  3. An empty result (a name that sanitized down to nothing) falls back to file-{ordinal}, the file’s 1-indexed position among that item’s attachments.
  4. The result is capped at 128 characters, preserving a recognizable extension: an extension of up to 16 characters is kept whole and the stem in front of it is trimmed to fit; anything longer than that, or a name with no . at all, is truncated flat instead.
Two attachments on the same item that sanitize to the same name don’t overwrite each other: the second gets an ordinal prefix, looped until unique. See Job Session Object → Attachments for the resolution and containment rules, and Thread Items for what happens when a file can’t be fetched at all.

The managed hook config — antigravity only, and only while threads run

The third thing the worker writes inside a work_folder, and the only one that is not the worker’s own scratch space. An antigravity thread running under permissions: guarded or approval is gated by a PreToolUse hook (see Job Session Object → Antigravity: the gate is a hook in another process), and the Antigravity CLI discovers hooks from the workspace root and nowhere else. So the config has to live in the repository you pointed the thread at:
It is present only while such a thread is active, and it is shared: several threads may legitimately run against one work_folder, so the worker refcounts the file per resolved path — written when the first one starts, restored when the last one leaves, not written and removed per turn. It never clobbers a file you already have. .agents/ is a workspace convention you may already use for rules, skills, or agents of your own. An existing hooks.json is read, the worker’s one named key is merged into it, and on release the file is restored byte-for-byte to what it was — or deleted, if it was the worker’s alone. The .agents/ directory itself is removed only when the worker created it and nothing else has since moved in. Two things to expect while it is there:
  • It shows up in git status. work_folder is usually a checkout, and unlike the outbox and inbox this file is not self-ignored — an ignore rule would be a change to your repository that outlives the thread. Add .agents/hooks.json to your own ignore rules if the noise matters.
  • The hook command carries its own fallback, and that is load-bearing rather than defensive dressing. Every failure mode of an Antigravity hook fails closed: a command that cannot be run blocks the tool call. A hooks.json left behind by a worker that crashed or was uninstalled would therefore break every tool call in that folder — including in your own, unrelated Antigravity sessions. The configured command is command -v blobhub-worker … && blobhub-worker agy-gate || echo '{"decision":"allow"}', which degrades that specific case to inert instead of bricking the directory. A stale file is still worth deleting; it just is not an emergency.

threads/{alias}/thread.yaml — per-thread state

The worker’s local, authoritative-for-recovery view of a thread, plus the inbound/outbound item cursors. This local layout differs from the wire envelope: it keeps workspace.agent_type and agent.state, and the agent_session_id resume pointer and error block are local-only — they never appear on the wire. The mapping is documented on Job Session Object.

threads/{alias}/logs/thread.log

Captures the full untruncated agent stdio + tool I/O. When the worker truncates a thread item to fit the 60 KiB budget, the local log retains the full payload — see Thread Items.

Why both server-side metadata and thread.yaml?

  • The server-side envelope is authoritative for handoff. Anyone with API access (web UI, SDK, another tool) reads it to see the agent state machine.
  • thread.yaml is authoritative for local recovery. The worker writes it before any externally observable side-effect, so a crash-restart finds a truthful local record.

See also