@tangle-network/agent-app 0.44.7 → 0.44.9

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 (35) hide show
  1. package/dist/assistant/index.d.ts +4 -4
  2. package/dist/assistant/index.js +6 -6
  3. package/dist/{attachment-validation-DRuLEjAE.d.ts → attachment-validation-yJ6Id4pR.d.ts} +1 -1
  4. package/dist/chat-routes/index.d.ts +4 -4
  5. package/dist/chat-routes/index.js +3 -3
  6. package/dist/chat-store/index.d.ts +3 -3
  7. package/dist/chat-store/index.js +2 -2
  8. package/dist/{chunk-V55WJSR4.js → chunk-5GA3MKOC.js} +2 -2
  9. package/dist/{chunk-UDSY2F6N.js → chunk-72TPRENZ.js} +21 -8
  10. package/dist/chunk-72TPRENZ.js.map +1 -0
  11. package/dist/{chunk-AFNTRJQ7.js → chunk-FEUW4G2L.js} +2 -2
  12. package/dist/{chunk-TKRI463Z.js → chunk-J6TNPRQN.js} +4 -4
  13. package/dist/{chunk-F2CBC4DY.js → chunk-V3HO43PG.js} +15 -1
  14. package/dist/chunk-V3HO43PG.js.map +1 -0
  15. package/dist/{chunk-UP33Z633.js → chunk-ZRSUCTST.js} +2 -2
  16. package/dist/{parts-Co_lFgLI.d.ts → parts-DIKcC1p6.d.ts} +1 -1
  17. package/dist/{queue-CD5-_VTX.d.ts → queue-DBkulxW1.d.ts} +1 -1
  18. package/dist/turn-health/index.d.ts +57 -1
  19. package/dist/turn-health/index.js +14 -4
  20. package/dist/turn-health/index.js.map +1 -1
  21. package/dist/{types-XIZqIdX1.d.ts → types-DeEOhQbv.d.ts} +25 -3
  22. package/dist/web-react/index.d.ts +6 -6
  23. package/dist/web-react/index.js +6 -6
  24. package/dist/work-product/index.d.ts +73 -5
  25. package/dist/work-product/index.js +131 -17
  26. package/dist/work-product/index.js.map +1 -1
  27. package/dist/work-product-react/index.d.ts +1 -1
  28. package/dist/work-product-react/index.js +3 -3
  29. package/package.json +1 -1
  30. package/dist/chunk-F2CBC4DY.js.map +0 -1
  31. package/dist/chunk-UDSY2F6N.js.map +0 -1
  32. /package/dist/{chunk-V55WJSR4.js.map → chunk-5GA3MKOC.js.map} +0 -0
  33. /package/dist/{chunk-AFNTRJQ7.js.map → chunk-FEUW4G2L.js.map} +0 -0
  34. /package/dist/{chunk-TKRI463Z.js.map → chunk-J6TNPRQN.js.map} +0 -0
  35. /package/dist/{chunk-UP33Z633.js.map → chunk-ZRSUCTST.js.map} +0 -0
@@ -4,15 +4,15 @@ import { ToolDetailRenderers, ChatUiMessage } from '../web-react/index.js';
4
4
  import '../contract-CEewO6DI.js';
5
5
  import '@tangle-network/agent-interface';
6
6
  import '../plans/index.js';
7
- import '../parts-Co_lFgLI.js';
8
- import '../types-XIZqIdX1.js';
7
+ import '../parts-DIKcC1p6.js';
8
+ import '../types-DeEOhQbv.js';
9
9
  import '../agent-activity-C8ZG0F0M.js';
10
10
  import '../flow-types-CJxEmaRy.js';
11
- import '../queue-CD5-_VTX.js';
11
+ import '../queue-DBkulxW1.js';
12
12
  import '../sandbox-terminal-ChNEdHF8.js';
13
13
  import '../catalog/index.js';
14
14
  import '../harness/index.js';
15
- import '../attachment-validation-DRuLEjAE.js';
15
+ import '../attachment-validation-yJ6Id4pR.js';
16
16
  import '../stream-normalizer-DnuqkZvw.js';
17
17
 
