@dsh-enhanced/assistant-delivery 0.1.62 → 0.1.64
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 +32 -10
- package/bin/dsh-restart-supervisor.js +76 -0
- package/lib/agent-runtime.d.ts +21 -1
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +192 -23
- package/lib/agent-runtime.js.map +1 -1
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +10 -0
- package/lib/coordinator.js.map +1 -1
- package/lib/operator-snapshot.d.ts +1 -1
- package/lib/operator-snapshot.d.ts.map +1 -1
- package/lib/operator-snapshot.js +5 -5
- package/lib/operator-snapshot.js.map +1 -1
- package/lib/restart-native.d.ts +47 -0
- package/lib/restart-native.d.ts.map +1 -0
- package/lib/restart-native.js +170 -0
- package/lib/restart-native.js.map +1 -0
- package/lib/restart-runtime.d.ts +38 -0
- package/lib/restart-runtime.d.ts.map +1 -0
- package/lib/restart-runtime.js +335 -0
- package/lib/restart-runtime.js.map +1 -0
- package/lib/restart-store.d.ts +132 -0
- package/lib/restart-store.d.ts.map +1 -0
- package/lib/restart-store.js +757 -0
- package/lib/restart-store.js.map +1 -0
- package/lib/restart-supervisor.d.ts +129 -0
- package/lib/restart-supervisor.d.ts.map +1 -0
- package/lib/restart-supervisor.js +615 -0
- package/lib/restart-supervisor.js.map +1 -0
- package/lib/service.d.ts +45 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +488 -6
- package/lib/service.js.map +1 -1
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +76 -2
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +41 -1
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +433 -85
- package/lib/store.js.map +1 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +21 -2
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +33 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +11 -10
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { chmodSync, lstatSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute } from 'node:path';
|
|
4
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
5
|
+
export class RestartStoreError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = 'RestartStoreError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const statuses = new Set([
|
|
14
|
+
'requested', 'checkpoint', 'ready', 'restarting', 'restarted',
|
|
15
|
+
'resuming', 'completed', 'failed', 'cancelled',
|
|
16
|
+
]);
|
|
17
|
+
const activeStatuses = ['requested', 'checkpoint', 'ready', 'restarting', 'restarted', 'resuming'];
|
|
18
|
+
const digestPattern = /^[a-f0-9]{64}$/u;
|
|
19
|
+
const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,255}$/u;
|
|
20
|
+
function invalid(message) { throw new RestartStoreError('invalid', message); }
|
|
21
|
+
function conflict(message) { throw new RestartStoreError('conflict', message); }
|
|
22
|
+
function corrupt(message) { throw new RestartStoreError('corrupt', message); }
|
|
23
|
+
function identifier(value, label) {
|
|
24
|
+
if (typeof value !== 'string' || !identifierPattern.test(value))
|
|
25
|
+
invalid(`${label} is not a bounded identifier`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function boundedText(value, label, maxBytes) {
|
|
29
|
+
if (typeof value !== 'string' || value.trim() !== value || value.length === 0
|
|
30
|
+
|| Buffer.byteLength(value, 'utf8') > maxBytes
|
|
31
|
+
|| [...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)) {
|
|
32
|
+
invalid(`${label} is invalid`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function positive(value, label, maximum = Number.MAX_SAFE_INTEGER) {
|
|
37
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
|
|
38
|
+
invalid(`${label} is invalid`);
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function nonnegative(value, label) {
|
|
42
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
43
|
+
invalid(`${label} is invalid`);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function digest(value, label) {
|
|
47
|
+
if (typeof value !== 'string' || !digestPattern.test(value))
|
|
48
|
+
invalid(`${label} must be SHA-256 hex`);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function normalizePrincipal(value) {
|
|
52
|
+
return {
|
|
53
|
+
channel: identifier(value.channel, 'principal channel'),
|
|
54
|
+
account: identifier(value.account, 'principal account'),
|
|
55
|
+
tenant: identifier(value.tenant, 'principal tenant'),
|
|
56
|
+
user: identifier(value.user, 'principal user'),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function normalizeConversation(value) {
|
|
60
|
+
const conversation = {
|
|
61
|
+
channel: identifier(value.channel, 'conversation channel'),
|
|
62
|
+
account: identifier(value.account, 'conversation account'),
|
|
63
|
+
tenant: identifier(value.tenant, 'conversation tenant'),
|
|
64
|
+
kind: value.kind,
|
|
65
|
+
chat: identifier(value.chat, 'conversation chat'),
|
|
66
|
+
...(value.thread === undefined ? {} : { thread: identifier(value.thread, 'conversation thread') }),
|
|
67
|
+
};
|
|
68
|
+
if (conversation.kind !== 'dm' && conversation.kind !== 'group')
|
|
69
|
+
invalid('conversation kind is invalid');
|
|
70
|
+
return conversation;
|
|
71
|
+
}
|
|
72
|
+
function normalizeTask(input) {
|
|
73
|
+
const binding = input.binding;
|
|
74
|
+
const principal = normalizePrincipal(binding.principal);
|
|
75
|
+
const conversation = normalizeConversation(binding.conversation);
|
|
76
|
+
if (conversation.channel !== principal.channel || conversation.account !== principal.account
|
|
77
|
+
|| conversation.tenant !== principal.tenant)
|
|
78
|
+
invalid('binding route does not match principal');
|
|
79
|
+
const task = {
|
|
80
|
+
inboxId: identifier(input.inboxId, 'inbox ID'),
|
|
81
|
+
eventId: identifier(input.eventId, 'provider event ID'),
|
|
82
|
+
binding: {
|
|
83
|
+
id: identifier(binding.id, 'binding ID'),
|
|
84
|
+
version: positive(binding.version, 'binding version'),
|
|
85
|
+
generation: positive(binding.generation, 'binding generation'),
|
|
86
|
+
sessionId: identifier(binding.sessionId, 'session ID'),
|
|
87
|
+
workspace: boundedText(binding.workspace, 'workspace', 2_048),
|
|
88
|
+
preset: identifier(binding.preset, 'agent preset'),
|
|
89
|
+
policyRef: identifier(binding.policyRef, 'binding policy reference'),
|
|
90
|
+
authorityHash: digest(binding.authorityHash, 'owner authority hash'),
|
|
91
|
+
conversation, principal,
|
|
92
|
+
},
|
|
93
|
+
owner: {
|
|
94
|
+
principalRecordId: identifier(input.owner.principalRecordId, 'owner row ID'),
|
|
95
|
+
principalVersion: positive(input.owner.principalVersion, 'owner row version'),
|
|
96
|
+
},
|
|
97
|
+
sourceInvocationId: identifier(input.sourceInvocationId, 'source invocation'),
|
|
98
|
+
maxModelRequests: positive(input.maxModelRequests, 'model request budget', 10_000),
|
|
99
|
+
maxToolCalls: positive(input.maxToolCalls, 'tool call budget', 10_000),
|
|
100
|
+
maxOutputTokens: positive(input.maxOutputTokens, 'output token cap', 1_000_000),
|
|
101
|
+
capabilityDigest: digest(input.capabilityDigest, 'capability digest'),
|
|
102
|
+
deadline: positive(input.deadline, 'absolute deadline'),
|
|
103
|
+
};
|
|
104
|
+
return task;
|
|
105
|
+
}
|
|
106
|
+
function normalizeModel(input) {
|
|
107
|
+
return {
|
|
108
|
+
provider: identifier(input.provider, 'model provider'),
|
|
109
|
+
model: identifier(input.model, 'model ID'),
|
|
110
|
+
...(input.reasoningEffort === undefined ? {}
|
|
111
|
+
: { reasoningEffort: identifier(input.reasoningEffort, 'reasoning effort') }),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function normalizeCheckpoint(input) {
|
|
115
|
+
return {
|
|
116
|
+
sessionEventCount: nonnegative(input.sessionEventCount, 'Session event count'),
|
|
117
|
+
sessionDigest: digest(input.sessionDigest, 'Session digest'),
|
|
118
|
+
continuationMessageId: identifier(input.continuationMessageId, 'continuation message ID'),
|
|
119
|
+
continuationTextDigest: digest(input.continuationTextDigest, 'continuation text digest'),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function hash(value) { return createHash('sha256').update(value).digest('hex'); }
|
|
123
|
+
function currentUid() {
|
|
124
|
+
const uid = process.getuid?.();
|
|
125
|
+
if (uid === undefined)
|
|
126
|
+
invalid('restart journal requires a POSIX owner identity');
|
|
127
|
+
return uid;
|
|
128
|
+
}
|
|
129
|
+
/** Refuse symlinks, hardlinks, world-visible state, and state belonging to another UID. */
|
|
130
|
+
function assertPrivate(path) {
|
|
131
|
+
const stat = lstatSync(path);
|
|
132
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1
|
|
133
|
+
|| stat.uid !== currentUid() || (stat.mode & 0o077) !== 0) {
|
|
134
|
+
throw new RestartStoreError('invalid', `unsafe restart journal: ${path}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function taskFromRow(row) {
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(row.task_json);
|
|
140
|
+
const normalized = normalizeTask(parsed);
|
|
141
|
+
if (JSON.stringify(normalized) !== row.task_json || normalized.inboxId !== row.inbox_id
|
|
142
|
+
|| !Number.isSafeInteger(row.model_requests) || row.model_requests < 0
|
|
143
|
+
|| !Number.isSafeInteger(row.tool_calls) || row.tool_calls < 0
|
|
144
|
+
|| !Number.isSafeInteger(row.created_at) || row.created_at < 0)
|
|
145
|
+
corrupt('restart task row changed');
|
|
146
|
+
let model;
|
|
147
|
+
if (row.model_json !== null) {
|
|
148
|
+
model = normalizeModel(JSON.parse(row.model_json));
|
|
149
|
+
if (JSON.stringify(model) !== row.model_json)
|
|
150
|
+
corrupt('restart model row changed');
|
|
151
|
+
}
|
|
152
|
+
return { ...normalized, createdAt: row.created_at, ...(model === undefined ? {} : { model }),
|
|
153
|
+
modelRequests: row.model_requests, toolCalls: row.tool_calls };
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error instanceof RestartStoreError && error.code === 'corrupt')
|
|
157
|
+
throw error;
|
|
158
|
+
corrupt('restart task row is unreadable');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function requestFromRow(row) {
|
|
162
|
+
try {
|
|
163
|
+
const status = row.status;
|
|
164
|
+
if (!statuses.has(status) || !Number.isSafeInteger(row.created_at)
|
|
165
|
+
|| !Number.isSafeInteger(row.updated_at) || row.created_at < 0 || row.updated_at < row.created_at) {
|
|
166
|
+
corrupt('restart request row changed');
|
|
167
|
+
}
|
|
168
|
+
const checkpoint = row.checkpoint_json === null ? undefined
|
|
169
|
+
: normalizeCheckpoint(JSON.parse(row.checkpoint_json));
|
|
170
|
+
if (checkpoint !== undefined && JSON.stringify(checkpoint) !== row.checkpoint_json)
|
|
171
|
+
corrupt('restart checkpoint changed');
|
|
172
|
+
if (row.id !== `restart_${hash(`${row.inbox_id}\0${row.request_key}`).slice(0, 32)}`
|
|
173
|
+
|| (['checkpoint', 'ready', 'restarting', 'restarted', 'resuming', 'completed'].includes(status)
|
|
174
|
+
&& checkpoint === undefined)
|
|
175
|
+
|| (status === 'restarting' && row.restart_holder_id === null)
|
|
176
|
+
|| (['restarted', 'resuming', 'completed'].includes(status) && row.restart_invocation_id === null)
|
|
177
|
+
|| (['resuming', 'completed'].includes(status) && row.continuation_holder_id === null)
|
|
178
|
+
|| (status === 'failed' && row.failure_code === null)
|
|
179
|
+
|| (status !== 'failed' && row.failure_code !== null))
|
|
180
|
+
corrupt('restart request transition proof changed');
|
|
181
|
+
return {
|
|
182
|
+
id: identifier(row.id, 'restart ID'), inboxId: identifier(row.inbox_id, 'inbox ID'),
|
|
183
|
+
requestKey: identifier(row.request_key, 'restart key'), reason: boundedText(row.reason, 'reason', 240),
|
|
184
|
+
status, sourceInvocationId: identifier(row.source_invocation_id, 'source invocation'),
|
|
185
|
+
...(row.restart_invocation_id === null ? {} : { restartInvocationId: identifier(row.restart_invocation_id, 'restart invocation') }),
|
|
186
|
+
...(row.restart_holder_id === null ? {} : { restartHolderId: identifier(row.restart_holder_id, 'restart holder') }),
|
|
187
|
+
...(row.continuation_holder_id === null ? {} : { continuationHolderId: identifier(row.continuation_holder_id, 'continuation holder') }),
|
|
188
|
+
...(checkpoint === undefined ? {} : { checkpoint }),
|
|
189
|
+
...(row.failure_code === null ? {} : { failureCode: identifier(row.failure_code, 'failure code') }),
|
|
190
|
+
createdAt: row.created_at, updatedAt: row.updated_at,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error instanceof RestartStoreError && error.code === 'corrupt')
|
|
195
|
+
throw error;
|
|
196
|
+
corrupt('restart request row is unreadable');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const taskSelect = `SELECT inbox_id, task_json, model_json, model_requests, tool_calls, created_at
|
|
200
|
+
FROM restart_tasks`;
|
|
201
|
+
const restartApplicationId = 0x44534852;
|
|
202
|
+
const requestSelect = `SELECT id, inbox_id, request_key, reason, status, source_invocation_id,
|
|
203
|
+
restart_invocation_id, restart_holder_id, continuation_holder_id, checkpoint_json,
|
|
204
|
+
failure_code, created_at, updated_at FROM restart_requests`;
|
|
205
|
+
export class RestartStore {
|
|
206
|
+
database;
|
|
207
|
+
now;
|
|
208
|
+
readOnly;
|
|
209
|
+
closed = false;
|
|
210
|
+
constructor(options) {
|
|
211
|
+
if (!isAbsolute(options.path))
|
|
212
|
+
invalid('restart journal path must be absolute');
|
|
213
|
+
const parent = dirname(options.path);
|
|
214
|
+
if (!options.readOnly)
|
|
215
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
216
|
+
const directory = lstatSync(parent);
|
|
217
|
+
if (!directory.isDirectory() || directory.isSymbolicLink() || directory.uid !== currentUid()
|
|
218
|
+
|| (directory.mode & 0o077) !== 0)
|
|
219
|
+
invalid('restart journal parent is not private');
|
|
220
|
+
let existed = true;
|
|
221
|
+
try {
|
|
222
|
+
assertPrivate(options.path);
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (options.readOnly || error.code !== 'ENOENT')
|
|
226
|
+
throw error;
|
|
227
|
+
existed = false;
|
|
228
|
+
}
|
|
229
|
+
this.readOnly = options.readOnly ?? false;
|
|
230
|
+
this.now = options.now ?? Date.now;
|
|
231
|
+
this.database = new DatabaseSync(options.path, { readOnly: this.readOnly });
|
|
232
|
+
try {
|
|
233
|
+
this.database.exec('PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;');
|
|
234
|
+
const initialVersion = this.database.prepare('PRAGMA user_version').get();
|
|
235
|
+
const initialApplication = this.database.prepare('PRAGMA application_id').get();
|
|
236
|
+
if (initialVersion.user_version === 1) {
|
|
237
|
+
if (initialApplication.application_id !== restartApplicationId)
|
|
238
|
+
corrupt('restart journal identity changed');
|
|
239
|
+
}
|
|
240
|
+
else if (initialVersion.user_version !== 0 || existed || this.readOnly) {
|
|
241
|
+
corrupt('unsupported restart journal schema');
|
|
242
|
+
}
|
|
243
|
+
if (!this.readOnly) {
|
|
244
|
+
chmodSync(options.path, 0o600);
|
|
245
|
+
this.database.exec('PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;');
|
|
246
|
+
}
|
|
247
|
+
assertPrivate(options.path);
|
|
248
|
+
for (const extension of ['-wal', '-shm']) {
|
|
249
|
+
try {
|
|
250
|
+
assertPrivate(`${options.path}${extension}`);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (error.code !== 'ENOENT')
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (initialVersion.user_version === 0)
|
|
258
|
+
this.database.exec(`
|
|
259
|
+
CREATE TABLE IF NOT EXISTS restart_tasks (
|
|
260
|
+
inbox_id TEXT PRIMARY KEY, task_json TEXT NOT NULL, model_json TEXT,
|
|
261
|
+
model_requests INTEGER NOT NULL DEFAULT 0 CHECK(model_requests >= 0),
|
|
262
|
+
tool_calls INTEGER NOT NULL DEFAULT 0 CHECK(tool_calls >= 0),
|
|
263
|
+
created_at INTEGER NOT NULL CHECK(created_at >= 0)
|
|
264
|
+
) STRICT;
|
|
265
|
+
CREATE TABLE IF NOT EXISTS restart_requests (
|
|
266
|
+
id TEXT PRIMARY KEY, inbox_id TEXT NOT NULL UNIQUE REFERENCES restart_tasks(inbox_id),
|
|
267
|
+
request_key TEXT NOT NULL UNIQUE, reason TEXT NOT NULL,
|
|
268
|
+
status TEXT NOT NULL CHECK(status IN ('requested','checkpoint','ready','restarting','restarted','resuming','completed','failed','cancelled')),
|
|
269
|
+
source_invocation_id TEXT NOT NULL, restart_invocation_id TEXT,
|
|
270
|
+
restart_holder_id TEXT, continuation_holder_id TEXT, checkpoint_json TEXT,
|
|
271
|
+
failure_code TEXT, created_at INTEGER NOT NULL CHECK(created_at >= 0),
|
|
272
|
+
updated_at INTEGER NOT NULL CHECK(updated_at >= created_at)
|
|
273
|
+
) STRICT;
|
|
274
|
+
CREATE INDEX IF NOT EXISTS restart_request_status ON restart_requests(status,created_at,id);
|
|
275
|
+
CREATE TABLE IF NOT EXISTS restart_budget_events (
|
|
276
|
+
inbox_id TEXT NOT NULL REFERENCES restart_tasks(inbox_id), kind TEXT NOT NULL CHECK(kind IN ('model','tool')),
|
|
277
|
+
operation_id TEXT NOT NULL, phase TEXT NOT NULL CHECK(phase IN ('observed','reserved')),
|
|
278
|
+
reserved_at INTEGER NOT NULL CHECK(reserved_at >= 0),
|
|
279
|
+
PRIMARY KEY(inbox_id,kind,operation_id)
|
|
280
|
+
) STRICT;
|
|
281
|
+
CREATE TABLE IF NOT EXISTS restart_ineligible (
|
|
282
|
+
inbox_id TEXT PRIMARY KEY REFERENCES restart_tasks(inbox_id),
|
|
283
|
+
reason TEXT NOT NULL, marked_at INTEGER NOT NULL CHECK(marked_at >= 0)
|
|
284
|
+
) STRICT;
|
|
285
|
+
CREATE TABLE IF NOT EXISTS restart_events (
|
|
286
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT, request_id TEXT NOT NULL REFERENCES restart_requests(id),
|
|
287
|
+
event TEXT NOT NULL, at INTEGER NOT NULL CHECK(at >= 0)
|
|
288
|
+
) STRICT;
|
|
289
|
+
CREATE TRIGGER IF NOT EXISTS restart_events_no_update BEFORE UPDATE ON restart_events BEGIN SELECT RAISE(ABORT,'immutable restart event'); END;
|
|
290
|
+
CREATE TRIGGER IF NOT EXISTS restart_events_no_delete BEFORE DELETE ON restart_events BEGIN SELECT RAISE(ABORT,'immutable restart event'); END;
|
|
291
|
+
CREATE TRIGGER IF NOT EXISTS restart_budget_no_update BEFORE UPDATE ON restart_budget_events BEGIN SELECT RAISE(ABORT,'immutable restart reservation'); END;
|
|
292
|
+
CREATE TRIGGER IF NOT EXISTS restart_budget_no_delete BEFORE DELETE ON restart_budget_events BEGIN SELECT RAISE(ABORT,'immutable restart reservation'); END;
|
|
293
|
+
CREATE TRIGGER IF NOT EXISTS restart_ineligible_no_update BEFORE UPDATE ON restart_ineligible BEGIN SELECT RAISE(ABORT,'immutable restart ineligibility'); END;
|
|
294
|
+
CREATE TRIGGER IF NOT EXISTS restart_ineligible_no_delete BEFORE DELETE ON restart_ineligible BEGIN SELECT RAISE(ABORT,'immutable restart ineligibility'); END;
|
|
295
|
+
`);
|
|
296
|
+
if (initialVersion.user_version === 0) {
|
|
297
|
+
this.database.exec(`PRAGMA application_id = ${restartApplicationId}; PRAGMA user_version = 1;`);
|
|
298
|
+
}
|
|
299
|
+
for (const [name, kind] of [
|
|
300
|
+
['restart_tasks', 'table'], ['restart_requests', 'table'],
|
|
301
|
+
['restart_budget_events', 'table'], ['restart_ineligible', 'table'], ['restart_events', 'table'],
|
|
302
|
+
['restart_events_no_update', 'trigger'], ['restart_events_no_delete', 'trigger'],
|
|
303
|
+
['restart_budget_no_update', 'trigger'], ['restart_budget_no_delete', 'trigger'],
|
|
304
|
+
['restart_ineligible_no_update', 'trigger'], ['restart_ineligible_no_delete', 'trigger'],
|
|
305
|
+
]) {
|
|
306
|
+
const object = this.database.prepare(`SELECT type, sql FROM sqlite_master WHERE name = ?`)
|
|
307
|
+
.get(name);
|
|
308
|
+
if (object?.type !== kind || typeof object.sql !== 'string'
|
|
309
|
+
|| (kind === 'table' && !object.sql.endsWith('STRICT'))) {
|
|
310
|
+
corrupt(`restart journal ${name} schema is missing`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
for (const [table, expected] of [
|
|
314
|
+
['restart_budget_events', ['inbox_id', 'kind', 'operation_id', 'phase', 'reserved_at']],
|
|
315
|
+
['restart_ineligible', ['inbox_id', 'reason', 'marked_at']],
|
|
316
|
+
]) {
|
|
317
|
+
const columns = this.database.prepare(`PRAGMA table_info(${table})`).all();
|
|
318
|
+
if (JSON.stringify(columns.map(column => column.name)) !== JSON.stringify(expected)) {
|
|
319
|
+
corrupt(`restart journal ${table} columns changed`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
this.database.close();
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
assertOpen() {
|
|
329
|
+
if (this.closed)
|
|
330
|
+
throw new RestartStoreError('closed', 'restart journal is closed');
|
|
331
|
+
}
|
|
332
|
+
write(fn) {
|
|
333
|
+
this.assertOpen();
|
|
334
|
+
if (this.readOnly)
|
|
335
|
+
invalid('restart journal is read only');
|
|
336
|
+
this.database.exec('BEGIN IMMEDIATE');
|
|
337
|
+
try {
|
|
338
|
+
const value = fn();
|
|
339
|
+
this.database.exec('COMMIT');
|
|
340
|
+
return value;
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
this.database.exec('ROLLBACK');
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
task(inboxId) {
|
|
348
|
+
const row = this.database.prepare(`${taskSelect} WHERE inbox_id = ?`).get(inboxId);
|
|
349
|
+
if (row === undefined)
|
|
350
|
+
return undefined;
|
|
351
|
+
const task = taskFromRow(row);
|
|
352
|
+
const counts = this.database.prepare(`SELECT
|
|
353
|
+
COALESCE(SUM(CASE WHEN kind = 'model' THEN 1 ELSE 0 END),0) AS model_count,
|
|
354
|
+
COALESCE(SUM(CASE WHEN kind = 'tool' THEN 1 ELSE 0 END),0) AS tool_count
|
|
355
|
+
FROM restart_budget_events WHERE inbox_id = ?`).get(inboxId);
|
|
356
|
+
if (counts.model_count !== task.modelRequests || counts.tool_count !== task.toolCalls) {
|
|
357
|
+
corrupt('restart budget reservations changed');
|
|
358
|
+
}
|
|
359
|
+
return task;
|
|
360
|
+
}
|
|
361
|
+
getTask(inboxId) {
|
|
362
|
+
this.assertOpen();
|
|
363
|
+
return this.task(identifier(inboxId, 'inbox ID'));
|
|
364
|
+
}
|
|
365
|
+
ineligibility(inboxId) {
|
|
366
|
+
const row = this.database.prepare(`SELECT reason,marked_at FROM restart_ineligible WHERE inbox_id = ?`)
|
|
367
|
+
.get(inboxId);
|
|
368
|
+
if (row !== undefined) {
|
|
369
|
+
try {
|
|
370
|
+
boundedText(row.reason, 'ineligibility reason', 120);
|
|
371
|
+
nonnegative(row.marked_at, 'ineligibility time');
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
corrupt('restart ineligibility record changed');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return row;
|
|
378
|
+
}
|
|
379
|
+
markIneligibleWithinWrite(inboxId, reason) {
|
|
380
|
+
if (this.task(inboxId) === undefined)
|
|
381
|
+
throw new RestartStoreError('not-found', 'restart task is missing');
|
|
382
|
+
const prior = this.ineligibility(inboxId);
|
|
383
|
+
if (prior !== undefined)
|
|
384
|
+
return;
|
|
385
|
+
if (this.getRequestForTask(inboxId) !== undefined)
|
|
386
|
+
conflict('restart request already exists');
|
|
387
|
+
this.database.prepare(`INSERT INTO restart_ineligible(inbox_id,reason,marked_at) VALUES(?,?,?)`)
|
|
388
|
+
.run(inboxId, reason, nonnegative(this.now(), 'clock'));
|
|
389
|
+
}
|
|
390
|
+
markIneligible(inboxId, reason) {
|
|
391
|
+
identifier(inboxId, 'inbox ID');
|
|
392
|
+
boundedText(reason, 'ineligibility reason', 120);
|
|
393
|
+
this.write(() => this.markIneligibleWithinWrite(inboxId, reason));
|
|
394
|
+
}
|
|
395
|
+
isEligible(inboxId) {
|
|
396
|
+
this.assertOpen();
|
|
397
|
+
const task = this.task(identifier(inboxId, 'inbox ID'));
|
|
398
|
+
if (task === undefined || this.ineligibility(inboxId) !== undefined)
|
|
399
|
+
return false;
|
|
400
|
+
if (this.getRequestForTask(inboxId) !== undefined)
|
|
401
|
+
return false;
|
|
402
|
+
return task.model !== undefined && this.now() < task.deadline
|
|
403
|
+
&& task.modelRequests < task.maxModelRequests && task.toolCalls <= task.maxToolCalls;
|
|
404
|
+
}
|
|
405
|
+
beginTask(input) {
|
|
406
|
+
const task = normalizeTask(input);
|
|
407
|
+
return this.write(() => {
|
|
408
|
+
const prior = this.task(task.inboxId);
|
|
409
|
+
if (prior !== undefined) {
|
|
410
|
+
if (JSON.stringify(normalizeTask(prior)) !== JSON.stringify(task))
|
|
411
|
+
conflict('restart task identity or budget changed');
|
|
412
|
+
return prior;
|
|
413
|
+
}
|
|
414
|
+
const now = nonnegative(this.now(), 'clock');
|
|
415
|
+
this.database.prepare(`INSERT INTO restart_tasks(inbox_id,task_json,created_at) VALUES(?,?,?)`)
|
|
416
|
+
.run(task.inboxId, JSON.stringify(task), now);
|
|
417
|
+
return this.task(task.inboxId);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
captureModel(inboxId, input) {
|
|
421
|
+
identifier(inboxId, 'inbox ID');
|
|
422
|
+
const model = normalizeModel(input);
|
|
423
|
+
return this.write(() => {
|
|
424
|
+
const current = this.task(inboxId);
|
|
425
|
+
if (current === undefined)
|
|
426
|
+
throw new RestartStoreError('not-found', 'restart task is missing');
|
|
427
|
+
if (current.model !== undefined) {
|
|
428
|
+
if (JSON.stringify(current.model) !== JSON.stringify(model))
|
|
429
|
+
conflict('frozen model route changed');
|
|
430
|
+
return current;
|
|
431
|
+
}
|
|
432
|
+
this.database.prepare(`UPDATE restart_tasks SET model_json = ? WHERE inbox_id = ? AND model_json IS NULL`)
|
|
433
|
+
.run(JSON.stringify(model), inboxId);
|
|
434
|
+
return this.task(inboxId);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
/** A supplier change ends recovery eligibility without interrupting normal work. */
|
|
438
|
+
observeModel(inboxId, input) {
|
|
439
|
+
identifier(inboxId, 'inbox ID');
|
|
440
|
+
const model = normalizeModel(input);
|
|
441
|
+
return this.write(() => {
|
|
442
|
+
const current = this.task(inboxId);
|
|
443
|
+
if (current === undefined)
|
|
444
|
+
throw new RestartStoreError('not-found', 'restart task is missing');
|
|
445
|
+
if (this.getRequestForTask(inboxId) !== undefined)
|
|
446
|
+
conflict('restart request blocks ordinary model observation');
|
|
447
|
+
if (current.model === undefined) {
|
|
448
|
+
this.database.prepare(`UPDATE restart_tasks SET model_json = ? WHERE inbox_id = ? AND model_json IS NULL`)
|
|
449
|
+
.run(JSON.stringify(model), inboxId);
|
|
450
|
+
}
|
|
451
|
+
else if (JSON.stringify(current.model) !== JSON.stringify(model)) {
|
|
452
|
+
this.markIneligibleWithinWrite(inboxId, 'model-drift');
|
|
453
|
+
}
|
|
454
|
+
return this.task(inboxId);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
/** Normal foreground calls are observed durably, even after recovery becomes impossible. */
|
|
458
|
+
observe(inboxId, kind, operationId) {
|
|
459
|
+
identifier(inboxId, 'inbox ID');
|
|
460
|
+
identifier(operationId, 'operation ID');
|
|
461
|
+
if (kind !== 'model' && kind !== 'tool')
|
|
462
|
+
invalid('observation kind is invalid');
|
|
463
|
+
return this.write(() => {
|
|
464
|
+
const current = this.task(inboxId);
|
|
465
|
+
if (current === undefined)
|
|
466
|
+
throw new RestartStoreError('not-found', 'restart task is missing');
|
|
467
|
+
if (this.getRequestForTask(inboxId) !== undefined)
|
|
468
|
+
conflict('restart request blocks ordinary observations');
|
|
469
|
+
const prior = this.database.prepare(`SELECT phase FROM restart_budget_events
|
|
470
|
+
WHERE inbox_id = ? AND kind = ? AND operation_id = ?`).get(inboxId, kind, operationId);
|
|
471
|
+
if (prior !== undefined) {
|
|
472
|
+
if (prior.phase !== 'observed')
|
|
473
|
+
conflict('operation was reserved for recovery');
|
|
474
|
+
return current;
|
|
475
|
+
}
|
|
476
|
+
const column = kind === 'model' ? 'model_requests' : 'tool_calls';
|
|
477
|
+
const count = kind === 'model' ? current.modelRequests : current.toolCalls;
|
|
478
|
+
if (count >= Number.MAX_SAFE_INTEGER)
|
|
479
|
+
corrupt('restart task observation counter overflow');
|
|
480
|
+
this.database.prepare(`UPDATE restart_tasks SET ${column} = ${column} + 1 WHERE inbox_id = ?`).run(inboxId);
|
|
481
|
+
this.database.prepare(`INSERT INTO restart_budget_events(inbox_id,kind,operation_id,phase,reserved_at) VALUES(?,?,?,'observed',?)`)
|
|
482
|
+
.run(inboxId, kind, operationId, nonnegative(this.now(), 'clock'));
|
|
483
|
+
return this.task(inboxId);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
/** A held request/tool reservation is never refunded after an unknown result. */
|
|
487
|
+
reserve(inboxId, kind, operationId) {
|
|
488
|
+
identifier(inboxId, 'inbox ID');
|
|
489
|
+
identifier(operationId, 'operation ID');
|
|
490
|
+
if (kind !== 'model' && kind !== 'tool')
|
|
491
|
+
invalid('reservation kind is invalid');
|
|
492
|
+
return this.write(() => {
|
|
493
|
+
const current = this.task(inboxId);
|
|
494
|
+
if (current === undefined)
|
|
495
|
+
throw new RestartStoreError('not-found', 'restart task is missing');
|
|
496
|
+
if (current.model === undefined)
|
|
497
|
+
conflict('model route is not frozen');
|
|
498
|
+
const restart = this.getRequestForTask(inboxId);
|
|
499
|
+
if (restart?.status !== 'resuming')
|
|
500
|
+
conflict('only an accepted continuation may reserve work');
|
|
501
|
+
const prior = this.database.prepare(`SELECT phase FROM restart_budget_events
|
|
502
|
+
WHERE inbox_id = ? AND kind = ? AND operation_id = ?`).get(inboxId, kind, operationId);
|
|
503
|
+
if (prior !== undefined) {
|
|
504
|
+
if (prior.phase !== 'reserved')
|
|
505
|
+
conflict('operation was observed before recovery');
|
|
506
|
+
return current;
|
|
507
|
+
}
|
|
508
|
+
if (this.now() >= current.deadline)
|
|
509
|
+
throw new RestartStoreError('deadline', 'task deadline has elapsed');
|
|
510
|
+
const count = kind === 'model' ? current.modelRequests : current.toolCalls;
|
|
511
|
+
const maximum = kind === 'model' ? current.maxModelRequests : current.maxToolCalls;
|
|
512
|
+
if (count >= maximum)
|
|
513
|
+
throw new RestartStoreError('budget', `${kind} task budget exhausted`);
|
|
514
|
+
const column = kind === 'model' ? 'model_requests' : 'tool_calls';
|
|
515
|
+
this.database.prepare(`UPDATE restart_tasks SET ${column} = ${column} + 1 WHERE inbox_id = ?`).run(inboxId);
|
|
516
|
+
this.database.prepare(`INSERT INTO restart_budget_events(inbox_id,kind,operation_id,phase,reserved_at) VALUES(?,?,?,'reserved',?)`)
|
|
517
|
+
.run(inboxId, kind, operationId, nonnegative(this.now(), 'clock'));
|
|
518
|
+
return this.task(inboxId);
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
requestById(id) {
|
|
522
|
+
const row = this.database.prepare(`${requestSelect} WHERE id = ?`).get(id);
|
|
523
|
+
if (row === undefined)
|
|
524
|
+
return undefined;
|
|
525
|
+
const request = requestFromRow(row);
|
|
526
|
+
const task = this.task(request.inboxId);
|
|
527
|
+
if (task === undefined || request.sourceInvocationId !== task.sourceInvocationId) {
|
|
528
|
+
corrupt('restart request task lineage changed');
|
|
529
|
+
}
|
|
530
|
+
if (this.ineligibility(request.inboxId) !== undefined)
|
|
531
|
+
corrupt('ineligible task has a restart request');
|
|
532
|
+
return request;
|
|
533
|
+
}
|
|
534
|
+
getRequest(id) {
|
|
535
|
+
this.assertOpen();
|
|
536
|
+
return this.requestById(identifier(id, 'restart ID'));
|
|
537
|
+
}
|
|
538
|
+
getRequestForTask(inboxId) {
|
|
539
|
+
this.assertOpen();
|
|
540
|
+
const row = this.database.prepare(`${requestSelect} WHERE inbox_id = ?`)
|
|
541
|
+
.get(identifier(inboxId, 'inbox ID'));
|
|
542
|
+
return row === undefined ? undefined : this.requestById(row.id);
|
|
543
|
+
}
|
|
544
|
+
request(inboxId, input) {
|
|
545
|
+
identifier(inboxId, 'inbox ID');
|
|
546
|
+
const requestKey = identifier(input.requestKey, 'restart request key');
|
|
547
|
+
const reason = boundedText(input.reason, 'restart reason', 240);
|
|
548
|
+
return this.write(() => {
|
|
549
|
+
const task = this.task(inboxId);
|
|
550
|
+
if (task === undefined)
|
|
551
|
+
throw new RestartStoreError('not-found', 'restart task is missing');
|
|
552
|
+
const prior = this.getRequestForTask(inboxId);
|
|
553
|
+
if (prior !== undefined) {
|
|
554
|
+
if (prior.requestKey !== requestKey || prior.reason !== reason)
|
|
555
|
+
conflict('restart request identity changed');
|
|
556
|
+
return prior;
|
|
557
|
+
}
|
|
558
|
+
if (task.model === undefined)
|
|
559
|
+
conflict('restart requires frozen actual model');
|
|
560
|
+
const now = nonnegative(this.now(), 'clock');
|
|
561
|
+
if (now >= task.deadline)
|
|
562
|
+
throw new RestartStoreError('deadline', 'task deadline has elapsed');
|
|
563
|
+
if (this.ineligibility(inboxId) !== undefined)
|
|
564
|
+
conflict('task is not eligible for restart');
|
|
565
|
+
if (task.modelRequests >= task.maxModelRequests || task.toolCalls > task.maxToolCalls) {
|
|
566
|
+
throw new RestartStoreError('budget', 'task has no bounded continuation budget');
|
|
567
|
+
}
|
|
568
|
+
const id = `restart_${hash(`${inboxId}\0${requestKey}`).slice(0, 32)}`;
|
|
569
|
+
this.database.prepare(`INSERT INTO restart_requests(id,inbox_id,request_key,reason,status,source_invocation_id,created_at,updated_at)
|
|
570
|
+
VALUES(?,?,?,?,'requested',?,?,?)`).run(id, inboxId, requestKey, reason, task.sourceInvocationId, now, now);
|
|
571
|
+
this.event(id, 'requested', now);
|
|
572
|
+
return this.requestById(id);
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
event(id, name, now) {
|
|
576
|
+
this.database.prepare(`INSERT INTO restart_events(request_id,event,at) VALUES(?,?,?)`).run(id, name, now);
|
|
577
|
+
}
|
|
578
|
+
transition(id, expected, next, fields = {}) {
|
|
579
|
+
return this.write(() => {
|
|
580
|
+
const current = this.requestById(id);
|
|
581
|
+
if (current === undefined)
|
|
582
|
+
throw new RestartStoreError('not-found', 'restart request is missing');
|
|
583
|
+
if (!expected.includes(current.status))
|
|
584
|
+
conflict(`restart transition ${current.status} → ${next} refused`);
|
|
585
|
+
const now = nonnegative(this.now(), 'clock');
|
|
586
|
+
const task = this.task(current.inboxId);
|
|
587
|
+
if (task === undefined)
|
|
588
|
+
corrupt('restart task is missing');
|
|
589
|
+
if (next !== 'cancelled' && next !== 'failed' && next !== 'completed' && now >= task.deadline) {
|
|
590
|
+
throw new RestartStoreError('deadline', 'task deadline has elapsed');
|
|
591
|
+
}
|
|
592
|
+
const assignments = ['status = ?', 'updated_at = ?', ...Object.keys(fields).map(key => `${key} = ?`)].join(', ');
|
|
593
|
+
const params = [next, now, ...Object.values(fields), id, current.status];
|
|
594
|
+
const result = this.database.prepare(`UPDATE restart_requests SET ${assignments} WHERE id = ? AND status = ?`).run(...params);
|
|
595
|
+
if (result.changes !== 1)
|
|
596
|
+
conflict('restart transition lost its compare-and-set');
|
|
597
|
+
this.event(id, next, now);
|
|
598
|
+
return this.requestById(id);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
checkpoint(id, input) {
|
|
602
|
+
identifier(id, 'restart ID');
|
|
603
|
+
const checkpoint = normalizeCheckpoint(input);
|
|
604
|
+
const current = this.getRequest(id);
|
|
605
|
+
if (current?.status === 'checkpoint' && JSON.stringify(current.checkpoint) === JSON.stringify(checkpoint))
|
|
606
|
+
return current;
|
|
607
|
+
return this.transition(id, ['requested'], 'checkpoint', { checkpoint_json: JSON.stringify(checkpoint) });
|
|
608
|
+
}
|
|
609
|
+
ready(id) {
|
|
610
|
+
identifier(id, 'restart ID');
|
|
611
|
+
const current = this.getRequest(id);
|
|
612
|
+
if (current?.status === 'ready')
|
|
613
|
+
return current;
|
|
614
|
+
if (current?.checkpoint === undefined)
|
|
615
|
+
conflict('restart checkpoint is missing');
|
|
616
|
+
return this.transition(id, ['checkpoint'], 'ready');
|
|
617
|
+
}
|
|
618
|
+
claimRestart(id, input) {
|
|
619
|
+
identifier(id, 'restart ID');
|
|
620
|
+
const holder = identifier(input.holderId, 'restart holder');
|
|
621
|
+
const invocation = identifier(input.invocationId, 'current invocation');
|
|
622
|
+
const current = this.getRequest(id);
|
|
623
|
+
if (current?.sourceInvocationId !== invocation)
|
|
624
|
+
conflict('restart source invocation changed');
|
|
625
|
+
if (current?.status === 'restarting') {
|
|
626
|
+
if (current.restartHolderId !== holder)
|
|
627
|
+
conflict('restart already claimed by another holder');
|
|
628
|
+
return current;
|
|
629
|
+
}
|
|
630
|
+
return this.transition(id, ['ready'], 'restarting', { restart_holder_id: holder });
|
|
631
|
+
}
|
|
632
|
+
/** Observation only: this never grants another restart dispatch. */
|
|
633
|
+
markRestarted(id, input) {
|
|
634
|
+
identifier(id, 'restart ID');
|
|
635
|
+
const invocation = identifier(input.invocationId, 'new invocation');
|
|
636
|
+
const current = this.getRequest(id);
|
|
637
|
+
if (current === undefined)
|
|
638
|
+
throw new RestartStoreError('not-found', 'restart request is missing');
|
|
639
|
+
if (invocation === current.sourceInvocationId)
|
|
640
|
+
conflict('restart did not produce a new invocation');
|
|
641
|
+
if (current.status === 'restarted' && current.restartInvocationId === invocation)
|
|
642
|
+
return current;
|
|
643
|
+
// A supervisor may die before writing its dispatch receipt after systemd has
|
|
644
|
+
// already restarted the target. A fresh invocation proves the event without
|
|
645
|
+
// giving another supervisor a second command grant.
|
|
646
|
+
return this.transition(id, ['ready', 'restarting'], 'restarted', { restart_invocation_id: invocation });
|
|
647
|
+
}
|
|
648
|
+
claimContinuation(id, input) {
|
|
649
|
+
identifier(id, 'restart ID');
|
|
650
|
+
const holder = identifier(input.holderId, 'continuation holder');
|
|
651
|
+
const invocation = identifier(input.invocationId, 'continuation invocation');
|
|
652
|
+
const current = this.getRequest(id);
|
|
653
|
+
if (current?.restartInvocationId !== invocation)
|
|
654
|
+
conflict('continuation invocation is not the accepted restart');
|
|
655
|
+
if (current?.status === 'resuming') {
|
|
656
|
+
if (current.continuationHolderId !== holder)
|
|
657
|
+
conflict('continuation already claimed');
|
|
658
|
+
return current;
|
|
659
|
+
}
|
|
660
|
+
if (current?.checkpoint === undefined)
|
|
661
|
+
conflict('continuation checkpoint is missing');
|
|
662
|
+
return this.transition(id, ['restarted'], 'resuming', { continuation_holder_id: holder });
|
|
663
|
+
}
|
|
664
|
+
complete(id) {
|
|
665
|
+
identifier(id, 'restart ID');
|
|
666
|
+
const current = this.getRequest(id);
|
|
667
|
+
if (current?.status === 'completed')
|
|
668
|
+
return current;
|
|
669
|
+
return this.transition(id, ['resuming'], 'completed');
|
|
670
|
+
}
|
|
671
|
+
fail(id, code) {
|
|
672
|
+
identifier(id, 'restart ID');
|
|
673
|
+
const failureCode = identifier(code, 'failure code');
|
|
674
|
+
const current = this.getRequest(id);
|
|
675
|
+
if (current?.status === 'failed' && current.failureCode === failureCode)
|
|
676
|
+
return current;
|
|
677
|
+
return this.transition(id, activeStatuses, 'failed', { failure_code: failureCode });
|
|
678
|
+
}
|
|
679
|
+
cancelForBinding(bindingId) {
|
|
680
|
+
identifier(bindingId, 'binding ID');
|
|
681
|
+
this.assertOpen();
|
|
682
|
+
const rows = this.database.prepare(`SELECT r.id FROM restart_requests AS r
|
|
683
|
+
JOIN restart_tasks AS t ON t.inbox_id = r.inbox_id
|
|
684
|
+
WHERE json_extract(t.task_json, '$.binding.id') = ?
|
|
685
|
+
AND r.status IN ('requested','checkpoint','ready','restarting','restarted','resuming')
|
|
686
|
+
ORDER BY r.created_at,r.id`).all(bindingId);
|
|
687
|
+
return rows.map(row => {
|
|
688
|
+
const current = this.getRequest(row.id);
|
|
689
|
+
if (current === undefined || this.getTask(current.inboxId)?.binding.id !== bindingId)
|
|
690
|
+
corrupt('cancel target changed');
|
|
691
|
+
return this.transition(current.id, activeStatuses, 'cancelled');
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
listRequests(input = {}) {
|
|
695
|
+
this.assertOpen();
|
|
696
|
+
const statusesInput = input.statuses ?? activeStatuses;
|
|
697
|
+
if (statusesInput.length === 0 || statusesInput.some(status => !statuses.has(status)))
|
|
698
|
+
invalid('restart statuses are invalid');
|
|
699
|
+
const limit = input.limit ?? 100;
|
|
700
|
+
positive(limit, 'restart page size', 1_000);
|
|
701
|
+
const placeholders = statusesInput.map(() => '?').join(',');
|
|
702
|
+
const cursor = input.after;
|
|
703
|
+
if (cursor !== undefined) {
|
|
704
|
+
nonnegative(cursor.createdAt, 'restart cursor time');
|
|
705
|
+
identifier(cursor.id, 'restart cursor ID');
|
|
706
|
+
}
|
|
707
|
+
const rows = this.database.prepare(`${requestSelect} WHERE status IN (${placeholders})
|
|
708
|
+
${cursor === undefined ? '' : 'AND (created_at > ? OR (created_at = ? AND id > ?))'} ORDER BY created_at,id LIMIT ?`)
|
|
709
|
+
.all(...statusesInput, ...(cursor === undefined ? [] : [cursor.createdAt, cursor.createdAt, cursor.id]), limit);
|
|
710
|
+
return rows.map(row => {
|
|
711
|
+
const request = this.requestById(row.id);
|
|
712
|
+
if (request === undefined)
|
|
713
|
+
corrupt('restart request vanished during scan');
|
|
714
|
+
return request;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
/** Count durable restart dispatch claims for an external global circuit breaker. */
|
|
718
|
+
countDispatchedSince(sinceMs) {
|
|
719
|
+
this.assertOpen();
|
|
720
|
+
nonnegative(sinceMs, 'dispatch window start');
|
|
721
|
+
const row = this.database.prepare(`SELECT COUNT(*) AS count FROM restart_events
|
|
722
|
+
WHERE event = 'restarting' AND at >= ?`).get(sinceMs);
|
|
723
|
+
return nonnegative(row.count, 'restart dispatch count');
|
|
724
|
+
}
|
|
725
|
+
hasReservation(inboxId, kind, operationId) {
|
|
726
|
+
this.assertOpen();
|
|
727
|
+
identifier(inboxId, 'inbox ID');
|
|
728
|
+
identifier(operationId, 'operation ID');
|
|
729
|
+
if (kind !== 'model' && kind !== 'tool')
|
|
730
|
+
invalid('reservation kind is invalid');
|
|
731
|
+
return this.database.prepare(`SELECT 1 FROM restart_budget_events
|
|
732
|
+
WHERE inbox_id = ? AND kind = ? AND operation_id = ? AND phase = 'reserved'`)
|
|
733
|
+
.get(inboxId, kind, operationId) !== undefined;
|
|
734
|
+
}
|
|
735
|
+
activeRequestForBinding(bindingId) {
|
|
736
|
+
identifier(bindingId, 'binding ID');
|
|
737
|
+
this.assertOpen();
|
|
738
|
+
const row = this.database.prepare(`SELECT r.id FROM restart_requests AS r
|
|
739
|
+
JOIN restart_tasks AS t ON t.inbox_id = r.inbox_id
|
|
740
|
+
WHERE json_extract(t.task_json, '$.binding.id') = ?
|
|
741
|
+
AND r.status IN ('requested','checkpoint','ready','restarting','restarted','resuming')
|
|
742
|
+
ORDER BY r.created_at,r.id LIMIT 1`).get(bindingId);
|
|
743
|
+
if (row === undefined)
|
|
744
|
+
return undefined;
|
|
745
|
+
const request = this.getRequest(row.id);
|
|
746
|
+
if (request === undefined || this.getTask(request.inboxId)?.binding.id !== bindingId)
|
|
747
|
+
corrupt('active request changed');
|
|
748
|
+
return request;
|
|
749
|
+
}
|
|
750
|
+
close() {
|
|
751
|
+
if (this.closed)
|
|
752
|
+
return;
|
|
753
|
+
this.closed = true;
|
|
754
|
+
this.database.close();
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
//# sourceMappingURL=restart-store.js.map
|