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