18
18
  /**
@@ -3,14 +3,14 @@ import {
3
3
  ChatMessages,
4
4
  ModelPicker,
5
5
  ProviderLogo
6
- } from "../chunk-TKRI463Z.js";
6
+ } from "../chunk-J6TNPRQN.js";
7
7
  import "../chunk-FBVLEGEG.js";
8
- import "../chunk-UDSY2F6N.js";
9
- import "../chunk-UP33Z633.js";
8
+ import "../chunk-72TPRENZ.js";
9
+ import "../chunk-ZRSUCTST.js";
10
10
  import "../chunk-HCOROIRT.js";
11
- import "../chunk-V55WJSR4.js";
12
- import "../chunk-AFNTRJQ7.js";
13
- import "../chunk-F2CBC4DY.js";
11
+ import "../chunk-5GA3MKOC.js";
12
+ import "../chunk-FEUW4G2L.js";
13
+ import "../chunk-V3HO43PG.js";
14
14
  import "../chunk-5EPIPT4V.js";
15
15
  import "../chunk-3ZK5IJSW.js";
16
16
  import "../chunk-YJMCRXQQ.js";
@@ -1,4 +1,4 @@
1
- import { M as FileMention } from './parts-Co_lFgLI.js';
1
+ import { M as FileMention } from './parts-DIKcC1p6.js';
2
2
 
3
3
  /**
4
4
  * `createSandboxFileIndexRoute` — server side of `@`-file-mentions
@@ -1,6 +1,6 @@
1
- import { C as ChatMessagePart, N as ChatTurnRequestPayload, O as ChatTurnPartInput, P as ChatTurnFilePartInput, b as ChatAttachmentPart, a as ChatAttachmentKind, g as ChatMentionPart } from '../parts-Co_lFgLI.js';
2
- export { Q as ChatAttachmentInput, f as ChatMentionKind, R as ChatTurnInputError, T as ChatTurnTextPartInput, U as DISPATCH_MAX_MEDIA_PARTS, V as DISPATCH_MAX_PARTS, W as DISPATCH_REQUEST_MAX_BYTES, X as DISPATCH_STRUCTURAL_RESERVE_BYTES, M as FileMention, Y as FileMentionsToPartsOptions, Z as INLINE_PARTS_MAX_BYTES, _ as MENTION_MAX_COUNT, $ as ProducerErrorEvent, a0 as ProducerNoticeEvent, a1 as ProducerPassthroughEvent, a2 as ProducerPassthroughEventType, a3 as ProducerReasoningEvent, a4 as ProducerTextEvent, a5 as ProducerToolCallEvent, a6 as ProducerToolResultEvent, a7 as ProducerUsageEvent, a8 as ProducerWireEvent, a9 as SandboxMentionPathCheck, aa as assertPromptPartsWithinCap, ab as base64WireLen, ac as buildMentionPromptBlock, ad as chatTurnRequestInit, ae as fileMentionsToParts, af as formatBytes, ag as mediaTypeForMentionPath, ah as mentionKindForPath, ai as parseChatTurnParts, aj as parseFileMentions, ak as promptPartsByteSize, al as validateSandboxMentionPath } from '../parts-Co_lFgLI.js';
3
- export { A as ALLOWED_ATTACHMENT_SNIFFED_MIMES, a as ATTACHMENT_ACCEPT, b as ATTACHMENT_MAX_COUNT, c as AttachmentTypeCheckResult, C as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, d as FileIndexCache, e as FileIndexReadyResponse, f as FileIndexResponse, g as FileIndexWarmingResponse, M as MAX_ATTACHMENT_TOTAL_BYTES, h as MAX_BINARY_ATTACHMENT_BYTES, i as MAX_TEXT_ATTACHMENT_BYTES, S as SandboxFileTreeSource, j as SandboxTreeFile, k as SandboxTreeResult, l as SniffResult, m as attachmentSizeErrorMessage, n as attachmentTotalSizeErrorMessage, o as checkAttachmentType, p as createSandboxFileIndexRoute, s as sanitizeAttachmentFileName, q as sniffBinary } from '../attachment-validation-DRuLEjAE.js';
1
+ import { C as ChatMessagePart, N as ChatTurnRequestPayload, O as ChatTurnPartInput, P as ChatTurnFilePartInput, b as ChatAttachmentPart, a as ChatAttachmentKind, g as ChatMentionPart } from '../parts-DIKcC1p6.js';
2
+ export { Q as ChatAttachmentInput, f as ChatMentionKind, R as ChatTurnInputError, T as ChatTurnTextPartInput, U as DISPATCH_MAX_MEDIA_PARTS, V as DISPATCH_MAX_PARTS, W as DISPATCH_REQUEST_MAX_BYTES, X as DISPATCH_STRUCTURAL_RESERVE_BYTES, M as FileMention, Y as FileMentionsToPartsOptions, Z as INLINE_PARTS_MAX_BYTES, _ as MENTION_MAX_COUNT, $ as ProducerErrorEvent, a0 as ProducerNoticeEvent, a1 as ProducerPassthroughEvent, a2 as ProducerPassthroughEventType, a3 as ProducerReasoningEvent, a4 as ProducerTextEvent, a5 as ProducerToolCallEvent, a6 as ProducerToolResultEvent, a7 as ProducerUsageEvent, a8 as ProducerWireEvent, a9 as SandboxMentionPathCheck, aa as assertPromptPartsWithinCap, ab as base64WireLen, ac as buildMentionPromptBlock, ad as chatTurnRequestInit, ae as fileMentionsToParts, af as formatBytes, ag as mediaTypeForMentionPath, ah as mentionKindForPath, ai as parseChatTurnParts, aj as parseFileMentions, ak as promptPartsByteSize, al as validateSandboxMentionPath } from '../parts-DIKcC1p6.js';
3
+ export { A as ALLOWED_ATTACHMENT_SNIFFED_MIMES, a as ATTACHMENT_ACCEPT, b as ATTACHMENT_MAX_COUNT, c as AttachmentTypeCheckResult, C as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, d as FileIndexCache, e as FileIndexReadyResponse, f as FileIndexResponse, g as FileIndexWarmingResponse, M as MAX_ATTACHMENT_TOTAL_BYTES, h as MAX_BINARY_ATTACHMENT_BYTES, i as MAX_TEXT_ATTACHMENT_BYTES, S as SandboxFileTreeSource, j as SandboxTreeFile, k as SandboxTreeResult, l as SniffResult, m as attachmentSizeErrorMessage, n as attachmentTotalSizeErrorMessage, o as checkAttachmentType, p as createSandboxFileIndexRoute, s as sanitizeAttachmentFileName, q as sniffBinary } from '../attachment-validation-yJ6Id4pR.js';
4
4
  import { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtime';
5
5
  import { InteractionAnswerRoute, InteractionAnswerRouteOptions } from '../interactions/index.js';
6
6
  import { PersistedChatMessageForTurn } from '../stream/index.js';
@@ -11,7 +11,7 @@ import { J as JsonRecord } from '../stream-normalizer-DnuqkZvw.js';
11
11
  import { SandboxExecChannel, PromptInputPart } from '../sandbox/index.js';
12
12
  import '@tangle-network/agent-interface';
13
13
  import '../contract-CEewO6DI.js';
14
- import '../types-XIZqIdX1.js';
14
+ import '../types-DeEOhQbv.js';
15
15
  import '../plans/index.js';
16
16
  import '@tangle-network/sandbox';
17
17
  import '../auth-anc7mv2W.js';
@@ -11,7 +11,7 @@ import {
11
11
  createSandboxFileIndexRoute,
12
12
  sanitizeAttachmentFileName,
13
13
  sniffBinary
14
- } from "../chunk-V55WJSR4.js";
14
+ } from "../chunk-5GA3MKOC.js";
15
15
  import {
16
16
  coalesceDeltas,
17
17
  createBufferedTurnTap,
@@ -55,8 +55,8 @@ import {
55
55
  promptPartsByteSize,
56
56
  toChatMessageParts,
57
57
  validateSandboxMentionPath
58
- } from "../chunk-AFNTRJQ7.js";
59
- import "../chunk-F2CBC4DY.js";
58
+ } from "../chunk-FEUW4G2L.js";
59
+ import "../chunk-V3HO43PG.js";
60
60
  import {
61
61
  asRecord,
62
62
  asString,
@@ -1,12 +1,12 @@
1
- import { C as ChatMessagePart } from '../parts-Co_lFgLI.js';
2
- export { a as ChatAttachmentKind, b as ChatAttachmentPart, c as ChatFilePart, d as ChatImagePart, e as ChatInteractionPart, f as ChatMentionKind, g as ChatMentionPart, h as ChatNoticePart, i as ChatPartTime, j as ChatPlanPart, k as ChatReasoningPart, l as ChatStepFinishPart, m as ChatStepStartPart, n as ChatSubtaskPart, o as ChatTextPart, p as ChatToolPart, q as ChatToolState, r as ChatToolStatus, s as ChatUsageTokens, t as ChatWorkProductPart, D as DEFAULT_ATTACHMENT_PROMPT_HEADER, S as StorableHarnessPartKind, u as attachmentInputToPart, v as attachmentKindForMime, w as attachmentPartsFromMessageParts, x as buildAttachmentPromptBlock, y as historyContentWithAttachments, z as isChatAttachmentPart, A as isChatInteractionPart, B as isChatMentionPart, E as isChatPlanPart, F as isChatStepFinishPart, G as isChatTextPart, H as isChatToolPart, I as isChatWorkProductPart, J as mentionInputToPart, K as mentionPartsFromMessageParts, L as toChatMessageParts } from '../parts-Co_lFgLI.js';
1
+ import { C as ChatMessagePart } from '../parts-DIKcC1p6.js';
2
+ export { a as ChatAttachmentKind, b as ChatAttachmentPart, c as ChatFilePart, d as ChatImagePart, e as ChatInteractionPart, f as ChatMentionKind, g as ChatMentionPart, h as ChatNoticePart, i as ChatPartTime, j as ChatPlanPart, k as ChatReasoningPart, l as ChatStepFinishPart, m as ChatStepStartPart, n as ChatSubtaskPart, o as ChatTextPart, p as ChatToolPart, q as ChatToolState, r as ChatToolStatus, s as ChatUsageTokens, t as ChatWorkProductPart, D as DEFAULT_ATTACHMENT_PROMPT_HEADER, S as StorableHarnessPartKind, u as attachmentInputToPart, v as attachmentKindForMime, w as attachmentPartsFromMessageParts, x as buildAttachmentPromptBlock, y as historyContentWithAttachments, z as isChatAttachmentPart, A as isChatInteractionPart, B as isChatMentionPart, E as isChatPlanPart, F as isChatStepFinishPart, G as isChatTextPart, H as isChatToolPart, I as isChatWorkProductPart, J as mentionInputToPart, K as mentionPartsFromMessageParts, L as toChatMessageParts } from '../parts-DIKcC1p6.js';
3
3
  import * as drizzle_orm from 'drizzle-orm';
4
4
  import * as drizzle_orm_sqlite_core from 'drizzle-orm/sqlite-core';
5
5
  import { SQLiteColumnBuilderBase, AnySQLiteTable, AnySQLiteColumn, BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
6
6
  export { a as attachmentPartKey } from '../stream-normalizer-DnuqkZvw.js';
7
7
  import '@tangle-network/agent-interface';
8
8
  import '../contract-CEewO6DI.js';
9
- import '../types-XIZqIdX1.js';
9
+ import '../types-DeEOhQbv.js';
10
10
  import '../plans/index.js';
11
11
 
12
12
  /**
@@ -16,8 +16,8 @@ import {
16
16
  mentionInputToPart,
17
17
  mentionPartsFromMessageParts,
18
18
  toChatMessageParts
19
- } from "../chunk-AFNTRJQ7.js";
20
- import "../chunk-F2CBC4DY.js";
19
+ } from "../chunk-FEUW4G2L.js";
20
+ import "../chunk-V3HO43PG.js";
21
21
  import {
22
22
  attachmentPartKey
23
23
  } from "../chunk-5EPIPT4V.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  formatBytes
3
- } from "./chunk-AFNTRJQ7.js";
3
+ } from "./chunk-FEUW4G2L.js";
4
4
 
5
5
  // src/chat-routes/binary-sniff.ts
6
6
  function bytesStartWith(bytes, offset, signature) {
@@ -236,4 +236,4 @@ export {
236
236
  attachmentTotalSizeErrorMessage,
237
237
  createSandboxFileIndexRoute
238
238
  };
239
- //# sourceMappingURL=chunk-V55WJSR4.js.map
239
+ //# sourceMappingURL=chunk-5GA3MKOC.js.map
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  parseReviewQueueItem
3
- } from "./chunk-UP33Z633.js";
3
+ } from "./chunk-ZRSUCTST.js";
4
4
  import {
5
5
  persistedPartToWorkProduct
6
- } from "./chunk-F2CBC4DY.js";
6
+ } from "./chunk-V3HO43PG.js";
7
7
 
8
8
  // src/web-react/work-product.tsx
9
9
  import { useCallback, useEffect, useState } from "react";
@@ -226,11 +226,24 @@ function EvidenceLineageTable({ evidence, resolveSourceUrl, className }) {
226
226
  /* @__PURE__ */ jsx("td", { className: "py-2 pr-3 font-mono text-xs text-foreground", children: entry.target }),
227
227
  /* @__PURE__ */ jsxs("td", { className: "py-2 pr-3 text-[13px] leading-snug text-foreground", children: [
228
228
  entry.claim,
229
- entry.locator.quote && /* @__PURE__ */ jsxs("span", { className: "mt-0.5 block border-l-2 border-border pl-2 text-xs italic text-muted-foreground", children: [
230
- "\u201C",
231
- entry.locator.quote,
232
- "\u201D"
233
- ] })
229
+ entry.locator.quote && // The basis is what a reviewer weighs, so it has to be
230
+ // visible per row, not only in the aggregate check detail.
231
+ // A platform-sliced quote came out of the document's own
232
+ // bytes; a model-typed one was proved to occur but was
233
+ // transcribed. Same text, different strength of evidence.
234
+ /* @__PURE__ */ jsxs(
235
+ "span",
236
+ {
237
+ className: `mt-0.5 block border-l-2 pl-2 text-xs italic ${entry.locator.quoteBasis === "span" ? "border-primary/60 text-foreground" : "border-border text-muted-foreground"}`,
238
+ title: entry.locator.quoteBasis === "span" ? "Sliced from the source document by the platform" : entry.locator.quoteBasis === "model" ? "Quoted by the agent, verified to occur in the source" : "Unverified quote",
239
+ children: [
240
+ "\u201C",
241
+ entry.locator.quote,
242
+ "\u201D",
243
+ entry.locator.quoteBasis && /* @__PURE__ */ jsx("span", { className: "ml-1.5 align-middle text-[10px] not-italic uppercase tracking-wide text-muted-foreground", children: entry.locator.quoteBasis === "span" ? "from source" : "verified" })
244
+ ]
245
+ }
246
+ )
234
247
  ] }),
