@jini-ai/chat 0.5.4 → 0.5.6

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 (122) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/core/ag-ui/entry.d.ts +33 -0
  3. package/dist/core/ag-ui/entry.js +80 -0
  4. package/dist/core/agentic/chat-capabilities.d.ts +2 -4
  5. package/dist/core/agentic/index.d.ts +4 -17
  6. package/dist/core/agentic/index.js +4 -17
  7. package/dist/core/assistant-client/a2ui-action-poster.d.ts +46 -0
  8. package/dist/core/assistant-client/a2ui-action-poster.js +113 -0
  9. package/dist/core/assistant-client/ag-ui-transport.d.ts +68 -0
  10. package/dist/core/assistant-client/ag-ui-transport.js +147 -0
  11. package/dist/core/assistant-client/attachment-liveness.d.ts +37 -0
  12. package/dist/core/assistant-client/attachment-liveness.js +93 -0
  13. package/dist/core/assistant-client/durable-run-subscription.d.ts +39 -0
  14. package/dist/core/assistant-client/durable-run-subscription.js +96 -0
  15. package/dist/core/assistant-client/entry.d.ts +13 -0
  16. package/dist/core/assistant-client/entry.js +7 -0
  17. package/dist/core/assistant-client/messages.d.ts +73 -0
  18. package/dist/core/assistant-client/messages.js +68 -0
  19. package/dist/core/assistant-client/transport.d.ts +164 -0
  20. package/dist/core/assistant-client/transport.js +734 -0
  21. package/dist/core/browser/page-actions.d.ts +121 -0
  22. package/dist/core/browser/page-actions.js +113 -0
  23. package/dist/core/durable-runs/continuation.d.ts +18 -0
  24. package/dist/core/durable-runs/continuation.js +61 -0
  25. package/dist/core/durable-runs/ledger-contracts.d.ts +53 -0
  26. package/dist/core/durable-runs/ledger-contracts.js +2 -0
  27. package/dist/core/durable-runs/policies.d.ts +6 -0
  28. package/dist/core/durable-runs/policies.js +6 -0
  29. package/dist/core/durable-runs/ports.d.ts +123 -0
  30. package/dist/core/durable-runs/ports.js +2 -0
  31. package/dist/core/durable-runs/run-contracts.d.ts +17 -0
  32. package/dist/core/durable-runs/run-contracts.js +2 -0
  33. package/dist/core/durable-runs/run-lock.d.ts +4 -0
  34. package/dist/core/durable-runs/run-lock.js +4 -0
  35. package/dist/core/index.d.ts +6 -19
  36. package/dist/core/index.js +6 -19
  37. package/dist/core/messages.d.ts +2 -0
  38. package/dist/core/question-form/format.d.ts +0 -9
  39. package/dist/core/question-form/format.js +16 -2
  40. package/dist/core/question-form/normalize.js +1 -0
  41. package/dist/core/question-form/secret-answers.d.ts +38 -0
  42. package/dist/core/question-form/secret-answers.js +45 -0
  43. package/dist/core/question-form/types.d.ts +2 -0
  44. package/dist/core/question-form.d.ts +1 -0
  45. package/dist/core/question-form.js +1 -0
  46. package/dist/core/run-events/entry.d.ts +208 -16
  47. package/dist/core/run-events/entry.js +273 -39
  48. package/dist/core/transcript.d.ts +3 -2
  49. package/dist/core/transcript.js +19 -3
  50. package/dist/core/user-text-redaction.d.ts +56 -0
  51. package/dist/core/user-text-redaction.js +89 -0
  52. package/dist/embed-widget/browser/page-actions.d.ts +1 -120
  53. package/dist/embed-widget/browser/page-actions.js +2 -112
  54. package/dist/embed-widget/react/embed.d.ts +1 -1
  55. package/dist/embed-widget/react/embed.js +1 -1
  56. package/dist/react/artifact-types.d.ts +9 -15
  57. package/dist/react/components/QuestionForm.d.ts +2 -1
  58. package/dist/react/components/QuestionForm.js +18 -12
  59. package/dist/react/components/QuestionsPanel.d.ts +3 -2
  60. package/dist/react/components/QuestionsPanel.js +2 -2
  61. package/dist/react/features/chat-pane/agent-tools.d.ts +1 -1
  62. package/dist/react/features/chat-pane/agent-tools.js +0 -1
  63. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -2
  64. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  65. package/dist/react/features/chat-pane/components/ChatPane.js +5 -2
  66. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +2 -1
  67. package/dist/react/features/chat-pane/create-typed-answer-poster.js +33 -1
  68. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +3 -2
  69. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +4 -1
  70. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +43 -22
  71. package/dist/react/features/chat-pane/model-options.d.ts +12 -0
  72. package/dist/react/features/chat-pane/model-options.js +55 -0
  73. package/dist/react/features/chat-pane/rules.d.ts +3 -2
  74. package/dist/react/features/chat-pane/rules.js +12 -4
  75. package/dist/react/features/chat-pane/types.d.ts +25 -1
  76. package/dist/react/hooks/assistant-chats-contracts.d.ts +164 -0
  77. package/dist/react/hooks/assistant-chats-contracts.js +2 -0
  78. package/dist/react/hooks/assistant-chats.d.ts +7 -0
  79. package/dist/react/hooks/assistant-chats.js +960 -0
  80. package/dist/react/hooks/attachment-preview-cache.d.ts +1 -1
  81. package/dist/react/hooks/attachment-preview-cache.js +1 -1
  82. package/dist/react/hooks/composer-draft-cache.d.ts +2 -2
  83. package/dist/react/hooks/composer-draft-cache.js +2 -2
  84. package/dist/react/hooks/fab-position.d.ts +90 -0
  85. package/dist/react/hooks/fab-position.js +271 -0
  86. package/dist/react/hooks/useConversation.d.ts +4 -1
  87. package/dist/react/hooks/useConversation.js +14 -12
  88. package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
  89. package/dist/react/hooks/useUserTextGuard.js +19 -0
  90. package/dist/react/index.d.ts +12 -21
  91. package/dist/react/index.js +7 -0
  92. package/dist/server/durable-runs/accept.d.ts +25 -0
  93. package/dist/server/durable-runs/accept.js +21 -0
  94. package/dist/server/durable-runs/entry.d.ts +5 -0
  95. package/dist/server/durable-runs/entry.js +4 -0
  96. package/dist/server/durable-runs/recover.d.ts +5 -0
  97. package/dist/server/durable-runs/recover.js +147 -0
  98. package/dist/server/run-finalizer/ports.d.ts +2 -16
  99. package/dist/store/legacy/sqlite/messages.d.ts +24 -18
  100. package/dist/store/legacy/sqlite/messages.js +12 -2
  101. package/dist/store/pglite/index.d.ts +2 -1
  102. package/dist/store/postgres/index.d.ts +2 -1
  103. package/dist/store/runtime/chat-expiry-sweep.d.ts +40 -0
  104. package/dist/store/runtime/chat-expiry-sweep.js +70 -0
  105. package/dist/store/runtime/chat-history-store.d.ts +20 -0
  106. package/dist/store/runtime/chat-history-store.js +50 -0
  107. package/dist/store/runtime/durable-run-store.d.ts +21 -0
  108. package/dist/store/runtime/durable-run-store.js +342 -0
  109. package/dist/store/runtime/entry.d.ts +7 -0
  110. package/dist/store/runtime/entry.js +6 -0
  111. package/dist/store/runtime/kernel.d.ts +29 -0
  112. package/dist/store/runtime/kernel.js +2 -0
  113. package/dist/store/runtime/run-guarded-chat-store.d.ts +10 -0
  114. package/dist/store/runtime/run-guarded-chat-store.js +42 -0
  115. package/dist/store/runtime/run-ledger.d.ts +15 -0
  116. package/dist/store/runtime/run-ledger.js +206 -0
  117. package/dist/store/sql/boundary.d.ts +6 -0
  118. package/dist/store/sql/boundary.js +7 -1
  119. package/dist/store/sql/store.d.ts +3 -2
  120. package/dist/store/sql/store.js +9 -2
  121. package/dist/store/sqlite/index.d.ts +3 -2
  122. package/package.json +75 -10
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @file Restore cached attachment references only after the host confirms their bytes still exist.
3
+ * Drafts persist references separately from text; staged bytes have a host-defined TTL. Dropping
4
+ * an unconfirmed live reference costs a re-drag, while restoring a dead one causes a failed turn.
5
+ * Ref shape, endpoint and probe cap enter through host ports: malformed/excess refs, refusals,
6
+ * network failures and timeouts are discarded, without losing the draft's words.
7
+ * HEAD avoids downloading every attachment to answer a liveness question. The host endpoint must
8
+ * support it (Express routes HEAD through a GET handler when no HEAD handler exists and omits the
9
+ * body); get-chat-attachment-route.test.ts covers the original composed route.
10
+ */
11
+ /** In-flight probes. Matches `createDaemonAttachmentUploader`'s own upload concurrency — enough to
12
+ * hide localhost latency, few enough to leave the page's connection budget alone. */
13
+ const DEFAULT_CONCURRENCY = 2;
14
+ /** Abort deadline per probe. A restore runs at mount behind the composer, so a wedged request must
15
+ * not keep a worker (and the operator's attachment chips) pending indefinitely. */
16
+ const DEFAULT_TIMEOUT_MS = 10_000;
17
+ /**
18
+ * Whether the server still serves this one reference.
19
+ *
20
+ * @param ref opaque `attachment:<uuid>` capability id (`ChatAttachment.path`).
21
+ * @returns `true` only on a 2xx. Every refusal and every fault answers `false` — the caller cannot
22
+ * act differently on them, and a probe that could not reach the server has not confirmed anything.
23
+ * @complexity O(1); one request, abandoned after `timeoutMs`.
24
+ */
25
+ async function isAttachmentStillServed(ref, options) {
26
+ try {
27
+ const response = await options.fetchImpl(options.endpoint({ ref }, {}), {
28
+ method: "HEAD",
29
+ credentials: "same-origin",
30
+ signal: AbortSignal.timeout(options.timeoutMs),
31
+ });
32
+ return response.ok;
33
+ }
34
+ catch {
35
+ // A network fault, an abort, or a same-origin policy refusal. Indistinguishable from here, and
36
+ // all mean the same thing: nothing was confirmed, so the reference does not come back.
37
+ return false;
38
+ }
39
+ }
40
+ /**
41
+ * Runs `probe` over `refs` with at most `concurrency` in flight, answering positionally.
42
+ *
43
+ * Shared-iterator workers rather than a chunked `Promise.all` loop, for the same reason
44
+ * `createDaemonAttachmentUploader` uses them: a chunked loop stalls the whole batch on its slowest
45
+ * member, and this one runs while the operator is looking at a half-restored composer.
46
+ *
47
+ * @complexity O(n) requests, at most `concurrency` concurrent; O(n) space for the verdicts.
48
+ */
49
+ async function probeAll(refs, concurrency, probe) {
50
+ const verdicts = new Array(refs.length).fill(false);
51
+ const entries = refs.entries();
52
+ async function worker() {
53
+ for (const [index, ref] of entries) {
54
+ verdicts[index] = await probe(ref);
55
+ }
56
+ }
57
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(concurrency, refs.length)) }, worker));
58
+ return verdicts;
59
+ }
60
+ /**
61
+ * Builds the `validateAttachments` callback `<ChatPane>` calls when it restores a persisted draft.
62
+ *
63
+ * @param input.fetch injected request implementation; the host supplies it explicitly.
64
+ * @param input.endpoint resolves a reference to the host's HEAD-capable read endpoint.
65
+ * @param input.isSupportedRef validates the host's staged-reference grammar before a request.
66
+ * @param input.maxProbedAttachments bounds request fan-out from malformed or hand-edited drafts.
67
+ * @param options.concurrency in-flight probe cap; defaults to {@link DEFAULT_CONCURRENCY}.
68
+ * @param options.timeoutMs per-probe abort deadline; defaults to {@link DEFAULT_TIMEOUT_MS}.
69
+ * @returns a validator taking the cached references and resolving to the subset still served, in
70
+ * the order they were given. Never rejects: a caller's only sane response to a rejection is to
71
+ * restore nothing, which is what an empty subset already means.
72
+ * @complexity O(n) requests bounded by input.maxProbedAttachments, at most concurrency at once.
73
+ * @example
74
+ * <ChatPane validateAttachments={createChatAttachmentValidator({ fetch, endpoint, isSupportedRef, maxProbedAttachments })} />
75
+ */
76
+ export function createChatAttachmentValidator({ fetch, endpoint, isSupportedRef, maxProbedAttachments }, options = {}) {
77
+ const probeOptions = {
78
+ fetchImpl: fetch,
79
+ endpoint,
80
+ timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
81
+ };
82
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
83
+ return async (attachments) => {
84
+ const candidates = attachments
85
+ .slice(0, maxProbedAttachments)
86
+ .filter((candidate) => isSupportedRef({ ref: candidate.path }, {}));
87
+ if (candidates.length === 0)
88
+ return [];
89
+ const verdicts = await probeAll(candidates.map((candidate) => candidate.path), concurrency, (ref) => isAttachmentStillServed(ref, probeOptions));
90
+ return candidates.filter((_, index) => verdicts[index]);
91
+ };
92
+ }
93
+ //# sourceMappingURL=attachment-liveness.js.map
@@ -0,0 +1,39 @@
1
+ import type { ChatMessage } from "../messages.js";
2
+ import type { RunHandlers } from "../transport.js";
3
+ import type { RunNotices } from "../run-events/entry.js";
4
+ export interface DurableRunBinding {
5
+ readonly messageId: string;
6
+ readonly conversationId: string;
7
+ }
8
+ export interface DurableSubscriptionPorts {
9
+ readonly read: (required: {
10
+ runId: string;
11
+ messageId?: string;
12
+ }, optional: {}) => Promise<ChatMessage | null | "gone">;
13
+ readonly stream: (required: {
14
+ runId: string;
15
+ cursor: string;
16
+ frame: (kind: string, data: string, cursor: string) => void;
17
+ dropped: () => void;
18
+ }, optional: {}) => () => void;
19
+ readonly schedule: (required: {
20
+ work: () => void;
21
+ delayMs: number;
22
+ }, optional: {}) => () => void;
23
+ }
24
+ type CheckpointHandlers = RunHandlers & {
25
+ onCheckpoint?: (message: ChatMessage) => void;
26
+ };
27
+ /** SSE is a view of one attempt. The saved message owns terminal state and can point at a new
28
+ * attempt after recovery; neither a browser 404 nor a closed stream may fail the logical run. */
29
+ export declare function followDurableRun({ runId: initialRunId, handlers, signal, binding, ports, notices, bindings }: {
30
+ runId: string;
31
+ handlers: CheckpointHandlers;
32
+ signal?: AbortSignal;
33
+ binding?: DurableRunBinding;
34
+ ports: DurableSubscriptionPorts;
35
+ notices: RunNotices;
36
+ bindings: Map<string, DurableRunBinding>;
37
+ }, _optional?: {}): () => void;
38
+ export {};
39
+ //# sourceMappingURL=durable-run-subscription.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { translateRunFrame } from "../run-events/entry.js";
2
+ /** SSE is a view of one attempt. The saved message owns terminal state and can point at a new
3
+ * attempt after recovery; neither a browser 404 nor a closed stream may fail the logical run. */
4
+ export function followDurableRun({ runId: initialRunId, handlers, signal, binding, ports, notices, bindings }, _optional = {}) {
5
+ let runId = initialRunId;
6
+ let cursor = "";
7
+ let stopped = false;
8
+ let reading = false;
9
+ let dropped = false;
10
+ let stopStream;
11
+ let stopTimer;
12
+ let messageId = binding?.messageId;
13
+ function stop() {
14
+ stopped = true;
15
+ stopStream?.();
16
+ stopTimer?.();
17
+ signal?.removeEventListener("abort", stop);
18
+ }
19
+ function finish(message) {
20
+ stop();
21
+ handlers.onCheckpoint?.(message);
22
+ if (message.runStatus === "failed" && !handlers.onCheckpoint)
23
+ handlers.onError(new Error("The assistant could not finish this answer. Saved work is above."));
24
+ handlers.onDone(message.events ?? []);
25
+ }
26
+ function open() {
27
+ dropped = false;
28
+ stopStream?.();
29
+ stopStream = ports.stream({ runId, cursor, frame, dropped: () => { dropped = true; void read().catch(() => undefined); } }, {});
30
+ }
31
+ function frame(kind, data, nextCursor) {
32
+ if (stopped)
33
+ return;
34
+ if (nextCursor)
35
+ cursor = nextCursor;
36
+ const outcome = translateRunFrame({ kind, raw: data, notices }, {});
37
+ // An attempt's error/end may be followed by a continuation. Only the row can finalize it.
38
+ if (outcome.terminal || outcome.error) {
39
+ void read().catch(() => undefined);
40
+ return;
41
+ }
42
+ // With the checkpoint-capable hook, render only durable projections. Replaying a daemon
43
+ // prefix into a seeded bubble would duplicate its text; the server folds those deltas once.
44
+ if (handlers.onCheckpoint)
45
+ return;
46
+ for (const event of outcome.events)
47
+ handlers.onEvent(event);
48
+ }
49
+ async function read() {
50
+ if (stopped || reading)
51
+ return;
52
+ reading = true;
53
+ stopTimer?.();
54
+ stopTimer = undefined;
55
+ try {
56
+ const message = await ports.read({ runId, ...(messageId ? { messageId } : {}) }, {});
57
+ if (stopped || !message)
58
+ return;
59
+ if (message === "gone") {
60
+ stop();
61
+ return;
62
+ }
63
+ applySavedMessage(message);
64
+ }
65
+ finally {
66
+ reading = false;
67
+ if (!stopped)
68
+ stopTimer = ports.schedule({ work: () => void read().catch(() => undefined), delayMs: 1_000 }, {});
69
+ }
70
+ }
71
+ function applySavedMessage(message) {
72
+ messageId = message.id;
73
+ if (["succeeded", "failed", "canceled"].includes(message.runStatus ?? "")) {
74
+ finish(message);
75
+ return;
76
+ }
77
+ handlers.onCheckpoint?.(message);
78
+ if (message.runId && message.runId !== runId) {
79
+ runId = message.runId;
80
+ cursor = "";
81
+ if (binding)
82
+ bindings.set(runId, binding);
83
+ dropped = true;
84
+ }
85
+ if (dropped)
86
+ open();
87
+ }
88
+ if (signal?.aborted)
89
+ return stop;
90
+ signal?.addEventListener("abort", stop);
91
+ open();
92
+ // Polling also covers an attempt that dies before a first frame/connection error arrives.
93
+ stopTimer = ports.schedule({ work: () => void read().catch(() => undefined), delayMs: 1_000 }, {});
94
+ return stop;
95
+ }
96
+ //# sourceMappingURL=durable-run-subscription.js.map
@@ -0,0 +1,13 @@
1
+ /** Daemon/BYOK/AG-UI chat client lifecycles. Host routes, identity and authenticated I/O are ports. */
2
+ export { createAssistantTransportClient } from "./transport.js";
3
+ export type { AssistantTransportPorts, AssistantTransportOptions, AssistantTransportClient, ByokFrameContext, ByokStreamContext } from "./transport.js";
4
+ export type { AgUiClient, AgUiClientPorts, AgUiRunClient, AgUiEventContext } from "./ag-ui-transport.js";
5
+ export { followDurableRun } from "./durable-run-subscription.js";
6
+ export type { DurableRunBinding, DurableSubscriptionPorts } from "./durable-run-subscription.js";
7
+ export { activeRunStub, messageWriteKey, persistableMessages } from "./messages.js";
8
+ export type { AssistantConversation } from "./messages.js";
9
+ export { createChatAttachmentValidator } from "./attachment-liveness.js";
10
+ export type { ChatAttachmentValidatorInput, ChatAttachmentValidatorOptions } from "./attachment-liveness.js";
11
+ export { createA2uiActionPoster } from "./a2ui-action-poster.js";
12
+ export type { A2uiActionPosterInput, CreateA2uiActionPosterOptions, A2uiActionDeliveryOutcome } from "./a2ui-action-poster.js";
13
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1,7 @@
1
+ /** Daemon/BYOK/AG-UI chat client lifecycles. Host routes, identity and authenticated I/O are ports. */
2
+ export { createAssistantTransportClient } from "./transport.js";
3
+ export { followDurableRun } from "./durable-run-subscription.js";
4
+ export { activeRunStub, messageWriteKey, persistableMessages } from "./messages.js";
5
+ export { createChatAttachmentValidator } from "./attachment-liveness.js";
6
+ export { createA2uiActionPoster } from "./a2ui-action-poster.js";
7
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1,73 @@
1
+ import type { ChatMessage } from "../messages.js";
2
+ export interface AssistantConversation {
3
+ id: string;
4
+ title: string | null;
5
+ titleSource: "fallback" | "generated" | "manual";
6
+ messageCount: number;
7
+ createdAt: number;
8
+ updatedAt: number;
9
+ }
10
+ /**
11
+ * The subset of a live transcript that is worth writing: user turns, and assistant turns whose run
12
+ * has reached a terminal status.
13
+ *
14
+ * An assistant message that is still `queued` or `running` is rewritten on every delta, so
15
+ * persisting it would turn one reply into hundreds of writes for a row that is about to be
16
+ * replaced anyway.
17
+ *
18
+ * A `failed`/`canceled` assistant turn with EMPTY content is kept deliberately, and this is the
19
+ * place a future "stop writing those, they are just noise" change would land — don't (2026-09-11
20
+ * chat-lifecycle repair, Defect 3). The row's `events_json` is the only durable record of why the
21
+ * run died: the daemon's own event log is in-memory and gone with the process, so the stored
22
+ * `"Run failed — the agent process exited without answering"` notice with its exit code and signal
23
+ * is all that survives, and the pane renders it on reload. The real problem those rows caused was
24
+ * being replayed into the NEXT run's prompt as if they were answers, and that is fixed on the read
25
+ * side instead — see `assistant-transport.ts`'s `historyForTranscript`/`isAnsweredAssistantTurn`.
26
+ * Deleting them here would trade a history bug for a forensics hole.
27
+ */
28
+ export declare function persistableMessages({ messages }: {
29
+ messages: ChatMessage[];
30
+ }, _options?: Record<string, never>): ChatMessage[];
31
+ /**
32
+ * The key the admin's chat writers use to remember "this message is already saved": the message id,
33
+ * plus the run id when there is one.
34
+ *
35
+ * The run id is part of the key because `@jini-ai/chat`'s `retry` keeps the assistant message id and
36
+ * starts a NEW run under it. Keyed by id alone, the first run had already marked the id saved, so the
37
+ * retry's in-flight stub was never written (and the API finalizer never followed it) and its final
38
+ * answer was never saved either. A message with no run id (a user turn, an old row) keys by id alone.
39
+ */
40
+ export declare function messageWriteKey({ message }: {
41
+ message: Pick<ChatMessage, "id" | "runId">;
42
+ }, _options?: Record<string, never>): string;
43
+ /**
44
+ * The one message worth a durable "this run is in flight" stub write — the newest message, if it is
45
+ * a non-terminal assistant turn that has acquired a `runId`. `null` when there is nothing to record.
46
+ *
47
+ * Exists for reattach (2026-09-11 investigation): {@link persistableMessages} above deliberately
48
+ * never durably writes a `queued`/`running` assistant row (this file's own module doc explains why —
49
+ * avoiding a write per streamed token), which means today a run's `runId` exists NOWHERE outside the
50
+ * live browser tab that started it. Reload the tab, switch conversations and back, or lose the tab
51
+ * to a crash mid-run, and the id needed to resume watching that run is gone even though the run
52
+ * itself is still going server-side — there is nothing left for a reattach to find. `useAssistantChats`'s
53
+ * `persistRunStub` calls this once per `onMessagesChange` delta and writes at most once per message
54
+ * id (see that function's own doc), so this adds exactly one extra `PUT` per run, not one per token.
55
+ *
56
+ * Scoped to the LAST message only, not "any non-terminal assistant message anywhere in the array":
57
+ * `useConversation.ts`'s `sendMessage`/`retry` always append the run's assistant placeholder last,
58
+ * so a non-terminal row earlier in the array can only be a stale leftover from an earlier defect or
59
+ * race — recording ITS id as "the current run" would point a future reattach at the wrong turn.
60
+ *
61
+ * `saveMessage`'s server-side route upserts by message id (`ON CONFLICT(id) DO UPDATE`,
62
+ * `Jini/packages/sqlite`'s `chat-history/store.ts`), so this stub write and the message's eventual
63
+ * terminal-state write (through the normal {@link persistableMessages} path) converge on the same
64
+ * row rather than creating two — the terminal write simply overwrites `run_status`/`content`/
65
+ * `events_json` in place once the run settles.
66
+ *
67
+ * @complexity O(1) — reads only the last array element, unlike {@link persistableMessages}'s O(n)
68
+ * scan, so calling this on every delta alongside the existing `flush` call costs nothing material.
69
+ */
70
+ export declare function activeRunStub({ messages }: {
71
+ messages: ChatMessage[];
72
+ }, _options?: Record<string, never>): ChatMessage | null;
73
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { isTerminalRunStatus } from "../messages.js";
2
+ /**
3
+ * The subset of a live transcript that is worth writing: user turns, and assistant turns whose run
4
+ * has reached a terminal status.
5
+ *
6
+ * An assistant message that is still `queued` or `running` is rewritten on every delta, so
7
+ * persisting it would turn one reply into hundreds of writes for a row that is about to be
8
+ * replaced anyway.
9
+ *
10
+ * A `failed`/`canceled` assistant turn with EMPTY content is kept deliberately, and this is the
11
+ * place a future "stop writing those, they are just noise" change would land — don't (2026-09-11
12
+ * chat-lifecycle repair, Defect 3). The row's `events_json` is the only durable record of why the
13
+ * run died: the daemon's own event log is in-memory and gone with the process, so the stored
14
+ * `"Run failed — the agent process exited without answering"` notice with its exit code and signal
15
+ * is all that survives, and the pane renders it on reload. The real problem those rows caused was
16
+ * being replayed into the NEXT run's prompt as if they were answers, and that is fixed on the read
17
+ * side instead — see `assistant-transport.ts`'s `historyForTranscript`/`isAnsweredAssistantTurn`.
18
+ * Deleting them here would trade a history bug for a forensics hole.
19
+ */
20
+ export function persistableMessages({ messages }, _options = {}) {
21
+ return messages.filter((message) => message.role === "user" || isTerminalRunStatus({ status: message.runStatus }));
22
+ }
23
+ /**
24
+ * The key the admin's chat writers use to remember "this message is already saved": the message id,
25
+ * plus the run id when there is one.
26
+ *
27
+ * The run id is part of the key because `@jini-ai/chat`'s `retry` keeps the assistant message id and
28
+ * starts a NEW run under it. Keyed by id alone, the first run had already marked the id saved, so the
29
+ * retry's in-flight stub was never written (and the API finalizer never followed it) and its final
30
+ * answer was never saved either. A message with no run id (a user turn, an old row) keys by id alone.
31
+ */
32
+ export function messageWriteKey({ message }, _options = {}) {
33
+ return message.runId === undefined ? message.id : `${message.id}:${message.runId}`;
34
+ }
35
+ /**
36
+ * The one message worth a durable "this run is in flight" stub write — the newest message, if it is
37
+ * a non-terminal assistant turn that has acquired a `runId`. `null` when there is nothing to record.
38
+ *
39
+ * Exists for reattach (2026-09-11 investigation): {@link persistableMessages} above deliberately
40
+ * never durably writes a `queued`/`running` assistant row (this file's own module doc explains why —
41
+ * avoiding a write per streamed token), which means today a run's `runId` exists NOWHERE outside the
42
+ * live browser tab that started it. Reload the tab, switch conversations and back, or lose the tab
43
+ * to a crash mid-run, and the id needed to resume watching that run is gone even though the run
44
+ * itself is still going server-side — there is nothing left for a reattach to find. `useAssistantChats`'s
45
+ * `persistRunStub` calls this once per `onMessagesChange` delta and writes at most once per message
46
+ * id (see that function's own doc), so this adds exactly one extra `PUT` per run, not one per token.
47
+ *
48
+ * Scoped to the LAST message only, not "any non-terminal assistant message anywhere in the array":
49
+ * `useConversation.ts`'s `sendMessage`/`retry` always append the run's assistant placeholder last,
50
+ * so a non-terminal row earlier in the array can only be a stale leftover from an earlier defect or
51
+ * race — recording ITS id as "the current run" would point a future reattach at the wrong turn.
52
+ *
53
+ * `saveMessage`'s server-side route upserts by message id (`ON CONFLICT(id) DO UPDATE`,
54
+ * `Jini/packages/sqlite`'s `chat-history/store.ts`), so this stub write and the message's eventual
55
+ * terminal-state write (through the normal {@link persistableMessages} path) converge on the same
56
+ * row rather than creating two — the terminal write simply overwrites `run_status`/`content`/
57
+ * `events_json` in place once the run settles.
58
+ *
59
+ * @complexity O(1) — reads only the last array element, unlike {@link persistableMessages}'s O(n)
60
+ * scan, so calling this on every delta alongside the existing `flush` call costs nothing material.
61
+ */
62
+ export function activeRunStub({ messages }, _options = {}) {
63
+ const last = messages.at(-1);
64
+ if (!last || last.role !== "assistant" || last.runId === undefined)
65
+ return null;
66
+ return isTerminalRunStatus({ status: last.runStatus }) ? null : last;
67
+ }
68
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1,164 @@
1
+ import type { AgentEvent } from "../events.js";
2
+ import type { ChatMessage } from "../messages.js";
3
+ import type { ChatTransport, RunHandlers, StartRunInput } from "../transport.js";
4
+ import type { ExecutionConfig } from "@jini-ai/ui";
5
+ import type { RunNotices } from "../run-events/entry.js";
6
+ import type { createAgUiRunClient } from "./ag-ui-transport.js";
7
+ import type { DurableRunBinding } from "./durable-run-subscription.js";
8
+ export interface AssistantTransportOptions {
9
+ /**
10
+ * Read fresh on every `startRun` call, never captured once — matching `AssistantDock.tsx`'s own
11
+ * `runContext` convention (see that file's doc on why `frontendBindToken` is read the same way):
12
+ * the operator can flip the runtime picker's mode mid-session, and a captured value would keep
13
+ * routing every later turn through whichever mode was selected when the transport was first
14
+ * built (`AssistantDock.tsx` memoizes the transport once, for the reason its own comment gives —
15
+ * rebuilding it would drop in-flight runs).
16
+ */
17
+ getExecutionConfig?: (required: Record<string, never>, optional: Record<string, never>) => ExecutionConfig;
18
+ /**
19
+ * ADR-059's AG-UI canary toggle — deliberately NOT a value on {@link ExecutionConfig}: that type
20
+ * is `@jini-ai/ui`'s own closed `'local-cli' | 'byok'` union, a separate repo/package, and
21
+ * extending it would mean a cross-repo edit this ADR explicitly avoids (Decision 1: "a zero-touch
22
+ * addition to Jini"). Read fresh on every `startRun` call, same "never captured" convention
23
+ * {@link getExecutionConfig} already establishes, for the same reason: an operator can flip this
24
+ * mid-session without rebuilding the memoized transport.
25
+ */
26
+ getAgUiEnabled?: (required: Record<string, never>, optional: Record<string, never>) => boolean;
27
+ /**
28
+ * Agent ids whose own CLI/ACP session already carries multi-turn conversation memory across
29
+ * spawns — `@jini-ai/agent-runtime`'s `resumesSessionViaCli`/`resumesSessionViaAcpLoad`, projected
30
+ * client-safe as `AssistantAgentSummary.carriesOwnMemory` (`assistant/agents.ts` in the host server).
31
+ * {@link runPrompt}'s own doc has the full "used to send only the newest message" background; this
32
+ * option is the inverse regression it introduced for exactly these agents — see `startRun`'s Local
33
+ * CLI branch below, which resends the doc's own contract instead of the full transcript ONLY for an
34
+ * agentId this set contains: "the caller trusts this adapter's CLI to carry its own multi-turn
35
+ * conversation memory... and should skip resending the rendered transcript on follow-up turns"
36
+ * (`types.ts`). Resending it anyway would duplicate everything the daemon's own `--resume`/
37
+ * `session/load` already restores (`agent-session-resume.ts`).
38
+ *
39
+ * Read fresh on every `startRun` call, same "never captured" convention {@link getExecutionConfig}/
40
+ * {@link getAgUiEnabled} already establish: the admin's own `/api/agents` probe resolves
41
+ * asynchronously and can still be in flight when this transport is first built, and an operator can
42
+ * switch the Local CLI agent pick mid-session.
43
+ *
44
+ * Omitted, or an agentId absent from the returned set, keeps today's behavior — the full transcript
45
+ * is sent. That is the SAFE default for a def this option's source has not resolved as resume-capable
46
+ * yet: sending redundant history to a resume-capable def costs extra tokens, but withholding history
47
+ * from a stateless def would silently erase its memory — the two failure directions are not
48
+ * symmetric, so "unknown" must resolve to "send everything," not to "send only the latest message."
49
+ */
50
+ getResumeCapableAgentIds?: (required: Record<string, never>, optional: Record<string, never>) => ReadonlySet<string>;
51
+ /**
52
+ * This pane's conversation id, adopting one if none is active yet —
53
+ * `useAssistantChats.ensureConversationId` (`hooks/use-assistant-chats.hooks.ts`), wired through
54
+ * `AssistantDock`'s `useAssistantTransport`. Awaited by `startRun` ONLY when `input.context` names
55
+ * no conversation, which in practice means the first turn of a chat.
56
+ *
57
+ * Why the transport has to be the one to ask: the admin adopts a conversation lazily, from the
58
+ * first message delta — and `@jini-ai/chat`'s `useChatPane.sendPrompt` freezes `runContext(...)`
59
+ * into `input.context` BEFORE calling `conversation.sendMessage(...)`, the very call that produces
60
+ * that delta. So on turn 1 there is no id to capture yet, and `runContext` (synchronous by
61
+ * contract) has no way to wait for one. `startRun` is the last point in the chain that can still
62
+ * `await`, which makes it the only place the run can acquire the identity its agent-CLI session id
63
+ * gets filed under. Without it, `agent-daemon-server.ts`'s `onStarted` skipped its entire
64
+ * session-capture subscription for turn 1, and turn 2 — sent only the bare latest user message,
65
+ * trusting a resume that had nothing stored — answered with none of the conversation.
66
+ *
67
+ * Contract: resolves `null` rather than rejecting when creation fails. `startRun` treats that as
68
+ * "send the turn anyway, with no conversationId" — one run with no resumable session is a far
69
+ * smaller loss than a run that never happens.
70
+ */
71
+ ensureConversationId?: (required: Record<string, never>, optional: Record<string, never>) => Promise<string | null>;
72
+ /**
73
+ * Writes one user message to durable storage — `useAssistantChats.persistUserTurn`
74
+ * (`hooks/use-assistant-chats.hooks.ts`), wired through `AssistantDock`'s `useAssistantTransport`.
75
+ * Awaited by `startRun` on the Local CLI path before `POST /api/runs`, including a delta's pending
76
+ * PUT. Acceptance persists the same message with its stub if the browser write fails. See
77
+ * {@link persistUserTurnBeforeDispatch} for the defect and the idempotency contract.
78
+ *
79
+ * Optional for transports without the dock's persistence hook; acceptance still receives the
80
+ * exact user message, and the delta-driven `flush` can write the same ID afterward.
81
+ * Its rejection is swallowed at the call site rather than failing the turn.
82
+ */
83
+ persistUserTurn?: (required: {
84
+ conversationId: string;
85
+ message: ChatMessage;
86
+ }, optional: Record<string, never>) => Promise<void>;
87
+ }
88
+ export interface AssistantTransportPorts {
89
+ readonly runsUrl: string;
90
+ readonly byokTurnUrl: string;
91
+ readonly byokRunIdPrefix: string;
92
+ readonly notices: RunNotices;
93
+ readonly fetch: typeof fetch;
94
+ readonly mintId: (required: Record<string, never>, optional: Record<string, never>) => string;
95
+ readonly projectPrompt: (required: {
96
+ prompt: string;
97
+ context: StartRunInput["context"];
98
+ }, optional: Record<string, never>) => string;
99
+ readonly projectInput: (required: {
100
+ input: StartRunInput;
101
+ }, optional: Record<string, never>) => StartRunInput;
102
+ readonly bindRun: (required: {
103
+ runId: string;
104
+ binding: DurableRunBinding;
105
+ }, optional: Record<string, never>) => void;
106
+ readonly runBinding: (required: {
107
+ runId: string;
108
+ }, optional: Record<string, never>) => DurableRunBinding | undefined;
109
+ readonly followRun: (required: {
110
+ runId: string;
111
+ handlers: RunHandlers;
112
+ signal?: AbortSignal;
113
+ }, optional: Record<string, never>) => void;
114
+ readonly acceptRun: (required: {
115
+ body: unknown;
116
+ signal?: AbortSignal;
117
+ requestSignal?: AbortSignal;
118
+ messageId: unknown;
119
+ conversationId?: string;
120
+ }, optional: Record<string, never>) => Promise<Response>;
121
+ readonly agUi: ReturnType<typeof createAgUiRunClient>;
122
+ }
123
+ export interface ByokFrameContext {
124
+ collected: AgentEvent[];
125
+ handlers: RunHandlers;
126
+ finish: () => void;
127
+ }
128
+ export interface ByokStreamContext extends ByokFrameContext {
129
+ runId: string;
130
+ controller: AbortController;
131
+ }
132
+ export interface AssistantTransportClient {
133
+ createAssistantTransport(required: {
134
+ options?: AssistantTransportOptions;
135
+ }, optional?: Record<string, never>): ChatTransport;
136
+ historyForTranscript(required: {
137
+ history: readonly ChatMessage[];
138
+ }, optional?: Record<string, never>): ChatMessage[];
139
+ undeliveredUserPrompt(required: {
140
+ history: readonly ChatMessage[];
141
+ }, optional?: Record<string, never>): string;
142
+ buildLocalCliContextRef(required: {
143
+ input: StartRunInput;
144
+ prompt: string;
145
+ resolvedConversationId?: string;
146
+ }, optional?: Record<string, never>): Record<string, unknown>;
147
+ handleByokFrame(required: {
148
+ frame: {
149
+ event: string;
150
+ data: string;
151
+ };
152
+ ctx: ByokFrameContext;
153
+ }, optional?: Record<string, never>): void;
154
+ consumeByokStream(required: {
155
+ body: ReadableStream<Uint8Array>;
156
+ ctx: ByokStreamContext;
157
+ }, optional?: Record<string, never>): Promise<void>;
158
+ }
159
+ /** Bind transport effects once; in-flight controllers belong to this client, never the package.
160
+ * Positional run methods implement the existing ChatTransport contract. */
161
+ export declare function createAssistantTransportClient({ ports }: {
162
+ ports: AssistantTransportPorts;
163
+ }, _options?: Record<string, never>): AssistantTransportClient;
164
+ //# sourceMappingURL=transport.d.ts.map