@tangle-network/agent-app 0.43.46 → 0.43.48

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.
Files changed (40) hide show
  1. package/dist/assistant/index.d.ts +3 -2
  2. package/dist/assistant/index.js +3 -2
  3. package/dist/assistant/index.js.map +1 -1
  4. package/dist/chat-routes/index.d.ts +407 -160
  5. package/dist/chat-routes/index.js +512 -102
  6. package/dist/chat-routes/index.js.map +1 -1
  7. package/dist/chat-store/index.d.ts +3 -2
  8. package/dist/chat-store/index.js +19 -1
  9. package/dist/chat-store/index.js.map +1 -1
  10. package/dist/chunk-23LUHHHN.js +282 -0
  11. package/dist/chunk-23LUHHHN.js.map +1 -0
  12. package/dist/{chunk-4AUQIAYU.js → chunk-5MG74GVQ.js} +2 -281
  13. package/dist/chunk-5MG74GVQ.js.map +1 -0
  14. package/dist/chunk-BQV42AFK.js +99 -0
  15. package/dist/chunk-BQV42AFK.js.map +1 -0
  16. package/dist/{chunk-6E2XJSCT.js → chunk-JGYOYY5D.js} +65 -2
  17. package/dist/chunk-JGYOYY5D.js.map +1 -0
  18. package/dist/{chunk-7VMUOD3G.js → chunk-PHIXZC6Y.js} +1 -1
  19. package/dist/chunk-PHIXZC6Y.js.map +1 -0
  20. package/dist/{file-index-Bn6sitKb.d.ts → file-index-b26ee-_R.d.ts} +1 -1
  21. package/dist/index.d.ts +4 -2
  22. package/dist/index.js +38 -22
  23. package/dist/{parts-1_3y2JmR.d.ts → parts-IB-Kbb7z.d.ts} +146 -2
  24. package/dist/sandbox/index.d.ts +1 -1
  25. package/dist/stale-turn-lock-C8Na1cFZ.d.ts +149 -0
  26. package/dist/stream/index.d.ts +5 -192
  27. package/dist/stream/index.js +14 -12
  28. package/dist/stream-normalizer-DWvtmY6F.d.ts +48 -0
  29. package/dist/teams/index.js +9 -9
  30. package/dist/teams/invitations-api.js +7 -7
  31. package/dist/turn-buffer-C9mEgoop.d.ts +146 -0
  32. package/dist/turn-stream/index.d.ts +551 -0
  33. package/dist/turn-stream/index.js +837 -0
  34. package/dist/turn-stream/index.js.map +1 -0
  35. package/dist/web-react/index.d.ts +4 -3
  36. package/dist/web-react/index.js +24 -2
  37. package/package.json +6 -1
  38. package/dist/chunk-4AUQIAYU.js.map +0 -1
  39. package/dist/chunk-6E2XJSCT.js.map +0 -1
  40. package/dist/chunk-7VMUOD3G.js.map +0 -1