235
248
  /* @__PURE__ */ jsxs("td", { className: "py-2 text-xs", children: [
236
249
  url ? /* @__PURE__ */ jsx(
@@ -328,4 +341,4 @@ export {
328
341
  QualityCheckList,
329
342
  ProvenanceStamp
330
343
  };
331
- //# sourceMappingURL=chunk-UDSY2F6N.js.map
344
+ //# sourceMappingURL=chunk-72TPRENZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/web-react/work-product.tsx"],"sourcesContent":["/**\n * Work-product review surfaces — the sandbox-ui-FREE half of the vertical:\n *\n * - {@link ReviewQueuePanel}: the practice/matters/campaign review queue over\n * a `fetchQueue(cursor)` data port (the `AgentActivityPanel` shape).\n * - {@link WorkProductCard}: the persisted `type:'work_product'` transcript\n * anchor rendered inside `ChatMessages` — chat stays the driver surface.\n * - {@link EvidenceLineageTable}: target → claim → source rows with\n * click-through via an injected `resolveSourceUrl` (the\n * `MessageAttachments.resolveFileUrl` pattern).\n * - {@link ExceptionList} / {@link QualityCheckList}: severity-badged\n * exception rows and pass/fail check rows.\n * - {@link ProvenanceStamp}: profileHash prefix + runId + serving-model\n * chips + the optional backtest slot (\"182 cases · composite 0.81 ·\n * trust PASS\" — or \"quality: unverified\" when the trust gate failed).\n *\n * Anything hosting sandbox-ui (DiffView, CodeView, PillTabs) lives in\n * `./work-product-react` instead — this module must stay importable without\n * the optional sandbox-ui peer.\n */\n\nimport { useCallback, useEffect, useState } from 'react'\n\nimport {\n parseReviewQueueItem,\n type ReviewQueueItem,\n type ReviewQueueState,\n} from '../work-product/queue'\nimport type {\n EvidenceEntry,\n ExceptionEntry,\n ProfileBacktestSummary,\n QualityCheck,\n WorkProductPersistedPart,\n WorkProductProvenance,\n WorkProductStatus,\n} from '../work-product/types'\nimport { persistedPartToWorkProduct } from '../work-product/types'\n\nexport { parseReviewQueueItem }\nexport type { ReviewQueueItem, ReviewQueueState }\n\n// ── shared presentation vocabulary ────────────────────────────────────────\n\nconst STATE_LABELS: Record<ReviewQueueState, string> = {\n intake: 'Intake',\n missing_info: 'Missing info',\n working: 'Working',\n ready_for_review: 'Ready for review',\n changes_requested: 'Changes requested',\n approved: 'Approved',\n blocked: 'Blocked',\n}\n\nconst STATE_TONES: Record<ReviewQueueState, string> = {\n intake: 'bg-muted/40 text-muted-foreground',\n missing_info: 'bg-warning/10 text-warning',\n working: 'bg-primary/10 text-primary',\n ready_for_review: 'bg-success/10 text-success',\n changes_requested: 'bg-warning/10 text-warning',\n approved: 'bg-success/10 text-success',\n blocked: 'bg-destructive/10 text-destructive',\n}\n\nconst STATUS_LABELS: Record<WorkProductStatus, string> = {\n draft: 'Draft',\n blocked: 'Blocked',\n ready: 'Ready for review',\n changes_requested: 'Changes requested',\n approved: 'Approved',\n superseded: 'Superseded',\n}\n\nconst STATUS_TONES: Record<WorkProductStatus, string> = {\n draft: 'bg-primary/10 text-primary',\n blocked: 'bg-destructive/10 text-destructive',\n ready: 'bg-success/10 text-success',\n changes_requested: 'bg-warning/10 text-warning',\n approved: 'bg-success/10 text-success',\n superseded: 'bg-muted/40 text-muted-foreground',\n}\n\n/** Human label for a queue state. */\nexport function reviewQueueStateLabel(state: ReviewQueueState): string {\n return STATE_LABELS[state]\n}\n\n/** Human label for a work-product status. */\nexport function workProductStatusLabel(status: WorkProductStatus): string {\n return STATUS_LABELS[status]\n}\n\nfunction StatePill({ state }: { state: ReviewQueueState }) {\n return (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${STATE_TONES[state]}`}>\n {STATE_LABELS[state]}\n </span>\n )\n}\n\nfunction StatusPill({ status }: { status: WorkProductStatus }) {\n return (\n <span className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${STATUS_TONES[status]}`}>\n {STATUS_LABELS[status]}\n </span>\n )\n}\n\n// ── transcript anchor helpers + card ──────────────────────────────────────\n\n/** Every persisted work-product anchor on one message, re-validated. */\nexport function workProductPartsFromMessageParts(\n parts: ReadonlyArray<Record<string, unknown>> | null | undefined,\n): WorkProductPersistedPart[] {\n if (!parts) return []\n const out: WorkProductPersistedPart[] = []\n for (const part of parts) {\n const typed = persistedPartToWorkProduct(part)\n if (typed) out.push(typed)\n }\n return out\n}\n\n/** Properties for the transcript anchor card rendered in chat */\nexport interface WorkProductCardProps {\n part: WorkProductPersistedPart\n /** Open the work product (queue detail / pane) — the card's only action. */\n onOpen?: (part: WorkProductPersistedPart) => void\n className?: string\n}\n\n/** The chat transcript anchor card for one work-product version — a compact,\n * system-authored pointer, not a parallel review surface. */\nexport function WorkProductCard({ part, onOpen, className }: WorkProductCardProps) {\n return (\n <div\n className={`flex items-center gap-3 rounded-xl border border-border bg-card px-4 py-3 ${className ?? ''}`}\n data-work-product-id={part.ref.id}\n >\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary\">\n <svg viewBox=\"0 0 24 24\" className=\"h-4 w-4\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" />\n <polyline points=\"14 2 14 8 20 8\" />\n <path d=\"m9 15 2 2 4-4\" />\n </svg>\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-semibold text-foreground\">{part.title}</p>\n <p className=\"text-[11px] text-muted-foreground\">\n {part.kind && <span className=\"font-mono\">{part.kind}</span>}\n {part.kind && ' · '}v{part.ref.version}\n </p>\n </div>\n <StatusPill status={part.status} />\n {onOpen && (\n <button\n type=\"button\"\n onClick={() => onOpen(part)}\n className=\"rounded-md border border-border px-2.5 py-1 text-xs font-medium text-foreground transition hover:bg-accent/30\"\n >\n Review\n </button>\n )}\n </div>\n )\n}\n\n// ── review queue panel ────────────────────────────────────────────────────\n\n/** One fetched page of queue items with an optional continuation cursor */\nexport interface ReviewQueuePage {\n items: ReviewQueueItem[]\n /** Opaque continuation token; absent ⇒ no further pages. */\n nextCursor?: string\n}\n\n/** Properties for the review queue panel over a fetch data port */\nexport interface ReviewQueuePanelProps {\n /** Data port — fetch one page of queue items (the projection's output,\n * re-validated here at the JSON boundary). */\n fetchQueue: (cursor?: string) => Promise<ReviewQueuePage>\n /** Open one queue item (navigate to its thread / detail pane). */\n onSelect?: (item: ReviewQueueItem) => void\n title?: string\n emptyLabel?: string\n className?: string\n}\n\n/** Merge a fetched page into held rows: dedupe by scopeKey, incoming wins,\n * newest updatedAt first. Exported for tests. */\nexport function mergeReviewQueuePages(existing: ReviewQueueItem[], incoming: ReviewQueueItem[]): ReviewQueueItem[] {\n const byScope = new Map<string, ReviewQueueItem>()\n for (const item of existing) byScope.set(item.scopeKey, item)\n for (const item of incoming) byScope.set(item.scopeKey, item)\n return [...byScope.values()].sort((a, b) => b.updatedAt - a.updatedAt)\n}\n\n/** The workspace review queue — cursor-paged, refreshable, selection via\n * callback. States, counts, and provenance render from the projection; the\n * panel holds no domain logic. */\nexport function ReviewQueuePanel({\n fetchQueue,\n onSelect,\n title = 'Review queue',\n emptyLabel = 'Nothing awaiting review.',\n className,\n}: ReviewQueuePanelProps) {\n const [items, setItems] = useState<ReviewQueueItem[]>([])\n const [cursor, setCursor] = useState<string | undefined>(undefined)\n const [loading, setLoading] = useState(false)\n const [error, setError] = useState<string | null>(null)\n\n const load = useCallback(\n async (from?: string) => {\n setLoading(true)\n setError(null)\n try {\n const page = await fetchQueue(from)\n const validated = page.items\n .map((item) => parseReviewQueueItem(item))\n .filter((item): item is ReviewQueueItem => item !== null)\n setItems((prev) => mergeReviewQueuePages(from === undefined ? [] : prev, validated))\n setCursor(page.nextCursor)\n } catch (e) {\n setError(e instanceof Error ? e.message : String(e))\n } finally {\n setLoading(false)\n }\n },\n [fetchQueue],\n )\n\n useEffect(() => {\n void load()\n }, [load])\n\n return (\n <div className={`space-y-2 ${className ?? ''}`}>\n <div className=\"flex items-center gap-2\">\n <h2 className=\"flex-1 text-sm font-semibold\">{title}</h2>\n <button\n type=\"button\"\n onClick={() => void load()}\n disabled={loading}\n aria-label=\"Refresh\"\n className=\"rounded-md p-1.5 text-muted-foreground transition hover:bg-accent/30 hover:text-foreground disabled:opacity-50\"\n >\n <svg viewBox=\"0 0 24 24\" className={`h-3.5 w-3.5 ${loading ? 'animate-spin' : ''}`} fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <path d=\"M21 12a9 9 0 1 1-2.64-6.36\" />\n <polyline points=\"21 3 21 9 15 9\" />\n </svg>\n </button>\n </div>\n {error && <p className=\"rounded-md bg-destructive/10 px-3 py-2 text-xs text-destructive\">{error}</p>}\n {!error && items.length === 0 && !loading && (\n <p className=\"rounded-md border border-dashed border-border px-3 py-4 text-center text-xs text-muted-foreground\">{emptyLabel}</p>\n )}\n <ul className=\"space-y-1.5\">\n {items.map((item) => (\n <li key={item.scopeKey}>\n <button\n type=\"button\"\n onClick={() => onSelect?.(item)}\n className=\"flex w-full items-center gap-3 rounded-lg border border-border bg-card px-3 py-2.5 text-left transition hover:border-primary/40 hover:bg-accent/20\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-foreground\">\n {item.workProduct?.title ?? item.scopeKey}\n {item.workProduct && <span className=\"ml-1.5 text-[11px] text-muted-foreground\">v{item.workProduct.version}</span>}\n </p>\n <p className=\"mt-0.5 flex flex-wrap items-center gap-x-2 text-[11px] text-muted-foreground\">\n {item.workProduct?.kind && <span className=\"font-mono\">{item.workProduct.kind}</span>}\n {item.pendingAsk && <span>asks: {item.pendingAsk.title}</span>}\n {item.blockingExceptions > 0 && (\n <span className=\"text-destructive\">{item.blockingExceptions} blocking</span>\n )}\n {item.failedChecks > 0 && <span className=\"text-warning\">{item.failedChecks} failed checks</span>}\n {item.provenance && item.provenance.profileHash && (\n <span className=\"font-mono\">{item.provenance.profileHash.slice(0, 8)}</span>\n )}\n </p>\n </div>\n <StatePill state={item.state} />\n </button>\n </li>\n ))}\n </ul>\n {cursor && (\n <button\n type=\"button\"\n onClick={() => void load(cursor)}\n disabled={loading}\n className=\"w-full rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition hover:bg-accent/30 disabled:opacity-50\"\n >\n Load more\n </button>\n )}\n </div>\n )\n}\n\n// ── evidence lineage table ────────────────────────────────────────────────\n\n/** Properties for the evidence lineage table with source click-through */\nexport interface EvidenceLineageTableProps {\n evidence: readonly EvidenceEntry[]\n /** Resolve one entry's source document to an openable URL (signed\n * object-store/vault download) — the `resolveFileUrl` pattern. Absent →\n * sources render as plain refs without links. */\n resolveSourceUrl?: (entry: EvidenceEntry) => string\n className?: string\n}\n\nfunction locatorLabel(entry: EvidenceEntry): string | null {\n const parts: string[] = []\n if (entry.locator.page !== undefined) parts.push(`p.${entry.locator.page}`)\n if (entry.locator.range) parts.push(entry.locator.range)\n return parts.length ? parts.join(' · ') : null\n}\n\n/** Target → claim → source lineage rows: the \"every material value traceable\n * to source evidence\" surface. Each row names the artifact target, the\n * claim, and the source document location supporting it. */\nexport function EvidenceLineageTable({ evidence, resolveSourceUrl, className }: EvidenceLineageTableProps) {\n if (evidence.length === 0) {\n return <p className={`text-xs text-muted-foreground ${className ?? ''}`}>No evidence recorded.</p>\n }\n return (\n <div className={`overflow-x-auto ${className ?? ''}`}>\n <table className=\"w-full border-collapse text-left text-sm\">\n <thead>\n <tr className=\"border-b border-border text-[11px] uppercase tracking-wide text-muted-foreground\">\n <th className=\"py-1.5 pr-3 font-medium\">Target</th>\n <th className=\"py-1.5 pr-3 font-medium\">Claim</th>\n <th className=\"py-1.5 font-medium\">Source</th>\n </tr>\n </thead>\n <tbody>\n {evidence.map((entry) => {\n const url = resolveSourceUrl?.(entry)\n const locator = locatorLabel(entry)\n return (\n <tr key={entry.id} className=\"border-b border-border/50 align-top\">\n <td className=\"py-2 pr-3 font-mono text-xs text-foreground\">{entry.target}</td>\n <td className=\"py-2 pr-3 text-[13px] leading-snug text-foreground\">\n {entry.claim}\n {entry.locator.quote && (\n // The basis is what a reviewer weighs, so it has to be\n // visible per row, not only in the aggregate check detail.\n // A platform-sliced quote came out of the document's own\n // bytes; a model-typed one was proved to occur but was\n // transcribed. Same text, different strength of evidence.\n <span\n className={`mt-0.5 block border-l-2 pl-2 text-xs italic ${\n entry.locator.quoteBasis === 'span'\n ? 'border-primary/60 text-foreground'\n : 'border-border text-muted-foreground'\n }`}\n title={\n entry.locator.quoteBasis === 'span'\n ? 'Sliced from the source document by the platform'\n : entry.locator.quoteBasis === 'model'\n ? 'Quoted by the agent, verified to occur in the source'\n : 'Unverified quote'\n }\n >\n “{entry.locator.quote}”\n {entry.locator.quoteBasis && (\n <span className=\"ml-1.5 align-middle text-[10px] not-italic uppercase tracking-wide text-muted-foreground\">\n {entry.locator.quoteBasis === 'span' ? 'from source' : 'verified'}\n </span>\n )}\n </span>\n )}\n </td>\n <td className=\"py-2 text-xs\">\n {url ? (\n <a\n href={url}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"font-mono text-primary underline-offset-2 hover:underline\"\n >\n {entry.sourceRef}\n </a>\n ) : (\n <span className=\"font-mono text-muted-foreground\">{entry.sourceRef}</span>\n )}\n {locator && <span className=\"ml-1.5 text-muted-foreground\">{locator}</span>}\n </td>\n </tr>\n )\n })}\n </tbody>\n </table>\n </div>\n )\n}\n\n// ── exceptions + checks ───────────────────────────────────────────────────\n\nconst SEVERITY_TONES: Record<ExceptionEntry['severity'], string> = {\n blocking: 'bg-destructive/10 text-destructive',\n material: 'bg-warning/10 text-warning',\n advisory: 'bg-muted/40 text-muted-foreground',\n}\n\n/** Properties for the severity-badged exception list */\nexport interface ExceptionListProps {\n exceptions: readonly ExceptionEntry[]\n className?: string\n}\n\n/** Severity-badged exception rows with resolution state. */\nexport function ExceptionList({ exceptions, className }: ExceptionListProps) {\n if (exceptions.length === 0) {\n return <p className={`text-xs text-muted-foreground ${className ?? ''}`}>No exceptions flagged.</p>\n }\n return (\n <ul className={`space-y-1.5 ${className ?? ''}`}>\n {exceptions.map((entry) => (\n <li key={entry.id} className=\"flex items-start gap-2.5 rounded-lg border border-border bg-card px-3 py-2\">\n <span className={`mt-0.5 inline-flex shrink-0 items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${SEVERITY_TONES[entry.severity]}`}>\n {entry.severity}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className={`text-[13px] leading-snug ${entry.resolved ? 'text-muted-foreground line-through' : 'text-foreground'}`}>\n {entry.message}\n </p>\n <p className=\"mt-0.5 flex flex-wrap gap-x-2 text-[11px] text-muted-foreground\">\n <span className=\"font-mono\">{entry.kind}</span>\n {entry.targets && entry.targets.length > 0 && <span className=\"font-mono\">{entry.targets.join(', ')}</span>}\n {entry.resolved && <span>resolved{entry.resolvedBy ? ` by ${entry.resolvedBy}` : ''}</span>}\n {entry.resolutionNote && <span>{entry.resolutionNote}</span>}\n </p>\n </div>\n </li>\n ))}\n </ul>\n )\n}\n\n/** Properties for the pass/fail quality check list */\nexport interface QualityCheckListProps {\n checks: readonly QualityCheck[]\n className?: string\n}\n\n/** Pass/fail quality-check rows tagged with their source (agent self-report,\n * platform gate, judge ensemble). */\nexport function QualityCheckList({ checks, className }: QualityCheckListProps) {\n if (checks.length === 0) {\n return <p className={`text-xs text-muted-foreground ${className ?? ''}`}>No checks recorded.</p>\n }\n return (\n <ul className={`space-y-1 ${className ?? ''}`}>\n {checks.map((check) => (\n <li key={check.id} className=\"flex items-start gap-2 rounded-md px-1 py-1\">\n {check.passed ? (\n <svg viewBox=\"0 0 24 24\" className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-success\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <polyline points=\"20 6 9 17 4 12\" />\n </svg>\n ) : (\n <svg viewBox=\"0 0 24 24\" className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-destructive\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden>\n <line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\" />\n <line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\" />\n </svg>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-[13px] text-foreground\">\n {check.name}\n <span className=\"ml-1.5 text-[10px] uppercase tracking-wide text-muted-foreground\">{check.source}</span>\n </p>\n {check.detail && <p className=\"text-[11px] leading-snug text-muted-foreground\">{check.detail}</p>}\n </div>\n </li>\n ))}\n </ul>\n )\n}\n\n// ── provenance stamp ──────────────────────────────────────────────────────\n\n/** Properties for the provenance stamp with the optional backtest slot */\nexport interface ProvenanceStampProps {\n provenance: Pick<WorkProductProvenance, 'profileHash' | 'runId' | 'servingModels'> &\n Partial<Pick<WorkProductProvenance, 'costUsd' | 'producedAt'>>\n /** The product-resolved backtest summary for this profile hash. When its\n * trust gate failed, the composite renders as \"quality: unverified\" —\n * never a naked number. */\n backtest?: ProfileBacktestSummary\n className?: string\n}\n\n/** The audit line a reviewer approves against: which configuration produced\n * this document, what served it, and how that configuration measured on its\n * backtest. */\nexport function ProvenanceStamp({ provenance, backtest, className }: ProvenanceStampProps) {\n return (\n <div className={`flex flex-wrap items-center gap-x-2.5 gap-y-1 text-[11px] text-muted-foreground ${className ?? ''}`}>\n <span className=\"font-mono\" title={provenance.profileHash}>\n profile {provenance.profileHash ? provenance.profileHash.slice(0, 10) : '—'}\n </span>\n <span className=\"font-mono\" title={provenance.runId}>\n run {provenance.runId ? provenance.runId.slice(0, 10) : '—'}\n </span>\n {provenance.servingModels.length > 0 ? (\n provenance.servingModels.map((model) => (\n <span key={model} className=\"rounded-full bg-muted/40 px-2 py-0.5 font-mono\">\n {model}\n </span>\n ))\n ) : (\n <span className=\"italic\">serving model pending</span>\n )}\n {typeof provenance.costUsd === 'number' && <span>${provenance.costUsd.toFixed(provenance.costUsd < 0.01 ? 4 : 2)}</span>}\n {backtest &&\n (backtest.trust === 'pass' ? (\n <span className=\"text-success\">\n {backtest.cases} backtest cases · composite {backtest.composite.toFixed(2)} · trust PASS\n </span>\n ) : (\n <span className=\"text-warning\" title={backtest.trustReasons.join('; ')}>\n quality: unverified\n </span>\n ))}\n </div>\n )\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  persistedPartToWorkProduct
3
- } from "./chunk-F2CBC4DY.js";
3
+ } from "./chunk-V3HO43PG.js";
4
4
  import {
5
5
  persistedPartToInteraction
6
6
  } from "./chunk-3ZK5IJSW.js";
@@ -367,4 +367,4 @@ export {
367
367
  buildAttachmentPromptBlock,
368
368
  historyContentWithAttachments
369
369
  };
370
- //# sourceMappingURL=chunk-AFNTRJQ7.js.map
370
+ //# sourceMappingURL=chunk-FEUW4G2L.js.map
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  WorkProductCard,
6
6
  workProductPartsFromMessageParts
7
- } from "./chunk-UDSY2F6N.js";
7
+ } from "./chunk-72TPRENZ.js";
8
8
  import {
9
9
  ATTACHMENT_ACCEPT,
10
10
  ATTACHMENT_MAX_COUNT,
@@ -16,11 +16,11 @@ import {
16
16
  checkAttachmentType,
17
17
  sanitizeAttachmentFileName,
18
18
  sniffBinary
19
- } from "./chunk-V55WJSR4.js";
19
+ } from "./chunk-5GA3MKOC.js";
20
20
  import {
21
21
  attachmentPartsFromMessageParts,
22
22
  formatBytes
23
- } from "./chunk-AFNTRJQ7.js";
23
+ } from "./chunk-FEUW4G2L.js";
24
24
  import {
25
25
  cancelStatusFor,
26
26
  fieldAcceptsFreeText,
@@ -3956,4 +3956,4 @@ export {
3956
3956
  useThinkingSeconds,
3957
3957
  ChatMessages
3958
3958
  };
3959
- //# sourceMappingURL=chunk-TKRI463Z.js.map
3959
+ //# sourceMappingURL=chunk-J6TNPRQN.js.map
@@ -46,6 +46,20 @@ function parseEvidenceInput(raw, path = "entry") {
46
46
  if (typeof locatorRaw.quote !== "string") return fail(`${path}.locator.quote`, "must be a string when present");
47
47
  locator.quote = locatorRaw.quote;
48
48
  }
49
+ if (locatorRaw.span !== void 0) {
50
+ const spanRaw = asRecord(locatorRaw.span);
51
+ if (!spanRaw) return fail(`${path}.locator.span`, "must be an object { start, end } when present");
52
+ for (const field of ["start", "end"]) {
53
+ const value = spanRaw[field];
54
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
55
+ return fail(`${path}.locator.span.${field}`, "must be a non-negative integer character offset");
56
+ }
57
+ }
58
+ if (spanRaw.end <= spanRaw.start) {
59
+ return fail(`${path}.locator.span`, `end (${String(spanRaw.end)}) must be greater than start (${String(spanRaw.start)}) \u2014 the range is half-open [start, end)`);
60
+ }
61
+ locator.span = { start: spanRaw.start, end: spanRaw.end };
62
+ }
49
63
  const entry = {
50
64
  id: record.id.trim(),
51
65
  sourceRef: record.sourceRef.trim(),
@@ -190,4 +204,4 @@ export {
190
204
  workProductToPersistedPart,
191
205
  persistedPartToWorkProduct
192
206
  };
193
- //# sourceMappingURL=chunk-F2CBC4DY.js.map
207
+ //# sourceMappingURL=chunk-V3HO43PG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/work-product/types.ts"],"sourcesContent":["/**\n * The reviewable work-product contract — the one durable object every Tangle\n * agent product converges on: an artifact plus its evidence map (source→field\n * lineage), exceptions, quality checks, version history, and run provenance,\n * produced by an agent and signed off by a professional.\n *\n * Import-free and client-safe by construction: the queue projection and the\n * React surfaces re-validate these shapes at JSON boundaries, so every type\n * and codec here must load in a browser bundle. Every domain word — artifact\n * `kind`, evidence `target`, exception `kind`, check `name`, `scopeKey`\n * format — is a STRING PARAMETER validated against product-supplied\n * vocabularies in the tool layer; nothing domain-specific is baked here.\n */\n\n// ── identity / status ────────────────────────────────────────────────────────\n\n/** Stable pointer at one version of one work product. */\nexport interface WorkProductRef {\n id: string\n version: number\n}\n\nexport type WorkProductStatus =\n | 'draft' // agent is emitting incrementally (queue: working)\n | 'blocked' // unresolved blocking exception (queue: blocked / missing-info)\n | 'ready' // submit_work_product accepted (queue: ready-for-review)\n | 'changes_requested' // reviewer verdict; the correction re-enters chat as a turn\n | 'approved' // reviewer sign-off on THIS version\n | 'superseded' // a newer version replaced this one\n\nconst WORK_PRODUCT_STATUSES: readonly WorkProductStatus[] = [\n 'draft',\n 'blocked',\n 'ready',\n 'changes_requested',\n 'approved',\n 'superseded',\n]\n\n/** Runtime guard for re-validating a status read off a JSON boundary. */\nexport function isWorkProductStatus(value: unknown): value is WorkProductStatus {\n return typeof value === 'string' && (WORK_PRODUCT_STATUSES as readonly string[]).includes(value)\n}\n\n// ── artifact ─────────────────────────────────────────────────────────────────\n\n/** Define the reviewable artifact body with its kind, sources, and structured field map */\nexport interface WorkProductArtifact {\n /** PARAMETER: 'return_package' | 'redline' | 'outreach_campaign' — validated\n * against the product's `artifactKinds` vocabulary at submit. */\n kind: string\n title: string\n /** Vault/object-store ref for the rendered document (large bodies). */\n path?: string\n /** Inline body when small (markdown/JSON). */\n content?: string\n mediaType?: string\n /** Diff-first products (legal): the source document the artifact redlines. */\n baseline?: { path?: string; content?: string }\n /** The structured field map lineage targets anchor to (tax: form-line ids). */\n fields?: Record<string, unknown>\n}\n\n// ── evidence / lineage: source doc + locator → field/claim ───────────────────\n\n/** A half-open `[start, end)` character range into the source document's text.\n * Offsets index exactly the string the product's `readSourceText` seam\n * returns for the same `sourceRef` — which is the string its document-reading\n * tool pages with an `offset`. The PLATFORM slices `locator.quote` out of it,\n * so a span citation cannot name text the document does not contain. */\nexport interface EvidenceSpan {\n start: number\n end: number\n}\n\n/** How `locator.quote` got there. Server-set on every path — never read from\n * model args, because the whole value of the distinction is that a reviewer\n * can trust it:\n * - `span` the platform sliced it out of the source bytes (unfalsifiable)\n * - `model` the model supplied the text and the platform PROVED it occurs */\nexport type QuoteBasis = 'span' | 'model'\n\n/** Point into a source document: page, free-form range, span, verbatim quote */\nexport interface EvidenceLocator {\n page?: number\n /** 'L120-L134' | 'B7' | '¶4' — free-form, non-empty when present. */\n range?: string\n /** Verbatim supporting quote from the source. Model-supplied and verified,\n * or platform-sliced from {@link EvidenceLocator.span} — read `quoteBasis`\n * to tell which. */\n quote?: string\n /** Character range the quote is sliced from. The preferred citation form:\n * two integers cannot be a fabricated quote. */\n span?: EvidenceSpan\n /** Server-set provenance for `quote`; a model-supplied value is discarded. */\n quoteBasis?: QuoteBasis\n}\n\n/** One lineage row: a source document location supporting one artifact claim */\nexport interface EvidenceEntry {\n /** Agent-supplied stable id — the same id re-emitted is an upsert/replace. */\n id: string\n /** Vault path / attachment id / object-store key of the SOURCE document. */\n sourceRef: string\n locator: EvidenceLocator\n /** Artifact field/claim the evidence supports: '1040.line_9' |\n * 'clause:indemnification' | 'icp.employee_count' — product vocabulary. */\n target: string\n /** The value/assertion at the target. */\n claim: string\n /** 0..1; absent = unstated (never defaulted). */\n confidence?: number\n}\n\n// ── exceptions ───────────────────────────────────────────────────────────────\n\nexport type ExceptionSeverity = 'blocking' | 'material' | 'advisory'\n\n/** One flagged problem with the work product, resolvable by agent or reviewer */\nexport interface ExceptionEntry {\n id: string\n severity: ExceptionSeverity\n /** PARAMETER: 'missing_document' | 'inconsistent_source' | … — validated\n * against the product's `exceptionKinds` vocabulary. */\n kind: string\n message: string\n /** Affected artifact targets. */\n targets?: string[]\n resolved: boolean\n resolvedBy?: 'agent' | 'reviewer'\n resolutionNote?: string\n}\n\n/** Unresolved blocking exceptions are what park a work product in `blocked`. */\nexport function unresolvedBlockingExceptions(exceptions: readonly ExceptionEntry[]): ExceptionEntry[] {\n return exceptions.filter((entry) => entry.severity === 'blocking' && !entry.resolved)\n}\n\n// ── quality checks ───────────────────────────────────────────────────────────\n\n/** One named quality verdict on the work product, tagged with who computed it */\nexport interface QualityCheck {\n id: string\n /** Product vocabulary: 'totals_reconcile', 'evidence_coverage', … */\n name: string\n passed: boolean\n detail?: string\n /** agent self-report | shell-computed | eval-ensemble verdict. */\n source: 'agent' | 'platform' | 'judge'\n}\n\n// ── provenance (the backtest spine) ──────────────────────────────────────────\n\n/** The audit triple binding a work product to the exact configuration and run\n * that produced it. `profileHash` is agent-eval's `agentProfileHash()` of the\n * EXACT shipped profile — the same hash that keys scorecard cells, so a\n * reviewer can see the measured backtest history of the configuration that\n * produced the document. */\nexport interface WorkProductProvenance {\n profileHash: string\n /** Chat turnId or mission-step run id. */\n runId: string\n /** Back-filled from the usage receipt / serving-model header at turn\n * completion — only the completed turn knows what actually served. */\n servingModels: string[]\n sessionId?: string\n missionRef?: { missionId: string; stepId: string }\n costUsd?: number\n producedAt: number\n}\n\n/** The product-resolved backtest summary for one profile hash (from its own\n * eval artifacts via agent-eval's `loadScorecard`) — the shell defines only\n * this TYPE and the `ProvenanceStamp` slot that renders it. */\nexport interface ProfileBacktestSummary {\n profileHash: string\n /** Backtest cases behind the composite. */\n cases: number\n composite: number\n /** The eval-campaign trust gate's verdict on whether the composite is\n * allowed to be believed; 'fail' renders as \"quality: unverified\". */\n trust: 'pass' | 'fail'\n trustReasons: string[]\n}\n\n// ── version history ──────────────────────────────────────────────────────────\n\n/** Frozen milestone row for one version of the work product */\nexport interface WorkProductVersionEntry {\n version: number\n status: WorkProductStatus\n provenance: WorkProductProvenance\n /** Frozen snapshot ref of this version's body (object-store key) — the\n * DiffView input for vN-1 vs vN. */\n artifactPath?: string\n reviewedBy?: string\n reviewNote?: string\n at: number\n}\n\n// ── the durable row ──────────────────────────────────────────────────────────\n\n/** Define the durable work product row accumulating artifact, lineage, exceptions, checks, and history */\nexport interface WorkProductRecord {\n /** Minted server-side; the model NEVER supplies ids — it addresses work by\n * `scopeKey`. */\n id: string\n workspaceId: string\n /** The chat thread that drives it (chat stays first). */\n threadId: string | null\n /** Product engagement key: 'return:acme:2025' | 'contract:acme-msa' |\n * 'campaign:cpa-pilots'. */\n scopeKey: string\n status: WorkProductStatus\n version: number\n /** `null` while the draft accumulates (evidence streams in before the\n * artifact arrives); non-null from the submit transition onward. Typed\n * honestly rather than forcing a placeholder artifact on every draft. */\n artifact: WorkProductArtifact | null\n evidence: EvidenceEntry[]\n exceptions: ExceptionEntry[]\n checks: QualityCheck[]\n provenance: WorkProductProvenance\n history: WorkProductVersionEntry[]\n createdAt: number\n updatedAt: number\n}\n\n// ── store port — the /missions MissionStorePort pattern ──────────────────────\n\n/** Fields a guarded write compares against the values the caller read. An\n * absent field is unguarded. A SQL implementation compares like-for-like\n * scalar columns; the in-memory store does the same. */\nexport interface WorkProductUpdateGuard {\n status?: WorkProductStatus\n version?: number\n}\n\n/** Fields a guarded write sets when the guard holds. */\nexport interface WorkProductPatch {\n status?: WorkProductStatus\n version?: number\n artifact?: WorkProductArtifact\n evidence?: EvidenceEntry[]\n exceptions?: ExceptionEntry[]\n checks?: QualityCheck[]\n provenance?: WorkProductProvenance\n history?: WorkProductVersionEntry[]\n updatedAt?: number\n}\n\n/** One audit-trail row, appended after every committed state change. */\nexport interface WorkProductAuditEvent {\n workProductId: string\n workspaceId: string\n /** Machine-readable transition name ('wp.created' | 'wp.evidence' |\n * 'wp.ready' | 'wp.verdict' | …). */\n step: string\n message: string\n metadata: Record<string, unknown>\n at: number\n}\n\n/**\n * Persistence seam — the product implements this over its own tables. The\n * invariant the implementation MUST keep: `update` applies `patch` ONLY when\n * every guard field still equals the stored value, and returns `null` when\n * the guard misses — a typed conflict, never a clobber.\n */\nexport interface WorkProductStorePort {\n load(id: string): Promise<WorkProductRecord | null>\n /** The scope's single OPEN accumulator row — status `draft` or `blocked` —\n * or null when the scope has only reviewed/terminal rows (or none). The\n * `changes_requested` row is found via {@link listByWorkspace}; reopening\n * it is the service's job. */\n findDraft(workspaceId: string, scopeKey: string): Promise<WorkProductRecord | null>\n listByWorkspace(workspaceId: string, opts?: { status?: WorkProductStatus[] }): Promise<WorkProductRecord[]>\n /** `extras` are opaque product-column values written in the SAME statement\n * as the record (single-write creation), or ignored when the table has no\n * extra columns. */\n insert(record: WorkProductRecord, extras?: Record<string, unknown>): Promise<WorkProductRecord>\n /** CAS: apply `patch` ONLY when guard fields equal stored values; null on a\n * miss — typed conflict, never a clobber. */\n update(id: string, guard: WorkProductUpdateGuard, patch: WorkProductPatch): Promise<WorkProductRecord | null>\n appendEvent(event: WorkProductAuditEvent): Promise<void>\n}\n\n// ── input codecs (strict, named-field errors) ────────────────────────────────\n\n/** Validation result whose failure names the exact field, so a tool layer can\n * hand the model a correctable error it can act on. */\nexport type WorkProductParseResult<T> =\n | { ok: true; value: T }\n | { ok: false; field: string; error: string }\n\nfunction fail<T>(field: string, error: string): WorkProductParseResult<T> {\n return { ok: false, field, error }\n}\n\nfunction nonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.trim().length > 0\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null\n}\n\n/** Validate one raw evidence entry field-by-field. `path` prefixes the failure\n * field name (e.g. `entries[3]`) so batched calls name the exact offender. */\nexport function parseEvidenceInput(raw: unknown, path = 'entry'): WorkProductParseResult<EvidenceEntry> {\n const record = asRecord(raw)\n if (!record) return fail(path, 'must be an object')\n if (!nonEmptyString(record.id)) return fail(`${path}.id`, 'must be a non-empty string (stable per entry; re-emitting the same id replaces it)')\n if (!nonEmptyString(record.sourceRef)) return fail(`${path}.sourceRef`, 'must be a non-empty source document ref')\n if (!nonEmptyString(record.target)) return fail(`${path}.target`, 'must name the artifact field/claim this evidence supports')\n if (!nonEmptyString(record.claim)) return fail(`${path}.claim`, 'must state the value/assertion at the target')\n const locatorRaw = record.locator === undefined ? {} : asRecord(record.locator)\n if (!locatorRaw) return fail(`${path}.locator`, 'must be an object when present')\n const locator: EvidenceLocator = {}\n if (locatorRaw.page !== undefined) {\n if (typeof locatorRaw.page !== 'number' || !Number.isFinite(locatorRaw.page)) {\n return fail(`${path}.locator.page`, 'must be a finite number when present')\n }\n locator.page = locatorRaw.page\n }\n if (locatorRaw.range !== undefined) {\n if (!nonEmptyString(locatorRaw.range)) return fail(`${path}.locator.range`, 'must be a non-empty string when present')\n locator.range = locatorRaw.range\n }\n if (locatorRaw.quote !== undefined) {\n if (typeof locatorRaw.quote !== 'string') return fail(`${path}.locator.quote`, 'must be a string when present')\n locator.quote = locatorRaw.quote\n }\n if (locatorRaw.span !== undefined) {\n const spanRaw = asRecord(locatorRaw.span)\n if (!spanRaw) return fail(`${path}.locator.span`, 'must be an object { start, end } when present')\n for (const field of ['start', 'end'] as const) {\n const value = spanRaw[field]\n if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {\n return fail(`${path}.locator.span.${field}`, 'must be a non-negative integer character offset')\n }\n }\n if ((spanRaw.end as number) <= (spanRaw.start as number)) {\n return fail(`${path}.locator.span`, `end (${String(spanRaw.end)}) must be greater than start (${String(spanRaw.start)}) — the range is half-open [start, end)`)\n }\n locator.span = { start: spanRaw.start as number, end: spanRaw.end as number }\n }\n // `quoteBasis` is server-set. A model-supplied value is DROPPED rather than\n // rejected: it is not a correctable mistake the model should burn a turn on,\n // and silently honouring it would let a model label an unverified quote\n // 'span'. The tool layer stamps the real basis after resolution.\n const entry: EvidenceEntry = {\n id: (record.id as string).trim(),\n sourceRef: (record.sourceRef as string).trim(),\n locator,\n target: (record.target as string).trim(),\n claim: record.claim as string,\n }\n if (record.confidence !== undefined) {\n if (typeof record.confidence !== 'number' || !(record.confidence >= 0 && record.confidence <= 1)) {\n return fail(`${path}.confidence`, 'must be a number in 0..1 when present')\n }\n entry.confidence = record.confidence\n }\n return { ok: true, value: entry }\n}\n\nconst EXCEPTION_SEVERITIES: readonly ExceptionSeverity[] = ['blocking', 'material', 'advisory']\n\n/** Validate one raw exception entry field-by-field. Kind MEMBERSHIP in the\n * product vocabulary is the tool layer's check (it owns the config). */\nexport function parseExceptionInput(raw: unknown, path = 'exception'): WorkProductParseResult<ExceptionEntry> {\n const record = asRecord(raw)\n if (!record) return fail(path, 'must be an object')\n if (!nonEmptyString(record.id)) return fail(`${path}.id`, 'must be a non-empty string (stable per entry; re-emitting the same id replaces it)')\n if (!EXCEPTION_SEVERITIES.includes(record.severity as ExceptionSeverity)) {\n return fail(`${path}.severity`, `must be one of: ${EXCEPTION_SEVERITIES.join(', ')}`)\n }\n if (!nonEmptyString(record.kind)) return fail(`${path}.kind`, 'must be a non-empty exception kind')\n if (!nonEmptyString(record.message)) return fail(`${path}.message`, 'must describe the problem')\n const entry: ExceptionEntry = {\n id: (record.id as string).trim(),\n severity: record.severity as ExceptionSeverity,\n kind: (record.kind as string).trim(),\n message: record.message as string,\n resolved: record.resolved === true,\n }\n if (record.targets !== undefined) {\n if (!Array.isArray(record.targets) || !record.targets.every(nonEmptyString)) {\n return fail(`${path}.targets`, 'must be an array of non-empty strings when present')\n }\n entry.targets = record.targets as string[]\n }\n if (record.resolvedBy !== undefined) {\n if (record.resolvedBy !== 'agent' && record.resolvedBy !== 'reviewer') {\n return fail(`${path}.resolvedBy`, \"must be 'agent' or 'reviewer' when present\")\n }\n entry.resolvedBy = record.resolvedBy\n }\n if (record.resolutionNote !== undefined) {\n if (typeof record.resolutionNote !== 'string') return fail(`${path}.resolutionNote`, 'must be a string when present')\n entry.resolutionNote = record.resolutionNote\n }\n return { ok: true, value: entry }\n}\n\n/** Validate a raw artifact. Kind MEMBERSHIP in `artifactKinds` is the tool\n * layer's check. An artifact must carry a body: at least one of `path`,\n * `content`, or `fields`. */\nexport function parseArtifactInput(raw: unknown, path = 'artifact'): WorkProductParseResult<WorkProductArtifact> {\n const record = asRecord(raw)\n if (!record) return fail(path, 'must be an object')\n if (!nonEmptyString(record.kind)) return fail(`${path}.kind`, 'must be a non-empty artifact kind')\n if (!nonEmptyString(record.title)) return fail(`${path}.title`, 'must be a non-empty title')\n for (const key of ['path', 'content', 'mediaType'] as const) {\n if (record[key] !== undefined && !nonEmptyString(record[key])) {\n return fail(`${path}.${key}`, 'must be a non-empty string when present')\n }\n }\n let baseline: WorkProductArtifact['baseline']\n if (record.baseline !== undefined) {\n const baselineRecord = asRecord(record.baseline)\n if (!baselineRecord) return fail(`${path}.baseline`, 'must be an object when present')\n baseline = {}\n if (baselineRecord.path !== undefined) {\n if (!nonEmptyString(baselineRecord.path)) return fail(`${path}.baseline.path`, 'must be a non-empty string when present')\n baseline.path = (baselineRecord.path as string).trim()\n }\n if (baselineRecord.content !== undefined) {\n if (typeof baselineRecord.content !== 'string') return fail(`${path}.baseline.content`, 'must be a string when present')\n baseline.content = baselineRecord.content\n }\n }\n let fields: Record<string, unknown> | undefined\n if (record.fields !== undefined) {\n const fieldsRecord = asRecord(record.fields)\n if (!fieldsRecord) return fail(`${path}.fields`, 'must be an object when present')\n fields = fieldsRecord\n }\n if (record.path === undefined && record.content === undefined && fields === undefined) {\n return fail(path, 'must carry a body: at least one of path, content, or fields')\n }\n const artifact: WorkProductArtifact = {\n kind: (record.kind as string).trim(),\n title: (record.title as string).trim(),\n }\n if (record.path !== undefined) artifact.path = (record.path as string).trim()\n if (record.content !== undefined) artifact.content = record.content as string\n if (record.mediaType !== undefined) artifact.mediaType = (record.mediaType as string).trim()\n if (baseline !== undefined) artifact.baseline = baseline\n if (fields !== undefined) artifact.fields = fields\n return { ok: true, value: artifact }\n}\n\n/** Agent self-reported check input for `submit_work_product` (persisted with\n * `source: 'agent'`; `platform`/`judge` sources are never model-suppliable). */\nexport interface AgentCheckInput {\n id: string\n name: string\n passed: boolean\n detail?: string\n}\n\n/** Validate one agent self-check ensuring identifiers, verdict flag, and optional detail are well formed */\nexport function parseAgentCheckInput(raw: unknown, path = 'check'): WorkProductParseResult<AgentCheckInput> {\n const record = asRecord(raw)\n if (!record) return fail(path, 'must be an object')\n if (!nonEmptyString(record.id)) return fail(`${path}.id`, 'must be a non-empty string')\n if (!nonEmptyString(record.name)) return fail(`${path}.name`, 'must be a non-empty check name')\n if (typeof record.passed !== 'boolean') return fail(`${path}.passed`, 'must be a boolean')\n const check: AgentCheckInput = {\n id: (record.id as string).trim(),\n name: (record.name as string).trim(),\n passed: record.passed,\n }\n if (record.detail !== undefined) {\n if (typeof record.detail !== 'string') return fail(`${path}.detail`, 'must be a string when present')\n check.detail = record.detail\n }\n return { ok: true, value: check }\n}\n\n// ── chat transcript anchor (system-authored) ─────────────────────────────────\n\n/**\n * The persisted `type:'work_product'` transcript anchor — the chat card that\n * keeps chat the driver surface for review. The PLATFORM writes it on the\n * ready transition (and updates it on a verdict); no prompt ever teaches an\n * agent to author one. `/chat-store` aliases this as `ChatWorkProductPart` in\n * its stored-part union, exactly like the `interaction`/`plan` members.\n */\nexport interface WorkProductPersistedPart {\n type: 'work_product'\n ref: WorkProductRef\n kind: string\n title: string\n status: WorkProductStatus\n}\n\n/** Project the transcript anchor part from a record. Requires the submitted\n * artifact (the anchor is written on the ready transition, after which\n * `artifact` is always non-null); falls back to the scopeKey label for\n * defensive callers on an accumulating draft. */\nexport function workProductToPersistedPart(record: WorkProductRecord): WorkProductPersistedPart {\n return {\n type: 'work_product',\n ref: { id: record.id, version: record.version },\n kind: record.artifact?.kind ?? '',\n title: record.artifact?.title ?? record.scopeKey,\n status: record.status,\n }\n}\n\n/** Re-validate a stored/wire part into the typed anchor; null for junk. */\nexport function persistedPartToWorkProduct(part: Record<string, unknown>): WorkProductPersistedPart | null {\n if (!part || part.type !== 'work_product') return null\n const ref = asRecord(part.ref)\n if (!ref || !nonEmptyString(ref.id) || typeof ref.version !== 'number' || !Number.isFinite(ref.version)) return null\n if (typeof part.kind !== 'string' || !nonEmptyString(part.title) || !isWorkProductStatus(part.status)) return null\n return {\n type: 'work_product',\n ref: { id: ref.id as string, version: ref.version },\n kind: part.kind,\n title: part.title as string,\n status: part.status,\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  unresolvedBlockingExceptions
3
- } from "./chunk-F2CBC4DY.js";
3
+ } from "./chunk-V3HO43PG.js";
4
4
 
