@cueloop/daemon 0.1.0-alpha.96 → 0.1.0-alpha.98

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueloop/daemon",
3
- "version": "0.1.0-alpha.96",
3
+ "version": "0.1.0-alpha.98",
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": {
@@ -39,8 +39,8 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@cueloop/extension-api": "0.1.0-alpha.96",
43
- "@cueloop/schema": "0.1.0-alpha.96",
42
+ "@cueloop/extension-api": "0.1.0-alpha.98",
43
+ "@cueloop/schema": "0.1.0-alpha.98",
44
44
  "@pierre/diffs": "^1.4.1",
45
45
  "effect": "^4.0.0-rc.117",
46
46
  "smol-toml": "^1.8.0",
@@ -0,0 +1,98 @@
1
+ import * as v from "valibot";
2
+ import { resolve } from "node:path";
3
+ import { MESSAGE_OUTCOMES, type Thread } from "@cueloop/schema";
4
+ import type { MethodName } from "./validate";
5
+
6
+ /** Tool arguments are validated before entering the owner's daemon API. */
7
+ export function parseAgentToolInput(name: string, serialized: string) {
8
+ const input: unknown = JSON.parse(serialized);
9
+
10
+ if (name === "reply_to_comment") {
11
+ const args = v.parse(
12
+ v.object({
13
+ id: v.string(),
14
+ commentId: v.string(),
15
+ body: v.pipe(v.string(), v.minLength(1), v.maxLength(32768)),
16
+ }),
17
+ input,
18
+ );
19
+
20
+ return { kind: "reply" as const, ...args };
21
+ }
22
+ if (name === "send_message") {
23
+ const args = v.parse(
24
+ v.object({
25
+ id: v.string(),
26
+ outcome: v.picklist(MESSAGE_OUTCOMES),
27
+ summary: v.optional(v.string(), ""),
28
+ }),
29
+ input,
30
+ );
31
+
32
+ return { kind: "api" as const, method: "session.sendMessage", params: args };
33
+ }
34
+ if (name !== "cueloop_api") throw new Error("Thread agent tool name is unavailable");
35
+ const args = v.parse(v.object({ method: v.string(), params: v.unknown() }), input);
36
+
37
+ return { kind: "api" as const, ...args };
38
+ }
39
+
40
+ const THREAD_AGENT_SESSION_METHODS = new Set<string>([
41
+ "session.get",
42
+ "session.wait",
43
+ "session.annotate",
44
+ "session.comment",
45
+ "session.removeAnnotation",
46
+ "session.setParticipantName",
47
+ "session.setWorkingCopy",
48
+ "session.cutBlock",
49
+ "session.navigate",
50
+ "session.branch",
51
+ "session.switch",
52
+ "session.label",
53
+ "session.restoreBlock",
54
+ "session.curate",
55
+ "session.setViewed",
56
+ "session.setTitle",
57
+ "session.projectFiles",
58
+ "session.fileContents",
59
+ "session.refreshDiff",
60
+ "session.sendMessage",
61
+ "session.submitRevision",
62
+ ] satisfies MethodName[]);
63
+ const THREAD_AGENT_REPO_METHODS = new Set<string>([
64
+ "repo.files",
65
+ "repo.fileContents",
66
+ "repo.changes",
67
+ "repo.diff",
68
+ ] satisfies MethodName[]);
69
+
70
+ /** Harness tools are bound to their originating Thread; model arguments cannot widen that scope. */
71
+ export function assertAgentToolScope(
72
+ thread: Thread,
73
+ input: ReturnType<typeof parseAgentToolInput>,
74
+ ): void {
75
+ if (input.kind === "reply") {
76
+ if (input.id !== thread.id) throw new Error("Thread agent tool cannot access another Thread");
77
+
78
+ return;
79
+ }
80
+ if (input.method === "session.list") return;
81
+ const params = v.parse(v.record(v.string(), v.unknown()), input.params);
82
+
83
+ if (THREAD_AGENT_SESSION_METHODS.has(input.method)) {
84
+ if (params.id !== thread.id) throw new Error("Thread agent tool cannot access another Thread");
85
+
86
+ return;
87
+ }
88
+ if (THREAD_AGENT_REPO_METHODS.has(input.method)) {
89
+ const { cwd } = v.parse(v.object({ cwd: v.string() }), params);
90
+
91
+ if (resolve(cwd) !== resolve(thread.workspace.repoRoot))
92
+ throw new Error("Thread agent tool cannot access another repository");
93
+
94
+ return;
95
+ }
96
+
97
+ throw new Error("Thread agent tool API method is unavailable");
98
+ }
@@ -0,0 +1,24 @@
1
+ import { expect, test } from "bun:test";
2
+ import { stepAgentTurn, type AgentTurn } from "./agent-turn";
3
+
4
+ test("an initialization stop settles before accepting a fresh turn", () => {
5
+ const starting = stepAgentTurn({ kind: "idle" }, "start");
6
+ const stopped = stepAgentTurn(starting, "stop");
7
+ const ready = stepAgentTurn(stopped, "ready");
8
+
9
+ expect(ready).toEqual({ kind: "idle" });
10
+ expect(stepAgentTurn(ready, "start")).toEqual({ kind: "starting", cancelled: false });
11
+ });
12
+
13
+ test("an active stop waits for completion before accepting another turn", () => {
14
+ const running: AgentTurn = { kind: "running" };
15
+ const stopped = stepAgentTurn(running, "stop");
16
+
17
+ expect(stopped).toEqual({ kind: "stopping" });
18
+ expect(stepAgentTurn(stopped, "start")).toEqual(stopped);
19
+ expect(stepAgentTurn(stopped, "ready")).toEqual(stopped);
20
+ expect(stepAgentTurn(stepAgentTurn(stopped, "finished"), "start")).toEqual({
21
+ kind: "starting",
22
+ cancelled: false,
23
+ });
24
+ });
@@ -0,0 +1,34 @@
1
+ /** A harness turn distinguishes initialization cancellation from a running stop request. */
2
+ export type AgentTurn =
3
+ | { kind: "idle" }
4
+ | { kind: "starting"; cancelled: boolean }
5
+ | { kind: "running" }
6
+ | { kind: "stopping" };
7
+
8
+ /** Lifecycle events are independent of the harness wire protocol. */
9
+ export type AgentTurnEvent = "start" | "ready" | "stop" | "finished";
10
+
11
+ /** Start is accepted only at idle; readiness consumes an initialization cancellation. */
12
+ export function stepAgentTurn(state: AgentTurn, event: AgentTurnEvent): AgentTurn {
13
+ switch (event) {
14
+ case "start":
15
+ return state.kind === "idle" ? { kind: "starting", cancelled: false } : state;
16
+ case "ready":
17
+ return state.kind === "starting"
18
+ ? state.cancelled
19
+ ? { kind: "idle" }
20
+ : { kind: "running" }
21
+ : state;
22
+ case "stop":
23
+ if (state.kind === "starting") return { kind: "starting", cancelled: true };
24
+
25
+ return state.kind === "running" ? { kind: "stopping" } : state;
26
+ case "finished":
27
+ return { kind: "idle" };
28
+ }
29
+ }
30
+
31
+ /** Pending stop requests suppress later tool and permission events from this turn. */
32
+ export function agentTurnCancelled(state: AgentTurn): boolean {
33
+ return state.kind === "stopping" || (state.kind === "starting" && state.cancelled);
34
+ }
@@ -18,6 +18,12 @@ export type DaemonRole = "owner" | "collaborator" | "agent";
18
18
  type Audience = "any" | "owner";
