agent-world 0.12.3 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -17
- package/dist/cli/commands.d.ts +7 -1
- package/dist/cli/commands.js +27 -10
- package/dist/cli/hitl.d.ts +9 -2
- package/dist/cli/hitl.js +61 -20
- package/dist/cli/index.js +250 -96
- package/dist/cli/system-events.d.ts +27 -0
- package/dist/cli/system-events.js +63 -0
- package/dist/core/activity-tracker.d.ts +38 -2
- package/dist/core/activity-tracker.d.ts.map +1 -1
- package/dist/core/activity-tracker.js +62 -9
- package/dist/core/activity-tracker.js.map +1 -1
- package/dist/core/anthropic-direct.d.ts +2 -0
- package/dist/core/anthropic-direct.d.ts.map +1 -1
- package/dist/core/anthropic-direct.js +43 -1
- package/dist/core/anthropic-direct.js.map +1 -1
- package/dist/core/chat-constants.d.ts +12 -0
- package/dist/core/chat-constants.d.ts.map +1 -1
- package/dist/core/chat-constants.js +5 -0
- package/dist/core/chat-constants.js.map +1 -1
- package/dist/core/create-agent-tool.d.ts +28 -25
- package/dist/core/create-agent-tool.d.ts.map +1 -1
- package/dist/core/create-agent-tool.js +264 -141
- package/dist/core/create-agent-tool.js.map +1 -1
- package/dist/core/events/index.d.ts +5 -2
- package/dist/core/events/index.d.ts.map +1 -1
- package/dist/core/events/index.js +5 -2
- package/dist/core/events/index.js.map +1 -1
- package/dist/core/events/memory-manager.d.ts +26 -1
- package/dist/core/events/memory-manager.d.ts.map +1 -1
- package/dist/core/events/memory-manager.js +877 -72
- package/dist/core/events/memory-manager.js.map +1 -1
- package/dist/core/events/orchestrator.d.ts +8 -0
- package/dist/core/events/orchestrator.d.ts.map +1 -1
- package/dist/core/events/orchestrator.js +214 -38
- package/dist/core/events/orchestrator.js.map +1 -1
- package/dist/core/events/persistence.d.ts +21 -14
- package/dist/core/events/persistence.d.ts.map +1 -1
- package/dist/core/events/persistence.js +100 -61
- package/dist/core/events/persistence.js.map +1 -1
- package/dist/core/events/publishers.d.ts +13 -16
- package/dist/core/events/publishers.d.ts.map +1 -1
- package/dist/core/events/publishers.js +54 -55
- package/dist/core/events/publishers.js.map +1 -1
- package/dist/core/events/subscribers.d.ts +17 -14
- package/dist/core/events/subscribers.d.ts.map +1 -1
- package/dist/core/events/subscribers.js +68 -147
- package/dist/core/events/subscribers.js.map +1 -1
- package/dist/core/events/title-scheduler.d.ts +27 -0
- package/dist/core/events/title-scheduler.d.ts.map +1 -0
- package/dist/core/events/title-scheduler.js +135 -0
- package/dist/core/events/title-scheduler.js.map +1 -0
- package/dist/core/events/tool-bridge-logging.d.ts +4 -1
- package/dist/core/events/tool-bridge-logging.d.ts.map +1 -1
- package/dist/core/events/tool-bridge-logging.js +112 -13
- package/dist/core/events/tool-bridge-logging.js.map +1 -1
- package/dist/core/events-metadata.d.ts.map +1 -1
- package/dist/core/events-metadata.js +8 -4
- package/dist/core/events-metadata.js.map +1 -1
- package/dist/core/export.d.ts +1 -1
- package/dist/core/export.d.ts.map +1 -1
- package/dist/core/export.js +2 -15
- package/dist/core/export.js.map +1 -1
- package/dist/core/feature-path-logging.d.ts +50 -0
- package/dist/core/feature-path-logging.d.ts.map +1 -0
- package/dist/core/feature-path-logging.js +130 -0
- package/dist/core/feature-path-logging.js.map +1 -0
- package/dist/core/file-tools.d.ts +57 -1
- package/dist/core/file-tools.d.ts.map +1 -1
- package/dist/core/file-tools.js +329 -29
- package/dist/core/file-tools.js.map +1 -1
- package/dist/core/google-direct.d.ts +6 -1
- package/dist/core/google-direct.d.ts.map +1 -1
- package/dist/core/google-direct.js +76 -7
- package/dist/core/google-direct.js.map +1 -1
- package/dist/core/heartbeat.d.ts +34 -0
- package/dist/core/heartbeat.d.ts.map +1 -0
- package/dist/core/heartbeat.js +153 -0
- package/dist/core/heartbeat.js.map +1 -0
- package/dist/core/hitl-tool.d.ts +73 -0
- package/dist/core/hitl-tool.d.ts.map +1 -0
- package/dist/core/hitl-tool.js +284 -0
- package/dist/core/hitl-tool.js.map +1 -0
- package/dist/core/hitl.d.ts +85 -8
- package/dist/core/hitl.d.ts.map +1 -1
- package/dist/core/hitl.js +375 -61
- package/dist/core/hitl.js.map +1 -1
- package/dist/core/index.d.ts +12 -7
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +11 -6
- package/dist/core/index.js.map +1 -1
- package/dist/core/llm-manager.d.ts +17 -0
- package/dist/core/llm-manager.d.ts.map +1 -1
- package/dist/core/llm-manager.js +335 -43
- package/dist/core/llm-manager.js.map +1 -1
- package/dist/core/load-skill-tool.d.ts +36 -3
- package/dist/core/load-skill-tool.d.ts.map +1 -1
- package/dist/core/load-skill-tool.js +807 -93
- package/dist/core/load-skill-tool.js.map +1 -1
- package/dist/core/logger.d.ts +14 -0
- package/dist/core/logger.d.ts.map +1 -1
- package/dist/core/logger.js +15 -0
- package/dist/core/logger.js.map +1 -1
- package/dist/core/managers.d.ts +41 -52
- package/dist/core/managers.d.ts.map +1 -1
- package/dist/core/managers.js +422 -533
- package/dist/core/managers.js.map +1 -1
- package/dist/core/mcp-server-registry.d.ts +19 -2
- package/dist/core/mcp-server-registry.d.ts.map +1 -1
- package/dist/core/mcp-server-registry.js +168 -12
- package/dist/core/mcp-server-registry.js.map +1 -1
- package/dist/core/message-cutoff.d.ts +29 -0
- package/dist/core/message-cutoff.d.ts.map +1 -0
- package/dist/core/message-cutoff.js +63 -0
- package/dist/core/message-cutoff.js.map +1 -0
- package/dist/core/message-edit-manager.d.ts +54 -0
- package/dist/core/message-edit-manager.d.ts.map +1 -0
- package/dist/core/message-edit-manager.js +602 -0
- package/dist/core/message-edit-manager.js.map +1 -0
- package/dist/core/message-prep.d.ts +2 -0
- package/dist/core/message-prep.d.ts.map +1 -1
- package/dist/core/message-prep.js +39 -12
- package/dist/core/message-prep.js.map +1 -1
- package/dist/core/message-processing-control.d.ts +1 -0
- package/dist/core/message-processing-control.d.ts.map +1 -1
- package/dist/core/message-processing-control.js +23 -6
- package/dist/core/message-processing-control.js.map +1 -1
- package/dist/core/openai-direct.d.ts +9 -3
- package/dist/core/openai-direct.d.ts.map +1 -1
- package/dist/core/openai-direct.js +267 -33
- package/dist/core/openai-direct.js.map +1 -1
- package/dist/core/optional-tracers/opik-runtime.d.ts +32 -0
- package/dist/core/optional-tracers/opik-runtime.d.ts.map +1 -0
- package/dist/core/optional-tracers/opik-runtime.js +141 -0
- package/dist/core/optional-tracers/opik-runtime.js.map +1 -0
- package/dist/core/queue-manager.d.ts +84 -0
- package/dist/core/queue-manager.d.ts.map +1 -0
- package/dist/core/queue-manager.js +814 -0
- package/dist/core/queue-manager.js.map +1 -0
- package/dist/core/reasoning-controls.d.ts +30 -0
- package/dist/core/reasoning-controls.d.ts.map +1 -0
- package/dist/core/reasoning-controls.js +118 -0
- package/dist/core/reasoning-controls.js.map +1 -0
- package/dist/core/reliability-config.d.ts +82 -0
- package/dist/core/reliability-config.d.ts.map +1 -0
- package/dist/core/reliability-config.js +106 -0
- package/dist/core/reliability-config.js.map +1 -0
- package/dist/core/reliability-runtime.d.ts +53 -0
- package/dist/core/reliability-runtime.d.ts.map +1 -0
- package/dist/core/reliability-runtime.js +92 -0
- package/dist/core/reliability-runtime.js.map +1 -0
- package/dist/core/security/guardrails.d.ts +21 -0
- package/dist/core/security/guardrails.d.ts.map +1 -0
- package/dist/core/security/guardrails.js +111 -0
- package/dist/core/security/guardrails.js.map +1 -0
- package/dist/core/send-message-tool.d.ts +79 -0
- package/dist/core/send-message-tool.d.ts.map +1 -0
- package/dist/core/send-message-tool.js +222 -0
- package/dist/core/send-message-tool.js.map +1 -0
- package/dist/core/shell-cmd-tool.d.ts +82 -1
- package/dist/core/shell-cmd-tool.d.ts.map +1 -1
- package/dist/core/shell-cmd-tool.js +854 -42
- package/dist/core/shell-cmd-tool.js.map +1 -1
- package/dist/core/skill-registry.d.ts +2 -0
- package/dist/core/skill-registry.d.ts.map +1 -1
- package/dist/core/skill-registry.js +52 -2
- package/dist/core/skill-registry.js.map +1 -1
- package/dist/core/storage/eventStorage/fileEventStorage.d.ts +5 -0
- package/dist/core/storage/eventStorage/fileEventStorage.d.ts.map +1 -1
- package/dist/core/storage/eventStorage/fileEventStorage.js +61 -0
- package/dist/core/storage/eventStorage/fileEventStorage.js.map +1 -1
- package/dist/core/storage/eventStorage/memoryEventStorage.d.ts +5 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.d.ts.map +1 -1
- package/dist/core/storage/eventStorage/memoryEventStorage.js +34 -0
- package/dist/core/storage/eventStorage/memoryEventStorage.js.map +1 -1
- package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts +1 -0
- package/dist/core/storage/eventStorage/sqliteEventStorage.d.ts.map +1 -1
- package/dist/core/storage/eventStorage/sqliteEventStorage.js +19 -2
- package/dist/core/storage/eventStorage/sqliteEventStorage.js.map +1 -1
- package/dist/core/storage/eventStorage/types.d.ts +6 -0
- package/dist/core/storage/eventStorage/types.d.ts.map +1 -1
- package/dist/core/storage/eventStorage/types.js +1 -0
- package/dist/core/storage/eventStorage/types.js.map +1 -1
- package/dist/core/storage/eventStorage/validation.d.ts.map +1 -1
- package/dist/core/storage/eventStorage/validation.js +2 -1
- package/dist/core/storage/eventStorage/validation.js.map +1 -1
- package/dist/core/storage/github-world-import.d.ts +84 -0
- package/dist/core/storage/github-world-import.d.ts.map +1 -0
- package/dist/core/storage/github-world-import.js +365 -0
- package/dist/core/storage/github-world-import.js.map +1 -0
- package/dist/core/storage/memory-storage.d.ts +19 -8
- package/dist/core/storage/memory-storage.d.ts.map +1 -1
- package/dist/core/storage/memory-storage.js +147 -49
- package/dist/core/storage/memory-storage.js.map +1 -1
- package/dist/core/storage/queue-storage.d.ts +1 -0
- package/dist/core/storage/queue-storage.d.ts.map +1 -1
- package/dist/core/storage/queue-storage.js +3 -2
- package/dist/core/storage/queue-storage.js.map +1 -1
- package/dist/core/storage/sqlite-storage.d.ts +14 -9
- package/dist/core/storage/sqlite-storage.d.ts.map +1 -1
- package/dist/core/storage/sqlite-storage.js +131 -154
- package/dist/core/storage/sqlite-storage.js.map +1 -1
- package/dist/core/storage/storage-factory.d.ts +3 -0
- package/dist/core/storage/storage-factory.d.ts.map +1 -1
- package/dist/core/storage/storage-factory.js +175 -89
- package/dist/core/storage/storage-factory.js.map +1 -1
- package/dist/core/storage/world-storage.d.ts +1 -1
- package/dist/core/storage/world-storage.d.ts.map +1 -1
- package/dist/core/storage/world-storage.js +5 -1
- package/dist/core/storage/world-storage.js.map +1 -1
- package/dist/core/storage-init.d.ts +11 -0
- package/dist/core/storage-init.d.ts.map +1 -0
- package/dist/core/storage-init.js +122 -0
- package/dist/core/storage-init.js.map +1 -0
- package/dist/core/subscription.d.ts +8 -1
- package/dist/core/subscription.d.ts.map +1 -1
- package/dist/core/subscription.js +130 -23
- package/dist/core/subscription.js.map +1 -1
- package/dist/core/tool-approval.d.ts +45 -0
- package/dist/core/tool-approval.d.ts.map +1 -0
- package/dist/core/tool-approval.js +223 -0
- package/dist/core/tool-approval.js.map +1 -0
- package/dist/core/tool-execution-envelope.d.ts +87 -0
- package/dist/core/tool-execution-envelope.d.ts.map +1 -0
- package/dist/core/tool-execution-envelope.js +168 -0
- package/dist/core/tool-execution-envelope.js.map +1 -0
- package/dist/core/tool-utils.d.ts +9 -2
- package/dist/core/tool-utils.d.ts.map +1 -1
- package/dist/core/tool-utils.js +122 -28
- package/dist/core/tool-utils.js.map +1 -1
- package/dist/core/types.d.ts +69 -36
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +3 -2
- package/dist/core/types.js.map +1 -1
- package/dist/core/utils.d.ts +16 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +99 -24
- package/dist/core/utils.js.map +1 -1
- package/dist/core/web-fetch-tool.d.ts +72 -0
- package/dist/core/web-fetch-tool.d.ts.map +1 -0
- package/dist/core/web-fetch-tool.js +491 -0
- package/dist/core/web-fetch-tool.js.map +1 -0
- package/dist/core/world-registry.d.ts +84 -0
- package/dist/core/world-registry.d.ts.map +1 -0
- package/dist/core/world-registry.js +247 -0
- package/dist/core/world-registry.js.map +1 -0
- package/dist/public/assets/index-Be-1xtV-.js +104 -0
- package/dist/public/assets/index-tsDdiXDU.css +1 -0
- package/dist/public/index.html +2 -2
- package/dist/public/mcp-sandbox-proxy.html +148 -0
- package/dist/server/api.js +288 -58
- package/dist/server/error-response.d.ts +27 -0
- package/dist/server/error-response.js +77 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +6 -2
- package/dist/server/sse-handler.d.ts +13 -2
- package/dist/server/sse-handler.js +194 -26
- package/migrations/0015_add_message_queue.sql +36 -0
- package/migrations/0016_add_world_heartbeat.sql +13 -0
- package/migrations/0017_add_title_provenance.sql +7 -0
- package/package.json +31 -10
- package/dist/public/assets/index-BO20H4xt.js +0 -96
- package/dist/public/assets/index-ETY7W5_S.css +0 -1
package/dist/server/api.js
CHANGED
|
@@ -5,12 +5,29 @@
|
|
|
5
5
|
* Supports world/agent/chat management with optimized serialization and error handling.
|
|
6
6
|
*
|
|
7
7
|
* Changes:
|
|
8
|
-
* - 2026-
|
|
8
|
+
* - 2026-03-12: World status now keeps the current chat in `queuedChatIds` while a durable `queued`/`sending`
|
|
9
|
+
* message_queue row still exists, preventing false-idle status during post-response queue cleanup.
|
|
10
|
+
* - 2026-03-12: Trailing comma cleanup in WorldUpdateSchema; tool_permission is stored in world.variables env key — no dedicated API schema field needed.
|
|
11
|
+
* - 2026-03-06: Removed runtime `world.currentChatId` fallback from message send routes; chat-scoped sends now require explicit `chatId`.
|
|
12
|
+
* - 2026-03-06: Added `/tool-artifact` for stable, restorable adopted-tool preview URLs limited to approved world working directories and registered skill roots.
|
|
13
|
+
* - 2026-03-06: Hardened `POST /worlds/:worldName/hitl/respond` for restart-safe validation: when the runtime pending map lacks the request entry but it exists in persisted messages, trigger `activateChatWithSnapshot` to seed the runtime map and return an actionable error.
|
|
14
|
+
* - 2026-03-10: Queue-backed message ingress is now user-only; non-user API senders use explicit immediate dispatch instead of the mixed queue helper.
|
|
15
|
+
* - 2026-03-04: Added queue metadata fields to non-streaming `/messages` success responses (`queueMessageId`, `queueStatus`, `queueRetryCount`) to expose queue terminal/error state.
|
|
16
|
+
* - 2026-02-27: Hardened non-streaming `/messages` event collection with chat-scoped filtering to prevent cross-chat response contamination.
|
|
17
|
+
* - 2026-02-24: Added `hitlPrompts` payload to `POST /worlds/:worldName/setChat/:chatId` responses so web chat switches can render replayed pending HITL prompts.
|
|
18
|
+
* - 2026-03-11: Added restoreChat(suppressAutoResume: true) before subscribeWorld in streaming edit path
|
|
19
|
+
* to sync agent memory from storage, matching Electron editMessageInChat flow and fixing stale-runtime
|
|
20
|
+
* cause of edit-success hang after world delete+recreate in e2e tests.
|
|
21
|
+
* - 2026-03-11: Added restoreChat before subscribeWorld in handleStreamingChat and handleNonStreamingChat
|
|
22
|
+
* send-message paths to match Electron sendChatMessage pattern and prevent stale-agent hangs.
|
|
23
|
+
* - 2026-02-21: Removed temporary server-side folder-picker endpoint in favor of web File API based selection.
|
|
24
|
+
* - 2026-02-20: Enforced options-only HITL response endpoint `POST /worlds/:worldName/hitl/respond` (`optionId` required).
|
|
9
25
|
* - 2026-02-14: Added HITL option response endpoint `POST /worlds/:worldName/hitl/respond` for web/CLI approval submissions.
|
|
10
26
|
* - 2026-02-13: Added core-managed message edit endpoint `PUT /worlds/:worldName/messages/:messageId`
|
|
11
27
|
* - Delegates edit/remove/resubmit flow to `core.editUserMessage` for cross-client consistency
|
|
12
28
|
* - Streams edit-resubmission follow-up events over SSE by default (`stream: true`)
|
|
13
29
|
* - Keeps DELETE endpoint focused on removal-only behavior
|
|
30
|
+
* - 2026-02-21: Extended non-streaming timeout refresh to include shell assistant-stream SSE activity (`start`/`chunk`/`end` with `toolName='shell_cmd'`) in addition to legacy `tool-stream`.
|
|
14
31
|
* - 2026-02-11: Extended non-streaming timeout on tool-stream events to prevent premature timeout during long-running tools
|
|
15
32
|
* - Standardized world-scoped routes to use validateWorld middleware to load and attach worldCtx/world
|
|
16
33
|
* - Removed ad-hoc world loading and undefined getWorldOrError usage; handlers now use (req as any).worldCtx and (req as any).world
|
|
@@ -47,12 +64,18 @@
|
|
|
47
64
|
* - 2026-02-08: Removed legacy manual intervention endpoint and related server handling
|
|
48
65
|
*/
|
|
49
66
|
import express from 'express';
|
|
67
|
+
import { promises as fs } from 'fs';
|
|
68
|
+
import * as path from 'path';
|
|
50
69
|
import { z } from 'zod';
|
|
51
70
|
import { createSSEHandler } from './sse-handler.js';
|
|
52
|
-
import { createWorld, listWorlds, createCategoryLogger,
|
|
71
|
+
import { createWorld, listWorlds, createCategoryLogger, enqueueAndProcessUserTurn, dispatchImmediateChatMessage, enableStreaming, disableStreaming,
|
|
53
72
|
// core managers (function-based)
|
|
54
|
-
getWorld, updateWorld, deleteWorld, createAgent, getAgent, updateAgent, deleteAgent, listChats, newChat, restoreChat, deleteChat as deleteChatCore, clearAgentMemory, listAgents as listAgentsCore, getMemory as coreGetMemory, exportWorldToMarkdown, removeMessagesFrom,
|
|
73
|
+
getWorld, updateWorld, deleteWorld, createAgent, getAgent, updateAgent, deleteAgent, listChats, newChat, activateChatWithSnapshot, restoreChat, deleteChat as deleteChatCore, clearAgentMemory, listAgents as listAgentsCore, getMemory as coreGetMemory, exportWorldToMarkdown, removeMessagesFrom, editUserMessage, stopMessageProcessing, submitWorldHitlResponse, listPendingHitlPromptEventsFromMessages, getQueueMessages, getActiveProcessingChatIds, getActiveAgentNames, EventType } from '../core/index.js';
|
|
55
74
|
import { subscribeWorld } from '../core/index.js';
|
|
75
|
+
// Opik integration: optional tracer attach for API-managed world subscriptions.
|
|
76
|
+
import { attachOptionalOpikTracer } from '../core/optional-tracers/opik-runtime.js';
|
|
77
|
+
import { getSkillSourcePath, getSkills } from '../core/skill-registry.js';
|
|
78
|
+
import { getDefaultWorkingDirectory, getEnvValueFromText, toKebabCase } from '../core/utils.js';
|
|
56
79
|
import { listMCPServers, restartMCPServer, getMCPSystemHealth, getMCPRegistryStats } from '../core/mcp-server-registry.js';
|
|
57
80
|
// Function-specific loggers for granular debugging control
|
|
58
81
|
const loggerWorld = createCategoryLogger('api.world');
|
|
@@ -62,6 +85,21 @@ const loggerStream = createCategoryLogger('api.stream');
|
|
|
62
85
|
const loggerValidation = createCategoryLogger('api.validation');
|
|
63
86
|
const loggerMcp = createCategoryLogger('api.mcp');
|
|
64
87
|
const loggerExport = createCategoryLogger('api.export');
|
|
88
|
+
function isUserSender(sender) {
|
|
89
|
+
const normalized = String(sender || '').trim().toLowerCase();
|
|
90
|
+
return normalized === 'human' || normalized === 'world' || normalized.startsWith('user');
|
|
91
|
+
}
|
|
92
|
+
async function hasPendingCurrentChatQueueMessage(world) {
|
|
93
|
+
const currentChatId = String(world.currentChatId || '').trim();
|
|
94
|
+
if (!currentChatId) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const queuedMessages = await getQueueMessages(world.id, currentChatId);
|
|
98
|
+
return queuedMessages.some((entry) => {
|
|
99
|
+
const status = String(entry?.status || '').trim().toLowerCase();
|
|
100
|
+
return status === 'queued' || status === 'sending';
|
|
101
|
+
});
|
|
102
|
+
}
|
|
65
103
|
const DEFAULT_WORLD_NAME = 'Default World';
|
|
66
104
|
// World context factory - eliminates repetitive worldId passing
|
|
67
105
|
function createWorldContext(worldId) {
|
|
@@ -97,6 +135,8 @@ function serializeWorld(world) {
|
|
|
97
135
|
currentChatId: world.currentChatId || null,
|
|
98
136
|
mcpConfig: world.mcpConfig || null,
|
|
99
137
|
variables: typeof world.variables === 'string' ? world.variables : '',
|
|
138
|
+
uiMode: world.uiMode || 'chat',
|
|
139
|
+
dashboardZones: world.dashboardZones || [],
|
|
100
140
|
agents: Array.from(world.agents.values()).map(serializeAgent),
|
|
101
141
|
chats: Array.from(world.chats.values()).map(serializeChat)
|
|
102
142
|
};
|
|
@@ -132,8 +172,77 @@ function sendError(res, status, message, code, details) {
|
|
|
132
172
|
error.details = details;
|
|
133
173
|
res.status(status).json(error);
|
|
134
174
|
}
|
|
135
|
-
function
|
|
136
|
-
|
|
175
|
+
function isPathWithinRoot(rootPath, candidatePath) {
|
|
176
|
+
const normalizedRoot = normalizeResolvedPath(rootPath);
|
|
177
|
+
const normalizedCandidate = normalizeResolvedPath(candidatePath);
|
|
178
|
+
return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}/`);
|
|
179
|
+
}
|
|
180
|
+
function normalizeResolvedPath(targetPath) {
|
|
181
|
+
return path.resolve(targetPath).replace(/\\/g, '/').replace(/\/+/g, '/').replace(/\/$/, '') || '/';
|
|
182
|
+
}
|
|
183
|
+
async function getRegisteredSkillRoots() {
|
|
184
|
+
const skillRoots = await Promise.all(getSkills()
|
|
185
|
+
.map((skill) => getSkillSourcePath(skill.skill_id))
|
|
186
|
+
.filter((skillPath) => typeof skillPath === 'string' && skillPath.trim().length > 0)
|
|
187
|
+
.map(async (skillPath) => {
|
|
188
|
+
try {
|
|
189
|
+
return normalizeResolvedPath(path.dirname(await fs.realpath(path.resolve(skillPath))));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}));
|
|
195
|
+
return [...new Set(skillRoots.filter((skillRoot) => typeof skillRoot === 'string' && skillRoot.length > 0))];
|
|
196
|
+
}
|
|
197
|
+
async function resolveWorldArtifactRoot(worldId) {
|
|
198
|
+
if (!worldId) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const world = await getWorld(toKebabCase(worldId));
|
|
202
|
+
if (!world) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const lexicalRoot = path.resolve(getEnvValueFromText(typeof world.variables === 'string' ? world.variables : '', 'working_directory') || getDefaultWorkingDirectory());
|
|
206
|
+
try {
|
|
207
|
+
return normalizeResolvedPath(await fs.realpath(lexicalRoot));
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function resolveToolArtifactPath(requestedPath, worldId) {
|
|
214
|
+
const normalizedPath = String(requestedPath || '').trim();
|
|
215
|
+
if (!normalizedPath) {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const worldRoot = await resolveWorldArtifactRoot(worldId);
|
|
219
|
+
const skillRoots = await getRegisteredSkillRoots();
|
|
220
|
+
const uniqueRoots = [...new Set([
|
|
221
|
+
...(worldRoot ? [worldRoot] : []),
|
|
222
|
+
...skillRoots,
|
|
223
|
+
].map((root) => normalizeResolvedPath(root)))];
|
|
224
|
+
if (uniqueRoots.length === 0) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const candidatePaths = path.isAbsolute(normalizedPath)
|
|
228
|
+
? [normalizeResolvedPath(normalizedPath)]
|
|
229
|
+
: uniqueRoots.map((root) => normalizeResolvedPath(path.resolve(root, normalizedPath)));
|
|
230
|
+
for (const candidatePath of candidatePaths) {
|
|
231
|
+
try {
|
|
232
|
+
const realCandidatePath = normalizeResolvedPath(await fs.realpath(candidatePath));
|
|
233
|
+
if (!uniqueRoots.some((root) => isPathWithinRoot(root, realCandidatePath))) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const stat = await fs.stat(realCandidatePath);
|
|
237
|
+
if (stat.isFile()) {
|
|
238
|
+
return realCandidatePath;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
137
246
|
}
|
|
138
247
|
async function isAgentNameUnique(worldCtx, agentName, excludeAgent) {
|
|
139
248
|
const normalizedAgentName = toKebabCase(agentName);
|
|
@@ -183,7 +292,7 @@ const WorldUpdateSchema = z.object({
|
|
|
183
292
|
chatLLMProvider: z.enum(['openai', 'anthropic', 'azure', 'google', 'xai', 'openai-compatible', 'ollama']).nullable().optional(),
|
|
184
293
|
chatLLMModel: z.string().nullable().optional(),
|
|
185
294
|
mcpConfig: z.string().nullable().optional(),
|
|
186
|
-
variables: z.string().nullable().optional()
|
|
295
|
+
variables: z.string().nullable().optional(),
|
|
187
296
|
});
|
|
188
297
|
const AgentCreateSchema = z.object({
|
|
189
298
|
name: z.string().min(1).max(100),
|
|
@@ -201,7 +310,7 @@ const ChatMessageSchema = z.object({
|
|
|
201
310
|
message: z.string().min(1),
|
|
202
311
|
sender: z.string().default("human"),
|
|
203
312
|
stream: z.boolean().optional().default(true),
|
|
204
|
-
chatId: z.string().min(1)
|
|
313
|
+
chatId: z.string().min(1),
|
|
205
314
|
messages: z.array(z.any()).optional()
|
|
206
315
|
});
|
|
207
316
|
const MessageEditSchema = z.object({
|
|
@@ -217,6 +326,10 @@ const HitlResponseSchema = z.object({
|
|
|
217
326
|
optionId: z.string().min(1),
|
|
218
327
|
chatId: z.string().nullable().optional()
|
|
219
328
|
});
|
|
329
|
+
const ToolArtifactQuerySchema = z.object({
|
|
330
|
+
path: z.string().min(1),
|
|
331
|
+
worldId: z.string().min(1).optional(),
|
|
332
|
+
});
|
|
220
333
|
const AgentUpdateSchema = z.object({
|
|
221
334
|
name: z.string().min(1).max(100).optional(),
|
|
222
335
|
type: z.string().optional(),
|
|
@@ -230,6 +343,19 @@ const AgentUpdateSchema = z.object({
|
|
|
230
343
|
clearMemory: z.boolean().optional()
|
|
231
344
|
});
|
|
232
345
|
const router = express.Router();
|
|
346
|
+
router.get('/tool-artifact', async (req, res) => {
|
|
347
|
+
const validation = ToolArtifactQuerySchema.safeParse(req.query);
|
|
348
|
+
if (!validation.success) {
|
|
349
|
+
sendError(res, 400, 'Invalid tool artifact request', 'VALIDATION_ERROR', validation.error.issues);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const resolvedPath = await resolveToolArtifactPath(validation.data.path, validation.data.worldId);
|
|
353
|
+
if (!resolvedPath) {
|
|
354
|
+
sendError(res, 404, 'Tool artifact not found', 'TOOL_ARTIFACT_NOT_FOUND');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
res.sendFile(resolvedPath);
|
|
358
|
+
});
|
|
233
359
|
// World Routes
|
|
234
360
|
router.get('/worlds', async (req, res) => {
|
|
235
361
|
try {
|
|
@@ -268,6 +394,33 @@ router.get('/worlds/:worldName', validateWorld, async (req, res) => {
|
|
|
268
394
|
sendError(res, 500, 'Internal server error', 'INTERNAL_ERROR');
|
|
269
395
|
}
|
|
270
396
|
});
|
|
397
|
+
router.get('/worlds/:worldName/status', validateWorld, async (req, res) => {
|
|
398
|
+
try {
|
|
399
|
+
const world = req.world;
|
|
400
|
+
const includeCurrentChatQueue = await hasPendingCurrentChatQueueMessage(world);
|
|
401
|
+
const activeChatIds = [...getActiveProcessingChatIds(world)];
|
|
402
|
+
const queuedChatIds = [
|
|
403
|
+
...new Set([
|
|
404
|
+
...(world._queuedChatIds ?? []),
|
|
405
|
+
...(includeCurrentChatQueue && world.currentChatId ? [world.currentChatId] : []),
|
|
406
|
+
]),
|
|
407
|
+
];
|
|
408
|
+
const activeAgentNames = getActiveAgentNames(world);
|
|
409
|
+
res.json({
|
|
410
|
+
worldId: world.id,
|
|
411
|
+
isProcessing: world.isProcessing ?? false,
|
|
412
|
+
activeChatIds,
|
|
413
|
+
queuedChatIds,
|
|
414
|
+
activeAgentNames,
|
|
415
|
+
queueDepth: queuedChatIds.length,
|
|
416
|
+
sendingCount: activeChatIds.length,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
loggerWorld.error('Error getting world status', { error: error instanceof Error ? error.message : error, worldName: req.params.worldName });
|
|
421
|
+
sendError(res, 500, 'Internal server error', 'INTERNAL_ERROR');
|
|
422
|
+
}
|
|
423
|
+
});
|
|
271
424
|
router.post('/worlds', async (req, res) => {
|
|
272
425
|
try {
|
|
273
426
|
const validation = WorldCreateSchema.safeParse(req.body);
|
|
@@ -551,8 +704,28 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
|
|
|
551
704
|
disableStreaming();
|
|
552
705
|
let subscription = null;
|
|
553
706
|
let listeners = new Map();
|
|
707
|
+
const normalizedChatId = typeof chatId === 'string' ? chatId.trim() : '';
|
|
708
|
+
const scopedChatId = normalizedChatId.length > 0 ? normalizedChatId : null;
|
|
709
|
+
const isChatEventInScope = (eventChatId, includeUnscopedWhenScoped = false) => {
|
|
710
|
+
if (!scopedChatId) {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
if (eventChatId === undefined || eventChatId === null) {
|
|
714
|
+
return includeUnscopedWhenScoped;
|
|
715
|
+
}
|
|
716
|
+
return String(eventChatId).trim() === scopedChatId;
|
|
717
|
+
};
|
|
554
718
|
try {
|
|
719
|
+
// Sync agent memory from storage before subscribing, mirroring the Electron sendChatMessage pattern.
|
|
720
|
+
// Prevents stale-runtime agents (e.g. after agent add/delete with a live runtime) from silently
|
|
721
|
+
// dropping the message with no response-start event.
|
|
722
|
+
if (chatId) {
|
|
723
|
+
await restoreChat(worldName, chatId);
|
|
724
|
+
}
|
|
555
725
|
let responseContent = '';
|
|
726
|
+
let queuedMessageId = null;
|
|
727
|
+
let queuedStatus = null;
|
|
728
|
+
let queuedRetryCount = null;
|
|
556
729
|
let isComplete = false;
|
|
557
730
|
let hasError = false;
|
|
558
731
|
let errorMessage = '';
|
|
@@ -566,7 +739,7 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
|
|
|
566
739
|
reject(new Error(errorMessage));
|
|
567
740
|
}
|
|
568
741
|
}, 60000); // Longer timeout as fallback since we rely on events
|
|
569
|
-
// Helper to reset the fallback timeout
|
|
742
|
+
// Helper to reset the fallback timeout when long-running shell stream activity arrives.
|
|
570
743
|
const resetTimeout = () => {
|
|
571
744
|
clearTimeout(timeoutTimer);
|
|
572
745
|
timeoutTimer = setTimeout(() => {
|
|
@@ -579,7 +752,7 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
|
|
|
579
752
|
}, 60000);
|
|
580
753
|
};
|
|
581
754
|
// Subscribe with minimal client (no forwarding callbacks)
|
|
582
|
-
subscribeWorld(worldName, { isOpen: true }).then(sub => {
|
|
755
|
+
subscribeWorld(worldName, { isOpen: true }).then(async (sub) => {
|
|
583
756
|
if (!sub) {
|
|
584
757
|
hasError = true;
|
|
585
758
|
errorMessage = 'Failed to subscribe to world';
|
|
@@ -588,8 +761,12 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
|
|
|
588
761
|
}
|
|
589
762
|
subscription = sub;
|
|
590
763
|
const world = subscription.world;
|
|
764
|
+
await attachOptionalOpikTracer(world, { source: 'server' });
|
|
591
765
|
// Listen to world activity events to detect when all processing is complete
|
|
592
766
|
const worldActivityListener = (eventData) => {
|
|
767
|
+
if (!isChatEventInScope(eventData?.chatId, true)) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
593
770
|
if (eventData.type === 'response-start') {
|
|
594
771
|
awaitingIdle = true;
|
|
595
772
|
loggerChat.debug('Non-streaming: world processing started', {
|
|
@@ -608,6 +785,9 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
|
|
|
608
785
|
};
|
|
609
786
|
// Collect message events for response
|
|
610
787
|
const messageListener = (eventData) => {
|
|
788
|
+
if (!isChatEventInScope(eventData?.chatId, false)) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
611
791
|
responseContent = JSON.stringify({ type: 'message', data: eventData });
|
|
612
792
|
};
|
|
613
793
|
// Listen to activity events for completion detection
|
|
@@ -616,16 +796,33 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
|
|
|
616
796
|
// Listen to message events for response content
|
|
617
797
|
world.eventEmitter.on(EventType.MESSAGE, messageListener);
|
|
618
798
|
listeners.set(EventType.MESSAGE, messageListener);
|
|
619
|
-
// Listen to SSE events to extend timeout on
|
|
799
|
+
// Listen to SSE events to extend timeout on shell stream activity.
|
|
620
800
|
const sseListener = (eventData) => {
|
|
621
|
-
if (eventData
|
|
801
|
+
if (!isChatEventInScope(eventData?.chatId, false)) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
const isLegacyToolStream = eventData.type === 'tool-stream';
|
|
805
|
+
const isShellAssistantStream = eventData.toolName === 'shell_cmd' &&
|
|
806
|
+
(eventData.type === 'start' || eventData.type === 'chunk' || eventData.type === 'end');
|
|
807
|
+
if (isLegacyToolStream || isShellAssistantStream) {
|
|
622
808
|
resetTimeout();
|
|
623
809
|
}
|
|
624
810
|
};
|
|
625
811
|
world.eventEmitter.on(EventType.SSE, sseListener);
|
|
626
812
|
listeners.set(EventType.SSE, sseListener);
|
|
627
|
-
|
|
628
|
-
|
|
813
|
+
if (isUserSender(sender)) {
|
|
814
|
+
// Queue-backed user ingress: enqueue then trigger event-driven processing.
|
|
815
|
+
const queued = await enqueueAndProcessUserTurn(world.id, chatId, message, sender, world);
|
|
816
|
+
queuedMessageId = queued?.messageId || null;
|
|
817
|
+
queuedStatus = queued?.status || null;
|
|
818
|
+
queuedRetryCount = typeof queued?.retryCount === 'number' ? queued.retryCount : null;
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
const dispatched = await dispatchImmediateChatMessage(world.id, chatId, message, sender, world);
|
|
822
|
+
queuedMessageId = dispatched?.messageId || null;
|
|
823
|
+
queuedStatus = null;
|
|
824
|
+
queuedRetryCount = null;
|
|
825
|
+
}
|
|
629
826
|
}).catch(error => {
|
|
630
827
|
hasError = true;
|
|
631
828
|
errorMessage = `Failed to connect to world: ${error instanceof Error ? error.message : error}`;
|
|
@@ -642,7 +839,10 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
|
|
|
642
839
|
message: 'Message processed successfully',
|
|
643
840
|
data: {
|
|
644
841
|
content: responseContent || 'No response received',
|
|
645
|
-
timestamp: new Date().toISOString()
|
|
842
|
+
timestamp: new Date().toISOString(),
|
|
843
|
+
queueMessageId: queuedMessageId,
|
|
844
|
+
queueStatus: queuedStatus,
|
|
845
|
+
queueRetryCount: queuedRetryCount
|
|
646
846
|
}
|
|
647
847
|
});
|
|
648
848
|
}
|
|
@@ -680,6 +880,12 @@ async function handleNonStreamingChat(res, worldName, message, sender, chatId) {
|
|
|
680
880
|
* @returns Promise that resolves when stream is complete
|
|
681
881
|
*/
|
|
682
882
|
async function handleStreamingChat(req, res, worldName, message, sender, chatId) {
|
|
883
|
+
// Sync agent memory from storage before subscribing, mirroring the Electron sendChatMessage pattern.
|
|
884
|
+
// Prevents stale-runtime agents (e.g. after agent add/delete with a live runtime) from silently
|
|
885
|
+
// dropping the message with no response-start event.
|
|
886
|
+
if (chatId) {
|
|
887
|
+
await restoreChat(worldName, chatId);
|
|
888
|
+
}
|
|
683
889
|
// Subscribe to world to get the world instance
|
|
684
890
|
const subscription = await subscribeWorld(worldName, { isOpen: true });
|
|
685
891
|
if (!subscription) {
|
|
@@ -690,16 +896,29 @@ async function handleStreamingChat(req, res, worldName, message, sender, chatId)
|
|
|
690
896
|
return;
|
|
691
897
|
}
|
|
692
898
|
const world = subscription.world;
|
|
899
|
+
await attachOptionalOpikTracer(world, { source: 'server' });
|
|
693
900
|
// Create SSE handler - automatically sets up headers, listeners, and cleanup
|
|
694
901
|
const sseHandler = createSSEHandler(req, res, world, 'chat', chatId);
|
|
695
|
-
|
|
902
|
+
await sseHandler.ready;
|
|
903
|
+
// Clean up subscription when the HTTP response closes/finishes to prevent stale world
|
|
696
904
|
// instances from accumulating in activeSubscribedWorlds.
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
905
|
+
let subscriptionCleanedUp = false;
|
|
906
|
+
const cleanupSubscription = () => {
|
|
907
|
+
if (subscriptionCleanedUp) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
subscriptionCleanedUp = true;
|
|
911
|
+
void subscription.unsubscribe();
|
|
912
|
+
};
|
|
913
|
+
res.on('finish', cleanupSubscription);
|
|
914
|
+
res.on('close', cleanupSubscription);
|
|
700
915
|
try {
|
|
701
|
-
|
|
702
|
-
|
|
916
|
+
if (isUserSender(sender)) {
|
|
917
|
+
await enqueueAndProcessUserTurn(world.id, chatId, message, sender, world);
|
|
918
|
+
}
|
|
919
|
+
else {
|
|
920
|
+
await dispatchImmediateChatMessage(world.id, chatId, message, sender, world);
|
|
921
|
+
}
|
|
703
922
|
}
|
|
704
923
|
catch (error) {
|
|
705
924
|
sseHandler.sendSSE({
|
|
@@ -791,6 +1010,10 @@ router.put('/worlds/:worldName/messages/:messageId', validateWorld, async (req,
|
|
|
791
1010
|
});
|
|
792
1011
|
return;
|
|
793
1012
|
}
|
|
1013
|
+
// Sync agent memory from storage before subscribing so a stale runtime
|
|
1014
|
+
// (e.g. after a world delete+recreate) picks up the fresh agent list.
|
|
1015
|
+
// Mirrors the Electron editMessageInChat flow: restoreChat → ensureWorldSubscribed.
|
|
1016
|
+
await restoreChat(worldCtx.id, chatId, { suppressAutoResume: true });
|
|
794
1017
|
const subscription = await subscribeWorld(worldCtx.id, { isOpen: true });
|
|
795
1018
|
if (!subscription?.world) {
|
|
796
1019
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
@@ -800,7 +1023,17 @@ router.put('/worlds/:worldName/messages/:messageId', validateWorld, async (req,
|
|
|
800
1023
|
res.end();
|
|
801
1024
|
return;
|
|
802
1025
|
}
|
|
1026
|
+
await attachOptionalOpikTracer(subscription.world, { source: 'server' });
|
|
803
1027
|
const sseHandler = createSSEHandler(req, res, subscription.world, 'edit', chatId);
|
|
1028
|
+
await sseHandler.ready;
|
|
1029
|
+
let subscriptionCleanedUp = false;
|
|
1030
|
+
const cleanupSubscription = () => {
|
|
1031
|
+
if (subscriptionCleanedUp) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
subscriptionCleanedUp = true;
|
|
1035
|
+
void subscription.unsubscribe();
|
|
1036
|
+
};
|
|
804
1037
|
const finalizeWithError = (message, data) => {
|
|
805
1038
|
sseHandler.sendSSE({
|
|
806
1039
|
type: 'error',
|
|
@@ -809,13 +1042,12 @@ router.put('/worlds/:worldName/messages/:messageId', validateWorld, async (req,
|
|
|
809
1042
|
});
|
|
810
1043
|
setTimeout(() => {
|
|
811
1044
|
sseHandler.endResponse();
|
|
812
|
-
|
|
1045
|
+
cleanupSubscription();
|
|
813
1046
|
}, 500);
|
|
814
1047
|
};
|
|
815
|
-
// Clean up subscription when the HTTP response finishes.
|
|
816
|
-
res.on('finish',
|
|
817
|
-
|
|
818
|
-
});
|
|
1048
|
+
// Clean up subscription when the HTTP response closes/finishes.
|
|
1049
|
+
res.on('finish', cleanupSubscription);
|
|
1050
|
+
res.on('close', cleanupSubscription);
|
|
819
1051
|
// Pass subscription.world so editUserMessage emits on the same eventEmitter
|
|
820
1052
|
// that the SSE handler is listening on, avoiding stale-world mismatch.
|
|
821
1053
|
const result = await editUserMessage(worldCtx.id, messageId, newContent, chatId, subscription.world);
|
|
@@ -921,12 +1153,33 @@ router.post('/worlds/:worldName/hitl/respond', validateWorld, async (req, res) =
|
|
|
921
1153
|
return;
|
|
922
1154
|
}
|
|
923
1155
|
const worldCtx = req.worldCtx;
|
|
924
|
-
const { requestId, optionId } = validation.data;
|
|
925
|
-
const result =
|
|
1156
|
+
const { requestId, optionId, chatId } = validation.data;
|
|
1157
|
+
const result = submitWorldHitlResponse({
|
|
926
1158
|
worldId: worldCtx.id,
|
|
927
1159
|
requestId,
|
|
928
|
-
optionId
|
|
1160
|
+
optionId,
|
|
1161
|
+
...(chatId !== undefined ? { chatId } : {}),
|
|
929
1162
|
});
|
|
1163
|
+
// Restart-safe fallback: if the runtime pending map lacks the entry (e.g. server
|
|
1164
|
+
// restarted before /setChat was called) but the request exists in persisted messages,
|
|
1165
|
+
// trigger chat activation to seed the pending map via the tool-call resume path.
|
|
1166
|
+
// The caller should retry /hitl/respond after receiving the HITL SSE event.
|
|
1167
|
+
if (!result.accepted && result.reason?.includes('No pending HITL request') && chatId) {
|
|
1168
|
+
const memory = await coreGetMemory(worldCtx.id, chatId);
|
|
1169
|
+
if (Array.isArray(memory)) {
|
|
1170
|
+
const surviving = listPendingHitlPromptEventsFromMessages(memory, chatId);
|
|
1171
|
+
const inMessages = surviving.some((item) => item.prompt.requestId === requestId);
|
|
1172
|
+
if (inMessages) {
|
|
1173
|
+
// Trigger restore/resume asynchronously to repopulate the pending map.
|
|
1174
|
+
void activateChatWithSnapshot(worldCtx.id, chatId).catch(() => undefined);
|
|
1175
|
+
res.json({
|
|
1176
|
+
accepted: false,
|
|
1177
|
+
reason: `HITL request '${requestId}' is pending in chat messages but the chat is not yet activated on this server. Call POST /worlds/${req.params.worldName}/setChat/${chatId} to activate, then retry this request.`,
|
|
1178
|
+
});
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
930
1183
|
res.json(result);
|
|
931
1184
|
}
|
|
932
1185
|
catch (error) {
|
|
@@ -1016,33 +1269,6 @@ router.post('/worlds/:worldName/chats', validateWorld, async (req, res) => {
|
|
|
1016
1269
|
sendError(res, 500, 'Failed to create new chat', 'NEW_CHAT_ERROR');
|
|
1017
1270
|
}
|
|
1018
1271
|
});
|
|
1019
|
-
router.post('/worlds/:worldName/chats/:chatId/branch/:messageId', validateWorld, async (req, res) => {
|
|
1020
|
-
try {
|
|
1021
|
-
const { chatId, messageId } = req.params;
|
|
1022
|
-
const worldCtx = req.worldCtx;
|
|
1023
|
-
if (!(await chatExists(worldCtx, chatId))) {
|
|
1024
|
-
sendError(res, 404, 'Chat not found', 'CHAT_NOT_FOUND');
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
const result = await branchChatFromMessage(worldCtx.id, chatId, messageId);
|
|
1028
|
-
res.json({
|
|
1029
|
-
success: true,
|
|
1030
|
-
world: serializeWorld(result.world),
|
|
1031
|
-
chatId: result.newChatId,
|
|
1032
|
-
copiedMessageCount: result.copiedMessageCount,
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
catch (error) {
|
|
1036
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1037
|
-
const isNotFound = errorMessage.includes('not found');
|
|
1038
|
-
const isClientValidationError = errorMessage.includes('required') ||
|
|
1039
|
-
errorMessage.includes('Can only branch from assistant messages.') ||
|
|
1040
|
-
errorMessage.includes('Message not found in source chat');
|
|
1041
|
-
const status = isNotFound ? 404 : (isClientValidationError ? 400 : 500);
|
|
1042
|
-
const code = isNotFound ? 'CHAT_BRANCH_NOT_FOUND' : 'CHAT_BRANCH_ERROR';
|
|
1043
|
-
sendError(res, status, errorMessage || 'Failed to branch chat', code);
|
|
1044
|
-
}
|
|
1045
|
-
});
|
|
1046
1272
|
router.post('/worlds/:worldName/setChat/:chatId', validateWorld, async (req, res) => {
|
|
1047
1273
|
try {
|
|
1048
1274
|
const { chatId } = req.params;
|
|
@@ -1052,18 +1278,22 @@ router.post('/worlds/:worldName/setChat/:chatId', validateWorld, async (req, res
|
|
|
1052
1278
|
sendError(res, 404, 'World not found', 'WORLD_NOT_FOUND');
|
|
1053
1279
|
return;
|
|
1054
1280
|
}
|
|
1055
|
-
const
|
|
1056
|
-
if (!
|
|
1281
|
+
const activated = await activateChatWithSnapshot(worldCtx.id, chatId);
|
|
1282
|
+
if (!activated) {
|
|
1057
1283
|
res.json({
|
|
1058
1284
|
world: serializeWorld(currentWorld),
|
|
1059
1285
|
chatId: currentWorld.currentChatId,
|
|
1286
|
+
hitlPrompts: [],
|
|
1060
1287
|
success: false
|
|
1061
1288
|
});
|
|
1062
1289
|
return;
|
|
1063
1290
|
}
|
|
1291
|
+
const updatedWorld = activated.world;
|
|
1292
|
+
const pendingHitlPrompts = activated.hitlPrompts;
|
|
1064
1293
|
res.json({
|
|
1065
1294
|
world: serializeWorld(updatedWorld),
|
|
1066
|
-
chatId:
|
|
1295
|
+
chatId: activated.chatId,
|
|
1296
|
+
hitlPrompts: pendingHitlPrompts,
|
|
1067
1297
|
success: true
|
|
1068
1298
|
});
|
|
1069
1299
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Error Response Mapping
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Convert runtime/server errors into stable, user-meaningful HTTP responses.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Specific mappings for oversized request payloads and invalid JSON bodies.
|
|
9
|
+
* - Readonly SQLite detection with actionable error messaging.
|
|
10
|
+
* - Safe fallback for unknown internal errors.
|
|
11
|
+
*
|
|
12
|
+
* Implementation Notes:
|
|
13
|
+
* - JSON parse mapping is intentionally strict to body-parser parse errors only,
|
|
14
|
+
* so unrelated `SyntaxError`s in route logic are not mislabeled as request-body issues.
|
|
15
|
+
*
|
|
16
|
+
* Changes:
|
|
17
|
+
* - 2026-02-26: Tightened invalid JSON detection to avoid misclassifying generic runtime SyntaxErrors.
|
|
18
|
+
* - 2026-02-26: Initial extraction from `server/index.ts` for reusable, testable global error mapping.
|
|
19
|
+
*/
|
|
20
|
+
export type ErrorResponsePayload = {
|
|
21
|
+
error: string;
|
|
22
|
+
code: string;
|
|
23
|
+
};
|
|
24
|
+
export declare function getErrorResponse(error: unknown): {
|
|
25
|
+
status: number;
|
|
26
|
+
payload: ErrorResponsePayload;
|
|
27
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Error Response Mapping
|
|
3
|
+
*
|
|
4
|
+
* Purpose:
|
|
5
|
+
* - Convert runtime/server errors into stable, user-meaningful HTTP responses.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Specific mappings for oversized request payloads and invalid JSON bodies.
|
|
9
|
+
* - Readonly SQLite detection with actionable error messaging.
|
|
10
|
+
* - Safe fallback for unknown internal errors.
|
|
11
|
+
*
|
|
12
|
+
* Implementation Notes:
|
|
13
|
+
* - JSON parse mapping is intentionally strict to body-parser parse errors only,
|
|
14
|
+
* so unrelated `SyntaxError`s in route logic are not mislabeled as request-body issues.
|
|
15
|
+
*
|
|
16
|
+
* Changes:
|
|
17
|
+
* - 2026-02-26: Tightened invalid JSON detection to avoid misclassifying generic runtime SyntaxErrors.
|
|
18
|
+
* - 2026-02-26: Initial extraction from `server/index.ts` for reusable, testable global error mapping.
|
|
19
|
+
*/
|
|
20
|
+
function isEntityTooLargeError(error) {
|
|
21
|
+
if (!error || typeof error !== 'object')
|
|
22
|
+
return false;
|
|
23
|
+
const candidate = error;
|
|
24
|
+
return candidate.type === 'entity.too.large' || candidate.status === 413 || candidate.statusCode === 413;
|
|
25
|
+
}
|
|
26
|
+
function isJsonParseError(error) {
|
|
27
|
+
if (!error || typeof error !== 'object')
|
|
28
|
+
return false;
|
|
29
|
+
const candidate = error;
|
|
30
|
+
const isBodyParserParseType = candidate.type === 'entity.parse.failed';
|
|
31
|
+
const isBodyParserSyntaxError = error instanceof SyntaxError
|
|
32
|
+
&& (candidate.status === 400 || candidate.statusCode === 400)
|
|
33
|
+
&& 'body' in candidate;
|
|
34
|
+
return isBodyParserParseType || isBodyParserSyntaxError;
|
|
35
|
+
}
|
|
36
|
+
function isReadonlySqliteError(error) {
|
|
37
|
+
if (!error || typeof error !== 'object')
|
|
38
|
+
return false;
|
|
39
|
+
const candidate = error;
|
|
40
|
+
return candidate.code === 'SQLITE_READONLY' || String(candidate.message || '').includes('SQLITE_READONLY');
|
|
41
|
+
}
|
|
42
|
+
export function getErrorResponse(error) {
|
|
43
|
+
if (isEntityTooLargeError(error)) {
|
|
44
|
+
return {
|
|
45
|
+
status: 413,
|
|
46
|
+
payload: {
|
|
47
|
+
error: 'Request payload too large. Try submitting a smaller update payload.',
|
|
48
|
+
code: 'PAYLOAD_TOO_LARGE'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (isJsonParseError(error)) {
|
|
53
|
+
return {
|
|
54
|
+
status: 400,
|
|
55
|
+
payload: {
|
|
56
|
+
error: 'Invalid JSON body. Please check request formatting.',
|
|
57
|
+
code: 'INVALID_JSON_BODY'
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (isReadonlySqliteError(error)) {
|
|
62
|
+
return {
|
|
63
|
+
status: 503,
|
|
64
|
+
payload: {
|
|
65
|
+
error: 'Database is read-only. Check database file permissions and retry.',
|
|
66
|
+
code: 'DATABASE_READONLY'
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
status: 500,
|
|
72
|
+
payload: {
|
|
73
|
+
error: 'Server failed to process the request.',
|
|
74
|
+
code: 'INTERNAL_ERROR'
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|