@namzu/sdk 20.2.0 → 20.4.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 (82) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/dist/manager/run/persistence.d.ts +2 -2
  3. package/dist/manager/run/persistence.d.ts.map +1 -1
  4. package/dist/manager/run/persistence.js +14 -5
  5. package/dist/manager/run/persistence.js.map +1 -1
  6. package/dist/public-runtime.d.ts +2 -1
  7. package/dist/public-runtime.d.ts.map +1 -1
  8. package/dist/public-runtime.js +6 -1
  9. package/dist/public-runtime.js.map +1 -1
  10. package/dist/runtime/query/checkpoint.d.ts +27 -1
  11. package/dist/runtime/query/checkpoint.d.ts.map +1 -1
  12. package/dist/runtime/query/checkpoint.js +34 -4
  13. package/dist/runtime/query/checkpoint.js.map +1 -1
  14. package/dist/runtime/query/context.d.ts +2 -0
  15. package/dist/runtime/query/context.d.ts.map +1 -1
  16. package/dist/runtime/query/context.js +1 -0
  17. package/dist/runtime/query/context.js.map +1 -1
  18. package/dist/runtime/query/index.d.ts +32 -1
  19. package/dist/runtime/query/index.d.ts.map +1 -1
  20. package/dist/runtime/query/index.js +5 -0
  21. package/dist/runtime/query/index.js.map +1 -1
  22. package/dist/runtime/query/resume-run.d.ts +9 -1
  23. package/dist/runtime/query/resume-run.d.ts.map +1 -1
  24. package/dist/runtime/query/resume-run.js +2 -1
  25. package/dist/runtime/query/resume-run.js.map +1 -1
  26. package/dist/store/index.d.ts +2 -1
  27. package/dist/store/index.d.ts.map +1 -1
  28. package/dist/store/index.js +2 -1
  29. package/dist/store/index.js.map +1 -1
  30. package/dist/store/run/checkpoint-disk.d.ts +18 -2
  31. package/dist/store/run/checkpoint-disk.d.ts.map +1 -1
  32. package/dist/store/run/checkpoint-disk.js +50 -5
  33. package/dist/store/run/checkpoint-disk.js.map +1 -1
  34. package/dist/store/run/checkpoint-memory.d.ts +22 -2
  35. package/dist/store/run/checkpoint-memory.d.ts.map +1 -1
  36. package/dist/store/run/checkpoint-memory.js +99 -4
  37. package/dist/store/run/checkpoint-memory.js.map +1 -1
  38. package/dist/store/run/claim-disk.d.ts +130 -0
  39. package/dist/store/run/claim-disk.d.ts.map +1 -0
  40. package/dist/store/run/claim-disk.js +550 -0
  41. package/dist/store/run/claim-disk.js.map +1 -0
  42. package/dist/store/run/disk.d.ts +10 -8
  43. package/dist/store/run/disk.d.ts.map +1 -1
  44. package/dist/store/run/disk.js.map +1 -1
  45. package/dist/store/run/listing.d.ts +44 -1
  46. package/dist/store/run/listing.d.ts.map +1 -1
  47. package/dist/store/run/listing.js +92 -1
  48. package/dist/store/run/listing.js.map +1 -1
  49. package/dist/store/run/memory.d.ts +46 -0
  50. package/dist/store/run/memory.d.ts.map +1 -0
  51. package/dist/store/run/memory.js +104 -0
  52. package/dist/store/run/memory.js.map +1 -0
  53. package/dist/types/run/checkpoint-store.d.ts +178 -2
  54. package/dist/types/run/checkpoint-store.d.ts.map +1 -1
  55. package/dist/types/run/config.d.ts +12 -0
  56. package/dist/types/run/config.d.ts.map +1 -1
  57. package/dist/types/run/index.d.ts +1 -0
  58. package/dist/types/run/index.d.ts.map +1 -1
  59. package/dist/types/run/index.js +1 -0
  60. package/dist/types/run/index.js.map +1 -1
  61. package/dist/types/run/store.d.ts +103 -0
  62. package/dist/types/run/store.d.ts.map +1 -0
  63. package/dist/types/run/store.js +30 -0
  64. package/dist/types/run/store.js.map +1 -0
  65. package/package.json +1 -1
  66. package/src/manager/run/persistence.ts +17 -7
  67. package/src/public-runtime.ts +6 -0
  68. package/src/runtime/query/checkpoint.ts +42 -5
  69. package/src/runtime/query/context.ts +3 -0
  70. package/src/runtime/query/index.ts +39 -1
  71. package/src/runtime/query/resume-run.ts +12 -2
  72. package/src/store/index.ts +5 -0
  73. package/src/store/run/checkpoint-disk.ts +70 -5
  74. package/src/store/run/checkpoint-memory.ts +118 -3
  75. package/src/store/run/claim-disk.ts +593 -0
  76. package/src/store/run/disk.ts +10 -8
  77. package/src/store/run/listing.ts +116 -1
  78. package/src/store/run/memory.ts +121 -0
  79. package/src/types/run/checkpoint-store.ts +189 -2
  80. package/src/types/run/config.ts +13 -0
  81. package/src/types/run/index.ts +1 -0
  82. package/src/types/run/store.ts +112 -0
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/run/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAK3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,YAAY,EACX,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,GACtB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAChE,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACzF,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/types/run/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAK3B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAC3C,YAAY,EACX,oBAAoB,EACpB,wBAAwB,EACxB,uBAAuB,EACvB,sBAAsB,GACtB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,YAAY,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAChE,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACzF,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA"}
@@ -3,6 +3,7 @@ export * from './prepare-step.js';
3
3
  export * from './stop-reason.js';
