@gkorepanov/ccodex 0.3.0 → 0.4.0
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.
- package/README.md +141 -63
- package/assets/ccodex/goals/continuation.md +1 -0
- package/compatibility.json +5 -6
- package/config.example.toml +19 -4
- package/dist/claude/asyncQueue.d.ts +3 -0
- package/dist/claude/asyncQueue.js +13 -2
- package/dist/claude/asyncQueue.js.map +1 -1
- package/dist/claude/commandActions.d.ts +6 -0
- package/dist/claude/commandActions.js +57 -0
- package/dist/claude/commandActions.js.map +1 -0
- package/dist/claude/compactCommand.d.ts +2 -0
- package/dist/claude/compactCommand.js +7 -0
- package/dist/claude/compactCommand.js.map +1 -0
- package/dist/claude/goalTools.d.ts +15 -0
- package/dist/claude/goalTools.js +128 -0
- package/dist/claude/goalTools.js.map +1 -0
- package/dist/claude/hookMapper.d.ts +17 -4
- package/dist/claude/hookMapper.js +7 -5
- package/dist/claude/hookMapper.js.map +1 -1
- package/dist/claude/inputMapper.d.ts +1 -0
- package/dist/claude/inputMapper.js +1 -0
- package/dist/claude/inputMapper.js.map +1 -1
- package/dist/claude/rateLimits.d.ts +72 -0
- package/dist/claude/rateLimits.js +330 -0
- package/dist/claude/rateLimits.js.map +1 -0
- package/dist/claude/responseItemValidation.d.ts +2 -0
- package/dist/claude/responseItemValidation.js +40 -0
- package/dist/claude/responseItemValidation.js.map +1 -0
- package/dist/claude/resultClassifier.d.ts +8 -2
- package/dist/claude/resultClassifier.js +9 -1
- package/dist/claude/resultClassifier.js.map +1 -1
- package/dist/claude/service.d.ts +116 -29
- package/dist/claude/service.js +1337 -680
- package/dist/claude/service.js.map +1 -1
- package/dist/claude/session/backgroundOutput.d.ts +2 -0
- package/dist/claude/session/backgroundOutput.js +29 -0
- package/dist/claude/session/backgroundOutput.js.map +1 -0
- package/dist/claude/session/commands.d.ts +862 -0
- package/dist/claude/session/commands.js +2 -0
- package/dist/claude/session/commands.js.map +1 -0
- package/dist/claude/session/goalState.d.ts +49 -0
- package/dist/claude/session/goalState.js +407 -0
- package/dist/claude/session/goalState.js.map +1 -0
- package/dist/claude/session/mailbox.d.ts +52 -0
- package/dist/claude/session/mailbox.js +190 -0
- package/dist/claude/session/mailbox.js.map +1 -0
- package/dist/claude/session/outputAdapter.d.ts +11 -0
- package/dist/claude/session/outputAdapter.js +31 -0
- package/dist/claude/session/outputAdapter.js.map +1 -0
- package/dist/claude/session/permissionUpdates.d.ts +4 -0
- package/dist/claude/session/permissionUpdates.js +17 -0
- package/dist/claude/session/permissionUpdates.js.map +1 -0
- package/dist/claude/session/providerFacts.d.ts +47 -0
- package/dist/claude/session/providerFacts.js +84 -0
- package/dist/claude/session/providerFacts.js.map +1 -0
- package/dist/claude/session/providerRuntime.d.ts +1 -0
- package/dist/claude/session/providerRuntime.js +2 -0
- package/dist/claude/session/providerRuntime.js.map +1 -0
- package/dist/claude/session/providerRuntimeFactory.d.ts +49 -0
- package/dist/claude/session/providerRuntimeFactory.js +122 -0
- package/dist/claude/session/providerRuntimeFactory.js.map +1 -0
- package/dist/claude/session/repository.d.ts +57 -0
- package/dist/claude/session/repository.js +161 -0
- package/dist/claude/session/repository.js.map +1 -0
- package/dist/claude/session/runtime.d.ts +46 -0
- package/dist/claude/session/runtime.js +140 -0
- package/dist/claude/session/runtime.js.map +1 -0
- package/dist/claude/session/scopeState.d.ts +61 -0
- package/dist/claude/session/scopeState.js +74 -0
- package/dist/claude/session/scopeState.js.map +1 -0
- package/dist/claude/session/session.d.ts +285 -0
- package/dist/claude/session/session.js +6778 -0
- package/dist/claude/session/session.js.map +1 -0
- package/dist/claude/session/shellRunner.d.ts +13 -0
- package/dist/claude/session/shellRunner.js +103 -0
- package/dist/claude/session/shellRunner.js.map +1 -0
- package/dist/claude/session/usage.d.ts +2 -0
- package/dist/claude/session/usage.js +10 -0
- package/dist/claude/session/usage.js.map +1 -0
- package/dist/claude/sessionRegistry.d.ts +26 -0
- package/dist/claude/sessionRegistry.js +118 -0
- package/dist/claude/sessionRegistry.js.map +1 -0
- package/dist/claude/shellSupervisor.d.ts +1 -0
- package/dist/claude/shellSupervisor.js +69 -0
- package/dist/claude/shellSupervisor.js.map +1 -0
- package/dist/claude/statusCommand.d.ts +21 -0
- package/dist/claude/statusCommand.js +97 -0
- package/dist/claude/statusCommand.js.map +1 -0
- package/dist/claude/toolMapper.d.ts +9 -1
- package/dist/claude/toolMapper.js +98 -18
- package/dist/claude/toolMapper.js.map +1 -1
- package/dist/claude/transcriptBrancher.d.ts +37 -0
- package/dist/claude/transcriptBrancher.js +103 -0
- package/dist/claude/transcriptBrancher.js.map +1 -0
- package/dist/cli/delegate.js +23 -1
- package/dist/cli/delegate.js.map +1 -1
- package/dist/cli/main.js +28 -12
- package/dist/cli/main.js.map +1 -1
- package/dist/codex/generated/ResponseItemRuntimeSchema.d.ts +8 -0
- package/dist/codex/generated/ResponseItemRuntimeSchema.js +7 -0
- package/dist/codex/generated/ResponseItemRuntimeSchema.js.map +1 -0
- package/dist/compatibility/probe.js +11 -6
- package/dist/compatibility/probe.js.map +1 -1
- package/dist/config/config.d.ts +12 -0
- package/dist/config/config.js +103 -11
- package/dist/config/config.js.map +1 -1
- package/dist/daemon/daemon.js +91 -43
- package/dist/daemon/daemon.js.map +1 -1
- package/dist/daemon/ownership.d.ts +15 -0
- package/dist/daemon/ownership.js +185 -0
- package/dist/daemon/ownership.js.map +1 -0
- package/dist/daemon/probe.js +1 -1
- package/dist/daemon/supervisor.d.ts +3 -0
- package/dist/daemon/supervisor.js +37 -1
- package/dist/daemon/supervisor.js.map +1 -1
- package/dist/gateway/clientConnection.d.ts +7 -1
- package/dist/gateway/clientConnection.js +656 -60
- package/dist/gateway/clientConnection.js.map +1 -1
- package/dist/gateway/logicalThreadProjection.d.ts +8 -0
- package/dist/gateway/logicalThreadProjection.js +59 -0
- package/dist/gateway/logicalThreadProjection.js.map +1 -0
- package/dist/gateway/remoteRelay.js +0 -1
- package/dist/gateway/remoteRelay.js.map +1 -1
- package/dist/gateway/server.d.ts +1 -1
- package/dist/gateway/server.js +69 -11
- package/dist/gateway/server.js.map +1 -1
- package/dist/gateway/stockRpc.d.ts +5 -2
- package/dist/gateway/stockRpc.js +52 -6
- package/dist/gateway/stockRpc.js.map +1 -1
- package/dist/gateway/subscriptions.d.ts +19 -3
- package/dist/gateway/subscriptions.js +152 -13
- package/dist/gateway/subscriptions.js.map +1 -1
- package/dist/gateway/threadList.d.ts +7 -2
- package/dist/gateway/threadList.js +8 -4
- package/dist/gateway/threadList.js.map +1 -1
- package/dist/gateway/transientNotice.d.ts +15 -0
- package/dist/gateway/transientNotice.js +96 -1
- package/dist/gateway/transientNotice.js.map +1 -1
- package/dist/handoff/providerEpochs.d.ts +18 -0
- package/dist/handoff/providerEpochs.js +77 -0
- package/dist/handoff/providerEpochs.js.map +1 -0
- package/dist/handoff/service.d.ts +86 -6
- package/dist/handoff/service.js +1276 -168
- package/dist/handoff/service.js.map +1 -1
- package/dist/handoff/store.d.ts +189 -2
- package/dist/handoff/store.js +618 -7
- package/dist/handoff/store.js.map +1 -1
- package/dist/handoff/titleGeneration.d.ts +28 -0
- package/dist/handoff/titleGeneration.js +138 -0
- package/dist/handoff/titleGeneration.js.map +1 -0
- package/dist/management/doctor.d.ts +1 -1
- package/dist/management/doctor.js +32 -23
- package/dist/management/doctor.js.map +1 -1
- package/dist/management/lifecycle.js +14 -2
- package/dist/management/lifecycle.js.map +1 -1
- package/dist/management/remoteShim.d.ts +10 -0
- package/dist/management/remoteShim.js +65 -0
- package/dist/management/remoteShim.js.map +1 -0
- package/dist/management/setup.d.ts +5 -0
- package/dist/management/setup.js +120 -39
- package/dist/management/setup.js.map +1 -1
- package/dist/observability/metrics.d.ts +2 -0
- package/dist/observability/metrics.js +6 -0
- package/dist/observability/metrics.js.map +1 -1
- package/dist/protocol/errors.d.ts +1 -0
- package/dist/protocol/errors.js +4 -1
- package/dist/protocol/errors.js.map +1 -1
- package/dist/runtime/dependencies.js +3 -3
- package/dist/runtime/dependencies.js.map +1 -1
- package/dist/runtime/providerAvailability.d.ts +24 -0
- package/dist/runtime/providerAvailability.js +107 -0
- package/dist/runtime/providerAvailability.js.map +1 -0
- package/dist/state/stateCommand.d.ts +21 -0
- package/dist/state/stateCommand.js +198 -0
- package/dist/state/stateCommand.js.map +1 -0
- package/dist/state/stockStateTracker.d.ts +16 -0
- package/dist/state/stockStateTracker.js +137 -0
- package/dist/state/stockStateTracker.js.map +1 -0
- package/dist/store/HybridStore.d.ts +118 -2
- package/dist/store/HybridStore.js +23 -1
- package/dist/store/HybridStore.js.map +1 -1
- package/dist/store/memoryStore.d.ts +57 -7
- package/dist/store/memoryStore.js +337 -5
- package/dist/store/memoryStore.js.map +1 -1
- package/dist/store/sqliteStore.d.ts +30 -4
- package/dist/store/sqliteStore.js +395 -42
- package/dist/store/sqliteStore.js.map +1 -1
- package/install.sh +2 -2
- package/{LICENSES.md → legal/LICENSES.md} +2 -2
- package/{THIRD_PARTY_NOTICES.md → legal/THIRD_PARTY_NOTICES.md} +177 -148
- package/package.json +28 -18
- package/vendor/codex/continuation.md +51 -0
- package/BUILD_BENCHMARKS.md +0 -27
- package/RELEASING.md +0 -98
- package/SSH_VERIFY.md +0 -69
- package/dist/claude/interactions.d.ts +0 -23
- package/dist/claude/interactions.js +0 -77
- package/dist/claude/interactions.js.map +0 -1
- package/dist/claude/sessionRuntime.d.ts +0 -102
- package/dist/claude/sessionRuntime.js +0 -1361
- package/dist/claude/sessionRuntime.js.map +0 -1
package/dist/handoff/store.js
CHANGED
|
@@ -10,13 +10,16 @@ export class HandoffStore {
|
|
|
10
10
|
PRAGMA journal_mode=WAL;
|
|
11
11
|
PRAGMA synchronous=FULL;
|
|
12
12
|
PRAGMA busy_timeout=5000;
|
|
13
|
+
PRAGMA foreign_keys=ON;
|
|
13
14
|
CREATE TABLE IF NOT EXISTS pending_provider_switches (
|
|
14
15
|
thread_id TEXT PRIMARY KEY,
|
|
15
16
|
source_provider TEXT NOT NULL,
|
|
16
17
|
target_provider TEXT NOT NULL,
|
|
17
18
|
target_model TEXT NOT NULL,
|
|
18
19
|
settings_json TEXT NOT NULL,
|
|
19
|
-
updated_at INTEGER NOT NULL
|
|
20
|
+
updated_at INTEGER NOT NULL,
|
|
21
|
+
revision INTEGER NOT NULL DEFAULT 1,
|
|
22
|
+
expected_epoch_id TEXT
|
|
20
23
|
);
|
|
21
24
|
CREATE TABLE IF NOT EXISTS stock_history_overlays (
|
|
22
25
|
thread_id TEXT PRIMARY KEY,
|
|
@@ -25,21 +28,136 @@ export class HandoffStore {
|
|
|
25
28
|
turns_json TEXT NOT NULL,
|
|
26
29
|
created_at INTEGER NOT NULL
|
|
27
30
|
);
|
|
31
|
+
CREATE TABLE IF NOT EXISTS handoff_jobs (
|
|
32
|
+
job_id TEXT PRIMARY KEY,
|
|
33
|
+
source_thread_id TEXT NOT NULL,
|
|
34
|
+
params_json TEXT NOT NULL,
|
|
35
|
+
pending_json TEXT,
|
|
36
|
+
status TEXT NOT NULL,
|
|
37
|
+
result_json TEXT,
|
|
38
|
+
target_json TEXT,
|
|
39
|
+
error TEXT,
|
|
40
|
+
error_notified INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
created_at INTEGER NOT NULL,
|
|
42
|
+
updated_at INTEGER NOT NULL
|
|
43
|
+
);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS handoff_jobs_recovery
|
|
45
|
+
ON handoff_jobs(status, created_at);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS handoff_jobs_failure
|
|
47
|
+
ON handoff_jobs(source_thread_id, status, error_notified, updated_at);
|
|
48
|
+
CREATE TABLE IF NOT EXISTS logical_threads (
|
|
49
|
+
public_thread_id TEXT PRIMARY KEY,
|
|
50
|
+
current_epoch_id TEXT NOT NULL,
|
|
51
|
+
thread_json TEXT NOT NULL,
|
|
52
|
+
revision INTEGER NOT NULL DEFAULT 1,
|
|
53
|
+
created_at INTEGER NOT NULL,
|
|
54
|
+
updated_at INTEGER NOT NULL
|
|
55
|
+
);
|
|
56
|
+
CREATE TABLE IF NOT EXISTS provider_epochs (
|
|
57
|
+
epoch_id TEXT PRIMARY KEY,
|
|
58
|
+
public_thread_id TEXT NOT NULL REFERENCES logical_threads(public_thread_id) ON DELETE CASCADE,
|
|
59
|
+
ordinal INTEGER NOT NULL,
|
|
60
|
+
provider TEXT NOT NULL CHECK(provider IN ('claude', 'stock')),
|
|
61
|
+
backend_thread_id TEXT NOT NULL,
|
|
62
|
+
model TEXT NOT NULL,
|
|
63
|
+
settings_json TEXT NOT NULL,
|
|
64
|
+
state TEXT NOT NULL CHECK(state IN ('current', 'sealed', 'provisional')),
|
|
65
|
+
created_at INTEGER NOT NULL,
|
|
66
|
+
sealed_at INTEGER,
|
|
67
|
+
UNIQUE(public_thread_id, ordinal),
|
|
68
|
+
UNIQUE(provider, backend_thread_id)
|
|
69
|
+
);
|
|
70
|
+
CREATE UNIQUE INDEX IF NOT EXISTS provider_epochs_one_current
|
|
71
|
+
ON provider_epochs(public_thread_id) WHERE state = 'current';
|
|
72
|
+
CREATE INDEX IF NOT EXISTS provider_epochs_public
|
|
73
|
+
ON provider_epochs(public_thread_id, ordinal);
|
|
74
|
+
CREATE TABLE IF NOT EXISTS logical_turns (
|
|
75
|
+
public_thread_id TEXT NOT NULL REFERENCES logical_threads(public_thread_id) ON DELETE CASCADE,
|
|
76
|
+
position INTEGER NOT NULL,
|
|
77
|
+
public_turn_id TEXT NOT NULL,
|
|
78
|
+
epoch_id TEXT REFERENCES provider_epochs(epoch_id),
|
|
79
|
+
provider_turn_id TEXT,
|
|
80
|
+
turn_json TEXT NOT NULL,
|
|
81
|
+
kind TEXT NOT NULL CHECK(kind IN ('provider', 'migrationCompact')),
|
|
82
|
+
PRIMARY KEY(public_thread_id, position),
|
|
83
|
+
UNIQUE(public_thread_id, public_turn_id),
|
|
84
|
+
CHECK((kind = 'provider' AND epoch_id IS NOT NULL AND provider_turn_id IS NOT NULL)
|
|
85
|
+
OR (kind = 'migrationCompact' AND epoch_id IS NULL AND provider_turn_id IS NULL))
|
|
86
|
+
);
|
|
87
|
+
CREATE UNIQUE INDEX IF NOT EXISTS logical_turns_provider
|
|
88
|
+
ON logical_turns(public_thread_id, epoch_id, provider_turn_id)
|
|
89
|
+
WHERE epoch_id IS NOT NULL AND provider_turn_id IS NOT NULL;
|
|
90
|
+
CREATE TABLE IF NOT EXISTS provider_switch_jobs (
|
|
91
|
+
job_id TEXT PRIMARY KEY,
|
|
92
|
+
public_thread_id TEXT NOT NULL REFERENCES logical_threads(public_thread_id) ON DELETE CASCADE,
|
|
93
|
+
expected_epoch_id TEXT NOT NULL REFERENCES provider_epochs(epoch_id),
|
|
94
|
+
expected_thread_revision INTEGER NOT NULL,
|
|
95
|
+
pending_revision INTEGER NOT NULL,
|
|
96
|
+
target_provider TEXT NOT NULL CHECK(target_provider IN ('claude', 'stock')),
|
|
97
|
+
target_model TEXT NOT NULL,
|
|
98
|
+
settings_json TEXT NOT NULL,
|
|
99
|
+
turn_params_json TEXT NOT NULL,
|
|
100
|
+
compaction_turn_json TEXT NOT NULL,
|
|
101
|
+
status TEXT NOT NULL CHECK(status IN ('queued', 'running', 'targetCreated', 'committed', 'failed')),
|
|
102
|
+
summary TEXT,
|
|
103
|
+
target_backend_thread_id TEXT,
|
|
104
|
+
target_provider_turn_id TEXT,
|
|
105
|
+
error TEXT,
|
|
106
|
+
created_at INTEGER NOT NULL,
|
|
107
|
+
updated_at INTEGER NOT NULL
|
|
108
|
+
);
|
|
109
|
+
CREATE UNIQUE INDEX IF NOT EXISTS provider_switch_jobs_active
|
|
110
|
+
ON provider_switch_jobs(public_thread_id)
|
|
111
|
+
WHERE status IN ('queued', 'running', 'targetCreated');
|
|
112
|
+
CREATE INDEX IF NOT EXISTS provider_switch_jobs_recovery
|
|
113
|
+
ON provider_switch_jobs(status, created_at);
|
|
114
|
+
CREATE TABLE IF NOT EXISTS fork_selections (
|
|
115
|
+
target_public_thread_id TEXT PRIMARY KEY REFERENCES logical_threads(public_thread_id) ON DELETE CASCADE,
|
|
116
|
+
source_public_thread_id TEXT NOT NULL REFERENCES logical_threads(public_thread_id) ON DELETE CASCADE,
|
|
117
|
+
provisional_epoch_id TEXT NOT NULL REFERENCES provider_epochs(epoch_id),
|
|
118
|
+
expected_target_revision INTEGER NOT NULL,
|
|
119
|
+
selected_epoch_id TEXT REFERENCES provider_epochs(epoch_id),
|
|
120
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'finalized')),
|
|
121
|
+
created_at INTEGER NOT NULL,
|
|
122
|
+
updated_at INTEGER NOT NULL
|
|
123
|
+
);
|
|
28
124
|
`);
|
|
125
|
+
const pendingColumns = this.database.prepare("PRAGMA table_info(pending_provider_switches)")
|
|
126
|
+
.all();
|
|
127
|
+
if (!pendingColumns.some((column) => column.name === "revision")) {
|
|
128
|
+
this.database.exec("ALTER TABLE pending_provider_switches ADD COLUMN revision INTEGER NOT NULL DEFAULT 1");
|
|
129
|
+
}
|
|
130
|
+
if (!pendingColumns.some((column) => column.name === "expected_epoch_id")) {
|
|
131
|
+
this.database.exec("ALTER TABLE pending_provider_switches ADD COLUMN expected_epoch_id TEXT");
|
|
132
|
+
}
|
|
133
|
+
const jobColumns = this.database.prepare("PRAGMA table_info(handoff_jobs)")
|
|
134
|
+
.all();
|
|
135
|
+
if (!jobColumns.some((column) => column.name === "target_json")) {
|
|
136
|
+
this.database.exec("ALTER TABLE handoff_jobs ADD COLUMN target_json TEXT");
|
|
137
|
+
}
|
|
138
|
+
const switchColumns = this.database.prepare("PRAGMA table_info(provider_switch_jobs)")
|
|
139
|
+
.all();
|
|
140
|
+
if (!switchColumns.some((column) => column.name === "compaction_turn_json")) {
|
|
141
|
+
this.database.exec("ALTER TABLE provider_switch_jobs ADD COLUMN compaction_turn_json TEXT");
|
|
142
|
+
}
|
|
29
143
|
chmodSync(path, 0o600);
|
|
30
144
|
}
|
|
31
145
|
setPending(pending) {
|
|
32
146
|
this.database.prepare(`
|
|
33
147
|
INSERT INTO pending_provider_switches (
|
|
34
|
-
thread_id, source_provider, target_provider, target_model, settings_json, updated_at
|
|
35
|
-
|
|
148
|
+
thread_id, source_provider, target_provider, target_model, settings_json, updated_at, revision,
|
|
149
|
+
expected_epoch_id
|
|
150
|
+
) VALUES (?, ?, ?, ?, ?, ?, 1, ?)
|
|
36
151
|
ON CONFLICT(thread_id) DO UPDATE SET
|
|
37
152
|
source_provider = excluded.source_provider,
|
|
38
153
|
target_provider = excluded.target_provider,
|
|
39
154
|
target_model = excluded.target_model,
|
|
40
155
|
settings_json = excluded.settings_json,
|
|
41
|
-
updated_at = excluded.updated_at
|
|
42
|
-
|
|
156
|
+
updated_at = excluded.updated_at,
|
|
157
|
+
expected_epoch_id = excluded.expected_epoch_id,
|
|
158
|
+
revision = pending_provider_switches.revision + 1
|
|
159
|
+
`).run(pending.threadId, pending.sourceProvider, pending.targetProvider, pending.targetModel, JSON.stringify(pending.settings), Date.now(), pending.expectedEpochId ?? null);
|
|
160
|
+
return this.getPending(pending.threadId);
|
|
43
161
|
}
|
|
44
162
|
getPending(threadId) {
|
|
45
163
|
const row = this.database.prepare("SELECT * FROM pending_provider_switches WHERE thread_id = ?")
|
|
@@ -50,10 +168,382 @@ export class HandoffStore {
|
|
|
50
168
|
targetProvider: row.target_provider,
|
|
51
169
|
targetModel: row.target_model,
|
|
52
170
|
settings: JSON.parse(row.settings_json),
|
|
171
|
+
revision: row.revision,
|
|
172
|
+
...(row.expected_epoch_id ? { expectedEpochId: row.expected_epoch_id } : {}),
|
|
53
173
|
} : undefined;
|
|
54
174
|
}
|
|
55
|
-
clearPending(threadId) {
|
|
56
|
-
|
|
175
|
+
clearPending(threadId, revision) {
|
|
176
|
+
const result = revision === undefined
|
|
177
|
+
? this.database.prepare("DELETE FROM pending_provider_switches WHERE thread_id = ?").run(threadId)
|
|
178
|
+
: this.database.prepare("DELETE FROM pending_provider_switches WHERE thread_id = ? AND revision = ?")
|
|
179
|
+
.run(threadId, revision);
|
|
180
|
+
return Number(result.changes) > 0;
|
|
181
|
+
}
|
|
182
|
+
createLogicalThread(input) {
|
|
183
|
+
const now = input.epoch.createdAt ?? Date.now();
|
|
184
|
+
return this.transaction(() => {
|
|
185
|
+
this.database.prepare(`
|
|
186
|
+
INSERT INTO logical_threads (
|
|
187
|
+
public_thread_id, current_epoch_id, thread_json, revision, created_at, updated_at
|
|
188
|
+
) VALUES (?, ?, ?, 1, ?, ?)
|
|
189
|
+
`).run(input.thread.id, input.epoch.id, JSON.stringify({ ...input.thread, turns: [] }), now, now);
|
|
190
|
+
this.insertEpoch(input.thread.id, 0, input.epoch, "current", now);
|
|
191
|
+
return this.getLogicalThread(input.thread.id);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
getLogicalThread(publicThreadId) {
|
|
195
|
+
const row = this.database.prepare("SELECT * FROM logical_threads WHERE public_thread_id = ?")
|
|
196
|
+
.get(publicThreadId);
|
|
197
|
+
return row ? this.logicalThreadFromRow(row) : undefined;
|
|
198
|
+
}
|
|
199
|
+
updateLogicalThread(publicThreadId, expectedRevision, thread) {
|
|
200
|
+
const result = this.database.prepare(`
|
|
201
|
+
UPDATE logical_threads SET thread_json = ?, updated_at = ?
|
|
202
|
+
WHERE public_thread_id = ? AND revision = ?
|
|
203
|
+
`).run(JSON.stringify({ ...thread, id: publicThreadId, turns: [] }), Date.now(), publicThreadId, expectedRevision);
|
|
204
|
+
return Number(result.changes) === 1 ? this.getLogicalThread(publicThreadId) : undefined;
|
|
205
|
+
}
|
|
206
|
+
deleteLogicalThread(publicThreadId) {
|
|
207
|
+
return Number(this.database.prepare("DELETE FROM logical_threads WHERE public_thread_id = ?")
|
|
208
|
+
.run(publicThreadId).changes) === 1;
|
|
209
|
+
}
|
|
210
|
+
getEpoch(epochId) {
|
|
211
|
+
const row = this.database.prepare("SELECT * FROM provider_epochs WHERE epoch_id = ?")
|
|
212
|
+
.get(epochId);
|
|
213
|
+
return row ? this.epochFromRow(row) : undefined;
|
|
214
|
+
}
|
|
215
|
+
listEpochs(publicThreadId) {
|
|
216
|
+
return this.database.prepare(`
|
|
217
|
+
SELECT * FROM provider_epochs WHERE public_thread_id = ? ORDER BY ordinal ASC
|
|
218
|
+
`).all(publicThreadId).map((row) => this.epochFromRow(row));
|
|
219
|
+
}
|
|
220
|
+
updateCurrentEpoch(publicThreadId, expectedEpochId, patch) {
|
|
221
|
+
return this.transaction(() => {
|
|
222
|
+
const logical = this.getLogicalThread(publicThreadId);
|
|
223
|
+
const epoch = logical && this.getEpoch(expectedEpochId);
|
|
224
|
+
if (!logical || logical.currentEpochId !== expectedEpochId || epoch?.state !== "current")
|
|
225
|
+
return undefined;
|
|
226
|
+
const result = this.database.prepare(`
|
|
227
|
+
UPDATE provider_epochs SET model = ?, settings_json = ?
|
|
228
|
+
WHERE epoch_id = ? AND public_thread_id = ? AND state = 'current'
|
|
229
|
+
`).run(patch.model ?? epoch.model, JSON.stringify({ ...epoch.settings, ...(patch.settings ?? {}) }), expectedEpochId, publicThreadId);
|
|
230
|
+
return Number(result.changes) === 1 ? this.getEpoch(expectedEpochId) : undefined;
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
findEpochByBackend(provider, backendThreadId) {
|
|
234
|
+
const row = this.database.prepare(`
|
|
235
|
+
SELECT * FROM provider_epochs WHERE provider = ? AND backend_thread_id = ?
|
|
236
|
+
`).get(provider, backendThreadId);
|
|
237
|
+
return row ? this.epochFromRow(row) : undefined;
|
|
238
|
+
}
|
|
239
|
+
listBackendMappings() {
|
|
240
|
+
return this.database.prepare(`
|
|
241
|
+
SELECT public_thread_id, epoch_id, provider, backend_thread_id, state
|
|
242
|
+
FROM provider_epochs ORDER BY public_thread_id ASC, ordinal ASC
|
|
243
|
+
`).all().map((row) => ({
|
|
244
|
+
publicThreadId: row.public_thread_id,
|
|
245
|
+
epochId: row.epoch_id,
|
|
246
|
+
provider: row.provider,
|
|
247
|
+
backendThreadId: row.backend_thread_id,
|
|
248
|
+
state: row.state,
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
listLogicalTurns(publicThreadId) {
|
|
252
|
+
return this.database.prepare(`
|
|
253
|
+
SELECT * FROM logical_turns WHERE public_thread_id = ? ORDER BY position ASC
|
|
254
|
+
`).all(publicThreadId).map((row) => this.logicalTurnFromRow(row));
|
|
255
|
+
}
|
|
256
|
+
findLogicalTurn(publicThreadId, publicTurnId) {
|
|
257
|
+
const row = this.database.prepare(`
|
|
258
|
+
SELECT * FROM logical_turns WHERE public_thread_id = ? AND public_turn_id = ?
|
|
259
|
+
`).get(publicThreadId, publicTurnId);
|
|
260
|
+
return row ? this.logicalTurnFromRow(row) : undefined;
|
|
261
|
+
}
|
|
262
|
+
replaceLogicalTurns(publicThreadId, expectedThreadRevision, turns) {
|
|
263
|
+
return this.transaction(() => {
|
|
264
|
+
const current = this.getLogicalThread(publicThreadId);
|
|
265
|
+
if (!current || current.revision !== expectedThreadRevision)
|
|
266
|
+
return false;
|
|
267
|
+
this.replaceTurns(publicThreadId, turns);
|
|
268
|
+
const result = this.database.prepare(`
|
|
269
|
+
UPDATE logical_threads SET revision = revision + 1, updated_at = ?
|
|
270
|
+
WHERE public_thread_id = ? AND revision = ?
|
|
271
|
+
`).run(Date.now(), publicThreadId, expectedThreadRevision);
|
|
272
|
+
return Number(result.changes) === 1;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
stageProviderSwitch(input) {
|
|
276
|
+
return this.transaction(() => {
|
|
277
|
+
const logical = this.getLogicalThread(input.pending.threadId);
|
|
278
|
+
const epoch = logical && this.getEpoch(logical.currentEpochId);
|
|
279
|
+
if (!logical || logical.currentEpochId !== input.expectedEpochId
|
|
280
|
+
|| epoch?.provider !== input.pending.sourceProvider)
|
|
281
|
+
return undefined;
|
|
282
|
+
return this.setPending({ ...input.pending, expectedEpochId: input.expectedEpochId });
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
createProviderSwitchJob(input) {
|
|
286
|
+
return this.transaction(() => {
|
|
287
|
+
const logical = this.getLogicalThread(input.publicThreadId);
|
|
288
|
+
const pending = this.getPending(input.publicThreadId);
|
|
289
|
+
const epoch = logical && this.getEpoch(logical.currentEpochId);
|
|
290
|
+
const active = this.database.prepare(`
|
|
291
|
+
SELECT 1 AS found FROM provider_switch_jobs
|
|
292
|
+
WHERE public_thread_id = ? AND status IN ('queued', 'running', 'targetCreated')
|
|
293
|
+
`).get(input.publicThreadId);
|
|
294
|
+
if (!logical || logical.currentEpochId !== input.expectedEpochId || active
|
|
295
|
+
|| epoch?.provider !== pending?.sourceProvider
|
|
296
|
+
|| pending?.revision !== input.pendingRevision || pending.expectedEpochId !== input.expectedEpochId
|
|
297
|
+
|| pending.targetProvider !== input.targetProvider || pending.targetModel !== input.targetModel)
|
|
298
|
+
return undefined;
|
|
299
|
+
const now = input.createdAt ?? Date.now();
|
|
300
|
+
this.database.prepare(`
|
|
301
|
+
INSERT INTO provider_switch_jobs (
|
|
302
|
+
job_id, public_thread_id, expected_epoch_id, expected_thread_revision, pending_revision,
|
|
303
|
+
target_provider, target_model, settings_json, turn_params_json, status, created_at, updated_at
|
|
304
|
+
, compaction_turn_json
|
|
305
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, ?)
|
|
306
|
+
`).run(input.id, input.publicThreadId, input.expectedEpochId, logical.revision, input.pendingRevision, input.targetProvider, input.targetModel, JSON.stringify(input.settings), JSON.stringify(input.turnParams), now, now, JSON.stringify(input.compactionTurn));
|
|
307
|
+
return this.getProviderSwitchJob(input.id);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
getProviderSwitchJob(jobId) {
|
|
311
|
+
const row = this.database.prepare("SELECT * FROM provider_switch_jobs WHERE job_id = ?")
|
|
312
|
+
.get(jobId);
|
|
313
|
+
return row ? this.providerSwitchJobFromRow(row) : undefined;
|
|
314
|
+
}
|
|
315
|
+
recoverableProviderSwitchJobs() {
|
|
316
|
+
return this.database.prepare(`
|
|
317
|
+
SELECT * FROM provider_switch_jobs
|
|
318
|
+
WHERE status IN ('queued', 'running', 'targetCreated') ORDER BY created_at ASC
|
|
319
|
+
`).all().map((row) => this.providerSwitchJobFromRow(row));
|
|
320
|
+
}
|
|
321
|
+
claimProviderSwitchJob(jobId) {
|
|
322
|
+
const result = this.database.prepare(`
|
|
323
|
+
UPDATE provider_switch_jobs SET status = 'running', updated_at = ?
|
|
324
|
+
WHERE job_id = ? AND status = 'queued'
|
|
325
|
+
`).run(Date.now(), jobId);
|
|
326
|
+
return Number(result.changes) === 1 ? this.getProviderSwitchJob(jobId) : undefined;
|
|
327
|
+
}
|
|
328
|
+
requeueProviderSwitchJob(jobId) {
|
|
329
|
+
const result = this.database.prepare(`
|
|
330
|
+
UPDATE provider_switch_jobs SET
|
|
331
|
+
status = 'queued', summary = NULL, target_backend_thread_id = NULL,
|
|
332
|
+
target_provider_turn_id = NULL, error = NULL, updated_at = ?
|
|
333
|
+
WHERE job_id = ? AND status IN ('running', 'targetCreated')
|
|
334
|
+
`).run(Date.now(), jobId);
|
|
335
|
+
return Number(result.changes) === 1 ? this.getProviderSwitchJob(jobId) : this.getProviderSwitchJob(jobId);
|
|
336
|
+
}
|
|
337
|
+
checkpointProviderSwitchTarget(jobId, checkpoint) {
|
|
338
|
+
return this.transaction(() => {
|
|
339
|
+
const job = this.getProviderSwitchJob(jobId);
|
|
340
|
+
if (!job || !["running", "targetCreated"].includes(job.status)
|
|
341
|
+
|| (job.targetBackendThreadId && job.targetBackendThreadId !== checkpoint.backendThreadId))
|
|
342
|
+
return false;
|
|
343
|
+
const result = this.database.prepare(`
|
|
344
|
+
UPDATE provider_switch_jobs SET
|
|
345
|
+
status = 'targetCreated', target_backend_thread_id = ?,
|
|
346
|
+
summary = COALESCE(?, summary), target_provider_turn_id = COALESCE(?, target_provider_turn_id),
|
|
347
|
+
updated_at = ?
|
|
348
|
+
WHERE job_id = ? AND status IN ('running', 'targetCreated')
|
|
349
|
+
`).run(checkpoint.backendThreadId, checkpoint.summary ?? null, checkpoint.providerTurnId ?? null, Date.now(), jobId);
|
|
350
|
+
return Number(result.changes) === 1;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
commitProviderSwitch(input) {
|
|
354
|
+
return this.transaction(() => {
|
|
355
|
+
const job = this.getProviderSwitchJob(input.jobId);
|
|
356
|
+
const logical = job && this.getLogicalThread(job.publicThreadId);
|
|
357
|
+
const pending = job && this.getPending(job.publicThreadId);
|
|
358
|
+
if (!job || job.status !== "targetCreated" || !job.targetProviderTurnId || !logical
|
|
359
|
+
|| logical.currentEpochId !== job.expectedEpochId || logical.revision !== job.expectedThreadRevision
|
|
360
|
+
|| pending?.revision !== job.pendingRevision || pending.expectedEpochId !== job.expectedEpochId
|
|
361
|
+
|| input.targetEpoch.provider !== job.targetProvider || input.targetEpoch.model !== job.targetModel
|
|
362
|
+
|| input.targetEpoch.backendThreadId !== job.targetBackendThreadId)
|
|
363
|
+
return undefined;
|
|
364
|
+
const committedAt = input.committedAt ?? Date.now();
|
|
365
|
+
const ordinal = this.database.prepare(`
|
|
366
|
+
SELECT COALESCE(MAX(ordinal), -1) + 1 AS ordinal FROM provider_epochs WHERE public_thread_id = ?
|
|
367
|
+
`).get(job.publicThreadId).ordinal;
|
|
368
|
+
this.database.prepare(`
|
|
369
|
+
UPDATE provider_epochs SET state = 'sealed', sealed_at = ?
|
|
370
|
+
WHERE epoch_id = ? AND public_thread_id = ? AND state = 'current'
|
|
371
|
+
`).run(committedAt, job.expectedEpochId, job.publicThreadId);
|
|
372
|
+
this.insertEpoch(job.publicThreadId, ordinal, input.targetEpoch, "current", committedAt);
|
|
373
|
+
this.replaceTurns(job.publicThreadId, input.sourceTurns);
|
|
374
|
+
const update = this.database.prepare(`
|
|
375
|
+
UPDATE logical_threads SET current_epoch_id = ?, thread_json = ?, revision = revision + 1, updated_at = ?
|
|
376
|
+
WHERE public_thread_id = ? AND current_epoch_id = ? AND revision = ?
|
|
377
|
+
`).run(input.targetEpoch.id, JSON.stringify({ ...input.thread, turns: [] }), committedAt, job.publicThreadId, job.expectedEpochId, job.expectedThreadRevision);
|
|
378
|
+
if (Number(update.changes) !== 1)
|
|
379
|
+
throw new Error("Provider switch lost its logical-thread CAS during commit.");
|
|
380
|
+
this.database.prepare(`
|
|
381
|
+
DELETE FROM pending_provider_switches WHERE thread_id = ? AND revision = ? AND expected_epoch_id = ?
|
|
382
|
+
`).run(job.publicThreadId, job.pendingRevision, job.expectedEpochId);
|
|
383
|
+
this.database.prepare(`
|
|
384
|
+
UPDATE provider_switch_jobs SET status = 'committed', error = NULL, updated_at = ?
|
|
385
|
+
WHERE job_id = ? AND status = 'targetCreated'
|
|
386
|
+
`).run(committedAt, job.id);
|
|
387
|
+
return this.getLogicalThread(job.publicThreadId);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
failProviderSwitch(jobId, error) {
|
|
391
|
+
return this.transaction(() => {
|
|
392
|
+
const job = this.getProviderSwitchJob(jobId);
|
|
393
|
+
if (!job || !["queued", "running", "targetCreated"].includes(job.status))
|
|
394
|
+
return false;
|
|
395
|
+
const result = this.database.prepare(`
|
|
396
|
+
UPDATE provider_switch_jobs SET status = 'failed', error = ?, updated_at = ?
|
|
397
|
+
WHERE job_id = ? AND status IN ('queued', 'running', 'targetCreated')
|
|
398
|
+
`).run(error, Date.now(), jobId);
|
|
399
|
+
if (Number(result.changes) !== 1)
|
|
400
|
+
return false;
|
|
401
|
+
const logical = this.getLogicalThread(job.publicThreadId);
|
|
402
|
+
if (logical?.currentEpochId === job.expectedEpochId) {
|
|
403
|
+
this.database.prepare(`
|
|
404
|
+
DELETE FROM pending_provider_switches WHERE thread_id = ? AND revision = ? AND expected_epoch_id = ?
|
|
405
|
+
`).run(job.publicThreadId, job.pendingRevision, job.expectedEpochId);
|
|
406
|
+
}
|
|
407
|
+
return true;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
createForkSelection(selection) {
|
|
411
|
+
return this.transaction(() => {
|
|
412
|
+
const epoch = this.getEpoch(selection.provisionalEpochId);
|
|
413
|
+
const target = this.getLogicalThread(selection.targetPublicThreadId);
|
|
414
|
+
if (!epoch || epoch.publicThreadId !== selection.targetPublicThreadId
|
|
415
|
+
|| target?.currentEpochId !== selection.provisionalEpochId
|
|
416
|
+
|| !this.getLogicalThread(selection.sourcePublicThreadId))
|
|
417
|
+
return false;
|
|
418
|
+
const now = selection.createdAt ?? Date.now();
|
|
419
|
+
const result = this.database.prepare(`
|
|
420
|
+
INSERT OR IGNORE INTO fork_selections (
|
|
421
|
+
target_public_thread_id, source_public_thread_id, provisional_epoch_id, expected_target_revision,
|
|
422
|
+
status, created_at, updated_at
|
|
423
|
+
) VALUES (?, ?, ?, ?, 'pending', ?, ?)
|
|
424
|
+
`).run(selection.targetPublicThreadId, selection.sourcePublicThreadId, selection.provisionalEpochId, target.revision, now, now);
|
|
425
|
+
return Number(result.changes) === 1;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
getForkSelection(targetPublicThreadId) {
|
|
429
|
+
const row = this.database.prepare("SELECT * FROM fork_selections WHERE target_public_thread_id = ?")
|
|
430
|
+
.get(targetPublicThreadId);
|
|
431
|
+
return row ? this.forkSelectionFromRow(row) : undefined;
|
|
432
|
+
}
|
|
433
|
+
finalizeForkSelection(targetPublicThreadId, expectedProvisionalEpochId, selectedEpochId) {
|
|
434
|
+
return this.transaction(() => {
|
|
435
|
+
const selection = this.getForkSelection(targetPublicThreadId);
|
|
436
|
+
const selected = this.getEpoch(selectedEpochId);
|
|
437
|
+
if (!selection || selection.status !== "pending"
|
|
438
|
+
|| selection.provisionalEpochId !== expectedProvisionalEpochId
|
|
439
|
+
|| this.getLogicalThread(targetPublicThreadId)?.revision !== selection.expectedTargetRevision
|
|
440
|
+
|| selected?.publicThreadId !== selection.sourcePublicThreadId)
|
|
441
|
+
return false;
|
|
442
|
+
const result = this.database.prepare(`
|
|
443
|
+
UPDATE fork_selections SET selected_epoch_id = ?, status = 'finalized', updated_at = ?
|
|
444
|
+
WHERE target_public_thread_id = ? AND provisional_epoch_id = ? AND status = 'pending'
|
|
445
|
+
`).run(selectedEpochId, Date.now(), targetPublicThreadId, expectedProvisionalEpochId);
|
|
446
|
+
return Number(result.changes) === 1;
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
commitForkSelection(input) {
|
|
450
|
+
return this.transaction(() => {
|
|
451
|
+
const selection = this.getForkSelection(input.targetPublicThreadId);
|
|
452
|
+
const target = selection && this.getLogicalThread(selection.targetPublicThreadId);
|
|
453
|
+
const selected = this.getEpoch(input.selectedSourceEpochId);
|
|
454
|
+
const provisional = this.getEpoch(input.expectedProvisionalEpochId);
|
|
455
|
+
if (!selection || selection.status !== "pending" || !target
|
|
456
|
+
|| selection.provisionalEpochId !== input.expectedProvisionalEpochId
|
|
457
|
+
|| selection.expectedTargetRevision !== target.revision
|
|
458
|
+
|| target.currentEpochId !== input.expectedProvisionalEpochId
|
|
459
|
+
|| selected?.publicThreadId !== selection.sourcePublicThreadId
|
|
460
|
+
|| provisional?.publicThreadId !== target.publicThreadId || provisional.state !== "current"
|
|
461
|
+
|| input.targetEpoch.provider !== selected.provider)
|
|
462
|
+
return undefined;
|
|
463
|
+
const committedAt = input.committedAt ?? Date.now();
|
|
464
|
+
const ordinal = this.database.prepare(`
|
|
465
|
+
SELECT COALESCE(MAX(ordinal), -1) + 1 AS ordinal FROM provider_epochs WHERE public_thread_id = ?
|
|
466
|
+
`).get(target.publicThreadId).ordinal;
|
|
467
|
+
this.database.prepare(`
|
|
468
|
+
UPDATE provider_epochs SET state = 'sealed', sealed_at = ?
|
|
469
|
+
WHERE epoch_id = ? AND public_thread_id = ? AND state = 'current'
|
|
470
|
+
`).run(committedAt, provisional.id, target.publicThreadId);
|
|
471
|
+
this.insertEpoch(target.publicThreadId, ordinal, input.targetEpoch, "current", committedAt);
|
|
472
|
+
this.replaceTurns(target.publicThreadId, input.turns);
|
|
473
|
+
const updated = this.database.prepare(`
|
|
474
|
+
UPDATE logical_threads SET current_epoch_id = ?, thread_json = ?, revision = revision + 1, updated_at = ?
|
|
475
|
+
WHERE public_thread_id = ? AND current_epoch_id = ? AND revision = ?
|
|
476
|
+
`).run(input.targetEpoch.id, JSON.stringify({ ...input.thread, turns: [] }), committedAt, target.publicThreadId, provisional.id, selection.expectedTargetRevision);
|
|
477
|
+
if (Number(updated.changes) !== 1)
|
|
478
|
+
throw new Error("Fork selection lost its logical-thread CAS during commit.");
|
|
479
|
+
const finalized = this.database.prepare(`
|
|
480
|
+
UPDATE fork_selections SET selected_epoch_id = ?, status = 'finalized', updated_at = ?
|
|
481
|
+
WHERE target_public_thread_id = ? AND provisional_epoch_id = ? AND status = 'pending'
|
|
482
|
+
`).run(selected.id, committedAt, target.publicThreadId, provisional.id);
|
|
483
|
+
if (Number(finalized.changes) !== 1)
|
|
484
|
+
throw new Error("Fork selection lost its finalize CAS during commit.");
|
|
485
|
+
return this.getLogicalThread(target.publicThreadId);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
createJob(job) {
|
|
489
|
+
const now = Date.now();
|
|
490
|
+
this.database.prepare(`
|
|
491
|
+
INSERT INTO handoff_jobs (
|
|
492
|
+
job_id, source_thread_id, params_json, pending_json, status, created_at, updated_at
|
|
493
|
+
) VALUES (?, ?, ?, ?, 'queued', ?, ?)
|
|
494
|
+
`).run(job.id, job.sourceThreadId, JSON.stringify(job.params), job.pending ? JSON.stringify(job.pending) : null, now, now);
|
|
495
|
+
return { ...job, status: "queued" };
|
|
496
|
+
}
|
|
497
|
+
recoverableJobs() {
|
|
498
|
+
const rows = this.database.prepare(`
|
|
499
|
+
SELECT * FROM handoff_jobs WHERE status IN ('queued', 'running') ORDER BY created_at ASC
|
|
500
|
+
`).all();
|
|
501
|
+
this.database.prepare(`
|
|
502
|
+
UPDATE handoff_jobs SET status = 'queued', updated_at = ? WHERE status = 'running'
|
|
503
|
+
`).run(Date.now());
|
|
504
|
+
return rows.map((row) => this.jobFromRow({ ...row, status: "queued" }));
|
|
505
|
+
}
|
|
506
|
+
markJobRunning(jobId) {
|
|
507
|
+
this.database.prepare(`
|
|
508
|
+
UPDATE handoff_jobs SET status = 'running', updated_at = ? WHERE job_id = ?
|
|
509
|
+
`).run(Date.now(), jobId);
|
|
510
|
+
}
|
|
511
|
+
completeJob(jobId, result) {
|
|
512
|
+
this.database.prepare(`
|
|
513
|
+
UPDATE handoff_jobs
|
|
514
|
+
SET status = 'completed', result_json = ?, error = NULL, updated_at = ?
|
|
515
|
+
WHERE job_id = ?
|
|
516
|
+
`).run(JSON.stringify(result), Date.now(), jobId);
|
|
517
|
+
}
|
|
518
|
+
checkpointJobTarget(jobId, target) {
|
|
519
|
+
this.database.prepare(`
|
|
520
|
+
UPDATE handoff_jobs SET target_json = ?, updated_at = ? WHERE job_id = ?
|
|
521
|
+
`).run(JSON.stringify(target), Date.now(), jobId);
|
|
522
|
+
}
|
|
523
|
+
clearJobTarget(jobId) {
|
|
524
|
+
this.database.prepare(`
|
|
525
|
+
UPDATE handoff_jobs SET target_json = NULL, updated_at = ? WHERE job_id = ?
|
|
526
|
+
`).run(Date.now(), jobId);
|
|
527
|
+
}
|
|
528
|
+
failJob(jobId, error) {
|
|
529
|
+
this.database.prepare(`
|
|
530
|
+
UPDATE handoff_jobs
|
|
531
|
+
SET status = 'failed', error = ?, updated_at = ?
|
|
532
|
+
WHERE job_id = ?
|
|
533
|
+
`).run(error, Date.now(), jobId);
|
|
534
|
+
}
|
|
535
|
+
claimFailedJob(sourceThreadId) {
|
|
536
|
+
const row = this.database.prepare(`
|
|
537
|
+
SELECT * FROM handoff_jobs
|
|
538
|
+
WHERE source_thread_id = ? AND status = 'failed' AND error_notified = 0
|
|
539
|
+
ORDER BY updated_at ASC LIMIT 1
|
|
540
|
+
`).get(sourceThreadId);
|
|
541
|
+
if (!row?.error)
|
|
542
|
+
return undefined;
|
|
543
|
+
this.database.prepare(`
|
|
544
|
+
UPDATE handoff_jobs SET error_notified = 1, updated_at = ? WHERE job_id = ?
|
|
545
|
+
`).run(Date.now(), row.job_id);
|
|
546
|
+
return row.error;
|
|
57
547
|
}
|
|
58
548
|
setOverlay(overlay) {
|
|
59
549
|
this.database.prepare(`
|
|
@@ -82,5 +572,126 @@ export class HandoffStore {
|
|
|
82
572
|
close() {
|
|
83
573
|
this.database.close();
|
|
84
574
|
}
|
|
575
|
+
jobFromRow(row) {
|
|
576
|
+
return {
|
|
577
|
+
id: row.job_id,
|
|
578
|
+
sourceThreadId: row.source_thread_id,
|
|
579
|
+
params: JSON.parse(row.params_json),
|
|
580
|
+
...(row.pending_json
|
|
581
|
+
? { pending: JSON.parse(row.pending_json) }
|
|
582
|
+
: {}),
|
|
583
|
+
status: row.status,
|
|
584
|
+
...(row.result_json ? { result: JSON.parse(row.result_json) } : {}),
|
|
585
|
+
...(row.target_json ? { target: JSON.parse(row.target_json) } : {}),
|
|
586
|
+
...(row.error ? { error: row.error } : {}),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
insertEpoch(publicThreadId, ordinal, epoch, state, defaultCreatedAt) {
|
|
590
|
+
this.database.prepare(`
|
|
591
|
+
INSERT INTO provider_epochs (
|
|
592
|
+
epoch_id, public_thread_id, ordinal, provider, backend_thread_id,
|
|
593
|
+
model, settings_json, state, created_at, sealed_at
|
|
594
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)
|
|
595
|
+
`).run(epoch.id, publicThreadId, ordinal, epoch.provider, epoch.backendThreadId, epoch.model, JSON.stringify(epoch.settings), state, epoch.createdAt ?? defaultCreatedAt);
|
|
596
|
+
}
|
|
597
|
+
replaceTurns(publicThreadId, turns) {
|
|
598
|
+
this.database.prepare("DELETE FROM logical_turns WHERE public_thread_id = ?").run(publicThreadId);
|
|
599
|
+
const insert = this.database.prepare(`
|
|
600
|
+
INSERT INTO logical_turns (
|
|
601
|
+
public_thread_id, position, public_turn_id, epoch_id, provider_turn_id, turn_json, kind
|
|
602
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
603
|
+
`);
|
|
604
|
+
turns.forEach((turn, position) => {
|
|
605
|
+
if (turn.publicTurnId !== turn.turn.id) {
|
|
606
|
+
throw new Error(`Logical turn id '${turn.publicTurnId}' does not match payload '${turn.turn.id}'.`);
|
|
607
|
+
}
|
|
608
|
+
insert.run(publicThreadId, position, turn.publicTurnId, turn.epochId ?? null, turn.providerTurnId ?? null, JSON.stringify(turn.turn), turn.kind);
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
logicalThreadFromRow(row) {
|
|
612
|
+
return {
|
|
613
|
+
publicThreadId: row.public_thread_id,
|
|
614
|
+
currentEpochId: row.current_epoch_id,
|
|
615
|
+
thread: JSON.parse(row.thread_json),
|
|
616
|
+
revision: row.revision,
|
|
617
|
+
createdAt: row.created_at,
|
|
618
|
+
updatedAt: row.updated_at,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
epochFromRow(row) {
|
|
622
|
+
return {
|
|
623
|
+
id: row.epoch_id,
|
|
624
|
+
publicThreadId: row.public_thread_id,
|
|
625
|
+
ordinal: row.ordinal,
|
|
626
|
+
provider: row.provider,
|
|
627
|
+
backendThreadId: row.backend_thread_id,
|
|
628
|
+
model: row.model,
|
|
629
|
+
settings: JSON.parse(row.settings_json),
|
|
630
|
+
state: row.state,
|
|
631
|
+
createdAt: row.created_at,
|
|
632
|
+
...(row.sealed_at === null ? {} : { sealedAt: row.sealed_at }),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
logicalTurnFromRow(row) {
|
|
636
|
+
return {
|
|
637
|
+
publicThreadId: row.public_thread_id,
|
|
638
|
+
position: row.position,
|
|
639
|
+
publicTurnId: row.public_turn_id,
|
|
640
|
+
...(row.epoch_id ? { epochId: row.epoch_id } : {}),
|
|
641
|
+
...(row.provider_turn_id ? { providerTurnId: row.provider_turn_id } : {}),
|
|
642
|
+
turn: JSON.parse(row.turn_json),
|
|
643
|
+
kind: row.kind,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
providerSwitchJobFromRow(row) {
|
|
647
|
+
return {
|
|
648
|
+
id: row.job_id,
|
|
649
|
+
publicThreadId: row.public_thread_id,
|
|
650
|
+
expectedEpochId: row.expected_epoch_id,
|
|
651
|
+
expectedThreadRevision: row.expected_thread_revision,
|
|
652
|
+
pendingRevision: row.pending_revision,
|
|
653
|
+
targetProvider: row.target_provider,
|
|
654
|
+
targetModel: row.target_model,
|
|
655
|
+
settings: JSON.parse(row.settings_json),
|
|
656
|
+
turnParams: JSON.parse(row.turn_params_json),
|
|
657
|
+
compactionTurn: row.compaction_turn_json
|
|
658
|
+
? JSON.parse(row.compaction_turn_json)
|
|
659
|
+
: {
|
|
660
|
+
id: row.job_id, items: [], itemsView: "full", status: "inProgress", error: null,
|
|
661
|
+
startedAt: null, completedAt: null, durationMs: null,
|
|
662
|
+
},
|
|
663
|
+
status: row.status,
|
|
664
|
+
...(row.summary === null ? {} : { summary: row.summary }),
|
|
665
|
+
...(row.target_backend_thread_id === null ? {} : { targetBackendThreadId: row.target_backend_thread_id }),
|
|
666
|
+
...(row.target_provider_turn_id === null ? {} : { targetProviderTurnId: row.target_provider_turn_id }),
|
|
667
|
+
...(row.error === null ? {} : { error: row.error }),
|
|
668
|
+
createdAt: row.created_at,
|
|
669
|
+
updatedAt: row.updated_at,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
forkSelectionFromRow(row) {
|
|
673
|
+
return {
|
|
674
|
+
targetPublicThreadId: row.target_public_thread_id,
|
|
675
|
+
sourcePublicThreadId: row.source_public_thread_id,
|
|
676
|
+
provisionalEpochId: row.provisional_epoch_id,
|
|
677
|
+
expectedTargetRevision: row.expected_target_revision,
|
|
678
|
+
...(row.selected_epoch_id === null ? {} : { selectedEpochId: row.selected_epoch_id }),
|
|
679
|
+
status: row.status,
|
|
680
|
+
createdAt: row.created_at,
|
|
681
|
+
updatedAt: row.updated_at,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
transaction(action) {
|
|
685
|
+
this.database.exec("BEGIN IMMEDIATE");
|
|
686
|
+
try {
|
|
687
|
+
const result = action();
|
|
688
|
+
this.database.exec("COMMIT");
|
|
689
|
+
return result;
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
this.database.exec("ROLLBACK");
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
85
696
|
}
|
|
86
697
|
//# sourceMappingURL=store.js.map
|