@cueloop/daemon 0.1.0-alpha.97 → 0.1.0-alpha.99

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.
@@ -0,0 +1,55 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { Effect } from "effect";
3
+ import { CueloopSdk } from "../daemon-sdk-effect";
4
+
5
+ const reviewConversation = Effect.gen(function* () {
6
+ const sdk = yield* CueloopSdk;
7
+ const thread = yield* sdk.threads.create({
8
+ workspace: { repoRoot: process.cwd(), branch: "main" },
9
+ artifact: { type: "plan", content: "Review this proposal.", meta: { title: "SDK example" } },
10
+ });
11
+
12
+ yield* sdk.threads.get(thread.id);
13
+ yield* sdk.comments.add({
14
+ threadId: thread.id,
15
+ annotation: {
16
+ id: "question",
17
+ kind: "comment",
18
+ body: "Explain the proposal",
19
+ anchor: { quote: "proposal", prefix: "Review this ", suffix: "." },
20
+ },
21
+ });
22
+ const accepted = yield* sdk.agents.prompt({
23
+ threadId: thread.id,
24
+ operationId: sdk.ids.operation(randomUUID()),
25
+ text: "Explain this Thread and its comment.",
26
+ });
27
+ const completed = yield* sdk.agents.wait(accepted, { timeoutMs: 300_000 });
28
+
29
+ if (completed.outcome !== "completed")
30
+ throw new Error(`SDK example agent finished: ${completed.outcome}`);
31
+ const comments = yield* sdk.comments.list(thread.id);
32
+
33
+ if (comments[0])
34
+ yield* sdk.comments.reply({
35
+ threadId: thread.id,
36
+ commentId: comments[0].id,
37
+ body: "The explanation is clear.",
38
+ });
39
+ yield* sdk.sessions.sendMessage({
40
+ threadId: thread.id,
41
+ operationId: sdk.ids.operation(randomUUID()),
42
+ outcome: "approved",
43
+ summary: "Review complete",
44
+ });
45
+
46
+ return thread.id;
47
+ });
48
+
49
+ if (import.meta.main) {
50
+ const threadId = await Effect.runPromise(
51
+ reviewConversation.pipe(Effect.provide(CueloopSdk.ownerLayer({ autostart: true }))),
52
+ );
53
+
54
+ console.log(`Thread approved: ${threadId}`);
55
+ }
@@ -164,6 +164,7 @@ describe("durable harness delivery", () => {
164
164
  harnessSessionId: "fake_1",
165
165
  });
166
166
  const sent = first.sessionSendMessage(thread.id, "approved", "Ready.");
