agent-world 0.13.0 → 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 +90 -17
- package/dist/cli/commands.d.ts +7 -1
- package/dist/cli/commands.js +27 -10
- package/dist/cli/hitl.d.ts +4 -1
- package/dist/cli/hitl.js +55 -20
- package/dist/cli/index.js +249 -97
- package/dist/cli/system-events.d.ts +27 -0
- package/dist/cli/system-events.js +63 -0
- package/dist/core/activity-tracker.d.ts +26 -0
- package/dist/core/activity-tracker.d.ts.map +1 -1
- package/dist/core/activity-tracker.js +21 -4
- 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 +5 -0
- package/dist/core/create-agent-tool.d.ts.map +1 -1
- package/dist/core/create-agent-tool.js +57 -34
- 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 +203 -36
- 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 -35
- package/dist/core/events/persistence.js.map +1 -1
- package/dist/core/events/publishers.d.ts +13 -7
- package/dist/core/events/publishers.d.ts.map +1 -1
- package/dist/core/events/publishers.js +53 -37
- 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 +61 -148
- 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 +6 -12
- package/dist/core/hitl-tool.d.ts.map +1 -1
- package/dist/core/hitl-tool.js +66 -88
- package/dist/core/hitl-tool.js.map +1 -1
- package/dist/core/hitl.d.ts +61 -4
- package/dist/core/hitl.d.ts.map +1 -1
- package/dist/core/hitl.js +324 -60
- package/dist/core/hitl.js.map +1 -1
- package/dist/core/index.d.ts +11 -7
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +10 -6
- package/dist/core/index.js.map +1 -1
- package/dist/core/llm-manager.d.ts +15 -0
- package/dist/core/llm-manager.d.ts.map +1 -1
- package/dist/core/llm-manager.js +325 -40
- 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 +18 -50
- package/dist/core/managers.d.ts.map +1 -1
- package/dist/core/managers.js +340 -502
- package/dist/core/managers.js.map +1 -1
- package/dist/core/mcp-server-registry.d.ts +16 -1
- package/dist/core/mcp-server-registry.d.ts.map +1 -1
- package/dist/core/mcp-server-registry.js +162 -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 +7 -2
- package/dist/core/tool-utils.d.ts.map +1 -1
- package/dist/core/tool-utils.js +81 -17
- package/dist/core/tool-utils.js.map +1 -1
- package/dist/core/types.d.ts +67 -19
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/utils.d.ts +7 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/utils.js +71 -21
- 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 +260 -18
- 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 +11 -1
- package/dist/server/sse-handler.js +194 -34
- 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-BW41BxMy.css +0 -1
- package/dist/public/assets/index-kO6UJFwK.js +0 -96
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-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.
|
|
8
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, editUserMessage, stopMessageProcessing, submitWorldHitlResponse, EventType } from '../core/index.js';
|
|
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,8 +896,10 @@ 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);
|
|
902
|
+
await sseHandler.ready;
|
|
695
903
|
// Clean up subscription when the HTTP response closes/finishes to prevent stale world
|
|
696
904
|
// instances from accumulating in activeSubscribedWorlds.
|
|
697
905
|
let subscriptionCleanedUp = false;
|
|
@@ -705,8 +913,12 @@ async function handleStreamingChat(req, res, worldName, message, sender, chatId)
|
|
|
705
913
|
res.on('finish', cleanupSubscription);
|
|
706
914
|
res.on('close', cleanupSubscription);
|
|
707
915
|
try {
|
|
708
|
-
|
|
709
|
-
|
|
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
|
+
}
|
|
710
922
|
}
|
|
711
923
|
catch (error) {
|
|
712
924
|
sseHandler.sendSSE({
|
|
@@ -798,6 +1010,10 @@ router.put('/worlds/:worldName/messages/:messageId', validateWorld, async (req,
|
|
|
798
1010
|
});
|
|
799
1011
|
return;
|
|
800
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 });
|
|
801
1017
|
const subscription = await subscribeWorld(worldCtx.id, { isOpen: true });
|
|
802
1018
|
if (!subscription?.world) {
|
|
803
1019
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
@@ -807,7 +1023,9 @@ router.put('/worlds/:worldName/messages/:messageId', validateWorld, async (req,
|
|
|
807
1023
|
res.end();
|
|
808
1024
|
return;
|
|
809
1025
|
}
|
|
1026
|
+
await attachOptionalOpikTracer(subscription.world, { source: 'server' });
|
|
810
1027
|
const sseHandler = createSSEHandler(req, res, subscription.world, 'edit', chatId);
|
|
1028
|
+
await sseHandler.ready;
|
|
811
1029
|
let subscriptionCleanedUp = false;
|
|
812
1030
|
const cleanupSubscription = () => {
|
|
813
1031
|
if (subscriptionCleanedUp) {
|
|
@@ -942,6 +1160,26 @@ router.post('/worlds/:worldName/hitl/respond', validateWorld, async (req, res) =
|
|
|
942
1160
|
optionId,
|
|
943
1161
|
...(chatId !== undefined ? { chatId } : {}),
|
|
944
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
|
+
}
|
|
945
1183
|
res.json(result);
|
|
946
1184
|
}
|
|
947
1185
|
catch (error) {
|
|
@@ -1040,18 +1278,22 @@ router.post('/worlds/:worldName/setChat/:chatId', validateWorld, async (req, res
|
|
|
1040
1278
|
sendError(res, 404, 'World not found', 'WORLD_NOT_FOUND');
|
|
1041
1279
|
return;
|
|
1042
1280
|
}
|
|
1043
|
-
const
|
|
1044
|
-
if (!
|
|
1281
|
+
const activated = await activateChatWithSnapshot(worldCtx.id, chatId);
|
|
1282
|
+
if (!activated) {
|
|
1045
1283
|
res.json({
|
|
1046
1284
|
world: serializeWorld(currentWorld),
|
|
1047
1285
|
chatId: currentWorld.currentChatId,
|
|
1286
|
+
hitlPrompts: [],
|
|
1048
1287
|
success: false
|
|
1049
1288
|
});
|
|
1050
1289
|
return;
|
|
1051
1290
|
}
|
|
1291
|
+
const updatedWorld = activated.world;
|
|
1292
|
+
const pendingHitlPrompts = activated.hitlPrompts;
|
|
1052
1293
|
res.json({
|
|
1053
1294
|
world: serializeWorld(updatedWorld),
|
|
1054
|
-
chatId:
|
|
1295
|
+
chatId: activated.chatId,
|
|
1296
|
+
hitlPrompts: pendingHitlPrompts,
|
|
1055
1297
|
success: true
|
|
1056
1298
|
});
|
|
1057
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
|
+
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -15,15 +15,16 @@
|
|
|
15
15
|
* Endpoints: /health + API routes from ./api.ts
|
|
16
16
|
*
|
|
17
17
|
* Recent Changes:
|
|
18
|
+
* - 2026-02-26: Added specific global API error mapping for oversized JSON payloads, invalid JSON bodies, and readonly database errors to avoid generic INTERNAL_ERROR responses.
|
|
18
19
|
* - 2026-02-08: Fixed auto-run detection to only trigger on direct execution
|
|
19
20
|
* - 2026-02-08: Made browser auto-open configurable via AGENT_WORLD_AUTO_OPEN env var
|
|
20
21
|
* - 2026-02-08: Prevented duplicate process signal handler registration using WeakSet
|
|
21
22
|
* - 2026-02-08: Added shutdown guard to prevent race conditions during graceful shutdown
|
|
22
23
|
*/
|
|
23
24
|
import { Server } from 'http';
|
|
25
|
+
export { getErrorResponse } from './error-response.js';
|
|
24
26
|
type StartWebServerOptions = {
|
|
25
27
|
openBrowser?: boolean;
|
|
26
28
|
registerProcessHandlers?: boolean;
|
|
27
29
|
};
|
|
28
30
|
export declare function startWebServer(port?: number, host?: string, options?: StartWebServerOptions): Promise<Server>;
|
|
29
|
-
export {};
|
package/dist/server/index.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* Endpoints: /health + API routes from ./api.ts
|
|
16
16
|
*
|
|
17
17
|
* Recent Changes:
|
|
18
|
+
* - 2026-02-26: Added specific global API error mapping for oversized JSON payloads, invalid JSON bodies, and readonly database errors to avoid generic INTERNAL_ERROR responses.
|
|
18
19
|
* - 2026-02-08: Fixed auto-run detection to only trigger on direct execution
|
|
19
20
|
* - 2026-02-08: Made browser auto-open configurable via AGENT_WORLD_AUTO_OPEN env var
|
|
20
21
|
* - 2026-02-08: Prevented duplicate process signal handler registration using WeakSet
|
|
@@ -32,11 +33,13 @@ import path from 'path';
|
|
|
32
33
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
33
34
|
import apiRouter from './api.js';
|
|
34
35
|
import { initializeMCPRegistry, shutdownAllMCPServers } from '../core/mcp-server-registry.js';
|
|
36
|
+
import { getErrorResponse } from './error-response.js';
|
|
37
|
+
export { getErrorResponse } from './error-response.js';
|
|
35
38
|
// ES modules setup
|
|
36
39
|
const __filename = fileURLToPath(import.meta.url);
|
|
37
40
|
const __dirname = path.dirname(__filename);
|
|
38
41
|
// Configuration
|
|
39
|
-
const PORT = Number(process.env.PORT) ||
|
|
42
|
+
const PORT = Number(process.env.PORT) || 3000;
|
|
40
43
|
const HOST = process.env.HOST || '127.0.0.1';
|
|
41
44
|
// Create server logger after logger auto-initialization
|
|
42
45
|
const serverLogger = createCategoryLogger('server');
|
|
@@ -117,7 +120,8 @@ app.get('*', (req, res) => {
|
|
|
117
120
|
// Error handling middleware
|
|
118
121
|
app.use((error, req, res, next) => {
|
|
119
122
|
console.error('Unhandled error:', error);
|
|
120
|
-
|
|
123
|
+
const { status, payload } = getErrorResponse(error);
|
|
124
|
+
res.status(status).json(payload);
|
|
121
125
|
});
|
|
122
126
|
// 404 handler
|
|
123
127
|
app.use((req, res) => {
|
|
@@ -29,8 +29,14 @@
|
|
|
29
29
|
* ```
|
|
30
30
|
*
|
|
31
31
|
* Created: 2025-11-10 - Extracted from api.ts for reusability
|
|
32
|
+
* Updated: 2026-03-01 - Skip synthesis for 'edit' context to prevent duplicate "From human" messages after message edits.
|
|
33
|
+
* Updated: 2026-03-11 - Exposed a readiness promise so chat/edit dispatch waits until SSE listeners are attached, preventing the web client from missing the initial user-message echo.
|
|
34
|
+
* Updated: 2026-03-11 - Replaced optional-call resolution of the readiness promise with an explicit helper to satisfy
|
|
35
|
+
* strict TypeScript control-flow analysis in the build config.
|
|
36
|
+
* Updated: 2026-02-27 - Scoped realtime log forwarding by world/chat to prevent cross-chat log leakage in chat-scoped streams.
|
|
37
|
+
* Updated: 2026-02-26 - Added realtime log-stream forwarding (`type: 'log'`) to SSE clients to align web error visibility with Electron.
|
|
32
38
|
* Updated: 2026-02-20 - Removed stale legacy event-channel SSE forwarding from this handler.
|
|
33
|
-
* Updated: 2026-02-
|
|
39
|
+
* Updated: 2026-02-21 - Refresh fallback timeout on shell assistant-stream SSE activity (`start`/`chunk`/`end` + `toolName='shell_cmd'`) as well as legacy `tool-stream`.
|
|
34
40
|
* Updated: 2026-02-11 - Extended fallback timeout on tool-stream events to prevent premature timeout
|
|
35
41
|
* Updated: 2026-02-08 - Removed manual tool-intervention SSE commentary and kept generic tool_call forwarding
|
|
36
42
|
* Updated: 2025-11-10 - Added tool event forwarding to SSE channel
|
|
@@ -38,6 +44,10 @@
|
|
|
38
44
|
import { Request, Response } from 'express';
|
|
39
45
|
import { World } from '../core/index.js';
|
|
40
46
|
export interface SSEHandler {
|
|
47
|
+
/**
|
|
48
|
+
* Resolves once synthesis has finished and live listeners are attached.
|
|
49
|
+
*/
|
|
50
|
+
ready: Promise<void>;
|
|
41
51
|
/**
|
|
42
52
|
* Send a Server-Sent Event to the client
|
|
43
53
|
* @param data - Data object to send (will be JSON stringified)
|