@sema-agent/sdk 19.0.0 → 20.0.0

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 (46) hide show
  1. package/README.md +56 -0
  2. package/dist/errors.d.ts +4 -1
  3. package/dist/errors.d.ts.map +1 -1
  4. package/dist/errors.js +4 -1
  5. package/dist/errors.js.map +1 -1
  6. package/dist/events.d.ts +15 -15
  7. package/dist/events.d.ts.map +1 -1
  8. package/dist/idempotency.d.ts +11 -0
  9. package/dist/idempotency.d.ts.map +1 -1
  10. package/dist/idempotency.js +11 -0
  11. package/dist/idempotency.js.map +1 -1
  12. package/dist/index.d.ts +4 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/resources/fleet.d.ts +7 -0
  16. package/dist/resources/fleet.d.ts.map +1 -1
  17. package/dist/resources/fleet.js.map +1 -1
  18. package/dist/resources/leader.d.ts +9 -0
  19. package/dist/resources/leader.d.ts.map +1 -1
  20. package/dist/resources/leader.js +9 -0
  21. package/dist/resources/leader.js.map +1 -1
  22. package/dist/resources/runs.d.ts +16 -0
  23. package/dist/resources/runs.d.ts.map +1 -1
  24. package/dist/resources/runs.js +22 -0
  25. package/dist/resources/runs.js.map +1 -1
  26. package/dist/resources/session-sync.d.ts +14 -5
  27. package/dist/resources/session-sync.d.ts.map +1 -1
  28. package/dist/resources/session-sync.js +3 -2
  29. package/dist/resources/session-sync.js.map +1 -1
  30. package/dist/resources/sessions.d.ts +33 -2
  31. package/dist/resources/sessions.d.ts.map +1 -1
  32. package/dist/resources/sessions.js +29 -1
  33. package/dist/resources/sessions.js.map +1 -1
  34. package/dist/resources/tasks.d.ts +10 -2
  35. package/dist/resources/tasks.d.ts.map +1 -1
  36. package/dist/resources/tasks.js +8 -0
  37. package/dist/resources/tasks.js.map +1 -1
  38. package/dist/sync.d.ts +23 -0
  39. package/dist/sync.d.ts.map +1 -1
  40. package/dist/sync.js +43 -0
  41. package/dist/sync.js.map +1 -1
  42. package/dist/types.d.ts +156 -6
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/openapi.yaml +370 -20
  46. package/package.json +1 -1
package/openapi.yaml CHANGED
@@ -87,7 +87,9 @@ info:
87
87
  browser guard refuses to hold this in a browser; the portal's browser talks to its own BFF) and
88
88
  `x-agent-principal: <principal>` (REQUIRED; opaque to the service; principal-first, CORE-PRINCIPLES §3).
89
89
  • Submit-class POSTs (`/v1/tasks`, `/v1/runs`) carry an `Idempotency-Key` (the SDK auto-generates one;
90
- in-process dedupe window = 15 min; cross-replica hard-dedupe = the session-active 409, see §below).
90
+ server <= 7.124.x: in-process dedupe window = 15 min; cross-replica hard-dedupe = the session-active 409, see §below.
91
+ server >= 7.125.0: the key is a unique key on the RUN ROW — a same-key resubmit reads that row and answers from it, across
92
+ replicas and restarts; the process only merges concurrent in-flight duplicates. `GET /v1/runs?idempotencyKey=` reads what a key built).
91
93
  • New contract fields are OPTIONAL with no default (CORE-PRINCIPLES §6).
92
94
  • Async results are delivered by POLL (`GET /v1/runs/:id`) or SSE (`/v1/runs/:id/events`).
93
95
  There is NO generic webhook/callback and NO signing (SERVICE-CORE-CONTEXT §2.4).
@@ -427,6 +429,12 @@ paths:
427
429
  `verify` / `cascade` run's earlier tier already ran turns and only the last tier is refused, the SAME code arrives
428
430
  as 200 with a failed body (`terminal.code`; billed; a same-key resend replays). Multi-tier callers must read both
429
431
  forms. On a park the 200 body is the result itself plus the three poll-receipt keys (`SyncSubmitResult`, S-173).
432
+ 🔴 IDEMPOTENT REPLAY (server >= 7.125.0, contract §0.7): a same-key resubmit that finds the run row the key already built (another process, another
433
+ replica, a restart, or an original request that has since settled) does NOT run again and does NOT answer the first answer's engine-result shape —
434
+ it answers `200` with that run's READ FACE (`GET /v1/runs/{taskId}`) plus `replay: true` (`RunReplayRecord`). Branch on `replay === true`; the outcome is
435
+ in `result` (a nested `TaskResult`), and `wiringManifest` / `notice` are absent. A same-key duplicate that is still IN FLIGHT in the same process is merged
436
+ instead and gets the first answer's bytes (no `replay` key). Only a submit that builds a row can be replayed this way (a run store AND a `sessionId`);
437
+ otherwise a settled key re-runs. A key whose row the caller cannot read answers 409 `conflict.run_exists`.
430
438
  parameters:
431
439
  - $ref: '#/components/parameters/IdempotencyKey'
432
440
  requestBody:
@@ -437,13 +445,17 @@ paths:
437
445
  responses:
438
446
  '200':
439
447
  description: >-
440
- The task result (`SyncSubmitResult`): the sealed + redacted engine result; on a park (`terminal.kind:"paused"`)
448
+ One of two shapes, told apart by the `replay` key. (1) The task result (`SyncSubmitResult`): the sealed + redacted engine result; on a park (`terminal.kind:"paused"`)
441
449
  it additionally carries the poll receipt `{taskId, sessionId, status}` with `status` ∈ {suspended, needs_review}
442
450
  (server >= 7.104.0 / S-173 — before that a park answered ONLY those three keys). Terminal bodies carry no `status`
443
451
  on >= 7.64; a server <= 7.63 answers the flat form instead (terminal word in `status`, no `terminal`) — see SyncSubmitResult.
452
+ (2) server >= 7.125.0, same-key replay: the existing run's read face with `replay: true` (`RunReplayRecord`) — NOT an engine result.
444
453
  content:
445
454
  application/json:
446
- schema: { $ref: '#/components/schemas/SyncSubmitResult' }
455
+ schema:
456
+ oneOf:
457
+ - $ref: '#/components/schemas/SyncSubmitResult'
458
+ - $ref: '#/components/schemas/RunReplayRecord'
447
459
  '400': { $ref: '#/components/responses/BadRequest' }
448
460
  '401': { $ref: '#/components/responses/Unauthorized' }
449
461
  '403': { $ref: '#/components/responses/Forbidden' } # (declared 2026-08-01) 带 `sessionId` 复用他人会话:归属不匹配 ⇒ 403(自公开快照起点即有,spec 此前漏登记)。server 3.15.0 起多一个触发条件:多租部署上 session store 无归属判别能力时,复用**任何** caller 自报的 sessionId 都 403(含本 worker 自己刚铸的 —— 无 register seam 时归属从未被记下)。core 2.11.0 起默认内存店自带该能力 ⇒ 该触发条件在默认部署上不再出现。 ⚠️ (declared 2026-08-05,server >=7.0.0)`memory.admission_denied` 也走本状态 —— 本状态的 producer 全清单与分支判据写在 `Forbidden` 组件的 description 里(**那段是被解析的**,本行注释只是指路,不复述以免两处漂移)。
