|
|
@@ -17,7 +17,7 @@ The `dsh-headless` bundle owns an opt-in machine-readable run surface. The defau
|
|
|
Three additions extend the app-owned command line that [Apps own their command lines](../../archived/architecture/2026-08-06-app-owned-command-line.md) established:
|
|
|
|
|
|
- `--json` replaces the stdout payload with newline-delimited JSON run events. Reasoning becomes an event instead of stderr output, so stderr carries only `dsh:` diagnostics.
|
|
|
-- `--session-id <id>` selects the exact session identity: adopt the persisted session when it exists, otherwise create it. Without the flag the run mints `session-<uuid>` as before.
|
|
|
+- `--session-id <id>` selects the exact session identity: adopt the persisted session with that id, and fail when no such log exists. Without the flag the run mints `session-<uuid>` as before.
|
|
|
- The task text also arrives on stdin when no positional task is present, or when the positional is `-`.
|
|
|
|
|
|
A per-run `--model` override is deliberately out of scope; the composition default stays authoritative.
|
|
|
@@ -63,18 +63,18 @@ Projection rules:
|
|
|
|
|
|
The runtime owns identity. A run without `--session-id` mints `session-<uuid>` and reports it in the first event. A supervisor persists that value and passes it back on the next wake.
|
|
|
|
|
|
-`--session-id <id>` is adopt-or-create: observe the persisted session, resume it when it exists, create it otherwise. Create-only would fail the second run, because the JSONL store rejects an existing log id ([session persistence](../../implemented/architecture/2026-06-14-session-persistence.md)). The id is opaque, so the runner validates non-emptiness on the trimmed value and passes the caller's exact string through, whitespace included.
|
|
|
+`--session-id <id>` is adopt-only: observe the persisted session and resume it, failing when the log does not exist. The first wake omits the flag, so the runtime mints the identity and reports it in the `session` event; every later wake names that value and continues the history. A requested id with no stored log is an error rather than a fresh conversation, so a mistyped or stale id cannot silently open an empty history the caller believes it is resuming, and the JSONL store's refusal of an existing log id ([session persistence](../../implemented/architecture/2026-06-14-session-persistence.md)) is never on this path. The id is opaque, so the runner validates non-emptiness on the trimmed value and passes the caller's exact string through, whitespace included.
|
|
|
|
|
|
Adoption compares the persisted session's recorded cwd with the process cwd, since sessions are organized per project directory ([project session directories](../../implemented/architecture/2026-07-24-project-session-directories.md)). A mismatch exits 1 with a `dsh:` diagnostic instead of silently continuing a conversation rooted elsewhere, and a session that recorded no cwd is rejected for the same reason. A session running under an agent preset is rejected because this bundle composes no preset roster: resuming it here would run it under the headless tools and prompts instead of the composition its log records. The check reads the preset the log currently records — the creation header advanced by any `agent-preset/selected` event — because a blank session may switch preset after creation while the header stays a creation fact, and a malformed selection record fails closed rather than reading as no preset. A session linked to a parent or subagent — including a user fork — is rejected. All checks run when a live Agent already holds the requested id, and again after the runner's idle wait, so a live identity cannot bypass them and a preset selected in that window is still rejected. A whitespace-only `sessionId` is rejected on both the CLI and the direct-config path. Two live processes cannot write one id; the store's write lease already rejects the second writer. The runner reads the observation through the composed `sessionQuery` service and fails loudly when `--session-id` is requested without it — including when a live Agent already holds the id, because a later process has to find it — or when the requested identity would lack the `sessionPersistence` service that makes it durable; a live identity must also carry a stored record, because one registered only in memory would flush nothing.
|
|
|
|
|
|
## Consequences
|
|
|
|
|
|
-What landed: `src/startup.ts` parses `--json` and `--session-id <id>`, treats an absent or `-` task as "read stdin", and raises the usage error only when stdin is a terminal. `src/index.ts` resolves the task, adopts or creates the exact session, and wires either the stderr reasoning projection or the new `src/json-stream.ts` projection. `cordis.patch.yml` forwards the two new settings. `package.json` publishes the shared `lib/json-stream-*.js` chunk both entries import, so the installed tarball loads.
|
|
|
+What landed: `src/startup.ts` parses `--json` and `--session-id <id>`, treats an absent or `-` task as "read stdin", and raises the usage error only when stdin is a terminal. `src/index.ts` resolves the task, adopts the named session — or mints a fresh identity when the flag is absent — and wires either the stderr reasoning projection or the new `src/json-stream.ts` projection. `cordis.patch.yml` forwards the two new settings. `package.json` publishes the shared `lib/json-stream-*.js` chunk both entries import, so the installed tarball loads.
|
|
|
|
|
|
- Default mode is unchanged: a text-only run writes one final assistant line to stdout and nothing to stderr, and exit status still follows the terminal reason.
|
|
|
- `--json` stdout parses line by line as JSON, starts with `session`, ends with `final`, and contains no plain text. Stderr carries no reasoning in this mode.
|
|
|
- A step that retries publishes `text` and `thinking` only for the attempt that commits, so a discarded attempt leaves no trace in the stream.
|
|
|
-- Two consecutive runs with the same `--session-id` share history. A run whose cwd differs from the persisted session, that recorded no cwd, that is a subagent or forked session, that runs under an agent preset, that carries a malformed preset record, or whose live identity has no stored record, exits 1 with a diagnostic, whether the identity is live or persisted.
|
|
|
+- Two consecutive runs with the same `--session-id` share history, and a requested id with no stored log exits 1 before the task runs. A run whose cwd differs from the persisted session, that recorded no cwd, that is a subagent or forked session, that runs under an agent preset, that carries a malformed preset record, or whose live identity has no stored record, exits 1 with a diagnostic, whether the identity is live or persisted.
|
|
|
- A piped task with no positional task is honored, a whitespace-only positional is rejected instead of consuming the pipe, and an interactive invocation without a task still fails with the usage error.
|
|
|
- Unit coverage lands in `packages/bundle/headless/tests/startup.spec.ts`, `tests/headless.spec.ts`, and `tests/json-stream.spec.ts`. The product headless profile expectation test in `apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts` covers both output modes end to end.
|
|
|
|
|
|
@@ -91,10 +91,10 @@ Deferred and open:
|
|
|
|
|
|
**Dump raw session events.** They repeat the assembled message beside its deltas, echo `user/message`, and include internal events. Measured on one prompt, pi's delta stream produced 84 lines and 11.7 KB against opencode's 3 lines and 962 B, with roughly a quarter of pi's bytes spent repeating one message across `message_end`, `turn_end`, and `agent_end`.
|
|
|
|
|
|
-**A long-lived SDK process instead of one process per wake.** The SDK already speaks structured events and create-or-adopt identity, but it replaces the one-process-per-wake model the supervisor is built on. Measured cold start for the headless profile is about 0.45 s warm and 1.2 s cold, small against a real turn.
|
|
|
+**A long-lived SDK process instead of one process per wake.** The SDK already speaks structured events and an explicit session identity, but it replaces the one-process-per-wake model the supervisor is built on. Measured cold start for the headless profile is about 0.45 s warm and 1.2 s cold, small against a real turn.
|
|
|
|
|
|
**Let the supervisor mint the session id.** Identity belongs to the runtime that owns the log. The supervisor records what the first event reports.
|
|
|
|
|
|
-**Create-only `--session-id`.** The second wake would fail against the existing log, which is the opposite of the continuity the flag exists for.
|
|
|
+**Adopt-or-create `--session-id`.** Creating the requested id when no log exists would let a mistyped or stale id silently open an empty history that the supervisor believes it is continuing; the first wake already omits the flag and reads the minted id from the `session` event, so no caller needs `--session-id` to create.
|
|
|
|
|
|
**Task from argv only.** Long prompts exceed `ARG_MAX` and expose the prompt in the process list.
|