@poolzin/pool-bot 2026.2.6 → 2026.2.8
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 +14 -0
- package/dist/agents/models-config.providers.js +14 -0
- package/dist/agents/nvidia-models.js +228 -0
- package/dist/agents/ollama-stream.js +294 -0
- package/dist/agents/pi-embedded-runner/compaction-safety-timeout.js +5 -0
- package/dist/agents/pi-embedded-runner/run/compaction-timeout.js +27 -0
- package/dist/agents/pi-embedded-runner/wait-for-idle-before-flush.js +29 -0
- package/dist/agents/pi-embedded-subscribe.js +59 -4
- package/dist/agents/tool-mutation.js +164 -0
- package/dist/agents/tool-policy-pipeline.js +69 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/gateway-cli/run-loop.js +33 -1
- package/dist/daemon/constants.js +7 -3
- package/dist/gateway/auth.js +3 -8
- package/dist/media-understanding/runner.js +9 -1
- package/dist/process/command-queue.js +138 -16
- package/dist/process/restart-recovery.js +16 -0
- package/dist/security/dangerous-tools.js +34 -0
- package/dist/security/secret-equal.js +12 -0
- package/dist/tui/components/filterable-select-list.js +5 -2
- package/dist/tui/components/searchable-select-list.js +6 -1
- package/dist/tui/tui-command-handlers.js +13 -5
- package/package.json +5 -5
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
import { diagnosticLogger as diag, logLaneDequeue, logLaneEnqueue } from "../logging/diagnostic.js";
|
|
2
|
+
/**
|
|
3
|
+
* Dedicated error type thrown when a queued command is rejected because
|
|
4
|
+
* its lane was cleared. Callers that fire-and-forget enqueued tasks can
|
|
5
|
+
* catch (or ignore) this specific type to avoid unhandled-rejection noise.
|
|
6
|
+
*/
|
|
7
|
+
export class CommandLaneClearedError extends Error {
|
|
8
|
+
constructor(lane) {
|
|
9
|
+
super(lane ? `Command lane "${lane}" cleared` : "Command lane cleared");
|
|
10
|
+
this.name = "CommandLaneClearedError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
2
13
|
const lanes = new Map();
|
|
14
|
+
let nextTaskId = 1;
|
|
3
15
|
function getLaneState(lane) {
|
|
4
16
|
const existing = lanes.get(lane);
|
|
5
|
-
if (existing)
|
|
17
|
+
if (existing) {
|
|
6
18
|
return existing;
|
|
19
|
+
}
|
|
7
20
|
const created = {
|
|
8
21
|
lane,
|
|
9
22
|
queue: [],
|
|
10
|
-
|
|
23
|
+
activeTaskIds: new Set(),
|
|
11
24
|
maxConcurrent: 1,
|
|
12
25
|
draining: false,
|
|
26
|
+
generation: 0,
|
|
13
27
|
};
|
|
14
28
|
lanes.set(lane, created);
|
|
15
29
|
return created;
|
|
16
30
|
}
|
|
31
|
+
function completeTask(state, taskId, taskGeneration) {
|
|
32
|
+
if (taskGeneration !== state.generation) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
state.activeTaskIds.delete(taskId);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
17
38
|
function drainLane(lane) {
|
|
18
39
|
const state = getLaneState(lane);
|
|
19
|
-
if (state.draining)
|
|
40
|
+
if (state.draining) {
|
|
20
41
|
return;
|
|
42
|
+
}
|
|
21
43
|
state.draining = true;
|
|
22
44
|
const pump = () => {
|
|
23
|
-
while (state.
|
|
45
|
+
while (state.activeTaskIds.size < state.maxConcurrent && state.queue.length > 0) {
|
|
24
46
|
const entry = state.queue.shift();
|
|
25
47
|
const waitedMs = Date.now() - entry.enqueuedAt;
|
|
26
48
|
if (waitedMs >= entry.warnAfterMs) {
|
|
@@ -28,23 +50,29 @@ function drainLane(lane) {
|
|
|
28
50
|
diag.warn(`lane wait exceeded: lane=${lane} waitedMs=${waitedMs} queueAhead=${state.queue.length}`);
|
|
29
51
|
}
|
|
30
52
|
logLaneDequeue(lane, waitedMs, state.queue.length);
|
|
31
|
-
|
|
53
|
+
const taskId = nextTaskId++;
|
|
54
|
+
const taskGeneration = state.generation;
|
|
55
|
+
state.activeTaskIds.add(taskId);
|
|
32
56
|
void (async () => {
|
|
33
57
|
const startTime = Date.now();
|
|
34
58
|
try {
|
|
35
59
|
const result = await entry.task();
|
|
36
|
-
state
|
|
37
|
-
|
|
38
|
-
|
|
60
|
+
const completedCurrentGeneration = completeTask(state, taskId, taskGeneration);
|
|
61
|
+
if (completedCurrentGeneration) {
|
|
62
|
+
diag.debug(`lane task done: lane=${lane} durationMs=${Date.now() - startTime} active=${state.activeTaskIds.size} queued=${state.queue.length}`);
|
|
63
|
+
pump();
|
|
64
|
+
}
|
|
39
65
|
entry.resolve(result);
|
|
40
66
|
}
|
|
41
67
|
catch (err) {
|
|
42
|
-
state
|
|
68
|
+
const completedCurrentGeneration = completeTask(state, taskId, taskGeneration);
|
|
43
69
|
const isProbeLane = lane.startsWith("auth-probe:") || lane.startsWith("session:probe-");
|
|
44
70
|
if (!isProbeLane) {
|
|
45
71
|
diag.error(`lane task error: lane=${lane} durationMs=${Date.now() - startTime} error="${String(err)}"`);
|
|
46
72
|
}
|
|
47
|
-
|
|
73
|
+
if (completedCurrentGeneration) {
|
|
74
|
+
pump();
|
|
75
|
+
}
|
|
48
76
|
entry.reject(err);
|
|
49
77
|
}
|
|
50
78
|
})();
|
|
@@ -72,7 +100,7 @@ export function enqueueCommandInLane(lane, task, opts) {
|
|
|
72
100
|
warnAfterMs,
|
|
73
101
|
onWait: opts?.onWait,
|
|
74
102
|
});
|
|
75
|
-
logLaneEnqueue(cleaned, state.queue.length + state.
|
|
103
|
+
logLaneEnqueue(cleaned, state.queue.length + state.activeTaskIds.size);
|
|
76
104
|
drainLane(cleaned);
|
|
77
105
|
});
|
|
78
106
|
}
|
|
@@ -82,23 +110,117 @@ export function enqueueCommand(task, opts) {
|
|
|
82
110
|
export function getQueueSize(lane = "main" /* CommandLane.Main */) {
|
|
83
111
|
const resolved = lane.trim() || "main" /* CommandLane.Main */;
|
|
84
112
|
const state = lanes.get(resolved);
|
|
85
|
-
if (!state)
|
|
113
|
+
if (!state) {
|
|
86
114
|
return 0;
|
|
87
|
-
|
|
115
|
+
}
|
|
116
|
+
return state.queue.length + state.activeTaskIds.size;
|
|
88
117
|
}
|
|
89
118
|
export function getTotalQueueSize() {
|
|
90
119
|
let total = 0;
|
|
91
120
|
for (const s of lanes.values()) {
|
|
92
|
-
total += s.queue.length + s.
|
|
121
|
+
total += s.queue.length + s.activeTaskIds.size;
|
|
93
122
|
}
|
|
94
123
|
return total;
|
|
95
124
|
}
|
|
96
125
|
export function clearCommandLane(lane = "main" /* CommandLane.Main */) {
|
|
97
126
|
const cleaned = lane.trim() || "main" /* CommandLane.Main */;
|
|
98
127
|
const state = lanes.get(cleaned);
|
|
99
|
-
if (!state)
|
|
128
|
+
if (!state) {
|
|
100
129
|
return 0;
|
|
130
|
+
}
|
|
101
131
|
const removed = state.queue.length;
|
|
102
|
-
state.queue.
|
|
132
|
+
const pending = state.queue.splice(0);
|
|
133
|
+
for (const entry of pending) {
|
|
134
|
+
entry.reject(new CommandLaneClearedError(cleaned));
|
|
135
|
+
}
|
|
103
136
|
return removed;
|
|
104
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Reset all lane runtime state to idle. Used after SIGUSR1 in-process
|
|
140
|
+
* restarts where interrupted tasks' finally blocks may not run, leaving
|
|
141
|
+
* stale active task IDs that permanently block new work from draining.
|
|
142
|
+
*
|
|
143
|
+
* Bumps lane generation and clears execution counters so stale completions
|
|
144
|
+
* from old in-flight tasks are ignored. Queued entries are intentionally
|
|
145
|
+
* preserved — they represent pending user work that should still execute
|
|
146
|
+
* after restart.
|
|
147
|
+
*
|
|
148
|
+
* After resetting, drains any lanes that still have queued entries so
|
|
149
|
+
* preserved work is pumped immediately rather than waiting for a future
|
|
150
|
+
* `enqueueCommandInLane()` call (which may never come).
|
|
151
|
+
*/
|
|
152
|
+
export function resetAllLanes() {
|
|
153
|
+
const lanesToDrain = [];
|
|
154
|
+
for (const state of lanes.values()) {
|
|
155
|
+
state.generation += 1;
|
|
156
|
+
state.activeTaskIds.clear();
|
|
157
|
+
state.draining = false;
|
|
158
|
+
if (state.queue.length > 0) {
|
|
159
|
+
lanesToDrain.push(state.lane);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Drain after the full reset pass so all lanes are in a clean state first.
|
|
163
|
+
for (const lane of lanesToDrain) {
|
|
164
|
+
drainLane(lane);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Returns the total number of actively executing tasks across all lanes
|
|
169
|
+
* (excludes queued-but-not-started entries).
|
|
170
|
+
*/
|
|
171
|
+
export function getActiveTaskCount() {
|
|
172
|
+
let total = 0;
|
|
173
|
+
for (const s of lanes.values()) {
|
|
174
|
+
total += s.activeTaskIds.size;
|
|
175
|
+
}
|
|
176
|
+
return total;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Wait for all currently active tasks across all lanes to finish.
|
|
180
|
+
* Polls at a short interval; resolves when no tasks are active or
|
|
181
|
+
* when `timeoutMs` elapses (whichever comes first).
|
|
182
|
+
*
|
|
183
|
+
* New tasks enqueued after this call are ignored — only tasks that are
|
|
184
|
+
* already executing are waited on.
|
|
185
|
+
*/
|
|
186
|
+
export function waitForActiveTasks(timeoutMs) {
|
|
187
|
+
// Keep shutdown/drain checks responsive without busy looping.
|
|
188
|
+
const POLL_INTERVAL_MS = 50;
|
|
189
|
+
const deadline = Date.now() + timeoutMs;
|
|
190
|
+
const activeAtStart = new Set();
|
|
191
|
+
for (const state of lanes.values()) {
|
|
192
|
+
for (const taskId of state.activeTaskIds) {
|
|
193
|
+
activeAtStart.add(taskId);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return new Promise((resolve) => {
|
|
197
|
+
const check = () => {
|
|
198
|
+
if (activeAtStart.size === 0) {
|
|
199
|
+
resolve({ drained: true });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
let hasPending = false;
|
|
203
|
+
for (const state of lanes.values()) {
|
|
204
|
+
for (const taskId of state.activeTaskIds) {
|
|
205
|
+
if (activeAtStart.has(taskId)) {
|
|
206
|
+
hasPending = true;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (hasPending) {
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!hasPending) {
|
|
215
|
+
resolve({ drained: true });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (Date.now() >= deadline) {
|
|
219
|
+
resolve({ drained: false });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
setTimeout(check, POLL_INTERVAL_MS);
|
|
223
|
+
};
|
|
224
|
+
check();
|
|
225
|
+
});
|
|
226
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns an iteration hook for in-process restart loops.
|
|
3
|
+
* The first call is considered initial startup and does nothing.
|
|
4
|
+
* Each subsequent call represents a restart iteration and invokes `onRestart`.
|
|
5
|
+
*/
|
|
6
|
+
export function createRestartIterationHook(onRestart) {
|
|
7
|
+
let isFirstIteration = true;
|
|
8
|
+
return () => {
|
|
9
|
+
if (isFirstIteration) {
|
|
10
|
+
isFirstIteration = false;
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
onRestart();
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Shared tool-risk constants.
|
|
2
|
+
// Keep these centralized so gateway HTTP restrictions, security audits, and ACP prompts don't drift.
|
|
3
|
+
/**
|
|
4
|
+
* Tools denied via Gateway HTTP `POST /tools/invoke` by default.
|
|
5
|
+
* These are high-risk because they enable session orchestration, control-plane actions,
|
|
6
|
+
* or interactive flows that don't make sense over a non-interactive HTTP surface.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_GATEWAY_HTTP_TOOL_DENY = [
|
|
9
|
+
// Session orchestration — spawning agents remotely is RCE
|
|
10
|
+
"sessions_spawn",
|
|
11
|
+
// Cross-session injection — message injection across sessions
|
|
12
|
+
"sessions_send",
|
|
13
|
+
// Gateway control plane — prevents gateway reconfiguration via HTTP
|
|
14
|
+
"gateway",
|
|
15
|
+
// Interactive setup — requires terminal QR scan, hangs on HTTP
|
|
16
|
+
"whatsapp_login",
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* ACP tools that should always require explicit user approval.
|
|
20
|
+
* ACP is an automation surface; we never want "silent yes" for mutating/execution tools.
|
|
21
|
+
*/
|
|
22
|
+
export const DANGEROUS_ACP_TOOL_NAMES = [
|
|
23
|
+
"exec",
|
|
24
|
+
"spawn",
|
|
25
|
+
"shell",
|
|
26
|
+
"sessions_spawn",
|
|
27
|
+
"sessions_send",
|
|
28
|
+
"gateway",
|
|
29
|
+
"fs_write",
|
|
30
|
+
"fs_delete",
|
|
31
|
+
"fs_move",
|
|
32
|
+
"apply_patch",
|
|
33
|
+
];
|
|
34
|
+
export const DANGEROUS_ACP_TOOLS = new Set(DANGEROUS_ACP_TOOL_NAMES);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
export function safeEqualSecret(provided, expected) {
|
|
3
|
+
if (typeof provided !== "string" || typeof expected !== "string") {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const providedBuffer = Buffer.from(provided);
|
|
7
|
+
const expectedBuffer = Buffer.from(expected);
|
|
8
|
+
if (providedBuffer.length !== expectedBuffer.length) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
return timingSafeEqual(providedBuffer, expectedBuffer);
|
|
12
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Input, matchesKey, SelectList, getEditorKeybindings, } from "@mariozechner/pi-tui";
|
|
1
|
+
import { Input, matchesKey, SelectList, getEditorKeybindings, truncateToWidth, } from "@mariozechner/pi-tui";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { fuzzyFilterLower, prepareSearchItems } from "./fuzzy-filter.js";
|
|
4
4
|
/**
|
|
@@ -46,7 +46,10 @@ export class FilterableSelectList {
|
|
|
46
46
|
// Select list
|
|
47
47
|
const listLines = this.selectList.render(width);
|
|
48
48
|
lines.push(...listLines);
|
|
49
|
-
|
|
49
|
+
// Defensive truncation: ensure no line exceeds the target width after
|
|
50
|
+
// all ANSI formatting. Prevents pi-tui compositeLineAt crash on
|
|
51
|
+
// ANSI-heavy backgrounds ("Rendered line N exceeds terminal width").
|
|
52
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
50
53
|
}
|
|
51
54
|
handleInput(keyData) {
|
|
52
55
|
const allowVimNav = !this.filterText.trim();
|
|
@@ -149,7 +149,12 @@ export class SearchableSelectList {
|
|
|
149
149
|
const scrollInfo = `${this.selectedIndex + 1}/${this.filteredItems.length}`;
|
|
150
150
|
lines.push(this.theme.scrollInfo(` ${scrollInfo}`));
|
|
151
151
|
}
|
|
152
|
-
|
|
152
|
+
// Defensive truncation: ensure no line exceeds the target width after
|
|
153
|
+
// all ANSI formatting has been applied. pi-tui's compositeLineAt can
|
|
154
|
+
// miscalculate visible widths on ANSI-heavy lines, causing a crash
|
|
155
|
+
// ("Rendered line N exceeds terminal width"). Truncating here is a
|
|
156
|
+
// safe final guard that prevents the crash.
|
|
157
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
153
158
|
}
|
|
154
159
|
renderItemLine(item, isSelected, width, query) {
|
|
155
160
|
const prefix = isSelected ? "→ " : " ";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { truncateToWidth } from "@mariozechner/pi-tui";
|
|
1
2
|
import { formatThinkingLevels, normalizeUsageDisplay, resolveResponseUsageMode, } from "../auto-reply/thinking.js";
|
|
2
3
|
import { normalizeAgentId } from "../routing/session-key.js";
|
|
3
4
|
import { formatRelativeTime } from "../utils/time-format.js";
|
|
@@ -10,6 +11,10 @@ export function createCommandHandlers(context) {
|
|
|
10
11
|
state.currentAgentId = normalizeAgentId(id);
|
|
11
12
|
await setSession("");
|
|
12
13
|
};
|
|
14
|
+
// Maximum visible width for overlay item strings. The overlay has its own
|
|
15
|
+
// padding/chrome (~4 chars). We subtract a generous margin so that even
|
|
16
|
+
// after ANSI formatting, lines stay within terminal bounds.
|
|
17
|
+
const overlayItemWidth = () => Math.max(30, (tui.terminal.columns ?? 80) - 6);
|
|
13
18
|
const openModelSelector = async () => {
|
|
14
19
|
try {
|
|
15
20
|
const models = await client.listModels();
|
|
@@ -18,10 +23,11 @@ export function createCommandHandlers(context) {
|
|
|
18
23
|
tui.requestRender();
|
|
19
24
|
return;
|
|
20
25
|
}
|
|
26
|
+
const maxW = overlayItemWidth();
|
|
21
27
|
const items = models.map((model) => ({
|
|
22
28
|
value: `${model.provider}/${model.id}`,
|
|
23
|
-
label: `${model.provider}/${model.id}`,
|
|
24
|
-
description: model.name && model.name !== model.id ? model.name : "",
|
|
29
|
+
label: truncateToWidth(`${model.provider}/${model.id}`, maxW),
|
|
30
|
+
description: model.name && model.name !== model.id ? truncateToWidth(model.name, maxW) : "",
|
|
25
31
|
}));
|
|
26
32
|
const selector = createSearchableSelectList(items, 9);
|
|
27
33
|
selector.onSelect = (item) => {
|
|
@@ -60,9 +66,10 @@ export function createCommandHandlers(context) {
|
|
|
60
66
|
tui.requestRender();
|
|
61
67
|
return;
|
|
62
68
|
}
|
|
69
|
+
const maxW = overlayItemWidth();
|
|
63
70
|
const items = state.agents.map((agent) => ({
|
|
64
71
|
value: agent.id,
|
|
65
|
-
label: agent.name ? `${agent.id} (${agent.name})` : agent.id,
|
|
72
|
+
label: truncateToWidth(agent.name ? `${agent.id} (${agent.name})` : agent.id, maxW),
|
|
66
73
|
description: agent.id === state.agentDefaultId ? "default" : "",
|
|
67
74
|
}));
|
|
68
75
|
const selector = createSearchableSelectList(items, 9);
|
|
@@ -89,6 +96,7 @@ export function createCommandHandlers(context) {
|
|
|
89
96
|
includeLastMessage: true,
|
|
90
97
|
agentId: state.currentAgentId,
|
|
91
98
|
});
|
|
99
|
+
const maxW = overlayItemWidth();
|
|
92
100
|
const items = result.sessions.map((session) => {
|
|
93
101
|
const title = session.derivedTitle ?? session.displayName;
|
|
94
102
|
const formattedKey = formatSessionKey(session.key);
|
|
@@ -100,8 +108,8 @@ export function createCommandHandlers(context) {
|
|
|
100
108
|
const description = timePart && preview ? `${timePart} · ${preview}` : (preview ?? timePart);
|
|
101
109
|
return {
|
|
102
110
|
value: session.key,
|
|
103
|
-
label,
|
|
104
|
-
description,
|
|
111
|
+
label: truncateToWidth(label, maxW),
|
|
112
|
+
description: truncateToWidth(description, maxW),
|
|
105
113
|
searchText: [
|
|
106
114
|
session.displayName,
|
|
107
115
|
session.label,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poolzin/pool-bot",
|
|
3
|
-
"version": "2026.2.
|
|
3
|
+
"version": "2026.2.8",
|
|
4
4
|
"description": "🎱 Pool Bot - AI assistant with PLCODE integrations",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "MIT",
|
|
@@ -110,10 +110,10 @@
|
|
|
110
110
|
"@larksuiteoapi/node-sdk": "^1.58.0",
|
|
111
111
|
"@line/bot-sdk": "^10.6.0",
|
|
112
112
|
"@lydell/node-pty": "1.2.0-beta.3",
|
|
113
|
-
"@mariozechner/pi-agent-core": "0.52.
|
|
114
|
-
"@mariozechner/pi-ai": "0.52.
|
|
115
|
-
"@mariozechner/pi-coding-agent": "0.52.
|
|
116
|
-
"@mariozechner/pi-tui": "0.52.
|
|
113
|
+
"@mariozechner/pi-agent-core": "0.52.12",
|
|
114
|
+
"@mariozechner/pi-ai": "0.52.12",
|
|
115
|
+
"@mariozechner/pi-coding-agent": "0.52.12",
|
|
116
|
+
"@mariozechner/pi-tui": "0.52.12",
|
|
117
117
|
"@mozilla/readability": "^0.6.0",
|
|
118
118
|
"@sinclair/typebox": "0.34.48",
|
|
119
119
|
"@slack/bolt": "^4.6.0",
|