@logicseek/hello-cc 1.1.0-rc.8 → 1.1.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/CHANGELOG.md +107 -0
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/docs/README.md +2 -0
- package/docs/README.zh-CN.md +2 -0
- package/docs/commands.md +16 -0
- package/docs/commands.zh-CN.md +14 -0
- package/docs/dsh.md +6 -0
- package/docs/dsh.zh-CN.md +5 -0
- package/docs/native.md +21 -1
- package/docs/native.zh-CN.md +19 -1
- package/docs/private-state.md +115 -0
- package/docs/private-state.zh-CN.md +92 -0
- package/lib/cli/app.mjs +46 -5
- package/lib/cli/commands/hook.mjs +31 -4
- package/lib/cli/commands/mcp.mjs +6 -0
- package/lib/cli/commands/native.mjs +17 -4
- package/lib/cli/commands/peer.mjs +3 -0
- package/lib/cli/commands/run.mjs +196 -62
- package/lib/cli/commands/tmux.mjs +28 -19
- package/lib/cli/commands/up.mjs +25 -17
- package/lib/cli-runtime.mjs +16 -1
- package/lib/core/coordination/event-retention.mjs +2 -0
- package/lib/core/sessions/codex-thread-root.mjs +144 -0
- package/lib/core/sessions/launch.mjs +4 -0
- package/lib/discover.mjs +95 -231
- package/lib/integrations/dsh-collaboration.mjs +9 -2
- package/lib/integrations/native/claude.mjs +6 -0
- package/lib/integrations/native/codex.mjs +4 -2
- package/lib/integrations/native/dsh-acp.mjs +1 -1
- package/lib/integrations/native/jsonrpc.mjs +2 -2
- package/lib/mcp/scope.mjs +50 -8
- package/lib/mcp/tools.mjs +15 -2
- package/lib/process/pinned-cwd.mjs +64 -0
- package/lib/process/pinned-guidance.mjs +1 -0
- package/lib/process/pinned-project-state.mjs +1 -0
- package/lib/process/tmux-cwd-handoff.mjs +277 -0
- package/lib/runtime/native/cli-submissions.mjs +220 -0
- package/lib/runtime/native/client.mjs +7 -1
- package/lib/runtime/native/launcher.mjs +20 -4
- package/lib/runtime/native/service.mjs +143 -50
- package/lib/runtime/native/store.mjs +303 -14
- package/lib/runtime/private-state.mjs +435 -24
- package/lib/runtime/project-path.mjs +53 -1
- package/lib/runtime/project-trust.mjs +6 -2
- package/lib/runtime/state-migration.mjs +66 -13
- package/lib/shared/redact.mjs +4 -1
- package/lib/terminal/tmux.mjs +13 -2
- package/lib/ui/help.mjs +1 -0
- package/lib/web/codex-app-server.mjs +3 -1
- package/lib/web/codex-sessions.mjs +68 -8
- package/lib/web/context-files.mjs +194 -41
- package/lib/web/http-routes.mjs +43 -12
- package/lib/web/native-sessions.mjs +3 -0
- package/lib/web/project-contexts.mjs +27 -5
- package/lib/web/project-files.mjs +108 -26
- package/lib/web/pty-sessions.mjs +15 -2
- package/lib/web/session-serialize.mjs +6 -1
- package/lib/web/startup.mjs +3 -1
- package/lib/web/tmux-sessions.mjs +153 -50
- package/lib/web/ui-interactions.mjs +7 -4
- package/native/bin/darwin-arm64/hcc-cwd-handoff +0 -0
- package/native/bin/darwin-x64/hcc-cwd-handoff +0 -0
- package/native/bin/linux-arm64/hcc-cwd-handoff +0 -0
- package/native/bin/linux-x64/hcc-cwd-handoff +0 -0
- package/native/cwd-handoff/README.md +45 -0
- package/native/cwd-handoff/hcc-cwd-handoff.c +693 -0
- package/package.json +8 -2
- package/scripts/build-cwd-handoff.mjs +47 -0
- package/scripts/pack-cwd-handoff.mjs +27 -0
- package/scripts/regression.mjs +113 -7
- package/scripts/verify-cwd-handoff-package.mjs +60 -0
- package/scripts/web-workbench-acceptance.mjs +79 -1
- package/scripts/web-workbench-installed-acceptance.mjs +11 -1
|
@@ -5,6 +5,9 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
5
5
|
import { randomBytes } from 'node:crypto';
|
|
6
6
|
import { CliError } from '../../shared/errors.mjs';
|
|
7
7
|
import { resolveProjectDatabase, ensurePrivateProjectStateSubdirectory } from '../project-path.mjs';
|
|
8
|
+
import { privateProjectStateManifest } from '../private-state.mjs';
|
|
9
|
+
import { assertSelectedCwdSnapshot, captureSelectedCwdSnapshot, sameSelectedCwdIdentity } from '../../process/selected-cwd-identity.mjs';
|
|
10
|
+
import { compareProcessIdentity, inspectProcessIdentity } from '../../process/identity.mjs';
|
|
8
11
|
import { unsafeDirectoryAcl } from '../project-trust.mjs';
|
|
9
12
|
import { readPrivateTextFile } from '../private-file.mjs';
|
|
10
13
|
|
|
@@ -45,11 +48,17 @@ function privateDatabase(file) {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export function nativePaths(ctx, { create = false } = {}) {
|
|
48
|
-
const
|
|
49
|
-
|
|
51
|
+
const requestedRoot = ctx.root;
|
|
52
|
+
const root = fs.realpathSync(requestedRoot);
|
|
53
|
+
let dir, selected;
|
|
50
54
|
try {
|
|
51
|
-
|
|
55
|
+
// Preserve the caller's root spelling so the resolver can map a legacy
|
|
56
|
+
// /var/... DB spelling through macOS's /var -> /private/var alias.
|
|
57
|
+
selected = resolveProjectDatabase({ root: requestedRoot, db: ctx.dbPath, createStateDir: create });
|
|
52
58
|
dir = ensurePrivateProjectStateSubdirectory(root, 'native', { create });
|
|
59
|
+
if (dir && path.dirname(dir) !== selected.stateDir) {
|
|
60
|
+
throw new CliError('PROJECT_PATH_CHANGED', 'Native state does not match the selected project database generation');
|
|
61
|
+
}
|
|
53
62
|
} catch (error) {
|
|
54
63
|
if (error?.code === 'PROJECT_PATH_FORBIDDEN') {
|
|
55
64
|
throw new CliError('NATIVE_STATE_UNSAFE', error.message);
|
|
@@ -71,7 +80,8 @@ export function nativePaths(ctx, { create = false } = {}) {
|
|
|
71
80
|
throw new CliError('NATIVE_STATE_UNSAFE', `Native state file is not a regular file: ${file}`);
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
|
-
return { dir, pointer: path.join(dir, 'runtime.json'), db: path.join(dir, 'state.db'), log: path.join(dir, 'runtime.log')
|
|
83
|
+
return { dir, pointer: path.join(dir, 'runtime.json'), db: path.join(dir, 'state.db'), log: path.join(dir, 'runtime.log'),
|
|
84
|
+
meshDb: selected.db };
|
|
75
85
|
}
|
|
76
86
|
|
|
77
87
|
export function writeNativePointer(ctx, value) {
|
|
@@ -104,6 +114,10 @@ export function readNativePointer(ctx) {
|
|
|
104
114
|
typeof value.generation !== 'string' || !value.generation || value.generation.length > 128) {
|
|
105
115
|
throw new CliError('NATIVE_STATE_INVALID', 'Native runtime pointer does not match this project');
|
|
106
116
|
}
|
|
117
|
+
const selectedRootIdentity = ctx.initialRootIdentity || ctx.rootIdentity;
|
|
118
|
+
if (selectedRootIdentity && !sameSelectedCwdIdentity(selectedRootIdentity, value.rootIdentity)) {
|
|
119
|
+
throw new CliError('PROJECT_PATH_CHANGED', 'Native runtime pointer does not match the selected project directory');
|
|
120
|
+
}
|
|
107
121
|
return value;
|
|
108
122
|
}
|
|
109
123
|
|
|
@@ -120,12 +134,280 @@ export function parseNativeCwdIdentity(value, cwd) {
|
|
|
120
134
|
return record;
|
|
121
135
|
}
|
|
122
136
|
|
|
123
|
-
|
|
137
|
+
function completeProcessIdentity(value) {
|
|
138
|
+
return Number.isInteger(value?.pid) && value.pid > 0 &&
|
|
139
|
+
typeof value.startToken === 'string' && value.startToken.length > 0 &&
|
|
140
|
+
typeof value.commandHash === 'string' && /^[a-f0-9]{64}$/.test(value.commandHash);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function priorProcessExited(identity, inspect) {
|
|
144
|
+
const observed = inspect(identity.pid);
|
|
145
|
+
if (observed?.state === 'dead') return true;
|
|
146
|
+
return observed?.state === 'live' && completeProcessIdentity(observed.identity) &&
|
|
147
|
+
compareProcessIdentity(identity, observed.identity) === 'dead';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// A legacy daemon does not know the owner row. A live or uninspectable pointer
|
|
151
|
+
// cannot be silently upgraded, even if its loopback control endpoint is down.
|
|
152
|
+
function assertPriorPointerStopped(ctx, rootIdentity, inspect) {
|
|
153
|
+
const pointer = readNativePointer({ root: ctx.root, dbPath: ctx.dbPath });
|
|
154
|
+
if (!pointer) return;
|
|
155
|
+
if (pointer.rootIdentity && !sameSelectedCwdIdentity(rootIdentity, pointer.rootIdentity)) {
|
|
156
|
+
throw new CliError('PROJECT_PATH_CHANGED', 'Native runtime pointer belongs to another project directory');
|
|
157
|
+
}
|
|
158
|
+
if (!Number.isInteger(pointer.pid) || pointer.pid <= 0) {
|
|
159
|
+
throw new CliError('NATIVE_OWNER_UNVERIFIED', 'Saved native runtime has no verifiable process identity; drain it with its installed build');
|
|
160
|
+
}
|
|
161
|
+
const savedIdentity = pointer.ownerIdentity || pointer.processIdentity;
|
|
162
|
+
const identity = completeProcessIdentity(savedIdentity) && savedIdentity.pid === pointer.pid
|
|
163
|
+
? savedIdentity : null;
|
|
164
|
+
if (!identity) {
|
|
165
|
+
if (inspect(pointer.pid)?.state !== 'dead') {
|
|
166
|
+
throw new CliError('NATIVE_OWNER_UNVERIFIED', 'A legacy native runtime may still be alive; drain it with its installed build');
|
|
167
|
+
}
|
|
168
|
+
} else if (!priorProcessExited(identity, inspect)) {
|
|
169
|
+
throw new CliError('NATIVE_RUNTIME_IN_USE', 'Saved native runtime is live or its process exit is unconfirmed');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The same SQLite connection owns the fencing row and all native-store writes.
|
|
174
|
+
// Every synchronous write also holds BEGIN IMMEDIATE here, so a claimant cannot
|
|
175
|
+
// replace the generation until the old write finishes or its process dies.
|
|
176
|
+
export function claimNativeOwner(ctx, { rootIdentity, identity, generation,
|
|
177
|
+
inspect = inspectProcessIdentity, onLoss = () => {} }) {
|
|
178
|
+
if (!completeProcessIdentity(identity) || identity.pid !== process.pid ||
|
|
179
|
+
typeof generation !== 'string' || !generation || !rootIdentity) {
|
|
180
|
+
throw new CliError('NATIVE_OWNER_UNVERIFIED', 'Native runtime needs a complete process identity before it can write');
|
|
181
|
+
}
|
|
182
|
+
assertSelectedCwdSnapshot(rootIdentity);
|
|
183
|
+
assertPriorPointerStopped(ctx, rootIdentity, inspect);
|
|
124
184
|
const paths = nativePaths(ctx, { create: true });
|
|
185
|
+
assertSelectedCwdSnapshot(rootIdentity);
|
|
186
|
+
const meshDb = paths.meshDb;
|
|
187
|
+
const stateGeneration = privateProjectStateManifest(ctx.root).generation || null;
|
|
188
|
+
privateDatabase(paths.db);
|
|
189
|
+
const db = new DatabaseSync(paths.db, { timeout: 5000 });
|
|
190
|
+
let claimed = false;
|
|
191
|
+
try {
|
|
192
|
+
db.exec(`PRAGMA busy_timeout = 5000;
|
|
193
|
+
PRAGMA journal_mode = WAL;
|
|
194
|
+
CREATE TABLE IF NOT EXISTS native_owner (
|
|
195
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
196
|
+
epoch INTEGER NOT NULL, generation TEXT NOT NULL,
|
|
197
|
+
root_identity TEXT NOT NULL, mesh_db TEXT NOT NULL, state_generation TEXT,
|
|
198
|
+
pid INTEGER NOT NULL,
|
|
199
|
+
start_token TEXT NOT NULL, command_hash TEXT NOT NULL,
|
|
200
|
+
state TEXT NOT NULL CHECK (state IN ('active', 'released'))
|
|
201
|
+
);`);
|
|
202
|
+
db.exec('BEGIN IMMEDIATE');
|
|
203
|
+
try {
|
|
204
|
+
const previous = db.prepare('SELECT * FROM native_owner WHERE singleton=1').get();
|
|
205
|
+
if (previous) {
|
|
206
|
+
let savedRoot;
|
|
207
|
+
try { savedRoot = JSON.parse(previous.root_identity); } catch { /* Fail closed below. */ }
|
|
208
|
+
if (!sameSelectedCwdIdentity(rootIdentity, savedRoot)) {
|
|
209
|
+
throw new CliError('PROJECT_PATH_CHANGED', 'Native owner record belongs to another project directory');
|
|
210
|
+
}
|
|
211
|
+
if (previous.mesh_db !== meshDb || previous.state_generation !== stateGeneration) {
|
|
212
|
+
throw new CliError('PROJECT_PATH_CHANGED', 'Native owner record belongs to another state generation');
|
|
213
|
+
}
|
|
214
|
+
if (previous.state !== 'released' && !priorProcessExited({ pid: previous.pid,
|
|
215
|
+
startToken: previous.start_token, commandHash: previous.command_hash }, inspect)) {
|
|
216
|
+
throw new CliError('NATIVE_RUNTIME_IN_USE', 'Native runtime owner is live or its process exit is unconfirmed');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
db.prepare(`INSERT INTO native_owner(singleton,epoch,generation,root_identity,mesh_db,state_generation,pid,start_token,command_hash,state)
|
|
220
|
+
VALUES (1,?,?,?,?,?,?,?,?,'active') ON CONFLICT(singleton) DO UPDATE SET
|
|
221
|
+
epoch=excluded.epoch,generation=excluded.generation,root_identity=excluded.root_identity,
|
|
222
|
+
mesh_db=excluded.mesh_db,state_generation=excluded.state_generation,
|
|
223
|
+
pid=excluded.pid,start_token=excluded.start_token,command_hash=excluded.command_hash,state='active'`)
|
|
224
|
+
.run((previous?.epoch || 0) + 1, generation, JSON.stringify(rootIdentity), meshDb, stateGeneration,
|
|
225
|
+
identity.pid, identity.startToken, identity.commandHash);
|
|
226
|
+
assertSelectedCwdSnapshot(rootIdentity);
|
|
227
|
+
db.exec('COMMIT');
|
|
228
|
+
claimed = true;
|
|
229
|
+
} catch (error) { db.exec('ROLLBACK'); throw error; }
|
|
230
|
+
} catch (error) { db.close(); throw error; }
|
|
231
|
+
if (!claimed) throw new CliError('NATIVE_OWNER_UNVERIFIED', 'Native owner was not claimed');
|
|
232
|
+
let writeDepth = 0;
|
|
233
|
+
let lost = false;
|
|
234
|
+
let mesh = null;
|
|
235
|
+
function failStop() {
|
|
236
|
+
if (lost) return;
|
|
237
|
+
lost = true;
|
|
238
|
+
try { mesh?.exec('PRAGMA query_only = ON'); } catch {}
|
|
239
|
+
onLoss();
|
|
240
|
+
}
|
|
241
|
+
function assertRow() {
|
|
242
|
+
const row = db.prepare('SELECT generation,mesh_db,state_generation,pid,start_token,command_hash,state FROM native_owner WHERE singleton=1').get();
|
|
243
|
+
if (!row || row.generation !== generation || row.pid !== identity.pid ||
|
|
244
|
+
row.start_token !== identity.startToken || row.command_hash !== identity.commandHash ||
|
|
245
|
+
row.mesh_db !== meshDb || row.state_generation !== stateGeneration || row.state !== 'active') {
|
|
246
|
+
failStop();
|
|
247
|
+
throw new CliError('NATIVE_OWNER_LOST', 'Native runtime ownership changed; all further writes are stopped');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const owner = {
|
|
251
|
+
db, generation, stateGeneration,
|
|
252
|
+
setMesh(value) { mesh = value; if (mesh) mesh.exec('PRAGMA query_only = ON'); },
|
|
253
|
+
assertCurrent() {
|
|
254
|
+
if (lost) throw new CliError('NATIVE_OWNER_LOST', 'Native runtime ownership was lost');
|
|
255
|
+
assertRow();
|
|
256
|
+
},
|
|
257
|
+
withWrite(fn) {
|
|
258
|
+
if (lost) throw new CliError('NATIVE_OWNER_LOST', 'Native runtime ownership was lost');
|
|
259
|
+
if (writeDepth) return fn();
|
|
260
|
+
db.exec('BEGIN IMMEDIATE');
|
|
261
|
+
let enabled = false;
|
|
262
|
+
try {
|
|
263
|
+
assertRow();
|
|
264
|
+
writeDepth = 1;
|
|
265
|
+
if (mesh) { mesh.exec('PRAGMA query_only = OFF'); enabled = true; }
|
|
266
|
+
const result = fn();
|
|
267
|
+
if (result && typeof result.then === 'function') throw new TypeError('Native owner writes must be synchronous');
|
|
268
|
+
if (enabled) { mesh.exec('PRAGMA query_only = ON'); enabled = false; }
|
|
269
|
+
db.exec('COMMIT');
|
|
270
|
+
return result;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
try { if (enabled) mesh.exec('PRAGMA query_only = ON'); } catch { failStop(); }
|
|
273
|
+
try { db.exec('ROLLBACK'); } catch { failStop(); }
|
|
274
|
+
throw error;
|
|
275
|
+
} finally { writeDepth = 0; }
|
|
276
|
+
},
|
|
277
|
+
release(fn) {
|
|
278
|
+
this.withWrite(() => {
|
|
279
|
+
fn?.();
|
|
280
|
+
db.prepare("UPDATE native_owner SET state='released' WHERE singleton=1 AND generation=?").run(generation);
|
|
281
|
+
});
|
|
282
|
+
lost = true;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
Object.defineProperty(owner, 'lost', { enumerable: true, get: () => lost });
|
|
286
|
+
return owner;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function readNativeOwnerStatus(ctx, inspect = inspectProcessIdentity) {
|
|
290
|
+
const paths = nativePaths(ctx);
|
|
291
|
+
if (!paths || !statOrMissing(paths.db)) return { state: 'absent' };
|
|
292
|
+
privateDatabase(paths.db);
|
|
293
|
+
const db = new DatabaseSync(paths.db, { readOnly: true, timeout: 1000 });
|
|
294
|
+
try {
|
|
295
|
+
if (!db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='native_owner'").get()) {
|
|
296
|
+
return { state: 'absent' };
|
|
297
|
+
}
|
|
298
|
+
const row = db.prepare('SELECT generation,mesh_db,state_generation,pid,start_token,command_hash,state FROM native_owner WHERE singleton=1').get();
|
|
299
|
+
if (!row) return { state: 'absent' };
|
|
300
|
+
const identity = { pid: row.pid, startToken: row.start_token, commandHash: row.command_hash };
|
|
301
|
+
const details = { generation: row.generation, meshDb: row.mesh_db, stateGeneration: row.state_generation, identity };
|
|
302
|
+
if (row.state === 'released') return { state: 'released', ...details };
|
|
303
|
+
if (row.state !== 'active' || !completeProcessIdentity(identity)) {
|
|
304
|
+
return { state: 'unknown', ...details };
|
|
305
|
+
}
|
|
306
|
+
const observed = inspect(identity.pid);
|
|
307
|
+
if (observed?.state === 'dead' || (observed?.state === 'live' &&
|
|
308
|
+
completeProcessIdentity(observed.identity) && compareProcessIdentity(identity, observed.identity) === 'dead')) {
|
|
309
|
+
return { state: 'dead', ...details };
|
|
310
|
+
}
|
|
311
|
+
return { state: observed?.state === 'live' && completeProcessIdentity(observed.identity) &&
|
|
312
|
+
compareProcessIdentity(identity, observed.identity) === 'live'
|
|
313
|
+
? 'live' : 'unknown', ...details };
|
|
314
|
+
} finally { db.close(); }
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function assertNativePointerOwner(ctx, pointer) {
|
|
318
|
+
if (!pointer || pointer.ownerVersion !== 2 || !completeProcessIdentity(pointer.ownerIdentity) ||
|
|
319
|
+
pointer.ownerIdentity.pid !== pointer.pid) {
|
|
320
|
+
throw new CliError('NATIVE_OWNER_UNVERIFIED', 'Native runtime pointer predates owner fencing; drain it with its installed build');
|
|
321
|
+
}
|
|
322
|
+
const owner = readNativeOwnerStatus(ctx);
|
|
323
|
+
const expectedGeneration = privateProjectStateManifest(ctx.root).generation || null;
|
|
324
|
+
if (owner.state === 'dead' && owner.generation === pointer.generation) {
|
|
325
|
+
throw new CliError('NATIVE_RUNTIME_OFFLINE', 'Native runtime owner has exited');
|
|
326
|
+
}
|
|
327
|
+
if (owner.state !== 'live' || owner.generation !== pointer.generation ||
|
|
328
|
+
owner.meshDb !== nativePaths(ctx).meshDb || owner.stateGeneration !== expectedGeneration ||
|
|
329
|
+
pointer.stateGeneration !== expectedGeneration ||
|
|
330
|
+
owner.identity.pid !== pointer.ownerIdentity.pid ||
|
|
331
|
+
owner.identity.startToken !== pointer.ownerIdentity.startToken ||
|
|
332
|
+
owner.identity.commandHash !== pointer.ownerIdentity.commandHash) {
|
|
333
|
+
throw new CliError('NATIVE_OWNER_UNVERIFIED', 'Native runtime pointer does not match a live persistent owner');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function nativeOwnerGeneration(value) {
|
|
338
|
+
if (typeof value !== 'string') return null;
|
|
339
|
+
const parts = value.split(':');
|
|
340
|
+
return parts.length === 4 && parts[0] === 'native' && parts[1] && parts[1].length <= 128 &&
|
|
341
|
+
/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(parts[2]) && /^[a-f0-9-]{36}$/.test(parts[3])
|
|
342
|
+
? parts[1] : null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Hooks and scoped MCP run in different processes. They acquire the same
|
|
346
|
+
// state.db writer lock before opening or writing mesh.db; the native daemon's
|
|
347
|
+
// writes take this lock in the same order. A new generation cannot claim the
|
|
348
|
+
// row until an old derived write has finished.
|
|
349
|
+
export function acquireNativeOwnerWrite(ctx, expectedGeneration) {
|
|
350
|
+
if (typeof expectedGeneration !== 'string' || !expectedGeneration) {
|
|
351
|
+
throw new CliError('NATIVE_OWNER_UNVERIFIED', 'Native worker has no owner generation');
|
|
352
|
+
}
|
|
353
|
+
const rootIdentity = ctx.initialRootIdentity || ctx.rootIdentity || captureSelectedCwdSnapshot(ctx.root);
|
|
354
|
+
assertSelectedCwdSnapshot(rootIdentity);
|
|
355
|
+
const paths = nativePaths(ctx);
|
|
356
|
+
if (!paths || !statOrMissing(paths.db)) {
|
|
357
|
+
throw new CliError('NATIVE_OWNER_UNVERIFIED', 'Native worker has no persistent owner record');
|
|
358
|
+
}
|
|
125
359
|
privateDatabase(paths.db);
|
|
126
360
|
const db = new DatabaseSync(paths.db, { timeout: 5000 });
|
|
127
361
|
try {
|
|
128
|
-
db.exec(
|
|
362
|
+
db.exec('BEGIN IMMEDIATE');
|
|
363
|
+
const row = db.prepare('SELECT * FROM native_owner WHERE singleton=1').get();
|
|
364
|
+
let savedRoot;
|
|
365
|
+
try { savedRoot = JSON.parse(row?.root_identity || 'null'); } catch {}
|
|
366
|
+
const identity = { pid: row?.pid, startToken: row?.start_token, commandHash: row?.command_hash };
|
|
367
|
+
const observed = completeProcessIdentity(identity) ? inspectProcessIdentity(identity.pid) : null;
|
|
368
|
+
if (!row || row.state !== 'active' || row.generation !== expectedGeneration ||
|
|
369
|
+
row.mesh_db !== paths.meshDb ||
|
|
370
|
+
row.state_generation !== (privateProjectStateManifest(ctx.root).generation || null) ||
|
|
371
|
+
!sameSelectedCwdIdentity(rootIdentity, savedRoot) ||
|
|
372
|
+
observed?.state !== 'live' || !completeProcessIdentity(observed.identity) ||
|
|
373
|
+
compareProcessIdentity(identity, observed.identity) !== 'live') {
|
|
374
|
+
throw new CliError('NATIVE_OWNER_UNVERIFIED', 'Native worker owner is no longer live or does not own this state generation');
|
|
375
|
+
}
|
|
376
|
+
assertSelectedCwdSnapshot(rootIdentity);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
try { db.exec('ROLLBACK'); } catch {}
|
|
379
|
+
db.close();
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
let finished = false;
|
|
383
|
+
return function (success = false) {
|
|
384
|
+
if (finished) return;
|
|
385
|
+
finished = true;
|
|
386
|
+
try { db.exec(success ? 'COMMIT' : 'ROLLBACK'); }
|
|
387
|
+
finally { db.close(); }
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function withNativeOwnerWrite(ctx, expectedGeneration, fn) {
|
|
392
|
+
const finish = acquireNativeOwnerWrite(ctx, expectedGeneration);
|
|
393
|
+
let success = false;
|
|
394
|
+
try {
|
|
395
|
+
const result = fn();
|
|
396
|
+
if (result && typeof result.then === 'function') throw new TypeError('Native owner writes must be synchronous');
|
|
397
|
+
success = true;
|
|
398
|
+
return result;
|
|
399
|
+
} finally { finish(success); }
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function createNativeStore(ctx, { owner = null } = {}) {
|
|
403
|
+
const paths = nativePaths(ctx, { create: true });
|
|
404
|
+
if (!owner) privateDatabase(paths.db);
|
|
405
|
+
const db = owner?.db || new DatabaseSync(paths.db, { timeout: 5000 });
|
|
406
|
+
const write = fn => owner ? owner.withWrite(fn) : fn();
|
|
407
|
+
try {
|
|
408
|
+
if (!owner) db.exec('PRAGMA journal_mode = WAL');
|
|
409
|
+
write(() => {
|
|
410
|
+
db.exec(`
|
|
129
411
|
PRAGMA busy_timeout = 5000;
|
|
130
412
|
PRAGMA foreign_keys = ON;
|
|
131
413
|
CREATE TABLE IF NOT EXISTS workers (
|
|
@@ -150,7 +432,7 @@ export function createNativeStore(ctx) {
|
|
|
150
432
|
if (!db.prepare('PRAGMA table_info(workers)').all().some(column => column.name === 'cwd_identity')) {
|
|
151
433
|
db.exec('ALTER TABLE workers ADD COLUMN cwd_identity TEXT');
|
|
152
434
|
}
|
|
153
|
-
db.exec('BEGIN IMMEDIATE');
|
|
435
|
+
if (!owner) db.exec('BEGIN IMMEDIATE');
|
|
154
436
|
try {
|
|
155
437
|
if (!db.prepare('PRAGMA table_info(workers)').all().some(column => column.name === 'sandbox')) {
|
|
156
438
|
// Only the legacy schema implies the former workspace-write default.
|
|
@@ -158,13 +440,14 @@ export function createNativeStore(ctx) {
|
|
|
158
440
|
db.exec(`ALTER TABLE workers ADD COLUMN sandbox TEXT;
|
|
159
441
|
UPDATE workers SET sandbox='workspace-write' WHERE provider='codex';`);
|
|
160
442
|
}
|
|
161
|
-
db.exec('COMMIT');
|
|
443
|
+
if (!owner) db.exec('COMMIT');
|
|
162
444
|
} catch (error) {
|
|
163
|
-
try { db.exec('ROLLBACK'); } catch { /* Preserve the migration failure. */ }
|
|
445
|
+
try { if (!owner) db.exec('ROLLBACK'); } catch { /* Preserve the migration failure. */ }
|
|
164
446
|
throw error;
|
|
165
447
|
}
|
|
448
|
+
});
|
|
166
449
|
} catch (error) {
|
|
167
|
-
try { db.close(); } catch { /* Preserve the initialization failure. */ }
|
|
450
|
+
if (!owner) try { db.close(); } catch { /* Preserve the initialization failure. */ }
|
|
168
451
|
throw error;
|
|
169
452
|
}
|
|
170
453
|
const now = () => Date.now();
|
|
@@ -173,6 +456,7 @@ export function createNativeStore(ctx) {
|
|
|
173
456
|
worker(peer) { return db.prepare('SELECT * FROM workers WHERE peer = ?').get(peer) || null; },
|
|
174
457
|
workers() { return db.prepare('SELECT * FROM workers ORDER BY peer').all(); },
|
|
175
458
|
saveWorker(worker) {
|
|
459
|
+
return write(() => {
|
|
176
460
|
const previous = this.worker(worker.peer);
|
|
177
461
|
const sandbox = worker.provider === 'codex'
|
|
178
462
|
? worker.sandbox === undefined ? previous ? previous.sandbox : 'workspace-write' : worker.sandbox
|
|
@@ -192,35 +476,40 @@ export function createNativeStore(ctx) {
|
|
|
192
476
|
cwd=excluded.cwd, cwd_identity=COALESCE(excluded.cwd_identity,workers.cwd_identity),
|
|
193
477
|
status=excluded.status, updated_at=excluded.updated_at, sandbox=excluded.sandbox`)
|
|
194
478
|
.run(worker.peer, worker.provider, worker.sessionId || null, worker.cwd, cwdIdentity, worker.status, now(), sandbox);
|
|
479
|
+
});
|
|
195
480
|
},
|
|
196
481
|
queue(peer, messageId, submissionId, origin = 'peer') {
|
|
197
482
|
if (!['peer', 'user'].includes(origin)) throw new CliError('BAD_ARGS', 'Unknown native message origin');
|
|
198
|
-
db.prepare(`INSERT OR IGNORE INTO deliveries(peer,message_id,submission_id,state,origin,updated_at) VALUES (?,?,?,'queued',?,?)`)
|
|
199
|
-
.run(peer, messageId, submissionId, origin, now());
|
|
483
|
+
write(() => db.prepare(`INSERT OR IGNORE INTO deliveries(peer,message_id,submission_id,state,origin,updated_at) VALUES (?,?,?,'queued',?,?)`)
|
|
484
|
+
.run(peer, messageId, submissionId, origin, now()));
|
|
200
485
|
return this.delivery(peer, messageId);
|
|
201
486
|
},
|
|
202
487
|
delivery(peer, messageId) { return db.prepare('SELECT * FROM deliveries WHERE peer=? AND message_id=?').get(peer, messageId) || null; },
|
|
203
488
|
pending(peer) { return db.prepare("SELECT * FROM deliveries WHERE peer=? AND state='queued' ORDER BY id LIMIT 1").get(peer) || null; },
|
|
204
489
|
updateDelivery(id, state, turnId = null, detail = null) {
|
|
205
|
-
db.prepare('UPDATE deliveries SET state=?,turn_id=COALESCE(?,turn_id),detail=?,updated_at=? WHERE id=?')
|
|
206
|
-
.run(state, turnId, detail ? JSON.stringify(detail) : null, now(), id);
|
|
490
|
+
write(() => db.prepare('UPDATE deliveries SET state=?,turn_id=COALESCE(?,turn_id),detail=?,updated_at=? WHERE id=?')
|
|
491
|
+
.run(state, turnId, detail ? JSON.stringify(detail) : null, now(), id));
|
|
207
492
|
},
|
|
208
493
|
deliveries(peer = null) {
|
|
209
494
|
return peer ? db.prepare('SELECT * FROM deliveries WHERE peer=? ORDER BY id DESC LIMIT 100').all(peer)
|
|
210
495
|
: db.prepare('SELECT * FROM deliveries ORDER BY id DESC LIMIT 100').all();
|
|
211
496
|
},
|
|
212
497
|
event(peer, payload) {
|
|
498
|
+
write(() => {
|
|
213
499
|
db.prepare('INSERT INTO provider_events(peer,payload,created_at) VALUES (?,?,?)').run(peer, JSON.stringify(payload), now());
|
|
214
500
|
// Event inspection is bounded; authoritative messages remain in mesh.db.
|
|
215
501
|
db.prepare('DELETE FROM provider_events WHERE id < (SELECT MAX(id)-2000 FROM provider_events)').run();
|
|
502
|
+
});
|
|
216
503
|
},
|
|
217
504
|
events(peer, after = 0) {
|
|
218
505
|
return db.prepare('SELECT * FROM provider_events WHERE peer=? AND id>? ORDER BY id LIMIT 100').all(peer, after)
|
|
219
506
|
.map((row) => ({ ...row, payload: JSON.parse(row.payload) }));
|
|
220
507
|
},
|
|
221
508
|
disconnected() {
|
|
509
|
+
write(() => {
|
|
222
510
|
db.prepare("UPDATE workers SET status='disconnected',updated_at=? WHERE status!='closed'").run(now());
|
|
223
511
|
db.prepare("UPDATE deliveries SET state='uncertain',updated_at=? WHERE state IN ('dispatching','submitted','accepted')").run(now());
|
|
512
|
+
});
|
|
224
513
|
},
|
|
225
514
|
close() { db.close(); }
|
|
226
515
|
};
|