@tangle-network/agent-app 0.43.42 → 0.43.44

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 (39) hide show
  1. package/dist/assistant/index.d.ts +2 -1
  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 +152 -4
  5. package/dist/chat-routes/index.js +134 -20
  6. package/dist/chat-routes/index.js.map +1 -1
  7. package/dist/chat-store/index.d.ts +2 -2
  8. package/dist/chat-store/index.js +7 -1
  9. package/dist/chat-store/index.js.map +1 -1
  10. package/dist/{chunk-5GWXCSLQ.js → chunk-5RJNEEO2.js} +86 -5
  11. package/dist/chunk-5RJNEEO2.js.map +1 -0
  12. package/dist/chunk-6E2XJSCT.js +298 -0
  13. package/dist/chunk-6E2XJSCT.js.map +1 -0
  14. package/dist/chunk-LCNY3DCM.js +84 -0
  15. package/dist/chunk-LCNY3DCM.js.map +1 -0
  16. package/dist/{chunk-ZLHK25C3.js → chunk-Q4EU6MGU.js} +54 -6
  17. package/dist/chunk-Q4EU6MGU.js.map +1 -0
  18. package/dist/{chunk-UMSOSUEU.js → chunk-QYOD3K56.js} +2 -2
  19. package/dist/chunk-QYOD3K56.js.map +1 -0
  20. package/dist/file-index-Bn6sitKb.d.ts +114 -0
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.js +17 -3
  23. package/dist/object-store/index.d.ts +6 -4
  24. package/dist/object-store/index.js +1 -1
  25. package/dist/parts-1_3y2JmR.d.ts +368 -0
  26. package/dist/sandbox/index.d.ts +111 -2
  27. package/dist/sandbox/index.js +9 -1
  28. package/dist/web-react/index.d.ts +51 -3
  29. package/dist/web-react/index.js +13 -4
  30. package/package.json +1 -1
  31. package/dist/chunk-2EO7CPL3.js +0 -191
  32. package/dist/chunk-2EO7CPL3.js.map +0 -1
  33. package/dist/chunk-5GWXCSLQ.js.map +0 -1
  34. package/dist/chunk-I2ATYB7R.js +0 -78
  35. package/dist/chunk-I2ATYB7R.js.map +0 -1
  36. package/dist/chunk-UMSOSUEU.js.map +0 -1
  37. package/dist/chunk-ZLHK25C3.js.map +0 -1
  38. package/dist/file-index-Bw_IQE_G.d.ts +0 -194
  39. package/dist/parts-DjX0RRTS.d.ts +0 -182
