@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
@@ -2,19 +2,17 @@ import {
2
2
  createSandboxFileIndexRoute
3
3
  } from "../chunk-LCNY3DCM.js";
4
4
  import {
5
- asRecord,
6
- asString,
5
+ DEFAULT_STALE_TURN_LOCK_GRACE_MS,
6
+ DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
7
+ reconcileStaleTurnLock
8
+ } from "../chunk-BQV42AFK.js";
9
+ import {
7
10
  coalesceDeltas,
8
11
  createBufferedTurnTap,
9
- finalizeAssistantParts,
10
- getPartKey,
11
- mergePersistedPart,
12
12
  normalizeClientTurnId,
13
- normalizePersistedPart,
14
- normalizeToolEvent,
15
13
  replayTurnEvents,
16
14
  resolveChatTurn
17
- } from "../chunk-4AUQIAYU.js";
15
+ } from "../chunk-23LUHHHN.js";
18
16
  import {
19
17
  createInteractionAnswerRoute
20
18
  } from "../chunk-6KVH5SP5.js";
@@ -23,12 +21,20 @@ import {
23
21
  } from "../chunk-HFC4BTWJ.js";
24
22
  import {
25
23
  ChatTurnInputError,
24
+ DISPATCH_MAX_MEDIA_PARTS,
25
+ DISPATCH_MAX_PARTS,
26
+ DISPATCH_REQUEST_MAX_BYTES,
27
+ DISPATCH_STRUCTURAL_RESERVE_BYTES,
26
28
  INLINE_PARTS_MAX_BYTES,
27
29
  MENTION_MAX_COUNT,
28
30
  assertPromptPartsWithinCap,
31
+ attachmentInputToPart,
32
+ attachmentKindForMime,
33
+ base64WireLen,
29
34
  buildMentionPromptBlock,
30
35
  chatTurnRequestInit,
31
36
  fileMentionsToParts,
37
+ formatBytes,
32
38
  mediaTypeForMentionPath,
33
39
  mentionInputToPart,
34
40
  mentionKindForPath,
@@ -37,7 +43,16 @@ import {
37
43
  promptPartsByteSize,
38
44
  toChatMessageParts,
39
45
  validateSandboxMentionPath
40
- } from "../chunk-6E2XJSCT.js";
46
+ } from "../chunk-JGYOYY5D.js";
47
+ import {
48
+ asRecord,
49
+ asString,
50
+ finalizeAssistantParts,
51
+ getPartKey,
52
+ mergePersistedPart,
53
+ normalizePersistedPart,
54
+ normalizeToolEvent
55
+ } from "../chunk-5MG74GVQ.js";
41
56
  import {
42
57
  cancelStatusFor,
43
58
  interactionPartKey,
@@ -50,6 +65,17 @@ import {
50
65
  parsePlanSubmittedEvent,
51
66
  planToPersistedPart
52
67
  } from "../chunk-SIXYZ2FB.js";
68
+ import {
69
+ flattenHistory,
70
+ readSandboxBinaryBytes,
71
+ statSandboxFileSize
72
+ } from "../chunk-5RJNEEO2.js";
73
+ import "../chunk-E7QYOOON.js";
74
+ import "../chunk-PJC4NXPA.js";
75
+ import "../chunk-7EVZUIHW.js";
76
+ import "../chunk-S5SRJJQG.js";
77
+ import "../chunk-7W5XSTUF.js";
78
+ import "../chunk-MFRCM32T.js";
53
79
 
54
80
  // src/chat-routes/turn-routes.ts
55
81
  import { deriveExecutionId, handleChatTurn } from "@tangle-network/agent-runtime";
@@ -499,99 +525,6 @@ function concatStreams(streams) {
499
525
  });
500
526
  }
501
527
 
