@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,147 @@
1
+ import { recoveredRunEvents } from "../../core/index.js";
2
+ import { classifyAgentServiceFailure } from "@jini-ai/agent-runtime";
3
+ import { continuationRequest } from "../../core/durable-runs/continuation.js";
4
+ import { RECOVERY_LIMIT, RECOVERY_WINDOW_MS, RUN_STALENESS_MS, SAVED_WORK_NOTICE, EXHAUSTED_NOTICE } from "../../core/durable-runs/policies.js";
5
+ export { RECOVERY_LIMIT, RECOVERY_WINDOW_MS, RUN_STALENESS_MS, SAVED_WORK_NOTICE, EXHAUSTED_NOTICE } from "../../core/durable-runs/policies.js";
6
+ /** Retryability is independent of session resumability. Inspect executor diagnostics, never
7
+ * answer text or tool results: those can quote unrelated failures from completed work. Old
8
+ * attempt diagnostics remain visible but cannot decide the fate of their continuation. */
9
+ function attemptFailure(run) {
10
+ const events = run.message.events ?? [];
11
+ // Checkpoints merge adjacent text, so a base's event count is not a durable offset. The
12
+ // coordinator's recovery marker separates attempts and survives that compaction.
13
+ let marker = -1;
14
+ // Reverse scan preserves the last-marker rule on the desktop's ES2022 target.
15
+ for (let index = run.attemptBase.length ? events.length - 1 : -1; index >= 0; index--) {
16
+ const event = events[index];
17
+ if (event.kind === "status" && event.code === "run_recovering") {
18
+ marker = index;
19
+ break;
20
+ }
21
+ }
22
+ const diagnostics = events.slice(marker >= 0 ? marker + 1 : run.attemptBase.length).flatMap((event) => event.kind === "status" ? [event.label, event.detail ?? ""] : event.kind === "raw" ? [event.line] : []).join("\n");
23
+ if (/inactivity[_ -]?(timeout|watchdog)|watchdog.*(kill|stop)/i.test(diagnostics))
24
+ return "inactivity-watchdog";
25
+ const service = classifyAgentServiceFailure({ text: diagnostics });
26
+ if (service === "AGENT_AUTH_REQUIRED")
27
+ return "auth-failure";
28
+ // Quota/billing and bad requests need human repair. A 429 for rate throttling and a 5xx
29
+ // overload remain retryable; the provider's coarse RATE_LIMITED class also includes billing.
30
+ const requestStatuses = diagnostics.matchAll(/\b(?:http(?:[ /]?\d(?:\.\d)?)?|(?:status|error|response)(?:[ _-]?code)?|code(?=\s*[:=#]))[\s:=#-]*(4\d\d)\b/gi);
31
+ if ([...requestStatuses].some((match) => match[1] !== "408" && match[1] !== "429"))
32
+ return "permanent";
33
+ if (/another answer in this chat is still running|invalid[_ -]?request|context[_ -]?(length|window)|maximum context|model.*(?:not[_ -]found|does not exist|not supported)|unsupported|does not support|permission denied|forbidden|insufficient[_ -]?(quota|balance|credit|funds)|credit balance is too low|exceeded your current quota|quota[_ -]?exceeded|usage limit|billing|payment required/i.test(diagnostics))
34
+ return "permanent";
35
+ return "retryable";
36
+ }
37
+ function exclusion(run, failure) {
38
+ if (run.cancelReason)
39
+ return run.cancelReason;
40
+ if (!run.principalId || run.engine !== "daemon")
41
+ return "excluded";
42
+ if (failure === "auth-failure" || failure === "inactivity-watchdog")
43
+ return failure;
44
+ return null;
45
+ }
46
+ function budgetNotice(run, now) {
47
+ if (now - run.lastProgressAt >= RUN_STALENESS_MS)
48
+ return SAVED_WORK_NOTICE;
49
+ if (run.recoveryCount >= RECOVERY_LIMIT)
50
+ return EXHAUSTED_NOTICE;
51
+ if (run.recoveryDeadline !== null && now >= run.recoveryDeadline)
52
+ return EXHAUSTED_NOTICE;
53
+ return null;
54
+ }
55
+ /** Recovery coordinator behind explicit host ports. One CAS owns both the attempt and its conversation slot. */
56
+ export function createDurableRecovery(ports, _optional = {}) {
57
+ async function finalize(run, notice, canceled = false) {
58
+ await ports.cancelAttempt(run, {}).catch(() => undefined);
59
+ // Persist the notice as an event: hook errors are live-only and would leave a reloaded
60
+ // terminal message without its explanation. Only this coordinator decides exhaustion.
61
+ const events = [...(run.message.events ?? []), { kind: "status", label: notice }];
62
+ await ports.settle({ ...run, content: run.message.content, events, status: canceled ? "canceled" : "failed", endedAt: ports.now() }, {});
63
+ return "finalized";
64
+ }
65
+ async function nativeAllowed(run) {
66
+ if (!run.sessionConfirmed || !run.sessionId || !run.child)
67
+ return false;
68
+ if (!ports.supportsNativeResume({ agentId: run.request.agentId ?? "claude" }, {}))
69
+ return false;
70
+ return ports.verifyChildDead(run.child, {}).catch(() => false);
71
+ }
72
+ async function continueDead(run) {
73
+ await ports.cancelAttempt(run, {}).catch(() => undefined);
74
+ const native = await nativeAllowed(run);
75
+ const nextRunId = ports.mintRunId();
76
+ const saved = [...(run.message.events ?? [])];
77
+ if (!saved.some((event) => event.kind === "text") && run.message.content)
78
+ saved.unshift({ kind: "text", text: run.message.content });
79
+ const events = recoveredRunEvents({ saved }, {});
80
+ if (!await ports.store.advance({ run, nextRunId, now: ports.now(), events }, {}))
81
+ return "superseded";
82
+ const next = await ports.store.load({ messageId: run.messageId }, {});
83
+ // Cancel/deletion can win while the old attempt is being fenced. Re-read after CAS and do
84
+ // not launch against a removed binding or a newer generation.
85
+ if (!next || next.runId !== nextRunId)
86
+ return "superseded";
87
+ if (next.cancelReason)
88
+ return finalize(next, "Stopped. Saved work is above.", true);
89
+ try {
90
+ await ports.launch({ run: next, request: continuationRequest({ run: { ...run, message: next.message, transcript: next.transcript }, native }, {}) }, {});
91
+ await ports.store.recoveryClock({ runId: next.runId, now: ports.now(), active: false }, {});
92
+ }
93
+ catch {
94
+ // A rejected/lost start response is uncertain: the daemon may already be executing. The
95
+ // same idempotent attempt is probed by the attached watcher before another CAS is possible.
96
+ }
97
+ ports.attach(next, {});
98
+ return "continued";
99
+ }
100
+ async function decide(run, trigger, liveRunId) {
101
+ const failure = attemptFailure(run);
102
+ const reason = exclusion(run, failure);
103
+ if (reason)
104
+ return finalize(run, reason === "auth-failure" ? "Not logged in. Saved work is above." : "Stopped. Saved work is above.", true);
105
+ // A terminal executor end confirms this attempt is dead even when the daemon retains its
106
+ // replayable record (HTTP 200). Other triggers must obtain fresh liveness evidence.
107
+ const probe = trigger === "attempt-failed" ? "dead" : liveRunId === run.runId ? "live" : await ports.probe(run, {});
108
+ if (probe === "live") {
109
+ await ports.store.recoveryClock({ runId: run.runId, now: ports.now(), active: false }, {});
110
+ ports.attach(run, {});
111
+ return "reattached";
112
+ }
113
+ if (failure === "permanent")
114
+ return finalize(run, EXHAUSTED_NOTICE);
115
+ return resolveMissing(run, probe, trigger);
116
+ }
117
+ async function resolveMissing(run, probe, trigger) {
118
+ await ports.store.recoveryClock({ runId: run.runId, now: ports.now(), active: true }, {});
119
+ const timed = await ports.store.load({ messageId: run.messageId }, {}) ?? run;
120
+ // A probe only speaks about the generation it inspected. Never apply its dead result to
121
+ // a newer attempt committed by another caller while this one was awaiting storage/HTTP.
122
+ if (timed.runId !== run.runId)
123
+ return "superseded";
124
+ const notice = budgetNotice(timed, ports.now());
125
+ if (notice)
126
+ return finalize(run, notice);
127
+ // A queued attempt may be between durable acceptance and the daemon's start transaction.
128
+ // Give that idempotent launch time to appear before a 404 can advance its generation.
129
+ if (probe === "dead" && (trigger === "attempt-failed" || ports.now() - run.attemptStartedAt >= 10_000))
130
+ return continueDead(timed);
131
+ ports.attach(run, {});
132
+ return "waiting";
133
+ }
134
+ return {
135
+ async recover({ messageId, trigger, expectedRunId }, options) {
136
+ const run = await ports.store.load({ messageId }, {});
137
+ if (!run)
138
+ return "gone";
139
+ if (run.message.runStatus !== "queued" && run.message.runStatus !== "running")
140
+ return "terminal";
141
+ if (expectedRunId && expectedRunId !== run.runId)
142
+ return "superseded";
143
+ return decide(run, trigger, options.liveRunId);
144
+ },
145
+ };
146
+ }
147
+ //# sourceMappingURL=recover.js.map
@@ -1,19 +1,5 @@
1
- import type { AgentEvent } from '../../core/events.js';
2
- import type { ChatRunStatus } from '../../core/messages.js';
3
- /** Exactly which run owns the row; the host binds authorization and storage scope. */
4
- export interface RunRef {
5
- readonly conversationId: string;
6
- readonly messageId: string;
7
- readonly runId: string;
8
- }
9
- export interface RunProgress extends RunRef {
10
- readonly content: string;
11
- readonly events: readonly AgentEvent[];
12
- }
13
- export interface RunSettlement extends RunProgress {
14
- readonly status: Extract<ChatRunStatus, 'succeeded' | 'failed' | 'canceled'>;
15
- readonly endedAt: number;
16
- }
1
+ import type { RunProgress, RunSettlement } from "../../core/durable-runs/run-contracts.js";
2
+ export type { RunRef, RunProgress, RunSettlement } from "../../core/durable-runs/run-contracts.js";
17
3
  /** Storage contract only. Both methods must atomically reject a different run or terminal row.
18
4
  * First terminal write wins; a late checkpoint cannot reopen or replace that row. */
19
5
  export interface RunLedger {
@@ -1,3 +1,4 @@
1
+ import { type SecretRedactionSignal, type UserTextRedactionOptions } from '../../../core/user-text-redaction.js';
1
2
  /**
2
3
  * @module db/messages/messages
3
4
  * Chat message CRUD, streamed-event append, and telemetry finalization. Imports only the
@@ -84,27 +85,13 @@ export declare class MessageConversationMismatchError extends Error {
84
85
  * @returns The normalized, re-fetched message row, or null if the row vanished between write and read.
85
86
  * @throws {MessageConversationMismatchError} if `m.id` already exists under a different conversation.
86
87
  */
87
- export declare function upsertMessage({ db, conversationId, m }: {
88
+ export declare function upsertMessage({ db, conversationId, m: incoming }: {
88
89
  db: SqliteDb;
89
90
  conversationId: string;
90
91
  m: DbRow;
91
- }): {
92
- id: any;
93
- role: any;
94
- content: any;
95
- agentId: any;
96
- agentName: any;
97
- runId: any;
98
- runStatus: any;
99
- lastRunEventId: any;
100
- events: any;
101
- attachments: any;
102
- sessionMode: ChatSessionMode | undefined;
103
- runContext: any;
104
- createdAt: any;
105
- startedAt: any;
106
- endedAt: any;
107
- } | null;
92
+ }, options?: UserTextRedactionOptions): (ReturnType<typeof normalizeMessage> & {
93
+ secretRedaction?: SecretRedactionSignal;
94
+ }) | null;
108
95
  /**
109
96
  * Reads whether telemetry has been finalized for a message without loading the full row.
110
97
  * Used to decide whether to enqueue a finalization job.
@@ -153,4 +140,23 @@ export declare function deleteMessage({ db, id }: {
153
140
  db: SqliteDb;
154
141
  id: string;
155
142
  }): void;
143
+ /** @internal Converts a raw SQLite row to a plain object with JSON columns parsed and null coerced to undefined. */
144
+ declare function normalizeMessage(row: DbRow): {
145
+ id: any;
146
+ role: any;
147
+ content: any;
148
+ agentId: any;
149
+ agentName: any;
150
+ runId: any;
151
+ runStatus: any;
152
+ lastRunEventId: any;
153
+ events: any;
154
+ attachments: any;
155
+ sessionMode: ChatSessionMode | undefined;
156
+ runContext: any;
157
+ createdAt: any;
158
+ startedAt: any;
159
+ endedAt: any;
160
+ };
161
+ export {};
156
162
  //# sourceMappingURL=messages.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { redactUserText } from '../../../core/user-text-redaction.js';
1
2
  import { parseJsonOrUndef } from '@jini-ai/db/core';
2
3
  /** @internal Column alias list used by both `listMessages` and `upsertMessage`'s re-fetch. */
3
4
  const MESSAGE_COLS = `id, role, content, agent_id AS agentId, agent_name AS agentName,
@@ -71,7 +72,11 @@ export class MessageConversationMismatchError extends Error {
71
72
  * @returns The normalized, re-fetched message row, or null if the row vanished between write and read.
72
73
  * @throws {MessageConversationMismatchError} if `m.id` already exists under a different conversation.
73
74
  */
74
- export function upsertMessage({ db, conversationId, m }) {
75
+ export function upsertMessage({ db, conversationId, m: incoming }, options = {}) {
76
+ const redaction = incoming.role === 'user' && typeof incoming.content === 'string'
77
+ ? (options.redactUserText ?? redactUserText)({ text: incoming.content })
78
+ : null;
79
+ const m = redaction ? { ...incoming, content: redaction.text } : incoming;
75
80
  const runUpsert = db.transaction(() => {
76
81
  const existing = db
77
82
  .prepare(`SELECT conversation_id AS conversationId, position FROM messages WHERE id = ?`)
@@ -119,7 +124,12 @@ export function upsertMessage({ db, conversationId, m }) {
119
124
  .get(m.id, conversationId);
120
125
  return row ? normalizeMessage(row) : null;
121
126
  });
122
- return runUpsert.immediate();
127
+ const saved = runUpsert.immediate();
128
+ if (!saved || !redaction?.secretRedacted)
129
+ return saved;
130
+ const signal = { secretRedacted: true, count: redaction.count };
131
+ options.onSecretRedacted?.(signal);
132
+ return { ...saved, secretRedaction: signal };
123
133
  }
124
134
  /**
125
135
  * Reads whether telemetry has been finalized for a message without loading the full row.
@@ -1,3 +1,4 @@
1
+ import type { UserTextRedactionOptions } from '../../core/user-text-redaction.js';
1
2
  /** Pglite transcript adapter. The host owns the opened, migrated kernel and its lifetime. */
2
3
  import type { Clock } from '@jini-ai/core/primitives';
3
4
  import type { StorageKernel } from '@jini-ai/db/kernel';
@@ -13,7 +14,7 @@ export declare function createPgliteChatStore<DB extends ChatDatabase>(required:
13
14
  scope: ChatOwnerScope;
14
15
  }, optional?: {
15
16
  clock?: Clock;
16
- }): ChatStore;
17
+ } & UserTextRedactionOptions): ChatStore;
17
18
  /** Privileged bounded expiry maintenance; never hand this collaborator to request handlers. */
18
19
  export declare function createPgliteChatMaintenance<DB extends ChatDatabase>(required: {
19
20
  kernel: StorageKernel<DB>;
@@ -1,3 +1,4 @@
1
+ import type { UserTextRedactionOptions } from '../../core/user-text-redaction.js';
1
2
  /** Postgres transcript adapter. The host owns the opened, migrated kernel and its lifetime. */
2
3
  import type { Clock } from '@jini-ai/core/primitives';
3
4
  import type { StorageKernel } from '@jini-ai/db/kernel';
@@ -13,7 +14,7 @@ export declare function createPostgresChatStore<DB extends ChatDatabase>(require
13
14
  scope: ChatOwnerScope;
14
15
  }, optional?: {
15
16
  clock?: Clock;
16
- }): ChatStore;
17
+ } & UserTextRedactionOptions): ChatStore;
17
18
  /** Privileged bounded expiry maintenance; never hand this collaborator to request handlers. */
18
19
  export declare function createPostgresChatMaintenance<DB extends ChatDatabase>(required: {
19
20
  kernel: StorageKernel<DB>;
@@ -0,0 +1,40 @@
1
+ import type { ChatHistoryMaintenance } from "../ports.js";
2
+ /**
3
+ * @file Retention for chat history: deletes every conversation whose `expires_at` has passed.
4
+ *
5
+ * This is the one chat operation that legitimately spans owners (`tenant-scope.ts`'s doc), so it
6
+ * lives here, on the raw chat kernel, and never in a scoped store. Only guest chats carry an
7
+ * `expires_at` (`chatExpiryFor`); a NULL one never expires. Messages, agent sessions and tool
8
+ * approvals go with each conversation through `ON DELETE CASCADE`.
9
+ */
10
+ /** How often {@link startChatExpirySweep} re-runs after its boot pass: hourly. */
11
+ export declare const CHAT_EXPIRY_SWEEP_INTERVAL_MS: number;
12
+ /**
13
+ * Deletes every conversation that expired at or before `now`.
14
+ *
15
+ * @param store the chat kernel, or the open `chat.db` handle whose kernel to use.
16
+ * @returns how many conversations were deleted.
17
+ * @complexity One bounded DELETE per 500 expired parents, plus the final partial/empty batch.
18
+ */
19
+ export declare function sweepExpiredChats({ maintenance }: {
20
+ maintenance: ChatHistoryMaintenance;
21
+ }, { now }?: {
22
+ now?: number;
23
+ }): Promise<number>;
24
+ /**
25
+ * Runs {@link sweepExpiredChats} once now and then every `intervalMs`, until the returned stop
26
+ * function is called. The timer is `unref`'d so it never keeps the process alive. A failed pass is
27
+ * reported through `onError` and the next pass still runs. A tick that fires while a pass is still
28
+ * running is skipped, so passes never overlap.
29
+ *
30
+ * @returns `stop`: clears the timer and resolves once the pass in flight (if any) has finished, so
31
+ * the store can be closed right after it. Safe to call more than once.
32
+ */
33
+ export declare function startChatExpirySweep({ maintenance }: {
34
+ maintenance: ChatHistoryMaintenance;
35
+ }, options?: {
36
+ readonly intervalMs?: number;
37
+ readonly now?: () => number;
38
+ readonly onError?: (error: unknown) => void;
39
+ }): () => Promise<void>;
40
+ //# sourceMappingURL=chat-expiry-sweep.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { ChatStoreError } from "../errors.js";
2
+ /**
3
+ * @file Retention for chat history: deletes every conversation whose `expires_at` has passed.
4
+ *
5
+ * This is the one chat operation that legitimately spans owners (`tenant-scope.ts`'s doc), so it
6
+ * lives here, on the raw chat kernel, and never in a scoped store. Only guest chats carry an
7
+ * `expires_at` (`chatExpiryFor`); a NULL one never expires. Messages, agent sessions and tool
8
+ * approvals go with each conversation through `ON DELETE CASCADE`.
9
+ */
10
+ /** How often {@link startChatExpirySweep} re-runs after its boot pass: hourly. */
11
+ export const CHAT_EXPIRY_SWEEP_INTERVAL_MS = 60 * 60 * 1000;
12
+ /**
13
+ * Deletes every conversation that expired at or before `now`.
14
+ *
15
+ * @param store the chat kernel, or the open `chat.db` handle whose kernel to use.
16
+ * @returns how many conversations were deleted.
17
+ * @complexity One bounded DELETE per 500 expired parents, plus the final partial/empty batch.
18
+ */
19
+ export async function sweepExpiredChats({ maintenance }, { now = Date.now() } = {}) {
20
+ let total = 0;
21
+ let deleted;
22
+ try {
23
+ do {
24
+ deleted = await maintenance.sweepExpired({ now }, { limit: 500 });
25
+ total += deleted;
26
+ } while (deleted === 500);
27
+ }
28
+ catch (error) {
29
+ // Preserve the host timer's existing onError collaborator and retry behavior.
30
+ if (error instanceof ChatStoreError && error.cause !== undefined)
31
+ throw error.cause;
32
+ throw error;
33
+ }
34
+ return total;
35
+ }
36
+ /**
37
+ * Runs {@link sweepExpiredChats} once now and then every `intervalMs`, until the returned stop
38
+ * function is called. The timer is `unref`'d so it never keeps the process alive. A failed pass is
39
+ * reported through `onError` and the next pass still runs. A tick that fires while a pass is still
40
+ * running is skipped, so passes never overlap.
41
+ *
42
+ * @returns `stop`: clears the timer and resolves once the pass in flight (if any) has finished, so
43
+ * the store can be closed right after it. Safe to call more than once.
44
+ */
45
+ export function startChatExpirySweep({ maintenance }, options = {}) {
46
+ const { intervalMs = CHAT_EXPIRY_SWEEP_INTERVAL_MS, now = Date.now } = options;
47
+ const onError = options.onError ??
48
+ ((error) => {
49
+ // eslint-disable-next-line no-console
50
+ console.warn(`[chat-expiry-sweep] sweep failed: ${error instanceof Error ? error.message : String(error)}`);
51
+ });
52
+ let active;
53
+ const pass = () => {
54
+ if (active !== undefined)
55
+ return;
56
+ active = sweepExpiredChats({ maintenance }, { now: now() })
57
+ .then(() => undefined, onError)
58
+ .finally(() => {
59
+ active = undefined;
60
+ });
61
+ };
62
+ pass();
63
+ const timer = setInterval(pass, intervalMs);
64
+ timer.unref();
65
+ return async () => {
66
+ clearInterval(timer);
67
+ await active;
68
+ };
69
+ }
70
+ //# sourceMappingURL=chat-expiry-sweep.js.map
@@ -0,0 +1,20 @@
1
+ import type { RunIdPolicy } from "../../core/durable-runs/ports.js";
2
+ import type { ChatOwnerScope, ChatStore } from "../ports.js";
3
+ import type { ChatDatabase, ChatKernel } from "./kernel.js";
4
+ /**
5
+ * Host compatibility factory over Jini's owner-scoped transcript adapters (PLAN C3).
6
+ * Borrows the exact migrated kernel: the adapter's append joins the host's run-ledger transaction.
7
+ * The host retains principal hashing, migrations and connection lifetime.
8
+ * @param kernel The already-open chat kernel; never wrap or open a second connection here.
9
+ * @param scope The authenticated owner tuple; guest ownerId is already hashed by tenant-scope.
10
+ * @param now Injectable epoch-millisecond clock.
11
+ * @returns Full ChatStore, also assignable to existing eight-method ChatHistoryStore consumers.
12
+ */
13
+ export declare function createChatHistoryStore<DB extends ChatDatabase>({ kernel: hostKernel, scope, isDaemonRunId }: {
14
+ kernel: ChatKernel<DB>;
15
+ scope: ChatOwnerScope;
16
+ isDaemonRunId: RunIdPolicy;
17
+ }, { now }?: {
18
+ now?: () => number;
19
+ }): ChatStore;
20
+ //# sourceMappingURL=chat-history-store.d.ts.map
@@ -0,0 +1,50 @@
1
+ import { createSqliteChatStore } from "../sqlite/index.js";
2
+ // Owner isolation versus legacy project-only storage: packages/chat/src/store/sqlite/index.ts.
3
+ import { createPgliteChatStore } from "../pglite/index.js";
4
+ import { createPostgresChatStore } from "../postgres/index.js";
5
+ import { chatKernelTables } from "../sql/boundary.js";
6
+ function repairLegacyTurnOrder({ messages, isDaemonRunId }, _optional = {}) {
7
+ const ordered = [];
8
+ for (let index = 0; index < messages.length; index++) {
9
+ const answer = messages[index];
10
+ const question = messages[index + 1];
11
+ // Legacy acceptance saved no question ID. Repair only an orphan daemon answer immediately
12
+ // followed by an earlier/equal-clock user row; a preceding question already owns its answer.
13
+ // A global timestamp sort would reorder valid follow-ups when browser/server clocks disagree.
14
+ if (ordered.at(-1)?.role !== "user" && answer.role === "assistant" && answer.runId && isDaemonRunId({ runId: answer.runId })
15
+ && question?.role === "user" && question.createdAt !== undefined && answer.createdAt !== undefined
16
+ && question.createdAt <= answer.createdAt) {
17
+ ordered.push(question, answer);
18
+ index++;
19
+ }
20
+ else
21
+ ordered.push(answer);
22
+ }
23
+ return ordered;
24
+ }
25
+ // Transcript-isolation rationale: Jini packages/chat/src/store/sql/store.ts.
26
+ /**
27
+ * Host compatibility factory over Jini's owner-scoped transcript adapters (PLAN C3).
28
+ * Borrows the exact migrated kernel: the adapter's append joins the host's run-ledger transaction.
29
+ * The host retains principal hashing, migrations and connection lifetime.
30
+ * @param kernel The already-open chat kernel; never wrap or open a second connection here.
31
+ * @param scope The authenticated owner tuple; guest ownerId is already hashed by tenant-scope.
32
+ * @param now Injectable epoch-millisecond clock.
33
+ * @returns Full ChatStore, also assignable to existing eight-method ChatHistoryStore consumers.
34
+ */
35
+ export function createChatHistoryStore({ kernel: hostKernel, scope, isDaemonRunId }, { now = Date.now } = {}) {
36
+ const kernel = chatKernelTables({ kernel: hostKernel }, {});
37
+ const required = { kernel, scope };
38
+ const optional = { clock: { nowMs: now } };
39
+ const store = kernel.transport === "better-sqlite3" ? createSqliteChatStore(required, optional)
40
+ : kernel.transport === "pglite" || kernel.transport === "pglite-socket" ? createPgliteChatStore(required, optional)
41
+ : createPostgresChatStore(required, optional);
42
+ return { ...store,
43
+ // Admin reload and recovery use this full-transcript compatibility read. Preserve stored
44
+ // positions (and Jini's position-based page cursors); existing user databases need no migration.
45
+ async messages(required, optional = {}) {
46
+ return repairLegacyTurnOrder({ messages: await store.messages(required, optional), isDaemonRunId }, {});
47
+ },
48
+ };
49
+ }
50
+ //# sourceMappingURL=chat-history-store.js.map
@@ -0,0 +1,21 @@
1
+ import type { RunIdPolicy } from "../../core/durable-runs/ports.js";
2
+ import { type AgentEvent } from "../../core/index.js";
3
+ import type { ChatKernel, ChatDatabase } from "./kernel.js";
4
+ import type { DurableRun, DurableRunStore } from "../../core/durable-runs/ports.js";
5
+ export declare const RUN_SLOT_BUSY = "Another answer in this chat is still running. This turn is queued in your browser.";
6
+ export declare class RunSlotBusyError extends Error {
7
+ constructor();
8
+ }
9
+ export declare function attemptCanExecute({ run }: {
10
+ run: DurableRun | null;
11
+ }, _optional?: {}): boolean;
12
+ export declare function savedRunEvents({ json }: {
13
+ json: string | null;
14
+ }, _optional?: {}): AgentEvent[];
15
+ /** Same kernel and message lock as checkpoint/settle. Claim changes run_id before any spawn;
16
+ * old attempts cannot write projections or dispatch tools after that transaction commits. */
17
+ export declare function createDurableRunStore<DB extends ChatDatabase>({ kernel: hostKernel, isDaemonRunId }: {
18
+ kernel: ChatKernel<DB>;
19
+ isDaemonRunId: RunIdPolicy;
20
+ }, _optional?: {}): DurableRunStore;
21
+ //# sourceMappingURL=durable-run-store.d.ts.map