@@ -467,6 +479,10 @@ paths:
467
479
  deployment has NO file-history store but the request asks to restore files; the request is well-formed, the
468
480
  deployment shape conflicts with it (change the deployment, not the request; `capabilities.rewindFiles` is false
469
481
  there). Before 7.104.0 it arrived as 200 with a failed body.
482
+ `conflict.run_exists` (server >= 7.125.0, contract §0.7): the submit's `Idempotency-Key` (or `body.taskId`) already points at a run row you cannot read,
483
+ or at a row that is not the one this submit's `taskId` names. The sentence is `a run already exists for this submission's taskId or Idempotency-Key`
484
+ (the code is unchanged; only the sentence moved); nothing about the row is echoed (no cross-tenant oracle). Submit under a NEW key. On
485
+ `POST /v1/tasks/stream`, once the stream is open the same refusal arrives as the `done` frame's `{status:"failed", errorCode, errorMessage}`.
470
486
  content:
471
487
  application/json:
472
488
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -653,6 +669,53 @@ paths:
653
669
  '429': { $ref: '#/components/responses/RateLimited' }
654
670
  '501': { $ref: '#/components/responses/NotImplemented' }
655
671
  '503': { $ref: '#/components/responses/Unauthorized' }
672
+ get:
673
+ tags: [runs]
674
+ operationId: runsGetByIdempotencyKey
675
+ x-status: live
676
+ summary: Read the run an `Idempotency-Key` built (look before you re-submit).
677
+ description: >
678
+ server >= 7.125.0 (contract §0.7). Answers "which run did this submit key build?" for a client that crashed or lost the connection
679
+ after a submit and must decide whether to submit again. `idempotencyKey` is the RAW value the submit was sent with; the server scopes
680
+ it exactly as the submit did (your verified identity × resource family `run` — `POST /v1/tasks`, `/v1/tasks/stream`, `/v1/runs` and A2A
681
+ `message/send` all share family `run`, so a key used on any of them is found here). Another caller's key resolves to a different key and
682
+ reads as the same 404, byte-for-byte (no existence oracle). 200 = the same read face as `GET /v1/runs/{taskId}` (`RunRecord`), and the gates
683
+ run in the same order: 501 no run store → 401 → read row → owner gate → `?session=` assertion. A submit that never built a row leaves
684
+ nothing to find (404): no run store, or a `POST /v1/tasks` / `/v1/tasks/stream` submit without `sessionId` (those legs build a row only
685
+ with both), and a zero-work refusal (the key is released). NOT a submit: no billing, no admission gates.
686
+ parameters:
687
+ - in: query
688
+ name: idempotencyKey
689
+ required: true
690
+ schema: { type: string, minLength: 1 }
691
+ description: 'The RAW `Idempotency-Key` the submit carried (the server applies the scope; send it unhashed and unmodified). Missing or empty ⇒ 400 `request.query_invalid`.'
692
+ - in: query
693
+ name: session
694
+ required: false
695
+ schema: { type: string }
696
+ description: >-
697
+ The session assertion `GET /v1/runs/{taskId}` honors: for a session-bound run a non-operator caller sends the conversation's `sessionId` here;
698
+ a mismatch answers the same 404 as an unknown key.
699
+ responses:
700
+ '200':
701
+ description: The run row's read face — byte-for-byte what `GET /v1/runs/{taskId}` returns for that run. NO `replay` marker (that is only on a submit's replay body).
702
+ content:
703
+ application/json:
704
+ schema: { $ref: '#/components/schemas/RunRecord' }
705
+ '400':
706
+ description: 'errorCode `request.query_invalid` — `idempotencyKey` is missing or empty. (Unlike `GET /v1/leader?idempotencyKey=`, no key-shape check: the key is only hashed here.)'
707
+ content:
708
+ application/json:
709
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
710
+ '401': { $ref: '#/components/responses/Unauthorized' }
711
+ '404':
712
+ description: >-
713
+ errorCode `not_found.run` — this key (under your identity) built no run row, or the row is not yours, or `?session=` mismatched. One
714
+ code, one sentence, for every cause: absence of the key is not distinguishable from another caller's run.
715
+ content:
716
+ application/json:
717
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
718
+ '501': { $ref: '#/components/responses/NotImplemented' }
656
719
 
657
720
  /v1/runs/{taskId}:
658
721
  parameters:
@@ -1777,6 +1840,48 @@ paths:
1777
1840
  properties:
1778
1841
  error: { type: string }
1779
1842
  total: { type: integer, description: 'present on the out-of-range `?message=` 404 — the message count available.' }
1843
+ patch:
1844
+ tags: [sessions]
1845
+ operationId: sessionsPatch
1846
+ x-status: live # server >= 7.125.0 (contract section 20).
1847
+ summary: Rename and/or pin a session (the session row's two writable bits).
1848
+ description: >
1849
+ server >= 7.125.0 (contract section 20) — write the session row's two writable bits: `title` (rename, or `null` = clear back to the
1850
+ automatic title) and `pinned`. ONE COLUMN, ONE FACT: `title` is a single column with two writers under one law — the user writes it through
1851
+ this verb UNCONDITIONALLY (a string renames; `null` clears the column back to NULL); the service's auto-titler fills it ONLY when it is
1852
+ NULL (read once per submit). So a session the user named is never overwritten by the auto-titler, and after a clear the next submit has
1853
+ the auto-titler fill it again. There is no second column. `pinned` is one boolean column (default `false`).
1854
+ `200` is the session's LIST ROW — the same projection as the matching row of `GET /v1/sessions`, key for key (including the derivation of
1855
+ `tempo` and the always-present `pinned`). Renaming / pinning does NOT move `lastActivityAt` (the list's recency order is unchanged).
1856
+ PROBE: there is no capability bit; probe by status code — `405` (`Allow` without `PATCH`, `errorCode: method_not_allowed`) = an older server (<= 7.124.x);
1857
+ `501 capability.session_store_required` = this deployment has no durable session store (the in-memory dev store has no row read / write face).
1858
+ Same gate as `GET /v1/sessions/{sessionId}`, same order: store fact (501) -> ownership (the SAME predicate as the session-view read — whoever can read
1859
+ the row can write it, never wider; an operator / service token gets no extra reach; a stranger and "no such session" answer byte-identical
1860
+ `404 not_found.session`, no existence oracle) -> body (400) -> the store's one write door -> read back and answer with the list row.
1861
+ Only rows on the LIST face are writable: a placed (delegation-transcript) session is not in the list, so a PATCH on it answers 404.
1862
+ requestBody:
1863
+ required: true
1864
+ content:
1865
+ application/json:
1866
+ schema: { $ref: '#/components/schemas/SessionPatchBody' }
1867
+ responses:
1868
+ '200':
1869
+ description: The session's list row (same projection as the matching `GET /v1/sessions` row, key for key).
1870
+ content:
1871
+ application/json:
1872
+ schema: { $ref: '#/components/schemas/SessionSummary' }
1873
+ '400':
1874
+ description: >-
1875
+ Malformed body, told apart by `errorCode` (branch on the code, never the sentence). `request.body_shape` — not a JSON object / empty body /
1876
+ a key outside the closed set `{ title, pinned }` / `title: ""` (the only spelling of "clear" is `null`). `request.field_invalid` — `title`
1877
+ neither a string nor `null`, longer than 120 characters, or with leading / trailing whitespace; `pinned` not a boolean. Invalid JSON answers
1878
+ like every other JSON-body route (`request.invalid_json`). The row is untouched on any 400.
1879
+ content:
1880
+ application/json:
1881
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
1882
+ '401': { $ref: '#/components/responses/Unauthorized' }
1883
+ '404': { $ref: '#/components/responses/NotFound' }
1884
+ '501': { $ref: '#/components/responses/NotImplemented' }
1780
1885
  delete:
1781
1886
  tags: [sessions]
1782
1887
  operationId: sessionsDelete
@@ -1810,7 +1915,9 @@ paths:
1810
1915
  deleted: { type: boolean }
1811
1916
  '400':
1812
1917
  # (declared 2026-07-31) errorCode "request.id_invalid" — the path sessionId must decode to a
1813
- # well-formed uuidv7 (`routes/sessions.ts`); a malformed id never reaches the store.
1918
+ # canonical uuid of ANY version (`routes/sessions.ts`; server >= 7.125.0 — the ONE shape gate shared with sync / fork / policy;
1919
+ # <= 7.124.x accepted only uuidv7 here); a malformed id never reaches the store. Branch on the code, never the sentence
1920
+ # (the sentence changed with the gate: "invalid session id (must be a canonical uuid)").
1814
1921
  $ref: '#/components/responses/BadRequest'
1815
1922
  '401': { $ref: '#/components/responses/Unauthorized' }
1816
1923
  '404': { $ref: '#/components/responses/NotFound' }
@@ -1845,8 +1952,9 @@ paths:
1845
1952
  summary: Branch a NEW session off this one's whole history.
1846
1953
  description: >
1847
1954
  E17 (shell-host) — branch a fresh session off the source's WHOLE history (a "branch this conversation" /
1848
- regenerate-from-here). Owner-scoped (non-owner → 404; the fork is owned by the caller). 400 on a non-uuidv7
1849
- id. 501 without a durable session store.
1955
+ regenerate-from-here). Owner-scoped (non-owner → 404; the fork is owned by the caller). 400 `request.id_invalid` on an id that is not
1956
+ a canonical uuid — ANY uuid version passes since server >= 7.125.0 (the ONE shape gate shared with sync / policy / delete; <= 7.124.x accepted only
1957
+ uuidv7 here, so a shell-preset v4 id was creatable and syncable but not forkable). 501 without a durable session store.
1850
1958
  responses:
1851
1959
  '200':
1852
1960
  description: The new branched session id.
@@ -2364,7 +2472,7 @@ paths:
2364
2472
  rules: { $ref: '#/components/schemas/StoredSessionRules' }
2365
2473
  '400':
2366
2474
  # (declared 2026-07-31) errorCode "request.id_invalid" — GET and PUT share the SAME id-shape check
2367
- # in `routes/sessions.ts` (uuidv7 required) ahead of the method branch; only PUT had this declared.
2475
+ # in `routes/sessions.ts` (a canonical uuid of any version since server >= 7.125.0; uuidv7 only on <= 7.124.x) ahead of the method branch; only PUT had this declared.
2368
2476
  $ref: '#/components/responses/BadRequest'
2369
2477
  '401': { $ref: '#/components/responses/Unauthorized' }
2370
2478
  '404': { $ref: '#/components/responses/NotFound' }
@@ -2845,8 +2953,8 @@ paths:
2845
2953
  lacks. Owner-scoped (non-owner → 404, no oracle); 400 on an id that is not uuid-SHAPED — 🔴 the sync family's
2846
2954
  gate is `isUuidShape` (ANY uuid version), NOT `isUuidV7`: server `routes/session-sync.ts` deliberately
2847
2955
  keeps a v4-keyed legacy shell session pushable (it was creatable via the lenient submit face), while
2848
- crafted-id classes stay shut out. `fork`/`delete`/`policy` DO use `isUuidV7` — only this family is lenient;
2849
- 501 without a durable session store.
2956
+ crafted-id classes stay shut out. Since server >= 7.125.0 `fork` / `delete` / `policy` read the SAME one gate (any canonical uuid version);
2957
+ <= 7.124.x those three used `isUuidV7` and only this family was lenient. 501 without a durable session store.
2850
2958
  responses:
2851
2959
  '200':
2852
2960
  description: The manifest, wrapped as `{ manifest }`.
@@ -3111,6 +3219,14 @@ paths:
3111
3219
  description: 'server >= 7.52: the file-history envelope (replaces `snapshots`); `null` / absent = none. Every blob it references must already be PUT.'
3112
3220
  policy: { type: array, items: { $ref: '#/components/schemas/SessionRulesRecord' } }
3113
3221
  anchors: { type: array, items: { $ref: '#/components/schemas/SyncAnchor' } }
3222
+ title:
3223
+ type: [string, 'null']
3224
+ description: >
3225
+ server >= 7.125.0 (contract section 20): the session row's title from the manifest. Judged by the SAME reader as `PATCH /v1/sessions/{sessionId}` cell by cell
3226
+ (a string 1..120 characters with no leading / trailing whitespace; `""` is `400 request.body_shape`; a non-string-non-null / too long / whitespace-edged value is
3227
+ `400 request.field_invalid`) BEFORE any staging is opened, so a refused push leaves nothing behind. `null` CLEARS the destination back to the automatic title. After the
3228
+ commit (and on the `identical` short-circuit arm) the keys that were sent are replayed through the store's one write door. KEY NOT SENT = the destination's two bits stay (older callers).
3229
+ pinned: { type: boolean, description: 'server >= 7.125.0 (contract section 20): the session row''s pin from the manifest; judged by the PATCH reader (non-boolean is `400 request.field_invalid`); not sent = the destination''s pin stays.' }
3114
3230
  resolution: { type: string, enum: [overwrite-dst], description: Override a fork/stale loss-of-history conflict. }
3115
3231
  logDigest:
3116
3232
  type: string
@@ -4779,6 +4895,45 @@ paths:
4779
4895
  content:
4780
4896
  application/json:
4781
4897
  schema: { $ref: '#/components/schemas/ErrorResponse' }
4898
+ get:
4899
+ tags: [leader]
4900
+ operationId: leaderGetByIdempotencyKey
4901
+ x-status: gated
4902
+ summary: Read the leader run an `Idempotency-Key` built (look before you re-submit).
4903
+ description: >
4904
+ server >= 7.125.0 (contract §0.7). Same shape as `GET /v1/runs?idempotencyKey=` for family `leader`: `idempotencyKey` is the RAW key the
4905
+ `POST /v1/leader` carried; the server scopes it by your verified identity exactly as the submit did. 200 = the same read face as
4906
+ `GET /v1/leader/{leaderRunId}` (`LeaderRecord`). A key that built no leader run, and a run that is not yours, are the SAME 404 (no existence
4907
+ oracle). A leader key is a different key from a run key even with the same raw text (resource families do not cross). Not a submit: no billing, no admission gates.
4908
+ The leader face being absent on the deployment is the same 501 `capability.leader_endpoint_required` as the other leader verbs.
4909
+ parameters:
4910
+ - in: query
4911
+ name: idempotencyKey
4912
+ required: true
4913
+ schema: { type: string, minLength: 1, maxLength: 255 }
4914
+ description: 'The RAW `Idempotency-Key` the `POST /v1/leader` carried (1–255 characters, no leading or trailing whitespace — the submit-header rule). Missing, or a malformed shape ⇒ 400 `request.query_invalid`.'
4915
+ responses:
4916
+ '200':
4917
+ description: The leader run's read face — what `GET /v1/leader/{leaderRunId}` returns for that run.
4918
+ content:
4919
+ application/json:
4920
+ schema: { $ref: '#/components/schemas/LeaderRecord' }
4921
+ '400':
4922
+ description: 'errorCode `request.query_invalid` — `idempotencyKey` is missing, or not a well-formed key (empty / over 255 characters / leading or trailing whitespace).'
4923
+ content:
4924
+ application/json:
4925
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
4926
+ '401': { $ref: '#/components/responses/Unauthorized' }
4927
+ '404':
4928
+ description: 'errorCode `not_found.leader_run` — this key (under your identity) built no leader run, or the run is not yours. Same code and sentence for both.'
4929
+ content:
4930
+ application/json:
4931
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
4932
+ '501':
4933
+ description: 'errorCode `capability.leader_endpoint_required` — no leader face on this deployment (the same refusal the other leader verbs give).'
4934
+ content:
4935
+ application/json:
4936
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
4782
4937
 
4783
4938
  /v1/leader/{leaderRunId}:
4784
4939
  parameters:
@@ -6669,8 +6824,11 @@ components:
6669
6824
  required: false
6670
6825
  schema: { type: string }
6671
6826
  description: >
6672
- Submit-class dedupe key (the SDK auto-generates a UUID → retries are safe by default). In-process window
6673
- = 15 min (per-instance). A submit is retried by the SDK ONLY when a key exists.
6827
+ Submit-class dedupe key (the SDK auto-generates a UUID → retries are safe by default). server <= 7.124.x: in-process window
6828
+ = 15 min (per-instance). server >= 7.125.0: the key is a unique key on the run row (scope = identity bit (the verified principal, else the credential family) × resource family × raw key,
6829
+ families `run` / `leader` / `bake`; the endpoint is NOT part of the scope, so `POST /v1/tasks`, `/v1/tasks/stream`, `/v1/runs` and A2A
6830
+ `message/send` share family `run`), valid across replicas and restarts for the run's retention; only concurrent in-flight duplicates are
6831
+ merged in process. A submit is retried by the SDK ONLY when a key exists.
6674
6832
  LastEventId:
6675
6833
  in: header
6676
6834
  name: Last-Event-ID
@@ -6772,6 +6930,9 @@ components:
6772
6930
  does NOT silently resolve it to that run: `activeTaskId` may be the caller's own first attempt OR an
6773
6931
  unrelated concurrent run on the same session — 409 alone cannot tell (the pinned wire contract Q6). Only triggers when
6774
6932
  a `sessionId` is present.
6933
+ server >= 7.125.0 also: `conflict.run_exists` — on a submit (`POST /v1/runs`, `/v1/tasks`, `/v1/tasks/stream`), the `Idempotency-Key` /
6934
+ `body.taskId` points at a run the caller cannot read, or the two keys of one submit point at different rows. The sentence is
6935
+ `a run already exists for this submission's taskId or Idempotency-Key` (code unchanged, sentence new; nothing echoed). Submit again under a new key.
6775
6936
  content:
