@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.
- package/package.json +3 -3
- package/src/agent-tools.ts +6 -0
- package/src/api.ts +65 -8
- package/src/client-reliability.test.ts +2 -0
- package/src/client.ts +25 -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 +23 -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 +353 -24
- package/src/thread-review.test.ts +4 -0
- package/src/thread-review.ts +16 -4
- package/src/thread-subscription.test.ts +3 -0
- package/src/thread-subscription.ts +9 -0
- package/src/validate.test.ts +11 -0
- package/src/validate.ts +5 -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.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.
|
|
48
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
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",
|
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
|
@@ -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({
|
|
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
|
}
|