5
5
  // src/work-product/queue.ts
6
6
  var OPEN_STATUSES = /* @__PURE__ */ new Set(["draft", "blocked", "ready", "changes_requested"]);
@@ -138,4 +138,4 @@ export {
138
138
  projectReviewQueue,
139
139
  parseReviewQueueItem
140
140
  };
141
- //# sourceMappingURL=chunk-UP33Z633.js.map
141
+ //# sourceMappingURL=chunk-ZRSUCTST.js.map
@@ -1,6 +1,6 @@
1
1
  import { Part } from '@tangle-network/agent-interface';
2
2
  import { a as ChatInteractionField, C as ChatInteractionStatus, I as InteractionAnswers, N as NoticeKind } from './contract-CEewO6DI.js';
3
- import { c as WorkProductPersistedPart } from './types-XIZqIdX1.js';
3
+ import { c as WorkProductPersistedPart } from './types-DeEOhQbv.js';
4
4
  import { ChatPlanPersistedPart } from './plans/index.js';
5
5
 
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { W as WorkProductRef, a as WorkProductProvenance, b as WorkProductRecord } from './types-XIZqIdX1.js';
1
+ import { W as WorkProductRef, a as WorkProductProvenance, b as WorkProductRecord } from './types-DeEOhQbv.js';
2
2
 
