@poolzin/pool-bot 2026.2.7 → 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/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/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseReplyDirectives } from "../auto-reply/reply/reply-directives.js";
|
|
2
2
|
import { createStreamingDirectiveAccumulator } from "../auto-reply/reply/streaming-directives.js";
|
|
3
3
|
import { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
|
4
|
+
import { emitAgentEvent } from "../infra/agent-events.js";
|
|
4
5
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
5
6
|
import { buildCodeSpanIndex, createInlineCodeState } from "../markdown/code-spans.js";
|
|
6
7
|
import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js";
|
|
@@ -46,7 +47,9 @@ export function subscribeEmbeddedPiSession(params) {
|
|
|
46
47
|
compactionInFlight: false,
|
|
47
48
|
pendingCompactionRetry: 0,
|
|
48
49
|
compactionRetryResolve: undefined,
|
|
50
|
+
compactionRetryReject: undefined,
|
|
49
51
|
compactionRetryPromise: null,
|
|
52
|
+
unsubscribed: false,
|
|
50
53
|
messagingToolSentTexts: [],
|
|
51
54
|
messagingToolSentTextsNormalized: [],
|
|
52
55
|
messagingToolSentTargets: [],
|
|
@@ -168,8 +171,12 @@ export function subscribeEmbeddedPiSession(params) {
|
|
|
168
171
|
};
|
|
169
172
|
const ensureCompactionPromise = () => {
|
|
170
173
|
if (!state.compactionRetryPromise) {
|
|
171
|
-
state.compactionRetryPromise = new Promise((resolve) => {
|
|
174
|
+
state.compactionRetryPromise = new Promise((resolve, reject) => {
|
|
172
175
|
state.compactionRetryResolve = resolve;
|
|
176
|
+
state.compactionRetryReject = reject;
|
|
177
|
+
});
|
|
178
|
+
state.compactionRetryPromise.catch((err) => {
|
|
179
|
+
log.debug(`compaction promise rejected (no waiter): ${String(err)}`);
|
|
173
180
|
});
|
|
174
181
|
}
|
|
175
182
|
};
|
|
@@ -185,6 +192,7 @@ export function subscribeEmbeddedPiSession(params) {
|
|
|
185
192
|
if (state.pendingCompactionRetry === 0 && !state.compactionInFlight) {
|
|
186
193
|
state.compactionRetryResolve?.();
|
|
187
194
|
state.compactionRetryResolve = undefined;
|
|
195
|
+
state.compactionRetryReject = undefined;
|
|
188
196
|
state.compactionRetryPromise = null;
|
|
189
197
|
}
|
|
190
198
|
};
|
|
@@ -192,6 +200,7 @@ export function subscribeEmbeddedPiSession(params) {
|
|
|
192
200
|
if (state.pendingCompactionRetry === 0 && !state.compactionInFlight) {
|
|
193
201
|
state.compactionRetryResolve?.();
|
|
194
202
|
state.compactionRetryResolve = undefined;
|
|
203
|
+
state.compactionRetryReject = undefined;
|
|
195
204
|
state.compactionRetryPromise = null;
|
|
196
205
|
}
|
|
197
206
|
};
|
|
@@ -460,7 +469,17 @@ export function subscribeEmbeddedPiSession(params) {
|
|
|
460
469
|
if (formatted === state.lastStreamedReasoning) {
|
|
461
470
|
return;
|
|
462
471
|
}
|
|
472
|
+
const prior = state.lastStreamedReasoning ?? "";
|
|
473
|
+
const delta = formatted.startsWith(prior) ? formatted.slice(prior.length) : formatted;
|
|
463
474
|
state.lastStreamedReasoning = formatted;
|
|
475
|
+
emitAgentEvent({
|
|
476
|
+
runId: params.runId,
|
|
477
|
+
stream: "thinking",
|
|
478
|
+
data: {
|
|
479
|
+
text: formatted,
|
|
480
|
+
delta,
|
|
481
|
+
},
|
|
482
|
+
});
|
|
464
483
|
void params.onReasoningStream({
|
|
465
484
|
text: formatted,
|
|
466
485
|
});
|
|
@@ -478,12 +497,19 @@ export function subscribeEmbeddedPiSession(params) {
|
|
|
478
497
|
pendingMessagingTargets.clear();
|
|
479
498
|
resetAssistantMessageState(0);
|
|
480
499
|
};
|
|
500
|
+
const noteLastAssistant = (msg) => {
|
|
501
|
+
if (msg?.role === "assistant") {
|
|
502
|
+
state.lastAssistant = msg;
|
|
503
|
+
}
|
|
504
|
+
};
|
|
481
505
|
const ctx = {
|
|
482
506
|
params,
|
|
483
507
|
state,
|
|
484
508
|
log,
|
|
485
509
|
blockChunking,
|
|
486
510
|
blockChunker,
|
|
511
|
+
hookRunner: params.hookRunner,
|
|
512
|
+
noteLastAssistant,
|
|
487
513
|
shouldEmitToolResult,
|
|
488
514
|
shouldEmitToolOutput,
|
|
489
515
|
emitToolSummary,
|
|
@@ -507,12 +533,30 @@ export function subscribeEmbeddedPiSession(params) {
|
|
|
507
533
|
getUsageTotals,
|
|
508
534
|
getCompactionCount: () => compactionCount,
|
|
509
535
|
};
|
|
510
|
-
const
|
|
536
|
+
const sessionUnsubscribe = params.session.subscribe(createEmbeddedPiSessionEventHandler(ctx));
|
|
537
|
+
const unsubscribe = () => {
|
|
538
|
+
if (state.unsubscribed) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
state.unsubscribed = true;
|
|
542
|
+
if (state.compactionRetryPromise) {
|
|
543
|
+
log.debug(`unsubscribe: rejecting compaction wait runId=${params.runId}`);
|
|
544
|
+
const reject = state.compactionRetryReject;
|
|
545
|
+
state.compactionRetryResolve = undefined;
|
|
546
|
+
state.compactionRetryReject = undefined;
|
|
547
|
+
state.compactionRetryPromise = null;
|
|
548
|
+
const abortErr = new Error("Unsubscribed during compaction");
|
|
549
|
+
abortErr.name = "AbortError";
|
|
550
|
+
reject?.(abortErr);
|
|
551
|
+
}
|
|
552
|
+
sessionUnsubscribe();
|
|
553
|
+
};
|
|
511
554
|
return {
|
|
512
555
|
assistantTexts,
|
|
513
556
|
toolMetas,
|
|
514
557
|
unsubscribe,
|
|
515
558
|
isCompacting: () => state.compactionInFlight || state.pendingCompactionRetry > 0,
|
|
559
|
+
isCompactionInFlight: () => state.compactionInFlight,
|
|
516
560
|
getMessagingToolSentTexts: () => messagingToolSentTexts.slice(),
|
|
517
561
|
getMessagingToolSentTargets: () => messagingToolSentTargets.slice(),
|
|
518
562
|
// Returns true if any messaging tool successfully sent a message.
|
|
@@ -523,15 +567,26 @@ export function subscribeEmbeddedPiSession(params) {
|
|
|
523
567
|
getUsageTotals,
|
|
524
568
|
getCompactionCount: () => compactionCount,
|
|
525
569
|
waitForCompactionRetry: () => {
|
|
570
|
+
if (state.unsubscribed) {
|
|
571
|
+
const err = new Error("Unsubscribed during compaction wait");
|
|
572
|
+
err.name = "AbortError";
|
|
573
|
+
return Promise.reject(err);
|
|
574
|
+
}
|
|
526
575
|
if (state.compactionInFlight || state.pendingCompactionRetry > 0) {
|
|
527
576
|
ensureCompactionPromise();
|
|
528
577
|
return state.compactionRetryPromise ?? Promise.resolve();
|
|
529
578
|
}
|
|
530
|
-
return new Promise((resolve) => {
|
|
579
|
+
return new Promise((resolve, reject) => {
|
|
531
580
|
queueMicrotask(() => {
|
|
581
|
+
if (state.unsubscribed) {
|
|
582
|
+
const err = new Error("Unsubscribed during compaction wait");
|
|
583
|
+
err.name = "AbortError";
|
|
584
|
+
reject(err);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
532
587
|
if (state.compactionInFlight || state.pendingCompactionRetry > 0) {
|
|
533
588
|
ensureCompactionPromise();
|
|
534
|
-
void (state.compactionRetryPromise ?? Promise.resolve()).then(resolve);
|
|
589
|
+
void (state.compactionRetryPromise ?? Promise.resolve()).then(resolve, reject);
|
|
535
590
|
}
|
|
536
591
|
else {
|
|
537
592
|
resolve();
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const MUTATING_TOOL_NAMES = new Set([
|
|
2
|
+
"write",
|
|
3
|
+
"edit",
|
|
4
|
+
"apply_patch",
|
|
5
|
+
"exec",
|
|
6
|
+
"bash",
|
|
7
|
+
"process",
|
|
8
|
+
"message",
|
|
9
|
+
"sessions_send",
|
|
10
|
+
"cron",
|
|
11
|
+
"gateway",
|
|
12
|
+
"canvas",
|
|
13
|
+
"nodes",
|
|
14
|
+
"session_status",
|
|
15
|
+
]);
|
|
16
|
+
const READ_ONLY_ACTIONS = new Set([
|
|
17
|
+
"get",
|
|
18
|
+
"list",
|
|
19
|
+
"read",
|
|
20
|
+
"status",
|
|
21
|
+
"show",
|
|
22
|
+
"fetch",
|
|
23
|
+
"search",
|
|
24
|
+
"query",
|
|
25
|
+
"view",
|
|
26
|
+
"poll",
|
|
27
|
+
"log",
|
|
28
|
+
"inspect",
|
|
29
|
+
"check",
|
|
30
|
+
"probe",
|
|
31
|
+
]);
|
|
32
|
+
const PROCESS_MUTATING_ACTIONS = new Set(["write", "send_keys", "submit", "paste", "kill"]);
|
|
33
|
+
const MESSAGE_MUTATING_ACTIONS = new Set([
|
|
34
|
+
"send",
|
|
35
|
+
"reply",
|
|
36
|
+
"thread_reply",
|
|
37
|
+
"threadreply",
|
|
38
|
+
"edit",
|
|
39
|
+
"delete",
|
|
40
|
+
"react",
|
|
41
|
+
"pin",
|
|
42
|
+
"unpin",
|
|
43
|
+
]);
|
|
44
|
+
function asRecord(value) {
|
|
45
|
+
return value && typeof value === "object" ? value : undefined;
|
|
46
|
+
}
|
|
47
|
+
function normalizeActionName(value) {
|
|
48
|
+
if (typeof value !== "string") {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const normalized = value
|
|
52
|
+
.trim()
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/[\s-]+/g, "_");
|
|
55
|
+
return normalized || undefined;
|
|
56
|
+
}
|
|
57
|
+
function normalizeFingerprintValue(value) {
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
const normalized = value.trim();
|
|
60
|
+
return normalized ? normalized.toLowerCase() : undefined;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
|
|
63
|
+
return String(value).toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
export function isLikelyMutatingToolName(toolName) {
|
|
68
|
+
const normalized = toolName.trim().toLowerCase();
|
|
69
|
+
if (!normalized) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return (MUTATING_TOOL_NAMES.has(normalized) ||
|
|
73
|
+
normalized.endsWith("_actions") ||
|
|
74
|
+
normalized.startsWith("message_") ||
|
|
75
|
+
normalized.includes("send"));
|
|
76
|
+
}
|
|
77
|
+
export function isMutatingToolCall(toolName, args) {
|
|
78
|
+
const normalized = toolName.trim().toLowerCase();
|
|
79
|
+
const record = asRecord(args);
|
|
80
|
+
const action = normalizeActionName(record?.action);
|
|
81
|
+
switch (normalized) {
|
|
82
|
+
case "write":
|
|
83
|
+
case "edit":
|
|
84
|
+
case "apply_patch":
|
|
85
|
+
case "exec":
|
|
86
|
+
case "bash":
|
|
87
|
+
case "sessions_send":
|
|
88
|
+
return true;
|
|
89
|
+
case "process":
|
|
90
|
+
return action != null && PROCESS_MUTATING_ACTIONS.has(action);
|
|
91
|
+
case "message":
|
|
92
|
+
return ((action != null && MESSAGE_MUTATING_ACTIONS.has(action)) ||
|
|
93
|
+
typeof record?.content === "string" ||
|
|
94
|
+
typeof record?.message === "string");
|
|
95
|
+
case "session_status":
|
|
96
|
+
return typeof record?.model === "string" && record.model.trim().length > 0;
|
|
97
|
+
default: {
|
|
98
|
+
if (normalized === "cron" || normalized === "gateway" || normalized === "canvas") {
|
|
99
|
+
return action == null || !READ_ONLY_ACTIONS.has(action);
|
|
100
|
+
}
|
|
101
|
+
if (normalized === "nodes") {
|
|
102
|
+
return action == null || action !== "list";
|
|
103
|
+
}
|
|
104
|
+
if (normalized.endsWith("_actions")) {
|
|
105
|
+
return action == null || !READ_ONLY_ACTIONS.has(action);
|
|
106
|
+
}
|
|
107
|
+
if (normalized.startsWith("message_") || normalized.includes("send")) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function buildToolActionFingerprint(toolName, args, meta) {
|
|
115
|
+
if (!isMutatingToolCall(toolName, args)) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
const normalizedTool = toolName.trim().toLowerCase();
|
|
119
|
+
const record = asRecord(args);
|
|
120
|
+
const action = normalizeActionName(record?.action);
|
|
121
|
+
const parts = [`tool=${normalizedTool}`];
|
|
122
|
+
if (action) {
|
|
123
|
+
parts.push(`action=${action}`);
|
|
124
|
+
}
|
|
125
|
+
for (const key of [
|
|
126
|
+
"path",
|
|
127
|
+
"filePath",
|
|
128
|
+
"oldPath",
|
|
129
|
+
"newPath",
|
|
130
|
+
"to",
|
|
131
|
+
"target",
|
|
132
|
+
"messageId",
|
|
133
|
+
"sessionKey",
|
|
134
|
+
"jobId",
|
|
135
|
+
"id",
|
|
136
|
+
"model",
|
|
137
|
+
]) {
|
|
138
|
+
const value = normalizeFingerprintValue(record?.[key]);
|
|
139
|
+
if (value) {
|
|
140
|
+
parts.push(`${key.toLowerCase()}=${value}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const normalizedMeta = meta?.trim().replace(/\s+/g, " ").toLowerCase();
|
|
144
|
+
if (normalizedMeta) {
|
|
145
|
+
parts.push(`meta=${normalizedMeta}`);
|
|
146
|
+
}
|
|
147
|
+
return parts.join("|");
|
|
148
|
+
}
|
|
149
|
+
export function buildToolMutationState(toolName, args, meta) {
|
|
150
|
+
const actionFingerprint = buildToolActionFingerprint(toolName, args, meta);
|
|
151
|
+
return {
|
|
152
|
+
mutatingAction: actionFingerprint != null,
|
|
153
|
+
actionFingerprint,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export function isSameToolMutationAction(existing, next) {
|
|
157
|
+
if (existing.actionFingerprint != null || next.actionFingerprint != null) {
|
|
158
|
+
// For mutating flows, fail closed: only clear when both fingerprints exist and match.
|
|
159
|
+
return (existing.actionFingerprint != null &&
|
|
160
|
+
next.actionFingerprint != null &&
|
|
161
|
+
existing.actionFingerprint === next.actionFingerprint);
|
|
162
|
+
}
|
|
163
|
+
return existing.toolName === next.toolName && (existing.meta ?? "") === (next.meta ?? "");
|
|
164
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { filterToolsByPolicy } from "./pi-tools.policy.js";
|
|
2
|
+
import { buildPluginToolGroups, expandPolicyWithPluginGroups, normalizeToolName, stripPluginOnlyAllowlist, } from "./tool-policy.js";
|
|
3
|
+
export function buildDefaultToolPolicyPipelineSteps(params) {
|
|
4
|
+
const agentId = params.agentId?.trim();
|
|
5
|
+
const profile = params.profile?.trim();
|
|
6
|
+
const providerProfile = params.providerProfile?.trim();
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
policy: params.profilePolicy,
|
|
10
|
+
label: profile ? `tools.profile (${profile})` : "tools.profile",
|
|
11
|
+
stripPluginOnlyAllowlist: true,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
policy: params.providerProfilePolicy,
|
|
15
|
+
label: providerProfile
|
|
16
|
+
? `tools.byProvider.profile (${providerProfile})`
|
|
17
|
+
: "tools.byProvider.profile",
|
|
18
|
+
stripPluginOnlyAllowlist: true,
|
|
19
|
+
},
|
|
20
|
+
{ policy: params.globalPolicy, label: "tools.allow", stripPluginOnlyAllowlist: true },
|
|
21
|
+
{
|
|
22
|
+
policy: params.globalProviderPolicy,
|
|
23
|
+
label: "tools.byProvider.allow",
|
|
24
|
+
stripPluginOnlyAllowlist: true,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
policy: params.agentPolicy,
|
|
28
|
+
label: agentId ? `agents.${agentId}.tools.allow` : "agent tools.allow",
|
|
29
|
+
stripPluginOnlyAllowlist: true,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
policy: params.agentProviderPolicy,
|
|
33
|
+
label: agentId ? `agents.${agentId}.tools.byProvider.allow` : "agent tools.byProvider.allow",
|
|
34
|
+
stripPluginOnlyAllowlist: true,
|
|
35
|
+
},
|
|
36
|
+
{ policy: params.groupPolicy, label: "group tools.allow", stripPluginOnlyAllowlist: true },
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
export function applyToolPolicyPipeline(params) {
|
|
40
|
+
const coreToolNames = new Set(params.tools
|
|
41
|
+
.filter((tool) => !params.toolMeta(tool))
|
|
42
|
+
.map((tool) => normalizeToolName(tool.name))
|
|
43
|
+
.filter(Boolean));
|
|
44
|
+
const pluginGroups = buildPluginToolGroups({
|
|
45
|
+
tools: params.tools,
|
|
46
|
+
toolMeta: params.toolMeta,
|
|
47
|
+
});
|
|
48
|
+
let filtered = params.tools;
|
|
49
|
+
for (const step of params.steps) {
|
|
50
|
+
if (!step.policy) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
let policy = step.policy;
|
|
54
|
+
if (step.stripPluginOnlyAllowlist) {
|
|
55
|
+
const resolved = stripPluginOnlyAllowlist(policy, pluginGroups, coreToolNames);
|
|
56
|
+
if (resolved.unknownAllowlist.length > 0) {
|
|
57
|
+
const entries = resolved.unknownAllowlist.join(", ");
|
|
58
|
+
const suffix = resolved.strippedAllowlist
|
|
59
|
+
? "Ignoring allowlist so core tools remain available. Use tools.alsoAllow for additive plugin tool enablement."
|
|
60
|
+
: "These entries won't match any tool unless the plugin is enabled.";
|
|
61
|
+
params.warn(`tools: ${step.label} allowlist contains unknown entries (${entries}). ${suffix}`);
|
|
62
|
+
}
|
|
63
|
+
policy = resolved.policy;
|
|
64
|
+
}
|
|
65
|
+
const expanded = expandPolicyWithPluginGroups(policy, pluginGroups);
|
|
66
|
+
filtered = expanded ? filterToolsByPolicy(filtered, expanded) : filtered;
|
|
67
|
+
}
|
|
68
|
+
return filtered;
|
|
69
|
+
}
|
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { acquireGatewayLock } from "../../infra/gateway-lock.js";
|
|
2
2
|
import { consumeGatewaySigusr1RestartAuthorization, isGatewaySigusr1RestartExternallyAllowed, } from "../../infra/restart.js";
|
|
3
3
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
|
4
|
+
import { getActiveTaskCount, resetAllLanes, waitForActiveTasks, } from "../../process/command-queue.js";
|
|
5
|
+
import { createRestartIterationHook } from "../../process/restart-recovery.js";
|
|
4
6
|
const gatewayLog = createSubsystemLogger("gateway");
|
|
5
7
|
export async function runGatewayLoop(params) {
|
|
6
8
|
const lock = await acquireGatewayLock();
|
|
@@ -12,6 +14,8 @@ export async function runGatewayLoop(params) {
|
|
|
12
14
|
process.removeListener("SIGINT", onSigint);
|
|
13
15
|
process.removeListener("SIGUSR1", onSigusr1);
|
|
14
16
|
};
|
|
17
|
+
const DRAIN_TIMEOUT_MS = 30_000;
|
|
18
|
+
const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
15
19
|
const request = (action, signal) => {
|
|
16
20
|
if (shuttingDown) {
|
|
17
21
|
gatewayLog.info(`received ${signal} during shutdown; ignoring`);
|
|
@@ -20,13 +24,30 @@ export async function runGatewayLoop(params) {
|
|
|
20
24
|
shuttingDown = true;
|
|
21
25
|
const isRestart = action === "restart";
|
|
22
26
|
gatewayLog.info(`received ${signal}; ${isRestart ? "restarting" : "shutting down"}`);
|
|
27
|
+
// Allow extra time for draining active turns on restart.
|
|
28
|
+
const forceExitMs = isRestart ? DRAIN_TIMEOUT_MS + SHUTDOWN_TIMEOUT_MS : SHUTDOWN_TIMEOUT_MS;
|
|
23
29
|
const forceExitTimer = setTimeout(() => {
|
|
24
30
|
gatewayLog.error("shutdown timed out; exiting without full cleanup");
|
|
25
31
|
cleanupSignals();
|
|
26
32
|
params.runtime.exit(0);
|
|
27
|
-
},
|
|
33
|
+
}, forceExitMs);
|
|
28
34
|
void (async () => {
|
|
29
35
|
try {
|
|
36
|
+
// On restart, wait for in-flight agent turns to finish before
|
|
37
|
+
// tearing down the server so buffered messages are delivered.
|
|
38
|
+
if (isRestart) {
|
|
39
|
+
const activeTasks = getActiveTaskCount();
|
|
40
|
+
if (activeTasks > 0) {
|
|
41
|
+
gatewayLog.info(`draining ${activeTasks} active task(s) before restart (timeout ${DRAIN_TIMEOUT_MS}ms)`);
|
|
42
|
+
const { drained } = await waitForActiveTasks(DRAIN_TIMEOUT_MS);
|
|
43
|
+
if (drained) {
|
|
44
|
+
gatewayLog.info("all active tasks drained");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
gatewayLog.warn("drain timeout reached; proceeding with restart");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
30
51
|
await server?.close({
|
|
31
52
|
reason: isRestart ? "gateway restarting" : "gateway stopping",
|
|
32
53
|
restartExpectedMs: isRestart ? 1500 : null,
|
|
@@ -39,6 +60,7 @@ export async function runGatewayLoop(params) {
|
|
|
39
60
|
clearTimeout(forceExitTimer);
|
|
40
61
|
server = null;
|
|
41
62
|
if (isRestart) {
|
|
63
|
+
// In-process restart (no process-respawn in pool-bot).
|
|
42
64
|
shuttingDown = false;
|
|
43
65
|
restartResolver?.();
|
|
44
66
|
}
|
|
@@ -70,10 +92,20 @@ export async function runGatewayLoop(params) {
|
|
|
70
92
|
process.on("SIGINT", onSigint);
|
|
71
93
|
process.on("SIGUSR1", onSigusr1);
|
|
72
94
|
try {
|
|
95
|
+
const onIteration = createRestartIterationHook(() => {
|
|
96
|
+
// After an in-process restart (SIGUSR1), reset command-queue lane state.
|
|
97
|
+
// Interrupted tasks from the previous lifecycle may have left `active`
|
|
98
|
+
// counts elevated (their finally blocks never ran), permanently blocking
|
|
99
|
+
// new work from draining. This must happen here — at the restart
|
|
100
|
+
// coordinator level — rather than inside individual subsystem init
|
|
101
|
+
// functions, to avoid surprising cross-cutting side effects.
|
|
102
|
+
resetAllLanes();
|
|
103
|
+
});
|
|
73
104
|
// Keep process alive; SIGUSR1 triggers an in-process restart (no supervisor required).
|
|
74
105
|
// SIGTERM/SIGINT still exit after a graceful shutdown.
|
|
75
106
|
// eslint-disable-next-line no-constant-condition
|
|
76
107
|
while (true) {
|
|
108
|
+
onIteration();
|
|
77
109
|
server = await params.start();
|
|
78
110
|
await new Promise((resolve) => {
|
|
79
111
|
restartResolver = resolve;
|
package/dist/gateway/auth.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { safeEqualSecret } from "../security/secret-equal.js";
|
|
2
2
|
import { readTailscaleWhoisIdentity } from "../infra/tailscale.js";
|
|
3
3
|
import { isTrustedProxyAddress, parseForwardedForClientIp, resolveGatewayClientIp } from "./net.js";
|
|
4
|
-
function safeEqual(a, b) {
|
|
5
|
-
if (a.length !== b.length)
|
|
6
|
-
return false;
|
|
7
|
-
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
8
|
-
}
|
|
9
4
|
function normalizeLogin(login) {
|
|
10
5
|
return login.trim().toLowerCase();
|
|
11
6
|
}
|
|
@@ -175,7 +170,7 @@ export async function authorizeGatewayConnect(params) {
|
|
|
175
170
|
if (!connectAuth?.token) {
|
|
176
171
|
return { ok: false, reason: "token_missing" };
|
|
177
172
|
}
|
|
178
|
-
if (!
|
|
173
|
+
if (!safeEqualSecret(connectAuth.token, auth.token)) {
|
|
179
174
|
return { ok: false, reason: "token_mismatch" };
|
|
180
175
|
}
|
|
181
176
|
return { ok: true, method: "token" };
|
|
@@ -188,7 +183,7 @@ export async function authorizeGatewayConnect(params) {
|
|
|
188
183
|
if (!password) {
|
|
189
184
|
return { ok: false, reason: "password_missing" };
|
|
190
185
|
}
|
|
191
|
-
if (!
|
|
186
|
+
if (!safeEqualSecret(password, auth.password)) {
|
|
192
187
|
return { ok: false, reason: "password_mismatch" };
|
|
193
188
|
}
|
|
194
189
|
return { ok: true, method: "password" };
|
|
@@ -477,10 +477,18 @@ async function resolveActiveModelEntry(params) {
|
|
|
477
477
|
catch {
|
|
478
478
|
return null;
|
|
479
479
|
}
|
|
480
|
+
// Use the default vision/video model for the provider rather than the
|
|
481
|
+
// user's chat model — the chat model (e.g. glm-4.7) may not support
|
|
482
|
+
// vision, while the provider has a dedicated model (e.g. glm-4.6v).
|
|
483
|
+
const model = params.capability === "image"
|
|
484
|
+
? (DEFAULT_IMAGE_MODELS[providerId] ?? params.activeModel?.model)
|
|
485
|
+
: params.capability === "audio"
|
|
486
|
+
? (DEFAULT_AUDIO_MODELS[providerId] ?? params.activeModel?.model)
|
|
487
|
+
: params.activeModel?.model;
|
|
480
488
|
return {
|
|
481
489
|
type: "provider",
|
|
482
490
|
provider: providerId,
|
|
483
|
-
model
|
|
491
|
+
model,
|
|
484
492
|
};
|
|
485
493
|
}
|
|
486
494
|
function trimOutput(text, maxChars) {
|