@cueloop/daemon 0.1.0-alpha.10 → 0.1.0-alpha.101

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 (83) hide show
  1. package/README.md +7 -0
  2. package/SDK.md +102 -0
  3. package/package.json +44 -22
  4. package/src/agent-tools.ts +98 -0
  5. package/src/agent-turn.test.ts +24 -0
  6. package/src/agent-turn.ts +34 -0
  7. package/src/api.test.ts +1081 -65
  8. package/src/api.ts +1425 -89
  9. package/src/capabilities.test.ts +57 -0
  10. package/src/capabilities.ts +95 -0
  11. package/src/client-errors.ts +24 -0
  12. package/src/client-reliability.test.ts +189 -0
  13. package/src/client.test.ts +37 -0
  14. package/src/client.ts +820 -94
  15. package/src/curate.ts +109 -0
  16. package/src/daemon-sdk-effect.ts +113 -0
  17. package/src/daemon-sdk.test.ts +306 -0
  18. package/src/daemon-sdk.ts +397 -0
  19. package/src/daemon-version.test.ts +142 -0
  20. package/src/diff-hot-reload.test.ts +419 -0
  21. package/src/diff-watcher.test.ts +117 -0
  22. package/src/diff-watcher.ts +326 -0
  23. package/src/examples/daemon-sdk.ts +55 -0
  24. package/src/gh.test.ts +59 -0
  25. package/src/gh.ts +74 -0
  26. package/src/ghostty-thread-surface-store.test.ts +49 -0
  27. package/src/ghostty-thread-surface-store.ts +101 -0
  28. package/src/ghostty-thread-surface.test.ts +398 -0
  29. package/src/ghostty-thread-surface.ts +267 -0
  30. package/src/harness-delivery.test.ts +269 -0
  31. package/src/harness-diagnostics.test.ts +26 -0
  32. package/src/harness-diagnostics.ts +30 -0
  33. package/src/harness-state-store.test.ts +93 -0
  34. package/src/harness-state-store.ts +213 -0
  35. package/src/herdr-split.test.ts +106 -0
  36. package/src/herdr-split.ts +85 -0
  37. package/src/herdr-thread-surface-store.test.ts +81 -0
  38. package/src/herdr-thread-surface-store.ts +66 -0
  39. package/src/herdr-thread-surface.test.ts +465 -0
  40. package/src/herdr-thread-surface.ts +378 -0
  41. package/src/index.ts +8 -9
  42. package/src/interruptible-wait.test.ts +127 -0
  43. package/src/interruptible-wait.ts +66 -0
  44. package/src/jj-working-tree.ts +131 -0
  45. package/src/main.ts +4 -1
  46. package/src/operation-receipts.test.ts +111 -0
  47. package/src/operation-receipts.ts +98 -0
  48. package/src/paths.ts +49 -0
  49. package/src/pr-poller.test.ts +102 -0
  50. package/src/pr-poller.ts +73 -0
  51. package/src/project-files.test.ts +134 -0
  52. package/src/project-files.ts +68 -0
  53. package/src/protocol.ts +78 -7
  54. package/src/retention.test.ts +191 -0
  55. package/src/retention.ts +123 -0
  56. package/src/sdk-identities.ts +35 -0
  57. package/src/server.test.ts +502 -42
  58. package/src/server.ts +631 -88
  59. package/src/share-blob.test.ts +119 -0
  60. package/src/share-blob.ts +57 -0
  61. package/src/shared-agent-protocol.ts +30 -0
  62. package/src/single-instance.test.ts +162 -13
  63. package/src/store.conformance.test.ts +41 -0
  64. package/src/store.jsonl.test.ts +173 -0
  65. package/src/store.ts +267 -34
  66. package/src/testing/store-conformance.ts +192 -0
  67. package/src/thread-agent-validation.ts +121 -0
  68. package/src/thread-agent.test.ts +719 -0
  69. package/src/thread-agent.ts +835 -0
  70. package/src/thread-review.test.ts +483 -0
  71. package/src/thread-review.ts +382 -0
  72. package/src/thread-subscription.test.ts +174 -0
  73. package/src/thread-subscription.ts +129 -0
  74. package/src/thread-surface-config.test.ts +63 -0
  75. package/src/thread-surface-config.ts +61 -0
  76. package/src/validate.test.ts +279 -37
  77. package/src/validate.ts +422 -33
  78. package/src/vcs-source.test.ts +180 -0
  79. package/src/vcs-source.ts +229 -0
  80. package/src/version.ts +6 -0
  81. package/src/working-tree.ts +150 -0
  82. package/src/review.test.ts +0 -167
  83. package/src/review.ts +0 -174
