> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blobhub.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Filesystem Layout

> session_agent_harness per-section subtree inside a profile state tree

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](/worker/filesystem).

## Tree

```
~/.blobhub-worker/profiles/{profile}/jobs/
└── session_agent_harness-{session_id}/
    ├── section.yaml             # per-section runtime state (event-poll cursor, attachment)
    ├── not_mine.yaml            # cache of aliases classified as not this worker's work
    ├── logs/
    │   └── section.log
    └── threads/
        └── {alias, as nested directories}/
            ├── thread.yaml      # per-thread state (agent state, agent_session_id, cursors)
            └── logs/
                └── thread.log   # full untruncated agent stdio + tool I/O
```

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](/worker/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:

```
jobs/session_agent_harness-{session_id}/threads/projects/<pid>/items/<iid>/runs/<n>/thread/
    thread.yaml
    logs/
        thread.log
```

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.

```yaml theme={null}
name: my-project
job_type: session_agent_harness
session:
  org_id: org_01J...
  blob_id: blb_01J...
  revision_id: rev_01J...
  session_id: ses_01J...               # or an alias — see note below
  session_prefix: ""
attachment:
  status: attached                    # attached | refused
  attached_at: 2026-05-27T12:35:01Z
  error:
    code: null
    message: null
events:
  last_processed_at: 2026-05-27T12:40:00Z
```

`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.

```yaml theme={null}
aliases:
  - projects/p1/graph
  - projects/p1/items/i1/record
```

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](/worker/session-agent-harness/thread-object))
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](/worker/concepts) 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](/worker/session-agent-harness/thread-object) 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](/worker/session-agent-harness/thread-object).

```yaml theme={null}
alias: my-feature-x
session: { ... }
workspace:
  agent_type: claude_code         # local layout keeps agent_type here (wire uses agent.type)
  work_folder: /Users/me/projects/foo
agent:
  state: active                   # "" | pending | active | completed | failed
  agent_session_id: ag_sess_...   # local-only resume pointer (never on the wire)
  error:                          # local-only failure detail (surfaces via thread_failed)
    code: null
    message: null
items:
  last_consumed:
    item_id: itm_01J...
    created_at: 2026-05-27T12:50:00Z
  last_posted:
    item_id: itm_01J...
    created_at: 2026-05-27T12:50:05Z
```

### `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](/worker/session-agent-harness/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

* [Filesystem Layout](/worker/filesystem) — generic top-level files.
* [Job Session Object](/worker/session-agent-harness/thread-object) — wire vs local field layout, the
  `artifacts` and `policy` blocks.
* [Concepts](/worker/concepts) — the list+watch discovery model this page's `not_mine.yaml` supports.
* [Configuration](/worker/configuration)
* [Recovery](/worker/session-agent-harness/recovery)
