@cueloop/daemon 0.1.0-alpha.101 → 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 +361 -25
  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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/daemon",
3
- "version": "0.1.0-alpha.101",
3
+ "version": "0.1.0-alpha.103",
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": {
@@ -44,8 +44,8 @@
44
44
  "access": "public"
45
45
  },
46
46
  "dependencies": {
47
- "@cueloop/extension-api": "0.1.0-alpha.101",
48
- "@cueloop/schema": "0.1.0-alpha.101",
47
+ "@cueloop/extension-api": "0.1.0-alpha.103",
48
+ "@cueloop/schema": "0.1.0-alpha.103",
49
49
  "@pierre/diffs": "^1.5.1",
50
50
  "effect": "^4.0.0",
51
51
  "smol-toml": "^1.9.0",
@@ -19,6 +19,7 @@ export function parseAgentToolInput(name: string, serialized: string) {
19
19
 
20
20
  return { kind: "reply" as const, ...args };
21
21
  }
22
+
22
23
  if (name === "send_message") {
23
24
  const args = v.parse(
24
25
  v.object({
@@ -31,7 +32,9 @@ export function parseAgentToolInput(name: string, serialized: string) {
31
32
 
32
33
  return { kind: "api" as const, method: "session.sendMessage", params: args };
33
34
  }
35
+
34
36
  if (name !== "cueloop_api") throw new Error("Thread agent tool name is unavailable");
37
+
35
38
  const args = v.parse(v.object({ method: v.string(), params: v.unknown() }), input);
36
39
 
37
40
  return { kind: "api" as const, ...args };
@@ -77,7 +80,9 @@ export function assertAgentToolScope(
77
80
 
78
81
  return;
79
82
  }
83
+
80
84
  if (input.method === "session.list") return;
85
+
81
86
  const params = v.parse(v.record(v.string(), v.unknown()), input.params);
82
87
 
83
88
  if (THREAD_AGENT_SESSION_METHODS.has(input.method)) {
@@ -85,6 +90,7 @@ export function assertAgentToolScope(
85
90
 
86
91
  return;
87
92
  }
93
+
88
94
  if (THREAD_AGENT_REPO_METHODS.has(input.method)) {
89
95
  const { cwd } = v.parse(v.object({ cwd: v.string() }), params);
90
96
 
package/src/api.ts CHANGED
@@ -238,6 +238,7 @@ export class DaemonCore {
238
238
  const binding = this.harnessState.binding(bindingId);
239
239
 
240
240
  if (!binding) throw new DaemonError("not_found", `no harness binding ${bindingId}`);
241
+
241
242
  const session = this.sessionGet(binding.threadId);
242
243
 
243
244
  if (
@@ -259,6 +260,7 @@ export class DaemonCore {
259
260
  const binding = this.harnessState.binding(bindingId);
260
261
 
261
262
  if (!binding) throw new DaemonError("not_found", `no harness binding ${bindingId}`);
263
+
262
264
  const session = this.sessionGet(binding.threadId);
263
265
 
264
266
  return this.harnessState.pending(bindingId).map((delivery) => {
@@ -377,8 +379,10 @@ export class DaemonCore {
377
379
  }): Thread[] {
378
380
  return this.store.list().filter((session) => {
379
381
  if (filter?.status && session.status !== filter.status) return false;
382
+
380
383
  if (filter?.workspace?.repoRoot && session.workspace.repoRoot !== filter.workspace.repoRoot)
381
384
  return false;
385
+
382
386
  if (filter?.workspace?.branch && session.workspace.branch !== filter.workspace.branch)
383
387
  return false;
384
388
 
@@ -401,14 +405,17 @@ export class DaemonCore {
401
405
  let done = false;
402
406
  const timer = setTimeout(() => {
403
407
  if (done) return;
408
+
404
409
  done = true;
405
410
  const waiterIndex = list.indexOf(waiter);
406
411
 
407
412
  if (waiterIndex !== -1) list.splice(waiterIndex, 1);
413
+
408
414
  resolve(null);
409
415
  }, timeoutMs);
410
416
  const waiter = (session: Thread | null) => {
411
417
  if (done) return;
418
+
412
419
  done = true;
413
420
  clearTimeout(timer);
414
421
  resolve(session);
@@ -432,6 +439,7 @@ export class DaemonCore {
432
439
 
433
440
  // a welcome-playground note is ephemeral by design: never persisted, so never fed back
434
441
  if (annotationTarget(annotation).kind === "welcome") return session;
442
+
435
443
  const existing = session.annotations.findIndex((candidate) => candidate.id === annotation.id);
436
444
  const full: Annotation = { ...annotation, createdAt: new Date().toISOString() };
437
445
 
@@ -449,12 +457,14 @@ export class DaemonCore {
449
457
  ...full,
450
458
  createdAt: session.annotations[existing]!.createdAt,
451
459
  };
460
+
452
461
  if (annotation.author && authorName)
453
462
  session.participants = registerParticipant(
454
463
  session,
455
464
  annotation.author,
456
465
  authorName,
457
466
  ).participants;
467
+
458
468
  this.store.upsert(session);
459
469
  this.emit("session.updated", id, entryId);
460
470
 
@@ -473,6 +483,7 @@ export class DaemonCore {
473
483
  if (onBehalfOf !== undefined && target !== undefined && target.author !== onBehalfOf) {
474
484
  throw new DaemonError("forbidden", `${onBehalfOf} cannot remove another author's comment`);
475
485
  }
486
+
476
487
  session.annotations = session.annotations.filter((candidate) => candidate.id !== annotationId);
477
488
  let entryId: string | undefined;
478
489
 
@@ -508,6 +519,7 @@ export class DaemonCore {
508
519
  const session = this.mutable(id);
509
520
 
510
521
  if (textCuts?.length) this.assertTextCuts(session.artifact.content, workingCopy, textCuts);
522
+
511
523
  const entryId = this.applyWorkingCopy(session, workingCopy, textCuts);
512
524
 
513
525
  this.store.upsert(session);
@@ -552,6 +564,7 @@ export class DaemonCore {
552
564
 
553
565
  if (viewedPaths.length === 0) delete session.viewedPaths;
554
566
  else session.viewedPaths = [...new Set([...(session.viewedPaths ?? []), ...viewedPaths])];
567
+
555
568
  this.store.upsert(session);
556
569
  this.emit("session.updated", id);
557
570
 
@@ -565,6 +578,7 @@ export class DaemonCore {
565
578
 
566
579
  if (trimmed.length === 0) delete session.artifact.meta.title;
567
580
  else session.artifact.meta.title = trimmed;
581
+
568
582
  this.store.upsert(session);
569
583
  // the title shows in the Threads sidebar, so a rename is an inbox change, not just a content edit
570
584
  this.emit("inbox.changed", id);
@@ -668,10 +682,12 @@ export class DaemonCore {
668
682
  );
669
683
 
670
684
  if (open) return { session: open };
685
+
671
686
  // serialize concurrent bare launches for the same repo onto one creation, so they share a thread
672
687
  const inFlight = this.workbenchCreation.get(key);
673
688
 
674
689
  if (inFlight) return inFlight;
690
+
675
691
  const creation = this.vcsSources.capture(cwd, selected.adapter.id).then((diff) => ({
676
692
  session: this.sessionCreate({
677
693
  workspace,
@@ -716,7 +732,9 @@ export class DaemonCore {
716
732
  const session = this.store.get(id);
717
733
 
718
734
  if (!this.store.delete(id)) throw new DaemonError("not_found", `no session ${id}`);
735
+
719
736
  if (session) this.untrackLiveDiffSession(session);
737
+
720
738
  this.diffRefreshGenerations.delete(id);
721
739
  this.herdrThreadSurfaces.delete(id);
722
740
  this.ghosttyThreadSurfaces.delete(id);
@@ -740,6 +758,7 @@ export class DaemonCore {
740
758
 
741
759
  if (!block)
742
760
  throw new DaemonError("invalid_params", `no block ${blockIndex} in the working copy`);
761
+
743
762
  const entryId = this.applyWorkingCopy(session, cutBlock(working, block));
744
763
 
745
764
  this.store.upsert(session);
@@ -764,6 +783,7 @@ export class DaemonCore {
764
783
  `no block ${baseBlockIndex} in the submitted revision`,
765
784
  );
766
785
  }
786
+
767
787
  const working = session.workingCopy ?? base;
768
788
 
769
789
  if (!isBlockCut(base, working, block)) {
@@ -772,6 +792,7 @@ export class DaemonCore {
772
792
  `block ${baseBlockIndex} of the submitted revision is present in the working copy`,
773
793
  );
774
794
  }
795
+
775
796
  const beforeLine = Math.min(line ?? working.split("\n").length, working.split("\n").length);
776
797
  const entryId = this.applyWorkingCopy(session, restoreBlock(base, working, block, beforeLine));
777
798
 
@@ -792,11 +813,14 @@ export class DaemonCore {
792
813
  if (session.artifact.type !== "diff") {
793
814
  throw new DaemonError("invalid_params", "only a diff review is curated by hunk");
794
815
  }
816
+
795
817
  if (!session.artifact.files?.length) {
796
818
  throw new DaemonError("invalid_params", "hunk curation needs full file contents");
797
819
  }
820
+
798
821
  if (rejections.length === 0) delete session.curation;
799
822
  else session.curation = rejections;
823
+
800
824
  const entryId = this.applyWorkingCopy(
801
825
  session,
802
826
  rejections.length === 0 ? undefined : curateDiff(session.artifact.files, rejections),
@@ -828,6 +852,7 @@ export class DaemonCore {
828
852
 
829
853
  for (const annotation of incoming.annotations) {
830
854
  if (known.has(annotation.id)) continue;
855
+
831
856
  known.add(annotation.id);
832
857
  session.annotations.push(annotation);
833
858
  this.recordOn(session, branch, {
@@ -841,6 +866,7 @@ export class DaemonCore {
841
866
 
842
867
  for (const removal of incoming.removals ?? []) {
843
868
  if (recorded.has(removal.id) || !known.has(removal.annotationId)) continue;
869
+
844
870
  this.recordOn(session, branch, {
845
871
  id: removal.id,
846
872
  type: "comment-removed",
@@ -849,6 +875,7 @@ export class DaemonCore {
849
875
  });
850
876
  changed = true;
851
877
  }
878
+
852
879
  if (incoming.participants?.length) {
853
880
  const registry = new Map(
854
881
  (session.participants ?? []).map((participant) => [participant.id, participant]),
@@ -857,8 +884,10 @@ export class DaemonCore {
857
884
  for (const participant of incoming.participants) registry.set(participant.id, participant);
858
885
  session.participants = [...registry.values()];
859
886
  }
887
+
860
888
  if (changed && session.history) this.refreshView(session, session.history);
861
889
  else this.store.upsert(session);
890
+
862
891
  this.emit("session.updated", id);
863
892
 
864
893
  return session;
@@ -880,6 +909,7 @@ export class DaemonCore {
880
909
  const receipt = findOperationReceipt(current.messageOperations, operationId, fingerprint);
881
910
 
882
911
  if (receipt) return { ...structuredClone(current), message: structuredClone(receipt.result) };
912
+
883
913
  const mutable = this.mutable(id);
884
914
  const session = operationId ? structuredClone(mutable) : mutable;
885
915
  const sentAt = new Date().toISOString();
@@ -906,6 +936,7 @@ export class DaemonCore {
906
936
 
907
937
  if (operationId)
908
938
  (session.messageOperations ??= []).push({ operationId, fingerprint, result: message });
939
+
909
940
  session.message = message;
910
941
  session.status = outcome === "comment" ? "pending" : "resolved";
911
942
  const entryId = this.record(session, {
@@ -917,11 +948,13 @@ export class DaemonCore {
917
948
  this.store.upsert(session);
918
949
  this.reconcileDeliveries(session);
919
950
  this.emit("message.sent", id, entryId);
951
+
920
952
  if (outcome === "comment") {
921
953
  this.emit("session.updated", id, entryId);
922
954
 
923
955
  return session;
924
956
  }
957
+
925
958
  // a resolved diff review is frozen; stop hot-reloading its working tree
926
959
  this.untrackLiveDiffSession(session);
927
960
  const parked = this.waiters.get(id) ?? [];
@@ -993,6 +1026,7 @@ export class DaemonCore {
993
1026
 
994
1027
  for (const annotation of session.annotations) {
995
1028
  if (isAddressed(annotation) || isAgentNote(annotation)) continue;
1029
+
996
1030
  if (reportedIds.has(annotation.id)) {
997
1031
  annotation.resolution = { revision: revisionNumber, source: "agent" };
998
1032
  } else if (
@@ -1107,8 +1141,10 @@ export class DaemonCore {
1107
1141
  fork.annotations = view.annotations.map((annotation) =>
1108
1142
  forkedAnnotation(annotation, source, fork),
1109
1143
  );
1144
+
1110
1145
  if (source.participants)
1111
1146
  fork.participants = source.participants.map((identity) => ({ ...identity }));
1147
+
1112
1148
  this.store.upsert(fork);
1113
1149
  this.trackLiveDiffSession(fork);
1114
1150
  this.emit("session.created", fork.id);
@@ -1122,8 +1158,10 @@ export class DaemonCore {
1122
1158
  const session = this.mutable(id);
1123
1159
 
1124
1160
  if (session.artifact.type !== "diff") return { changed: false };
1161
+
1125
1162
  // a PR review has no local working tree; re-pull it from the PR instead of clobbering it
1126
1163
  if (session.artifact.meta.pr !== undefined) return this.sessionRefreshPrDiff(id);
1164
+
1127
1165
  const generation = (this.diffRefreshGenerations.get(id) ?? 0) + 1;
1128
1166
 
1129
1167
  this.diffRefreshGenerations.set(id, generation);
@@ -1145,14 +1183,17 @@ export class DaemonCore {
1145
1183
  // capture, and re-read so a resolved review is never mutated and a deleted
1146
1184
  // one is never revived by this upsert.
1147
1185
  if (this.diffRefreshGenerations.get(id) !== generation) return { changed: false };
1186
+
1148
1187
  const current = this.store.get(id);
1149
1188
 
1150
1189
  if (!current || current.status !== "pending" || current.artifact.type !== "diff")
1151
1190
  return { changed: false };
1191
+
1152
1192
  const contentChanged = diff.patch !== current.artifact.content;
1153
1193
  const sourceChanged = diff.source?.revisionId !== current.artifact.meta.vcsRevisionId;
1154
1194
 
1155
1195
  if (!contentChanged && !sourceChanged) return { changed: false };
1196
+
1156
1197
  current.artifact = {
1157
1198
  ...current.artifact,
1158
1199
  content: diff.patch,
@@ -1206,12 +1247,15 @@ export class DaemonCore {
1206
1247
 
1207
1248
  return;
1208
1249
  }
1250
+
1209
1251
  if (changed) {
1210
1252
  const history = withHistory(session).history;
1211
1253
 
1212
1254
  if (history) session.history = recaptureMainHead(history, diff.patch);
1213
1255
  }
1256
+
1214
1257
  if (pinned) return;
1258
+
1215
1259
  const liveRevision = session.revisions.at(-1);
1216
1260
 
1217
1261
  if (liveRevision) {
@@ -1233,9 +1277,11 @@ export class DaemonCore {
1233
1277
 
1234
1278
  if (!session || session.status !== "pending" || session.artifact.type !== "diff")
1235
1279
  return { changed: false };
1280
+
1236
1281
  const pr = session.artifact.meta.prUrl ?? session.artifact.meta.pr;
1237
1282
 
1238
1283
  if (pr === undefined) return { changed: false };
1284
+
1239
1285
  const generation = (this.diffRefreshGenerations.get(id) ?? 0) + 1;
1240
1286
 
1241
1287
  this.diffRefreshGenerations.set(id, generation);
@@ -1243,12 +1289,15 @@ export class DaemonCore {
1243
1289
 
1244
1290
  // gh failed (offline, unauthenticated): keep the diff we have, try again next poll
1245
1291
  if (snapshot === null) return { changed: false };
1292
+
1246
1293
  // the pull yields the event loop: discard a stale capture or a closed session
1247
1294
  if (this.diffRefreshGenerations.get(id) !== generation) return { changed: false };
1295
+
1248
1296
  const current = this.store.get(id);
1249
1297
 
1250
1298
  if (!current || current.status !== "pending" || current.artifact.type !== "diff")
1251
1299
  return { changed: false };
1300
+
1252
1301
  const contentChanged = snapshot.patch !== current.artifact.content;
1253
1302
  const sourceChanged =
1254
1303
  snapshot.baseSha !== current.artifact.meta.prBaseSha ||
@@ -1262,6 +1311,7 @@ export class DaemonCore {
1262
1311
  current.artifact.meta.prRefreshHeadSha === undefined
1263
1312
  )
1264
1313
  return { changed: false };
1314
+
1265
1315
  current.artifact = {
1266
1316
  ...current.artifact,
1267
1317
  content: snapshot.patch,
@@ -1273,6 +1323,7 @@ export class DaemonCore {
1273
1323
  prRefreshHeadSha: undefined,
1274
1324
  },
1275
1325
  };
1326
+
1276
1327
  if (contentChanged || sourceChanged) {
1277
1328
  const now = new Date().toISOString();
1278
1329
 
@@ -1293,6 +1344,7 @@ export class DaemonCore {
1293
1344
  createdAt: now,
1294
1345
  });
1295
1346
  }
1347
+
1296
1348
  this.store.upsert(current);
1297
1349
  this.emit("session.updated", id);
1298
1350
 
@@ -1304,11 +1356,13 @@ export class DaemonCore {
1304
1356
  const session = this.store.get(id);
1305
1357
 
1306
1358
  if (!session || session.status !== "pending" || session.artifact.meta.pr === undefined) return;
1359
+
1307
1360
  if (
1308
1361
  session.artifact.meta.prBaseSha === refs.baseSha &&
1309
1362
  session.artifact.meta.prHeadSha === refs.headSha
1310
1363
  )
1311
1364
  return;
1365
+
1312
1366
  session.artifact = {
1313
1367
  ...session.artifact,
1314
1368
  meta: {
@@ -1361,6 +1415,7 @@ export class DaemonCore {
1361
1415
 
1362
1416
  private untrackLiveDiffSession(session: Thread): void {
1363
1417
  if (session.artifact.type !== "diff") return;
1418
+
1364
1419
  this.diffWatcher.untrackDiffRepo(session.workspace.repoRoot, session.id);
1365
1420
  this.prPoller.untrackPr(session.id);
1366
1421
  }
@@ -1382,8 +1437,10 @@ export class DaemonCore {
1382
1437
 
1383
1438
  if (textCuts?.length) session.textCuts = textCuts;
1384
1439
  else delete session.textCuts;
1440
+
1385
1441
  if (next === undefined) delete session.workingCopy;
1386
1442
  else session.workingCopy = next;
1443
+
1387
1444
  const after = next ?? session.artifact.content;
1388
1445
 
1389
1446
  if (after === before) return undefined;
@@ -1415,6 +1472,7 @@ export class DaemonCore {
1415
1472
  return operation();
1416
1473
  } catch (error) {
1417
1474
  if (error instanceof HistoryError) throw new DaemonError("invalid_params", error.message);
1475
+
1418
1476
  throw error;
1419
1477
  }
1420
1478
  }
@@ -1434,6 +1492,7 @@ export class DaemonCore {
1434
1492
  const history = withHistory(session).history;
1435
1493
 
1436
1494
  if (!history) return undefined;
1495
+
1437
1496
  const appended = appendEntry(history, entry);
1438
1497
 
1439
1498
  session.history = appended.history;
@@ -1451,6 +1510,7 @@ export class DaemonCore {
1451
1510
  const history = withHistory(session).history;
1452
1511
 
1453
1512
  if (!history) return undefined;
1513
+
1454
1514
  const appended = appendEntry(switchBranch(history, branch), entry);
1455
1515
 
1456
1516
  session.history = { ...appended.history, branch: history.branch };
@@ -1464,6 +1524,7 @@ export class DaemonCore {
1464
1524
 
1465
1525
  if (shareId !== undefined && !link)
1466
1526
  throw new DaemonError("not_found", `no share link ${shareId}`);
1527
+
1467
1528
  const branch = link?.shareBranch ?? MAIN_BRANCH;
1468
1529
 
1469
1530
  return session.history?.tips[branch] === undefined ? MAIN_BRANCH : branch;
@@ -1491,6 +1552,7 @@ function forkedAnnotation(annotation: Annotation, source: Thread, fork: Thread):
1491
1552
  const { resolution, ...open } = annotation;
1492
1553
 
1493
1554
  if (resolution === undefined) return { ...annotation };
1555
+
1494
1556
  const addressedBy = source.revisions.find(
1495
1557
  (revision) => revision.revision === resolution.revision,
1496
1558
  );
@@ -1503,6 +1565,7 @@ function forkedAnnotation(annotation: Annotation, source: Thread, fork: Thread):
1503
1565
 
1504
1566
  function applyRevisionSource(artifact: Artifact, source?: DiffSource): void {
1505
1567
  if (source) artifact.meta.vcs = source.vcs;
1568
+
1506
1569
  if (artifact.type !== "diff") return;
1507
1570
 
1508
1571
  artifact.meta.vcsChangeId = source?.changeId;
@@ -84,6 +84,7 @@ test("deadlines, local abort and close reject outstanding requests without cance
84
84
 
85
85
  return;
86
86
  }
87
+
87
88
  socket.write(
88
89
  JSON.stringify({
89
90
  id: request.id,
@@ -154,6 +155,7 @@ test("malformed frames reject all pending requests as protocol failures", async
154
155
 
155
156
  return;
156
157
  }
158
+
157
159
  socket.write(
158
160
  JSON.stringify({
159
161
  id: request.id,
package/src/client.ts CHANGED
@@ -80,7 +80,11 @@ type PendingRequest = {
80
80
  const EmptyResultSchema = v.object({});
81
81
  // version is optional: a daemon from before the handshake carried one reads as
82
82
  // undefined, which never equals this build.
83
- const PingResultSchema = v.object({ pid: v.number(), version: v.optional(v.string()) });
83
+ const PingResultSchema = v.object({
84
+ pid: v.number(),
85
+ version: v.optional(v.string()),
86
+ agentHarness: v.optional(v.string()),
87
+ });
84
88
  const RefreshDiffResultSchema = v.object({ changed: v.boolean() });
85
89
  const HerdrThreadSurfaceResultSchema = v.nullable(
86
90
  v.object({
@@ -181,6 +185,7 @@ function readOwnerToken(home: string): string | undefined {
181
185
  const path = ownerTokenPath(home);
182
186
 
183
187
  if (!existsSync(path)) return undefined;
188
+
184
189
  const parsed = v.safeParse(OwnerTokenSchema, readFileSync(path, "utf8"));
185
190
 
186
191
  if (!parsed.success) {
@@ -219,7 +224,9 @@ export class DaemonClient implements ThreadClient {
219
224
  client.home = home;
220
225
  try {
221
226
  await client.dial(path);
227
+
222
228
  if (client.daemonVersion === DAEMON_VERSION) return client;
229
+
223
230
  const daemonVersion = client.daemonVersion ?? "unknown";
224
231
 
225
232
  client.close();
@@ -273,8 +280,10 @@ export class DaemonClient implements ThreadClient {
273
280
  while (Date.now() < deadline) {
274
281
  try {
275
282
  await this.dial(path);
283
+
276
284
  if (this.daemonVersion !== DAEMON_VERSION) {
277
285
  this.close();
286
+
278
287
  if (typeof Bun === "undefined") {
279
288
  throw new DaemonClientError(
280
289
  "version_mismatch",
@@ -288,6 +297,7 @@ export class DaemonClient implements ThreadClient {
288
297
  return this;
289
298
  } catch (err) {
290
299
  if (err instanceof DaemonClientError) throw err;
300
+
291
301
  this.socket?.end();
292
302
  this.resetConnection();
293
303
  lastError = err;
@@ -315,6 +325,7 @@ export class DaemonClient implements ThreadClient {
315
325
 
316
326
  const close = () => {
317
327
  if (this.connectionEpoch !== epoch) return;
328
+
318
329
  this.closed = true;
319
330
  for (const pendingRequest of this.pending.values())
320
331
  pendingRequest.reject(
@@ -365,6 +376,7 @@ export class DaemonClient implements ThreadClient {
365
376
  },
366
377
  });
367
378
  }
379
+
368
380
  // Verify liveness: a dead socket file accepts connects on some platforms
369
381
  // only to fail later, so a ping is the actual handshake. It also carries the
370
382
  // daemon's build version, so connect() can reject incompatible clients.
@@ -383,6 +395,7 @@ export class DaemonClient implements ThreadClient {
383
395
  ? { role: this.role, clientVersion: DAEMON_VERSION }
384
396
  : { role: this.role, clientVersion: DAEMON_VERSION, author: this.author };
385
397
  }
398
+
386
399
  const token = readOwnerToken(this.home);
387
400
 
388
401
  // a daemon from before owner tokens has no file; it still knows the bare hello
@@ -432,15 +445,19 @@ export class DaemonClient implements ThreadClient {
432
445
 
433
446
  return;
434
447
  }
448
+
435
449
  if ("event" in frame) {
436
450
  for (const listener of this.eventListeners) listener(frame);
437
451
 
438
452
  return;
439
453
  }
454
+
440
455
  const pendingRequest = this.pending.get(frame.id);
441
456
 
442
457
  if (!pendingRequest) return;
458
+
443
459
  this.pending.delete(frame.id);
460
+
444
461
  if (frame.error)
445
462
  pendingRequest.reject(new DaemonClientError(frame.error.code, frame.error.message));
446
463
  else pendingRequest.resolve(frame.result);
@@ -460,10 +477,12 @@ export class DaemonClient implements ThreadClient {
460
477
  return Promise.reject(
461
478
  new DaemonTransportError("cancelled", `request ${method} cancelled`, "not_sent", method),
462
479
  );
480
+
463
481
  if (this.closed || !this.socket)
464
482
  return Promise.reject(
465
483
  new DaemonTransportError("connection", "not connected", "not_sent", method),
466
484
  );
485
+
467
486
  const id = this.nextId++;
468
487
 
469
488
  return new Promise<TOutput>((resolve, reject) => {
@@ -498,6 +517,7 @@ export class DaemonClient implements ThreadClient {
498
517
  method,
499
518
  { cause: parsed.issues },
500
519
  );
520
+
501
521
  resolve(parsed.output);
502
522
  } catch (cause) {
503
523
  reject(
@@ -579,7 +599,7 @@ export class DaemonClient implements ThreadClient {
579
599
  }): Promise<ThreadAgentState> {
580
600
  return this.request("agent.permission", params, ThreadAgentSchema);
581
601
  }
582
- ping(): Promise<{ pid: number }> {
602
+ ping(): Promise<{ pid: number; agentHarness?: string }> {
583
603
  return this.request("daemon.ping", {}, PingResultSchema);
584
604
  }
585
605
  subscribe(): Promise<void> {
@@ -642,6 +662,7 @@ export class DaemonClient implements ThreadClient {
642
662
  const params: v.InferInput<(typeof Params)["session.navigate"]> = { id, entryId };
643
663
 
644
664
  if (summary !== undefined) params.summary = summary;
665
+
645
666
  if (branch !== undefined) params.branch = branch;
646
667
 
647
668
  return this.request("session.navigate", params, ThreadRecordSchema);
@@ -864,6 +885,7 @@ export function daemonSpawnCommand(
864
885
  moduleUrl.includes("$bunfs") || moduleUrl.includes("~BUN") || moduleUrl.includes("%7EBUN");
865
886
 
866
887
  if (compiled) return [execPath, "daemon", "--autostart"];
888
+
867
889
  const mainPath = entry ?? new URL("./main.ts", moduleUrl).pathname;
868
890
 
869
891
  const args = entry ? ["daemon", "--autostart"] : [];
package/src/curate.ts CHANGED
@@ -64,6 +64,7 @@ function curateFilePatch(file: DiffFileContents, rejections: HunkRejection[]): s
64
64
  const curatedNew = model.additionLines.join("");
65
65
 
66
66
  if (curatedNew === file.oldContents) return null;
67
+
67
68
  const patch = unifiedDiffText(file.oldContents, curatedNew, file.path);
68
69
 
69
70
  if (patch === null) return null;
@@ -84,6 +85,7 @@ function withFileStateHeaders(
84
85
  curatedNew: string,
85
86
  ): string {
86
87
  if (status === "added") return patch.replace(`--- a/${path}`, "--- /dev/null");
88
+
87
89
  if (status === "deleted" && curatedNew === "") {
88
90
  return patch.replace(`+++ b/${path}`, "+++ /dev/null");
89
91
  }
package/src/daemon-sdk.ts CHANGED
@@ -81,6 +81,7 @@ export class ReviewDaemonSdk {
81
81
 
82
82
  return () => {};
83
83
  }
84
+
84
85
  const dispose = subscribeThreadState({
85
86
  connect: connectThreadObserver(this.connectionOptions),
86
87
  matches: (event) => event.sessionId === input.threadId,
@@ -128,6 +129,7 @@ export class ReviewDaemonSdk {
128
129
 
129
130
  if (!comment)
130
131
  throw new DaemonClientError("not_found", "SDK comment reply target does not exist");
132
+
131
133
  const rootId = comment.replyTo ?? comment.id;
132
134
  const root = thread.annotations.find((entry) => entry.id === rootId);
133
135
 
@@ -277,6 +279,7 @@ export class OwnerDaemonSdk extends ReviewDaemonSdk {
277
279
  return Promise.reject(
278
280
  new DaemonTransportError("connection", "SDK connection is closed", "not_sent"),
279
281
  );
282
+
280
283
  if (!accepted.submissionIds.length)
281
284
  return Promise.reject(
282
285
  new DaemonClientError("invalid_params", "SDK wait requires accepted submissions"),
@@ -294,11 +297,13 @@ export class OwnerDaemonSdk extends ReviewDaemonSdk {
294
297
  let detach: (() => void) | undefined;
295
298
  const finish = (result?: AgentCompletion, error?: DaemonSdkError) => {
296
299
  if (done) return;
300
+
297
301
  done = true;
298
302
  clearTimeout(timer);
299
303
  signal.removeEventListener("abort", abort);
300
304
  this.waits.delete(controller);
301
305
  detach?.();
306
+
302
307
  if (result) resolve(result);
303
308
  else reject(error);
304
309
  };
@@ -321,11 +326,13 @@ export class OwnerDaemonSdk extends ReviewDaemonSdk {
321
326
  );
322
327
 
323
328
  signal.addEventListener("abort", abort, { once: true });
329
+
324
330
  if (signal.aborted) {
325
331
  abort();
326
332
 
327
333
  return;
328
334
  }
335
+
329
336
  detach = subscribeThreadState({
330
337
  connect: connectThreadObserver(this.connectionOptions),
331
338
  matches: (event) =>
@@ -347,6 +354,7 @@ export class OwnerDaemonSdk extends ReviewDaemonSdk {
347
354
 
348
355
  return;
349
356
  }
357
+
350
358
  if (receipt.outcome) finish({ ...accepted, outcome: receipt.outcome, state });
351
359
  },
352
360
  onError: (error) => {