@jini-ai/chat 0.5.3 → 0.5.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 (138) hide show
  1. package/CHANGELOG.md +11 -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/composer-history.d.ts +54 -0
  24. package/dist/core/composer-history.js +57 -0
  25. package/dist/core/durable-projection.d.ts +28 -0
  26. package/dist/core/durable-projection.js +71 -0
  27. package/dist/core/durable-runs/continuation.d.ts +18 -0
  28. package/dist/core/durable-runs/continuation.js +61 -0
  29. package/dist/core/durable-runs/ledger-contracts.d.ts +53 -0
  30. package/dist/core/durable-runs/ledger-contracts.js +2 -0
  31. package/dist/core/durable-runs/policies.d.ts +6 -0
  32. package/dist/core/durable-runs/policies.js +6 -0
  33. package/dist/core/durable-runs/ports.d.ts +123 -0
  34. package/dist/core/durable-runs/ports.js +2 -0
  35. package/dist/core/durable-runs/run-contracts.d.ts +17 -0
  36. package/dist/core/durable-runs/run-contracts.js +2 -0
  37. package/dist/core/durable-runs/run-lock.d.ts +4 -0
  38. package/dist/core/durable-runs/run-lock.js +4 -0
  39. package/dist/core/index.d.ts +9 -19
  40. package/dist/core/index.js +8 -19
  41. package/dist/core/messages.d.ts +2 -0
  42. package/dist/core/question-form/format.d.ts +0 -9
  43. package/dist/core/question-form/format.js +16 -2
  44. package/dist/core/question-form/normalize.js +1 -0
  45. package/dist/core/question-form/secret-answers.d.ts +38 -0
  46. package/dist/core/question-form/secret-answers.js +45 -0
  47. package/dist/core/question-form/types.d.ts +2 -0
  48. package/dist/core/question-form.d.ts +1 -0
  49. package/dist/core/question-form.js +1 -0
  50. package/dist/core/run-events/entry.d.ts +208 -16
  51. package/dist/core/run-events/entry.js +273 -39
  52. package/dist/core/transcript.d.ts +3 -2
  53. package/dist/core/transcript.js +19 -3
  54. package/dist/core/transport.d.ts +4 -0
  55. package/dist/core/user-text-redaction.d.ts +56 -0
  56. package/dist/core/user-text-redaction.js +89 -0
  57. package/dist/embed-widget/browser/page-actions.d.ts +1 -120
  58. package/dist/embed-widget/browser/page-actions.js +2 -112
  59. package/dist/embed-widget/react/embed.d.ts +1 -1
  60. package/dist/embed-widget/react/embed.js +1 -1
  61. package/dist/react/artifact-types.d.ts +9 -15
  62. package/dist/react/components/Composer.js +6 -0
  63. package/dist/react/components/MessageRow.js +24 -11
  64. package/dist/react/components/QuestionForm.d.ts +2 -1
  65. package/dist/react/components/QuestionForm.js +18 -12
  66. package/dist/react/components/QuestionsPanel.d.ts +3 -2
  67. package/dist/react/components/QuestionsPanel.js +2 -2
  68. package/dist/react/features/chat-pane/agent-tools.d.ts +1 -1
  69. package/dist/react/features/chat-pane/agent-tools.js +0 -1
  70. package/dist/react/features/chat-pane/components/AgentRuntimePicker.js +5 -2
  71. package/dist/react/features/chat-pane/components/ChatPane.d.ts +1 -1
  72. package/dist/react/features/chat-pane/components/ChatPane.js +9 -4
  73. package/dist/react/features/chat-pane/create-typed-answer-poster.d.ts +2 -1
  74. package/dist/react/features/chat-pane/create-typed-answer-poster.js +33 -1
  75. package/dist/react/features/chat-pane/hooks/useAgentRuntimePicker.hooks.js +3 -2
  76. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.d.ts +7 -1
  77. package/dist/react/features/chat-pane/hooks/useChatPane.hooks.js +38 -23
  78. package/dist/react/features/chat-pane/model-options.d.ts +10 -0
  79. package/dist/react/features/chat-pane/model-options.js +51 -0
  80. package/dist/react/features/chat-pane/rules.d.ts +3 -2
  81. package/dist/react/features/chat-pane/rules.js +12 -4
  82. package/dist/react/features/chat-pane/types.d.ts +30 -1
  83. package/dist/react/hooks/assistant-chats-contracts.d.ts +164 -0
  84. package/dist/react/hooks/assistant-chats-contracts.js +2 -0
  85. package/dist/react/hooks/assistant-chats.d.ts +7 -0
  86. package/dist/react/hooks/assistant-chats.js +960 -0
  87. package/dist/react/hooks/attachment-preview-cache.d.ts +1 -1
  88. package/dist/react/hooks/attachment-preview-cache.js +1 -1
  89. package/dist/react/hooks/composer-draft-cache.d.ts +2 -2
  90. package/dist/react/hooks/composer-draft-cache.js +2 -2
  91. package/dist/react/hooks/composer-history-storage.d.ts +8 -0
  92. package/dist/react/hooks/composer-history-storage.js +40 -0
  93. package/dist/react/hooks/fab-position.d.ts +90 -0
  94. package/dist/react/hooks/fab-position.js +271 -0
  95. package/dist/react/hooks/useComposer.d.ts +10 -1
  96. package/dist/react/hooks/useComposer.js +14 -2
  97. package/dist/react/hooks/useComposerHistory.hooks.d.ts +15 -0
  98. package/dist/react/hooks/useComposerHistory.hooks.js +90 -0
  99. package/dist/react/hooks/useComposerHistoryKeys.hooks.d.ts +9 -0
  100. package/dist/react/hooks/useComposerHistoryKeys.hooks.js +90 -0
  101. package/dist/react/hooks/useConversation.d.ts +4 -1
  102. package/dist/react/hooks/useConversation.js +20 -14
  103. package/dist/react/hooks/useRunStream.js +12 -2
  104. package/dist/react/hooks/useUserTextGuard.d.ts +11 -0
  105. package/dist/react/hooks/useUserTextGuard.js +19 -0
  106. package/dist/react/index.d.ts +14 -21
  107. package/dist/react/index.js +8 -0
  108. package/dist/server/durable-runs/accept.d.ts +25 -0
  109. package/dist/server/durable-runs/accept.js +21 -0
  110. package/dist/server/durable-runs/entry.d.ts +5 -0
  111. package/dist/server/durable-runs/entry.js +4 -0
  112. package/dist/server/durable-runs/recover.d.ts +5 -0
  113. package/dist/server/durable-runs/recover.js +147 -0
  114. package/dist/server/run-finalizer/ports.d.ts +2 -16
  115. package/dist/store/legacy/sqlite/messages.d.ts +24 -18
  116. package/dist/store/legacy/sqlite/messages.js +12 -2
  117. package/dist/store/pglite/index.d.ts +2 -1
  118. package/dist/store/postgres/index.d.ts +2 -1
  119. package/dist/store/runtime/chat-expiry-sweep.d.ts +40 -0
  120. package/dist/store/runtime/chat-expiry-sweep.js +70 -0
  121. package/dist/store/runtime/chat-history-store.d.ts +20 -0
  122. package/dist/store/runtime/chat-history-store.js +50 -0
  123. package/dist/store/runtime/durable-run-store.d.ts +21 -0
  124. package/dist/store/runtime/durable-run-store.js +342 -0
  125. package/dist/store/runtime/entry.d.ts +7 -0
  126. package/dist/store/runtime/entry.js +6 -0
  127. package/dist/store/runtime/kernel.d.ts +29 -0
  128. package/dist/store/runtime/kernel.js +2 -0
  129. package/dist/store/runtime/run-guarded-chat-store.d.ts +10 -0
  130. package/dist/store/runtime/run-guarded-chat-store.js +42 -0
  131. package/dist/store/runtime/run-ledger.d.ts +15 -0
  132. package/dist/store/runtime/run-ledger.js +206 -0
  133. package/dist/store/sql/boundary.d.ts +6 -0
  134. package/dist/store/sql/boundary.js +7 -1
  135. package/dist/store/sql/store.d.ts +3 -2
  136. package/dist/store/sql/store.js +9 -2
  137. package/dist/store/sqlite/index.d.ts +3 -2
  138. package/package.json +75 -10