6776
6937
  application/json:
6777
6938
  schema: { $ref: '#/components/schemas/ErrorResponse' }
@@ -7027,13 +7188,15 @@ components:
7027
7188
  type: string
7028
7189
  description: >-
7029
7190
  [2400] TR-16 (declared 2026-08-02) — CALLER-MINTED uuidv7 task id: the DURABLE second tier of
7030
- idempotency on POST /v1/runs (`src/http/routes/runs.ts`). The `Idempotency-Key` header's
7191
+ idempotency on POST /v1/runs (`src/http/routes/runs.ts`). On server <= 7.124.x the `Idempotency-Key` header's
7031
7192
  cache is in-memory and PER-POD, so a dispatch gateway that re-routes a retry to ANOTHER instance
7032
7193
  after a network error would start a SECOND run; THIS replay reads the durable run store, so it
7033
7194
  survives the re-route and a restart — it is the prerequisite for gateway failover. A hit replays the
7034
7195
  original 202 receipt (`{taskId, sessionId, status}`); nothing re-runs, nothing re-bills.
7035
7196
  Shape-gated: a non-uuidv7 value is 400 `request.id_invalid`. Owner-gated: a taskId owned by another
7036
7197
  principal answers 409 `conflict.run_exists` (no cross-tenant id oracle). Omit ⇒ the server mints one.
