@myagentroam/node 0.10.35 → 0.10.37
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 +115 -18
- package/dist/database.js +29 -12
- package/dist/main.js +18 -2
- package/dist/migrations/v019.js +9 -0
- package/dist/migrations/v020.js +46 -0
- package/dist/runner/mar-agent/computer-use-probe.js +126 -0
- package/dist/runner/mar-agent/computer-use.js +50 -20
- package/dist/runner/mar-agent/conversation-normalizer.js +28 -0
- package/dist/runner/mar-agent/marctl-skill.js +3 -2
- package/dist/runner/mar-agent-runner.js +32 -0
- package/dist/runtime-state.js +15 -10
- package/dist/service/continuous-run-coordinator.js +58 -5
- package/dist/service/direct-transfer-service.js +58 -11
- package/dist/service/external-session-resume-service.js +2 -1
- package/dist/service/goal-runtime.js +13 -0
- package/dist/service/hosted-conversation-node-service.js +5 -4
- package/dist/service/native-session-projection-service.js +77 -11
- package/dist/service/planned-restart-handoff.js +279 -0
- package/dist/service/restart-session-restorer.js +141 -0
- package/dist/service/{scheduled-run-authorization-control.js → run-authorization-control.js} +17 -10
- package/dist/service/run-event-service.js +2 -0
- package/dist/service/scheduled-session-coordinator.js +13 -0
- package/dist/service/scheduled-session-store.js +24 -0
- package/dist/service/session-catalog-service.js +57 -4
- package/dist/service/session-command-service.js +1 -0
- package/dist/service/session-lifecycle-service.js +25 -8
- package/dist/service/session-message-service.js +80 -15
- package/dist/service/session-presentation-service.js +2 -0
- package/dist/service/session-query-service.js +14 -9
- package/dist/service/workspace-queue-workbench-service.js +15 -8
- package/dist/service/workspace-upload-service.js +128 -29
- package/dist/supervisor.js +7 -3
- package/package.json +4 -4
|
@@ -50,20 +50,16 @@ export class NativeSessionProjectionService {
|
|
|
50
50
|
this.remember(sessions);
|
|
51
51
|
return sessions;
|
|
52
52
|
}
|
|
53
|
-
async resolve(sessionId) {
|
|
53
|
+
async resolve(sessionId, registeredOnly = false) {
|
|
54
54
|
const cached = this.options.state.directNative.get(sessionId);
|
|
55
55
|
if (cached !== undefined)
|
|
56
56
|
return cached;
|
|
57
|
-
const discovered = (await this.list()).find((session) => session.id === sessionId);
|
|
58
|
-
if (discovered !== undefined)
|
|
59
|
-
return discovered;
|
|
60
57
|
const database = this.options.database();
|
|
61
|
-
const record = database
|
|
62
|
-
?.listWorkspaces()
|
|
63
|
-
.flatMap((workspace) => database.listSessionRecords(workspace.id))
|
|
64
|
-
.find((candidate) => candidate.id === sessionId);
|
|
58
|
+
const record = database?.getSessionRecord(sessionId);
|
|
65
59
|
if (record === undefined)
|
|
66
|
-
return
|
|
60
|
+
return registeredOnly
|
|
61
|
+
? undefined
|
|
62
|
+
: (await this.list()).find((session) => session.id === sessionId);
|
|
67
63
|
const workspace = database?.getWorkspace(record.workspaceId);
|
|
68
64
|
if (workspace === undefined)
|
|
69
65
|
return undefined;
|
|
@@ -80,6 +76,7 @@ export class NativeSessionProjectionService {
|
|
|
80
76
|
effort: record.metadata.effort,
|
|
81
77
|
access: record.metadata.access,
|
|
82
78
|
customTitle: record.metadata.customTitle,
|
|
79
|
+
parentSessionId: record.metadata.parentSessionId ?? null,
|
|
83
80
|
sessionPrompt: record.metadata.sessionPrompt ?? null,
|
|
84
81
|
runnerTitle: record.metadata.runnerTitle,
|
|
85
82
|
pinnedAt: record.pinOrder === null ? null : 1,
|
|
@@ -114,7 +111,8 @@ export class NativeSessionProjectionService {
|
|
|
114
111
|
...(direct.access === null ? {} : { access: direct.access }),
|
|
115
112
|
...(direct.runnerTitle === null ? {} : { title: direct.runnerTitle }),
|
|
116
113
|
...(direct.customTitle === null ? {} : { customTitle: direct.customTitle }),
|
|
117
|
-
sessionPrompt: direct.sessionPrompt ?? null
|
|
114
|
+
sessionPrompt: direct.sessionPrompt ?? null,
|
|
115
|
+
parentSessionId: direct.parentSessionId ?? null
|
|
118
116
|
});
|
|
119
117
|
}
|
|
120
118
|
resumedConfiguration(session) {
|
|
@@ -186,6 +184,7 @@ export class NativeSessionProjectionService {
|
|
|
186
184
|
access: metadata?.access ?? null,
|
|
187
185
|
customTitle: metadata?.customTitle ?? null,
|
|
188
186
|
sessionPrompt: metadata?.sessionPrompt ?? null,
|
|
187
|
+
parentSessionId: metadata?.parentSessionId ?? null,
|
|
189
188
|
runnerTitle,
|
|
190
189
|
pinnedAt: metadata?.pinnedAt ?? null,
|
|
191
190
|
pinOrder: metadata?.pinOrder ?? null,
|
|
@@ -202,8 +201,61 @@ export class NativeSessionProjectionService {
|
|
|
202
201
|
};
|
|
203
202
|
}
|
|
204
203
|
remember(sessions) {
|
|
205
|
-
|
|
204
|
+
const database = this.options.database();
|
|
205
|
+
for (const session of sessions) {
|
|
206
206
|
this.options.state.directNative.set(session.id, session);
|
|
207
|
+
if (database === undefined || session.externalSessionId === null)
|
|
208
|
+
continue;
|
|
209
|
+
const metadata = database.getNativeSessionMetadata(session.runner, session.externalSessionId, session.workspaceId);
|
|
210
|
+
const title = session.runnerTitle?.trim().slice(0, 160) || undefined;
|
|
211
|
+
if (metadata === undefined ||
|
|
212
|
+
(title !== undefined && title !== metadata.runnerTitle && !metadata.runnerTitlePending))
|
|
213
|
+
database.saveNativeSessionMetadata({
|
|
214
|
+
runner: session.runner,
|
|
215
|
+
externalSessionId: session.externalSessionId,
|
|
216
|
+
workspaceId: session.workspaceId,
|
|
217
|
+
...(title === undefined || metadata?.runnerTitlePending ? {} : { runnerTitle: title })
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
similarSessionDetails(sessionId, workspaceId) {
|
|
222
|
+
if (sessionId.length < 8 || sessionId.length > 128)
|
|
223
|
+
return undefined;
|
|
224
|
+
const records = this.options.database()?.listSessionRecords(workspaceId) ?? [];
|
|
225
|
+
const recordById = new Map(records.map((record) => [record.id, record]));
|
|
226
|
+
const known = new Map(records.map((record) => [
|
|
227
|
+
record.id,
|
|
228
|
+
record.metadata.customTitle ?? record.metadata.runnerTitle ?? 'Untitled session'
|
|
229
|
+
]));
|
|
230
|
+
for (const session of [
|
|
231
|
+
...this.options.state.directNative.values(),
|
|
232
|
+
...this.options.runtime.listAgentSessions()
|
|
233
|
+
]) {
|
|
234
|
+
if (session.workspaceId !== workspaceId)
|
|
235
|
+
continue;
|
|
236
|
+
const metadata = recordById.get(session.id)?.metadata;
|
|
237
|
+
known.set(session.id, metadata?.customTitle ??
|
|
238
|
+
session.customTitle ??
|
|
239
|
+
session.runnerTitle ??
|
|
240
|
+
metadata?.runnerTitle ??
|
|
241
|
+
'Untitled session');
|
|
242
|
+
}
|
|
243
|
+
const maximum = Math.min(3, Math.floor(sessionId.length / 8));
|
|
244
|
+
const candidates = [...known]
|
|
245
|
+
.map(([id, title]) => ({
|
|
246
|
+
id,
|
|
247
|
+
title,
|
|
248
|
+
distance: sessionIdDistance(sessionId.toLowerCase(), id.toLowerCase(), maximum)
|
|
249
|
+
}))
|
|
250
|
+
.filter((candidate) => candidate.distance <= maximum)
|
|
251
|
+
.sort((left, right) => left.distance - right.distance || left.id.localeCompare(right.id))
|
|
252
|
+
.slice(0, 3);
|
|
253
|
+
return candidates.length === 0
|
|
254
|
+
? undefined
|
|
255
|
+
: [
|
|
256
|
+
'Similar Sessions (suggestions only; no action was redirected):',
|
|
257
|
+
...candidates.map(({ id, title }) => id + ' ' + title.replace(/[\u0000-\u001f\u007f]/gu, ' ').slice(0, 160))
|
|
258
|
+
].join('\n');
|
|
207
259
|
}
|
|
208
260
|
projectionForNative(native) {
|
|
209
261
|
if (native.externalSessionId === null)
|
|
@@ -297,3 +349,17 @@ export class NativeSessionProjectionService {
|
|
|
297
349
|
return database;
|
|
298
350
|
}
|
|
299
351
|
}
|
|
352
|
+
function sessionIdDistance(left, right, maximum) {
|
|
353
|
+
if (Math.abs(left.length - right.length) > maximum)
|
|
354
|
+
return maximum + 1;
|
|
355
|
+
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
356
|
+
for (let row = 1; row <= left.length; row += 1) {
|
|
357
|
+
const current = [row];
|
|
358
|
+
for (let column = 1; column <= right.length; column += 1)
|
|
359
|
+
current[column] = Math.min(current[column - 1] + 1, previous[column] + 1, previous[column - 1] + (left[row - 1] === right[column - 1] ? 0 : 1));
|
|
360
|
+
if (Math.min(...current) > maximum)
|
|
361
|
+
return maximum + 1;
|
|
362
|
+
previous = current;
|
|
363
|
+
}
|
|
364
|
+
return previous[right.length];
|
|
365
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { parseComposerTags } from '@myagentroam/protocol';
|
|
6
|
+
const identifier = z.string().min(1).max(512);
|
|
7
|
+
const count = z.number().int().nonnegative();
|
|
8
|
+
const attachmentSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
name: z.string().min(1).max(512),
|
|
11
|
+
mime: z.string().min(1).max(128),
|
|
12
|
+
dataBase64: z.string().min(1).max(7_000_000)
|
|
13
|
+
})
|
|
14
|
+
.strict();
|
|
15
|
+
const messageSchema = z
|
|
16
|
+
.object({
|
|
17
|
+
id: identifier,
|
|
18
|
+
clientMessageId: identifier,
|
|
19
|
+
content: z.string().max(1_000_000),
|
|
20
|
+
attachments: z.array(attachmentSchema).max(5),
|
|
21
|
+
initiatedByUserId: z.number().int().positive(),
|
|
22
|
+
immediate: z.boolean()
|
|
23
|
+
})
|
|
24
|
+
.strict();
|
|
25
|
+
const goalSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
goal: z
|
|
28
|
+
.object({
|
|
29
|
+
id: identifier,
|
|
30
|
+
sessionId: identifier,
|
|
31
|
+
objective: z.string().min(1).max(20_000),
|
|
32
|
+
inputTags: z.string().default(''),
|
|
33
|
+
startedAt: count,
|
|
34
|
+
revisionStartedAt: count,
|
|
35
|
+
revision: z.number().int().positive(),
|
|
36
|
+
status: z.enum(['active', 'completed', 'blocked']),
|
|
37
|
+
turns: count,
|
|
38
|
+
retryRequired: z.boolean(),
|
|
39
|
+
retryStartedAtTurn: count.nullable(),
|
|
40
|
+
consecutiveNoProgressTurns: count
|
|
41
|
+
})
|
|
42
|
+
.strict(),
|
|
43
|
+
executionFailures: count
|
|
44
|
+
})
|
|
45
|
+
.strict();
|
|
46
|
+
const sessionSchema = z
|
|
47
|
+
.object({
|
|
48
|
+
session: z
|
|
49
|
+
.object({
|
|
50
|
+
id: identifier,
|
|
51
|
+
workspaceId: identifier,
|
|
52
|
+
cwd: z.string().min(1),
|
|
53
|
+
model: z.string().nullable(),
|
|
54
|
+
effort: z.string().nullable(),
|
|
55
|
+
access: z.string().nullable()
|
|
56
|
+
})
|
|
57
|
+
.strict(),
|
|
58
|
+
ownerUserId: z.number().int().positive(),
|
|
59
|
+
paused: z.boolean(),
|
|
60
|
+
plan: z.boolean(),
|
|
61
|
+
awaitingGoal: z.boolean(),
|
|
62
|
+
goal: goalSchema.optional(),
|
|
63
|
+
current: messageSchema.optional(),
|
|
64
|
+
queue: z.array(messageSchema)
|
|
65
|
+
})
|
|
66
|
+
.strict();
|
|
67
|
+
export const plannedRestartSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
schemaVersion: z.literal(1),
|
|
70
|
+
id: identifier,
|
|
71
|
+
nodeId: identifier,
|
|
72
|
+
serverUrl: z.string().min(1),
|
|
73
|
+
createdAt: count,
|
|
74
|
+
sessions: z.array(sessionSchema)
|
|
75
|
+
})
|
|
76
|
+
.strict();
|
|
77
|
+
export const RESTART_NOTICE = 'The previous execution was interrupted by a system restart. Check the session history and current workspace, then continue unfinished work. Verify completed or uncertain actions before proceeding; do not blindly repeat actions with external side effects.';
|
|
78
|
+
/** A one-shot orderly-shutdown handoff, not a crash-recovery journal. */
|
|
79
|
+
export class PlannedRestartHandoff {
|
|
80
|
+
options;
|
|
81
|
+
pending;
|
|
82
|
+
blocked = new Set();
|
|
83
|
+
recovery;
|
|
84
|
+
owners = new Map();
|
|
85
|
+
constructor(options) {
|
|
86
|
+
this.options = options;
|
|
87
|
+
}
|
|
88
|
+
path(processing = false) {
|
|
89
|
+
return resolve(this.options.directory(), processing ? 'planned-restart-handoff.processing.json' : 'planned-restart-handoff.json');
|
|
90
|
+
}
|
|
91
|
+
blocks(sessionId) {
|
|
92
|
+
return this.blocked.has(sessionId);
|
|
93
|
+
}
|
|
94
|
+
blocksOperation(operation, input) {
|
|
95
|
+
const sessionId = typeof input.sessionId === 'string'
|
|
96
|
+
? input.sessionId
|
|
97
|
+
: typeof input.queueItemId === 'string'
|
|
98
|
+
? this.options.runtime.getQueueItem(input.queueItemId)?.sessionId
|
|
99
|
+
: undefined;
|
|
100
|
+
if (sessionId === undefined || !this.blocks(sessionId))
|
|
101
|
+
return false;
|
|
102
|
+
return (operation === 'session.message' ||
|
|
103
|
+
operation.startsWith('session.command.') ||
|
|
104
|
+
(operation.startsWith('workspace.queue.') && operation !== 'workspace.queue.get') ||
|
|
105
|
+
[
|
|
106
|
+
'session.remove',
|
|
107
|
+
'session.rewind',
|
|
108
|
+
'session.rollback',
|
|
109
|
+
'session.config.update',
|
|
110
|
+
'session.metadata.update'
|
|
111
|
+
].includes(operation));
|
|
112
|
+
}
|
|
113
|
+
rememberUser(sessionId, userId) {
|
|
114
|
+
this.owners.set(sessionId, userId);
|
|
115
|
+
}
|
|
116
|
+
async load() {
|
|
117
|
+
this.recovery = undefined;
|
|
118
|
+
this.pending = undefined;
|
|
119
|
+
this.blocked.clear();
|
|
120
|
+
let text;
|
|
121
|
+
try {
|
|
122
|
+
text = await readFile(this.path(), 'utf8');
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error.code !== 'ENOENT')
|
|
126
|
+
this.options.diagnose('RESTART_HANDOFF_READ_FAILED');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const value = plannedRestartSchema.parse(JSON.parse(text));
|
|
131
|
+
const identity = this.options.identity();
|
|
132
|
+
if (value.nodeId !== identity.nodeId || value.serverUrl !== identity.serverUrl)
|
|
133
|
+
throw new Error('RESTART_HANDOFF_IDENTITY_MISMATCH');
|
|
134
|
+
this.pending = value;
|
|
135
|
+
for (const entry of value.sessions)
|
|
136
|
+
this.blocked.add(entry.session.id);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
this.options.diagnose('RESTART_HANDOFF_INVALID');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
capture() {
|
|
143
|
+
const { runtime, coordinator, goals } = this.options;
|
|
144
|
+
const sessions = [...(this.pending?.sessions ?? [])];
|
|
145
|
+
for (const session of runtime.listAgentSessions()) {
|
|
146
|
+
if (session.runner !== 'mar-agent' || this.blocks(session.id))
|
|
147
|
+
continue;
|
|
148
|
+
const queue = runtime.sessionQueue(session.id);
|
|
149
|
+
const active = runtime.activeSessionRun(session.id);
|
|
150
|
+
const goal = goals.restartState(session.id);
|
|
151
|
+
const awaitingGoal = goal === undefined && this.options.commandActive(session.id, 'goal');
|
|
152
|
+
const currentId = active && (runtime.currentExecutionId(active.id) ?? active.id);
|
|
153
|
+
const pending = currentId === undefined ? undefined : coordinator.pending(currentId);
|
|
154
|
+
const toMessage = (id, clientMessageId, content, attachments, owner) => ({
|
|
155
|
+
id,
|
|
156
|
+
clientMessageId,
|
|
157
|
+
content: withoutAttachmentTags(content),
|
|
158
|
+
attachments,
|
|
159
|
+
initiatedByUserId: owner,
|
|
160
|
+
immediate: coordinator.isImmediate(id)
|
|
161
|
+
});
|
|
162
|
+
const items = queue.itemIds.flatMap((id) => {
|
|
163
|
+
if (coordinator.pending(id)?.goal !== undefined)
|
|
164
|
+
return [];
|
|
165
|
+
const item = runtime.getQueueItem(id);
|
|
166
|
+
const original = coordinator.pending(id)?.restartMessage;
|
|
167
|
+
return [
|
|
168
|
+
toMessage(id, item.clientMessageId, original?.content ?? item.content, [...(original?.attachments ?? item.attachments)], item.initiatedByUserId)
|
|
169
|
+
];
|
|
170
|
+
});
|
|
171
|
+
const current = pending?.restartMessage === undefined || pending.goal !== undefined
|
|
172
|
+
? undefined
|
|
173
|
+
: toMessage(currentId, pending.clientMessageId ?? active.clientMessageId, pending.restartMessage.content, [...pending.restartMessage.attachments], pending.restartMessage.initiatedByUserId ?? active?.initiatedByUserId);
|
|
174
|
+
if (current === undefined && items.length === 0 && goal === undefined && !awaitingGoal)
|
|
175
|
+
continue;
|
|
176
|
+
if (session.externalSessionId === null)
|
|
177
|
+
throw new Error('RESTART_SESSION_NOT_READY');
|
|
178
|
+
const ownerUserId = active?.initiatedByUserId ??
|
|
179
|
+
items[0]?.initiatedByUserId ??
|
|
180
|
+
this.owners.get(session.id) ??
|
|
181
|
+
runtime.listRunsForSession(session.id).find((run) => run.initiatedByUserId !== undefined)
|
|
182
|
+
?.initiatedByUserId;
|
|
183
|
+
sessions.push({
|
|
184
|
+
session: {
|
|
185
|
+
id: session.externalSessionId,
|
|
186
|
+
workspaceId: session.workspaceId,
|
|
187
|
+
cwd: session.cwd,
|
|
188
|
+
model: session.model,
|
|
189
|
+
effort: session.effort,
|
|
190
|
+
access: session.access
|
|
191
|
+
},
|
|
192
|
+
ownerUserId: ownerUserId,
|
|
193
|
+
paused: queue.paused,
|
|
194
|
+
plan: this.options.commandActive(session.id, 'plan'),
|
|
195
|
+
awaitingGoal,
|
|
196
|
+
...(goal === undefined ? {} : { goal: goalSchema.parse(goal) }),
|
|
197
|
+
...(current === undefined ? {} : { current }),
|
|
198
|
+
queue: items
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return plannedRestartSchema.parse({
|
|
202
|
+
schemaVersion: 1,
|
|
203
|
+
id: randomUUID(),
|
|
204
|
+
...this.options.identity(),
|
|
205
|
+
createdAt: Date.now(),
|
|
206
|
+
sessions
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
async publish(snapshot) {
|
|
210
|
+
for (const entry of snapshot.sessions) {
|
|
211
|
+
const goal = this.options.goals.restartState(entry.session.id);
|
|
212
|
+
if (goal !== undefined)
|
|
213
|
+
entry.goal = goalSchema.parse(goal);
|
|
214
|
+
if (entry.current !== undefined) {
|
|
215
|
+
const state = await this.options.inputState(entry, entry.current);
|
|
216
|
+
if (state === 'completed')
|
|
217
|
+
delete entry.current;
|
|
218
|
+
if (state === 'paused')
|
|
219
|
+
entry.paused = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (snapshot.sessions.length === 0)
|
|
223
|
+
return;
|
|
224
|
+
const temporary = `${this.path()}.${process.pid}.tmp`;
|
|
225
|
+
await mkdir(this.options.directory(), { recursive: true, mode: 0o700 });
|
|
226
|
+
try {
|
|
227
|
+
await writeFile(temporary, JSON.stringify(plannedRestartSchema.parse(snapshot)), {
|
|
228
|
+
mode: 0o600
|
|
229
|
+
});
|
|
230
|
+
await rename(temporary, this.path());
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
await rm(temporary, { force: true });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
recover() {
|
|
237
|
+
if (this.pending === undefined)
|
|
238
|
+
return this.recovery ?? Promise.resolve();
|
|
239
|
+
return (this.recovery ??= this.consume());
|
|
240
|
+
}
|
|
241
|
+
async wait() {
|
|
242
|
+
await this.recovery;
|
|
243
|
+
}
|
|
244
|
+
async consume() {
|
|
245
|
+
const value = this.pending;
|
|
246
|
+
this.pending = undefined;
|
|
247
|
+
let failed = false;
|
|
248
|
+
try {
|
|
249
|
+
await rename(this.path(), this.path(true));
|
|
250
|
+
for (const entry of value.sessions) {
|
|
251
|
+
try {
|
|
252
|
+
this.rememberUser(entry.session.id, entry.ownerUserId);
|
|
253
|
+
await this.options.restore(entry, value.id);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
failed = true;
|
|
257
|
+
this.options.diagnose('RESTART_SESSION_RESTORE_FAILED', entry.session.id);
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
this.blocked.delete(entry.session.id);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!failed)
|
|
264
|
+
await rm(this.path(true), { force: true });
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
this.options.diagnose('RESTART_HANDOFF_CONSUME_FAILED');
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
this.blocked.clear();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function withoutAttachmentTags(content) {
|
|
275
|
+
return parseComposerTags(content)
|
|
276
|
+
.map((part) => part.kind === 'text' ? part.text : part.kind === 'mar_attachments' ? '' : part.raw)
|
|
277
|
+
.join('')
|
|
278
|
+
.trim();
|
|
279
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { serializeComposerTag } from '@myagentroam/protocol';
|
|
2
|
+
import { parseMessageAttachments } from '../util/node-operation-parsers.js';
|
|
3
|
+
import { RESTART_NOTICE } from './planned-restart-handoff.js';
|
|
4
|
+
/** Rebuild inputs through the existing preparation and FIFO boundary. */
|
|
5
|
+
export class RestartSessionRestorer {
|
|
6
|
+
options;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
async restore(entry, handoffId) {
|
|
11
|
+
const contexts = new Map();
|
|
12
|
+
const authorize = async (ownerUserId) => {
|
|
13
|
+
if (!contexts.has(ownerUserId))
|
|
14
|
+
contexts.set(ownerUserId, await this.options.authorize({
|
|
15
|
+
handoffId,
|
|
16
|
+
sessionId: entry.session.id,
|
|
17
|
+
workspaceId: entry.session.workspaceId,
|
|
18
|
+
ownerUserId,
|
|
19
|
+
runner: 'mar-agent',
|
|
20
|
+
model: entry.session.model
|
|
21
|
+
}));
|
|
22
|
+
return contexts.get(ownerUserId);
|
|
23
|
+
};
|
|
24
|
+
await authorize(entry.ownerUserId);
|
|
25
|
+
for (const input of [...entry.queue, ...(entry.current ? [entry.current] : [])]) {
|
|
26
|
+
parseMessageAttachments(input.attachments);
|
|
27
|
+
await authorize(input.initiatedByUserId);
|
|
28
|
+
}
|
|
29
|
+
let session = await this.options.resolve(entry.session.id);
|
|
30
|
+
if (session.runner !== 'mar-agent' ||
|
|
31
|
+
session.workspaceId !== entry.session.workspaceId ||
|
|
32
|
+
session.cwd !== entry.session.cwd ||
|
|
33
|
+
session.externalSessionId !== entry.session.id)
|
|
34
|
+
throw new Error('RESTART_SESSION_MISMATCH');
|
|
35
|
+
session = this.options.runtime.updateAgentSessionConfiguration({
|
|
36
|
+
sessionId: session.id,
|
|
37
|
+
model: entry.session.model ?? session.model,
|
|
38
|
+
effort: entry.session.effort ?? session.effort,
|
|
39
|
+
access: entry.session.access ?? session.access
|
|
40
|
+
});
|
|
41
|
+
const { runtime, coordinator, goals, commands } = this.options;
|
|
42
|
+
runtime.pauseSessionQueue(session.id);
|
|
43
|
+
if (entry.plan)
|
|
44
|
+
commands.set(session.id, {
|
|
45
|
+
commandId: 'plan',
|
|
46
|
+
active: true,
|
|
47
|
+
label: 'Plan',
|
|
48
|
+
detail: null,
|
|
49
|
+
closable: true,
|
|
50
|
+
closeInput: '/plan'
|
|
51
|
+
});
|
|
52
|
+
if (entry.goal) {
|
|
53
|
+
if (entry.goal.goal.sessionId !== session.id)
|
|
54
|
+
throw new Error('RESTART_GOAL_SESSION_MISMATCH');
|
|
55
|
+
goals.restoreRestartState(entry.goal);
|
|
56
|
+
this.options.publishGoal(session, entry.goal.goal);
|
|
57
|
+
}
|
|
58
|
+
else if (entry.awaitingGoal)
|
|
59
|
+
commands.set(session.id, {
|
|
60
|
+
commandId: 'goal',
|
|
61
|
+
active: true,
|
|
62
|
+
label: 'Goal',
|
|
63
|
+
detail: null,
|
|
64
|
+
closable: true,
|
|
65
|
+
closeInput: '/goal --clear'
|
|
66
|
+
});
|
|
67
|
+
const submit = async (input, content = input.content) => {
|
|
68
|
+
const context = contexts.get(input.initiatedByUserId);
|
|
69
|
+
await this.options.withGrant(context, () => this.options.messages.restoreQueued({
|
|
70
|
+
sessionId: session.id,
|
|
71
|
+
content,
|
|
72
|
+
clientMessageId: input.clientMessageId,
|
|
73
|
+
...(content.startsWith('<mar_restart>')
|
|
74
|
+
? { __runnerContent: `${content}${RESTART_NOTICE}` }
|
|
75
|
+
: {}),
|
|
76
|
+
attachments: input.attachments,
|
|
77
|
+
__queueItemId: input.id,
|
|
78
|
+
__requestUserId: input.initiatedByUserId,
|
|
79
|
+
secretEnvironment: context.secretEnvironment,
|
|
80
|
+
personalInstructions: context.personalInstructions,
|
|
81
|
+
marAgentModel: context.marAgentModel,
|
|
82
|
+
marAgentModels: context.marAgentModels,
|
|
83
|
+
marAgentCompact: context.marAgentCompact,
|
|
84
|
+
marAgentRuntimeScopeId: context.marAgentRuntimeScopeId,
|
|
85
|
+
__usageGrant: context.usageGrant
|
|
86
|
+
}));
|
|
87
|
+
if (input.immediate)
|
|
88
|
+
coordinator.restoreImmediate(input.id);
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
if (entry.current &&
|
|
92
|
+
entry.goal?.goal.status !== 'active' &&
|
|
93
|
+
!entry.queue.some((item) => item.immediate)) {
|
|
94
|
+
const state = await this.options.inputState(entry, entry.current);
|
|
95
|
+
if (state === 'paused')
|
|
96
|
+
entry.paused = true;
|
|
97
|
+
if (state === 'missing')
|
|
98
|
+
await submit(entry.current);
|
|
99
|
+
else if (state !== 'completed')
|
|
100
|
+
await submit({
|
|
101
|
+
...entry.current,
|
|
102
|
+
id: `restart:${handoffId}:${session.id}`,
|
|
103
|
+
clientMessageId: `restart:${handoffId}:${session.id}`,
|
|
104
|
+
attachments: []
|
|
105
|
+
}, serializeComposerTag('mar_restart', {}));
|
|
106
|
+
}
|
|
107
|
+
for (const input of entry.queue)
|
|
108
|
+
await submit(input);
|
|
109
|
+
if (entry.goal?.goal.status === 'active') {
|
|
110
|
+
const context = contexts.get(entry.ownerUserId);
|
|
111
|
+
await this.options.withGrant(context, () => this.options.messages.submitGoal({
|
|
112
|
+
sessionId: session.id,
|
|
113
|
+
kind: 'continue',
|
|
114
|
+
deliveryIntent: 'QUEUE',
|
|
115
|
+
content: `${goals.prompt(session.id, 'continue')}
|
|
116
|
+
${RESTART_NOTICE}`,
|
|
117
|
+
goal: { id: entry.goal.goal.id, revision: entry.goal.goal.revision, epoch: 0 },
|
|
118
|
+
initiatedByUserId: entry.ownerUserId,
|
|
119
|
+
secretEnvironment: context.secretEnvironment,
|
|
120
|
+
restoreQueued: true,
|
|
121
|
+
...(context.usageGrant === undefined ? {} : { usageGrant: context.usageGrant }),
|
|
122
|
+
...(typeof context.personalInstructions === 'string'
|
|
123
|
+
? { personalInstructions: context.personalInstructions }
|
|
124
|
+
: {}),
|
|
125
|
+
marAgentModel: context.marAgentModel,
|
|
126
|
+
marAgentModels: context.marAgentModels,
|
|
127
|
+
marAgentCompact: context.marAgentCompact,
|
|
128
|
+
marAgentRuntimeScopeId: context.marAgentRuntimeScopeId
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
if (!entry.paused)
|
|
132
|
+
coordinator.resume(session.id);
|
|
133
|
+
this.options.publishQueue(session);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
runtime.pauseSessionQueue(session.id);
|
|
137
|
+
this.options.publishQueue(session);
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
package/dist/service/{scheduled-run-authorization-control.js → run-authorization-control.js}
RENAMED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { createEnvelope, scheduledRunAuthorizeRequestSchema, scheduledRunAuthorizeResponseSchema } from '@myagentroam/protocol';
|
|
2
|
-
export class
|
|
1
|
+
import { createEnvelope, scheduledRunAuthorizeRequestSchema, scheduledRunAuthorizeResponseSchema, restartRunAuthorizeRequestSchema } from '@myagentroam/protocol';
|
|
2
|
+
export class RunAuthorizationControl {
|
|
3
3
|
send;
|
|
4
|
+
purpose;
|
|
4
5
|
#pending = new Map();
|
|
5
6
|
#generation = 0;
|
|
6
7
|
#connected = false;
|
|
7
8
|
#closed = false;
|
|
8
|
-
constructor(send) {
|
|
9
|
+
constructor(send, purpose = 'schedule') {
|
|
9
10
|
this.send = send;
|
|
11
|
+
this.purpose = purpose;
|
|
12
|
+
}
|
|
13
|
+
code(suffix) {
|
|
14
|
+
return `${this.purpose.toUpperCase()}_${suffix}`;
|
|
10
15
|
}
|
|
11
16
|
connected() {
|
|
12
17
|
this.disconnected();
|
|
@@ -16,29 +21,31 @@ export class ScheduledRunAuthorizationControl {
|
|
|
16
21
|
this.#connected = false;
|
|
17
22
|
this.#generation++;
|
|
18
23
|
for (const pending of [...this.#pending.values()])
|
|
19
|
-
pending.reject(new Error('
|
|
24
|
+
pending.reject(new Error(this.code('SERVER_UNAVAILABLE')));
|
|
20
25
|
}
|
|
21
26
|
close() {
|
|
22
27
|
this.#closed = true;
|
|
23
28
|
this.disconnected();
|
|
24
29
|
}
|
|
25
30
|
handle(envelope) {
|
|
26
|
-
if (envelope.type !==
|
|
31
|
+
if (envelope.type !== `${this.purpose}.authorize.response`)
|
|
27
32
|
return false;
|
|
28
33
|
const pending = envelope.replyTo === undefined ? undefined : this.#pending.get(envelope.replyTo);
|
|
29
34
|
if (pending === undefined || pending.generation !== this.#generation)
|
|
30
35
|
return true;
|
|
31
36
|
const parsed = scheduledRunAuthorizeResponseSchema.safeParse(envelope.payload);
|
|
32
37
|
if (!parsed.success)
|
|
33
|
-
pending.reject(new Error('
|
|
38
|
+
pending.reject(new Error(this.code('AUTH_RESPONSE_INVALID')));
|
|
34
39
|
else
|
|
35
40
|
pending.resolve(parsed.data);
|
|
36
41
|
return true;
|
|
37
42
|
}
|
|
38
43
|
authorize(request) {
|
|
39
44
|
if (!this.#connected || this.#closed)
|
|
40
|
-
return Promise.reject(new Error('
|
|
41
|
-
const envelope = createEnvelope(
|
|
45
|
+
return Promise.reject(new Error(this.code('SERVER_UNAVAILABLE')));
|
|
46
|
+
const envelope = createEnvelope(`${this.purpose}.authorize`, (this.purpose === 'schedule'
|
|
47
|
+
? scheduledRunAuthorizeRequestSchema
|
|
48
|
+
: restartRunAuthorizeRequestSchema).parse(request));
|
|
42
49
|
return new Promise((resolve, reject) => {
|
|
43
50
|
const settle = (action) => {
|
|
44
51
|
const pending = this.#pending.get(envelope.id);
|
|
@@ -48,7 +55,7 @@ export class ScheduledRunAuthorizationControl {
|
|
|
48
55
|
clearTimeout(pending.timer);
|
|
49
56
|
action();
|
|
50
57
|
};
|
|
51
|
-
const timer = setTimeout(() => settle(() => reject(new Error('
|
|
58
|
+
const timer = setTimeout(() => settle(() => reject(new Error(this.code('AUTH_TIMEOUT')))), 15_000);
|
|
52
59
|
timer.unref();
|
|
53
60
|
this.#pending.set(envelope.id, {
|
|
54
61
|
generation: this.#generation,
|
|
@@ -65,7 +72,7 @@ export class ScheduledRunAuthorizationControl {
|
|
|
65
72
|
this.send(envelope);
|
|
66
73
|
}
|
|
67
74
|
catch {
|
|
68
|
-
settle(() => reject(new Error('
|
|
75
|
+
settle(() => reject(new Error(this.code('SERVER_UNAVAILABLE'))));
|
|
69
76
|
}
|
|
70
77
|
});
|
|
71
78
|
}
|
|
@@ -232,6 +232,8 @@ export class RunEventService {
|
|
|
232
232
|
const run = this.options.runtime.getRun(runId);
|
|
233
233
|
if (run === undefined)
|
|
234
234
|
return undefined;
|
|
235
|
+
if (run.suppressNotifications === true)
|
|
236
|
+
return undefined;
|
|
235
237
|
if (run.source === 'SCHEDULED' && eventType === 'approval.request')
|
|
236
238
|
return undefined;
|
|
237
239
|
const session = this.options.runtime.getAgentSession(run.sessionId);
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
+
import { SCHEDULE_RESTART_OUTCOME_UNKNOWN } from './scheduled-session-store.js';
|
|
1
2
|
export class ScheduledSessionCoordinator {
|
|
2
3
|
options;
|
|
3
4
|
#now;
|
|
4
5
|
#timer;
|
|
5
6
|
#running = false;
|
|
6
7
|
#closed = true;
|
|
8
|
+
#recovered = [];
|
|
7
9
|
constructor(options) {
|
|
8
10
|
this.options = options;
|
|
9
11
|
this.#now = options.now ?? Date.now;
|
|
10
12
|
}
|
|
13
|
+
/** Called only at Node startup, before admitting any new scheduled occurrence. */
|
|
14
|
+
recoverInterrupted() {
|
|
15
|
+
for (const recovered of this.options.store().recoverInterrupted())
|
|
16
|
+
this.#recovered.push(recovered);
|
|
17
|
+
}
|
|
18
|
+
connected() {
|
|
19
|
+
// Startup has no control connection yet. Send these best-effort alerts once it is ready.
|
|
20
|
+
for (const { task, occurrenceId } of this.#recovered.splice(0)) {
|
|
21
|
+
this.options.notify(task, occurrenceId, SCHEDULE_RESTART_OUTCOME_UNKNOWN, 'FAILED');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
11
24
|
start() {
|
|
12
25
|
if (!this.#closed)
|
|
13
26
|
return;
|