@epoch-agent/core 4.0.0 → 4.1.0

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 (4) hide show
  1. package/README.md +31 -31
  2. package/dist/index.d.ts +3223 -4402
  3. package/dist/index.js +1073 -383
  4. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync, statSync, readdirSync, re
4
4
  import { platform, tmpdir, homedir, hostname } from 'os';
5
5
  import { join, resolve, dirname, relative, isAbsolute, extname, sep, basename, delimiter } from 'path';
6
6
  import { execFile, spawn, execSync, execFileSync } from 'child_process';
7
- import { createLogger, t, dbPath, resolveHomeDir, DEFAULT_MAX_TURNS, PROJECT_DIR_NAME, guardSpawn, detectBackend, EXEC_PATH_AS_NODE_ENV, isolate, createStreamDecoder, registerJob, JOB_OUTPUT_LIMIT, artifactsDir, suggestKey, misspelledKeyIssues, openDatabase, migrate, releaseDatabase, parseLenient, issueDetails, splitWords, unknownKeyIssues, checkpointsDir, expandPath, pluginsDir, projectHooksPath, projectClaudeSettingsPath, projectClaudeLocalSettingsPath, projectSettingsPath, projectLocalSettingsPath, projectPoliciesDir, trustedImportsPath, canProbeGit, budgetStatePath, isInWorkspace, UNLIMITED_TURNS, turnLimitOf, configPath, managedSettingsPath, listStoredSecretNames, maskApiKey, unsetSectionField, setSectionField, countCodePoints, headCodePoints, attachmentsDir, workspacesPath, samePath, lastSessionsPath, appearancePath, automationLogsDir, projectComplianceDir, projectSkillsDir, trustPath, projectCommandsDir, parseShellCommand, writeArtifact, sandboxModeForLevel, planEscalation, getSecretValues, envPath, checkDangerousCommand, checkObfuscation, shellSpawnArgs, confine, classifyFailure, keepHeadAndTail, SANDBOX_PERMISSIONS_PARAM, JUSTIFICATION_PARAM, widerTargets, normalizeForMatch, isReadOnlyCommand, CODE_EXEC_ROOTS, tailCodePoints } from '@epoch-agent/infra';
7
+ import { createLogger, t, dbPath, resolveHomeDir, DEFAULT_MAX_TURNS, PROJECT_DIR_NAME, guardSpawn, detectBackend, EXEC_PATH_AS_NODE_ENV, isolate, createStreamDecoder, registerJob, JOB_OUTPUT_LIMIT, artifactsDir, openDatabase, migrate, releaseDatabase, suggestKey, misspelledKeyIssues, parseLenient, issueDetails, splitWords, unknownKeyIssues, checkpointsDir, expandPath, pluginsDir, projectHooksPath, projectClaudeSettingsPath, projectClaudeLocalSettingsPath, projectSettingsPath, projectLocalSettingsPath, projectPoliciesDir, trustedImportsPath, canProbeGit, budgetStatePath, isInWorkspace, UNLIMITED_TURNS, turnLimitOf, configPath, managedSettingsPath, listStoredSecretNames, maskApiKey, unsetSectionField, setSectionField, countCodePoints, headCodePoints, attachmentsDir, workspacesPath, samePath, lastSessionsPath, appearancePath, automationLogsDir, projectComplianceDir, projectSkillsDir, trustPath, projectCommandsDir, parseShellCommand, writeArtifact, sandboxModeForLevel, planEscalation, getSecretValues, envPath, checkDangerousCommand, checkObfuscation, shellSpawnArgs, confine, classifyFailure, keepHeadAndTail, SANDBOX_PERMISSIONS_PARAM, JUSTIFICATION_PARAM, widerTargets, normalizeForMatch, isReadOnlyCommand, CODE_EXEC_ROOTS, tailCodePoints } from '@epoch-agent/infra';
8
8
  export { IS_WINDOWS, WINDOWS_HIDE_FLAGS, buildBwrapArgs, buildProfile, checkDangerousCommand, checkObfuscation, createLogger, detectBackend, getDefaultShell, getPythonCommand, isInWorkspace, isReadOnlyCommand, isolate, maskApiKey, probeBubblewrap, probeSeatbelt, complianceDir as resolveComplianceDir, policiesDir as resolvePoliciesDir, setLogLevel } from '@epoch-agent/infra';
9
9
  import { createRequire } from 'module';
10
10
  import { randomUUID, createHash, randomBytes } from 'crypto';
