@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.
Files changed (74) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +2 -0
  3. package/README.zh-CN.md +2 -0
  4. package/docs/README.md +2 -0
  5. package/docs/README.zh-CN.md +2 -0
  6. package/docs/commands.md +16 -0
  7. package/docs/commands.zh-CN.md +14 -0
  8. package/docs/dsh.md +6 -0
  9. package/docs/dsh.zh-CN.md +5 -0
  10. package/docs/native.md +21 -1
  11. package/docs/native.zh-CN.md +19 -1
  12. package/docs/private-state.md +115 -0
  13. package/docs/private-state.zh-CN.md +92 -0
  14. package/lib/cli/app.mjs +46 -5
  15. package/lib/cli/commands/hook.mjs +31 -4
  16. package/lib/cli/commands/mcp.mjs +6 -0
  17. package/lib/cli/commands/native.mjs +17 -4
  18. package/lib/cli/commands/peer.mjs +3 -0
  19. package/lib/cli/commands/run.mjs +196 -62
  20. package/lib/cli/commands/tmux.mjs +28 -19
  21. package/lib/cli/commands/up.mjs +25 -17
  22. package/lib/cli-runtime.mjs +16 -1
  23. package/lib/core/coordination/event-retention.mjs +2 -0
  24. package/lib/core/sessions/codex-thread-root.mjs +144 -0
  25. package/lib/core/sessions/launch.mjs +4 -0
  26. package/lib/discover.mjs +95 -231
  27. package/lib/integrations/dsh-collaboration.mjs +9 -2
  28. package/lib/integrations/native/claude.mjs +6 -0
  29. package/lib/integrations/native/codex.mjs +4 -2
  30. package/lib/integrations/native/dsh-acp.mjs +1 -1
  31. package/lib/integrations/native/jsonrpc.mjs +2 -2
  32. package/lib/mcp/scope.mjs +50 -8
  33. package/lib/mcp/tools.mjs +15 -2
  34. package/lib/process/pinned-cwd.mjs +64 -0
  35. package/lib/process/pinned-guidance.mjs +1 -0
  36. package/lib/process/pinned-project-state.mjs +1 -0
  37. package/lib/process/tmux-cwd-handoff.mjs +277 -0
  38. package/lib/runtime/native/cli-submissions.mjs +220 -0
  39. package/lib/runtime/native/client.mjs +7 -1
  40. package/lib/runtime/native/launcher.mjs +20 -4
  41. package/lib/runtime/native/service.mjs +143 -50
  42. package/lib/runtime/native/store.mjs +303 -14
  43. package/lib/runtime/private-state.mjs +435 -24
  44. package/lib/runtime/project-path.mjs +53 -1
  45. package/lib/runtime/project-trust.mjs +6 -2
  46. package/lib/runtime/state-migration.mjs +66 -13
  47. package/lib/shared/redact.mjs +4 -1
  48. package/lib/terminal/tmux.mjs +13 -2
  49. package/lib/ui/help.mjs +1 -0
  50. package/lib/web/codex-app-server.mjs +3 -1
  51. package/lib/web/codex-sessions.mjs +68 -8
  52. package/lib/web/context-files.mjs +194 -41
  53. package/lib/web/http-routes.mjs +43 -12
  54. package/lib/web/native-sessions.mjs +3 -0
  55. package/lib/web/project-contexts.mjs +27 -5
  56. package/lib/web/project-files.mjs +108 -26
  57. package/lib/web/pty-sessions.mjs +15 -2
  58. package/lib/web/session-serialize.mjs +6 -1
  59. package/lib/web/startup.mjs +3 -1
  60. package/lib/web/tmux-sessions.mjs +153 -50
  61. package/lib/web/ui-interactions.mjs +7 -4
  62. package/native/bin/darwin-arm64/hcc-cwd-handoff +0 -0
  63. package/native/bin/darwin-x64/hcc-cwd-handoff +0 -0
  64. package/native/bin/linux-arm64/hcc-cwd-handoff +0 -0
  65. package/native/bin/linux-x64/hcc-cwd-handoff +0 -0
  66. package/native/cwd-handoff/README.md +45 -0
  67. package/native/cwd-handoff/hcc-cwd-handoff.c +693 -0
  68. package/package.json +8 -2
  69. package/scripts/build-cwd-handoff.mjs +47 -0
  70. package/scripts/pack-cwd-handoff.mjs +27 -0
  71. package/scripts/regression.mjs +113 -7
  72. package/scripts/verify-cwd-handoff-package.mjs +60 -0
  73. package/scripts/web-workbench-acceptance.mjs +79 -1
  74. 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 root = fs.realpathSync(ctx.root);
49
- let dir;
51
+ const requestedRoot = ctx.root;
52
+ const root = fs.realpathSync(requestedRoot);
53
+ let dir, selected;
50
54
  try {
51
- resolveProjectDatabase({ root, createStateDir: create });
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
- export function createNativeStore(ctx) {
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(`PRAGMA journal_mode = WAL;
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
  };