@gkorepanov/ccodex 0.3.0 → 0.3.4

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 (176) hide show
  1. package/README.md +27 -9
  2. package/RELEASING.md +14 -15
  3. package/SSH_VERIFY.md +11 -0
  4. package/archive/README.md +23 -0
  5. package/compatibility.json +1 -2
  6. package/config.example.toml +12 -4
  7. package/dist/claude/asyncQueue.d.ts +3 -0
  8. package/dist/claude/asyncQueue.js +13 -2
  9. package/dist/claude/asyncQueue.js.map +1 -1
  10. package/dist/claude/commandActions.d.ts +6 -0
  11. package/dist/claude/commandActions.js +57 -0
  12. package/dist/claude/commandActions.js.map +1 -0
  13. package/dist/claude/compactCommand.d.ts +2 -0
  14. package/dist/claude/compactCommand.js +7 -0
  15. package/dist/claude/compactCommand.js.map +1 -0
  16. package/dist/claude/goalTools.d.ts +15 -0
  17. package/dist/claude/goalTools.js +173 -0
  18. package/dist/claude/goalTools.js.map +1 -0
  19. package/dist/claude/hookMapper.d.ts +17 -4
  20. package/dist/claude/hookMapper.js +7 -5
  21. package/dist/claude/hookMapper.js.map +1 -1
  22. package/dist/claude/rateLimits.d.ts +72 -0
  23. package/dist/claude/rateLimits.js +330 -0
  24. package/dist/claude/rateLimits.js.map +1 -0
  25. package/dist/claude/responseItemValidation.d.ts +2 -0
  26. package/dist/claude/responseItemValidation.js +40 -0
  27. package/dist/claude/responseItemValidation.js.map +1 -0
  28. package/dist/claude/resultClassifier.d.ts +8 -2
  29. package/dist/claude/resultClassifier.js +1 -1
  30. package/dist/claude/resultClassifier.js.map +1 -1
  31. package/dist/claude/service.d.ts +108 -28
  32. package/dist/claude/service.js +1276 -668
  33. package/dist/claude/service.js.map +1 -1
  34. package/dist/claude/session/backgroundOutput.d.ts +2 -0
  35. package/dist/claude/session/backgroundOutput.js +29 -0
  36. package/dist/claude/session/backgroundOutput.js.map +1 -0
  37. package/dist/claude/session/commands.d.ts +855 -0
  38. package/dist/claude/session/commands.js +2 -0
  39. package/dist/claude/session/commands.js.map +1 -0
  40. package/dist/claude/session/goalState.d.ts +49 -0
  41. package/dist/claude/session/goalState.js +407 -0
  42. package/dist/claude/session/goalState.js.map +1 -0
  43. package/dist/claude/session/mailbox.d.ts +52 -0
  44. package/dist/claude/session/mailbox.js +190 -0
  45. package/dist/claude/session/mailbox.js.map +1 -0
  46. package/dist/claude/session/outputAdapter.d.ts +11 -0
  47. package/dist/claude/session/outputAdapter.js +31 -0
  48. package/dist/claude/session/outputAdapter.js.map +1 -0
  49. package/dist/claude/session/providerFacts.d.ts +47 -0
  50. package/dist/claude/session/providerFacts.js +84 -0
  51. package/dist/claude/session/providerFacts.js.map +1 -0
  52. package/dist/claude/session/providerRuntime.d.ts +1 -0
  53. package/dist/claude/session/providerRuntime.js +2 -0
  54. package/dist/claude/session/providerRuntime.js.map +1 -0
  55. package/dist/claude/session/providerRuntimeFactory.d.ts +49 -0
  56. package/dist/claude/session/providerRuntimeFactory.js +127 -0
  57. package/dist/claude/session/providerRuntimeFactory.js.map +1 -0
  58. package/dist/claude/session/repository.d.ts +57 -0
  59. package/dist/claude/session/repository.js +161 -0
  60. package/dist/claude/session/repository.js.map +1 -0
  61. package/dist/claude/session/runtime.d.ts +46 -0
  62. package/dist/claude/session/runtime.js +140 -0
  63. package/dist/claude/session/runtime.js.map +1 -0
  64. package/dist/claude/session/scopeState.d.ts +61 -0
  65. package/dist/claude/session/scopeState.js +74 -0
  66. package/dist/claude/session/scopeState.js.map +1 -0
  67. package/dist/claude/session/session.d.ts +281 -0
  68. package/dist/claude/session/session.js +6664 -0
  69. package/dist/claude/session/session.js.map +1 -0
  70. package/dist/claude/session/shellRunner.d.ts +13 -0
  71. package/dist/claude/session/shellRunner.js +103 -0
  72. package/dist/claude/session/shellRunner.js.map +1 -0
  73. package/dist/claude/session/usage.d.ts +2 -0
  74. package/dist/claude/session/usage.js +10 -0
  75. package/dist/claude/session/usage.js.map +1 -0
  76. package/dist/claude/sessionRegistry.d.ts +26 -0
  77. package/dist/claude/sessionRegistry.js +118 -0
  78. package/dist/claude/sessionRegistry.js.map +1 -0
  79. package/dist/claude/shellSupervisor.d.ts +1 -0
  80. package/dist/claude/shellSupervisor.js +69 -0
  81. package/dist/claude/shellSupervisor.js.map +1 -0
  82. package/dist/claude/statusCommand.d.ts +21 -0
  83. package/dist/claude/statusCommand.js +97 -0
  84. package/dist/claude/statusCommand.js.map +1 -0
  85. package/dist/claude/toolMapper.d.ts +9 -1
  86. package/dist/claude/toolMapper.js +92 -16
  87. package/dist/claude/toolMapper.js.map +1 -1
  88. package/dist/claude/transcriptBrancher.d.ts +37 -0
  89. package/dist/claude/transcriptBrancher.js +103 -0
  90. package/dist/claude/transcriptBrancher.js.map +1 -0
  91. package/dist/cli/delegate.js +23 -1
  92. package/dist/cli/delegate.js.map +1 -1
  93. package/dist/cli/main.js +28 -12
  94. package/dist/cli/main.js.map +1 -1
  95. package/dist/codex/generated/ResponseItemRuntimeSchema.d.ts +8 -0
  96. package/dist/codex/generated/ResponseItemRuntimeSchema.js +7 -0
  97. package/dist/codex/generated/ResponseItemRuntimeSchema.js.map +1 -0
  98. package/dist/compatibility/probe.js +11 -6
  99. package/dist/compatibility/probe.js.map +1 -1
  100. package/dist/config/config.d.ts +9 -0
  101. package/dist/config/config.js +80 -11
  102. package/dist/config/config.js.map +1 -1
  103. package/dist/daemon/daemon.js +91 -43
  104. package/dist/daemon/daemon.js.map +1 -1
  105. package/dist/daemon/ownership.d.ts +15 -0
  106. package/dist/daemon/ownership.js +185 -0
  107. package/dist/daemon/ownership.js.map +1 -0
  108. package/dist/daemon/supervisor.d.ts +3 -0
  109. package/dist/daemon/supervisor.js +37 -1
  110. package/dist/daemon/supervisor.js.map +1 -1
  111. package/dist/gateway/clientConnection.d.ts +7 -1
  112. package/dist/gateway/clientConnection.js +435 -48
  113. package/dist/gateway/clientConnection.js.map +1 -1
  114. package/dist/gateway/remoteRelay.js +0 -1
  115. package/dist/gateway/remoteRelay.js.map +1 -1
  116. package/dist/gateway/server.d.ts +1 -1
  117. package/dist/gateway/server.js +37 -9
  118. package/dist/gateway/server.js.map +1 -1
  119. package/dist/gateway/subscriptions.d.ts +9 -2
  120. package/dist/gateway/subscriptions.js +87 -9
  121. package/dist/gateway/subscriptions.js.map +1 -1
  122. package/dist/gateway/transientNotice.d.ts +2 -0
  123. package/dist/gateway/transientNotice.js +42 -1
  124. package/dist/gateway/transientNotice.js.map +1 -1
  125. package/dist/handoff/service.d.ts +32 -3
  126. package/dist/handoff/service.js +304 -26
  127. package/dist/handoff/service.js.map +1 -1
  128. package/dist/handoff/titleGeneration.d.ts +28 -0
  129. package/dist/handoff/titleGeneration.js +151 -0
  130. package/dist/handoff/titleGeneration.js.map +1 -0
  131. package/dist/management/doctor.d.ts +1 -1
  132. package/dist/management/doctor.js +32 -23
  133. package/dist/management/doctor.js.map +1 -1
  134. package/dist/management/lifecycle.js +14 -2
  135. package/dist/management/lifecycle.js.map +1 -1
  136. package/dist/management/remoteShim.d.ts +10 -0
  137. package/dist/management/remoteShim.js +65 -0
  138. package/dist/management/remoteShim.js.map +1 -0
  139. package/dist/management/setup.d.ts +4 -0
  140. package/dist/management/setup.js +108 -39
  141. package/dist/management/setup.js.map +1 -1
  142. package/dist/observability/metrics.d.ts +2 -0
  143. package/dist/observability/metrics.js +6 -0
  144. package/dist/observability/metrics.js.map +1 -1
  145. package/dist/protocol/errors.d.ts +1 -0
  146. package/dist/protocol/errors.js +4 -1
  147. package/dist/protocol/errors.js.map +1 -1
  148. package/dist/runtime/dependencies.js +2 -2
  149. package/dist/runtime/dependencies.js.map +1 -1
  150. package/dist/runtime/providerAvailability.d.ts +24 -0
  151. package/dist/runtime/providerAvailability.js +107 -0
  152. package/dist/runtime/providerAvailability.js.map +1 -0
  153. package/dist/state/stateCommand.d.ts +21 -0
  154. package/dist/state/stateCommand.js +198 -0
  155. package/dist/state/stateCommand.js.map +1 -0
  156. package/dist/state/stockStateTracker.d.ts +14 -0
  157. package/dist/state/stockStateTracker.js +120 -0
  158. package/dist/state/stockStateTracker.js.map +1 -0
  159. package/dist/store/HybridStore.d.ts +118 -2
  160. package/dist/store/HybridStore.js +23 -1
  161. package/dist/store/HybridStore.js.map +1 -1
  162. package/dist/store/memoryStore.d.ts +57 -7
  163. package/dist/store/memoryStore.js +337 -5
  164. package/dist/store/memoryStore.js.map +1 -1
  165. package/dist/store/sqliteStore.d.ts +30 -4
  166. package/dist/store/sqliteStore.js +395 -42
  167. package/dist/store/sqliteStore.js.map +1 -1
  168. package/install.sh +2 -2
  169. package/package.json +18 -10
  170. package/dist/claude/interactions.d.ts +0 -23
  171. package/dist/claude/interactions.js +0 -77
  172. package/dist/claude/interactions.js.map +0 -1
  173. package/dist/claude/sessionRuntime.d.ts +0 -102
  174. package/dist/claude/sessionRuntime.js +0 -1361
  175. package/dist/claude/sessionRuntime.js.map +0 -1
  176. /package/{BUILD_BENCHMARKS.md → archive/BUILD_BENCHMARKS.md} +0 -0