4
4
  export * from './config.js';
5
5
  export * from './checkpoint-store.js';
6
+ export * from './store.js';
6
7
  export * from './entity.js';
7
8
  export * from './replay.js';
8
9
  // Domain `RunStatus` (session-hierarchy.md §4.6 state machine). Safe to
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/run/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,wEAAwE;AACxE,4EAA4E;AAC5E,0EAA0E;AAC1E,sBAAsB;AACtB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAQ1B,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/types/run/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAC3B,cAAc,uBAAuB,CAAA;AACrC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,wEAAwE;AACxE,4EAA4E;AAC5E,0EAA0E;AAC1E,sBAAsB;AACtB,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAQ1B,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAA"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * RunStore — persistence contract for a run's own evidence.
3
+ *
4
+ * The checkpoint store got an injectable seam and this did not, which left
5
+ * the run record, its messages, its transcript and its report reachable only
6
+ * through a concrete filesystem class. For a kernel whose stated purpose is
7
+ * auditable evidence, the evidence was the one thing that could not be
8
+ * pointed at durable storage: on ephemeral infrastructure the transcript dies
9
+ * with the container, and behind a load balancer two replicas write two
10
+ * disjoint run trees for one tenant.
11
+ *
12
+ * The location was already injectable through a path builder — but that
13
+ * returns filesystem path strings, so it relocates the directory without
14
+ * changing the medium.
15
+ *
16
+ * ## Bound to one run, unlike {@link CheckpointStore}
17
+ *
18
+ * Every accessor here addresses the run the store was bound to by
19
+ * {@link RunStore.initRun}, where a `CheckpointStore` takes an explicit scope
20
+ * per call. That asymmetry is inherited rather than chosen: this contract is
21
+ * extracted from a class the runtime already constructs per run and holds for
22
+ * the run's lifetime, and re-keying it would change every call site in the
23
+ * same change that introduces the seam — two risks where one will do.
24
+ *
25
+ * A host implementing a shared backend therefore keys its rows by the
26
+ * attribution it was constructed with plus the bound run id. If this is later
27
+ * re-keyed per call, it happens once, deliberately, as its own change.
28
+ */
29
+ import type { Run } from './entity.js';
30
+ import type { RunEvent } from './events.js';
31
+ /**
32
+ * One finished tool call, recovered from the run's own transcript.
33
+ *
34
+ * Re-declared here rather than imported from the disk store so the contract
35
+ * does not depend on an implementation of itself.
36
+ */
37
+ export interface CompletedToolRecord {
38
+ readonly toolUseId: string;
39
+ readonly toolName: string;
40
+ readonly result: string;
41
+ readonly isError: boolean;
42
+ }
43
+ export interface RunStore {
44
+ /**
45
+ * Bind this store to a run, before any other call.
46
+ *
47
+ * Returns a location when the backend has one — the built-in disk store
48
+ * returns the run's directory — and `null` when it does not. A caller
49
+ * that renders the value must treat `null` as "this run is not on a
50
+ * filesystem" rather than as an error: an in-memory or object-storage
51
+ * backend has nothing to print, and inventing a path for it would put a
52
+ * directory that does not exist in front of an operator.
53
+ */
54
+ initRun(runId: string, parentRunId?: string): Promise<string | null>;
55
+ /** Persist the run record: status, metadata, usage, timings. */
56
+ writeRunMeta(run: Run): Promise<void>;
57
+ /** Persist the run's full message history. */
58
+ writeMessages(run: Run): Promise<void>;
59
+ /**
60
+ * Append one event to the run's durable event log.
61
+ *
62
+ * High-frequency streaming deltas are excluded before they reach here —
63
+ * that exclusion is a deliberate trade and belongs to the emitter, not to
64
+ * the backend, so a store must not re-filter.
65
+ */
66
+ appendEvent(event: RunEvent): Promise<void>;
67
+ /**
68
+ * Persist the run's final report. Returns a location, or `null` when the
69
+ * backend has none. See {@link RunStore.initRun}.
70
+ */
71
+ writeReport(content: string): Promise<string | null>;
72
+ /**
73
+ * Every tool call this run has already finished, keyed by `toolUseId`.
74
+ *
75
+ * A batch's results reach the message history only once the WHOLE batch
76
+ * settles, so a hard kill part-way through loses every result that had
77
+ * already come back, and the resumed run re-executes those calls. For a
78
+ * file write that is waste; for a payment or an email it is a second one.
79
+ *
80
+ * A backend that does not retain individual events answers with an empty
81
+ * map, which costs re-execution and is honest. It must not answer with a
82
+ * PARTIAL map: a caller reads a present entry as "this call is already
83
+ * answered", so a half-remembered batch is worse than a forgotten one.
84
+ */
85
+ readCompletedTools(): Promise<Map<string, CompletedToolRecord>>;
86
+ /**
87
+ * Where this run's evidence lives, or `null` when it is not on a
88
+ * filesystem. Valid only after {@link RunStore.initRun}.
89
+ */
90
+ getRunDir(): string | null;
91
+ /**
92
+ * Record the run in a browsable catalogue of runs. OPTIONAL.
93
+ *
94
+ * Optional because it is the one method here that is not evidence: it
95
+ * maintains a convenience listing for a human reading the directory, and
96
+ * a backend whose runs are already queryable has nothing to add. The
97
+ * programmatic answer to "which runs are there" is
98
+ * `CheckpointStore.listDurableRuns`, which carries attribution and
99
+ * includes sub-runs; this does neither.
100
+ */
101
+ addToIndex?(run: Run): Promise<void>;
102
+ }
103
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/types/run/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,WAAW,QAAQ;IACxB;;;;;;;;;OASG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAEpE,gEAAgE;IAChE,YAAY,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAErC,8CAA8C;IAC9C,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEtC;;;;;;OAMG;IACH,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE3C;;;OAGG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAEpD;;;;;;;;;;;;OAYG;IACH,kBAAkB,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC,CAAA;IAE/D;;;OAGG;IACH,SAAS,IAAI,MAAM,GAAG,IAAI,CAAA;IAE1B;;;;;;;;;OASG;IACH,UAAU,CAAC,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACpC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * RunStore — persistence contract for a run's own evidence.
3
+ *
4
+ * The checkpoint store got an injectable seam and this did not, which left
5
+ * the run record, its messages, its transcript and its report reachable only
6
+ * through a concrete filesystem class. For a kernel whose stated purpose is
7
+ * auditable evidence, the evidence was the one thing that could not be
8
+ * pointed at durable storage: on ephemeral infrastructure the transcript dies
9
+ * with the container, and behind a load balancer two replicas write two
10
+ * disjoint run trees for one tenant.
11
+ *
12
+ * The location was already injectable through a path builder — but that
13
+ * returns filesystem path strings, so it relocates the directory without
14
+ * changing the medium.
15
+ *
16
+ * ## Bound to one run, unlike {@link CheckpointStore}
17
+ *
18
+ * Every accessor here addresses the run the store was bound to by
19
+ * {@link RunStore.initRun}, where a `CheckpointStore` takes an explicit scope
20
+ * per call. That asymmetry is inherited rather than chosen: this contract is
21
+ * extracted from a class the runtime already constructs per run and holds for
22
+ * the run's lifetime, and re-keying it would change every call site in the
23
+ * same change that introduces the seam — two risks where one will do.
24
+ *
25
+ * A host implementing a shared backend therefore keys its rows by the
26
+ * attribution it was constructed with plus the bound run id. If this is later
27
+ * re-keyed per call, it happens once, deliberately, as its own change.
28
+ */
29
+ export {};
30
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/types/run/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/sdk",
3
- "version": "20.2.0",
3
+ "version": "20.4.0",
4
4
  "description": "Open-source AI agent SDK with a built-in runtime. Nothing between you and your agents.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "type": "module",