167
+
167
168
  first.store.upsert({
168
169
  ...first.sessionGet(thread.id),
169
170
  createdAt: "2020-01-01T00:00:00.000Z",
@@ -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
+ }
@@ -0,0 +1,111 @@
1
+ import { expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { DaemonCore } from "./api";
6
+
7
+ test("send message operation receipts survive restart and reject changed payloads", () => {
8
+ const home = mkdtempSync(join(tmpdir(), "cueloop-operation-"));
9
+ let core = new DaemonCore(home);
10
+
11
+ try {
12
+ const thread = core.sessionCreate({
13
+ workspace: { repoRoot: home, branch: "main" },
14
+ artifact: { type: "plan", content: "Review this", meta: {} },
15
+ });
16
+ const accepted = core.sessionSendMessage(thread.id, "comment", "First", undefined, "op-first");
17
+ const message = structuredClone(accepted.message);
18
+
19
+ core.sessionSendMessage(thread.id, "comment", "Second", undefined, "op-second");
20
+ core.dispose();
21
+ core = new DaemonCore(home);
22
+ const replay = core.sessionSendMessage(thread.id, "comment", "First", undefined, "op-first");
23
+
24
+ expect(replay.message).toEqual(message);
25
+ expect(
26
+ core.sessionGet(thread.id).history?.entries.filter((entry) => entry.type === "message"),
27
+ ).toHaveLength(2);
28
+ expect(() =>
29
+ core.sessionSendMessage(thread.id, "comment", "Changed", undefined, "op-first"),
30
+ ).toThrow("Operation payload conflict");
31
+ } finally {
32
+ core.dispose();
33
+ rmSync(home, { recursive: true, force: true });
34
+ }
35
+ });
36
+
37
+ test("receipt capacity never evicts accepted IDs and share blobs omit private receipts", async () => {
38
+ const { packSessionBlob, unpackSessionBlob } = await import("./share-blob");
39
+ const home = mkdtempSync(join(tmpdir(), "cueloop-operation-capacity-"));
40
+ const core = new DaemonCore(home);
41
+
42
+ try {
43
+ const thread = core.sessionCreate({
44
+ workspace: { repoRoot: home, branch: "main" },
45
+ artifact: { type: "plan", content: "Review", meta: {} },
46
+ });
47
+ const first = structuredClone(
48
+ core.sessionSendMessage(thread.id, "comment", "First", undefined, "first").message,
49
+ );
50
+
51
+ for (let index = 1; index < 128; index++)
52
+ core.sessionSendMessage(
53
+ thread.id,
54
+ "comment",
55
+ `Message ${index}`,
56
+ undefined,
57
+ `operation-${index}`,
58
+ );
59
+ expect(() =>
60
+ core.sessionSendMessage(thread.id, "comment", "Overflow", undefined, "overflow"),
61
+ ).toThrow("Operation receipt capacity");
62
+ expect(
63
+ core.sessionSendMessage(thread.id, "comment", "First", undefined, "first").message,
64
+ ).toEqual(first);
65
+ expect(
66
+ unpackSessionBlob(packSessionBlob(core.sessionGet(thread.id))).messageOperations,
67
+ ).toBeUndefined();
68
+ } finally {
69
+ core.dispose();
70
+ rmSync(home, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ test("failed receipt persistence never acknowledges an in-memory-only review message", async () => {
75
+ const { mkdirSync, readdirSync } = await import("node:fs");
76
+ const { threadsDir } = await import("./paths");
77
+ const home = mkdtempSync(join(tmpdir(), "cueloop-operation-disk-failure-"));
78
+ const core = new DaemonCore(home);
79
+
80
+ try {
81
+ const thread = core.sessionCreate({
82
+ workspace: { repoRoot: home, branch: "main" },
83
+ artifact: { type: "plan", content: "Review", meta: {} },
84
+ });
85
+ const bucket = readdirSync(threadsDir(home))[0]!;
86
+ const tempPath = join(threadsDir(home), bucket, `${thread.id}.jsonl.tmp`);
87
+
88
+ mkdirSync(tempPath);
89
+ expect(() =>
90
+ core.sessionSendMessage(thread.id, "comment", "Once", undefined, "persist-once"),
91
+ ).toThrow();
92
+ expect(core.sessionGet(thread.id).messageOperations).toBeUndefined();
93
+ expect(core.sessionGet(thread.id).message).toBeNull();
94
+ rmSync(tempPath, { recursive: true });
95
+ const accepted = core.sessionSendMessage(
96
+ thread.id,
97
+ "comment",
98
+ "Once",
99
+ undefined,
100
+ "persist-once",
101
+ );
102
+
103
+ expect(accepted.messageOperations).toHaveLength(1);
104
+ expect(
105
+ core.sessionSendMessage(thread.id, "comment", "Once", undefined, "persist-once").message,
106
+ ).toEqual(accepted.message);
107
+ } finally {
108
+ core.dispose();
109
+ rmSync(home, { recursive: true, force: true });
110
+ }
111
+ });
@@ -0,0 +1,91 @@
1
+ import type { ThreadAgentState, OperationReceipt, AgentPromptRequest } from "@cueloop/schema";
2
+ import { createHash } from "node:crypto";
3
+ import { DaemonError } from "./errors";
4
+
5
+ /** Hash normalized payloads rather than persisting another copy of private input. */
6
+ export function operationFingerprint(
7
+ payload: readonly (string | null | readonly (readonly [string, string])[])[],
8
+ ): string {
9
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
10
+ }
11
+
12
+ /** A duplicate returns its original acceptance even after later operations or restart. */
13
+ export function findOperationReceipt<Result>(
14
+ receipts: readonly OperationReceipt<Result>[] | undefined,
15
+ operationId: string | undefined,
16
+ fingerprint: string,
17
+ ): OperationReceipt<Result> | undefined {
18
+ if (!operationId) return;
19
+ const receipt = receipts?.find((entry) => entry.operationId === operationId);
20
+
21
+ if (receipt && receipt.fingerprint !== fingerprint)
22
+ throw new DaemonError(
23
+ "operation_conflict",
24
+ "Operation payload conflict: use a new operation ID for changed input",
25
+ );
26
+ if (!receipt && (receipts?.length ?? 0) >= 128)
27
+ throw new DaemonError(
28
+ "operation_capacity",
29
+ "Operation receipt capacity: create a new Thread before submitting more operations",
30
+ );
31
+
32
+ return receipt;
33
+ }
34
+
35
+ /** Each accepted prompt freezes the original batch before it can start a harness turn. */
36
+ export function recordPromptOperation(
37
+ state: ThreadAgentState,
38
+ before: ThreadAgentState,
39
+ input: AgentPromptRequest,
40
+ ): void {
41
+ if (!input.operationId) return;
42
+ const previous = new Set(before.submissions?.map((entry) => entry.id));
43
+ const accepted = input.retry
44
+ ? [input.retry]
45
+ : (state.submissions ?? []).filter((entry) => !previous.has(entry.id)).map((entry) => entry.id);
46
+
47
+ if (!accepted.length) throw new DaemonError("invalid_params", "Agent operation requires input");
48
+ const fingerprint = promptOperationFingerprint(input);
49
+
50
+ (state.promptOperations ??= []).push({
51
+ operationId: input.operationId,
52
+ fingerprint,
53
+ result: accepted,
54
+ });
55
+ }
56
+
57
+ /** Completion is an immutable fact about acceptance, including across explicit retries. */
58
+ export function settlePromptOperations(state: ThreadAgentState): void {
59
+ for (const receipt of state.promptOperations ?? []) {
60
+ if (receipt.outcome || !receipt.result.length) continue;
61
+ const submissions = receipt.result.map((id) =>
62
+ state.submissions?.find((entry) => entry.id === id),
63
+ );
64
+
65
+ if (
66
+ submissions.some((entry) => !entry || entry.status === "queued" || entry.status === "running")
67
+ )
68
+ continue;
69
+ receipt.outcome = submissions.some((entry) => entry?.cancelled)
70
+ ? "cancelled"
71
+ : submissions.some((entry) => entry?.status === "failed")
72
+ ? "failed"
73
+ : "completed";
74
+ }
75
+ }
76
+
77
+ /** Prompt identity hashes only explicit input; a retry does not consume newly pending comments. */
78
+ export function findPromptOperationReceipt(
79
+ state: ThreadAgentState,
80
+ input: AgentPromptRequest,
81
+ ): OperationReceipt<string[]> | undefined {
82
+ return findOperationReceipt(
83
+ state.promptOperations,
84
+ input.operationId,
85
+ promptOperationFingerprint(input),
86
+ );
87
+ }
88
+
89
+ function promptOperationFingerprint(input: AgentPromptRequest): string {
90
+ return operationFingerprint([input.text, input.context ?? null, input.retry ?? null]);
91
+ }
package/src/pr-poller.ts CHANGED
@@ -29,6 +29,7 @@ export class PrReviewPoller {
29
29
  trackPr(sessionId: string, pr: string, reviewedRefs: PullRequestRefs | null = null): void {
30
30
  if (this.polls.has(sessionId)) return;
31
31
  const timer = setInterval(() => void this.refreshHead(sessionId), this.intervalMs);
32
+
32
33
  // the poll timer must not by itself keep the daemon alive against idle-exit
33
34
  timer.unref?.();
34
35
  this.polls.set(sessionId, { pr, lastRefs: reviewedRefs, timer });
@@ -0,0 +1,35 @@
1
+ import * as v from "valibot";
2
+
3
+ const ThreadIdSchema = v.pipe(v.string(), v.minLength(1), v.maxLength(128), v.brand("SdkThreadId"));
4
+ const OperationIdSchema = v.pipe(
5
+ v.string(),
6
+ v.minLength(1),
7
+ v.maxLength(128),
8
+ v.brand("SdkOperationId"),
9
+ );
10
+ const SubmissionIdSchema = v.pipe(
11
+ v.string(),
12
+ v.minLength(1),
13
+ v.maxLength(128),
14
+ v.brand("SdkSubmissionId"),
15
+ );
16
+
17
+ /** Thread, operation and submission IDs share a wire representation, not a domain type. */
18
+ export type SdkThreadId = v.InferOutput<typeof ThreadIdSchema>;
19
+ /** Operation IDs are caller-owned and persist across explicit retries. */
20
+ export type SdkOperationId = v.InferOutput<typeof OperationIdSchema>;
21
+ /** Submission IDs are minted by the daemon after accepting agent input. */
22
+ export type SdkSubmissionId = v.InferOutput<typeof SubmissionIdSchema>;
23
+
24
+ /** External Thread identities must be nonempty and at most 128 characters. */
25
+ export function createSdkThreadId(value: string): SdkThreadId {
26
+ return v.parse(ThreadIdSchema, value);
27
+ }
28
+ /** Caller operation identities are stable across retries and at most 128 characters. */
29
+ export function createSdkOperationId(value: string): SdkOperationId {
30
+ return v.parse(OperationIdSchema, value);
31
+ }
32
+ /** Submission identities are daemon-minted; parsing grants no capability. */
33
+ export function createSdkSubmissionId(value: string): SdkSubmissionId {
34
+ return v.parse(SubmissionIdSchema, value);
35
+ }
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,110 @@ 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
+
357
+ assertAgentToolScope(origin, args);
358
+ if (args.kind === "api" && args.method === "session.list") {
359
+ const { filter } = parseParams("session.list", args.params);
360
+
361
+ return JSON.stringify(!filter?.status || origin.status === filter.status ? [origin] : []);
362
+ }
363
+ if (args.kind === "reply") {
364
+ const thread = origin;
365
+ const comment = thread.annotations.find((entry) => entry.id === args.commentId);
366
+
367
+ if (!comment) return JSON.stringify(this.replyAgentComment(args));
368
+ const root =
369
+ thread.annotations.find((entry) => entry.id === (comment.replyTo ?? comment.id)) ?? comment;
370
+
371
+ return JSON.stringify(
372
+ this.core.sessionAnnotate(
373
+ args.id,
374
+ {
375
+ id: randomUUID(),
376
+ kind: "comment",
377
+ anchor: root.anchor,
378
+ target: root.target,
379
+ replyTo: root.id,
380
+ body: args.body,
381
+ author: "embedded-agent",
382
+ },
383
+ "Agent",
384
+ ),
385
+ );
386
+ }
387
+ const method = args.method;
388
+
389
+ if (!isKnownMethod(method)) throw new Error("Thread agent tool API method is unavailable");
390
+
391
+ return JSON.stringify(
392
+ await this.handlers[method](
393
+ { role: "owner", subscribed: false, write: () => {} },
394
+ { id: 0, method, params: args.params },
395
+ ),
396
+ );
397
+ }
398
+
399
+ private replyAgentComment(params: {
400
+ id: string;
401
+ commentId: string;
402
+ body: string;
403
+ }): ReturnType<ThreadAgentManager["reply"]> {
404
+ this.threadAgent.assertEnabled(params.id);
405
+
406
+ return this.threadAgent.reply(params.id, params.commentId, params.body);
407
+ }
408
+
290
409
  private readonly handlers: Record<MethodName, MethodHandler> = {
410
+ "agent.configure": (_connection, request) => {
411
+ const params = parseParams("agent.configure", request.params);
412
+
413
+ this.threadAgent.assertEnabled(params.id);
414
+
415
+ return this.threadAgent.configure(params);
416
+ },
417
+ "agent.get": (_connection, request) => {
418
+ const { id } = parseParams("agent.get", request.params);
419
+
420
+ this.threadAgent.assertEnabled(id);
421
+
422
+ return this.threadAgent.get(id);
423
+ },
424
+ "agent.prompt": (_connection, request) => {
425
+ const params = parseParams("agent.prompt", request.params);
426
+
427
+ this.threadAgent.assertEnabled(params.id);
428
+
429
+ return this.threadAgent.prompt(params);
430
+ },
431
+ "agent.cancel": (_connection, request) => {
432
+ const { id } = parseParams("agent.cancel", request.params);
433
+
434
+ this.threadAgent.assertEnabled(id);
435
+
436
+ return this.threadAgent.cancel(id);
437
+ },
438
+ "agent.reply": (_connection, request) => {
439
+ const params = parseParams("agent.reply", request.params);
440
+
441
+ return this.replyAgentComment(params);
442
+ },
443
+ "agent.comment": (_connection, request) => {
444
+ const params = parseParams("agent.comment", request.params);
445
+
446
+ this.threadAgent.assertEnabled(params.id);
447
+
448
+ return this.threadAgent.comment(params);
449
+ },
450
+ "agent.permission": (_connection, request) => {
451
+ const params = parseParams("agent.permission", request.params);
452
+
453
+ this.threadAgent.assertEnabled(params.id);
454
+
455
+ return this.threadAgent.permission(params);
456
+ },
291
457
  "daemon.ping": () => ({ pid: process.pid, version: this.version }),
292
458
  "daemon.hello": (connection, request) => {
293
459
  const params = parseParams("daemon.hello", request.params);
@@ -340,11 +506,17 @@ export class DaemonServer {
340
506
  "session.annotate": (_connection, request) => {
341
507
  const params = parseParams("session.annotate", request.params);
342
508
 
509
+ if (this.threadAgent.isReadOnly(params.id, params.annotation.id))
510
+ throw new Error("Thread agent submitted comments are read-only");
511
+
343
512
  return this.core.sessionAnnotate(params.id, params.annotation, params.authorName);
344
513
  },
345
514
  "session.comment": (_connection, request) => {
346
515
  const params = parseParams("session.comment", request.params);
347
516
 
517
+ if (this.threadAgent.isReadOnly(params.id, params.annotation.id))
518
+ throw new Error("Thread agent submitted comments are read-only");
519
+
348
520
  return this.core.sessionAnnotate(params.id, params.annotation, params.authorName);
349
521
  },
350
522
  "session.removeAnnotation": (connection, request) => {
@@ -358,6 +530,9 @@ export class DaemonServer {
358
530
  );
359
531
  }
360
532
 
533
+ if (this.threadAgent.isReadOnly(params.id, params.annotationId))
534
+ throw new Error("Thread agent submitted comments are read-only");
535
+
361
536
  return this.core.sessionRemoveAnnotation(
362
537
  params.id,
363
538
  params.annotationId,
@@ -475,7 +650,10 @@ export class DaemonServer {
475
650
  return this.core.sessionSetShares(params.id, params.shares);
476
651
  },
477
652
  "session.delete": (_connection, request) => {
478
- this.core.sessionDelete(parseParams("session.delete", request.params).id);
653
+ const { id } = parseParams("session.delete", request.params);
654
+
655
+ this.core.sessionDelete(id);
656
+ this.threadAgent.remove(id);
479
657
 
480
658
  return {};
481
659
  },
@@ -496,6 +674,7 @@ export class DaemonServer {
496
674
  params.outcome,
497
675
  params.summary,
498
676
  params.actionBodies,
677
+ params.operationId,
499
678
  );
500
679
  },
501
680
  "harness.bind": (_connection, request) => {
package/src/share-blob.ts CHANGED
@@ -25,7 +25,9 @@ export const DEFAULT_SHARE_PORT = 22;
25
25
 
26
26
  /** Serialise + compress a session for upload. */
27
27
  export function packSessionBlob(session: Thread): Buffer {
28
- return gzipSync(Buffer.from(JSON.stringify(session), "utf8"));
28
+ const { messageOperations: _privateReceipts, ...shared } = session;
29
+
30
+ return gzipSync(Buffer.from(JSON.stringify(shared), "utf8"));
29
31
  }
30
32
 
31
33
  /** Decompress + validate an uploaded blob, or throw a precise reason. */