vellum 0.2.0 → 0.2.1
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/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +28 -0
- package/src/__tests__/app-bundler.test.ts +12 -33
- package/src/__tests__/browser-skill-endstate.test.ts +1 -5
- package/src/__tests__/call-orchestrator.test.ts +328 -0
- package/src/__tests__/call-state.test.ts +133 -0
- package/src/__tests__/call-store.test.ts +476 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +409 -0
- package/src/__tests__/config-schema.test.ts +49 -0
- package/src/__tests__/doordash-session.test.ts +9 -0
- package/src/__tests__/ipc-snapshot.test.ts +34 -0
- package/src/__tests__/registry.test.ts +13 -8
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +218 -0
- package/src/__tests__/run-orchestrator.test.ts +3 -3
- package/src/__tests__/runtime-attachment-metadata.test.ts +17 -19
- package/src/__tests__/runtime-runs-http.test.ts +1 -19
- package/src/__tests__/runtime-runs.test.ts +7 -7
- package/src/__tests__/session-queue.test.ts +50 -0
- package/src/__tests__/turn-commit.test.ts +56 -0
- package/src/__tests__/workspace-git-service.test.ts +217 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +129 -0
- package/src/bundler/app-bundler.ts +29 -12
- package/src/calls/call-constants.ts +10 -0
- package/src/calls/call-orchestrator.ts +364 -0
- package/src/calls/call-state.ts +64 -0
- package/src/calls/call-store.ts +229 -0
- package/src/calls/relay-server.ts +298 -0
- package/src/calls/twilio-config.ts +34 -0
- package/src/calls/twilio-provider.ts +169 -0
- package/src/calls/twilio-routes.ts +236 -0
- package/src/calls/types.ts +37 -0
- package/src/calls/voice-provider.ts +14 -0
- package/src/cli/doordash.ts +5 -24
- package/src/config/bundled-skills/doordash/SKILL.md +104 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -1
- package/src/config/defaults.ts +11 -0
- package/src/config/schema.ts +57 -0
- package/src/config/system-prompt.ts +50 -1
- package/src/config/types.ts +1 -0
- package/src/daemon/handlers/config.ts +30 -0
- package/src/daemon/handlers/index.ts +6 -0
- package/src/daemon/handlers/work-items.ts +142 -2
- package/src/daemon/ipc-contract-inventory.json +12 -0
- package/src/daemon/ipc-contract.ts +52 -0
- package/src/daemon/lifecycle.ts +27 -5
- package/src/daemon/server.ts +10 -12
- package/src/daemon/session-tool-setup.ts +6 -0
- package/src/daemon/session.ts +40 -1
- package/src/index.ts +2 -0
- package/src/media/gemini-image-service.ts +1 -1
- package/src/memory/db.ts +266 -0
- package/src/memory/schema.ts +42 -0
- package/src/runtime/http-server.ts +189 -25
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/routes/attachment-routes.ts +6 -6
- package/src/runtime/routes/channel-routes.ts +16 -18
- package/src/runtime/routes/conversation-routes.ts +5 -9
- package/src/runtime/routes/run-routes.ts +4 -8
- package/src/runtime/run-orchestrator.ts +32 -5
- package/src/tools/calls/call-end.ts +117 -0
- package/src/tools/calls/call-start.ts +134 -0
- package/src/tools/calls/call-status.ts +97 -0
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/registry.ts +2 -4
- package/src/tools/tasks/index.ts +2 -0
- package/src/tools/tasks/task-delete.ts +49 -8
- package/src/tools/tasks/task-run.ts +9 -1
- package/src/tools/tasks/work-item-enqueue.ts +93 -3
- package/src/tools/tasks/work-item-list.ts +10 -25
- package/src/tools/tasks/work-item-remove.ts +112 -0
- package/src/tools/tasks/work-item-update.ts +186 -0
- package/src/tools/tool-manifest.ts +39 -31
- package/src/tools/ui-surface/definitions.ts +3 -0
- package/src/work-items/work-item-store.ts +209 -0
- package/src/workspace/commit-message-enrichment-service.ts +260 -0
- package/src/workspace/commit-message-provider.ts +95 -0
- package/src/workspace/git-service.ts +187 -32
- package/src/workspace/heartbeat-service.ts +70 -13
- package/src/workspace/turn-commit.ts +39 -49
|
@@ -10,6 +10,8 @@ import { resolve } from 'node:path';
|
|
|
10
10
|
import { timingSafeEqual } from 'node:crypto';
|
|
11
11
|
import { ConfigError, IngressBlockedError } from '../util/errors.js';
|
|
12
12
|
import { getLogger } from '../util/logger.js';
|
|
13
|
+
import { getSecureKey } from '../security/secure-keys.js';
|
|
14
|
+
import { TwilioConversationRelayProvider } from '../calls/twilio-provider.js';
|
|
13
15
|
import type { RunOrchestrator } from './run-orchestrator.js';
|
|
14
16
|
|
|
15
17
|
// Route handlers — grouped by domain
|
|
@@ -45,6 +47,14 @@ import {
|
|
|
45
47
|
handleDeleteSharedApp,
|
|
46
48
|
} from './routes/app-routes.js';
|
|
47
49
|
import { handleAddSecret } from './routes/secret-routes.js';
|
|
50
|
+
import {
|
|
51
|
+
handleVoiceWebhook,
|
|
52
|
+
handleStatusCallback,
|
|
53
|
+
handleConnectAction,
|
|
54
|
+
handleCallAnswer,
|
|
55
|
+
} from '../calls/twilio-routes.js';
|
|
56
|
+
import { RelayConnection, activeRelayConnections } from '../calls/relay-server.js';
|
|
57
|
+
import type { RelayWebSocketData } from '../calls/relay-server.js';
|
|
48
58
|
|
|
49
59
|
// Re-export shared types so existing consumers don't need to update imports
|
|
50
60
|
export type {
|
|
@@ -95,6 +105,82 @@ function getDiskSpaceInfo(): DiskSpaceInfo | null {
|
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Regex to extract the Twilio webhook subpath from both top-level and
|
|
110
|
+
* assistant-scoped route shapes:
|
|
111
|
+
* /v1/calls/twilio/<subpath>
|
|
112
|
+
* /v1/assistants/<id>/calls/twilio/<subpath>
|
|
113
|
+
*/
|
|
114
|
+
const TWILIO_WEBHOOK_RE = /^\/v1\/(?:assistants\/[^/]+\/)?calls\/twilio\/(.+)$/;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Validate a Twilio webhook request's X-Twilio-Signature header.
|
|
118
|
+
*
|
|
119
|
+
* Returns the raw body text on success so callers can reconstruct the Request
|
|
120
|
+
* for downstream handlers (which also need to read the body).
|
|
121
|
+
* Returns a 403 Response if signature validation fails.
|
|
122
|
+
* If the Twilio auth token is not configured, skips validation with a warning.
|
|
123
|
+
*/
|
|
124
|
+
async function validateTwilioWebhook(
|
|
125
|
+
req: Request,
|
|
126
|
+
): Promise<{ body: string } | Response> {
|
|
127
|
+
const rawBody = await req.text();
|
|
128
|
+
const authToken = getSecureKey('twilio_auth_token');
|
|
129
|
+
|
|
130
|
+
if (!authToken) {
|
|
131
|
+
log.warn('Twilio auth token not configured — skipping webhook signature validation');
|
|
132
|
+
return { body: rawBody };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const signature = req.headers.get('x-twilio-signature');
|
|
136
|
+
if (!signature) {
|
|
137
|
+
log.warn('Twilio webhook request missing X-Twilio-Signature header');
|
|
138
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Parse form-urlencoded body into key-value params for signature computation
|
|
142
|
+
const params: Record<string, string> = {};
|
|
143
|
+
const formData = new URLSearchParams(rawBody);
|
|
144
|
+
for (const [key, value] of formData.entries()) {
|
|
145
|
+
params[key] = value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Reconstruct the public-facing URL that Twilio signed against.
|
|
149
|
+
// Behind proxies/gateways, req.url is the local server URL (e.g.
|
|
150
|
+
// http://127.0.0.1:7821/...) which differs from the public URL Twilio
|
|
151
|
+
// used to compute the HMAC-SHA1 signature.
|
|
152
|
+
const publicBaseUrl = process.env.TWILIO_WEBHOOK_BASE_URL;
|
|
153
|
+
const parsedUrl = new URL(req.url);
|
|
154
|
+
const publicUrl = publicBaseUrl
|
|
155
|
+
? publicBaseUrl.replace(/\/$/, '') + parsedUrl.pathname + parsedUrl.search
|
|
156
|
+
: req.url;
|
|
157
|
+
|
|
158
|
+
const isValid = TwilioConversationRelayProvider.verifyWebhookSignature(
|
|
159
|
+
publicUrl,
|
|
160
|
+
params,
|
|
161
|
+
signature,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (!isValid) {
|
|
165
|
+
log.warn('Twilio webhook signature validation failed');
|
|
166
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { body: rawBody };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Re-create a Request with the same method, headers, and URL but with a
|
|
174
|
+
* pre-read body string so downstream handlers can call req.text() again.
|
|
175
|
+
*/
|
|
176
|
+
function cloneRequestWithBody(original: Request, body: string): Request {
|
|
177
|
+
return new Request(original.url, {
|
|
178
|
+
method: original.method,
|
|
179
|
+
headers: original.headers,
|
|
180
|
+
body,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
98
184
|
export class RuntimeHttpServer {
|
|
99
185
|
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
100
186
|
private port: number;
|
|
@@ -120,11 +206,37 @@ export class RuntimeHttpServer {
|
|
|
120
206
|
}
|
|
121
207
|
|
|
122
208
|
async start(): Promise<void> {
|
|
123
|
-
this.server = Bun.serve({
|
|
209
|
+
this.server = Bun.serve<RelayWebSocketData>({
|
|
124
210
|
port: this.port,
|
|
125
211
|
hostname: this.hostname,
|
|
126
212
|
maxRequestBodySize: MAX_REQUEST_BODY_BYTES,
|
|
127
|
-
fetch: (req) => this.handleRequest(req),
|
|
213
|
+
fetch: (req, server) => this.handleRequest(req, server),
|
|
214
|
+
websocket: {
|
|
215
|
+
open(ws) {
|
|
216
|
+
const callSessionId = ws.data?.callSessionId;
|
|
217
|
+
log.info({ callSessionId }, 'ConversationRelay WebSocket opened');
|
|
218
|
+
if (callSessionId) {
|
|
219
|
+
const connection = new RelayConnection(ws, callSessionId);
|
|
220
|
+
activeRelayConnections.set(callSessionId, connection);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
message(ws, message) {
|
|
224
|
+
const callSessionId = ws.data?.callSessionId;
|
|
225
|
+
if (callSessionId) {
|
|
226
|
+
const connection = activeRelayConnections.get(callSessionId);
|
|
227
|
+
connection?.handleMessage(typeof message === 'string' ? message : new TextDecoder().decode(message));
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
close(ws, code, reason) {
|
|
231
|
+
const callSessionId = ws.data?.callSessionId;
|
|
232
|
+
log.info({ callSessionId, code, reason: reason?.toString() }, 'ConversationRelay WebSocket closed');
|
|
233
|
+
if (callSessionId) {
|
|
234
|
+
const connection = activeRelayConnections.get(callSessionId);
|
|
235
|
+
connection?.destroy();
|
|
236
|
+
activeRelayConnections.delete(callSessionId);
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
},
|
|
128
240
|
});
|
|
129
241
|
|
|
130
242
|
// Sweep failed channel inbound events for retry every 30 seconds
|
|
@@ -162,7 +274,7 @@ export class RuntimeHttpServer {
|
|
|
162
274
|
return timingSafeEqual(a, b);
|
|
163
275
|
}
|
|
164
276
|
|
|
165
|
-
private async handleRequest(req: Request): Promise<Response> {
|
|
277
|
+
private async handleRequest(req: Request, server: ReturnType<typeof Bun.serve>): Promise<Response> {
|
|
166
278
|
const url = new URL(req.url);
|
|
167
279
|
const path = url.pathname;
|
|
168
280
|
|
|
@@ -171,6 +283,49 @@ export class RuntimeHttpServer {
|
|
|
171
283
|
return this.handleHealth();
|
|
172
284
|
}
|
|
173
285
|
|
|
286
|
+
// WebSocket upgrade for ConversationRelay — before auth check because
|
|
287
|
+
// Twilio WebSocket connections don't use bearer tokens.
|
|
288
|
+
if (path.startsWith('/v1/calls/relay') && req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
|
|
289
|
+
const wsUrl = new URL(req.url);
|
|
290
|
+
const callSessionId = wsUrl.searchParams.get('callSessionId');
|
|
291
|
+
if (!callSessionId) {
|
|
292
|
+
return new Response('Missing callSessionId', { status: 400 });
|
|
293
|
+
}
|
|
294
|
+
const upgraded = server.upgrade(req, { data: { callSessionId } });
|
|
295
|
+
if (!upgraded) {
|
|
296
|
+
return new Response('WebSocket upgrade failed', { status: 500 });
|
|
297
|
+
}
|
|
298
|
+
// Bun handles the response after a successful upgrade.
|
|
299
|
+
// The RelayConnection is created in the websocket.open handler.
|
|
300
|
+
return undefined as unknown as Response;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Twilio webhook endpoints — before auth check because Twilio
|
|
304
|
+
// webhook POSTs don't include bearer tokens.
|
|
305
|
+
// Supports both /v1/calls/twilio/* and /v1/assistants/:id/calls/twilio/*
|
|
306
|
+
// Validates X-Twilio-Signature to prevent unauthorized access. ──
|
|
307
|
+
const twilioMatch = path.match(TWILIO_WEBHOOK_RE);
|
|
308
|
+
if (twilioMatch && req.method === 'POST') {
|
|
309
|
+
const twilioSubpath = twilioMatch[1];
|
|
310
|
+
|
|
311
|
+
// Validate Twilio request signature before dispatching
|
|
312
|
+
const validation = await validateTwilioWebhook(req);
|
|
313
|
+
if (validation instanceof Response) return validation;
|
|
314
|
+
|
|
315
|
+
// Reconstruct request so handlers can read the body
|
|
316
|
+
const validatedReq = cloneRequestWithBody(req, validation.body);
|
|
317
|
+
|
|
318
|
+
if (twilioSubpath === 'voice-webhook') {
|
|
319
|
+
return await handleVoiceWebhook(validatedReq);
|
|
320
|
+
}
|
|
321
|
+
if (twilioSubpath === 'status') {
|
|
322
|
+
return await handleStatusCallback(validatedReq);
|
|
323
|
+
}
|
|
324
|
+
if (twilioSubpath === 'connect-action') {
|
|
325
|
+
return await handleConnectAction(validatedReq);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
174
329
|
// Require bearer token when configured
|
|
175
330
|
if ((process.env.DISABLE_HTTP_AUTH ?? "").toLowerCase() !== "true" && this.bearerToken) {
|
|
176
331
|
const authHeader = req.headers.get('authorization');
|
|
@@ -180,6 +335,17 @@ export class RuntimeHttpServer {
|
|
|
180
335
|
}
|
|
181
336
|
}
|
|
182
337
|
|
|
338
|
+
// ── Call answer endpoint — behind auth gate ──────────────────────
|
|
339
|
+
const callAnswerMatch = path.match(/^\/v1\/calls\/([^/]+)\/answer$/);
|
|
340
|
+
if (callAnswerMatch && req.method === 'POST') {
|
|
341
|
+
try {
|
|
342
|
+
return await handleCallAnswer(req, callAnswerMatch[1]);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
log.error({ err, callSessionId: callAnswerMatch[1] }, 'Runtime HTTP handler error answering call');
|
|
345
|
+
return Response.json({ error: 'Internal server error' }, { status: 500 });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
183
349
|
// Serve shareable app pages
|
|
184
350
|
const pagesMatch = path.match(/^\/pages\/([^/]+)$/);
|
|
185
351
|
if (pagesMatch && req.method === 'GET') {
|
|
@@ -247,7 +413,7 @@ export class RuntimeHttpServer {
|
|
|
247
413
|
// Paths already handled above (/v1/apps/..., /v1/secrets) will never reach here.
|
|
248
414
|
const newRouteMatch = path.match(/^\/v1\/(?!assistants\/)(.+)$/);
|
|
249
415
|
if (newRouteMatch) {
|
|
250
|
-
return this.dispatchEndpoint(
|
|
416
|
+
return this.dispatchEndpoint(newRouteMatch[1], req, url);
|
|
251
417
|
}
|
|
252
418
|
|
|
253
419
|
// Legacy: /v1/assistants/:assistantId/<endpoint>
|
|
@@ -259,7 +425,7 @@ export class RuntimeHttpServer {
|
|
|
259
425
|
const assistantId = match[1];
|
|
260
426
|
const endpoint = match[2];
|
|
261
427
|
log.warn({ endpoint, assistantId }, '[deprecated] /v1/assistants/:assistantId/... route used; migrate to /v1/...');
|
|
262
|
-
return this.dispatchEndpoint(
|
|
428
|
+
return this.dispatchEndpoint(endpoint, req, url);
|
|
263
429
|
}
|
|
264
430
|
|
|
265
431
|
/**
|
|
@@ -268,7 +434,6 @@ export class RuntimeHttpServer {
|
|
|
268
434
|
* legacy assistant-scoped routes (/v1/assistants/:assistantId/<endpoint>).
|
|
269
435
|
*/
|
|
270
436
|
private async dispatchEndpoint(
|
|
271
|
-
assistantId: string,
|
|
272
437
|
endpoint: string,
|
|
273
438
|
req: Request,
|
|
274
439
|
url: URL,
|
|
@@ -279,32 +444,32 @@ export class RuntimeHttpServer {
|
|
|
279
444
|
}
|
|
280
445
|
|
|
281
446
|
if (endpoint === 'messages' && req.method === 'GET') {
|
|
282
|
-
return handleListMessages(
|
|
447
|
+
return handleListMessages(url, this.interfacesDir);
|
|
283
448
|
}
|
|
284
449
|
|
|
285
450
|
if (endpoint === 'messages' && req.method === 'POST') {
|
|
286
|
-
return await handleSendMessage(
|
|
451
|
+
return await handleSendMessage(req, {
|
|
287
452
|
processMessage: this.processMessage,
|
|
288
453
|
persistAndProcessMessage: this.persistAndProcessMessage,
|
|
289
454
|
});
|
|
290
455
|
}
|
|
291
456
|
|
|
292
457
|
if (endpoint === 'attachments' && req.method === 'POST') {
|
|
293
|
-
return await handleUploadAttachment(
|
|
458
|
+
return await handleUploadAttachment(req);
|
|
294
459
|
}
|
|
295
460
|
|
|
296
461
|
if (endpoint === 'attachments' && req.method === 'DELETE') {
|
|
297
|
-
return await handleDeleteAttachment(
|
|
462
|
+
return await handleDeleteAttachment(req);
|
|
298
463
|
}
|
|
299
464
|
|
|
300
465
|
// Match attachments/:attachmentId
|
|
301
466
|
const attachmentMatch = endpoint.match(/^attachments\/([^/]+)$/);
|
|
302
467
|
if (attachmentMatch && req.method === 'GET') {
|
|
303
|
-
return handleGetAttachment(
|
|
468
|
+
return handleGetAttachment(attachmentMatch[1]);
|
|
304
469
|
}
|
|
305
470
|
|
|
306
471
|
if (endpoint === 'suggestion' && req.method === 'GET') {
|
|
307
|
-
return await handleGetSuggestion(
|
|
472
|
+
return await handleGetSuggestion(url, {
|
|
308
473
|
suggestionCache: this.suggestionCache,
|
|
309
474
|
suggestionInFlight: this.suggestionInFlight,
|
|
310
475
|
});
|
|
@@ -314,7 +479,7 @@ export class RuntimeHttpServer {
|
|
|
314
479
|
if (!this.runOrchestrator) {
|
|
315
480
|
return Response.json({ error: 'Run orchestration not configured' }, { status: 503 });
|
|
316
481
|
}
|
|
317
|
-
return await handleCreateRun(
|
|
482
|
+
return await handleCreateRun(req, this.runOrchestrator);
|
|
318
483
|
}
|
|
319
484
|
|
|
320
485
|
// Match runs/:runId, runs/:runId/decision, runs/:runId/trust-rule
|
|
@@ -325,17 +490,17 @@ export class RuntimeHttpServer {
|
|
|
325
490
|
}
|
|
326
491
|
const runId = runsMatch[1];
|
|
327
492
|
if (runsMatch[2] === '/decision' && req.method === 'POST') {
|
|
328
|
-
return await handleRunDecision(
|
|
493
|
+
return await handleRunDecision(runId, req, this.runOrchestrator);
|
|
329
494
|
}
|
|
330
495
|
if (runsMatch[2] === '/trust-rule' && req.method === 'POST') {
|
|
331
496
|
const run = this.runOrchestrator.getRun(runId);
|
|
332
|
-
if (!run
|
|
497
|
+
if (!run) {
|
|
333
498
|
return Response.json({ error: 'Run not found' }, { status: 404 });
|
|
334
499
|
}
|
|
335
500
|
return await handleAddTrustRule(runId, req);
|
|
336
501
|
}
|
|
337
502
|
if (req.method === 'GET') {
|
|
338
|
-
return handleGetRun(
|
|
503
|
+
return handleGetRun(runId, this.runOrchestrator);
|
|
339
504
|
}
|
|
340
505
|
}
|
|
341
506
|
|
|
@@ -345,36 +510,36 @@ export class RuntimeHttpServer {
|
|
|
345
510
|
}
|
|
346
511
|
|
|
347
512
|
if (endpoint === 'channels/conversation' && req.method === 'DELETE') {
|
|
348
|
-
return await handleDeleteConversation(
|
|
513
|
+
return await handleDeleteConversation(req);
|
|
349
514
|
}
|
|
350
515
|
|
|
351
516
|
if (endpoint === 'channels/inbound' && req.method === 'POST') {
|
|
352
|
-
return await handleChannelInbound(
|
|
517
|
+
return await handleChannelInbound(req, this.processMessage);
|
|
353
518
|
}
|
|
354
519
|
|
|
355
520
|
if (endpoint === 'channels/delivery-ack' && req.method === 'POST') {
|
|
356
|
-
return await handleChannelDeliveryAck(
|
|
521
|
+
return await handleChannelDeliveryAck(req);
|
|
357
522
|
}
|
|
358
523
|
|
|
359
524
|
if (endpoint === 'channels/dead-letters' && req.method === 'GET') {
|
|
360
|
-
return handleListDeadLetters(
|
|
525
|
+
return handleListDeadLetters();
|
|
361
526
|
}
|
|
362
527
|
|
|
363
528
|
if (endpoint === 'channels/replay' && req.method === 'POST') {
|
|
364
|
-
return await handleReplayDeadLetters(
|
|
529
|
+
return await handleReplayDeadLetters(req);
|
|
365
530
|
}
|
|
366
531
|
|
|
367
532
|
return Response.json({ error: 'Not found', source: 'runtime' }, { status: 404 });
|
|
368
533
|
} catch (err) {
|
|
369
534
|
if (err instanceof IngressBlockedError) {
|
|
370
|
-
log.warn({ endpoint,
|
|
535
|
+
log.warn({ endpoint, detectedTypes: err.detectedTypes }, 'Blocked HTTP request containing secrets');
|
|
371
536
|
return Response.json({ error: err.message, code: err.code }, { status: 422 });
|
|
372
537
|
}
|
|
373
538
|
if (err instanceof ConfigError) {
|
|
374
|
-
log.warn({ err, endpoint
|
|
539
|
+
log.warn({ err, endpoint }, 'Runtime HTTP config error');
|
|
375
540
|
return Response.json({ error: err.message, code: err.code }, { status: 422 });
|
|
376
541
|
}
|
|
377
|
-
log.error({ err, endpoint
|
|
542
|
+
log.error({ err, endpoint }, 'Runtime HTTP handler error');
|
|
378
543
|
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
379
544
|
return Response.json({ error: message }, { status: 500 });
|
|
380
545
|
}
|
|
@@ -428,7 +593,6 @@ export class RuntimeHttpServer {
|
|
|
428
593
|
|
|
429
594
|
try {
|
|
430
595
|
const { messageId: userMessageId } = await this.processMessage(
|
|
431
|
-
event.assistantId,
|
|
432
596
|
event.conversationId,
|
|
433
597
|
content,
|
|
434
598
|
attachmentIds,
|
|
@@ -12,7 +12,6 @@ export interface RuntimeMessageSessionOptions {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export type MessageProcessor = (
|
|
15
|
-
assistantId: string,
|
|
16
15
|
conversationId: string,
|
|
17
16
|
content: string,
|
|
18
17
|
attachmentIds?: string[],
|
|
@@ -26,7 +25,6 @@ export type MessageProcessor = (
|
|
|
26
25
|
* immediately.
|
|
27
26
|
*/
|
|
28
27
|
export type NonBlockingMessageProcessor = (
|
|
29
|
-
assistantId: string,
|
|
30
28
|
conversationId: string,
|
|
31
29
|
content: string,
|
|
32
30
|
attachmentIds?: string[],
|
|
@@ -7,7 +7,7 @@ import { validateAttachmentUpload, AttachmentUploadError } from '../../memory/at
|
|
|
7
7
|
/** 30 MB — base64-encoded 20 MB attachment ≈ 27 MB plus JSON wrapper overhead. */
|
|
8
8
|
const MAX_UPLOAD_BODY_BYTES = 30 * 1024 * 1024;
|
|
9
9
|
|
|
10
|
-
export async function handleUploadAttachment(
|
|
10
|
+
export async function handleUploadAttachment(req: Request): Promise<Response> {
|
|
11
11
|
const rawBody = await req.arrayBuffer();
|
|
12
12
|
if (rawBody.byteLength > MAX_UPLOAD_BODY_BYTES) {
|
|
13
13
|
return Response.json(
|
|
@@ -56,7 +56,7 @@ export async function handleUploadAttachment(assistantId: string, req: Request):
|
|
|
56
56
|
let attachment: attachmentsStore.StoredAttachment;
|
|
57
57
|
try {
|
|
58
58
|
attachment = attachmentsStore.uploadAttachment(
|
|
59
|
-
|
|
59
|
+
"self",
|
|
60
60
|
filename,
|
|
61
61
|
mimeType,
|
|
62
62
|
data,
|
|
@@ -78,7 +78,7 @@ export async function handleUploadAttachment(assistantId: string, req: Request):
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
export async function handleDeleteAttachment(
|
|
81
|
+
export async function handleDeleteAttachment(req: Request): Promise<Response> {
|
|
82
82
|
let body: { attachmentId?: string };
|
|
83
83
|
try {
|
|
84
84
|
body = await req.json() as { attachmentId?: string };
|
|
@@ -98,7 +98,7 @@ export async function handleDeleteAttachment(assistantId: string, req: Request):
|
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
const result = attachmentsStore.deleteAttachment(
|
|
101
|
+
const result = attachmentsStore.deleteAttachment("self", attachmentId);
|
|
102
102
|
|
|
103
103
|
if (result === 'not_found') {
|
|
104
104
|
return Response.json(
|
|
@@ -117,8 +117,8 @@ export async function handleDeleteAttachment(assistantId: string, req: Request):
|
|
|
117
117
|
return new Response(null, { status: 204 });
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
export function handleGetAttachment(
|
|
121
|
-
const attachment = attachmentsStore.getAttachmentById(
|
|
120
|
+
export function handleGetAttachment(attachmentId: string): Response {
|
|
121
|
+
const attachment = attachmentsStore.getAttachmentById("self", attachmentId);
|
|
122
122
|
if (!attachment) {
|
|
123
123
|
return Response.json({ error: 'Attachment not found' }, { status: 404 });
|
|
124
124
|
}
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
|
|
19
19
|
const log = getLogger('runtime-http');
|
|
20
20
|
|
|
21
|
-
export async function handleDeleteConversation(
|
|
21
|
+
export async function handleDeleteConversation(req: Request): Promise<Response> {
|
|
22
22
|
const body = await req.json() as {
|
|
23
23
|
sourceChannel?: string;
|
|
24
24
|
externalChatId?: string;
|
|
@@ -34,13 +34,12 @@ export async function handleDeleteConversation(assistantId: string, req: Request
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const conversationKey = `${sourceChannel}:${externalChatId}`;
|
|
37
|
-
deleteConversationKey(
|
|
37
|
+
deleteConversationKey("self", conversationKey);
|
|
38
38
|
|
|
39
39
|
return Response.json({ ok: true });
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export async function handleChannelInbound(
|
|
43
|
-
assistantId: string,
|
|
44
43
|
req: Request,
|
|
45
44
|
processMessage?: MessageProcessor,
|
|
46
45
|
): Promise<Response> {
|
|
@@ -90,7 +89,7 @@ export async function handleChannelInbound(
|
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
if (hasAttachments) {
|
|
93
|
-
const resolved = attachmentsStore.getAttachmentsByIds(
|
|
92
|
+
const resolved = attachmentsStore.getAttachmentsByIds("self", attachmentIds);
|
|
94
93
|
if (resolved.length !== attachmentIds.length) {
|
|
95
94
|
const resolvedIds = new Set(resolved.map((a) => a.id));
|
|
96
95
|
const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
|
|
@@ -113,7 +112,7 @@ export async function handleChannelInbound(
|
|
|
113
112
|
if (isEdit && sourceMessageId) {
|
|
114
113
|
// Dedup the edit event itself (retried edited_message webhooks)
|
|
115
114
|
const editResult = channelDeliveryStore.recordInbound(
|
|
116
|
-
|
|
115
|
+
"self",
|
|
117
116
|
sourceChannel,
|
|
118
117
|
externalChatId,
|
|
119
118
|
externalMessageId,
|
|
@@ -137,7 +136,7 @@ export async function handleChannelInbound(
|
|
|
137
136
|
let original: { messageId: string; conversationId: string } | null = null;
|
|
138
137
|
for (let attempt = 0; attempt <= EDIT_LOOKUP_RETRIES; attempt++) {
|
|
139
138
|
original = channelDeliveryStore.findMessageBySourceId(
|
|
140
|
-
|
|
139
|
+
"self",
|
|
141
140
|
sourceChannel,
|
|
142
141
|
externalChatId,
|
|
143
142
|
sourceMessageId,
|
|
@@ -145,7 +144,7 @@ export async function handleChannelInbound(
|
|
|
145
144
|
if (original) break;
|
|
146
145
|
if (attempt < EDIT_LOOKUP_RETRIES) {
|
|
147
146
|
log.info(
|
|
148
|
-
{ assistantId, sourceMessageId, attempt: attempt + 1, maxAttempts: EDIT_LOOKUP_RETRIES },
|
|
147
|
+
{ assistantId: "self", sourceMessageId, attempt: attempt + 1, maxAttempts: EDIT_LOOKUP_RETRIES },
|
|
149
148
|
'Original message not linked yet, retrying edit lookup',
|
|
150
149
|
);
|
|
151
150
|
await new Promise((resolve) => setTimeout(resolve, EDIT_LOOKUP_DELAY_MS));
|
|
@@ -155,12 +154,12 @@ export async function handleChannelInbound(
|
|
|
155
154
|
if (original) {
|
|
156
155
|
conversationStore.updateMessageContent(original.messageId, content ?? '');
|
|
157
156
|
log.info(
|
|
158
|
-
{ assistantId, sourceMessageId, messageId: original.messageId },
|
|
157
|
+
{ assistantId: "self", sourceMessageId, messageId: original.messageId },
|
|
159
158
|
'Updated message content from edited_message',
|
|
160
159
|
);
|
|
161
160
|
} else {
|
|
162
161
|
log.warn(
|
|
163
|
-
{ assistantId, sourceChannel, externalChatId, sourceMessageId },
|
|
162
|
+
{ assistantId: "self", sourceChannel, externalChatId, sourceMessageId },
|
|
164
163
|
'Could not find original message for edit after retries, ignoring',
|
|
165
164
|
);
|
|
166
165
|
}
|
|
@@ -174,7 +173,7 @@ export async function handleChannelInbound(
|
|
|
174
173
|
|
|
175
174
|
// ── New message path ──
|
|
176
175
|
const result = channelDeliveryStore.recordInbound(
|
|
177
|
-
|
|
176
|
+
"self",
|
|
178
177
|
sourceChannel,
|
|
179
178
|
externalChatId,
|
|
180
179
|
externalMessageId,
|
|
@@ -221,7 +220,6 @@ export async function handleChannelInbound(
|
|
|
221
220
|
}
|
|
222
221
|
|
|
223
222
|
const { messageId: userMessageId } = await processMessage(
|
|
224
|
-
assistantId,
|
|
225
223
|
result.conversationId,
|
|
226
224
|
content ?? '',
|
|
227
225
|
hasAttachments ? attachmentIds : undefined,
|
|
@@ -259,7 +257,7 @@ export async function handleChannelInbound(
|
|
|
259
257
|
try { parsed = JSON.parse(msgs[i].content); } catch { parsed = msgs[i].content; }
|
|
260
258
|
const rendered = renderHistoryContent(parsed);
|
|
261
259
|
|
|
262
|
-
const linked = attachmentsStore.getAttachmentMetadataForMessage(msgs[i].id,
|
|
260
|
+
const linked = attachmentsStore.getAttachmentMetadataForMessage(msgs[i].id, "self");
|
|
263
261
|
const replyAttachments: RuntimeAttachmentMetadata[] = linked.map((a) => ({
|
|
264
262
|
id: a.id,
|
|
265
263
|
filename: a.originalFilename,
|
|
@@ -291,12 +289,12 @@ export async function handleChannelInbound(
|
|
|
291
289
|
});
|
|
292
290
|
}
|
|
293
291
|
|
|
294
|
-
export function handleListDeadLetters(
|
|
295
|
-
const events = channelDeliveryStore.getDeadLetterEvents(
|
|
292
|
+
export function handleListDeadLetters(): Response {
|
|
293
|
+
const events = channelDeliveryStore.getDeadLetterEvents("self");
|
|
296
294
|
return Response.json({ events });
|
|
297
295
|
}
|
|
298
296
|
|
|
299
|
-
export async function handleReplayDeadLetters(
|
|
297
|
+
export async function handleReplayDeadLetters(req: Request): Promise<Response> {
|
|
300
298
|
const body = await req.json() as { eventIds?: string[] };
|
|
301
299
|
const eventIds = body.eventIds;
|
|
302
300
|
|
|
@@ -304,11 +302,11 @@ export async function handleReplayDeadLetters(assistantId: string, req: Request)
|
|
|
304
302
|
return Response.json({ error: 'eventIds array is required' }, { status: 400 });
|
|
305
303
|
}
|
|
306
304
|
|
|
307
|
-
const replayed = channelDeliveryStore.replayDeadLetters(
|
|
305
|
+
const replayed = channelDeliveryStore.replayDeadLetters("self", eventIds);
|
|
308
306
|
return Response.json({ replayed });
|
|
309
307
|
}
|
|
310
308
|
|
|
311
|
-
export async function handleChannelDeliveryAck(
|
|
309
|
+
export async function handleChannelDeliveryAck(req: Request): Promise<Response> {
|
|
312
310
|
const body = await req.json() as {
|
|
313
311
|
sourceChannel?: string;
|
|
314
312
|
externalChatId?: string;
|
|
@@ -328,7 +326,7 @@ export async function handleChannelDeliveryAck(assistantId: string, req: Request
|
|
|
328
326
|
}
|
|
329
327
|
|
|
330
328
|
const acked = channelDeliveryStore.acknowledgeDelivery(
|
|
331
|
-
|
|
329
|
+
"self",
|
|
332
330
|
sourceChannel,
|
|
333
331
|
externalChatId,
|
|
334
332
|
externalMessageId,
|
|
@@ -43,7 +43,6 @@ function getInterfaceFilesWithMtimes(interfacesDir: string | null): Array<{ path
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function handleListMessages(
|
|
46
|
-
assistantId: string,
|
|
47
46
|
url: URL,
|
|
48
47
|
interfacesDir: string | null,
|
|
49
48
|
): Response {
|
|
@@ -55,7 +54,7 @@ export function handleListMessages(
|
|
|
55
54
|
);
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
const mapping = getConversationByKey(
|
|
57
|
+
const mapping = getConversationByKey("self", conversationKey);
|
|
59
58
|
if (!mapping) {
|
|
60
59
|
return Response.json({ messages: [] });
|
|
61
60
|
}
|
|
@@ -90,7 +89,7 @@ export function handleListMessages(
|
|
|
90
89
|
const messages: RuntimeMessagePayload[] = merged.map((m) => {
|
|
91
90
|
let msgAttachments: RuntimeAttachmentMetadata[] = [];
|
|
92
91
|
if (m.role === 'assistant' && m.id) {
|
|
93
|
-
const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id,
|
|
92
|
+
const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id, "self");
|
|
94
93
|
if (linked.length > 0) {
|
|
95
94
|
msgAttachments = linked.map((a) => ({
|
|
96
95
|
id: a.id,
|
|
@@ -129,7 +128,6 @@ export function handleListMessages(
|
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
export async function handleSendMessage(
|
|
132
|
-
assistantId: string,
|
|
133
131
|
req: Request,
|
|
134
132
|
deps: {
|
|
135
133
|
processMessage?: MessageProcessor;
|
|
@@ -172,7 +170,7 @@ export async function handleSendMessage(
|
|
|
172
170
|
|
|
173
171
|
// Validate that all attachment IDs resolve
|
|
174
172
|
if (hasAttachments) {
|
|
175
|
-
const resolved = attachmentsStore.getAttachmentsByIds(
|
|
173
|
+
const resolved = attachmentsStore.getAttachmentsByIds("self", attachmentIds);
|
|
176
174
|
if (resolved.length !== attachmentIds.length) {
|
|
177
175
|
const resolvedIds = new Set(resolved.map((a) => a.id));
|
|
178
176
|
const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
|
|
@@ -183,7 +181,7 @@ export async function handleSendMessage(
|
|
|
183
181
|
}
|
|
184
182
|
}
|
|
185
183
|
|
|
186
|
-
const mapping = getOrCreateConversation(
|
|
184
|
+
const mapping = getOrCreateConversation("self", conversationKey);
|
|
187
185
|
|
|
188
186
|
const processor = deps.persistAndProcessMessage ?? deps.processMessage;
|
|
189
187
|
if (!processor) {
|
|
@@ -192,7 +190,6 @@ export async function handleSendMessage(
|
|
|
192
190
|
|
|
193
191
|
try {
|
|
194
192
|
const result = await processor(
|
|
195
|
-
assistantId,
|
|
196
193
|
mapping.conversationId,
|
|
197
194
|
content ?? '',
|
|
198
195
|
hasAttachments ? attachmentIds : undefined,
|
|
@@ -236,7 +233,6 @@ async function generateLlmSuggestion(provider: Provider, assistantText: string):
|
|
|
236
233
|
}
|
|
237
234
|
|
|
238
235
|
export async function handleGetSuggestion(
|
|
239
|
-
assistantId: string,
|
|
240
236
|
url: URL,
|
|
241
237
|
deps: {
|
|
242
238
|
suggestionCache: Map<string, string>;
|
|
@@ -251,7 +247,7 @@ export async function handleGetSuggestion(
|
|
|
251
247
|
);
|
|
252
248
|
}
|
|
253
249
|
|
|
254
|
-
const mapping = getConversationByKey(
|
|
250
|
+
const mapping = getConversationByKey("self", conversationKey);
|
|
255
251
|
if (!mapping) {
|
|
256
252
|
return Response.json({ suggestion: null, messageId: null, source: 'none' as const });
|
|
257
253
|
}
|