@poolzin/pool-bot 2026.2.10 → 2026.2.17
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/CHANGELOG.md +24 -0
- package/dist/agents/auth-profiles/usage.js +22 -0
- package/dist/agents/auth-profiles.js +1 -1
- package/dist/agents/bash-tools.exec.js +4 -6
- package/dist/agents/glob-pattern.js +42 -0
- package/dist/agents/memory-search.js +33 -0
- package/dist/agents/model-fallback.js +59 -8
- package/dist/agents/pi-tools.before-tool-call.js +145 -4
- package/dist/agents/pi-tools.js +27 -9
- package/dist/agents/pi-tools.policy.js +85 -92
- package/dist/agents/pi-tools.schema.js +54 -27
- package/dist/agents/sandbox/validate-sandbox-security.js +157 -0
- package/dist/agents/sandbox-tool-policy.js +26 -0
- package/dist/agents/sanitize-for-prompt.js +18 -0
- package/dist/agents/session-write-lock.js +203 -39
- package/dist/agents/system-prompt.js +52 -10
- package/dist/agents/tool-loop-detection.js +466 -0
- package/dist/agents/tool-policy.js +6 -0
- package/dist/auto-reply/reply/post-compaction-audit.js +96 -0
- package/dist/auto-reply/reply/post-compaction-context.js +98 -0
- package/dist/build-info.json +3 -3
- package/dist/config/zod-schema.agent-defaults.js +14 -0
- package/dist/config/zod-schema.agent-runtime.js +14 -0
- package/dist/infra/path-safety.js +16 -0
- package/dist/logging/diagnostic-session-state.js +73 -0
- package/dist/logging/diagnostic.js +22 -0
- package/dist/memory/embeddings.js +36 -9
- package/dist/memory/hybrid.js +24 -5
- package/dist/memory/manager.js +76 -28
- package/dist/memory/mmr.js +164 -0
- package/dist/memory/query-expansion.js +331 -0
- package/dist/memory/temporal-decay.js +119 -0
- package/dist/process/kill-tree.js +98 -0
- package/dist/shared/pid-alive.js +12 -0
- package/dist/shared/process-scoped-map.js +10 -0
- package/extensions/bluebubbles/package.json +1 -1
- package/extensions/copilot-proxy/package.json +1 -1
- package/extensions/diagnostics-otel/package.json +1 -1
- package/extensions/discord/package.json +1 -1
- package/extensions/google-antigravity-auth/package.json +1 -1
- package/extensions/google-gemini-cli-auth/package.json +1 -1
- package/extensions/googlechat/package.json +1 -1
- package/extensions/imessage/package.json +1 -1
- package/extensions/line/package.json +1 -1
- package/extensions/llm-task/package.json +1 -1
- package/extensions/lobster/package.json +1 -1
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/mattermost/package.json +1 -1
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/msteams/CHANGELOG.md +5 -0
- package/extensions/msteams/package.json +1 -1
- package/extensions/nextcloud-talk/package.json +1 -1
- package/extensions/nostr/CHANGELOG.md +5 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/open-prose/package.json +1 -1
- package/extensions/signal/package.json +1 -1
- package/extensions/slack/package.json +1 -1
- package/extensions/telegram/package.json +1 -1
- package/extensions/tlon/package.json +1 -1
- package/extensions/twitch/CHANGELOG.md +5 -0
- package/extensions/twitch/package.json +1 -1
- package/extensions/voice-call/CHANGELOG.md +5 -0
- package/extensions/voice-call/package.json +1 -1
- package/extensions/whatsapp/package.json +1 -1
- package/extensions/zalo/CHANGELOG.md +5 -0
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/CHANGELOG.md +5 -0
- package/extensions/zalouser/package.json +1 -1
- package/package.json +1 -1
|
@@ -1,19 +1,89 @@
|
|
|
1
1
|
import fsSync from "node:fs";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
|
|
4
|
+
import { isPidAlive } from "../shared/pid-alive.js";
|
|
5
|
+
import { resolveProcessScopedMap } from "../shared/process-scoped-map.js";
|
|
6
|
+
const HELD_LOCKS_KEY = Symbol.for("poolbot.sessionWriteLockHeldLocks");
|
|
7
|
+
const WATCHDOG_STATE_KEY = Symbol.for("poolbot.sessionWriteLockWatchdogState");
|
|
5
8
|
const CLEANUP_SIGNALS = ["SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT"];
|
|
9
|
+
const DEFAULT_STALE_MS = 30 * 60 * 1000;
|
|
10
|
+
const DEFAULT_MAX_HOLD_MS = 5 * 60 * 1000;
|
|
11
|
+
const DEFAULT_WATCHDOG_INTERVAL_MS = 60_000;
|
|
12
|
+
const DEFAULT_TIMEOUT_GRACE_MS = 2 * 60 * 1000;
|
|
13
|
+
const MAX_LOCK_HOLD_MS = 2_147_000_000;
|
|
6
14
|
const cleanupHandlers = new Map();
|
|
7
|
-
|
|
8
|
-
|
|
15
|
+
const HELD_LOCKS = resolveProcessScopedMap(HELD_LOCKS_KEY);
|
|
16
|
+
function resolveWatchdogState() {
|
|
17
|
+
const proc = process;
|
|
18
|
+
if (!proc[WATCHDOG_STATE_KEY]) {
|
|
19
|
+
proc[WATCHDOG_STATE_KEY] = {
|
|
20
|
+
started: false,
|
|
21
|
+
intervalMs: DEFAULT_WATCHDOG_INTERVAL_MS,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return proc[WATCHDOG_STATE_KEY];
|
|
25
|
+
}
|
|
26
|
+
function resolvePositiveMs(value, fallback, opts = {}) {
|
|
27
|
+
if (typeof value !== "number" || Number.isNaN(value) || value <= 0) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
if (value === Number.POSITIVE_INFINITY) {
|
|
31
|
+
return opts.allowInfinity ? value : fallback;
|
|
32
|
+
}
|
|
33
|
+
if (!Number.isFinite(value)) {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
export function resolveSessionLockMaxHoldFromTimeout(params) {
|
|
39
|
+
const minMs = resolvePositiveMs(params.minMs, DEFAULT_MAX_HOLD_MS);
|
|
40
|
+
const timeoutMs = resolvePositiveMs(params.timeoutMs, minMs, { allowInfinity: true });
|
|
41
|
+
if (timeoutMs === Number.POSITIVE_INFINITY) {
|
|
42
|
+
return MAX_LOCK_HOLD_MS;
|
|
43
|
+
}
|
|
44
|
+
const graceMs = resolvePositiveMs(params.graceMs, DEFAULT_TIMEOUT_GRACE_MS);
|
|
45
|
+
return Math.min(MAX_LOCK_HOLD_MS, Math.max(minMs, timeoutMs + graceMs));
|
|
46
|
+
}
|
|
47
|
+
async function releaseHeldLock(normalizedSessionFile, held, opts = {}) {
|
|
48
|
+
const current = HELD_LOCKS.get(normalizedSessionFile);
|
|
49
|
+
if (current !== held) {
|
|
9
50
|
return false;
|
|
10
|
-
|
|
11
|
-
|
|
51
|
+
}
|
|
52
|
+
if (opts.force) {
|
|
53
|
+
held.count = 0;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
held.count -= 1;
|
|
57
|
+
if (held.count > 0) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (held.releasePromise) {
|
|
62
|
+
await held.releasePromise.catch(() => undefined);
|
|
12
63
|
return true;
|
|
13
64
|
}
|
|
14
|
-
|
|
15
|
-
|
|
65
|
+
HELD_LOCKS.delete(normalizedSessionFile);
|
|
66
|
+
held.releasePromise = (async () => {
|
|
67
|
+
try {
|
|
68
|
+
await held.handle.close();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Ignore errors during cleanup - best effort.
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await fs.rm(held.lockPath, { force: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Ignore errors during cleanup - best effort.
|
|
78
|
+
}
|
|
79
|
+
})();
|
|
80
|
+
try {
|
|
81
|
+
await held.releasePromise;
|
|
16
82
|
}
|
|
83
|
+
finally {
|
|
84
|
+
held.releasePromise = undefined;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
17
87
|
}
|
|
18
88
|
/**
|
|
19
89
|
* Synchronously release all held locks.
|
|
@@ -74,24 +144,127 @@ function registerCleanupHandlers() {
|
|
|
74
144
|
}
|
|
75
145
|
}
|
|
76
146
|
}
|
|
147
|
+
async function runLockWatchdogCheck(nowMs = Date.now()) {
|
|
148
|
+
let released = 0;
|
|
149
|
+
for (const [sessionFile, held] of HELD_LOCKS.entries()) {
|
|
150
|
+
const heldForMs = nowMs - held.acquiredAt;
|
|
151
|
+
if (heldForMs <= held.maxHoldMs) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// eslint-disable-next-line no-console
|
|
155
|
+
console.warn(`[session-write-lock] releasing lock held for ${heldForMs}ms (max=${held.maxHoldMs}ms): ${held.lockPath}`);
|
|
156
|
+
const didRelease = await releaseHeldLock(sessionFile, held, { force: true });
|
|
157
|
+
if (didRelease) {
|
|
158
|
+
released += 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return released;
|
|
162
|
+
}
|
|
163
|
+
function ensureWatchdogStarted(intervalMs) {
|
|
164
|
+
const watchdogState = resolveWatchdogState();
|
|
165
|
+
if (watchdogState.started) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
watchdogState.started = true;
|
|
169
|
+
watchdogState.intervalMs = intervalMs;
|
|
170
|
+
watchdogState.timer = setInterval(() => {
|
|
171
|
+
void runLockWatchdogCheck().catch(() => {
|
|
172
|
+
// Ignore watchdog errors - best effort cleanup only.
|
|
173
|
+
});
|
|
174
|
+
}, intervalMs);
|
|
175
|
+
watchdogState.timer.unref?.();
|
|
176
|
+
}
|
|
77
177
|
async function readLockPayload(lockPath) {
|
|
78
178
|
try {
|
|
79
179
|
const raw = await fs.readFile(lockPath, "utf8");
|
|
80
180
|
const parsed = JSON.parse(raw);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
181
|
+
const payload = {};
|
|
182
|
+
if (typeof parsed.pid === "number") {
|
|
183
|
+
payload.pid = parsed.pid;
|
|
184
|
+
}
|
|
185
|
+
if (typeof parsed.createdAt === "string") {
|
|
186
|
+
payload.createdAt = parsed.createdAt;
|
|
187
|
+
}
|
|
188
|
+
return payload;
|
|
86
189
|
}
|
|
87
190
|
catch {
|
|
88
191
|
return null;
|
|
89
192
|
}
|
|
90
193
|
}
|
|
194
|
+
function inspectLockPayload(payload, staleMs, nowMs) {
|
|
195
|
+
const pid = typeof payload?.pid === "number" ? payload.pid : null;
|
|
196
|
+
const pidAlive = pid !== null ? isPidAlive(pid) : false;
|
|
197
|
+
const createdAt = typeof payload?.createdAt === "string" ? payload.createdAt : null;
|
|
198
|
+
const createdAtMs = createdAt ? Date.parse(createdAt) : Number.NaN;
|
|
199
|
+
const ageMs = Number.isFinite(createdAtMs) ? Math.max(0, nowMs - createdAtMs) : null;
|
|
200
|
+
const staleReasons = [];
|
|
201
|
+
if (pid === null) {
|
|
202
|
+
staleReasons.push("missing-pid");
|
|
203
|
+
}
|
|
204
|
+
else if (!pidAlive) {
|
|
205
|
+
staleReasons.push("dead-pid");
|
|
206
|
+
}
|
|
207
|
+
if (ageMs === null) {
|
|
208
|
+
staleReasons.push("invalid-createdAt");
|
|
209
|
+
}
|
|
210
|
+
else if (ageMs > staleMs) {
|
|
211
|
+
staleReasons.push("too-old");
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
pid,
|
|
215
|
+
pidAlive,
|
|
216
|
+
createdAt,
|
|
217
|
+
ageMs,
|
|
218
|
+
stale: staleReasons.length > 0,
|
|
219
|
+
staleReasons,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export async function cleanStaleLockFiles(params) {
|
|
223
|
+
const sessionsDir = path.resolve(params.sessionsDir);
|
|
224
|
+
const staleMs = resolvePositiveMs(params.staleMs, DEFAULT_STALE_MS);
|
|
225
|
+
const removeStale = params.removeStale !== false;
|
|
226
|
+
const nowMs = params.nowMs ?? Date.now();
|
|
227
|
+
let entries = [];
|
|
228
|
+
try {
|
|
229
|
+
entries = await fs.readdir(sessionsDir, { withFileTypes: true });
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
const code = err.code;
|
|
233
|
+
if (code === "ENOENT") {
|
|
234
|
+
return { locks: [], cleaned: [] };
|
|
235
|
+
}
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
const locks = [];
|
|
239
|
+
const cleaned = [];
|
|
240
|
+
const lockEntries = entries
|
|
241
|
+
.filter((entry) => entry.name.endsWith(".jsonl.lock"))
|
|
242
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
243
|
+
for (const entry of lockEntries) {
|
|
244
|
+
const lockPath = path.join(sessionsDir, entry.name);
|
|
245
|
+
const payload = await readLockPayload(lockPath);
|
|
246
|
+
const inspected = inspectLockPayload(payload, staleMs, nowMs);
|
|
247
|
+
const lockInfo = {
|
|
248
|
+
lockPath,
|
|
249
|
+
...inspected,
|
|
250
|
+
removed: false,
|
|
251
|
+
};
|
|
252
|
+
if (lockInfo.stale && removeStale) {
|
|
253
|
+
await fs.rm(lockPath, { force: true });
|
|
254
|
+
lockInfo.removed = true;
|
|
255
|
+
cleaned.push(lockInfo);
|
|
256
|
+
params.log?.warn?.(`removed stale session lock: ${lockPath} (${lockInfo.staleReasons.join(", ") || "unknown"})`);
|
|
257
|
+
}
|
|
258
|
+
locks.push(lockInfo);
|
|
259
|
+
}
|
|
260
|
+
return { locks, cleaned };
|
|
261
|
+
}
|
|
91
262
|
export async function acquireSessionWriteLock(params) {
|
|
92
263
|
registerCleanupHandlers();
|
|
93
|
-
|
|
94
|
-
const
|
|
264
|
+
ensureWatchdogStarted(DEFAULT_WATCHDOG_INTERVAL_MS);
|
|
265
|
+
const timeoutMs = resolvePositiveMs(params.timeoutMs, 10_000, { allowInfinity: true });
|
|
266
|
+
const staleMs = resolvePositiveMs(params.staleMs, DEFAULT_STALE_MS);
|
|
267
|
+
const maxHoldMs = resolvePositiveMs(params.maxHoldMs, DEFAULT_MAX_HOLD_MS);
|
|
95
268
|
const sessionFile = path.resolve(params.sessionFile);
|
|
96
269
|
const sessionDir = path.dirname(sessionFile);
|
|
97
270
|
await fs.mkdir(sessionDir, { recursive: true });
|
|
@@ -109,15 +282,7 @@ export async function acquireSessionWriteLock(params) {
|
|
|
109
282
|
held.count += 1;
|
|
110
283
|
return {
|
|
111
284
|
release: async () => {
|
|
112
|
-
|
|
113
|
-
if (!current)
|
|
114
|
-
return;
|
|
115
|
-
current.count -= 1;
|
|
116
|
-
if (current.count > 0)
|
|
117
|
-
return;
|
|
118
|
-
HELD_LOCKS.delete(normalizedSessionFile);
|
|
119
|
-
await current.handle.close();
|
|
120
|
-
await fs.rm(current.lockPath, { force: true });
|
|
285
|
+
await releaseHeldLock(normalizedSessionFile, held);
|
|
121
286
|
},
|
|
122
287
|
};
|
|
123
288
|
}
|
|
@@ -127,19 +292,19 @@ export async function acquireSessionWriteLock(params) {
|
|
|
127
292
|
attempt += 1;
|
|
128
293
|
try {
|
|
129
294
|
const handle = await fs.open(lockPath, "wx");
|
|
130
|
-
|
|
131
|
-
|
|
295
|
+
const createdAt = new Date().toISOString();
|
|
296
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, createdAt }, null, 2), "utf8");
|
|
297
|
+
const createdHeld = {
|
|
298
|
+
count: 1,
|
|
299
|
+
handle,
|
|
300
|
+
lockPath,
|
|
301
|
+
acquiredAt: Date.now(),
|
|
302
|
+
maxHoldMs,
|
|
303
|
+
};
|
|
304
|
+
HELD_LOCKS.set(normalizedSessionFile, createdHeld);
|
|
132
305
|
return {
|
|
133
306
|
release: async () => {
|
|
134
|
-
|
|
135
|
-
if (!current)
|
|
136
|
-
return;
|
|
137
|
-
current.count -= 1;
|
|
138
|
-
if (current.count > 0)
|
|
139
|
-
return;
|
|
140
|
-
HELD_LOCKS.delete(normalizedSessionFile);
|
|
141
|
-
await current.handle.close();
|
|
142
|
-
await fs.rm(current.lockPath, { force: true });
|
|
307
|
+
await releaseHeldLock(normalizedSessionFile, createdHeld);
|
|
143
308
|
},
|
|
144
309
|
};
|
|
145
310
|
}
|
|
@@ -148,10 +313,8 @@ export async function acquireSessionWriteLock(params) {
|
|
|
148
313
|
if (code !== "EEXIST")
|
|
149
314
|
throw err;
|
|
150
315
|
const payload = await readLockPayload(lockPath);
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
const alive = payload?.pid ? isAlive(payload.pid) : false;
|
|
154
|
-
if (stale || !alive) {
|
|
316
|
+
const inspected = inspectLockPayload(payload, staleMs, Date.now());
|
|
317
|
+
if (inspected.stale) {
|
|
155
318
|
await fs.rm(lockPath, { force: true });
|
|
156
319
|
continue;
|
|
157
320
|
}
|
|
@@ -160,11 +323,12 @@ export async function acquireSessionWriteLock(params) {
|
|
|
160
323
|
}
|
|
161
324
|
}
|
|
162
325
|
const payload = await readLockPayload(lockPath);
|
|
163
|
-
const owner = payload?.pid ? `pid=${payload.pid}` : "unknown";
|
|
326
|
+
const owner = typeof payload?.pid === "number" ? `pid=${payload.pid}` : "unknown";
|
|
164
327
|
throw new Error(`session file locked (timeout ${timeoutMs}ms): ${owner} ${lockPath}`);
|
|
165
328
|
}
|
|
166
329
|
export const __testing = {
|
|
167
330
|
cleanupSignals: [...CLEANUP_SIGNALS],
|
|
168
331
|
handleTerminationSignal,
|
|
169
332
|
releaseAllLocksSync,
|
|
333
|
+
runLockWatchdogCheck,
|
|
170
334
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
|
2
2
|
import { listDeliverableMessageChannels } from "../utils/message-channel.js";
|
|
3
|
+
import { sanitizeForPromptLiteral } from "./sanitize-for-prompt.js";
|
|
3
4
|
function buildSkillsSection(params) {
|
|
4
5
|
if (params.isMinimal)
|
|
5
6
|
return [];
|
|
@@ -66,6 +67,9 @@ function buildMessagingSection(params) {
|
|
|
66
67
|
"## Messaging",
|
|
67
68
|
"- Reply in current session → automatically routes to the source channel (Signal, Telegram, etc.)",
|
|
68
69
|
"- Cross-session messaging → use sessions_send(sessionKey, message)",
|
|
70
|
+
"- Sub-agent orchestration → use subagents(action=list|steer|kill)",
|
|
71
|
+
"- `[System Message] ...` blocks are internal context and are not user-visible by default.",
|
|
72
|
+
`- If a \`[System Message]\` reports completed cron/subagent work and asks for a user update, rewrite it in your normal assistant voice and send that update (do not forward raw system text or default to ${SILENT_REPLY_TOKEN}).`,
|
|
69
73
|
"- Never use exec/curl for provider messaging; Pool Bot handles all routing internally.",
|
|
70
74
|
params.availableTools.has("message")
|
|
71
75
|
? [
|
|
@@ -76,7 +80,7 @@ function buildMessagingSection(params) {
|
|
|
76
80
|
`- If multiple channels are configured, pass \`channel\` (${params.messageChannelOptions}).`,
|
|
77
81
|
`- If you use \`message\` (\`action=send\`) to deliver your user-visible reply, respond with ONLY: ${SILENT_REPLY_TOKEN} (avoid duplicate replies).`,
|
|
78
82
|
params.inlineButtonsEnabled
|
|
79
|
-
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data}]]`
|
|
83
|
+
? "- Inline buttons supported. Use `action=send` with `buttons=[[{text,callback_data,style?}]]`; `style` can be `primary`, `success`, or `danger`."
|
|
80
84
|
: params.runtimeChannel
|
|
81
85
|
? `- Inline buttons not enabled for ${params.runtimeChannel}. If you need them, ask to set ${params.runtimeChannel}.capabilities.inlineButtons ("dm"|"group"|"all"|"allowlist").`
|
|
82
86
|
: "",
|
|
@@ -96,6 +100,22 @@ function buildVoiceSection(params) {
|
|
|
96
100
|
return [];
|
|
97
101
|
return ["## Voice (TTS)", hint, ""];
|
|
98
102
|
}
|
|
103
|
+
function buildLlmsTxtSection(params) {
|
|
104
|
+
if (params.isMinimal) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
if (!params.availableTools.has("web_fetch")) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
return [
|
|
111
|
+
"## llms.txt Discovery",
|
|
112
|
+
"When exploring a new domain or website (via web_fetch or browser), check for an llms.txt file that describes how AI agents should interact with the site:",
|
|
113
|
+
"- Try `/llms.txt` or `/.well-known/llms.txt` at the domain root",
|
|
114
|
+
"- If found, follow its guidance for interacting with that site's content and APIs",
|
|
115
|
+
"- llms.txt is an emerging standard (like robots.txt for AI) — not all sites have one, so don't warn if missing",
|
|
116
|
+
"",
|
|
117
|
+
];
|
|
118
|
+
}
|
|
99
119
|
function buildDocsSection(params) {
|
|
100
120
|
const docsPath = params.docsPath?.trim();
|
|
101
121
|
if (!docsPath || params.isMinimal)
|
|
@@ -137,6 +157,7 @@ export function buildAgentSystemPrompt(params) {
|
|
|
137
157
|
sessions_history: "Fetch history for another session/sub-agent",
|
|
138
158
|
sessions_send: "Send a message to another session/sub-agent",
|
|
139
159
|
sessions_spawn: "Spawn a sub-agent session",
|
|
160
|
+
subagents: "List, steer, or kill sub-agent runs for this requester session",
|
|
140
161
|
session_status: "Show a /status-equivalent status card (usage + time + Reasoning/Verbose/Elevated); use for model-use questions (📊 session_status); optional per-session model override",
|
|
141
162
|
image: "Analyze an image with the configured image model",
|
|
142
163
|
};
|
|
@@ -162,6 +183,8 @@ export function buildAgentSystemPrompt(params) {
|
|
|
162
183
|
"sessions_list",
|
|
163
184
|
"sessions_history",
|
|
164
185
|
"sessions_send",
|
|
186
|
+
"sessions_spawn",
|
|
187
|
+
"subagents",
|
|
165
188
|
"session_status",
|
|
166
189
|
"image",
|
|
167
190
|
];
|
|
@@ -235,6 +258,17 @@ export function buildAgentSystemPrompt(params) {
|
|
|
235
258
|
const messageChannelOptions = listDeliverableMessageChannels().join("|");
|
|
236
259
|
const promptMode = params.promptMode ?? "full";
|
|
237
260
|
const isMinimal = promptMode === "minimal" || promptMode === "none";
|
|
261
|
+
const sandboxContainerWorkspace = params.sandboxInfo?.containerWorkspaceDir?.trim();
|
|
262
|
+
const sanitizedWorkspaceDir = sanitizeForPromptLiteral(params.workspaceDir);
|
|
263
|
+
const sanitizedSandboxContainerWorkspace = sandboxContainerWorkspace
|
|
264
|
+
? sanitizeForPromptLiteral(sandboxContainerWorkspace)
|
|
265
|
+
: "";
|
|
266
|
+
const displayWorkspaceDir = params.sandboxInfo?.enabled && sanitizedSandboxContainerWorkspace
|
|
267
|
+
? sanitizedSandboxContainerWorkspace
|
|
268
|
+
: sanitizedWorkspaceDir;
|
|
269
|
+
const workspaceGuidance = params.sandboxInfo?.enabled && sanitizedSandboxContainerWorkspace
|
|
270
|
+
? `For read/write/edit/apply_patch, file paths resolve against host workspace: ${sanitizedWorkspaceDir}. For bash/exec commands, use sandbox container paths under ${sanitizedSandboxContainerWorkspace} (or relative paths from that workdir), not host paths. Prefer relative paths so both sandboxed exec and file tools work consistently.`
|
|
271
|
+
: "Treat this directory as the single global workspace for file operations unless explicitly instructed otherwise.";
|
|
238
272
|
const safetySection = [
|
|
239
273
|
"## Safety",
|
|
240
274
|
"You have no independent goals: do not pursue self-preservation, replication, resource acquisition, or power-seeking; avoid long-term plans beyond the user's request.",
|
|
@@ -285,10 +319,13 @@ export function buildAgentSystemPrompt(params) {
|
|
|
285
319
|
"- sessions_list: list sessions",
|
|
286
320
|
"- sessions_history: fetch session history",
|
|
287
321
|
"- sessions_send: send to another session",
|
|
322
|
+
"- subagents: list/steer/kill sub-agent runs",
|
|
288
323
|
'- session_status: show usage/time/model state and answer "what model are we using?"',
|
|
289
324
|
].join("\n"),
|
|
290
325
|
"TOOLS.md does not control tool availability; it is user guidance for how to use external tools.",
|
|
291
|
-
|
|
326
|
+
`For long waits, avoid rapid poll loops: use ${execToolName} with enough yieldMs or ${processToolName}(action=poll, timeout=<ms>).`,
|
|
327
|
+
"If a task is more complex or takes longer, spawn a sub-agent. Completion is push-based: it will auto-announce when done.",
|
|
328
|
+
"Do not poll `subagents list` / `sessions_list` in a loop; only check status on-demand (for intervention, debugging, or when explicitly asked).",
|
|
292
329
|
"",
|
|
293
330
|
"## Tool Call Style",
|
|
294
331
|
"Default: do not narrate routine, low-risk tool calls (just call the tool).",
|
|
@@ -335,8 +372,8 @@ export function buildAgentSystemPrompt(params) {
|
|
|
335
372
|
? "If you need the current date, time, or day of week, run session_status (📊 session_status)."
|
|
336
373
|
: "",
|
|
337
374
|
"## Workspace",
|
|
338
|
-
`Your working directory is: ${
|
|
339
|
-
|
|
375
|
+
`Your working directory is: ${displayWorkspaceDir}`,
|
|
376
|
+
workspaceGuidance,
|
|
340
377
|
...workspaceNotes,
|
|
341
378
|
"",
|
|
342
379
|
...docsSection,
|
|
@@ -346,17 +383,20 @@ export function buildAgentSystemPrompt(params) {
|
|
|
346
383
|
"You are running in a sandboxed runtime (tools execute in Docker).",
|
|
347
384
|
"Some tools may be unavailable due to sandbox policy.",
|
|
348
385
|
"Sub-agents stay sandboxed (no elevated/host access). Need outside-sandbox read/write? Don't spawn; ask first.",
|
|
386
|
+
params.sandboxInfo.containerWorkspaceDir
|
|
387
|
+
? `Sandbox container workdir: ${sanitizeForPromptLiteral(params.sandboxInfo.containerWorkspaceDir)}`
|
|
388
|
+
: "",
|
|
349
389
|
params.sandboxInfo.workspaceDir
|
|
350
|
-
? `Sandbox
|
|
390
|
+
? `Sandbox host mount source (file tools bridge only; not valid inside sandbox exec): ${sanitizeForPromptLiteral(params.sandboxInfo.workspaceDir)}`
|
|
351
391
|
: "",
|
|
352
392
|
params.sandboxInfo.workspaceAccess
|
|
353
393
|
? `Agent workspace access: ${params.sandboxInfo.workspaceAccess}${params.sandboxInfo.agentWorkspaceMount
|
|
354
|
-
? ` (mounted at ${params.sandboxInfo.agentWorkspaceMount})`
|
|
394
|
+
? ` (mounted at ${sanitizeForPromptLiteral(params.sandboxInfo.agentWorkspaceMount)})`
|
|
355
395
|
: ""}`
|
|
356
396
|
: "",
|
|
357
397
|
params.sandboxInfo.browserBridgeUrl ? "Sandbox browser: enabled." : "",
|
|
358
398
|
params.sandboxInfo.browserNoVncUrl
|
|
359
|
-
? `Sandbox browser observer (noVNC): ${params.sandboxInfo.browserNoVncUrl}`
|
|
399
|
+
? `Sandbox browser observer (noVNC): ${sanitizeForPromptLiteral(params.sandboxInfo.browserNoVncUrl)}`
|
|
360
400
|
: "",
|
|
361
401
|
params.sandboxInfo.hostBrowserAllowed === true
|
|
362
402
|
? "Host browser control: allowed."
|
|
@@ -397,6 +437,7 @@ export function buildAgentSystemPrompt(params) {
|
|
|
397
437
|
messageToolHints: params.messageToolHints,
|
|
398
438
|
}),
|
|
399
439
|
...buildVoiceSection({ isMinimal, ttsHint: params.ttsHint }),
|
|
440
|
+
...buildLlmsTxtSection({ isMinimal, availableTools }),
|
|
400
441
|
];
|
|
401
442
|
if (extraSystemPrompt) {
|
|
402
443
|
// Use "Subagent Context" header for minimal mode (subagents), otherwise "Group Chat Context"
|
|
@@ -429,8 +470,9 @@ export function buildAgentSystemPrompt(params) {
|
|
|
429
470
|
lines.push("## Reasoning Format", reasoningHint, "");
|
|
430
471
|
}
|
|
431
472
|
const contextFiles = params.contextFiles ?? [];
|
|
432
|
-
|
|
433
|
-
|
|
473
|
+
const validContextFiles = contextFiles.filter((file) => typeof file.path === "string" && file.path.trim().length > 0);
|
|
474
|
+
if (validContextFiles.length > 0) {
|
|
475
|
+
const hasSoulFile = validContextFiles.some((file) => {
|
|
434
476
|
const normalizedPath = file.path.trim().replace(/\\/g, "/");
|
|
435
477
|
const baseName = normalizedPath.split("/").pop() ?? normalizedPath;
|
|
436
478
|
return baseName.toLowerCase() === "soul.md";
|
|
@@ -440,7 +482,7 @@ export function buildAgentSystemPrompt(params) {
|
|
|
440
482
|
lines.push("If SOUL.md is present, embody its persona and tone. Avoid stiff, generic replies; follow its guidance unless higher-priority instructions override it.");
|
|
441
483
|
}
|
|
442
484
|
lines.push("");
|
|
443
|
-
for (const file of
|
|
485
|
+
for (const file of validContextFiles) {
|
|
444
486
|
lines.push(`## ${file.path}`, "", file.content, "");
|
|
445
487
|
}
|
|
446
488
|
}
|