@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.
- package/package.json +3 -3
- package/src/agent-tools.ts +6 -0
- package/src/api.ts +63 -0
- package/src/client-reliability.test.ts +2 -0
- package/src/client.ts +24 -2
- package/src/curate.ts +2 -0
- package/src/daemon-sdk.ts +8 -0
- package/src/diff-hot-reload.test.ts +4 -0
- package/src/diff-watcher.test.ts +2 -0
- package/src/diff-watcher.ts +33 -0
- package/src/examples/daemon-sdk.ts +2 -0
- package/src/gh.ts +4 -0
- package/src/ghostty-thread-surface-store.ts +3 -0
- package/src/ghostty-thread-surface.test.ts +2 -0
- package/src/ghostty-thread-surface.ts +6 -0
- package/src/harness-diagnostics.ts +1 -0
- package/src/harness-handoff.test.ts +725 -0
- package/src/harness-handoff.ts +27 -0
- package/src/harness-state-store.ts +5 -0
- package/src/herdr-split.ts +5 -0
- package/src/herdr-thread-surface.ts +14 -0
- package/src/interruptible-wait.ts +3 -0
- package/src/jj-working-tree.ts +9 -0
- package/src/main.ts +1 -0
- package/src/operation-receipts.ts +6 -0
- package/src/pr-poller.ts +7 -0
- package/src/project-files.ts +4 -0
- package/src/protocol.ts +4 -0
- package/src/retention.ts +8 -0
- package/src/server.ts +36 -7
- package/src/share-blob.test.ts +2 -0
- package/src/single-instance.test.ts +3 -0
- package/src/store.ts +11 -0
- package/src/thread-agent-validation.ts +20 -0
- package/src/thread-agent.test.ts +14 -1
- package/src/thread-agent.ts +361 -25
- package/src/thread-review.test.ts +4 -0
- package/src/thread-review.ts +8 -0
- package/src/thread-subscription.test.ts +3 -0
- package/src/thread-subscription.ts +9 -0
- package/src/validate.test.ts +4 -0
- package/src/validate.ts +3 -0
- package/src/vcs-source.ts +6 -0
- 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.
|
|
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.
|
|
48
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
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",
|
package/src/agent-tools.ts
CHANGED
|
@@ -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({
|
|
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) => {
|