@tangle-network/agent-app 0.50.3 → 0.50.5

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 (33) hide show
  1. package/dist/assistant/index.js +2 -2
  2. package/dist/channels/hooks.d.ts +5 -13
  3. package/dist/channels/index.js +8 -22
  4. package/dist/channels/index.js.map +1 -1
  5. package/dist/channels/numbers.d.ts +1 -1
  6. package/dist/channels/payment.d.ts +1 -1
  7. package/dist/chat-routes/index.js +3 -3
  8. package/dist/{chunk-KJEW23GR.js → chunk-3MIQCPUX.js} +2 -2
  9. package/dist/{chunk-FRVVHMZO.js → chunk-5DEQ2FJK.js} +2 -2
  10. package/dist/{chunk-3NM4GCAY.js → chunk-DPO7FGCN.js} +9 -6
  11. package/dist/chunk-DPO7FGCN.js.map +1 -0
  12. package/dist/{chunk-GEHULTVU.js → chunk-P5ZJQKOI.js} +31 -40
  13. package/dist/{chunk-GEHULTVU.js.map → chunk-P5ZJQKOI.js.map} +1 -1
  14. package/dist/{chunk-3UBAO3N5.js → chunk-Q3ZHNN5X.js} +38 -61
  15. package/dist/chunk-Q3ZHNN5X.js.map +1 -0
  16. package/dist/hosted-agent/application.js +1 -0
  17. package/dist/hosted-agent/application.js.map +1 -1
  18. package/dist/hosted-agent/react/index.js +9 -8
  19. package/dist/hosted-agent/react/index.js.map +1 -1
  20. package/dist/public-consultation/index.js +2 -2
  21. package/dist/sandbox/index.js +1 -1
  22. package/dist/sandbox/recovery.d.ts +31 -29
  23. package/dist/stream/index.js +1 -1
  24. package/dist/stream/turn-identity.d.ts +16 -11
  25. package/dist/web-react/async/index.js +1 -1
  26. package/dist/web-react/async/use-async-resource.d.ts +2 -2
  27. package/dist/web-react/async/use-confirmed-mutation.d.ts +6 -3
  28. package/dist/web-react/index.js +2 -2
  29. package/package.json +1 -1
  30. package/dist/chunk-3NM4GCAY.js.map +0 -1
  31. package/dist/chunk-3UBAO3N5.js.map +0 -1
  32. /package/dist/{chunk-KJEW23GR.js.map → chunk-3MIQCPUX.js.map} +0 -0
  33. /package/dist/{chunk-FRVVHMZO.js.map → chunk-5DEQ2FJK.js.map} +0 -0
@@ -17,7 +17,7 @@ function buildUserTextParts(text, turnId) {
17
17
  }