19
19
 
20
20
  const PRIMITIVE_ROLES = {
21
+ "agent.configure": "owner",
22
+ "agent.get": "owner",
23
+ "agent.prompt": "owner",
24
+ "agent.cancel": "owner",
25
+ "agent.comment": "owner",
26
+ "agent.permission": "owner",
21
27
  "daemon.ping": "any",
22
28
  "daemon.hello": "any",
23
29
  "daemon.shutdown": "owner",
@@ -22,3 +22,16 @@ describe("daemonSpawnCommand", () => {
22
22
  ).toEqual(["/path/to/bun", "run", "/repo/packages/daemon/src/main.ts"]);
23
23
  });
24
24
  });
25
+
26
+ test("published daemon starts itself unless the CLI supplies its real entry path", () => {
27
+ const published = "file:///project/node_modules/@cueloop/daemon/src/client.ts";
28
+
29
+ expect(daemonSpawnCommand("bun", published, false)).toEqual([
30
+ "bun",
31
+ "run",
32
+ "/project/node_modules/@cueloop/daemon/src/main.ts",
33
+ ]);
34
+ expect(
35
+ daemonSpawnCommand("bun", published, false, "/project/node_modules/cueloop/src/main.ts"),
36
+ ).toEqual(["bun", "run", "/project/node_modules/cueloop/src/main.ts", "daemon", "--autostart"]);
37
+ });
package/src/client.ts CHANGED
@@ -58,6 +58,8 @@ import {
58
58
  } from "./validate";