3
3
  /**
4
4
  * The review queue is a PROJECTION, not a store — a client-safe pure fold of
@@ -315,6 +315,8 @@ interface PersistedTurnRow {
315
315
  interface TurnHealthSource {
316
316
  findUnansweredThreads(input: {
317
317
  minAgeMs: number;
318
+ /** Ignore user messages older than this. See {@link SweepOptions.maxAgeMs}. */
319
+ maxAgeMs: number;
318
320
  now: number;
319
321
  }): Promise<UnansweredThread[]>;
320
322
  listRecentAssistantTurns(input: {
@@ -330,6 +332,17 @@ interface SweepOptions {
330
332
  /** A user message must go unanswered this long before it counts. Guards
331
333
  * against alerting on a turn that is simply still streaming. Default 15 min. */
332
334
  minAgeMs?: number;
335
+ /**
336
+ * A user message OLDER than this is abandoned, not unanswered — it stops
337
+ * counting. Default 7 days.
338
+ *
339
+ * Without this bound the sweep is worse than useless. gtm-agent's table
340
+ * holds 384 unanswered messages whose oldest is 1,676 h (70 days) old;
341
+ * paging hourly on a backlog nobody will ever reply to is exactly how an
342
+ * alert channel gets muted, and a muted channel is the state this module
343
+ * exists to escape. The alert has to mean "something broke recently".
344
+ */
345
+ maxAgeMs?: number;
333
346
  /** How far back to judge settled turns. Default 24 h. */
334
347
  lookbackMs?: number;
335
348
  /** Cap on rows judged per sweep. Default 500. */
@@ -356,6 +369,19 @@ interface SweepResult {
356
369
  toolCallsWithoutEffect: number;
357
370
  alerts: TurnHealthAlert[];
358
371
  }
372
+ /**
373
+ * Assistant-row openers agent-app writes ITSELF when a sandbox turn fails.
374
+ *
375
+ * Kept byte-identical to the strings `createSandboxChatProducer` composes
376
+ * (`src/chat-routes/sandbox-producer.ts`). They are shell vocabulary, not
377
+ * product domain, so recognising them is this package's job — a product on
378
+ * the shared producer gets a correct sweep with no configuration.
379
+ *
380
+ * `tests/turn-health/turn-health.test.ts` pins these against the producer, so
381
+ * changing the producer's wording without changing this list fails CI rather
382
+ * than silently making dead threads look answered.
383
+ */
384
+ declare const SHELL_ERROR_REPLY_PREFIXES: readonly string[];
359
385
  /**
360
386
  * Run one sweep and deliver whatever it finds.
361
387
  *
@@ -393,6 +419,36 @@ interface D1LikeForHealth {
393
419
  declare function createD1TurnHealthSource(db: D1LikeForHealth, options?: {
394
420
  messageTable?: string;
395
421
  threadTable?: string;
422
+ /**
423
+ * Content prefixes that mark an assistant row as an ERROR SURFACE rather
424
+ * than an answer. A row matching one of these stops counting as a reply,
425
+ * so the thread keeps reporting as unanswered.
426
+ *
427
+ * This exists because the obvious rule — "an assistant row with non-empty
428
+ * content is an answer" — is wrong in the exact case that matters. On
429
+ * 2026-07-27 gtm-agent's newest assistant row read:
430
+ *
431
+ * "The sandbox model stream stopped before a clean completion.
432
+ * Error: All 2 model(s) failed. gpt-5-mini: TANGLE_HUB_URL is required …"
433
+ *
434
+ * 246 characters of well-formed prose that answers nothing. Counting it
435
+ * marks a dead product healthy — the same failure-returning-success shape
436
+ * this module exists to catch, recursing into the detector itself.
437
+ *
438
+ * There is no schema-level way to recognise it: `output_tokens IS NULL`
439
+ * looked promising until legal-agent showed 22 of 25 GENUINE replies with
440
+ * null usage — it would have reported a working product broken.
441
+ *
442
+ * Defaults to {@link SHELL_ERROR_REPLY_PREFIXES}, the openers agent-app
443
+ * ITSELF writes in `createSandboxChatProducer`. Those are not domain —
444
+ * this package composed them, so this package is what must recognise
445
+ * them, and every product on the shared producer is correct with no
446
+ * configuration. Pass your own list to ADD product-specific error prose;
447
+ * pass `[]` to disable the rule.
448
+ *
449
+ * Prefixes are bound as query parameters, never interpolated.
450
+ */
451
+ errorReplyPrefixes?: readonly string[];
396
452
  }): TurnHealthSource;
397
453
 
398
- export { type AlertSink, type AlertThrottleStore, type D1LikeForHealth, type FetchLike, type PersistedTurnRow, type SweepOptions, type SweepResult, type TurnHealthAlert, type TurnHealthCompleteInfo, type TurnHealthErrorInfo, type TurnHealthLifecycle, type TurnHealthLifecycleOptions, type TurnHealthReason, type TurnHealthSeverity, type TurnHealthSource, type TurnHealthVerdict, type TurnOutcomeInput, type UnansweredThread, classifyTurnOutcome, createConsoleAlertSink, createD1TurnHealthSource, createGuardedAlertSink, createMemoryThrottleStore, createMultiAlertSink, createThrottledAlertSink, createTurnHealthLifecycle, createWebhookAlertSink, describeReason, sweepSilentFailures, turnAlert };
454
+ export { type AlertSink, type AlertThrottleStore, type D1LikeForHealth, type FetchLike, type PersistedTurnRow, SHELL_ERROR_REPLY_PREFIXES, type SweepOptions, type SweepResult, type TurnHealthAlert, type TurnHealthCompleteInfo, type TurnHealthErrorInfo, type TurnHealthLifecycle, type TurnHealthLifecycleOptions, type TurnHealthReason, type TurnHealthSeverity, type TurnHealthSource, type TurnHealthVerdict, type TurnOutcomeInput, type UnansweredThread, classifyTurnOutcome, createConsoleAlertSink, createD1TurnHealthSource, createGuardedAlertSink, createMemoryThrottleStore, createMultiAlertSink, createThrottledAlertSink, createTurnHealthLifecycle, createWebhookAlertSink, describeReason, sweepSilentFailures, turnAlert };
@@ -255,15 +255,20 @@ function parseParts(raw) {
255
255
  }
256
256
  }
