@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/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) => {
@@ -59,6 +59,7 @@ describe("session.refreshDiff", () => {
59
59
  });
60
60
 
61
61
  if (init.exitCode !== 0) throw new Error(init.stderr.toString());
62
+
62
63
  writeFileSync(join(repo, "a.ts"), "export const a = 2;\n");
63
64
  const first = await core.repoDiff(repo);
64
65
  const workspace = await resolveWorkspace(repo);
@@ -81,6 +82,7 @@ describe("session.refreshDiff", () => {
81
82
  });
82
83
 
83
84
  if (describe.exitCode !== 0) throw new Error(describe.stderr.toString());
85
+
84
86
  const result = await core.sessionRefreshDiff(session.id);
85
87
  const current = core.sessionGet(session.id);
86
88
 
@@ -100,6 +102,7 @@ describe("session.refreshDiff", () => {
100
102
  });
101
103
 
102
104
  if (init.exitCode !== 0) throw new Error(init.stderr.toString());
105
+
103
106
  writeFileSync(join(repo, "a.ts"), "export const a = 2;\n");
104
107
  const first = await core.repoDiff(repo);
105
108
  const workspace = await resolveWorkspace(repo);
@@ -339,6 +342,7 @@ describe("the fs watcher drives hot-reload", () => {
339
342
  });
340
343
 
341
344
  if (init.exitCode !== 0) throw new Error(init.stderr.toString());
345
+
342
346
  const session = await core.workbenchSession(repo);
343
347
 
344
348
  expect(session.artifact.meta.vcs).toBe("jj");
@@ -18,6 +18,7 @@ test("startup reconciliation watches a directory whose creation event was missed
18
18
  const change = Promise.withResolvers<void>();
19
19
  const watcher = new DiffWatcher(() => {
20
20
  refreshes++;
21
+
21
22
  if (refreshes === 1) startup.resolve();
22
23
  else change.resolve();
23
24
  });
@@ -91,6 +92,7 @@ test("replacing a watch during reconciliation stops the old directory traversal"
91
92
  const watcher = new DiffWatcher(() => {});
92
93
  const spy = spyOn(directoryReads, "readdir").mockImplementation((path) => {
93
94
  scanned.push(String(path));
95
+
94
96
  if (path === first) {
95
97
  scanStarted.resolve();
96
98
 
@@ -38,6 +38,7 @@ function ignoredDirectories(repoRoot: string): Set<string> {
38
38
  );
39
39
 
40
40
  if (!result || result.exitCode !== 0) return ignored;
41
+
41
42
  for (const line of result.stdout.toString().split("\n")) {
42
43
  const relative = line.trim().replace(/\/$/, "");
43
44
 
@@ -77,9 +78,11 @@ function resolveGitDirs(repoRoot: string): { gitDir: string; commonDir: string }
77
78
  const result = spawnGit(["rev-parse", "--absolute-git-dir", "--git-common-dir"], repoRoot);
78
79
 
79
80
  if (!result || result.exitCode !== 0) return null;
81
+
80
82
  const [gitDir, commonRaw] = result.stdout.toString().trim().split("\n");
81
83
 
82
84
  if (!gitDir) return null;
85
+
83
86
  const commonDir = commonRaw
84
87
  ? isAbsolute(commonRaw)
85
88
  ? commonRaw
@@ -117,10 +120,12 @@ export class DiffWatcher {
117
120
 
118
121
  if (existing) {
119
122
  existing.sessionIds.add(sessionId);
123
+
120
124
  if (vcs === "jj") this.startJjPoll(existing, repoRoot, sessionId);
121
125
 
122
126
  return;
123
127
  }
128
+
124
129
  const repoWatch: RepoWatch = {
125
130
  handles: [],
126
131
  sessionIds: new Set([sessionId]),
@@ -133,6 +138,7 @@ export class DiffWatcher {
133
138
  };
134
139
 
135
140
  this.repoWatches.set(repoRoot, repoWatch);
141
+
136
142
  if (vcs === "jj") this.startJjPoll(repoWatch, repoRoot, sessionId);
137
143
 
138
144
  // one watcher per non-ignored working-tree directory; a tracked-file change re-captures
@@ -157,6 +163,7 @@ export class DiffWatcher {
157
163
  this.watchInto(repoWatch.handles, join(gitDirs.commonDir, "refs"), { recursive: true }, () =>
158
164
  this.scheduleRepoRefresh(repoRoot),
159
165
  );
166
+
160
167
  if (gitDirs.commonDir !== gitDirs.gitDir)
161
168
  this.watchInto(repoWatch.handles, gitDirs.commonDir, { recursive: false }, (filename) => {
162
169
  if (isRefChange(filename)) this.scheduleRepoRefresh(repoRoot);
@@ -169,7 +176,9 @@ export class DiffWatcher {
169
176
 
170
177
  private startJjPoll(repoWatch: RepoWatch, repoRoot: string, sessionId: string): void {
171
178
  repoWatch.jjSessionIds.add(sessionId);
179
+
172
180
  if (repoWatch.jjPoll !== null) return;
181
+
173
182
  repoWatch.jjPoll = setInterval(() => this.scheduleRepoRefresh(repoRoot), 2000);
174
183
  }
175
184
 
@@ -183,7 +192,9 @@ export class DiffWatcher {
183
192
  const repoWatch = this.repoWatches.get(repoRoot);
184
193
 
185
194
  if (!repoWatch) return false;
195
+
186
196
  if (repoWatch.watchedDirs.has(dir)) return true;
197
+
187
198
  if (ALWAYS_IGNORED_DIRS.has(basename(dir)) || repoWatch.ignored.has(dir)) return true;
188
199
 
189
200
  const opened = this.watchInto(repoWatch.handles, dir, { recursive: false }, (filename) => {
@@ -193,6 +204,7 @@ export class DiffWatcher {
193
204
  });
194
205
 
195
206
  if (!opened) return false;
207
+
196
208
  repoWatch.watchedDirs.add(dir);
197
209
 
198
210
  let entries: Dirent[];
@@ -223,8 +235,11 @@ export class DiffWatcher {
223
235
  // the path is already gone (a transient temp dir); nothing to watch
224
236
  return;
225
237
  }
238
+
226
239
  if (ALWAYS_IGNORED_DIRS.has(basename(path))) return;
240
+
227
241
  if (isGitIgnored(repoRoot, path)) return;
242
+
228
243
  this.watchTree(repoRoot, path);
229
244
  }
230
245
 
@@ -233,11 +248,13 @@ export class DiffWatcher {
233
248
  const repoWatch = this.repoWatches.get(repoRoot);
234
249
 
235
250
  if (!repoWatch) return;
251
+
236
252
  if (!repoWatch.watchedDirs.has(dir)) {
237
253
  this.extendWatch(repoRoot, dir);
238
254
 
239
255
  return;
240
256
  }
257
+
241
258
  let entries: Dirent[];
242
259
 
243
260
  try {
@@ -245,9 +262,12 @@ export class DiffWatcher {
245
262
  } catch {
246
263
  return;
247
264
  }
265
+
248
266
  if (this.repoWatches.get(repoRoot) !== repoWatch) return;
267
+
249
268
  for (const entry of entries) {
250
269
  if (this.repoWatches.get(repoRoot) !== repoWatch) return;
270
+
251
271
  if (entry.isDirectory() && !entry.isSymbolicLink())
252
272
  // eslint-disable-next-line no-await-in-loop
253
273
  await this.reconcileWatchTree(repoRoot, join(dir, entry.name));
@@ -285,15 +305,21 @@ export class DiffWatcher {
285
305
  const repoWatch = this.repoWatches.get(repoRoot);
286
306
 
287
307
  if (!repoWatch) return;
308
+
288
309
  repoWatch.sessionIds.delete(sessionId);
289
310
  repoWatch.jjSessionIds.delete(sessionId);
311
+
290
312
  if (repoWatch.jjSessionIds.size === 0 && repoWatch.jjPoll !== null) {
291
313
  clearInterval(repoWatch.jjPoll);
292
314
  repoWatch.jjPoll = null;
293
315
  }
316
+
294
317
  if (repoWatch.sessionIds.size > 0) return;
318
+
295
319
  if (repoWatch.debounce !== null) clearTimeout(repoWatch.debounce);
320
+
296
321
  if (repoWatch.jjPoll !== null) clearInterval(repoWatch.jjPoll);
322
+
297
323
  for (const handle of repoWatch.handles) handle.close();
298
324
  this.repoWatches.delete(repoRoot);
299
325
  }
@@ -302,14 +328,19 @@ export class DiffWatcher {
302
328
  const repoWatch = this.repoWatches.get(repoRoot);
303
329
 
304
330
  if (!repoWatch) return;
331
+
305
332
  if (repoWatch.debounce !== null) clearTimeout(repoWatch.debounce);
333
+
306
334
  repoWatch.debounce = setTimeout(async () => {
307
335
  repoWatch.debounce = null;
336
+
308
337
  if (repoWatch.reconcilePending) {
309
338
  repoWatch.reconcilePending = false;
310
339
  await this.reconcileWatchTree(repoRoot, repoRoot);
311
340
  }
341
+
312
342
  if (this.repoWatches.get(repoRoot) !== repoWatch) return;
343
+
313
344
  this.onRepoChange(repoRoot);
314
345
  }, DIFF_REFRESH_DEBOUNCE_MS);
315
346
  }
@@ -318,7 +349,9 @@ export class DiffWatcher {
318
349
  close(): void {
319
350
  for (const repoWatch of this.repoWatches.values()) {
320
351
  if (repoWatch.debounce !== null) clearTimeout(repoWatch.debounce);
352
+
321
353
  if (repoWatch.jjPoll !== null) clearInterval(repoWatch.jjPoll);
354
+
322
355
  for (const handle of repoWatch.handles) handle.close();
323
356
  }
324
357
  this.repoWatches.clear();
@@ -28,6 +28,7 @@ const reviewConversation = Effect.gen(function* () {
28
28
 
29
29
  if (completed.outcome !== "completed")
30
30
  throw new Error(`SDK example agent finished: ${completed.outcome}`);
31
+
31
32
  const comments = yield* sdk.comments.list(thread.id);
32
33
 
33
34
  if (comments[0])
@@ -36,6 +37,7 @@ const reviewConversation = Effect.gen(function* () {
36
37
  commentId: comments[0].id,
37
38
  body: "The explanation is clear.",
38
39
  });
40
+
39
41
  yield* sdk.sessions.sendMessage({
40
42
  threadId: thread.id,
41
43
  operationId: sdk.ids.operation(randomUUID()),
package/src/gh.ts CHANGED
@@ -26,6 +26,7 @@ export async function prRefs(pr: string): Promise<PullRequestRefs | null> {
26
26
  const { code, stdout } = await gh(["pr", "view", pr, "--json", "baseRefOid,headRefOid"]);
27
27
 
28
28
  if (code !== 0) return null;
29
+
29
30
  let input: unknown;
30
31
 
31
32
  try {
@@ -60,12 +61,15 @@ export async function prSnapshot(
60
61
  const before = await readRefs(pr);
61
62
 
62
63
  if (before === null) return null;
64
+
63
65
  const patch = await readDiff(pr);
64
66
 
65
67
  if (patch === null) return null;
68
+
66
69
  const after = await readRefs(pr);
67
70
 
68
71
  if (after === null) return null;
72
+
69
73
  if (before.baseSha === after.baseSha && before.headSha === after.headSha)
70
74
  return { patch, ...after };
71
75
  }
@@ -65,6 +65,7 @@ export class GhosttyThreadSurfaceStore {
65
65
  const previous = this.surfaces.get(threadId);
66
66
 
67
67
  if (previous?.opening) return false;
68
+
68
69
  this.surfaces.set(threadId, { ...previous, opening: true });
69
70
 
70
71
  try {
@@ -83,8 +84,10 @@ export class GhosttyThreadSurfaceStore {
83
84
  const record = this.surfaces.get(threadId);
84
85
 
85
86
  if (!record?.opening) return;
87
+
86
88
  if (record.terminalId) this.surfaces.set(threadId, { terminalId: record.terminalId });
87
89
  else this.surfaces.delete(threadId);
90
+
88
91
  this.persist();
89
92
  }
90
93
 
@@ -54,6 +54,7 @@ function persistence(initial: GhosttyThreadSurfaceHandle | null = null) {
54
54
  const port: GhosttyThreadSurfacePersistence = {
55
55
  ghosttyClaimThreadSurface: async () => {
56
56
  if (claimed) return false;
57
+
57
58
  claimed = true;
58
59
 
59
60
  return true;
@@ -316,6 +317,7 @@ describe("openGhosttyThreadSurface", () => {
316
317
  const store: GhosttyThreadSurfacePersistence = {
317
318
  ghosttyClaimThreadSurface: async () => {
318
319
  if (claimed) return false;
320
+
319
321
  claimed = true;
320
322
 
321
323
  return true;
@@ -139,10 +139,12 @@ export function resolveCueloopLaunchCommand(
139
139
 
140
140
  for (const directory of path?.split(delimiter) ?? []) {
141
141
  if (!isAbsolute(directory)) continue;
142
+
142
143
  const candidate = join(directory, "cueloop");
143
144
 
144
145
  try {
145
146
  accessSync(candidate, constants.X_OK);
147
+
146
148
  if (statSync(candidate).isFile()) return shellQuote(candidate);
147
149
  } catch {
148
150
  // A PATH entry without an executable cannot launch the Thread.
@@ -161,6 +163,7 @@ export function supportsGhosttyAppleScript(version: string | null): boolean {
161
163
  const parsed = version?.match(/^(\d+)\.(\d+)\.(\d+)/);
162
164
 
163
165
  if (!parsed) return false;
166
+
164
167
  const major = Number(parsed[1]);
165
168
  const minor = Number(parsed[2]);
166
169
 
@@ -183,7 +186,9 @@ export async function openGhosttyThreadSurface(
183
186
  cueloopBinPath?: string | null,
184
187
  ): Promise<ThreadSurfaceOpenStatus> {
185
188
  if (platform !== "darwin" || !insideGhostty(env)) return "unavailable";
189
+
186
190
  if (mode === "none") return "disabled";
191
+
187
192
  if (!supportsGhosttyAppleScript(runAppleScript(binPath, VERSION_SCRIPT))) return "failed";
188
193
 
189
194
  try {
@@ -236,6 +241,7 @@ async function openClaimedGhosttyThreadSurface(
236
241
  const result = runAppleScript(binPath, FOCUS_SCRIPT, [recorded.terminalId]);
237
242
 
238
243
  if (result === "focused") return { status: "focused", retainClaim: false };
244
+
239
245
  if (result !== "closed") return { status: "failed", retainClaim: false };
240
246
  }
241
247
 
@@ -20,6 +20,7 @@ export function writeHarnessDiagnostic(
20
20
 
21
21
  try {
22
22
  mkdirSync(directory, { recursive: true, mode: 0o700 });
23
+
23
24
  if (existsSync(path) && statSync(path).size + Buffer.byteLength(record) > 256 * 1024)
24
25
  writeFileSync(path, record, { mode: 0o600 });
25
26
  else appendFileSync(path, record, { mode: 0o600 });