@@ -820,6 +820,7 @@ var COMMAND_TOOLS = /* @__PURE__ */ new Set([
820
820
  "delegate_task",
821
821
  "spawn_teammate",
822
822
  "interrupt_agent",
823
+ "close_agent",
823
824
  "followup_task",
824
825
  "run_code",
825
826
  "mcp_add",
@@ -831,6 +832,7 @@ var NON_SHELL_COMMAND_TOOLS = /* @__PURE__ */ new Set([
831
832
  "delegate_task",
832
833
  "spawn_teammate",
833
834
  "interrupt_agent",
835
+ "close_agent",
834
836
  "followup_task"
835
837
  ]);
836
838
  var CODE_EXEC_TOOLS = /* @__PURE__ */ new Set(["code_exec", "sandbox"]);
@@ -4430,6 +4432,12 @@ var AgentInbox = class {
4430
4432
  }
4431
4433
  return [lines.join("\n")];
4432
4434
  }
4435
+ drain() {
4436
+ const out = this.queue;
4437
+ this.queue = [];
4438
+ this.dropped = 0;
4439
+ return out;
4440
+ }
4433
4441
  get size() {
4434
4442
  return this.queue.length;
4435
4443
  }
@@ -4518,6 +4526,7 @@ var TeamTaskBoard = class {
4518
4526
  var DEFAULT_MAX_RUNNING_MEMBERS = 3;
4519
4527
  var DEFAULT_MAX_WOKEN_SESSIONS = 3;
4520
4528
  var WAKE_RAMP_MS = 6e4;
4529
+ var CLOSED_TOMBSTONE_LIMIT = 200;
4521
4530
  var TeamRegistry = class {
4522
4531
  constructor(inboxCapacity = DEFAULT_INBOX_CAPACITY, limits = {}) {
4523
4532
  this.inboxCapacity = inboxCapacity;
@@ -4538,6 +4547,8 @@ var TeamRegistry = class {
4538
4547
  limits;
4539
4548
  bridge;
4540
4549
  aborters = /* @__PURE__ */ new Map();
4550
+ closedSessions = /* @__PURE__ */ new Set();
4551
+ closedOrder = [];
4541
4552
  listeners = /* @__PURE__ */ new Set();
4542
4553
  tasks;
4543
4554
  onChange(cb) {
@@ -4617,6 +4628,94 @@ var TeamRegistry = class {
4617
4628
  list() {
4618
4629
  return [...this.members.values()].map((e) => e.info).sort((a, b) => a.startedAt - b.startedAt);
4619
4630
  }
4631
+ subtreeOf(rootSessionId) {
4632
+ return this.walkSubtree(rootSessionId).map((entry) => ({ ...entry.info }));
4633
+ }
4634
+ treeRootOf(sessionId) {
4635
+ const path = [sessionId];
4636
+ const index = /* @__PURE__ */ new Map([[sessionId, 0]]);
4637
+ let current = sessionId;
4638
+ for (; ; ) {
4639
+ const parent = this.bySession(current)?.parentSessionId;
4640
+ if (!parent) return current;
4641
+ const seenAt = index.get(parent);
4642
+ if (seenAt !== void 0) {
4643
+ return path.slice(seenAt).sort()[0] ?? current;
4644
+ }
4645
+ index.set(parent, path.length);
4646
+ path.push(parent);
4647
+ current = parent;
4648
+ }
4649
+ }
4650
+ sameTree(a, b) {
4651
+ return this.treeRootOf(a) === this.treeRootOf(b);
4652
+ }
4653
+ walkSubtree(rootSessionId) {
4654
+ const out = [];
4655
+ const seen = /* @__PURE__ */ new Set([rootSessionId]);
4656
+ const walk3 = (parentSessionId) => {
4657
+ for (const entry of this.members.values()) {
4658
+ if (entry.info.parentSessionId !== parentSessionId) continue;
4659
+ if (seen.has(entry.info.sessionId)) continue;
4660
+ seen.add(entry.info.sessionId);
4661
+ out.push(entry);
4662
+ walk3(entry.info.sessionId);
4663
+ }
4664
+ };
4665
+ walk3(rootSessionId);
4666
+ return out;
4667
+ }
4668
+ close(agentId) {
4669
+ const entry = this.members.get(agentId);
4670
+ if (!entry) {
4671
+ return {
4672
+ ok: false,
4673
+ reason: `${agentId} is not in the roster \u2014 use list_agents to see who is around`
4674
+ };
4675
+ }
4676
+ const previous = entry.info.status;
4677
+ const doomed = [entry, ...this.walkSubtree(entry.info.sessionId).filter((e) => e !== entry)];
4678
+ const descendants = [];
4679
+ const senders = [];
4680
+ let dropped = 0;
4681
+ for (const item of doomed) {
4682
+ if (item !== entry) descendants.push(item.info.agentId);
4683
+ for (const mail of item.inbox.drain()) {
4684
+ dropped += 1;
4685
+ if (!senders.includes(mail.from)) senders.push(mail.from);
4686
+ }
4687
+ const abort = this.aborters.get(item.info.agentId);
4688
+ if (abort) {
4689
+ this.aborters.delete(item.info.agentId);
4690
+ try {
4691
+ abort();
4692
+ } catch {
4693
+ }
4694
+ }
4695
+ this.members.delete(item.info.agentId);
4696
+ this.bySessionId.delete(item.info.sessionId);
4697
+ this.rememberClosed(item.info.sessionId);
4698
+ }
4699
+ this.notify();
4700
+ return {
4701
+ ok: true,
4702
+ previous,
4703
+ descendants,
4704
+ ...dropped > 0 ? { dropped: { count: dropped, from: senders } } : {}
4705
+ };
4706
+ }
4707
+ isClosed(sessionId) {
4708
+ return this.closedSessions.has(sessionId);
4709
+ }
4710
+ rememberClosed(sessionId) {
4711
+ if (this.closedSessions.has(sessionId)) return;
4712
+ this.closedSessions.add(sessionId);
4713
+ this.closedOrder.push(sessionId);
4714
+ while (this.closedOrder.length > CLOSED_TOMBSTONE_LIMIT) {
4715
+ const old = this.closedOrder.shift();
4716
+ if (old !== void 0) this.closedSessions.delete(old);
4717
+ }
4718
+ }
4620
4719
  inboxOf(agentId) {
4621
4720
  return this.members.get(agentId)?.inbox;
4622
4721
  }
@@ -4684,6 +4783,9 @@ var TeamRegistry = class {
4684
4783
  get memberLimit() {
4685
4784
  return this.limits.maxRunningMembers;
4686
4785
  }
4786
+ get wakeLimit() {
4787
+ return this.limits.maxWokenSessions;
4788
+ }
4687
4789
  availableMemberSlots(parentSessionId) {
4688
4790
  let running = 0;
4689
4791
  for (const entry of this.members.values()) {
@@ -4700,6 +4802,12 @@ var TeamRegistry = class {
4700
4802
  sessionTitle(sessionId) {
4701
4803
  return this.bridge?.title?.(sessionId);
4702
4804
  }
4805
+ sessionWorkDir(sessionId) {
4806
+ return this.bridge?.workDir?.(sessionId);
4807
+ }
4808
+ isSubagentSession(sessionId) {
4809
+ return this.bridge?.isSubagent?.(sessionId) ?? false;
4810
+ }
4703
4811
  sessionIds() {
4704
4812
  return [...this.sessions];
4705
4813
  }
@@ -4757,11 +4865,26 @@ var TeamRegistry = class {
4757
4865
  this.notify();
4758
4866
  return { ok: true, to: msg.to, status: entry.info.status };
4759
4867
  }
4868
+ const namedFor = this.bySessionId.get(msg.to);
4869
+ if (namedFor) {
4870
+ return {
4871
+ ok: false,
4872
+ to: msg.to,
4873
+ reason: `${msg.to} is the session of member ${namedFor}, not the member itself \u2014 address it by its name (${namedFor})`
4874
+ };
4875
+ }
4760
4876
  if (this.isSessionActive(msg.to)) {
4761
4877
  this.mainInbox(msg.to).push(msg);
4762
4878
  this.notify();
4763
4879
  return { ok: true, to: msg.to, status: "running" };
4764
4880
  }
4881
+ if (this.isClosed(msg.to)) {
4882
+ return {
4883
+ ok: false,
4884
+ to: msg.to,
4885
+ reason: `${msg.to} was closed with close_agent \u2014 it has no next turn, and it cannot be woken again. The message was not delivered. If that work still matters, dispatch a new member with spawn_teammate`
4886
+ };
4887
+ }
4765
4888
  if (this.sessionExists(msg.to)) {
4766
4889
  const senderIsMember = this.members.has(msg.from);
4767
4890
  return {
@@ -4798,6 +4921,7 @@ function createSendMessageTool(team) {
4798
4921
  `To \`${LEAD_AGENT_ID}\` (the session that dispatched you): if it is running it sees it on the following steps; if it is not, the message just queues in its inbox until it next runs (so you will not get a reply now).`,
4799
4922
  "To **another main session** (the sessionId you saw with list_agents --all): delivered only if it is running **at this moment**.",
4800
4923
  "If the recipient is not running (asleep / already finished) this fails immediately (except for lead, which queues) \u2014 in that case use followup_task to wake it for a run rather than waiting.",
4924
+ "Only members **on your own side of the roster** can be reached \u2014 the ones this session dispatched (when you are a member yourself: your teammates). Another session\u2019s member is refused even if you know its name. Another main session is not covered by that rule \u2014 see above.",
4801
4925
  "Use list_agents to see this session's members; add --all to see every session and member."
4802
4926
  ].join("\n"),
4803
4927
  parameters: {
@@ -4850,7 +4974,19 @@ function createSendMessageTool(team) {
4850
4974
  }
4851
4975
  const memberInfo = team.bySession(ctx.sessionId);
4852
4976
  const target = team.get(to);
4853
- const ownMember = target !== void 0 && target.parentSessionId === ctx.sessionId;
4977
+ if (target && !team.sameTree(ctx.sessionId, target.sessionId)) {
4978
+ return {
4979
+ success: false,
4980
+ output: "",
4981
+ error: {
4982
+ code: 7104,
4983
+ message: `${to} was dispatched by another session, so this session cannot message it`,
4984
+ suggestion: "The roster is shared across sessions, the right to reach into a member is not. list_agents shows the members this session dispatched; to reach that work, message the session that dispatched it, or write it into your own conclusion"
4985
+ },
4986
+ duration: Date.now() - start
4987
+ };
4988
+ }
4989
+ const ownMember = target !== void 0;
4854
4990
  const from = memberInfo ? memberInfo.agentId : ownMember ? LEAD_AGENT_ID : ctx.sessionId;
4855
4991
  const labelled = from === ctx.sessionId ? sessionSenderLabel(from, team.sessionTitle(from)) : "";
4856
4992
  const result = team.deliver(
@@ -4885,11 +5021,454 @@ function createSendMessageTool(team) {
4885
5021
  }
4886
5022
  };
4887
5023
  }
5024
+ var MAX_POST_CHARS = 2e4;
5025
+ var CHANNEL_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
5026
+ var MAX_CHANNEL_DESCRIPTION_BYTES = 512;
5027
+ var MAX_PAGE_LIMIT = 200;
5028
+ var MIGRATIONS = [
5029
+ {
5030
+ id: "board-initial",
5031
+ version: 1,
5032
+ description: "Discussion board: channels / threads / posts / subscriptions",
5033
+ up: (db) => {
5034
+ db.exec(`
5035
+ CREATE TABLE IF NOT EXISTS board_channels (
5036
+ id TEXT PRIMARY KEY,
5037
+ name TEXT NOT NULL UNIQUE,
5038
+ description TEXT,
5039
+ created_by TEXT NOT NULL,
5040
+ created_at INTEGER NOT NULL
5041
+ );
5042
+ CREATE TABLE IF NOT EXISTS board_threads (
5043
+ id TEXT PRIMARY KEY,
5044
+ channel TEXT NOT NULL,
5045
+ title TEXT NOT NULL,
5046
+ created_by TEXT NOT NULL,
5047
+ created_at INTEGER NOT NULL,
5048
+ last_post_at INTEGER NOT NULL
5049
+ );
5050
+ CREATE INDEX IF NOT EXISTS board_threads_channel ON board_threads(channel, last_post_at DESC);
5051
+ CREATE TABLE IF NOT EXISTS board_posts (
5052
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
5053
+ id TEXT NOT NULL UNIQUE,
5054
+ thread_id TEXT NOT NULL,
5055
+ author TEXT NOT NULL,
5056
+ text TEXT NOT NULL,
5057
+ created_at INTEGER NOT NULL
5058
+ );
5059
+ CREATE INDEX IF NOT EXISTS board_posts_thread ON board_posts(thread_id, seq);
5060
+ CREATE TABLE IF NOT EXISTS board_subscriptions (
5061
+ agent TEXT NOT NULL,
5062
+ target TEXT NOT NULL,
5063
+ last_seen_seq INTEGER NOT NULL DEFAULT 0,
5064
+ PRIMARY KEY (agent, target)
5065
+ );
5066
+ `);
5067
+ }
5068
+ },
5069
+ {
5070
+ id: "board-notices-and-request-id",
5071
+ version: 2,
5072
+ description: "One-time notifications (agents_to_notify) and idempotent posts",
5073
+ up: (db) => {
5074
+ db.exec(`
5075
+ ALTER TABLE board_posts ADD COLUMN request_id TEXT;
5076
+ CREATE UNIQUE INDEX IF NOT EXISTS board_posts_request
5077
+ ON board_posts(author, request_id) WHERE request_id IS NOT NULL;
5078
+ CREATE TABLE IF NOT EXISTS board_notices (
5079
+ agent TEXT NOT NULL,
5080
+ target TEXT NOT NULL,
5081
+ seq INTEGER NOT NULL,
5082
+ created_at INTEGER NOT NULL,
5083
+ PRIMARY KEY (agent, target, seq)
5084
+ );
5085
+ `);
5086
+ }
5087
+ }
5088
+ ];
5089
+ function targetKey(t83) {
5090
+ return t83.kind === "channel" ? `channel:${t83.name}` : `thread:${t83.id}`;
5091
+ }
5092
+ function encodeCursor(kind, payload) {
5093
+ return `v1:${kind}:${payload}`;
5094
+ }
5095
+ function decodeCursor(cursor, kind) {
5096
+ if (!cursor) return void 0;
5097
+ const parts = cursor.split(":");
5098
+ if (parts.length < 3 || parts[0] !== "v1" || parts[1] !== kind) return void 0;
5099
+ return parts.slice(2).join(":");
5100
+ }
5101
+ function decodeSeqCursor(cursor, kind) {
5102
+ const raw = decodeCursor(cursor, kind);
5103
+ if (raw === void 0) return 0;
5104
+ const n = Number.parseInt(raw, 10);
5105
+ return Number.isFinite(n) ? n : 0;
5106
+ }
5107
+ function charLength(s) {
5108
+ return [...s].length;
5109
+ }
5110
+ function sliceChars(s, offset, limit) {
5111
+ return [...s].slice(offset, offset + limit).join("");
5112
+ }
5113
+ function previewPostText(post, maxChars) {
5114
+ const nChars = charLength(post.text);
5115
+ const limit = Math.min(Math.max(Math.floor(maxChars), 1), MAX_POST_CHARS);
5116
+ if (nChars <= limit) return { text: post.text, truncated: false, nChars };
5117
+ return { text: sliceChars(post.text, 0, limit), truncated: true, nChars };
5118
+ }
5119
+ var BoardStore = class {
5120
+ path;
5121
+ db;
5122
+ constructor(homeDir) {
5123
+ this.path = dbPath(homeDir);
5124
+ this.db = openDatabase(this.path);
5125
+ migrate(this.db, "board", MIGRATIONS);
5126
+ }
5127
+ close() {
5128
+ releaseDatabase(this.path);
5129
+ }
5130
+ createChannel(args) {
5131
+ if (!CHANNEL_PATTERN.test(args.name)) {
5132
+ throw new Error(
5133
+ `Invalid channel name: ${args.name} (letters, digits, _ . - only, at most 64 characters)`
5134
+ );
5135
+ }
5136
+ if (args.description !== void 0) assertDescription(args.description);
5137
+ const existing = this.channel(args.name);
5138
+ if (existing) return existing;
5139
+ const row = {
5140
+ id: `ch-${randomUUID().slice(0, 8)}`,
5141
+ name: args.name,
5142
+ ...args.description ? { description: args.description } : {},
5143
+ createdBy: args.by,
5144
+ createdAt: Date.now()
5145
+ };
5146
+ this.db.prepare(
5147
+ `INSERT INTO board_channels (id, name, description, created_by, created_at)
5148
+ VALUES (?, ?, ?, ?, ?)`
5149
+ ).run(row.id, row.name, row.description ?? null, row.createdBy, row.createdAt);
5150
+ return row;
5151
+ }
5152
+ upsertChannel(args) {
5153
+ assertDescription(args.description);
5154
+ const existing = this.channel(args.name);
5155
+ if (!existing) return this.createChannel(args);
5156
+ this.db.prepare(`UPDATE board_channels SET description = ? WHERE name = ?`).run(args.description, args.name);
5157
+ return { ...existing, description: args.description };
5158
+ }
5159
+ channel(name) {
5160
+ const row = this.db.prepare(
5161
+ `SELECT id, name, description, created_by, created_at FROM board_channels WHERE name = ?`
5162
+ ).get(name);
5163
+ return row ? toChannel(row) : void 0;
5164
+ }
5165
+ listChannels(opts = {}) {
5166
+ const limit = pageLimit(opts.limit, 100);
5167
+ const after = decodeCursor(opts.cursor, "ch");
5168
+ const rows = this.db.prepare(
5169
+ `SELECT id, name, description, created_by, created_at FROM board_channels
5170
+ WHERE (? IS NULL OR LOWER(name) LIKE LOWER(?))
5171
+ AND (? IS NULL OR name > ?)
5172
+ ORDER BY name ASC LIMIT ?`
5173
+ ).all(
5174
+ opts.query ?? null,
5175
+ opts.query ? `%${opts.query}%` : null,
5176
+ after ?? null,
5177
+ after ?? null,
5178
+ limit + 1
5179
+ );
5180
+ return toPage(rows.map(toChannel), limit, (c) => encodeCursor("ch", c.name));
5181
+ }
5182
+ listThreads(channel, opts = {}) {
5183
+ const limit = pageLimit(opts.limit, 20);
5184
+ const after = decodeCursor(opts.cursor, "thr");
5185
+ const cur = after ? splitThreadCursor(after) : void 0;
5186
+ const atTs = cur?.[0] ?? null;
5187
+ const atId = cur?.[1] ?? null;
5188
+ const rows = this.db.prepare(
5189
+ `SELECT t.id, t.channel, t.title, t.created_by, t.created_at, t.last_post_at,
5190
+ (SELECT COUNT(*) FROM board_posts p WHERE p.thread_id = t.id) AS posts
5191
+ FROM board_threads t
5192
+ WHERE t.channel = ?
5193
+ AND (? IS NULL OR t.last_post_at < ? OR (t.last_post_at = ? AND t.id < ?))
5194
+ ORDER BY t.last_post_at DESC, t.id DESC LIMIT ?`
5195
+ ).all(channel, atTs, atTs, atTs, atId, limit + 1);
5196
+ return toPage(rows.map(toThread), limit, (t83) => encodeCursor("thr", `${t83.lastPostAt}:${t83.id}`));
5197
+ }
5198
+ post(args) {
5199
+ return this.write({ ...args, notify: true, oneShot: args.notify });
5200
+ }
5201
+ append(args) {
5202
+ return this.write({ ...args, notify: false });
5203
+ }
5204
+ write(args) {
5205
+ const text = clip(args.text);
5206
+ const now = Date.now();
5207
+ if (args.requestId) {
5208
+ const existing = this.byRequestId(args.author, args.requestId);
5209
+ if (existing) return { ...existing, duplicate: true };
5210
+ }
5211
+ const run = this.db.transaction(() => {
5212
+ let threadId;
5213
+ if (args.target.kind === "thread") {
5214
+ const exists2 = this.db.prepare(`SELECT id FROM board_threads WHERE id = ?`).get(args.target.id);
5215
+ if (!exists2) throw new Error(`No such thread: ${args.target.id}`);
5216
+ threadId = args.target.id;
5217
+ } else {
5218
+ let ch = this.channel(args.target.name);
5219
+ if (!ch && args.createChannel) {
5220
+ ch = this.createChannel({ name: args.target.name, by: args.author });
5221
+ }
5222
+ if (!ch) throw new Error(`No such channel: ${args.target.name}`);
5223
+ threadId = `th-${randomUUID().slice(0, 8)}`;
5224
+ this.db.prepare(
5225
+ `INSERT INTO board_threads (id, channel, title, created_by, created_at, last_post_at)
5226
+ VALUES (?, ?, ?, ?, ?, ?)`
5227
+ ).run(threadId, ch.name, firstLine(text), args.author, now, now);
5228
+ }
5229
+ const id = `po-${randomUUID().slice(0, 8)}`;
5230
+ const info = this.db.prepare(
5231
+ `INSERT INTO board_posts (id, thread_id, author, text, created_at, request_id)
5232
+ VALUES (?, ?, ?, ?, ?, ?)`
5233
+ ).run(id, threadId, args.author, text, now, args.requestId ?? null);
5234
+ const seq = Number(info.lastInsertRowid);
5235
+ this.db.prepare(`UPDATE board_threads SET last_post_at = ? WHERE id = ?`).run(now, threadId);
5236
+ if (args.notify) {
5237
+ this.subscribe(args.author, { kind: "thread", id: threadId }, seq);
5238
+ }
5239
+ const target = `thread:${threadId}`;
5240
+ for (const who of new Set(args.oneShot ?? [])) {
5241
+ if (!who || who === args.author) continue;
5242
+ this.db.prepare(
5243
+ `INSERT OR IGNORE INTO board_notices (agent, target, seq, created_at)
5244
+ VALUES (?, ?, ?, ?)`
5245
+ ).run(who, target, seq, now);
5246
+ }
5247
+ return {
5248
+ post: { id, threadId, author: args.author, text, createdAt: now, seq },
5249
+ threadId,
5250
+ duplicate: false
5251
+ };
5252
+ });
5253
+ return run();
5254
+ }
5255
+ byRequestId(author, requestId) {
5256
+ const row = this.db.prepare(
5257
+ `SELECT seq, id, thread_id, author, text, created_at FROM board_posts
5258
+ WHERE author = ? AND request_id = ?`
5259
+ ).get(author, requestId);
5260
+ if (!row) return void 0;
5261
+ return { post: toPost(row), threadId: row.thread_id };
5262
+ }
5263
+ readThread(threadId, opts = {}) {
5264
+ const limit = pageLimit(opts.limit, 50);
5265
+ const afterSeq = decodeSeqCursor(opts.cursor, "rd");
5266
+ const rows = this.db.prepare(
5267
+ `SELECT seq, id, thread_id, author, text, created_at FROM board_posts
5268
+ WHERE thread_id = ? AND seq > ?
5269
+ ORDER BY seq ASC LIMIT ?`
5270
+ ).all(threadId, afterSeq, limit + 1);
5271
+ return toPage(rows.map(toPost), limit, (p) => encodeCursor("rd", String(p.seq)));
5272
+ }
5273
+ readPost(id, opts = {}) {
5274
+ const row = this.db.prepare(`SELECT seq, id, thread_id, author, text, created_at FROM board_posts WHERE id = ?`).get(id);
5275
+ if (!row) return void 0;
5276
+ return slicePost(toPost(row), opts.offsetChars, opts.limitChars);
5277
+ }
5278
+ searchPosts(opts = {}) {
5279
+ const limit = pageLimit(opts.limit, 20);
5280
+ const upper = decodeCursor(opts.cursor, "sea");
5281
+ const cursorSeq = upper === void 0 ? void 0 : Number.parseInt(upper, 10);
5282
+ const border = Number.isFinite(cursorSeq) ? cursorSeq : null;
5283
+ const sinceSeq = opts.afterMessageId ? this.seqOf(opts.afterMessageId) ?? null : null;
5284
+ const rows = opts.channel ? this.db.prepare(
5285
+ `SELECT p.seq, p.id, p.thread_id, p.author, p.text, p.created_at
5286
+ FROM board_posts p JOIN board_threads t ON t.id = p.thread_id
5287
+ WHERE t.channel = ?
5288
+ AND (? IS NULL OR p.text LIKE ?)
5289
+ AND (? IS NULL OR p.author = ?)
5290
+ AND (? IS NULL OR p.seq < ?)
5291
+ AND (? IS NULL OR p.seq > ?)
5292
+ ORDER BY p.seq DESC LIMIT ?`
5293
+ ).all(
5294
+ opts.channel,
5295
+ opts.query ?? null,
5296
+ opts.query ? `%${opts.query}%` : null,
5297
+ opts.author ?? null,
5298
+ opts.author ?? null,
5299
+ border,
5300
+ border,
5301
+ sinceSeq,
5302
+ sinceSeq,
5303
+ limit + 1
5304
+ ) : this.db.prepare(
5305
+ `SELECT seq, id, thread_id, author, text, created_at FROM board_posts
5306
+ WHERE (? IS NULL OR text LIKE ?)
5307
+ AND (? IS NULL OR author = ?)
5308
+ AND (? IS NULL OR seq < ?)
5309
+ AND (? IS NULL OR seq > ?)
5310
+ ORDER BY seq DESC LIMIT ?`
5311
+ ).all(
5312
+ opts.query ?? null,
5313
+ opts.query ? `%${opts.query}%` : null,
5314
+ opts.author ?? null,
5315
+ opts.author ?? null,
5316
+ border,
5317
+ border,
5318
+ sinceSeq,
5319
+ sinceSeq,
5320
+ limit + 1
5321
+ );
5322
+ return toPage(rows.map(toPost), limit, (p) => encodeCursor("sea", String(p.seq)));
5323
+ }
5324
+ seqOf(postId) {
5325
+ const row = this.db.prepare(`SELECT seq FROM board_posts WHERE id = ?`).get(postId);
5326
+ return row?.seq;
5327
+ }
5328
+ subscribe(agent, target, seenSeq) {
5329
+ const key = targetKey(target);
5330
+ const initial = seenSeq ?? this.maxSeq();
5331
+ this.db.prepare(
5332
+ `INSERT INTO board_subscriptions (agent, target, last_seen_seq) VALUES (?, ?, ?)
5333
+ ON CONFLICT(agent, target) DO UPDATE SET
5334
+ last_seen_seq = MAX(board_subscriptions.last_seen_seq, excluded.last_seen_seq)`
5335
+ ).run(agent, key, initial);
5336
+ }
5337
+ maxSeq() {
5338
+ const row = this.db.prepare(`SELECT COALESCE(MAX(seq), 0) AS n FROM board_posts`).get();
5339
+ return row.n;
5340
+ }
5341
+ unsubscribe(agent, target) {
5342
+ const info = this.db.prepare(`DELETE FROM board_subscriptions WHERE agent = ? AND target = ?`).run(agent, targetKey(target));
5343
+ return info.changes > 0;
5344
+ }
5345
+ pendingFor(agent) {
5346
+ const subs = this.db.prepare(`SELECT target, last_seen_seq FROM board_subscriptions WHERE agent = ?`).all(agent);
5347
+ const counts = /* @__PURE__ */ new Map();
5348
+ for (const s of subs) {
5349
+ const count = this.countSince(s.target, s.last_seen_seq);
5350
+ if (count > 0) counts.set(s.target, count);
5351
+ }
5352
+ const noticed = this.db.prepare(`SELECT target, COUNT(*) AS n FROM board_notices WHERE agent = ? GROUP BY target`).all(agent);
5353
+ for (const n of noticed) {
5354
+ if (counts.has(n.target)) continue;
5355
+ counts.set(n.target, n.n);
5356
+ }
5357
+ return [...counts].map(([target, count]) => ({ target, count }));
5358
+ }
5359
+ markRead(agent, target) {
5360
+ if (target) {
5361
+ const key = targetKey(target);
5362
+ this.db.prepare(
5363
+ `UPDATE board_subscriptions SET last_seen_seq = (SELECT MAX(seq) FROM board_posts) WHERE agent = ? AND target = ?`
5364
+ ).run(agent, key);
5365
+ this.db.prepare(
5366
+ `DELETE FROM board_notices WHERE agent = ? AND target = ?
5367
+ AND seq <= (SELECT COALESCE(MAX(seq), 0) FROM board_posts)`
5368
+ ).run(agent, key);
5369
+ return;
5370
+ }
5371
+ this.db.prepare(
5372
+ `UPDATE board_subscriptions SET last_seen_seq = (SELECT COALESCE(MAX(seq), 0) FROM board_posts) WHERE agent = ?`
5373
+ ).run(agent);
5374
+ this.db.prepare(`DELETE FROM board_notices WHERE agent = ?`).run(agent);
5375
+ }
5376
+ countSince(target, sinceSeq) {
5377
+ if (target.startsWith("thread:")) {
5378
+ const id = target.slice("thread:".length);
5379
+ const row2 = this.db.prepare(`SELECT COUNT(*) AS n FROM board_posts WHERE thread_id = ? AND seq > ?`).get(id, sinceSeq);
5380
+ return row2.n;
5381
+ }
5382
+ const name = target.slice("channel:".length);
5383
+ const row = this.db.prepare(
5384
+ `SELECT COUNT(*) AS n FROM board_posts p JOIN board_threads t ON t.id = p.thread_id
5385
+ WHERE t.channel = ? AND p.seq > ?
5386
+ AND p.seq = (SELECT MIN(seq) FROM board_posts WHERE thread_id = p.thread_id)`
5387
+ ).get(name, sinceSeq);
5388
+ return row.n;
5389
+ }
5390
+ };
5391
+ function pageLimit(raw, fallback2) {
5392
+ if (typeof raw !== "number" || !Number.isFinite(raw)) return fallback2;
5393
+ return Math.min(Math.max(Math.floor(raw), 1), MAX_PAGE_LIMIT);
5394
+ }
5395
+ function toPage(rows, limit, cursorOf) {
5396
+ if (rows.length <= limit) return { items: rows };
5397
+ const items = rows.slice(0, limit);
5398
+ return { items, nextCursor: cursorOf(items[items.length - 1]) };
5399
+ }
5400
+ function splitThreadCursor(payload) {
5401
+ const i = payload.indexOf(":");
5402
+ if (i < 0) return [Number.NaN, payload];
5403
+ return [Number.parseInt(payload.slice(0, i), 10) || 0, payload.slice(i + 1)];
5404
+ }
5405
+ function assertDescription(description) {
5406
+ const bytes = Buffer.byteLength(description, "utf8");
5407
+ if (bytes > MAX_CHANNEL_DESCRIPTION_BYTES) {
5408
+ throw new Error(
5409
+ `Channel description is ${bytes} bytes; at most ${MAX_CHANNEL_DESCRIPTION_BYTES} UTF-8 bytes`
5410
+ );
5411
+ }
5412
+ }
5413
+ function slicePost(post, offsetChars, limitChars) {
5414
+ const nChars = charLength(post.text);
5415
+ const offset = Math.min(Math.max(Math.floor(offsetChars ?? 0), 0), nChars);
5416
+ const limit = Math.min(Math.max(Math.floor(limitChars ?? MAX_POST_CHARS), 1), MAX_POST_CHARS);
5417
+ const text = sliceChars(post.text, offset, limit);
5418
+ return {
5419
+ post: { ...post, text },
5420
+ nChars,
5421
+ offsetChars: offset,
5422
+ nextOffsetChars: offset + charLength(text)
5423
+ };
5424
+ }
5425
+ function clip(text) {
5426
+ if (text.length <= MAX_POST_CHARS) return text;
5427
+ return `${text.slice(0, MAX_POST_CHARS)}
5428
+ \u2026 (truncated; the original is ${text.length} characters)`;
5429
+ }
5430
+ function firstLine(text) {
5431
+ const line = text.split("\n", 1)[0] ?? text;
5432
+ return line.length > 80 ? `${line.slice(0, 80)}\u2026` : line;
5433
+ }
5434
+ function toChannel(r) {
5435
+ return {
5436
+ id: r.id,
5437
+ name: r.name,
5438
+ ...r.description ? { description: r.description } : {},
5439
+ createdBy: r.created_by,
5440
+ createdAt: r.created_at
5441
+ };
5442
+ }
5443
+ function toPost(r) {
5444
+ return {
5445
+ id: r.id,
5446
+ threadId: r.thread_id,
5447
+ author: r.author,
5448
+ text: r.text,
5449
+ createdAt: r.created_at,
5450
+ seq: r.seq
5451
+ };
5452
+ }
5453
+ function toThread(r) {
5454
+ return {
5455
+ id: r.id,
5456
+ channel: r.channel,
5457
+ title: r.title,
5458
+ createdBy: r.created_by,
5459
+ createdAt: r.created_at,
5460
+ lastPostAt: r.last_post_at,
5461
+ postCount: r.posts
5462
+ };
5463
+ }
5464
+ // src/tools/builtin/board.ts
5465
+ var MAX_NOTIFY = 256;
5466
+ var DEFAULT_PREVIEW_CHARS = 1e3;
4888
5467
  function err(code, message, suggestion) {
4889
5468
  return {
4890
5469
  success: false,
4891
5470
  output: "",
4892
- error: { code, message, ...{} },
5471
+ error: { code, message, ...suggestion ? { suggestion } : {} },
4893
5472
  duration: 0
4894
5473
  };
4895
5474
  }
@@ -4900,6 +5479,11 @@ function str2(args, key) {
4900
5479
  const v = args[key];
4901
5480
  return typeof v === "string" ? v.trim() : "";
4902
5481
  }
5482
+ function num(args, key, fallback2, max) {
5483
+ const v = args[key];
5484
+ if (typeof v !== "number" || !Number.isFinite(v)) return fallback2;
5485
+ return Math.min(Math.max(Math.floor(v), 1), max);
5486
+ }
4903
5487
  function parseTarget(raw) {
4904
5488
  const t83 = raw.trim();
4905
5489
  if (!t83) return null;
@@ -4908,26 +5492,59 @@ function parseTarget(raw) {
4908
5492
  function authorOf(deps, ctx) {
4909
5493
  return deps.agentOf(ctx.sessionId) ?? LEAD_AGENT_ID;
4910
5494
  }
4911
- function renderPost(p) {
4912
- const who = p.author;
5495
+ function renderPost(p, maxChars) {
4913
5496
  const when = new Date(p.createdAt).toISOString().slice(0, 16).replace("T", " ");
4914
- const body2 = p.text.length > 2e3 ? `${p.text.slice(0, 2e3)}\u2026` : p.text;
4915
- return `#${p.seq} [${p.id}] ${who} \xB7 ${when}
5497
+ const { text, truncated, nChars } = previewPostText(p, maxChars);
5498
+ const body2 = truncated ? `${text}
5499
+ \u2026 (showing ${maxChars} of ${nChars} characters; the rest: read_post ${p.id})` : text;
5500
+ return `#${p.seq} [${p.id}] ${p.author} \xB7 ${when}
4916
5501
  ${body2}`;
4917
5502
  }
4918
5503
  function renderThread(t83) {
4919
5504
  return `- ${t83.id} ${t83.title} (${t83.postCount} posts, last ${new Date(t83.lastPostAt).toISOString().slice(0, 16).replace("T", " ")})`;
4920
5505
  }
5506
+ function moreLine(label, nextCursor) {
5507
+ return nextCursor ? `
5508
+ (${label} \u2014 pass cursor=${nextCursor} to continue)` : "";
5509
+ }
4921
5510
  function createBoardTools(store, deps) {
4922
5511
  const targetParam = {
4923
5512
  type: "string",
4924
5513
  description: "A channel name (starts a new thread), or a thread id like `th-xxxxxxxx` (replies into it)."
4925
5514
  };
5515
+ const cursorParam = {
5516
+ type: "string",
5517
+ description: "The `next_cursor` from the previous page, unchanged. Keep the same filters; leave it out to start over."
5518
+ };
5519
+ const previewParam = {
5520
+ type: "number",
5521
+ description: `How many characters of each post to show (default ${DEFAULT_PREVIEW_CHARS}, at most ${MAX_POST_CHARS}). Long posts say so and can be read in full with read_post.`
5522
+ };
5523
+ const notifyParam = {
5524
+ type: "array",
5525
+ items: { type: "string" },
5526
+ description: `Agents to notify about **this post only** (they are not subscribed, so later replies do not reach them). At most ${MAX_NOTIFY}.`
5527
+ };
5528
+ const requestIdParam = {
5529
+ type: "string",
5530
+ description: "Idempotency key: posting again with the same id returns the post already created instead of a second copy. Use it when a call may be retried."
5531
+ };
4926
5532
  const tools = [
4927
5533
  {
4928
5534
  name: "get_channels",
4929
- description: "List the channels on the discussion board. **The board is shared across processes** \u2014 a channel created in another of your windows shows up here.",
4930
- parameters: { type: "object", properties: {}, required: [] },
5535
+ description: [
5536
+ "List the channels on the discussion board, by name. **The board is shared across processes** \u2014 a channel created in another of your windows shows up here.",
5537
+ "Pass a query to search part of a name (case-insensitive)."
5538
+ ].join("\n"),
5539
+ parameters: {
5540
+ type: "object",
5541
+ properties: {
5542
+ query: { type: "string", description: "Only channels whose name contains this." },
5543
+ limit: { type: "number", description: "How many to return (default 100)." },
5544
+ cursor: cursorParam
5545
+ },
5546
+ required: []
5547
+ },
4931
5548
  exposure: ToolExposure.Direct,
4932
5549
  supportsParallel: true,
4933
5550
  operation: "file_read",
@@ -4938,17 +5555,29 @@ function createBoardTools(store, deps) {
4938
5555
  openWorldHint: false
4939
5556
  },
4940
5557
  describeTarget: () => "channel list",
4941
- async execute() {
4942
- const channels = store.listChannels();
4943
- if (channels.length === 0) return ok2("No channels yet. Create one with create_channel.");
5558
+ async execute(args) {
5559
+ const query = str2(args, "query");
5560
+ const page = store.listChannels({
5561
+ ...query ? { query } : {},
5562
+ limit: num(args, "limit", 100, 200),
5563
+ ...str2(args, "cursor") ? { cursor: str2(args, "cursor") } : {}
5564
+ });
5565
+ if (page.items.length === 0) {
5566
+ return ok2(
5567
+ query ? `No channel matches "${query}".` : "No channels yet. Create one with create_channel."
5568
+ );
5569
+ }
4944
5570
  return ok2(
4945
- channels.map((c) => `- ${c.name}${c.description ? `: ${c.description}` : ""}`).join("\n")
5571
+ page.items.map((c) => `- ${c.name}${c.description ? `: ${c.description}` : ""}`).join("\n") + moreLine("more channels", page.nextCursor)
4946
5572
  );
4947
5573
  }
4948
5574
  },
4949
5575
  {
4950
5576
  name: "create_channel",
4951
- description: "Create a channel. If it already exists it is returned as-is (no error, description left alone).",
5577
+ description: [
5578
+ "Create a channel. If it already exists it is returned as-is (no error, description left alone).",
5579
+ `The description is capped at ${MAX_CHANNEL_DESCRIPTION_BYTES} UTF-8 bytes.`
5580
+ ].join("\n"),
4952
5581
  parameters: {
4953
5582
  type: "object",
4954
5583
  properties: {
@@ -4985,7 +5614,10 @@ function createBoardTools(store, deps) {
4985
5614
  },
4986
5615
  {
4987
5616
  name: "upsert_channel",
4988
- description: "Change a channel description (posts and subscriptions are untouched). Creates it if it does not exist.",
5617
+ description: [
5618
+ "Change a channel description (posts and subscriptions are untouched). Creates it if it does not exist.",
5619
+ `The description is capped at ${MAX_CHANNEL_DESCRIPTION_BYTES} UTF-8 bytes.`
5620
+ ].join("\n"),
4989
5621
  parameters: {
4990
5622
  type: "object",
4991
5623
  properties: {
@@ -5019,12 +5651,13 @@ function createBoardTools(store, deps) {
5019
5651
  },
5020
5652
  {
5021
5653
  name: "list_threads",
5022
- description: "List the threads in a channel, most recent activity first.",
5654
+ description: "List the threads in a channel, most recent activity first. Paged: pass the returned cursor to see more.",
5023
5655
  parameters: {
5024
5656
  type: "object",
5025
5657
  properties: {
5026
5658
  channel: { type: "string", description: "Channel name." },
5027
- limit: { type: "number", description: "How many to return (default 20)." }
5659
+ limit: { type: "number", description: "How many to return (default 20)." },
5660
+ cursor: cursorParam
5028
5661
  },
5029
5662
  required: ["channel"]
5030
5663
  },
@@ -5042,20 +5675,29 @@ function createBoardTools(store, deps) {
5042
5675
  const channel = str2(args, "channel");
5043
5676
  if (!channel) return err(7406, "Missing channel");
5044
5677
  if (!store.channel(channel)) return err(7407, `No such channel: ${channel}`);
5045
- const limit = typeof args["limit"] === "number" ? Math.min(Math.max(1, args["limit"]), 200) : 20;
5046
- const threads = store.listThreads(channel, limit);
5047
- if (threads.length === 0) return ok2(`Channel ${channel} has no threads yet.`);
5048
- return ok2(threads.map(renderThread).join("\n"));
5678
+ const page = store.listThreads(channel, {
5679
+ limit: num(args, "limit", 20, 200),
5680
+ ...str2(args, "cursor") ? { cursor: str2(args, "cursor") } : {}
5681
+ });
5682
+ if (page.items.length === 0) return ok2(`Channel ${channel} has no (more) threads.`);
5683
+ return ok2(
5684
+ page.items.map(renderThread).join("\n") + moreLine("more threads", page.nextCursor)
5685
+ );
5049
5686
  }
5050
5687
  },
5051
5688
  {
5052
5689
  name: "read_thread",
5053
- description: "Read the posts in a thread, oldest first. **Reading advances the subscription cursor to now** via markRead, so these posts are not reported again.",
5690
+ description: [
5691
+ "Read the posts in a thread, oldest first, **paged**. Each post is truncated to maxCharsPerPost (default 1000); read_post gives the rest.",
5692
+ "**Reading the last page advances the subscription cursor** via markRead, so those posts are not reported again. Reading only the first page leaves the rest unread \u2014 that is deliberate."
5693
+ ].join("\n"),
5054
5694
  parameters: {
5055
5695
  type: "object",
5056
5696
  properties: {
5057
5697
  threadId: { type: "string", description: "Thread id (starts with th-)" },
5058
- limit: { type: "number", description: "How many posts to return (default 50)." }
5698
+ limit: { type: "number", description: "How many posts to return (default 50)." },
5699
+ cursor: cursorParam,
5700
+ maxCharsPerPost: previewParam
5059
5701
  },
5060
5702
  required: ["threadId"]
5061
5703
  },
@@ -5072,21 +5714,38 @@ function createBoardTools(store, deps) {
5072
5714
  async execute(args, ctx) {
5073
5715
  const threadId = str2(args, "threadId");
5074
5716
  if (!threadId) return err(7408, "Missing threadId");
5075
- const limit = typeof args["limit"] === "number" ? Math.min(Math.max(1, args["limit"]), 500) : 50;
5076
- const posts = store.readThread(threadId, limit);
5077
- if (posts.length === 0) {
5078
- return ok2(`Thread ${threadId} has no posts (or the thread does not exist).`);
5717
+ const maxChars = num(args, "maxCharsPerPost", DEFAULT_PREVIEW_CHARS, MAX_POST_CHARS);
5718
+ const page = store.readThread(threadId, {
5719
+ limit: num(args, "limit", 50, 200),
5720
+ ...str2(args, "cursor") ? { cursor: str2(args, "cursor") } : {}
5721
+ });
5722
+ if (page.items.length === 0) {
5723
+ return ok2(`Thread ${threadId} has no (more) posts (or the thread does not exist).`);
5724
+ }
5725
+ if (!page.nextCursor) {
5726
+ store.markRead(authorOf(deps, ctx), { kind: "thread", id: threadId });
5079
5727
  }
5080
- store.markRead(authorOf(deps, ctx), { kind: "thread", id: threadId });
5081
- return ok2(posts.map(renderPost).join("\n\n"));
5728
+ return ok2(
5729
+ page.items.map((p) => renderPost(p, maxChars)).join("\n\n") + moreLine("more posts", page.nextCursor)
5730
+ );
5082
5731
  }
5083
5732
  },
5084
5733
  {
5085
5734
  name: "read_post",
5086
- description: "Read one post in full (read_thread truncates long posts).",
5735
+ description: [
5736
+ "Read one post in full (read_thread only gives a preview). Long posts are read in slices: pass offsetChars / limitChars,",
5737
+ "and keep going at nextOffsetChars while it is smaller than nChars. Offsets count Unicode characters."
5738
+ ].join("\n"),
5087
5739
  parameters: {
5088
5740
  type: "object",
5089
- properties: { postId: { type: "string", description: "Post id (starts with po-)." } },
5741
+ properties: {
5742
+ postId: { type: "string", description: "Post id (starts with po-)." },
5743
+ offsetChars: { type: "number", description: "Where to start (default 0)." },
5744
+ limitChars: {
5745
+ type: "number",
5746
+ description: `How many characters to return (default and maximum ${MAX_POST_CHARS}).`
5747
+ }
5748
+ },
5090
5749
  required: ["postId"]
5091
5750
  },
5092
5751
  exposure: ToolExposure.Deferred,
@@ -5101,22 +5760,41 @@ function createBoardTools(store, deps) {
5101
5760
  describeTarget: (args) => String(args["postId"] ?? ""),
5102
5761
  async execute(args) {
5103
5762
  const id = str2(args, "postId");
5104
- const p = store.readPost(id);
5105
- if (!p) return err(7409, `No such post: ${id}`);
5106
- return ok2(renderPost(p));
5763
+ const rawOffset = typeof args["offsetChars"] === "number" ? args["offsetChars"] : 0;
5764
+ const slice = store.readPost(id, {
5765
+ offsetChars: Math.max(0, Math.floor(rawOffset)),
5766
+ limitChars: num(args, "limitChars", MAX_POST_CHARS, MAX_POST_CHARS)
5767
+ });
5768
+ if (!slice) return err(7409, `No such post: ${id}`);
5769
+ const head = `#${slice.post.seq} [${slice.post.id}] ${slice.post.author}`;
5770
+ const more = slice.nextOffsetChars < slice.nChars ? `
5771
+ (more \u2014 pass offsetChars=${slice.nextOffsetChars}; ${slice.nextOffsetChars}/${slice.nChars} shown)` : "";
5772
+ return ok2(`${head}
5773
+ ${slice.post.text}${more}`);
5107
5774
  }
5108
5775
  },
5109
5776
  {
5110
5777
  name: "search_posts",
5111
- description: "Full-text search across posts on the board (substring match). Can be limited to one channel.",
5778
+ description: [
5779
+ "Search posts on the board, newest first. Text matches are case-insensitive substrings.",
5780
+ "Omit the query to see recent activity; narrow by channel, author, or posts after a message id.",
5781
+ "Results are previews \u2014 read_post gets the rest."
5782
+ ].join("\n"),
5112
5783
  parameters: {
5113
5784
  type: "object",
5114
5785
  properties: {
5115
- query: { type: "string", description: "Term to search for." },
5786
+ query: { type: "string", description: "Term to search for (omit for recent activity)." },
5116
5787
  channel: { type: "string", description: "Search only within this channel." },
5117
- limit: { type: "number", description: "How many to return (default 20)." }
5788
+ author: { type: "string", description: "Only posts by this agent." },
5789
+ afterMessageId: {
5790
+ type: "string",
5791
+ description: "Only posts after this one (a po- id)."
5792
+ },
5793
+ limit: { type: "number", description: "How many to return (default 20)." },
5794
+ cursor: cursorParam,
5795
+ maxCharsPerPost: previewParam
5118
5796
  },
5119
- required: ["query"]
5797
+ required: []
5120
5798
  },
5121
5799
  exposure: ToolExposure.Deferred,
5122
5800
  supportsParallel: true,
@@ -5127,28 +5805,49 @@ function createBoardTools(store, deps) {
5127
5805
  idempotentHint: true,
5128
5806
  openWorldHint: false
5129
5807
  },
5130
- describeTarget: (args) => String(args["query"] ?? ""),
5808
+ describeTarget: (args) => String(args["query"] ?? "(recent)"),
5131
5809
  async execute(args) {
5132
5810
  const query = str2(args, "query");
5133
- if (!query) return err(7410, "Missing query");
5134
5811
  const channel = str2(args, "channel");
5135
- const limit = typeof args["limit"] === "number" ? Math.min(Math.max(1, args["limit"]), 100) : 20;
5136
- const posts = store.searchPosts(query, channel || void 0, limit);
5137
- if (posts.length === 0) return ok2(`No posts containing "${query}" were found.`);
5138
- return ok2(posts.map(renderPost).join("\n\n"));
5812
+ const author = str2(args, "author");
5813
+ const afterMessageId = str2(args, "afterMessageId");
5814
+ const maxChars = num(args, "maxCharsPerPost", DEFAULT_PREVIEW_CHARS, MAX_POST_CHARS);
5815
+ const page = store.searchPosts({
5816
+ ...query ? { query } : {},
5817
+ ...channel ? { channel } : {},
5818
+ ...author ? { author } : {},
5819
+ ...afterMessageId ? { afterMessageId } : {},
5820
+ limit: num(args, "limit", 20, 100),
5821
+ ...str2(args, "cursor") ? { cursor: str2(args, "cursor") } : {}
5822
+ });
5823
+ if (page.items.length === 0) {
5824
+ return ok2(query ? `No posts containing "${query}" were found.` : "No recent posts.");
5825
+ }
5826
+ return ok2(
5827
+ page.items.map((p) => renderPost(p, maxChars)).join("\n\n") + moreLine("more results", page.nextCursor)
5828
+ );
5139
5829
  }
5140
5830
  },
5141
5831
  {
5142
5832
  name: "post",
5143
5833
  description: [
5144
- "Post a message. A channel name as target **opens a new thread**; a thread id starting with th- replies into it.",
5145
- "You are **subscribed to your own post automatically** (so you get notified of later replies).",
5834
+ "Post a message. A channel name as target **opens a new thread**; a thread id starting with th- replies into it; newChannelName creates the channel in the same call (give exactly one destination).",
5835
+ "You are **subscribed to your own post automatically** (so you get notified of later replies). agentsToNotify gives people a one-time heads-up without subscribing them.",
5146
5836
  "To record something silently without notifying anyone, use append."
5147
5837
  ].join("\n"),
5148
5838
  parameters: {
5149
5839
  type: "object",
5150
- properties: { target: targetParam, text: { type: "string", description: "Post body." } },
5151
- required: ["target", "text"]
5840
+ properties: {
5841
+ target: targetParam,
5842
+ newChannelName: {
5843
+ type: "string",
5844
+ description: "Create this channel (if needed) and start the thread there."
5845
+ },
5846
+ text: { type: "string", description: "Post body." },
5847
+ agentsToNotify: notifyParam,
5848
+ requestId: requestIdParam
5849
+ },
5850
+ required: ["text"]
5152
5851
  },
5153
5852
  exposure: ToolExposure.Deferred,
5154
5853
  supportsParallel: false,
@@ -5159,15 +5858,26 @@ function createBoardTools(store, deps) {
5159
5858
  idempotentHint: false,
5160
5859
  openWorldHint: false
5161
5860
  },
5162
- describeTarget: (args) => String(args["target"] ?? ""),
5861
+ describeTarget: (args) => String(args["target"] ?? args["newChannelName"] ?? ""),
5163
5862
  async execute(args, ctx) {
5164
- const target = parseTarget(str2(args, "target"));
5863
+ const dest = destinationOf(args);
5864
+ if ("error" in dest) return dest.error;
5165
5865
  const text = str2(args, "text");
5166
- if (!target) return err(7411, "Missing target");
5167
5866
  if (!text) return err(7412, "Missing text");
5867
+ const notify = notifyList(args);
5868
+ if ("error" in notify) return notify.error;
5168
5869
  try {
5169
- const r = store.post({ target, author: authorOf(deps, ctx), text });
5170
- return ok2(`Posted ${r.post.id} (thread ${r.threadId}, #${r.post.seq}).`);
5870
+ const r = store.post({
5871
+ target: dest.target,
5872
+ author: authorOf(deps, ctx),
5873
+ text,
5874
+ ...dest.create ? { createChannel: true } : {},
5875
+ ...notify.list.length > 0 ? { notify: notify.list } : {},
5876
+ ...str2(args, "requestId") ? { requestId: str2(args, "requestId") } : {}
5877
+ });
5878
+ const retry = r.duplicate ? " (this request id already produced that post \u2014 no duplicate made)" : "";
5879
+ const told = notify.list.length > 0 ? ` Notified ${notify.list.join(", ")} for this post only.` : "";
5880
+ return ok2(`Posted ${r.post.id} (thread ${r.threadId}, #${r.post.seq}).${retry}${told}`);
5171
5881
  } catch (cause) {
5172
5882
  return err(7413, cause instanceof Error ? cause.message : String(cause));
5173
5883
  }
@@ -5182,8 +5892,16 @@ function createBoardTools(store, deps) {
5182
5892
  ].join("\n"),
5183
5893
  parameters: {
5184
5894
  type: "object",
5185
- properties: { target: targetParam, text: { type: "string", description: "Body." } },
5186
- required: ["target", "text"]
5895
+ properties: {
5896
+ target: targetParam,
5897
+ newChannelName: {
5898
+ type: "string",
5899
+ description: "Create this channel (if needed) without subscribing anyone."
5900
+ },
5901
+ text: { type: "string", description: "Body." },
5902
+ requestId: requestIdParam
5903
+ },
5904
+ required: ["text"]
5187
5905
  },
5188
5906
  exposure: ToolExposure.Deferred,
5189
5907
  supportsParallel: false,
@@ -5194,16 +5912,23 @@ function createBoardTools(store, deps) {
5194
5912
  idempotentHint: false,
5195
5913
  openWorldHint: false
5196
5914
  },
5197
- describeTarget: (args) => String(args["target"] ?? ""),
5915
+ describeTarget: (args) => String(args["target"] ?? args["newChannelName"] ?? ""),
5198
5916
  async execute(args, ctx) {
5199
- const target = parseTarget(str2(args, "target"));
5917
+ const dest = destinationOf(args);
5918
+ if ("error" in dest) return dest.error;
5200
5919
  const text = str2(args, "text");
5201
- if (!target) return err(7414, "Missing target");
5202
5920
  if (!text) return err(7415, "Missing text");
5203
5921
  try {
5204
- const r = store.append({ target, author: authorOf(deps, ctx), text });
5922
+ const r = store.append({
5923
+ target: dest.target,
5924
+ author: authorOf(deps, ctx),
5925
+ text,
5926
+ ...dest.create ? { createChannel: true } : {},
5927
+ ...str2(args, "requestId") ? { requestId: str2(args, "requestId") } : {}
5928
+ });
5929
+ const retry = r.duplicate ? " (this request id already produced that post)" : "";
5205
5930
  return ok2(
5206
- `Appended ${r.post.id} (thread ${r.threadId}, #${r.post.seq}). Nobody was notified.`
5931
+ `Appended ${r.post.id} (thread ${r.threadId}, #${r.post.seq}). Nobody was notified.${retry}`
5207
5932
  );
5208
5933
  } catch (cause) {
5209
5934
  return err(7416, cause instanceof Error ? cause.message : String(cause));
@@ -5212,10 +5937,19 @@ function createBoardTools(store, deps) {
5212
5937
  },
5213
5938
  {
5214
5939
  name: "subscribe",
5215
- description: "Subscribe to a channel or thread. When something new appears there, you get a reminder at the start of your next turn (**it does not wake you up**).",
5940
+ description: [
5941
+ "Subscribe yourself or another agent to a channel (new top-level posts) or a thread (replies).",
5942
+ "When something new appears there, you get a reminder at the start of your next turn (**it does not wake you up**)."
5943
+ ].join("\n"),
5216
5944
  parameters: {
5217
5945
  type: "object",
5218
- properties: { target: { ...targetParam, description: "Channel name or thread id." } },
5946
+ properties: {
5947
+ target: { ...targetParam, description: "Channel name or thread id." },
5948
+ targetAgent: {
5949
+ type: "string",
5950
+ description: "Who to subscribe (default: you). A member name, or lead for the main session."
5951
+ }
5952
+ },
5219
5953
  required: ["target"]
5220
5954
  },
5221
5955
  exposure: ToolExposure.Deferred,
@@ -5231,16 +5965,20 @@ function createBoardTools(store, deps) {
5231
5965
  async execute(args, ctx) {
5232
5966
  const target = parseTarget(str2(args, "target"));
5233
5967
  if (!target) return err(7417, "Missing target");
5234
- store.subscribe(authorOf(deps, ctx), target);
5235
- return ok2(`Subscribed to ${str2(args, "target")}.`);
5968
+ const who = str2(args, "targetAgent") || authorOf(deps, ctx);
5969
+ store.subscribe(who, target);
5970
+ return ok2(`Subscribed ${who} to ${str2(args, "target")}.`);
5236
5971
  }
5237
5972
  },
5238
5973
  {
5239
5974
  name: "unsubscribe",
5240
- description: "Unsubscribe.",
5975
+ description: "Unsubscribe yourself or another agent from a channel or a thread.",
5241
5976
  parameters: {
5242
5977
  type: "object",
5243
- properties: { target: { ...targetParam, description: "Channel name or thread id." } },
5978
+ properties: {
5979
+ target: { ...targetParam, description: "Channel name or thread id." },
5980
+ targetAgent: { type: "string", description: "Who to unsubscribe (default: you)." }
5981
+ },
5244
5982
  required: ["target"]
5245
5983
  },
5246
5984
  exposure: ToolExposure.Deferred,
@@ -5256,15 +5994,56 @@ function createBoardTools(store, deps) {
5256
5994
  async execute(args, ctx) {
5257
5995
  const target = parseTarget(str2(args, "target"));
5258
5996
  if (!target) return err(7418, "Missing target");
5259
- const removed = store.unsubscribe(authorOf(deps, ctx), target);
5997
+ const who = str2(args, "targetAgent") || authorOf(deps, ctx);
5998
+ const removed = store.unsubscribe(who, target);
5260
5999
  return ok2(
5261
- removed ? `Unsubscribed from ${str2(args, "target")}.` : `Was not subscribed to ${str2(args, "target")}.`
6000
+ removed ? `Unsubscribed ${who} from ${str2(args, "target")}.` : `${who} was not subscribed to ${str2(args, "target")}.`
5262
6001
  );
5263
6002
  }
5264
6003
  }
5265
6004
  ];
5266
6005
  return tools;
5267
6006
  }
6007
+ function destinationOf(args) {
6008
+ const raw = str2(args, "target");
6009
+ const newChannel = str2(args, "newChannelName");
6010
+ if (raw && newChannel) {
6011
+ return {
6012
+ error: err(
6013
+ 7419,
6014
+ "Both target and newChannelName were given",
6015
+ "Give exactly one destination: target (a channel name or a th- thread id) or newChannelName"
6016
+ )
6017
+ };
6018
+ }
6019
+ if (newChannel) return { target: { kind: "channel", name: newChannel }, create: true };
6020
+ const target = parseTarget(raw);
6021
+ if (!target) {
6022
+ return {
6023
+ error: err(
6024
+ 7411,
6025
+ "Missing target",
6026
+ "Give a channel name or a th- thread id as target, or create one with newChannelName"
6027
+ )
6028
+ };
6029
+ }
6030
+ return { target, create: false };
6031
+ }
6032
+ function notifyList(args) {
6033
+ const raw = args["agentsToNotify"];
6034
+ if (!Array.isArray(raw)) return { list: [] };
6035
+ const list = [...new Set(raw.map((v) => String(v).trim()).filter(Boolean))];
6036
+ if (list.length > MAX_NOTIFY) {
6037
+ return {
6038
+ error: err(
6039
+ 7420,
6040
+ `agentsToNotify has ${list.length} entries; at most ${MAX_NOTIFY}`,
6041
+ "Notify fewer people in one post, or post again in the same thread"
6042
+ )
6043
+ };
6044
+ }
6045
+ return { list };
6046
+ }
5268
6047
  var BOARD_TOOL_NAMES = Object.freeze([
5269
6048
  "create_channel",
5270
6049
  "upsert_channel",
@@ -5295,6 +6074,28 @@ function str3(args, key) {
5295
6074
  const v = args[key];
5296
6075
  return typeof v === "string" ? v.trim() : "";
5297
6076
  }
6077
+ function crossDirAllowed(ctx, targetDir) {
6078
+ if (!targetDir) return true;
6079
+ if (ctx.permissionLevel === "auto") return true;
6080
+ return isInWorkspace(targetDir, ctx.workDir, [
6081
+ ...ctx.extraRoots ?? [],
6082
+ ...ctx.grantedPaths ?? []
6083
+ ]);
6084
+ }
6085
+ function ownsMember(team, ctx, member) {
6086
+ return team.sameTree(ctx.sessionId, member.sessionId);
6087
+ }
6088
+ function notYours(verb) {
6089
+ return err2(
6090
+ 7217,
6091
+ `That member was dispatched by another session, so this session cannot ${verb} it`,
6092
+ [
6093
+ "The roster is shared across sessions, the right to reach into a member is not.",
6094
+ "Only members on your own side of the roster can be touched here \u2014 list_agents shows them",
6095
+ "To reach that work, talk to the session that dispatched it; to get the same work done yourself, dispatch a new member with spawn_teammate"
6096
+ ].join(". ")
6097
+ );
6098
+ }
5298
6099
  function spawnTeammate(team, manager) {
5299
6100
  return {
5300
6101
  name: "spawn_teammate",
@@ -5468,7 +6269,8 @@ function interruptAgent(team) {
5468
6269
  name: "interrupt_agent",
5469
6270
  description: [
5470
6271
  "Interrupt a teammate that is running (it is aborted, and whatever half-finished result it produced is kept).",
5471
- "A teammate that has already finished cannot be interrupted \u2014 that returns false; confirm with list_agents."
6272
+ "A teammate that has already finished cannot be interrupted \u2014 that returns false; confirm with list_agents.",
6273
+ "Only members **on your own side of the roster** can be interrupted \u2014 the ones this session dispatched (when you are a member yourself: your teammates). Another session\u2019s member is refused even if you know its name."
5472
6274
  ].join("\n"),
5473
6275
  parameters: {
5474
6276
  type: "object",
@@ -5487,12 +6289,13 @@ function interruptAgent(team) {
5487
6289
  openWorldHint: false
5488
6290
  },
5489
6291
  describeTarget: (args) => String(args["agentId"] ?? ""),
5490
- async execute(args) {
6292
+ async execute(args, ctx) {
5491
6293
  const agentId = str3(args, "agentId");
5492
6294
  if (!agentId) return err2(7206, "Missing agentId");
5493
6295
  const member = team.get(agentId);
5494
6296
  if (!member)
5495
6297
  return err2(7207, `No such member: ${agentId}`, "Use list_agents to see who is around");
6298
+ if (!ownsMember(team, ctx, member)) return notYours("interrupt");
5496
6299
  if (!team.interrupt(agentId)) {
5497
6300
  return err2(
5498
6301
  7208,
@@ -5504,13 +6307,59 @@ function interruptAgent(team) {
5504
6307
  }
5505
6308
  };
5506
6309
  }
6310
+ function closeAgent(team) {
6311
+ return {
6312
+ name: "close_agent",
6313
+ description: [
6314
+ "Close a member (and anything it dispatched) when you no longer need it, and get back the status it had before it was closed.",
6315
+ "Finished members stay in the roster until they are closed \u2014 they are not costing you a concurrency slot, but they do clutter list_agents. If you are done with one, close it; do not let them pile up.",
6316
+ "This is different from interrupt_agent: that one aborts the current turn and keeps the member around for more messages; this one takes it off the roster.",
6317
+ "Only members **on your own side of the roster** can be closed \u2014 the ones this session dispatched (when you are a member yourself: your teammates). Another session\u2019s member is refused even if you know its name."
6318
+ ].join("\n"),
6319
+ parameters: {
6320
+ type: "object",
6321
+ properties: {
6322
+ agentId: {
6323
+ type: "string",
6324
+ description: "Name of the member to close (as listed by list_agents)."
6325
+ }
6326
+ },
6327
+ required: ["agentId"]
6328
+ },
6329
+ exposure: ToolExposure.Deferred,
6330
+ supportsParallel: false,
6331
+ operation: "command",
6332
+ annotations: {
6333
+ readOnlyHint: false,
6334
+ destructiveHint: true,
6335
+ idempotentHint: false,
6336
+ openWorldHint: false
6337
+ },
6338
+ describeTarget: (args) => String(args["agentId"] ?? ""),
6339
+ async execute(args, ctx) {
6340
+ const agentId = str3(args, "agentId");
6341
+ if (!agentId) return err2(7212, "Missing agentId");
6342
+ const member = team.get(agentId);
6343
+ if (member && !ownsMember(team, ctx, member)) return notYours("close");
6344
+ const r = team.close(agentId);
6345
+ if (!r.ok) return err2(7213, r.reason ?? `Could not close ${agentId}`);
6346
+ const line = `Closed ${agentId} (its status before closing: ${r.previous ?? "unknown"}).`;
6347
+ const kin = r.descendants && r.descendants.length > 0 ? ` Also closed what it had dispatched: ${r.descendants.join(", ")}.` : "";
6348
+ const lost = r.dropped && r.dropped.count > 0 ? ` It still had ${r.dropped.count} unread message(s) from ${r.dropped.from.join(", ")}; they went with it.` : "";
6349
+ return ok3(
6350
+ `${line}${kin}${lost} Its session is still in the session list \u2014 closing only takes it off the roster.`
6351
+ );
6352
+ }
6353
+ };
6354
+ }
5507
6355
  function taskCreate(team) {
5508
6356
  return {
5509
6357
  name: "team_task_create",
5510
6358
  description: [
5511
6359
  "Create a task on the shared task board. The whole team sees the same board.",
5512
6360
  "dependsOn takes other task ids (they must already exist).",
5513
- "owner takes a member name, marking the task as theirs."
6361
+ "owner takes a member name, marking the task as theirs.",
6362
+ "Task readiness never starts an owner: when the tasks it depends on are marked done, **no turn is started for anybody** \u2014 the board is coordination state, not an alarm clock. Tell the owner yourself (send_message while it runs, followup_task when it is idle)."
5514
6363
  ].join("\n"),
5515
6364
  parameters: {
5516
6365
  type: "object",
@@ -5642,7 +6491,8 @@ function taskUpdate(team) {
5642
6491
  description: [
5643
6492
  "Change a task. **Call team_task_get first to get the revision, and pass it back unchanged as expectedRevision.**",
5644
6493
  "If someone else changed it in the meantime, this write is **rejected as a whole** and you are told what the revision is now \u2014",
5645
- "read it again and decide; do not guess. Leaving expectedRevision out means no check at all (only when you are certain you are the only writer)."
6494
+ "read it again and decide; do not guess. Leaving expectedRevision out means no check at all (only when you are certain you are the only writer).",
6495
+ "Marking a task done does not start the tasks waiting on it, and it does not wake their owner \u2014 the board never runs anybody. Say so yourself with send_message / followup_task."
5646
6496
  ].join("\n"),
5647
6497
  parameters: {
5648
6498
  type: "object",
@@ -5699,16 +6549,22 @@ function followupTask(team, manager) {
5699
6549
  description: [
5700
6550
  "Wake a counterpart that is **not running** and have it run another turn with all of its previous context.",
5701
6551
  "The target can be a teammate that has finished (agentId from list_agents), or **another main session** (sessionId from list_agents --all).",
6552
+ "Only the members **this session dispatched** can be woken; another session's subagent is refused even if you know its name (the roster is shared, the right to wake it is not).",
6553
+ "A member that was closed with close_agent is gone for good \u2014 it cannot be woken again, not even by its session id.",
5702
6554
  "How this splits with send_message: send_message only works while the other side **is running** (it lands in that turn);",
5703
6555
  "when it is not running, use this \u2014 it continues its own session rather than starting from scratch.",
5704
6556
  "Waking is asynchronous: the tool returns as soon as it is accepted, its own turn runs in the background, and you do not wait for the answer.",
5705
6557
  "One wake = one full model call (the whole history is resent), so say everything in one go instead of splitting it three ways.",
5706
- "There is a cap on concurrently woken sessions (3 by default); waking a fourth is rejected \u2014 wait for one to finish first."
6558
+ `There is a cap on concurrently woken sessions (${team.wakeLimit} right now); waking one more is rejected \u2014 wait for one to finish first.`,
6559
+ "Waking a session that works in a different directory follows the permission level: at auto it is allowed, below that a cross-directory wake is refused."
5707
6560
  ].join("\n"),
5708
6561
  parameters: {
5709
6562
  type: "object",
5710
6563
  properties: {
5711
- agentId: { type: "string", description: "Member name (as listed by list_agents)." },
6564
+ agentId: {
6565
+ type: "string",
6566
+ description: "Member name (as listed by list_agents); its session id works too and is resolved to the member."
6567
+ },
5712
6568
  message: { type: "string", description: "What it should do this turn." }
5713
6569
  },
5714
6570
  required: ["agentId", "message"]
@@ -5729,12 +6585,20 @@ function followupTask(team, manager) {
5729
6585
  const message = str3(args, "message");
5730
6586
  if (!agentId) return err2(7205, "Missing agentId");
5731
6587
  if (!message) return err2(7206, "Missing message");
5732
- const member = team.get(agentId);
6588
+ const member = team.get(agentId) ?? team.bySession(agentId);
5733
6589
  if (member) {
6590
+ const name = member.agentId;
6591
+ if (!ownsMember(team, ctx, member)) {
6592
+ return err2(
6593
+ 7217,
6594
+ `${name} was dispatched by another session, so this session cannot wake it`,
6595
+ "list_agents shows the members this session dispatched \u2014 those are the ones you can wake. To reach that work, wake the session that dispatched it (followup_task with its session id), or dispatch a new member with spawn_teammate"
6596
+ );
6597
+ }
5734
6598
  if (member.status === "running") {
5735
6599
  return err2(
5736
6600
  7208,
5737
- `${agentId} is already running; no need to wake it`,
6601
+ `${name} is already running; no need to wake it`,
5738
6602
  "To talk to it now use send_message \u2014 that lands at its next step boundary and takes effect in the current turn"
5739
6603
  );
5740
6604
  }
@@ -5743,7 +6607,7 @@ function followupTask(team, manager) {
5743
6607
  tasks: [
5744
6608
  {
5745
6609
  goal: message,
5746
- agentId,
6610
+ agentId: name,
5747
6611
  resumeSessionId: member.sessionId,
5748
6612
  ...member.role ? { role: member.role } : {}
5749
6613
  }
@@ -5757,12 +6621,29 @@ function followupTask(team, manager) {
5757
6621
  if (!r.success) return { ...err2(7209, r.output), duration: Date.now() - start };
5758
6622
  return ok3(
5759
6623
  [
5760
- `Woke ${agentId} (background job ${r.jobId ?? "(none)"}); it carries on with the previous context.`,
6624
+ `Woke ${name} (background job ${r.jobId ?? "(none)"}); it carries on with the previous context.`,
6625
+ ...name === agentId ? [] : [
6626
+ `(${agentId} is the session of member ${name}; address it by its name next time)`
6627
+ ],
5761
6628
  `See how far it got: task_output ${r.jobId ?? ""} or list_agents`
5762
6629
  ].join("\n"),
5763
6630
  start
5764
6631
  );
5765
6632
  }
6633
+ if (team.isClosed(agentId)) {
6634
+ return err2(
6635
+ 7214,
6636
+ `${agentId} was closed with close_agent, so it cannot be woken again \u2014 not even by its session id`,
6637
+ 'Closing means "I do not want it any more". If that work still matters, dispatch a new member with spawn_teammate and give it the context it needs'
6638
+ );
6639
+ }
6640
+ if (team.isSubagentSession(agentId)) {
6641
+ return err2(
6642
+ 7215,
6643
+ `${agentId} is a sub-agent session that is not on the roster \u2014 it is not a main session and cannot be woken`,
6644
+ "Wake a member by its name from list_agents. A closed member, or one dispatched by an earlier process, is gone for good"
6645
+ );
6646
+ }
5766
6647
  if (team.sessionExists(agentId)) {
5767
6648
  if (team.isSessionActive(agentId)) {
5768
6649
  return err2(
@@ -5773,6 +6654,14 @@ function followupTask(team, manager) {
5773
6654
  }
5774
6655
  const self = team.bySession(ctx.sessionId);
5775
6656
  const from = self ? self.agentId : ctx.sessionId;
6657
+ const targetDir = team.sessionWorkDir(agentId);
6658
+ if (!crossDirAllowed(ctx, targetDir)) {
6659
+ return err2(
6660
+ 7216,
6661
+ `${agentId} works in ${String(targetDir)}, outside this session's workspace (${ctx.workDir}) \u2014 a wake across directories needs a permission level that allows it, and this one is ${ctx.permissionLevel}`,
6662
+ "Below auto, an outside directory has to be approved by the user, and a wake cannot ask. Ask the user to open that session and send it there, or add the directory with --add-dir"
6663
+ );
6664
+ }
5776
6665
  const fromLabel = self ? void 0 : sessionSenderLabel(from, team.sessionTitle(from));
5777
6666
  const r = await team.wakeSession(
5778
6667
  agentId,
@@ -5801,6 +6690,7 @@ function createTeamTools(team, manager) {
5801
6690
  spawnTeammate(team, manager),
5802
6691
  waitAgent(team),
5803
6692
  interruptAgent(team),
6693
+ closeAgent(team),
5804
6694
  followupTask(team, manager),
5805
6695
  taskCreate(team),
5806
6696
  taskList(team),
@@ -5813,6 +6703,7 @@ var TEAM_TOOL_NAMES = Object.freeze([
5813
6703
  "list_agents",
5814
6704
  "wait_agent",
5815
6705
  "interrupt_agent",
6706
+ "close_agent",
5816
6707
  "followup_task",
5817
6708
  "team_task_create",
5818
6709
  "team_task_list",
@@ -5906,6 +6797,7 @@ function belowTrigramFloor(safeQuery) {
5906
6797
  }
5907
6798
  // src/session/trace.ts
5908
6799
  var NODE_COLUMNS = `id, title, cwd, started_at, message_count, archived, parent_id`;
6800
+ var SUBTREE_COLUMNS = `id, title, started_at, updated_at, archived, parent_id`;
5909
6801
  function toNode(ctx, row, depth) {
5910
6802
  const base2 = { sessionId: row.id, depth, parentId: row.parent_id ?? void 0 };
5911
6803
  if (!ctx.visible(row.id)) return { ...base2, visible: false };
@@ -5919,39 +6811,49 @@ function toNode(ctx, row, depth) {
5919
6811
  archived: row.archived === 1
5920
6812
  };
5921
6813
  }
5922
- function ancestorsOf(ctx, startParent) {
6814
+ function walkAncestors(startParent, byId, parentOf2) {
5923
6815
  const out = [];
5924
6816
  const seen = /* @__PURE__ */ new Set();
5925
6817
  let at = startParent;
5926
6818
  let depth = -1;
5927
6819
  while (at && !seen.has(at)) {
5928
6820
  seen.add(at);
5929
- const row = ctx.byId(at);
5930
- if (!row) break;
5931
- out.push(toNode(ctx, row, depth));
5932
- at = row.parent_id ?? void 0;
6821
+ const node = byId(at);
6822
+ if (!node) break;
6823
+ out.push({ node, depth });
6824
+ at = parentOf2(node);
5933
6825
  depth -= 1;
5934
6826
  }
5935
6827
  return out;
5936
6828
  }
5937
- function descendantsOf(ctx, rootId, limit) {
5938
- const nodes = [];
6829
+ function walkDescendants(rootId, children, idOf, limit) {
6830
+ const steps = [];
5939
6831
  const seen = /* @__PURE__ */ new Set([rootId]);
5940
6832
  let truncated = 0;
5941
6833
  const walk3 = (parentId, depth) => {
5942
- for (const row of ctx.children(parentId)) {
5943
- if (seen.has(row.id)) continue;
5944
- seen.add(row.id);
5945
- if (nodes.length >= limit) {
6834
+ for (const node of children(parentId)) {
6835
+ const id = idOf(node);
6836
+ if (seen.has(id)) continue;
6837
+ seen.add(id);
6838
+ if (steps.length >= limit) {
5946
6839
  truncated += 1;
5947
6840
  continue;
5948
6841
  }
5949
- nodes.push(toNode(ctx, row, depth));
5950
- walk3(row.id, depth + 1);
6842
+ steps.push({ node, depth });
6843
+ walk3(id, depth + 1);
5951
6844
  }
5952
6845
  };
5953
6846
  walk3(rootId, 1);
5954
- return { nodes, truncated };
6847
+ return { steps, truncated };
6848
+ }
6849
+ function ancestorsOf(ctx, startParent) {
6850
+ return walkAncestors(startParent, ctx.byId, (row) => row.parent_id ?? void 0).map(
6851
+ ({ node, depth }) => toNode(ctx, node, depth)
6852
+ );
6853
+ }
6854
+ function descendantsOf(ctx, rootId, limit) {
6855
+ const { steps, truncated } = walkDescendants(rootId, ctx.children, (row) => row.id, limit);
6856
+ return { nodes: steps.map(({ node, depth }) => toNode(ctx, node, depth)), truncated };
5955
6857
  }
5956
6858
  function traceSession(db, input) {
5957
6859
  const vis = resolveSessionVisibility({ ...input, db });
@@ -5980,6 +6882,37 @@ function traceSession(db, input) {
5980
6882
  truncated
5981
6883
  };
5982
6884
  }
6885
+ var SUBAGENT_TREE_LIMIT = 200;
6886
+ function sessionSubtree(db, input) {
6887
+ const byIdStmt = db.prepare(`SELECT ${SUBTREE_COLUMNS} FROM sessions WHERE id = @id`);
6888
+ const childrenStmt = db.prepare(
6889
+ `SELECT ${SUBTREE_COLUMNS} FROM sessions WHERE parent_id = @id ORDER BY started_at ASC`
6890
+ );
6891
+ const byId = (id) => byIdStmt.get({ id });
6892
+ const children = (parentId) => childrenStmt.all({ id: parentId });
6893
+ const self = byId(input.sessionId);
6894
+ if (!self) return { rootId: input.sessionId, nodes: [], truncated: 0 };
6895
+ const ancestors = walkAncestors(
6896
+ self.parent_id ?? void 0,
6897
+ byId,
6898
+ (row) => row.parent_id ?? void 0
6899
+ );
6900
+ const rootId = ancestors.at(-1)?.node.id ?? self.id;
6901
+ const { steps, truncated } = walkDescendants(rootId, children, (row) => row.id, input.limit);
6902
+ return {
6903
+ rootId,
6904
+ nodes: steps.map(({ node, depth }) => ({
6905
+ sessionId: node.id,
6906
+ ...node.parent_id ? { parentId: node.parent_id } : {},
6907
+ title: node.title,
6908
+ startedAt: node.started_at,
6909
+ ...node.updated_at === null ? {} : { updatedAt: node.updated_at },
6910
+ archived: node.archived === 1,
6911
+ depth
6912
+ })),
6913
+ truncated
6914
+ };
6915
+ }
5983
6916
  // src/session/types.ts
5984
6917
  var DELEGATE_SESSION_SOURCE = "delegate";
5985
6918
  // src/session/query.ts
@@ -7361,7 +8294,7 @@ function isOpenTodo(status) {
7361
8294
  return status === "pending" || status === "in_progress";
7362
8295
  }
7363
8296
  var NS = "tracker";
7364
- var MIGRATIONS = [
8297
+ var MIGRATIONS2 = [
7365
8298
  {
7366
8299
  id: "tracker-baseline",
7367
8300
  version: 1,
@@ -7396,7 +8329,7 @@ var TrackerDB = class {
7396
8329
  constructor(dbPath2) {
7397
8330
  this.path = dbPath2;
7398
8331
  this.db = openDatabase(this.path);
7399
- migrate(this.db, NS, MIGRATIONS);
8332
+ migrate(this.db, NS, MIGRATIONS2);
7400
8333
  }
7401
8334
  listBySession(sessionId) {
7402
8335
  const rows = this.db.prepare(
@@ -7560,262 +8493,6 @@ function createTodoTool(db) {
7560
8493
  }
7561
8494
  };
7562
8495
  }
7563
- var MAX_POST_CHARS = 2e4;
7564
- var CHANNEL_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
7565
- var MIGRATIONS2 = [
7566
- {
7567
- id: "board-initial",
7568
- version: 1,
7569
- description: "Discussion board: channels / threads / posts / subscriptions",
7570
- up: (db) => {
7571
- db.exec(`
7572
- CREATE TABLE IF NOT EXISTS board_channels (
7573
- id TEXT PRIMARY KEY,
7574
- name TEXT NOT NULL UNIQUE,
7575
- description TEXT,
7576
- created_by TEXT NOT NULL,
7577
- created_at INTEGER NOT NULL
7578
- );
7579
- CREATE TABLE IF NOT EXISTS board_threads (
7580
- id TEXT PRIMARY KEY,
7581
- channel TEXT NOT NULL,
7582
- title TEXT NOT NULL,
7583
- created_by TEXT NOT NULL,
7584
- created_at INTEGER NOT NULL,
7585
- last_post_at INTEGER NOT NULL
7586
- );
7587
- CREATE INDEX IF NOT EXISTS board_threads_channel ON board_threads(channel, last_post_at DESC);
7588
- CREATE TABLE IF NOT EXISTS board_posts (
7589
- seq INTEGER PRIMARY KEY AUTOINCREMENT,
7590
- id TEXT NOT NULL UNIQUE,
7591
- thread_id TEXT NOT NULL,
7592
- author TEXT NOT NULL,
7593
- text TEXT NOT NULL,
7594
- created_at INTEGER NOT NULL
7595
- );
7596
- CREATE INDEX IF NOT EXISTS board_posts_thread ON board_posts(thread_id, seq);
7597
- CREATE TABLE IF NOT EXISTS board_subscriptions (
7598
- agent TEXT NOT NULL,
7599
- target TEXT NOT NULL,
7600
- last_seen_seq INTEGER NOT NULL DEFAULT 0,
7601
- PRIMARY KEY (agent, target)
7602
- );
7603
- `);
7604
- }
7605
- }
7606
- ];
7607
- function targetKey(t83) {
7608
- return t83.kind === "channel" ? `channel:${t83.name}` : `thread:${t83.id}`;
7609
- }
7610
- var BoardStore = class {
7611
- path;
7612
- db;
7613
- constructor(homeDir) {
7614
- this.path = dbPath(homeDir);
7615
- this.db = openDatabase(this.path);
7616
- migrate(this.db, "board", MIGRATIONS2);
7617
- }
7618
- close() {
7619
- releaseDatabase(this.path);
7620
- }
7621
- createChannel(args) {
7622
- if (!CHANNEL_PATTERN.test(args.name)) {
7623
- throw new Error(
7624
- `Invalid channel name: ${args.name} (letters, digits, _ . - only, at most 64 characters)`
7625
- );
7626
- }
7627
- const existing = this.channel(args.name);
7628
- if (existing) return existing;
7629
- const row = {
7630
- id: `ch-${randomUUID().slice(0, 8)}`,
7631
- name: args.name,
7632
- ...args.description ? { description: args.description } : {},
7633
- createdBy: args.by,
7634
- createdAt: Date.now()
7635
- };
7636
- this.db.prepare(
7637
- `INSERT INTO board_channels (id, name, description, created_by, created_at)
7638
- VALUES (?, ?, ?, ?, ?)`
7639
- ).run(row.id, row.name, row.description ?? null, row.createdBy, row.createdAt);
7640
- return row;
7641
- }
7642
- upsertChannel(args) {
7643
- const existing = this.channel(args.name);
7644
- if (!existing) return this.createChannel(args);
7645
- this.db.prepare(`UPDATE board_channels SET description = ? WHERE name = ?`).run(args.description, args.name);
7646
- return { ...existing, description: args.description };
7647
- }
7648
- channel(name) {
7649
- const row = this.db.prepare(
7650
- `SELECT id, name, description, created_by, created_at FROM board_channels WHERE name = ?`
7651
- ).get(name);
7652
- return row ? toChannel(row) : void 0;
7653
- }
7654
- listChannels() {
7655
- const rows = this.db.prepare(
7656
- `SELECT id, name, description, created_by, created_at FROM board_channels ORDER BY name`
7657
- ).all();
7658
- return rows.map(toChannel);
7659
- }
7660
- listThreads(channel, limit = 20) {
7661
- const rows = this.db.prepare(
7662
- `SELECT t.id, t.channel, t.title, t.created_by, t.created_at, t.last_post_at,
7663
- (SELECT COUNT(*) FROM board_posts p WHERE p.thread_id = t.id) AS posts
7664
- FROM board_threads t WHERE t.channel = ? ORDER BY t.last_post_at DESC LIMIT ?`
7665
- ).all(channel, limit);
7666
- return rows.map((r) => ({
7667
- id: r.id,
7668
- channel: r.channel,
7669
- title: r.title,
7670
- createdBy: r.created_by,
7671
- createdAt: r.created_at,
7672
- lastPostAt: r.last_post_at,
7673
- postCount: r.posts
7674
- }));
7675
- }
7676
- post(args) {
7677
- return this.write({ ...args, notify: true });
7678
- }
7679
- append(args) {
7680
- return this.write({ ...args, notify: false });
7681
- }
7682
- write(args) {
7683
- const text = clip(args.text);
7684
- const now = Date.now();
7685
- const run = this.db.transaction(() => {
7686
- let threadId;
7687
- if (args.target.kind === "thread") {
7688
- const exists2 = this.db.prepare(`SELECT id FROM board_threads WHERE id = ?`).get(args.target.id);
7689
- if (!exists2) throw new Error(`No such thread: ${args.target.id}`);
7690
- threadId = args.target.id;
7691
- } else {
7692
- const ch = this.channel(args.target.name);
7693
- if (!ch) throw new Error(`No such channel: ${args.target.name}`);
7694
- threadId = `th-${randomUUID().slice(0, 8)}`;
7695
- this.db.prepare(
7696
- `INSERT INTO board_threads (id, channel, title, created_by, created_at, last_post_at)
7697
- VALUES (?, ?, ?, ?, ?, ?)`
7698
- ).run(threadId, ch.name, firstLine(text), args.author, now, now);
7699
- }
7700
- const id = `po-${randomUUID().slice(0, 8)}`;
7701
- const info = this.db.prepare(
7702
- `INSERT INTO board_posts (id, thread_id, author, text, created_at) VALUES (?, ?, ?, ?, ?)`
7703
- ).run(id, threadId, args.author, text, now);
7704
- const seq = Number(info.lastInsertRowid);
7705
- this.db.prepare(`UPDATE board_threads SET last_post_at = ? WHERE id = ?`).run(now, threadId);
7706
- if (args.notify) {
7707
- this.subscribe(args.author, { kind: "thread", id: threadId }, seq);
7708
- }
7709
- return {
7710
- post: { id, threadId, author: args.author, text, createdAt: now, seq },
7711
- threadId
7712
- };
7713
- });
7714
- return run();
7715
- }
7716
- readThread(threadId, limit = 50) {
7717
- const rows = this.db.prepare(
7718
- `SELECT seq, id, thread_id, author, text, created_at FROM board_posts
7719
- WHERE thread_id = ? ORDER BY seq ASC LIMIT ?`
7720
- ).all(threadId, limit);
7721
- return rows.map(toPost);
7722
- }
7723
- readPost(id) {
7724
- const row = this.db.prepare(`SELECT seq, id, thread_id, author, text, created_at FROM board_posts WHERE id = ?`).get(id);
7725
- return row ? toPost(row) : void 0;
7726
- }
7727
- searchPosts(query, channel, limit = 20) {
7728
- const like = `%${query}%`;
7729
- const rows = channel ? this.db.prepare(
7730
- `SELECT p.seq, p.id, p.thread_id, p.author, p.text, p.created_at
7731
- FROM board_posts p JOIN board_threads t ON t.id = p.thread_id
7732
- WHERE t.channel = ? AND p.text LIKE ? ORDER BY p.seq DESC LIMIT ?`
7733
- ).all(channel, like, limit) : this.db.prepare(
7734
- `SELECT seq, id, thread_id, author, text, created_at FROM board_posts
7735
- WHERE text LIKE ? ORDER BY seq DESC LIMIT ?`
7736
- ).all(like, limit);
7737
- return rows.map(toPost);
7738
- }
7739
- subscribe(agent, target, seenSeq) {
7740
- const key = targetKey(target);
7741
- const initial = seenSeq ?? this.maxSeq();
7742
- this.db.prepare(
7743
- `INSERT INTO board_subscriptions (agent, target, last_seen_seq) VALUES (?, ?, ?)
7744
- ON CONFLICT(agent, target) DO UPDATE SET
7745
- last_seen_seq = MAX(board_subscriptions.last_seen_seq, excluded.last_seen_seq)`
7746
- ).run(agent, key, initial);
7747
- }
7748
- maxSeq() {
7749
- const row = this.db.prepare(`SELECT COALESCE(MAX(seq), 0) AS n FROM board_posts`).get();
7750
- return row.n;
7751
- }
7752
- unsubscribe(agent, target) {
7753
- const info = this.db.prepare(`DELETE FROM board_subscriptions WHERE agent = ? AND target = ?`).run(agent, targetKey(target));
7754
- return info.changes > 0;
7755
- }
7756
- pendingFor(agent) {
7757
- const subs = this.db.prepare(`SELECT target, last_seen_seq FROM board_subscriptions WHERE agent = ?`).all(agent);
7758
- const out = [];
7759
- for (const s of subs) {
7760
- const count = this.countSince(s.target, s.last_seen_seq);
7761
- if (count > 0) out.push({ target: s.target, count });
7762
- }
7763
- return out;
7764
- }
7765
- markRead(agent, target) {
7766
- if (target) {
7767
- const key = targetKey(target);
7768
- this.db.prepare(
7769
- `UPDATE board_subscriptions SET last_seen_seq = (SELECT MAX(seq) FROM board_posts) WHERE agent = ? AND target = ?`
7770
- ).run(agent, key);
7771
- return;
7772
- }
7773
- this.db.prepare(
7774
- `UPDATE board_subscriptions SET last_seen_seq = (SELECT COALESCE(MAX(seq), 0) FROM board_posts) WHERE agent = ?`
7775
- ).run(agent);
7776
- }
7777
- countSince(target, sinceSeq) {
7778
- if (target.startsWith("thread:")) {
7779
- const id = target.slice("thread:".length);
7780
- const row2 = this.db.prepare(`SELECT COUNT(*) AS n FROM board_posts WHERE thread_id = ? AND seq > ?`).get(id, sinceSeq);
7781
- return row2.n;
7782
- }
7783
- const name = target.slice("channel:".length);
7784
- const row = this.db.prepare(
7785
- `SELECT COUNT(*) AS n FROM board_posts p JOIN board_threads t ON t.id = p.thread_id
7786
- WHERE t.channel = ? AND p.seq > ?`
7787
- ).get(name, sinceSeq);
7788
- return row.n;
7789
- }
7790
- };
7791
- function clip(text) {
7792
- if (text.length <= MAX_POST_CHARS) return text;
7793
- return `${text.slice(0, MAX_POST_CHARS)}
7794
- \u2026 (truncated; the original is ${text.length} characters)`;
7795
- }
7796
- function firstLine(text) {
7797
- const line = text.split("\n", 1)[0] ?? text;
7798
- return line.length > 80 ? `${line.slice(0, 80)}\u2026` : line;
7799
- }
7800
- function toChannel(r) {
7801
- return {
7802
- id: r.id,
7803
- name: r.name,
7804
- ...r.description ? { description: r.description } : {},
7805
- createdBy: r.created_by,
7806
- createdAt: r.created_at
7807
- };
7808
- }
7809
- function toPost(r) {
7810
- return {
7811
- seq: r.seq,
7812
- id: r.id,
7813
- threadId: r.thread_id,
7814
- author: r.author,
7815
- text: r.text,
7816
- createdAt: r.created_at
7817
- };
7818
- }
7819
8496
  var PlanModeState = class {
7820
8497
  constructor(permission2) {
7821
8498
  this.permission = permission2;
@@ -17446,15 +18123,15 @@ function toEntry2(m) {
17446
18123
  return {
17447
18124
  id,
17448
18125
  name: String(m.name ?? id),
17449
- contextLength: num(m.context_length) ?? num(top?.context_length) ?? DEFAULT_CONTEXT_LENGTH2,
17450
- maxOutputTokens: num(top?.max_completion_tokens) ?? 4096,
18126
+ contextLength: num2(m.context_length) ?? num2(top?.context_length) ?? DEFAULT_CONTEXT_LENGTH2,
18127
+ maxOutputTokens: num2(top?.max_completion_tokens) ?? 4096,
17451
18128
  pricing: m.pricing,
17452
18129
  supportsTools: params.includes("tools"),
17453
18130
  supportsImages: inputs.includes("image"),
17454
18131
  supportsReasoning: params.includes("reasoning")
17455
18132
  };
17456
18133
  }
17457
- function num(v) {
18134
+ function num2(v) {
17458
18135
  return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
17459
18136
  }
17460
18137
  function pricingFor(modelId, homeDir, at = Date.now()) {
@@ -20782,8 +21459,8 @@ function parseParts(json) {
20782
21459
  ...sessionId ? { sessionId } : {},
20783
21460
  ...title ? { title } : {},
20784
21461
  ...omitted && isSessionOmitReason(omitted) ? { omitted } : {},
20785
- ...num2(item["messageCount"]) === void 0 ? {} : { messageCount: num2(item["messageCount"]) },
20786
- ...num2(item["totalMessages"]) === void 0 ? {} : { totalMessages: num2(item["totalMessages"]) }
21462
+ ...num3(item["messageCount"]) === void 0 ? {} : { messageCount: num3(item["messageCount"]) },
21463
+ ...num3(item["totalMessages"]) === void 0 ? {} : { totalMessages: num3(item["totalMessages"]) }
20787
21464
  });
20788
21465
  continue;
20789
21466
  }
@@ -20795,7 +21472,7 @@ function parseParts(json) {
20795
21472
  type: "file",
20796
21473
  path: filePath,
20797
21474
  ...fileOmitted && isFileOmitReason(fileOmitted) ? { omitted: fileOmitted } : {},
20798
- ...num2(item["bytes"]) === void 0 ? {} : { bytes: num2(item["bytes"]) }
21475
+ ...num3(item["bytes"]) === void 0 ? {} : { bytes: num3(item["bytes"]) }
20799
21476
  });
20800
21477
  continue;
20801
21478
  }
@@ -20813,19 +21490,19 @@ function isFileOmitReason(value) {
20813
21490
  function encodeColumn(value) {
20814
21491
  return value && value.length > 0 ? JSON.stringify(value) : null;
20815
21492
  }
20816
- function num2(v) {
21493
+ function num3(v) {
20817
21494
  return typeof v === "number" && Number.isFinite(v) ? v : void 0;
20818
21495
  }
20819
21496
  function parseUsage(raw) {
20820
21497
  if (!isRecord4(raw)) return void 0;
20821
- const inputTokens = num2(raw["inputTokens"]);
20822
- const outputTokens = num2(raw["outputTokens"]);
21498
+ const inputTokens = num3(raw["inputTokens"]);
21499
+ const outputTokens = num3(raw["outputTokens"]);
20823
21500
  if (inputTokens === void 0 || outputTokens === void 0) return void 0;
20824
- const cacheHitTokens = num2(raw["cacheHitTokens"]);
20825
- const cacheWriteTokens = num2(raw["cacheWriteTokens"]);
20826
- const reasoningTokens = num2(raw["reasoningTokens"]);
20827
- const totalTokens2 = num2(raw["totalTokens"]);
20828
- const costUsd = num2(raw["costUsd"]);
21501
+ const cacheHitTokens = num3(raw["cacheHitTokens"]);
21502
+ const cacheWriteTokens = num3(raw["cacheWriteTokens"]);
21503
+ const reasoningTokens = num3(raw["reasoningTokens"]);
21504
+ const totalTokens2 = num3(raw["totalTokens"]);
21505
+ const costUsd = num3(raw["costUsd"]);
20829
21506
  return {
20830
21507
  inputTokens,
20831
21508
  outputTokens,
@@ -20845,12 +21522,12 @@ function parseTurnFacts(json) {
20845
21522
  return void 0;
20846
21523
  }
20847
21524
  if (!isRecord4(raw)) return void 0;
20848
- const toolCalls = num2(raw["toolCalls"]);
21525
+ const toolCalls = num3(raw["toolCalls"]);
20849
21526
  if (toolCalls === void 0) return void 0;
20850
21527
  const usage = parseUsage(raw["usage"]);
20851
21528
  const model = str5(raw["model"]);
20852
21529
  const requestedModel = str5(raw["requestedModel"]);
20853
- const durationMs = num2(raw["durationMs"]);
21530
+ const durationMs = num3(raw["durationMs"]);
20854
21531
  const error = str5(raw["error"])?.trim() || void 0;
20855
21532
  return {
20856
21533
  toolCalls,
@@ -21276,6 +21953,10 @@ var SessionDB = class {
21276
21953
  hasSession(id) {
21277
21954
  return this.db.prepare("SELECT 1 FROM sessions WHERE id = ? LIMIT 1").get(id) !== void 0;
21278
21955
  }
21956
+ subagentTree(fromId, limit = SUBAGENT_TREE_LIMIT) {
21957
+ const tree = sessionSubtree(this.db, { sessionId: fromId, limit });
21958
+ return { ...tree, nodes: tree.nodes.filter((n) => !n.archived) };
21959
+ }
21279
21960
  listSessions(input) {
21280
21961
  const limit = input.limit ?? 20;
21281
21962
  const key = input.sortKey ?? "startedAt";
@@ -21918,15 +22599,18 @@ function listSessionCandidates(db, input) {
21918
22599
  const filter = query ? `AND (s.id LIKE @refLike ESCAPE '\\' OR s.title LIKE @refLike ESCAPE '\\'
21919
22600
  OR COALESCE(s.cwd, '') LIKE @refLike ESCAPE '\\')` : "";
21920
22601
  const affinity = input.workspaceCwd ? "(s.cwd = @refCwd) DESC," : "";
22602
+ const noDelegate = "AND COALESCE(s.source, '') != @delegateSource";
21921
22603
  const rows = db.prepare(
21922
22604
  `
21923
22605
  SELECT s.id, s.title, s.cwd, s.updated_at, s.started_at, s.message_count
21924
22606
  FROM sessions s
21925
- WHERE s.archived = 0 AND s.id <> @refCurrent AND ${vis.clause} ${filter}
22607
+ WHERE s.archived = 0 AND s.id <> @refCurrent AND ${vis.clause}
22608
+ ${noDelegate} ${filter}
21926
22609
  ORDER BY ${affinity} COALESCE(s.updated_at, s.started_at) DESC
21927
22610
  LIMIT @refLimit`
21928
22611
  ).all({
21929
22612
  ...vis.params,
22613
+ delegateSource: DELEGATE_SESSION_SOURCE,
21930
22614
  refCurrent: input.currentSessionId,
21931
22615
  refLimit: input.limit,
21932
22616
  ...input.workspaceCwd ? { refCwd: input.workspaceCwd } : {},
@@ -21991,8 +22675,13 @@ function uniqueOf(db, where, params, vis) {
21991
22675
  const rows = db.prepare(
21992
22676
  `SELECT s.id FROM sessions s
21993
22677
  WHERE s.archived = 0 AND ${where} AND ${vis.clause}
22678
+ AND COALESCE(s.source, '') != @delegateSource
21994
22679
  LIMIT 2`
21995
- ).all({ ...vis.params, ...params });
22680
+ ).all({
22681
+ ...vis.params,
22682
+ ...params,
22683
+ delegateSource: DELEGATE_SESSION_SOURCE
22684
+ });
21996
22685
  if (rows.length !== 1) return void 0;
21997
22686
  return { sessionId: rows[0]?.id };
21998
22687
  }
@@ -22170,6 +22859,9 @@ var SessionManager = class {
22170
22859
  has(id) {
22171
22860
  return this.db.hasSession(id);
22172
22861
  }
22862
+ subagentTree(fromId, limit) {
22863
+ return this.db.subagentTree(fromId, limit);
22864
+ }
22173
22865
  getLatest() {
22174
22866
  const result = this.db.listSessions({ limit: 1, sortKey: "updatedAt", direction: "desc" });
22175
22867
  if (result.sessions.length === 0) return void 0;
@@ -28706,21 +29398,19 @@ function hasDedicatedExecContent(inv) {
28706
29398
  * - 去掉 proper-lockfile 依赖,写入改成「临时文件 + rename」原子替换;
28707
29399
  * - 读取失败不抛致命错误,而是记进 `loadErrors` 交装配层写进启动诊断。
28708
29400
  *
28709
- * ## 两条**明确不做**(2026-08-08 定,不是待办)
29401
+ * ## 两条**明确不做**(不是待办)
28710
29402
  *
28711
- * 1. **符号链接不做全局归一。** 这里只 `resolve()`,不 `realpathSync()`
28712
- * (gemini-cli 会)。于是「经软链进入」和「经真实路径进入」是两个目录 ——
28713
- * 但**两侧都 fail closed**(判成 `unknown` → 不加载),是可用性问题不是安全
28714
- * 问题。真会踩到的那种(用户手写 `epoch trust add /tmp/proj`,判定侧拿到的
28715
- * `process.cwd()` 却是物理路径)已经在 CLI 层用 `physicalPath()` 堵掉了;
28716
- * 剩下的缺口只有「SDK 调用方给 `buildRuntime({ workDir })` 传逻辑路径」。
28717
- * 为这一种在判定热路径上加系统调用不划算。
28718
- * 2. **信任按路径存,不认仓库身份。** `trust add --tree ~/code` 之后,以后
28719
- * clone 进 `~/code` 的任何仓库都直接可信;删掉再在同一路径 clone 另一个也继承。
28720
- * gemini-cli 同样如此。能做的方向是记下 git remote 或首次信任时指令文件的
28721
- * 内容 hash,变了就重新问 —— 代价是「改一行 AGENTS.md 就被打断一次」,
28722
- * 这个打断比它挡住的风险更贵。代价写在根 README 的「工作区信任」一节和
28723
- * `--tree` 的命令帮助里,让用户自己知道 `--tree` 意味着什么。
29403
+ * 1. **符号链接不做全局归一。** 这里只 `resolve()`,不 `realpathSync()`,于是「经软链
29404
+ * 进入」和「经真实路径进入」是两个目录 —— 但**两侧都 fail closed**(判成 `unknown`
29405
+ * → 不加载),是可用性问题不是安全问题。真会踩到的那种(CLI 手写 `epoch trust add
29406
+ * /tmp/proj`,判定侧拿到的 `process.cwd()` 却是物理路径)已在 CLI 层用
29407
+ * `physicalPath()` 堵掉;剩下的缺口只有「SDK 调用方给 `buildRuntime({ workDir })`
29408
+ * 传逻辑路径」,为这一种在判定热路径上加系统调用不划算。
29409
+ * 2. **信任按路径存,不认仓库身份。** `trust add --tree ~/code` 之后,以后 clone 进
29410
+ * `~/code` 的任何仓库都直接可信;删掉再在同一路径 clone 另一个也继承。能做的方向
29411
+ * 是记下 git remote 或首次信任时指令文件的内容 hash,变了就重新问 —— 代价是「改一行
29412
+ * AGENTS.md 就被打断一次」,这个打断比它挡住的风险更贵。代价写在根 README 的
29413
+ * 「工作区信任」一节和 `--tree` 的命令帮助里,让用户自己知道 `--tree` 意味着什么。
28724
29414
  */
28725
29415
 
28726
- export { ABOUT_DESC_MAX_CHARS, ABOUT_README_MAX_CHARS, AGENT_ROLE_DIAG_MODULE, ARTIFACT_MAX_TOTAL_BYTES, ARTIFACT_RETENTION_DEFAULTS, ATTACHMENT_MAX_TOTAL_BYTES, ATTACHMENT_RETENTION_DEFAULTS, AgentInbox, AgentLoop, AppearanceStore, ApprovalCache, BLOCKED_TOOLS, BOARD_TOOL_NAMES, BoardStore, BudgetGuard, BudgetStore, CHECKPOINT_MAX_TOTAL_BYTES, CLAUDE_EVENT_NAMES, CLAUDE_TOOL_NAMES, CODE_MODE_LIMITS, CODE_MODE_SDK_TAG, CONFIG_KEYS, CURRENT_CONFIG_VERSION, CheckpointManager, CheckpointStore, CodeSandbox, ComplianceEngine, ContextCompressor, DEFAULT_COMPLIANCE_ACTIONS, DEFAULT_COMPLIANCE_SETTINGS, DEFAULT_COMPRESSION_THRESHOLD, DEFAULT_GOAL_MAX_ROUNDS, DEFAULT_INBOX_CAPACITY, DEFAULT_MAX_HOLD_CHARS, DEFAULT_MAX_RUNNING_MEMBERS, DEFAULT_MAX_WOKEN_SESSIONS, DELEGATE_SESSION_SOURCE, DelegateManager, EMPTY_RULES, EPOCH_DISK_LEDGER, EpochConfigSchema, FTS_TABLES, FTS_TRIGGERS, GOAL_BLOCK_CODES, GoalService, GoalStore, HEADLESS_AUDIT_CODES, HookManager, INSTRUCTION_BUDGET_CHARS, INSTRUCTION_FILES, ImageLoadError, ImportTrustStore, LAUNCHD_LABEL_PREFIX, LOCK_HEARTBEAT_MS, LOCK_STALE_MS, LastOpenSessions, MARKETPLACE_DISPLAY_FIELDS, MARKETPLACE_FILE, MAX_AUDIT_ENTRIES, MAX_COMMAND_DEPTH, MAX_GOAL_MAX_ROUNDS, MAX_IMPORT_DEPTH, MAX_LAST_OPEN_SESSIONS, MAX_OBJECTIVE_CHARS, MAX_POST_CHARS, MAX_REASON_CHARS, MAX_RECENT_WORKSPACES, MODEL_ENTRIES_FILE, MODEL_GIVE_UP_THRESHOLD, MULTIMODAL_DEFAULTS, ManagedPolicyError, ManagedSettingsSchema, MemoryManager, ModelFailureTracker, NOMINAL_TOTAL_BYTES, NO_REVEALED_TOOLS, PACK_EXCLUDES, PERMISSION_AUDIT_CODES, PLUGIN_ICON_MAX_BYTES, PLUGIN_LAYOUT, PLUGIN_MANIFEST_FILE, PLUGIN_MANIFEST_KEYS, PLUGIN_NAME_PATTERN, PLUGIN_ROOT_ENV, PLUGIN_ROOT_TOKEN, PLUGIN_VISIBILITY, POLICY_DECISIONS, PROBE_VERSION, PROJECT_AGENTS_SUBDIR, PROVIDER_NO_CREDENTIAL, PermissionManager, PlanModeState, PluginAuthorSchema, PluginStore, PolicyRuleSchema, ProviderError, ProviderRouter, RECORDING_KEEP_RUNS, RECORDING_MAX_BYTES, ROLE_FRONTMATTER_SCHEMA, RUN_CODE_TOOL, RecentWorkspaces, SANDBOX_COVERS, SANDBOX_EXCLUDES, SCALAR_KEYS, SCHEDULE_DEFAULTS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_ISSUE_CODES, SCHTASKS_FOLDER, SEND_MESSAGE_TOOL, SESSION_MESSAGES_READ_TOOL, SESSION_MESSAGES_SEARCH_TOOL, SESSION_QUERY_TOOLS, SESSION_SEARCH_TOOL, SESSION_TRACE_TOOL, SETTINGS_LAYER_ORDER, SETTING_WRITE_KEYS, SETTING_WRITE_LAYERS, SKILL_ASSET_DIRS, SKILL_CREATE_TOOL, SKILL_EDIT_TOOL, SKILL_FILE, SKILL_PATCH_TOOL, SKILL_VIEW_TOOL, SKILL_WRITE_FILE_TOOL, SLASH_COMMANDS_DIAG_MODULE, ScheduleRecorder, ScheduleRegistrar, ScheduleStore, SessionManager, SessionQueries, SessionReferences, SettingsFileError, SettingsFileSchema, SkillLearner, SkillSystem, TEAM_TOOL_NAMES, TOOL_SEARCH_TOOL, TaskNoticeTracker, TeamRegistry, ToolRegistry, TrackerDB, TrustManager, UNSETTABLE_USER_KEYS, Workspace, acquireScheduleLock, addMarketplace, addUsage, admitArtifact, admitArtifacts, affectedPaths, approvalEntryFor, approvalRootFor, artifactLaunchArgv, artifactOpenRefusal, assertLevelAllowed, blockedRoleTools, buildContextBreakdown, buildFireArgv, buildFtsMatchQuery, buildSchemaExports, buildSystemPrompt, buildTrigramMatchQuery, capabilityOfRef, checkConfigValue, checkEpochVersion, checkFtsHealth, checkShadowing, classifyPersistFailure, classifyProviderError, clearModelCache, coalesceRecording, codeModeWorkerPath, commandNameFromRelPath, compileRuleLists, compressionOptions, computeCost, computeCostMicros, computeExecHash, configKeyByPath, configKeyTable, countRules, createAllowAllGate, createAllowAllImportGate, createAskQuestionTool, createBoardTools, createCodeExecTool, createDelegateTool, createDenyAllGate, createDenyAllImportGate, createGoalTool, createImportGate, createMemoryTool, createPlanModeTools, createPluginScope, createRoleFile, createRoleScope, createRunCodeTool, createScheduleDraftTool, createSendMessageTool, createSessionMessagesReadTool, createSessionMessagesSearchTool, createSessionSearchTool, createSessionSearchTools, createSessionTraceTool, createSkillViewTool, createSkillWriteTools, createTeamTools, createTodoTool, createToolRevealState, createToolScope, createToolSearchTool, createTrustGate, currentEpochVersion, decideImport, decideTrust, defaultConfig, describeArtifactRefusal, describeBackgroundTask, describeHeadlessPolicy, describeIsolation, describeJsonError, diagnoseSchedules, discoverMarkdown, discoverModels, discoverProject, discoverySpec, displayAllows, displayCurrency, draftCommandLine, draftWidening, escapeRuleContent, estimateImageTokens, estimateRunTokens, estimateTokens, evaluateSelection, expandCommand, expandImports, explainSetting, extractArchive, findInstructionFiles, findProjectInstructions, findRole, formatBytes, formatSkillIndex, formatUsd, gateForTool, getCapability, getConfigIssues, getConfigOrigins, getImportGate, getTrustGate, grantedPathsFor, hasDedicatedExecContent, hasExecTierContent, hasPromptCaching, hasProviderCredentials, hasReadTierContent, hasUsableModel, imageFromBase64, imageFromPath, imagePartTokens, importParts, importSkills, installPlugin, instructionDirs, instructionNotices, intervalSlots, inventoryProjectExtensions, inventoryTotal, isBlockCode, isCodeModeDeferred, isEmptyRules, isGoalPhase, isNonInteractive, isReadOnlySubCall, isSettingWriteLayer, isTransientPersistFailure, isValidInterval, isValidPluginName, isWithinWindow, isWritableSettingKey, labelSlug, layerRank, listEntryModels, listRunningSchedules, listSessionCandidates, listWorkspaceFiles, loadCommandDefinitions, loadCommandFile, loadCommands, loadConfig, loadHookSources, loadKeybindings, loadLexiconDir, loadLexiconFiles, loadModelEntries, loadPlugins, loadPolicyFiles, loadPolicyRules, loadRoleDefinitions, loadRoleDir, loadedImports, localDateString, managedSwitchNotes, mapClaudeToolName, mapEpochToolName, mapOperationType, matchPreauthorization, matchRuleLists, matchesHook, mentionsPluginRoot, mergeHeadlessPolicy, mergeRoles, mergeSchedulePatch, microsToUsd, modelEntriesPath, modelListCandidates, needsAllowlist, nextRunAt, noConfigOrigins, noManagedPolicy, noPlugins, normalizeModelEntry, normalizeTrigger, offloadOldestImages, openArtifact, originOf, packFileName, packPlugin, parseClaudeSettingsHooks, parseClock, parseDate, parseFrontmatter, parseHooksConfig, parseHooksConfigVerbose, parseImportLine, parseModelEntry, parseModelList, parseRoleDefinition, parseScheduleDraft, parseSkillFrontmatter, parseTrigger, pendingImageTokens, persistFailureCode, persistParts, persistToolResults, persistedUsage, planPack, previewSkillImport, pricingFor, projectEntryForDisplay, pruneArtifacts, pruneAttachments, pruneRecordings, pruneSchedules, readCancelRequest, readEntryKey, readImageSize, readMarketplaces, readPluginAbout, readPluginIcon, readPluginManifest, readPluginRecords, readRecording, readSessionSurface, reclaimFtsSpace, redactLine, refreshOpenRouterMetadata, removeMarketplace, removeRecordings, removeRoleFile, removeSessionArtifacts, removeSessionAttachments, renderImportTree, renderProjectContext, renderSdkDeclaration, renderSelfIndex, repairSchedules, requestImageBytes, requestScheduleCancel, resolveArtifactRetention, resolveCodeModeWorker, resolveComplianceDirs, resolveEntrySource, resolveInWorkspace, resolveMarketplaceRef, resolveMentions, resolveMultimodalPolicy, resolvePolicyDirs, resolvePricingAt, resolveProjectRoot, resolveScheduleBackend, resolveSessionReference, resolveSessionVisibility, resolveSettings, resolveSkillDirs, resolveSource, resolveTar, resolveWorkspace, restoreParts, revealScopedProvider, roleScopedProvider, roleSourceLabel, ruleCovers, ruleFromString, ruleToString, runCode, runCommand2 as runCommand, sanitizeArgNames, sanitizeCommand, sanitizeFtsQuery, sanitizePath, sanitizeToolOutput, saveModelEntries, scanInstructions, scanPluginDir, scanSkillDirs, searchMarketplaces, setDisplayCurrency, setImportGate, setPluginEnabled, setTrustGate, settingValueChoices, settingValueKind, settingWriteApply, settingWriteLayers, settingWriteTargets, skillIndexDescription, skillIndexResidency, skillIndexStats, sniffMediaType, splitFrontmatter, staticToolProvider, stripArtifactData, substitutePluginRoot, suggestRuleFromApproval, systemOpenArgv, systemPromptSegments, testModelEntry, toPosix, toolDefinitionTokens, toolMatches, toolTableTokens, totalTokens, traceSession, triggerPeriodMs, unescapeRuleContent, uninstallPlugin, unknownRoleSkills, unknownRoleTools, unloadedInstructionFiles, unsetUserSetting, updateMarketplace, updatePlugin, usdToMicros, validateSchedule, validateTrigger, validateWindow, withGoalCompression, withGoalPrompt, workspaceDisplayPath, writableConfigKeys, writeSettingValue };
29416
+ export { ABOUT_DESC_MAX_CHARS, ABOUT_README_MAX_CHARS, AGENT_ROLE_DIAG_MODULE, ARTIFACT_MAX_TOTAL_BYTES, ARTIFACT_RETENTION_DEFAULTS, ATTACHMENT_MAX_TOTAL_BYTES, ATTACHMENT_RETENTION_DEFAULTS, AgentInbox, AgentLoop, AppearanceStore, ApprovalCache, BLOCKED_TOOLS, BOARD_TOOL_NAMES, BoardStore, BudgetGuard, BudgetStore, CHECKPOINT_MAX_TOTAL_BYTES, CLAUDE_EVENT_NAMES, CLAUDE_TOOL_NAMES, CLOSED_TOMBSTONE_LIMIT, CODE_MODE_LIMITS, CODE_MODE_SDK_TAG, CONFIG_KEYS, CURRENT_CONFIG_VERSION, CheckpointManager, CheckpointStore, CodeSandbox, ComplianceEngine, ContextCompressor, DEFAULT_COMPLIANCE_ACTIONS, DEFAULT_COMPLIANCE_SETTINGS, DEFAULT_COMPRESSION_THRESHOLD, DEFAULT_GOAL_MAX_ROUNDS, DEFAULT_INBOX_CAPACITY, DEFAULT_MAX_HOLD_CHARS, DEFAULT_MAX_RUNNING_MEMBERS, DEFAULT_MAX_WOKEN_SESSIONS, DELEGATE_SESSION_SOURCE, DelegateManager, EMPTY_RULES, EPOCH_DISK_LEDGER, EpochConfigSchema, FTS_TABLES, FTS_TRIGGERS, GOAL_BLOCK_CODES, GoalService, GoalStore, HEADLESS_AUDIT_CODES, HookManager, INSTRUCTION_BUDGET_CHARS, INSTRUCTION_FILES, ImageLoadError, ImportTrustStore, LAUNCHD_LABEL_PREFIX, LOCK_HEARTBEAT_MS, LOCK_STALE_MS, LastOpenSessions, MARKETPLACE_DISPLAY_FIELDS, MARKETPLACE_FILE, MAX_AUDIT_ENTRIES, MAX_COMMAND_DEPTH, MAX_GOAL_MAX_ROUNDS, MAX_IMPORT_DEPTH, MAX_LAST_OPEN_SESSIONS, MAX_OBJECTIVE_CHARS, MAX_POST_CHARS, MAX_REASON_CHARS, MAX_RECENT_WORKSPACES, MODEL_ENTRIES_FILE, MODEL_GIVE_UP_THRESHOLD, MULTIMODAL_DEFAULTS, ManagedPolicyError, ManagedSettingsSchema, MemoryManager, ModelFailureTracker, NOMINAL_TOTAL_BYTES, NO_REVEALED_TOOLS, PACK_EXCLUDES, PERMISSION_AUDIT_CODES, PLUGIN_ICON_MAX_BYTES, PLUGIN_LAYOUT, PLUGIN_MANIFEST_FILE, PLUGIN_MANIFEST_KEYS, PLUGIN_NAME_PATTERN, PLUGIN_ROOT_ENV, PLUGIN_ROOT_TOKEN, PLUGIN_VISIBILITY, POLICY_DECISIONS, PROBE_VERSION, PROJECT_AGENTS_SUBDIR, PROVIDER_NO_CREDENTIAL, PermissionManager, PlanModeState, PluginAuthorSchema, PluginStore, PolicyRuleSchema, ProviderError, ProviderRouter, RECORDING_KEEP_RUNS, RECORDING_MAX_BYTES, ROLE_FRONTMATTER_SCHEMA, RUN_CODE_TOOL, RecentWorkspaces, SANDBOX_COVERS, SANDBOX_EXCLUDES, SCALAR_KEYS, SCHEDULE_DEFAULTS, SCHEDULE_DEFAULT_ALLOWLIST, SCHEDULE_ISSUE_CODES, SCHTASKS_FOLDER, SEND_MESSAGE_TOOL, SESSION_MESSAGES_READ_TOOL, SESSION_MESSAGES_SEARCH_TOOL, SESSION_QUERY_TOOLS, SESSION_SEARCH_TOOL, SESSION_TRACE_TOOL, SETTINGS_LAYER_ORDER, SETTING_WRITE_KEYS, SETTING_WRITE_LAYERS, SKILL_ASSET_DIRS, SKILL_CREATE_TOOL, SKILL_EDIT_TOOL, SKILL_FILE, SKILL_PATCH_TOOL, SKILL_VIEW_TOOL, SKILL_WRITE_FILE_TOOL, SLASH_COMMANDS_DIAG_MODULE, SUBAGENT_TREE_LIMIT, ScheduleRecorder, ScheduleRegistrar, ScheduleStore, SessionManager, SessionQueries, SessionReferences, SettingsFileError, SettingsFileSchema, SkillLearner, SkillSystem, TEAM_TOOL_NAMES, TOOL_SEARCH_TOOL, TaskNoticeTracker, TeamRegistry, ToolRegistry, TrackerDB, TrustManager, UNSETTABLE_USER_KEYS, Workspace, acquireScheduleLock, addMarketplace, addUsage, admitArtifact, admitArtifacts, affectedPaths, approvalEntryFor, approvalRootFor, artifactLaunchArgv, artifactOpenRefusal, assertLevelAllowed, blockedRoleTools, buildContextBreakdown, buildFireArgv, buildFtsMatchQuery, buildSchemaExports, buildSystemPrompt, buildTrigramMatchQuery, capabilityOfRef, checkConfigValue, checkEpochVersion, checkFtsHealth, checkShadowing, classifyPersistFailure, classifyProviderError, clearModelCache, coalesceRecording, codeModeWorkerPath, commandNameFromRelPath, compileRuleLists, compressionOptions, computeCost, computeCostMicros, computeExecHash, configKeyByPath, configKeyTable, countRules, createAllowAllGate, createAllowAllImportGate, createAskQuestionTool, createBoardTools, createCodeExecTool, createDelegateTool, createDenyAllGate, createDenyAllImportGate, createGoalTool, createImportGate, createMemoryTool, createPlanModeTools, createPluginScope, createRoleFile, createRoleScope, createRunCodeTool, createScheduleDraftTool, createSendMessageTool, createSessionMessagesReadTool, createSessionMessagesSearchTool, createSessionSearchTool, createSessionSearchTools, createSessionTraceTool, createSkillViewTool, createSkillWriteTools, createTeamTools, createTodoTool, createToolRevealState, createToolScope, createToolSearchTool, createTrustGate, currentEpochVersion, decideImport, decideTrust, defaultConfig, describeArtifactRefusal, describeBackgroundTask, describeHeadlessPolicy, describeIsolation, describeJsonError, diagnoseSchedules, discoverMarkdown, discoverModels, discoverProject, discoverySpec, displayAllows, displayCurrency, draftCommandLine, draftWidening, escapeRuleContent, estimateImageTokens, estimateRunTokens, estimateTokens, evaluateSelection, expandCommand, expandImports, explainSetting, extractArchive, findInstructionFiles, findProjectInstructions, findRole, formatBytes, formatSkillIndex, formatUsd, gateForTool, getCapability, getConfigIssues, getConfigOrigins, getImportGate, getTrustGate, grantedPathsFor, hasDedicatedExecContent, hasExecTierContent, hasPromptCaching, hasProviderCredentials, hasReadTierContent, hasUsableModel, imageFromBase64, imageFromPath, imagePartTokens, importParts, importSkills, installPlugin, instructionDirs, instructionNotices, intervalSlots, inventoryProjectExtensions, inventoryTotal, isBlockCode, isCodeModeDeferred, isEmptyRules, isGoalPhase, isNonInteractive, isReadOnlySubCall, isSettingWriteLayer, isTransientPersistFailure, isValidInterval, isValidPluginName, isWithinWindow, isWritableSettingKey, labelSlug, layerRank, listEntryModels, listRunningSchedules, listSessionCandidates, listWorkspaceFiles, loadCommandDefinitions, loadCommandFile, loadCommands, loadConfig, loadHookSources, loadKeybindings, loadLexiconDir, loadLexiconFiles, loadModelEntries, loadPlugins, loadPolicyFiles, loadPolicyRules, loadRoleDefinitions, loadRoleDir, loadedImports, localDateString, managedSwitchNotes, mapClaudeToolName, mapEpochToolName, mapOperationType, matchPreauthorization, matchRuleLists, matchesHook, mentionsPluginRoot, mergeHeadlessPolicy, mergeRoles, mergeSchedulePatch, microsToUsd, modelEntriesPath, modelListCandidates, needsAllowlist, nextRunAt, noConfigOrigins, noManagedPolicy, noPlugins, normalizeModelEntry, normalizeTrigger, offloadOldestImages, openArtifact, originOf, packFileName, packPlugin, parseClaudeSettingsHooks, parseClock, parseDate, parseFrontmatter, parseHooksConfig, parseHooksConfigVerbose, parseImportLine, parseModelEntry, parseModelList, parseRoleDefinition, parseScheduleDraft, parseSkillFrontmatter, parseTrigger, pendingImageTokens, persistFailureCode, persistParts, persistToolResults, persistedUsage, planPack, previewSkillImport, pricingFor, projectEntryForDisplay, pruneArtifacts, pruneAttachments, pruneRecordings, pruneSchedules, readCancelRequest, readEntryKey, readImageSize, readMarketplaces, readPluginAbout, readPluginIcon, readPluginManifest, readPluginRecords, readRecording, readSessionSurface, reclaimFtsSpace, redactLine, refreshOpenRouterMetadata, removeMarketplace, removeRecordings, removeRoleFile, removeSessionArtifacts, removeSessionAttachments, renderImportTree, renderProjectContext, renderSdkDeclaration, renderSelfIndex, repairSchedules, requestImageBytes, requestScheduleCancel, resolveArtifactRetention, resolveCodeModeWorker, resolveComplianceDirs, resolveEntrySource, resolveInWorkspace, resolveMarketplaceRef, resolveMentions, resolveMultimodalPolicy, resolvePolicyDirs, resolvePricingAt, resolveProjectRoot, resolveScheduleBackend, resolveSessionReference, resolveSessionVisibility, resolveSettings, resolveSkillDirs, resolveSource, resolveTar, resolveWorkspace, restoreParts, revealScopedProvider, roleScopedProvider, roleSourceLabel, ruleCovers, ruleFromString, ruleToString, runCode, runCommand2 as runCommand, sanitizeArgNames, sanitizeCommand, sanitizeFtsQuery, sanitizePath, sanitizeToolOutput, saveModelEntries, scanInstructions, scanPluginDir, scanSkillDirs, searchMarketplaces, sessionSubtree, setDisplayCurrency, setImportGate, setPluginEnabled, setTrustGate, settingValueChoices, settingValueKind, settingWriteApply, settingWriteLayers, settingWriteTargets, skillIndexDescription, skillIndexResidency, skillIndexStats, sniffMediaType, splitFrontmatter, staticToolProvider, stripArtifactData, substitutePluginRoot, suggestRuleFromApproval, systemOpenArgv, systemPromptSegments, testModelEntry, toPosix, toolDefinitionTokens, toolMatches, toolTableTokens, totalTokens, traceSession, triggerPeriodMs, unescapeRuleContent, uninstallPlugin, unknownRoleSkills, unknownRoleTools, unloadedInstructionFiles, unsetUserSetting, updateMarketplace, updatePlugin, usdToMicros, validateSchedule, validateTrigger, validateWindow, walkAncestors, walkDescendants, withGoalCompression, withGoalPrompt, workspaceDisplayPath, writableConfigKeys, writeSettingValue };