502
- // src/chat-routes/stale-turn-lock.ts
503
- var DEFAULT_STALE_TURN_LOCK_GRACE_MS = 5 * 60 * 1e3;
504
- var DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS = 60 * 1e3;
505
- function messageOf(err) {
506
- return err instanceof Error ? err.message : String(err);
507
- }
508
- async function reconcileStaleTurnLock(options) {
509
- let sandbox;
510
- try {
511
- sandbox = await options.probeSandbox();
512
- } catch (err) {
513
- return forceReleaseUnreachable(options, {
514
- unreachableReason: "SANDBOX_PROBE_FAILED",
515
- unreachableDetail: messageOf(err)
516
- });
517
- }
518
- if (sandbox.status !== "running") {
519
- return forceReleaseUnreachable(options, {
520
- unreachableReason: sandbox.status === "absent" ? "SANDBOX_ABSENT" : "SANDBOX_NOT_RUNNING",
521
- ...sandbox.status === "not-running" && sandbox.state !== void 0 ? { sandboxState: sandbox.state } : {}
522
- });
523
- }
524
- const observedAt = (options.now ?? Date.now)();
525
- let session;
526
- try {
527
- session = await options.probeSession();
528
- } catch (err) {
529
- return forceReleaseUnreachable(options, {
530
- unreachableReason: "SESSION_PROBE_FAILED",
531
- unreachableDetail: messageOf(err)
532
- });
533
- }
534
- if (!session.reachable) {
535
- return forceReleaseUnreachable(options, {
536
- unreachableReason: "SESSION_UNREACHABLE",
537
- ...session.reason !== void 0 ? { sessionProbeError: session.reason } : {}
538
- });
539
- }
540
- const diagnostics = {
541
- sandboxReachable: true,
542
- sessionTerminal: session.terminal,
543
- ...session.diagnostics ?? {}
544
- };
545
- if (!session.terminal) return { released: false, diagnostics };
546
- const terminalGraceMs = options.terminalGraceMs ?? DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS;
547
- const lockAgeMs = observedAt - options.lockStartedAt;
548
- if (lockAgeMs < terminalGraceMs) {
549
- const log = options.log ?? ((message, meta) => console.warn(message, meta));
550
- const withheld = {
551
- ...diagnostics,
552
- lockAgeMs,
553
- terminalReleaseGraceMs: terminalGraceMs,
554
- terminalReleaseWithheld: "LOCK_WITHIN_TERMINAL_GRACE_PERIOD"
555
- };
556
- log("[chat-routes] stale turn lock held: terminal verdict but lock is younger than the registration window", {
557
- ...options.context ?? {},
558
- ...withheld
559
- });
560
- return { released: false, diagnostics: withheld };
561
- }
562
- const released = await options.release({ observedAt });
563
- return {
564
- released,
565
- diagnostics: { ...diagnostics, lockAgeMs, terminalReleaseGraceMs: terminalGraceMs, released, observedAt }
566
- };
567
- }
568
- async function forceReleaseUnreachable(options, reason) {
569
- const log = options.log ?? ((message, meta) => console.warn(message, meta));
570
- const graceMs = options.graceMs ?? DEFAULT_STALE_TURN_LOCK_GRACE_MS;
571
- const at = (options.now ?? Date.now)();
572
- const lockAgeMs = at - options.lockStartedAt;
573
- const diagnostics = {
574
- ...reason,
575
- sandboxReachable: false,
576
- lockAgeMs,
577
- forceReleaseGraceMs: graceMs
578
- };
579
- if (lockAgeMs < graceMs) {
580
- log("[chat-routes] stale turn lock held: sandbox unreachable but lock is inside the grace period", {
581
- ...options.context ?? {},
582
- ...diagnostics
583
- });
584
- return { released: false, diagnostics: { ...diagnostics, forceReleaseWithheld: "LOCK_WITHIN_GRACE_PERIOD" } };
585
- }
586
- const released = await options.release({ observedAt: at });
587
- log("[chat-routes] force-released stale turn lock: sandbox unreachable, no turn can be executing", {
588
- ...options.context ?? {},
589
- ...diagnostics,
590
- released
591
- });
592
- return { released, diagnostics: { ...diagnostics, forceReleased: released } };
593
- }
594
-
595
528
  // src/chat-routes/sandbox-producer.ts
