sept-agent 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CONTRIBUTING.md +8 -4
  2. package/README.md +33 -8
  3. package/dist/src/cli/commands.js +4 -0
  4. package/dist/src/cli/commands.js.map +1 -1
  5. package/dist/src/cli/hands-docker.d.ts +7 -0
  6. package/dist/src/cli/hands-docker.js +182 -0
  7. package/dist/src/cli/hands-docker.js.map +1 -0
  8. package/dist/src/cli/hands-finish.d.ts +2 -0
  9. package/dist/src/cli/hands-finish.js +164 -0
  10. package/dist/src/cli/hands-finish.js.map +1 -0
  11. package/dist/src/cli/hands-owner.d.ts +10 -0
  12. package/dist/src/cli/hands-owner.js +83 -0
  13. package/dist/src/cli/hands-owner.js.map +1 -0
  14. package/dist/src/cli/hands-record.d.ts +33 -0
  15. package/dist/src/cli/hands-record.js +164 -0
  16. package/dist/src/cli/hands-record.js.map +1 -0
  17. package/dist/src/cli/hands.d.ts +3 -0
  18. package/dist/src/cli/hands.js +169 -0
  19. package/dist/src/cli/hands.js.map +1 -0
  20. package/dist/src/cli/options.d.ts +1 -0
  21. package/dist/src/cli/options.js +2 -1
  22. package/dist/src/cli/options.js.map +1 -1
  23. package/dist/src/cli.js +1 -1
  24. package/dist/src/cli.js.map +1 -1
  25. package/dist/src/execution/hands-process-owner.d.ts +2 -4
  26. package/dist/src/execution/hands-process-owner.js +3 -1
  27. package/dist/src/execution/hands-process-owner.js.map +1 -1
  28. package/dist/src/execution/index.d.ts +4 -0
  29. package/dist/src/execution/index.js +4 -0
  30. package/dist/src/execution/index.js.map +1 -1
  31. package/examples/incident-investigator/README.md +4 -4
  32. package/examples/incident-investigator/package.json +1 -1
  33. package/examples/node-app/README.md +4 -4
  34. package/examples/node-app/package.json +1 -1
  35. package/package.json +1 -8
  36. package/src/cli/commands.ts +4 -0
  37. package/src/cli/hands-docker.ts +198 -0
  38. package/src/cli/hands-finish.ts +204 -0
  39. package/src/cli/hands-owner.ts +118 -0
  40. package/src/cli/hands-record.ts +213 -0
  41. package/src/cli/hands.ts +207 -0
  42. package/src/cli/options.ts +3 -1
  43. package/src/cli.ts +1 -1
  44. package/src/execution/hands-process-owner.ts +4 -5
  45. package/src/execution/index.ts +10 -0
  46. package/docs/architecture.md +0 -168
  47. package/docs/index.md +0 -55
  48. package/docs/models.md +0 -78
  49. package/docs/observability.md +0 -63
  50. package/docs/pi-contract.md +0 -482
  51. package/docs/sdk.md +0 -180
  52. package/docs/worker-api.md +0 -138
