@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { assertScheduleTimezone, nextScheduledAt, parseScheduledCron } from './scheduled-session-recurrence.js';
|
|
3
|
+
export const SCHEDULE_RESTART_OUTCOME_UNKNOWN = 'SCHEDULE_RESTART_OUTCOME_UNKNOWN';
|
|
3
4
|
export class ScheduledSessionStore {
|
|
4
5
|
database;
|
|
5
6
|
now;
|
|
@@ -176,6 +177,26 @@ export class ScheduledSessionStore {
|
|
|
176
177
|
throw error;
|
|
177
178
|
}
|
|
178
179
|
}
|
|
180
|
+
recoverInterrupted() {
|
|
181
|
+
return this.write(() => {
|
|
182
|
+
// FAILED closes scheduling bookkeeping, not the unknown execution or its side effects.
|
|
183
|
+
const rows = this.database
|
|
184
|
+
.prepare(`UPDATE scheduled_session_occurrences SET status='FAILED',code=?,updated_at=?
|
|
185
|
+
WHERE status IN ('CLAIMED','SUBMITTED') RETURNING id,task_id`)
|
|
186
|
+
.all(SCHEDULE_RESTART_OUTCOME_UNKNOWN, this.now());
|
|
187
|
+
const tasks = new Map();
|
|
188
|
+
for (const row of rows) {
|
|
189
|
+
if (tasks.has(row.task_id))
|
|
190
|
+
continue;
|
|
191
|
+
const task = this.getById(row.task_id);
|
|
192
|
+
if (task === undefined)
|
|
193
|
+
throw new Error('SCHEDULE_TASK_NOT_FOUND');
|
|
194
|
+
tasks.set(task.id, task);
|
|
195
|
+
this.pruneTerminal(row.id);
|
|
196
|
+
}
|
|
197
|
+
return rows.map((row) => ({ task: tasks.get(row.task_id), occurrenceId: row.id }));
|
|
198
|
+
});
|
|
199
|
+
}
|
|
179
200
|
finish(occurrenceId, status, code, runId) {
|
|
180
201
|
const result = this.database
|
|
181
202
|
.prepare(`UPDATE scheduled_session_occurrences SET status=?,code=?,run_id=?,updated_at=?
|
|
@@ -183,6 +204,9 @@ export class ScheduledSessionStore {
|
|
|
183
204
|
.run(status, code ?? null, runId ?? null, this.now(), occurrenceId);
|
|
184
205
|
if (result.changes === 0 || status === 'SUBMITTED')
|
|
185
206
|
return;
|
|
207
|
+
this.pruneTerminal(occurrenceId);
|
|
208
|
+
}
|
|
209
|
+
pruneTerminal(occurrenceId) {
|
|
186
210
|
this.database
|
|
187
211
|
.prepare(`DELETE FROM scheduled_session_occurrences
|
|
188
212
|
WHERE task_id=(SELECT task_id FROM scheduled_session_occurrences WHERE id=?)
|
|
@@ -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
|
|
@@ -5,17 +5,29 @@ import { parseSecretEnvironment } from '../util/secret-environment.js';
|
|
|
5
5
|
import { personalInstructionsForRunner } from '../util/personal-instructions.js';
|
|
6
6
|
import { safeErrorCode } from '../util/safe-error.js';
|
|
7
7
|
import { publicGoalStartText } from './goal-runtime.js';
|
|
8
|
-
import { serializeComposerTag, composerVisibleText } from '@myagentroam/protocol';
|
|
8
|
+
import { serializeComposerTag, composerVisibleText, steerComposerMessage } from '@myagentroam/protocol';
|
|
9
9
|
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,48 @@ 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 userSteer = requestedIntent === 'REPLACE_CURRENT' &&
|
|
165
|
+
goalMessage?.activated !== true &&
|
|
166
|
+
!internalGoal &&
|
|
167
|
+
!internalSchedule &&
|
|
168
|
+
!internalRestore;
|
|
169
|
+
const steerTarget = userSteer
|
|
170
|
+
? {
|
|
171
|
+
turnId: active === undefined
|
|
172
|
+
? undefined
|
|
173
|
+
: this.options.runtime.conversationTurnForRun(active.id)?.id,
|
|
174
|
+
executionId: active === undefined ? undefined : this.options.runtime.currentExecutionId(active.id)
|
|
175
|
+
}
|
|
176
|
+
: undefined;
|
|
177
|
+
const acceptedMessage = {
|
|
138
178
|
runId,
|
|
139
179
|
sessionId: session.id,
|
|
140
180
|
clientMessageId,
|
|
141
181
|
content: publicContent,
|
|
142
182
|
...(internalSchedule ? { source: 'SCHEDULED' } : {}),
|
|
183
|
+
...(input.__suppressNotifications === true ? { suppressNotifications: true } : {}),
|
|
143
184
|
...(Number.isSafeInteger(input.__requestUserId)
|
|
144
185
|
? { initiatedByUserId: input.__requestUserId }
|
|
145
186
|
: {}),
|
|
146
187
|
...(inlineAttachments.length === 0 ? {} : { attachments: inlineAttachments })
|
|
147
|
-
}
|
|
188
|
+
};
|
|
189
|
+
const created = internalRestore
|
|
190
|
+
? {
|
|
191
|
+
run: undefined,
|
|
192
|
+
queueItem: this.options.runtime.createQueueItem({
|
|
193
|
+
...acceptedMessage,
|
|
194
|
+
queueItemId: runId
|
|
195
|
+
}).item
|
|
196
|
+
}
|
|
197
|
+
: this.options.coordinator.accept(acceptedMessage, secretEnvironment);
|
|
148
198
|
const submissionId = created.queueItem?.id ?? created.run.id;
|
|
149
199
|
const sessionForPresentation = input.__goal === true
|
|
150
200
|
? session
|
|
@@ -193,6 +243,13 @@ export class SessionMessageService {
|
|
|
193
243
|
else
|
|
194
244
|
this.options.runtime.updateQueueItemAttachments(created.queueItem.id, attachments, displayInput);
|
|
195
245
|
this.options.coordinator.prepare(submissionId, {
|
|
246
|
+
restartMessage: {
|
|
247
|
+
content,
|
|
248
|
+
attachments,
|
|
249
|
+
...(Number.isSafeInteger(input.__requestUserId)
|
|
250
|
+
? { initiatedByUserId: input.__requestUserId }
|
|
251
|
+
: {})
|
|
252
|
+
},
|
|
196
253
|
...(input.__usageGrant === undefined
|
|
197
254
|
? {}
|
|
198
255
|
: { usageGrant: modelUsageGrantSchema.parse(input.__usageGrant) }),
|
|
@@ -201,6 +258,11 @@ export class SessionMessageService {
|
|
|
201
258
|
workspaceId: currentSession.workspaceId,
|
|
202
259
|
runner: currentSession.runner,
|
|
203
260
|
input: runner.prepareMessageInput(runnerInput, planActive ? 'plan' : 'default'),
|
|
261
|
+
...(userSteer
|
|
262
|
+
? {
|
|
263
|
+
prepareSteerInput: () => runner.prepareMessageInput(steerComposerMessage(runnerInput), planActive ? 'plan' : 'default')
|
|
264
|
+
}
|
|
265
|
+
: {}),
|
|
204
266
|
clientMessageId,
|
|
205
267
|
attachments: runnerImageAttachments,
|
|
206
268
|
attachmentPaths,
|
|
@@ -235,12 +297,15 @@ export class SessionMessageService {
|
|
|
235
297
|
!this.options.prepareGoalInsertion(session.id, submissionId, goalMessage.goal))
|
|
236
298
|
throw new Error('GOAL_REVISION_STALE');
|
|
237
299
|
this.options.attachments.retainSessionFiles(currentSession.id, submissionId);
|
|
238
|
-
const delivery =
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
300
|
+
const delivery = internalRestore
|
|
301
|
+
? 'QUEUED'
|
|
302
|
+
: this.options.coordinator.settle({
|
|
303
|
+
submissionId,
|
|
304
|
+
intent,
|
|
305
|
+
queueBefore: queueBeforeCreate,
|
|
306
|
+
...(steerTarget === undefined ? {} : { steerTarget }),
|
|
307
|
+
...(replaceCurrentRun === undefined ? {} : { activeRunId: replaceCurrentRun.id })
|
|
308
|
+
});
|
|
244
309
|
reservation?.commit((directories) => this.options.attachments.adoptSessionFiles(currentSession.id, submissionId, directories, currentSession.workspaceId, attachmentPaths));
|
|
245
310
|
this.options.attachments.acceptSessionInput(currentSession.id);
|
|
246
311
|
return delivery;
|
|
@@ -263,7 +328,7 @@ export class SessionMessageService {
|
|
|
263
328
|
...(created.queueItem === undefined ? {} : { queueItemId: created.queueItem.id }),
|
|
264
329
|
delivery,
|
|
265
330
|
idempotent: false,
|
|
266
|
-
auditReceipt: auditReceipt(sessionForPresentation, intent === 'QUEUE' ? 'QUEUE' : intent === 'REPLACE_CURRENT' ? 'REPLACE_CURRENT' : 'SEND', created.run
|
|
331
|
+
auditReceipt: auditReceipt(sessionForPresentation, intent === 'QUEUE' ? 'QUEUE' : intent === 'REPLACE_CURRENT' ? 'REPLACE_CURRENT' : 'SEND', created.run?.id ?? submissionId, created.queueItem?.id)
|
|
267
332
|
};
|
|
268
333
|
})();
|
|
269
334
|
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,
|
|
@@ -11,7 +11,7 @@ export class SessionQueryService {
|
|
|
11
11
|
'session.cli.context': (data) => this.cliContext(record(data)),
|
|
12
12
|
'session.goal.get': async (data) => {
|
|
13
13
|
const input = record(data);
|
|
14
|
-
const session = await this.requireSession(input.sessionId, input.workspaceId);
|
|
14
|
+
const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
|
|
15
15
|
return { goal: this.options.goal?.(session.id) ?? null };
|
|
16
16
|
},
|
|
17
17
|
'session.runs': (data) => this.runs(record(data)),
|
|
@@ -24,7 +24,7 @@ export class SessionQueryService {
|
|
|
24
24
|
}
|
|
25
25
|
async get(input) {
|
|
26
26
|
return {
|
|
27
|
-
session: this.options.present(await this.requireSession(input.sessionId, input.workspaceId))
|
|
27
|
+
session: this.options.present(await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true))
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
async cliContext(input) {
|
|
@@ -35,7 +35,7 @@ export class SessionQueryService {
|
|
|
35
35
|
throw new Error('WORKSPACE_NOT_FOUND');
|
|
36
36
|
const session = input.sessionId === undefined
|
|
37
37
|
? undefined
|
|
38
|
-
: await this.requireSession(input.sessionId, workspace.id);
|
|
38
|
+
: await this.requireSession(input.sessionId, workspace.id, input.registeredOnly === true);
|
|
39
39
|
return {
|
|
40
40
|
workspace: { id: workspace.id, path: workspace.path },
|
|
41
41
|
session: session === undefined ? null : this.options.present(session),
|
|
@@ -43,12 +43,12 @@ export class SessionQueryService {
|
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
45
|
async runs(input) {
|
|
46
|
-
const session = await this.requireSession(input.sessionId, input.workspaceId);
|
|
46
|
+
const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
|
|
47
47
|
return { runs: this.options.runtime.listRunsForSession(session.id, { includeTerminal: true }) };
|
|
48
48
|
}
|
|
49
49
|
async turns(input) {
|
|
50
50
|
const page = pageInput(input);
|
|
51
|
-
const session = await this.requireSession(input.sessionId, input.workspaceId);
|
|
51
|
+
const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
|
|
52
52
|
if (input.unit === 'SEGMENT')
|
|
53
53
|
return this.options.segments(session, page);
|
|
54
54
|
if (input.unit !== undefined && input.unit !== 'TURN')
|
|
@@ -66,7 +66,7 @@ export class SessionQueryService {
|
|
|
66
66
|
return this.options.runtime.listConversationItems({ sessionId: session.id, ...page });
|
|
67
67
|
}
|
|
68
68
|
async readToolResource(input) {
|
|
69
|
-
const session = await this.requireSession(input.sessionId, input.workspaceId);
|
|
69
|
+
const session = await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true);
|
|
70
70
|
if (typeof input.workspaceId !== 'string' || input.workspaceId !== session.workspaceId)
|
|
71
71
|
throw new Error('SESSION_NOT_FOUND');
|
|
72
72
|
const resource = toolResultResourceInputSchema.parse({
|
|
@@ -80,7 +80,7 @@ export class SessionQueryService {
|
|
|
80
80
|
return toolResultResourcePageSchema.parse(await runner.readToolResultResource(session, resource));
|
|
81
81
|
}
|
|
82
82
|
async readContext(input) {
|
|
83
|
-
return this.options.context.read(await this.requireSession(input.sessionId, input.workspaceId));
|
|
83
|
+
return this.options.context.read(await this.requireSession(input.sessionId, input.workspaceId, input.registeredOnly === true));
|
|
84
84
|
}
|
|
85
85
|
async discover(input) {
|
|
86
86
|
const runner = this.options.runners.product(input.runner);
|
|
@@ -96,10 +96,15 @@ export class SessionQueryService {
|
|
|
96
96
|
const sessions = await this.options.discover(runner.name, workspaceId);
|
|
97
97
|
return { sessions: sessions.map((session) => this.options.present(session)) };
|
|
98
98
|
}
|
|
99
|
-
async requireSession(sessionId, workspaceId) {
|
|
99
|
+
async requireSession(sessionId, workspaceId, registeredOnly = false) {
|
|
100
100
|
if (typeof sessionId !== 'string')
|
|
101
101
|
throw new Error('SESSION_INVALID');
|
|
102
|
-
const session = this.options.runtime.getAgentSession(sessionId) ??
|
|
102
|
+
const session = this.options.runtime.getAgentSession(sessionId) ??
|
|
103
|
+
(await this.options.resolve(sessionId, registeredOnly));
|
|
104
|
+
if (session === undefined && registeredOnly && typeof workspaceId === 'string')
|
|
105
|
+
throw Object.assign(new Error('SESSION_NOT_FOUND'), {
|
|
106
|
+
publicDetails: this.options.similar(sessionId, workspaceId)
|
|
107
|
+
});
|
|
103
108
|
if (session === undefined || (workspaceId !== undefined && session.workspaceId !== workspaceId))
|
|
104
109
|
throw new Error('SESSION_NOT_FOUND');
|
|
105
110
|
return session;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { steerComposerMessage } from '@myagentroam/protocol';
|
|
1
2
|
import { isPlainRecord, withNonImageAttachmentPrompt } from '../util/node-operation-parsers.js';
|
|
2
3
|
import { assertQueueItemOwner, assertSessionControlOwner } from './run-ownership-policy.js';
|
|
3
4
|
export class WorkspaceQueueWorkbenchService {
|
|
@@ -63,10 +64,15 @@ export class WorkspaceQueueWorkbenchService {
|
|
|
63
64
|
})
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
|
-
get(input) {
|
|
67
|
-
|
|
67
|
+
async get(input) {
|
|
68
|
+
if (typeof input.workspaceId !== 'string' || typeof input.sessionId !== 'string')
|
|
69
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
70
|
+
const session = this.options.runtime.getAgentSession(input.sessionId) ??
|
|
71
|
+
(await this.options.resolve(input.sessionId, input.registeredOnly === true));
|
|
72
|
+
if (session?.workspaceId !== input.workspaceId)
|
|
73
|
+
throw new Error('SESSION_NOT_FOUND');
|
|
68
74
|
return {
|
|
69
|
-
queue: this.
|
|
75
|
+
queue: this.snapshot(session.id)
|
|
70
76
|
};
|
|
71
77
|
}
|
|
72
78
|
pauseOrResume(mode, input) {
|
|
@@ -113,12 +119,13 @@ export class WorkspaceQueueWorkbenchService {
|
|
|
113
119
|
const item = this.requireItem(input.queueItemId, input.workspaceId);
|
|
114
120
|
assertQueueItemOwner(item, input.__requestUserId);
|
|
115
121
|
const content = input.content.trim();
|
|
116
|
-
this.options.runtime.updateQueueItem(item.id, content);
|
|
117
122
|
const prepared = this.options.coordinator.pending(item.id);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.options.
|
|
121
|
-
|
|
123
|
+
const runnerInput = prepared === undefined
|
|
124
|
+
? undefined
|
|
125
|
+
: this.options.prepareInput(prepared.runner, withNonImageAttachmentPrompt(prepared.steering ? steerComposerMessage(content) : content, prepared.attachmentPaths, prepared.attachmentFiles), prepared.collaborationMode);
|
|
126
|
+
this.options.runtime.updateQueueItem(item.id, content);
|
|
127
|
+
if (runnerInput !== undefined)
|
|
128
|
+
this.options.coordinator.updatePreparedInput(item.id, runnerInput);
|
|
122
129
|
this.options.emit(item.workspaceId, item.sessionId);
|
|
123
130
|
return {
|
|
124
131
|
queue: this.present(item.workspaceId, item.sessionId),
|