@@ -0,0 +1,269 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { DaemonCore } from "./api";
6
+ import { harnessStatePath } from "./paths";
7
+
8
+ let home: string;
9
+ let cores: DaemonCore[];
10
+
11
+ beforeEach(() => {
12
+ home = mkdtempSync(join(tmpdir(), "cueloop-harness-delivery-"));
13
+ cores = [];
14
+ });
15
+
16
+ afterEach(() => {
17
+ for (const core of cores) core.dispose();
18
+ rmSync(home, { recursive: true, force: true });
19
+ });
20
+
21
+ function core(): DaemonCore {
22
+ const instance = new DaemonCore(home);
23
+
24
+ cores.push(instance);
25
+
26
+ return instance;
27
+ }
28
+
29
+ function createPlan(instance: DaemonCore) {
30
+ return instance.sessionCreate({
31
+ workspace: { repoRoot: "/repo", branch: "main" },
32
+ artifact: { type: "plan", content: "# Plan", meta: {} },
33
+ });
34
+ }
35
+
36
+ describe("durable harness delivery", () => {
37
+ test("delivers Comment without resolving the Thread", () => {
38
+ const instance = core();
39
+ const thread = createPlan(instance);
40
+ const binding = instance.harnessBind({
41
+ threadId: thread.id,
42
+ harness: "fake",
43
+ harnessSessionId: "fake_1",
44
+ });
45
+
46
+ const sent = instance.sessionSendMessage(thread.id, "comment", "Interim note.");
47
+ const pending = instance.deliveryPending(binding.id);
48
+
49
+ expect(sent.status).toBe("pending");
50
+ expect(pending).toHaveLength(1);
51
+ expect(pending[0]!.message.outcome).toBe("comment");
52
+ });
53
+
54
+ test("reload finds only bindings for the active harness conversation", () => {
55
+ const first = core();
56
+ const thread = createPlan(first);
57
+ const current = first.harnessBind({
58
+ threadId: thread.id,
59
+ harness: "pi",
60
+ harnessSessionId: "pi-current",
61
+ });
62
+
63
+ first.harnessBind({ threadId: thread.id, harness: "pi", harnessSessionId: "pi-other" });
64
+ first.harnessBind({ threadId: thread.id, harness: "codex", harnessSessionId: "pi-current" });
65
+
66
+ expect(core().harnessBindingsForSession("pi", "pi-current")).toEqual([current]);
67
+ });
68
+
69
+ test("Thread deletion removes pi, Codex, and Claude harness state", () => {
70
+ const instance = core();
71
+ const thread = createPlan(instance);
72
+ const bindings = ["pi", "codex", "claude-code"].map((harness) =>
73
+ instance.harnessBind({
74
+ threadId: thread.id,
75
+ harness,
76
+ harnessSessionId: `${harness}-session`,
77
+ }),
78
+ );
79
+
80
+ instance.sessionSendMessage(thread.id, "approved", "Ready.");
81
+ const deliveryId = instance.deliveryPending(bindings[0]!.id)[0]!.delivery.id;
82
+
83
+ instance.sessionDelete(thread.id);
84
+
85
+ for (const [index, harness] of ["pi", "codex", "claude-code"].entries()) {
86
+ expect(instance.harnessBindingsForSession(harness, `${harness}-session`)).toEqual([]);
87
+ expect(() => instance.harnessGetBinding(bindings[index]!.id)).toThrow("no harness binding");
88
+ }
89
+ expect(() => instance.deliveryAcknowledge(deliveryId)).toThrow("no delivery");
90
+ });
91
+
92
+ test("startup removes harness state orphaned by an older deletion", () => {
93
+ const first = core();
94
+ const thread = createPlan(first);
95
+ const binding = first.harnessBind({
96
+ threadId: thread.id,
97
+ harness: "codex",
98
+ harnessSessionId: "codex-orphan",
99
+ });
100
+
101
+ first.sessionSendMessage(thread.id, "approved", "Ready.");
102
+ const deliveryId = first.deliveryPending(binding.id)[0]!.delivery.id;
103
+
104
+ first.store.delete(thread.id);
105
+ const restarted = core();
106
+
107
+ expect(restarted.harnessBindingsForSession("codex", "codex-orphan")).toEqual([]);
108
+ expect(() => restarted.harnessGetBinding(binding.id)).toThrow("no harness binding");
109
+ expect(() => restarted.deliveryAcknowledge(deliveryId)).toThrow("no delivery");
110
+ });
111
+
112
+ test("redelivers after reload until the harness acknowledges the Message", () => {
113
+ const first = core();
114
+ const thread = createPlan(first);
115
+ const binding = first.harnessBind({
116
+ threadId: thread.id,
117
+ harness: "fake",
118
+ harnessSessionId: "fake_1",
119
+ });
120
+ const sent = first.sessionSendMessage(thread.id, "changes_requested", "Add detail.");
121
+ const initial = first.deliveryPending(binding.id);
122
+
123
+ expect(initial).toHaveLength(1);
124
+ expect(initial[0]!.message).toEqual(sent.message!);
125
+
126
+ const reloaded = core();
127
+ const redelivered = reloaded.deliveryPending(binding.id);
128
+
129
+ expect(redelivered).toEqual(initial);
130
+ reloaded.deliveryAcknowledge(redelivered[0]!.delivery.id);
131
+
132
+ expect(core().deliveryPending(binding.id)).toEqual([]);
133
+ });
134
+
135
+ test("retains an expired Thread until its pending Message is acknowledged", () => {
136
+ const first = core();
137
+ const thread = createPlan(first);
138
+ const binding = first.harnessBind({
139
+ threadId: thread.id,
140
+ harness: "fake",
141
+ harnessSessionId: "fake_1",
142
+ });
143
+
144
+ first.sessionSendMessage(thread.id, "approved", "Ready.");
145
+ const oldThread = first.sessionGet(thread.id);
146
+
147
+ first.store.upsert({ ...oldThread, createdAt: "2020-01-01T00:00:00.000Z" });
148
+
149
+ const reloaded = core();
150
+ const delivery = reloaded.deliveryPending(binding.id)[0];
151
+
152
+ expect(delivery?.message.body).toContain("Ready.");
153
+ reloaded.deliveryAcknowledge(delivery!.delivery.id);
154
+
155
+ expect(core().store.get(thread.id)).toBeUndefined();
156
+ });
157
+
158
+ test("rebuilds a delivery if the Thread saved its Message before enqueue", () => {
159
+ const first = core();
160
+ const thread = createPlan(first);
161
+ const binding = first.harnessBind({
162
+ threadId: thread.id,
163
+ harness: "fake",
164
+ harnessSessionId: "fake_1",
165
+ });
166
+ const sent = first.sessionSendMessage(thread.id, "approved", "Ready.");
167
+
168
+ first.store.upsert({
169
+ ...first.sessionGet(thread.id),
170
+ createdAt: "2020-01-01T00:00:00.000Z",
171
+ });
172
+
173
+ writeFileSync(harnessStatePath(home), JSON.stringify({ bindings: [binding], deliveries: [] }));
174
+
175
+ const recovered = core().deliveryPending(binding.id);
176
+
177
+ expect(recovered).toHaveLength(1);
178
+ expect(recovered[0]!.message).toEqual(sent.message!);
179
+ });
180
+
181
+ test("routes an approved retry as a new Message", () => {
182
+ const instance = core();
183
+ const thread = createPlan(instance);
184
+ const binding = instance.harnessBind({
185
+ threadId: thread.id,
186
+ harness: "fake",
187
+ harnessSessionId: "fake_1",
188
+ });
189
+
190
+ instance.sessionSendMessage(thread.id, "changes_requested", "Try once more.");
191
+ const first = instance.deliveryPending(binding.id)[0]!;
192
+
193
+ instance.deliveryAcknowledge(first.delivery.id);
194
+ instance.sessionSubmitRevision(thread.id, "# Plan");
195
+ instance.sessionSendMessage(thread.id, "approved", "Ready.");
196
+ const retry = instance.deliveryPending(binding.id);
197
+
198
+ expect(retry).toHaveLength(1);
199
+ expect(retry[0]!.message.outcome).toBe("approved");
200
+ expect(retry[0]!.message.id).not.toBe(first.message.id);
201
+ });
202
+
203
+ test("keeps an older pending Message available after a Thread revision", () => {
204
+ const instance = core();
205
+ const thread = createPlan(instance);
206
+ const binding = instance.harnessBind({
207
+ threadId: thread.id,
208
+ harness: "fake",
209
+ harnessSessionId: "fake_1",
210
+ });
211
+
212
+ instance.sessionSendMessage(thread.id, "changes_requested", "Add detail.");
213
+ const first = instance.deliveryPending(binding.id)[0]!;
214
+
215
+ instance.sessionSubmitRevision(thread.id, "# Revised plan");
216
+ instance.sessionSendMessage(thread.id, "approved", "Ready.");
217
+
218
+ const pending = core().deliveryPending(binding.id);
219
+
220
+ expect(pending.map((item) => item.message.id)).toEqual([first.message.id, expect.any(String)]);
221
+ expect(pending[0]!.message.body).toBe(first.message.body);
222
+ });
223
+
224
+ test("defaults delivery to the submitting binding only", () => {
225
+ const instance = core();
226
+ const thread = createPlan(instance);
227
+ const submitter = instance.harnessBind({
228
+ threadId: thread.id,
229
+ harness: "fake",
230
+ harnessSessionId: "fake_1",
231
+ });
232
+ const other = instance.harnessBind({
233
+ threadId: thread.id,
234
+ harness: "other",
235
+ harnessSessionId: "other_1",
236
+ });
237
+
238
+ instance.sessionSendMessage(thread.id, "approved", "Ready.");
239
+
240
+ expect(instance.deliveryPending(submitter.id)).toHaveLength(1);
241
+ expect(instance.deliveryPending(other.id)).toEqual([]);
242
+ });
243
+
244
+ test("consumes the unchanged approved retry once across daemon reload", () => {
245
+ const instance = core();
246
+ const thread = createPlan(instance);
247
+ const binding = instance.harnessBind({
248
+ threadId: thread.id,
249
+ harness: "fake",
250
+ harnessSessionId: "fake_1",
251
+ });
252
+ const sent = instance.sessionSendMessage(thread.id, "approved", "Ready.");
253
+ const messageId = sent.message!.id;
254
+
255
+ expect(() => instance.harnessConsumeApprovedRetry(binding.id, messageId, "# Plan")).toThrow(
256
+ "no unchanged approved plan",
257
+ );
258
+
259
+ const delivery = instance.deliveryPending(binding.id)[0]!.delivery;
260
+
261
+ instance.deliveryAcknowledge(delivery.id);
262
+
263
+ expect(instance.harnessConsumeApprovedRetry(binding.id, messageId, "# Plan")).toBe(true);
264
+ expect(core().harnessConsumeApprovedRetry(binding.id, messageId, "# Plan")).toBe(false);
265
+ expect(() => core().harnessConsumeApprovedRetry(binding.id, messageId, "# Changed")).toThrow(
266
+ "no unchanged approved plan",
267
+ );
268
+ });
269
+ });
@@ -0,0 +1,26 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { writeHarnessDiagnostic } from "./harness-diagnostics";
6
+
7
+ test("diagnostic logs are private and bounded across repeated notices", () => {
8
+ const directory = mkdtempSync(join(tmpdir(), "cueloop-diagnostics-"));
9
+ const path = join(directory, "thread.diagnostics.ndjson");
10
+
11
+ try {
12
+ for (let i = 0; i < 100; i++)
13
+ writeHarnessDiagnostic(directory, "thread", {
14
+ kind: "diagnostic",
15
+ severity: "error",
16
+ source: "protocol",
17
+ title: "Advisory",
18
+ text: "x".repeat(32768),
19
+ });
20
+ expect(statSync(path).size).toBeLessThanOrEqual(256 * 1024);
21
+ expect(statSync(path).mode & 0o777).toBe(0o600);
22
+ expect(JSON.parse(readFileSync(path, "utf8").trim().split("\n")[0]!).text).toHaveLength(16384);
23
+ } finally {
24
+ rmSync(directory, { recursive: true, force: true });
25
+ }
26
+ });
@@ -0,0 +1,30 @@
1
+ import { appendFileSync, existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { AgentHarnessDiagnostic } from "@cueloop/schema";
4
+
5
+ /** Store harness diagnostics in a private, bounded log, outside Thread history and model context. */
6
+ export function writeHarnessDiagnostic(
7
+ directory: string,
8
+ threadId: string,
9
+ diagnostic: AgentHarnessDiagnostic,
10
+ ): void {
11
+ const path = join(directory, `${encodeURIComponent(threadId)}.diagnostics.ndjson`);
12
+ const record =
13
+ JSON.stringify({
14
+ at: new Date().toISOString(),
15
+ ...diagnostic,
16
+ severity: diagnostic.severity.slice(0, 128),
17
+ title: diagnostic.title.slice(0, 512),
18
+ text: diagnostic.text.slice(0, 16384),
19
+ }) + "\n";
20
+
21
+ try {
22
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
23
+ if (existsSync(path) && statSync(path).size + Buffer.byteLength(record) > 256 * 1024)
24
+ writeFileSync(path, record, { mode: 0o600 });
25
+ else appendFileSync(path, record, { mode: 0o600 });
26
+ } catch {
27
+ // A diagnostic log failure must not change the agent turn outcome.
28
+ console.error("Thread agent diagnostic log could not be written");
29
+ }
30
+ }
@@ -0,0 +1,93 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { HarnessStateStore } from "./harness-state-store";
6
+ import { harnessStatePath } from "./paths";
7
+
8
+ let home: string;
9
+
10
+ beforeEach(() => {
11
+ home = mkdtempSync(join(tmpdir(), "cueloop-harness-state-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ rmSync(home, { recursive: true, force: true });
16
+ });
17
+
18
+ describe("HarnessStateStore", () => {
19
+ test("redelivers an unacknowledged Message after reload", () => {
20
+ const store = new HarnessStateStore(home);
21
+ const binding = store.bind({
22
+ threadId: "ses_1",
23
+ harness: "fake",
24
+ harnessSessionId: "fake_1",
25
+ });
26
+ const delivery = store.enqueue({ bindingId: binding.id, messageId: "msg_1" });
27
+
28
+ expect(store.pending(binding.id)).toEqual([delivery]);
29
+
30
+ const reloaded = new HarnessStateStore(home);
31
+
32
+ expect(reloaded.pending(binding.id)).toEqual([delivery]);
33
+ });
34
+
35
+ test("does not redeliver an acknowledged Message", () => {
36
+ const store = new HarnessStateStore(home);
37
+ const binding = store.bind({
38
+ threadId: "ses_1",
39
+ harness: "fake",
40
+ harnessSessionId: "fake_1",
41
+ });
42
+ const delivery = store.enqueue({ bindingId: binding.id, messageId: "msg_1" });
43
+
44
+ store.acknowledge(delivery.id, "2026-09-20T20:00:00.000Z");
45
+
46
+ expect(new HarnessStateStore(home).pending(binding.id)).toEqual([]);
47
+ });
48
+
49
+ test("reuses the binding for the same harness session and thread", () => {
50
+ const store = new HarnessStateStore(home);
51
+ const input = {
52
+ threadId: "ses_1",
53
+ harness: "fake",
54
+ harnessSessionId: "fake_1",
55
+ };
56
+
57
+ expect(store.bind(input)).toEqual(store.bind(input));
58
+ });
59
+
60
+ test("deletes every harness binding and delivery for a Thread", () => {
61
+ const store = new HarnessStateStore(home);
62
+ const bindings = ["pi", "codex", "claude-code"].map((harness) =>
63
+ store.bind({ threadId: "ses_1", harness, harnessSessionId: `${harness}_1` }),
64
+ );
65
+ const deliveries = bindings.map((binding, index) =>
66
+ store.enqueue({ bindingId: binding.id, messageId: `msg_${index}` }),
67
+ );
68
+
69
+ expect(store.deleteThreadState("ses_1")).toBeTrue();
70
+ expect(store.deleteThreadState("ses_1")).toBeFalse();
71
+ const reloaded = new HarnessStateStore(home);
72
+
73
+ for (const binding of bindings) expect(reloaded.binding(binding.id)).toBeNull();
74
+ for (const delivery of deliveries) expect(reloaded.delivery(delivery.id)).toBeNull();
75
+ });
76
+
77
+ test("deletes state for Threads missing at startup", () => {
78
+ const store = new HarnessStateStore(home);
79
+
80
+ store.bind({ threadId: "ses_kept", harness: "pi", harnessSessionId: "pi_1" });
81
+ store.bind({ threadId: "ses_orphan", harness: "codex", harnessSessionId: "codex_1" });
82
+
83
+ expect(store.deleteOrphanedThreadState(new Set(["ses_kept"]))).toEqual(["ses_orphan"]);
84
+ expect(store.bindingsForSession("pi", "pi_1")).toHaveLength(1);
85
+ expect(store.bindingsForSession("codex", "codex_1")).toEqual([]);
86
+ });
87
+
88
+ test("does not discard pending deliveries when persisted state is corrupt", () => {
89
+ writeFileSync(harnessStatePath(home), "{broken");
90
+
91
+ expect(() => new HarnessStateStore(home)).toThrow();
92
+ });
93
+ });
@@ -0,0 +1,213 @@
1
+ import { readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import type { Delivery, HarnessBinding } from "@cueloop/schema";
3
+ import * as v from "valibot";
4
+ import { harnessStatePath } from "./paths";
5
+ import { DeliverySchema, HarnessBindingSchema } from "./validate";
6
+
7
+ type BindInput = Pick<HarnessBinding, "threadId" | "harness" | "harnessSessionId">;
8
+ type EnqueueInput = Pick<Delivery, "bindingId" | "messageId">;
9
+
10
+ const HarnessStateSchema = v.object({
11
+ bindings: v.array(HarnessBindingSchema),
12
+ deliveries: v.array(DeliverySchema),
13
+ });
14
+ const FileErrorSchema = v.object({ code: v.string() });
15
+
16
+ export class HarnessStateStore {
17
+ private readonly path: string;
18
+ private bindings = new Map<string, HarnessBinding>();
19
+ private deliveries = new Map<string, Delivery>();
20
+
21
+ constructor(home: string) {
22
+ this.path = harnessStatePath(home);
23
+ this.load();
24
+ }
25
+
26
+ bind(input: BindInput): HarnessBinding {
27
+ const existing = [...this.bindings.values()].find(
28
+ (binding) =>
29
+ binding.threadId === input.threadId &&
30
+ binding.harness === input.harness &&
31
+ binding.harnessSessionId === input.harnessSessionId,
32
+ );
33
+
34
+ if (existing) return existing;
35
+ const binding: HarnessBinding = {
36
+ ...input,
37
+ id: newRoutingId("bind"),
38
+ createdAt: new Date().toISOString(),
39
+ };
40
+
41
+ this.bindings.set(binding.id, binding);
42
+ this.persist();
43
+
44
+ return binding;
45
+ }
46
+
47
+ binding(id: string): HarnessBinding | null {
48
+ return this.bindings.get(id) ?? null;
49
+ }
50
+
51
+ bindingsForSession(harness: string, harnessSessionId: string): HarnessBinding[] {
52
+ return [...this.bindings.values()].filter(
53
+ (binding) => binding.harness === harness && binding.harnessSessionId === harnessSessionId,
54
+ );
55
+ }
56
+
57
+ deleteThreadState(threadId: string): boolean {
58
+ const bindingIds = new Set(
59
+ [...this.bindings.values()]
60
+ .filter((binding) => binding.threadId === threadId)
61
+ .map((binding) => binding.id),
62
+ );
63
+
64
+ return this.deleteBindingsAndDeliveries(bindingIds);
65
+ }
66
+
67
+ deleteOrphanedThreadState(existingThreadIds: ReadonlySet<string>): string[] {
68
+ const orphanedBindings = [...this.bindings.values()].filter(
69
+ (binding) => !existingThreadIds.has(binding.threadId),
70
+ );
71
+ const orphanedThreadIds = [...new Set(orphanedBindings.map((binding) => binding.threadId))];
72
+
73
+ this.deleteBindingsAndDeliveries(new Set(orphanedBindings.map((binding) => binding.id)));
74
+
75
+ return orphanedThreadIds;
76
+ }
77
+
78
+ private deleteBindingsAndDeliveries(bindingIds: ReadonlySet<string>): boolean {
79
+ if (bindingIds.size === 0) return false;
80
+ for (const bindingId of bindingIds) this.bindings.delete(bindingId);
81
+ for (const delivery of this.deliveries.values()) {
82
+ if (bindingIds.has(delivery.bindingId)) this.deliveries.delete(delivery.id);
83
+ }
84
+ this.persist();
85
+
86
+ return true;
87
+ }
88
+
89
+ consumeApprovedRetry(bindingId: string, messageId: string): boolean {
90
+ const binding = this.bindings.get(bindingId);
91
+
92
+ if (!binding) throw new Error(`no harness binding ${bindingId}`);
93
+
94
+ if (binding.approvedRetryMessageId === messageId) return false;
95
+
96
+ this.bindings.set(bindingId, { ...binding, approvedRetryMessageId: messageId });
97
+ this.persist();
98
+
99
+ return true;
100
+ }
101
+
102
+ /** The first binding is the submitter; other bindings need explicit future routing. */
103
+ submittingBinding(threadId: string): HarnessBinding | null {
104
+ return [...this.bindings.values()].find((binding) => binding.threadId === threadId) ?? null;
105
+ }
106
+
107
+ enqueue(input: EnqueueInput): Delivery {
108
+ const existing = [...this.deliveries.values()].find(
109
+ (delivery) =>
110
+ delivery.bindingId === input.bindingId && delivery.messageId === input.messageId,
111
+ );
112
+
113
+ if (existing) return existing;
114
+ const delivery: Delivery = {
115
+ ...input,
116
+ id: newRoutingId("del"),
117
+ status: "pending",
118
+ createdAt: new Date().toISOString(),
119
+ };
120
+
121
+ this.deliveries.set(delivery.id, delivery);
122
+ this.persist();
123
+
124
+ return delivery;
125
+ }
126
+
127
+ pending(bindingId: string): Delivery[] {
128
+ return [...this.deliveries.values()].filter(
129
+ (delivery) => delivery.bindingId === bindingId && delivery.status === "pending",
130
+ );
131
+ }
132
+
133
+ /** A pending delivery keeps its Thread's immutable Message body available for redelivery. */
134
+ pendingThreadIds(): Set<string> {
135
+ const ids = new Set<string>();
136
+
137
+ for (const delivery of this.deliveries.values()) {
138
+ if (delivery.status !== "pending") continue;
139
+ const binding = this.bindings.get(delivery.bindingId);
140
+
141
+ if (binding) ids.add(binding.threadId);
142
+ }
143
+
144
+ return ids;
145
+ }
146
+
147
+ acknowledged(bindingId: string, messageId: string): boolean {
148
+ return [...this.deliveries.values()].some(
149
+ (delivery) =>
150
+ delivery.bindingId === bindingId &&
151
+ delivery.messageId === messageId &&
152
+ delivery.status === "acknowledged",
153
+ );
154
+ }
155
+
156
+ delivery(deliveryId: string): Delivery | null {
157
+ return this.deliveries.get(deliveryId) ?? null;
158
+ }
159
+
160
+ acknowledge(deliveryId: string, acknowledgedAt = new Date().toISOString()): Delivery {
161
+ const delivery = this.deliveries.get(deliveryId);
162
+
163
+ if (!delivery) throw new Error(`no delivery ${deliveryId}`);
164
+
165
+ if (delivery.status === "acknowledged") return delivery;
166
+ const acknowledged: Delivery = {
167
+ ...delivery,
168
+ status: "acknowledged",
169
+ acknowledgedAt,
170
+ };
171
+
172
+ this.deliveries.set(deliveryId, acknowledged);
173
+ this.persist();
174
+
175
+ return acknowledged;
176
+ }
177
+
178
+ private load(): void {
179
+ let serialized: string;
180
+
181
+ try {
182
+ serialized = readFileSync(this.path, "utf8");
183
+ } catch (error) {
184
+ const parsed = v.safeParse(FileErrorSchema, error);
185
+
186
+ if (parsed.success && parsed.output.code === "ENOENT") {
187
+ return;
188
+ }
189
+
190
+ throw error;
191
+ }
192
+
193
+ const state = v.parse(HarnessStateSchema, JSON.parse(serialized));
194
+
195
+ this.bindings = new Map(state.bindings.map((binding) => [binding.id, binding]));
196
+ this.deliveries = new Map(state.deliveries.map((delivery) => [delivery.id, delivery]));
197
+ }
198
+
199
+ private persist(): void {
200
+ const temporaryPath = this.path + ".tmp";
201
+ const state = {
202
+ bindings: [...this.bindings.values()],
203
+ deliveries: [...this.deliveries.values()],
204
+ };
205
+
206
+ writeFileSync(temporaryPath, JSON.stringify(state, null, 2));
207
+ renameSync(temporaryPath, this.path);
208
+ }
209
+ }
210
+
211
+ function newRoutingId(prefix: "bind" | "del"): string {
212
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
213
+ }