59
59
  import type { WorkingTreeDiff } from "./working-tree";
60
60
  import { DAEMON_VERSION } from "./version";
61
+ import { ThreadAgentSchema } from "./thread-agent-validation";
62
+ import type { AgentComment, ThreadAgentState } from "@cueloop/schema";
61
63
 
62
64
  export type { EventFrame } from "./protocol";
63
65
 
@@ -101,6 +103,8 @@ export interface ThreadClient {
101
103
  onEvent(listener: (event: EventFrame) => void): () => void;
102
104
  subscribe(): Promise<void>;
103
105
  sessionGet(id: string): Promise<Thread>;
106
+ /** Create a local owner Thread; shared clients omit this capability. */
107
+ sessionCreate?(workspace: WorkspaceKey, artifact: Artifact): Promise<Thread>;
104
108
  sessionList(filter?: { status?: "pending" | "resolved" }): Promise<Thread[]>;
105
109
  /** Add a comment; the primary annotate method. `sessionAnnotate` is the retained alias. */
106
110
  sessionComment(
@@ -458,6 +462,44 @@ export class DaemonClient implements ThreadClient {
458
462
  }
459
463
 
460
464
  // ── typed primitives ─────────────────────────────
465
+ /** Read the agent transcript without starting a model request. */
466
+ agentGet(id: string): Promise<ThreadAgentState> {
467
+ return this.request("agent.get", { id }, ThreadAgentSchema);
468
+ }
469
+ /** Submit a question and optional selected passage to the daemon-owned agent. */
470
+ agentPrompt(params: {
471
+ id: string;
472
+ text: string;
473
+ context?: string;
474
+ retry?: string;
475
+ }): Promise<ThreadAgentState> {
476
+ return this.request("agent.prompt", params, ThreadAgentSchema);
477
+ }
478
+ /** Cancel the active turn; the daemon retains partial output. */
479
+ agentCancel(id: string): Promise<ThreadAgentState> {
480
+ return this.request("agent.cancel", { id }, ThreadAgentSchema);
481
+ }
482
+ /** Save quote-primary feedback on a finalized agent answer. */
483
+ agentComment(params: { id: string; comment: AgentComment }): Promise<ThreadAgentState> {
484
+ return this.request("agent.comment", params, ThreadAgentSchema);
485
+ }
486
+ /** Configure only an option advertised by the current harness. */
487
+ agentConfigure(params: {
488
+ id: string;
489
+ configId?: string;
490
+ value?: string;
491
+ }): Promise<ThreadAgentState> {
492
+ return this.request("agent.configure", params, ThreadAgentSchema);
493
+ }
494
+
495
+ /** Select an option from the active agent permission card. */
496
+ agentPermission(params: {
497
+ id: string;
498
+ requestId: string;
499
+ optionId: string;
500
+ }): Promise<ThreadAgentState> {
501
+ return this.request("agent.permission", params, ThreadAgentSchema);
502
+ }
461
503
  ping(): Promise<{ pid: number }> {
462
504
  return this.request("daemon.ping", {}, PingResultSchema);
463
505
  }
@@ -731,7 +773,7 @@ export class DaemonClientError extends Error {
731
773
  }
732
774
 
733
775
  // A compiled binary re-execs `cueloop daemon --autostart` (idle-exits like main.ts,
734
- // unlike the never-exiting foreground daemon); from source, bun runs main.ts. In
776
+ // unlike the never-exiting foreground daemon); source also uses the CLI composition root. In
735
777
  // dev (CUELOOP_DEV_WATCH=1, source only) it runs under --watch so daemon-code edits
736
778
  // reload the daemon without a manual restart - the version handshake only catches
737
779
  // release upgrades, not same-version source changes.
@@ -739,14 +781,19 @@ export function daemonSpawnCommand(
739
781
  execPath: string,
740
782
  moduleUrl: string,
741
783
  devWatch = process.env.CUELOOP_DEV_WATCH === "1",
784
+ entry = process.env.CUELOOP_DAEMON_ENTRY,
742
785
  ): string[] {
743
786
  const compiled =
744
787
  moduleUrl.includes("$bunfs") || moduleUrl.includes("~BUN") || moduleUrl.includes("%7EBUN");
745
788
 
746
789
  if (compiled) return [execPath, "daemon", "--autostart"];
747
- const mainPath = new URL("./main.ts", moduleUrl).pathname;
790
+ const mainPath = entry ?? new URL("./main.ts", moduleUrl).pathname;
791
+
792
+ const args = entry ? ["daemon", "--autostart"] : [];
748
793
 
749
- return devWatch ? [execPath, "--watch", "run", mainPath] : [execPath, "run", mainPath];
794
+ return devWatch
795
+ ? [execPath, "--watch", "run", mainPath, ...args]
796
+ : [execPath, "run", mainPath, ...args];
750
797
  }
751
798
 
752
799
  function spawnDaemon(home: string): string {
@@ -0,0 +1,26 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { writeHarnessDiagnostic } from "./harness-diagnostics";
6
+
7
+ test("diagnostic logs are private and bounded across repeated notices", () => {
8
+ const directory = mkdtempSync(join(tmpdir(), "cueloop-diagnostics-"));
9
+ const path = join(directory, "thread.diagnostics.ndjson");
10
+
11
+ try {
12
+ for (let i = 0; i < 100; i++)
13
+ writeHarnessDiagnostic(directory, "thread", {
14
+ kind: "diagnostic",
15
+ severity: "error",
16
+ source: "protocol",
17
+ title: "Advisory",
18
+ text: "x".repeat(32768),
19
+ });
20
+ expect(statSync(path).size).toBeLessThanOrEqual(256 * 1024);
21
+ expect(statSync(path).mode & 0o777).toBe(0o600);
22
+ expect(JSON.parse(readFileSync(path, "utf8").trim().split("\n")[0]!).text).toHaveLength(16384);
23
+ } finally {
24
+ rmSync(directory, { recursive: true, force: true });
25
+ }
26
+ });
@@ -0,0 +1,30 @@
1
+ import { appendFileSync, existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { AgentHarnessDiagnostic } from "@cueloop/schema";
4
+
5
+ /** Store harness diagnostics in a private, bounded log, outside Thread history and model context. */
6
+ export function writeHarnessDiagnostic(
7
+ directory: string,
8
+ threadId: string,
9
+ diagnostic: AgentHarnessDiagnostic,
10
+ ): void {
11
+ const path = join(directory, `${encodeURIComponent(threadId)}.diagnostics.ndjson`);
12
+ const record =
13
+ JSON.stringify({
14
+ at: new Date().toISOString(),
15
+ ...diagnostic,
16
+ severity: diagnostic.severity.slice(0, 128),
17
+ title: diagnostic.title.slice(0, 512),
18
+ text: diagnostic.text.slice(0, 16384),
19
+ }) + "\n";
20
+
21
+ try {
22
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
23
+ if (existsSync(path) && statSync(path).size + Buffer.byteLength(record) > 256 * 1024)
24
+ writeFileSync(path, record, { mode: 0o600 });
25
+ else appendFileSync(path, record, { mode: 0o600 });
26
+ } catch {
27
+ // A diagnostic log failure must not change the agent turn outcome.
28
+ console.error("Thread agent diagnostic log could not be written");
29
+ }
30
+ }
package/src/server.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  statSync,
18
18
  writeFileSync,
19
19
  } from "node:fs";
20
+ import type { AgentHarnessTools } from "@cueloop/schema";
20
21
  import { DaemonCore, type DaemonEvent } from "./api";
21
22
  import { DaemonError } from "./errors";
22
23
  import { DEFAULT_ROLE, roleAllowsMethod, type DaemonRole } from "./capabilities";
@@ -30,7 +31,9 @@ import {
30
31
  } from "./protocol";
31
32
  import { cueloopHome, lockPath, ownerTokenPath, pidPath, socketPath } from "./paths";
32
33
  import { DAEMON_VERSION } from "./version";
33
- import { randomBytes } from "node:crypto";
34
+ import { randomUUID, randomBytes } from "node:crypto";
35
+ import { parseAgentToolInput, assertAgentToolScope } from "./agent-tools";
36
+ import { ThreadAgentManager, type ThreadAgentOptions } from "./thread-agent";
34
37
 
35
38
  interface Connection {
36
39
  /** The author a non-owner connection acts as, bound in the handshake. */
@@ -44,6 +47,8 @@ interface Connection {
44
47
  type MethodHandler = (connection: Connection, request: Request) => Response["result"];
45
48
 
46
49
  export interface DaemonOptions {
50
+ /** Explicit prototype configuration; commands cannot arrive from socket callers. */
51
+ threadAgent?: Pick<ThreadAgentOptions, "enabled" | "enabledForThread" | "adapter">;
47
52
  home?: string;
48
53
  /** Idle-exit delay; 0 disables (tests, foreground runs). */
49
54
  idleExitMs?: number;
@@ -61,6 +66,7 @@ const HELD_HOMES = new Set<string>();
61
66
  const LOCK_WAIT = new Int32Array(new SharedArrayBuffer(4));
62
67
 
63
68
  export class DaemonServer {
69
+ private readonly threadAgent: ThreadAgentManager;
64
70
  readonly core: DaemonCore;
65
71
  readonly home: string;
66
72
  private connections = new Set<Connection>();
@@ -80,6 +86,62 @@ export class DaemonServer {
80
86
  this.version = options.version ?? DAEMON_VERSION;
81
87
  mkdirSync(this.home, { recursive: true, mode: 0o700 });
82
88
  this.core = new DaemonCore(this.home);
89
+ this.threadAgent = new ThreadAgentManager({
90
+ home: this.home,
91
+ enabled: options.threadAgent?.enabled ?? false,
92
+ enabledForThread: options.threadAgent?.enabledForThread,
93
+ adapter: options.threadAgent?.adapter,
94
+ getThread: (id) => this.core.sessionGet(id),
95
+ tools: (thread): AgentHarnessTools => ({
96
+ definitions: [
97
+ {
98
+ name: "send_message",
99
+ description:
100
+ "Return the Thread and its comments to the main session using the existing Send message action.",
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: {
104
+ id: { type: "string" },
105
+ outcome: { type: "string", enum: ["comment", "approved", "changes_requested"] },
106
+ summary: { type: "string" },
107
+ },
108
+ required: ["id", "outcome"],
109
+ },
110
+ },
111
+ {
112
+ name: "reply_to_comment",
113
+ description:
114
+ "Append a reply to an existing comment discussion, preserving its quote and Changes origin.",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ id: { type: "string" },
119
+ commentId: { type: "string" },
120
+ body: { type: "string" },
121
+ },
122
+ required: ["id", "commentId", "body"],
123
+ },
124
+ },
125
+ {
126
+ name: "cueloop_api",
127
+ description:
128
+ "Call a cueloop API operation on this Thread or its repository. Use session.get to inspect this Thread. Global harness, sharing, deletion and Thread creation operations are unavailable. Parameters follow the cueloop socket API.",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: { method: { type: "string" }, params: { type: "object" } },
132
+ required: ["method", "params"],
133
+ },
134
+ },
135
+ ],
136
+ call: async (name, input) => this.callAgentTool(thread.id, name, input),
137
+ }),
138
+ onChange: (id) => {
139
+ for (const connection of this.connections) {
140
+ if (connection.subscribed && connection.role === "owner")
141
+ connection.write(JSON.stringify({ event: "agent.updated", sessionId: id }) + "\n");
142
+ }
143
+ },
144
+ });
83
145
  this.core.onEvent((event) => this.broadcast(event));
84
146
  }
85
147
 
@@ -230,6 +292,7 @@ export class DaemonServer {
230
292
  }
231
293
 
232
294
  stop(): void {
295
+ this.threadAgent.dispose();
233
296
  this.core.dispose();
234
297
  if (this.lockFd === null) return;
235
298
  this.server?.stop(true);
@@ -287,7 +350,81 @@ export class DaemonServer {
287
350
  }
288
351
  }
289
352
 
353
+ private async callAgentTool(threadId: string, name: string, input: string): Promise<string> {
354
+ const args = parseAgentToolInput(name, input);
355
+ const origin = this.core.sessionGet(threadId);
356
+ assertAgentToolScope(origin, args);
357
+ if (args.kind === "api" && args.method === "session.list") {
358
+ const { filter } = parseParams("session.list", args.params);
359
+
360
+ return JSON.stringify(!filter?.status || origin.status === filter.status ? [origin] : []);
361
+ }
362
+ if (args.kind === "reply") {
363
+ const thread = origin;
364
+ const comment = thread.annotations.find((entry) => entry.id === args.commentId);
365
+ if (!comment)
366
+ return JSON.stringify(this.threadAgent.reply(args.id, args.commentId, args.body));
367
+ const root =
368
+ thread.annotations.find((entry) => entry.id === (comment.replyTo ?? comment.id)) ?? comment;
369
+
370
+ return JSON.stringify(
371
+ this.core.sessionAnnotate(
372
+ args.id,
373
+ {
374
+ id: randomUUID(),
375
+ kind: "comment",
376
+ anchor: root.anchor,
377
+ target: root.target,
378
+ replyTo: root.id,
379
+ body: args.body,
380
+ author: "embedded-agent",
381
+ },
382
+ "Agent",
383
+ ),
384
+ );
385
+ }
386
+ const method = args.method;
387
+ if (!isKnownMethod(method)) throw new Error("Thread agent tool API method is unavailable");
388
+
389
+ return JSON.stringify(
390
+ await this.handlers[method](
391
+ { role: "owner", subscribed: false, write: () => {} },
392
+ { id: 0, method, params: args.params },
393
+ ),
394
+ );
395
+ }
396
+
290
397
  private readonly handlers: Record<MethodName, MethodHandler> = {
398
+ "agent.configure": (_connection, request) => {
399
+ const params = parseParams("agent.configure", request.params);
400
+ this.threadAgent.assertEnabled(params.id);
401
+ return this.threadAgent.configure(params);
402
+ },
403
+ "agent.get": (_connection, request) => {
404
+ const { id } = parseParams("agent.get", request.params);
405
+ this.threadAgent.assertEnabled(id);
406
+ return this.threadAgent.get(id);
407
+ },
408
+ "agent.prompt": (_connection, request) => {
409
+ const params = parseParams("agent.prompt", request.params);
410
+ this.threadAgent.assertEnabled(params.id);
411
+ return this.threadAgent.prompt(params);
412
+ },
413
+ "agent.cancel": (_connection, request) => {
414
+ const { id } = parseParams("agent.cancel", request.params);
415
+ this.threadAgent.assertEnabled(id);
416
+ return this.threadAgent.cancel(id);
417
+ },
418
+ "agent.comment": (_connection, request) => {
419
+ const params = parseParams("agent.comment", request.params);
420
+ this.threadAgent.assertEnabled(params.id);
421
+ return this.threadAgent.comment(params);
422
+ },
423
+ "agent.permission": (_connection, request) => {
424
+ const params = parseParams("agent.permission", request.params);
425
+ this.threadAgent.assertEnabled(params.id);
426
+ return this.threadAgent.permission(params);
427
+ },
291
428
  "daemon.ping": () => ({ pid: process.pid, version: this.version }),
292
429
  "daemon.hello": (connection, request) => {
293
430
  const params = parseParams("daemon.hello", request.params);
@@ -340,11 +477,17 @@ export class DaemonServer {
340
477
  "session.annotate": (_connection, request) => {
341
478
  const params = parseParams("session.annotate", request.params);
342
479
 
480
+ if (this.threadAgent.isReadOnly(params.id, params.annotation.id))
481
+ throw new Error("Thread agent submitted comments are read-only");
482
+
343
483
  return this.core.sessionAnnotate(params.id, params.annotation, params.authorName);
344
484
  },
345
485
  "session.comment": (_connection, request) => {
346
486
  const params = parseParams("session.comment", request.params);
347
487
 
488
+ if (this.threadAgent.isReadOnly(params.id, params.annotation.id))
489
+ throw new Error("Thread agent submitted comments are read-only");
490
+
348
491
  return this.core.sessionAnnotate(params.id, params.annotation, params.authorName);
349
492
  },
350
493
  "session.removeAnnotation": (connection, request) => {
@@ -358,6 +501,9 @@ export class DaemonServer {
358
501
  );
359
502
  }
360
503
 
504
+ if (this.threadAgent.isReadOnly(params.id, params.annotationId))
505
+ throw new Error("Thread agent submitted comments are read-only");
506
+
361
507
  return this.core.sessionRemoveAnnotation(
362
508
  params.id,
363
509
  params.annotationId,
@@ -475,7 +621,10 @@ export class DaemonServer {
475
621
  return this.core.sessionSetShares(params.id, params.shares);
476
622
  },
477
623
  "session.delete": (_connection, request) => {
478
- this.core.sessionDelete(parseParams("session.delete", request.params).id);
624
+ const { id } = parseParams("session.delete", request.params);
625
+
626
+ this.core.sessionDelete(id);
627
+ this.threadAgent.remove(id);
479
628
 
480
629
  return {};
481
630
  },