596
529
  function textDelta(tracker, key, part, rawDelta) {
597
530
  const explicit = typeof rawDelta === "string" ? rawDelta : void 0;
@@ -640,6 +573,7 @@ function createSandboxChatProducer(options) {
640
573
  const announcedTools = /* @__PURE__ */ new Set();
641
574
  const settledTools = /* @__PURE__ */ new Set();
642
575
  let stepCounter = 0;
576
+ const promotedFileParts = /* @__PURE__ */ new Map();
643
577
  function recordPersistedPart(part, delta, keyOverride) {
644
578
  const persisted = normalizePersistedPart(part);
645
579
  if (!persisted) return;
@@ -711,6 +645,33 @@ function createSandboxChatProducer(options) {
711
645
  recordPersistedPart(part, void 0, `step-start:#${stepCounter}`);
712
646
  continue;
713
647
  }
648
+ if (partType === "file" && options.promoteFilePart) {
649
+ const promote = options.promoteFilePart;
650
+ const rawId = asString(part.id);
651
+ const rawUrl = asString(part.url);
652
+ const memoKey = rawId ? `id:${rawId}` : rawUrl ? `url:${rawUrl}` : void 0;
653
+ const attempt = () => Promise.resolve().then(() => promote(part)).catch((err) => {
654
+ const reason = err instanceof Error ? err.message : String(err);
655
+ log("[chat-routes] file part promotion threw", { key: memoKey ?? "(keyless)", error: reason });
656
+ return { succeeded: false, reason };
657
+ });
658
+ let pending;
659
+ if (memoKey) {
660
+ pending = promotedFileParts.get(memoKey) ?? attempt();
661
+ promotedFileParts.set(memoKey, pending);
662
+ } else {
663
+ pending = attempt();
664
+ }
665
+ const outcome = await pending;
666
+ if (outcome.succeeded) {
667
+ recordPersistedPart(outcome.part, void 0, outcome.key);
668
+ } else if (outcome.part) {
669
+ recordPersistedPart(outcome.part, void 0, outcome.key);
670
+ } else {
671
+ recordPersistedPart(part, void 0);
672
+ }
673
+ continue;
674
+ }
714
675
  recordPersistedPart(part, void 0);
715
676
  continue;
716
677
  }
@@ -933,15 +894,459 @@ function createUploadRoute(options) {
933
894
  return Response.json({ files: uploaded });
934
895
  };
935
896
  }
897
+
898
+ // src/chat-routes/resolve-attachments.ts
899
+ var ATTACHMENT_MAX_COUNT = 10;
900
+ var MAX_ATTACHMENT_TOTAL_BYTES = 25 * 1024 * 1024;
901
+ var MAX_ATTACHMENT_NAME_LENGTH = 256;
902
+ function attachmentTotalSizeErrorMessage(totalBytes, limitBytes) {
903
+ return `Attachments total ${formatBytes(totalBytes)}; each message is limited to ${formatBytes(limitBytes)}`;
904
+ }
905
+ function isAttachmentKind(value) {
906
+ return value === "image" || value === "file";
907
+ }
908
+ var CONTROL_CHARS = /[\x00-\x1F\x7F]/;
909
+ function defaultValidateAttachmentPath(path) {
910
+ if (path.includes("\0")) return { succeeded: false, error: "attachment path must not contain null bytes" };
911
+ if (path.includes("\\")) return { succeeded: false, error: "attachment path must not contain backslashes" };
912
+ if (CONTROL_CHARS.test(path)) return { succeeded: false, error: "attachment path must not contain control characters" };
913
+ if (path.startsWith("/")) return { succeeded: false, error: "attachment path must be store-relative, not absolute" };
914
+ const segments = path.split("/");
915
+ if (segments.some((segment) => segment === "..")) {
916
+ return { succeeded: false, error: 'attachment path must not contain ".." segments' };
917
+ }
918
+ if (segments.some((segment) => segment.startsWith("."))) {
919
+ return { succeeded: false, error: "attachment path must not contain a hidden (dotfile) segment" };
920
+ }
921
+ return { succeeded: true };
922
+ }
923
+ function parseAttachmentInput(value, index, validatePath) {
924
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
925
+ return { succeeded: false, error: `attachments[${index}] must be an object` };
926
+ }
927
+ const record = value;
928
+ const path = record.path;
929
+ if (typeof path !== "string" || !path) {
930
+ return { succeeded: false, error: `attachments[${index}].path must be a non-empty string` };
931
+ }
932
+ const name = record.name;
933
+ if (typeof name !== "string" || !name.trim()) {
934
+ return { succeeded: false, error: `attachments[${index}].name must be a non-empty string` };
935
+ }
936
+ if (name.length > MAX_ATTACHMENT_NAME_LENGTH) {
937
+ return { succeeded: false, error: `attachments[${index}].name must not exceed ${MAX_ATTACHMENT_NAME_LENGTH} characters` };
938
+ }
939
+ if (CONTROL_CHARS.test(name)) {
940
+ return { succeeded: false, error: `attachments[${index}].name must not contain control characters` };
941
+ }
942
+ const size = record.size;
943
+ if (typeof size !== "number" || !Number.isFinite(size)) {
944
+ return { succeeded: false, error: `attachments[${index}].size must be a finite number` };
945
+ }
946
+ if (size < 0) {
947
+ return { succeeded: false, error: `attachments[${index}].size must not be negative` };
948
+ }
949
+ const mediaType = record.mediaType;
950
+ if (typeof mediaType !== "string") {
951
+ return { succeeded: false, error: `attachments[${index}].mediaType must be a string` };
952
+ }
953
+ const kind = record.kind;
954
+ if (!isAttachmentKind(kind)) {
955
+ return { succeeded: false, error: `attachments[${index}].kind must be "image" or "file"` };
956
+ }
957
+ const pathCheck = validatePath(path);
958
+ if (!pathCheck.succeeded) return { succeeded: false, error: pathCheck.error };
959
+ return { succeeded: true, value: { path, name, size, mediaType, kind } };
960
+ }
961
+ async function resolveChatAttachments(value, options) {
962
+ const maxCount = options.maxCount ?? ATTACHMENT_MAX_COUNT;
963
+ const maxTotalBytes = options.maxTotalBytes ?? MAX_ATTACHMENT_TOTAL_BYTES;
964
+ const validatePath = options.validatePath ?? defaultValidateAttachmentPath;
965
+ if (value === void 0 || value === null) return { succeeded: true, value: [] };
966
+ if (!Array.isArray(value)) return { succeeded: false, error: "attachments must be an array" };
967
+ if (value.length > maxCount) {
968
+ return { succeeded: false, error: `attachments must not exceed ${maxCount} entries` };
969
+ }
970
+ const inputs = [];
971
+ const seenPaths = /* @__PURE__ */ new Set();
972
+ for (let index = 0; index < value.length; index += 1) {
973
+ const parsed = parseAttachmentInput(value[index], index, validatePath);
974
+ if (!parsed.succeeded) return parsed;
975
+ if (seenPaths.has(parsed.value.path)) {
976
+ return { succeeded: false, error: `attachments must not repeat a path: ${parsed.value.path}` };
977
+ }
978
+ seenPaths.add(parsed.value.path);
979
+ inputs.push(parsed.value);
980
+ }
981
+ const advisoryTotal = inputs.reduce((sum, input) => sum + input.size, 0);
982
+ if (advisoryTotal > maxTotalBytes) {
983
+ return { succeeded: false, error: attachmentTotalSizeErrorMessage(advisoryTotal, maxTotalBytes) };
984
+ }
985
+ let totalStoredBytes = 0;
986
+ for (const input of inputs) {
987
+ const read = await options.readAttachment(options.scopeId, input.path);
988
+ if (!read.ok) return { succeeded: false, error: read.reason };
989
+ totalStoredBytes += read.size;
990
+ if (totalStoredBytes > maxTotalBytes) {
991
+ return { succeeded: false, error: attachmentTotalSizeErrorMessage(totalStoredBytes, maxTotalBytes) };
992
+ }
993
+ input.size = read.size;
994
+ }
995
+ return { succeeded: true, value: inputs.map(attachmentInputToPart) };
996
+ }
997
+
998
+ // src/chat-routes/dispatch-parts.ts
999
+ function byteLen(value) {
1000
+ return new TextEncoder().encode(value).length;
1001
+ }
1002
+ async function readSandboxMention(box, absolutePath, options) {
1003
+ const stat = await statSandboxFileSize(box, absolutePath);
1004
+ if (!stat.succeeded) {
1005
+ return { succeeded: false, error: `mentioned sandbox file missing or unreadable: ${absolutePath} \u2014 ${stat.error}` };
1006
+ }
1007
+ if (!options.readBytes) return { succeeded: true, value: { size: stat.value } };
1008
+ const read = await readSandboxBinaryBytes(box, absolutePath, stat.value);
1009
+ if (!read.succeeded) {
1010
+ return { succeeded: false, error: `mentioned sandbox file read failed: ${absolutePath} \u2014 ${read.error}` };
1011
+ }
1012
+ return { succeeded: true, value: { size: stat.value, base64: bytesToBase64(read.value.bytes) } };
1013
+ }
1014
+ function violatesUrlPathXor(part) {
1015
+ if (part.type === "text") return false;
1016
+ const hasUrl = typeof part.url === "string" && part.url.startsWith("data:");
1017
+ const hasPath = typeof part.path === "string" && part.path.startsWith("/");
1018
+ return hasUrl === hasPath;
1019
+ }
1020
+ function readResultToBase64(read) {
1021
+ if (typeof read.base64 === "string") return read.base64;
1022
+ if (read.bytes) return bytesToBase64(read.bytes);
1023
+ return void 0;
1024
+ }
1025
+ async function buildDispatchParts(input) {
1026
+ const readMention = input.readSandboxMention ?? readSandboxMention;
1027
+ const resolveMentionPath = input.resolveMentionPath ?? input.resolveAttachmentPath;
1028
+ const forcePath = input.forcePath ?? false;
1029
+ const mentions = input.mentions ?? [];
1030
+ const requestMaxBytes = input.requestMaxBytes ?? DISPATCH_REQUEST_MAX_BYTES;
1031
+ const structuralReserveBytes = input.structuralReserveBytes ?? DISPATCH_STRUCTURAL_RESERVE_BYTES;
1032
+ const maxParts = input.maxParts ?? DISPATCH_MAX_PARTS;
1033
+ const parts = [{ type: "text", text: input.text }];
1034
+ const emittedAbsPaths = /* @__PURE__ */ new Set();
1035
+ const flattenedForSizing = flattenHistory(input.text, input.history);
1036
+ const inlineBudget = requestMaxBytes - base64WireLen(byteLen(flattenedForSizing)) - byteLen(JSON.stringify(input.systemPrompt)) - input.profileWireBytes - structuralReserveBytes;
1037
+ let runningInline = 0;
1038
+ for (const attachment of input.attachments) {
1039
+ if (!attachment.path) {
1040
+ return { succeeded: false, error: `attachment path must be non-empty: ${attachment.name}` };
1041
+ }
1042
+ let read;
1043
+ try {
1044
+ read = await input.readAttachment(input.scopeId, attachment.path);
1045
+ } catch (err) {
1046
+ return {
1047
+ succeeded: false,
1048
+ error: `attachment store read failed: ${attachment.path} \u2014 ${err instanceof Error ? err.message : String(err)}`
1049
+ };
1050
+ }
1051
+ if (!read.ok) return { succeeded: false, error: read.reason };
1052
+ const base64 = readResultToBase64(read);
1053
+ if (base64 === void 0) {
1054
+ return { succeeded: false, error: `attachment store read produced no content: ${attachment.path}` };
1055
+ }
1056
+ const mediaType = attachment.mediaType ?? read.mediaType;
1057
+ if (attachment.type === "image" && !mediaType) {
1058
+ return { succeeded: false, error: `attachment is missing a mediaType required for an image data URI: ${attachment.path}` };
1059
+ }
1060
+ const absPath = input.resolveAttachmentPath(attachment.path);
1061
+ emittedAbsPaths.add(absPath);
1062
+ if (attachment.type === "image") {
1063
+ const inlinePart2 = {
1064
+ type: "image",
1065
+ filename: attachment.name,
1066
+ mediaType,
1067
+ url: `data:${mediaType};base64,${base64}`
1068
+ };
1069
+ const cost2 = byteLen(JSON.stringify(inlinePart2));
1070
+ if (!forcePath && runningInline + cost2 <= inlineBudget) {
1071
+ parts.push(inlinePart2);
1072
+ runningInline += cost2;
1073
+ } else {
1074
+ parts.push({ type: "image", filename: attachment.name, mediaType, path: absPath });
1075
+ }
1076
+ continue;
1077
+ }
1078
+ const fileMediaType = mediaType ?? "application/octet-stream";
1079
+ const inlinePart = {
1080
+ type: "file",
1081
+ filename: attachment.name,
1082
+ mediaType: fileMediaType,
1083
+ url: `data:${fileMediaType};base64,${base64}`
1084
+ };
1085
+ const cost = byteLen(JSON.stringify(inlinePart));
1086
+ if (!forcePath && runningInline + cost <= inlineBudget) {
1087
+ parts.push(inlinePart);
1088
+ runningInline += cost;
1089
+ } else {
1090
+ parts.push({ type: "file", path: absPath });
1091
+ }
1092
+ }
1093
+ if (mentions.length > 0 && !input.box) {
1094
+ return { succeeded: false, error: "internal error: sandbox mentions require a box to read from" };
1095
+ }
1096
+ for (const mention of mentions) {
1097
+ if (!mention.path) {
1098
+ return { succeeded: false, error: `mention path must be non-empty: ${mention.name}` };
1099
+ }
1100
+ const absPath = resolveMentionPath(mention.path);
1101
+ if (emittedAbsPaths.has(absPath)) continue;
1102
+ emittedAbsPaths.add(absPath);
1103
+ const isImage = mention.mentionKind === "image";
1104
+ const mediaType = isImage ? mediaTypeForMentionPath(mention.path) : void 0;
1105
+ let stat;
1106
+ try {
1107
+ stat = await readMention(input.box, absPath, { readBytes: false });
1108
+ } catch (err) {
1109
+ return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
1110
+ }
1111
+ if (!stat.succeeded) return { succeeded: false, error: stat.error };
1112
+ const projectedInlineCost = base64WireLen(stat.value.size) + byteLen(JSON.stringify({ type: "image", filename: mention.name, mediaType: mediaType ?? "", url: "" }));
1113
+ if (isImage && mediaType && !forcePath && runningInline + projectedInlineCost <= inlineBudget) {
1114
+ let read;
1115
+ try {
1116
+ read = await readMention(input.box, absPath, { readBytes: true });
1117
+ } catch (err) {
1118
+ return { succeeded: false, error: `mention read failed: ${absPath} \u2014 ${err instanceof Error ? err.message : String(err)}` };
1119
+ }
1120
+ if (!read.succeeded) return { succeeded: false, error: read.error };
1121
+ if (!read.value.base64) return { succeeded: false, error: `mentioned image produced no bytes: ${absPath}` };
1122
+ const inlinePart = {
1123
+ type: "image",
1124
+ filename: mention.name,
1125
+ mediaType,
1126
+ url: `data:${mediaType};base64,${read.value.base64}`
1127
+ };
1128
+ const cost = byteLen(JSON.stringify(inlinePart));
1129
+ if (runningInline + cost <= inlineBudget) {
1130
+ parts.push(inlinePart);
1131
+ runningInline += cost;
1132
+ continue;
1133
+ }
1134
+ }
1135
+ parts.push(
1136
+ isImage && mediaType ? { type: "image", filename: mention.name, mediaType, path: absPath } : { type: "file", path: absPath }
1137
+ );
1138
+ }
1139
+ for (const part of parts) {
1140
+ if (violatesUrlPathXor(part)) {
1141
+ return { succeeded: false, error: "internal error: emitted media part violates the url/path exclusivity invariant" };
1142
+ }
1143
+ }
1144
+ const textPartSize = base64WireLen(byteLen(flattenedForSizing));
1145
+ const mediaPartsSize = parts.slice(1).reduce((total, part) => total + byteLen(JSON.stringify(part)), 0);
1146
+ const systemPromptSize = byteLen(JSON.stringify(input.systemPrompt));
1147
+ if (textPartSize + mediaPartsSize + systemPromptSize + input.profileWireBytes + structuralReserveBytes > requestMaxBytes) {
1148
+ return { succeeded: false, error: "dispatch parts exceed the sandbox proxy request cap even after path demotion" };
1149
+ }
1150
+ if (parts.length > maxParts) {
1151
+ return { succeeded: false, error: `dispatch parts exceed the sidecar per-request cap of ${maxParts}` };
1152
+ }
1153
+ return { succeeded: true, value: parts };
1154
+ }
1155
+
1156
+ // src/chat-routes/promote-file-part.ts
1157
+ var PROMOTE_MAX_FILE_BYTES = 10 * 1024 * 1024;
1158
+ var EXT_TO_MIME = {
1159
+ md: "text/markdown",
1160
+ markdown: "text/markdown",
1161
+ txt: "text/plain",
1162
+ log: "text/plain",
1163
+ csv: "text/csv",
1164
+ tsv: "text/tab-separated-values",
1165
+ json: "application/json",
1166
+ yaml: "text/yaml",
1167
+ yml: "text/yaml",
1168
+ xml: "application/xml",
1169
+ html: "text/html",
1170
+ htm: "text/html",
1171
+ pdf: "application/pdf",
1172
+ png: "image/png",
1173
+ jpg: "image/jpeg",
1174
+ jpeg: "image/jpeg",
1175
+ gif: "image/gif",
1176
+ webp: "image/webp",
1177
+ avif: "image/avif",
1178
+ heic: "image/heic",
1179
+ heif: "image/heif",
1180
+ svg: "image/svg+xml",
1181
+ mp4: "video/mp4",
1182
+ webm: "video/webm",
1183
+ mov: "video/quicktime",
1184
+ mp3: "audio/mpeg",
1185
+ wav: "audio/wav",
1186
+ ogg: "audio/ogg",
1187
+ m4a: "audio/mp4",
1188
+ aac: "audio/aac"
1189
+ };
1190
+ function sniffMimeFromName(filename) {
1191
+ const ext = filename.split(".").pop()?.toLowerCase();
1192
+ if (!ext) return "text/plain";
1193
+ return EXT_TO_MIME[ext] ?? "text/plain";
1194
+ }
1195
+ function sanitizeAttachmentFileName(name) {
1196
+ const sanitized = name.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
1197
+ return sanitized || "file";
1198
+ }
1199
+ function base64ToBytes(base64) {
1200
+ const binary = atob(base64);
1201
+ const bytes = new Uint8Array(binary.length);
1202
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
1203
+ return bytes;
1204
+ }
1205
+ function parseDataUrl(url) {
1206
+ const match = /^data:[^,]*,([\s\S]*)$/.exec(url);
1207
+ if (!match) return null;
1208
+ return { base64: /;base64,/i.test(url), data: match[1] ?? "" };
1209
+ }
1210
+ function dataUrlMime(url) {
1211
+ if (!url) return void 0;
1212
+ const match = /^data:([^;,]+)[;,]/.exec(url);
1213
+ return match ? match[1] : void 0;
1214
+ }
1215
+ function basenameFromUrl(url) {
1216
+ if (!url || url.startsWith("data:")) return void 0;
1217
+ const withoutQuery = url.split(/[?#]/)[0] ?? url;
1218
+ const segments = withoutQuery.split("/").filter(Boolean);
1219
+ return segments[segments.length - 1] || void 0;
1220
+ }
1221
+ function resolveFileUrlPath(url) {
1222
+ const withoutScheme = url.startsWith("file://") ? url.slice("file://".length) : url;
1223
+ try {
1224
+ return { succeeded: true, path: decodeURIComponent(withoutScheme) };
1225
+ } catch (err) {
1226
+ return { succeeded: false, reason: `malformed file path: ${err instanceof Error ? err.message : String(err)}` };
1227
+ }
1228
+ }
1229
+ function oversizeReason(filename, actual, limit) {
1230
+ return `${filename} is ${formatBytes(actual)}; attachments are limited to ${formatBytes(limit)}`;
1231
+ }
1232
+ function resolveDataUrlBytes(url, filename, maxBytes) {
1233
+ const parsed = parseDataUrl(url);
1234
+ if (!parsed) return { succeeded: false, reason: "malformed data URI" };
1235
+ let bytes;
1236
+ try {
1237
+ bytes = parsed.base64 ? base64ToBytes(parsed.data) : new TextEncoder().encode(decodeURIComponent(parsed.data));
1238
+ } catch (err) {
1239
+ return { succeeded: false, reason: `failed to decode data URI: ${err instanceof Error ? err.message : String(err)}` };
1240
+ }
1241
+ if (bytes.byteLength > maxBytes) {
1242
+ return { succeeded: false, reason: oversizeReason(filename, bytes.byteLength, maxBytes) };
1243
+ }
1244
+ return { succeeded: true, bytes };
1245
+ }
1246
+ async function resolveSandboxFileBytes(input) {
1247
+ const stat = await statSandboxFileSize(input.box, input.path, { sessionId: input.sessionId });
1248
+ if (!stat.succeeded) {
1249
+ return { succeeded: false, reason: `could not stat agent file: ${stat.error}` };
1250
+ }
1251
+ if (stat.value > input.maxBytes) {
1252
+ return { succeeded: false, reason: oversizeReason(input.filename, stat.value, input.maxBytes) };
1253
+ }
1254
+ const read = await readSandboxBinaryBytes(input.box, input.path, stat.value, { sessionId: input.sessionId });
1255
+ if (!read.succeeded) {
1256
+ return { succeeded: false, reason: `could not read agent file: ${read.error}` };
1257
+ }
1258
+ return { succeeded: true, bytes: read.value.bytes };
1259
+ }
1260
+ async function resolveBytes(input) {
1261
+ const url = input.raw.url;
1262
+ if (!url) return { succeeded: false, reason: "the file part carries no url" };
1263
+ if (url.startsWith("data:")) return resolveDataUrlBytes(url, input.filename, input.maxBytes);
1264
+ const isSandboxPath = url.startsWith("file://") || url.startsWith("/");
1265
+ if (!isSandboxPath) return { succeeded: false, reason: `unsupported file URL scheme: ${url}` };
1266
+ if (!input.box) return { succeeded: false, reason: "no sandbox to read agent file" };
1267
+ const resolvedPath = resolveFileUrlPath(url);
1268
+ if (!resolvedPath.succeeded) return resolvedPath;
1269
+ return resolveSandboxFileBytes({
1270
+ path: resolvedPath.path,
1271
+ box: input.box,
1272
+ sessionId: input.sessionId,
1273
+ filename: input.filename,
1274
+ maxBytes: input.maxBytes
1275
+ });
1276
+ }
1277
+ async function hash8(seed) {
1278
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed));
1279
+ return Array.from(new Uint8Array(digest).slice(0, 4)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
1280
+ }
1281
+ function defaultBuildAttachmentPath(args) {
1282
+ const extensionMatch = /\.[A-Za-z0-9]+$/.exec(args.filename);
1283
+ const extension = extensionMatch ? extensionMatch[0] : "";
1284
+ const base = extension ? args.filename.slice(0, -extension.length) : args.filename;
1285
+ return `uploads/agent/${args.date}/${base}-${args.hash8}${extension}`;
1286
+ }
1287
+ async function promoteAgentFilePart(options) {
1288
+ const maxBytes = options.maxBytes ?? PROMOTE_MAX_FILE_BYTES;
1289
+ const sniffMime = options.sniffMime ?? sniffMimeFromName;
1290
+ const buildAttachmentPath = options.buildAttachmentPath ?? defaultBuildAttachmentPath;
1291
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
1292
+ const filename = sanitizeAttachmentFileName(
1293
+ options.raw.filename ?? basenameFromUrl(options.raw.url) ?? "agent-file"
1294
+ );
1295
+ const resolved = await resolveBytes({
1296
+ raw: options.raw,
1297
+ box: options.box,
1298
+ sessionId: options.sessionId,
1299
+ filename,
1300
+ maxBytes
1301
+ });
1302
+ if (!resolved.succeeded) return { succeeded: false, filename, reason: resolved.reason };
1303
+ const mediaType = options.raw.mediaType ?? options.raw.mime ?? dataUrlMime(options.raw.url) ?? sniffMime(filename);
1304
+ const kind = attachmentKindForMime(mediaType);
1305
+ const digest = await hash8(options.raw.id ?? options.raw.url ?? filename);
1306
+ const date = now().toISOString().split("T")[0] ?? "";
1307
+ const path = buildAttachmentPath({ filename, hash8: digest, date, mediaType, kind });
1308
+ let written;
1309
+ try {
1310
+ written = await options.writeAttachment(options.scopeId, path, resolved.bytes, {
1311
+ mediaType,
1312
+ name: filename,
1313
+ originalName: options.raw.filename ?? filename,
1314
+ size: resolved.bytes.byteLength
1315
+ });
1316
+ } catch (err) {
1317
+ return { succeeded: false, filename, reason: err instanceof Error ? err.message : String(err) };
1318
+ }
1319
+ if (!written.ok) return { succeeded: false, filename, reason: written.reason };
1320
+ return {
1321
+ succeeded: true,
1322
+ part: {
1323
+ type: kind,
1324
+ path,
1325
+ name: filename,
1326
+ size: resolved.bytes.byteLength,
1327
+ mediaType
1328
+ }
1329
+ };
1330
+ }
936
1331
  export {
1332
+ ATTACHMENT_MAX_COUNT,
937
1333
  ChatTurnInputError,
938
1334
  DEFAULT_STALE_TURN_LOCK_GRACE_MS,
939
1335
  DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
1336
+ DISPATCH_MAX_MEDIA_PARTS,
1337
+ DISPATCH_MAX_PARTS,
1338
+ DISPATCH_REQUEST_MAX_BYTES,
1339
+ DISPATCH_STRUCTURAL_RESERVE_BYTES,
940
1340
  INLINE_PARTS_MAX_BYTES,
1341
+ MAX_ATTACHMENT_TOTAL_BYTES,
941
1342
  MENTION_MAX_COUNT,
1343
+ PROMOTE_MAX_FILE_BYTES,
942
1344
  UPLOAD_INLINE_MAX_BYTES,
943
1345
  UPLOAD_MAX_FILE_BYTES,
944
1346
  assertPromptPartsWithinCap,
1347
+ attachmentTotalSizeErrorMessage,
1348
+ base64WireLen,
1349
+ buildDispatchParts,
945
1350
  buildMentionPromptBlock,
946
1351
  bytesToBase64,
947
1352
  chatTurnRequestInit,
@@ -949,14 +1354,19 @@ export {
949
1354
  createSandboxChatProducer,
950
1355
  createSandboxFileIndexRoute,
951
1356
  createUploadRoute,
1357
+ defaultValidateAttachmentPath,
952
1358
  fileMentionsToParts,
1359
+ formatBytes,
953
1360
  mediaTypeForMentionPath,
954
1361
  mentionKindForPath,
955
1362
  parseChatTurnParts,
956
1363
  parseFileMentions,
1364
+ promoteAgentFilePart,
957
1365
  promptPartsByteSize,
958
1366
  reconcileStaleTurnLock,
1367
+ resolveChatAttachments,
959
1368
  sanitizeUploadFilename,
1369
+ sniffMimeFromName,
960
1370
  validateSandboxMentionPath,
961
1371
  withDurableChatProjection
962
1372
  };