@cueloop/daemon 0.1.0-alpha.102 → 0.1.0-alpha.103

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 (44) hide show
  1. package/package.json +3 -3
  2. package/src/agent-tools.ts +6 -0
  3. package/src/api.ts +63 -0
  4. package/src/client-reliability.test.ts +2 -0
  5. package/src/client.ts +24 -2
  6. package/src/curate.ts +2 -0
  7. package/src/daemon-sdk.ts +8 -0
  8. package/src/diff-hot-reload.test.ts +4 -0
  9. package/src/diff-watcher.test.ts +2 -0
  10. package/src/diff-watcher.ts +33 -0
  11. package/src/examples/daemon-sdk.ts +2 -0
  12. package/src/gh.ts +4 -0
  13. package/src/ghostty-thread-surface-store.ts +3 -0
  14. package/src/ghostty-thread-surface.test.ts +2 -0
  15. package/src/ghostty-thread-surface.ts +6 -0
  16. package/src/harness-diagnostics.ts +1 -0
  17. package/src/harness-handoff.test.ts +725 -0
  18. package/src/harness-handoff.ts +27 -0
  19. package/src/harness-state-store.ts +5 -0
  20. package/src/herdr-split.ts +5 -0
  21. package/src/herdr-thread-surface.ts +14 -0
  22. package/src/interruptible-wait.ts +3 -0
  23. package/src/jj-working-tree.ts +9 -0
  24. package/src/main.ts +1 -0
  25. package/src/operation-receipts.ts +6 -0
  26. package/src/pr-poller.ts +7 -0
  27. package/src/project-files.ts +4 -0
  28. package/src/protocol.ts +4 -0
  29. package/src/retention.ts +8 -0
  30. package/src/server.ts +36 -7
  31. package/src/share-blob.test.ts +2 -0
  32. package/src/single-instance.test.ts +3 -0
  33. package/src/store.ts +11 -0
  34. package/src/thread-agent-validation.ts +20 -0
  35. package/src/thread-agent.test.ts +14 -1
  36. package/src/thread-agent.ts +353 -24
  37. package/src/thread-review.test.ts +4 -0
  38. package/src/thread-review.ts +8 -0
  39. package/src/thread-subscription.test.ts +3 -0
  40. package/src/thread-subscription.ts +9 -0
  41. package/src/validate.test.ts +4 -0
  42. package/src/validate.ts +3 -0
  43. package/src/vcs-source.ts +6 -0
  44. package/src/working-tree.ts +4 -0