package/docs/sdk.md DELETED
@@ -1,180 +0,0 @@
1
- # SEPT Node SDK
2
-
3
- Use ESM on Node.js >=22.19.0. Installation/prerequisites are in the [README](../README.md); complete operator preparation is in [Pi and hands](pi-contract.md#executable-supplied-hands-setup). A valid supplied hands profile is mandatory for execution, including text-only work.
4
-
5
- ## Public imports
6
-
7
- ```ts
8
- import { createWorker, SeptError } from "sept-agent";
9
- import type { Worker, WorkerOptions, WorkerAddress, Observability } from "sept-agent";
10
- import { createTaskClient, TaskClientError } from "sept-agent/client";
11
- import type {
12
- TaskClient, TaskClientOptions, RequestOptions, CreateTaskInput, SendMessageInput,
13
- TaskAdmission, MessageAdmission, TaskProjection, ConversationOptions,
14
- TerminalStatus, ConversationPage, CancelResult,
15
- TaskRequest, RunStatus, Manifest, RunSnapshot, ConversationEvent,
16
- } from "sept-agent/client";
17
- import { loadHandsConfiguration, networkFirewall } from "sept-agent/hands";
18
- import type { DockerHandsConfiguration } from "sept-agent/hands";
19
- ```
20
-
21
- These are the complete public entries. There is no raw service/store/runtime/host injection or public policy writer. The client entry imports no CLI/worker composition. Imports start no worker, listener, process handler, exporter or configuration write. Source is shipped for maps and inspection, not deep imports. Root and hands declarations do not expose the native Pi authoring declaration graph.
22
-
23
- ## Worker options
24
-
25
- ```ts
26
- interface WorkerOptions {
27
- stateRoot: string;
28
- agentDir: string;
29
- workspaceRoot: string;
30
- token: string;
31
- handsConfigurationPath?: string;
32
- host?: string;
33
- port?: number;
34
- telemetry?: Observability;
35
- }
36
- interface WorkerAddress { readonly host: string; readonly port: number; }
37
- interface Worker { readonly address: WorkerAddress; stop(): Promise<void>; }
38
- // createWorker(options: WorkerOptions): Promise<Worker>
39
- ```
40
-
41
- Supply explicit nonempty roots/token. Use canonical absolute directories: persistent private state, a private trusted Pi agent directory for credentials/configuration/code, and a separate workspace root. A worker canonicalizes requested workspaces beneath its assigned root; a symlink cannot escape it. Do not mount state, credentials, agent code or the Engine socket into hands. The library has no CLI/environment/path defaults. Listener defaults are `127.0.0.1:8090`; `port: 0` selects a real ephemeral port. Every route, including health/history/model access, requires the bearer.
42
-
43
- The [README host](../README.md#embed-a-javascript-worker) and [complete Node example](../examples/node-app/README.md) show composition through the public factory. `loadHandsConfiguration(path)` parses bounded trusted operator configuration; `networkFirewall(configuration, inspected, family)` generates an exact filter family from actual inspection. They neither provision a namespace nor install filters nor grant permissions; see [operator network setup](pi-contract.md#explicit-hands-network).
44
-
45
- ## Worker lifecycle
46
-
47
- `await createWorker(options)` binds the listener before adoption and resolves after router construction and existing synchronous restart adoption. The returned address is the actual bound endpoint used by backend validation. Pi/backend/model opening continues asynchronously; readiness does not certify a successful first invocation. Observe subsequent lifecycle/error state through `getTask` and authenticated evidence.
48
-
49
- `await worker.stop()` stops admission, closes streams/listener, waits for the current durable checkpoint/handover/terminal boundary and releases ownership. Repeated/concurrent calls share idempotent shutdown. There is no force deadline, `start()` or `restart()` method. Failed startup attempts cleanup before rejecting. The caller owns process signals and any supplied telemetry shutdown; default telemetry is no-op. To stop independent resources even after another finalizer fails, attempt each and collect errors (for example `Promise.allSettled` over worker, provider/listener and application-owned telemetry finalizers), then report failed closure.
50
-
51
- To reopen, await stop and call `createWorker` again using the same persistent state/agent/workspace roots and validated hands authority. WAIT remains dormant, retaining the run, session, slot and original hard budgets. An accepted later message wakes that same session. A run-bound hands recovery requires an explicitly supplied fresh generation and operator policy activation after old-owner reconciliation; do not reuse a stopped generation or reset state to evade that requirement. Workspace tmpfs export/reseed is consumer-owned.
52
-
53
- A same-process stop/reopen resumes durable state but does not promise native extension code reload. The loader imports stable canonical file URLs; Node's process/module-URL cache survives worker and Pi host replacement. Restart the Node process to reliably load edited code at the same path. Instructions/skills have their separate resource-refresh lifecycle; see [native authoring](pi-contract.md#author-build-and-load-a-native-extension).
54
-
55
- Stop, drain and cancel are distinct. `POST /v1/drain` durably rejects new tasks/messages across restarts while previously accepted input settles; the five-method client does not wrap it. `cancel(runId)` persists targeted cancellation and requests native abort. `cancelled: true` may accompany `status: "cancelling"`; inspect later state. Cancellation/loss can already have stopped hands and lost tmpfs bytes. Failed cleanup verification becomes blocked with uncertainty, never a successful cancellation claim.
56
-
57
- ## Integrate an existing service
58
-
59
- Create one client for a worker origin. Store delivery identities in your existing request/event store; do not generate a fresh event ID when retrying an uncertain delivery. The service owns retry decisions, finite polling, user-visible status and shutdown. SEPT supplies no scheduler, automatic retries, polling or SSE client.
60
-
61
- ```ts
62
- const client = createTaskClient({
63
- baseUrl: "http://127.0.0.1:8090",
64
- token: process.env.SEPT_TOKEN ?? "",
65
- timeoutMs: 30_000,
66
- });
67
- const input: CreateTaskInput = {
68
- source: "case-service",
69
- eventId: "case-42-initial",
70
- task: {
71
- task: "Assess E1. Wait for E2 before writing the final report; cite evidence IDs.",
72
- role: "Careful evidence reviewer",
73
- maxInvocations: 3,
74
- maxEpochs: 2,
75
- timeoutSeconds: 120,
76
- },
77
- };
78
- const admission = await client.createTask(input);
79
- ```
80
-
81
- Omitted workspace uses the worker's configured root. Optional `provider`/`model` must appear together; `thinking` is per task. [Model selection](models.md) is validated before durable admission. One slot includes waiting runs, so a separate task may receive HTTP409 until the resident releases ownership.
82
-
83
- ## Five client calls
84
-
85
- ```ts
86
- const admission = await client.createTask(input, { signal });
87
- const current = await client.getTask(admission.runId, { signal });
88
- const page = await client.getConversation(admission.runId, { after: 0, limit: 100, signal });
89
- const message = await client.sendMessage({
90
- source: "case-service",
91
- eventId: "case-42-evidence-E2",
92
- runId: admission.runId,
93
- message: "E2: the accepted later evidence, preserved exactly\n",
94
- }, { signal });
95
- const cancellation = await client.cancel(admission.runId, { signal });
96
- ```
97
-
98
- Here `signal` is an application-owned `AbortSignal`; omit options when cancellation is unnecessary. These are independent operations, not an instruction to cancel every completed evidence workflow. `createTask` and `sendMessage` construct version-1 task/operator-message envelopes for `POST /v1/webhooks`. Reads use task/conversation GETs; cancel uses the existing empty-body POST. The [incident example](../examples/incident-investigator/README.md) supplies finite WAIT/evidence/report orchestration.
99
-
100
- | Result | Contract |
101
- | --- | --- |
102
- | `TaskAdmission` | `{ kind: "task", runId, status: "active", duplicate }` |
103
- | `MessageAdmission` | `{ kind: "operator_message", runId, messageId, duplicate }` |
104
- | `TaskProjection` | `{ manifest: Manifest, state: RunSnapshot }`, current durable state with public workspace/session aliases |
105
- | `ConversationPage` | `{ events: ConversationEvent[], nextCursor, retainedFrom, terminal }`; terminal is null or `{ status: TerminalStatus }` |
106
- | `CancelResult` | `{ runId, status: RunStatus, cancelled: boolean }`; true need not mean settled |
107
-
108
- `TerminalStatus` is `cancelled | complete | blocked | failed`; `RunStatus` also includes `active | waiting | cancelling`. A historical blocked terminal event can precede a later reopened run; current terminal metadata is independent of page position. Request/options types match the import list above: `RequestOptions` is `{ signal?: AbortSignal }`, `ConversationOptions` adds `after?`/`limit?`, `CreateTaskInput` is `{ source, eventId, task }`, and `SendMessageInput` is `{ source, eventId, runId, message }`. `TaskClientOptions` is `{ baseUrl, token, timeoutMs? }`; every method returns a Promise of its named result.
109
-
110
- Identical `(source,eventId)` replay returns the original receipt and routing IDs with `duplicate: true`, even after the task finishes. Its `active` admission status is not current status: always use `getTask`. Changed content under the same key returns HTTP409. Stable message identity prevents a duplicate durable admission; operator delivery itself remains at least once and exact accepted text is checkpointed. A timeout, caller abort or transport failure cannot retract acceptance already made durable. Reconcile using the same identity and current state; never assume cancellation of fetch cancelled the task.
111
-
112
- ## Bounds and errors
113
-
114
- | Boundary | Exact limit/default |
115
- | --- | --- |
116
- | Worker origin | HTTP(S), origin only (optional trailing `/`), at most 8192 characters; no userinfo, query, fragment or nonroot path |
117
- | Bearer token | 1..8192 printable ASCII characters (`!` through `~`), no spaces/controls |
118
- | Run ID | `[A-Za-z0-9_-]{1,128}` |
119
- | Timeout | Default 30,000 ms; integer 1..2,147,483,647 |
120
- | Request | At most 256 KiB encoded UTF-8 bytes before sending |
121
- | Response | At most 5 MiB streamed bytes before JSON parsing |
122
- | Conversation options | `after` defaults 0, nonnegative safe integer; `limit` defaults 100, integer 1..250 |
123
- | Delivery source/event | Source `[A-Za-z0-9][A-Za-z0-9._:-]{0,127}`; event ID1..256 characters, trimmed, no controls |
124
-
125
- Node fetch refuses redirects. Caller abort and timeout share the first owned cancellation cause; timers/readers settle. JSON is decoded as unknown and checked for exact successful status, identity, durable DTO validity, count/cursor ordering and terminal metadata. The retained 4 MiB conversation bound already includes JSON escaping; the transport cap is not a raw-text approximation.
126
-
127
- `TaskClientError extends SeptError` has readonly `kind`, optional `status`, and optional validated positive `retainedFrom` for conversation HTTP410. Classify with `instanceof` and `kind`, not message parsing:
128
-
129
- | `kind` | Meaning |
130
- | --- | --- |
131
- | `configuration` | Invalid options, identifiers, page/signal or oversized/unencodable request |
132
- | `aborted` | Caller cancellation won; durable acceptance may have occurred |
133
- | `timeout` | Client timeout won; durable acceptance may have occurred |
134
- | `transport` | Fetch/stream transport failed, including redirect refusal; acceptance may have occurred |
135
- | `http` | Non-success HTTP status; `status` is available |
136
- | `protocol` | Malformed, oversized or contract-invalid successful response |
137
-
138
- Messages are fixed bounded safe text. They exclude tokens, hostile response bodies and URL/parser secrets, and do not attach unsafe underlying causes. HTTP409 includes occupied slot/drain/key collision; HTTP429 is admission capacity. The [wire API](worker-api.md) retains its routes/statuses and bounds independently of the client.
139
-
140
- ## Finite 410 recovery
141
-
142
- Expired history is a visible evidence gap, including when requesting `after: 0`. Never silently restart at zero or imply retained tail is full history. This example attempts at most three page requests, records the gap, checks authoritative state and requires the application's explicit decision before moving its cursor:
143
-
144
- ```ts
145
- async function readPage(
146
- client: TaskClient,
147
- runId: string,
148
- after: number,
149
- acceptGap: (gap: { after: number; retainedFrom: number; current: TaskProjection }) => Promise<boolean>,
150
- ): Promise<ConversationPage> {
151
- for (let attempt = 0; attempt < 3; attempt += 1) {
152
- try {
153
- return await client.getConversation(runId, { after, limit: 100 });
154
- } catch (error: unknown) {
155
- if (!(error instanceof TaskClientError) || error.status !== 410 || error.retainedFrom === undefined) {
156
- throw error;
157
- }
158
- const current = await client.getTask(runId);
159
- const gap = { after, retainedFrom: error.retainedFrom, current };
160
- console.warn("Conversation evidence gap", { after, retainedFrom: error.retainedFrom });
161
- if (!(await acceptGap(gap))) {
162
- throw error;
163
- }
164
- after = error.retainedFrom - 1;
165
- }
166
- }
167
- throw new SeptError("Conversation recovery attempt budget exhausted");
168
- }
169
- ```
170
-
171
- The decision callback must preserve/report the gap in the application's own evidence. New retention can advance between requests, hence the finite budget. Full pagination/polling also needs caller-owned finite budgets and cancellation.
172
-
173
- ## Troubleshooting
174
-
175
- - Ready but execution refused: verify hands/profile/image, current policy revision and exact running generation. Missing/corrupt policy leaves authenticated history available; do not edit durable JSON or assume default grants.
176
- - Admission HTTP409: inspect current state and readiness; WAIT owns the slot and drain persists. Retry only with a deliberate stable-ID decision after the condition changes.
177
- - WAIT survives restart: expected. Send an explicitly accepted later operator message once authority and budgets permit; do not create a replacement run to hide lost evidence.
178
- - Model failure after ready: inspect task state and run `sept models validate` with the same private agent directory. Readiness is not a successful provider call.
179
- - Edited extension still runs old code: stop/join the worker and start a fresh Node process. Same-process reopen retains module caching.
180
- - Native TypeScript authoring errors: SEPT's public declarations and raw Pi authoring headers are separate graphs. Follow the [Node example's authoring command](../examples/node-app/README.md) and its stated upstream limitations; do not weaken a strict SDK consumer to hide them.
@@ -1,138 +0,0 @@
1
- # SEPT worker API
2
-
3
- Every request requires `Authorization: Bearer <SEPT_TOKEN>`. Use the installed `sept` entry point (or `./sept` after a checkout build), or the same complete [SDK worker factory](sdk.md#worker-lifecycle). Prepare [hands and first-boot rights](pi-contract.md#executable-supplied-hands-setup) before initialization. Operators start and monitor the local worker through that entry point. Server operation must configure a persistent external state root; the cwd-relative `.sept/runs` fallback is for explicit local development only:
4
-
5
- ```bash
6
- SEPT_TOKEN='a-long-random-secret' ./sept worker --host 127.0.0.1 --port 8090 --state-dir /persistent/sept/runs --workspace-root /assigned/workspaces --hands /trusted/hands.json
7
- SEPT_TOKEN='a-long-random-secret' ./sept health --url http://127.0.0.1:8090/health
8
- ```
9
-
10
- - `GET /health` — worker state (`idle`, `busy`, or `waiting`), durable readiness (`accepting`, `draining`, or `drained`), `acceptingWork`, and the resident run ID in `activeRunId`. `waiting` means no Pi runtime/controller is allocated, but the resumable run retains the single slot. Health and reads remain available while drained.
11
- - `POST /v1/drain` — durably begin drain. The body is empty. Returns the current worker/readiness object and is idempotent.
12
- - `GET /v1/model-access/providers` — normalized Pi providers with credential-blind authentication state/methods and model counts.
13
- - `GET /v1/model-access/models?provider=<id>` — normalized Pi models. The optional provider appears at most once. Fields are only provider/id/name, availability, API dialect, context/output limits, and reasoning/input capabilities.
14
- - `GET /v1/model-access/selection` — resolved Pod selection with `selected.source`, `available`, `authenticated`, and normalized model metadata. It fails safely if the current default is no longer usable.
15
- - `PUT /v1/model-access/selection` — atomically set a future-run Pod default from exactly `{ "provider": "...", "model": "..." }` after full Pi preflight. The body limit is 8 KiB.
16
- - `POST /v1/tasks` — create the single active task. Required JSON: `task`. Optional: `role`, `workspace`, the complete `provider`/`model` pair, `thinking`, `maxInvocations`, `maxEpochs`, and `timeoutSeconds`. The workspace defaults to the configured workspace root and must resolve to a directory beneath it; symlink escapes are rejected.
17
- - `GET /v1/tasks/:runId` — manifest and current durable state projected with hands workspace aliases and bounded session locator names; private host roots remain only in recovery storage.
18
- - `GET /v1/tasks/:runId/events?after=<cursor>&limit=<1..1000>` — ordered durable events and next cursor.
19
- - `GET /v1/tasks/:runId/log` — at most the last 1 MiB of the bounded agent lifecycle log.
20
- - `GET /v1/tasks/:runId/conversation?after=<cursor>&limit=<1..250>` — replay bounded durable conversation events. Returns `events`, `nextCursor`, `retainedFrom`, and authoritative terminal state even when the terminal event is outside the requested page. A cursor older than retention receives `410`; malformed, duplicate, fractional, negative, or future cursor values receive `400` rather than being coerced.
21
- - `GET /v1/tasks/:runId/stream?after=<cursor>` — replay and follow the same events as authenticated `text/event-stream`. SSE `id` is the durable cursor; reconnect with it as `after`. At the authoritative terminal boundary, a terminal event is followed by clean closure or a `disconnect` carrying terminal metadata when that event is before the requested cursor. `disconnect` events include `{ reason, retry, nextCursor, terminal? }` for server shutdown, five-minute connection renewal, backpressure, terminal reconnect, or retention expiry.
22
- - `POST /v1/tasks/:runId/messages` — queue `{ "message": "..." }` durably; returns `202` and its ID.
23
- - `POST /v1/tasks/:runId/cancel` — targeted idempotent cancellation with an empty body. Cancellation intent and a bounded fixed terminal reason are fsynced before Pi's native abort is requested. Returns `202` while settlement is still in progress or `200` once deterministic terminal/no-op state is known.
24
- - `POST /v1/webhooks` — accept one normalized provider-neutral event and route it through the same task or operator-message lifecycle.
25
-
26
- All responses use `Cache-Control: no-store`. Model routes expose no credential values, headers, OAuth records, auth paths, raw provider payloads, or arbitrary provider configuration. Login is intentionally absent from this plain HTTP listener; use the installed `sept auth status|login|logout` commands, which delegate to Pi and never accept a secret as an argument.
27
-
28
- For every direct and task-webhook admission, SEPT validates the request, rereads `sept-model.json`, resolves explicit pair > Pod default > valid built-in default, and asks Pi for provider/model existence, availability, required metadata, and resolved authentication. Failure returns before `RunStore.create()`, webhook receipt claim, or controller launch. The final pair is immutable in each run manifest, so changing the Pod default affects only later runs.
29
-
30
- Run states are `active`, `waiting`, `cancelling`, `cancelled`, `blocked`, `complete`, and `failed`. The agent control object accepts `continue`, `waiting`, `blocked`, or `complete`: `continue` immediately invokes Pi again; `waiting` means current work is coherent and only new input can make progress; `blocked` records a genuine operator impediment; and `complete` is successful termination.
31
-
32
- On `waiting`, SEPT fsyncs the control checkpoint, then records the waiting state while preserving the Pi session ID/file. It closes the Pi runtime and controller lease and makes no further model call. An authenticated direct or webhook message is fsynced before acknowledgement, changes the run back to `active`, and reopens that same Pi session. A waiting run retains the single slot, so unrelated tasks receive `409`. If input arrives during the waiting decision or while its controller is closing, SEPT either continues in-place or launches one replacement controller after the old one releases ownership. On restart, a waiting run remains dormant; if a queued message survived a crash before the wake transition, startup completes the wake and adopts the session.
33
-
34
- Operator delivery is at least once. SEPT preserves every accepted character, including edge whitespace, and writes the exact ID and text in a delivery-attempt event. Queue persistence precedes conversation projection, and a fsynced projection marker lets restart idempotently recover a crash between those writes without losing accepted text or defeating bounded transcript retention. An identical retry of accepted but unconsumed input completes projection and wake under the same live slot/readiness/limit checks before completing its webhook receipt. A consumed duplicate is inert and cannot revive expired text. Historical reconciliation retains at most 64 records / 512 KiB of text per batch while consulting durable projection markers. It acknowledges consumption only after a fsynced work checkpoint or handover event contains that same ID and text. The handover JSON, Markdown, event, and `handover_durable` state commit precede message consumption, so a crash before consumption leaves input pending and restart must replace the predecessor before delivery. If input is accepted while an invocation is returning `complete`, `blocked`, or `waiting`, SEPT defers that transition and delivers the pending input in the same session. A message reopens a model-blocked task only when the worker slot is genuinely idle; if any controller or another waiting resident owns the slot, both message routes return `409` before queueing or mutating the blocked run, and the caller may retry after the worker becomes idle.
35
-
36
- `timeoutSeconds` limits accumulated Pi invocation wall time, not run age, so dormant waiting does not consume the execution budget. SEPT fsyncs an in-flight start before dispatch. Restart conservatively debits the uncertain crash interval, including downtime, up to the remaining hard budget because SIGKILL supplies no reliable death timestamp. A settled native answer can still be reused at exhaustion; new model/tool dispatch is refused. Each invocation gets only the remaining budget. Invocation and epoch caps remain hard across every wait/wake cycle. New input is rejected before durable acceptance when no invocation/timeout budget remains and while an invocation is running in the final permitted epoch; limit exhaustion is stored as a typed terminal cause rather than inferred from agent-authored text.
37
-
38
- SIGINT/SIGTERM closes HTTP admission first and waits for the active invocation to reach its next durable checkpoint, handover, or terminal transition. SEPT then disposes the Pi runtime, releases controller and state-root ownership, and leaves nonterminal work active for restart adoption. A bind failure performs no adoption/controller work.
39
-
40
- Drain is not process shutdown. The first authenticated drain writes `/worker-state.json` beneath the owned state root before returning. From that point direct and webhook task/message admission return `409`; accepted queued input continues. An active run reports `draining` until it reaches waiting or terminal, then reports `drained`. An already waiting or idle worker becomes drained immediately. Repeated drain requests return the same state, and startup never silently returns a drained root to accepting. The one-shot `sept run` command also refuses a drained state root.
41
-
42
- Cancellation is run-targeted and does not alter unrelated terminal runs. Active or waiting cancellation moves the durable state through `cancelling` to `cancelled` with terminal cause `operator_cancelled`. Only after intent is durable does the controller call native Durable `conversation.abort()`, which joins Pi model/tool work, including background descendants. Controller checks after every Pi invocation and conversation-replacement await prevent late complete/wait/blocked/failure writes. Restart finalizes historical cancellation without invoking Pi when no hands binding exists. An interrupted supplied-hands cancellation has unknown cleanup ownership and becomes `blocked` with `controller_failure` until the consumer reconciles it. A live shutdown-verification failure uses the same durable blocked outcome, preserving cancellation intent and never publishing `cancelled`. Repeated cancellation of `cancelled` is successful; cancellation after another terminal result is a deterministic no-op. New messages to cancelling/cancelled runs are rejected.
43
-
44
- New runs use native Pi Durable SQLite and `*.durable.json` conversation locators. Public session identity remains a string, containing the native numeric conversation ID. Context rotation creates a fresh conversation/provider identity only after the fsynced SEPT handover. Existing coding-agent JSONL sessions require the preserved old build; this build does not convert historical transcripts. Trusted operator plugins must use the native extension contract in [pi-contract.md](pi-contract.md).
45
-
46
- ## SDK and response statuses
47
-
48
- The public [client](sdk.md) sends stable-ID writes through webhooks, not the non-idempotent direct task/message routes. A new webhook returns202; an identical replay returns200 with the original IDs and `duplicate:true`. A task receipt remains `{ "kind":"task", "runId":"...", "status":"active", "duplicate":true }` after later completion. `GET /v1/tasks/:runId` is the authority for current state. A message receipt is `{ "kind":"operator_message", "runId":"...", "messageId":"...", "duplicate":false }`. Cancel returns `{ "runId":"...", "status":"cancelling", "cancelled":true }` with202 while still settling; true alone does not certify terminal cancellation.
49
-
50
- Successful reads return200, new direct task/message admissions202 and drain202. Errors preserve `{ "error": "bounded message" }`:400 malformed/invalid input,401 bearer failure,404 missing route/run,409 slot/drain/state/collision conflict,413 oversized body,415 wrong JSON content type,429 capacity and500 internal failure. Conversation expiry returns410 with additive machine-readable metadata:
51
-
52
- ```json
53
- { "error": "conversation cursor expired; earliest retained cursor is 25", "retainedFrom": 25 }
54
- ```
55
-
56
- The exact existing error wording remains server-owned. `retainedFrom` is the earliest retained positive cursor for JSON replay and initial SSE replay errors. After a visible evidence-gap report, read current task state and explicitly choose whether to retry at `retainedFrom - 1` under a finite attempt budget; even after0 can expire. Never silently skip missing history. See [bounded recovery code](sdk.md#finite-410-recovery). A timeout/abort cannot retract durable task/message acceptance.
57
-
58
- ## Conversation contract
59
-
60
- `conversation.jsonl` is separate from `events.jsonl`, `agent.log`, Pi session files, and WorkControl/Handover evidence. Its version-1 event union is:
61
-
62
- ```jsonl
63
- {"formatVersion":1,"cursor":1,"id":"stable-id","type":"message","role":"user|assistant","text":"exact text","at":"ISO-8601"}
64
- {"formatVersion":1,"cursor":2,"id":"stable-id","type":"state","state":"active|waiting","at":"ISO-8601"}
65
- {"formatVersion":1,"cursor":3,"id":"terminal-complete-1-1","type":"terminal","status":"cancelled|complete|blocked|failed","at":"ISO-8601"}
66
- ```
67
-
68
- Cursors are strictly increasing and IDs are deterministic for task, operator delivery, invocation output, lifecycle seams, and terminal occurrences identified by status, epoch and invocation. Reopening a blocked run therefore creates a distinct terminal event when it blocks again. Task/operator text is preserved exactly. Assistant records are committed before their corresponding control checkpoint and come only from the explicit `message` member of `{ "message": "...", "control": { ...WorkControl } }`; a validated message completed immediately before Pi steering is retained before the durable handover and fresh-session replacement. SEPT never projects Pi `thinking_delta`, raw streamed model output, prompts, tool calls/results, internal controls, webhook bodies, or lifecycle logs into conversation. Assistant message validation is bounded and rejects unsafe control characters, internal protocol fragments, and recognizable credential forms; rejection fails the run closed without publishing the text.
69
-
70
- At most 1,024 events and 4 MiB are retained per run; one message is at most 32,768 characters. Retention atomically keeps the newest complete records and reports a gap with `410`. Replay pages are capped at 250. At most 32 streams are active per worker; each connection renews after five minutes. A write-side backpressure signal closes with a reconnect cursor rather than accumulating an unbounded application buffer. Streams poll only the bounded durable file, so restart and session rotation require no in-memory replay state.
71
-
72
- ## Webhook contract
73
-
74
- Edge adapters normalize provider payloads before calling SEPT. SEPT does not implement Telegram, Slack, WhatsApp, monitoring, or other provider-specific behavior.
75
-
76
- Task event:
77
-
78
- ```json
79
- {
80
- "version": 1,
81
- "eventId": "producer-stable-event-id",
82
- "source": "monitoring-edge",
83
- "kind": "task",
84
- "task": {
85
- "task": "Investigate and verify the alert",
86
- "role": "Repository maintainer",
87
- "workspace": "/workspace"
88
- }
89
- }
90
- ```
91
-
92
- Operator-message event:
93
-
94
- ```json
95
- {
96
- "version": 1,
97
- "eventId": "producer-stable-event-id",
98
- "source": "slack-edge",
99
- "kind": "operator_message",
100
- "runId": "target-run-id",
101
- "message": "Use the newer operator direction"
102
- }
103
- ```
104
-
105
- The task object accepts exactly the fields supported by `POST /v1/tasks`, including only a complete provider/model override. It uses the same fresh default resolution and Pi preflight before receipt claim. Event and nested object schemas reject unknown fields. Operator messages preserve normal tabs and line breaks while rejecting unsafe control characters. `source` is a bounded adapter identifier, and `(source, eventId)` is the durable idempotency key. The first accepted delivery returns `202`; an identical retry returns `200` with `"duplicate": true` and the original routing IDs. Reusing the key for a different event returns `409` without routing either retry. A distinct task event returns `409` while the single slot is busy. Receipts contain only hashes and routing IDs, never source payloads.
106
-
107
- Durable run evidence is written beneath the configured state root. The production image fixes this at `/state/runs` and requires a persistent volume mounted at `/state`; it must not depend on an ephemeral container layer.
108
-
109
- Task limits default to100 invocations,20 epochs and86,400 seconds of active invocation time; integer ranges are1..10,000,1..1000 and1..604,800 respectively. Thinking accepts `off|minimal|low|medium|high|xhigh|max`, default `xhigh`.
110
-
111
- Request bodies accept at most 256 KiB. Operator messages and task text are limited to 32 KiB. At most 64 pending operator messages and 512 KiB of pending message text are accepted; saturation returns `429` without queueing the new message. JSON POSTs require `Content-Type: application/json`. The service authenticates before reading a body, binds to `127.0.0.1` by default, accepts the CLI bearer secret only from `SEPT_TOKEN` (library callers explicitly supply `token`), and does not expose credentials, assistant payload text, or raw webhook payloads in lifecycle logs and errors.
112
-
113
- A non-limit failure during durable handover/replacement recovery holds the active resident and stops automatic relaunch. Health reports that slot as busy; a worker restart or explicitly admitted same-run input may attempt recovery again. Acquired native hosts are closed when publication fails before controller ownership transfers. Lease takeover requires a provably dead different local PID plus stale lease age; an unknown, malformed or foreign-host owner remains fail closed regardless of age. For container identity changes, the operator must independently establish that the previous owner cannot execute before retiring its lease through an explicit recovery procedure; automatic cross-host takeover is unsupported.
114
-
115
- Execution requires one consumer-supplied Docker Linux hands container configured by `--hands` or `SEPT_HANDS_CONFIGURATION`; API task text cannot choose it. Health/history remain available if enforcement is missing, but scheduling fails closed. The supported `docker-linux-warm-v1` profile requires the explicit trusted local `engineSocket` plus inspected socket/daemon/container/generation facts. Native Pi remains the scheduler/tool-body/replay owner; one resident retains imports and an authenticated bounded attachment. The default private workspace is a finite consumer-seeded read/write tmpfs with Bash absent; explicitly granted Bash/custom use the same data. Consumers own export/preservation before stop and seed into a fresh generation. The separate `mutable-file-bind` domain permits anchored individual effects without commands, discovery or imageSelected. Cancellation stops the dedicated namespace (including detached descendants), may lose ephemeral contents and requires a fresh consumer generation before retry. Normal close joins the resident and preserves the idle consumer namespace. Ordinary body errors and verified command timeout/overflow may retain the channel; transport/identity loss or unverified joins seal authority and retain typed bounded stage/exit/closure facts. Each effect checks current rights; cached imports never cache grants. See [profile/configuration details](pi-contract.md#supplied-hands-reference-profile).
116
-
117
- The resident runs UID10002; commands and fixed rg/fd run UID10003:GID10002 without the brain group. Private app/HOME/physical capture bytes and resident memory/control/signals are protected by DAC, nondumpability and guards, including a separate command session for SIGCONT. Some public `/proc` metadata and resident cmdline correlation remain visible; neither correlation nor command stdout can grant/acknowledge control. Native grep/find require their admitted parent's READ authority and uniform readability, consume no Bash execute grant and always deny network. They use enclosing run/revoke/native limit cancellation with no new helper timer. Stdout/stderr remain separate for search, Bash combines decoded chunks in arrival order and custom results use stdout only; the combined 4 MiB command-output budget, drain and exact joins remain authoritative. Helper loss/overflow/cancellation cannot be reported as partial normal success.
118
-
119
- Opening verifies the image/DAC/private provenance and four effective Pi search-file hashes, then records literal isolation revision `uid-dac-fixed-helper-v1`. Old images/receipts refuse without rewriting history. The image-only `sept-native-search-v1` patch consists of four assets and changes only find/grep JS plus matching declarations; host Pi 1.1.0 remains stock; the exact locked package graph is retained. The pinned registry archive omits LICENSE; the exact upstream MIT notice is retained in the [asset README](../image/pi-native-search/README.md), privately copied with provenance. This internal patch adds no HTTP or client API.
120
-
121
- ## Local capability configuration
122
-
123
- Capability activation is a local operator operation, exposed only by `sept permissions show|set|revoke|requests|inspect|approve|deny`; there is no HTTP grant endpoint or text approval parser. See [local policy schema and commands](pi-contract.md#local-hands-permissions). `GET /health` and authenticated history stay readable with missing, denied or malformed policy, and `acceptingWork` is false. New task or message admission under known denial returns a bounded refusal before persistence. If denial races an already durable accepted input, that input retains its identity and remains dormant without effects; restart does not reset budgets or silently grant authority. An interrupted active run can remain recorded as active while native scheduling is denied.
124
-
125
- The default for genuinely empty state is workspace read/write, absent Bash, denied selected mounts and network none. Existing state requires explicit operator adoption. Full access means the supplied hands domain with brain exclusions and kernel mount protections. Explicit network grants require the connected consumer kernel profile documented in [the hands contract](pi-contract.md#explicit-hands-network); full_access alone keeps network false. Bash/custom network intent is an optional boolean in the native call and exact request digest. Missing network in request mode follows local decision plus explicit retry; default/scoped denial cannot produce a network effect. Write-only arbitrary execution and persistent selected RW plus Bash remain unsupported and are refused before activation. Independent file rights are supported; edit requires both. Partial-read namespaces omit native search/list and workspace resource discovery rather than broadening read authority. Narrow/revoke stops the exact dedicated namespace after fsynced policy publication; consumers supply a fresh validated generation and explicit restart/retry for expansion. Interrupted mutations remain uncertain and are not blindly replayed.
126
-
127
- Only trusted local hands configuration may add optional `imageSelected`: exact `{ "alias": "reference" }` objects, normalized to `[]` when omitted. Image aliases match `[A-Za-z0-9_-]{1,64}`, denote baked RO `/selected/<alias>` trees and have no host source/stat/seed path. Existing host aliases retain their longer `[A-Za-z0-9_-]+` grammar. All image/RO/RW aliases share the eight-unique-alias bound and existing selected policy rights; configuration alone grants nothing. Private-domain image roots require the pinned image/read-only rootfs and no equal, ancestor or subtree overlapping mount at opening/live checks. Optional genuinely `FS_IMMUTABLE`-sealed Linux RO host inputs remain supported; ordinary RO/private RW alternatives refuse. Changed baked bytes need a new image and fresh generation/authority after quiescence. [Bounded consumer Dockerfile recipe](pi-contract.md#selected-image-data-and-code) adds no provisioner or HTTP input.
128
-
129
- Capability expansion requires a consumer-supplied fresh namespace generation and verified quiescence of the old owner. Existing lease verification may reclaim a dead local owner after its grace period; a live, foreign or unverifiable owner refuses. After a crash/revoke, an active record can remain dormant with retained uncertain tool history. Explicit validated operator configuration and worker restart recover the same native session and captured admission/limits; unsafe interrupted mutations are not blindly re-executed. An operator message already durably accepted when denial wins is acknowledged with its exact queue identity and stays dormant; known denial refuses subsequent input before enqueue.
130
-
131
-
132
- Permission waiting is durable domain state, not a model-authored wait response. The original native call remains denied; requests contain exact work-visible operation/content digest and source authority. Pending restart never auto-wakes from old accepted queue entries. Local approval activates an exact exception for an explicitly selected fresh namespace/current revision and never schedules a call. A newly accepted operator message supplies the explicit wake using existing queue, slot and limit rules. Local direct `sept run --resume <run-id> --message <text>` uses that same wake and cannot reset original limits or model choices. Deny/cancel/revoke/expiry prevent effects; expiry during active approved work stops and joins the namespace. Request/decision activation has no HTTP endpoint and task/chat content cannot create a grant. [Local decisions and the single custom hand](pi-contract.md#exact-permission-requests-and-local-retry) document the bounded operator commands.
133
-
134
- The one operator `customHand` registration contains only path/SHA256/replay, below an admitted read-only image or genuinely sealed host alias, with the existing shell/execute/bounded RW workspace/uniform-read requirements. The parent checks anchored code (64 KiB maximum) and exact input against the pin; no native API object or credentials reach the clean child. Native `custom_hand` has text `maxLength: 256` plus a 1024-byte transport bound, optional boolean network intent, stdout-only 4096-byte output with native 4096-byte/64-line limits, and its existing 5-second timeout. Stderr is separately bounded/drained inside the 4 MiB output budget. SAFE replay checks current pin/rights; UNSAFE interruption retains uncertainty without repeating effects. These are native tool calls within a task, not a new worker endpoint.
135
-
136
- Support is limited to the exact qualified source/image/host combination. Native Windows worker operation remains unsupported because Unix control/POSIX leases remain; Intel Mac and Windows Docker Linux/WSL recipes remain unqualified. [Repository-only historical evidence at d361123](https://github.com/DB-Conneely/sept/blob/d361123d7c242db5af3ae2d1cb4daea4a2acdeff/docs/as-built/capability-confinement.md) retains publicIPv6 ENETUNREACH/UFW INPUT-DROP, provider-outage/OAuth-refresh gaps and the unknown original physical stream-loss trigger.
137
-
138
- The [task client](sdk.md#five-client-calls) wraps five operations only. Direct admissions, events/log retrieval, SSE, drain and model routes remain available here. The [canonical index](index.md) maps the same Markdown for people and AI readers.