@@ -1,8 +1,7 @@
1
1
  import type { Thread } from "../codex/generated/v2/Thread.js";
2
2
  import type { ThreadListParams } from "../codex/generated/v2/ThreadListParams.js";
3
3
  import type { Turn } from "../codex/generated/v2/Turn.js";
4
- import type { ThreadGoal } from "../codex/generated/v2/ThreadGoal.js";
5
- import type { ClaudeThreadRecord, EventPersistence, HybridStore, PendingRequestRecord } from "./HybridStore.js";
4
+ import type { AppendProviderEvent, ClaudeThreadRecord, EventPersistence, GoalPatch, GoalUsageInput, HybridStore, InternalGoal, PendingRequestRecord, PendingThreadRemoval, ProviderEventDisposition, ProviderEventRecord, ProviderItemCorrelation, ProviderRetractionMutation, StoredEvent, ThreadStateCommit, TurnProviderBoundary } from "./HybridStore.js";
6
5
  export declare class SqliteHybridStore implements HybridStore {
7
6
  private readonly database;
8
7
  private readonly path;
@@ -13,7 +12,13 @@ export declare class SqliteHybridStore implements HybridStore {
13
12
  allThreadRecords(): ClaudeThreadRecord[];
14
13
  listThreads(params: ThreadListParams): Thread[];
15
14
  updateThread(record: ClaudeThreadRecord): void;
15
+ isThreadArchived(threadId: string): boolean;
16
16
  setThreadArchived(threadId: string, archived: boolean): void;
17
+ commitThreadsArchived(threadIds: readonly string[], archived: boolean): void;
18
+ beginThreadRemoval(removal: PendingThreadRemoval): void;
19
+ cancelThreadRemoval(rootThreadId: string): void;
20
+ listPendingThreadRemovals(): PendingThreadRemoval[];
21
+ commitThreadRemoval(rootThreadId: string, threadIds: readonly string[]): void;
17
22
  deleteThread(threadId: string): void;
18
23
  createTurn(threadId: string, turn: Turn): void;
19
24
  updateTurn(threadId: string, turn: Turn): void;
@@ -22,20 +27,41 @@ export declare class SqliteHybridStore implements HybridStore {
22
27
  setTurnClaudeMessageUuid(threadId: string, turnId: string, messageUuid: string): void;
23
28
  getTurnClaudeMessageUuid(threadId: string, turnId: string): string | undefined;
24
29
  truncateTurns(threadId: string, keepCount: number): void;
30
+ commitForkedThread(record: ClaudeThreadRecord, turns: readonly Turn[], boundaries: readonly TurnProviderBoundary[]): void;
31
+ commitThreadRollback(record: ClaudeThreadRecord, keepCount: number, boundaries: readonly TurnProviderBoundary[], removedThreadIds?: readonly string[]): void;
32
+ commitThreadState(commit: ThreadStateCommit): number[];
25
33
  appendEvent(threadId: string, turnId: string | null, method: string, params: unknown, persistence?: EventPersistence): number;
34
+ eventHighWatermark(threadId: string): number;
35
+ listEventsAfter(threadId: string, sequence: number): StoredEvent[];
26
36
  hasProcessedProviderEvent(threadId: string, providerEventId: string): boolean;
27
37
  markProviderEventProcessed(threadId: string, providerEventType: string, providerEventId: string): void;
38
+ appendProviderEvent(event: AppendProviderEvent): {
39
+ record: ProviderEventRecord;
40
+ inserted: boolean;
41
+ };
42
+ completeProviderEvent(threadId: string, sequence: number, disposition: Exclude<ProviderEventDisposition, "pending">, error?: string | null): void;
43
+ listProviderEvents(threadId: string, disposition?: ProviderEventDisposition): ProviderEventRecord[];
44
+ pruneProviderEvents(threadId: string, maxEvents: number, maxBytes: number): number;
45
+ linkProviderItems(threadId: string, providerMessageId: string, ownerThreadId: string, turnId: string, itemIds: readonly string[]): void;
46
+ private insertProviderItems;
47
+ listProviderItemCorrelations(threadId: string, providerMessageIds: readonly string[]): ProviderItemCorrelation[];
48
+ deleteProviderItemCorrelations(threadId: string, providerMessageIds: readonly string[]): void;
49
+ commitProviderRetraction(record: ClaudeThreadRecord, providerMessageIds: readonly string[], mutations: readonly ProviderRetractionMutation[], removedThreadIds?: readonly string[]): void;
28
50
  createPendingRequest(request: PendingRequestRecord): void;
29
51
  getPendingRequest(requestId: string): PendingRequestRecord | undefined;
30
52
  findPendingRequestByClaudeId(threadId: string, claudeRequestId: string): PendingRequestRecord | undefined;
31
53
  listPendingRequests(threadId: string): PendingRequestRecord[];
32
54
  resolvePendingRequest(requestId: string, status: "resolved" | "cancelled", response: unknown): void;
33
- getGoal(threadId: string): ThreadGoal | undefined;
34
- setGoal(goal: ThreadGoal): void;
55
+ getGoal(threadId: string): InternalGoal | undefined;
56
+ setGoal(threadId: string, patch: GoalPatch): InternalGoal;
35
57
  clearGoal(threadId: string): boolean;
58
+ accountGoalUsage(input: GoalUsageInput): InternalGoal | undefined;
36
59
  close(): void;
37
60
  private transaction;
61
+ private insertEvent;
62
+ private deleteThreadRows;
38
63
  private writeTurn;
64
+ private insertTurn;
39
65
  private syncItems;
40
66
  private backup;
41
67
  private migrate;
@@ -2,6 +2,7 @@ import { chmodSync, copyFileSync, existsSync, mkdirSync, renameSync, unlinkSync,
2
2
  import { dirname } from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  import { v7 as uuidv7 } from "uuid";
5
+ import { settingsGeneration, withSettingsFrom } from "./HybridStore.js";
5
6
  import { filterSortThreads } from "./threadFilter.js";
6
7
  function json(value) {
7
8
  return JSON.stringify(value);
@@ -67,6 +68,12 @@ function parseRecord(row, turns) {
67
68
  tokenUsageTotal: runtime.tokenUsageTotal && typeof runtime.tokenUsageTotal === "object"
68
69
  ? runtime.tokenUsageTotal
69
70
  : { totalTokens: 0, inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, reasoningOutputTokens: 0 },
71
+ tokenUsageLast: runtime.tokenUsageLast && typeof runtime.tokenUsageLast === "object"
72
+ ? runtime.tokenUsageLast
73
+ : null,
74
+ modelContextWindow: typeof runtime.modelContextWindow === "number" ? runtime.modelContextWindow : null,
75
+ providerCostUsdTotal: typeof runtime.providerCostUsdTotal === "number" ? runtime.providerCostUsdTotal : 0,
76
+ settingsGeneration: typeof runtime.settingsGeneration === "number" ? runtime.settingsGeneration : 0,
70
77
  };
71
78
  }
72
79
  function parsePending(row) {
@@ -83,6 +90,21 @@ function parsePending(row) {
83
90
  resolvedAt: row.resolved_at,
84
91
  };
85
92
  }
93
+ function parseProviderEvent(row) {
94
+ return {
95
+ sequence: row.sequence,
96
+ threadId: row.thread_id,
97
+ processEpoch: row.process_epoch,
98
+ providerSequence: row.provider_sequence,
99
+ providerEventType: row.provider_event_type,
100
+ providerEventId: row.provider_event_id,
101
+ payload: JSON.parse(row.payload_json),
102
+ disposition: row.disposition,
103
+ error: row.error,
104
+ createdAt: row.created_at,
105
+ projectedAt: row.projected_at,
106
+ };
107
+ }
86
108
  export class SqliteHybridStore {
87
109
  database;
88
110
  path;
@@ -113,6 +135,10 @@ export class SqliteHybridStore {
113
135
  collaborationMode: record.collaborationMode,
114
136
  outputSchema: record.outputSchema,
115
137
  tokenUsageTotal: record.tokenUsageTotal,
138
+ tokenUsageLast: record.tokenUsageLast,
139
+ modelContextWindow: record.modelContextWindow,
140
+ providerCostUsdTotal: record.providerCostUsdTotal ?? 0,
141
+ settingsGeneration: settingsGeneration(record),
116
142
  }));
117
143
  }
118
144
  hasThread(threadId) {
@@ -149,44 +175,85 @@ export class SqliteHybridStore {
149
175
  return filterSortThreads(rows.map((row) => JSON.parse(row.thread_json)), params);
150
176
  }
151
177
  updateThread(record) {
178
+ const current = this.getThreadRecord(record.thread.id, false);
179
+ const merged = current && settingsGeneration(current) > settingsGeneration(record)
180
+ ? withSettingsFrom(record, current)
181
+ : record;
152
182
  this.database.prepare(`
153
183
  UPDATE threads SET
154
184
  claude_session_id = ?, model_picker_id = ?, claude_model_value = ?,
155
- service_tier = ?, cwd = ?, archived = ?, updated_at = ?, thread_json = ?,
185
+ service_tier = ?, cwd = ?, updated_at = ?, thread_json = ?,
156
186
  approval_policy_json = ?, sandbox_policy_json = ?, base_instructions = ?,
157
187
  developer_instructions = ?, personality = ?, resolved_model = ?,
158
188
  last_claude_message_uuid = ?, last_completed_turn_id = ?, claude_code_version = ?,
159
189
  runtime_settings_json = ?
160
190
  WHERE id = ?
161
- `).run(record.claudeSessionId, record.modelPickerId, record.claudeModelValue, record.serviceTier, record.thread.cwd, record.thread.archived ? 1 : 0, record.thread.updatedAt, json({ ...record.thread, turns: [] }), json(record.approvalPolicy), json(record.sandboxPolicy), record.baseInstructions, record.developerInstructions, record.personality, record.resolvedModel, record.lastClaudeMessageUuid, record.lastCompletedTurnId, record.claudeCodeVersion, json({
162
- approvalsReviewer: record.approvalsReviewer,
163
- reasoningEffort: record.reasoningEffort,
164
- reasoningSummary: record.reasoningSummary,
165
- collaborationMode: record.collaborationMode,
166
- outputSchema: record.outputSchema,
167
- tokenUsageTotal: record.tokenUsageTotal,
168
- }), record.thread.id);
191
+ `).run(merged.claudeSessionId, merged.modelPickerId, merged.claudeModelValue, merged.serviceTier, merged.thread.cwd, merged.thread.updatedAt, json({ ...merged.thread, turns: [] }), json(merged.approvalPolicy), json(merged.sandboxPolicy), merged.baseInstructions, merged.developerInstructions, merged.personality, merged.resolvedModel, merged.lastClaudeMessageUuid, merged.lastCompletedTurnId, merged.claudeCodeVersion, json({
192
+ approvalsReviewer: merged.approvalsReviewer,
193
+ reasoningEffort: merged.reasoningEffort,
194
+ reasoningSummary: merged.reasoningSummary,
195
+ collaborationMode: merged.collaborationMode,
196
+ outputSchema: merged.outputSchema,
197
+ tokenUsageTotal: merged.tokenUsageTotal,
198
+ tokenUsageLast: merged.tokenUsageLast,
199
+ modelContextWindow: merged.modelContextWindow,
200
+ providerCostUsdTotal: merged.providerCostUsdTotal ?? 0,
201
+ settingsGeneration: settingsGeneration(merged),
202
+ }), merged.thread.id);
203
+ }
204
+ isThreadArchived(threadId) {
205
+ const row = this.database.prepare("SELECT archived FROM threads WHERE id = ?").get(threadId);
206
+ return row?.archived === 1;
169
207
  }
170
208
  setThreadArchived(threadId, archived) {
171
209
  this.database.prepare("UPDATE threads SET archived = ? WHERE id = ?").run(archived ? 1 : 0, threadId);
172
210
  }
173
- deleteThread(threadId) {
211
+ commitThreadsArchived(threadIds, archived) {
212
+ const method = archived ? "thread/archived" : "thread/unarchived";
174
213
  this.transaction(() => {
175
- this.database.prepare("DELETE FROM goals WHERE thread_id = ?").run(threadId);
176
- this.database.prepare("DELETE FROM pending_requests WHERE thread_id = ?").run(threadId);
177
- this.database.prepare("DELETE FROM events WHERE thread_id = ?").run(threadId);
178
- this.database.prepare("DELETE FROM turns WHERE thread_id = ?").run(threadId);
179
- this.database.prepare("DELETE FROM threads WHERE id = ?").run(threadId);
214
+ for (const threadId of threadIds) {
215
+ this.setThreadArchived(threadId, archived);
216
+ this.insertEvent(threadId, null, method, { threadId });
217
+ }
180
218
  });
181
219
  }
182
- createTurn(threadId, turn) {
220
+ beginThreadRemoval(removal) {
221
+ this.database.prepare(`
222
+ INSERT INTO pending_thread_removals (root_thread_id, claude_session_id, cwd, kind)
223
+ VALUES (?, ?, ?, ?)
224
+ ON CONFLICT(root_thread_id) DO UPDATE SET
225
+ claude_session_id = excluded.claude_session_id,
226
+ cwd = excluded.cwd,
227
+ kind = excluded.kind
228
+ `).run(removal.rootThreadId, removal.claudeSessionId, removal.cwd, removal.kind);
229
+ }
230
+ cancelThreadRemoval(rootThreadId) {
231
+ this.database.prepare("DELETE FROM pending_thread_removals WHERE root_thread_id = ?").run(rootThreadId);
232
+ }
233
+ listPendingThreadRemovals() {
234
+ return this.database.prepare(`
235
+ SELECT root_thread_id, claude_session_id, cwd, kind
236
+ FROM pending_thread_removals ORDER BY root_thread_id
237
+ `).all().map((row) => ({
238
+ rootThreadId: row.root_thread_id,
239
+ claudeSessionId: row.claude_session_id,
240
+ cwd: row.cwd,
241
+ kind: row.kind,
242
+ }));
243
+ }
244
+ commitThreadRemoval(rootThreadId, threadIds) {
183
245
  this.transaction(() => {
184
- const ordinalRow = this.database.prepare("SELECT COALESCE(MAX(ordinal), -1) + 1 AS ordinal FROM turns WHERE thread_id = ?").get(threadId);
185
- this.database.prepare("INSERT INTO turns (id, thread_id, ordinal, status, turn_json) VALUES (?, ?, ?, ?, ?)")
186
- .run(turn.id, threadId, ordinalRow.ordinal, turn.status, json(turn));
187
- this.syncItems(threadId, turn);
246
+ for (const threadId of [...threadIds].reverse())
247
+ this.deleteThreadRows(threadId);
248
+ this.database.prepare("DELETE FROM pending_thread_removals WHERE root_thread_id = ?").run(rootThreadId);
188
249
  });
189
250
  }
251
+ deleteThread(threadId) {
252
+ this.transaction(() => this.deleteThreadRows(threadId));
253
+ }
254
+ createTurn(threadId, turn) {
255
+ this.transaction(() => this.insertTurn(threadId, turn));
256
+ }
190
257
  updateTurn(threadId, turn) {
191
258
  this.transaction(() => this.writeTurn(threadId, turn));
192
259
  }
@@ -212,25 +279,68 @@ export class SqliteHybridStore {
212
279
  truncateTurns(threadId, keepCount) {
213
280
  this.database.prepare("DELETE FROM turns WHERE thread_id = ? AND ordinal >= ?").run(threadId, keepCount);
214
281
  }
215
- appendEvent(threadId, turnId, method, params, persistence) {
282
+ commitForkedThread(record, turns, boundaries) {
283
+ this.transaction(() => {
284
+ this.createThread(record);
285
+ for (const turn of turns)
286
+ this.insertTurn(record.thread.id, turn);
287
+ for (const boundary of boundaries) {
288
+ this.setTurnClaudeMessageUuid(record.thread.id, boundary.turnId, boundary.messageUuid);
289
+ }
290
+ });
291
+ }
292
+ commitThreadRollback(record, keepCount, boundaries, removedThreadIds = []) {
293
+ this.transaction(() => {
294
+ for (const threadId of removedThreadIds)
295
+ this.deleteThreadRows(threadId);
296
+ this.truncateTurns(record.thread.id, keepCount);
297
+ for (const boundary of boundaries) {
298
+ this.setTurnClaudeMessageUuid(record.thread.id, boundary.turnId, boundary.messageUuid);
299
+ }
300
+ this.updateThread(record);
301
+ });
302
+ }
303
+ commitThreadState(commit) {
216
304
  return this.transaction(() => {
217
- if (persistence?.dedupKey) {
218
- const found = this.database.prepare("SELECT 1 FROM events WHERE thread_id = ? AND dedup_key = ?")
219
- .get(threadId, persistence.dedupKey);
220
- if (found)
221
- return 0;
305
+ this.updateThread(commit.record);
306
+ if (commit.turn) {
307
+ if (commit.insertTurn)
308
+ this.insertTurn(commit.record.thread.id, commit.turn);
309
+ else
310
+ this.writeTurn(commit.record.thread.id, commit.turn);
222
311
  }
223
- if (persistence?.turn)
224
- this.writeTurn(threadId, persistence.turn);
225
- const result = this.database.prepare(`
226
- INSERT INTO events (
227
- event_id, thread_id, turn_id, method, params_json, provider_event_type,
228
- provider_event_id, dedup_key, created_at
229
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
230
- `).run(uuidv7(), threadId, turnId, method, json(params), persistence?.providerEventType ?? null, persistence?.providerEventId ?? null, persistence?.dedupKey ?? null, Date.now());
231
- return Number(result.lastInsertRowid);
312
+ if (commit.providerBoundary) {
313
+ const boundary = commit.providerBoundary;
314
+ this.setTurnClaudeMessageUuid(boundary.ownerThreadId, boundary.turnId, boundary.messageUuid);
315
+ this.insertProviderItems(commit.record.thread.id, boundary.messageUuid, boundary.ownerThreadId, boundary.turnId, boundary.itemIds ?? []);
316
+ }
317
+ return commit.events.map((event) => this.insertEvent(commit.record.thread.id, event.turnId, event.method, event.params, event));
232
318
  });
233
319
  }
320
+ appendEvent(threadId, turnId, method, params, persistence) {
321
+ return this.transaction(() => this.insertEvent(threadId, turnId, method, params, persistence));
322
+ }
323
+ eventHighWatermark(threadId) {
324
+ const row = this.database.prepare("SELECT COALESCE(MAX(sequence), 0) AS sequence FROM events WHERE thread_id = ?")
325
+ .get(threadId);
326
+ return row.sequence;
327
+ }
328
+ listEventsAfter(threadId, sequence) {
329
+ const rows = this.database.prepare(`
330
+ SELECT sequence, thread_id, turn_id, method, params_json, created_at
331
+ FROM events
332
+ WHERE thread_id = ? AND sequence > ? AND method NOT LIKE 'hybrid/%'
333
+ ORDER BY sequence ASC
334
+ `).all(threadId, sequence);
335
+ return rows.map((row) => ({
336
+ sequence: row.sequence,
337
+ threadId: row.thread_id,
338
+ turnId: row.turn_id,
339
+ method: row.method,
340
+ params: JSON.parse(row.params_json),
341
+ createdAt: row.created_at,
342
+ }));
343
+ }
234
344
  hasProcessedProviderEvent(threadId, providerEventId) {
235
345
  return this.database.prepare(`
236
346
  SELECT 1 FROM events
@@ -242,6 +352,111 @@ export class SqliteHybridStore {
242
352
  providerEventType, providerEventId, dedupKey: `provider:${providerEventId}`,
243
353
  });
244
354
  }
355
+ appendProviderEvent(event) {
356
+ return this.transaction(() => {
357
+ const existing = event.providerEventId === null ? undefined : this.database.prepare(`
358
+ SELECT * FROM provider_events WHERE thread_id = ? AND provider_event_id = ?
359
+ `).get(event.threadId, event.providerEventId);
360
+ if (existing)
361
+ return { record: parseProviderEvent(existing), inserted: false };
362
+ const result = this.database.prepare(`
363
+ INSERT INTO provider_events (
364
+ thread_id, process_epoch, provider_sequence, provider_event_type,
365
+ provider_event_id, payload_json, disposition, error, created_at, projected_at
366
+ ) VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, ?, NULL)
367
+ `).run(event.threadId, event.processEpoch, event.providerSequence, event.providerEventType, event.providerEventId, json(event.payload), event.createdAt);
368
+ const row = this.database.prepare("SELECT * FROM provider_events WHERE sequence = ?")
369
+ .get(Number(result.lastInsertRowid));
370
+ return { record: parseProviderEvent(row), inserted: true };
371
+ });
372
+ }
373
+ completeProviderEvent(threadId, sequence, disposition, error = null) {
374
+ this.database.prepare(`
375
+ UPDATE provider_events SET disposition = ?, error = ?, projected_at = ? WHERE thread_id = ? AND sequence = ?
376
+ `).run(disposition, error, Date.now(), threadId, sequence);
377
+ }
378
+ listProviderEvents(threadId, disposition) {
379
+ const rows = (disposition
380
+ ? this.database.prepare("SELECT * FROM provider_events WHERE thread_id = ? AND disposition = ? ORDER BY sequence")
381
+ .all(threadId, disposition)
382
+ : this.database.prepare("SELECT * FROM provider_events WHERE thread_id = ? ORDER BY sequence").all(threadId));
383
+ return rows.map(parseProviderEvent);
384
+ }
385
+ pruneProviderEvents(threadId, maxEvents, maxBytes) {
386
+ if (maxEvents < 1)
387
+ throw new Error("Provider event retention must keep at least one event.");
388
+ const result = this.database.prepare(`
389
+ DELETE FROM provider_events
390
+ WHERE thread_id = ?
391
+ AND disposition IN ('projected', 'stateOnly', 'retainedOnly', 'unsupportedVisible')
392
+ AND sequence IN (
393
+ SELECT sequence FROM (
394
+ SELECT sequence,
395
+ ROW_NUMBER() OVER (ORDER BY sequence DESC) AS ordinal,
396
+ SUM(LENGTH(CAST(payload_json AS BLOB))) OVER (ORDER BY sequence DESC) AS retained_bytes
397
+ FROM provider_events
398
+ WHERE thread_id = ?
399
+ AND disposition IN ('projected', 'stateOnly', 'retainedOnly', 'unsupportedVisible')
400
+ )
401
+ WHERE ordinal > ? OR (ordinal > 1 AND retained_bytes > ?)
402
+ )
403
+ `).run(threadId, threadId, maxEvents, maxBytes);
404
+ return Number(result.changes);
405
+ }
406
+ linkProviderItems(threadId, providerMessageId, ownerThreadId, turnId, itemIds) {
407
+ this.transaction(() => this.insertProviderItems(threadId, providerMessageId, ownerThreadId, turnId, itemIds));
408
+ }
409
+ insertProviderItems(threadId, providerMessageId, ownerThreadId, turnId, itemIds) {
410
+ const insert = this.database.prepare(`
411
+ INSERT OR IGNORE INTO provider_item_correlations (
412
+ thread_id, provider_message_id, owner_thread_id, turn_id, item_id
413
+ ) VALUES (?, ?, ?, ?, ?)
414
+ `);
415
+ for (const itemId of itemIds)
416
+ insert.run(threadId, providerMessageId, ownerThreadId, turnId, itemId);
417
+ }
418
+ listProviderItemCorrelations(threadId, providerMessageIds) {
419
+ if (providerMessageIds.length === 0)
420
+ return [];
421
+ const placeholders = providerMessageIds.map(() => "?").join(", ");
422
+ const rows = this.database.prepare(`
423
+ SELECT provider_message_id, owner_thread_id, turn_id, item_id
424
+ FROM provider_item_correlations
425
+ WHERE thread_id = ? AND provider_message_id IN (${placeholders})
426
+ `).all(threadId, ...providerMessageIds);
427
+ return rows.map((row) => ({
428
+ providerMessageId: row.provider_message_id,
429
+ ownerThreadId: row.owner_thread_id,
430
+ turnId: row.turn_id,
431
+ itemId: row.item_id,
432
+ }));
433
+ }
434
+ deleteProviderItemCorrelations(threadId, providerMessageIds) {
435
+ if (providerMessageIds.length === 0)
436
+ return;
437
+ const placeholders = providerMessageIds.map(() => "?").join(", ");
438
+ this.database.prepare(`
439
+ DELETE FROM provider_item_correlations
440
+ WHERE thread_id = ? AND provider_message_id IN (${placeholders})
441
+ `).run(threadId, ...providerMessageIds);
442
+ }
443
+ commitProviderRetraction(record, providerMessageIds, mutations, removedThreadIds = []) {
444
+ this.transaction(() => {
445
+ for (const threadId of removedThreadIds)
446
+ this.deleteThreadRows(threadId);
447
+ for (const mutation of mutations) {
448
+ this.writeTurn(mutation.ownerThreadId, mutation.turn);
449
+ if (mutation.clearBoundary) {
450
+ this.database.prepare(`
451
+ UPDATE turns SET last_claude_message_uuid = NULL
452
+ WHERE id = ? AND thread_id = ?
453
+ `).run(mutation.turn.id, mutation.ownerThreadId);
454
+ }
455
+ }
456
+ this.deleteProviderItemCorrelations(record.thread.id, providerMessageIds);
457
+ this.updateThread(record);
458
+ });
459
+ }
245
460
  createPendingRequest(request) {
246
461
  this.database.prepare(`
247
462
  INSERT INTO pending_requests (
@@ -273,17 +488,72 @@ export class SqliteHybridStore {
273
488
  }
274
489
  getGoal(threadId) {
275
490
  const row = this.database.prepare("SELECT goal_json FROM goals WHERE thread_id = ?").get(threadId);
276
- return row ? JSON.parse(row.goal_json) : undefined;
277
- }
278
- setGoal(goal) {
279
- this.database.prepare(`
280
- INSERT INTO goals (thread_id, goal_json) VALUES (?, ?)
281
- ON CONFLICT(thread_id) DO UPDATE SET goal_json = excluded.goal_json
282
- `).run(goal.threadId, json(goal));
491
+ if (!row)
492
+ return undefined;
493
+ const stored = JSON.parse(row.goal_json);
494
+ if (stored.goalId)
495
+ return stored;
496
+ const migrated = { ...stored, goalId: uuidv7() };
497
+ this.database.prepare("UPDATE goals SET goal_json = ? WHERE thread_id = ?").run(json(migrated), threadId);
498
+ return migrated;
499
+ }
500
+ setGoal(threadId, patch) {
501
+ return this.transaction(() => {
502
+ const previous = this.getGoal(threadId);
503
+ const now = patch.now ?? Math.floor(Date.now() / 1_000);
504
+ const replace = patch.replace === true || !previous;
505
+ if (replace && patch.objective === undefined)
506
+ throw new Error(`cannot create goal for thread ${threadId} without an objective`);
507
+ const goal = {
508
+ threadId,
509
+ goalId: replace ? uuidv7() : previous.goalId,
510
+ objective: patch.objective ?? previous?.objective ?? "",
511
+ status: patch.status ?? (replace ? "active" : previous.status),
512
+ tokenBudget: patch.tokenBudget === undefined ? (replace ? null : previous.tokenBudget) : patch.tokenBudget,
513
+ tokensUsed: replace ? 0 : previous.tokensUsed,
514
+ timeUsedSeconds: replace ? 0 : previous.timeUsedSeconds,
515
+ createdAt: replace ? now : previous.createdAt,
516
+ updatedAt: now,
517
+ };
518
+ if (goal.status === "active" && goal.tokenBudget !== null && goal.tokensUsed >= goal.tokenBudget)
519
+ goal.status = "budgetLimited";
520
+ this.database.prepare(`
521
+ INSERT INTO goals (thread_id, goal_json) VALUES (?, ?)
522
+ ON CONFLICT(thread_id) DO UPDATE SET goal_json = excluded.goal_json
523
+ `).run(threadId, json(goal));
524
+ return goal;
525
+ });
283
526
  }
284
527
  clearGoal(threadId) {
285
528
  return Number(this.database.prepare("DELETE FROM goals WHERE thread_id = ?").run(threadId).changes) > 0;
286
529
  }
530
+ accountGoalUsage(input) {
531
+ return this.transaction(() => {
532
+ if (input.checkpointKey) {
533
+ const inserted = this.database.prepare(`
534
+ INSERT OR IGNORE INTO goal_checkpoints (thread_id, goal_id, checkpoint_key)
535
+ VALUES (?, ?, ?)
536
+ `).run(input.threadId, input.expectedGoalId, input.checkpointKey);
537
+ if (Number(inserted.changes) === 0)
538
+ return this.getGoal(input.threadId);
539
+ }
540
+ const previous = this.getGoal(input.threadId);
541
+ if (!previous || previous.goalId !== input.expectedGoalId)
542
+ return previous;
543
+ if (previous.status !== "active" && previous.status !== "budgetLimited")
544
+ return previous;
545
+ const goal = {
546
+ ...previous,
547
+ tokensUsed: previous.tokensUsed + Math.max(0, input.tokenDelta),
548
+ timeUsedSeconds: previous.timeUsedSeconds + Math.max(0, input.timeDeltaSeconds),
549
+ updatedAt: Math.floor(Date.now() / 1_000),
550
+ };
551
+ if (goal.status === "active" && goal.tokenBudget !== null && goal.tokensUsed >= goal.tokenBudget)
552
+ goal.status = "budgetLimited";
553
+ this.database.prepare("UPDATE goals SET goal_json = ? WHERE thread_id = ?").run(json(goal), input.threadId);
554
+ return goal;
555
+ });
556
+ }
287
557
  close() {
288
558
  try {
289
559
  this.backup();
@@ -304,11 +574,43 @@ export class SqliteHybridStore {
304
574
  throw error;
305
575
  }
306
576
  }
577
+ insertEvent(threadId, turnId, method, params, persistence) {
578
+ if (persistence?.dedupKey) {
579
+ const found = this.database.prepare("SELECT 1 FROM events WHERE thread_id = ? AND dedup_key = ?")
580
+ .get(threadId, persistence.dedupKey);
581
+ if (found)
582
+ return 0;
583
+ }
584
+ if (persistence?.turn)
585
+ this.writeTurn(threadId, persistence.turn);
586
+ const result = this.database.prepare(`
587
+ INSERT INTO events (
588
+ event_id, thread_id, turn_id, method, params_json, provider_event_type,
589
+ provider_event_id, dedup_key, created_at
590
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
591
+ `).run(uuidv7(), threadId, turnId, method, json(params), persistence?.providerEventType ?? null, persistence?.providerEventId ?? null, persistence?.dedupKey ?? null, Date.now());
592
+ return Number(result.lastInsertRowid);
593
+ }
594
+ deleteThreadRows(threadId) {
595
+ this.database.prepare("DELETE FROM provider_item_correlations WHERE owner_thread_id = ?").run(threadId);
596
+ this.database.prepare("DELETE FROM goals WHERE thread_id = ?").run(threadId);
597
+ this.database.prepare("DELETE FROM pending_requests WHERE thread_id = ?").run(threadId);
598
+ this.database.prepare("DELETE FROM events WHERE thread_id = ?").run(threadId);
599
+ this.database.prepare("DELETE FROM turns WHERE thread_id = ?").run(threadId);
600
+ this.database.prepare("DELETE FROM threads WHERE id = ?").run(threadId);
601
+ }
307
602
  writeTurn(threadId, turn) {
308
603
  this.database.prepare("UPDATE turns SET status = ?, turn_json = ? WHERE id = ? AND thread_id = ?")
309
604
  .run(turn.status, json(turn), turn.id, threadId);
310
605
  this.syncItems(threadId, turn);
311
606
  }
607
+ insertTurn(threadId, turn) {
608
+ const ordinalRow = this.database.prepare("SELECT COALESCE(MAX(ordinal), -1) + 1 AS ordinal FROM turns WHERE thread_id = ?")
609
+ .get(threadId);
610
+ this.database.prepare("INSERT INTO turns (id, thread_id, ordinal, status, turn_json) VALUES (?, ?, ?, ?, ?)")
611
+ .run(turn.id, threadId, ordinalRow.ordinal, turn.status, json(turn));
612
+ this.syncItems(threadId, turn);
613
+ }
312
614
  syncItems(threadId, turn) {
313
615
  this.database.prepare("DELETE FROM items WHERE turn_id = ? AND thread_id = ?").run(turn.id, threadId);
314
616
  const insert = this.database.prepare(`
@@ -356,6 +658,7 @@ export class SqliteHybridStore {
356
658
  ,last_completed_turn_id TEXT
357
659
  ,claude_code_version TEXT
358
660
  ,runtime_settings_json TEXT
661
+ ,deletion_pending INTEGER NOT NULL DEFAULT 0
359
662
  );
360
663
  CREATE TABLE IF NOT EXISTS turns (
361
664
  id TEXT PRIMARY KEY,
@@ -409,6 +712,44 @@ export class SqliteHybridStore {
409
712
  thread_id TEXT PRIMARY KEY,
410
713
  goal_json TEXT NOT NULL
411
714
  );
715
+ CREATE TABLE IF NOT EXISTS goal_checkpoints (
716
+ thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
717
+ goal_id TEXT NOT NULL,
718
+ checkpoint_key TEXT NOT NULL,
719
+ PRIMARY KEY(thread_id, goal_id, checkpoint_key)
720
+ );
721
+ CREATE TABLE IF NOT EXISTS provider_events (
722
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
723
+ thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
724
+ process_epoch TEXT NOT NULL,
725
+ provider_sequence INTEGER NOT NULL,
726
+ provider_event_type TEXT NOT NULL,
727
+ provider_event_id TEXT,
728
+ payload_json TEXT NOT NULL,
729
+ disposition TEXT NOT NULL,
730
+ error TEXT,
731
+ created_at INTEGER NOT NULL,
732
+ projected_at INTEGER,
733
+ UNIQUE(thread_id, process_epoch, provider_sequence)
734
+ );
735
+ CREATE UNIQUE INDEX IF NOT EXISTS provider_events_thread_event_id
736
+ ON provider_events(thread_id, provider_event_id) WHERE provider_event_id IS NOT NULL;
737
+ CREATE TABLE IF NOT EXISTS provider_item_correlations (
738
+ thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
739
+ provider_message_id TEXT NOT NULL,
740
+ owner_thread_id TEXT NOT NULL,
741
+ turn_id TEXT NOT NULL,
742
+ item_id TEXT NOT NULL,
743
+ PRIMARY KEY(thread_id, provider_message_id, item_id)
744
+ );
745
+ CREATE INDEX IF NOT EXISTS provider_item_correlations_owner
746
+ ON provider_item_correlations(owner_thread_id, turn_id);
747
+ CREATE TABLE IF NOT EXISTS pending_thread_removals (
748
+ root_thread_id TEXT PRIMARY KEY,
749
+ claude_session_id TEXT NOT NULL,
750
+ cwd TEXT NOT NULL,
751
+ kind TEXT NOT NULL
752
+ );
412
753
  INSERT OR IGNORE INTO schema_migrations(version) VALUES (1);
413
754
  `);
414
755
  this.ensureColumn("threads", "resolved_model", "TEXT");
@@ -416,6 +757,15 @@ export class SqliteHybridStore {
416
757
  this.ensureColumn("threads", "last_completed_turn_id", "TEXT");
417
758
  this.ensureColumn("threads", "claude_code_version", "TEXT");
418
759
  this.ensureColumn("threads", "runtime_settings_json", "TEXT");
760
+ this.ensureColumn("threads", "deletion_pending", "INTEGER NOT NULL DEFAULT 0");
761
+ const threadColumns = new Set(this.database.prepare("PRAGMA table_info(threads)").all()
762
+ .map((column) => column.name));
763
+ if (threadColumns.has("claude_session_id") && threadColumns.has("cwd")) {
764
+ this.database.exec(`
765
+ INSERT OR IGNORE INTO pending_thread_removals (root_thread_id, claude_session_id, cwd, kind)
766
+ SELECT id, claude_session_id, cwd, 'delete' FROM threads WHERE deletion_pending = 1
767
+ `);
768
+ }
419
769
  this.ensureColumn("turns", "last_claude_message_uuid", "TEXT");
420
770
  this.ensureColumn("events", "event_id", "TEXT");
421
771
  this.ensureColumn("events", "provider_event_type", "TEXT");
@@ -429,6 +779,9 @@ export class SqliteHybridStore {
429
779
  const threadScopedIds = this.database.prepare("SELECT 1 FROM schema_migrations WHERE version = 3").get();
430
780
  if (!threadScopedIds)
431
781
  this.migrateThreadScopedIds();
782
+ this.database.exec("INSERT OR IGNORE INTO schema_migrations(version) VALUES (4)");
783
+ this.database.exec("INSERT OR IGNORE INTO schema_migrations(version) VALUES (5)");
784
+ this.database.exec("INSERT OR IGNORE INTO schema_migrations(version) VALUES (6)");
432
785
  });
433
786
  }
434
787
  migrateThreadScopedIds() {