@@ -1,9 +1,9 @@
1
- import { C as ChatTurnRequestPayload, a as ChatTurnPartInput, b as ChatTurnFilePartInput } from '../file-index-Bw_IQE_G.js';
2
- export { c as ChatTurnInputError, d as ChatTurnTextPartInput, e as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, f as FileIndexCache, g as FileIndexReadyResponse, h as FileIndexResponse, i as FileIndexWarmingResponse, j as FileMention, k as FileMentionsToPartsOptions, I as INLINE_PARTS_MAX_BYTES, S as SandboxFileTreeSource, l as SandboxTreeFile, m as SandboxTreeResult, n as assertPromptPartsWithinCap, o as buildMentionPromptBlock, p as chatTurnRequestInit, q as createSandboxFileIndexRoute, r as fileMentionsToParts, s as mediaTypeForMentionPath, t as parseChatTurnParts, u as promptPartsByteSize } from '../file-index-Bw_IQE_G.js';
1
+ import { A as ChatTurnRequestPayload, B as ChatTurnPartInput, e as ChatMessagePart, D as ChatTurnFilePartInput } from '../parts-1_3y2JmR.js';
2
+ export { c as ChatMentionKind, E as ChatTurnInputError, G as ChatTurnTextPartInput, F as FileMention, H as FileMentionsToPartsOptions, I as INLINE_PARTS_MAX_BYTES, M as MENTION_MAX_COUNT, J as SandboxMentionPathCheck, K as assertPromptPartsWithinCap, L as buildMentionPromptBlock, N as chatTurnRequestInit, O as fileMentionsToParts, P as mediaTypeForMentionPath, Q as mentionKindForPath, R as parseChatTurnParts, T as parseFileMentions, U as promptPartsByteSize, V as validateSandboxMentionPath } from '../parts-1_3y2JmR.js';
3
3
  import { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtime';
4
- import { c as ChatMessagePart } from '../parts-DjX0RRTS.js';
5
4
  import { InteractionAnswerRoute, InteractionAnswerRouteOptions } from '../interactions/index.js';
6
5
  import { PersistedChatMessageForTurn, TurnEventStore } from '../stream/index.js';
6
+ export { C as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, a as FileIndexCache, b as FileIndexReadyResponse, c as FileIndexResponse, d as FileIndexWarmingResponse, S as SandboxFileTreeSource, e as SandboxTreeFile, f as SandboxTreeResult, g as createSandboxFileIndexRoute } from '../file-index-Bn6sitKb.js';
7
7
  import '@tangle-network/agent-interface';
8
8
  import '../contract-KfqJh_au.js';
9
9
  import '../plans/index.js';
@@ -318,6 +318,154 @@ interface ChatTurnRoutes {
318
318
  }
319
319
  declare function createChatTurnRoutes<TContext = void>(options: CreateChatTurnRoutesOptions<TContext>): ChatTurnRoutes;
320
320
 
321
+ /**
322
+ * Recovery policy for a `ChatTurnLock` whose holder died.
323
+ *
324
+ * `createChatTurnRoutes` takes the lock as a seam (`acquire`/`release`) and a
325
+ * lock is a single-flight guard: while it is held, a second turn on the same
326
+ * scope is refused. Products give it a TTL measured in tens of minutes, so a
327
+ * turn that dies without releasing wedges chat for that whole window. Every
328
+ * app on the seam inherits that wedge, which is why the way OUT of it is
329
+ * policy this package owns rather than something each app rediscovers.
330
+ *
331
+ * The policy takes PROBES, not clients: it imports no sandbox SDK, opens no
332
+ * connection, and knows nothing about how a product finds its box or talks to
333
+ * a sidecar. That is what makes the rules testable and what keeps the concrete
334
+ * probes — which box key, which session id, which sidecar endpoint — in the
335
+ * product.
336
+ *
337
+ * The rules, in precedence order:
338
+ *
339
+ * 1. The session probe answered and the execution is TERMINAL ⇒ release, once
340
+ * the lock is past a short grace period. The authority on "is this turn
341
+ * still running" is whatever is actually running it; a terminal verdict is
342
+ * proof the lock outlived its turn — but only if the verdict is about THIS
343
+ * turn, which is what the grace buys (see
344
+ * {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS}).
345
+ * 2. The session probe answered and the execution is LIVE ⇒ hold, always.
346
+ * Nothing below may override this. The lock is doing exactly its job.
347
+ * 3. The probes could not reach that authority at all — the sandbox could not
348
+ * be listed, is gone, is not running, or its session probe failed ⇒ fall
349
+ * back on the physical argument: an execution runs INSIDE the box, so a box
350
+ * that is not there is running nothing, and the lock is releasable. Without
351
+ * this fallback the recovery would depend on the very subsystem whose
352
+ * failure produced the stale lock.
353
+ *
354
+ * Rule 3 is gated on a grace period because it is an inference, not an
355
+ * observation — see {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}.
356
+ */
357
+ /** Where the box is, as far as the caller can see. `state` on `not-running`
358
+ * is the platform's own status string, carried through for the log. */
359
+ type StaleTurnLockSandboxProbeResult = {
360
+ status: 'running';
361
+ } | {
362
+ status: 'absent';
363
+ } | {
364
+ status: 'not-running';
365
+ state?: string;
366
+ };
367
+ /** What the thing running the turn says about it. `terminal: false` means an
368
+ * execution is LIVE — the strongest signal in the policy. `diagnostics` rides
369
+ * through to the result and the logs unread. */
370
+ type StaleTurnLockSessionProbeResult = {
371
+ reachable: true;
372
+ terminal: boolean;
373
+ diagnostics?: Record<string, unknown>;
374
+ } | {
375
+ reachable: false;
376
+ reason?: string;
377
+ };
378
+ /**
379
+ * Minimum age a lock must reach before the "sandbox unreachable ⇒ nothing can
380
+ * be running" fallback may force-release it.
381
+ *
382
+ * The lock is acquired BEFORE the box is ensured, so during a cold workspace's
383
+ * first turn there is a real window in which the lock is held and no box exists
384
+ * yet — indistinguishable, from a peek, from a box that vanished. The grace
385
+ * period has to outlast that window (create + bootstrap + whatever the product
386
+ * hydrates) or a concurrent request steals the lock from a turn that is merely
387
+ * still provisioning. Five minutes clears observed cold starts with room to
388
+ * spare while cutting the worst case from a TTL-length wedge down to five
389
+ * minutes. Raising it makes recovery slower; lowering it risks stealing a lock
390
+ * mid-provision.
391
+ */
392
+ declare const DEFAULT_STALE_TURN_LOCK_GRACE_MS: number;
393
+ /**
394
+ * Minimum age a lock must reach before a TERMINAL session verdict may release
395
+ * it.
396
+ *
397
+ * The session probe is keyed on the THREAD, not on the execution the lock
398
+ * holds: a sidecar that has nothing running reports `terminal` with
399
+ * `activeExecutionId: null`, so there is no id to match the lock against. The
400
+ * lock, meanwhile, is acquired BEFORE the box is ensured and before the
401
+ * execution registers with the sidecar. Between those two moments a second
402
+ * request that reconciles the lock asks the sidecar about a turn it has not
403
+ * heard of yet and gets back the PREVIOUS turn's terminal state — proof about
404
+ * the wrong execution. Releasing on that verdict hands the second request a
405
+ * lock the first one is still using, which is two concurrent turns on a scope
406
+ * whose single-flight guard just voted for itself.
407
+ *
408
+ * One minute covers the acquire → box-ensure → sidecar-registration window on
409
+ * a warm box (the cold-box case is Rule 3's, and has its own, much longer
410
+ * grace). Deliberately NOT
411
+ * {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}: this branch has a positive
412
+ * observation behind it, so it should recover fast, and stretching it to five
413
+ * minutes would leave a genuinely dead turn wedged for the whole window that
414
+ * the session probe exists to shortcut. Raising it delays recovery from a
415
+ * crashed turn; lowering it narrows the registration window it protects.
416
+ */
417
+ declare const DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS: number;
418
+ interface ReconcileStaleTurnLockOptions {
419
+ /** When the held lock was acquired (epoch ms). The grace period is measured
420
+ * from here, so it must be the LOCK's start, not the turn's. */
421
+ lockStartedAt: number;
422
+ /** Is the box there and running? Never provisions — a peek, not an ensure.
423
+ * A throw is treated as unreachable, same as `absent`. */
424
+ probeSandbox(): Promise<StaleTurnLockSandboxProbeResult>;
425
+ /** Ask the running box whether the execution is still live. Only called when
426
+ * `probeSandbox` reported `running`. A throw is treated as unreachable. */
427
+ probeSession(): Promise<StaleTurnLockSessionProbeResult>;
428
+ /** Release the lock, fenced by the instant the releasing evidence was
429
+ * observed. `fence.observedAt` is snapshotted BEFORE the probe that
430
+ * justified the release, so a store that can compare it against the held
431
+ * lock's start refuses to delete a SUCCESSOR lock acquired while the probe
432
+ * was in flight. A store that cannot make that comparison may ignore the
433
+ * fence, but must not substitute its own `Date.now()` — that timestamp is
434
+ * by construction newer than any successor and makes the check vacuous.
435
+ *
436
+ * Returns whether the release actually landed — `false` when the lock was
437
+ * already gone (someone else got there first), which is reported, never
438
+ * treated as a release. */
439
+ release(fence: {
440
+ observedAt: number;
441
+ }): boolean | Promise<boolean>;
442
+ /** Override {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS} (Rule 3's fallback). */
443
+ graceMs?: number;
444
+ /** Override {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS} (Rule 1's release). */
445
+ terminalGraceMs?: number;
446
+ /** Identity fields merged into every log line (workspace, thread, execution
447
+ * id — whatever makes the entry findable in the product's logs). */
448
+ context?: Record<string, unknown>;
449
+ /** Defaults to `console.warn`. Both the withheld and the force-released
450
+ * branches log; a force-release is never silent. */
451
+ log?(message: string, meta: Record<string, unknown>): void;
452
+ /** Injectable clock, for tests. */
453
+ now?(): number;
454
+ }
455
+ interface ReconcileStaleTurnLockResult {
456
+ released: boolean;
457
+ /** Why the policy decided what it did — the probe's own diagnostics on the
458
+ * reachable path, the unreachable reason and lock age on the fallback. */
459
+ diagnostics: Record<string, unknown>;
460
+ }
461
+ /**
462
+ * Decide whether a held lock is stale and, if so, release it.
463
+ *
464
+ * Never provisions and never mutates anything but the lock: a reconciliation
465
+ * attempt on a cold workspace leaves it cold.
466
+ */
467
+ declare function reconcileStaleTurnLock(options: ReconcileStaleTurnLockOptions): Promise<ReconcileStaleTurnLockResult>;
468
+
321
469
  /**
322
470
  * Sandbox lane: bridge a raw sandbox event stream (`streamSandboxPrompt`) into
323
471
  * the `ChatTurnProducer` shape agent-runtime's `handleChatTurn` consumes AND
@@ -440,4 +588,4 @@ declare function sanitizeUploadFilename(name: string): string;
440
588
  declare function bytesToBase64(bytes: Uint8Array): string;
441
589
  declare function createUploadRoute(options: CreateUploadRouteOptions): (request: Request) => Promise<Response>;
442
590
 
443
- export { type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, bytesToBase64, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, sanitizeUploadFilename, withDurableChatProjection };
591
+ export { type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, DEFAULT_STALE_TURN_LOCK_GRACE_MS, DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS, type ReconcileStaleTurnLockOptions, type ReconcileStaleTurnLockResult, type SandboxChatProducerOptions, type SandboxUploadSink, type StaleTurnLockSandboxProbeResult, type StaleTurnLockSessionProbeResult, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, bytesToBase64, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, reconcileStaleTurnLock, sanitizeUploadFilename, withDurableChatProjection };
@@ -1,15 +1,6 @@
1
1
  import {
2
- ChatTurnInputError,
3
- INLINE_PARTS_MAX_BYTES,
4
- assertPromptPartsWithinCap,
5
- buildMentionPromptBlock,
6
- chatTurnRequestInit,
7
- createSandboxFileIndexRoute,
8
- fileMentionsToParts,
9
- mediaTypeForMentionPath,
10
- parseChatTurnParts,
11
- promptPartsByteSize
12
- } from "../chunk-2EO7CPL3.js";
2
+ createSandboxFileIndexRoute
3
+ } from "../chunk-LCNY3DCM.js";
13
4
  import {
14
5
  asRecord,
15
6
  asString,
@@ -31,8 +22,22 @@ import {
31
22
  parseJsonObjectBody
32
23
  } from "../chunk-HFC4BTWJ.js";
33
24
  import {
34
- toChatMessageParts
35
- } from "../chunk-I2ATYB7R.js";
25
+ ChatTurnInputError,
26
+ INLINE_PARTS_MAX_BYTES,
27
+ MENTION_MAX_COUNT,
28
+ assertPromptPartsWithinCap,
29
+ buildMentionPromptBlock,
30
+ chatTurnRequestInit,
31
+ fileMentionsToParts,
32
+ mediaTypeForMentionPath,
33
+ mentionInputToPart,
34
+ mentionKindForPath,
35
+ parseChatTurnParts,
36
+ parseFileMentions,
37
+ promptPartsByteSize,
38
+ toChatMessageParts,
39
+ validateSandboxMentionPath
40
+ } from "../chunk-6E2XJSCT.js";
36
41
  import {
37
42
  cancelStatusFor,
38
43
  interactionPartKey,
@@ -64,8 +69,9 @@ function validateTurnBody(body, maxInlinePartBytes) {
64
69
  if (typeof rawContent !== "string") throw new ChatTurnInputError("content must be a string");
65
70
  const content = rawContent.trim();
66
71
  const fileParts = parseChatTurnParts(body.parts);
67
- if (!content && fileParts.length === 0) {
68
- throw new ChatTurnInputError("Missing content (send text, parts, or both)");
72
+ const mentions = parseFileMentions(body.mentions);
73
+ if (!content && fileParts.length === 0 && mentions.length === 0) {
74
+ throw new ChatTurnInputError("Missing content (send text, parts, mentions, or any combination)");
69
75
  }
70
76
  assertPromptPartsWithinCap(fileParts, maxInlinePartBytes);
71
77
  let turnId;
@@ -75,14 +81,22 @@ function validateTurnBody(body, maxInlinePartBytes) {
75
81
  throw new ChatTurnInputError(err instanceof Error ? err.message : "Invalid turnId");
76
82
  }
77
83
  return {
78
- payload: { ...body, threadId, content },
84
+ // The VALIDATED, deduped mention list replaces the raw one on the payload,
85
+ // so every downstream seam (`authorize`, `contextGate`, `beforeTurn`,
86
+ // `produce`) reads checked paths and never the request's own.
87
+ payload: { ...body, threadId, content, mentions },
79
88
  content,
80
89
  fileParts,
90
+ mentions,
81
91
  turnId
82
92
  };
83
93
  }
84
- function userPartsWithFiles(userParts, fileParts) {
85
- return toChatMessageParts([...userParts, ...fileParts.map((part) => ({ ...part }))]);
94
+ function userPartsWithFiles(userParts, fileParts, mentions) {
95
+ return toChatMessageParts([
96
+ ...userParts,
97
+ ...fileParts.map((part) => ({ ...part })),
98
+ ...mentions.map((mention) => ({ ...mentionInputToPart(mention) }))
99
+ ]);
86
100
  }
87
101
  async function* tapRawEvents(source, onRawEvent, log) {
88
102
  for await (const event of source) {
@@ -139,7 +153,7 @@ function createChatTurnRoutes(options) {
139
153
  if (err instanceof ChatTurnInputError) return errorResponse(err);
140
154
  throw err;
141
155
  }
142
- const { payload, content, fileParts, turnId } = parsed;
156
+ const { payload, content, fileParts, mentions, turnId } = parsed;
143
157
  const auth = await options.authorize({ request, intent: "turn", body: payload });
144
158
  if (!auth.ok) return auth.response;
145
159
  const { tenantId, userId, context } = auth;
@@ -241,7 +255,7 @@ function createChatTurnRoutes(options) {
241
255
  threadId: payload.threadId,
242
256
  role: "user",
243
257
  content,
244
- parts: userPartsWithFiles(chatTurn.userParts, fileParts)
258
+ parts: userPartsWithFiles(chatTurn.userParts, fileParts, mentions)
245
259
  });
246
260
  }
247
261
  if (options.contextGate) {
@@ -485,6 +499,99 @@ function concatStreams(streams) {
485
499
  });
486
500
  }
487
501
 
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
+
488
595
  // src/chat-routes/sandbox-producer.ts
489
596
  function textDelta(tracker, key, part, rawDelta) {
490
597
  const explicit = typeof rawDelta === "string" ? rawDelta : void 0;
@@ -828,7 +935,10 @@ function createUploadRoute(options) {
828
935
  }
829
936
  export {
830
937
  ChatTurnInputError,
938
+ DEFAULT_STALE_TURN_LOCK_GRACE_MS,
939
+ DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS,
831
940
  INLINE_PARTS_MAX_BYTES,
941
+ MENTION_MAX_COUNT,
832
942
  UPLOAD_INLINE_MAX_BYTES,
833
943
  UPLOAD_MAX_FILE_BYTES,
834
944
  assertPromptPartsWithinCap,
@@ -841,9 +951,13 @@ export {
841
951
  createUploadRoute,
842
952
  fileMentionsToParts,
843
953
  mediaTypeForMentionPath,
954
+ mentionKindForPath,
844
955
  parseChatTurnParts,
956
+ parseFileMentions,
845
957
  promptPartsByteSize,
958
+ reconcileStaleTurnLock,
846
959
  sanitizeUploadFilename,
960
+ validateSandboxMentionPath,
847
961
  withDurableChatProjection
848
962
  };
849
963
  //# sourceMappingURL=index.js.map