@myagentroam/node 0.10.66 → 0.10.67
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/dist/connector.js +15 -0
- package/dist/main.js +27 -7
- package/dist/node-process-lock.js +43 -0
- package/dist/runner-command-engine.js +7 -2
- package/dist/runtime-state.js +34 -13
- package/dist/service/continuous-run-coordinator.js +165 -13
- package/dist/service/external-session-resume-service.js +9 -2
- package/dist/service/restart-session-restorer.js +1 -1
- package/dist/service/session-catalog-service.js +7 -6
- package/dist/service/session-identity-service.js +35 -14
- package/dist/service/session-lifecycle-service.js +6 -2
- package/dist/service/session-message-service.js +15 -4
- package/dist/service/workspace-queue-workbench-service.js +11 -4
- package/dist/supervisor.js +5 -0
- package/dist/workspace.js +44 -1
- package/package.json +4 -4
package/dist/connector.js
CHANGED
|
@@ -7,6 +7,7 @@ import { createEnvelope, directNodeCommandSchema } from '@myagentroam/protocol';
|
|
|
7
7
|
import { loadNodeConfig, computerUseRuntimeExecutable, directTransferUdpPortRange, nodeDataDirectory, nodeDatabasePath } from './config.js';
|
|
8
8
|
import { NodeMetrics, nodeLog } from './operational.js';
|
|
9
9
|
import { assertLocalSqlitePath } from './storage.js';
|
|
10
|
+
import { resolveWorkspaceExecutionDirectory } from './workspace.js';
|
|
10
11
|
import { detectCapabilitiesAsync, unavailableCapabilities } from './capabilities.js';
|
|
11
12
|
import { NodeDatabase } from './database.js';
|
|
12
13
|
import { TerminalManager } from './terminal.js';
|
|
@@ -734,7 +735,17 @@ export class NodeConnector {
|
|
|
734
735
|
discover: (runner, workspaceId) => this.nativeSessionProjectionService.discover(runner, workspaceId),
|
|
735
736
|
workspaceIdForCwd: (cwd) => this.database?.listWorkspaces().find((candidate) => candidate.path === cwd)?.id
|
|
736
737
|
});
|
|
738
|
+
const resolveExecutionDirectory = async (session) => {
|
|
739
|
+
const workspace = this.database?.getWorkspace(session.workspaceId);
|
|
740
|
+
if (workspace === undefined)
|
|
741
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
742
|
+
const directory = await resolveWorkspaceExecutionDirectory(workspace.path, session.cwd);
|
|
743
|
+
if (this.database?.getWorkspace(workspace.id)?.path !== workspace.path)
|
|
744
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
745
|
+
return directory;
|
|
746
|
+
};
|
|
737
747
|
this.continuousRunCoordinator = new ContinuousRunCoordinator({
|
|
748
|
+
resolveExecutionDirectory,
|
|
738
749
|
runtime: this.runtime,
|
|
739
750
|
steer: (prepared, item, executionId) => {
|
|
740
751
|
const runner = this.runners.optional(prepared.runner);
|
|
@@ -868,6 +879,7 @@ export class NodeConnector {
|
|
|
868
879
|
: { runnerTitle: session.runnerTitle.slice(0, 160) })
|
|
869
880
|
});
|
|
870
881
|
},
|
|
882
|
+
validateCommands: (previousId, nextId) => this.commandStates.validateSessionIdentityPromotion(previousId, nextId),
|
|
871
883
|
migrateCommands: (previousId, nextId) => this.commandStates.promoteSessionIdentity(previousId, nextId),
|
|
872
884
|
migrateGoal: (previousId, nextId) => {
|
|
873
885
|
this.runAttachmentService.migrateSession(previousId, nextId);
|
|
@@ -905,6 +917,7 @@ export class NodeConnector {
|
|
|
905
917
|
interceptedSessionId: (clientMessageId) => this.fakeRunner === undefined ? undefined : `fake:${clientMessageId}`
|
|
906
918
|
});
|
|
907
919
|
this.externalSessionResumeService = new ExternalSessionResumeService({
|
|
920
|
+
resolveExecutionDirectory,
|
|
908
921
|
runtime: this.runtime,
|
|
909
922
|
runners: this.runners,
|
|
910
923
|
state: this.sessionState,
|
|
@@ -939,6 +952,7 @@ export class NodeConnector {
|
|
|
939
952
|
clearGoal: (session) => this.goalCoordinator.clear(session)
|
|
940
953
|
});
|
|
941
954
|
this.sessionMessageService = new SessionMessageService({
|
|
955
|
+
resolveExecutionDirectory,
|
|
942
956
|
runtime: this.runtime,
|
|
943
957
|
uploads: this.workspaceUploadService,
|
|
944
958
|
attachments: this.runAttachmentService,
|
|
@@ -986,6 +1000,7 @@ export class NodeConnector {
|
|
|
986
1000
|
diagnose: (code, sessionId) => nodeLog('node.restart.diagnostic', { code, sessionId })
|
|
987
1001
|
});
|
|
988
1002
|
this.sessionCatalogService = new SessionCatalogService({
|
|
1003
|
+
resolveExecutionDirectory,
|
|
989
1004
|
database: () => {
|
|
990
1005
|
if (this.database === undefined)
|
|
991
1006
|
throw new Error('NODE_DATABASE_UNAVAILABLE');
|
package/dist/main.js
CHANGED
|
@@ -8,6 +8,8 @@ import { browserCachePath, removeServiceDefinition, writeServiceDefinition } fro
|
|
|
8
8
|
import { superviseNode } from './supervisor.js';
|
|
9
9
|
import { configureNodeLog } from './rotating-log.js';
|
|
10
10
|
import { nodeLog } from './operational.js';
|
|
11
|
+
import { acquireNodeProcessLock, NodeProcessOwnerConflictError, NODE_OWNER_CONFLICT_EXIT_CODE } from './node-process-lock.js';
|
|
12
|
+
import { safeErrorCode } from './util/safe-error.js';
|
|
11
13
|
const [command = 'status', ...args] = process.argv.slice(2);
|
|
12
14
|
const config = readOption(args, '--config') ?? nodeConfigPath();
|
|
13
15
|
const output = readOption(args, '--output') ?? defaultServicePath();
|
|
@@ -15,11 +17,22 @@ const browserPath = readOption(args, '--browser-cache');
|
|
|
15
17
|
if (browserPath !== undefined)
|
|
16
18
|
process.env['PLAYWRIGHT_BROWSERS_PATH'] = browserPath;
|
|
17
19
|
if (command === 'run') {
|
|
20
|
+
const nodeConfig = await loadNodeConfig(config);
|
|
21
|
+
let releaseOwner;
|
|
22
|
+
try {
|
|
23
|
+
releaseOwner = await acquireNodeProcessLock(nodeDataDirectory(nodeConfig, config));
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (!(error instanceof NodeProcessOwnerConflictError))
|
|
27
|
+
throw error;
|
|
28
|
+
console.error(`${error.code}: ${error.message}`);
|
|
29
|
+
process.exit(NODE_OWNER_CONFLICT_EXIT_CODE);
|
|
30
|
+
}
|
|
18
31
|
const { NodeConnector } = await import('./connector.js');
|
|
19
|
-
configureNodeLog(nodeDataDirectory(
|
|
20
|
-
const connector = new NodeConnector({ configPath: config });
|
|
32
|
+
configureNodeLog(nodeDataDirectory(nodeConfig, config));
|
|
33
|
+
const connector = new NodeConnector({ configPath: config, config: nodeConfig });
|
|
21
34
|
let shuttingDown = false;
|
|
22
|
-
const shutdown = () => {
|
|
35
|
+
const shutdown = (handoff = true, exitCode = 0) => {
|
|
23
36
|
if (shuttingDown)
|
|
24
37
|
return;
|
|
25
38
|
shuttingDown = true;
|
|
@@ -27,9 +40,10 @@ if (command === 'run') {
|
|
|
27
40
|
nodeLog('node.restart.diagnostic', { code: 'RESTART_HANDOFF_SHUTDOWN_TIMEOUT' });
|
|
28
41
|
process.exit(1);
|
|
29
42
|
}, 25_000);
|
|
30
|
-
void connector.stop({ handoff
|
|
43
|
+
void connector.stop({ handoff }).then(() => {
|
|
31
44
|
clearTimeout(deadline);
|
|
32
|
-
|
|
45
|
+
releaseOwner();
|
|
46
|
+
process.exit(exitCode);
|
|
33
47
|
}, () => {
|
|
34
48
|
clearTimeout(deadline);
|
|
35
49
|
nodeLog('node.restart.diagnostic', { code: 'RESTART_HANDOFF_SHUTDOWN_FAILED' });
|
|
@@ -37,9 +51,15 @@ if (command === 'run') {
|
|
|
37
51
|
});
|
|
38
52
|
};
|
|
39
53
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
40
|
-
process.on(signal, shutdown);
|
|
54
|
+
process.on(signal, () => shutdown());
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await connector.start();
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
nodeLog('node.start.failed', { code: safeErrorCode(error, 'NODE_START_FAILED') });
|
|
61
|
+
shutdown(false, 1);
|
|
41
62
|
}
|
|
42
|
-
await connector.start();
|
|
43
63
|
}
|
|
44
64
|
else if (command === 'supervise') {
|
|
45
65
|
if (process.argv[1] === undefined)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdir, realpath } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { assertLocalSqlitePath, isUnsupportedSqlitePath } from './storage.js';
|
|
5
|
+
export const NODE_OWNER_CONFLICT_EXIT_CODE = 76;
|
|
6
|
+
export class NodeProcessOwnerConflictError extends Error {
|
|
7
|
+
code = 'NODE_DATA_DIRECTORY_IN_USE';
|
|
8
|
+
constructor() {
|
|
9
|
+
super('Another Node process is using this data directory. Stop that instance before starting another.');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/** The independent transaction owns the directory, not the business database. */
|
|
13
|
+
export async function acquireNodeProcessLock(dataDirectory) {
|
|
14
|
+
if (isUnsupportedSqlitePath(dataDirectory))
|
|
15
|
+
throw new Error('SQLITE_NETWORK_FILESYSTEM_UNSUPPORTED');
|
|
16
|
+
await mkdir(dataDirectory, { recursive: true, mode: 0o700 });
|
|
17
|
+
const path = join(await realpath(dataDirectory), 'node-owner.sqlite');
|
|
18
|
+
await assertLocalSqlitePath(path);
|
|
19
|
+
let database;
|
|
20
|
+
try {
|
|
21
|
+
database = new DatabaseSync(path, { timeout: 0 });
|
|
22
|
+
database.exec('PRAGMA journal_mode = DELETE; BEGIN EXCLUSIVE;');
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
try {
|
|
26
|
+
database?.close();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* Preserve the acquisition error. */
|
|
30
|
+
}
|
|
31
|
+
if (error instanceof Error && 'errcode' in error && error.errcode === 5)
|
|
32
|
+
throw new NodeProcessOwnerConflictError();
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
const owner = database;
|
|
36
|
+
let released = false;
|
|
37
|
+
return () => {
|
|
38
|
+
if (released)
|
|
39
|
+
return;
|
|
40
|
+
owner.close();
|
|
41
|
+
released = true;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -54,14 +54,19 @@ export class CommandStateStore {
|
|
|
54
54
|
removeSession(sessionId) {
|
|
55
55
|
this.states.delete(sessionId);
|
|
56
56
|
}
|
|
57
|
+
validateSessionIdentityPromotion(sessionId, nativeSessionId) {
|
|
58
|
+
if (sessionId === nativeSessionId)
|
|
59
|
+
return;
|
|
60
|
+
if (this.states.has(sessionId) && this.states.has(nativeSessionId))
|
|
61
|
+
throw new Error('SESSION_ID_CONFLICT');
|
|
62
|
+
}
|
|
57
63
|
promoteSessionIdentity(sessionId, nativeSessionId) {
|
|
64
|
+
this.validateSessionIdentityPromotion(sessionId, nativeSessionId);
|
|
58
65
|
if (sessionId === nativeSessionId)
|
|
59
66
|
return;
|
|
60
67
|
const values = this.states.get(sessionId);
|
|
61
68
|
if (values === undefined)
|
|
62
69
|
return;
|
|
63
|
-
if (this.states.has(nativeSessionId))
|
|
64
|
-
throw new Error('SESSION_ID_CONFLICT');
|
|
65
70
|
this.states.delete(sessionId);
|
|
66
71
|
this.states.set(nativeSessionId, values);
|
|
67
72
|
}
|
package/dist/runtime-state.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { nodeLog } from './operational.js';
|
|
3
|
+
import { safeErrorCode } from './util/safe-error.js';
|
|
2
4
|
import { permissionStopSchema } from '@myagentroam/protocol';
|
|
3
5
|
/** User inputs arrive with a durable Session identity, unlike execution-local output IDs. */
|
|
4
6
|
export function runConversationItemId(runId, itemId, kind) {
|
|
@@ -206,22 +208,25 @@ export class NodeRuntimeState {
|
|
|
206
208
|
throw new Error('SESSION_EXTERNAL_ID_INVALID');
|
|
207
209
|
this.promoteSessionIdentity(id, externalSessionId);
|
|
208
210
|
}
|
|
211
|
+
validateSessionIdentityPromotion(id, nativeSessionId) {
|
|
212
|
+
if (nativeSessionId.length === 0)
|
|
213
|
+
throw new Error('SESSION_EXTERNAL_ID_INVALID');
|
|
214
|
+
const current = this.requireSession(id);
|
|
215
|
+
if (id !== nativeSessionId && this.sessions.has(nativeSessionId))
|
|
216
|
+
throw new Error('SESSION_ID_CONFLICT');
|
|
217
|
+
return current;
|
|
218
|
+
}
|
|
209
219
|
/**
|
|
210
220
|
* Atomically replaces the bootstrap Session key with the Runner-native ID.
|
|
211
221
|
* No caller may migrate the individual runtime maps independently.
|
|
212
222
|
*/
|
|
213
|
-
promoteSessionIdentity(id, nativeSessionId) {
|
|
214
|
-
|
|
215
|
-
throw new Error('SESSION_EXTERNAL_ID_INVALID');
|
|
216
|
-
const current = this.requireSession(id);
|
|
223
|
+
promoteSessionIdentity(id, nativeSessionId, options = {}) {
|
|
224
|
+
const current = this.validateSessionIdentityPromotion(id, nativeSessionId);
|
|
217
225
|
if (id === nativeSessionId)
|
|
218
226
|
return this.replaceSession(id, {
|
|
219
227
|
externalSessionId: nativeSessionId,
|
|
220
228
|
nativeControl: 'MAR_MANAGED'
|
|
221
229
|
});
|
|
222
|
-
const collision = this.sessions.get(nativeSessionId);
|
|
223
|
-
if (collision !== undefined && collision.id !== current.id)
|
|
224
|
-
throw new Error('SESSION_ID_CONFLICT');
|
|
225
230
|
const next = {
|
|
226
231
|
...current,
|
|
227
232
|
id: nativeSessionId,
|
|
@@ -286,12 +291,27 @@ export class NodeRuntimeState {
|
|
|
286
291
|
...reference,
|
|
287
292
|
sessionId: nativeSessionId
|
|
288
293
|
});
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
this.emitRunChanged(promoted, next);
|
|
292
|
-
}
|
|
294
|
+
if (options.deferRunChanges !== true)
|
|
295
|
+
this.publishSessionIdentity(nativeSessionId);
|
|
293
296
|
return next;
|
|
294
297
|
}
|
|
298
|
+
/** Identity observers run only after the owning identity commit is complete. */
|
|
299
|
+
publishSessionIdentity(sessionId) {
|
|
300
|
+
const session = this.requireSession(sessionId);
|
|
301
|
+
for (const run of this.listRunsForSession(sessionId)) {
|
|
302
|
+
try {
|
|
303
|
+
this.emitRunChanged(run, session);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
nodeLog('session.identity.publish.failed', {
|
|
307
|
+
sessionId,
|
|
308
|
+
runId: run.id,
|
|
309
|
+
phase: 'run.observer',
|
|
310
|
+
errorCode: safeErrorCode(error, 'SESSION_IDENTITY_PUBLICATION_FAILED')
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
295
315
|
setChannelToken(id, channelToken) {
|
|
296
316
|
if (channelToken.length < 16)
|
|
297
317
|
throw new Error('SESSION_CHANNEL_TOKEN_INVALID');
|
|
@@ -528,6 +548,7 @@ export class NodeRuntimeState {
|
|
|
528
548
|
}
|
|
529
549
|
startRunFromQueueItem(itemId, content) {
|
|
530
550
|
const item = this.requireQueueItem(itemId);
|
|
551
|
+
this.assertSessionHistoryMutable(item.sessionId);
|
|
531
552
|
if (this.activeSessionRun(item.sessionId) !== undefined)
|
|
532
553
|
throw new Error('RUN_ALREADY_ACTIVE');
|
|
533
554
|
const queue = this.queuedItems.get(item.sessionId) ?? [];
|
|
@@ -1081,12 +1102,12 @@ export class NodeRuntimeState {
|
|
|
1081
1102
|
if (runIds.has(approval.runId) || executionIds.has(approval.runId))
|
|
1082
1103
|
this.approvals.delete(approvalId);
|
|
1083
1104
|
}
|
|
1084
|
-
beginSessionHistoryMutation(sessionId) {
|
|
1105
|
+
beginSessionHistoryMutation(sessionId, options) {
|
|
1085
1106
|
if (this.sessionHistoryMutations.has(sessionId))
|
|
1086
1107
|
throw new Error('SESSION_ACTIVE');
|
|
1087
1108
|
if (this.activeSessionRun(sessionId) !== undefined)
|
|
1088
1109
|
throw new Error('SESSION_ACTIVE');
|
|
1089
|
-
if (this.sessionQueue(sessionId).itemIds.length > 0)
|
|
1110
|
+
if (options?.allowQueued !== true && this.sessionQueue(sessionId).itemIds.length > 0)
|
|
1090
1111
|
throw new Error('SESSION_QUEUE_NOT_EMPTY');
|
|
1091
1112
|
this.sessionHistoryMutations.add(sessionId);
|
|
1092
1113
|
let released = false;
|
|
@@ -110,6 +110,25 @@ export class ContinuousRunCoordinator {
|
|
|
110
110
|
this.options.emitRun(run.id, 'run.rejected', { code }, 'FAILED');
|
|
111
111
|
}
|
|
112
112
|
settle(input) {
|
|
113
|
+
const pending = this.preparedInputs.get(input.submissionId);
|
|
114
|
+
if (this.options.resolveExecutionDirectory &&
|
|
115
|
+
pending &&
|
|
116
|
+
this.options.runtime.getQueueItem(input.submissionId)) {
|
|
117
|
+
const version = this.options.runtime.sessionQueue(pending.sessionId).version;
|
|
118
|
+
return this.directoryForInput(input.submissionId, pending).then((cwd) => {
|
|
119
|
+
if (cwd === undefined)
|
|
120
|
+
return 'QUEUED';
|
|
121
|
+
if (!this.inputCurrent(input.submissionId, pending) ||
|
|
122
|
+
this.options.runtime.sessionQueue(pending.sessionId).version !== version) {
|
|
123
|
+
this.schedule(pending.sessionId);
|
|
124
|
+
return 'QUEUED';
|
|
125
|
+
}
|
|
126
|
+
return this.settleReady(input);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return this.settleReady(input);
|
|
130
|
+
}
|
|
131
|
+
settleReady(input) {
|
|
113
132
|
const item = this.options.runtime.getQueueItem(input.submissionId);
|
|
114
133
|
const active = item && this.options.runtime.activeSessionRun(item.sessionId);
|
|
115
134
|
const turn = active && this.options.runtime.conversationTurnForRun(active.id);
|
|
@@ -192,8 +211,7 @@ export class ContinuousRunCoordinator {
|
|
|
192
211
|
if (active === undefined && item !== undefined) {
|
|
193
212
|
if (input.queueBefore.paused && intent === 'SEND')
|
|
194
213
|
this.options.runtime.prioritizeQueueItem(item.id);
|
|
195
|
-
this.startIdleQueue(sessionId);
|
|
196
|
-
return 'STARTED';
|
|
214
|
+
return this.startIdleQueue(sessionId).then((started) => started === item.id ? 'STARTED' : 'QUEUED');
|
|
197
215
|
}
|
|
198
216
|
if (executionEnded && item !== undefined) {
|
|
199
217
|
if (input.queueBefore.paused && intent === 'SEND') {
|
|
@@ -208,6 +226,20 @@ export class ContinuousRunCoordinator {
|
|
|
208
226
|
return 'QUEUED';
|
|
209
227
|
}
|
|
210
228
|
executeQueueItem(itemId) {
|
|
229
|
+
const pending = this.preparedInputs.get(itemId);
|
|
230
|
+
if (this.options.resolveExecutionDirectory && pending) {
|
|
231
|
+
const version = this.options.runtime.sessionQueue(pending.sessionId).version;
|
|
232
|
+
return this.directoryForInput(itemId, pending, true).then((cwd) => {
|
|
233
|
+
if (cwd === undefined || !this.inputCurrent(itemId, pending))
|
|
234
|
+
throw new Error('QUEUE_ITEM_INVALID');
|
|
235
|
+
if (this.options.runtime.sessionQueue(pending.sessionId).version !== version)
|
|
236
|
+
throw new Error('QUEUE_VERSION_CONFLICT');
|
|
237
|
+
return this.executeQueueItemReady(itemId);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return this.executeQueueItemReady(itemId);
|
|
241
|
+
}
|
|
242
|
+
executeQueueItemReady(itemId) {
|
|
211
243
|
const item = this.requirePreparedItem(itemId);
|
|
212
244
|
const active = this.options.runtime.activeSessionRun(item.sessionId);
|
|
213
245
|
if (active) {
|
|
@@ -301,7 +333,7 @@ export class ContinuousRunCoordinator {
|
|
|
301
333
|
this.steerInFlight.set(itemId, { sessionId: item.sessionId, task });
|
|
302
334
|
return task;
|
|
303
335
|
}
|
|
304
|
-
executeQueueItemFallback(itemId) {
|
|
336
|
+
async executeQueueItemFallback(itemId) {
|
|
305
337
|
const item = this.requirePreparedItem(itemId);
|
|
306
338
|
const active = this.options.runtime.activeSessionRun(item.sessionId);
|
|
307
339
|
if (active !== undefined && this.canSteer(active))
|
|
@@ -319,7 +351,7 @@ export class ContinuousRunCoordinator {
|
|
|
319
351
|
}
|
|
320
352
|
return { run: active, delivery: 'ACCEPTED' };
|
|
321
353
|
}
|
|
322
|
-
this.startIdleQueue(item.sessionId);
|
|
354
|
+
await this.startIdleQueue(item.sessionId, true);
|
|
323
355
|
const started = this.options.runtime.activeSessionRun(item.sessionId);
|
|
324
356
|
if (started === undefined)
|
|
325
357
|
throw new Error('RUN_NOT_FOUND');
|
|
@@ -408,9 +440,29 @@ export class ContinuousRunCoordinator {
|
|
|
408
440
|
this.options.markInterrupted(executionId);
|
|
409
441
|
this.options.control('run.interrupt', { runId: executionId });
|
|
410
442
|
}
|
|
411
|
-
resume(sessionId) {
|
|
443
|
+
async resume(sessionId) {
|
|
444
|
+
const session = this.options.runtime.getAgentSession(sessionId);
|
|
445
|
+
if (!session)
|
|
446
|
+
throw new Error('SESSION_NOT_FOUND');
|
|
447
|
+
const version = this.options.runtime.sessionQueue(sessionId).version;
|
|
448
|
+
if (this.options.resolveExecutionDirectory) {
|
|
449
|
+
try {
|
|
450
|
+
await this.options.resolveExecutionDirectory(session);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
if (this.options.runtime.getAgentSession(sessionId)?.id === session.id &&
|
|
454
|
+
this.options.runtime.sessionQueue(sessionId).version === version)
|
|
455
|
+
this.pauseForDirectory(sessionId, error);
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
if (this.options.runtime.getAgentSession(sessionId)?.id !== session.id ||
|
|
459
|
+
this.options.runtime.sessionQueue(sessionId).version !== version)
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
412
462
|
this.options.runtime.resumeSessionQueue(sessionId);
|
|
413
|
-
this.
|
|
463
|
+
if (this.suspended || this.options.stopped())
|
|
464
|
+
return;
|
|
465
|
+
await this.startIdleQueue(sessionId, true);
|
|
414
466
|
}
|
|
415
467
|
schedule(sessionId, delayMs = 0) {
|
|
416
468
|
if (this.suspended)
|
|
@@ -420,7 +472,10 @@ export class ContinuousRunCoordinator {
|
|
|
420
472
|
const timer = setTimeout(() => {
|
|
421
473
|
this.scheduledSessions.delete(sessionId);
|
|
422
474
|
if (!this.options.stopped())
|
|
423
|
-
void this.dispatch(sessionId)
|
|
475
|
+
void this.dispatch(sessionId).catch((error) => nodeLog('queue.dispatch.failed', {
|
|
476
|
+
sessionId,
|
|
477
|
+
code: safeErrorCode(error, 'QUEUE_DISPATCH_FAILED')
|
|
478
|
+
}));
|
|
424
479
|
}, delayMs);
|
|
425
480
|
this.scheduledSessions.set(sessionId, timer);
|
|
426
481
|
timer.unref();
|
|
@@ -438,14 +493,26 @@ export class ContinuousRunCoordinator {
|
|
|
438
493
|
if ([...this.steerInFlight.values()].some((entry) => entry.sessionId === sessionId))
|
|
439
494
|
return;
|
|
440
495
|
const active = this.options.runtime.activeSessionRun(sessionId);
|
|
441
|
-
if (active === undefined)
|
|
496
|
+
if (active === undefined) {
|
|
497
|
+
await this.startIdleQueue(sessionId);
|
|
442
498
|
return;
|
|
499
|
+
}
|
|
443
500
|
let executionId = this.options.runtime.currentExecutionId(active.id);
|
|
444
501
|
if (executionId !== undefined && this.dispatchedExecutions.has(executionId))
|
|
445
502
|
return;
|
|
446
503
|
let pending = executionId === undefined
|
|
447
504
|
? this.preparedInputs.get(active.id)
|
|
448
505
|
: this.preparedInputs.get(executionId);
|
|
506
|
+
let executionCwd = pending?.cwd;
|
|
507
|
+
if (pending && this.options.resolveExecutionDirectory) {
|
|
508
|
+
const key = executionId ?? active.id;
|
|
509
|
+
executionCwd = await this.directoryForInput(key, pending);
|
|
510
|
+
if (executionCwd === undefined ||
|
|
511
|
+
!this.inputCurrent(key, pending) ||
|
|
512
|
+
this.options.runtime.activeSessionRun(sessionId)?.id !== active.id ||
|
|
513
|
+
this.options.runtime.currentExecutionId(active.id) !== executionId)
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
449
516
|
if (executionId === undefined && pending === undefined) {
|
|
450
517
|
const itemId = this.nextQueueItem(sessionId);
|
|
451
518
|
if (itemId === undefined)
|
|
@@ -456,6 +523,20 @@ export class ContinuousRunCoordinator {
|
|
|
456
523
|
const item = this.options.runtime.getQueueItem(itemId);
|
|
457
524
|
if (item === undefined)
|
|
458
525
|
return;
|
|
526
|
+
if (this.options.resolveExecutionDirectory) {
|
|
527
|
+
const version = this.options.runtime.sessionQueue(sessionId).version;
|
|
528
|
+
executionCwd = await this.directoryForInput(itemId, pending);
|
|
529
|
+
if (executionCwd === undefined)
|
|
530
|
+
return;
|
|
531
|
+
if (!this.inputCurrent(itemId, pending) ||
|
|
532
|
+
this.options.runtime.activeSessionRun(sessionId)?.id !== active.id ||
|
|
533
|
+
this.options.runtime.currentExecutionId(active.id) !== undefined ||
|
|
534
|
+
this.options.runtime.sessionQueue(sessionId).version !== version ||
|
|
535
|
+
this.nextQueueItem(sessionId) !== itemId) {
|
|
536
|
+
this.schedule(sessionId);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
459
540
|
if (!this.goalCurrent(pending)) {
|
|
460
541
|
this.deleteQueueItem(item.id);
|
|
461
542
|
this.schedule(sessionId);
|
|
@@ -501,7 +582,11 @@ export class ContinuousRunCoordinator {
|
|
|
501
582
|
let dispatched = false;
|
|
502
583
|
try {
|
|
503
584
|
// Capabilities belong to this Execution, never the Run or a claimed QueueItem.
|
|
504
|
-
const executionInput = {
|
|
585
|
+
const executionInput = {
|
|
586
|
+
...pending,
|
|
587
|
+
cwd: executionCwd ?? pending.cwd,
|
|
588
|
+
runId: executionId
|
|
589
|
+
};
|
|
505
590
|
const goalMcp = pending.goal === undefined ? undefined : await this.options.goalMcp?.(executionInput);
|
|
506
591
|
const scheduleMcp = await this.options.scheduleMcp?.(executionInput);
|
|
507
592
|
const notificationMcp = await this.options.notificationMcp?.(executionInput);
|
|
@@ -524,7 +609,7 @@ export class ContinuousRunCoordinator {
|
|
|
524
609
|
? {}
|
|
525
610
|
: { clientMessageId: pending.clientMessageId }),
|
|
526
611
|
...(pending.attachments.length === 0 ? {} : { attachments: pending.attachments }),
|
|
527
|
-
cwd: pending.cwd,
|
|
612
|
+
cwd: executionCwd ?? pending.cwd,
|
|
528
613
|
...(pending.externalSessionId === null
|
|
529
614
|
? {}
|
|
530
615
|
: { externalSessionId: pending.externalSessionId }),
|
|
@@ -638,7 +723,7 @@ export class ContinuousRunCoordinator {
|
|
|
638
723
|
clearTimeout(timer);
|
|
639
724
|
this.scheduledSessions.clear();
|
|
640
725
|
}
|
|
641
|
-
startIdleQueue(sessionId) {
|
|
726
|
+
async startIdleQueue(sessionId, throwOnError = false) {
|
|
642
727
|
if (this.options.runtime.activeSessionRun(sessionId) !== undefined) {
|
|
643
728
|
this.schedule(sessionId);
|
|
644
729
|
return;
|
|
@@ -654,8 +739,20 @@ export class ContinuousRunCoordinator {
|
|
|
654
739
|
throw new Error('QUEUE_ITEM_INVALID');
|
|
655
740
|
if (!this.goalCurrent(pending)) {
|
|
656
741
|
this.deleteQueueItem(item.id);
|
|
657
|
-
this.startIdleQueue(sessionId);
|
|
658
|
-
|
|
742
|
+
return this.startIdleQueue(sessionId, throwOnError);
|
|
743
|
+
}
|
|
744
|
+
if (this.options.resolveExecutionDirectory) {
|
|
745
|
+
const version = this.options.runtime.sessionQueue(sessionId).version;
|
|
746
|
+
const cwd = await this.directoryForInput(itemId, pending, throwOnError);
|
|
747
|
+
if (cwd === undefined)
|
|
748
|
+
return;
|
|
749
|
+
if (!this.inputCurrent(itemId, pending) ||
|
|
750
|
+
this.options.runtime.activeSessionRun(sessionId) !== undefined ||
|
|
751
|
+
this.options.runtime.sessionQueue(sessionId).version !== version ||
|
|
752
|
+
this.nextQueueItem(sessionId) !== itemId) {
|
|
753
|
+
this.schedule(sessionId);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
659
756
|
}
|
|
660
757
|
this.prioritizeNextQueueItem(sessionId, itemId);
|
|
661
758
|
const { run, turn } = this.options.runtime.startRunFromQueueItem(itemId, withNonImageAttachmentPrompt(item.content, pending.attachmentPaths, pending.attachmentFiles));
|
|
@@ -665,6 +762,61 @@ export class ContinuousRunCoordinator {
|
|
|
665
762
|
this.options.emitQueue(run.workspaceId, sessionId);
|
|
666
763
|
this.options.emitConversation(turn);
|
|
667
764
|
this.schedule(sessionId);
|
|
765
|
+
return itemId;
|
|
766
|
+
}
|
|
767
|
+
inputCurrent(id, pending) {
|
|
768
|
+
const session = this.options.runtime.getAgentSession(pending.sessionId);
|
|
769
|
+
return (!this.suspended &&
|
|
770
|
+
!this.options.stopped() &&
|
|
771
|
+
this.preparedInputs.get(id) === pending &&
|
|
772
|
+
session?.id === pending.sessionId &&
|
|
773
|
+
session.workspaceId === pending.workspaceId);
|
|
774
|
+
}
|
|
775
|
+
async directoryForInput(id, pending, throwOnError = false) {
|
|
776
|
+
const session = this.options.runtime.getAgentSession(pending.sessionId);
|
|
777
|
+
if (!session || !this.inputCurrent(id, pending))
|
|
778
|
+
return undefined;
|
|
779
|
+
const version = this.options.runtime.sessionQueue(session.id).version;
|
|
780
|
+
try {
|
|
781
|
+
return await this.options.resolveExecutionDirectory(session);
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
if (!this.inputCurrent(id, pending)) {
|
|
785
|
+
if (this.options.runtime.getAgentSession(session.id) !== undefined &&
|
|
786
|
+
this.preparedInputs.get(id) !== pending)
|
|
787
|
+
this.schedule(session.id);
|
|
788
|
+
return undefined;
|
|
789
|
+
}
|
|
790
|
+
if (this.options.runtime.getQueueItem(id)) {
|
|
791
|
+
if (this.options.runtime.sessionQueue(session.id).version === version)
|
|
792
|
+
this.pauseForDirectory(session.id, error);
|
|
793
|
+
else
|
|
794
|
+
this.schedule(session.id);
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
this.options.emitRun(pending.runId, 'run.rejected', { code: safeErrorCode(error, 'QUEUE_DISPATCH_FAILED') }, 'FAILED');
|
|
798
|
+
}
|
|
799
|
+
if (throwOnError)
|
|
800
|
+
throw error;
|
|
801
|
+
return undefined;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
pauseForDirectory(sessionId, error) {
|
|
805
|
+
if (!this.options.runtime.sessionQueue(sessionId).paused)
|
|
806
|
+
this.options.runtime.pauseSessionQueue(sessionId);
|
|
807
|
+
nodeLog('queue.execution_directory.invalid', {
|
|
808
|
+
sessionId,
|
|
809
|
+
code: safeErrorCode(error, 'QUEUE_DISPATCH_FAILED')
|
|
810
|
+
});
|
|
811
|
+
const active = this.options.runtime.activeSessionRun(sessionId);
|
|
812
|
+
if (active && this.options.runtime.currentExecutionId(active.id) === undefined)
|
|
813
|
+
this.finishTerminalCommandRun(active);
|
|
814
|
+
try {
|
|
815
|
+
this.publish(sessionId);
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
nodeLog('queue.execution_directory.publish_failed', { sessionId });
|
|
819
|
+
}
|
|
668
820
|
}
|
|
669
821
|
nextQueueItem(sessionId) {
|
|
670
822
|
const queue = this.options.runtime.sessionQueue(sessionId);
|
|
@@ -35,10 +35,16 @@ export class ExternalSessionResumeService {
|
|
|
35
35
|
this.setFailure(external.id, 'RUNNER_UNAVAILABLE');
|
|
36
36
|
return undefined;
|
|
37
37
|
}
|
|
38
|
+
const releaseMutation = this.options.runtime.beginSessionHistoryMutation(external.id, {
|
|
39
|
+
allowQueued: true
|
|
40
|
+
});
|
|
38
41
|
this.transitions.add(external.id);
|
|
39
42
|
let result;
|
|
40
43
|
try {
|
|
41
|
-
|
|
44
|
+
const cwd = this.options.resolveExecutionDirectory
|
|
45
|
+
? await this.options.resolveExecutionDirectory(external)
|
|
46
|
+
: external.cwd;
|
|
47
|
+
result = await runner.resumeExternal({ ...external, cwd }, {
|
|
42
48
|
available: this.options.available(external.runner),
|
|
43
49
|
projectionFor: this.options.projectionFor,
|
|
44
50
|
managedAliasFor: this.options.managedAliasFor,
|
|
@@ -62,7 +68,7 @@ export class ExternalSessionResumeService {
|
|
|
62
68
|
? {}
|
|
63
69
|
: { externalSessionId: session.externalSessionId }),
|
|
64
70
|
nativeControl: 'MAR_MANAGED',
|
|
65
|
-
cwd
|
|
71
|
+
cwd,
|
|
66
72
|
createdAt: session.createdAt,
|
|
67
73
|
lastActivityAt: session.lastActivityAt,
|
|
68
74
|
model: configuration.model,
|
|
@@ -80,6 +86,7 @@ export class ExternalSessionResumeService {
|
|
|
80
86
|
}
|
|
81
87
|
finally {
|
|
82
88
|
this.transitions.delete(external.id);
|
|
89
|
+
releaseMutation();
|
|
83
90
|
}
|
|
84
91
|
if (result.activity !== undefined)
|
|
85
92
|
this.options.state.externalActivity.set(external.id, result.activity);
|
|
@@ -240,11 +240,14 @@ export class SessionCatalogService {
|
|
|
240
240
|
.listAgentSessions()
|
|
241
241
|
.flatMap((candidate) => [candidate.customTitle ?? candidate.runnerTitle].filter(isString)));
|
|
242
242
|
const runner = this.options.runners.require(source.runner);
|
|
243
|
-
const
|
|
243
|
+
const cwd = this.options.resolveExecutionDirectory
|
|
244
|
+
? await this.options.resolveExecutionDirectory(source)
|
|
245
|
+
: source.cwd;
|
|
246
|
+
const nativeId = await runner.forkNative({ ...source, cwd }, input.boundary, title);
|
|
244
247
|
const session = this.options.runtime.createAgentSession({
|
|
245
248
|
workspaceId: source.workspaceId,
|
|
246
249
|
runner: source.runner,
|
|
247
|
-
cwd
|
|
250
|
+
cwd,
|
|
248
251
|
nativeControl: 'MAR_MANAGED',
|
|
249
252
|
...(nativeId === null ? {} : { externalSessionId: nativeId }),
|
|
250
253
|
...(source.model === null ? {} : { model: source.model }),
|
|
@@ -375,9 +378,7 @@ export class SessionCatalogService {
|
|
|
375
378
|
async removeResolved(session, requireCleanupSafety = false) {
|
|
376
379
|
if (session.externalSessionId === null)
|
|
377
380
|
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
378
|
-
const releaseMutation = requireCleanupSafety
|
|
379
|
-
? this.options.runtime.beginSessionHistoryMutation(session.id)
|
|
380
|
-
: undefined;
|
|
381
|
+
const releaseMutation = this.options.runtime.beginSessionHistoryMutation(session.id, requireCleanupSafety ? undefined : { allowQueued: true });
|
|
381
382
|
try {
|
|
382
383
|
if (this.options.isRecovering?.(session.id))
|
|
383
384
|
throw new Error('SESSION_RESTART_PENDING');
|
|
@@ -431,7 +432,7 @@ export class SessionCatalogService {
|
|
|
431
432
|
return deleted ? 'deleted' : 'skipped';
|
|
432
433
|
}
|
|
433
434
|
finally {
|
|
434
|
-
releaseMutation
|
|
435
|
+
releaseMutation();
|
|
435
436
|
}
|
|
436
437
|
}
|
|
437
438
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { nodeLog } from '../operational.js';
|
|
2
2
|
import { isTerminalRunStatus } from '../util/node-operation-parsers.js';
|
|
3
|
+
import { safeErrorCode } from '../util/safe-error.js';
|
|
3
4
|
export class SessionIdentityService {
|
|
4
5
|
options;
|
|
5
6
|
bootstrapIds = new Set();
|
|
@@ -12,12 +13,21 @@ export class SessionIdentityService {
|
|
|
12
13
|
this.bootstrapIds.add(sessionId);
|
|
13
14
|
}
|
|
14
15
|
promote(sessionId, nativeSessionId) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
try {
|
|
17
|
+
if (this.failedBootstrapIds.has(sessionId))
|
|
18
|
+
throw new Error('SESSION_START_FAILED');
|
|
19
|
+
const current = this.options.runtime.validateSessionIdentityPromotion(sessionId, nativeSessionId);
|
|
20
|
+
this.options.validateCommands(sessionId, nativeSessionId);
|
|
20
21
|
this.options.persist(current, nativeSessionId);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
this.reject(sessionId, error instanceof Error ? error : new Error('SESSION_START_FAILED'));
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
// No I/O or publication may interrupt the synchronous ownership migration.
|
|
28
|
+
const session = this.options.runtime.promoteSessionIdentity(sessionId, nativeSessionId, {
|
|
29
|
+
deferRunChanges: true
|
|
30
|
+
});
|
|
21
31
|
this.options.migrateGoal(sessionId, nativeSessionId);
|
|
22
32
|
this.options.migrateCommands(sessionId, nativeSessionId);
|
|
23
33
|
this.options.migrateQueue(sessionId, nativeSessionId);
|
|
@@ -29,16 +39,27 @@ export class SessionIdentityService {
|
|
|
29
39
|
this.waiters.delete(sessionId);
|
|
30
40
|
waiter.resolve(nativeSessionId);
|
|
31
41
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.options.emitSession(session);
|
|
42
|
+
if (sessionId !== nativeSessionId)
|
|
43
|
+
this.options.runtime.publishSessionIdentity(nativeSessionId);
|
|
44
|
+
this.publish(nativeSessionId, 'session', () => this.options.emitSession(session));
|
|
36
45
|
for (const run of this.options.runtime.listRunsForSession(nativeSessionId))
|
|
37
|
-
this.options.emitRun(run);
|
|
38
|
-
this.options.emitQueue(nativeSessionId);
|
|
46
|
+
this.publish(nativeSessionId, 'run', () => this.options.emitRun(run));
|
|
47
|
+
this.publish(nativeSessionId, 'queue', () => this.options.emitQueue(nativeSessionId));
|
|
39
48
|
for (const turn of this.options.runtime.listConversationTurns({ sessionId: nativeSessionId })
|
|
40
49
|
.turns)
|
|
41
|
-
this.options.emitTurn(turn);
|
|
50
|
+
this.publish(nativeSessionId, 'turn', () => this.options.emitTurn(turn));
|
|
51
|
+
}
|
|
52
|
+
publish(sessionId, phase, emit) {
|
|
53
|
+
try {
|
|
54
|
+
emit();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
nodeLog('session.identity.publish.failed', {
|
|
58
|
+
sessionId,
|
|
59
|
+
phase,
|
|
60
|
+
errorCode: safeErrorCode(error, 'SESSION_IDENTITY_PUBLICATION_FAILED')
|
|
61
|
+
});
|
|
62
|
+
}
|
|
42
63
|
}
|
|
43
64
|
wait(bootstrapSessionId) {
|
|
44
65
|
const existing = this.waiters.get(bootstrapSessionId);
|
|
@@ -64,7 +85,7 @@ export class SessionIdentityService {
|
|
|
64
85
|
});
|
|
65
86
|
return promise;
|
|
66
87
|
}
|
|
67
|
-
reject(sessionId,
|
|
88
|
+
reject(sessionId, reason) {
|
|
68
89
|
const bootstrap = this.bootstrapIds.has(sessionId);
|
|
69
90
|
if (bootstrap) {
|
|
70
91
|
this.failedBootstrapIds.add(sessionId);
|
|
@@ -74,7 +95,7 @@ export class SessionIdentityService {
|
|
|
74
95
|
if (waiter !== undefined) {
|
|
75
96
|
clearTimeout(waiter.timer);
|
|
76
97
|
this.waiters.delete(sessionId);
|
|
77
|
-
waiter.reject(new Error(
|
|
98
|
+
waiter.reject(typeof reason === 'string' ? new Error(reason) : reason);
|
|
78
99
|
}
|
|
79
100
|
if (!bootstrap)
|
|
80
101
|
return;
|
|
@@ -3,6 +3,7 @@ import { isPlainRecord } from '../util/node-operation-parsers.js';
|
|
|
3
3
|
import { parseSecretEnvironment } from '../util/secret-environment.js';
|
|
4
4
|
import { ScheduledSessionStore } from './scheduled-session-store.js';
|
|
5
5
|
import { normalizeSessionPrompt } from '../util/personal-instructions.js';
|
|
6
|
+
import { resolveWorkspaceExecutionDirectory } from '../workspace.js';
|
|
6
7
|
export class SessionLifecycleService {
|
|
7
8
|
options;
|
|
8
9
|
starts = new Map();
|
|
@@ -83,7 +84,7 @@ export class SessionLifecycleService {
|
|
|
83
84
|
this.options.invalidateWorkspaceSessions(workspaceId);
|
|
84
85
|
return { moved: true };
|
|
85
86
|
}
|
|
86
|
-
create(input) {
|
|
87
|
+
async create(input) {
|
|
87
88
|
const runner = this.options.runners.product(input.runner);
|
|
88
89
|
if (typeof input.workspaceId !== 'string' ||
|
|
89
90
|
runner === undefined ||
|
|
@@ -100,6 +101,9 @@ export class SessionLifecycleService {
|
|
|
100
101
|
const workspace = database.getWorkspace(input.workspaceId);
|
|
101
102
|
if (workspace === undefined)
|
|
102
103
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
104
|
+
const cwd = await resolveWorkspaceExecutionDirectory(workspace.path, input.cwd);
|
|
105
|
+
if (database.getWorkspace(workspace.id)?.path !== workspace.path)
|
|
106
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
103
107
|
let parentSessionId;
|
|
104
108
|
if (input.parentSessionId !== undefined) {
|
|
105
109
|
if (typeof input.parentSessionId !== 'string' ||
|
|
@@ -133,7 +137,7 @@ export class SessionLifecycleService {
|
|
|
133
137
|
...(parentSessionId === undefined ? {} : { parentSessionId }),
|
|
134
138
|
workspaceId: input.workspaceId,
|
|
135
139
|
runner: runner.name,
|
|
136
|
-
cwd
|
|
140
|
+
cwd,
|
|
137
141
|
...(typeof input.externalSessionId === 'string'
|
|
138
142
|
? { externalSessionId: input.externalSessionId }
|
|
139
143
|
: {}),
|
|
@@ -208,7 +208,18 @@ export class SessionMessageService {
|
|
|
208
208
|
let reservation;
|
|
209
209
|
let nativeAccepted = false;
|
|
210
210
|
try {
|
|
211
|
-
|
|
211
|
+
let currentSession = this.options.runtime.getAgentSession(session.id) ?? session;
|
|
212
|
+
const cwd = this.options.resolveExecutionDirectory
|
|
213
|
+
? await this.options.resolveExecutionDirectory(currentSession)
|
|
214
|
+
: currentSession.cwd;
|
|
215
|
+
if (this.options.resolveExecutionDirectory) {
|
|
216
|
+
const latest = this.options.runtime.getAgentSession(currentSession.id);
|
|
217
|
+
if (!latest ||
|
|
218
|
+
latest.workspaceId !== currentSession.workspaceId ||
|
|
219
|
+
latest.cwd !== currentSession.cwd)
|
|
220
|
+
throw new Error('SESSION_NOT_FOUND');
|
|
221
|
+
currentSession = latest;
|
|
222
|
+
}
|
|
212
223
|
if (attachmentIds.length > 0)
|
|
213
224
|
reservation = await this.options.uploads.reserve(session.workspaceId, attachmentIds);
|
|
214
225
|
const attachments = reservation === undefined
|
|
@@ -228,11 +239,11 @@ export class SessionMessageService {
|
|
|
228
239
|
this.options.attachments.resourcePath(attachmentPaths[index])
|
|
229
240
|
}));
|
|
230
241
|
const runnerFiles = attachmentFiles.map((file, index) => {
|
|
231
|
-
const path = relative(
|
|
242
|
+
const path = relative(cwd, attachmentPaths[index]).replaceAll('\\', '/');
|
|
232
243
|
return path === file.path ? file : { ...file, path, workspacePath: file.path };
|
|
233
244
|
});
|
|
234
245
|
const displayInput = withNonImageAttachmentPrompt(publicContent, attachmentPaths, attachmentFiles);
|
|
235
|
-
const resourceInput = prepareComposerResourceInput(runnerContent,
|
|
246
|
+
const resourceInput = prepareComposerResourceInput(runnerContent, cwd, workspacePath);
|
|
236
247
|
const runnerInput = withNonImageAttachmentPrompt(resourceInput, attachmentPaths, runnerFiles);
|
|
237
248
|
const planActive = this.options.commands
|
|
238
249
|
.list(session.id)
|
|
@@ -270,7 +281,7 @@ export class SessionMessageService {
|
|
|
270
281
|
attachments: runnerImageAttachments,
|
|
271
282
|
attachmentPaths,
|
|
272
283
|
attachmentFiles: runnerFiles,
|
|
273
|
-
cwd
|
|
284
|
+
cwd,
|
|
274
285
|
workspacePath,
|
|
275
286
|
externalSessionId: currentSession.externalSessionId,
|
|
276
287
|
model: currentSession.model,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { steerComposerMessage } from '@myagentroam/protocol';
|
|
2
2
|
import { isPlainRecord, withNonImageAttachmentPrompt } from '../util/node-operation-parsers.js';
|
|
3
3
|
import { assertQueueItemOwner, assertSessionControlOwner } from './run-ownership-policy.js';
|
|
4
|
+
import { nodeLog } from '../operational.js';
|
|
5
|
+
import { safeErrorCode } from '../util/safe-error.js';
|
|
4
6
|
export class WorkspaceQueueWorkbenchService {
|
|
5
7
|
options;
|
|
6
8
|
constructor(options) {
|
|
@@ -30,8 +32,13 @@ export class WorkspaceQueueWorkbenchService {
|
|
|
30
32
|
throw new Error('SESSION_NOT_FOUND');
|
|
31
33
|
if (!this.options.runtime.sessionQueue(session.id).paused)
|
|
32
34
|
return;
|
|
33
|
-
this.options.coordinator
|
|
34
|
-
|
|
35
|
+
void this.options.coordinator
|
|
36
|
+
.resume(session.id)
|
|
37
|
+
.then(() => this.options.emit(session.workspaceId, session.id))
|
|
38
|
+
.catch((error) => nodeLog('queue.resume.failed', {
|
|
39
|
+
sessionId,
|
|
40
|
+
code: safeErrorCode(error, 'QUEUE_RESUME_FAILED')
|
|
41
|
+
}));
|
|
35
42
|
}
|
|
36
43
|
present(workspaceId, sessionId) {
|
|
37
44
|
const session = this.options.runtime.getAgentSession(sessionId);
|
|
@@ -75,11 +82,11 @@ export class WorkspaceQueueWorkbenchService {
|
|
|
75
82
|
queue: this.snapshot(session.id)
|
|
76
83
|
};
|
|
77
84
|
}
|
|
78
|
-
pauseOrResume(mode, input) {
|
|
85
|
+
async pauseOrResume(mode, input) {
|
|
79
86
|
const session = this.requireSession(input);
|
|
80
87
|
assertSessionControlOwner(this.options.runtime, session.id, input.__requestUserId);
|
|
81
88
|
if (mode === 'resume')
|
|
82
|
-
this.options.coordinator.resume(session.id);
|
|
89
|
+
await this.options.coordinator.resume(session.id);
|
|
83
90
|
else
|
|
84
91
|
this.options.runtime.pauseSessionQueue(session.id);
|
|
85
92
|
this.options.emit(session.workspaceId, session.id);
|
package/dist/supervisor.js
CHANGED
|
@@ -5,6 +5,7 @@ import { loadNodeConfig, nodeDatabasePath } from './config.js';
|
|
|
5
5
|
import { nodeLog, platformCliInvocation } from './operational.js';
|
|
6
6
|
import { nodeUpgradeRequestPath } from './service/node-upgrade-coordinator.js';
|
|
7
7
|
import { safeErrorCode } from './util/safe-error.js';
|
|
8
|
+
import { NODE_OWNER_CONFLICT_EXIT_CODE } from './node-process-lock.js';
|
|
8
9
|
const UPGRADE_EXIT_CODE = 75;
|
|
9
10
|
const STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
|
|
10
11
|
export async function superviseNode(configPath, bootstrapEntrypoint) {
|
|
@@ -19,6 +20,10 @@ export async function superviseNode(configPath, bootstrapEntrypoint) {
|
|
|
19
20
|
return;
|
|
20
21
|
}
|
|
21
22
|
const code = childResult.code;
|
|
23
|
+
if (code === NODE_OWNER_CONFLICT_EXIT_CODE) {
|
|
24
|
+
process.exitCode = code;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
22
27
|
if (code !== UPGRADE_EXIT_CODE) {
|
|
23
28
|
if (code === 0)
|
|
24
29
|
return;
|
package/dist/workspace.js
CHANGED
|
@@ -999,9 +999,52 @@ export async function searchWorkspaceFiles(workspaceRoot, query, cursor, limit =
|
|
|
999
999
|
async function resolveRegisteredWorkspaceRoot(workspaceRoot) {
|
|
1000
1000
|
const root = await realpath(workspaceRoot);
|
|
1001
1001
|
if (normalizeWorkspacePath(root) !== normalizeWorkspacePath(workspaceRoot))
|
|
1002
|
-
throw new Error('WORKSPACE_NOT_ALLOWED');
|
|
1002
|
+
throw Object.assign(new Error('WORKSPACE_NOT_ALLOWED'), { code: 'WORKSPACE_NOT_ALLOWED' });
|
|
1003
1003
|
return root;
|
|
1004
1004
|
}
|
|
1005
|
+
export class SessionExecutionDirectoryError extends Error {
|
|
1006
|
+
publicMessage;
|
|
1007
|
+
code = 'SESSION_EXECUTION_DIRECTORY_INVALID';
|
|
1008
|
+
publicParameters;
|
|
1009
|
+
constructor(reason, publicMessage, options) {
|
|
1010
|
+
super(publicMessage, options);
|
|
1011
|
+
this.publicMessage = publicMessage;
|
|
1012
|
+
this.publicParameters = { reason };
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/** Bind ordinary Session execution to one registered Workspace, not all allowed roots. */
|
|
1016
|
+
export async function resolveWorkspaceExecutionDirectory(workspaceRoot, cwd) {
|
|
1017
|
+
if (!isAbsolute(cwd) || (process.platform === 'win32' && win32.parse(cwd).root.length <= 1))
|
|
1018
|
+
throw new SessionExecutionDirectoryError('ABSOLUTE_PATH_REQUIRED', 'The execution directory must be an absolute path on this Node.');
|
|
1019
|
+
try {
|
|
1020
|
+
const root = await resolveRegisteredWorkspaceRoot(workspaceRoot);
|
|
1021
|
+
const directory = await realpath(cwd);
|
|
1022
|
+
if (!isWithinAllowedRoot(directory, [root]))
|
|
1023
|
+
throw new SessionExecutionDirectoryError('OUTSIDE_WORKSPACE', 'The execution directory is outside the authorized Workspace.');
|
|
1024
|
+
if (!(await stat(directory)).isDirectory())
|
|
1025
|
+
throw new SessionExecutionDirectoryError('NOT_DIRECTORY', 'The execution directory is not a directory.');
|
|
1026
|
+
return directory;
|
|
1027
|
+
}
|
|
1028
|
+
catch (error) {
|
|
1029
|
+
if (error instanceof SessionExecutionDirectoryError)
|
|
1030
|
+
throw error;
|
|
1031
|
+
const code = error !== null && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
1032
|
+
const [reason, message] = code === 'WORKSPACE_NOT_ALLOWED'
|
|
1033
|
+
? [
|
|
1034
|
+
'WORKSPACE_ROOT_CHANGED',
|
|
1035
|
+
'The registered Workspace root no longer identifies the same directory.'
|
|
1036
|
+
]
|
|
1037
|
+
: code === 'ENOENT' || code === 'ENOTDIR'
|
|
1038
|
+
? ['NOT_FOUND', 'The execution directory or Workspace root is unavailable.']
|
|
1039
|
+
: code === 'EACCES' || code === 'EPERM'
|
|
1040
|
+
? [
|
|
1041
|
+
'ACCESS_DENIED',
|
|
1042
|
+
'The Node account cannot access the execution directory or Workspace root.'
|
|
1043
|
+
]
|
|
1044
|
+
: ['IO_ERROR', 'The execution directory could not be verified.'];
|
|
1045
|
+
throw new SessionExecutionDirectoryError(reason, message, { cause: error });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1005
1048
|
async function resolveWorkspaceEntry(workspaceRoot, requestedPath, directory) {
|
|
1006
1049
|
const root = await resolveRegisteredWorkspaceRoot(workspaceRoot);
|
|
1007
1050
|
return resolveWorkspaceEntryFromCanonicalRoot(root, requestedPath, directory);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.67",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"playwright": "1.62.1",
|
|
38
38
|
"ws": "^8.21.3",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@myagentroam/agent": "0.10.
|
|
40
|
+
"@myagentroam/agent": "0.10.67",
|
|
41
41
|
"@myagentroam/code-navigation": "0.10.1",
|
|
42
|
-
"@myagentroam/
|
|
43
|
-
"@myagentroam/
|
|
42
|
+
"@myagentroam/protocol": "0.10.67",
|
|
43
|
+
"@myagentroam/marctl": "0.10.12"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/ws": "^8.18.1"
|