@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
@@ -1,9 +1,13 @@
1
1
  import http from 'node:http';
2
2
  import { CliError } from '../../shared/errors.mjs';
3
- import { readNativePointer } from './store.mjs';
3
+ import { assertNativePointerOwner, readNativePointer } from './store.mjs';
4
+ import { assertSelectedCwdSnapshot } from '../../process/selected-cwd-identity.mjs';
4
5
 
5
6
  export function nativeRequest(ctx, method, route, body = null, { timeoutMs = 35000, expectedGeneration } = {}) {
7
+ const selectedRootIdentity = ctx.initialRootIdentity || ctx.rootIdentity;
8
+ if (selectedRootIdentity) assertSelectedCwdSnapshot(selectedRootIdentity);
6
9
  const pointer = readNativePointer(ctx);
10
+ if (selectedRootIdentity) assertSelectedCwdSnapshot(selectedRootIdentity);
7
11
  if (!pointer) return Promise.reject(new CliError('NATIVE_RUNTIME_OFFLINE', 'Native runtime is offline. Run hcc native up.'));
8
12
  // Keep a policy-checked request bound to that runtime. This same pointer's
9
13
  // endpoint and credential are used below; never reread and follow a new owner.
@@ -12,6 +16,8 @@ export function nativeRequest(ctx, method, route, body = null, { timeoutMs = 350
12
16
  return Promise.reject(new CliError('NATIVE_OWNER_CHANGED',
13
17
  'Native runtime changed after policy verification. Inspect its current status before retrying.', { uncertain: false }));
14
18
  }
19
+ try { assertNativePointerOwner(ctx, pointer); }
20
+ catch (error) { return Promise.reject(error); }
15
21
  return new Promise((resolve, reject) => {
16
22
  const payload = body === null ? null : JSON.stringify(body);
17
23
  let timer;
@@ -7,7 +7,7 @@ import { fileURLToPath } from 'node:url';
7
7
  import { CliError } from '../../shared/errors.mjs';
8
8
  import { fileLockEndpoint } from '../../shared/file-lock.mjs';
9
9
  import { openPrivateAppendFile } from '../private-file.mjs';
10
- import { nativePaths, readNativePointer } from './store.mjs';
10
+ import { assertNativePointerOwner, nativePaths, readNativeOwnerStatus, readNativePointer } from './store.mjs';
11
11
  import { nativeRequest } from './client.mjs';
12
12
  import { spawnPinnedCwdProcess } from '../../process/pinned-cwd.mjs';
13
13
  import { assertSelectedCwdSnapshot, captureSelectedCwdSnapshot } from '../../process/selected-cwd-identity.mjs';
@@ -19,14 +19,19 @@ export function createNativeLauncher({ spawnProcess = spawn, request = nativeReq
19
19
  const starting = new Map();
20
20
 
21
21
  async function readReady(ctx) {
22
- if (!readNativePointer(ctx)) return null;
22
+ if (ctx.initialRootIdentity) assertSelectedCwdSnapshot(ctx.initialRootIdentity);
23
+ const pointer = readNativePointer(ctx);
24
+ if (!pointer) return null;
25
+ try { assertNativePointerOwner(ctx, pointer); }
26
+ catch (error) { if (error.code === 'NATIVE_RUNTIME_OFFLINE') return null; throw error; }
23
27
  try {
24
28
  const status = await request(ctx, 'GET', '/status', null, { timeoutMs: 2000 });
25
29
  if (!status || status.root !== ctx.root || status.meshDb !== ctx.dbPath ||
26
- typeof status.generation !== 'string' || !status.generation) {
30
+ status.generation !== pointer.generation) {
27
31
  throw new CliError('NATIVE_RESPONSE_INVALID', 'Native runtime does not match this project');
28
32
  }
29
33
  if (status.stopping) throw new CliError('NATIVE_RUNTIME_STOPPING', 'Native runtime is stopping; finish its shutdown before creating another worker');
34
+ if (ctx.initialRootIdentity) assertSelectedCwdSnapshot(ctx.initialRootIdentity);
30
35
  return status;
31
36
  } catch (error) {
32
37
  if (error.code === 'NATIVE_RUNTIME_OFFLINE') return null;
@@ -58,12 +63,23 @@ export function createNativeLauncher({ spawnProcess = spawn, request = nativeReq
58
63
  try {
59
64
  const existing = await readReady(ctx);
60
65
  if (existing) return existing;
66
+ // A daemon can hold its durable owner before publishing runtime.json.
67
+ // Keep the rc8 launcher lock and wait for that owner, rather than launch
68
+ // a second process when the pointer is temporarily absent.
69
+ while (Date.now() < deadline && ['live', 'unknown'].includes(readNativeOwnerStatus(ctx).state)) {
70
+ const status = await readReady(ctx);
71
+ if (status) return status;
72
+ await delay(pollMs);
73
+ }
74
+ if (Date.now() >= deadline) throw new CliError('NATIVE_RUNTIME_START_FAILED', 'A native owner is still starting or its exit is unconfirmed; inspect native status before retrying');
61
75
  let log, child;
62
76
  try {
63
77
  log = openPrivateAppendFile(paths.log);
64
78
  child = spawnPinnedCwdProcess(process.execPath, [fileURLToPath(new URL('../../../bin/hcc.mjs', import.meta.url)), '--root', ctx.root, '--db', ctx.dbPath, 'native', 'serve'], {
65
79
  cwd: ctx.root, expectedIdentity: ctx.initialRootIdentity,
66
- env, detached: true, stdio: ['ignore', log, log]
80
+ env: { ...env, HCC_PINNED_ROOT_IDENTITY: JSON.stringify({
81
+ canonical: ctx.initialRootIdentity.canonical, identity: ctx.initialRootIdentity.identity
82
+ }) }, detached: true, stdio: ['ignore', log, log]
67
83
  }, spawnProcess);
68
84
  } catch (error) {
69
85
  if (error?.code === 'PROJECT_PATH_FORBIDDEN') throw new CliError('NATIVE_STATE_UNSAFE', error.message);
@@ -7,13 +7,13 @@ import { redactSecrets } from '../../shared/redact.mjs';
7
7
  import { acquireFileLock } from '../../shared/file-lock.mjs';
8
8
  import { aggregateCleanupFailure } from '../../shared/cleanup-error.mjs';
9
9
  import { inspectProcessIdentity, compareProcessIdentity } from '../../process/identity.mjs';
10
- import { shellQuoteArg } from '../../format.mjs';
11
- import { commandPath } from '../../cli-runtime.mjs';
10
+ import { recordCodexThreadRoot } from '../../core/sessions/codex-thread-root.mjs';
11
+ import { assertPinnedSessionLaunchAllowed } from '../../process/pinned-cwd.mjs';
12
12
  import { tx } from '../../db/schema.mjs';
13
13
  import { createScopedMcpConfig } from '../../mcp/scope.mjs';
14
14
  import { createNativeAdapter, nativeWorkerEnv, NATIVE_PROVIDERS } from '../../integrations/native/index.mjs';
15
15
  import { assertSelectedCwdSnapshot, captureSelectedCwdSnapshot, sameSelectedCwdIdentity } from '../../process/selected-cwd-identity.mjs';
16
- import { createNativeStore, nativePaths, parseNativeCwdIdentity, readNativePointer, writeNativePointer } from './store.mjs';
16
+ import { claimNativeOwner, createNativeStore, nativePaths, parseNativeCwdIdentity, readNativePointer, writeNativePointer } from './store.mjs';
17
17
 
18
18
  function publicError(error) {
19
19
  return redactSecrets({ code: error.code || 'NATIVE_REQUEST_FAILED', message: error.message || 'Native operation failed', extra: error.extra || {} });
@@ -65,8 +65,23 @@ export async function startNativeService(ctx, deps, options = {}) {
65
65
  throw new CliError('NATIVE_RUNTIME_IN_USE', 'The previous native runtime is still alive or its exit cannot be confirmed.');
66
66
  }
67
67
  assertPreviousOwnerExited();
68
+ const workers = new Map();
69
+ const creating = new Set();
70
+ const forking = new Set();
71
+ const operations = new Set();
72
+ const generation = randomUUID();
73
+ const token = randomBytes(32).toString('hex');
74
+ let stopping = false;
75
+ let shutdownPromise;
76
+ let shutdownError = null;
77
+ let ownershipLost = false;
78
+ let pollTimer;
79
+ const adapterFactory = options.adapterFactory || createNativeAdapter;
80
+
81
+ let server;
68
82
  const identity = deps.liveProcessIdentity(process.pid);
69
83
  let ownership;
84
+ let owner;
70
85
  try {
71
86
  ownership = (options.acquireOwnership || acquireFileLock)(path.join(paths.dir, 'service-owner'), { nonblocking: true, rejectLegacyOwner: true });
72
87
  } catch (error) {
@@ -86,10 +101,13 @@ export async function startNativeService(ctx, deps, options = {}) {
86
101
  storeClosed = true;
87
102
  const failures = [];
88
103
  if (mesh && !meshClosed) {
89
- try { mesh.close(); meshClosed = true; } catch (error) { failures.push(error); }
104
+ try { mesh.close(); meshClosed = true; owner?.setMesh(null); } catch (error) { failures.push(error); }
90
105
  }
91
- if (store && !nativeStoreClosed) {
92
- try { store.close(); nativeStoreClosed = true; } catch (error) { failures.push(error); }
106
+ if (!failures.length && owner && !owner.lost) {
107
+ try { owner.release(); } catch (error) { failures.push(error); }
108
+ }
109
+ if (!failures.length && !nativeStoreClosed && (store || owner)) {
110
+ try { if (store) store.close(); else owner.db.close(); nativeStoreClosed = true; } catch (error) { failures.push(error); }
93
111
  }
94
112
  if (failures.length === 1) throw failures[0];
95
113
  if (failures.length > 1) throw new AggregateError(failures, 'Native persistence cleanup failed');
@@ -106,26 +124,32 @@ export async function startNativeService(ctx, deps, options = {}) {
106
124
  }
107
125
  try {
108
126
  assertPreviousOwnerExited();
109
- store = createNativeStore(ctx);
110
- mesh = deps.connect(ctx, { migrateRegistered: false });
111
- mesh.exec(`CREATE TABLE IF NOT EXISTS native_user_submissions (
112
- submission_id TEXT PRIMARY KEY, message_id INTEGER NOT NULL UNIQUE,
113
- peer TEXT NOT NULL, payload_hash TEXT NOT NULL
114
- )`);
127
+ assertSelectedCwdSnapshot(serviceRootIdentity);
128
+ owner = claimNativeOwner(ctx, { rootIdentity: serviceRootIdentity, identity, generation,
129
+ inspect: options.inspectProcessIdentity || inspectProcessIdentity,
130
+ onLoss() {
131
+ stopping = true;
132
+ clearInterval(pollTimer);
133
+ shutdownError = publicError(new CliError('NATIVE_OWNER_LOST', 'Native runtime ownership changed; all further writes are stopped'));
134
+ for (const worker of workers.values()) { worker.quarantined = true; worker.mcpCapability?.dispose(); }
135
+ queueMicrotask(() => { void shutdown().catch(() => {}); });
136
+ } });
137
+ store = createNativeStore(ctx, { owner });
138
+ assertSelectedCwdSnapshot(serviceRootIdentity);
139
+ mesh = owner.withWrite(() => deps.connect(ctx, { migrateRegistered: false }));
140
+ owner.setMesh(mesh);
141
+ owner.withWrite(() => mesh.exec(`
142
+ CREATE TABLE IF NOT EXISTS native_user_submissions (
143
+ submission_id TEXT PRIMARY KEY, message_id INTEGER NOT NULL UNIQUE,
144
+ peer TEXT NOT NULL, payload_hash TEXT NOT NULL
145
+ );
146
+ CREATE TABLE IF NOT EXISTS native_worker_reservations (
147
+ peer TEXT PRIMARY KEY, owner TEXT NOT NULL UNIQUE, provider TEXT NOT NULL,
148
+ cwd TEXT NOT NULL, pid INTEGER NOT NULL
149
+ );`));
115
150
  store.disconnected();
151
+ recoverWorkerReservations();
116
152
  } catch (error) { releaseAfterInitializationFailure(error); }
117
- const workers = new Map();
118
- const creating = new Set();
119
- const forking = new Set();
120
- const operations = new Set();
121
- const generation = randomUUID();
122
- const token = randomBytes(32).toString('hex');
123
- let stopping = false;
124
- let shutdownPromise;
125
- let shutdownError = null;
126
- let ownershipLost = false;
127
- let pollTimer;
128
- const adapterFactory = options.adapterFactory || createNativeAdapter;
129
153
 
130
154
  function track(promise) {
131
155
  operations.add(promise);
@@ -137,7 +161,7 @@ export async function startNativeService(ctx, deps, options = {}) {
137
161
  if (stopping) throw new CliError('NATIVE_RUNTIME_STOPPING', 'Native runtime is stopping');
138
162
  }
139
163
  function assertOwnership() {
140
- try { ownership.assertHeld(); }
164
+ try { ownership.assertHeld(); owner?.assertCurrent(); }
141
165
  catch {
142
166
  const error = new CliError('NATIVE_RUNTIME_OWNERSHIP_LOST', 'Native ownership is no longer confirmed; the runtime is stopping.');
143
167
  if (!ownershipLost) {
@@ -149,6 +173,10 @@ export async function startNativeService(ctx, deps, options = {}) {
149
173
  throw error;
150
174
  }
151
175
  }
176
+ function assertWorkerDirectory(worker) {
177
+ assertSelectedCwdSnapshot(serviceRootIdentity);
178
+ assertSelectedCwdSnapshot(worker.cwdIdentity);
179
+ }
152
180
  function assertOwner(worker) {
153
181
  assertOwnership();
154
182
  const binding = mesh.prepare('SELECT * FROM peer_bindings WHERE peer=?').get(worker.peer);
@@ -166,6 +194,33 @@ export async function startNativeService(ctx, deps, options = {}) {
166
194
  throw new CliError('NATIVE_OWNER_CHANGED', 'Native worker identity changed; refresh its current state before controlling it');
167
195
  }
168
196
  }
197
+ function recoverWorkerReservations() {
198
+ owner.withWrite(() => tx(mesh, () => {
199
+ for (const reservation of mesh.prepare('SELECT * FROM native_worker_reservations').all()) {
200
+ if (!store.worker(reservation.peer)) {
201
+ const binding = mesh.prepare('SELECT * FROM peer_bindings WHERE peer=?').get(reservation.peer);
202
+ const peer = mesh.prepare('SELECT * FROM peers WHERE id=?').get(reservation.peer);
203
+ if (binding?.runtime_target === reservation.owner) {
204
+ if (binding.transport !== 'native' || binding.provider !== reservation.provider ||
205
+ binding.runtime_session_id !== reservation.peer ||
206
+ binding.provider_session_id || binding.provider_session_name ||
207
+ !peer || peer.kind !== reservation.provider || peer.role !== 'native-worker' ||
208
+ peer.worktree !== reservation.cwd || peer.pid !== reservation.pid) {
209
+ throw new CliError('NATIVE_OWNER_UNVERIFIED', 'Reserved native peer changed before recovery');
210
+ }
211
+ mesh.prepare('DELETE FROM peer_bindings WHERE peer=? AND runtime_target=?')
212
+ .run(reservation.peer, reservation.owner);
213
+ mesh.prepare('DELETE FROM peers WHERE id=?').run(reservation.peer);
214
+ } else if (binding || peer) {
215
+ // Another writer changed the peer; do not erase its record.
216
+ throw new CliError('NATIVE_OWNER_UNVERIFIED', 'Reserved native peer has ambiguous ownership');
217
+ }
218
+ }
219
+ mesh.prepare('DELETE FROM native_worker_reservations WHERE peer=? AND owner=?')
220
+ .run(reservation.peer, reservation.owner);
221
+ }
222
+ }));
223
+ }
169
224
  function submissionHash(input) {
170
225
  return createHash('sha256').update(JSON.stringify([
171
226
  input.peer, input.from, input.taskId || null, input.body
@@ -192,6 +247,7 @@ export async function startNativeService(ctx, deps, options = {}) {
192
247
  function save(worker, options) {
193
248
  if (!worker.adapter || storeClosed) return;
194
249
  const snapshot = workerSnapshot(worker, options);
250
+ owner.withWrite(() => {
195
251
  tx(mesh, () => {
196
252
  assertOwner(worker);
197
253
  if (snapshot.sessionId && mesh.prepare('SELECT peer FROM peer_bindings WHERE provider=? AND (provider_session_id=? OR provider_session_name=?) AND peer<>?')
@@ -210,6 +266,7 @@ export async function startNativeService(ctx, deps, options = {}) {
210
266
  status, capabilities: JSON.stringify(snapshot.capabilities || worker.adapter.capabilities) });
211
267
  });
212
268
  store.saveWorker({ peer: worker.peer, provider: worker.provider, cwd: worker.cwd, cwdIdentity: worker.cwdIdentity, ...snapshot });
269
+ });
213
270
  }
214
271
  function belongs(event, active) {
215
272
  if (!active) return false;
@@ -220,6 +277,7 @@ export async function startNativeService(ctx, deps, options = {}) {
220
277
  function record(worker, event) {
221
278
  if (storeClosed || worker.closing) return;
222
279
  try {
280
+ owner.withWrite(() => {
223
281
  assertOwner(worker);
224
282
  const safe = redactSecrets(event);
225
283
  if (event.type !== 'delta' && event.type !== 'output') store.event(worker.peer, safe);
@@ -237,7 +295,8 @@ export async function startNativeService(ctx, deps, options = {}) {
237
295
  tx(mesh, () => {
238
296
  assertOwner(worker);
239
297
  if (completed) {
240
- const text = worker.output.join('\n\n') || event.text;
298
+ const text = typeof event.text === 'string' && event.text
299
+ ? event.text : worker.output.join('\n\n');
241
300
  // A reply is delivered for context, but must not create another
242
301
  // automatic reply and an unbounded loop between native workers.
243
302
  if (text && active.message.kind !== 'reply') deps.sendMessage(mesh, worker.peer, active.message.sender, active.message.task_id, 'reply', text,
@@ -252,16 +311,18 @@ export async function startNativeService(ctx, deps, options = {}) {
252
311
  worker.active = null;
253
312
  worker.output = [];
254
313
  }
255
- if (event.type === 'error' && active && (associated || (!event.turnId && !event.submissionId))) {
314
+ if (event.type === 'error' && event.willRetry !== true && active && (associated || (!event.turnId && !event.submissionId))) {
256
315
  store.updateDelivery(active.delivery.id, 'uncertain', active.turnId, safe);
257
316
  worker.active = null;
258
317
  worker.output = [];
259
318
  worker.quarantined = true;
260
319
  }
261
320
  save(worker);
321
+ });
262
322
  } catch (error) {
263
323
  worker.quarantined = true;
264
324
  console.error(JSON.stringify(publicError(error)));
325
+ if (owner.lost) return;
265
326
  void closeWorker(worker.peer).catch((closeError) => console.error(JSON.stringify(publicError(closeError))));
266
327
  }
267
328
  }
@@ -290,7 +351,9 @@ export async function startNativeService(ctx, deps, options = {}) {
290
351
  }
291
352
  async function createWorkerNow(input) {
292
353
  requireRunning();
354
+ assertPinnedSessionLaunchAllowed();
293
355
  assertSelectedCwdSnapshot(serviceRootIdentity);
356
+ recoverWorkerReservations();
294
357
  const peer = typeof input.peer === 'string' ? input.peer : '';
295
358
  const provider = input.provider;
296
359
  if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(peer) || peer === 'all' || !NATIVE_PROVIDERS.includes(provider)) {
@@ -352,7 +415,7 @@ export async function startNativeService(ctx, deps, options = {}) {
352
415
  active: null, output: [], quarantined: false, closing: false, dispatch: null, closePromise: null };
353
416
  // Reserve identity before starting the child so installed hooks can only
354
417
  // heartbeat this exact worker. Every subsequent write checks this owner.
355
- tx(mesh, () => {
418
+ owner.withWrite(() => tx(mesh, () => {
356
419
  const binding = mesh.prepare('SELECT * FROM peer_bindings WHERE peer=?').get(peer);
357
420
  const existing = mesh.prepare('SELECT * FROM peers WHERE id=?').get(peer);
358
421
  if (fencedResume && (!binding || binding.runtime_target !== input.resumeFence.owner ||
@@ -374,14 +437,20 @@ export async function startNativeService(ctx, deps, options = {}) {
374
437
  assertOwner(worker);
375
438
  worker.lastMessageId = mesh.prepare('SELECT COALESCE(MAX(id),0) AS id FROM messages').get().id;
376
439
  ingest(worker, true);
377
- });
440
+ if (!previous) mesh.prepare(`INSERT INTO native_worker_reservations(peer,owner,provider,cwd,pid)
441
+ VALUES (?,?,?,?,?)`).run(peer, worker.owner, provider, cwd, process.pid);
442
+ }));
443
+ options.afterWorkerReservation?.({ peer, owner: worker.owner });
378
444
  store.saveWorker({ peer, provider, cwd, cwdIdentity: expectedCwdIdentity, sandbox, sessionId, status: 'opening' });
445
+ if (!previous) owner.withWrite(() => mesh.prepare('DELETE FROM native_worker_reservations WHERE peer=? AND owner=?')
446
+ .run(peer, worker.owner));
379
447
  creating.add(peer);
380
448
  workers.set(peer, worker);
381
449
  try {
382
450
  // A provider file sandbox cannot restrict a mutating MCP server.
383
451
  if (sandbox !== 'read-only') {
384
452
  worker.mcpCapability = (options.mcpConfigFactory || createScopedMcpConfig)({ root: ctx.root, dbPath: ctx.dbPath,
453
+ rootIdentity: serviceRootIdentity,
385
454
  peer, executorId: worker.owner, ownerIdentity: identity,
386
455
  binding: { transport: 'native', runtimeSessionId: peer, runtimeTarget: worker.owner } });
387
456
  }
@@ -390,7 +459,7 @@ export async function startNativeService(ctx, deps, options = {}) {
390
459
  expectedCwdIdentity,
391
460
  executorId: worker.owner, interactive: true,
392
461
  ...(worker.mcpCapability ? { mcpServers: { hello_cc_scoped: worker.mcpCapability.config } } : {}),
393
- env: nativeWorkerEnv(process.env, ctx, peer, worker.owner), onEvent: (event) => {
462
+ env: nativeWorkerEnv(process.env, { ...ctx, initialRootIdentity: serviceRootIdentity }, peer, worker.owner), onEvent: (event) => {
394
463
  if (worker.adapter) record(worker, event);
395
464
  } });
396
465
  assertSelectedCwdSnapshot(expectedCwdIdentity);
@@ -407,8 +476,14 @@ export async function startNativeService(ctx, deps, options = {}) {
407
476
  requireRunning();
408
477
  assertSelectedCwdSnapshot(expectedCwdIdentity);
409
478
  assertSelectedCwdSnapshot(serviceRootIdentity);
479
+ if (provider === 'codex' && !sessionId && !input.forkSessionId && worker.adapter.snapshot().sessionId &&
480
+ sameSelectedCwdIdentity(expectedCwdIdentity, serviceRootIdentity)) {
481
+ assertSelectedCwdSnapshot(serviceRootIdentity);
482
+ owner.withWrite(() => tx(mesh, () => recordCodexThreadRoot(mesh, deps.addEvent,
483
+ worker.adapter.snapshot().sessionId, serviceRootIdentity, 'native-new')));
484
+ }
410
485
  save(worker);
411
- deps.addEvent(mesh, 'native.worker.opened', peer, null, { provider, session_id: worker.adapter.snapshot().sessionId || null, resumed: Boolean(sessionId) });
486
+ owner.withWrite(() => deps.addEvent(mesh, 'native.worker.opened', peer, null, { provider, session_id: worker.adapter.snapshot().sessionId || null, resumed: Boolean(sessionId) }));
412
487
  return { peer, ...workerSnapshot(worker) };
413
488
  } catch (error) {
414
489
  worker.closing = true;
@@ -422,7 +497,7 @@ export async function startNativeService(ctx, deps, options = {}) {
422
497
  if (!cleanupError) workers.delete(peer);
423
498
  try {
424
499
  if (worker.adapter) save(worker, { checkSandbox: false });
425
- else tx(mesh, () => { assertOwner(worker); mesh.prepare("UPDATE peers SET status='blocked' WHERE id=?").run(peer); });
500
+ else owner.withWrite(() => tx(mesh, () => { assertOwner(worker); mesh.prepare("UPDATE peers SET status='blocked' WHERE id=?").run(peer); }));
426
501
  } catch {}
427
502
  store.saveWorker({ peer, provider, cwd, cwdIdentity: expectedCwdIdentity, ...(worker.adapter?.snapshot() || {}), sandbox, status: 'error' });
428
503
  if (cleanupError) throw new CliError('NATIVE_CLOSE_FAILED', 'Worker initialization failed and its owned provider exit was not confirmed',
@@ -431,15 +506,13 @@ export async function startNativeService(ctx, deps, options = {}) {
431
506
  } finally { creating.delete(peer); }
432
507
  }
433
508
  function context(worker, message, delivery) {
434
- const command = ['env', `HCC_PEER=${worker.peer}`, `HCC_ROOT=${ctx.root}`, `HCC_DB=${ctx.dbPath}`,
435
- process.execPath, commandPath()].map(shellQuoteArg).join(' ');
436
509
  const tasks = mesh.prepare("SELECT id,status,owner,title FROM tasks WHERE status NOT IN ('done','abandoned') ORDER BY id DESC LIMIT 8").all();
437
510
  const locks = mesh.prepare('SELECT resource,owner,task_id FROM locks WHERE expires_at>? ORDER BY resource LIMIT 8').all(Math.floor(Date.now() / 1000));
438
511
  return ['[hello-cc native coordination]', `peer: ${worker.peer}`, `project: ${ctx.root}`,
439
512
  ...(worker.sandbox === 'read-only'
440
513
  ? ['This worker uses the Codex read-only file sandbox. Inspect and report only; do not modify files or execute mutating coordination commands. HCC coordination MCP tools are not attached.']
441
514
  : ['Use the scoped hello_cc_scoped MCP tools for HCC state, tasks, locks, results and handoffs. Continue your owned task until done, blocked or handed off.',
442
- `Use this exact prefix for HCC commands: ${command}`]),
515
+ 'Ordinary hcc CLI commands from this native worker are blocked because they cannot preserve the runtime owner fence; use the scoped MCP tools instead.']),
443
516
  'Only the message below is newly delivered from the inbox in this turn. You may use previous turns already present in this same provider session as context. Do not independently consume or acknowledge other inbox messages.',
444
517
  delivery.origin === 'user'
445
518
  ? 'The request below was submitted by the local user through authenticated HCC CLI or Web controls. Follow it within the user scope; tool permissions still require explicit approval.'
@@ -450,7 +523,10 @@ export async function startNativeService(ctx, deps, options = {}) {
450
523
  '[open tasks]', ...tasks.map((row) => JSON.stringify(row)), '[active locks]', ...locks.map((row) => JSON.stringify(row))].join('\n');
451
524
  }
452
525
  async function dispatch(worker, delivery, message) {
526
+ let directoryChanged = false;
453
527
  try {
528
+ owner.assertCurrent();
529
+ assertWorkerDirectory(worker);
454
530
  const label = delivery.origin === 'user' ? 'Local user request' : 'Peer coordination message';
455
531
  const receipt = await worker.adapter.send({ text: `${context(worker, message, delivery)}\n\n${label} #${message.id} (${message.kind}) from ${message.sender}:\n${message.body}`,
456
532
  submissionId: delivery.submission_id });
@@ -461,7 +537,8 @@ export async function startNativeService(ctx, deps, options = {}) {
461
537
  store.updateDelivery(delivery.id, state, worker.active.turnId, { receipt_status: receipt.status });
462
538
  }
463
539
  } catch (error) {
464
- if (!storeClosed) {
540
+ directoryChanged = error.code === 'PROJECT_PATH_CHANGED';
541
+ if (!storeClosed && !directoryChanged) {
465
542
  const current = store.delivery(worker.peer, message.id);
466
543
  if (current && !['completed', 'failed'].includes(current.state)) {
467
544
  store.updateDelivery(delivery.id, error.extra?.uncertain || current.state === 'uncertain' ? 'uncertain' : 'failed', null, publicError(error));
@@ -469,8 +546,9 @@ export async function startNativeService(ctx, deps, options = {}) {
469
546
  }
470
547
  if (worker.active?.delivery.id === delivery.id) { worker.active = null; worker.output = []; }
471
548
  worker.quarantined = true;
549
+ if (directoryChanged) worker.mcpCapability?.dispose();
472
550
  }
473
- if (!worker.closing) { try { save(worker); } catch (error) { record(worker, { type: 'error', ...publicError(error) }); } }
551
+ if (!worker.closing && !directoryChanged) { try { save(worker); } catch (error) { record(worker, { type: 'error', ...publicError(error) }); } }
474
552
  }
475
553
  async function forkWorker(input) {
476
554
  requireRunning();
@@ -495,7 +573,7 @@ export async function startNativeService(ctx, deps, options = {}) {
495
573
  : saved.status !== 'closed')) {
496
574
  throw new CliError('NATIVE_FORK_NOT_READY', 'Finish or resolve the parent operation before forking');
497
575
  }
498
- if (parent) { assertRequestedOwner(parent, input); assertSelectedCwdSnapshot(parent.cwdIdentity); ingest(parent); }
576
+ if (parent) { assertRequestedOwner(parent, input); assertWorkerDirectory(parent); ingest(parent); }
499
577
  else if ((input.owner !== undefined && input.owner !== binding.runtime_target) ||
500
578
  (input.sessionId !== undefined && input.sessionId !== saved.session_id) ||
501
579
  (input.generation !== undefined && input.generation !== generation)) {
@@ -529,8 +607,8 @@ export async function startNativeService(ctx, deps, options = {}) {
529
607
  throw new CliError('NATIVE_SANDBOX_MISMATCH', 'Parent sandbox policy changed while forking');
530
608
  }
531
609
  assertSelectedCwdSnapshot(cwdIdentity);
532
- deps.addEvent(mesh, 'native.worker.forked', input.peer, null,
533
- { provider: saved.provider, parent_peer: input.parent, parent_session_id: saved.session_id, session_id: child.sessionId });
610
+ owner.withWrite(() => deps.addEvent(mesh, 'native.worker.forked', input.peer, null,
611
+ { provider: saved.provider, parent_peer: input.parent, parent_session_id: saved.session_id, session_id: child.sessionId }));
534
612
  return { ...child, parent: input.parent, parentSessionId: saved.session_id };
535
613
  } catch (error) {
536
614
  try { await closeWorker(input.peer); }
@@ -547,6 +625,7 @@ export async function startNativeService(ctx, deps, options = {}) {
547
625
  for (const worker of workers.values()) {
548
626
  try {
549
627
  if (worker.closing || creating.has(worker.peer) || forking.has(worker.peer)) continue;
628
+ assertWorkerDirectory(worker);
550
629
  save(worker);
551
630
  ingest(worker);
552
631
  if (worker.active || worker.dispatch || worker.quarantined) continue;
@@ -561,7 +640,12 @@ export async function startNativeService(ctx, deps, options = {}) {
561
640
  const operation = dispatch(worker, delivery, message);
562
641
  worker.dispatch = operation;
563
642
  void track(operation).finally(() => { worker.dispatch = null; }).catch(() => {});
564
- } catch (error) { record(worker, { type: 'error', ...publicError(error) }); }
643
+ } catch (error) {
644
+ if (error.code === 'PROJECT_PATH_CHANGED') {
645
+ worker.quarantined = true;
646
+ worker.mcpCapability?.dispose();
647
+ } else record(worker, { type: 'error', ...publicError(error) });
648
+ }
565
649
  }
566
650
  }
567
651
  function closeWorker(peer) {
@@ -570,7 +654,7 @@ export async function startNativeService(ctx, deps, options = {}) {
570
654
  if (worker.closePromise) return worker.closePromise;
571
655
  worker.closing = true;
572
656
  worker.mcpCapability?.dispose();
573
- if (worker.active && !storeClosed) {
657
+ if (worker.active && !storeClosed && !owner.lost) {
574
658
  store.updateDelivery(worker.active.delivery.id, 'uncertain', worker.active.turnId, { reason: 'worker_closed' });
575
659
  worker.active = null;
576
660
  }
@@ -582,7 +666,7 @@ export async function startNativeService(ctx, deps, options = {}) {
582
666
  // Factories can resolve after close begins. Confirm their cleanup even
583
667
  // when initialization itself rejected; its cleanup may also have failed.
584
668
  if (worker.adapter && worker.adapter !== adapterAtClose) await worker.adapter.close();
585
- try { save(worker, { checkSandbox: false }); } catch (error) { if (!['NATIVE_PEER_IN_USE', 'NATIVE_RUNTIME_OWNERSHIP_LOST'].includes(error.code)) throw error; }
669
+ try { save(worker, { checkSandbox: false }); } catch (error) { if (!['NATIVE_PEER_IN_USE', 'NATIVE_RUNTIME_OWNERSHIP_LOST', 'NATIVE_OWNER_LOST'].includes(error.code)) throw error; }
586
670
  if (!creating.has(peer)) workers.delete(peer);
587
671
  return { peer, status: 'closed' };
588
672
  })();
@@ -590,7 +674,7 @@ export async function startNativeService(ctx, deps, options = {}) {
590
674
  void closing.catch((error) => {
591
675
  worker.quarantined = true;
592
676
  worker.closePromise = null;
593
- if (!storeClosed) store.saveWorker({ peer: worker.peer, provider: worker.provider, cwd: worker.cwd,
677
+ if (!storeClosed && !owner.lost) store.saveWorker({ peer: worker.peer, provider: worker.provider, cwd: worker.cwd,
594
678
  ...worker.adapter?.snapshot(), sandbox: worker.sandbox, status: 'uncertain' });
595
679
  console.error(JSON.stringify(publicError(error)));
596
680
  });
@@ -606,6 +690,7 @@ export async function startNativeService(ctx, deps, options = {}) {
606
690
  try {
607
691
  const url = new URL(request.url, 'http://localhost');
608
692
  let result;
693
+ owner.assertCurrent();
609
694
  if (request.method === 'POST' && !['/down', '/close'].includes(url.pathname)) requireRunning();
610
695
  if (request.method === 'GET' && url.pathname === '/status') {
611
696
  result = { generation, sandboxPolicyVersion: 1, root: fs.realpathSync(ctx.root), meshDb: path.resolve(ctx.dbPath), pid: process.pid, stopping, shutdown_error: shutdownError,
@@ -660,6 +745,7 @@ export async function startNativeService(ctx, deps, options = {}) {
660
745
  const worker = workers.get(input.peer);
661
746
  if (forking.has(input.peer)) throw new CliError('NATIVE_FORK_NOT_READY', 'Wait for the parent fork to finish');
662
747
  if (!worker || worker.closing || creating.has(input.peer)) throw new CliError('NATIVE_WORKER_NOT_FOUND', 'Target native worker is not open');
748
+ assertWorkerDirectory(worker);
663
749
  if (worker.quarantined) throw new CliError('NATIVE_WORKER_UNCERTAIN', 'Worker needs inspection and an explicit close/resume before more submissions');
664
750
  if (typeof input.body !== 'string' || !input.body.trim() || typeof input.from !== 'string' || !input.from.trim() ||
665
751
  (input.taskId != null && (!Number.isSafeInteger(input.taskId) || input.taskId < 1))) throw new CliError('BAD_ARGS', 'Message needs a sender, nonempty body, and valid task ID');
@@ -682,7 +768,7 @@ export async function startNativeService(ctx, deps, options = {}) {
682
768
  return;
683
769
  }
684
770
  }
685
- result = (() => {
771
+ result = owner.withWrite(() => {
686
772
  const submissionId = input.submissionId || randomUUID();
687
773
  let intent = mesh.prepare('SELECT * FROM native_user_submissions WHERE submission_id=?').get(submissionId);
688
774
  if (intent) matchingUserSubmission(input, intent);
@@ -703,7 +789,7 @@ export async function startNativeService(ctx, deps, options = {}) {
703
789
  throw new CliError('NATIVE_SUBMISSION_MISMATCH', 'Submission delivery conflicts with the durable message intent');
704
790
  }
705
791
  return { message_id: intent.message_id, submission_id: submissionId, state: delivery.state };
706
- })();
792
+ });
707
793
  } else if (request.method === 'POST' && url.pathname === '/interrupt') {
708
794
  const input = await jsonBody(request);
709
795
  requireRunning();
@@ -717,6 +803,7 @@ export async function startNativeService(ctx, deps, options = {}) {
717
803
  requireRunning();
718
804
  const worker = workers.get(input.peer);
719
805
  if (!worker?.adapter || worker.closing || creating.has(input.peer)) throw new CliError('NATIVE_WORKER_NOT_FOUND', 'Target native worker is not open');
806
+ assertWorkerDirectory(worker);
720
807
  assertRequestedOwner(worker, input);
721
808
  const snapshot = workerSnapshot(worker);
722
809
  const matches = (snapshot.pendingApprovals || []).filter(pending => pending.requestId === input.requestId &&
@@ -750,7 +837,9 @@ export async function startNativeService(ctx, deps, options = {}) {
750
837
  reply(response, 200, { ok: true, data: redactSecrets(result) });
751
838
  } catch (error) { reply(response, error.code === 'BAD_ARGS' ? 400 : 409, { ok: false, error: publicError(error) }); }
752
839
  }
753
- const server = http.createServer((request, response) => { void route(request, response); });
840
+ server = http.createServer((request, response) => {
841
+ void track(route(request, response)).catch(error => console.error(JSON.stringify(publicError(error))));
842
+ });
754
843
  server.requestTimeout = 40000;
755
844
  server.headersTimeout = 10000;
756
845
  let serverClosed = false;
@@ -765,8 +854,10 @@ export async function startNativeService(ctx, deps, options = {}) {
765
854
  try {
766
855
  await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
767
856
  ownership.assertHeld();
768
- writeNativePointer(ctx, { root: fs.realpathSync(ctx.root), meshDb: path.resolve(ctx.dbPath), pid: process.pid,
769
- port: server.address().port, token, generation, processIdentity: identity });
857
+ assertSelectedCwdSnapshot(serviceRootIdentity);
858
+ owner.withWrite(() => writeNativePointer(ctx, { root: fs.realpathSync(ctx.root), meshDb: path.resolve(ctx.dbPath), pid: process.pid,
859
+ rootIdentity: serviceRootIdentity, ownerVersion: 2, ownerIdentity: identity, stateGeneration: owner.stateGeneration,
860
+ port: server.address().port, token, generation, processIdentity: identity }));
770
861
  } catch (error) {
771
862
  try { await closeServer(); }
772
863
  catch (cleanup) {
@@ -797,7 +888,9 @@ export async function startNativeService(ctx, deps, options = {}) {
797
888
  }
798
889
  await closeServer();
799
890
  closePersistence();
800
- if (readNativePointer(ctx)?.generation === generation) fs.rmSync(paths.pointer, { force: true });
891
+ try {
892
+ if (readNativePointer(ctx)?.generation === generation) fs.rmSync(paths.pointer, { force: true });
893
+ } catch (error) { if (error.code !== 'PROJECT_PATH_CHANGED') throw error; }
801
894
  ownership.release();
802
895
  options.onShutdown?.();
803
896
  })();