7198
+ server >= 7.125.0: `Idempotency-Key` is ALSO a unique key on the run row (contract §0.7), so the two are two keys to one row. Sent together
7199
+ they must point at the same row; a key that points at a row this `taskId` does not name is 409 `conflict.run_exists`.
7037
7200
  scenario: { $ref: '#/components/schemas/Scenario' }
7038
7201
  sessionId:
7039
7202
  type: string
@@ -8221,6 +8384,37 @@ components:
8221
8384
  toolCallId: { type: string }
8222
8385
  retryAfterMs: { type: integer }
8223
8386
 
8387
+ RunReplayRecord:
8388
+ description: >
8389
+ server >= 7.125.0 (contract §0.7). The idempotent-REPLAY body of a submit whose `Idempotency-Key` had already built a run row: `RunRecord` (the
8390
+ `GET /v1/runs/{taskId}` read face, same projection) plus `replay: true`. It answers `POST /v1/tasks` (200) and is the `done.result` of
8391
+ `POST /v1/tasks/stream`. 🔴 IT IS NOT A `TaskResult`: a synchronous first answer carries the engine result at the top level (`terminal`, `stats`, `result` as a
8392
+ string); a replay has only the row, so `result` is the NESTED `TaskResult` (or absent while the run has no result), there is no top-level `terminal` / `stats`, and
8393
+ `wiringManifest` / `notice` are absent (the wiring receipt is a live capture that is not stored on the row). `taskId` is the row's id — on the stream leg, when the
8394
+ replay is discovered only after the stream opened (another replica created the row first), the `x-task-id` header and `meta` frame carried an id this leg minted
8395
+ and never created: the id in this body is the real one. Narrow on `replay === true` (the two shapes cannot both match). A same-key duplicate still IN FLIGHT in the same
8396
+ process is merged instead and gets the first answer's bytes with no `replay` key.
8397
+ allOf:
8398
+ - $ref: '#/components/schemas/RunRecord'
8399
+ - type: object
8400
+ required: [replay]
8401
+
8402
+ SubmitRefusalDoneResult:
8403
+ type: object
8404
+ description: >
8405
+ The `done.result` of a submit REFUSED after the SSE stream was already open (`POST /v1/tasks/stream`): the status line is gone, so the refusal travels as the
8406
+ stream's terminal frame in the triple `{status:"failed", errorCode, errorMessage}` — the same code and sentence the JSON refusal body would have carried
8407
+ (server `src/http/routes/tasks.ts`). Two producers: (1) server >= 7.125.0 — the submit's key (or `body.taskId`) turned out, only at row-creation time, to point at a run row
8408
+ the caller cannot read or that is not the one this submit names: `errorCode: "conflict.run_exists"` (contract §0.7; the frame carries `replay: true`); (2) a device-placement
8409
+ refusal (the placement error body's code and sentence). It is NOT an engine result and NOT `ActiveRunConflictDoneResult` (that one is `conflict.session_active_run` and requires
8410
+ `activeTaskId`). Branch on `errorCode`; the sentence is for people.
8411
+ additionalProperties: false
8412
+ required: [status, errorCode, errorMessage]
8413
+ properties:
8414
+ status: { const: failed }
8415
+ errorCode: { type: string, description: 'Open string: `conflict.run_exists` (server >= 7.125.0) or the device-placement refusal code.' }
8416
+ errorMessage: { type: string, description: 'The refusal sentence (display only — never match on it).' }
8417
+
8224
8418
  RunReceipt:
8225
8419
  type: object
8226
8420
  # 封闭(census 轴一,2026-07-30):producer 铸造点 routes/runs.ts 与 idem 重放臂各自逐字写死
@@ -8249,6 +8443,25 @@ components:
8249
8443
  description: 'v1 sole value, the same word as settings-schema `SEALED_BOX_ALG`; another alg is a loud per-model refusal.'
8250
8444
  publicKeyId: { type: string, description: 'Goes into `sealedApiKey.publicKeyId` (server default mint `exec-<first 12 hex of sha256(raw public key)>`; treat as opaque); the engine opens with EXACTLY this key.' }
8251
8445
  publicKey: { type: string, description: 'STANDARD base64 of the 32-byte X25519 public key (44 chars). Public — safe to log.' }
8446
+ WorkspaceSyncCapability:
8447
+ type: object
8448
+ description: >
8449
+ server >= 7.125.0 (contract appendix B.1 / B.1a) — the `GET /v1/capabilities` `workspaceSync` segment: EXACTLY two keys, key-for-key the server's `src/workspace-space.ts`
8450
+ `WorkspaceSyncCapability`. Both are ALWAYS-PRESENT members of the object (B.1a); the object itself is present-iff (see `Capabilities.workspaceSync`).
8451
+ additionalProperties: false
8452
+ required: [persist, keep]
8453
+ properties:
8454
+ persist:
8455
+ type: string
8456
+ enum: [space, off]
8457
+ description: >
8458
+ The operator knob `WORKSPACE_PERSIST` (default `off` — persistence is opt-in), a TWO-WORD closed set. `space` = the operator switched it on: the k8s `/workspace` persists across runs per session occupant. `off` = the snapshot
8459
+ store IS wired but persistence is not switched on (the default, or the operator switched it off): a fresh workspace every run — NOT the same as the whole object being absent (absent = no such facility on this deployment).
8460
+ keep:
8461
+ type: integer
8462
+ minimum: 1
8463
+ maximum: 50
8464
+ description: 'Snapshots kept per space (the session occupant) (`WORKSPACE_SNAPSHOT_KEEP`, an integer in [1, 50], default 3); older ones are deleted after each successful save.'
8252
8465
  StoreCapability:
8253
8466
  type: object
8254
8467
  description: >
@@ -8413,6 +8626,13 @@ components:
8413
8626
  taskId: { type: string }
8414
8627
  sessionId: { type: string }
8415
8628
  status: { $ref: '#/components/schemas/RunStatus' }
8629
+ replay:
8630
+ const: true
8631
+ description: >
8632
+ server >= 7.125.0 (additive; contract §0.7). Present ONLY on the IDEMPOTENT-REPLAY body of a submit — `POST /v1/tasks` 200, and the same body as
8633
+ `done.result` on `POST /v1/tasks/stream` (see `RunReplayRecord`) — where it is always `true`: the submit's key had already built this run row, so the
8634
+ answer is the row's read face plus this marker, NOT the first answer's engine result. ABSENT on `GET /v1/runs/{taskId}` and
8635
+ `GET /v1/runs?idempotencyKey=` (those are the read face itself) and never `false`. Branch on `replay === true`; the outcome is in `result`.
8416
8636
  msSinceLastActivity:
8417
8637
  type: integer
8418
8638
  description: >
@@ -8741,6 +8961,13 @@ components:
8741
8961
  usedMicroUsd: { type: integer }
8742
8962
  limitMicroUsd: { type: integer }
8743
8963
  retryAfterSec: { type: integer }
8964
+ windowResetsAt:
8965
+ type: string
8966
+ format: date-time
8967
+ description: >
8968
+ server >= 7.125.0 (additive). The end of the current quota window (ISO 8601) — the same instant `GET /v1/usage` reports as
8969
+ `UsageInfo.windowResetsAt`. `retryAfterSec` and the `Retry-After` header stay as they were and share its clock read.
8970
+ Absent = an older server.
8744
8971
 
8745
8972
  TraceTurn:
8746
8973
  type: object
@@ -9909,6 +10136,10 @@ components:
9909
10136
  (`DB_BACKEND=memory`, multi-tenant bare boot) this bit is now false and the card and the RPC both answer 404 (7.115.x:
9910
10137
  true, card 200, every `message/send` / `tasks/get` a JSON-RPC `-32004`); the startup log carries a warn
9911
10138
  `a2a_serve_not_served`. The old "the server deliberately does not fold the run store into this bit" note is retired.