@@ -213,6 +213,7 @@ describe("one daemon per home", () => {
213
213
  expect(existsSync(path!)).toBe(true);
214
214
  } finally {
215
215
  client.close();
216
+
216
217
  if (previousTimeout === undefined) delete process.env.CUELOOP_START_TIMEOUT_MS;
217
218
  else process.env.CUELOOP_START_TIMEOUT_MS = previousTimeout;
218
219
  }
@@ -242,7 +243,9 @@ describe("one daemon per home", () => {
242
243
  expect((await client.ping()).pid).toBe(process.pid);
243
244
  } finally {
244
245
  client.close();
246
+
245
247
  if (transient.listening) transient.close();
248
+
246
249
  if (previousTimeout === undefined) delete process.env.CUELOOP_START_TIMEOUT_MS;
247
250
  else process.env.CUELOOP_START_TIMEOUT_MS = previousTimeout;
248
251
  }
package/src/store.ts CHANGED
@@ -85,8 +85,10 @@ export class ThreadStore implements ThreadRepository {
85
85
  const bucketPath = join(root, bucket);
86
86
 
87
87
  if (!statSync(bucketPath).isDirectory()) continue;
88
+
88
89
  for (const file of readdirSync(bucketPath)) {
89
90
  if (!file.endsWith(".jsonl")) continue;
91
+
90
92
  const filePath = join(bucketPath, file);
91
93
 
92
94
  try {
@@ -96,6 +98,7 @@ export class ThreadStore implements ThreadRepository {
96
98
  this.files.set(session.id, filePath);
97
99
  this.lineCounts.set(session.id, lines);
98
100
  report.recovered.push(session.id);
101
+
99
102
  // a torn trailing fragment would fuse with the next append and lose that update, so rewrite
100
103
  // the log to a single clean snapshot line now that the last valid record is in hand
101
104
  if (torn) this.writeWhole(session);
@@ -117,10 +120,12 @@ export class ThreadStore implements ThreadRepository {
117
120
  const legacyDir = sessionsDir(this.home);
118
121
 
119
122
  if (!existsSync(legacyDir)) return;
123
+
120
124
  const parked = migratedSessionsDir(this.home);
121
125
 
122
126
  for (const file of readdirSync(legacyDir)) {
123
127
  if (!file.endsWith(".json")) continue;
128
+
124
129
  const legacyPath = join(legacyDir, file);
125
130
  let session: Thread;
126
131
 
@@ -128,6 +133,7 @@ export class ThreadStore implements ThreadRepository {
128
133
  const parsed = validateThreadRecord(JSON.parse(readFileSync(legacyPath, "utf8")));
129
134
 
130
135
  if (!parsed.ok) throw new Error(`invalid record - ${parsed.error}`);
136
+
131
137
  session = withHistory(parsed.value);
132
138
  } catch (error) {
133
139
  report.skipped.push({
@@ -144,6 +150,7 @@ export class ThreadStore implements ThreadRepository {
144
150
  // after a rollback a newer JSONL can already hold this thread; never overwrite it with the
145
151
  // older legacy snapshot - the existing log stays authoritative and the threads scan recovers it
146
152
  if (!existsSync(targetPath)) this.writeWhole(session);
153
+
147
154
  // forget the in-memory tracking migration seeded: the threads scan is the one recovery path
148
155
  this.files.delete(session.id);
149
156
  this.lineCounts.delete(session.id);
@@ -192,6 +199,7 @@ export class ThreadStore implements ThreadRepository {
192
199
 
193
200
  return;
194
201
  }
202
+
195
203
  appendFileSync(filePath, JSON.stringify(session) + "\n");
196
204
  this.lineCounts.set(session.id, lines + 1);
197
205
  this.sessions.set(session.id, session);
@@ -199,9 +207,11 @@ export class ThreadStore implements ThreadRepository {
199
207
 
200
208
  delete(id: string): boolean {
201
209
  if (!this.sessions.delete(id)) return false;
210
+
202
211
  const filePath = this.files.get(id);
203
212
 
204
213
  if (filePath) rmSync(filePath, { force: true });
214
+
205
215
  this.files.delete(id);
206
216
  this.lineCounts.delete(id);
207
217
 
@@ -261,6 +271,7 @@ export class MemoryThreadStore implements ThreadRepository {
261
271
 
262
272
  return;
263
273
  }
274
+
264
275
  const session = withHistory(parsed.value);
265
276
 
266
277
  this.sessions.set(session.id, session);
@@ -79,6 +79,7 @@ const StoredAgentSchema = v.object({
79
79
  messageId: v.optional(v.string()),
80
80
  prompt: v.string(),
81
81
  harnessPrompt: v.optional(v.string()),
82
+ attemptId: v.optional(v.string()),
82
83
  quote: v.optional(v.string()),
83
84
  context: v.optional(v.string()),
84
85
  status: v.picklist(["queued", "running", "completed", "failed"]),
@@ -96,6 +97,25 @@ const StoredAgentSchema = v.object({
96
97
  }),
97
98
  ),
98
99
  ),
100
+ continuation: v.optional(v.string()),
101
+ handoff: v.optional(
102
+ v.intersect([
103
+ v.object({
104
+ target: v.string(),
105
+ requestedTarget: v.optional(v.string()),
106
+ operationId: v.string(),
107
+ prompt: v.optional(v.string()),
108
+ source: v.optional(
109
+ v.object({ id: v.string(), label: v.string(), sessionId: v.optional(v.string()) }),
110
+ ),
111
+ }),
112
+ v.variant("status", [
113
+ v.object({ status: v.literal("queued") }),
114
+ v.object({ status: v.literal("summarizing") }),
115
+ v.object({ status: v.literal("prepared"), text: v.string() }),
116
+ ]),
117
+ ]),
118
+ ),
99
119
  configOptions: v.optional(
100
120
  v.array(
101
121
  v.object({
@@ -416,6 +416,7 @@ test("a shared plain input does not consume the owner's pending comments", () =>
416
416
  const thread = createTestThread(home);
417
417
  const adapter: AgentHarnessAdapter = {
418
418
  id: "pi",
419
+ recovery: "durable",
419
420
  label: "pi",
420
421
  connect: () => ({
421
422
  start: () => new Promise(() => {}),
@@ -474,12 +475,19 @@ test("Pi recovery keeps the frozen harness input and submission identity after i
474
475
  let resumed = false;
475
476
  const adapter: AgentHarnessAdapter = {
476
477
  id: "pi",
478
+ recovery: "durable",
477
479
  label: "pi",
478
480
  connect: ({ onEvent }) => ({
479
481
  start: async () => "durable-session",
480
482
  prompt: async (text, id) => {
481
483
  prompts.push({ text, id });
482
- if (!resumed) return new Promise(() => {});
484
+
485
+ if (!resumed) {
486
+ onEvent({ kind: "message", id: "durable-answer", text: "Partial reply", replace: true });
487
+
488
+ return new Promise(() => {});
489
+ }
490
+
483
491
  onEvent({ kind: "message", id: "durable-answer", text: "Recovered answer", replace: true });
484
492
 
485
493
  return { outcome: "completed" as const };
@@ -515,6 +523,7 @@ test("Pi recovery keeps the frozen harness input and submission identity after i
515
523
  await Bun.sleep(1);
516
524
  expect(prompts).toHaveLength(2);
517
525
  expect(prompts[1]).toEqual(prompts[0]);
526
+ expect(manager.get(thread.id).messages.at(-1)?.complete).toBe(true);
518
527
  expect(manager.get(thread.id).messages.map((message) => message.text)).toEqual([
519
528
  "Recover",
520
529
  "Recovered answer",
@@ -545,6 +554,7 @@ test("explicit shared comments execute the frozen payload and retain the origina
545
554
  enabled: true,
546
555
  adapter: {
547
556
  id: "pi",
557
+ recovery: "durable",
548
558
  label: "pi",
549
559
  connect: () => ({
550
560
  start: async () => "session",
@@ -592,6 +602,7 @@ test("a failed connection finishes asynchronous close before the next queued tur
592
602
  onChange() {},
593
603
  adapter: {
594
604
  id: "pi",
605
+ recovery: "durable",
595
606
  label: "pi",
596
607
  connect: () => {
597
608
  const first = ++connects === 1;
@@ -650,6 +661,7 @@ test("shutdown joins a failed turn that is already retiring its durable writer",
650
661
  onChange() {},
651
662
  adapter: {
652
663
  id: "pi",
664
+ recovery: "durable",
653
665
  label: "pi",
654
666
  connect: () => ({
655
667
  start: async () => "session",
@@ -699,6 +711,7 @@ test("shutdown seals admission so a later configuration cannot open a new harnes
699
711
  onChange() {},
700
712
  adapter: {
701
713
  id: "pi",
714
+ recovery: "durable",
702
715
  label: "pi",
703
716
  connect: () => {
704
717
  connects++;