18
18
  function messageHasTurnId(message, turnId) {
19
19
  for (const part of message.parts ?? []) {
20
- if (part && typeof part === "object" && String(part.turnId ?? "") === turnId) {
20
+ if (part && typeof part === "object" && "turnId" in part && String(part.turnId ?? "") === turnId) {
21
21
  return true;
22
22
  }
23
23
  }
@@ -33,10 +33,11 @@ function resolveChatTurn(input) {
33
33
  );
34
34
  if (reusableIndex >= 0) {
35
35
  const reusedId = existingMessages[reusableIndex]?.id;
36
+ const priorMessages = existingMessages.slice(0, reusableIndex);
36
37
  return {
37
- turnIndex: countUserMessages(existingMessages.slice(0, reusableIndex)),
38
+ turnIndex: countUserMessages(priorMessages),
38
39
  shouldInsertUserMessage: false,
39
- priorMessages: existingMessages.slice(0, reusableIndex),
40
+ priorMessages,
40
41
  userParts: buildUserTextParts(userContent, turnId),
41
42
  ...typeof reusedId === "string" && reusedId ? { reusedUserMessageId: reusedId } : {}
42
43
  };
@@ -44,7 +45,7 @@ function resolveChatTurn(input) {
44
45
  return {
45
46
  turnIndex: countUserMessages(existingMessages),
46
47
  shouldInsertUserMessage: true,
47
- priorMessages: existingMessages,
48
+ priorMessages: existingMessages.slice(),
48
49
  userParts: buildUserTextParts(userContent, turnId)
49
50
  };
50
51
  }
@@ -65,7 +66,9 @@ function findReusableUserMessageIndex(messages, userContent, turnId, hasRunningT
65
66
  return -1;
66
67
  }
67
68
  function countUserMessages(messages) {
68
- return messages.filter((message) => message.role === "user").length;
69
+ let count = 0;
70
+ for (const message of messages) if (message.role === "user") count++;
71
+ return count;
69
72
  }
70
73
 
71
74
  // src/stream/turn-observation.ts
@@ -167,4 +170,4 @@ export {
167
170
  observeTurnEvent,
168
171
  consumeTurnStream
169
172
  };
170
- //# sourceMappingURL=chunk-3NM4GCAY.js.map
173
+ //# sourceMappingURL=chunk-DPO7FGCN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stream/turn-identity.ts","../src/stream/turn-observation.ts"],"sourcesContent":["import type { JsonRecord } from './stream-normalizer'\n\n/** Minimum read-only shape; typed chat parts need no dictionary index signature. */\ninterface TurnIdentityMessage {\n readonly id: string\n readonly role: 'user' | 'assistant' | 'system' | 'tool'\n readonly content: string\n readonly parts: readonly object[] | null\n}\n\n/** Retained compatibility shape for callers using unstructured message records. */\nexport interface PersistedChatMessageForTurn extends TurnIdentityMessage {\n parts: Array<Record<string, unknown>> | null\n}\n\n/** Represent a chat turn with resolved user message insertion and prior message context */\nexport interface ResolvedChatTurn<Message extends TurnIdentityMessage = PersistedChatMessageForTurn> {\n turnIndex: number\n shouldInsertUserMessage: boolean\n priorMessages: Message[]\n userParts: JsonRecord[]\n /** The id of the user row this turn REUSES (retry dedup), when one was\n * found. Absent on the insert path, where no row exists yet.\n *\n * The reused row is deliberately EXCLUDED from `priorMessages` (it is this\n * turn's own user message, not prior context), so this field is the only\n * way a caller can name it. */\n reusedUserMessageId?: string\n}\n\n/** Normalize and validate a client turn ID string ensuring it meets format and length requirements */\nexport function normalizeClientTurnId(value: unknown): string | undefined {\n if (value === undefined || value === null) return undefined\n if (typeof value !== 'string') throw new Error('turnId must be a string')\n const trimmed = value.trim()\n if (!trimmed) throw new Error('turnId must not be blank')\n if (trimmed.length > 160) throw new Error('turnId is too long')\n if (!/^[A-Za-z0-9:_-]+$/.test(trimmed)) {\n throw new Error('turnId contains unsupported characters')\n }\n return trimmed\n}\n\n/** Build an array of text parts with optional turn ID for user input */\nexport function buildUserTextParts(text: string, turnId: string | undefined): JsonRecord[] {\n const part: JsonRecord = { type: 'text', text }\n if (turnId) part.turnId = turnId\n return [part]\n}\n\n/** Resolve whether a message contains any part with the specified turn ID */\nexport function messageHasTurnId(message: Pick<TurnIdentityMessage, 'parts'>, turnId: string): boolean {\n for (const part of message.parts ?? []) {\n if (part && typeof part === 'object' && 'turnId' in part && String(part.turnId ?? '') === turnId) {\n return true\n }\n }\n return false\n}\n\n/** Resolve a chat turn by determining message reuse and constructing user message parts */\nexport function resolveChatTurn<Message extends TurnIdentityMessage>(input: {\n existingMessages: readonly Message[]\n userContent: string\n turnId?: string\n /** True when the thread has a turn still RUNNING in the turn-event buffer\n * (`turnStore.listRunning(threadId)`).\n *\n * Without incremental persistence the trailing row of a thread mid-turn is\n * always the user row, so the content fallback below could assume it. With\n * incremental persistence the assistant row lands seconds into the turn, so\n * a retry of that same turn finds an ASSISTANT row trailing and would\n * insert a duplicate user row.\n *\n * This flag is the discriminator that keeps both cases right, and it needs\n * no new state: an assistant row trailing a turn that is still running is\n * that turn's in-flight draft (walk past it — this is a retry), whereas an\n * assistant row trailing a SETTLED turn is a completed answer (stop — the\n * user genuinely repeated a message and deserves a new turn). */\n hasRunningTurn?: boolean\n}): ResolvedChatTurn<Message> {\n const { existingMessages, userContent, turnId } = input\n const reusableIndex = findReusableUserMessageIndex(\n existingMessages,\n userContent,\n turnId,\n input.hasRunningTurn === true,\n )\n if (reusableIndex >= 0) {\n // Guarded read: `id` is typed `string`, but these rows come from a product\n // store the shell does not validate, so an adapter that omits it must\n // surface as \"no id\" rather than as the string \"undefined\".\n const reusedId = existingMessages[reusableIndex]?.id\n const priorMessages = existingMessages.slice(0, reusableIndex)\n return {\n turnIndex: countUserMessages(priorMessages),\n shouldInsertUserMessage: false,\n priorMessages,\n userParts: buildUserTextParts(userContent, turnId),\n ...(typeof reusedId === 'string' && reusedId ? { reusedUserMessageId: reusedId } : {}),\n }\n }\n\n return {\n turnIndex: countUserMessages(existingMessages),\n shouldInsertUserMessage: true,\n priorMessages: existingMessages.slice(),\n userParts: buildUserTextParts(userContent, turnId),\n }\n}\n\nfunction findReusableUserMessageIndex(\n messages: readonly TurnIdentityMessage[],\n userContent: string,\n turnId: string | undefined,\n hasRunningTurn: boolean,\n): number {\n if (turnId) {\n for (let index = messages.length - 1; index >= 0; index -= 1) {\n const message = messages[index]\n if (message?.role === 'user' && messageHasTurnId(message, turnId)) return index\n }\n // A caller-supplied id is authoritative. A different id with identical\n // text is a deliberate new turn, while a transport retry reuses the same\n // id and takes the match above. Falling through to content dedup here lets\n // an unrelated abandoned running row swallow an intentional repeat.\n return -1\n }\n\n // Content fallback for a client that sends no turnId. Only the trailing rows\n // of a turn still RUNNING are walked past (they are that turn's incrementally\n // persisted assistant draft); a settled assistant row still ends the scan, so\n // a user who genuinely repeats a message gets a new turn exactly as before.\n let index = messages.length - 1\n if (hasRunningTurn) {\n while (index >= 0 && messages[index]?.role === 'assistant') index -= 1\n }\n const latest = index >= 0 ? messages[index] : undefined\n if (latest?.role === 'user' && latest.content === userContent) return index\n\n return -1\n}\n\nfunction countUserMessages(messages: readonly TurnIdentityMessage[]): number {\n let count = 0\n for (const message of messages) if (message.role === 'user') count++\n return count\n}\n","/** Read-side checkpoints for native chat NDJSON. This never submits or cancels work. */\nexport interface TurnObservation {\n schema: 'turn-observation-v1'\n streamId: string | null\n /** Replay ordinals only. Unsequenced live frames cannot advance this cursor. */\n lastSeq: number\n outcome: 'completed' | 'failed' | null\n replayStatus: string | null\n eventCount: number\n}\n\nexport function parseTurnObservation(value?: unknown): TurnObservation {\n if (value === undefined) return {\n schema: 'turn-observation-v1', streamId: null, lastSeq: 0,\n outcome: null, replayStatus: null, eventCount: 0,\n }\n if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('Invalid turn observation')\n const v = value as Record<string, unknown>\n if (v.schema !== 'turn-observation-v1'\n || !(v.streamId === null || (typeof v.streamId === 'string' && /^[A-Za-z0-9_-]+$/.test(v.streamId)))\n || !Number.isSafeInteger(v.lastSeq) || Number(v.lastSeq) < 0\n || !Number.isSafeInteger(v.eventCount) || Number(v.eventCount) < 0\n || !(v.outcome === null || v.outcome === 'completed' || v.outcome === 'failed')\n || !(v.replayStatus === null || typeof v.replayStatus === 'string')\n || (v.streamId === null && Number(v.lastSeq) > 0)) throw new TypeError('Invalid turn observation')\n return {\n schema: 'turn-observation-v1', streamId: v.streamId as string | null,\n lastSeq: Number(v.lastSeq), outcome: v.outcome as TurnObservation['outcome'],\n replayStatus: v.replayStatus as string | null, eventCount: Number(v.eventCount),\n }\n}\n\nexport interface TurnObservationUpdate {\n checkpoint: TurnObservation\n /** False only for an already-checkpointed replay ordinal. */\n accepted: boolean\n event: Record<string, unknown>\n}\n\n/** Reduce an observed frame. Ordered replay gaps are errors, not silent lost work. */\nexport function observeTurnEvent(previous: TurnObservation, raw: unknown): TurnObservationUpdate {\n const checkpoint = parseTurnObservation(previous)\n if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new TypeError('Turn frame must be an object')\n const outer = raw as Record<string, unknown>\n const inner = outer.kind === 'event' ? outer.event : outer\n if (!inner || typeof inner !== 'object' || Array.isArray(inner)) throw new TypeError('Invalid turn event envelope')\n const event = inner as Record<string, unknown>\n if (typeof event.type !== 'string' || !event.type) throw new TypeError('Turn frame requires a type')\n\n // Even a duplicate marker must not switch which execution we are observing.\n if (event.type === 'turn') {\n if (typeof event.turnId !== 'string' || !/^[A-Za-z0-9_-]+$/.test(event.turnId)) throw new TypeError('Invalid stream identity')\n if (checkpoint.streamId !== null && checkpoint.streamId !== event.turnId) throw new Error('Replay stream identity changed')\n checkpoint.streamId = event.turnId\n }\n const seq = outer.seq\n if (seq !== undefined && seq !== -1) {\n if (!Number.isSafeInteger(seq) || Number(seq) < 1) throw new TypeError('Invalid replay ordinal')\n if (checkpoint.streamId === null) throw new Error('Replay ordinal arrived before stream identity')\n if (Number(seq) <= checkpoint.lastSeq) return { checkpoint, accepted: false, event }\n if (Number(seq) !== checkpoint.lastSeq + 1) throw new Error('Replay has a gap; retain the checkpoint and inspect durable history')\n checkpoint.lastSeq = Number(seq)\n } else if (seq === -1 && event.type !== 'turn_status') {\n throw new TypeError('Only a turn-status sentinel may have ordinal -1')\n }\n\n if (event.type === 'error' || event.type === 'session.run.failed') checkpoint.outcome = 'failed'\n if (event.type === 'session.run.completed' && checkpoint.outcome !== 'failed') checkpoint.outcome = 'completed'\n if (event.type === 'turn_status') {\n if (typeof event.status !== 'string') throw new TypeError('Turn status requires a value')\n checkpoint.replayStatus = event.status\n if (['error', 'failed', 'aborted', 'cancelled'].includes(event.status)) checkpoint.outcome = 'failed'\n if (['complete', 'completed'].includes(event.status) && checkpoint.outcome !== 'failed') checkpoint.outcome = 'completed'\n }\n checkpoint.eventCount += 1\n return { checkpoint, accepted: true, event }\n}\n\nexport interface ConsumeTurnStreamOptions {\n checkpoint?: TurnObservation\n /** Commit evidence and its checkpoint durably before acknowledging this frame. */\n commit(update: TurnObservationUpdate): Promise<void>\n /** Bound untrusted frame memory, not total run duration or total transcript size. */\n maxFrameBytes?: number\n}\n\n/**\n * Consume one viewing connection. EOF without a terminal event stays incomplete.\n * A disconnect/abort propagates; the last committed checkpoint remains resumable.\n * The caller owns network observation windows separately from execution deadlines.\n */\nexport async function consumeTurnStream(\n body: AsyncIterable<Uint8Array>,\n options: ConsumeTurnStreamOptions,\n): Promise<TurnObservation> {\n const maxFrameBytes = options.maxFrameBytes ?? 4 * 1024 * 1024\n if (!Number.isSafeInteger(maxFrameBytes) || maxFrameBytes < 1) throw new RangeError('Frame byte limit must be positive')\n let state = parseTurnObservation(options.checkpoint)\n const decoder = new TextDecoder('utf-8', { fatal: true })\n let pending = ''\n let frameBytes = 0\n async function accept(line: string): Promise<void> {\n if (!line.trim()) return\n const update = observeTurnEvent(state, JSON.parse(line))\n if (update.accepted) await options.commit(update)\n state = update.checkpoint\n }\n for await (const chunk of body) {\n // Count bytes before decoding: a malicious unterminated frame cannot grow\n // without bound, and splitting UTF-8 characters across chunks stays valid.\n let start = 0\n for (let end = 0; end < chunk.length; end += 1) {\n if (chunk[end] !== 10) continue\n frameBytes += end - start\n if (frameBytes > maxFrameBytes) throw new RangeError('Turn frame exceeds byte limit')\n pending += decoder.decode(chunk.subarray(start, end + 1), { stream: true })\n await accept(pending)\n pending = ''; frameBytes = 0; start = end + 1\n }\n frameBytes += chunk.length - start\n if (frameBytes > maxFrameBytes) throw new RangeError('Turn frame exceeds byte limit')\n pending += decoder.decode(chunk.subarray(start), { stream: true })\n }\n pending += decoder.decode()\n await accept(pending)\n return state\n}\n"],"mappings":"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","names":["index"]}
@@ -172,18 +172,18 @@ var CODES = {
172
172
  [WORKSPACE_SANDBOX_UNRECOVERABLE]: { snapshotUsable: false }
173
173
  };
174
174
  var ACTIONS = {
175
- confirmation_required: { replacementChosen: false },
176
- deletion_declined: { replacementChosen: false },
175
+ confirmation_required: { replacementChosen: false, canDecide: true },
176
+ deletion_declined: { replacementChosen: false, canDecide: true },
177
177
  replacement_authorized: { replacementChosen: false },
178
178
  snapshot_replacement_authorized: { replacementChosen: false },
179
- replacement_started: { replacementChosen: true },
179
+ replacement_started: { replacementChosen: true, completedAs: "replacement_completed" },
180
180
  replacement_completed: { replacementChosen: true },
181
- snapshot_replacement_started: { replacementChosen: true },
182
- snapshot_restore_failed: { replacementChosen: true },
181
+ snapshot_replacement_started: { replacementChosen: true, completedAs: "snapshot_replacement_completed" },
182
+ snapshot_restore_failed: { replacementChosen: true, completedAs: "snapshot_replacement_completed" },
183
183
  snapshot_replacement_completed: { replacementChosen: true },
184
- missing_replacement_started: { replacementChosen: true },
184
+ missing_replacement_started: { replacementChosen: true, completedAs: "missing_replacement_completed" },
185
185
  missing_replacement_completed: { replacementChosen: true },
186
- unrecoverable_replacement_started: { replacementChosen: true },
186
+ unrecoverable_replacement_started: { replacementChosen: true, completedAs: "unrecoverable_replacement_completed" },
187
187
  unrecoverable_replacement_completed: { replacementChosen: true }
188
188
  };
189
189
  var WorkspaceSandboxRecoveryRequiredError = class extends Error {
@@ -313,50 +313,41 @@ function shouldRestoreWorkspaceSandboxRecovery(recovery) {
313
313
  return !!preferredWorkspaceSandboxRecoveryBoxKey(recovery) && recovery.snapshot.availability === "available";
314
314
  }
315
315
  function createWorkspaceSandboxRecoveryManager(store) {
316
- async function record(workspaceId, recovery) {
317
- await store.write(workspaceId, recovery);
316
+ async function transition(workspaceId, expected, next) {
317
+ if (store.compareAndSet) {
318
+ if (!await store.compareAndSet(workspaceId, expected, next))
319
+ throw new Error("Sandbox recovery changed; reload before retrying its transition");
320
+ } else {
321
+ await store.write(workspaceId, next);
322
+ }
323
+ return next;
318
324
  }
319
325
  return {
320
- read: store.read,
321
- record,
326
+ read: (workspaceId) => store.read(workspaceId),
327
+ record: (workspaceId, recovery) => store.write(workspaceId, recovery),
322
328
  async decide({ workspaceId, sandboxId, decision, replacementBoxKey }) {
323
329
  const current = await store.read(workspaceId);
324
- if (!current || current.sandboxId !== sandboxId) return void 0;
325
- const next = {
330
+ if (!current || current.sandboxId !== sandboxId || !("canDecide" in ACTIONS[current.action])) return void 0;
331
+ if (decision === "replace" && !asNonEmptyString(replacementBoxKey))
332
+ throw new Error("Sandbox replacement authorization requires a fresh replacement box key");
333
+ return transition(workspaceId, current, {
326
334
  ...current,
327
335
  action: decision === "replace" ? current.snapshot.availability === "available" ? "snapshot_replacement_authorized" : "replacement_authorized" : "deletion_declined",
328
336
  confirmedAt: (/* @__PURE__ */ new Date()).toISOString(),
329
- ...decision === "replace" && replacementBoxKey ? { replacementBoxKey } : {}
330
- };
331
- await record(workspaceId, next);
332
- return next;
337
+ ...decision === "replace" ? { replacementBoxKey } : {}
338
+ });
333
339
  },
334
- async complete({ workspaceId, replacementSandboxId }) {
340
+ async complete({ workspaceId, replacementSandboxId, replacementBoxKey }) {
335
341
  const current = await store.read(workspaceId);
336
- if (!current) return void 0;
337
- const next = {
338
- ...current,
339
- action: completionFor(current.action),
340
- replacementSandboxId
341
- };
342
- await record(workspaceId, next);
343
- return next;
342
+ if (!current || !asNonEmptyString(current.replacementBoxKey)) return void 0;
343
+ if (replacementBoxKey !== void 0 && current.replacementBoxKey !== replacementBoxKey) return void 0;
344
+ const policy = ACTIONS[current.action];
345
+ if (!("completedAs" in policy)) return void 0;
346
+ if (!asNonEmptyString(replacementSandboxId)) throw new Error("A replacement sandbox id is required");
347
+ return transition(workspaceId, current, { ...current, action: policy.completedAs, replacementSandboxId });
344
348
  }
345
349
  };
346
350
  }
347
- function completionFor(action) {
348
- switch (action) {
349
- case "missing_replacement_started":
350
- return "missing_replacement_completed";
351
- case "unrecoverable_replacement_started":
352
- return "unrecoverable_replacement_completed";
353
- case "snapshot_replacement_started":
354
- case "snapshot_replacement_authorized":
355
- return "snapshot_replacement_completed";
356
- default:
357
- return "replacement_completed";
358
- }
359
- }
360
351
  var WORKSPACE_SANDBOX_RECOVERY_ACTIONS = Object.keys(
361
352
  ACTIONS
362
353
  );
@@ -2845,4 +2836,4 @@ export {
2845
2836
  isTerminalPromptEvent,
2846
2837
  detectInteractiveQuestion
2847
2838
  };
2848
- //# sourceMappingURL=chunk-GEHULTVU.js.map
2839
+ //# sourceMappingURL=chunk-P5ZJQKOI.js.map