@@ -9,6 +9,7 @@ import type { ProviderErrorInfo } from '../../types/provider/index.js'
9
9
  import type { CheckpointRunScope, CheckpointStore } from '../../types/run/checkpoint-store.js'
10
10
  import type { EmergencySaveData } from '../../types/run/emergency.js'
11
11
  import type { Run, RunPersistenceConfig, StepResult, StopReason } from '../../types/run/index.js'
12
+ import type { RunStore } from '../../types/run/store.js'
12
13
  import type { ProjectId, ThreadId } from '../../types/session/ids.js'
13
14
  import { type ModelPricing, ZERO_COST, accumulateCost } from '../../utils/cost.js'
14
15
  import { generateEmergencySaveId } from '../../utils/id.js'
@@ -16,7 +17,7 @@ import type { Logger } from '../../utils/logger.js'
16
17
 
17
18
  export class RunPersistence {
18
19
  private run: Run
19
- private runStore: RunDiskStore
20
+ private runStore: RunStore
20
21
  private checkpointStore: CheckpointStore
21
22
  private pricing?: ModelPricing
22
23
 
@@ -38,10 +39,17 @@ export class RunPersistence {
38
39
  this._tenantId = config.tenantId
39
40
  this._projectId = config.projectId
40
41
 
41
- this.runStore = new RunDiskStore({
42
- baseDir: config.outputDir,
43
- logger: config.log,
44
- })
42
+ // The run's own evidence goes through the same kind of injectable seam
43
+ // the checkpoint store already had. Without it the transcript, the run
44
+ // record and the report could only ever live on a local filesystem —
45
+ // which for a kernel whose purpose is auditable evidence made the
46
+ // evidence the one thing that could not be pointed at durable storage.
47
+ this.runStore =
48
+ config.runStore ??
49
+ new RunDiskStore({
50
+ baseDir: config.outputDir,
51
+ logger: config.log,
52
+ })
45
53
 
46
54
  // Checkpoints go through the injectable seam; the disk layout under
47
55
  // `outputDir` (same tree the runStore writes to) stays the default.
@@ -137,7 +145,7 @@ export class RunPersistence {
137
145
  return this.run
138
146
  }
139
147
 
140
- getRunStore(): RunDiskStore {
148
+ getRunStore(): RunStore {
141
149
  return this.runStore
142
150
  }
143
151
 
@@ -457,7 +465,9 @@ export class RunPersistence {
457
465
  async persist(): Promise<void> {
458
466
  await this.runStore.writeRunMeta(this.run)
459
467
  await this.runStore.writeMessages(this.run)
460
- await this.runStore.addToIndex(this.run)
468
+ // Optional on the contract: a backend whose runs are already queryable
469
+ // has no browsable directory to add a row to.
470
+ await this.runStore.addToIndex?.(this.run)
461
471
 
462
472
  if (this.run.result) {
463
473
  await this.runStore.writeReport(this.run.result)
@@ -200,6 +200,7 @@ export {
200
200
  DiskTaskStore,
201
201
  InMemoryCheckpointStore,
202
202
  InMemoryMemoryIndex,
203
+ InMemoryRunStore,
203
204
  InMemoryMemoryStore,
204
205
  InMemoryStore,
205
206
  InMemoryTaskStore,
@@ -217,6 +218,11 @@ export {
217
218
  paginateDurableRuns,
218
219
  toDurableRunEntry,
219
220
  } from './store/index.js'
221
+ // Cross-process possession of a run. `claimRun` REFUSES on a store that
222
+ // cannot arbitrate rather than proceeding unclaimed, because proceeding lets
223
+ // two workers restore one checkpoint, both run its tools and both write under
224
+ // one run id — which loses half the work and reports nothing.
225
+ export { claimRun, fencedOut, releaseRun, toClaimSummary } from './store/index.js'
220
226
 
221
227
  export {
222
228
  AgentRegistry,
@@ -11,7 +11,11 @@ import type {
11
11
  PendingDecision,
12
12
  } from '../../types/hitl/index.js'
13
13
  import type { AssistantMessage } from '../../types/message/index.js'
14
- import type { CheckpointRunScope, CheckpointStore } from '../../types/run/checkpoint-store.js'
14
+ import type {
15
+ CheckpointRunScope,
16
+ CheckpointStore,
17
+ ClaimFence,
18
+ } from '../../types/run/checkpoint-store.js'
15
19
  import type { EmergencySaveData } from '../../types/run/emergency.js'
16
20
  import type { CheckpointListEntry } from '../../types/run/replay.js'
17
21
  import { ZERO_COST } from '../../utils/cost.js'
@@ -154,6 +158,22 @@ export class CheckpointManager {
154
158
  /** See {@link setParkTtl}. */
155
159
  private parkTtlMs?: number
156
160
 
161
+ /**
162
+ * The claim this run holds, presented on every checkpoint write.
163
+ *
164
+ * Unset means unfenced, which is correct for a single-writer deployment
165
+ * and is what every run did before claims existed. Set it and a write from
166
+ * a superseded holding is refused by the store.
167
+ *
168
+ * This existed nowhere for one release, and the omission was invisible in
169
+ * the worst way: the claim, the fence and the refusal were all built and
170
+ * tested, and no code path between a run and its store carried the number,
171
+ * so every checkpoint a RUN wrote went out unfenced. A capability that is
172
+ * complete except for the wire between its halves reads exactly like a
173
+ * working one.
174
+ */
175
+ private claimFence?: ClaimFence
176
+
157
177
  /**
158
178
  * The run's attribution instant, stamped onto every checkpoint this
159
179
  * manager writes.
@@ -234,7 +254,7 @@ export class CheckpointManager {
234
254
  traceContext: this.traceSource?.(),
235
255
  }
236
256
 
237
- await this.store.writeCheckpoint(this.scope, checkpoint)
257
+ await this.store.writeCheckpoint(this.scope, checkpoint, this.claimFence)
238
258
  this.lastCreatedId = checkpoint.id
239
259
  return checkpoint
240
260
  }
@@ -298,10 +318,27 @@ export class CheckpointManager {
298
318
  ...(ttl !== undefined && ttl > 0 ? { deadlineAt: parkedAt + ttl } : {}),
299
319
  },
300
320
  }
301
- await this.store.writeCheckpoint(this.scope, parked)
321
+ await this.store.writeCheckpoint(this.scope, parked, this.claimFence)
302
322
  return parked
303
323
  }
304
324
 
325
+ /**
326
+ * Present this claim on every subsequent write. See {@link claimFence}.
327
+ *
328
+ * A setter rather than a constructor argument because a run is claimed at
329
+ * a different moment than it is constructed — a worker draining a queue
330
+ * takes the run, then builds the pipeline around it — and because a
331
+ * renewal mints a NEW fence mid-run that has to replace the old one.
332
+ */
333
+ setClaimFence(fence: ClaimFence | undefined): void {
334
+ this.claimFence = fence
335
+ }
336
+
337
+ /** The claim currently presented on writes, if any. */
338
+ get presentedFence(): ClaimFence | undefined {
339
+ return this.claimFence
340
+ }
341
+
305
342
  /** Default time-to-live applied to every park this manager records. */
306
343
  setParkTtl(ttlMs: number | undefined): void {
307
344
  this.parkTtlMs = ttlMs
@@ -329,7 +366,7 @@ export class CheckpointManager {
329
366
  decision: { action: 'pause', reason: 'The approval request expired without an answer.' },
330
367
  },
331
368
  }
332
- await this.store.writeCheckpoint(this.scope, expired)
369
+ await this.store.writeCheckpoint(this.scope, expired, this.claimFence)
333
370
  return expired
334
371
  }
335
372
 
@@ -352,7 +389,7 @@ export class CheckpointManager {
352
389
  ...checkpoint,
353
390
  pending: { ...checkpoint.pending, resolvedAt: Date.now(), decision },
354
391
  }
355
- await this.store.writeCheckpoint(this.scope, resolved)
392
+ await this.store.writeCheckpoint(this.scope, resolved, this.claimFence)
356
393
  return resolved
357
394
  }
358
395
 
@@ -17,6 +17,7 @@ import type { PermissionMode } from '../../types/permission/index.js'
17
17
  import type { LLMProvider } from '../../types/provider/index.js'
18
18
  import type { CheckpointStore } from '../../types/run/checkpoint-store.js'
19
19
  import type { AgentRunConfig } from '../../types/run/index.js'
20
+ import type { RunStore } from '../../types/run/store.js'
20
21
  import type { ProjectId, ThreadId } from '../../types/session/ids.js'
21
22
  import type { ModelPricing } from '../../utils/cost.js'
22
23
  import { generateRunId } from '../../utils/id.js'
@@ -62,6 +63,7 @@ export interface RunContextConfig {
62
63
  * output directory.
63
64
  */
64
65
  checkpointStore?: CheckpointStore
66
+ runStore?: RunStore
65
67
 
66
68
  /**
67
69
  * Optional injected migrator — tests pass a stub; production code relies
@@ -186,6 +188,7 @@ export class RunContextFactory {
186
188
  parentRunId: config.parentRunId,
187
189
  depth: config.depth,
188
190
  checkpointStore: config.checkpointStore,
191
+ runStore: config.runStore,
189
192
  })
190
193
 
191
194
  const trackingConfig = resolveActivityTracking(permissionMode, config.enableActivityTracking)
@@ -58,7 +58,7 @@ import type { AgentPersona } from '../../types/persona/index.js'
58
58
  import type { LLMProvider } from '../../types/provider/index.js'
59
59
  import type { TaskRouterConfig } from '../../types/router/index.js'
60
60
  import type { ReviewAnswer } from '../../types/run/answer-review.js'
61
- import type { CheckpointStore } from '../../types/run/checkpoint-store.js'
61
+ import type { CheckpointStore, ClaimFence } from '../../types/run/checkpoint-store.js'
62
62
  import type {
63
63
  AgentRunConfig,
64
64
  PrepareStepChain,
@@ -70,6 +70,7 @@ import type {
70
70
  } from '../../types/run/index.js'
71
71
  import type { PromoteMemory } from '../../types/run/memory-promotion.js'
72
72
  import { memoryCandidateFor } from '../../types/run/memory-promotion.js'
73
+ import type { RunStore } from '../../types/run/store.js'
73
74
  import type { Sandbox, SandboxProvider } from '../../types/sandbox/index.js'
74
75
  import type { ProjectId, ThreadId } from '../../types/session/ids.js'
75
76
  import type { Skill } from '../../types/skills/index.js'
@@ -412,6 +413,38 @@ export interface QueryParams {
412
413
  */
413
414
  checkpointStore?: CheckpointStore
414
415
 
416
+ /**
417
+ * The fence of the claim this worker holds on the run, from `claimRun`.
418
+ *
419
+ * Presented on every checkpoint the run writes, so a worker that stalled
420
+ * past its lease is refused rather than writing into a run somebody else
421
+ * has taken over. Omit it for single-writer deployments, which is what
422
+ * every run did before claims existed.
423
+ *
424
+ * This hop did not exist for a release. The claim, the fence and the
425
+ * store-side refusal were all built and tested, and no path between a run
426
+ * and its store carried the number — so every checkpoint a RUN wrote went
427
+ * out unfenced while the tests, which called the store directly, all
428
+ * passed. A capability complete except for the wire between its halves
429
+ * reads exactly like a working one.
430
+ *
431
+ * It fences checkpoints and nothing else. {@link QueryParams.runStore}
432
+ * takes no fence, so two workers that both took one run still overwrite
433
+ * each other's run record, transcript and report — see the changeset.
434
+ */
435
+ claimFence?: ClaimFence
436
+
437
+ /**
438
+ * Where this run records its own evidence — the run record, its messages,
439
+ * its transcript and its report. Defaults to the disk layout under the
440
+ * resolved output directory.
441
+ *
442
+ * The sibling of {@link QueryParams.checkpointStore}, and it should always
443
+ * have been one: checkpoints could be pointed at durable storage and the
444
+ * evidence could not.
445
+ */
446
+ runStore?: RunStore
447
+
415
448
  runId?: RunId
416
449
 
417
450
  parentRunId?: RunId
@@ -610,6 +643,7 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
610
643
  tenantId: params.tenantId,
611
644
  pathBuilder: params.pathBuilder,
612
645
  checkpointStore: params.checkpointStore,
646
+ runStore: params.runStore,
613
647
  runId: params.runId,
614
648
  parentRunId: params.parentRunId,
615
649
  depth: params.depth,
@@ -1000,6 +1034,10 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
1000
1034
  // And every park it records carries an absolute deadline, so an
1001
1035
  // unanswered approval cannot outlive the worker that asked for it.
1002
1036
  checkpointMgr.setParkTtl(params.runConfig.hitlParkTtlMs)
1037
+ // The claim this worker holds, if it took one. Without this hop the
1038
+ // fence exists, the refusal exists, and no checkpoint a RUN writes ever
1039
+ // carries a number — so a stalled worker is refused nowhere.
1040
+ checkpointMgr.setClaimFence(params.claimFence)
1003
1041
 
1004
1042
  // A question raised from inside a tool becomes a real checkpoint
1005
1043
  // here. It used to park under a synthetic id nothing ever wrote, so
@@ -1,5 +1,5 @@
1
1
  import type { PendingDecision } from '../../types/hitl/index.js'
2
- import type { CheckpointStore } from '../../types/run/checkpoint-store.js'
2
+ import type { CheckpointStore, ClaimFence } from '../../types/run/checkpoint-store.js'
3
3
  import type { Run } from '../../types/run/entity.js'
4
4
  import type { RunState } from '../../types/run/state.js'
5
5
  import { type QueryParams, drainQuery } from './index.js'
@@ -38,6 +38,15 @@ export interface ResumeRunParams
38
38
  readonly scope: RunStateScope
39
39
  /** Required to find the checkpoint; also threaded into the resumed run. */
40
40
  readonly checkpointStore: CheckpointStore
41
+
42
+ /**
43
+ * The fence of the claim this worker took on the run before resuming.
44
+ *
45
+ * A resume is the one moment two workers are most likely to collide — it
46
+ * is what a queue reader does with a parked run — so this is the call that
47
+ * most needs to carry one.
48
+ */
49
+ readonly claimFence?: ClaimFence
41
50
  /**
42
51
  * Resume a specific checkpoint instead of the one the store would pick.
43
52
  * Absent means the parked checkpoint if there is one, else the newest.
@@ -67,7 +76,7 @@ export interface ResumeRunParams
67
76
  * resumed past without the answer it is waiting for.
68
77
  */
69
78
  export async function resumeRun(params: ResumeRunParams): Promise<ResumeOutcome> {
70
- const { scope, checkpointStore, checkpointId, pendingDecision, ...rest } = params
79
+ const { scope, checkpointStore, checkpointId, pendingDecision, claimFence, ...rest } = params
71
80
 
72
81
  const state = await loadRunState(checkpointStore, scope, checkpointId)
73
82
  if (!state?.checkpointId) return { resumed: false, reason: 'no-checkpoint' }
@@ -86,6 +95,7 @@ export async function resumeRun(params: ResumeRunParams): Promise<ResumeOutcome>
86
95
  runId: state.runId,
87
96
  resumeFromCheckpoint: state.checkpointId,
88
97
  checkpointStore,
98
+ ...(claimFence !== undefined ? { claimFence } : {}),
89
99
  ...(pendingDecision ? { pendingDecision } : {}),
90
100
  } as QueryParams)
91
101
 
@@ -2,6 +2,7 @@ export { InMemoryStore } from './InMemoryStore.js'
2
2
  export type { Identifiable, Timestamped } from './InMemoryStore.js'
3
3
 
4
4
  export { RunDiskStore } from './run/disk.js'
5
+ export { InMemoryRunStore } from './run/memory.js'
5
6
  export { DiskCheckpointStore } from './run/checkpoint-disk.js'
6
7
  export type { DiskCheckpointStoreAttribution } from './run/checkpoint-disk.js'
7
8
  export { InMemoryCheckpointStore } from './run/checkpoint-memory.js'
@@ -17,8 +18,12 @@ export { InMemoryCheckpointStore } from './run/checkpoint-memory.js'
17
18
  // host has no use for is surface to keep correct forever for nobody.
18
19
  export {
19
20
  assertContiguousListingScope,
21
+ claimRun,
22
+ fencedOut,
20
23
  listDurableRuns,
21
24
  paginateDurableRuns,
25
+ releaseRun,
26
+ toClaimSummary,
22
27
  toDurableRunEntry,
23
28
  } from './run/listing.js'
24
29
 
@@ -7,14 +7,24 @@ import type {
7
7
  CheckpointListingScope,
8
8
  CheckpointRunScope,
9
9
  CheckpointStore,
10
+ ClaimFence,
11
+ ClaimRunOptions,
10
12
  DurableRunEntry,
11
13
  DurableRunPage,
12
14
  ListDurableRunsOptions,
15
+ RunClaim,
13
16
  } from '../../types/run/checkpoint-store.js'
14
17
  import type { RunStoreConfig } from '../../types/run/index.js'
15
18
  import type { ProjectId } from '../../types/session/ids.js'
19
+ import { acquireClaim, currentFence, readClaim, releaseClaim } from './claim-disk.js'
16
20
  import { RunDiskStore, readCheckpointsIn } from './disk.js'
17
- import { assertContiguousListingScope, paginateDurableRuns, toDurableRunEntry } from './listing.js'
21
+ import {
22
+ assertContiguousListingScope,
23
+ fencedOut,
24
+ paginateDurableRuns,
25
+ toClaimSummary,
26
+ toDurableRunEntry,
27
+ } from './listing.js'
18
28
 
19
29
  /**
20
30
  * The attribution a disk store's own layout does not record.
@@ -84,11 +94,47 @@ export class DiskCheckpointStore implements CheckpointStore {
84
94
  return promise
85
95
  }
86
96
 
87
- async writeCheckpoint(scope: CheckpointRunScope, checkpoint: IterationCheckpoint): Promise<void> {
97
+ async writeCheckpoint(
98
+ scope: CheckpointRunScope,
99
+ checkpoint: IterationCheckpoint,
100
+ fence?: ClaimFence,
101
+ ): Promise<void> {
88
102
  const store = await this.bind(scope)
103
+ if (fence !== undefined) {
104
+ // Read at the moment of the write, not at the start of the run.
105
+ // A holder that stalled past its lease believes it still holds,
106
+ // and this is the only point at which it can be told otherwise.
107
+ // Names only, no parsing. The fence is the file name, so a corrupt
108
+ // body cannot make this check skip itself — which the previous shape
109
+ // did, at the one site whose entire job is refusing.
110
+ const current = await currentFence(this.runDir(scope))
111
+ if (fence < current) throw fencedOut(scope, fence, current)
112
+ }
89
113
  await store.writeCheckpoint(checkpoint)
90
114
  }
91
115
 
116
+ async claimRun(scope: CheckpointRunScope, options: ClaimRunOptions): Promise<RunClaim | null> {
117
+ return acquireClaim(this.runDir(scope), options)
118
+ }
119
+
120
+ async releaseRun(scope: CheckpointRunScope, fence: ClaimFence): Promise<void> {
121
+ await releaseClaim(this.runDir(scope), fence)
122
+ }
123
+
124
+ /**
125
+ * The run's directory, resolved the same way `RunDiskStore.initRun` does.
126
+ *
127
+ * Duplicated rather than shared because the claim path must be derivable
128
+ * WITHOUT binding a store — binding creates the directory, and a claim
129
+ * read is a read. Kept beside the layout comment on `listDurableRuns` so
130
+ * the two stay together if the layout ever moves.
131
+ */
132
+ private runDir(scope: CheckpointRunScope): string {
133
+ return scope.parentRunId
134
+ ? join(this.config.baseDir, scope.parentRunId, 'children', scope.runId)
135
+ : join(this.config.baseDir, scope.runId)
136
+ }
137
+
92
138
  async readCheckpoint(
93
139
  scope: CheckpointRunScope,
94
140
  checkpointId: CheckpointId,
@@ -163,21 +209,40 @@ export class DiskCheckpointStore implements CheckpointStore {
163
209
  const runDir = join(this.config.baseDir, runId)
164
210
 
165
211
  const own = toDurableRunEntry({ ...attribution, runId }, await readCheckpointsIn(runDir), now)
166
- if (own) entries.push(own)
212
+ if (own) entries.push(await this.withClaim(own, runDir, now))
167
213
 
168
214
  for (const childId of await this.readRunDirs(join(runDir, 'children'))) {
215
+ const childDir = join(runDir, 'children', childId)
169
216
  const child = toDurableRunEntry(
170
217
  { ...attribution, runId: childId, parentRunId: runId },
171
- await readCheckpointsIn(join(runDir, 'children', childId)),
218
+ await readCheckpointsIn(childDir),
172
219
  now,
173
220
  )
174
- if (child) entries.push(child)
221
+ if (child) entries.push(await this.withClaim(child, childDir, now))
175
222
  }
176
223
  }
177
224
 
178
225
  return paginateDurableRuns(entries, options)
179
226
  }
180
227
 
228
+ /**
229
+ * Attach the run's claim to its listing row, judged against the page's
230
+ * own clock so one page cannot disagree with itself about availability.
231
+ */
232
+ private async withClaim(
233
+ entry: DurableRunEntry,
234
+ runDir: string,
235
+ now: number,
236
+ ): Promise<DurableRunEntry> {
237
+ const claim = await readClaim(runDir)
238
+ // A holding whose body could not be read still appears on the row,
239
+ // carrying an expiry of 0 so it reads as available. Dropping the field
240
+ // entirely — which is what happened before — put the run under
241
+ // `claimed: false` by looking unclaimed rather than by being
242
+ // reclaimable, so a queue reader was told a wedged run was free work.
243
+ return claim ? { ...entry, claim: toClaimSummary(claim, now) } : entry
244
+ }
245
+
181
246
  /** Directory names under `dir`, or none when `dir` does not exist. */
182
247
  private async readRunDirs(dir: string): Promise<RunId[]> {
183
248
  try {