10139
+ server >= 7.125.0 (contract §0.7): an A2A `message/send` shares the submit idempotency scope with the HTTP submits (family `run`; the key is derived from the
10140
+ message's `messageId`, its content, `contextId` and skill id, and scoped by the caller identity) — a repeat that finds the existing run answers that task's protocol object immediately (it never enters the
10141
+ `blocking` wait), and a hit the caller cannot read, or that names a different run, is a JSON-RPC `-32602` (Invalid params) error, the same refusal the HTTP
10142
+ faces give as 409 `conflict.run_exists`.
9912
10143
  agentRoster:
9913
10144
  type: boolean
9914
10145
  description: >
@@ -10075,6 +10306,20 @@ components:
10075
10306
  do NOT seal — use `apiKeyEnv`, or fix the custody directory and restart. Absent is a missing key, never `null`.
10076
10307
  🔴 ONE STORE PER HOST (`<data root>/sealed-key/`): replicas that do not share that directory answer different
10077
10308
  keys, and a ciphertext sealed against replica A's key is `stale_ciphertext` on replica B.
10309
+ workspaceSync:
10310
+ allOf: [{ $ref: '#/components/schemas/WorkspaceSyncCapability' }]
10311
+ description: >
10312
+ server >= 7.125.0 (contract appendix B.1 / B.1a) — WORKSPACE KEPT ACROSS RUNS: the k8s lane's `/workspace` is stored in object storage per session OCCUPANT (the session incarnation: delete the session and re-register the same id = a NEW space, starting from an empty directory). Each run's end
10313
+ snapshots the workspace into `<prefix>/<incarnation>/space/` (a sub-prefix this lane owns; the park-suspend snapshots live outside it and are never pruned), moves the `space/latest` pointer and keeps only
10314
+ the newest `keep`; the next run's start (when it connects the sandbox) restores from `latest`. A replica that cannot read the session occupant neither restores nor saves for that run (an operator line) and does NOT fall back to the session id.
10315
+ A shell / center renders "work files kept across turns" only when `persist` is `"space"` (the default is `off`; persistence is opt-in).
10316
+ 🔴 PRESENT-IFF — one of the three per-key exceptions on this surface (with `sealedKey` and `centerWiring`, contract appendix B.1): the facility = the k8s lane AND a snapshot store (the three
10317
+ `MINIO_*` keys). ABSENT is a missing key, never `false` / `null` / `{}`, and means one of TWO things with the SAME action: an older server (< 7.125.0, does not know this bit), or a deployment that
10318
+ is not the k8s lane / has no snapshot store (the same body's `version` tells them apart) — either way do NOT promise "last run's files are still there"; treat each run's workspace as an empty directory.
10319
+ `persist: "off"` means the store is present and the capability is present but nothing is retained today (the default, or the operator switched it off) — different from absent. Covers `/workspace` only (process / memory state does not cross runs).
10320
+ FAILURE IS LOUD: failing to read `latest` or the commit marker, a `latest` that points at a missing snapshot object, or a restore that fails midway fails THAT run's sandbox connect (never a silent empty
10321
+ directory; a partial directory is not handed to tools and is not snapshotted back); a failed end-of-run snapshot puts the residual key `workspaceSnapshot: "failed"` on the fleet terminal `task` frame
10322
+ (`FleetTaskRow.workspaceSnapshot`), and the next run restores the PREVIOUS `latest`.
10078
10323
  background:
10079
10324
  type: object
10080
10325
  additionalProperties: true
@@ -11322,15 +11567,23 @@ components:
11322
11567
  Per-principal cost/quota snapshot (GET /v1/usage; shape audited from source). Computed from
11323
11568
  the REQUEST principal. When no quota window is configured → enabled=false + ONLY windowSec/maxTask*
11324
11569
  (the amount/limit fields are ABSENT) — render must tolerate the gap.
11570
+ server >= 7.125.0: an `enabled:true` body ALWAYS carries `windowResetsAt` (the end of the current quota window),
11571
+ zero spend and the limit-0 switched-off sentinel included; an `enabled:false` body is unchanged and has no such key.
11325
11572
  # 🔴 封闭 + required 补铸造点(census 批2 第二段,2026-07-30)。两条腿都是逐字字面量
11326
11573
  # (`routes/observability.ts` —— 无配额臂 4 键 / 有配额臂 11 键),11 个属性就是并集。
11574
+ # server 7.125.0:有配额臂 +`windowResetsAt` = 12 键,并集 12 个属性(无配额臂 4 键不变)。
11327
11575
  # required 从 `[enabled]` 抬到四键:`windowSec`/`maxTaskCostUsd`/`maxTaskTokens` 在**两条腿上都**
11328
11576
  # 无条件发送(config 直读),只列 `enabled` 是把恒在键写成了可缺 —— 消费方因此得写永远为真的判空。
11329
11577
  required: [enabled, windowSec, maxTaskCostUsd, maxTaskTokens]
11330
11578
  additionalProperties: false
11331
11579
  properties:
11332
11580
  enabled: { type: boolean, description: 'A cost-quota WINDOW is configured (false ⇒ single-task ceilings only).' }
11333
- windowSec: { type: integer, description: 'Rolling quota window seconds (e.g. 86400 = daily).' }
11581
+ windowSec:
11582
+ type: integer
11583
+ description: >
11584
+ Quota window length in seconds (e.g. 86400 = daily). server >= 7.125.0: on EVERY deployment the window is a
11585
+ clock-aligned bucket (bucket number = `floor(now / windowSec)`, a daily window rolls at UTC midnight) — not a window that
11586
+ starts at the principal's first spend and not a sliding one; read the roll instant from `windowResetsAt`.
11334
11587
  usedMicroUsd: { type: integer, description: 'Spent in the current window (micro-USD = 1e-6 USD).' }
11335
11588
  usedUsd: { type: number, description: 'Spent in the current window (USD; convenience projection).' }
11336
11589
  limitMicroUsd: { type: integer, description: 'Window quota ceiling (micro-USD).' }
@@ -11338,6 +11591,15 @@ components:
11338
11591
  remainingMicroUsd: { type: integer, description: 'Window remaining (limit - used, floored at 0).' }
11339
11592
  overLimit: { type: boolean, description: 'Window quota exceeded (new tasks blocked until retryAfterSec).' }
11340
11593
  retryAfterSec: { type: integer, description: 'Suggested wait when overLimit.' }
11594
+ windowResetsAt:
11595
+ type: string
11596
+ format: date-time
11597
+ description: >
11598
+ server >= 7.125.0 (additive). The END of the current quota window (ISO 8601) — the instant the clock-aligned bucket rolls
11599
+ over and `usedMicroUsd` starts again from zero. ALWAYS present when `enabled` is true (the window is a clock-aligned bucket,
11600
+ so a zero-spend principal and the limit-0 switched-off sentinel report it too — the window length is a configured fact); ABSENT
11601
+ when `enabled` is false. ABSENT on an `enabled:true` body = an older server, NOT "the window does not roll". The same instant rides
11602
+ the 429 `limit.cost_quota_exceeded` body (`QuotaError.windowResetsAt`), derived from the same clock read as `retryAfterSec`.
11341
11603
  maxTaskCostUsd: { type: number, description: 'Single-task cost hard ceiling (USD; independent of the window, always present).' }
11342
11604
  maxTaskTokens: { type: integer, description: 'Single-task token hard ceiling (independent of the window).' }
11343
11605
 
@@ -12007,6 +12269,17 @@ components:
12007
12269
  workflow `wa*` agent, and the TOP-LEVEL run row — has no generation concept at all. Absent means "no
12008
12270
  such concept", NOT "generation 1"; a consumer that reads absence as 1 will misread a late first frame
12009
12271
  as a revive.
12272
+ workspaceSnapshot:
12273
+ type: string
12274
+ enum: [failed]
12275
+ description: >
12276
+ server >= 7.125.0 (contract appendix H.3b), ADDITIVE / tolerate-absent, a TERMINAL top-level run frame's residual key (`completed` / `failed` / `killed`; a parked frame NEVER carries it).
12277
+ Has a source only while the k8s lane keeps the workspace across runs (`capabilities.workspaceSync.persist: "space"`). A SINGLE-MEMBER CLOSED SET, the one word `failed`:
12278
+ the workspace snapshot at the end of THIS run was NOT saved (the upload failed, or the `latest` pointer write failed). The pod is deleted anyway; `latest` still points at the previous snapshot, so the next
12279
+ run of the same session restores the PREVIOUS one and this run's changes to `/workspace` are lost. Render "this turn's work files were not saved" — NOT as a run failure (`status` is still the run's own
12280
+ terminal state). ABSENT = saved / persistence off / not the k8s lane / the run never touched a sandbox / an older server / OR the save took longer than the engine's 15-second teardown bound and its outcome
12281
+ arrived after the terminal frame — so absence must NEVER be read as "definitely saved": it only says "no failure to report". Source: the save outcome the server notes before the engine hands back its result
12282
+ (keyed by the run's session id), reported once. ONLY the outcome that settled before THIS run's terminal frame is carried: one that settles after the teardown bound stays an operator log line and is never attached to the NEXT run's terminal frame of the same session. The `?session=` scoped (unattributed-row) projection keeps this key (a structural fact, zero conversation content).
12010
12283
  retiredBy:
12011
12284
  # 🔴 NO `enum` (codex R1-F3, verified): the description below REQUIRES open-on-read, and the SDK type is
12012
12285
  # open (`"reconcile" | (string & {})`). A closed enum would make a strict generated client reject a
@@ -13154,8 +13427,36 @@ components:
13154
13427
  taskId: { type: string, description: The later run on whose first beat the notice was emitted. }
13155
13428
  messages:
13156
13429
  type: array
13430
+ description: >
13431
+ The engine's message rows (opaque here; their shapes are the engine's). server >= 7.125.0 (contract section 20): the ONE key the server adds itself is the
13432
+ rewind handle `taskId` on a `role: "user"` row — the `taskId` of the run that submitted that prompt, to be handed to `resumeAt` (rewind =
13433
+ the existing `POST /v1/runs` / `POST /v1/tasks` with `{ sessionId, resumeAt: <taskId> }`, no new verb). It rides EVERY way of fetching the rows (whole read /
13434
+ `tail` / `before` / `blobCap` / `?message=N`). PRESENT-IFF the anchor store holds EXACTLY ONE row for that user entry whose owner is the reader (the same
13435
+ predicate `resumeAt` resolves with, so a handle on a row always resolves). ABSENT = no such anchor: an older entry (anchors are written only from the point
13436
+ they were introduced; no backfill), an anchor owned by someone else (mixed-owner session), the same entry named by two anchor rows (not picked), or a prompt
13437
+ submitted through the non-streaming `POST /v1/tasks` (only the durable legs — `POST /v1/runs`, `POST /v1/tasks/stream`, the resume leg — write user-message
13438
+ anchors; resuming at that run's `taskId` is 404 `resume_at.unknown_event`, the same fact as the key being absent). A later user message (steer) inside one run
13439
+ moves that run's anchor to the last-submitted one, so the handle appears on that row. assistant / toolResult / other-role rows never carry it. The value is a
13440
+ server-minted identifier, verbatim (not redacted). Older servers (<= 7.124.x) never emit it.
13157
13441
  items: {}
13158
13442
 
13443
+ SessionUserMessageRow:
13444
+ type: object
13445
+ description: >
13446
+ The `role: "user"` arm of a `SessionRecord.messages` row — the NARROWING TARGET for reading the rewind handle (server >= 7.125.0, contract section 20). `messages` rows are the
13447
+ engine's own shapes and stay open/opaque; the only key the server itself adds is `taskId` on a user row. OPEN object: every other key is the engine's.
13448
+ required: [role]
13449
+ additionalProperties: true
13450
+ properties:
13451
+ role: { const: user }
13452
+ taskId:
13453
+ type: string
13454
+ description: >
13455
+ The rewind handle: the `taskId` of the run that submitted this prompt, handed to `resumeAt` (`POST /v1/runs` / `POST /v1/tasks` with `{ sessionId, resumeAt: <taskId> }`).
13456
+ PRESENT-IFF the anchor store holds EXACTLY ONE row for this user entry owned by the reader — the same predicate `resumeAt` resolves with, so a handle on a row always
13457
+ resolves. ABSENT = an older entry (no backfill) / an anchor owned by someone else / the same entry named by two anchor rows / a prompt submitted through the non-streaming
13458
+ `POST /v1/tasks` (no anchor is written there; resuming at that run's `taskId` is 404 `resume_at.unknown_event`) / an older server (<= 7.124.x). A server-minted identifier, verbatim.
13459
+
13159
13460
  SessionSummary:
13160
13461
  type: object
13161
13462
  description: >
@@ -13186,6 +13487,9 @@ components:
13186
13487
  Auto-generated session summary title (service-side `session-titler`: one cheap model call kicked off in
13187
13488
  the background after the first submit, idempotent via an IS-NULL gate). Genuinely `null` until it lands,
13188
13489
  so render a fallback rather than waiting on it.
13490
+ server >= 7.125.0 (contract section 20): ONE column with TWO writers — the user's string (`PATCH /v1/sessions/{sessionId}`, unconditional;
13491
+ `null` clears it back) wins, otherwise the auto-titler's (fills ONLY a NULL column), otherwise `null`. So read it as "the user's title, else the
13492
+ automatic one, else null" — a reader that took it as "always the automatic title" must change. <= 7.124.x: only the auto-titler writes it.
13189
13493
  tempo:
13190
13494
  type: string
13191
13495
  enum: [active, idle, blocked]
@@ -13193,6 +13497,32 @@ components:
13193
13497
  server ≥7.91.0 (additive, absent on older servers): the session's activity word, derived from its most
13194
13498
  recent run's status by the ONE mapping table the peer directory also reads (running → active;
13195
13499
  suspended / needs_review → blocked; no non-terminal run → idle).
