@farmslot/agent-runtime 0.9.0 → 0.11.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 +20 -1
- package/README.md +1 -1
- package/bin/farmslot-agent.mjs +1 -1
- package/dist/execution-template/create.d.ts.map +1 -1
- package/dist/execution-template/create.js +4 -9
- package/dist/execution-template/create.js.map +1 -1
- package/dist/execution-template/execution-template.test.js +85 -34
- package/dist/execution-template/execution-template.test.js.map +1 -1
- package/dist/execution-template/frontmatter.d.ts +6 -3
- package/dist/execution-template/frontmatter.d.ts.map +1 -1
- package/dist/execution-template/frontmatter.js +8 -10
- package/dist/execution-template/frontmatter.js.map +1 -1
- package/dist/execution-template/index.d.ts +2 -2
- package/dist/execution-template/index.d.ts.map +1 -1
- package/dist/execution-template/index.js +2 -2
- package/dist/execution-template/index.js.map +1 -1
- package/dist/execution-template/infer.d.ts +2 -3
- package/dist/execution-template/infer.d.ts.map +1 -1
- package/dist/execution-template/infer.js +2 -19
- package/dist/execution-template/infer.js.map +1 -1
- package/dist/execution-template/lint.d.ts.map +1 -1
- package/dist/execution-template/lint.js +12 -14
- package/dist/execution-template/lint.js.map +1 -1
- package/dist/execution-template/resolve.d.ts.map +1 -1
- package/dist/execution-template/resolve.js +0 -2
- package/dist/execution-template/resolve.js.map +1 -1
- package/dist/execution-template/select.d.ts.map +1 -1
- package/dist/execution-template/select.js +4 -8
- package/dist/execution-template/select.js.map +1 -1
- package/dist/execution-template/snapshot.d.ts.map +1 -1
- package/dist/execution-template/snapshot.js +0 -1
- package/dist/execution-template/snapshot.js.map +1 -1
- package/dist/execution-template/types.d.ts +10 -5
- package/dist/execution-template/types.d.ts.map +1 -1
- package/dist/execution-template/types.js.map +1 -1
- package/dist/native/account-claude.d.ts +5 -0
- package/dist/native/account-claude.d.ts.map +1 -0
- package/dist/native/account-claude.js +42 -0
- package/dist/native/account-claude.js.map +1 -0
- package/dist/native/account-codex.d.ts +4 -0
- package/dist/native/account-codex.d.ts.map +1 -0
- package/dist/native/account-codex.js +51 -0
- package/dist/native/account-codex.js.map +1 -0
- package/dist/native/account-common.d.ts +30 -0
- package/dist/native/account-common.d.ts.map +1 -0
- package/dist/native/account-common.js +155 -0
- package/dist/native/account-common.js.map +1 -0
- package/dist/native/account-cursor.d.ts +5 -0
- package/dist/native/account-cursor.d.ts.map +1 -0
- package/dist/native/account-cursor.js +48 -0
- package/dist/native/account-cursor.js.map +1 -0
- package/dist/native/account-grok.d.ts +4 -0
- package/dist/native/account-grok.d.ts.map +1 -0
- package/dist/native/account-grok.js +55 -0
- package/dist/native/account-grok.js.map +1 -0
- package/dist/native/account-observation.test.d.ts +2 -0
- package/dist/native/account-observation.test.d.ts.map +1 -0
- package/dist/native/account-observation.test.js +256 -0
- package/dist/native/account-observation.test.js.map +1 -0
- package/dist/native/account-profiles.d.ts +11 -0
- package/dist/native/account-profiles.d.ts.map +1 -0
- package/dist/native/account-profiles.js +172 -0
- package/dist/native/account-profiles.js.map +1 -0
- package/dist/native/account-profiles.test.d.ts +2 -0
- package/dist/native/account-profiles.test.d.ts.map +1 -0
- package/dist/native/account-profiles.test.js +172 -0
- package/dist/native/account-profiles.test.js.map +1 -0
- package/dist/native/account-types.d.ts +11 -0
- package/dist/native/account-types.d.ts.map +1 -0
- package/dist/native/account-types.js +2 -0
- package/dist/native/account-types.js.map +1 -0
- package/dist/native/acp-requests.d.ts +11 -0
- package/dist/native/acp-requests.d.ts.map +1 -0
- package/dist/native/acp-requests.js +109 -0
- package/dist/native/acp-requests.js.map +1 -0
- package/dist/native/acp-rpc.d.ts +15 -0
- package/dist/native/acp-rpc.d.ts.map +1 -0
- package/dist/native/acp-rpc.js +73 -0
- package/dist/native/acp-rpc.js.map +1 -0
- package/dist/native/acp.d.ts +15 -0
- package/dist/native/acp.d.ts.map +1 -0
- package/dist/native/acp.js +285 -0
- package/dist/native/acp.js.map +1 -0
- package/dist/native/acp.test.d.ts +2 -0
- package/dist/native/acp.test.d.ts.map +1 -0
- package/dist/native/acp.test.js +257 -0
- package/dist/native/acp.test.js.map +1 -0
- package/dist/native/claude.d.ts.map +1 -1
- package/dist/native/claude.js +14 -0
- package/dist/native/claude.js.map +1 -1
- package/dist/native/client.d.ts +13 -0
- package/dist/native/client.d.ts.map +1 -1
- package/dist/native/client.js +112 -1
- package/dist/native/client.js.map +1 -1
- package/dist/native/codex.d.ts.map +1 -1
- package/dist/native/codex.js +61 -3
- package/dist/native/codex.js.map +1 -1
- package/dist/native/cursor-model.d.ts +6 -0
- package/dist/native/cursor-model.d.ts.map +1 -0
- package/dist/native/cursor-model.js +74 -0
- package/dist/native/cursor-model.js.map +1 -0
- package/dist/native/cursor-model.test.d.ts +2 -0
- package/dist/native/cursor-model.test.d.ts.map +1 -0
- package/dist/native/cursor-model.test.js +40 -0
- package/dist/native/cursor-model.test.js.map +1 -0
- package/dist/native/cursor.d.ts +2 -0
- package/dist/native/cursor.d.ts.map +1 -0
- package/dist/native/cursor.js +30 -0
- package/dist/native/cursor.js.map +1 -0
- package/dist/native/durability.test.js +461 -1
- package/dist/native/durability.test.js.map +1 -1
- package/dist/native/filesystem-policy.test.d.ts +2 -0
- package/dist/native/filesystem-policy.test.d.ts.map +1 -0
- package/dist/native/filesystem-policy.test.js +136 -0
- package/dist/native/filesystem-policy.test.js.map +1 -0
- package/dist/native/grok.d.ts +2 -0
- package/dist/native/grok.d.ts.map +1 -0
- package/dist/native/grok.js +17 -0
- package/dist/native/grok.js.map +1 -0
- package/dist/native/host.js +39 -0
- package/dist/native/host.js.map +1 -1
- package/dist/native/index.d.ts +3 -0
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +3 -0
- package/dist/native/index.js.map +1 -1
- package/dist/native/ipc.d.ts +59 -0
- package/dist/native/ipc.d.ts.map +1 -1
- package/dist/native/ipc.js +76 -4
- package/dist/native/ipc.js.map +1 -1
- package/dist/native/manager.d.ts +22 -4
- package/dist/native/manager.d.ts.map +1 -1
- package/dist/native/manager.js +386 -46
- package/dist/native/manager.js.map +1 -1
- package/dist/native/process-tree.d.ts +11 -0
- package/dist/native/process-tree.d.ts.map +1 -1
- package/dist/native/process-tree.js +123 -20
- package/dist/native/process-tree.js.map +1 -1
- package/dist/native/process-tree.test.js +126 -1
- package/dist/native/process-tree.test.js.map +1 -1
- package/dist/native/process.d.ts +5 -1
- package/dist/native/process.d.ts.map +1 -1
- package/dist/native/process.js +36 -13
- package/dist/native/process.js.map +1 -1
- package/dist/native/process.test.d.ts +2 -0
- package/dist/native/process.test.d.ts.map +1 -0
- package/dist/native/process.test.js +56 -0
- package/dist/native/process.test.js.map +1 -0
- package/dist/native/profile-service.d.ts +5 -0
- package/dist/native/profile-service.d.ts.map +1 -0
- package/dist/native/profile-service.js +76 -0
- package/dist/native/profile-service.js.map +1 -0
- package/dist/native/registry.d.ts +20 -0
- package/dist/native/registry.d.ts.map +1 -0
- package/dist/native/registry.js +89 -0
- package/dist/native/registry.js.map +1 -0
- package/dist/native/registry.test.d.ts +2 -0
- package/dist/native/registry.test.d.ts.map +1 -0
- package/dist/native/registry.test.js +13 -0
- package/dist/native/registry.test.js.map +1 -0
- package/dist/native/review-sandbox.d.ts +12 -0
- package/dist/native/review-sandbox.d.ts.map +1 -0
- package/dist/native/review-sandbox.js +6 -0
- package/dist/native/review-sandbox.js.map +1 -0
- package/dist/native/review-sandbox.test.d.ts +2 -0
- package/dist/native/review-sandbox.test.d.ts.map +1 -0
- package/dist/native/review-sandbox.test.js +40 -0
- package/dist/native/review-sandbox.test.js.map +1 -0
- package/dist/native/service.d.ts.map +1 -1
- package/dist/native/service.js +42 -8
- package/dist/native/service.js.map +1 -1
- package/dist/native/supervisor.js +121 -33
- package/dist/native/supervisor.js.map +1 -1
- package/dist/native/types.d.ts +11 -1
- package/dist/native/types.d.ts.map +1 -1
- package/dist/native/worker-history.d.ts +16 -0
- package/dist/native/worker-history.d.ts.map +1 -0
- package/dist/native/worker-history.js +77 -0
- package/dist/native/worker-history.js.map +1 -0
- package/dist/native/worker-history.test.d.ts +2 -0
- package/dist/native/worker-history.test.d.ts.map +1 -0
- package/dist/native/worker-history.test.js +195 -0
- package/dist/native/worker-history.test.js.map +1 -0
- package/dist/native/worker-launch.d.ts +72 -0
- package/dist/native/worker-launch.d.ts.map +1 -0
- package/dist/native/worker-launch.js +134 -0
- package/dist/native/worker-launch.js.map +1 -0
- package/dist/native/worker-launch.test.d.ts +2 -0
- package/dist/native/worker-launch.test.d.ts.map +1 -0
- package/dist/native/worker-launch.test.js +61 -0
- package/dist/native/worker-launch.test.js.map +1 -0
- package/dist/native/worker-relocation.test.d.ts +2 -0
- package/dist/native/worker-relocation.test.d.ts.map +1 -0
- package/dist/native/worker-relocation.test.js +110 -0
- package/dist/native/worker-relocation.test.js.map +1 -0
- package/dist/native/worker-service.d.ts +4 -0
- package/dist/native/worker-service.d.ts.map +1 -0
- package/dist/native/worker-service.js +85 -0
- package/dist/native/worker-service.js.map +1 -0
- package/dist/task-init/discover.d.ts +26 -0
- package/dist/task-init/discover.d.ts.map +1 -0
- package/dist/task-init/discover.js +64 -0
- package/dist/task-init/discover.js.map +1 -0
- package/dist/task-init/discover.test.d.ts +2 -0
- package/dist/task-init/discover.test.d.ts.map +1 -0
- package/dist/task-init/discover.test.js +58 -0
- package/dist/task-init/discover.test.js.map +1 -0
- package/dist/task-init/index.d.ts +6 -1
- package/dist/task-init/index.d.ts.map +1 -1
- package/dist/task-init/index.js +8 -3
- package/dist/task-init/index.js.map +1 -1
- package/dist/task-init/task-document.d.ts.map +1 -1
- package/dist/task-init/task-document.js +2 -2
- package/dist/task-init/task-document.js.map +1 -1
- package/dist/task-init/write.d.ts +0 -6
- package/dist/task-init/write.d.ts.map +1 -1
- package/dist/task-init/write.js +1 -8
- package/dist/task-init/write.js.map +1 -1
- package/package.json +3 -3
- package/scripts/cursor-session-discovery.cjs +48 -0
- package/scripts/execution-template-cli.mjs +8 -11
- package/scripts/review-filesystem.cjs +39 -0
- package/scripts/review-session.cjs +45 -0
- package/scripts/review-terminal.cjs +134 -0
- package/scripts/task-init-cli.mjs +3 -3
package/dist/native/manager.js
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { closeSync, constants, fsyncSync, openSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { closeSync, constants, existsSync, fsyncSync, openSync, readdirSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { access, realpath, stat } from 'node:fs/promises';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
-
import { delimiter, dirname, isAbsolute, join } from 'node:path';
|
|
6
|
+
import { basename, delimiter, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
7
7
|
import { promisify } from 'node:util';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { assertNativeProfileCurrent, nativeProfileEnvironment, requireNativeProfile, } from './account-profiles.js';
|
|
9
|
+
import { nativeRunnerDefinitions as adapters } from './registry.js';
|
|
10
|
+
import { hostReviewSandboxAvailable, reviewProcessSandbox } from './review-sandbox.js';
|
|
11
|
+
import { durableWrite, privateDirectory, readJournal, readJson } from './storage.js';
|
|
12
|
+
import { NativeWorkerHistory } from './worker-history.js';
|
|
13
|
+
import { nativeWorkerEnvironment, nativeWorkerLaunchDigest, validateNativeWorkerFilesystemPolicy, } from './worker-launch.js';
|
|
11
14
|
const exec = promisify(execFile);
|
|
12
|
-
|
|
13
|
-
codex: { adapter: codexNativeAdapter, binary: 'codex' },
|
|
14
|
-
claude: { adapter: claudeNativeAdapter, binary: 'claude' },
|
|
15
|
-
};
|
|
16
|
-
export async function resolveNativeExecutable(binary, cwd) {
|
|
15
|
+
export async function resolveNativeExecutable(binary, cwd, environment = process.env) {
|
|
17
16
|
let executable;
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const requested = binary.startsWith('~/')
|
|
18
|
+
? join(environment.HOME ?? homedir(), binary.slice(2))
|
|
19
|
+
: binary;
|
|
20
|
+
const candidates = isAbsolute(requested)
|
|
21
|
+
? [requested]
|
|
22
|
+
: requested.includes('/')
|
|
23
|
+
? [resolve(cwd, requested)]
|
|
24
|
+
: (environment.PATH ?? '')
|
|
25
|
+
.split(delimiter)
|
|
26
|
+
.map((directory) => resolve(cwd, directory, requested));
|
|
27
|
+
for (const candidate of candidates) {
|
|
20
28
|
try {
|
|
21
29
|
await access(candidate, constants.X_OK);
|
|
22
30
|
}
|
|
@@ -25,39 +33,74 @@ export async function resolveNativeExecutable(binary, cwd) {
|
|
|
25
33
|
continue;
|
|
26
34
|
throw error;
|
|
27
35
|
}
|
|
28
|
-
|
|
36
|
+
if (candidate.includes('/.asdf/shims/')) {
|
|
37
|
+
try {
|
|
38
|
+
const resolved = await exec('asdf', ['which', basename(binary)], {
|
|
39
|
+
cwd,
|
|
40
|
+
timeout: 10_000,
|
|
41
|
+
env: environment,
|
|
42
|
+
});
|
|
43
|
+
executable = resolved.stdout.trim();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
// Fresh worktrees may not select the shim's runtime. For a PATH lookup,
|
|
47
|
+
// continue to another installed executable; explicit paths remain exact.
|
|
48
|
+
if (error.code === 1 &&
|
|
49
|
+
!isAbsolute(requested) &&
|
|
50
|
+
!requested.includes('/'))
|
|
51
|
+
continue;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else
|
|
56
|
+
executable = candidate;
|
|
29
57
|
break;
|
|
30
58
|
}
|
|
31
59
|
if (!executable)
|
|
32
60
|
throw new Error(`Install and log into the native ${binary} runner first`);
|
|
33
|
-
if (executable.includes('/.asdf/shims/')) {
|
|
34
|
-
const resolved = await exec('asdf', ['which', binary], { cwd, timeout: 10_000 });
|
|
35
|
-
executable = resolved.stdout.trim();
|
|
36
|
-
}
|
|
37
61
|
executable = await realpath(executable);
|
|
38
62
|
const result = await exec(executable, ['--version'], {
|
|
39
63
|
cwd,
|
|
40
64
|
timeout: 10_000,
|
|
41
|
-
env: nativeEnvironment(executable),
|
|
65
|
+
env: nativeEnvironment(executable, environment),
|
|
42
66
|
});
|
|
43
67
|
const version = result.stdout.trim();
|
|
44
68
|
if (!version)
|
|
45
69
|
throw new Error('Native executable did not provide version metadata');
|
|
46
70
|
return { executable, version };
|
|
47
71
|
}
|
|
48
|
-
function executionContext() {
|
|
72
|
+
function executionContext(workerLaunch, runner, profile) {
|
|
73
|
+
const environment = profile
|
|
74
|
+
? nativeProfileEnvironment(profile, workerLaunch ? nativeWorkerEnvironment(workerLaunch) : process.env)
|
|
75
|
+
: process.env;
|
|
49
76
|
return {
|
|
50
|
-
home: homedir(),
|
|
51
|
-
codexHome:
|
|
52
|
-
claudeConfigDir:
|
|
77
|
+
home: profile ? (environment.HOME ?? homedir()) : homedir(),
|
|
78
|
+
codexHome: environment.CODEX_HOME ?? '',
|
|
79
|
+
claudeConfigDir: environment.CLAUDE_CONFIG_DIR ?? '',
|
|
80
|
+
...(runner === 'cursor'
|
|
81
|
+
? {
|
|
82
|
+
cursorConfigDir: environment.CURSOR_CONFIG_DIR ?? '',
|
|
83
|
+
cursorDataDir: environment.CURSOR_DATA_DIR ?? '',
|
|
84
|
+
xdgConfigHome: environment.XDG_CONFIG_HOME ?? '',
|
|
85
|
+
}
|
|
86
|
+
: {}),
|
|
87
|
+
...(runner === 'grok' ? { grokHome: environment.GROK_HOME ?? '' } : {}),
|
|
88
|
+
...(workerLaunch ? { workerLaunchDigest: nativeWorkerLaunchDigest(workerLaunch) } : {}),
|
|
89
|
+
...(profile
|
|
90
|
+
? {
|
|
91
|
+
profileId: profile.id,
|
|
92
|
+
profileDirectory: profile.directory,
|
|
93
|
+
profileAccountContextId: profile.accountContextId,
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
53
96
|
};
|
|
54
97
|
}
|
|
55
|
-
function nativeEnvironment(executable) {
|
|
98
|
+
function nativeEnvironment(executable, base = process.env) {
|
|
56
99
|
// Native configuration may reference environment credentials. Inherit it in-process only;
|
|
57
100
|
// never persist or send it over IPC. Remove parent Claude invocation context.
|
|
58
101
|
const env = {
|
|
59
|
-
...
|
|
60
|
-
PATH: `${dirname(executable)}${delimiter}${
|
|
102
|
+
...base,
|
|
103
|
+
PATH: `${dirname(executable)}${delimiter}${base.PATH ?? ''}`,
|
|
61
104
|
};
|
|
62
105
|
delete env.CLAUDECODE;
|
|
63
106
|
return env;
|
|
@@ -81,7 +124,9 @@ export class NativeSessionManager {
|
|
|
81
124
|
let pending = [];
|
|
82
125
|
const commands = new Map();
|
|
83
126
|
const events = [];
|
|
127
|
+
const workerHistory = new NativeWorkerHistory();
|
|
84
128
|
for (const entry of entries) {
|
|
129
|
+
workerHistory.observe(entry);
|
|
85
130
|
info = entry.info ?? info;
|
|
86
131
|
context = entry.context ?? context;
|
|
87
132
|
pending = entry.pending ?? pending;
|
|
@@ -95,6 +140,7 @@ export class NativeSessionManager {
|
|
|
95
140
|
if (info.executionNodeId !== executionNodeId)
|
|
96
141
|
throw new Error('Native journal belongs to another execution node');
|
|
97
142
|
const record = {
|
|
143
|
+
workerHistory,
|
|
98
144
|
info,
|
|
99
145
|
context,
|
|
100
146
|
commands,
|
|
@@ -157,29 +203,53 @@ export class NativeSessionManager {
|
|
|
157
203
|
for (const command of commands)
|
|
158
204
|
previous.commands.set(command.commandId, JSON.stringify(command));
|
|
159
205
|
this.persisted.set(record, { info, context, pending, commands: previous.commands });
|
|
206
|
+
record.workerHistory.observe(entry);
|
|
160
207
|
}
|
|
161
|
-
create(owner, params) {
|
|
162
|
-
return this.start(owner, params);
|
|
208
|
+
create(owner, params, workerLaunch) {
|
|
209
|
+
return this.start(owner, params, undefined, workerLaunch);
|
|
163
210
|
}
|
|
164
|
-
async ensure(owner, params) {
|
|
211
|
+
async ensure(owner, params, workerLaunch) {
|
|
165
212
|
if (typeof params.sessionId !== 'string' ||
|
|
166
213
|
!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(params.sessionId))
|
|
167
214
|
throw new Error('Reserved sessionId must be a lowercase UUID');
|
|
168
215
|
if ('resumeSessionId' in params && params.resumeSessionId !== undefined)
|
|
169
216
|
throw new Error('Reserved sessionId is for initial creation, not resume');
|
|
170
|
-
return this.start(owner, params, params.sessionId);
|
|
217
|
+
return this.start(owner, params, params.sessionId, workerLaunch);
|
|
171
218
|
}
|
|
172
|
-
|
|
219
|
+
resumeWorker(owner, params, launch) {
|
|
220
|
+
const assertCurrent = () => {
|
|
221
|
+
this.assertWorkerLease(owner, params.sessionId, params.generation, launch.leaseId, true);
|
|
222
|
+
const record = this.owned(owner, params.sessionId);
|
|
223
|
+
if (record.info.nativeSessionId !== params.resumeSessionId)
|
|
224
|
+
throw new Error('Worker recovery must preserve its recorded session identity');
|
|
225
|
+
if (existsSync(this.resumeCancellationPath(params.sessionId, launch.leaseId, params.commandId)))
|
|
226
|
+
throw new Error('Native worker recovery operation was cancelled before launch');
|
|
227
|
+
};
|
|
228
|
+
return this.start(owner, params, undefined, launch, assertCurrent, params.relocation);
|
|
229
|
+
}
|
|
230
|
+
async start(ownerPrincipalId, params, reservedId, workerLaunch, assertResumeCurrent, relocation) {
|
|
231
|
+
assertResumeCurrent?.();
|
|
232
|
+
const profile = params.profileId
|
|
233
|
+
? requireNativeProfile(params.profileId, params.runner)
|
|
234
|
+
: undefined;
|
|
235
|
+
if (profile && params.accountContextId !== profile.accountContextId)
|
|
236
|
+
throw new Error('Native account profile changed; select its current account binding');
|
|
173
237
|
if (params.executionNodeId !== undefined && params.executionNodeId !== this.executionNodeId)
|
|
174
238
|
throw new Error('Native session targets another execution node');
|
|
175
239
|
if (reservedId !== undefined) {
|
|
176
|
-
const existing = this.existingCreation(ownerPrincipalId, params, reservedId);
|
|
240
|
+
const existing = this.existingCreation(ownerPrincipalId, params, reservedId, workerLaunch);
|
|
177
241
|
if (existing)
|
|
178
242
|
return this.createdSession(existing);
|
|
243
|
+
if (workerLaunch)
|
|
244
|
+
this.assertWorkerNotCancelled(reservedId);
|
|
179
245
|
}
|
|
180
246
|
const transport = adapters[params.runner];
|
|
181
247
|
if (!transport)
|
|
182
248
|
throw new Error(`Runner has no native transport: ${params.runner}`);
|
|
249
|
+
if (workerLaunch && !transport.supportsWorkers)
|
|
250
|
+
throw new Error('Native worker execution is not supported for this runner');
|
|
251
|
+
if (workerLaunch?.filesystemPolicy && !transport.supportsReadOnlyWorkspace)
|
|
252
|
+
throw new Error('Native runner cannot enforce a read-only source workspace');
|
|
183
253
|
if (!isAbsolute(params.cwd) || !(await stat(params.cwd)).isDirectory())
|
|
184
254
|
throw new Error('Native cwd must be an existing absolute directory');
|
|
185
255
|
let previous;
|
|
@@ -198,27 +268,89 @@ export class NativeSessionManager {
|
|
|
198
268
|
throw new Error('Close the current native input owner before resuming');
|
|
199
269
|
if (previous.info.processPid && !previous.info.processStopped)
|
|
200
270
|
throw new Error('Native process cleanup is unconfirmed; recovery is blocked');
|
|
201
|
-
if (
|
|
271
|
+
if (previous.context.workerLaunchDigest && !workerLaunch)
|
|
272
|
+
throw new Error('Worker recovery requires its recorded launch configuration');
|
|
273
|
+
if (previous.info.workerLeaseId !== workerLaunch?.leaseId)
|
|
274
|
+
throw new Error('Worker recovery must preserve its task lease');
|
|
275
|
+
if (JSON.stringify(previous.context) !==
|
|
276
|
+
JSON.stringify(executionContext(workerLaunch, params.runner, profile)))
|
|
202
277
|
throw new Error('Resume must preserve the native account execution context');
|
|
203
|
-
if (
|
|
278
|
+
if (relocation) {
|
|
279
|
+
if (!assertResumeCurrent ||
|
|
280
|
+
relocation.fromCwd !== previous.info.cwd ||
|
|
281
|
+
!previous.info.capabilities.resumeAcrossWorkspaces)
|
|
282
|
+
throw new Error('Native relocation requires the exact previous workspace and a portable saved session');
|
|
283
|
+
const roots = await Promise.all([previous.info.cwd, params.cwd].map(async (cwd) => {
|
|
284
|
+
const result = await exec('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { cwd, timeout: 10_000 });
|
|
285
|
+
return realpath(result.stdout.trim());
|
|
286
|
+
}));
|
|
287
|
+
if (roots[0] !== roots[1])
|
|
288
|
+
throw new Error('Native relocation requires sibling worktrees of the same repository');
|
|
289
|
+
const unavailable = transport.adapter.workspaceResumeUnavailableReason?.(previous.info.version);
|
|
290
|
+
if (unavailable)
|
|
291
|
+
throw new Error(unavailable);
|
|
292
|
+
}
|
|
293
|
+
else if (previous.info.cwd !== params.cwd)
|
|
204
294
|
throw new Error('Resume must preserve the recorded working directory');
|
|
205
295
|
mode = params.mode ?? previous.info.mode;
|
|
206
296
|
}
|
|
207
297
|
if (!transport.adapter.capabilities.modes.includes(mode))
|
|
208
298
|
throw new Error('Native runner does not support this interaction mode');
|
|
209
|
-
const
|
|
299
|
+
const baseEnvironment = workerLaunch ? nativeWorkerEnvironment(workerLaunch) : process.env;
|
|
300
|
+
const environment = profile
|
|
301
|
+
? nativeProfileEnvironment(profile, baseEnvironment)
|
|
302
|
+
: baseEnvironment;
|
|
303
|
+
const resolved = await resolveNativeExecutable(workerLaunch?.executable ?? transport.binary, params.cwd, environment);
|
|
210
304
|
const unavailable = transport.adapter.resumeUnavailableReason?.(resolved.version);
|
|
211
305
|
if (params.resumeSessionId && unavailable)
|
|
212
306
|
throw new Error(unavailable);
|
|
307
|
+
let filesystemPolicy = workerLaunch?.filesystemPolicy;
|
|
308
|
+
if (filesystemPolicy) {
|
|
309
|
+
const unsupported = transport.adapter.filesystemPolicyUnavailableReason?.(resolved.version);
|
|
310
|
+
if (unsupported ||
|
|
311
|
+
(!transport.adapter.filesystemPolicyUnavailableReason && !hostReviewSandboxAvailable()))
|
|
312
|
+
throw new Error(unsupported ?? 'Native runner cannot enforce filesystem policy');
|
|
313
|
+
filesystemPolicy = validateNativeWorkerFilesystemPolicy({
|
|
314
|
+
readOnlyRoots: await Promise.all([...filesystemPolicy.readOnlyRoots, this.root].map((root) => realpath(root))),
|
|
315
|
+
writableRoots: await Promise.all(filesystemPolicy.writableRoots.map((root) => realpath(root))),
|
|
316
|
+
});
|
|
317
|
+
if (!filesystemPolicy.readOnlyRoots.includes(await realpath(params.cwd)))
|
|
318
|
+
throw new Error('Native worker cwd must be an explicit read-only source root');
|
|
319
|
+
if (!transport.adapter.filesystemPolicyUnavailableReason &&
|
|
320
|
+
existsSync(join(params.cwd, '.git'))) {
|
|
321
|
+
const gitDirectory = await exec('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { cwd: params.cwd, timeout: 10000 });
|
|
322
|
+
filesystemPolicy.readOnlyRoots.push(await realpath(gitDirectory.stdout.trim()));
|
|
323
|
+
}
|
|
324
|
+
if (workerLaunch?.safetyTier !== 'sandboxed')
|
|
325
|
+
throw new Error('Read-only native workers require sandboxed execution');
|
|
326
|
+
}
|
|
327
|
+
const hostProtection = filesystemPolicy && !transport.adapter.filesystemPolicyUnavailableReason
|
|
328
|
+
? await reviewProcessSandbox(filesystemPolicy, transport.reviewRuntimeRoots?.(environment) ?? [])
|
|
329
|
+
: undefined;
|
|
330
|
+
if (relocation) {
|
|
331
|
+
const unavailable = transport.adapter.workspaceResumeUnavailableReason?.(resolved.version);
|
|
332
|
+
if (unavailable)
|
|
333
|
+
throw new Error(unavailable);
|
|
334
|
+
}
|
|
213
335
|
// Validation above awaits filesystem/process probes. Recheck synchronously before reservation.
|
|
214
|
-
|
|
336
|
+
assertResumeCurrent?.();
|
|
337
|
+
if (profile)
|
|
338
|
+
assertNativeProfileCurrent(profile);
|
|
339
|
+
const existing = this.existingCreation(ownerPrincipalId, params, reservedId, workerLaunch);
|
|
215
340
|
if (existing)
|
|
216
341
|
return this.createdSession(existing);
|
|
342
|
+
if (reservedId && workerLaunch)
|
|
343
|
+
this.assertWorkerNotCancelled(reservedId);
|
|
217
344
|
// Reserve before starting the child, so simultaneous resume calls cannot create two input owners.
|
|
218
345
|
if (params.resumeSessionId &&
|
|
219
346
|
[...this.sessions.values()].some((record) => record.info.nativeSessionId === params.resumeSessionId &&
|
|
220
347
|
!['closed', 'failed'].includes(record.info.state)))
|
|
221
348
|
throw new Error('Native session already has an input owner');
|
|
349
|
+
const effort = workerLaunch
|
|
350
|
+
? workerLaunch.effort
|
|
351
|
+
: previous
|
|
352
|
+
? previous.info.effort
|
|
353
|
+
: transport.defaultEffort;
|
|
222
354
|
const info = {
|
|
223
355
|
id: previous?.info.id ?? reservedId ?? randomUUID(),
|
|
224
356
|
generation: randomUUID(),
|
|
@@ -227,23 +359,35 @@ export class NativeSessionManager {
|
|
|
227
359
|
nativeSessionId: params.resumeSessionId ?? '',
|
|
228
360
|
ownerPrincipalId,
|
|
229
361
|
executionNodeId: this.executionNodeId,
|
|
230
|
-
accountContextId:
|
|
231
|
-
|
|
232
|
-
|
|
362
|
+
accountContextId: profile?.accountContextId ??
|
|
363
|
+
(this.executionNodeId === 'local'
|
|
364
|
+
? `native-local:${ownerPrincipalId}`
|
|
365
|
+
: `native-node:${JSON.stringify([this.executionNodeId, ownerPrincipalId])}`),
|
|
366
|
+
...(profile ? { profileId: profile.id } : {}),
|
|
233
367
|
cwd: params.cwd,
|
|
234
368
|
...resolved,
|
|
235
369
|
model: params.model ?? previous?.info.model,
|
|
236
370
|
mode,
|
|
237
371
|
accountMode: 'native',
|
|
372
|
+
...(effort ? { effort } : {}),
|
|
373
|
+
...(workerLaunch ? { workerManaged: true, workerLeaseId: workerLaunch.leaseId } : {}),
|
|
238
374
|
state: 'starting',
|
|
239
|
-
capabilities: {
|
|
375
|
+
capabilities: {
|
|
376
|
+
...transport.adapter.capabilities,
|
|
377
|
+
resumeAcrossWorkspaces: transport.adapter.capabilities.resumeAcrossWorkspaces === true &&
|
|
378
|
+
// A source policy pins exact paths in the launch digest. Relocation must
|
|
379
|
+
// not silently rewrite its grants while resuming the saved conversation.
|
|
380
|
+
!workerLaunch?.filesystemPolicy &&
|
|
381
|
+
!transport.adapter.workspaceResumeUnavailableReason?.(resolved.version),
|
|
382
|
+
},
|
|
240
383
|
};
|
|
241
384
|
const record = {
|
|
385
|
+
workerHistory: previous?.workerHistory ?? new NativeWorkerHistory(),
|
|
242
386
|
info,
|
|
243
387
|
events: previous?.events ?? [],
|
|
244
388
|
commands: previous?.commands ?? new Map(),
|
|
245
389
|
pendingRequests: new Map(),
|
|
246
|
-
context: executionContext(),
|
|
390
|
+
context: executionContext(workerLaunch, params.runner, profile),
|
|
247
391
|
sending: false,
|
|
248
392
|
};
|
|
249
393
|
this.sessions.set(info.id, record);
|
|
@@ -262,10 +406,13 @@ export class NativeSessionManager {
|
|
|
262
406
|
throw error;
|
|
263
407
|
}
|
|
264
408
|
try {
|
|
409
|
+
record.startupAbort = new AbortController();
|
|
265
410
|
record.startup = transport.adapter.start({
|
|
266
411
|
...params,
|
|
412
|
+
signal: record.startupAbort.signal,
|
|
267
413
|
mode,
|
|
268
414
|
model: info.model,
|
|
415
|
+
effort,
|
|
269
416
|
onSpawn: (pid, identity) => {
|
|
270
417
|
info.processPid = pid;
|
|
271
418
|
info.processIdentity = identity;
|
|
@@ -273,12 +420,22 @@ export class NativeSessionManager {
|
|
|
273
420
|
this.persist(record);
|
|
274
421
|
},
|
|
275
422
|
executable: resolved.executable,
|
|
276
|
-
env: nativeEnvironment(resolved.executable
|
|
423
|
+
env: nativeEnvironment(resolved.executable, {
|
|
424
|
+
...environment,
|
|
425
|
+
...(hostProtection ? { TMPDIR: hostProtection.temporaryDirectory } : {}),
|
|
426
|
+
}),
|
|
427
|
+
processSandbox: hostProtection?.sandbox,
|
|
428
|
+
...(workerLaunch
|
|
429
|
+
? { effort: workerLaunch.effort, safetyTier: workerLaunch.safetyTier, filesystemPolicy }
|
|
430
|
+
: {}),
|
|
277
431
|
}, (event) => this.append(record, event));
|
|
278
432
|
record.adapter = await record.startup;
|
|
279
433
|
info.nativeSessionId = record.adapter.nativeSessionId;
|
|
280
434
|
if (record.adapter.capabilities)
|
|
281
|
-
info.capabilities = {
|
|
435
|
+
info.capabilities = {
|
|
436
|
+
...record.adapter.capabilities,
|
|
437
|
+
resumeAcrossWorkspaces: info.capabilities.resumeAcrossWorkspaces,
|
|
438
|
+
};
|
|
282
439
|
if (info.state === 'starting')
|
|
283
440
|
info.state = 'idle';
|
|
284
441
|
this.persist(record);
|
|
@@ -290,29 +447,179 @@ export class NativeSessionManager {
|
|
|
290
447
|
throw error;
|
|
291
448
|
}
|
|
292
449
|
}
|
|
293
|
-
existingCreation(owner, params, reservedId) {
|
|
450
|
+
existingCreation(owner, params, reservedId, workerLaunch) {
|
|
294
451
|
const record = reservedId ? this.sessions.get(reservedId) : undefined;
|
|
295
452
|
if (!record)
|
|
296
453
|
return undefined;
|
|
297
454
|
if (record.info.ownerPrincipalId !== owner)
|
|
298
455
|
throw new Error('Reserved sessionId belongs to another owner');
|
|
299
|
-
|
|
456
|
+
if (record.info.workerLeaseId !== workerLaunch?.leaseId)
|
|
457
|
+
throw new Error('Reserved sessionId belongs to another worker task lease');
|
|
458
|
+
for (const field of ['runner', 'cwd', 'model', 'mode', 'profileId']) {
|
|
300
459
|
const requested = field === 'mode' ? (params.mode ?? 'default') : params[field];
|
|
301
460
|
if (record.info[field] !== requested)
|
|
302
461
|
throw new Error(`Reserved sessionId launch configuration differs: ${field}`);
|
|
303
462
|
}
|
|
304
|
-
if (
|
|
463
|
+
if (record.context.workerLaunchDigest !==
|
|
464
|
+
(workerLaunch ? nativeWorkerLaunchDigest(workerLaunch) : undefined))
|
|
465
|
+
throw new Error('Reserved sessionId launch configuration differs: worker launch configuration');
|
|
466
|
+
const profile = params.profileId
|
|
467
|
+
? requireNativeProfile(params.profileId, params.runner)
|
|
468
|
+
: undefined;
|
|
469
|
+
if (JSON.stringify(record.context) !==
|
|
470
|
+
JSON.stringify(executionContext(workerLaunch, params.runner, profile)))
|
|
305
471
|
throw new Error('Reserved sessionId launch configuration differs: account execution context');
|
|
306
472
|
return record;
|
|
307
473
|
}
|
|
308
474
|
async createdSession(record) {
|
|
309
475
|
if (record.reservationError)
|
|
310
476
|
throw record.reservationError;
|
|
477
|
+
if (record.workerLeaseError)
|
|
478
|
+
throw record.workerLeaseError;
|
|
311
479
|
if (record.info.state === 'starting' && record.startup)
|
|
312
480
|
await record.startup;
|
|
313
481
|
// Failed/closed reservations remain explicit. A retry cannot create a replacement process.
|
|
314
482
|
return this.snapshot(record);
|
|
315
483
|
}
|
|
484
|
+
assertWorkerLease(owner, id, generation, leaseId, closing = false) {
|
|
485
|
+
const record = this.owned(owner, id);
|
|
486
|
+
if (record.info.generation !== generation || record.info.workerLeaseId !== leaseId)
|
|
487
|
+
throw new Error('Native worker generation or task lease changed');
|
|
488
|
+
if (record.workerLeaseError && !closing)
|
|
489
|
+
throw record.workerLeaseError;
|
|
490
|
+
}
|
|
491
|
+
async closeWorker(owner, id, generation, leaseId) {
|
|
492
|
+
this.assertWorkerLease(owner, id, generation, leaseId, true);
|
|
493
|
+
const record = this.owned(owner, id);
|
|
494
|
+
await this.close(owner, id);
|
|
495
|
+
return this.snapshot(record);
|
|
496
|
+
}
|
|
497
|
+
assertWorkerNotCancelled(id) {
|
|
498
|
+
if (existsSync(join(this.root, 'cancelled-workers', `${id}.json`)))
|
|
499
|
+
throw new Error('Native worker reservation was cancelled before launch');
|
|
500
|
+
}
|
|
501
|
+
resumeCancellationPath(id, leaseId, commandId) {
|
|
502
|
+
if (!commandId.trim())
|
|
503
|
+
throw new Error('Native recovery command ID is required');
|
|
504
|
+
const key = createHash('sha256')
|
|
505
|
+
.update(JSON.stringify([id, leaseId, commandId]))
|
|
506
|
+
.digest('hex');
|
|
507
|
+
return join(this.root, 'cancelled-resumes', `${key}.json`);
|
|
508
|
+
}
|
|
509
|
+
async cancelWorker(owner, id, leaseId, generation, sourceLeaseId, resumeCommandId) {
|
|
510
|
+
if (![id, leaseId].every((value) => /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(value)))
|
|
511
|
+
throw new Error('Native worker reservation and lease must be lowercase UUIDs');
|
|
512
|
+
const root = join(this.root, 'cancelled-workers');
|
|
513
|
+
privateDirectory(root);
|
|
514
|
+
const markerPath = join(root, `${id}.json`);
|
|
515
|
+
const marker = existsSync(markerPath)
|
|
516
|
+
? readJson(markerPath)
|
|
517
|
+
: undefined;
|
|
518
|
+
const record = this.sessions.get(id);
|
|
519
|
+
if (resumeCommandId !== undefined) {
|
|
520
|
+
if (!generation)
|
|
521
|
+
throw new Error('Native recovery cancellation requires its source generation');
|
|
522
|
+
if (!record ||
|
|
523
|
+
record.info.ownerPrincipalId !== owner ||
|
|
524
|
+
record.info.workerLeaseId !== leaseId)
|
|
525
|
+
throw new Error('Native recovery cancellation ownership changed');
|
|
526
|
+
// Keep every operation fence. A later explicit recovery uses a different ID,
|
|
527
|
+
// but must never make an older delayed request launchable again.
|
|
528
|
+
privateDirectory(join(this.root, 'cancelled-resumes'));
|
|
529
|
+
durableWrite(this.resumeCancellationPath(id, leaseId, resumeCommandId), { owner });
|
|
530
|
+
if (record.info.generation !== generation)
|
|
531
|
+
return {
|
|
532
|
+
cancelled: false,
|
|
533
|
+
reason: 'generation-changed',
|
|
534
|
+
sessionId: id,
|
|
535
|
+
leaseId,
|
|
536
|
+
generation: record.info.generation,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
if (sourceLeaseId !== undefined) {
|
|
540
|
+
if (!generation ||
|
|
541
|
+
!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/.test(sourceLeaseId) ||
|
|
542
|
+
sourceLeaseId === leaseId)
|
|
543
|
+
throw new Error('Native handoff cancellation requires an exact source lease and generation');
|
|
544
|
+
if (record?.info.workerLeaseId === sourceLeaseId) {
|
|
545
|
+
if (record.info.ownerPrincipalId !== owner || record.info.generation !== generation)
|
|
546
|
+
throw new Error('Native handoff cancellation ownership changed');
|
|
547
|
+
// Claim the reserved successor lease before stopping. A delayed transfer to
|
|
548
|
+
// that lease now observes the same closed record; stale source commands fail.
|
|
549
|
+
record.workerHistory.assertNewLease(leaseId);
|
|
550
|
+
record.info.workerLeaseId = leaseId;
|
|
551
|
+
try {
|
|
552
|
+
this.persist(record);
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
record.workerLeaseError = error;
|
|
556
|
+
throw error;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (marker &&
|
|
561
|
+
(marker.owner !== owner ||
|
|
562
|
+
(marker.leaseId !== leaseId && record?.info.workerLeaseId !== leaseId)))
|
|
563
|
+
throw new Error('Native worker cancellation belongs to another owner or task lease');
|
|
564
|
+
// A replay of an earlier cancel cannot stop an explicitly resumed generation.
|
|
565
|
+
if (marker?.leaseId === leaseId &&
|
|
566
|
+
record &&
|
|
567
|
+
record.info.generation !== (generation ?? marker.generation))
|
|
568
|
+
return {
|
|
569
|
+
cancelled: true,
|
|
570
|
+
sessionId: id,
|
|
571
|
+
leaseId,
|
|
572
|
+
generation: generation ?? marker.generation,
|
|
573
|
+
};
|
|
574
|
+
if (record) {
|
|
575
|
+
if (record.info.ownerPrincipalId !== owner ||
|
|
576
|
+
record.info.workerLeaseId !== leaseId ||
|
|
577
|
+
(generation !== undefined && record.info.generation !== generation))
|
|
578
|
+
throw new Error('Native worker cancellation ownership changed');
|
|
579
|
+
}
|
|
580
|
+
else if (generation && !marker) {
|
|
581
|
+
throw new Error('Known native worker session is unavailable; stop cannot be confirmed');
|
|
582
|
+
}
|
|
583
|
+
const cancelledGeneration = record?.info.generation ?? generation ?? marker?.generation;
|
|
584
|
+
// This durable tombstone also catches ensure calls still awaiting executable probes.
|
|
585
|
+
durableWrite(markerPath, { owner, leaseId, generation: cancelledGeneration });
|
|
586
|
+
if (record)
|
|
587
|
+
await this.close(owner, id);
|
|
588
|
+
return {
|
|
589
|
+
cancelled: true,
|
|
590
|
+
sessionId: id,
|
|
591
|
+
leaseId,
|
|
592
|
+
generation: cancelledGeneration,
|
|
593
|
+
...(record ? { session: this.snapshot(record) } : {}),
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
transferWorker(owner, id, generation, leaseId, launch) {
|
|
597
|
+
const record = this.owned(owner, id);
|
|
598
|
+
if (record.workerLeaseError)
|
|
599
|
+
throw record.workerLeaseError;
|
|
600
|
+
if (record.info.generation !== generation ||
|
|
601
|
+
record.context.workerLaunchDigest !== nativeWorkerLaunchDigest(launch))
|
|
602
|
+
throw new Error('Native worker handoff must preserve generation and launch configuration');
|
|
603
|
+
if (record.info.workerLeaseId === launch.leaseId)
|
|
604
|
+
return this.snapshot(record);
|
|
605
|
+
this.assertWorkerLease(owner, id, generation, leaseId);
|
|
606
|
+
const stopped = ['closed', 'failed'].includes(record.info.state) &&
|
|
607
|
+
record.info.processStopped &&
|
|
608
|
+
Boolean(record.info.nativeSessionId);
|
|
609
|
+
if ((record.info.state !== 'idle' && !stopped) || record.sending || record.pendingRequests.size)
|
|
610
|
+
throw new Error('Native worker handoff requires an idle session or a confirmed stopped session with no pending requests');
|
|
611
|
+
record.workerHistory.assertNewLease(launch.leaseId);
|
|
612
|
+
record.info.workerLeaseId = launch.leaseId;
|
|
613
|
+
try {
|
|
614
|
+
this.persist(record);
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
// The write may have landed. Neither lease may submit more worker input until recovery.
|
|
618
|
+
record.workerLeaseError = error;
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
return this.snapshot(record);
|
|
622
|
+
}
|
|
316
623
|
append(record, event) {
|
|
317
624
|
const enriched = {
|
|
318
625
|
...event,
|
|
@@ -420,8 +727,27 @@ export class NativeSessionManager {
|
|
|
420
727
|
pendingRequests: [...record.pendingRequests.values()],
|
|
421
728
|
};
|
|
422
729
|
}
|
|
730
|
+
readWorker(owner, id, leaseId, after, limit) {
|
|
731
|
+
const record = this.owned(owner, id);
|
|
732
|
+
if (record.workerLeaseError)
|
|
733
|
+
throw record.workerLeaseError;
|
|
734
|
+
const result = record.workerHistory.read(leaseId, record.events, [...record.pendingRequests.values()], after, limit);
|
|
735
|
+
return { ...result, session: this.snapshot({ info: result.session }) };
|
|
736
|
+
}
|
|
737
|
+
assertAccountCurrent(record) {
|
|
738
|
+
if (!record.info.profileId)
|
|
739
|
+
return;
|
|
740
|
+
assertNativeProfileCurrent({
|
|
741
|
+
id: record.info.profileId,
|
|
742
|
+
runner: record.info.runner,
|
|
743
|
+
directory: record.context.profileDirectory,
|
|
744
|
+
accountContextId: record.info.accountContextId,
|
|
745
|
+
state: 'active',
|
|
746
|
+
});
|
|
747
|
+
}
|
|
423
748
|
async send(owner, id, commandId, text) {
|
|
424
749
|
const record = this.owned(owner, id);
|
|
750
|
+
this.assertAccountCurrent(record);
|
|
425
751
|
const existing = record.commands.get(commandId);
|
|
426
752
|
if (existing) {
|
|
427
753
|
if (existing.text !== text)
|
|
@@ -472,6 +798,7 @@ export class NativeSessionManager {
|
|
|
472
798
|
}
|
|
473
799
|
async respond(owner, id, requestId, response) {
|
|
474
800
|
const record = this.owned(owner, id);
|
|
801
|
+
this.assertAccountCurrent(record);
|
|
475
802
|
const pending = record.pendingRequests.get(requestId);
|
|
476
803
|
if (!record.adapter || !pending || pending.generation !== record.info.generation)
|
|
477
804
|
throw new Error('Native request is stale or belongs to another session');
|
|
@@ -501,6 +828,19 @@ export class NativeSessionManager {
|
|
|
501
828
|
}
|
|
502
829
|
record.info.state = 'closing';
|
|
503
830
|
this.persist(record);
|
|
831
|
+
if (!record.adapter && record.startup) {
|
|
832
|
+
record.startupAbort?.abort();
|
|
833
|
+
try {
|
|
834
|
+
await record.startup;
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
// Startup rejection is expected after cancellation only when the process
|
|
838
|
+
// adapter has independently confirmed that its entire owned tree stopped.
|
|
839
|
+
if (record.info.processStopped && ['closed', 'failed'].includes(record.info.state))
|
|
840
|
+
return;
|
|
841
|
+
throw error;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
504
844
|
const adapter = record.adapter ?? (await record.startup);
|
|
505
845
|
await adapter?.close();
|
|
506
846
|
if (!['closed', 'failed'].includes(this.owned(owner, id).info.state))
|