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

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