13500
+ pinned:
13501
+ type: boolean
13502
+ description: >
13503
+ server >= 7.125.0 (contract section 20): the user's pin (written by `PATCH /v1/sessions/{sessionId}`), default `false`. ALWAYS present on
13504
+ >= 7.125.0 (a session's state, not a capability — NOT present-iff); a lister face with no session metadata (the in-memory run-store
13505
+ aggregate) always answers `false`. ABSENT = an older server (<= 7.124.x) — read as "not pinned". Not in `required` for that reason only.
13506
+ The list order is unchanged (recency by `lastActivityAt`); the client groups the pinned section. Renaming / pinning does not move `lastActivityAt`.
13507
+
13508
+ SessionPatchBody:
13509
+ type: object
13510
+ description: >
13511
+ `PATCH /v1/sessions/{sessionId}` body (server >= 7.125.0, contract section 20) — a CLOSED set of two keys, at least one: any other key, an empty body or a
13512
+ non-object is `400 request.body_shape`. `title`: a string renames (1..120 CHARACTERS counted as Unicode characters = the storage column width, no leading /
13513
+ trailing whitespace, stored verbatim — not rewritten, not redacted); `null` CLEARS it back to the automatic title (the next submit lets the auto-titler fill
13514
+ it again). `""` is NOT a clear — it is `400 request.body_shape`. A string of the wrong form / a non-string-non-null `title` / a non-boolean `pinned` is
13515
+ `400 request.field_invalid`.
13516
+ additionalProperties: false
13517
+ minProperties: 1
13518
+ properties:
13519
+ title:
13520
+ type: [string, 'null']
13521
+ minLength: 1
13522
+ maxLength: 120
13523
+ pattern: '^\S([\s\S]*\S)?$'
13524
+ description: 'A string = rename (1..120 characters, no leading / trailing whitespace); `null` = clear back to the automatic title; `""` is a 400 (the only spelling of "clear" is `null`).'
13525
+ pinned: { type: boolean, description: 'The pin (default `false` on a row that never set it).' }
13196
13526
 
13197
13527
  SessionWindow:
13198
13528
  type: object
@@ -13220,7 +13550,7 @@ components:
13220
13550
  type: ['string', 'null']
13221
13551
  description: 'the compaction floor this index is relative to, when one applies.'
13222
13552
  index: { type: integer, description: 'the requested message index (echoed).' }
13223
- message: { description: 'OPEN — the message payload as stored; shape belongs to the transcript format, not to this envelope.' }
13553
+ message: { description: 'OPEN — the message payload as stored; shape belongs to the transcript format, not to this envelope. server >= 7.125.0: a `role: "user"` message carries the same present-iff rewind handle `taskId` as on the whole-record read (see `SessionRecord.messages`).' }
13224
13554
 
13225
13555
  PromptEpoch:
13226
13556
  type: object
@@ -13645,13 +13975,15 @@ components:
13645
13975
  snapshot: entry IDS (oldest-first, NOT payloads) + the file-history envelope + policy + anchors + leaf.
13646
13976
  🔴 server >= 7.52 (S-15, design/381; re-read byte for byte against 7.116.0): the key `snapshots` (per-snapshot
13647
13977
  relPath→blobHash, the retired E19 whole-tree snapshot epoch) is NO LONGER SENT — the server's `exportSessionManifest` mints