@@ -0,0 +1,342 @@
1
+ import { recoveredRunEvents } from "../../core/index.js";
2
+ import { chatKernelTables } from "../sql/boundary.js";
3
+ import { hasUnknownToolCall, CONTINUATION_DIVIDER } from "../../core/durable-runs/continuation.js";
4
+ import { RECOVERY_WINDOW_MS } from "../../core/durable-runs/policies.js";
5
+ import { createChatHistoryStore } from "./chat-history-store.js";
6
+ import { runLockKey } from "../../core/durable-runs/run-lock.js";
7
+ import { runContentFromEvents } from "../../core/run-events/entry.js";
8
+ export const RUN_SLOT_BUSY = "Another answer in this chat is still running. This turn is queued in your browser.";
9
+ export class RunSlotBusyError extends Error {
10
+ constructor() { super(RUN_SLOT_BUSY); }
11
+ }
12
+ function acceptedUserMessage({ request, now }, _optional = {}) {
13
+ const context = JSON.parse(request.contextRef);
14
+ // Older clients supplied only a prompt; never invent a user identity from that transcript.
15
+ const message = context.userMessage;
16
+ if (message === undefined)
17
+ return null;
18
+ if (!message || typeof message.id !== "string" || !message.id || message.role !== "user" || typeof message.content !== "string"
19
+ || (message.createdAt !== undefined && !Number.isFinite(message.createdAt)))
20
+ throw new Error("Invalid accepted user message");
21
+ if (message.attachments !== undefined && (!Array.isArray(message.attachments) || !message.attachments.every((attachment) => attachment && typeof attachment.path === "string" && typeof attachment.name === "string"
22
+ && (attachment.kind === "file" || attachment.kind === "image"))))
23
+ throw new Error("Invalid accepted user attachments");
24
+ // User input cannot claim an assistant run's status/events. Keep the same identity and payload
25
+ // as its browser PUT, including the original clock and attachment order.
26
+ return { id: message.id, role: "user", content: message.content, createdAt: message.createdAt ?? now,
27
+ ...(message.attachments ? { attachments: message.attachments } : {}) };
28
+ }
29
+ function recoveryFields({ row, metadata }, _optional = {}) {
30
+ return { recoveryCount: metadata?.recovery_count ?? 0, recoveryDeadline: metadata?.recovery_deadline ?? null,
31
+ attemptStartedAt: metadata?.attempt_started_at ?? 0, lastProgressAt: metadata?.last_progress_at ?? row.started_at ?? row.created_at,
32
+ cancelReason: metadata?.cancel_reason ?? null };
33
+ }
34
+ function sessionFields({ metadata }, _optional = {}) {
35
+ return { sessionId: metadata?.session_id ?? null, sessionConfirmed: metadata?.session_confirmed === 1,
36
+ child: metadata?.child_pid && metadata.child_started_at ? { pid: metadata.child_pid, startedAt: metadata.child_started_at } : null,
37
+ attemptBase: savedRunEvents({ json: metadata?.attempt_base_json ?? null }, {}) };
38
+ }
39
+ function acceptedRequest({ row, metadata, transcript }, _optional = {}) {
40
+ if (metadata)
41
+ return JSON.parse(metadata.accepted_json);
42
+ return { agentId: row.agent_id ?? "claude", contextRef: JSON.stringify({ conversationId: row.conversation_id,
43
+ prompt: transcript.map((item) => `${item.role}: ${item.content}`).join("\n\n") }) };
44
+ }
45
+ function metadataForRun({ run }, _optional = {}) {
46
+ return { message_id: run.messageId, engine: run.engine, accepted_json: JSON.stringify(run.request),
47
+ recovery_count: run.recoveryCount, recovery_deadline: run.recoveryDeadline, recovery_elapsed_ms: 0,
48
+ attempt_started_at: run.attemptStartedAt, last_progress_at: run.lastProgressAt,
49
+ cancel_reason: run.cancelReason, session_id: run.sessionId, session_confirmed: run.sessionConfirmed ? 1 : 0,
50
+ child_pid: null, child_started_at: null, attempt_base_json: JSON.stringify(run.attemptBase) };
51
+ }
52
+ export function attemptCanExecute({ run }, _optional = {}) {
53
+ return Boolean(run && !run.cancelReason && ["queued", "running"].includes(run.message.runStatus ?? ""));
54
+ }
55
+ export function savedRunEvents({ json }, _optional = {}) {
56
+ try {
57
+ const value = JSON.parse(json ?? "[]");
58
+ return Array.isArray(value) ? value : [];
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ }
64
+ function continuationEvents({ current, incoming }, _optional = {}) {
65
+ let separator = -1;
66
+ // Scan backward to keep the latest separator without requiring ES2023 array methods.
67
+ for (let index = incoming.length - 1; index >= 0; index--) {
68
+ const event = incoming[index];
69
+ if (event.kind === "text" && event.text === CONTINUATION_DIVIDER) {
70
+ separator = index;
71
+ break;
72
+ }
73
+ }
74
+ const saved = [...(current.message.events ?? [])];
75
+ if (!saved.some((event) => event.kind === "text") && current.message.content)
76
+ saved.unshift({ kind: "text", text: current.message.content });
77
+ // A trailing checkpoint/tool barrier may land after the recovery probe but before its CAS.
78
+ // Build the continuation segment from the projection inside the transaction, never that probe.
79
+ if (separator >= 0)
80
+ return [...saved, ...incoming.slice(separator)];
81
+ // Empty startup retries have a recovery marker but no divider. Rebase their saved work under
82
+ // the same lock too: a checkpoint may have arrived since the recovery probe.
83
+ let marker = -1;
84
+ for (let index = incoming.length - 1; index >= 0; index--) {
85
+ const event = incoming[index];
86
+ if (event.kind === "status" && event.code === "run_recovering") {
87
+ marker = index;
88
+ break;
89
+ }
90
+ }
91
+ if (marker >= 0)
92
+ return [...recoveredRunEvents({ saved }, {}), ...incoming.slice(marker + 1)];
93
+ return runContentFromEvents({ events: incoming }).length >= current.message.content.length ? incoming : saved;
94
+ }
95
+ /** Same kernel and message lock as checkpoint/settle. Claim changes run_id before any spawn;
96
+ * old attempts cannot write projections or dispatch tools after that transaction commits. */
97
+ export function createDurableRunStore({ kernel: hostKernel, isDaemonRunId }, _optional = {}) {
98
+ const kernel = chatKernelTables({ kernel: hostKernel }, {});
99
+ async function loadSnapshot({ messageId }, _options = {}) {
100
+ const row = await kernel.run((db) => db.selectFrom("ai_chat_messages")
101
+ .innerJoin("ai_chats", "ai_chats.id", "ai_chat_messages.conversation_id")
102
+ .selectAll("ai_chat_messages").select(["owner_kind", "owner_id", "scope_id"])
103
+ .where("ai_chat_messages.id", "=", messageId).where("role", "=", "assistant").executeTakeFirst());
104
+ if (!row || !row.run_id)
105
+ return null;
106
+ const metadata = await kernel.run((db) => db.selectFrom("assistant_run_attempts").selectAll().where("message_id", "=", messageId).executeTakeFirst());
107
+ const history = createChatHistoryStore({ kernel, isDaemonRunId, scope: { scopeId: row.scope_id, ownerKind: row.owner_kind, ownerId: row.owner_id } }, {});
108
+ const transcript = await history.messages({ conversationId: row.conversation_id });
109
+ const message = transcript.find((item) => item.id === messageId);
110
+ if (!message)
111
+ return null; // Deletion may have cascaded between the two reads.
112
+ const request = acceptedRequest({ row, metadata, transcript }, {});
113
+ return {
114
+ messageId, conversationId: row.conversation_id, runId: row.run_id,
115
+ workspaceId: row.scope_id,
116
+ principalId: row.owner_kind === "user" ? row.owner_id : null,
117
+ engine: metadata?.engine ?? (isDaemonRunId({ runId: row.run_id }) ? "daemon" : "request"), message, transcript, request,
118
+ ...recoveryFields({ row, metadata }, {}), ...sessionFields({ metadata }, {}),
119
+ };
120
+ }
121
+ async function load({ messageId }, _options = {}) {
122
+ const binding = await kernel.run((db) => db.selectFrom("ai_chat_messages").select("conversation_id")
123
+ .where("id", "=", messageId).executeTakeFirst());
124
+ if (!binding)
125
+ return null;
126
+ return kernel.transaction(async () => {
127
+ // Message, attempt metadata and transcript must describe the same generation. A CAS in
128
+ // the gap between independent reads must never pair a new projection with an old session.
129
+ await kernel.lockKey(runLockKey({ messageId, conversationId: binding.conversation_id, runId: "" }));
130
+ return loadSnapshot({ messageId }, {});
131
+ });
132
+ }
133
+ async function find({ runId, principalId }, _options = {}) {
134
+ const row = await kernel.run((db) => db.selectFrom("ai_chat_messages").select("id").where("run_id", "=", runId).where("role", "=", "assistant").executeTakeFirst());
135
+ if (!row)
136
+ return null;
137
+ const run = await load({ messageId: row.id }, {});
138
+ return principalId !== undefined && run?.principalId !== principalId ? null : run;
139
+ }
140
+ async function accept(required, _options = {}) {
141
+ return kernel.transaction(async () => {
142
+ await kernel.lockKey(`chat-slot:${required.conversationId}`);
143
+ await kernel.lockKey(runLockKey(required));
144
+ const history = createChatHistoryStore({ kernel, isDaemonRunId, scope: { scopeId: required.workspaceId, ownerKind: "user", ownerId: required.principalId } }, {});
145
+ if (!await history.get({ id: required.conversationId }))
146
+ return null;
147
+ const existing = await load({ messageId: required.messageId }, {});
148
+ if (existing)
149
+ return existing.principalId === required.principalId && existing.conversationId === required.conversationId ? existing : null;
150
+ const occupied = await kernel.run((db) => db.selectFrom("ai_chat_messages").select("id")
151
+ .where("conversation_id", "=", required.conversationId).where("role", "=", "assistant")
152
+ .where("run_status", "in", ["queued", "running"]).executeTakeFirst());
153
+ if (occupied)
154
+ throw new RunSlotBusyError();
155
+ const userMessage = acceptedUserMessage({ request: required.request, now: required.now }, {});
156
+ if (userMessage) {
157
+ if (userMessage.id === required.messageId)
158
+ throw new Error("User and assistant messages must have different IDs");
159
+ // A pending/failed browser PUT must never let the assistant take the first position.
160
+ // Nested appends join this transaction and retain the conversation lock through both
161
+ // writes, so either arrival order of the idempotent PUT yields user then assistant.
162
+ const saved = await history.appendMessage({ conversationId: required.conversationId, message: userMessage });
163
+ if (!saved || saved.role !== "user")
164
+ throw new Error("Accepted user message could not be persisted");
165
+ }
166
+ const message = { id: required.messageId, role: "assistant", content: "", events: [],
167
+ runId: required.runId, runStatus: "queued", createdAt: required.now, startedAt: required.now,
168
+ ...(required.request.agentId ? { agentId: required.request.agentId } : {}) };
169
+ const savedMessage = await history.appendMessage({ conversationId: required.conversationId, message });
170
+ if (!savedMessage || savedMessage.role !== "assistant") {
171
+ // Returning null here would commit the question alone after an ID collision.
172
+ if (userMessage)
173
+ throw new Error("Accepted assistant message could not be persisted");
174
+ return null;
175
+ }
176
+ await kernel.run((db) => db.insertInto("assistant_run_attempts").values({
177
+ message_id: required.messageId, engine: "daemon", accepted_json: JSON.stringify(required.request),
178
+ recovery_count: 0, recovery_deadline: null, last_progress_at: required.now, cancel_reason: null,
179
+ recovery_elapsed_ms: 0, attempt_started_at: required.now,
180
+ session_id: null, session_confirmed: 0, child_pid: null, child_started_at: null, attempt_base_json: "[]",
181
+ }).execute());
182
+ return load({ messageId: required.messageId }, {});
183
+ });
184
+ }
185
+ async function advance({ run, nextRunId, now, events }, _options = {}) {
186
+ return kernel.transaction(async () => {
187
+ await kernel.lockKey(runLockKey(run));
188
+ const current = await load({ messageId: run.messageId }, {});
189
+ if (!current || current.cancelReason || current.runId !== run.runId)
190
+ return false;
191
+ const projection = continuationEvents({ current, incoming: events }, {});
192
+ const result = await kernel.run((db) => db.updateTable("ai_chat_messages")
193
+ .set({ run_id: nextRunId, run_status: "queued", content: runContentFromEvents({ events: projection }), events_json: JSON.stringify(projection) })
194
+ .where("id", "=", run.messageId).where("run_id", "=", run.runId)
195
+ .where("run_status", "in", ["queued", "running"]).executeTakeFirst());
196
+ if (Number(result.numUpdatedRows) === 0)
197
+ return false;
198
+ await kernel.run((db) => db.insertInto("assistant_run_attempts").values({
199
+ message_id: run.messageId, engine: run.engine, accepted_json: JSON.stringify(run.request),
200
+ recovery_count: current.recoveryCount + 1, recovery_deadline: current.recoveryDeadline ?? now + RECOVERY_WINDOW_MS,
201
+ recovery_elapsed_ms: 0, attempt_started_at: now,
202
+ last_progress_at: current.lastProgressAt, cancel_reason: null, session_id: null,
203
+ session_confirmed: 0, child_pid: null, child_started_at: null, attempt_base_json: JSON.stringify(projection),
204
+ }).onConflict((oc) => oc.column("message_id").doUpdateSet({
205
+ recovery_count: current.recoveryCount + 1, recovery_deadline: current.recoveryDeadline ?? now + RECOVERY_WINDOW_MS,
206
+ attempt_started_at: now,
207
+ session_id: null, session_confirmed: 0, child_pid: null, child_started_at: null, attempt_base_json: JSON.stringify(projection),
208
+ })).execute());
209
+ return true;
210
+ });
211
+ }
212
+ async function cancel({ runId, reason }, _options = {}) {
213
+ await kernel.transaction(async () => {
214
+ const run = await find({ runId }, {});
215
+ if (!run)
216
+ return;
217
+ await kernel.lockKey(runLockKey(run));
218
+ if (!await find({ runId }, {}))
219
+ return;
220
+ await kernel.run((db) => db.insertInto("assistant_run_attempts").values({ ...metadataForRun({ run }, {}), cancel_reason: reason })
221
+ .onConflict((oc) => oc.column("message_id").doUpdateSet({ cancel_reason: reason })).execute());
222
+ });
223
+ }
224
+ async function cancelPending(required, _options = {}) {
225
+ return kernel.transaction(async () => {
226
+ await kernel.lockKey(`chat-slot:${required.conversationId}`);
227
+ await kernel.lockKey(runLockKey(required));
228
+ const history = createChatHistoryStore({ kernel, isDaemonRunId, scope: { scopeId: required.workspaceId, ownerKind: "user", ownerId: required.principalId } }, {});
229
+ if (!await history.get({ id: required.conversationId }))
230
+ return null;
231
+ const existing = await load({ messageId: required.messageId }, {});
232
+ if (existing) {
233
+ if (existing.principalId !== required.principalId || existing.conversationId !== required.conversationId)
234
+ return null;
235
+ await cancel({ runId: existing.runId, reason: "user-stop" }, {});
236
+ return load({ messageId: existing.messageId }, {});
237
+ }
238
+ // Stop may beat acceptance or its response. A terminal tombstone for the same logical
239
+ // identity makes any later duplicate acceptance a no-op, without reserving the chat slot.
240
+ await history.appendMessage({ conversationId: required.conversationId, message: { id: required.messageId, role: "assistant", content: "",
241
+ events: [{ kind: "status", label: "Stopped." }], runId: required.runId, runStatus: "canceled", createdAt: required.now, endedAt: required.now } });
242
+ return load({ messageId: required.messageId }, {});
243
+ });
244
+ }
245
+ async function captureSession({ runId, sessionId, child, confirmed = true }, _options = {}) {
246
+ return kernel.transaction(async () => {
247
+ const run = await find({ runId }, {});
248
+ if (!run)
249
+ return false;
250
+ await kernel.lockKey(runLockKey(run));
251
+ const current = await find({ runId }, {});
252
+ if (!attemptCanExecute({ run: current }, {}) || !current)
253
+ return false;
254
+ const fields = { session_id: sessionId, session_confirmed: confirmed ? 1 : 0, ...(child ? { child_pid: child.pid, child_started_at: child.startedAt } : {}) };
255
+ await kernel.run((db) => db.insertInto("assistant_run_attempts").values({ ...metadataForRun({ run: current }, {}), ...fields })
256
+ .onConflict((oc) => oc.column("message_id").doUpdateSet(fields)).execute());
257
+ // Session and generation share the same lock/transaction; an old callback cannot write
258
+ // the conversation's locator in the gap between a successful guard and a later CAS.
259
+ await kernel.run((db) => db.insertInto("assistant_agent_sessions").values({ conversation_id: run.conversationId,
260
+ agent_id: run.request.agentId ?? "claude", session_id: sessionId, updated_at: Date.now() })
261
+ .onConflict((oc) => oc.columns(["conversation_id", "agent_id"]).doUpdateSet({ session_id: sessionId, updated_at: Date.now() })).execute());
262
+ return true;
263
+ });
264
+ }
265
+ async function clearSession({ runId, sessionId }, _options = {}) {
266
+ await kernel.transaction(async () => {
267
+ const before = await find({ runId }, {});
268
+ if (!before)
269
+ return;
270
+ await kernel.lockKey(runLockKey(before));
271
+ const run = await find({ runId }, {});
272
+ if (!run)
273
+ return;
274
+ await kernel.run((db) => db.deleteFrom("assistant_agent_sessions").where("conversation_id", "=", run.conversationId)
275
+ .where("agent_id", "=", run.request.agentId ?? "claude").where("session_id", "=", sessionId).execute());
276
+ await kernel.run((db) => db.updateTable("assistant_run_attempts").set({ session_id: null, session_confirmed: 0 })
277
+ .where("message_id", "=", run.messageId).where("session_id", "=", sessionId).execute());
278
+ });
279
+ }
280
+ async function guardTool(required, _options = {}) {
281
+ return kernel.transaction(async () => {
282
+ const before = await find({ runId: required.runId }, {});
283
+ if (!before)
284
+ return "stale";
285
+ await kernel.lockKey(runLockKey(before));
286
+ const run = await find({ runId: required.runId }, {});
287
+ if (!attemptCanExecute({ run }, {}))
288
+ return "stale";
289
+ if (!run)
290
+ return "stale";
291
+ const events = run.message.events ?? [];
292
+ const canonical = events.filter((event) => event.kind !== "tool_use" || event.name === required.toolId);
293
+ if (hasUnknownToolCall({ ...required, events: run.attemptBase }, {}) || hasUnknownToolCall({ ...required, events: canonical }, {}))
294
+ return "unknown";
295
+ if (required.toolUseId)
296
+ await writeToolEvent(run, { kind: "tool_use", id: required.toolUseId, name: required.toolId, input: required.input });
297
+ return "allowed";
298
+ });
299
+ }
300
+ async function writeToolEvent(run, event) {
301
+ await kernel.run((db) => db.updateTable("ai_chat_messages").set({ events_json: JSON.stringify([...(run.message.events ?? []), event]) })
302
+ .where("id", "=", run.messageId).where("run_id", "=", run.runId).where("run_status", "in", ["queued", "running"]).execute());
303
+ await kernel.run((db) => db.updateTable("assistant_run_attempts")
304
+ .set({ recovery_count: 0, recovery_deadline: null, recovery_elapsed_ms: 0, last_progress_at: Date.now() })
305
+ .where("message_id", "=", run.messageId).execute());
306
+ }
307
+ async function completeTool({ runId, toolUseId, content }, _options = {}) {
308
+ await kernel.transaction(async () => {
309
+ const before = await find({ runId }, {});
310
+ if (!before)
311
+ return;
312
+ await kernel.lockKey(runLockKey(before));
313
+ const run = await find({ runId }, {});
314
+ if (run)
315
+ await writeToolEvent(run, { kind: "tool_result", toolUseId, content, isError: false });
316
+ });
317
+ }
318
+ async function recoveryClock({ runId, now, active }, _options = {}) {
319
+ await kernel.transaction(async () => {
320
+ const run = await find({ runId }, {});
321
+ if (!run)
322
+ return;
323
+ await kernel.lockKey(runLockKey(run));
324
+ if (!await find({ runId }, {}))
325
+ return;
326
+ const row = await kernel.run((db) => db.selectFrom("assistant_run_attempts").selectAll().where("message_id", "=", run.messageId).executeTakeFirst())
327
+ ?? metadataForRun({ run }, {});
328
+ let deadline = row.recovery_deadline;
329
+ let elapsed = row.recovery_elapsed_ms;
330
+ if (active && deadline === null)
331
+ deadline = now + Math.max(0, RECOVERY_WINDOW_MS - elapsed);
332
+ if (!active && deadline !== null) {
333
+ elapsed = Math.max(0, RECOVERY_WINDOW_MS - (deadline - now));
334
+ deadline = null;
335
+ }
336
+ await kernel.run((db) => db.insertInto("assistant_run_attempts").values({ ...row, recovery_deadline: deadline, recovery_elapsed_ms: elapsed })
337
+ .onConflict((oc) => oc.column("message_id").doUpdateSet({ recovery_deadline: deadline, recovery_elapsed_ms: elapsed })).execute());
338
+ });
339
+ }
340
+ return { load, find, accept, advance, cancel, cancelPending, captureSession, clearSession, guardTool, completeTool, recoveryClock };
341
+ }
342
+ //# sourceMappingURL=durable-run-store.js.map
@@ -0,0 +1,7 @@
1
+ export * from "./durable-run-store.js";
2
+ export * from "./run-ledger.js";
3
+ export * from "./chat-history-store.js";
4
+ export * from "./run-guarded-chat-store.js";
5
+ export type { ChatKernel, ChatDatabase } from "./kernel.js";
6
+ export * from "./chat-expiry-sweep.js";
7
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1,6 @@
1
+ export * from "./durable-run-store.js";
2
+ export * from "./run-ledger.js";
3
+ export * from "./chat-history-store.js";
4
+ export * from "./run-guarded-chat-store.js";
5
+ export * from "./chat-expiry-sweep.js";
6
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1,29 @@
1
+ import type { StorageKernel } from "@jini-ai/db/kernel";
2
+ import type { ChatDatabase as TranscriptDatabase } from "../sql/tables.js";
3
+ /** Borrowed existing chat tables; the host owns migrations and connection lifetime. */
4
+ export type ChatDatabase = TranscriptDatabase & {
5
+ assistant_agent_sessions: {
6
+ conversation_id: string;
7
+ agent_id: string;
8
+ session_id: string;
9
+ updated_at: number;
10
+ };
11
+ assistant_run_attempts: {
12
+ message_id: string;
13
+ engine: string;
14
+ accepted_json: string;
15
+ recovery_count: number;
16
+ recovery_deadline: number | null;
17
+ recovery_elapsed_ms: number;
18
+ attempt_started_at: number;
19
+ last_progress_at: number;
20
+ cancel_reason: string | null;
21
+ session_id: string | null;
22
+ session_confirmed: number;
23
+ child_pid: number | null;
24
+ child_started_at: string | null;
25
+ attempt_base_json: string;
26
+ };
27
+ };
28
+ export type ChatKernel<DB extends ChatDatabase = ChatDatabase> = StorageKernel<DB>;
29
+ //# sourceMappingURL=kernel.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=kernel.js.map
@@ -0,0 +1,10 @@
1
+ import type { RunIdPolicy } from "../../core/durable-runs/ports.js";
2
+ import type { ChatHistoryStore } from "../ports.js";
3
+ import type { ChatRunLedger } from "../../core/durable-runs/ledger-contracts.js";
4
+ /** Bind the server-owned run write fence to an already owner-scoped transcript. */
5
+ export declare function createRunGuardedChatStore({ store, ledger, isDaemonRunId }: {
6
+ store: ChatHistoryStore;
7
+ ledger: ChatRunLedger;
8
+ isDaemonRunId: RunIdPolicy;
9
+ }, _optional?: {}): ChatHistoryStore;
10
+ //# sourceMappingURL=run-guarded-chat-store.d.ts.map
@@ -0,0 +1,42 @@
1
+ /** Bind the server-owned run write fence to an already owner-scoped transcript. */
2
+ export function createRunGuardedChatStore({ store, ledger, isDaemonRunId }, _optional = {}) {
3
+ return {
4
+ ...store,
5
+ /*
6
+ * First terminal write wins, per run (`run-ledger.ts`'s file doc). The browser and the server
7
+ * finalizer can both save the same finished turn; whichever lands second must not replace the
8
+ * first — in particular, a browser that comes back after a restart and saves "run forgotten"
9
+ * with no events must not erase the answer the finalizer already saved. The stored row is
10
+ * returned instead, read through the scoped store, so a caller that does not own the
11
+ * conversation still gets `null` exactly as before.
12
+ *
13
+ * The check and `store.appendMessage`'s write run in one ledger transaction under the run's
14
+ * lock, so nothing can settle the row in between. The store and the ledger share the chat
15
+ * kernel, so the store's own transaction joins the ledger's on every dialect.
16
+ */
17
+ async appendMessage({ conversationId, message }) {
18
+ if (message.role !== "assistant" || !message.runId)
19
+ return store.appendMessage({ conversationId, message });
20
+ const runId = message.runId;
21
+ const outcome = await ledger.unlessSettled({ run: { conversationId, messageId: message.id, runId },
22
+ write: async () => {
23
+ // Server acceptance and recovery own daemon rows. Browser snapshots can carry an old
24
+ // attempt id or a transport-only failure; returning the stored row fences both without
25
+ // matching presentation text. BYOK/AG-UI remain request-bound.
26
+ if (isDaemonRunId({ runId })) {
27
+ const saved = (await store.messages({ conversationId })).find((m) => m.id === message.id);
28
+ if (saved)
29
+ return saved;
30
+ if (message.runStatus !== "queued" && message.runStatus !== "running")
31
+ return null;
32
+ }
33
+ return store.appendMessage({ conversationId, message });
34
+ } }, {});
35
+ if (outcome.written)
36
+ return outcome.value;
37
+ const saved = await store.messages({ conversationId });
38
+ return saved.find((m) => m.id === message.id) ?? null;
39
+ },
40
+ };
41
+ }
42
+ //# sourceMappingURL=run-guarded-chat-store.js.map
@@ -0,0 +1,15 @@
1
+ import type { ChatDatabase, ChatKernel } from "./kernel.js";
2
+ import type { RunIdPolicy } from "../../core/durable-runs/ports.js";
3
+ export type * from "../../core/durable-runs/ledger-contracts.js";
4
+ import type { ChatRunLedger } from "../../core/durable-runs/ledger-contracts.js";
5
+ export { runLockKey } from "../../core/durable-runs/run-lock.js";
6
+ /**
7
+ * @param store the chat kernel, or the open `chat.db` handle `createChatStoreFactory` also wraps.
8
+ * @complexity each method is one or two indexed statements, except `reconcileInterrupted`, which
9
+ * is O(stuck rows).
10
+ */
11
+ export declare function createChatRunLedger<DB extends ChatDatabase>({ kernel: hostKernel, isDaemonRunId }: {
12
+ kernel: ChatKernel<DB>;
13
+ isDaemonRunId: RunIdPolicy;
14
+ }, _optional?: {}): ChatRunLedger;
15
+ //# sourceMappingURL=run-ledger.d.ts.map