@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.
- package/README.md +31 -31
- package/dist/index.d.ts +3223 -4402
- package/dist/index.js +1073 -383
- 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,
|
|
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
|
-
|
|
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
|
|
4915
|
-
|
|
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:
|
|
4930
|
-
|
|
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
|
|
4943
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
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:
|
|
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
|
|
5076
|
-
const
|
|
5077
|
-
|
|
5078
|
-
|
|
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
|
-
|
|
5081
|
-
|
|
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:
|
|
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: {
|
|
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
|
|
5105
|
-
|
|
5106
|
-
|
|
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:
|
|
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
|
-
|
|
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: [
|
|
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
|
|
5136
|
-
const
|
|
5137
|
-
|
|
5138
|
-
|
|
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: {
|
|
5151
|
-
|
|
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
|
|
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({
|
|
5170
|
-
|
|
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: {
|
|
5186
|
-
|
|
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
|
|
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({
|
|
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:
|
|
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: {
|
|
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
|
-
|
|
5235
|
-
|
|
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: {
|
|
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
|
|
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")}.` :
|
|
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
|
-
|
|
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: {
|
|
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
|
-
`${
|
|
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 ${
|
|
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
|
|
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
|
|
5930
|
-
if (!
|
|
5931
|
-
out.push(
|
|
5932
|
-
at =
|
|
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
|
|
5938
|
-
const
|
|
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
|
|
5943
|
-
|
|
5944
|
-
seen.
|
|
5945
|
-
|
|
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
|
-
|
|
5950
|
-
walk3(
|
|
6842
|
+
steps.push({ node, depth });
|
|
6843
|
+
walk3(id, depth + 1);
|
|
5951
6844
|
}
|
|
5952
6845
|
};
|
|
5953
6846
|
walk3(rootId, 1);
|
|
5954
|
-
return {
|
|
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
|
|
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,
|
|
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:
|
|
17450
|
-
maxOutputTokens:
|
|
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
|
|
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
|
-
...
|
|
20786
|
-
...
|
|
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
|
-
...
|
|
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
|
|
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 =
|
|
20822
|
-
const 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 =
|
|
20825
|
-
const cacheWriteTokens =
|
|
20826
|
-
const reasoningTokens =
|
|
20827
|
-
const totalTokens2 =
|
|
20828
|
-
const 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 =
|
|
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 =
|
|
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}
|
|
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({
|
|
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
|
-
* ##
|
|
29401
|
+
* ## 两条**明确不做**(不是待办)
|
|
28710
29402
|
*
|
|
28711
|
-
* 1. **符号链接不做全局归一。** 这里只 `resolve()`,不 `realpathSync()
|
|
28712
|
-
*
|
|
28713
|
-
*
|
|
28714
|
-
*
|
|
28715
|
-
* `
|
|
28716
|
-
*
|
|
28717
|
-
*
|
|
28718
|
-
*
|
|
28719
|
-
*
|
|
28720
|
-
*
|
|
28721
|
-
*
|
|
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 };
|