@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
|
@@ -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 (
|
|
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
|
-
|
|
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
|
|
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 {
|
|
11
|
-
import {
|
|
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 (
|
|
92
|
-
try {
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
769
|
-
|
|
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
|
-
|
|
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
|
})();
|