@poolzin/pool-bot 2026.2.23 → 2026.2.25
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 +29 -0
- package/dist/acp/client.js +207 -18
- package/dist/acp/secret-file.js +22 -0
- package/dist/agents/agent-scope.js +10 -0
- package/dist/agents/bash-process-registry.test-helpers.js +29 -0
- package/dist/agents/bash-tools.exec-approval-request.js +20 -0
- package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
- package/dist/agents/bash-tools.exec-host-node.js +235 -0
- package/dist/agents/bash-tools.exec-types.js +1 -0
- package/dist/agents/bash-tools.process.js +224 -218
- package/dist/agents/content-blocks.js +16 -0
- package/dist/agents/model-fallback.js +96 -101
- package/dist/agents/models-config.providers.js +299 -182
- package/dist/agents/pi-embedded-payloads.js +1 -0
- package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
- package/dist/agents/skills.test-helpers.js +13 -0
- package/dist/agents/stable-stringify.js +12 -0
- package/dist/agents/subagent-registry.mocks.shared.js +12 -0
- package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
- package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
- package/dist/agents/tool-policy-shared.js +108 -0
- package/dist/agents/tools/browser-tool.js +160 -54
- package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
- package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
- package/dist/agents/tools/image-tool.js +214 -99
- package/dist/agents/tools/sessions-history-tool.js +140 -108
- package/dist/agents/workspace.js +222 -46
- package/dist/auto-reply/commands-registry.js +15 -18
- package/dist/auto-reply/fallback-state.js +114 -0
- package/dist/auto-reply/model-runtime.js +68 -0
- package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
- package/dist/auto-reply/reply/agent-runner.js +165 -39
- package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
- package/dist/browser/config.js +26 -0
- package/dist/browser/navigation-guard.js +31 -0
- package/dist/browser/routes/agent.act.js +431 -424
- package/dist/browser/routes/agent.shared.js +47 -3
- package/dist/browser/routes/agent.snapshot.js +122 -116
- package/dist/browser/routes/agent.storage.js +303 -297
- package/dist/browser/routes/tabs.js +154 -100
- package/dist/browser/server-lifecycle.js +37 -0
- package/dist/build-info.json +3 -3
- package/dist/channels/allow-from.js +25 -0
- package/dist/channels/plugins/account-action-gate.js +13 -0
- package/dist/channels/plugins/message-actions.js +10 -0
- package/dist/channels/telegram/api.js +18 -0
- package/dist/cli/argv.js +84 -21
- package/dist/cli/banner.js +2 -1
- package/dist/cli/exec-approvals-cli.js +92 -124
- package/dist/cli/memory-cli.js +158 -61
- package/dist/cli/nodes-cli/register.push.js +63 -0
- package/dist/cli/nodes-media-utils.js +21 -0
- package/dist/cli/plugins-cli.js +245 -61
- package/dist/cli/program/build-program.js +3 -1
- package/dist/cli/program/command-registry.js +223 -136
- package/dist/cli/program/help.js +43 -12
- package/dist/cli/route.js +1 -1
- package/dist/cli/test-runtime-capture.js +24 -0
- package/dist/commands/agent.js +163 -87
- package/dist/commands/channels.mock-harness.js +23 -0
- package/dist/commands/daemon-install-runtime-warning.js +11 -0
- package/dist/commands/onboard-helpers.js +4 -4
- package/dist/commands/sessions.test-helpers.js +61 -0
- package/dist/compat/legacy-names.js +2 -2
- package/dist/config/commands.js +3 -0
- package/dist/config/config.js +1 -1
- package/dist/config/env-substitution.js +62 -34
- package/dist/config/env-vars.js +9 -0
- package/dist/config/io.js +571 -171
- package/dist/config/merge-patch.js +50 -4
- package/dist/config/redact-snapshot.js +404 -76
- package/dist/config/schema.js +58 -570
- package/dist/config/validation.js +140 -85
- package/dist/config/zod-schema.hooks.js +40 -11
- package/dist/config/zod-schema.installs.js +20 -0
- package/dist/config/zod-schema.js +8 -7
- package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
- package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
- package/dist/control-ui/index.html +1 -1
- package/dist/daemon/cmd-argv.js +21 -0
- package/dist/daemon/cmd-set.js +58 -0
- package/dist/daemon/service-types.js +1 -0
- package/dist/discord/monitor/exec-approvals.js +357 -162
- package/dist/gateway/auth.js +38 -3
- package/dist/gateway/call.js +149 -68
- package/dist/gateway/canvas-capability.js +75 -0
- package/dist/gateway/control-plane-audit.js +28 -0
- package/dist/gateway/control-plane-rate-limit.js +53 -0
- package/dist/gateway/events.js +1 -0
- package/dist/gateway/hooks.js +109 -54
- package/dist/gateway/http-common.js +22 -0
- package/dist/gateway/method-scopes.js +169 -0
- package/dist/gateway/net.js +23 -0
- package/dist/gateway/openresponses-http.js +120 -110
- package/dist/gateway/probe-auth.js +2 -0
- package/dist/gateway/protocol/index.js +3 -2
- package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
- package/dist/gateway/protocol/schema/push.js +18 -0
- package/dist/gateway/protocol/schema.js +1 -0
- package/dist/gateway/server-http.js +236 -52
- package/dist/gateway/server-methods/agent.js +162 -24
- package/dist/gateway/server-methods/chat.js +461 -130
- package/dist/gateway/server-methods/config.js +193 -150
- package/dist/gateway/server-methods/nodes.helpers.js +12 -0
- package/dist/gateway/server-methods/nodes.js +251 -69
- package/dist/gateway/server-methods/push.js +53 -0
- package/dist/gateway/server-reload-handlers.js +2 -3
- package/dist/gateway/server-runtime-config.js +5 -0
- package/dist/gateway/server-runtime-state.js +2 -0
- package/dist/gateway/server-ws-runtime.js +1 -0
- package/dist/gateway/server.impl.js +296 -139
- package/dist/gateway/session-preview.test-helpers.js +11 -0
- package/dist/gateway/startup-auth.js +126 -0
- package/dist/gateway/test-helpers.agent-results.js +15 -0
- package/dist/gateway/test-helpers.mocks.js +37 -14
- package/dist/gateway/test-helpers.server.js +161 -77
- package/dist/hooks/bundled/session-memory/handler.js +165 -34
- package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
- package/dist/infra/archive-path.js +49 -0
- package/dist/infra/device-pairing.js +148 -167
- package/dist/infra/exec-approvals-allowlist.js +19 -70
- package/dist/infra/exec-approvals-analysis.js +44 -17
- package/dist/infra/exec-safe-bin-policy.js +269 -0
- package/dist/infra/fixed-window-rate-limit.js +33 -0
- package/dist/infra/git-root.js +61 -0
- package/dist/infra/heartbeat-active-hours.js +2 -2
- package/dist/infra/heartbeat-reason.js +40 -0
- package/dist/infra/heartbeat-runner.js +72 -32
- package/dist/infra/install-source-utils.js +91 -7
- package/dist/infra/node-pairing.js +50 -105
- package/dist/infra/npm-integrity.js +45 -0
- package/dist/infra/npm-pack-install.js +40 -0
- package/dist/infra/outbound/channel-adapters.js +20 -7
- package/dist/infra/outbound/message-action-runner.js +107 -327
- package/dist/infra/outbound/message.js +59 -36
- package/dist/infra/outbound/outbound-policy.js +52 -25
- package/dist/infra/outbound/outbound-send-service.js +58 -71
- package/dist/infra/pairing-files.js +10 -0
- package/dist/infra/plain-object.js +9 -0
- package/dist/infra/push-apns.js +365 -0
- package/dist/infra/restart-sentinel.js +16 -1
- package/dist/infra/restart.js +229 -26
- package/dist/infra/scp-host.js +54 -0
- package/dist/infra/update-startup.js +86 -9
- package/dist/media/inbound-path-policy.js +114 -0
- package/dist/media/input-files.js +16 -0
- package/dist/memory/test-manager.js +8 -0
- package/dist/plugin-sdk/temp-path.js +47 -0
- package/dist/plugins/discovery.js +217 -23
- package/dist/plugins/hook-runner-global.js +16 -0
- package/dist/plugins/loader.js +192 -26
- package/dist/plugins/logger.js +8 -0
- package/dist/plugins/manifest-registry.js +3 -0
- package/dist/plugins/path-safety.js +34 -0
- package/dist/plugins/registry.js +5 -2
- package/dist/plugins/runtime/index.js +271 -206
- package/dist/providers/github-copilot-models.js +4 -1
- package/dist/security/audit-channel.js +8 -19
- package/dist/security/audit-extra.async.js +354 -182
- package/dist/security/audit-extra.js +11 -1
- package/dist/security/audit-extra.sync.js +340 -33
- package/dist/security/audit-fs.js +31 -13
- package/dist/security/audit.js +145 -371
- package/dist/security/dm-policy-shared.js +24 -0
- package/dist/security/external-content.js +20 -8
- package/dist/security/fix.js +49 -85
- package/dist/security/scan-paths.js +20 -0
- package/dist/security/secret-equal.js +3 -7
- package/dist/security/windows-acl.js +30 -15
- package/dist/shared/node-list-parse.js +13 -0
- package/dist/shared/operator-scope-compat.js +37 -0
- package/dist/shared/text-chunking.js +29 -0
- package/dist/slack/blocks.test-helpers.js +31 -0
- package/dist/slack/monitor/mrkdwn.js +8 -0
- package/dist/telegram/bot-message-dispatch.js +366 -164
- package/dist/telegram/draft-stream.js +30 -7
- package/dist/telegram/reasoning-lane-coordinator.js +128 -0
- package/dist/terminal/prompt-select-styled.js +9 -0
- package/dist/test-utils/command-runner.js +6 -0
- package/dist/test-utils/internal-hook-event-payload.js +10 -0
- package/dist/test-utils/model-auth-mock.js +12 -0
- package/dist/test-utils/provider-usage-fetch.js +14 -0
- package/dist/test-utils/temp-home.js +33 -0
- package/dist/tui/components/chat-log.js +9 -0
- package/dist/tui/tui-command-handlers.js +36 -27
- package/dist/tui/tui-event-handlers.js +122 -32
- package/dist/tui/tui.js +181 -45
- package/dist/utils/mask-api-key.js +10 -0
- package/dist/utils/run-with-concurrency.js +39 -0
- package/dist/web/media.js +4 -0
- package/docs/tools/slash-commands.md +5 -1
- 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/feishu/package.json +1 -1
- package/extensions/feishu/src/external-keys.ts +19 -0
- 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/irc/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/lobster/src/windows-spawn.ts +193 -0
- package/extensions/matrix/CHANGELOG.md +5 -0
- package/extensions/matrix/package.json +1 -1
- package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
- package/extensions/mattermost/package.json +1 -1
- package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
- package/extensions/memory-core/package.json +1 -1
- package/extensions/memory-lancedb/package.json +1 -1
- package/extensions/minimax-portal-auth/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/openai-codex-auth/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,34 +1,220 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { CURRENT_SESSION_VERSION } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
|
|
5
4
|
import { resolveSessionAgentId } from "../../agents/agent-scope.js";
|
|
6
|
-
import { resolveEffectiveMessagesConfig, resolveIdentityName } from "../../agents/identity.js";
|
|
7
5
|
import { resolveThinkingDefault } from "../../agents/model-selection.js";
|
|
8
6
|
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
|
9
7
|
import { dispatchInboundMessage } from "../../auto-reply/dispatch.js";
|
|
10
8
|
import { createReplyDispatcher } from "../../auto-reply/reply/reply-dispatcher.js";
|
|
11
|
-
import {
|
|
9
|
+
import { createReplyPrefixOptions } from "../../channels/reply-prefix.js";
|
|
10
|
+
import { resolveSessionFilePath } from "../../config/sessions.js";
|
|
12
11
|
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
|
13
12
|
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
|
|
14
13
|
import { abortChatRunById, abortChatRunsForSessionKey, isChatStopCommandText, resolveChatRunExpiresAtMs, } from "../chat-abort.js";
|
|
15
14
|
import { parseMessageWithAttachments } from "../chat-attachments.js";
|
|
15
|
+
import { stripEnvelopeFromMessages } from "../chat-sanitize.js";
|
|
16
|
+
import { GATEWAY_CLIENT_CAPS, hasGatewayClientCap } from "../protocol/client-info.js";
|
|
16
17
|
import { ErrorCodes, errorShape, formatValidationErrors, validateChatAbortParams, validateChatHistoryParams, validateChatInjectParams, validateChatSendParams, } from "../protocol/index.js";
|
|
17
18
|
import { getMaxChatHistoryMessagesBytes } from "../server-constants.js";
|
|
18
19
|
import { capArrayByJsonBytes, loadSessionEntry, readSessionMessages, resolveSessionModelRef, } from "../session-utils.js";
|
|
19
|
-
import { stripEnvelopeFromMessages } from "../chat-sanitize.js";
|
|
20
20
|
import { formatForLog } from "../ws-log.js";
|
|
21
|
+
import { injectTimestamp, timestampOptsFromConfig } from "./agent-timestamp.js";
|
|
22
|
+
import { normalizeRpcAttachmentsToChatAttachments } from "./attachment-normalize.js";
|
|
23
|
+
const CHAT_HISTORY_TEXT_MAX_CHARS = 12_000;
|
|
24
|
+
const CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES = 128 * 1024;
|
|
25
|
+
const CHAT_HISTORY_OVERSIZED_PLACEHOLDER = "[chat.history omitted: message too large]";
|
|
26
|
+
let chatHistoryPlaceholderEmitCount = 0;
|
|
27
|
+
function stripDisallowedChatControlChars(message) {
|
|
28
|
+
let output = "";
|
|
29
|
+
for (const char of message) {
|
|
30
|
+
const code = char.charCodeAt(0);
|
|
31
|
+
if (code === 9 || code === 10 || code === 13 || (code >= 32 && code !== 127)) {
|
|
32
|
+
output += char;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
37
|
+
export function sanitizeChatSendMessageInput(message) {
|
|
38
|
+
const normalized = message.normalize("NFC");
|
|
39
|
+
if (normalized.includes("\u0000")) {
|
|
40
|
+
return { ok: false, error: "message must not contain null bytes" };
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, message: stripDisallowedChatControlChars(normalized) };
|
|
43
|
+
}
|
|
44
|
+
function truncateChatHistoryText(text) {
|
|
45
|
+
if (text.length <= CHAT_HISTORY_TEXT_MAX_CHARS) {
|
|
46
|
+
return { text, truncated: false };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
text: `${text.slice(0, CHAT_HISTORY_TEXT_MAX_CHARS)}\n...(truncated)...`,
|
|
50
|
+
truncated: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function sanitizeChatHistoryContentBlock(block) {
|
|
54
|
+
if (!block || typeof block !== "object") {
|
|
55
|
+
return { block, changed: false };
|
|
56
|
+
}
|
|
57
|
+
const entry = { ...block };
|
|
58
|
+
let changed = false;
|
|
59
|
+
if (typeof entry.text === "string") {
|
|
60
|
+
const res = truncateChatHistoryText(entry.text);
|
|
61
|
+
entry.text = res.text;
|
|
62
|
+
changed ||= res.truncated;
|
|
63
|
+
}
|
|
64
|
+
if (typeof entry.partialJson === "string") {
|
|
65
|
+
const res = truncateChatHistoryText(entry.partialJson);
|
|
66
|
+
entry.partialJson = res.text;
|
|
67
|
+
changed ||= res.truncated;
|
|
68
|
+
}
|
|
69
|
+
if (typeof entry.arguments === "string") {
|
|
70
|
+
const res = truncateChatHistoryText(entry.arguments);
|
|
71
|
+
entry.arguments = res.text;
|
|
72
|
+
changed ||= res.truncated;
|
|
73
|
+
}
|
|
74
|
+
if (typeof entry.thinking === "string") {
|
|
75
|
+
const res = truncateChatHistoryText(entry.thinking);
|
|
76
|
+
entry.thinking = res.text;
|
|
77
|
+
changed ||= res.truncated;
|
|
78
|
+
}
|
|
79
|
+
if ("thinkingSignature" in entry) {
|
|
80
|
+
delete entry.thinkingSignature;
|
|
81
|
+
changed = true;
|
|
82
|
+
}
|
|
83
|
+
const type = typeof entry.type === "string" ? entry.type : "";
|
|
84
|
+
if (type === "image" && typeof entry.data === "string") {
|
|
85
|
+
const bytes = Buffer.byteLength(entry.data, "utf8");
|
|
86
|
+
delete entry.data;
|
|
87
|
+
entry.omitted = true;
|
|
88
|
+
entry.bytes = bytes;
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
return { block: changed ? entry : block, changed };
|
|
92
|
+
}
|
|
93
|
+
function sanitizeChatHistoryMessage(message) {
|
|
94
|
+
if (!message || typeof message !== "object") {
|
|
95
|
+
return { message, changed: false };
|
|
96
|
+
}
|
|
97
|
+
const entry = { ...message };
|
|
98
|
+
let changed = false;
|
|
99
|
+
if ("details" in entry) {
|
|
100
|
+
delete entry.details;
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
if ("usage" in entry) {
|
|
104
|
+
delete entry.usage;
|
|
105
|
+
changed = true;
|
|
106
|
+
}
|
|
107
|
+
if ("cost" in entry) {
|
|
108
|
+
delete entry.cost;
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
if (typeof entry.content === "string") {
|
|
112
|
+
const res = truncateChatHistoryText(entry.content);
|
|
113
|
+
entry.content = res.text;
|
|
114
|
+
changed ||= res.truncated;
|
|
115
|
+
}
|
|
116
|
+
else if (Array.isArray(entry.content)) {
|
|
117
|
+
const updated = entry.content.map((block) => sanitizeChatHistoryContentBlock(block));
|
|
118
|
+
if (updated.some((item) => item.changed)) {
|
|
119
|
+
entry.content = updated.map((item) => item.block);
|
|
120
|
+
changed = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (typeof entry.text === "string") {
|
|
124
|
+
const res = truncateChatHistoryText(entry.text);
|
|
125
|
+
entry.text = res.text;
|
|
126
|
+
changed ||= res.truncated;
|
|
127
|
+
}
|
|
128
|
+
return { message: changed ? entry : message, changed };
|
|
129
|
+
}
|
|
130
|
+
function sanitizeChatHistoryMessages(messages) {
|
|
131
|
+
if (messages.length === 0) {
|
|
132
|
+
return messages;
|
|
133
|
+
}
|
|
134
|
+
let changed = false;
|
|
135
|
+
const next = messages.map((message) => {
|
|
136
|
+
const res = sanitizeChatHistoryMessage(message);
|
|
137
|
+
changed ||= res.changed;
|
|
138
|
+
return res.message;
|
|
139
|
+
});
|
|
140
|
+
return changed ? next : messages;
|
|
141
|
+
}
|
|
142
|
+
function jsonUtf8Bytes(value) {
|
|
143
|
+
try {
|
|
144
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return Buffer.byteLength(String(value), "utf8");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function buildOversizedHistoryPlaceholder(message) {
|
|
151
|
+
const role = message &&
|
|
152
|
+
typeof message === "object" &&
|
|
153
|
+
typeof message.role === "string"
|
|
154
|
+
? message.role
|
|
155
|
+
: "assistant";
|
|
156
|
+
const timestamp = message &&
|
|
157
|
+
typeof message === "object" &&
|
|
158
|
+
typeof message.timestamp === "number"
|
|
159
|
+
? message.timestamp
|
|
160
|
+
: Date.now();
|
|
161
|
+
return {
|
|
162
|
+
role,
|
|
163
|
+
timestamp,
|
|
164
|
+
content: [{ type: "text", text: CHAT_HISTORY_OVERSIZED_PLACEHOLDER }],
|
|
165
|
+
__poolbot: { truncated: true, reason: "oversized" },
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function replaceOversizedChatHistoryMessages(params) {
|
|
169
|
+
const { messages, maxSingleMessageBytes } = params;
|
|
170
|
+
if (messages.length === 0) {
|
|
171
|
+
return { messages, replacedCount: 0 };
|
|
172
|
+
}
|
|
173
|
+
let replacedCount = 0;
|
|
174
|
+
const next = messages.map((message) => {
|
|
175
|
+
if (jsonUtf8Bytes(message) <= maxSingleMessageBytes) {
|
|
176
|
+
return message;
|
|
177
|
+
}
|
|
178
|
+
replacedCount += 1;
|
|
179
|
+
return buildOversizedHistoryPlaceholder(message);
|
|
180
|
+
});
|
|
181
|
+
return { messages: replacedCount > 0 ? next : messages, replacedCount };
|
|
182
|
+
}
|
|
183
|
+
function enforceChatHistoryFinalBudget(params) {
|
|
184
|
+
const { messages, maxBytes } = params;
|
|
185
|
+
if (messages.length === 0) {
|
|
186
|
+
return { messages, placeholderCount: 0 };
|
|
187
|
+
}
|
|
188
|
+
if (jsonUtf8Bytes(messages) <= maxBytes) {
|
|
189
|
+
return { messages, placeholderCount: 0 };
|
|
190
|
+
}
|
|
191
|
+
const last = messages.at(-1);
|
|
192
|
+
if (last && jsonUtf8Bytes([last]) <= maxBytes) {
|
|
193
|
+
return { messages: [last], placeholderCount: 0 };
|
|
194
|
+
}
|
|
195
|
+
const placeholder = buildOversizedHistoryPlaceholder(last);
|
|
196
|
+
if (jsonUtf8Bytes([placeholder]) <= maxBytes) {
|
|
197
|
+
return { messages: [placeholder], placeholderCount: 1 };
|
|
198
|
+
}
|
|
199
|
+
return { messages: [], placeholderCount: 0 };
|
|
200
|
+
}
|
|
21
201
|
function resolveTranscriptPath(params) {
|
|
22
|
-
const { sessionId, storePath, sessionFile } = params;
|
|
23
|
-
if (sessionFile)
|
|
24
|
-
return sessionFile;
|
|
25
|
-
if (!storePath)
|
|
202
|
+
const { sessionId, storePath, sessionFile, agentId } = params;
|
|
203
|
+
if (!storePath && !sessionFile) {
|
|
26
204
|
return null;
|
|
27
|
-
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const sessionsDir = storePath ? path.dirname(storePath) : undefined;
|
|
208
|
+
return resolveSessionFilePath(sessionId, sessionFile ? { sessionFile } : undefined, sessionsDir || agentId ? { sessionsDir, agentId } : undefined);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
28
213
|
}
|
|
29
214
|
function ensureTranscriptFile(params) {
|
|
30
|
-
if (fs.existsSync(params.transcriptPath))
|
|
215
|
+
if (fs.existsSync(params.transcriptPath)) {
|
|
31
216
|
return { ok: true };
|
|
217
|
+
}
|
|
32
218
|
try {
|
|
33
219
|
fs.mkdirSync(path.dirname(params.transcriptPath), { recursive: true });
|
|
34
220
|
const header = {
|
|
@@ -38,18 +224,40 @@ function ensureTranscriptFile(params) {
|
|
|
38
224
|
timestamp: new Date().toISOString(),
|
|
39
225
|
cwd: process.cwd(),
|
|
40
226
|
};
|
|
41
|
-
fs.writeFileSync(params.transcriptPath, `${JSON.stringify(header)}\n`,
|
|
227
|
+
fs.writeFileSync(params.transcriptPath, `${JSON.stringify(header)}\n`, {
|
|
228
|
+
encoding: "utf-8",
|
|
229
|
+
mode: 0o600,
|
|
230
|
+
});
|
|
42
231
|
return { ok: true };
|
|
43
232
|
}
|
|
44
233
|
catch (err) {
|
|
45
234
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
46
235
|
}
|
|
47
236
|
}
|
|
237
|
+
function transcriptHasIdempotencyKey(transcriptPath, idempotencyKey) {
|
|
238
|
+
try {
|
|
239
|
+
const lines = fs.readFileSync(transcriptPath, "utf-8").split(/\r?\n/);
|
|
240
|
+
for (const line of lines) {
|
|
241
|
+
if (!line.trim()) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const parsed = JSON.parse(line);
|
|
245
|
+
if (parsed?.message?.idempotencyKey === idempotencyKey) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
48
255
|
function appendAssistantTranscriptMessage(params) {
|
|
49
256
|
const transcriptPath = resolveTranscriptPath({
|
|
50
257
|
sessionId: params.sessionId,
|
|
51
258
|
storePath: params.storePath,
|
|
52
259
|
sessionFile: params.sessionFile,
|
|
260
|
+
agentId: params.agentId,
|
|
53
261
|
});
|
|
54
262
|
if (!transcriptPath) {
|
|
55
263
|
return { ok: false, error: "transcript path not resolved" };
|
|
@@ -66,29 +274,134 @@ function appendAssistantTranscriptMessage(params) {
|
|
|
66
274
|
return { ok: false, error: ensured.error ?? "failed to create transcript file" };
|
|
67
275
|
}
|
|
68
276
|
}
|
|
277
|
+
if (params.idempotencyKey && transcriptHasIdempotencyKey(transcriptPath, params.idempotencyKey)) {
|
|
278
|
+
return { ok: true };
|
|
279
|
+
}
|
|
69
280
|
const now = Date.now();
|
|
70
|
-
const messageId = randomUUID().slice(0, 8);
|
|
71
281
|
const labelPrefix = params.label ? `[${params.label}]\n\n` : "";
|
|
282
|
+
const usage = {
|
|
283
|
+
input: 0,
|
|
284
|
+
output: 0,
|
|
285
|
+
cacheRead: 0,
|
|
286
|
+
cacheWrite: 0,
|
|
287
|
+
totalTokens: 0,
|
|
288
|
+
cost: {
|
|
289
|
+
input: 0,
|
|
290
|
+
output: 0,
|
|
291
|
+
cacheRead: 0,
|
|
292
|
+
cacheWrite: 0,
|
|
293
|
+
total: 0,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
72
296
|
const messageBody = {
|
|
73
297
|
role: "assistant",
|
|
74
298
|
content: [{ type: "text", text: `${labelPrefix}${params.message}` }],
|
|
75
299
|
timestamp: now,
|
|
76
|
-
stopReason
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
300
|
+
// Pi stopReason is a strict enum; this is not model output, but we still store it as a
|
|
301
|
+
// normal assistant message so it participates in the session parentId chain.
|
|
302
|
+
stopReason: "stop",
|
|
303
|
+
usage,
|
|
304
|
+
// Make these explicit so downstream tooling never treats this as model output.
|
|
305
|
+
api: "openai-responses",
|
|
306
|
+
provider: "poolbot",
|
|
307
|
+
model: "gateway-injected",
|
|
308
|
+
...(params.idempotencyKey ? { idempotencyKey: params.idempotencyKey } : {}),
|
|
309
|
+
...(params.abortMeta
|
|
310
|
+
? {
|
|
311
|
+
poolbotAbort: {
|
|
312
|
+
aborted: true,
|
|
313
|
+
origin: params.abortMeta.origin,
|
|
314
|
+
runId: params.abortMeta.runId,
|
|
315
|
+
},
|
|
316
|
+
}
|
|
317
|
+
: {}),
|
|
84
318
|
};
|
|
85
319
|
try {
|
|
86
|
-
|
|
320
|
+
// IMPORTANT: Use SessionManager so the entry is attached to the current leaf via parentId.
|
|
321
|
+
// Raw jsonl appends break the parent chain and can hide compaction summaries from context.
|
|
322
|
+
const sessionManager = SessionManager.open(transcriptPath);
|
|
323
|
+
const messageId = sessionManager.appendMessage(messageBody);
|
|
324
|
+
return { ok: true, messageId, message: messageBody };
|
|
87
325
|
}
|
|
88
326
|
catch (err) {
|
|
89
327
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
90
328
|
}
|
|
91
|
-
|
|
329
|
+
}
|
|
330
|
+
function collectSessionAbortPartials(params) {
|
|
331
|
+
const out = [];
|
|
332
|
+
for (const [runId, active] of params.chatAbortControllers) {
|
|
333
|
+
if (active.sessionKey !== params.sessionKey) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const text = params.chatRunBuffers.get(runId);
|
|
337
|
+
if (!text || !text.trim()) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
out.push({
|
|
341
|
+
runId,
|
|
342
|
+
sessionId: active.sessionId,
|
|
343
|
+
text,
|
|
344
|
+
abortOrigin: params.abortOrigin,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
function persistAbortedPartials(params) {
|
|
350
|
+
if (params.snapshots.length === 0) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const { storePath, entry } = loadSessionEntry(params.sessionKey);
|
|
354
|
+
for (const snapshot of params.snapshots) {
|
|
355
|
+
const sessionId = entry?.sessionId ?? snapshot.sessionId ?? snapshot.runId;
|
|
356
|
+
const appended = appendAssistantTranscriptMessage({
|
|
357
|
+
message: snapshot.text,
|
|
358
|
+
sessionId,
|
|
359
|
+
storePath,
|
|
360
|
+
sessionFile: entry?.sessionFile,
|
|
361
|
+
createIfMissing: true,
|
|
362
|
+
idempotencyKey: `${snapshot.runId}:assistant`,
|
|
363
|
+
abortMeta: {
|
|
364
|
+
aborted: true,
|
|
365
|
+
origin: snapshot.abortOrigin,
|
|
366
|
+
runId: snapshot.runId,
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
if (!appended.ok) {
|
|
370
|
+
params.context.logGateway.warn(`chat.abort transcript append failed: ${appended.error ?? "unknown error"}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function createChatAbortOps(context) {
|
|
375
|
+
return {
|
|
376
|
+
chatAbortControllers: context.chatAbortControllers,
|
|
377
|
+
chatRunBuffers: context.chatRunBuffers,
|
|
378
|
+
chatDeltaSentAt: context.chatDeltaSentAt,
|
|
379
|
+
chatAbortedRuns: context.chatAbortedRuns,
|
|
380
|
+
removeChatRun: context.removeChatRun,
|
|
381
|
+
agentRunSeq: context.agentRunSeq,
|
|
382
|
+
broadcast: context.broadcast,
|
|
383
|
+
nodeSendToSession: context.nodeSendToSession,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
function abortChatRunsForSessionKeyWithPartials(params) {
|
|
387
|
+
const snapshots = collectSessionAbortPartials({
|
|
388
|
+
chatAbortControllers: params.context.chatAbortControllers,
|
|
389
|
+
chatRunBuffers: params.context.chatRunBuffers,
|
|
390
|
+
sessionKey: params.sessionKey,
|
|
391
|
+
abortOrigin: params.abortOrigin,
|
|
392
|
+
});
|
|
393
|
+
const res = abortChatRunsForSessionKey(params.ops, {
|
|
394
|
+
sessionKey: params.sessionKey,
|
|
395
|
+
stopReason: params.stopReason,
|
|
396
|
+
});
|
|
397
|
+
if (res.aborted) {
|
|
398
|
+
persistAbortedPartials({
|
|
399
|
+
context: params.context,
|
|
400
|
+
sessionKey: params.sessionKey,
|
|
401
|
+
snapshots,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return res;
|
|
92
405
|
}
|
|
93
406
|
function nextChatSeq(context, runId) {
|
|
94
407
|
const next = (context.agentRunSeq.get(runId) ?? 0) + 1;
|
|
@@ -106,6 +419,7 @@ function broadcastChatFinal(params) {
|
|
|
106
419
|
};
|
|
107
420
|
params.context.broadcast("chat", payload);
|
|
108
421
|
params.context.nodeSendToSession(params.sessionKey, "chat", payload);
|
|
422
|
+
params.context.agentRunSeq.delete(params.runId);
|
|
109
423
|
}
|
|
110
424
|
function broadcastChatError(params) {
|
|
111
425
|
const seq = nextChatSeq({ agentRunSeq: params.context.agentRunSeq }, params.runId);
|
|
@@ -118,6 +432,7 @@ function broadcastChatError(params) {
|
|
|
118
432
|
};
|
|
119
433
|
params.context.broadcast("chat", payload);
|
|
120
434
|
params.context.nodeSendToSession(params.sessionKey, "chat", payload);
|
|
435
|
+
params.context.agentRunSeq.delete(params.runId);
|
|
121
436
|
}
|
|
122
437
|
export const chatHandlers = {
|
|
123
438
|
"chat.history": async ({ params, respond, context }) => {
|
|
@@ -135,7 +450,20 @@ export const chatHandlers = {
|
|
|
135
450
|
const max = Math.min(hardMax, requested);
|
|
136
451
|
const sliced = rawMessages.length > max ? rawMessages.slice(-max) : rawMessages;
|
|
137
452
|
const sanitized = stripEnvelopeFromMessages(sliced);
|
|
138
|
-
const
|
|
453
|
+
const normalized = sanitizeChatHistoryMessages(sanitized);
|
|
454
|
+
const maxHistoryBytes = getMaxChatHistoryMessagesBytes();
|
|
455
|
+
const perMessageHardCap = Math.min(CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES, maxHistoryBytes);
|
|
456
|
+
const replaced = replaceOversizedChatHistoryMessages({
|
|
457
|
+
messages: normalized,
|
|
458
|
+
maxSingleMessageBytes: perMessageHardCap,
|
|
459
|
+
});
|
|
460
|
+
const capped = capArrayByJsonBytes(replaced.messages, maxHistoryBytes).items;
|
|
461
|
+
const bounded = enforceChatHistoryFinalBudget({ messages: capped, maxBytes: maxHistoryBytes });
|
|
462
|
+
const placeholderCount = replaced.replacedCount + bounded.placeholderCount;
|
|
463
|
+
if (placeholderCount > 0) {
|
|
464
|
+
chatHistoryPlaceholderEmitCount += placeholderCount;
|
|
465
|
+
context.logGateway.debug(`chat.history omitted oversized payloads placeholders=${placeholderCount} total=${chatHistoryPlaceholderEmitCount}`);
|
|
466
|
+
}
|
|
139
467
|
let thinkingLevel = entry?.thinkingLevel;
|
|
140
468
|
if (!thinkingLevel) {
|
|
141
469
|
const configured = cfg.agents?.defaults?.thinkingDefault;
|
|
@@ -143,7 +471,8 @@ export const chatHandlers = {
|
|
|
143
471
|
thinkingLevel = configured;
|
|
144
472
|
}
|
|
145
473
|
else {
|
|
146
|
-
const {
|
|
474
|
+
const sessionAgentId = resolveSessionAgentId({ sessionKey, config: cfg });
|
|
475
|
+
const { provider, model } = resolveSessionModelRef(cfg, entry, sessionAgentId);
|
|
147
476
|
const catalog = await context.loadGatewayModelCatalog();
|
|
148
477
|
thinkingLevel = resolveThinkingDefault({
|
|
149
478
|
cfg,
|
|
@@ -153,11 +482,13 @@ export const chatHandlers = {
|
|
|
153
482
|
});
|
|
154
483
|
}
|
|
155
484
|
}
|
|
485
|
+
const verboseLevel = entry?.verboseLevel ?? cfg.agents?.defaults?.verboseDefault;
|
|
156
486
|
respond(true, {
|
|
157
487
|
sessionKey,
|
|
158
488
|
sessionId,
|
|
159
|
-
messages:
|
|
489
|
+
messages: bounded.messages,
|
|
160
490
|
thinkingLevel,
|
|
491
|
+
verboseLevel,
|
|
161
492
|
});
|
|
162
493
|
},
|
|
163
494
|
"chat.abort": ({ params, respond, context }) => {
|
|
@@ -165,20 +496,14 @@ export const chatHandlers = {
|
|
|
165
496
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid chat.abort params: ${formatValidationErrors(validateChatAbortParams.errors)}`));
|
|
166
497
|
return;
|
|
167
498
|
}
|
|
168
|
-
const { sessionKey, runId } = params;
|
|
169
|
-
const ops =
|
|
170
|
-
chatAbortControllers: context.chatAbortControllers,
|
|
171
|
-
chatRunBuffers: context.chatRunBuffers,
|
|
172
|
-
chatDeltaSentAt: context.chatDeltaSentAt,
|
|
173
|
-
chatAbortedRuns: context.chatAbortedRuns,
|
|
174
|
-
removeChatRun: context.removeChatRun,
|
|
175
|
-
agentRunSeq: context.agentRunSeq,
|
|
176
|
-
broadcast: context.broadcast,
|
|
177
|
-
nodeSendToSession: context.nodeSendToSession,
|
|
178
|
-
};
|
|
499
|
+
const { sessionKey: rawSessionKey, runId } = params;
|
|
500
|
+
const ops = createChatAbortOps(context);
|
|
179
501
|
if (!runId) {
|
|
180
|
-
const res =
|
|
181
|
-
|
|
502
|
+
const res = abortChatRunsForSessionKeyWithPartials({
|
|
503
|
+
context,
|
|
504
|
+
ops,
|
|
505
|
+
sessionKey: rawSessionKey,
|
|
506
|
+
abortOrigin: "rpc",
|
|
182
507
|
stopReason: "rpc",
|
|
183
508
|
});
|
|
184
509
|
respond(true, { ok: true, aborted: res.aborted, runIds: res.runIds });
|
|
@@ -189,15 +514,30 @@ export const chatHandlers = {
|
|
|
189
514
|
respond(true, { ok: true, aborted: false, runIds: [] });
|
|
190
515
|
return;
|
|
191
516
|
}
|
|
192
|
-
if (active.sessionKey !==
|
|
517
|
+
if (active.sessionKey !== rawSessionKey) {
|
|
193
518
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "runId does not match sessionKey"));
|
|
194
519
|
return;
|
|
195
520
|
}
|
|
521
|
+
const partialText = context.chatRunBuffers.get(runId);
|
|
196
522
|
const res = abortChatRunById(ops, {
|
|
197
523
|
runId,
|
|
198
|
-
sessionKey,
|
|
524
|
+
sessionKey: rawSessionKey,
|
|
199
525
|
stopReason: "rpc",
|
|
200
526
|
});
|
|
527
|
+
if (res.aborted && partialText && partialText.trim()) {
|
|
528
|
+
persistAbortedPartials({
|
|
529
|
+
context,
|
|
530
|
+
sessionKey: rawSessionKey,
|
|
531
|
+
snapshots: [
|
|
532
|
+
{
|
|
533
|
+
runId,
|
|
534
|
+
sessionId: active.sessionId,
|
|
535
|
+
text: partialText,
|
|
536
|
+
abortOrigin: "rpc",
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
});
|
|
540
|
+
}
|
|
201
541
|
respond(true, {
|
|
202
542
|
ok: true,
|
|
203
543
|
aborted: res.aborted,
|
|
@@ -210,29 +550,24 @@ export const chatHandlers = {
|
|
|
210
550
|
return;
|
|
211
551
|
}
|
|
212
552
|
const p = params;
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
? Buffer.from(a.content.buffer, a.content.byteOffset, a.content.byteLength).toString("base64")
|
|
223
|
-
: undefined,
|
|
224
|
-
}))
|
|
225
|
-
.filter((a) => a.content) ?? [];
|
|
226
|
-
const rawMessage = p.message.trim();
|
|
553
|
+
const sanitizedMessageResult = sanitizeChatSendMessageInput(p.message);
|
|
554
|
+
if (!sanitizedMessageResult.ok) {
|
|
555
|
+
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, sanitizedMessageResult.error));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const inboundMessage = sanitizedMessageResult.message;
|
|
559
|
+
const stopCommand = isChatStopCommandText(inboundMessage);
|
|
560
|
+
const normalizedAttachments = normalizeRpcAttachmentsToChatAttachments(p.attachments);
|
|
561
|
+
const rawMessage = inboundMessage.trim();
|
|
227
562
|
if (!rawMessage && normalizedAttachments.length === 0) {
|
|
228
563
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "message or attachment required"));
|
|
229
564
|
return;
|
|
230
565
|
}
|
|
231
|
-
let parsedMessage =
|
|
566
|
+
let parsedMessage = inboundMessage;
|
|
232
567
|
let parsedImages = [];
|
|
233
568
|
if (normalizedAttachments.length > 0) {
|
|
234
569
|
try {
|
|
235
|
-
const parsed = await parseMessageWithAttachments(
|
|
570
|
+
const parsed = await parseMessageWithAttachments(inboundMessage, normalizedAttachments, {
|
|
236
571
|
maxBytes: 5_000_000,
|
|
237
572
|
log: context.logGateway,
|
|
238
573
|
});
|
|
@@ -244,7 +579,8 @@ export const chatHandlers = {
|
|
|
244
579
|
return;
|
|
245
580
|
}
|
|
246
581
|
}
|
|
247
|
-
const
|
|
582
|
+
const rawSessionKey = p.sessionKey;
|
|
583
|
+
const { cfg, entry, canonicalKey: sessionKey } = loadSessionEntry(rawSessionKey);
|
|
248
584
|
const timeoutMs = resolveAgentTimeoutMs({
|
|
249
585
|
cfg,
|
|
250
586
|
overrideMs: p.timeoutMs,
|
|
@@ -254,7 +590,7 @@ export const chatHandlers = {
|
|
|
254
590
|
const sendPolicy = resolveSendPolicy({
|
|
255
591
|
cfg,
|
|
256
592
|
entry,
|
|
257
|
-
sessionKey
|
|
593
|
+
sessionKey,
|
|
258
594
|
channel: entry?.channel,
|
|
259
595
|
chatType: entry?.chatType,
|
|
260
596
|
});
|
|
@@ -263,16 +599,13 @@ export const chatHandlers = {
|
|
|
263
599
|
return;
|
|
264
600
|
}
|
|
265
601
|
if (stopCommand) {
|
|
266
|
-
const res =
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
broadcast: context.broadcast,
|
|
274
|
-
nodeSendToSession: context.nodeSendToSession,
|
|
275
|
-
}, { sessionKey: p.sessionKey, stopReason: "stop" });
|
|
602
|
+
const res = abortChatRunsForSessionKeyWithPartials({
|
|
603
|
+
context,
|
|
604
|
+
ops: createChatAbortOps(context),
|
|
605
|
+
sessionKey: rawSessionKey,
|
|
606
|
+
abortOrigin: "stop-command",
|
|
607
|
+
stopReason: "stop",
|
|
608
|
+
});
|
|
276
609
|
respond(true, { ok: true, aborted: res.aborted, runIds: res.runIds });
|
|
277
610
|
return;
|
|
278
611
|
}
|
|
@@ -296,7 +629,7 @@ export const chatHandlers = {
|
|
|
296
629
|
context.chatAbortControllers.set(clientRunId, {
|
|
297
630
|
controller: abortController,
|
|
298
631
|
sessionId: entry?.sessionId ?? clientRunId,
|
|
299
|
-
sessionKey:
|
|
632
|
+
sessionKey: rawSessionKey,
|
|
300
633
|
startedAtMs: now,
|
|
301
634
|
expiresAtMs: resolveChatRunExpiresAtMs({ now, timeoutMs }),
|
|
302
635
|
});
|
|
@@ -309,13 +642,17 @@ export const chatHandlers = {
|
|
|
309
642
|
const injectThinking = Boolean(p.thinking && trimmedMessage && !trimmedMessage.startsWith("/"));
|
|
310
643
|
const commandBody = injectThinking ? `/think ${p.thinking} ${parsedMessage}` : parsedMessage;
|
|
311
644
|
const clientInfo = client?.connect?.client;
|
|
645
|
+
// Inject timestamp so agents know the current date/time.
|
|
646
|
+
// Only BodyForAgent gets the timestamp — Body stays raw for UI display.
|
|
647
|
+
// See: https://github.com/moltbot/moltbot/issues/3658
|
|
648
|
+
const stampedMessage = injectTimestamp(parsedMessage, timestampOptsFromConfig(cfg));
|
|
312
649
|
const ctx = {
|
|
313
650
|
Body: parsedMessage,
|
|
314
|
-
BodyForAgent:
|
|
651
|
+
BodyForAgent: stampedMessage,
|
|
315
652
|
BodyForCommands: commandBody,
|
|
316
653
|
RawBody: parsedMessage,
|
|
317
654
|
CommandBody: commandBody,
|
|
318
|
-
SessionKey:
|
|
655
|
+
SessionKey: sessionKey,
|
|
319
656
|
Provider: INTERNAL_MESSAGE_CHANNEL,
|
|
320
657
|
Surface: INTERNAL_MESSAGE_CHANNEL,
|
|
321
658
|
OriginatingChannel: INTERNAL_MESSAGE_CHANNEL,
|
|
@@ -325,27 +662,31 @@ export const chatHandlers = {
|
|
|
325
662
|
SenderId: clientInfo?.id,
|
|
326
663
|
SenderName: clientInfo?.displayName,
|
|
327
664
|
SenderUsername: clientInfo?.displayName,
|
|
665
|
+
GatewayClientScopes: client?.connect?.scopes,
|
|
328
666
|
};
|
|
329
667
|
const agentId = resolveSessionAgentId({
|
|
330
|
-
sessionKey
|
|
668
|
+
sessionKey,
|
|
331
669
|
config: cfg,
|
|
332
670
|
});
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
671
|
+
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
|
672
|
+
cfg,
|
|
673
|
+
agentId,
|
|
674
|
+
channel: INTERNAL_MESSAGE_CHANNEL,
|
|
675
|
+
});
|
|
336
676
|
const finalReplyParts = [];
|
|
337
677
|
const dispatcher = createReplyDispatcher({
|
|
338
|
-
|
|
339
|
-
responsePrefixContextProvider: () => prefixContext,
|
|
678
|
+
...prefixOptions,
|
|
340
679
|
onError: (err) => {
|
|
341
680
|
context.logGateway.warn(`webchat dispatch failed: ${formatForLog(err)}`);
|
|
342
681
|
},
|
|
343
682
|
deliver: async (payload, info) => {
|
|
344
|
-
if (info.kind !== "final")
|
|
683
|
+
if (info.kind !== "final") {
|
|
345
684
|
return;
|
|
685
|
+
}
|
|
346
686
|
const text = payload.text?.trim() ?? "";
|
|
347
|
-
if (!text)
|
|
687
|
+
if (!text) {
|
|
348
688
|
return;
|
|
689
|
+
}
|
|
349
690
|
finalReplyParts.push(text);
|
|
350
691
|
},
|
|
351
692
|
});
|
|
@@ -358,16 +699,23 @@ export const chatHandlers = {
|
|
|
358
699
|
runId: clientRunId,
|
|
359
700
|
abortSignal: abortController.signal,
|
|
360
701
|
images: parsedImages.length > 0 ? parsedImages : undefined,
|
|
361
|
-
|
|
362
|
-
onAgentRunStart: () => {
|
|
702
|
+
onAgentRunStart: (runId) => {
|
|
363
703
|
agentRunStarted = true;
|
|
704
|
+
const connId = typeof client?.connId === "string" ? client.connId : undefined;
|
|
705
|
+
const wantsToolEvents = hasGatewayClientCap(client?.connect?.caps, GATEWAY_CLIENT_CAPS.TOOL_EVENTS);
|
|
706
|
+
if (connId && wantsToolEvents) {
|
|
707
|
+
context.registerToolEventRecipient(runId, connId);
|
|
708
|
+
// Register for any other active runs *in the same session* so
|
|
709
|
+
// late-joining clients (e.g. page refresh mid-response) receive
|
|
710
|
+
// in-progress tool events without leaking cross-session data.
|
|
711
|
+
for (const [activeRunId, active] of context.chatAbortControllers) {
|
|
712
|
+
if (activeRunId !== runId && active.sessionKey === p.sessionKey) {
|
|
713
|
+
context.registerToolEventRecipient(activeRunId, connId);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
364
717
|
},
|
|
365
|
-
onModelSelected
|
|
366
|
-
prefixContext.provider = ctx.provider;
|
|
367
|
-
prefixContext.model = extractShortModelName(ctx.model);
|
|
368
|
-
prefixContext.modelFull = `${ctx.provider}/${ctx.model}`;
|
|
369
|
-
prefixContext.thinkingLevel = ctx.thinkLevel ?? "off";
|
|
370
|
-
},
|
|
718
|
+
onModelSelected,
|
|
371
719
|
},
|
|
372
720
|
})
|
|
373
721
|
.then(() => {
|
|
@@ -379,13 +727,14 @@ export const chatHandlers = {
|
|
|
379
727
|
.trim();
|
|
380
728
|
let message;
|
|
381
729
|
if (combinedReply) {
|
|
382
|
-
const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(
|
|
730
|
+
const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(sessionKey);
|
|
383
731
|
const sessionId = latestEntry?.sessionId ?? entry?.sessionId ?? clientRunId;
|
|
384
732
|
const appended = appendAssistantTranscriptMessage({
|
|
385
733
|
message: combinedReply,
|
|
386
734
|
sessionId,
|
|
387
735
|
storePath: latestStorePath,
|
|
388
736
|
sessionFile: latestEntry?.sessionFile,
|
|
737
|
+
agentId,
|
|
389
738
|
createIfMissing: true,
|
|
390
739
|
});
|
|
391
740
|
if (appended.ok) {
|
|
@@ -398,7 +747,9 @@ export const chatHandlers = {
|
|
|
398
747
|
role: "assistant",
|
|
399
748
|
content: [{ type: "text", text: combinedReply }],
|
|
400
749
|
timestamp: now,
|
|
401
|
-
stopReason
|
|
750
|
+
// Keep this compatible with Pi stopReason enums even though this message isn't
|
|
751
|
+
// persisted to the transcript due to the append failure.
|
|
752
|
+
stopReason: "stop",
|
|
402
753
|
usage: { input: 0, output: 0, totalTokens: 0 },
|
|
403
754
|
};
|
|
404
755
|
}
|
|
@@ -406,7 +757,7 @@ export const chatHandlers = {
|
|
|
406
757
|
broadcastChatFinal({
|
|
407
758
|
context,
|
|
408
759
|
runId: clientRunId,
|
|
409
|
-
sessionKey:
|
|
760
|
+
sessionKey: rawSessionKey,
|
|
410
761
|
message,
|
|
411
762
|
});
|
|
412
763
|
}
|
|
@@ -431,7 +782,7 @@ export const chatHandlers = {
|
|
|
431
782
|
broadcastChatError({
|
|
432
783
|
context,
|
|
433
784
|
runId: clientRunId,
|
|
434
|
-
sessionKey:
|
|
785
|
+
sessionKey: rawSessionKey,
|
|
435
786
|
errorMessage: String(err),
|
|
436
787
|
});
|
|
437
788
|
})
|
|
@@ -465,56 +816,36 @@ export const chatHandlers = {
|
|
|
465
816
|
}
|
|
466
817
|
const p = params;
|
|
467
818
|
// Load session to find transcript file
|
|
468
|
-
const
|
|
819
|
+
const rawSessionKey = p.sessionKey;
|
|
820
|
+
const { cfg, storePath, entry } = loadSessionEntry(rawSessionKey);
|
|
469
821
|
const sessionId = entry?.sessionId;
|
|
470
822
|
if (!sessionId || !storePath) {
|
|
471
823
|
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "session not found"));
|
|
472
824
|
return;
|
|
473
825
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const labelPrefix = p.label ? `[${p.label}]\n\n` : "";
|
|
486
|
-
const messageBody = {
|
|
487
|
-
role: "assistant",
|
|
488
|
-
content: [{ type: "text", text: `${labelPrefix}${p.message}` }],
|
|
489
|
-
timestamp: now,
|
|
490
|
-
stopReason: "injected",
|
|
491
|
-
usage: { input: 0, output: 0, totalTokens: 0 },
|
|
492
|
-
};
|
|
493
|
-
const transcriptEntry = {
|
|
494
|
-
type: "message",
|
|
495
|
-
id: messageId,
|
|
496
|
-
timestamp: new Date(now).toISOString(),
|
|
497
|
-
message: messageBody,
|
|
498
|
-
};
|
|
499
|
-
// Append to transcript file
|
|
500
|
-
try {
|
|
501
|
-
fs.appendFileSync(transcriptPath, `${JSON.stringify(transcriptEntry)}\n`, "utf-8");
|
|
502
|
-
}
|
|
503
|
-
catch (err) {
|
|
504
|
-
const errMessage = err instanceof Error ? err.message : String(err);
|
|
505
|
-
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, `failed to write transcript: ${errMessage}`));
|
|
826
|
+
const appended = appendAssistantTranscriptMessage({
|
|
827
|
+
message: p.message,
|
|
828
|
+
label: p.label,
|
|
829
|
+
sessionId,
|
|
830
|
+
storePath,
|
|
831
|
+
sessionFile: entry?.sessionFile,
|
|
832
|
+
agentId: resolveSessionAgentId({ sessionKey: rawSessionKey, config: cfg }),
|
|
833
|
+
createIfMissing: false,
|
|
834
|
+
});
|
|
835
|
+
if (!appended.ok || !appended.messageId || !appended.message) {
|
|
836
|
+
respond(false, undefined, errorShape(ErrorCodes.UNAVAILABLE, `failed to write transcript: ${appended.error ?? "unknown error"}`));
|
|
506
837
|
return;
|
|
507
838
|
}
|
|
508
839
|
// Broadcast to webchat for immediate UI update
|
|
509
840
|
const chatPayload = {
|
|
510
|
-
runId: `inject-${messageId}`,
|
|
511
|
-
sessionKey:
|
|
841
|
+
runId: `inject-${appended.messageId}`,
|
|
842
|
+
sessionKey: rawSessionKey,
|
|
512
843
|
seq: 0,
|
|
513
844
|
state: "final",
|
|
514
|
-
message:
|
|
845
|
+
message: appended.message,
|
|
515
846
|
};
|
|
516
847
|
context.broadcast("chat", chatPayload);
|
|
517
|
-
context.nodeSendToSession(
|
|
518
|
-
respond(true, { ok: true, messageId });
|
|
848
|
+
context.nodeSendToSession(rawSessionKey, "chat", chatPayload);
|
|
849
|
+
respond(true, { ok: true, messageId: appended.messageId });
|
|
519
850
|
},
|
|
520
851
|
};
|