@@ -355,272 +355,6 @@ function encodeEvent(encoder, event) {
355
355
  `);
356
356
  }
357
357
 
358
- // src/stream/turn-identity.ts
359
- function normalizeClientTurnId(value) {
360
- if (value === void 0 || value === null) return void 0;
361
- if (typeof value !== "string") throw new Error("turnId must be a string");
362
- const trimmed = value.trim();
363
- if (!trimmed) throw new Error("turnId must not be blank");
364
- if (trimmed.length > 160) throw new Error("turnId is too long");
365
- if (!/^[A-Za-z0-9:_-]+$/.test(trimmed)) {
366
- throw new Error("turnId contains unsupported characters");
367
- }
368
- return trimmed;
369
- }
370
- function buildUserTextParts(text, turnId) {
371
- const part = { type: "text", text };
372
- if (turnId) part.turnId = turnId;
373
- return [part];
374
- }
375
- function messageHasTurnId(message, turnId) {
376
- for (const part of message.parts ?? []) {
377
- if (part && typeof part === "object" && String(part.turnId ?? "") === turnId) {
378
- return true;
379
- }
380
- }
381
- return false;
382
- }
383
- function resolveChatTurn(input) {
384
- const { existingMessages, userContent, turnId } = input;
385
- const reusableIndex = findReusableUserMessageIndex(existingMessages, userContent, turnId);
386
- if (reusableIndex >= 0) {
387
- return {
388
- turnIndex: countUserMessages(existingMessages.slice(0, reusableIndex)),
389
- shouldInsertUserMessage: false,
390
- priorMessages: existingMessages.slice(0, reusableIndex),
391
- userParts: buildUserTextParts(userContent, turnId)
392
- };
393
- }
394
- return {
395
- turnIndex: countUserMessages(existingMessages),
396
- shouldInsertUserMessage: true,
397
- priorMessages: existingMessages,
398
- userParts: buildUserTextParts(userContent, turnId)
399
- };
400
- }
401
- function findReusableUserMessageIndex(messages, userContent, turnId) {
402
- if (turnId) {
403
- for (let index = messages.length - 1; index >= 0; index -= 1) {
404
- const message = messages[index];
405
- if (message?.role === "user" && messageHasTurnId(message, turnId)) return index;
406
- }
407
- }
408
- const latest = messages.at(-1);
409
- if (latest?.role === "user" && latest.content === userContent) {
410
- return messages.length - 1;
411
- }
412
- return -1;
413
- }
414
- function countUserMessages(messages) {
415
- return messages.filter((message) => message.role === "user").length;
416
- }
417
-
418
- // src/stream/turn-buffer.ts
419
- function deltaTypeOf(ev) {
420
- const e = ev;
421
- if (!e || typeof e !== "object") return null;
422
- const inner = e.kind === "event" ? e.event : e;
423
- if (!inner || typeof inner !== "object") return null;
424
- if ((inner.type === "text" || inner.type === "reasoning") && typeof inner.text === "string") {
425
- return inner.type;
426
- }
427
- return null;
428
- }
429
- function coalesceDeltas(events) {
430
- const out = [];
431
- for (const ev of events) {
432
- const type = deltaTypeOf(ev);
433
- const prev = out[out.length - 1];
434
- if (type && prev && deltaTypeOf(prev) === type) {
435
- const read = (x) => x.kind === "event" ? x.event : x;
436
- const merged = JSON.parse(JSON.stringify(prev));
437
- read(merged).text = String(read(prev).text) + String(read(ev).text);
438
- out[out.length - 1] = merged;
439
- continue;
440
- }
441
- out.push(ev);
442
- }
443
- return out;
444
- }
445
- function asPartUpdate(ev) {
446
- const e = ev;
447
- if (!e || typeof e !== "object" || e.type !== "message.part.updated") return null;
448
- const data = e.data;
449
- if (!data || typeof data !== "object") return null;
450
- const part = data.part;
451
- const partId = part?.id ?? data.partId ?? part?.partId ?? null;
452
- return { partId, delta: data.delta };
453
- }
454
- function coalesceChatStreamEvents(events) {
455
- const out = [];
456
- for (const ev of events) {
457
- const cur = asPartUpdate(ev);
458
- const prevEv = out[out.length - 1];
459
- const prev = prevEv ? asPartUpdate(prevEv) : null;
460
- if (cur && prev && cur.partId != null && cur.partId === prev.partId) {
461
- const merged = JSON.parse(JSON.stringify(ev));
462
- merged.data.delta = String(prev.delta ?? "") + String(cur.delta ?? "");
463
- out[out.length - 1] = merged;
464
- continue;
465
- }
466
- out.push(ev);
467
- }
468
- return out;
469
- }
470
- function createBufferedTurnTap(opts) {
471
- const flushIntervalMs = opts.flushIntervalMs ?? 400;
472
- const coalesce = opts.coalesce ?? coalesceDeltas;
473
- const startedAt = Date.now();
474
- let seq = 0;
475
- let clientGone = false;
476
- let pending = [];
477
- let lastFlush = Date.now();
478
- let started = false;
479
- async function flush() {
480
- if (pending.length === 0) return;
481
- const batch = coalesce(pending);
482
- pending = [];
483
- const rows = batch.map((ev) => ({ seq: ++seq, event: JSON.stringify(ev) }));
484
- await opts.store.append(opts.turnId, rows);
485
- lastFlush = Date.now();
486
- }
487
- async function ensureStarted() {
488
- if (started) return;
489
- started = true;
490
- await opts.store.setStatus(opts.turnId, "running", opts.scopeId);
491
- }
492
- return {
493
- async onEvent(raw) {
494
- await ensureStarted();
495
- const ev = raw && typeof raw === "object" ? { ...raw, _t: Date.now() - startedAt } : raw;
496
- pending.push(ev);
497
- if (!clientGone && opts.write) {
498
- try {
499
- await opts.write(JSON.stringify(ev));
500
- } catch {
501
- clientGone = true;
502
- }
503
- }
504
- if (Date.now() - lastFlush >= flushIntervalMs) await flush();
505
- },
506
- async done(status = "complete") {
507
- await ensureStarted();
508
- if (status === "error") {
509
- await flush().catch(() => {
510
- });
511
- await opts.store.setStatus(opts.turnId, "error", opts.scopeId).catch(() => {
512
- });
513
- return;
514
- }
515
- await flush();
516
- await opts.store.setStatus(opts.turnId, "complete", opts.scopeId);
517
- }
518
- };
519
- }
520
- async function pumpBufferedTurn(opts) {
521
- const tap = createBufferedTurnTap(opts);
522
- try {
523
- for await (const raw of opts.source) await tap.onEvent(raw);
524
- await tap.done("complete");
525
- } catch (err) {
526
- await tap.done("error");
527
- throw err;
528
- }
529
- }
530
- async function* replayTurnEvents(opts) {
531
- const pollMs = opts.pollMs ?? 500;
532
- const timeoutMs = opts.timeoutMs ?? 12e4;
533
- let cursor = opts.fromSeq ?? 0;
534
- const deadline = Date.now() + timeoutMs;
535
- for (; ; ) {
536
- const batch = await opts.store.read(opts.turnId, cursor);
537
- for (const row of batch) {
538
- cursor = Math.max(cursor, row.seq);
539
- yield row;
540
- }
541
- const status = await opts.store.getStatus(opts.turnId);
542
- if (status !== "running") {
543
- yield { seq: -1, event: JSON.stringify({ type: "turn_status", status: status ?? "unknown" }) };
544
- return;
545
- }
546
- if (Date.now() >= deadline) {
547
- yield { seq: -1, event: JSON.stringify({ type: "turn_status", status: "timeout" }) };
548
- return;
549
- }
550
- await new Promise((r) => setTimeout(r, pollMs));
551
- }
552
- }
553
- var TURN_EVENTS_MIGRATION_SQL = `
554
- CREATE TABLE IF NOT EXISTS turn_events (
555
- turnId TEXT NOT NULL,
556
- seq INTEGER NOT NULL,
557
- event TEXT NOT NULL,
558
- PRIMARY KEY (turnId, seq)
559
- );
560
- CREATE TABLE IF NOT EXISTS turn_status (
561
- turnId TEXT PRIMARY KEY,
562
- status TEXT NOT NULL,
563
- scopeId TEXT,
564
- updatedAt TEXT NOT NULL
565
- );
566
- CREATE INDEX IF NOT EXISTS idx_turn_status_scope ON turn_status (scopeId, status);
567
- `;
568
- var TURN_STATUS_SCOPE_MIGRATION_SQL = `ALTER TABLE turn_status ADD COLUMN scopeId TEXT;`;
569
- function createD1TurnEventStore(db) {
570
- return {
571
- async append(turnId, events) {
572
- if (!events.length) return;
573
- const placeholders = events.map(() => "(?, ?, ?)").join(", ");
574
- const values = events.flatMap((e) => [turnId, e.seq, e.event]);
575
- await db.prepare(`INSERT OR IGNORE INTO turn_events (turnId, seq, event) VALUES ${placeholders}`).bind(...values).run();
576
- },
577
- async read(turnId, fromSeq) {
578
- const { results } = await db.prepare("SELECT seq, event FROM turn_events WHERE turnId = ? AND seq > ? ORDER BY seq ASC").bind(turnId, fromSeq).all();
579
- return results;
580
- },
581
- async setStatus(turnId, status, scopeId) {
582
- await db.prepare(
583
- "INSERT INTO turn_status (turnId, status, scopeId, updatedAt) VALUES (?, ?, ?, ?) ON CONFLICT(turnId) DO UPDATE SET status = excluded.status, scopeId = COALESCE(excluded.scopeId, turn_status.scopeId), updatedAt = excluded.updatedAt"
584
- ).bind(turnId, status, scopeId ?? null, (/* @__PURE__ */ new Date()).toISOString()).run();
585
- },
586
- async getStatus(turnId) {
587
- const row = await db.prepare("SELECT status FROM turn_status WHERE turnId = ?").bind(turnId).first();
588
- return row?.status ?? null;
589
- },
590
- async listRunning(scopeId) {
591
- const { results } = await db.prepare("SELECT turnId FROM turn_status WHERE scopeId = ? AND status = 'running' ORDER BY updatedAt DESC").bind(scopeId).all();
592
- return results.map((r) => r.turnId);
593
- }
594
- };
595
- }
596
- function createMemoryTurnEventStore() {
597
- const events = /* @__PURE__ */ new Map();
598
- const status = /* @__PURE__ */ new Map();
599
- const scopes = /* @__PURE__ */ new Map();
600
- const order = [];
601
- return {
602
- async append(turnId, rows) {
603
- const list = events.get(turnId) ?? [];
604
- list.push(...rows);
605
- events.set(turnId, list);
606
- },
607
- async read(turnId, fromSeq) {
608
- return (events.get(turnId) ?? []).filter((e) => e.seq > fromSeq);
609
- },
610
- async setStatus(turnId, s, scopeId) {
611
- status.set(turnId, s);
612
- if (scopeId) scopes.set(turnId, scopeId);
613
- if (!order.includes(turnId)) order.push(turnId);
614
- },
615
- async getStatus(turnId) {
616
- return status.get(turnId) ?? null;
617
- },
618
- async listRunning(scopeId) {
619
- return [...order].reverse().filter((t) => status.get(t) === "running" && scopes.get(t) === scopeId);
620
- }
621
- };
622
- }
623
-
624
358
  export {
625
359
  asRecord,
626
360
  asString,
@@ -640,19 +374,6 @@ export {
640
374
  collapseRedundantTextParts,
641
375
  finalizeAssistantParts,
642
376
  terminalizeDanglingAssistantToolUpdates,
643
- encodeEvent,
644
- normalizeClientTurnId,
645
- buildUserTextParts,
646
- messageHasTurnId,
647
- resolveChatTurn,
648
- coalesceDeltas,
649
- coalesceChatStreamEvents,
650
- createBufferedTurnTap,
651
- pumpBufferedTurn,
652
- replayTurnEvents,
653
- TURN_EVENTS_MIGRATION_SQL,
654
- TURN_STATUS_SCOPE_MIGRATION_SQL,
655
- createD1TurnEventStore,
656
- createMemoryTurnEventStore
377
+ encodeEvent
657
378
  };
658
- //# sourceMappingURL=chunk-4AUQIAYU.js.map
379
+ //# sourceMappingURL=chunk-5MG74GVQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stream/stream-normalizer.ts"],"sourcesContent":["import {\n canTransitionInteractionStatus,\n persistedPartToInteraction,\n type ChatInteractionStatus,\n} from '../interactions/contract'\nimport {\n canTransitionPlanStatus,\n persistedPartToPlan,\n planPartKey,\n planToPersistedPart,\n type ChatPlanStatus,\n} from '../plans/index'\n\nexport type JsonRecord = Record<string, unknown>\n\nexport interface StreamEvent {\n type: string\n data?: JsonRecord\n}\n\nexport function asRecord(value: unknown): JsonRecord | undefined {\n return value && typeof value === 'object' && !Array.isArray(value)\n ? value as JsonRecord\n : undefined\n}\n\nexport function asString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nexport function resolveToolId(part: JsonRecord): string {\n return String(\n part.id ??\n part.callID ??\n part.callId ??\n part.toolUseId ??\n part.toolCallId ??\n part.tool ??\n part.name ??\n `tool-${Date.now()}`,\n )\n}\n\nexport function resolveToolName(part: JsonRecord): string {\n return String(part.tool ?? part.name ?? 'tool')\n}\n\nexport function normalizeTime(value: unknown): JsonRecord | undefined {\n const record = asRecord(value)\n if (!record) return undefined\n\n const start = Number(record.start ?? record.startedAt ?? record.started_at)\n const end = Number(record.end ?? record.completedAt ?? record.completed_at)\n if (!Number.isFinite(start) && !Number.isFinite(end)) return undefined\n\n return {\n start: Number.isFinite(start) ? start : undefined,\n end: Number.isFinite(end) ? end : undefined,\n }\n}\n\nexport function normalizeToolEvent(event: StreamEvent): StreamEvent {\n if (event.type === 'tool_call' || event.type === 'tool.call') {\n const data = event.data ?? {}\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n input: data.arguments ?? data.input,\n status: 'running',\n },\n },\n }\n }\n\n if (event.type === 'tool_result' || event.type === 'tool.result') {\n const data = event.data ?? {}\n const error = asString(data.error)\n return {\n type: 'message.part.updated',\n data: {\n part: {\n type: 'tool',\n id: data.id ?? data.callId ?? data.callID ?? data.name,\n tool: data.name ?? data.tool ?? 'tool',\n output: data.output,\n error,\n status: error ? 'error' : 'completed',\n },\n },\n }\n }\n\n return event\n}\n\nexport function normalizePersistedPart(rawPart: JsonRecord): JsonRecord | null {\n const type = String(rawPart.type ?? '')\n\n if (type === 'text') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'text',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n // id: per-segment identity from the harness; absent on legacy parts,\n // which collapse to a single keyed segment. Never invented here.\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'reasoning') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'reasoning',\n text: asString(rawPart.text) ?? asString(rawPart.content) ?? '',\n time: normalizeTime(rawPart.time),\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'file' || type === 'image') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type,\n ...(id ? { id } : {}),\n ...(asString(rawPart.filename) ? { filename: asString(rawPart.filename) } : {}),\n ...(asString(rawPart.mediaType) ? { mediaType: asString(rawPart.mediaType) } : {}),\n ...(asString(rawPart.url) ? { url: asString(rawPart.url) } : {}),\n ...(asString(rawPart.path) ? { path: asString(rawPart.path) } : {}),\n ...(type === 'file' && asString(rawPart.content) ? { content: asString(rawPart.content) } : {}),\n }\n }\n\n if (type === 'step-start') {\n return { type: 'step-start' }\n }\n\n // The harness's per-step usage receipt. Dropping it here silently loses the\n // turn's token/cost accounting from the persisted transcript.\n if (type === 'step-finish') {\n const tokens = asRecord(rawPart.tokens)\n const cost = Number(rawPart.cost)\n return {\n type: 'step-finish',\n ...(asString(rawPart.reason) ? { reason: asString(rawPart.reason) } : {}),\n ...(tokens ? { tokens } : {}),\n ...(Number.isFinite(cost) ? { cost } : {}),\n }\n }\n\n if (type === 'subtask') {\n const id = asString(rawPart.id) ?? asString(rawPart.partId)\n return {\n type: 'subtask',\n prompt: asString(rawPart.prompt) ?? '',\n description: asString(rawPart.description) ?? '',\n agent: asString(rawPart.agent) ?? '',\n ...(id ? { id } : {}),\n }\n }\n\n if (type === 'interaction') {\n return persistedPartToInteraction(rawPart) ? rawPart : null\n }\n\n if (type === 'plan') {\n const plan = persistedPartToPlan(rawPart)\n return plan ? { ...rawPart, ...planToPersistedPart(plan) } : null\n }\n\n // System-authored notices pass through verbatim; `/chat-store` owns their\n // final typed validation before persistence.\n if (type === 'notice') {\n return rawPart\n }\n\n if (type === 'tool') {\n const state = asRecord(rawPart.state)\n const output = state?.output ?? rawPart.output\n const error = asString(state?.error ?? rawPart.error)\n const terminalError =\n state?.status === 'error' ||\n state?.status === 'failed' ||\n rawPart.status === 'error' ||\n rawPart.status === 'failed' ||\n Boolean(error)\n const status =\n state?.status === 'completed' || rawPart.status === 'completed'\n ? 'completed'\n : terminalError\n ? 'error'\n : output !== undefined\n ? 'completed'\n : 'running'\n\n return {\n type: 'tool',\n id: resolveToolId(rawPart),\n tool: resolveToolName(rawPart),\n callID:\n rawPart.callID != null || rawPart.callId != null\n ? String(rawPart.callID ?? rawPart.callId)\n : undefined,\n state: {\n status,\n input: state?.input ?? rawPart.input,\n output,\n error,\n metadata: asRecord(state?.metadata) ?? asRecord(rawPart.metadata),\n time: normalizeTime(state?.time ?? rawPart.time),\n },\n }\n }\n\n return null\n}\n\n/** Stream/transcript part key for a promoted (path-bearing) attachment,\n * keyed on its storage path — re-emitting the same path folds into the same\n * segment instead of duplicating it. */\nexport function attachmentPartKey(path: string): string {\n return `attachment:${path}`\n}\n\nexport function getPartKey(part: JsonRecord): string {\n const type = String(part.type ?? 'unknown')\n if (type === 'tool') {\n return `tool:${resolveToolId(part)}`\n }\n if (type === 'plan') return planPartKey(String(part.planId ?? ''))\n if ((type === 'file' || type === 'image') && asString(part.path)) {\n return attachmentPartKey(String(part.path))\n }\n\n // Keyed by the part's OWN type so distinct kinds never merge into each\n // other. Untyped parts fall back to the text lane (legacy shape).\n const lane = type && type !== 'unknown' ? type : 'text'\n return `${lane}:${String(part.id ?? part.partId ?? part.index ?? 'current')}`\n}\n\n/** Shallow overlay that skips `undefined` incoming values, so a later partial\n * update never erases a field an earlier one captured. */\nfunction overlayDefined(base: JsonRecord, patch: JsonRecord): JsonRecord {\n const out: JsonRecord = { ...base }\n for (const [key, value] of Object.entries(patch)) {\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\nexport function mergePersistedPart(existing: JsonRecord | undefined, incoming: JsonRecord, delta?: string): JsonRecord {\n const type = String(incoming.type ?? '')\n if (!existing) {\n if (type === 'text' && delta) {\n return { type: 'text', text: delta }\n }\n return incoming\n }\n\n if (type === 'text' && String(existing.type ?? '') === 'text') {\n const existingText = String(existing.text ?? '')\n const incomingText = String(incoming.text ?? '')\n return {\n ...existing,\n ...incoming,\n // An empty snapshot never erases accumulated text (matches reasoning).\n text: delta ? `${existingText}${delta}` : incomingText || existingText,\n }\n }\n\n if (type === 'reasoning' && String(existing.type ?? '') === 'reasoning') {\n const existingText = String(existing.text ?? '')\n const incomingText = String(incoming.text ?? '')\n return {\n ...existing,\n ...incoming,\n text: delta && incomingText === existingText ? `${existingText}${delta}` : incomingText || existingText,\n time: incoming.time ?? existing.time,\n }\n }\n\n if (type === 'tool' && String(existing.type ?? '') === 'tool') {\n const existingState = asRecord(existing.state) ?? {}\n const incomingState = asRecord(incoming.state) ?? {}\n // Overlay only DEFINED incoming fields: a normalized tool part always\n // carries `output`/`error` keys (undefined when not captured), so a plain\n // spread would clobber a completed tool's output with a later empty update.\n const mergedState = overlayDefined(existingState, incomingState)\n // A partial update with no captured status/output/error normalizes to\n // `running`; never let it downgrade a tool that already settled.\n const existingStatus = String(existingState.status ?? '')\n if (\n (existingStatus === 'completed' || existingStatus === 'error') &&\n String(incomingState.status ?? '') === 'running'\n ) {\n mergedState.status = existingStatus\n }\n return {\n ...overlayDefined(existing, incoming),\n state: mergedState,\n }\n }\n\n if (type === 'interaction' && String(existing.type ?? '') === 'interaction') {\n const merged = overlayDefined(existing, incoming)\n const existingStatus = existing.status as ChatInteractionStatus | undefined\n const incomingStatus = incoming.status as ChatInteractionStatus | undefined\n if (\n existingStatus &&\n incomingStatus &&\n existingStatus !== incomingStatus &&\n !canTransitionInteractionStatus(existingStatus, incomingStatus)\n ) {\n merged.status = existingStatus\n }\n if (incoming.answers === undefined && existing.answers !== undefined) {\n merged.answers = existing.answers\n }\n return merged\n }\n\n if (type === 'plan' && String(existing.type ?? '') === 'plan') {\n const existingRevision = Number(existing.revision)\n const incomingRevision = Number(incoming.revision)\n if (Number.isInteger(existingRevision) && Number.isInteger(incomingRevision)) {\n if (incomingRevision < existingRevision) return existing\n if (incomingRevision > existingRevision) return incoming\n }\n const merged = overlayDefined(existing, incoming)\n const existingStatus = existing.status as ChatPlanStatus | undefined\n const incomingStatus = incoming.status as ChatPlanStatus | undefined\n if (\n existingStatus &&\n incomingStatus &&\n existingStatus !== incomingStatus &&\n !canTransitionPlanStatus(existingStatus, incomingStatus)\n ) {\n merged.status = existingStatus\n }\n return merged\n }\n\n return incoming\n}\n\nexport const MISSING_TOOL_TERMINAL_ERROR = 'Tool did not report a terminal result before the assistant turn completed.'\nexport const MISSING_TOOL_TERMINAL_REASON = 'missing-tool-terminal'\n\n/** Closes a tool part left `running` when a stream ended abnormally: settles\n * it as a terminal `error` and stamps `state.metadata.terminalized` so the\n * synthetic settlement is distinguishable from a real tool failure. Parts\n * that already settled (and non-tool parts) pass through untouched. */\nexport function terminalizeDanglingToolPart(part: JsonRecord): JsonRecord {\n if (String(part.type ?? '') !== 'tool') return part\n\n const state = asRecord(part.state) ?? {}\n if (String(state.status ?? part.status ?? '') !== 'running') return part\n\n const metadata = asRecord(state.metadata) ?? {}\n return {\n ...part,\n state: {\n ...state,\n status: 'error',\n error: asString(state.error ?? part.error) ?? MISSING_TOOL_TERMINAL_ERROR,\n metadata: {\n ...metadata,\n terminalized: true,\n terminalReason: MISSING_TOOL_TERMINAL_REASON,\n },\n },\n }\n}\n\nexport function terminalizeDanglingToolParts(parts: JsonRecord[]): JsonRecord[] {\n return parts.map(terminalizeDanglingToolPart)\n}\n\n/** Settles still-pending interaction parts at persist time. The broker\n * guarantees a resolved question either answered (run unblocked, no cancel\n * event) or cancelled/timed out (cancel event already updated the part), so\n * the success path finalizes remaining pendings as `answered` and the\n * failure/terminalize paths as `expired`. */\nexport function finalizePendingInteractionParts(\n parts: JsonRecord[],\n outcome: Extract<ChatInteractionStatus, 'answered' | 'expired'>,\n): JsonRecord[] {\n return parts.map((part) => {\n if (String(part.type ?? '') !== 'interaction') return part\n if (String(part.status ?? '') !== 'pending') return part\n return { ...part, status: outcome }\n })\n}\n\n/** Collapses text-part artifacts of unstable upstream segment identity: the\n * same text arriving under two keys (id-less delta stream, then an\n * id-bearing snapshot) folds into two segments, and interleaved empty\n * segments survive as blank parts. Consecutive identical text parts merge\n * into one; empty text parts drop when any non-empty text part exists. */\nexport function collapseRedundantTextParts(parts: JsonRecord[]): JsonRecord[] {\n const hasNonEmptyText = parts.some(\n (part) => String(part.type ?? '') === 'text' && String(part.text ?? '').trim().length > 0,\n )\n const collapsed: JsonRecord[] = []\n for (const part of parts) {\n if (String(part.type ?? '') !== 'text') {\n collapsed.push(part)\n continue\n }\n const text = String(part.text ?? '')\n if (hasNonEmptyText && text.trim().length === 0) continue\n const previous = collapsed[collapsed.length - 1]\n if (previous && String(previous.type ?? '') === 'text' && String(previous.text ?? '') === text) continue\n collapsed.push(part)\n }\n return collapsed\n}\n\nfunction assembleAssistantParts(\n partOrder: string[],\n partMap: Map<string, JsonRecord>,\n finalText: string,\n): JsonRecord[] {\n const parts = partOrder\n .map((key) => partMap.get(key))\n .filter((part): part is JsonRecord => Boolean(part))\n\n const textParts = parts.filter((part) => String(part.type ?? '') === 'text')\n\n if (textParts.length === 0) {\n if (finalText.trim()) {\n parts.push({ type: 'text', text: finalText })\n }\n return parts\n }\n\n // Id-less text parts form a single logical stream — the final text is\n // authoritative for it.\n if (!textParts.some((part) => asString(part.id))) {\n return parts.map((part) => {\n if (String(part.type ?? '') !== 'text') return part\n return {\n ...part,\n text: finalText || String(part.text ?? ''),\n }\n })\n }\n\n // Per-id text segments: invariant is concat(text parts) === persisted final\n // text, so segment boundaries survive without duplicating the answer into\n // every segment.\n const joined = textParts.map((part) => String(part.text ?? '')).join('')\n if (finalText === joined || finalText.trimEnd() === joined.trimEnd()) {\n return parts\n }\n\n if (finalText.startsWith(joined)) {\n // Final text extends the streamed segments (e.g. a failure diagnostic\n // appended after the stream) — persist the remainder as a trailing\n // id-less segment.\n return [...parts, { type: 'text', text: finalText.slice(joined.length) }]\n }\n\n // Final text replaced the streamed text outright. Keep non-text chronology;\n // collapse text to one authoritative segment at the last text position.\n const lastTextPart = textParts[textParts.length - 1]\n return parts\n .filter((part) => String(part.type ?? '') !== 'text' || part === lastTextPart)\n .map((part) => (part === lastTextPart ? { ...part, text: finalText } : part))\n}\n\nexport function finalizeAssistantParts(\n partOrder: string[],\n partMap: Map<string, JsonRecord>,\n finalText: string,\n): JsonRecord[] {\n // A stream that ended abnormally can leave tool parts `running` — never\n // persist one; collapsing then removes the duplicate/blank text segments an\n // unstable upstream segment identity produced.\n return collapseRedundantTextParts(terminalizeDanglingToolParts(\n assembleAssistantParts(partOrder, partMap, finalText),\n ))\n}\n\nfunction partStatus(part: JsonRecord | undefined): string {\n const state = asRecord(part?.state)\n return String(state?.status ?? part?.status ?? '')\n}\n\n/** Finalizes, then folds each synthetic tool settlement back into `partMap`\n * and returns just those updates — the shape a streaming loop needs to emit\n * closing `message.part.updated` frames for tools the stream never settled. */\nexport function terminalizeDanglingAssistantToolUpdates(\n partOrder: string[],\n partMap: Map<string, JsonRecord>,\n finalText: string,\n): JsonRecord[] {\n const finalizedParts = finalizeAssistantParts(partOrder, partMap, finalText)\n const updates: JsonRecord[] = []\n\n for (const part of finalizedParts) {\n if (String(part.type ?? '') !== 'tool') continue\n\n const key = getPartKey(part)\n const existing = partMap.get(key)\n if (partStatus(existing) !== 'running' || partStatus(part) === 'running') continue\n\n partMap.set(key, mergePersistedPart(existing, part))\n updates.push(part)\n }\n\n return updates\n}\n\nexport function encodeEvent(encoder: TextEncoder, event: StreamEvent): Uint8Array {\n return encoder.encode(`${JSON.stringify(event)}\\n`)\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,99 @@
1
+ // src/chat-routes/stale-turn-lock.ts
2
+ var DEFAULT_STALE_TURN_LOCK_GRACE_MS = 5 * 60 * 1e3;
3
+ var DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS = 60 * 1e3;
4
+ function messageOf(err) {
5
+ return err instanceof Error ? err.message : String(err);
6
+ }
7
+ async function reconcileStaleTurnLock(options) {
8
+ let sandbox;
9
+ try {
10
+ sandbox = await options.probeSandbox();
11
+ } catch (err) {
12
+ return forceReleaseUnreachable(options, {
13
+ unreachableReason: "SANDBOX_PROBE_FAILED",
14
+ unreachableDetail: messageOf(err)
15
+ });
16
+ }
17
+ if (sandbox.status !== "running") {
18
+ return forceReleaseUnreachable(options, {
19
+ unreachableReason: sandbox.status === "absent" ? "SANDBOX_ABSENT" : "SANDBOX_NOT_RUNNING",
20
+ ...sandbox.status === "not-running" && sandbox.state !== void 0 ? { sandboxState: sandbox.state } : {}
21
+ });
22
+ }
23
+ const observedAt = (options.now ?? Date.now)();
24
+ let session;
25
+ try {
26
+ session = await options.probeSession();
27
+ } catch (err) {
28
+ return forceReleaseUnreachable(options, {
29
+ unreachableReason: "SESSION_PROBE_FAILED",
30
+ unreachableDetail: messageOf(err)
31
+ });
32
+ }
33
+ if (!session.reachable) {
34
+ return forceReleaseUnreachable(options, {
35
+ unreachableReason: "SESSION_UNREACHABLE",
36
+ ...session.reason !== void 0 ? { sessionProbeError: session.reason } : {}
37
+ });
38
+ }
39
+ const diagnostics = {
40
+ sandboxReachable: true,
41
+ sessionTerminal: session.terminal,
42
+ ...session.diagnostics ?? {}
43
+ };
44
+ if (!session.terminal) return { released: false, diagnostics };
45
+ const terminalGraceMs = options.terminalGraceMs ?? DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS;
46
+ const lockAgeMs = observedAt - options.lockStartedAt;
47
+ if (lockAgeMs < terminalGraceMs) {
48
+ const log = options.log ?? ((message, meta) => console.warn(message, meta));
49
+ const withheld = {
50
+ ...diagnostics,
51
+ lockAgeMs,
52
+ terminalReleaseGraceMs: terminalGraceMs,
53
+ terminalReleaseWithheld: "LOCK_WITHIN_TERMINAL_GRACE_PERIOD"
54
+ };
55
+ log("[chat-routes] stale turn lock held: terminal verdict but lock is younger than the registration window", {
56
+ ...options.context ?? {},
57
+ ...withheld
58
+ });
59
+ return { released: false, diagnostics: withheld };
60
+ }
61
+ const released = await options.release({ observedAt });
62
+ return {
63
+ released,
64
+ diagnostics: { ...diagnostics, lockAgeMs, terminalReleaseGraceMs: terminalGraceMs, released, observedAt }
65
+ };
66
+ }
67
+ async function forceReleaseUnreachable(options, reason) {
68
+ const log = options.log ?? ((message, meta) => console.warn(message, meta));
69
+ const graceMs = options.graceMs ?? DEFAULT_STALE_TURN_LOCK_GRACE_MS;
70
+ const at = (options.now ?? Date.now)();
71
+ const lockAgeMs = at - options.lockStartedAt;
72
+ const diagnostics = {
73
+ ...reason,
74
+ sandboxReachable: false,
75
+ lockAgeMs,
76
+ forceReleaseGraceMs: graceMs
77
+ };
78
+ if (lockAgeMs < graceMs) {
79
+ log("[chat-routes] stale turn lock held: sandbox unreachable but lock is inside the grace period", {
80
+ ...options.context ?? {},
81
+ ...diagnostics
82
+ });
83
+ return { released: false, diagnostics: { ...diagnostics, forceReleaseWithheld: "LOCK_WITHIN_GRACE_PERIOD" } };
84
+ }
85
+ const released = await options.release({ observedAt: at });
86
+ log("[chat-routes] force-released stale turn lock: sandbox unreachable, no turn can be executing", {
87
+ ...options.context ?? {},
88
+ ...diagnostics,
89
+ released
90
+ });
91
+ return { released, diagnostics: { ...diagnostics, forceReleased: released } };
92
+ }
93
+
94
+ export {
95
+ DEFAULT_STALE_TURN_LOCK_GRACE_MS,
96
+ DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
97
+ reconcileStaleTurnLock
98
+ };
99
+ //# sourceMappingURL=chunk-BQV42AFK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chat-routes/stale-turn-lock.ts"],"sourcesContent":["/**\n * Recovery policy for a `ChatTurnLock` whose holder died.\n *\n * `createChatTurnRoutes` takes the lock as a seam (`acquire`/`release`) and a\n * lock is a single-flight guard: while it is held, a second turn on the same\n * scope is refused. Products give it a TTL measured in tens of minutes, so a\n * turn that dies without releasing wedges chat for that whole window. Every\n * app on the seam inherits that wedge, which is why the way OUT of it is\n * policy this package owns rather than something each app rediscovers.\n *\n * The policy takes PROBES, not clients: it imports no sandbox SDK, opens no\n * connection, and knows nothing about how a product finds its box or talks to\n * a sidecar. That is what makes the rules testable and what keeps the concrete\n * probes — which box key, which session id, which sidecar endpoint — in the\n * product.\n *\n * The rules, in precedence order:\n *\n * 1. The session probe answered and the execution is TERMINAL ⇒ release, once\n * the lock is past a short grace period. The authority on \"is this turn\n * still running\" is whatever is actually running it; a terminal verdict is\n * proof the lock outlived its turn — but only if the verdict is about THIS\n * turn, which is what the grace buys (see\n * {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS}).\n * 2. The session probe answered and the execution is LIVE ⇒ hold, always.\n * Nothing below may override this. The lock is doing exactly its job.\n * 3. The probes could not reach that authority at all — the sandbox could not\n * be listed, is gone, is not running, or its session probe failed ⇒ fall\n * back on the physical argument: an execution runs INSIDE the box, so a box\n * that is not there is running nothing, and the lock is releasable. Without\n * this fallback the recovery would depend on the very subsystem whose\n * failure produced the stale lock.\n *\n * Rule 3 is gated on a grace period because it is an inference, not an\n * observation — see {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}.\n */\n\n/** Where the box is, as far as the caller can see. `state` on `not-running`\n * is the platform's own status string, carried through for the log. */\nexport type StaleTurnLockSandboxProbeResult =\n | { status: 'running' }\n | { status: 'absent' }\n | { status: 'not-running'; state?: string }\n\n/** What the thing running the turn says about it. `terminal: false` means an\n * execution is LIVE — the strongest signal in the policy. `diagnostics` rides\n * through to the result and the logs unread. */\nexport type StaleTurnLockSessionProbeResult =\n | { reachable: true; terminal: boolean; diagnostics?: Record<string, unknown> }\n | { reachable: false; reason?: string }\n\n/**\n * Minimum age a lock must reach before the \"sandbox unreachable ⇒ nothing can\n * be running\" fallback may force-release it.\n *\n * The lock is acquired BEFORE the box is ensured, so during a cold workspace's\n * first turn there is a real window in which the lock is held and no box exists\n * yet — indistinguishable, from a peek, from a box that vanished. The grace\n * period has to outlast that window (create + bootstrap + whatever the product\n * hydrates) or a concurrent request steals the lock from a turn that is merely\n * still provisioning. Five minutes clears observed cold starts with room to\n * spare while cutting the worst case from a TTL-length wedge down to five\n * minutes. Raising it makes recovery slower; lowering it risks stealing a lock\n * mid-provision.\n */\nexport const DEFAULT_STALE_TURN_LOCK_GRACE_MS = 5 * 60 * 1000\n\n/**\n * Minimum age a lock must reach before a TERMINAL session verdict may release\n * it.\n *\n * The session probe is keyed on the THREAD, not on the execution the lock\n * holds: a sidecar that has nothing running reports `terminal` with\n * `activeExecutionId: null`, so there is no id to match the lock against. The\n * lock, meanwhile, is acquired BEFORE the box is ensured and before the\n * execution registers with the sidecar. Between those two moments a second\n * request that reconciles the lock asks the sidecar about a turn it has not\n * heard of yet and gets back the PREVIOUS turn's terminal state — proof about\n * the wrong execution. Releasing on that verdict hands the second request a\n * lock the first one is still using, which is two concurrent turns on a scope\n * whose single-flight guard just voted for itself.\n *\n * One minute covers the acquire → box-ensure → sidecar-registration window on\n * a warm box (the cold-box case is Rule 3's, and has its own, much longer\n * grace). Deliberately NOT\n * {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}: this branch has a positive\n * observation behind it, so it should recover fast, and stretching it to five\n * minutes would leave a genuinely dead turn wedged for the whole window that\n * the session probe exists to shortcut. Raising it delays recovery from a\n * crashed turn; lowering it narrows the registration window it protects.\n */\nexport const DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS = 60 * 1000\n\nexport interface ReconcileStaleTurnLockOptions {\n /** When the held lock was acquired (epoch ms). The grace period is measured\n * from here, so it must be the LOCK's start, not the turn's. */\n lockStartedAt: number\n /** Is the box there and running? Never provisions — a peek, not an ensure.\n * A throw is treated as unreachable, same as `absent`. */\n probeSandbox(): Promise<StaleTurnLockSandboxProbeResult>\n /** Ask the running box whether the execution is still live. Only called when\n * `probeSandbox` reported `running`. A throw is treated as unreachable. */\n probeSession(): Promise<StaleTurnLockSessionProbeResult>\n /** Release the lock, fenced by the instant the releasing evidence was\n * observed. `fence.observedAt` is snapshotted BEFORE the probe that\n * justified the release, so a store that can compare it against the held\n * lock's start refuses to delete a SUCCESSOR lock acquired while the probe\n * was in flight. A store that cannot make that comparison may ignore the\n * fence, but must not substitute its own `Date.now()` — that timestamp is\n * by construction newer than any successor and makes the check vacuous.\n *\n * Returns whether the release actually landed — `false` when the lock was\n * already gone (someone else got there first), which is reported, never\n * treated as a release. */\n release(fence: { observedAt: number }): boolean | Promise<boolean>\n /** Override {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS} (Rule 3's fallback). */\n graceMs?: number\n /** Override {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS} (Rule 1's release). */\n terminalGraceMs?: number\n /** Identity fields merged into every log line (workspace, thread, execution\n * id — whatever makes the entry findable in the product's logs). */\n context?: Record<string, unknown>\n /** Defaults to `console.warn`. Both the withheld and the force-released\n * branches log; a force-release is never silent. */\n log?(message: string, meta: Record<string, unknown>): void\n /** Injectable clock, for tests. */\n now?(): number\n}\n\nexport interface ReconcileStaleTurnLockResult {\n released: boolean\n /** Why the policy decided what it did — the probe's own diagnostics on the\n * reachable path, the unreachable reason and lock age on the fallback. */\n diagnostics: Record<string, unknown>\n}\n\nfunction messageOf(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n\n/**\n * Decide whether a held lock is stale and, if so, release it.\n *\n * Never provisions and never mutates anything but the lock: a reconciliation\n * attempt on a cold workspace leaves it cold.\n */\nexport async function reconcileStaleTurnLock(\n options: ReconcileStaleTurnLockOptions,\n): Promise<ReconcileStaleTurnLockResult> {\n let sandbox: StaleTurnLockSandboxProbeResult\n try {\n sandbox = await options.probeSandbox()\n } catch (err) {\n return forceReleaseUnreachable(options, {\n unreachableReason: 'SANDBOX_PROBE_FAILED',\n unreachableDetail: messageOf(err),\n })\n }\n if (sandbox.status !== 'running') {\n return forceReleaseUnreachable(options, {\n unreachableReason: sandbox.status === 'absent' ? 'SANDBOX_ABSENT' : 'SANDBOX_NOT_RUNNING',\n ...(sandbox.status === 'not-running' && sandbox.state !== undefined\n ? { sandboxState: sandbox.state }\n : {}),\n })\n }\n\n // Snapshot the clock BEFORE the probe and use that one instant for BOTH the\n // grace decision and the release fence: the release is only valid against the\n // lock as it was when the state was observed, not after an arbitrarily slow\n // round trip. Re-reading the clock after the probe would let a slow round\n // trip age the lock past the grace on paper, and would hand the store a fence\n // newer than a successor lock acquired meanwhile.\n const observedAt = (options.now ?? Date.now)()\n let session: StaleTurnLockSessionProbeResult\n try {\n session = await options.probeSession()\n } catch (err) {\n return forceReleaseUnreachable(options, {\n unreachableReason: 'SESSION_PROBE_FAILED',\n unreachableDetail: messageOf(err),\n })\n }\n if (!session.reachable) {\n return forceReleaseUnreachable(options, {\n unreachableReason: 'SESSION_UNREACHABLE',\n ...(session.reason !== undefined ? { sessionProbeError: session.reason } : {}),\n })\n }\n\n const diagnostics: Record<string, unknown> = {\n sandboxReachable: true,\n sessionTerminal: session.terminal,\n ...(session.diagnostics ?? {}),\n }\n // The authority answered and says the execution is live: the lock is doing\n // its job. Nothing below this point may override that.\n if (!session.terminal) return { released: false, diagnostics }\n\n // Terminal, but about WHICH execution? The probe is thread-keyed, so a lock\n // younger than the registration window may be reading the previous turn's\n // verdict — hold until it is old enough that the verdict has to be its own.\n const terminalGraceMs = options.terminalGraceMs ?? DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS\n const lockAgeMs = observedAt - options.lockStartedAt\n if (lockAgeMs < terminalGraceMs) {\n const log = options.log ?? ((message, meta) => console.warn(message, meta))\n const withheld = {\n ...diagnostics,\n lockAgeMs,\n terminalReleaseGraceMs: terminalGraceMs,\n terminalReleaseWithheld: 'LOCK_WITHIN_TERMINAL_GRACE_PERIOD',\n }\n log('[chat-routes] stale turn lock held: terminal verdict but lock is younger than the registration window', {\n ...(options.context ?? {}),\n ...withheld,\n })\n return { released: false, diagnostics: withheld }\n }\n\n const released = await options.release({ observedAt })\n return {\n released,\n diagnostics: { ...diagnostics, lockAgeMs, terminalReleaseGraceMs: terminalGraceMs, released, observedAt },\n }\n}\n\n/**\n * The fallback: the session authority could not attest to the execution's\n * state because the box is gone, stopped, or unreachable. Release the lock\n * once it is old enough that it cannot belong to a turn still provisioning its\n * box — and say so either way, with the reason, the lock's age, and the grace\n * period it was measured against.\n *\n * One clock read (`at`) serves as the age gate AND the release fence here too,\n * for the same reason the terminal branch uses `observedAt` for both.\n */\nasync function forceReleaseUnreachable(\n options: ReconcileStaleTurnLockOptions,\n reason: Record<string, unknown> & { unreachableReason: string },\n): Promise<ReconcileStaleTurnLockResult> {\n const log = options.log ?? ((message, meta) => console.warn(message, meta))\n const graceMs = options.graceMs ?? DEFAULT_STALE_TURN_LOCK_GRACE_MS\n const at = (options.now ?? Date.now)()\n const lockAgeMs = at - options.lockStartedAt\n const diagnostics: Record<string, unknown> = {\n ...reason,\n sandboxReachable: false,\n lockAgeMs,\n forceReleaseGraceMs: graceMs,\n }\n\n if (lockAgeMs < graceMs) {\n log('[chat-routes] stale turn lock held: sandbox unreachable but lock is inside the grace period', {\n ...(options.context ?? {}),\n ...diagnostics,\n })\n return { released: false, diagnostics: { ...diagnostics, forceReleaseWithheld: 'LOCK_WITHIN_GRACE_PERIOD' } }\n }\n\n const released = await options.release({ observedAt: at })\n log('[chat-routes] force-released stale turn lock: sandbox unreachable, no turn can be executing', {\n ...(options.context ?? {}),\n ...diagnostics,\n released,\n })\n return { released, diagnostics: { ...diagnostics, forceReleased: released } }\n}\n"],"mappings":"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","names":[]}
@@ -15,6 +15,22 @@ function chatTurnRequestInit(payload) {
15
15
  };
16
16
  }
17
17
  var INLINE_PARTS_MAX_BYTES = 95e4;
18
+ var DISPATCH_REQUEST_MAX_BYTES = 1024 * 1024;
19
+ var DISPATCH_STRUCTURAL_RESERVE_BYTES = 64 * 1024;
20
+ var DISPATCH_MAX_PARTS = 64;
21
+ var DISPATCH_MAX_MEDIA_PARTS = 24;
22
+ function base64WireLen(byteLen) {
23
+ return Math.ceil(byteLen / 3) * 4;
24
+ }
25
+ function formatBytes(bytes) {
26
+ if (bytes < 1024) return `${bytes}B`;
27
+ if (bytes >= 1024 * 1024) {
28
+ const megabytes = Math.floor(bytes / (1024 * 1024));
29
+ const remainder = bytes % (1024 * 1024);
30
+ return remainder === 0 ? `${megabytes}MB` : `${megabytes}MB ${formatBytes(remainder)}`;
31
+ }
32
+ return `${Math.round(bytes / 1024)}KB`;
33
+ }
18
34
  var ChatTurnInputError = class extends Error {
19
35
  constructor(message, status = 400, code = "INVALID_CHAT_TURN") {
20
36
  super(message);
@@ -270,10 +286,50 @@ function mentionInputToPart(input) {
270
286
  if (typeof input.size === "number" && Number.isFinite(input.size)) part.size = input.size;
271
287
  return part;
272
288
  }
289
+ function attachmentKindForMime(mime) {
290
+ return mime?.startsWith("image/") ? "image" : "file";
291
+ }
292
+ function isChatAttachmentPart(part) {
293
+ if (!part || typeof part !== "object") return false;
294
+ const record = part;
295
+ return (record.type === "image" || record.type === "file") && typeof record.path === "string" && record.path.length > 0;
296
+ }
297
+ function attachmentInputToPart(input) {
298
+ const part = { type: input.kind, path: input.path, name: input.name };
299
+ if (typeof input.size === "number" && Number.isFinite(input.size)) part.size = input.size;
300
+ if (input.mediaType) part.mediaType = input.mediaType;
301
+ return part;
302
+ }
303
+ function attachmentPartsFromMessageParts(parts) {
304
+ if (!parts) return [];
305
+ return parts.filter(isChatAttachmentPart);
306
+ }
307
+ var DEFAULT_ATTACHMENT_PROMPT_HEADER = "Attached files (already saved to the workspace vault \u2014 read them from these paths):";
308
+ var PROMPT_BLOCK_CONTROL_CHARS = /[\x00-\x1F\x7F]/g;
309
+ function buildAttachmentPromptBlock(atts, header = DEFAULT_ATTACHMENT_PROMPT_HEADER) {
310
+ if (atts.length === 0) return "";
311
+ const clean = (value) => value.replace(PROMPT_BLOCK_CONTROL_CHARS, " ");
312
+ const lines = atts.map((a) => `- ${clean(a.name)} (vault: ${clean(a.path)})`);
313
+ return `
314
+
315
+ ${header}
316
+ ${lines.join("\n")}`;
317
+ }
318
+ function historyContentWithAttachments(message, header) {
319
+ const attachmentParts = attachmentPartsFromMessageParts(message.parts);
320
+ if (attachmentParts.length === 0) return message.content;
321
+ return `${message.content}${buildAttachmentPromptBlock(attachmentParts, header)}`;
322
+ }
273
323
 
274
324
  export {
275
325
  chatTurnRequestInit,
276
326
  INLINE_PARTS_MAX_BYTES,
327
+ DISPATCH_REQUEST_MAX_BYTES,
328
+ DISPATCH_STRUCTURAL_RESERVE_BYTES,
329
+ DISPATCH_MAX_PARTS,
330
+ DISPATCH_MAX_MEDIA_PARTS,
331
+ base64WireLen,
332
+ formatBytes,
277
333
  ChatTurnInputError,
278
334
  promptPartsByteSize,
279
335
  assertPromptPartsWithinCap,
@@ -293,6 +349,13 @@ export {
293
349
  isChatStepFinishPart,
294
350
  isChatMentionPart,
295
351
  mentionPartsFromMessageParts,
296
- mentionInputToPart
352
+ mentionInputToPart,
353
+ attachmentKindForMime,
354
+ isChatAttachmentPart,
355
+ attachmentInputToPart,
356
+ attachmentPartsFromMessageParts,
357
+ DEFAULT_ATTACHMENT_PROMPT_HEADER,
358
+ buildAttachmentPromptBlock,
359
+ historyContentWithAttachments
297
360
  };
298
- //# sourceMappingURL=chunk-6E2XJSCT.js.map
361
+ //# sourceMappingURL=chunk-JGYOYY5D.js.map