257
257
  var HOUR_MS = 36e5;
258
+ var SHELL_ERROR_REPLY_PREFIXES = [
259
+ "The sandbox model stream stopped before a clean completion.",
260
+ "The sandbox agent returned an error before producing a visible answer."
261
+ ];
258
262
  async function sweepSilentFailures(options) {
259
263
  const now = options.now ?? Date.now();
260
264
  const minAgeMs = options.minAgeMs ?? 15 * 6e4;
265
+ const maxAgeMs = options.maxAgeMs ?? 7 * 24 * HOUR_MS;
261
266
  const lookbackMs = options.lookbackMs ?? 24 * HOUR_MS;
262
267
  const limit = options.limit ?? 500;
263
268
  const emptyRateThreshold = options.emptyRateThreshold ?? 0.05;
264
269
  const minTurnsForRate = options.minTurnsForRate ?? 10;
265
270
  const [unanswered, turns] = await Promise.all([
266
- options.source.findUnansweredThreads({ minAgeMs, now }),
271
+ options.source.findUnansweredThreads({ minAgeMs, maxAgeMs, now }),
267
272
  options.source.listRecentAssistantTurns({ sinceMs: now - lookbackMs, now, limit })
268
273
  ]);
269
274
  const alerts = [];
@@ -358,9 +363,12 @@ async function sweepSilentFailures(options) {
358
363
  function createD1TurnHealthSource(db, options = {}) {
359
364
  const message = safeIdentifier(options.messageTable ?? "message");
360
365
  const thread = safeIdentifier(options.threadTable ?? "thread");
366
+ const errorPrefixes = [...options.errorReplyPrefixes ?? SHELL_ERROR_REPLY_PREFIXES];
361
367
  return {
362
- async findUnansweredThreads({ minAgeMs, now }) {
368
+ async findUnansweredThreads({ minAgeMs, maxAgeMs, now }) {
363
369
  const cutoffSeconds = Math.floor((now - minAgeMs) / 1e3);
370
+ const floorSeconds = Math.floor((now - maxAgeMs) / 1e3);
371
+ const errorClause = errorPrefixes.map((_, i) => ` AND a.content NOT LIKE ?${i + 3} || '%'`).join("");
364
372
  const { results } = await db.prepare(
365
373
  `SELECT m.thread_id AS threadId,
366
374
  COUNT(*) AS pendingMessages,
@@ -368,15 +376,16 @@ function createD1TurnHealthSource(db, options = {}) {
368
376
  FROM ${message} m
369
377
  WHERE m.role = 'user'
370
378
  AND m.created_at <= ?1
379
+ AND m.created_at >= ?2
371
380
  AND m.created_at > COALESCE(
372
381
  (SELECT MAX(a.created_at)
373
382
  FROM ${message} a
374
383
  WHERE a.thread_id = m.thread_id
375
384
  AND a.role = 'assistant'
376
- AND length(trim(a.content)) > 0), 0)
385
+ AND length(trim(a.content)) > 0${errorClause}), 0)
377
386
  GROUP BY m.thread_id
378
387
  ORDER BY oldestCreatedAt ASC`
379
- ).bind(cutoffSeconds).all();
388
+ ).bind(cutoffSeconds, floorSeconds, ...errorPrefixes).all();
380
389
  return results.map((row) => ({
381
390
  threadId: row.threadId,
382
391
  pendingMessages: Number(row.pendingMessages),
@@ -412,6 +421,7 @@ function createD1TurnHealthSource(db, options = {}) {
412
421
  }
413
422
  }
414
423
  export {
424
+ SHELL_ERROR_REPLY_PREFIXES,
415
425
  classifyTurnOutcome,
416
426
  createConsoleAlertSink,
417
427
  createD1TurnHealthSource,