@cueloop/daemon 0.1.0-alpha.87 → 0.1.0-alpha.89
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.
- package/package.json +2 -2
- package/src/api.ts +13 -3
- package/src/harness-delivery.test.ts +43 -0
- package/src/harness-state-store.test.ts +28 -0
- package/src/harness-state-store.ts +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/daemon",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.89",
|
|
4
4
|
"description": "The cueloop session daemon and its client: socket protocol, atomic store, resumable waits",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"access": "public"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
41
|
+
"@cueloop/schema": "0.1.0-alpha.89",
|
|
42
42
|
"@pierre/diffs": "^1.4.1",
|
|
43
43
|
"effect": "^4.0.0-rc.116",
|
|
44
44
|
"smol-toml": "^1.8.0",
|
package/src/api.ts
CHANGED
|
@@ -106,6 +106,7 @@ export interface SharedMerge {
|
|
|
106
106
|
export type EventName =
|
|
107
107
|
| "session.created"
|
|
108
108
|
| "session.updated"
|
|
109
|
+
| "session.deleted"
|
|
109
110
|
| "message.sent"
|
|
110
111
|
| "session.revised"
|
|
111
112
|
| "inbox.changed";
|
|
@@ -124,7 +125,7 @@ export class DaemonCore {
|
|
|
124
125
|
readonly herdrThreadSurfaces: HerdrThreadSurfaceStore;
|
|
125
126
|
readonly ghosttyThreadSurfaces: GhosttyThreadSurfaceStore;
|
|
126
127
|
readonly harnessState: HarnessStateStore;
|
|
127
|
-
private waiters = new Map<string, ((session: Thread) => void)[]>();
|
|
128
|
+
private waiters = new Map<string, ((session: Thread | null) => void)[]>();
|
|
128
129
|
private listeners = new Set<EventListener>();
|
|
129
130
|
private seq = 0;
|
|
130
131
|
/** Drives diff hot-reload: watches each live diff session's repo for working-tree changes. */
|
|
@@ -158,6 +159,9 @@ export class DaemonCore {
|
|
|
158
159
|
this.herdrThreadSurfaces.delete(threadId);
|
|
159
160
|
this.ghosttyThreadSurfaces.delete(threadId);
|
|
160
161
|
}
|
|
162
|
+
this.harnessState.deleteOrphanedThreadState(
|
|
163
|
+
new Set(this.store.list().map((session) => session.id)),
|
|
164
|
+
);
|
|
161
165
|
this.diffWatcher = new DiffWatcher((repoRoot) => void this.refreshDiffsForRepo(repoRoot));
|
|
162
166
|
this.prPoller = new PrReviewPoller((sessionId, refs) =>
|
|
163
167
|
this.markPrRefreshAvailable(sessionId, refs),
|
|
@@ -377,7 +381,7 @@ export class DaemonCore {
|
|
|
377
381
|
if (waiterIndex !== -1) list.splice(waiterIndex, 1);
|
|
378
382
|
resolve(null);
|
|
379
383
|
}, timeoutMs);
|
|
380
|
-
const waiter = (session: Thread) => {
|
|
384
|
+
const waiter = (session: Thread | null) => {
|
|
381
385
|
if (done) return;
|
|
382
386
|
done = true;
|
|
383
387
|
clearTimeout(timer);
|
|
@@ -656,7 +660,7 @@ export class DaemonCore {
|
|
|
656
660
|
return session;
|
|
657
661
|
}
|
|
658
662
|
|
|
659
|
-
/** Remove a
|
|
663
|
+
/** Remove a Thread and all local state that refers to it. */
|
|
660
664
|
sessionDelete(id: string): void {
|
|
661
665
|
const session = this.store.get(id);
|
|
662
666
|
|
|
@@ -665,6 +669,12 @@ export class DaemonCore {
|
|
|
665
669
|
this.diffRefreshGenerations.delete(id);
|
|
666
670
|
this.herdrThreadSurfaces.delete(id);
|
|
667
671
|
this.ghosttyThreadSurfaces.delete(id);
|
|
672
|
+
this.harnessState.deleteThreadState(id);
|
|
673
|
+
const parked = this.waiters.get(id) ?? [];
|
|
674
|
+
|
|
675
|
+
this.waiters.delete(id);
|
|
676
|
+
for (const parkedWaiter of parked) parkedWaiter(null);
|
|
677
|
+
this.emit("session.deleted", id);
|
|
668
678
|
this.emit("inbox.changed", id);
|
|
669
679
|
}
|
|
670
680
|
|
|
@@ -66,6 +66,49 @@ describe("durable harness delivery", () => {
|
|
|
66
66
|
expect(core().harnessBindingsForSession("pi", "pi-current")).toEqual([current]);
|
|
67
67
|
});
|
|
68
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
|
+
|
|
69
112
|
test("redelivers after reload until the harness acknowledges the Message", () => {
|
|
70
113
|
const first = core();
|
|
71
114
|
const thread = createPlan(first);
|
|
@@ -57,6 +57,34 @@ describe("HarnessStateStore", () => {
|
|
|
57
57
|
expect(store.bind(input)).toEqual(store.bind(input));
|
|
58
58
|
});
|
|
59
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
|
+
|
|
60
88
|
test("does not discard pending deliveries when persisted state is corrupt", () => {
|
|
61
89
|
writeFileSync(harnessStatePath(home), "{broken");
|
|
62
90
|
|
|
@@ -54,6 +54,38 @@ export class HarnessStateStore {
|
|
|
54
54
|
);
|
|
55
55
|
}
|
|
56
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
|
+
|
|
57
89
|
consumeApprovedRetry(bindingId: string, messageId: string): boolean {
|
|
58
90
|
const binding = this.bindings.get(bindingId);
|
|
59
91
|
|