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
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:
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.
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.source> (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.source 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.
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 350 KB 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.yamlis 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
- Filesystem Layout — generic top-level files.
- Job Session Object — wire vs local field layout, the
artifactsandpolicyblocks. - Concepts — the list+watch discovery model this page’s
not_mine.yamlsupports. - Configuration
- Recovery

