@vellumai/assistant 0.3.14 → 0.3.16
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/ARCHITECTURE.md +142 -0
- package/Dockerfile +2 -2
- package/README.md +5 -5
- package/docs/architecture/http-token-refresh.md +252 -0
- package/docs/architecture/memory.md +5 -4
- package/docs/architecture/scheduling.md +4 -88
- package/docs/runbook-trusted-contacts.md +283 -0
- package/docs/trusted-contact-access.md +247 -0
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +2 -6
- package/src/__tests__/access-request-decision.test.ts +331 -0
- package/src/__tests__/asset-materialize-tool.test.ts +7 -7
- package/src/__tests__/asset-search-tool.test.ts +15 -15
- package/src/__tests__/attachments-store.test.ts +13 -13
- package/src/__tests__/call-controller.test.ts +150 -4
- package/src/__tests__/call-conversation-messages.test.ts +2 -2
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/call-start-guardian-guard.test.ts +93 -0
- package/src/__tests__/channel-approval-routes.test.ts +108 -12
- package/src/__tests__/channel-guardian.test.ts +16 -14
- package/src/__tests__/checker.test.ts +24 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +358 -0
- package/src/__tests__/conversation-pairing.test.ts +24 -24
- package/src/__tests__/conversation-store.test.ts +36 -36
- package/src/__tests__/date-context.test.ts +179 -1
- package/src/__tests__/db-migration-rollback.test.ts +4 -7
- package/src/__tests__/deterministic-verification-control-plane.test.ts +5 -5
- package/src/__tests__/emit-signal-routing-intent.test.ts +179 -0
- package/src/__tests__/gateway-only-guard.test.ts +188 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +451 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +197 -0
- package/src/__tests__/guardian-action-followup-executor.test.ts +379 -0
- package/src/__tests__/guardian-action-followup-store.test.ts +376 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +294 -0
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +71 -0
- package/src/__tests__/guardian-action-sweep.test.ts +9 -9
- package/src/__tests__/guardian-control-plane-policy.test.ts +1 -3
- package/src/__tests__/guardian-outbound-http.test.ts +202 -10
- package/src/__tests__/guardian-verification-intent-routing.test.ts +179 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +141 -0
- package/src/__tests__/handlers-telegram-config.test.ts +6 -6
- package/src/__tests__/hooks-runner.test.ts +13 -4
- package/src/__tests__/ingress-routes-http.test.ts +443 -0
- package/src/__tests__/intent-routing.test.ts +14 -0
- package/src/__tests__/ipc-snapshot.test.ts +2 -5
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-regressions.test.ts +16 -12
- package/src/__tests__/non-member-access-request.test.ts +282 -0
- package/src/__tests__/notification-decision-strategy.test.ts +136 -0
- package/src/__tests__/notification-routing-intent.test.ts +11 -2
- package/src/__tests__/notification-thread-candidates.test.ts +166 -0
- package/src/__tests__/recording-intent-fallback.test.ts +0 -1
- package/src/__tests__/recording-intent-handler.test.ts +6 -3
- package/src/__tests__/recording-intent.test.ts +3 -2
- package/src/__tests__/recording-state-machine.test.ts +337 -26
- package/src/__tests__/registry.test.ts +17 -8
- package/src/__tests__/relay-server.test.ts +105 -0
- package/src/__tests__/reminder.test.ts +13 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -4
- package/src/__tests__/scheduler-recurrence.test.ts +50 -0
- package/src/__tests__/server-history-render.test.ts +8 -8
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-runtime-assembly.test.ts +49 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -3
- package/src/__tests__/slack-channel-config.test.ts +230 -0
- package/src/__tests__/subagent-manager-notify.test.ts +4 -4
- package/src/__tests__/swarm-session-integration.test.ts +2 -2
- package/src/__tests__/system-prompt.test.ts +43 -0
- package/src/__tests__/task-management-tools.test.ts +3 -3
- package/src/__tests__/task-tools.test.ts +3 -3
- package/src/__tests__/trust-store.test.ts +17 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +491 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +409 -0
- package/src/__tests__/trusted-contact-verification.test.ts +360 -0
- package/src/__tests__/update-bulletin-format.test.ts +119 -0
- package/src/__tests__/update-bulletin-state.test.ts +129 -0
- package/src/__tests__/update-bulletin.test.ts +260 -0
- package/src/__tests__/update-template-contract.test.ts +29 -0
- package/src/agent/loop.ts +2 -2
- package/src/amazon/client.ts +2 -3
- package/src/calls/call-controller.ts +115 -34
- package/src/calls/call-conversation-messages.ts +2 -2
- package/src/calls/call-domain.ts +10 -3
- package/src/calls/call-pointer-messages.ts +17 -5
- package/src/calls/guardian-action-sweep.ts +77 -36
- package/src/calls/relay-server.ts +51 -12
- package/src/calls/twilio-routes.ts +3 -1
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +4 -4
- package/src/cli/core-commands.ts +3 -3
- package/src/cli/map.ts +8 -5
- package/src/config/bundled-skills/phone-calls/SKILL.md +16 -1
- package/src/config/bundled-skills/tasks/SKILL.md +1 -1
- package/src/config/bundled-skills/tasks/TOOLS.json +4 -4
- package/src/config/bundled-skills/time-based-actions/SKILL.md +11 -1
- package/src/config/computer-use-prompt.ts +1 -0
- package/src/config/core-schema.ts +16 -0
- package/src/config/env-registry.ts +1 -0
- package/src/config/env.ts +16 -1
- package/src/config/memory-schema.ts +5 -0
- package/src/config/schema.ts +4 -0
- package/src/config/system-prompt.ts +69 -2
- package/src/config/templates/BOOTSTRAP.md +1 -1
- package/src/config/templates/IDENTITY.md +8 -4
- package/src/config/templates/SOUL.md +14 -0
- package/src/config/templates/UPDATES.md +16 -0
- package/src/config/templates/USER.md +5 -1
- package/src/config/types.ts +1 -0
- package/src/config/update-bulletin-format.ts +52 -0
- package/src/config/update-bulletin-state.ts +49 -0
- package/src/config/update-bulletin.ts +82 -0
- package/src/config/vellum-skills/catalog.json +6 -0
- package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +44 -10
- package/src/config/vellum-skills/telegram-setup/SKILL.md +4 -4
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +147 -0
- package/src/config/vellum-skills/twilio-setup/SKILL.md +2 -2
- package/src/context/window-manager.ts +43 -3
- package/src/daemon/config-watcher.ts +1 -0
- package/src/daemon/connection-policy.ts +21 -1
- package/src/daemon/daemon-control.ts +164 -7
- package/src/daemon/date-context.ts +174 -1
- package/src/daemon/guardian-action-generators.ts +175 -0
- package/src/daemon/guardian-verification-intent.ts +120 -0
- package/src/daemon/handlers/apps.ts +1 -3
- package/src/daemon/handlers/config-channels.ts +8 -8
- package/src/daemon/handlers/config-heartbeat.ts +1 -1
- package/src/daemon/handlers/config-inbox.ts +55 -159
- package/src/daemon/handlers/config-ingress.ts +1 -1
- package/src/daemon/handlers/config-integrations.ts +1 -1
- package/src/daemon/handlers/config-platform.ts +1 -1
- package/src/daemon/handlers/config-scheduling.ts +2 -2
- package/src/daemon/handlers/config-slack-channel.ts +190 -0
- package/src/daemon/handlers/config-telegram.ts +1 -1
- package/src/daemon/handlers/config-twilio.ts +1 -1
- package/src/daemon/handlers/config-voice.ts +100 -0
- package/src/daemon/handlers/config.ts +3 -0
- package/src/daemon/handlers/index.ts +1 -1
- package/src/daemon/handlers/misc.ts +84 -6
- package/src/daemon/handlers/navigate-settings.ts +27 -0
- package/src/daemon/handlers/recording.ts +270 -144
- package/src/daemon/handlers/sessions.ts +107 -24
- package/src/daemon/handlers/subagents.ts +3 -3
- package/src/daemon/handlers/work-items.ts +10 -7
- package/src/daemon/ipc-contract/integrations.ts +9 -1
- package/src/daemon/ipc-contract/messages.ts +4 -0
- package/src/daemon/ipc-contract/sessions.ts +1 -1
- package/src/daemon/ipc-contract/settings.ts +26 -0
- package/src/daemon/ipc-contract/shared.ts +2 -0
- package/src/daemon/ipc-contract/work-items.ts +1 -7
- package/src/daemon/ipc-contract-inventory.json +5 -1
- package/src/daemon/ipc-contract.ts +5 -1
- package/src/daemon/lifecycle.ts +306 -266
- package/src/daemon/recording-executor.ts +1 -1
- package/src/daemon/recording-intent.ts +0 -41
- package/src/daemon/response-tier.ts +2 -2
- package/src/daemon/server.ts +6 -6
- package/src/daemon/session-agent-loop-handlers.ts +34 -9
- package/src/daemon/session-agent-loop.ts +15 -8
- package/src/daemon/session-history.ts +3 -2
- package/src/daemon/session-media-retry.ts +3 -0
- package/src/daemon/session-messaging.ts +38 -4
- package/src/daemon/session-notifiers.ts +2 -2
- package/src/daemon/session-process.ts +256 -23
- package/src/daemon/session-queue-manager.ts +2 -0
- package/src/daemon/session-runtime-assembly.ts +39 -0
- package/src/daemon/session-skill-tools.ts +13 -4
- package/src/daemon/session-tool-setup.ts +6 -7
- package/src/daemon/session.ts +19 -8
- package/src/daemon/tls-certs.ts +55 -13
- package/src/daemon/tool-side-effects.ts +13 -5
- package/src/gallery/default-gallery.ts +32 -9
- package/src/influencer/client.ts +2 -1
- package/src/memory/channel-delivery-store.ts +37 -567
- package/src/memory/channel-guardian-store.ts +66 -1317
- package/src/memory/conflict-store.ts +4 -4
- package/src/memory/conversation-attention-store.ts +4 -7
- package/src/memory/conversation-crud.ts +668 -0
- package/src/memory/conversation-queries.ts +361 -0
- package/src/memory/conversation-store.ts +45 -983
- package/src/memory/db-connection.ts +3 -0
- package/src/memory/db-init.ts +25 -0
- package/src/memory/delivery-channels.ts +175 -0
- package/src/memory/delivery-crud.ts +211 -0
- package/src/memory/delivery-status.ts +199 -0
- package/src/memory/embedding-backend.ts +70 -4
- package/src/memory/embedding-local.ts +12 -2
- package/src/memory/entity-extractor.ts +3 -8
- package/src/memory/fts-reconciler.ts +121 -0
- package/src/memory/guardian-action-store.ts +366 -3
- package/src/memory/guardian-approvals.ts +569 -0
- package/src/memory/guardian-bindings.ts +130 -0
- package/src/memory/guardian-rate-limits.ts +196 -0
- package/src/memory/guardian-verification.ts +520 -0
- package/src/memory/job-handlers/index-maintenance.ts +2 -1
- package/src/memory/job-utils.ts +8 -5
- package/src/memory/jobs-store.ts +66 -6
- package/src/memory/jobs-worker.ts +23 -1
- package/src/memory/migrations/030-guardian-action-followup.ts +21 -0
- package/src/memory/migrations/030-guardian-verification-purpose.ts +17 -0
- package/src/memory/migrations/031-conversations-thread-type-index.ts +5 -0
- package/src/memory/migrations/100-core-tables.ts +1 -1
- package/src/memory/migrations/101-watchers-and-logs.ts +4 -0
- package/src/memory/migrations/108-tasks-and-work-items.ts +1 -1
- package/src/memory/migrations/112-assistant-inbox.ts +1 -1
- package/src/memory/migrations/113-late-migrations.ts +1 -1
- package/src/memory/migrations/116-messages-fts.ts +13 -0
- package/src/memory/migrations/119-schema-indexes-and-columns.ts +37 -0
- package/src/memory/migrations/120-fk-cascade-rebuilds.ts +161 -0
- package/src/memory/migrations/index.ts +8 -3
- package/src/memory/migrations/validate-migration-state.ts +114 -15
- package/src/memory/qdrant-circuit-breaker.ts +105 -0
- package/src/memory/retriever.ts +46 -13
- package/src/memory/schema-migration.ts +3 -0
- package/src/memory/schema.ts +25 -7
- package/src/memory/search/semantic.ts +8 -90
- package/src/notifications/README.md +1 -1
- package/src/notifications/broadcaster.ts +20 -2
- package/src/notifications/conversation-pairing.ts +3 -3
- package/src/notifications/decision-engine.ts +173 -8
- package/src/notifications/deliveries-store.ts +27 -8
- package/src/notifications/preferences-store.ts +7 -7
- package/src/notifications/thread-candidates.ts +234 -0
- package/src/notifications/types.ts +18 -0
- package/src/permissions/defaults.ts +11 -1
- package/src/permissions/prompter.ts +17 -0
- package/src/permissions/trust-store.ts +2 -0
- package/src/providers/failover.ts +19 -0
- package/src/providers/registry.ts +46 -1
- package/src/runtime/approval-message-composer.ts +1 -1
- package/src/runtime/channel-guardian-service.ts +15 -3
- package/src/runtime/channel-retry-sweep.ts +7 -2
- package/src/runtime/guardian-action-conversation-turn.ts +85 -0
- package/src/runtime/guardian-action-followup-executor.ts +301 -0
- package/src/runtime/guardian-action-message-composer.ts +245 -0
- package/src/runtime/guardian-outbound-actions.ts +35 -15
- package/src/runtime/guardian-verification-templates.ts +15 -9
- package/src/runtime/http-errors.ts +93 -0
- package/src/runtime/http-server.ts +140 -51
- package/src/runtime/http-types.ts +53 -0
- package/src/runtime/ingress-service.ts +237 -0
- package/src/runtime/middleware/error-handler.ts +4 -3
- package/src/runtime/middleware/rate-limiter.ts +160 -0
- package/src/runtime/middleware/request-logger.ts +71 -0
- package/src/runtime/middleware/twilio-validation.ts +7 -6
- package/src/runtime/pending-interactions.ts +12 -0
- package/src/runtime/routes/access-request-decision.ts +215 -0
- package/src/runtime/routes/app-routes.ts +25 -18
- package/src/runtime/routes/approval-routes.ts +18 -47
- package/src/runtime/routes/attachment-routes.ts +15 -41
- package/src/runtime/routes/call-routes.ts +20 -20
- package/src/runtime/routes/channel-delivery-routes.ts +6 -5
- package/src/runtime/routes/contact-routes.ts +4 -9
- package/src/runtime/routes/conversation-attention-routes.ts +5 -4
- package/src/runtime/routes/conversation-routes.ts +26 -57
- package/src/runtime/routes/debug-routes.ts +71 -0
- package/src/runtime/routes/events-routes.ts +3 -2
- package/src/runtime/routes/guardian-approval-interception.ts +221 -0
- package/src/runtime/routes/identity-routes.ts +14 -10
- package/src/runtime/routes/inbound-conversation.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +527 -62
- package/src/runtime/routes/ingress-routes.ts +174 -0
- package/src/runtime/routes/integration-routes.ts +82 -20
- package/src/runtime/routes/pairing-routes.ts +11 -10
- package/src/runtime/routes/secret-routes.ts +10 -18
- package/src/runtime/verification-rate-limiter.ts +83 -0
- package/src/schedule/schedule-store.ts +13 -1
- package/src/schedule/scheduler.ts +2 -2
- package/src/security/secret-ingress.ts +5 -2
- package/src/security/secret-scanner.ts +72 -6
- package/src/subagent/manager.ts +6 -4
- package/src/swarm/plan-validator.ts +4 -1
- package/src/tasks/task-runner.ts +3 -1
- package/src/tools/browser/api-map.ts +9 -6
- package/src/tools/calls/call-start.ts +20 -0
- package/src/tools/executor.ts +50 -568
- package/src/tools/permission-checker.ts +272 -0
- package/src/tools/registry.ts +14 -6
- package/src/tools/reminder/reminder-store.ts +7 -7
- package/src/tools/reminder/reminder.ts +6 -3
- package/src/tools/secret-detection-handler.ts +301 -0
- package/src/tools/subagent/message.ts +1 -1
- package/src/tools/system/voice-config.ts +62 -0
- package/src/tools/tasks/index.ts +3 -3
- package/src/tools/tasks/work-item-list.ts +3 -3
- package/src/tools/tasks/work-item-update.ts +4 -5
- package/src/tools/tool-approval-handler.ts +192 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/watcher/watcher-store.ts +9 -9
- package/src/work-items/work-item-runner.ts +9 -6
- /package/src/memory/migrations/{026-embeddings-nullable-vector-json.ts → 026a-embeddings-nullable-vector-json.ts} +0 -0
- /package/src/memory/migrations/{027-guardian-bootstrap-token.ts → 027a-guardian-bootstrap-token.ts} +0 -0
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { answerCall, cancelCall, getCallStatus, relayInstruction,startCall } from '../../calls/call-domain.js';
|
|
12
12
|
import { getConfig } from '../../config/loader.js';
|
|
13
13
|
import { VALID_CALLER_IDENTITY_MODES } from '../../config/schema.js';
|
|
14
|
+
import { httpError, httpErrorCodeFromStatus } from '../http-errors.js';
|
|
14
15
|
|
|
15
16
|
// ── Idempotency cache ─────────────────────────────────────────────────────────
|
|
16
17
|
// Stores serialized 201 responses keyed by idempotencyKey for 5 minutes so
|
|
@@ -42,10 +43,7 @@ function pruneIdempotencyCache(): void {
|
|
|
42
43
|
*/
|
|
43
44
|
export async function handleStartCall(req: Request, assistantId: string = 'self'): Promise<Response> {
|
|
44
45
|
if (!getConfig().calls.enabled) {
|
|
45
|
-
return
|
|
46
|
-
{ error: 'Calls feature is disabled via configuration. Set calls.enabled to true to use this feature.' },
|
|
47
|
-
{ status: 403 },
|
|
48
|
-
);
|
|
46
|
+
return httpError('FORBIDDEN', 'Calls feature is disabled via configuration. Set calls.enabled to true to use this feature.', 403);
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
let body: {
|
|
@@ -59,23 +57,20 @@ export async function handleStartCall(req: Request, assistantId: string = 'self'
|
|
|
59
57
|
try {
|
|
60
58
|
body = await req.json() as typeof body;
|
|
61
59
|
} catch {
|
|
62
|
-
return
|
|
60
|
+
return httpError('BAD_REQUEST', 'Invalid JSON in request body', 400);
|
|
63
61
|
}
|
|
64
62
|
|
|
65
63
|
if (typeof body !== 'object' || body == null || Array.isArray(body)) {
|
|
66
|
-
return
|
|
64
|
+
return httpError('BAD_REQUEST', 'Request body must be a JSON object', 400);
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
if (!body.conversationId) {
|
|
70
|
-
return
|
|
68
|
+
return httpError('BAD_REQUEST', 'conversationId is required', 400);
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
if (body.callerIdentityMode != null &&
|
|
74
72
|
!(VALID_CALLER_IDENTITY_MODES as readonly string[]).includes(body.callerIdentityMode as string)) {
|
|
75
|
-
return
|
|
76
|
-
{ error: `Invalid callerIdentityMode: "${body.callerIdentityMode}". Must be one of: ${VALID_CALLER_IDENTITY_MODES.join(', ')}` },
|
|
77
|
-
{ status: 400 },
|
|
78
|
-
);
|
|
73
|
+
return httpError('BAD_REQUEST', `Invalid callerIdentityMode: "${body.callerIdentityMode}". Must be one of: ${VALID_CALLER_IDENTITY_MODES.join(', ')}`, 400);
|
|
79
74
|
}
|
|
80
75
|
|
|
81
76
|
// Idempotency check: return cached response for duplicate requests
|
|
@@ -101,7 +96,8 @@ export async function handleStartCall(req: Request, assistantId: string = 'self'
|
|
|
101
96
|
});
|
|
102
97
|
|
|
103
98
|
if (!result.ok) {
|
|
104
|
-
|
|
99
|
+
const status = result.status ?? 500;
|
|
100
|
+
return httpError(httpErrorCodeFromStatus(status), result.error, status);
|
|
105
101
|
}
|
|
106
102
|
|
|
107
103
|
const responseBody = {
|
|
@@ -127,7 +123,8 @@ export function handleGetCallStatus(callSessionId: string): Response {
|
|
|
127
123
|
const result = getCallStatus(callSessionId);
|
|
128
124
|
|
|
129
125
|
if (!result.ok) {
|
|
130
|
-
|
|
126
|
+
const status = result.status ?? 500;
|
|
127
|
+
return httpError(httpErrorCodeFromStatus(status), result.error, status);
|
|
131
128
|
}
|
|
132
129
|
|
|
133
130
|
const { session } = result;
|
|
@@ -166,7 +163,8 @@ export async function handleCancelCall(req: Request, callSessionId: string): Pro
|
|
|
166
163
|
const result = await cancelCall({ callSessionId, reason });
|
|
167
164
|
|
|
168
165
|
if (!result.ok) {
|
|
169
|
-
|
|
166
|
+
const status = result.status ?? 500;
|
|
167
|
+
return httpError(httpErrorCodeFromStatus(status), result.error, status);
|
|
170
168
|
}
|
|
171
169
|
|
|
172
170
|
return Response.json({
|
|
@@ -185,11 +183,11 @@ export async function handleAnswerCall(req: Request, callSessionId: string): Pro
|
|
|
185
183
|
try {
|
|
186
184
|
body = await req.json() as typeof body;
|
|
187
185
|
} catch {
|
|
188
|
-
return
|
|
186
|
+
return httpError('BAD_REQUEST', 'Invalid JSON in request body', 400);
|
|
189
187
|
}
|
|
190
188
|
|
|
191
189
|
if (typeof body !== 'object' || body == null || Array.isArray(body)) {
|
|
192
|
-
return
|
|
190
|
+
return httpError('BAD_REQUEST', 'Request body must be a JSON object', 400);
|
|
193
191
|
}
|
|
194
192
|
|
|
195
193
|
const result = await answerCall({
|
|
@@ -198,7 +196,8 @@ export async function handleAnswerCall(req: Request, callSessionId: string): Pro
|
|
|
198
196
|
});
|
|
199
197
|
|
|
200
198
|
if (!result.ok) {
|
|
201
|
-
|
|
199
|
+
const status = result.status ?? 500;
|
|
200
|
+
return httpError(httpErrorCodeFromStatus(status), result.error, status);
|
|
202
201
|
}
|
|
203
202
|
|
|
204
203
|
return Response.json({ ok: true, questionId: result.questionId });
|
|
@@ -214,11 +213,11 @@ export async function handleInstructionCall(req: Request, callSessionId: string)
|
|
|
214
213
|
try {
|
|
215
214
|
body = await req.json() as typeof body;
|
|
216
215
|
} catch {
|
|
217
|
-
return
|
|
216
|
+
return httpError('BAD_REQUEST', 'Invalid JSON in request body', 400);
|
|
218
217
|
}
|
|
219
218
|
|
|
220
219
|
if (typeof body !== 'object' || body == null || Array.isArray(body)) {
|
|
221
|
-
return
|
|
220
|
+
return httpError('BAD_REQUEST', 'Request body must be a JSON object', 400);
|
|
222
221
|
}
|
|
223
222
|
|
|
224
223
|
const result = await relayInstruction({
|
|
@@ -227,7 +226,8 @@ export async function handleInstructionCall(req: Request, callSessionId: string)
|
|
|
227
226
|
});
|
|
228
227
|
|
|
229
228
|
if (!result.ok) {
|
|
230
|
-
|
|
229
|
+
const status = result.status ?? 500;
|
|
230
|
+
return httpError(httpErrorCodeFromStatus(status), result.error, status);
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
return Response.json({ ok: true });
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* and post-decision delivery scheduling.
|
|
4
4
|
*/
|
|
5
5
|
import * as channelDeliveryStore from '../../memory/channel-delivery-store.js';
|
|
6
|
+
import { httpError } from '../http-errors.js';
|
|
6
7
|
export { type DeliverReplyOptions,deliverReplyViaCallback } from '../channel-reply-delivery.js';
|
|
7
8
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
@@ -19,7 +20,7 @@ export async function handleReplayDeadLetters(req: Request): Promise<Response> {
|
|
|
19
20
|
const eventIds = body.eventIds;
|
|
20
21
|
|
|
21
22
|
if (!Array.isArray(eventIds) || eventIds.length === 0) {
|
|
22
|
-
return
|
|
23
|
+
return httpError('BAD_REQUEST', 'eventIds array is required', 400);
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const replayed = channelDeliveryStore.replayDeadLetters(eventIds);
|
|
@@ -40,13 +41,13 @@ export async function handleChannelDeliveryAck(req: Request): Promise<Response>
|
|
|
40
41
|
const { sourceChannel, externalChatId, externalMessageId } = body;
|
|
41
42
|
|
|
42
43
|
if (!sourceChannel || typeof sourceChannel !== 'string') {
|
|
43
|
-
return
|
|
44
|
+
return httpError('BAD_REQUEST', 'sourceChannel is required', 400);
|
|
44
45
|
}
|
|
45
46
|
if (!externalChatId || typeof externalChatId !== 'string') {
|
|
46
|
-
return
|
|
47
|
+
return httpError('BAD_REQUEST', 'externalChatId is required', 400);
|
|
47
48
|
}
|
|
48
49
|
if (!externalMessageId || typeof externalMessageId !== 'string') {
|
|
49
|
-
return
|
|
50
|
+
return httpError('BAD_REQUEST', 'externalMessageId is required', 400);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
const acked = channelDeliveryStore.acknowledgeDelivery(
|
|
@@ -56,7 +57,7 @@ export async function handleChannelDeliveryAck(req: Request): Promise<Response>
|
|
|
56
57
|
);
|
|
57
58
|
|
|
58
59
|
if (!acked) {
|
|
59
|
-
return
|
|
60
|
+
return httpError('NOT_FOUND', 'Inbound event not found', 404);
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
return new Response(null, { status: 204 });
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { getContact, listContacts, mergeContacts } from '../../contacts/contact-store.js';
|
|
10
|
+
import { httpError } from '../http-errors.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* GET /v1/contacts?limit=50
|
|
@@ -23,10 +24,7 @@ export function handleListContacts(url: URL): Response {
|
|
|
23
24
|
export function handleGetContact(contactId: string): Response {
|
|
24
25
|
const contact = getContact(contactId);
|
|
25
26
|
if (!contact) {
|
|
26
|
-
return
|
|
27
|
-
{ ok: false, error: `Contact "${contactId}" not found` },
|
|
28
|
-
{ status: 404 },
|
|
29
|
-
);
|
|
27
|
+
return httpError('NOT_FOUND', `Contact "${contactId}" not found`, 404);
|
|
30
28
|
}
|
|
31
29
|
return Response.json({ ok: true, contact });
|
|
32
30
|
}
|
|
@@ -38,10 +36,7 @@ export async function handleMergeContacts(req: Request): Promise<Response> {
|
|
|
38
36
|
const body = (await req.json()) as { keepId?: string; mergeId?: string };
|
|
39
37
|
|
|
40
38
|
if (!body.keepId || !body.mergeId) {
|
|
41
|
-
return
|
|
42
|
-
{ ok: false, error: 'keepId and mergeId are required' },
|
|
43
|
-
{ status: 400 },
|
|
44
|
-
);
|
|
39
|
+
return httpError('BAD_REQUEST', 'keepId and mergeId are required', 400);
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
try {
|
|
@@ -49,6 +44,6 @@ export async function handleMergeContacts(req: Request): Promise<Response> {
|
|
|
49
44
|
return Response.json({ ok: true, contact });
|
|
50
45
|
} catch (err) {
|
|
51
46
|
const message = err instanceof Error ? err.message : String(err);
|
|
52
|
-
return
|
|
47
|
+
return httpError('BAD_REQUEST', message, 400);
|
|
53
48
|
}
|
|
54
49
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../../memory/conversation-attention-store.js';
|
|
11
11
|
import * as conversationStore from '../../memory/conversation-store.js';
|
|
12
12
|
import { truncate } from '../../util/truncate.js';
|
|
13
|
+
import { httpError } from '../http-errors.js';
|
|
13
14
|
|
|
14
15
|
export function handleListConversationAttention(url: URL): Response {
|
|
15
16
|
const stateParam = url.searchParams.get('state') ?? 'all';
|
|
@@ -22,7 +23,7 @@ export function handleListConversationAttention(url: URL): Response {
|
|
|
22
23
|
const before = rawBefore !== undefined && Number.isFinite(rawBefore) ? rawBefore : undefined;
|
|
23
24
|
|
|
24
25
|
if (!['seen', 'unseen', 'all'].includes(stateParam)) {
|
|
25
|
-
return
|
|
26
|
+
return httpError('BAD_REQUEST', 'Invalid state parameter. Must be seen, unseen, or all.', 400);
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const attentionStates = listConversationAttention({
|
|
@@ -61,9 +62,9 @@ export function handleListConversationAttention(url: URL): Response {
|
|
|
61
62
|
const results = pageStates.map((attn) => {
|
|
62
63
|
const conv = conversationMap.get(attn.conversationId);
|
|
63
64
|
const convSource = conv?.source ?? 'user';
|
|
64
|
-
const hasUnseen = attn.latestAssistantMessageAt
|
|
65
|
-
(attn.lastSeenAssistantMessageAt
|
|
66
|
-
const state: 'seen' | 'unseen' | 'no_assistant_message' = attn.latestAssistantMessageAt
|
|
65
|
+
const hasUnseen = attn.latestAssistantMessageAt != null &&
|
|
66
|
+
(attn.lastSeenAssistantMessageAt == null || attn.lastSeenAssistantMessageAt < attn.latestAssistantMessageAt);
|
|
67
|
+
const state: 'seen' | 'unseen' | 'no_assistant_message' = attn.latestAssistantMessageAt == null
|
|
67
68
|
? 'no_assistant_message'
|
|
68
69
|
: hasUnseen ? 'unseen' : 'seen';
|
|
69
70
|
|
|
@@ -17,6 +17,7 @@ import { getConfiguredProvider } from '../../providers/provider-send-message.js'
|
|
|
17
17
|
import type { Provider } from '../../providers/types.js';
|
|
18
18
|
import { getLogger } from '../../util/logger.js';
|
|
19
19
|
import { buildAssistantEvent } from '../assistant-event.js';
|
|
20
|
+
import { httpError } from '../http-errors.js';
|
|
20
21
|
import type {
|
|
21
22
|
MessageProcessor,
|
|
22
23
|
NonBlockingMessageProcessor,
|
|
@@ -64,10 +65,7 @@ export function handleListMessages(
|
|
|
64
65
|
const mapping = getConversationByKey(conversationKey);
|
|
65
66
|
resolvedConversationId = mapping?.conversationId;
|
|
66
67
|
} else {
|
|
67
|
-
return
|
|
68
|
-
{ error: 'conversationKey or conversationId query parameter is required' },
|
|
69
|
-
{ status: 400 },
|
|
70
|
-
);
|
|
68
|
+
return httpError('BAD_REQUEST', 'conversationKey or conversationId query parameter is required', 400);
|
|
71
69
|
}
|
|
72
70
|
|
|
73
71
|
if (!resolvedConversationId) {
|
|
@@ -181,10 +179,10 @@ function makeHubPublisher(
|
|
|
181
179
|
});
|
|
182
180
|
}
|
|
183
181
|
|
|
184
|
-
|
|
182
|
+
// ServerMessage is a large union; sessionId exists on most but not all variants.
|
|
185
183
|
const msgSessionId =
|
|
186
|
-
'sessionId' in msg && typeof
|
|
187
|
-
? (
|
|
184
|
+
'sessionId' in msg && typeof (msg as { sessionId?: unknown }).sessionId === 'string'
|
|
185
|
+
? (msg as { sessionId: string }).sessionId
|
|
188
186
|
: undefined;
|
|
189
187
|
const resolvedSessionId = msgSessionId ?? conversationId;
|
|
190
188
|
const event = buildAssistantEvent('self', msg, resolvedSessionId);
|
|
@@ -217,57 +215,36 @@ export async function handleSendMessage(
|
|
|
217
215
|
|
|
218
216
|
const { conversationKey, content, attachmentIds } = body;
|
|
219
217
|
if (!body.sourceChannel || typeof body.sourceChannel !== 'string') {
|
|
220
|
-
return
|
|
221
|
-
{ error: 'sourceChannel is required' },
|
|
222
|
-
{ status: 400 },
|
|
223
|
-
);
|
|
218
|
+
return httpError('BAD_REQUEST', 'sourceChannel is required', 400);
|
|
224
219
|
}
|
|
225
220
|
const sourceChannel = parseChannelId(body.sourceChannel);
|
|
226
221
|
|
|
227
222
|
if (!sourceChannel) {
|
|
228
|
-
return
|
|
229
|
-
{ error: `Invalid sourceChannel: ${body.sourceChannel}. Valid values: ${CHANNEL_IDS.join(', ')}` },
|
|
230
|
-
{ status: 400 },
|
|
231
|
-
);
|
|
223
|
+
return httpError('BAD_REQUEST', `Invalid sourceChannel: ${body.sourceChannel}. Valid values: ${CHANNEL_IDS.join(', ')}`, 400);
|
|
232
224
|
}
|
|
233
225
|
|
|
234
226
|
if (!body.interface || typeof body.interface !== 'string') {
|
|
235
|
-
return
|
|
236
|
-
{ error: 'interface is required' },
|
|
237
|
-
{ status: 400 },
|
|
238
|
-
);
|
|
227
|
+
return httpError('BAD_REQUEST', 'interface is required', 400);
|
|
239
228
|
}
|
|
240
229
|
const sourceInterface = parseInterfaceId(body.interface);
|
|
241
230
|
if (!sourceInterface) {
|
|
242
|
-
return
|
|
243
|
-
{ error: `Invalid interface: ${body.interface}. Valid values: ${INTERFACE_IDS.join(', ')}` },
|
|
244
|
-
{ status: 400 },
|
|
245
|
-
);
|
|
231
|
+
return httpError('BAD_REQUEST', `Invalid interface: ${body.interface}. Valid values: ${INTERFACE_IDS.join(', ')}`, 400);
|
|
246
232
|
}
|
|
247
233
|
|
|
248
234
|
if (!conversationKey) {
|
|
249
|
-
return
|
|
250
|
-
{ error: 'conversationKey is required' },
|
|
251
|
-
{ status: 400 },
|
|
252
|
-
);
|
|
235
|
+
return httpError('BAD_REQUEST', 'conversationKey is required', 400);
|
|
253
236
|
}
|
|
254
237
|
|
|
255
238
|
// Reject non-string content values (numbers, objects, etc.)
|
|
256
239
|
if (content != null && typeof content !== 'string') {
|
|
257
|
-
return
|
|
258
|
-
{ error: 'content must be a string' },
|
|
259
|
-
{ status: 400 },
|
|
260
|
-
);
|
|
240
|
+
return httpError('BAD_REQUEST', 'content must be a string', 400);
|
|
261
241
|
}
|
|
262
242
|
|
|
263
243
|
const trimmedContent = typeof content === 'string' ? content.trim() : '';
|
|
264
244
|
const hasAttachments = Array.isArray(attachmentIds) && attachmentIds.length > 0;
|
|
265
245
|
|
|
266
246
|
if (trimmedContent.length === 0 && !hasAttachments) {
|
|
267
|
-
return
|
|
268
|
-
{ error: 'content or attachmentIds is required' },
|
|
269
|
-
{ status: 400 },
|
|
270
|
-
);
|
|
247
|
+
return httpError('BAD_REQUEST', 'content or attachmentIds is required', 400);
|
|
271
248
|
}
|
|
272
249
|
|
|
273
250
|
// Validate that all attachment IDs resolve
|
|
@@ -276,10 +253,7 @@ export async function handleSendMessage(
|
|
|
276
253
|
if (resolved.length !== attachmentIds.length) {
|
|
277
254
|
const resolvedIds = new Set(resolved.map((a) => a.id));
|
|
278
255
|
const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
|
|
279
|
-
return
|
|
280
|
-
{ error: `Attachment IDs not found: ${missing.join(', ')}` },
|
|
281
|
-
{ status: 400 },
|
|
282
|
-
);
|
|
256
|
+
return httpError('BAD_REQUEST', `Attachment IDs not found: ${missing.join(', ')}`, 400);
|
|
283
257
|
}
|
|
284
258
|
}
|
|
285
259
|
|
|
@@ -299,6 +273,13 @@ export async function handleSendMessage(
|
|
|
299
273
|
: [];
|
|
300
274
|
|
|
301
275
|
if (session.isProcessing()) {
|
|
276
|
+
// If a tool confirmation is pending, auto-deny it so the agent
|
|
277
|
+
// can finish the current turn and process this queued message.
|
|
278
|
+
if (session.hasAnyPendingConfirmation()) {
|
|
279
|
+
session.denyAllPendingConfirmations();
|
|
280
|
+
pendingInteractions.removeBySession(session);
|
|
281
|
+
}
|
|
282
|
+
|
|
302
283
|
// Queue the message so it's processed when the current turn completes
|
|
303
284
|
const requestId = crypto.randomUUID();
|
|
304
285
|
const result = session.enqueueMessage(
|
|
@@ -317,10 +298,7 @@ export async function handleSendMessage(
|
|
|
317
298
|
{ isInteractive: false },
|
|
318
299
|
);
|
|
319
300
|
if (result.rejected) {
|
|
320
|
-
return
|
|
321
|
-
{ error: 'Message queue is full. Please retry later.' },
|
|
322
|
-
{ status: 429 },
|
|
323
|
-
);
|
|
301
|
+
return httpError('RATE_LIMITED', 'Message queue is full. Please retry later.', 429);
|
|
324
302
|
}
|
|
325
303
|
return Response.json({ accepted: true, queued: true }, { status: 202 });
|
|
326
304
|
}
|
|
@@ -335,7 +313,7 @@ export async function handleSendMessage(
|
|
|
335
313
|
assistantMessageInterface: sourceInterface,
|
|
336
314
|
});
|
|
337
315
|
const requestId = crypto.randomUUID();
|
|
338
|
-
const messageId = session.persistUserMessage(content ?? '', attachments, requestId);
|
|
316
|
+
const messageId = await session.persistUserMessage(content ?? '', attachments, requestId);
|
|
339
317
|
|
|
340
318
|
// Fire-and-forget the agent loop; events flow to the hub via onEvent.
|
|
341
319
|
// Mark non-interactive so conflict clarification doesn't block the turn.
|
|
@@ -349,7 +327,7 @@ export async function handleSendMessage(
|
|
|
349
327
|
// ── Legacy path (fallback when sendMessageDeps not wired) ───────────
|
|
350
328
|
const processor = deps.persistAndProcessMessage ?? deps.processMessage;
|
|
351
329
|
if (!processor) {
|
|
352
|
-
return
|
|
330
|
+
return httpError('SERVICE_UNAVAILABLE', 'Message processing not configured', 503);
|
|
353
331
|
}
|
|
354
332
|
|
|
355
333
|
try {
|
|
@@ -364,10 +342,7 @@ export async function handleSendMessage(
|
|
|
364
342
|
return Response.json({ accepted: true, messageId: result.messageId }, { status: 202 });
|
|
365
343
|
} catch (err) {
|
|
366
344
|
if (err instanceof Error && err.message === 'Session is already processing a message') {
|
|
367
|
-
return
|
|
368
|
-
{ error: 'Session is busy processing another message. Please retry.' },
|
|
369
|
-
{ status: 409 },
|
|
370
|
-
);
|
|
345
|
+
return httpError('CONFLICT', 'Session is busy processing another message. Please retry.', 409);
|
|
371
346
|
}
|
|
372
347
|
throw err;
|
|
373
348
|
}
|
|
@@ -406,10 +381,7 @@ export async function handleGetSuggestion(
|
|
|
406
381
|
): Promise<Response> {
|
|
407
382
|
const conversationKey = url.searchParams.get('conversationKey');
|
|
408
383
|
if (!conversationKey) {
|
|
409
|
-
return
|
|
410
|
-
{ error: 'conversationKey query parameter is required' },
|
|
411
|
-
{ status: 400 },
|
|
412
|
-
);
|
|
384
|
+
return httpError('BAD_REQUEST', 'conversationKey query parameter is required', 400);
|
|
413
385
|
}
|
|
414
386
|
|
|
415
387
|
const mapping = getConversationByKey(conversationKey);
|
|
@@ -517,10 +489,7 @@ export async function handleGetSuggestion(
|
|
|
517
489
|
export function handleSearchConversations(url: URL): Response {
|
|
518
490
|
const query = url.searchParams.get('q') ?? '';
|
|
519
491
|
if (!query.trim()) {
|
|
520
|
-
return
|
|
521
|
-
{ error: 'q query parameter is required' },
|
|
522
|
-
{ status: 400 },
|
|
523
|
-
);
|
|
492
|
+
return httpError('BAD_REQUEST', 'q query parameter is required', 400);
|
|
524
493
|
}
|
|
525
494
|
|
|
526
495
|
const limit = url.searchParams.has('limit')
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug introspection endpoint for monitoring and troubleshooting.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { statSync } from 'node:fs';
|
|
6
|
+
|
|
7
|
+
import { getDbPath } from '../../util/platform.js';
|
|
8
|
+
import { countConversations } from '../../memory/conversation-store.js';
|
|
9
|
+
import { getMemoryJobCounts } from '../../memory/jobs-store.js';
|
|
10
|
+
import { countSchedules } from '../../schedule/schedule-store.js';
|
|
11
|
+
import { rawAll } from '../../memory/db.js';
|
|
12
|
+
import { getConfig } from '../../config/loader.js';
|
|
13
|
+
import { getProviderDebugStatus } from '../../providers/registry.js';
|
|
14
|
+
|
|
15
|
+
/** Process start time — used to calculate uptime. */
|
|
16
|
+
const startedAt = Date.now();
|
|
17
|
+
|
|
18
|
+
function getDatabaseSizeBytes(): number | null {
|
|
19
|
+
try {
|
|
20
|
+
return statSync(getDbPath()).size;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getMemoryItemCount(): number {
|
|
27
|
+
try {
|
|
28
|
+
const rows = rawAll<{ c: number }>('SELECT COUNT(*) AS c FROM memory_items');
|
|
29
|
+
return rows[0]?.c ?? 0;
|
|
30
|
+
} catch {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function handleDebug(): Response {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const uptimeSeconds = Math.floor((now - startedAt) / 1000);
|
|
38
|
+
|
|
39
|
+
const conversationCount = countConversations();
|
|
40
|
+
const memoryItemCount = getMemoryItemCount();
|
|
41
|
+
const dbSizeBytes = getDatabaseSizeBytes();
|
|
42
|
+
|
|
43
|
+
const memoryJobCounts = getMemoryJobCounts();
|
|
44
|
+
|
|
45
|
+
const scheduleCounts = countSchedules();
|
|
46
|
+
|
|
47
|
+
const config = getConfig();
|
|
48
|
+
const providerOrder = Array.isArray(config.providerOrder) ? config.providerOrder : [];
|
|
49
|
+
const providerStatus = getProviderDebugStatus(config.provider, providerOrder);
|
|
50
|
+
|
|
51
|
+
return Response.json({
|
|
52
|
+
session: {
|
|
53
|
+
uptimeSeconds,
|
|
54
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
55
|
+
},
|
|
56
|
+
provider: providerStatus,
|
|
57
|
+
memory: {
|
|
58
|
+
conversationCount,
|
|
59
|
+
memoryItemCount,
|
|
60
|
+
...(dbSizeBytes != null ? { databaseSizeBytes: dbSizeBytes } : {}),
|
|
61
|
+
},
|
|
62
|
+
jobs: {
|
|
63
|
+
memory: memoryJobCounts,
|
|
64
|
+
},
|
|
65
|
+
schedules: {
|
|
66
|
+
total: scheduleCounts.total,
|
|
67
|
+
enabled: scheduleCounts.enabled,
|
|
68
|
+
},
|
|
69
|
+
timestamp: new Date(now).toISOString(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -11,6 +11,7 @@ import { getOrCreateConversation } from '../../memory/conversation-key-store.js'
|
|
|
11
11
|
import { formatSseFrame, formatSseHeartbeat } from '../assistant-event.js';
|
|
12
12
|
import type { AssistantEventSubscription } from '../assistant-event-hub.js';
|
|
13
13
|
import { AssistantEventHub,assistantEventHub } from '../assistant-event-hub.js';
|
|
14
|
+
import { httpError } from '../http-errors.js';
|
|
14
15
|
|
|
15
16
|
/** Keep-alive comment sent to idle clients every 30 s by default. */
|
|
16
17
|
const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
@@ -35,7 +36,7 @@ export function handleSubscribeAssistantEvents(
|
|
|
35
36
|
): Response {
|
|
36
37
|
const conversationKey = url.searchParams.get('conversationKey');
|
|
37
38
|
if (!conversationKey) {
|
|
38
|
-
return
|
|
39
|
+
return httpError('BAD_REQUEST', 'conversationKey is required', 400);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const hub = options?.hub ?? assistantEventHub;
|
|
@@ -88,7 +89,7 @@ export function handleSubscribeAssistantEvents(
|
|
|
88
89
|
);
|
|
89
90
|
} catch (err) {
|
|
90
91
|
if (err instanceof RangeError) {
|
|
91
|
-
return
|
|
92
|
+
return httpError('SERVICE_UNAVAILABLE', 'Too many concurrent connections', 503);
|
|
92
93
|
}
|
|
93
94
|
throw err;
|
|
94
95
|
}
|