@cueloop/daemon 0.1.0-alpha.59 → 0.1.0-alpha.60

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/src/herdr-pane.ts CHANGED
@@ -32,17 +32,20 @@ export interface HerdrTabPersistence {
32
32
  */
33
33
  export function openHerdrPane(options: OpenHerdrPaneOptions): HerdrTabHandle | null {
34
34
  const { sessionId, cwd, binPath, label } = options;
35
+
35
36
  try {
36
37
  const created = Bun.spawnSync(
37
38
  [binPath, "tab", "create", "--cwd", cwd, "--label", label, "--focus"],
38
39
  { stdout: "pipe", stderr: "ignore", timeout: HERDR_SPAWN_TIMEOUT_MS },
39
40
  );
41
+
40
42
  if (created.exitCode !== 0) return null;
41
43
  const parsed = JSON.parse(created.stdout.toString()) as {
42
44
  result?: { root_pane?: { pane_id?: string; tab_id?: string } };
43
45
  };
44
46
  const paneId = parsed.result?.root_pane?.pane_id;
45
47
  const tabId = parsed.result?.root_pane?.tab_id;
48
+
46
49
  if (!paneId || !tabId) return null;
47
50
  // A fresh tab hosts a plain shell, so the review is typed in like a human.
48
51
  const typed = Bun.spawnSync([binPath, "pane", "send-text", paneId, `cueloop ${sessionId}`], {
@@ -50,12 +53,14 @@ export function openHerdrPane(options: OpenHerdrPaneOptions): HerdrTabHandle | n
50
53
  stderr: "ignore",
51
54
  timeout: HERDR_SPAWN_TIMEOUT_MS,
52
55
  });
56
+
53
57
  if (typed.exitCode !== 0) return null;
54
58
  const entered = Bun.spawnSync([binPath, "pane", "send-keys", paneId, "enter"], {
55
59
  stdout: "ignore",
56
60
  stderr: "ignore",
57
61
  timeout: HERDR_SPAWN_TIMEOUT_MS,
58
62
  });
63
+
59
64
  return entered.exitCode === 0 ? { tabId, paneId } : null;
60
65
  } catch {
61
66
  return null;
@@ -70,8 +75,10 @@ function herdrPaneAlive(binPath: string, paneId: string): boolean {
70
75
  stderr: "ignore",
71
76
  timeout: HERDR_SPAWN_TIMEOUT_MS,
72
77
  });
78
+
73
79
  if (got.exitCode !== 0) return false;
74
80
  const parsed = JSON.parse(got.stdout.toString()) as { result?: { pane?: unknown } };
81
+
75
82
  return parsed.result?.pane != null;
76
83
  } catch {
77
84
  return false;
@@ -102,10 +109,13 @@ export async function openHerdrPaneForReview(
102
109
  env: HerdrEnv = process.env,
103
110
  ): Promise<void> {
104
111
  const herdr = detectHerdr(env);
112
+
105
113
  if (!herdr) return;
106
114
  const recorded = await recallHerdrTab(persistence, session.id);
115
+
107
116
  if (recorded && herdrPaneAlive(herdr.binPath, recorded.paneId)) {
108
117
  focusHerdrTab(herdr.binPath, recorded.tabId);
118
+
109
119
  return;
110
120
  }
111
121
  const opened = openHerdrPane({
@@ -114,6 +124,7 @@ export async function openHerdrPaneForReview(
114
124
  binPath: herdr.binPath,
115
125
  label: session.artifact.meta.title ?? session.id,
116
126
  });
127
+
117
128
  if (opened) await rememberHerdrTab(persistence, session.id, opened);
118
129
  }
119
130
 
@@ -15,6 +15,7 @@ function makeStub(name: string, splitOk = true): { binPath: string; logPath: str
15
15
  const splitResult = splitOk
16
16
  ? `printf '{"result":{"pane":{"pane_id":"w1:p9"}}}'`
17
17
  : `printf '{"result":{}}'`;
18
+
18
19
  writeFileSync(
19
20
  binPath,
20
21
  `#!/bin/sh
@@ -24,11 +25,13 @@ exit 0
24
25
  `,
25
26
  );
26
27
  chmodSync(binPath, 0o755);
28
+
27
29
  return { binPath, logPath };
28
30
  }
29
31
 
30
32
  function readLines(logPath: string): string[] {
31
33
  if (!existsSync(logPath)) return [];
34
+
32
35
  return readFileSync(logPath, "utf8").split("\n").filter(Boolean);
33
36
  }
34
37
 
@@ -35,6 +35,7 @@ export function launchHarnessInSplit(
35
35
  env: HerdrEnv = process.env,
36
36
  ): boolean {
37
37
  const herdr = detectHerdr(env);
38
+
38
39
  if (!herdr) return false;
39
40
  try {
40
41
  const split = Bun.spawnSync(
@@ -52,15 +53,18 @@ export function launchHarnessInSplit(
52
53
  ],
53
54
  { stdout: "pipe", stderr: "ignore", timeout: HERDR_SPAWN_TIMEOUT_MS },
54
55
  );
56
+
55
57
  if (split.exitCode !== 0) return false;
56
58
  const parsed = JSON.parse(split.stdout.toString()) as {
57
59
  result?: { pane?: { pane_id?: string } };
58
60
  };
59
61
  const paneId = parsed.result?.pane?.pane_id;
62
+
60
63
  if (!paneId) return false;
61
64
  sendText(herdr.binPath, paneId, options.command);
62
65
  sendKeys(herdr.binPath, paneId, "enter");
63
66
  if (options.seedText) sendText(herdr.binPath, paneId, options.seedText);
67
+
64
68
  return true;
65
69
  } catch {
66
70
  return false;
@@ -43,6 +43,7 @@ describe("HerdrTabStore", () => {
43
43
  test("delete removes the handle", () => {
44
44
  // Arrange
45
45
  const store = new HerdrTabStore(home);
46
+
46
47
  store.set("ses_1", { tabId: "w1:t2", paneId: "w1:p2" });
47
48
 
48
49
  // Act
@@ -36,6 +36,7 @@ export class HerdrTabStore {
36
36
  private load(): void {
37
37
  try {
38
38
  const parsed: unknown = JSON.parse(readFileSync(this.path, "utf8"));
39
+
39
40
  if (typeof parsed !== "object" || parsed === null) return;
40
41
  for (const [sessionId, value] of Object.entries(parsed)) {
41
42
  if (isHerdrTabHandle(value))
@@ -48,6 +49,7 @@ export class HerdrTabStore {
48
49
 
49
50
  private persist(): void {
50
51
  const tempPath = this.path + ".tmp";
52
+
51
53
  writeFileSync(tempPath, JSON.stringify(Object.fromEntries(this.tabs), null, 2));
52
54
  renameSync(tempPath, this.path);
53
55
  }
package/src/main.ts CHANGED
@@ -7,6 +7,7 @@ const idleExitMs = process.env.CUELOOP_IDLE_EXIT_MS
7
7
  : undefined;
8
8
  const server = new DaemonServer({ idleExitMs });
9
9
  const path = server.start();
10
+
10
11
  if (path === null) {
11
12
  // another daemon already owns this home; the client will attach to it
12
13
  console.log("cueloop daemon already running for this home");
package/src/protocol.ts CHANGED
@@ -31,8 +31,10 @@ export class LineBuffer {
31
31
  this.buffered += chunk;
32
32
  for (;;) {
33
33
  const newlineIndex = this.buffered.indexOf("\n");
34
+
34
35
  if (newlineIndex === -1) return;
35
36
  const line = this.buffered.slice(0, newlineIndex).trim();
37
+
36
38
  this.buffered = this.buffered.slice(newlineIndex + 1);
37
39
  if (line) onLine(line);
38
40
  }
@@ -58,11 +60,14 @@ export class BackpressureWriter {
58
60
 
59
61
  write(data: string): void {
60
62
  const bytes = Buffer.from(data, "utf8");
63
+
61
64
  if (this.pending) {
62
65
  this.pending = Buffer.concat([this.pending, bytes]);
66
+
63
67
  return;
64
68
  }
65
69
  const written = Math.max(0, this.socket.write(bytes));
70
+
66
71
  if (written < bytes.length) this.pending = bytes.subarray(written);
67
72
  }
68
73
 
@@ -70,8 +75,10 @@ export class BackpressureWriter {
70
75
  drain(): void {
71
76
  if (!this.pending) return;
72
77
  const tail = this.pending;
78
+
73
79
  this.pending = null;
74
80
  const written = Math.max(0, this.socket.write(tail));
81
+
75
82
  if (written < tail.length) this.pending = tail.subarray(written);
76
83
  }
77
84
  }
@@ -57,6 +57,7 @@ describe("resolveCleanupPeriodDays", () => {
57
57
  test("honors [cleanup] period_days", () => {
58
58
  // Arrange
59
59
  const path = join(tempDir("cueloop-retention-"), "config.toml");
60
+
60
61
  writeFileSync(path, "[cleanup]\nperiod_days = 7\n");
61
62
 
62
63
  // Act
@@ -69,6 +70,7 @@ describe("resolveCleanupPeriodDays", () => {
69
70
  test("falls back to the default when the value is not a number", () => {
70
71
  // Arrange
71
72
  const path = join(tempDir("cueloop-retention-"), "config.toml");
73
+
72
74
  writeFileSync(path, '[cleanup]\nperiod_days = "soon"\n');
73
75
 
74
76
  // Act
@@ -111,6 +113,7 @@ describe("pruneExpiredSessions", () => {
111
113
  // Arrange
112
114
  const home = tempDir("cueloop-retention-home-");
113
115
  const store = new SessionStore(home);
116
+
114
117
  store.upsert(session("ses_old", daysAgo(40)));
115
118
  store.upsert(session("ses_new", daysAgo(2)));
116
119
 
@@ -127,6 +130,7 @@ describe("pruneExpiredSessions", () => {
127
130
  // Arrange
128
131
  const home = tempDir("cueloop-retention-home-");
129
132
  const store = new SessionStore(home);
133
+
130
134
  store.upsert(session("ses_active", daysAgo(400), "pending"));
131
135
 
132
136
  // Act
@@ -141,6 +145,7 @@ describe("pruneExpiredSessions", () => {
141
145
  // Arrange
142
146
  const home = tempDir("cueloop-retention-home-");
143
147
  const store = new SessionStore(home);
148
+
144
149
  store.upsert(session("ses_old", daysAgo(999)));
145
150
 
146
151
  // Act
@@ -157,12 +162,15 @@ describe("pruneExpiredReports", () => {
157
162
  // Arrange
158
163
  const home = tempDir("cueloop-retention-home-");
159
164
  const directory = reportsDir(home);
165
+
160
166
  mkdirSync(directory, { recursive: true });
161
167
  const latest = join(directory, LATEST_REPORT_FILENAME);
162
168
  const stale = join(directory, "refine-2026-06-01T00-00-00-000Z.md");
163
169
  const fresh = join(directory, "refine-2026-08-25T00-00-00-000Z.md");
170
+
164
171
  for (const path of [latest, stale, fresh]) writeFileSync(path, "report");
165
172
  const staleSeconds = (NOW_MS - 60 * DAY_MS) / 1000;
173
+
166
174
  utimesSync(stale, staleSeconds, staleSeconds);
167
175
 
168
176
  // Act
package/src/retention.ts CHANGED
@@ -22,28 +22,35 @@ export type RefineState = v.InferOutput<typeof RefineStateSchema>;
22
22
 
23
23
  export function parseRefineState(raw: unknown): Map<string, string> {
24
24
  const parsed = v.safeParse(RefineStateSchema, raw);
25
+
25
26
  if (!parsed.success) return new Map();
27
+
26
28
  return new Map(Object.entries(parsed.output.analyzed));
27
29
  }
28
30
 
29
31
  function userConfigPath(env: NodeJS.ProcessEnv): string {
30
32
  const explicit = env.CUELOOP_CONFIG;
33
+
31
34
  if (explicit && explicit.trim()) return explicit;
32
35
  const xdgConfigHome = env.XDG_CONFIG_HOME;
33
36
  const base = xdgConfigHome && xdgConfigHome.trim() ? xdgConfigHome : join(homedir(), ".config");
37
+
34
38
  return join(base, "cueloop", "config.toml");
35
39
  }
36
40
 
37
41
  function readConfiguredPeriodDays(path: string): number | undefined {
38
42
  let raw: unknown;
43
+
39
44
  try {
40
45
  raw = Bun.TOML.parse(readFileSync(path, "utf8"));
41
46
  } catch {
42
47
  return undefined;
43
48
  }
44
49
  const parsed = v.safeParse(CleanupConfigSchema, raw);
50
+
45
51
  if (!parsed.success) return undefined;
46
52
  const value = parsed.output.cleanup?.period_days;
53
+
47
54
  return value !== undefined && Number.isFinite(value) && value >= 0 ? value : undefined;
48
55
  }
49
56
 
@@ -54,7 +61,9 @@ export function resolveCleanupPeriodDays(env: NodeJS.ProcessEnv = process.env):
54
61
  export function isExpired(createdAt: string, periodDays: number, nowMs: number): boolean {
55
62
  if (periodDays <= 0) return false;
56
63
  const createdMs = Date.parse(createdAt);
64
+
57
65
  if (Number.isNaN(createdMs)) return false;
66
+
58
67
  return nowMs - createdMs > periodDays * MILLISECONDS_PER_DAY;
59
68
  }
60
69
 
@@ -65,17 +74,20 @@ export function pruneExpiredSessions(
65
74
  ): string[] {
66
75
  if (periodDays <= 0) return [];
67
76
  const pruned: string[] = [];
77
+
68
78
  for (const session of store.list()) {
69
79
  if (session.status !== "resolved") continue;
70
80
  if (isExpired(session.createdAt, periodDays, nowMs) && store.delete(session.id)) {
71
81
  pruned.push(session.id);
72
82
  }
73
83
  }
84
+
74
85
  return pruned;
75
86
  }
76
87
 
77
88
  export function timestampedReportFilename(nowMs: number): string {
78
89
  const stamp = new Date(nowMs).toISOString().replace(/[:.]/g, "-");
90
+
79
91
  return `${TIMESTAMPED_REPORT_PREFIX}${stamp}.md`;
80
92
  }
81
93
 
@@ -86,6 +98,7 @@ export function pruneExpiredReports(
86
98
  ): string[] {
87
99
  if (periodDays <= 0) return [];
88
100
  let entries: string[];
101
+
89
102
  try {
90
103
  entries = readdirSync(reportsDirectory);
91
104
  } catch {
@@ -93,14 +106,17 @@ export function pruneExpiredReports(
93
106
  }
94
107
  const cutoffMs = nowMs - periodDays * MILLISECONDS_PER_DAY;
95
108
  const pruned: string[] = [];
109
+
96
110
  for (const entry of entries) {
97
111
  if (entry === LATEST_REPORT_FILENAME) continue;
98
112
  if (!entry.startsWith(TIMESTAMPED_REPORT_PREFIX)) continue;
99
113
  const path = join(reportsDirectory, entry);
114
+
100
115
  if (statSync(path).mtimeMs < cutoffMs) {
101
116
  rmSync(path, { force: true });
102
117
  pruned.push(entry);
103
118
  }
104
119
  }
120
+
105
121
  return pruned;
106
122
  }
@@ -29,12 +29,14 @@ afterEach(() => {
29
29
 
30
30
  function sh(args: string[], cwd: string): void {
31
31
  const gitResult = Bun.spawnSync(args, { cwd, stdout: "ignore", stderr: "ignore" });
32
+
32
33
  if (gitResult.exitCode !== 0) throw new Error(`${args.join(" ")} failed`);
33
34
  }
34
35
 
35
36
  describe("resolveWorkspace", () => {
36
37
  test("a git repo resolves to its root and branch", async () => {
37
38
  const repo = mkdtempSync(join(tmpdir(), "cueloop-ws-"));
39
+
38
40
  try {
39
41
  // Arrange
40
42
  sh(["git", "init", "-q", "-b", "main"], repo);
@@ -75,6 +77,7 @@ describe("openReview", () => {
75
77
 
76
78
  // Assert
77
79
  const session = review.session;
80
+
78
81
  expect(session.status).toBe("pending");
79
82
  expect(session.artifact.type).toBe("plan");
80
83
  expect(session.artifact.meta.agent).toBe("test-agent");
@@ -170,6 +173,7 @@ describe("openReview", () => {
170
173
 
171
174
  // Assert
172
175
  const notes = review.session.annotations;
176
+
173
177
  expect(notes.length).toBe(2);
174
178
  expect(notes.every((annotation) => annotation.kind === "note")).toBe(true);
175
179
  expect(notes.map((annotation) => annotation.anchor.quote)).toEqual(["src/a.ts", "src/b.ts"]);
@@ -296,6 +300,7 @@ describe("awaitResolve: the adapter wake seam (session id only)", () => {
296
300
  test("returns the stored verdict immediately when the session already resolved", async () => {
297
301
  // Arrange - a detached waiter that only attaches after the human decided
298
302
  const review = await openReview(client, { type: "plan", content: PLAN, cwd: home });
303
+
299
304
  await client.sessionResolve(review.id, "request_changes", "One stage only.");
300
305
 
301
306
  // Act
@@ -332,8 +337,10 @@ describe("awaitResolve: the held wait keeps the daemon alive", () => {
332
337
  idleExitMs: 30,
333
338
  onIdleExit: () => idleExits++,
334
339
  });
340
+
335
341
  idleServer.start();
336
342
  const waiterClient = await DaemonClient.connect({ home: idleHome });
343
+
337
344
  try {
338
345
  const review = await openReview(waiterClient, { type: "plan", content: PLAN, cwd: idleHome });
339
346
  const waiting = awaitResolve(waiterClient, review.id, { pollMs: 40 });
@@ -347,6 +354,7 @@ describe("awaitResolve: the held wait keeps the daemon alive", () => {
347
354
 
348
355
  // Act - the human returns a verdict; the parked wait collects it
349
356
  const resolver = await DaemonClient.connect({ home: idleHome });
357
+
350
358
  await resolver.sessionResolve(review.id, "approve", "Good to go.");
351
359
  resolver.close();
352
360
  const verdict = await waiting;
package/src/review.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import {
11
+ isMarkdownArtifact,
11
12
  newAnnotationId,
12
13
  type ArtifactType,
13
14
  type DiffFileContents,
@@ -25,7 +26,9 @@ async function git(args: string[], cwd: string): Promise<string | null> {
25
26
  try {
26
27
  const proc = Bun.spawn(["git", ...args], { cwd, stdout: "pipe", stderr: "ignore" });
27
28
  const out = await new Response(proc.stdout).text();
29
+
28
30
  if ((await proc.exited) !== 0) return null;
31
+
29
32
  return out.trim();
30
33
  } catch {
31
34
  return null;
@@ -36,11 +39,13 @@ async function git(args: string[], cwd: string): Promise<string | null> {
36
39
  export async function resolveWorkspace(cwd = process.cwd()): Promise<WorkspaceKey> {
37
40
  const repoRoot = (await git(["rev-parse", "--show-toplevel"], cwd)) ?? cwd;
38
41
  const branch = (await git(["rev-parse", "--abbrev-ref", "HEAD"], cwd)) ?? "detached";
42
+
39
43
  return { repoRoot, branch };
40
44
  }
41
45
 
42
46
  function firstHeading(markdown: string): string | undefined {
43
47
  const headingMatch = markdown.match(/^#\s+(.+)$/m);
48
+
44
49
  return headingMatch?.[1]?.trim();
45
50
  }
46
51
 
@@ -63,7 +68,7 @@ export interface OpenReviewOptions {
63
68
  * the artifact so hunk curation produces an exactly applyable patch.
64
69
  */
65
70
  files?: DiffFileContents[];
66
- /** Defaults to the plan's first markdown heading; diffs get no derived title. */
71
+ /** Defaults to a markdown artifact's first heading (plan, reply); diffs get no derived title. */
67
72
  title?: string;
68
73
  /**
69
74
  * Per-file agent notes for diff sessions: the submitting agent's own
@@ -123,8 +128,10 @@ function raceAbort<T>(
123
128
  if (!signal) return promise;
124
129
  if (signal.aborted) {
125
130
  promise.catch(() => {});
131
+
126
132
  return Promise.resolve(ABORTED);
127
133
  }
134
+
128
135
  return new Promise<T | typeof ABORTED>((resolve, reject) => {
129
136
  const onAbort = () => {
130
137
  // The daemon request keeps running until the client closes; swallow its
@@ -132,6 +139,7 @@ function raceAbort<T>(
132
139
  promise.catch(() => {});
133
140
  resolve(ABORTED);
134
141
  };
142
+
135
143
  signal.addEventListener("abort", onAbort, { once: true });
136
144
  promise.then(
137
145
  (value) => {
@@ -164,20 +172,26 @@ export class ReviewHandle {
164
172
  */
165
173
  async awaitVerdict(options: AwaitVerdictOptions): Promise<VerdictOutcome | "pending"> {
166
174
  const { timeoutMs, pollMs, onProgress, signal } = options;
175
+
167
176
  if (pollMs === undefined && onProgress === undefined && signal === undefined) {
168
177
  const resolved = await this.client.sessionWait(this.session.id, timeoutMs);
178
+
169
179
  return resolved === null ? "pending" : outcome(resolved);
170
180
  }
171
181
  const chunkMs = pollMs ?? 10_000;
172
182
  const deadline = Number.isFinite(timeoutMs) ? Date.now() + timeoutMs : undefined;
183
+
173
184
  for (;;) {
174
185
  const budget = deadline === undefined ? chunkMs : Math.min(chunkMs, deadline - Date.now());
186
+
175
187
  if (budget <= 0 || signal?.aborted) return "pending";
176
188
  const resolved = await raceAbort(this.client.sessionWait(this.session.id, budget), signal);
189
+
177
190
  if (resolved === ABORTED) return "pending";
178
191
  if (resolved !== null) return outcome(resolved);
179
192
  // Still pending after this chunk: re-read to surface reviewer progress.
180
193
  const current = await raceAbort(this.client.sessionGet(this.session.id), signal);
194
+
181
195
  if (current === ABORTED) return "pending";
182
196
  onProgress?.(current);
183
197
  }
@@ -213,9 +227,11 @@ export async function awaitResolve(
213
227
  ): Promise<VerdictOutcome | null> {
214
228
  const chunkMs = options.pollMs ?? 30_000;
215
229
  const { signal } = options;
230
+
216
231
  for (;;) {
217
232
  if (signal?.aborted) return null;
218
233
  const resolved = await raceAbort(client.sessionWait(sessionId, chunkMs), signal);
234
+
219
235
  if (resolved === ABORTED) return null;
220
236
  if (resolved !== null) return outcome(resolved);
221
237
  }
@@ -228,17 +244,21 @@ export async function openReview(
228
244
  ): Promise<ReviewHandle> {
229
245
  const cwd = options.cwd ?? process.cwd();
230
246
  const workspace = options.workspace ?? (await resolveWorkspace(cwd));
247
+
231
248
  // Resubmits from the same agent session become revisions, not new sessions.
232
249
  if (options.agentSessionId !== undefined) {
233
250
  const existing = (await client.sessionList()).find(
234
251
  (candidate) => candidate.artifact.meta.agentSessionId === options.agentSessionId,
235
252
  );
253
+
236
254
  if (existing !== undefined) {
237
255
  let revised = await client.sessionSubmitRevision(existing.id, options.content);
256
+
238
257
  if (options.notes?.length) {
239
258
  await attachNotes(client, revised.id, options.notes);
240
259
  revised = await client.sessionGet(revised.id);
241
260
  }
261
+
242
262
  return new ReviewHandle(client, revised);
243
263
  }
244
264
  }
@@ -253,13 +273,17 @@ export async function openReview(
253
273
  prototypePath: options.prototypePath,
254
274
  pr: options.pr,
255
275
  herdrPane: options.herdrPane,
256
- title: options.title ?? (options.type === "plan" ? firstHeading(options.content) : undefined),
276
+ title:
277
+ options.title ??
278
+ (isMarkdownArtifact(options.type) ? firstHeading(options.content) : undefined),
257
279
  cwd,
258
280
  },
259
281
  });
282
+
260
283
  if (options.notes?.length) {
261
284
  await attachNotes(client, session.id, options.notes);
262
285
  session = await client.sessionGet(session.id);
263
286
  }
287
+
264
288
  return new ReviewHandle(client, session);
265
289
  }
@@ -44,6 +44,7 @@ describe("socket round-trip", () => {
44
44
  body: "More detail please.",
45
45
  });
46
46
  const wait = client.sessionWait(session.id, 5_000);
47
+
47
48
  await client.sessionResolve(session.id, "request_changes", "Expand it.");
48
49
  const resolved = (await wait)!;
49
50
 
@@ -65,11 +66,13 @@ describe("socket round-trip", () => {
65
66
  // Arrange
66
67
  const observer = await DaemonClient.connect({ home });
67
68
  const seen: string[] = [];
69
+
68
70
  observer.onEvent((event) => seen.push(event.event));
69
71
  await observer.subscribe();
70
72
 
71
73
  // Act
72
74
  const session = await client.sessionCreate(WS, PLAN);
75
+
73
76
  await client.sessionResolve(session.id, "approve", "");
74
77
 
75
78
  // Assert
@@ -107,6 +110,7 @@ describe("socket round-trip", () => {
107
110
  client.request("toString", {}),
108
111
  client.request("constructor", {}),
109
112
  ]);
113
+
110
114
  for (const result of prototypeMethods) {
111
115
  expect(result.status).toBe("rejected");
112
116
  expect(((result as PromiseRejectedResult).reason as DaemonClientError).code).toBe(
@@ -115,6 +119,7 @@ describe("socket round-trip", () => {
115
119
  }
116
120
  // the daemon is still fully alive afterwards
117
121
  const session = await client.sessionCreate(WS, PLAN);
122
+
118
123
  expect((await client.sessionGet(session.id)).id).toBe(session.id);
119
124
  });
120
125
 
@@ -136,6 +141,7 @@ describe("socket round-trip", () => {
136
141
  // Given a shared session with one local note
137
142
  const session = await client.sessionCreate(WS, PLAN);
138
143
  const anchor = { quote: "Body text", prefix: "", suffix: "." };
144
+
139
145
  await client.sessionSetShareId(session.id, "p_abc123xy");
140
146
  await client.sessionAnnotate(session.id, { id: "a1", kind: "comment", anchor, body: "mine" });
141
147
  expect((await client.sessionGet(session.id)).shareId).toBe("p_abc123xy");
@@ -174,6 +180,7 @@ describe("socket round-trip", () => {
174
180
  test("mergeShared validates each annotation at the socket boundary", async () => {
175
181
  // Given a session
176
182
  const session = await client.sessionCreate(WS, PLAN);
183
+
177
184
  try {
178
185
  // When a malformed annotation is merged
179
186
  // @ts-expect-error intentionally malformed annotation (empty id, missing fields)