@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
@@ -308,7 +308,9 @@ describe("awaitMessage: one long-poll (the hook shape)", () => {
308
308
 
309
309
  // Assert
310
310
  expect(message).not.toBe("pending");
311
+
311
312
  if (message === "pending") throw new Error("unreachable");
313
+
312
314
  expect(message.allow).toBe(true);
313
315
  expect(message.message.body).toContain("Fine.");
314
316
  expect(message.session.message!.outcome).toBe("approved");
@@ -325,6 +327,7 @@ describe("awaitMessage: one long-poll (the hook shape)", () => {
325
327
 
326
328
  // Assert
327
329
  if (message === "pending") throw new Error("expected a message");
330
+
328
331
  expect(message.allow).toBe(false);
329
332
  expect(message.message.body).toContain("# Review: changes requested");
330
333
  expect(message.message.body).toContain("One stage only.");
@@ -362,6 +365,7 @@ describe("awaitMessage: chunked loop (the pi shape)", () => {
362
365
 
363
366
  // Assert
364
367
  if (message === "pending") throw new Error("expected a message");
368
+
365
369
  expect(message.allow).toBe(false);
366
370
  expect(message.session.annotations.length).toBe(1);
367
371
  }, 15_000);
@@ -43,6 +43,7 @@ async function jjRoot(cwd: string): Promise<string | null> {
43
43
  /** The oldest root commit reachable from HEAD; the project key that survives moving or re-cloning the repo. */
44
44
  function earliestRootCommit(revList: string | null): string | undefined {
45
45
  if (!revList) return undefined;
46
+
46
47
  const roots = revList.split("\n").filter((line) => line.length > 0);
47
48
 
48
49
  // --date-order lists newest first, so the last root is the earliest
@@ -77,6 +78,7 @@ export async function resolveWorkspace(cwd = process.cwd()): Promise<WorkspaceKe
77
78
 
78
79
  // a repo with no commits (or a shallow clone) has no reliable root, so the thread stays standalone
79
80
  if (rootCommit) workspace.rootCommit = rootCommit;
81
+
80
82
  if (remote) workspace.remote = remote;
81
83
 
82
84
  return workspace;
@@ -191,6 +193,7 @@ export class ReviewHandle {
191
193
 
192
194
  return resolved === null ? "pending" : outcome(resolved);
193
195
  }
196
+
194
197
  const chunkMs = pollMs ?? 10_000;
195
198
  const deadline = Number.isFinite(timeoutMs) ? Date.now() + timeoutMs : undefined;
196
199
 
@@ -198,11 +201,13 @@ export class ReviewHandle {
198
201
  const budget = deadline === undefined ? chunkMs : Math.min(chunkMs, deadline - Date.now());
199
202
 
200
203
  if (budget <= 0 || signal?.aborted) return "pending";
204
+
201
205
  const resolved = await raceAbort(this.client.sessionWait(this.session.id, budget), signal);
202
206
 
203
207
  if (resolved === ABORTED) return "pending";
204
208
 
205
209
  if (resolved !== null) return outcome(resolved);
210
+
206
211
  // Still pending after this chunk: re-read to surface reviewer progress.
207
212
  const current = await raceAbort(this.client.sessionGet(this.session.id), signal);
208
213
 
@@ -278,6 +283,7 @@ export async function findExistingReview(
278
283
  return candidates.find(
279
284
  (candidate) => candidate.artifact.meta.vcsChangeId === options.vcsChangeId,
280
285
  );
286
+
281
287
  if (isJjDiff) {
282
288
  const jjCandidates = candidates.filter(hasJjProvenance);
283
289
 
@@ -295,6 +301,7 @@ async function matchingDiffCapture(
295
301
  cwd: string,
296
302
  ): Promise<{ source?: DiffSource; files?: DiffFileContents[] }> {
297
303
  if (options.type !== "diff" || options.pr || !options.content.trim()) return {};
304
+
298
305
  try {
299
306
  const captured = await new VcsSourceManager().capture(cwd, options.vcs);
300
307
 
@@ -347,6 +354,7 @@ export async function openReview(
347
354
 
348
355
  return new ReviewHandle(client, revised);
349
356
  }
357
+
350
358
  let session = await client.sessionCreate(workspace, {
351
359
  type: options.type,
352
360
  content: options.content,
@@ -91,6 +91,7 @@ test("reconnect subscribes again and refreshes authoritative state even without
91
91
  },
92
92
  onValue: (state) => {
93
93
  values.push(state.artifact.meta.title ?? "Initial");
94
+
94
95
  if (connections === 1) initial.resolve();
95
96
  else recovered.resolve();
96
97
  },
@@ -126,6 +127,7 @@ test("a malformed protocol frame stops the observer instead of reconnecting", as
126
127
  open(socket) {
127
128
  reads.set(socket, new LineBuffer());
128
129
  connections++;
130
+
129
131
  if (connections > 1) failed.reject(new Error("Unexpected reconnect"));
130
132
  },
131
133
  data(socket, bytes) {
@@ -141,6 +143,7 @@ test("a malformed protocol frame stops the observer instead of reconnecting", as
141
143
  : {},
142
144
  }) + "\n",
143
145
  );
146
+
144
147
  if (request.method === "events.subscribe") socket.write("{broken\n");
145
148
  });
146
149
  },
@@ -34,11 +34,13 @@ export function subscribeThreadState<Value>(options: ThreadSubscriptionOptions<V
34
34
 
35
35
  try {
36
36
  api = await options.connect();
37
+
37
38
  if (stopped || epoch !== generation) {
38
39
  api.close();
39
40
 
40
41
  return;
41
42
  }
43
+
42
44
  connection = api;
43
45
  const current = api;
44
46
  const refresh = async () => {
@@ -62,6 +64,7 @@ export function subscribeThreadState<Value>(options: ThreadSubscriptionOptions<V
62
64
  detach?.();
63
65
  current.close();
64
66
  }
67
+
65
68
  options.onError(error);
66
69
  }
67
70
  } finally {
@@ -73,11 +76,13 @@ export function subscribeThreadState<Value>(options: ThreadSubscriptionOptions<V
73
76
  });
74
77
  const offDisconnect = api.onDisconnect((reason) => {
75
78
  if (stopped || epoch !== generation) return;
79
+
76
80
  ++generation;
77
81
  detach?.();
78
82
  for (const read of reads) read.abort();
79
83
  connection = undefined;
80
84
  current.close();
85
+
81
86
  if (isFatalSubscriptionError(reason)) {
82
87
  stopped = true;
83
88
  options.onError(reason);
@@ -89,16 +94,20 @@ export function subscribeThreadState<Value>(options: ThreadSubscriptionOptions<V
89
94
  offDisconnect();
90
95
  };
91
96
  await api.subscribe();
97
+
92
98
  if (stopped || epoch !== generation) return;
99
+
93
100
  options.onConnect?.(api);
94
101
  await refresh();
95
102
  } catch (error) {
96
103
  if (stopped || epoch !== generation) return;
104
+
97
105
  ++generation;
98
106
  detach?.();
99
107
  api?.close();
100
108
  connection = undefined;
101
109
  options.onError(error);
110
+
102
111
  if (!(error instanceof Error && isFatalSubscriptionError(error))) schedule();
103
112
  }
104
113
  };
@@ -58,6 +58,7 @@ describe("parseParams", () => {
58
58
  throw new Error("should have thrown");
59
59
  } catch (err) {
60
60
  expect(err).toBeInstanceOf(DaemonError);
61
+
61
62
  if (err instanceof DaemonError) {
62
63
  expect(err.code).toBe("invalid_params");
63
64
  expect(err.message).toContain("session.create: workspace");
@@ -180,6 +181,7 @@ describe("validateThreadRecord", () => {
180
181
 
181
182
  // Assert
182
183
  expect(result.ok).toBe(false);
184
+
183
185
  if (!result.ok) expect(result.error).toContain("schemaVersion");
184
186
  });
185
187
 
@@ -366,6 +368,7 @@ describe("wire pins", () => {
366
368
 
367
369
  // Assert
368
370
  expect(parsed.ok).toBe(false);
371
+
369
372
  if (!parsed.ok) expect(parsed.error).toContain("history:");
370
373
  });
371
374
 
@@ -376,6 +379,7 @@ describe("wire pins", () => {
376
379
  });
377
380
 
378
381
  expect(result.ok).toBe(false);
382
+
379
383
  if (!result.ok) {
380
384
  expect(result.error).toContain(
381
385
  "text Cuts do not match the submitted artifact and working copy",
package/src/validate.ts CHANGED
@@ -478,6 +478,7 @@ export const SessionHistorySchema = v.pipe(
478
478
  // the shape is not enough: the tree itself must hold, or a walk hangs or rewires the path
479
479
  v.rawCheck(({ dataset, addIssue }) => {
480
480
  if (!dataset.typed) return;
481
+
481
482
  const problem = validateHistory(dataset.value);
482
483
 
483
484
  if (problem !== null) addIssue({ message: `history: ${problem}` });
@@ -519,6 +520,7 @@ export const ThreadRecordSchema = v.pipe(
519
520
  } satisfies EntriesOf<Thread>),
520
521
  v.rawCheck(({ dataset, addIssue }) => {
521
522
  if (!dataset.typed || !dataset.value.textCuts?.length) return;
523
+
522
524
  const { artifact, textCuts, workingCopy } = dataset.value;
523
525
  const matchesSource = textCuts.every(
524
526
  (cut) => artifact.content.slice(cut.start, cut.end) === cut.quote,
@@ -536,6 +538,7 @@ export function validateThreadRecord(
536
538
  const result = v.safeParse(ThreadRecordSchema, raw);
537
539
 
538
540
  if (result.success) return { ok: true, value: result.output };
541
+
539
542
  const issue = result.issues[0]!;
540
543
  const path = issue.path?.map((pathSegment) => String(pathSegment.key)).join(".") ?? "";
541
544
 
package/src/vcs-source.ts CHANGED
@@ -71,6 +71,7 @@ const ExtensionModuleSchema = v.object({ default: v.function() });
71
71
 
72
72
  function readVcsToml(path: string): Partial<VcsConfig> {
73
73
  if (!existsSync(path)) return {};
74
+
74
75
  try {
75
76
  const parsed = v.safeParse(VcsConfigSchema, parseToml(readFileSync(path, "utf8")));
76
77
 
@@ -100,6 +101,7 @@ export class VcsSourceManager {
100
101
 
101
102
  private async adapters(): Promise<VcsAdapter[]> {
102
103
  if (!this.loaded) this.loaded = this.loadExtensions();
104
+
103
105
  await this.loaded;
104
106
 
105
107
  return [
@@ -131,6 +133,7 @@ export class VcsSourceManager {
131
133
  const parsed = v.safeParse(ExtensionModuleSchema, module);
132
134
 
133
135
  if (!parsed.success) throw new Error("VCS extension must export a default factory");
136
+
134
137
  const factory: ExtensionFactory = async (api) => {
135
138
  await parsed.output.default(api);
136
139
  };
@@ -177,6 +180,7 @@ export class VcsSourceManager {
177
180
 
178
181
  if (!selected && requested === "git" && detected.length === 0)
179
182
  return { adapter: gitVcsAdapter, repoRoot: cwd };
183
+
180
184
  if (!selected)
181
185
  throw new Error(
182
186
  `VCS adapter unavailable: ${requested}${this.extensionErrors.length > 0 ? ` (${this.extensionErrors.join("; ")})` : ""}`,
@@ -184,6 +188,7 @@ export class VcsSourceManager {
184
188
 
185
189
  return selected;
186
190
  }
191
+
187
192
  detected.sort((a, b) => {
188
193
  const rootDistance = b.repoRoot.length - a.repoRoot.length;
189
194
 
@@ -219,6 +224,7 @@ export class VcsSourceManager {
219
224
 
220
225
  if (!selected.adapter.captureChange)
221
226
  throw new Error(`VCS adapter ${provider} cannot follow a change ID`);
227
+
222
228
  const snapshot = v.parse(
223
229
  VcsDiffSnapshotSchema,
224
230
  await selected.adapter.captureChange(selected.repoRoot, changeId),
@@ -17,6 +17,7 @@ function statusFromCode(code: string): DiffFileStatus {
17
17
  const letter = code.charAt(0);
18
18
 
19
19
  if (letter === "D") return "deleted";
20
+
20
21
  if (letter === "A") return "added";
21
22
 
22
23
  return "modified";
@@ -35,6 +36,7 @@ export async function workingChangeList(
35
36
 
36
37
  for (const line of tracked.split("\n")) {
37
38
  if (line.trim().length === 0) continue;
39
+
38
40
  const parts = line.split("\t");
39
41
 
40
42
  // a rename is "R100<tab>old<tab>new"; the new path (last field) is what the reviewer opens
@@ -101,11 +103,13 @@ async function trackedFileContents(cwd: string): Promise<DiffFileContents[]> {
101
103
  cursor += 2;
102
104
  continue;
103
105
  }
106
+
104
107
  const path = tokens[cursor++]!;
105
108
  const oldContents = code === "A" ? "" : await headContents(path, cwd);
106
109
  const newContents = code === "D" ? "" : await workingContents(path, cwd);
107
110
 
108
111
  if (looksBinary(oldContents) || looksBinary(newContents)) continue;
112
+
109
113
  const status = code === "A" ? "added" : code === "D" ? "deleted" : "modified";
110
114
 
111
115
  files.push({ path, oldContents, newContents, status });