13648
- `{ sessionId, entryIds, entryCount, leafId, fileHistory, policy, anchors }` and `fileHistory` (core's versioned
13978
+ `{ sessionId, entryIds, entryCount, leafId, fileHistory, policy, anchors }` (server >= 7.125.0 adds the session row's two writable bits `title` / `pinned`,
13979
+ present-iff the session is a row of the list face — see the two properties) and `fileHistory` (core's versioned
13649
13980
  `FileHistoryExport` envelope; `null` = no history / the source store cannot export) replaces it. <= 7.51 servers still send
13650
13981
  `snapshots` and no `fileHistory`. The blob BYTES never ride the manifest: content-addressed, via the history blob faces
13651
13982
  (`GET …/sync/history/blobs/{hash}` / `PUT …/sync/blobs/{hash}`; probe `capabilities.sessionSyncFileHistory` on BOTH ends).
13652
13983
  The SDK's `manifest()` fills `snapshots: []` when the wire omits it.
13653
13984
  # 封闭(census 批2 第三段,2026-07-30;16.2.0 按 7.116.0 发布字节重核):铸造点 `session-sync.ts`(exportSessionManifest 尾部
13654
13985
  # 字面量)恰是 `{ sessionId, entryIds, entryCount, leafId, fileHistory, policy, anchors }` 7 键(≥7.52),无 `snapshots`;
13986
+ # server ≥7.125.0 另有 `title` / `pinned` 两键(会话行可写位,present-iff 会话是列表面上的行;条件展开,不进 required);
13655
13987
  # ≤7.51 老 server 发 `snapshots`、无 `fileHistory` ⇒ 两键都不进 required(支持底线 3.0.0)。
13656
13988
  additionalProperties: false
13657
13989
  required: [sessionId, entryIds, entryCount, leafId, policy, anchors]
@@ -13668,6 +14000,15 @@ components:
13668
14000
  description: 'server >= 7.52: the per-edited-file rewind history envelope; `null` = no history / the source store cannot export; key ABSENT = a <= 7.51 server (that generation sends `snapshots`).'
13669
14001
  policy: { type: array, items: { $ref: '#/components/schemas/SessionRulesRecord' } }
13670
14002
  anchors: { type: array, items: { $ref: '#/components/schemas/SyncAnchor' } }
14003
+ title:
14004
+ type: [string, 'null']
14005
+ description: >
14006
+ server >= 7.125.0 (contract section 20): the session row's title, read through the LIST ROW's one projection (the same value as `SessionSummary.title`: the
14007
+ user's string, else the auto-titler's). `null` = no title — and a PUSH that replays it (`sync/import` Phase A `title: null`) CLEARS the destination back to the
14008
+ automatic title, the same law as `PATCH`. PRESENT-IFF the session is a row of the list face (every ordinary session; a placed delegation-transcript
14009
+ session has no writable row bits, so BOTH keys are absent there). ABSENT also = an older server (<= 7.124.x). Not part of the `basis` / `payloadVerified` judgement
14010
+ (those two speak only for the conversation log).
14011
+ pinned: { type: boolean, description: 'server >= 7.125.0 (contract section 20): the session row''s pin (same value as `SessionSummary.pinned`; present-iff exactly as `title` above).' }
13671
14012
 
13672
14013
  FileHistoryExport:
13673
14014
  type: object
@@ -17892,12 +18233,18 @@ components:
17892
18233
  Event_done:
17893
18234
  type: object
17894
18235
  description: >
17895
- The stream's terminal frame. `result` is a DISCRIMINATED UNION of TWO real shapes (契约调和车1 E,
18236
+ The stream's terminal frame. `result` is a DISCRIMINATED UNION of real shapes (契约调和车1 E,
17896
18237
  2026-08-02) — do NOT assume the full TaskResult:
17897
18238
  · `TaskResult` — the normal terminal (output string at `.result`, stats at `.stats`), service Drift 4;
17898
18239
  · `ActiveRunConflictDoneResult` — the SSE lane's REJECTION terminal: when the session already has an
17899
18240
  active run, the submit's 409 is encoded as this done frame (`src/http/routes/tasks.ts`), and it
17900
- carries NO `taskId` / `sessionId` / `stats`.
18241
+ carries NO `taskId` / `sessionId` / `stats`;
18242
+ · `RunReplayRecord` (server >= 7.125.0, contract §0.7) — the IDEMPOTENT-REPLAY terminal of `POST /v1/tasks/stream`: the submit's key had already built a run row, so
18243
+ `result` is that row's read face plus `replay: true` — NOT an engine result (no top-level `terminal` / `stats`; the outcome is the nested `result`). The frame carries
18244
+ `replay: true` as well. (A same-key duplicate still in flight in the same process is merged instead: the frame has `replay: true` but `result` is the first answer's
18245
+ `TaskResult` verbatim — so the FRAME's `replay` does not tell the shapes apart; `result.replay` does.)
18246
+ · `SubmitRefusalDoneResult` — a submit refused after the stream was already open, as `{status:"failed", errorCode, errorMessage}` (server >= 7.125.0: `conflict.run_exists` when
18247
+ the replay is discovered at row-creation time and the caller cannot read that row; also the device-placement refusal). Not an engine result; carries no `taskId` / `stats`.
17901
18248
  Narrow in TWO steps: `stats` present (an engine result) vs absent (the conflict envelope) — or match
17902
18249
  `errorCode === "conflict.session_active_run"` — and then, among the engine results, `"terminal" in result`
17903
18250
  (the current cause form) vs absent (`LegacyPlaneTaskResult`, the flat bytes an older generation left in
@@ -17916,14 +18263,17 @@ components:
17916
18263
  - $ref: '#/components/schemas/TaskResult'
17917
18264
  - $ref: '#/components/schemas/ActiveRunConflictDoneResult'
17918
18265
  - $ref: '#/components/schemas/LegacyPlaneTaskResult'
18266
+ - $ref: '#/components/schemas/RunReplayRecord'
18267
+ - $ref: '#/components/schemas/SubmitRefusalDoneResult'
17919
18268
  replay:
17920
18269
  type: boolean
17921
18270
  description: >
17922
18271
  契约调和车1 F(2026-08-02;server `src/http/routes/tasks.ts` 的两处铸造点)— `true`
17923
18272
  marks the IDEMPOTENT-REPLAY leg: this terminal was NOT produced by a live run of your submit. Either
17924
- the `Idempotency-Key` hit the in-flight/settled cache, or your call was deduplicated into a
17925
- concurrent identical submit and received no live events at all. The `result` is the original
17926
- run's terminal, verbatim. ABSENT on every live leg (never `false`) — a UI can use it to say "replayed"
18273
+ your call was deduplicated into a concurrent identical submit still in flight in the same process (then `result` is the original
18274
+ run's terminal, verbatim, and you received no live events at all), or — server >= 7.125.0 — the `Idempotency-Key` had already built a run
18275
+ row and `result` is that row's read face plus `replay: true` (`RunReplayRecord`; NOT an engine result). The frame's flag does not
18276
+ tell the two apart; `result.replay === true` does. ABSENT on every live leg (never `false`) — a UI can use it to say "replayed"
17927
18277
  instead of implying the work just ran twice.
17928
18278
 
17929
18279
  ActiveRunConflictDoneResult: