@myagentroam/node 0.10.34 → 0.10.36
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 +113 -18
- package/dist/database.js +27 -12
- package/dist/main.js +18 -2
- package/dist/migrations/v019.js +9 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +28 -0
- package/dist/runner/mar-agent/marctl-skill.js +7 -5
- package/dist/runner/mar-agent-runner.js +32 -0
- package/dist/runtime-state.js +15 -10
- package/dist/service/continuous-run-coordinator.js +31 -0
- 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/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/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 +60 -14
- 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 +8 -3
- package/dist/service/workspace-upload-service.js +128 -29
- package/dist/supervisor.js +7 -3
- package/package.json +4 -4
|
@@ -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);
|
|
@@ -11,6 +11,7 @@ export class SessionCatalogService {
|
|
|
11
11
|
operations() {
|
|
12
12
|
return {
|
|
13
13
|
'session.list': (data) => this.list(record(data)),
|
|
14
|
+
'session.graph': (data) => this.graph(record(data)),
|
|
14
15
|
'session.remove': (data) => this.enqueueMutation(() => this.remove(record(data))),
|
|
15
16
|
'workspace.sessions.cleanup': (data) => this.enqueueMutation(() => this.cleanupWorkspaceSessions(record(data))),
|
|
16
17
|
'session.rewind': (data) => this.enqueueMutation(() => this.rewind(record(data))),
|
|
@@ -18,11 +19,59 @@ export class SessionCatalogService {
|
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
async list(input) {
|
|
22
|
+
const sessions = await this.collect(input);
|
|
23
|
+
return sessionPage(sessions.filter((session) => input.parentSessionId === undefined || session.parentSessionId === input.parentSessionId), input);
|
|
24
|
+
}
|
|
25
|
+
async graph(input) {
|
|
26
|
+
if (typeof input.workspaceId !== 'string')
|
|
27
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
28
|
+
if (typeof input.sessionId !== 'string')
|
|
29
|
+
throw new Error('SESSION_INVALID');
|
|
30
|
+
const limit = input.limit === undefined ? 100 : input.limit;
|
|
31
|
+
if (typeof limit !== 'number' || !Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
32
|
+
throw new Error('SESSION_INVALID');
|
|
33
|
+
const sessions = await this.collect({ workspaceId: input.workspaceId, registeredOnly: true });
|
|
34
|
+
const root = sessions.find((session) => session.id === input.sessionId);
|
|
35
|
+
if (root === undefined)
|
|
36
|
+
throw new Error('SESSION_NOT_FOUND');
|
|
37
|
+
const children = new Map();
|
|
38
|
+
for (const session of sessions) {
|
|
39
|
+
if (session.parentSessionId == null)
|
|
40
|
+
continue;
|
|
41
|
+
const siblings = children.get(session.parentSessionId) ?? [];
|
|
42
|
+
siblings.push(session);
|
|
43
|
+
children.set(session.parentSessionId, siblings);
|
|
44
|
+
}
|
|
45
|
+
const queue = [root];
|
|
46
|
+
const visited = new Set([root.id]);
|
|
47
|
+
const nodes = [];
|
|
48
|
+
for (let index = 0; index < queue.length && index < limit; index += 1) {
|
|
49
|
+
const session = queue[index];
|
|
50
|
+
nodes.push({
|
|
51
|
+
id: session.id,
|
|
52
|
+
parentSessionId: session.parentSessionId ?? null,
|
|
53
|
+
title: session.customTitle ?? session.runnerTitle ?? 'Untitled session',
|
|
54
|
+
runner: session.runner,
|
|
55
|
+
...('activity' in session ? { activity: session.activity } : {})
|
|
56
|
+
});
|
|
57
|
+
for (const child of children.get(session.id) ?? []) {
|
|
58
|
+
if (visited.has(child.id))
|
|
59
|
+
continue;
|
|
60
|
+
visited.add(child.id);
|
|
61
|
+
queue.push(child);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { rootSessionId: root.id, nodes, truncated: queue.length > nodes.length };
|
|
65
|
+
}
|
|
66
|
+
async collect(input) {
|
|
21
67
|
const workspaceId = typeof input.workspaceId === 'string' ? input.workspaceId : undefined;
|
|
68
|
+
const registeredOnly = input.registeredOnly === true;
|
|
69
|
+
if (registeredOnly && workspaceId === undefined)
|
|
70
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
22
71
|
const query = typeof input.query === 'string' && input.query.trim().length > 0
|
|
23
72
|
? input.query.trim().normalize('NFKC').toLocaleLowerCase()
|
|
24
73
|
: undefined;
|
|
25
|
-
const nativeSessions = workspaceId === undefined
|
|
74
|
+
const nativeSessions = registeredOnly || workspaceId === undefined
|
|
26
75
|
? this.options.cachedNative()
|
|
27
76
|
: query === undefined
|
|
28
77
|
? await this.options.cachedWorkspace(workspaceId)
|
|
@@ -57,7 +106,7 @@ export class SessionCatalogService {
|
|
|
57
106
|
this.options.projectionForNative(candidate) === undefined)
|
|
58
107
|
].map((session) => [session.id, session]));
|
|
59
108
|
if (workspaceId !== undefined)
|
|
60
|
-
this.mergePersisted(merged, workspaceId, query === undefined && this.options.discoveryPending(workspaceId));
|
|
109
|
+
this.mergePersisted(merged, workspaceId, !registeredOnly && query === undefined && this.options.discoveryPending(workspaceId));
|
|
61
110
|
const sessions = [...merged.values()].filter((session) => {
|
|
62
111
|
if (query === undefined)
|
|
63
112
|
return true;
|
|
@@ -66,9 +115,9 @@ export class SessionCatalogService {
|
|
|
66
115
|
.toLocaleLowerCase();
|
|
67
116
|
return title.includes(query);
|
|
68
117
|
});
|
|
69
|
-
return
|
|
118
|
+
return workspaceId === undefined
|
|
70
119
|
? sessions
|
|
71
|
-
: sessions.filter((session) => session.workspaceId === workspaceId)
|
|
120
|
+
: sessions.filter((session) => session.workspaceId === workspaceId);
|
|
72
121
|
}
|
|
73
122
|
mergePersisted(merged, workspaceId, discoveryPending) {
|
|
74
123
|
const database = this.options.database();
|
|
@@ -95,6 +144,7 @@ export class SessionCatalogService {
|
|
|
95
144
|
access: persisted.metadata.access,
|
|
96
145
|
customTitle: persisted.metadata.customTitle,
|
|
97
146
|
sessionPrompt: persisted.metadata.sessionPrompt ?? null,
|
|
147
|
+
parentSessionId: persisted.metadata.parentSessionId ?? null,
|
|
98
148
|
runnerTitle: persisted.metadata.runnerTitle,
|
|
99
149
|
pinnedAt: persisted.pinOrder === null ? null : 1,
|
|
100
150
|
pinOrder: persisted.pinOrder,
|
|
@@ -249,6 +299,7 @@ export class SessionCatalogService {
|
|
|
249
299
|
session.lastActivityAt >= cutoff)) ||
|
|
250
300
|
(session.pinOrder !== null && session.pinOrder !== undefined) ||
|
|
251
301
|
this.options.isWatching(session.id) ||
|
|
302
|
+
this.options.isRecovering?.(session.id) ||
|
|
252
303
|
this.options.runtime.sessionQueue(session.id).itemIds.length > 0 ||
|
|
253
304
|
this.options.runtime
|
|
254
305
|
.listRunsForSession(session.id)
|
|
@@ -301,6 +352,8 @@ export class SessionCatalogService {
|
|
|
301
352
|
? this.options.runtime.beginSessionHistoryMutation(session.id)
|
|
302
353
|
: undefined;
|
|
303
354
|
try {
|
|
355
|
+
if (this.options.isRecovering?.(session.id))
|
|
356
|
+
throw new Error('SESSION_RESTART_PENDING');
|
|
304
357
|
const runner = this.options.runners.require(session.runner);
|
|
305
358
|
const missingCandidate = requireCleanupSafety && session.externalDiscovered === false;
|
|
306
359
|
if (missingCandidate && !(await runner.isNativeSessionMissing(session)))
|
|
@@ -72,6 +72,7 @@ export class SessionCommandService {
|
|
|
72
72
|
return this.options.goal({
|
|
73
73
|
session,
|
|
74
74
|
objective: invocation.argument,
|
|
75
|
+
...(input.__suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
75
76
|
...(input.__usageGrant === undefined
|
|
76
77
|
? {}
|
|
77
78
|
: { usageGrant: modelUsageGrantSchema.parse(input.__usageGrant) }),
|
|
@@ -97,6 +97,20 @@ export class SessionLifecycleService {
|
|
|
97
97
|
const workspace = database.getWorkspace(input.workspaceId);
|
|
98
98
|
if (workspace === undefined)
|
|
99
99
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
100
|
+
let parentSessionId;
|
|
101
|
+
if (input.parentSessionId !== undefined) {
|
|
102
|
+
if (typeof input.parentSessionId !== 'string' ||
|
|
103
|
+
input.parentSessionId.length === 0 ||
|
|
104
|
+
input.parentSessionId.length > 512)
|
|
105
|
+
throw new Error('SESSION_INVALID');
|
|
106
|
+
const liveParent = this.options.runtime.getAgentSession(input.parentSessionId);
|
|
107
|
+
const parent = liveParent ?? database.getSessionRecord(input.parentSessionId);
|
|
108
|
+
if (parent === undefined || parent.workspaceId !== workspace.id)
|
|
109
|
+
throw new Error('SESSION_NOT_FOUND');
|
|
110
|
+
if (liveParent?.externalSessionId === null)
|
|
111
|
+
throw new Error('SESSION_ACTIVATION_REQUIRED');
|
|
112
|
+
parentSessionId = liveParent?.externalSessionId ?? parent.id;
|
|
113
|
+
}
|
|
100
114
|
const environment = parseSecretEnvironment(input.secretEnvironment);
|
|
101
115
|
const saved = database.getRunnerDefaults().find((value) => value.runner === runner.name);
|
|
102
116
|
const fallback = runner.defaultConfiguration(workspace, environment);
|
|
@@ -113,6 +127,7 @@ export class SessionLifecycleService {
|
|
|
113
127
|
if (runner.name === 'mar-agent' && !['auto', 'full-access'].includes(configuration.access))
|
|
114
128
|
throw new Error('SESSION_INVALID');
|
|
115
129
|
const session = this.options.runtime.createAgentSession({
|
|
130
|
+
...(parentSessionId === undefined ? {} : { parentSessionId }),
|
|
116
131
|
workspaceId: input.workspaceId,
|
|
117
132
|
runner: runner.name,
|
|
118
133
|
cwd: input.cwd,
|
|
@@ -134,24 +149,25 @@ export class SessionLifecycleService {
|
|
|
134
149
|
}
|
|
135
150
|
async updateConfiguration(input) {
|
|
136
151
|
if (typeof input.sessionId !== 'string' ||
|
|
137
|
-
|
|
152
|
+
(input.model === undefined && input.effort === undefined && input.access === undefined) ||
|
|
138
153
|
!validSessionOption(input.model) ||
|
|
139
|
-
typeof input.effort !== 'string' ||
|
|
140
154
|
!validSessionEffort(input.effort) ||
|
|
141
|
-
typeof input.access !== 'string' ||
|
|
142
155
|
!validSessionOption(input.access))
|
|
143
156
|
throw new Error('SESSION_INVALID');
|
|
144
157
|
const current = this.options.runtime.getAgentSession(input.sessionId) ??
|
|
145
158
|
(await this.options.projection.createMetadataProjection(input.sessionId));
|
|
146
|
-
if (current === undefined
|
|
159
|
+
if (current === undefined ||
|
|
160
|
+
(typeof input.workspaceId === 'string' && input.workspaceId !== current.workspaceId))
|
|
147
161
|
throw new Error('SESSION_NOT_FOUND');
|
|
148
|
-
if (current.runner === 'mar-agent' &&
|
|
162
|
+
if (current.runner === 'mar-agent' &&
|
|
163
|
+
input.access !== undefined &&
|
|
164
|
+
!['auto', 'full-access'].includes(input.access))
|
|
149
165
|
throw new Error('SESSION_INVALID');
|
|
150
166
|
const session = this.options.runtime.updateAgentSessionConfiguration({
|
|
151
167
|
sessionId: current.id,
|
|
152
|
-
model: input.model,
|
|
153
|
-
effort: input.effort,
|
|
154
|
-
access: input.access
|
|
168
|
+
...(typeof input.model === 'string' ? { model: input.model } : {}),
|
|
169
|
+
...(typeof input.effort === 'string' ? { effort: input.effort } : {}),
|
|
170
|
+
...(typeof input.access === 'string' ? { access: input.access } : {})
|
|
155
171
|
});
|
|
156
172
|
if (session.externalSessionId !== null)
|
|
157
173
|
this.options.database().saveNativeSessionMetadata({
|
|
@@ -232,6 +248,7 @@ export class SessionLifecycleService {
|
|
|
232
248
|
sessionId: messageSessionId,
|
|
233
249
|
clientMessageId: input.clientMessageId,
|
|
234
250
|
content: input.content,
|
|
251
|
+
...(input.__suppressNotifications === true ? { __suppressNotifications: true } : {}),
|
|
235
252
|
...(input.__requestUserId === undefined ? {} : { __requestUserId: input.__requestUserId }),
|
|
236
253
|
...(input.__usageGrant === undefined ? {} : { __usageGrant: input.__usageGrant }),
|
|
237
254
|
...(input.secretEnvironment === undefined
|
|
@@ -10,12 +10,24 @@ import { relative } from 'node:path';
|
|
|
10
10
|
import { prepareComposerResourceInput, composerResourceInputTags, validateComposerSkills } from './composer-message-input.js';
|
|
11
11
|
const INTERNAL_GOAL = Symbol('internalGoal');
|
|
12
12
|
const INTERNAL_SCHEDULE = Symbol('internalSchedule');
|
|
13
|
+
const INTERNAL_RESTORE = Symbol('internalRestore');
|
|
13
14
|
export class SessionMessageService {
|
|
14
15
|
options;
|
|
15
16
|
pendingMessages = new Map();
|
|
17
|
+
preparations = new Set();
|
|
18
|
+
accepting = true;
|
|
16
19
|
constructor(options) {
|
|
17
20
|
this.options = options;
|
|
18
21
|
}
|
|
22
|
+
async drain() {
|
|
23
|
+
await Promise.allSettled([...this.preparations]);
|
|
24
|
+
}
|
|
25
|
+
setAccepting(accepting) {
|
|
26
|
+
this.accepting = accepting;
|
|
27
|
+
}
|
|
28
|
+
restoreQueued(input) {
|
|
29
|
+
return this.message({ ...input, [INTERNAL_RESTORE]: true });
|
|
30
|
+
}
|
|
19
31
|
operations() {
|
|
20
32
|
return {
|
|
21
33
|
'session.message': (data) => this.message(record(data)),
|
|
@@ -26,7 +38,11 @@ export class SessionMessageService {
|
|
|
26
38
|
submitGoal(input) {
|
|
27
39
|
return this.message({
|
|
28
40
|
[INTERNAL_GOAL]: true,
|
|
41
|
+
[INTERNAL_RESTORE]: input.restoreQueued === true,
|
|
29
42
|
sessionId: input.sessionId,
|
|
43
|
+
__suppressNotifications: input.suppressNotifications === true ||
|
|
44
|
+
(input.kind === 'continue' &&
|
|
45
|
+
this.options.runtime.activeSessionRun(input.sessionId)?.suppressNotifications === true),
|
|
30
46
|
clientMessageId: `goal:${randomUUID()}`,
|
|
31
47
|
content: input.kind === 'start'
|
|
32
48
|
? publicGoalStartText(input.objective ?? '')
|
|
@@ -64,7 +80,14 @@ export class SessionMessageService {
|
|
|
64
80
|
marAgentRuntimeScopeId: input.marAgentRuntimeScopeId
|
|
65
81
|
});
|
|
66
82
|
}
|
|
67
|
-
|
|
83
|
+
message(input) {
|
|
84
|
+
if (!this.accepting)
|
|
85
|
+
return Promise.reject(new Error('NODE_STOPPED'));
|
|
86
|
+
const task = this.prepareMessage(input);
|
|
87
|
+
this.preparations.add(task);
|
|
88
|
+
return task.finally(() => this.preparations.delete(task));
|
|
89
|
+
}
|
|
90
|
+
async prepareMessage(input) {
|
|
68
91
|
if (typeof input.sessionId !== 'string' ||
|
|
69
92
|
typeof input.clientMessageId !== 'string' ||
|
|
70
93
|
input.clientMessageId.length === 0 ||
|
|
@@ -77,7 +100,9 @@ export class SessionMessageService {
|
|
|
77
100
|
const content = input.content;
|
|
78
101
|
const internalGoal = input[INTERNAL_GOAL] === true;
|
|
79
102
|
const internalSchedule = input[INTERNAL_SCHEDULE] === true;
|
|
80
|
-
const
|
|
103
|
+
const internalRestore = input[INTERNAL_RESTORE] === true;
|
|
104
|
+
const internalRunnerContent = (internalGoal || internalSchedule || internalRestore) &&
|
|
105
|
+
typeof input.__runnerContent === 'string'
|
|
81
106
|
? input.__runnerContent
|
|
82
107
|
: content;
|
|
83
108
|
const inlineAttachments = parseMessageAttachments(input.attachments);
|
|
@@ -112,9 +137,9 @@ export class SessionMessageService {
|
|
|
112
137
|
...(existing.kind === 'queue' ? { queueItemId: existing.item.id } : {}),
|
|
113
138
|
delivery: existing.kind === 'queue' ? 'QUEUED' : 'STARTED',
|
|
114
139
|
idempotent: true,
|
|
115
|
-
auditReceipt: auditReceipt(session, existing.kind === 'queue' ? 'QUEUE' : 'SEND', existing.run.id, existing.kind === 'queue' ? existing.item.id : undefined)
|
|
140
|
+
auditReceipt: auditReceipt(session, existing.kind === 'queue' ? 'QUEUE' : 'SEND', existing.kind === 'queue' ? existing.item.id : existing.run.id, existing.kind === 'queue' ? existing.item.id : undefined)
|
|
116
141
|
};
|
|
117
|
-
const goalMessage = internalGoal || internalSchedule
|
|
142
|
+
const goalMessage = internalGoal || internalSchedule || internalRestore
|
|
118
143
|
? undefined
|
|
119
144
|
: this.options.goalMessage(session, content, requestedIntent);
|
|
120
145
|
const runnerContent = goalMessage?.runnerContent ?? internalRunnerContent;
|
|
@@ -128,23 +153,35 @@ export class SessionMessageService {
|
|
|
128
153
|
const operation = (async () => {
|
|
129
154
|
const acceptedSession = accepted.session;
|
|
130
155
|
const session = this.options.runtime.getAgentSession(acceptedSession.id) ?? acceptedSession;
|
|
131
|
-
const runId =
|
|
156
|
+
const runId = internalRestore && typeof input.__queueItemId === 'string'
|
|
157
|
+
? input.__queueItemId
|
|
158
|
+
: randomUUID();
|
|
132
159
|
const queueBeforeCreate = this.options.runtime.sessionQueue(session.id);
|
|
133
160
|
const active = this.options.runtime.activeSessionRun(session.id);
|
|
134
161
|
if (internalSchedule && (active !== undefined || queueBeforeCreate.itemIds.length > 0))
|
|
135
162
|
throw new Error('SCHEDULE_SESSION_BUSY');
|
|
136
163
|
const replaceCurrentRun = intent === 'REPLACE_CURRENT' ? active : undefined;
|
|
137
|
-
const
|
|
164
|
+
const acceptedMessage = {
|
|
138
165
|
runId,
|
|
139
166
|
sessionId: session.id,
|
|
140
167
|
clientMessageId,
|
|
141
168
|
content: publicContent,
|
|
142
169
|
...(internalSchedule ? { source: 'SCHEDULED' } : {}),
|
|
170
|
+
...(input.__suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
143
171
|
...(Number.isSafeInteger(input.__requestUserId)
|
|
144
172
|
? { initiatedByUserId: input.__requestUserId }
|
|
145
173
|
: {}),
|
|
146
174
|
...(inlineAttachments.length === 0 ? {} : { attachments: inlineAttachments })
|
|
147
|
-
}
|
|
175
|
+
};
|
|
176
|
+
const created = internalRestore
|
|
177
|
+
? {
|
|
178
|
+
run: undefined,
|
|
179
|
+
queueItem: this.options.runtime.createQueueItem({
|
|
180
|
+
...acceptedMessage,
|
|
181
|
+
queueItemId: runId
|
|
182
|
+
}).item
|
|
183
|
+
}
|
|
184
|
+
: this.options.coordinator.accept(acceptedMessage, secretEnvironment);
|
|
148
185
|
const submissionId = created.queueItem?.id ?? created.run.id;
|
|
149
186
|
const sessionForPresentation = input.__goal === true
|
|
150
187
|
? session
|
|
@@ -193,6 +230,13 @@ export class SessionMessageService {
|
|
|
193
230
|
else
|
|
194
231
|
this.options.runtime.updateQueueItemAttachments(created.queueItem.id, attachments, displayInput);
|
|
195
232
|
this.options.coordinator.prepare(submissionId, {
|
|
233
|
+
restartMessage: {
|
|
234
|
+
content,
|
|
235
|
+
attachments,
|
|
236
|
+
...(Number.isSafeInteger(input.__requestUserId)
|
|
237
|
+
? { initiatedByUserId: input.__requestUserId }
|
|
238
|
+
: {})
|
|
239
|
+
},
|
|
196
240
|
...(input.__usageGrant === undefined
|
|
197
241
|
? {}
|
|
198
242
|
: { usageGrant: modelUsageGrantSchema.parse(input.__usageGrant) }),
|
|
@@ -235,12 +279,14 @@ export class SessionMessageService {
|
|
|
235
279
|
!this.options.prepareGoalInsertion(session.id, submissionId, goalMessage.goal))
|
|
236
280
|
throw new Error('GOAL_REVISION_STALE');
|
|
237
281
|
this.options.attachments.retainSessionFiles(currentSession.id, submissionId);
|
|
238
|
-
const delivery =
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
282
|
+
const delivery = internalRestore
|
|
283
|
+
? 'QUEUED'
|
|
284
|
+
: this.options.coordinator.settle({
|
|
285
|
+
submissionId,
|
|
286
|
+
intent,
|
|
287
|
+
queueBefore: queueBeforeCreate,
|
|
288
|
+
...(replaceCurrentRun === undefined ? {} : { activeRunId: replaceCurrentRun.id })
|
|
289
|
+
});
|
|
244
290
|
reservation?.commit((directories) => this.options.attachments.adoptSessionFiles(currentSession.id, submissionId, directories, currentSession.workspaceId, attachmentPaths));
|
|
245
291
|
this.options.attachments.acceptSessionInput(currentSession.id);
|
|
246
292
|
return delivery;
|
|
@@ -263,7 +309,7 @@ export class SessionMessageService {
|
|
|
263
309
|
...(created.queueItem === undefined ? {} : { queueItemId: created.queueItem.id }),
|
|
264
310
|
delivery,
|
|
265
311
|
idempotent: false,
|
|
266
|
-
auditReceipt: auditReceipt(sessionForPresentation, intent === 'QUEUE' ? 'QUEUE' : intent === 'REPLACE_CURRENT' ? 'REPLACE_CURRENT' : 'SEND', created.run
|
|
312
|
+
auditReceipt: auditReceipt(sessionForPresentation, intent === 'QUEUE' ? 'QUEUE' : intent === 'REPLACE_CURRENT' ? 'REPLACE_CURRENT' : 'SEND', created.run?.id ?? submissionId, created.queueItem?.id)
|
|
267
313
|
};
|
|
268
314
|
})();
|
|
269
315
|
this.pendingMessages.set(pendingKey, operation);
|
|
@@ -12,6 +12,7 @@ export class SessionPresentationService {
|
|
|
12
12
|
session = {
|
|
13
13
|
...session,
|
|
14
14
|
sessionPrompt: metadata.sessionPrompt ?? null,
|
|
15
|
+
parentSessionId: metadata.parentSessionId ?? session.parentSessionId ?? null,
|
|
15
16
|
pinnedAt: metadata.pinnedAt,
|
|
16
17
|
pinOrder: metadata.pinOrder
|
|
17
18
|
};
|
|
@@ -24,6 +25,7 @@ export class SessionPresentationService {
|
|
|
24
25
|
});
|
|
25
26
|
return {
|
|
26
27
|
...session,
|
|
28
|
+
parentSessionId: session.parentSessionId ?? null,
|
|
27
29
|
...presentation,
|
|
28
30
|
runtime: {
|
|
29
31
|
...presentation.runtime,
|