@myagentroam/node 0.11.46 → 0.11.48
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 +13 -33
- package/dist/marctl-guidance.js +2 -2
- package/dist/runner/mar-agent/agent-network-extension.js +65 -26
- package/dist/runner/mar-agent/agent-operation-presentation.js +1 -1
- package/dist/runner/mar-agent/conversation-normalizer.js +43 -3
- package/dist/service/agent-network-bindings.js +51 -75
- package/dist/service/agent-network-invoke.js +10 -11
- package/dist/service/agent-network-list.js +71 -0
- package/dist/service/agent-network-service.js +12 -6
- package/dist/service/composer-message-input.js +11 -5
- package/dist/service/runner-service.js +5 -1
- package/dist/session/native-session-projection-service.js +31 -22
- package/dist/session/session-catalog-service.js +114 -88
- package/dist/workspace/git.js +12 -0
- package/dist/workspace/workspace-change-service.js +15 -6
- package/package.json +5 -5
package/dist/connector.js
CHANGED
|
@@ -651,25 +651,29 @@ export class NodeConnector {
|
|
|
651
651
|
}),
|
|
652
652
|
resources: async (signal) => {
|
|
653
653
|
signal.throwIfAborted();
|
|
654
|
-
const
|
|
655
|
-
if (!
|
|
654
|
+
const currentSession = this.runtime.getAgentSessionById(sessionId);
|
|
655
|
+
if (!currentSession)
|
|
656
656
|
return [];
|
|
657
|
+
const currentSessionId = currentSession.id;
|
|
657
658
|
const run = this.runtime.activeSessionRun(currentSessionId);
|
|
658
659
|
if (!run || !run.initiatedByUserId)
|
|
659
660
|
return [];
|
|
660
|
-
const result = await this.
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
661
|
+
const result = await this.sessionCatalogService.operations()['session.list']({
|
|
662
|
+
workspaceId: currentSession.workspaceId,
|
|
663
|
+
registeredOnly: true,
|
|
664
|
+
view: 'marctl',
|
|
665
|
+
parentSessionId: currentSessionId,
|
|
666
|
+
limit: 100
|
|
667
|
+
});
|
|
665
668
|
signal.throwIfAborted();
|
|
666
669
|
const data = result;
|
|
667
|
-
|
|
670
|
+
const truncated = data.nextCursor != null;
|
|
671
|
+
if (!data.sessions?.length && !truncated)
|
|
668
672
|
return [];
|
|
669
673
|
return [
|
|
670
674
|
`Child Sessions (use agent_list for details): ${JSON.stringify({
|
|
671
675
|
sessions: data.sessions ?? [],
|
|
672
|
-
truncated
|
|
676
|
+
truncated
|
|
673
677
|
}).replace(/[<>&]/g, (c) => c === '<' ? '\\u003c' : c === '>' ? '\\u003e' : '\\u0026')}`
|
|
674
678
|
];
|
|
675
679
|
}
|
|
@@ -1093,31 +1097,7 @@ export class NodeConnector {
|
|
|
1093
1097
|
goals: this.goalRuntime,
|
|
1094
1098
|
runs: this.runWorkbenchService,
|
|
1095
1099
|
observations: this.agentNetworkObservations,
|
|
1096
|
-
listSessions: async (input) => this.sessionCatalogService.operations()['session.list'](input),
|
|
1097
1100
|
history: async (input) => this.sessionQueryService.operations()['session.turns.list'](input),
|
|
1098
|
-
profiles: async (environment, models) => {
|
|
1099
|
-
const summaries = Array.isArray(models)
|
|
1100
|
-
? models.flatMap((model, index) => {
|
|
1101
|
-
if (!isPlainRecord(model))
|
|
1102
|
-
return [];
|
|
1103
|
-
const { apiKey: _apiKey, credential: _credential, imageGeneration: _images, access: _access, ...summary } = model;
|
|
1104
|
-
return [
|
|
1105
|
-
{
|
|
1106
|
-
...summary,
|
|
1107
|
-
sortOrder: typeof summary.sortOrder === 'number' ? summary.sortOrder : index
|
|
1108
|
-
}
|
|
1109
|
-
];
|
|
1110
|
-
})
|
|
1111
|
-
: undefined;
|
|
1112
|
-
const result = await this.runnerService.operations()['runner.profiles.list']({
|
|
1113
|
-
secretEnvironment: environment,
|
|
1114
|
-
marAgentModels: summaries,
|
|
1115
|
-
waitForRefresh: true
|
|
1116
|
-
});
|
|
1117
|
-
if (!isPlainRecord(result) || !Array.isArray(result.profiles))
|
|
1118
|
-
throw new Error('RUNNER_PROFILE_INVALID');
|
|
1119
|
-
return result.profiles;
|
|
1120
|
-
},
|
|
1121
1101
|
ensureObservations: (actor) => {
|
|
1122
1102
|
const existing = this.agentNetworkObservations.get(actor.observationId ?? actor.sessionId);
|
|
1123
1103
|
if (existing)
|
package/dist/marctl-guidance.js
CHANGED
|
@@ -20,10 +20,10 @@ export const marctlInstructions = [
|
|
|
20
20
|
'## MAR Workspace CLI',
|
|
21
21
|
'Run marctl from the shell. Commands use the authenticated Node and Workspace; marctl help agents, nodes, ssh or scp shows options.',
|
|
22
22
|
'Use agents start --title <title> -m <text> [--runner <runner>] [--model-id <id>] [--effort <effort>] [--goal <objective>]. An Agent must supply a nonblank title; a manual terminal may omit it. The default Runner is the current Session Runner; a terminal without a current Session must specify --runner or an explicit --parent. Start returns without waiting.',
|
|
23
|
-
'Use agents send <id> -m <text> [--steer|--queue], agents wait [<id>] [--wait-for message|completion] [--timeout <duration>], agents stop <id> --execution <execution-id>, agents list [--scope
|
|
23
|
+
'Use agents send <id> -m <text> [--steer|--queue], agents wait [<id>] [--wait-for message|completion] [--timeout <duration>], agents stop <id> --execution <execution-id>, agents list [--scope tree|active], and agents view <id> [--history]. Wait defaults to direct-child completion for 30 seconds; --timeout 0ms takes a snapshot. Stop clears the target Session Goal.',
|
|
24
24
|
'Prefer agents send <id> --steer -m <text> for timely communication or course correction. Use --queue only for deliberately deferred input: it is consumed after current work and earlier queued inputs finish, and with an active Goal it is not consumed until that Goal exits. Acceptance is not a guarantee of immediate processing.',
|
|
25
25
|
'Start and send accept repeated --image <local-path> for PNG, JPEG, GIF or WebP; limits are five images, 5 MiB each and 10 MiB total. Send can update a Goal alone with --goal <objective> or --clear-goal; omitted Goal leaves it unchanged. Decide a direct child approval with send <id> --approval <approval-id> --decision approve|reject.',
|
|
26
|
-
'List accepts --
|
|
26
|
+
'List accepts only --scope tree|active, optional --query <title-fragment> and --limit <1..100> (default 100). With a current Session it defaults to the containing collaboration tree, including idle Sessions; without one it defaults to active Workspace Sessions (running or sleeping), and explicit tree is rejected. Returns currentAgentId (null without a Session), flat sessions with id, parentSessionId, title, runner and status, and truncated; no cursor or pagination. Reuse known IDs directly.',
|
|
27
27
|
'Only direct-child updates are collected automatically. Sending to a non-child never subscribes; an explicit wait observes it only for that call.',
|
|
28
28
|
'Use agents config, rename, prompt and schedule for management. marctl nodes, ssh and scp use authorized Nodes.',
|
|
29
29
|
marctlSafety
|
|
@@ -38,6 +38,7 @@ const goal = {
|
|
|
38
38
|
}
|
|
39
39
|
]
|
|
40
40
|
};
|
|
41
|
+
const deliveryGuidance = 'Default to STEER. Use QUEUE only when the user or recipient explicitly requests deferred delivery. Do not queue merely to avoid interrupting the recipient or because the message is a report.';
|
|
41
42
|
const tools = [
|
|
42
43
|
{
|
|
43
44
|
action: 'start',
|
|
@@ -49,7 +50,7 @@ const tools = [
|
|
|
49
50
|
runner,
|
|
50
51
|
modelId: {
|
|
51
52
|
...text,
|
|
52
|
-
description: 'Model
|
|
53
|
+
description: 'Model configuration ID; omit to use the target Runner default.'
|
|
53
54
|
},
|
|
54
55
|
effort: text,
|
|
55
56
|
title: text,
|
|
@@ -60,7 +61,7 @@ const tools = [
|
|
|
60
61
|
},
|
|
61
62
|
{
|
|
62
63
|
action: 'send',
|
|
63
|
-
description:
|
|
64
|
+
description: `Send input, decide an approval or update a Goal; goal:null leaves it unchanged, goal:{clear:true} clears it. ${deliveryGuidance} QUEUE is consumed only after current work and earlier queued inputs finish; an active Goal must exit first.`,
|
|
64
65
|
properties: {
|
|
65
66
|
agentId: text,
|
|
66
67
|
content: { type: 'string' },
|
|
@@ -73,10 +74,10 @@ const tools = [
|
|
|
73
74
|
required: ['approvalId', 'decision'],
|
|
74
75
|
properties: { approvalId: text, decision: { type: 'string', enum: ['approve', 'reject'] } }
|
|
75
76
|
},
|
|
76
|
-
|
|
77
|
+
delivery: {
|
|
77
78
|
type: 'string',
|
|
78
|
-
enum: ['
|
|
79
|
-
description:
|
|
79
|
+
enum: ['STEER', 'QUEUE'],
|
|
80
|
+
description: deliveryGuidance
|
|
80
81
|
},
|
|
81
82
|
clientMessageId: text
|
|
82
83
|
},
|
|
@@ -101,20 +102,19 @@ const tools = [
|
|
|
101
102
|
},
|
|
102
103
|
{
|
|
103
104
|
action: 'list',
|
|
104
|
-
description:
|
|
105
|
+
description: "List the current Session's collaboration tree by default, or active Workspace Sessions. Active includes running and sleeping. Reuse known Session IDs directly. Returns currentAgentId, flat sessions (id, parentSessionId, title, runner, status) and truncated; no pagination.",
|
|
105
106
|
properties: {
|
|
106
|
-
scope: {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
description: '
|
|
107
|
+
scope: {
|
|
108
|
+
type: 'string',
|
|
109
|
+
enum: ['tree', 'active'],
|
|
110
|
+
description: 'Defaults to tree: the containing tree, including ancestors, peers and descendants.'
|
|
110
111
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
query: {
|
|
113
|
+
type: 'string',
|
|
114
|
+
maxLength: 160,
|
|
115
|
+
description: 'Optional case-insensitive title fragment within the scope.'
|
|
114
116
|
},
|
|
115
|
-
|
|
116
|
-
limit: { type: 'integer', minimum: 1, maximum: 100 },
|
|
117
|
-
cursor: text
|
|
117
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, description: 'Defaults to 100.' }
|
|
118
118
|
}
|
|
119
119
|
},
|
|
120
120
|
{
|
|
@@ -140,7 +140,8 @@ const delegationPrompt = [
|
|
|
140
140
|
const collaborationPrompt = [
|
|
141
141
|
'# Workspace collaboration',
|
|
142
142
|
"Use native agent_* tools to collaborate within the Agent Network when another Session's context or ongoing work materially helps. Keep requests focused and avoid unnecessary cross-Session coordination. Peer Sessions retain their own work and lifecycle; do not treat them as task-owned children or stop them merely for task cleanup. Explicitly wait when you need progress or completion.",
|
|
143
|
-
|
|
143
|
+
'Reuse known Session IDs rather than rediscovering them. Send updates only when requested or when the recipient needs to decide or change course; combine related updates and avoid acknowledgment-only exchanges.',
|
|
144
|
+
'For delegated work, do not provide routine progress updates or acknowledgments unless requested. Contact the parent before completion only when requested or for blockers, scope conflicts, or information that requires the parent to decide or change course. Return a concise final handoff; do not also send a duplicate completion message.'
|
|
144
145
|
].join('\n\n');
|
|
145
146
|
const managementPrompt = [
|
|
146
147
|
'# Workspace management',
|
|
@@ -154,13 +155,27 @@ export function agentNetworkExtension(invoke, currentExecution) {
|
|
|
154
155
|
context.hooks.intercept('prompt.sections', (input, _call, next) => {
|
|
155
156
|
const available = new Set(input.facts?.tools);
|
|
156
157
|
const enabled = input.facts?.mode !== 'compact';
|
|
158
|
+
const delegate = enabled && available.has('agent_start');
|
|
159
|
+
const models = delegate
|
|
160
|
+
? (context.models
|
|
161
|
+
?.list()
|
|
162
|
+
.filter((model) => model.conversationModel && model.subagentModel)
|
|
163
|
+
.map((model) => ({
|
|
164
|
+
id: model.id,
|
|
165
|
+
name: model.name,
|
|
166
|
+
supportedEfforts: model.reasoningEfforts,
|
|
167
|
+
defaultEffort: model.defaultReasoningEffort
|
|
168
|
+
})) ?? [])
|
|
169
|
+
: [];
|
|
157
170
|
return next({
|
|
158
171
|
...input,
|
|
159
172
|
sections: [
|
|
160
173
|
...input.sections,
|
|
161
174
|
{
|
|
162
175
|
id: 'mar.node.agent-network:delegation',
|
|
163
|
-
text:
|
|
176
|
+
text: delegate
|
|
177
|
+
? `${delegationPrompt}\n\nAvailable subagent models:\n${JSON.stringify({ runner: 'mar-agent', models })}`
|
|
178
|
+
: null
|
|
164
179
|
},
|
|
165
180
|
{
|
|
166
181
|
id: 'mar.node.agent-network:collaboration',
|
|
@@ -190,7 +205,10 @@ export function agentNetworkExtension(invoke, currentExecution) {
|
|
|
190
205
|
'effort',
|
|
191
206
|
'waitFor',
|
|
192
207
|
'waitMs',
|
|
193
|
-
'scope'
|
|
208
|
+
'scope',
|
|
209
|
+
'query',
|
|
210
|
+
'limit',
|
|
211
|
+
'cursor'
|
|
194
212
|
],
|
|
195
213
|
inputSchema: {
|
|
196
214
|
type: 'object',
|
|
@@ -203,14 +221,35 @@ export function agentNetworkExtension(invoke, currentExecution) {
|
|
|
203
221
|
if (!executionId)
|
|
204
222
|
throw new Error('AGENT_ACTOR_NOT_ACTIVE');
|
|
205
223
|
const args = input;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
224
|
+
let operationInput = args;
|
|
225
|
+
if (tool.action === 'send') {
|
|
226
|
+
const { delivery, ...sendInput } = args;
|
|
227
|
+
const hasMessage = (typeof args.content === 'string' && !!args.content.trim()) ||
|
|
209
228
|
(Array.isArray(args.attachments) && args.attachments.length > 0) ||
|
|
210
|
-
(Array.isArray(args.attachmentIds) && args.attachmentIds.length > 0)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
229
|
+
(Array.isArray(args.attachmentIds) && args.attachmentIds.length > 0);
|
|
230
|
+
operationInput =
|
|
231
|
+
delivery !== undefined || hasMessage
|
|
232
|
+
? {
|
|
233
|
+
...sendInput,
|
|
234
|
+
deliveryIntent: delivery === 'QUEUE' ? 'QUEUE' : 'REPLACE_CURRENT'
|
|
235
|
+
}
|
|
236
|
+
: sendInput;
|
|
237
|
+
}
|
|
238
|
+
const onResolved = call.publishEventData
|
|
239
|
+
? async (sessions) => {
|
|
240
|
+
const rows = sessions.map((session) => ({
|
|
241
|
+
agentId: session.id,
|
|
242
|
+
title: session.customTitle ?? session.runnerTitle ?? undefined
|
|
243
|
+
}));
|
|
244
|
+
const result = tool.action === 'wait'
|
|
245
|
+
? { agents: rows }
|
|
246
|
+
: tool.action === 'view'
|
|
247
|
+
? { session: rows[0] }
|
|
248
|
+
: (rows[0] ?? {});
|
|
249
|
+
await call.publishEventData(agentOperationEvent(tool.action, {}, result).eventData);
|
|
250
|
+
}
|
|
251
|
+
: undefined;
|
|
252
|
+
const result = await invoke(executionId, tool.action, operationInput, call, onResolved);
|
|
214
253
|
const modelResult = tool.action === 'wait' ? agentNetworkWaitModelOutput(result) : result;
|
|
215
254
|
return {
|
|
216
255
|
data: result,
|
|
@@ -103,7 +103,7 @@ export function agentOperationEvent(action, input, result) {
|
|
|
103
103
|
receipt.modelCount = value.models.length;
|
|
104
104
|
}
|
|
105
105
|
if (action === 'view' && isPlainRecord(value.session)) {
|
|
106
|
-
for (const key of ['status', '
|
|
106
|
+
for (const key of ['status', 'modelId', 'effort']) {
|
|
107
107
|
const field = text(value.session[key]);
|
|
108
108
|
if (field)
|
|
109
109
|
receipt[key] = field;
|
|
@@ -6,7 +6,7 @@ export function normalizeMarAgentUsage(usage) {
|
|
|
6
6
|
}
|
|
7
7
|
import { boundedConversationItemId, compactRunnerText, compactRunnerValue } from '../normalization.js';
|
|
8
8
|
import { runnerErrorProjection } from '../runner-error.js';
|
|
9
|
-
import { toolPresentationSchema } from '@myagentroam/protocol';
|
|
9
|
+
import { toolPresentationSchema, agentOperationPresentationSchema } from '@myagentroam/protocol';
|
|
10
10
|
import { publicOutputSnapshot, publicToolOutput } from '../normalization.js';
|
|
11
11
|
import { boundToolOutput } from '@myagentroam/protocol';
|
|
12
12
|
import { agentOperationAction, agentOperationPresentation } from './agent-operation-presentation.js';
|
|
@@ -170,6 +170,20 @@ export class MarAgentConversationNormalizer {
|
|
|
170
170
|
...this.#codeModeToolResults(event)
|
|
171
171
|
];
|
|
172
172
|
}
|
|
173
|
+
if (event.type === 'tool.updated') {
|
|
174
|
+
const state = this.#tools.get(event.callId);
|
|
175
|
+
if (!state?.active ||
|
|
176
|
+
state.toolName !== event.toolName ||
|
|
177
|
+
!agentOperationAction(event.toolName))
|
|
178
|
+
return [];
|
|
179
|
+
const operation = agentOperationPresentationSchema.safeParse(record(event.data)?.agentOperation);
|
|
180
|
+
if (!operation.success || operation.data.action !== agentOperationAction(event.toolName))
|
|
181
|
+
return [];
|
|
182
|
+
this.#tools.set(event.callId, { ...state, presentationData: event.data });
|
|
183
|
+
return [
|
|
184
|
+
genericToolItem(state.itemId, state.toolName, 'IN_PROGRESS', state.input, null, null, false, event.data, state.subagentTitle, state.subagentDetails, undefined, undefined, state.inputTruncated)
|
|
185
|
+
];
|
|
186
|
+
}
|
|
173
187
|
if (event.type === 'sleep.started') {
|
|
174
188
|
const state = this.#tools.get(event.callId);
|
|
175
189
|
if (state?.toolName !== 'sleep')
|
|
@@ -673,7 +687,7 @@ export class MarAgentConversationNormalizer {
|
|
|
673
687
|
}
|
|
674
688
|
const item = genericToolItem(state?.itemId ?? itemId('tool', event.callId), event.toolName, 'COMPLETED', state?.input, generatedImages.length === 0
|
|
675
689
|
? event.summary
|
|
676
|
-
: generatedImages.map((image) => image.name).join(', '), null, event.outputTruncation !== undefined, event.data, agentId === undefined ? state?.subagentTitle : this.#subagentTitles.get(agentId), details, state?.sleepStartedAt, event.outputTruncation, state?.inputTruncated, event.summaryTruncated === true);
|
|
690
|
+
: generatedImages.map((image) => image.name).join(', '), null, event.outputTruncation !== undefined, mergeAgentOperationData(event.toolName, state?.presentationData, event.data), agentId === undefined ? state?.subagentTitle : this.#subagentTitles.get(agentId), details, state?.sleepStartedAt, event.outputTruncation, state?.inputTruncated, event.summaryTruncated === true);
|
|
677
691
|
return [
|
|
678
692
|
{
|
|
679
693
|
...item,
|
|
@@ -704,7 +718,11 @@ export class MarAgentConversationNormalizer {
|
|
|
704
718
|
return [
|
|
705
719
|
genericToolItem(state?.itemId ?? itemId('tool', event.callId), event.toolName, event.code === 'MAR_AGENT_CANCELLED' || event.code === 'MAR_AGENT_EXECUTION_CANCELLED'
|
|
706
720
|
? 'CANCELLED'
|
|
707
|
-
: 'FAILED', state?.input, event.message ?? event.code, event.code, event.truncated === true, event.
|
|
721
|
+
: 'FAILED', state?.input, event.message ?? event.code, event.code, event.truncated === true, agentOperationAction(event.toolName)
|
|
722
|
+
? mergeAgentOperationData(event.toolName, state?.presentationData, event.data)
|
|
723
|
+
: event.data === undefined
|
|
724
|
+
? state?.presentationData
|
|
725
|
+
: event.data, state?.subagentTitle, state?.subagentDetails, state?.sleepStartedAt, event.outputTruncation, state?.inputTruncated)
|
|
708
726
|
];
|
|
709
727
|
}
|
|
710
728
|
#codeModeToolResults(event) {
|
|
@@ -1027,6 +1045,28 @@ function codeExecutionError(data) {
|
|
|
1027
1045
|
message: compactRunnerText(error.message, 1000)
|
|
1028
1046
|
};
|
|
1029
1047
|
}
|
|
1048
|
+
function mergeAgentOperationData(toolName, previous, current) {
|
|
1049
|
+
if (!agentOperationAction(toolName))
|
|
1050
|
+
return current;
|
|
1051
|
+
const before = agentOperationPresentationSchema.safeParse(record(previous)?.agentOperation);
|
|
1052
|
+
if (!before.success || before.data.action !== agentOperationAction(toolName))
|
|
1053
|
+
return current;
|
|
1054
|
+
const after = agentOperationPresentationSchema.safeParse(record(current)?.agentOperation);
|
|
1055
|
+
if (!after.success || after.data.action !== before.data.action) {
|
|
1056
|
+
return { ...record(current), agentOperation: before.data };
|
|
1057
|
+
}
|
|
1058
|
+
return {
|
|
1059
|
+
...record(current),
|
|
1060
|
+
agentOperation: {
|
|
1061
|
+
...after.data,
|
|
1062
|
+
targets: before.data.targets.map((target) => ({
|
|
1063
|
+
...after.data.targets.find((candidate) => candidate.agentId === target.agentId),
|
|
1064
|
+
...target
|
|
1065
|
+
})),
|
|
1066
|
+
...(before.data.targetsTruncated ? { targetsTruncated: true } : {})
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1030
1070
|
function genericToolItem(id, toolName, status, input, outputSummary = null, errorCode = null, truncated = false, output, subagentTitle, knownSubagentDetails, sleepStartedAt, outputTruncation, inputTruncated = false, summaryTruncated = false) {
|
|
1031
1071
|
if (toolName === 'code_exec') {
|
|
1032
1072
|
const code = typeof input === 'string' ? input : record(input)?.code;
|
|
@@ -12,8 +12,8 @@ export function actorForAuthenticatedTerminal(actor, workspaceId) {
|
|
|
12
12
|
terminalCalls.set(authenticated, { workspaceId });
|
|
13
13
|
return authenticated;
|
|
14
14
|
}
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
15
|
+
import { marctlSessionDetail } from './session-recent-activity.js';
|
|
16
|
+
import { listAgentNetworkSessions } from './agent-network-list.js';
|
|
17
17
|
/** Bind the collaboration use case to the existing Session, Run and Goal owners. */
|
|
18
18
|
export function createNodeAgentNetwork(options) {
|
|
19
19
|
async function actorSession(actor) {
|
|
@@ -44,9 +44,35 @@ export function createNodeAgentNetwork(options) {
|
|
|
44
44
|
throw new Error('SESSION_NOT_FOUND');
|
|
45
45
|
return session;
|
|
46
46
|
}
|
|
47
|
-
function
|
|
47
|
+
function ancestorDistance(source, targetId) {
|
|
48
|
+
const seen = new Set([source.id]);
|
|
49
|
+
let parentId = source.parentSessionId;
|
|
50
|
+
let distance = 1;
|
|
51
|
+
while (parentId) {
|
|
52
|
+
if (seen.has(parentId))
|
|
53
|
+
throw Object.assign(new Error('Session ancestry contains a cycle.'), {
|
|
54
|
+
code: 'SESSION_RELATION_INVALID'
|
|
55
|
+
});
|
|
56
|
+
seen.add(parentId);
|
|
57
|
+
if (parentId === targetId)
|
|
58
|
+
return distance;
|
|
59
|
+
const parent = options.runtime.getAgentSession(parentId);
|
|
60
|
+
if (parent?.id === targetId)
|
|
61
|
+
return distance;
|
|
62
|
+
parentId = parent
|
|
63
|
+
? parent.parentSessionId
|
|
64
|
+
: options.database()?.getSessionRecord(parentId)?.metadata.parentSessionId;
|
|
65
|
+
distance++;
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
function origin(parent, targetId) {
|
|
70
|
+
const distance = targetId === undefined ? 0 : ancestorDistance(parent, targetId);
|
|
48
71
|
return {
|
|
49
72
|
sessionId: parent.id,
|
|
73
|
+
...(distance === 0
|
|
74
|
+
? {}
|
|
75
|
+
: { relation: distance === 1 ? 'child' : 'descendant' }),
|
|
50
76
|
name: parent.customTitle?.trim() || parent.runnerTitle?.trim() || parent.id
|
|
51
77
|
};
|
|
52
78
|
}
|
|
@@ -119,6 +145,18 @@ export function createNodeAgentNetwork(options) {
|
|
|
119
145
|
options.projection.resolve(record.id, true)));
|
|
120
146
|
return resolved.filter((session) => session !== undefined);
|
|
121
147
|
},
|
|
148
|
+
validateSend: async (actor, session) => {
|
|
149
|
+
if (terminalCalls.has(actor))
|
|
150
|
+
return;
|
|
151
|
+
const source = await actorSession(actor);
|
|
152
|
+
if (ancestorDistance(source, session.id) === 0)
|
|
153
|
+
return;
|
|
154
|
+
const run = options.runtime.activeSessionRun(session.id);
|
|
155
|
+
if (!run || run.status === 'CANCELLING')
|
|
156
|
+
throw Object.assign(new Error('Cannot send messages to an inactive ancestor session.'), {
|
|
157
|
+
code: 'AGENT_ANCESTOR_NOT_ACTIVE'
|
|
158
|
+
});
|
|
159
|
+
},
|
|
122
160
|
observe: (actor) => {
|
|
123
161
|
options.ensureObservations(actor);
|
|
124
162
|
},
|
|
@@ -243,7 +281,7 @@ export function createNodeAgentNetwork(options) {
|
|
|
243
281
|
: { personalInstructions: actor.personalInstructions }),
|
|
244
282
|
...(actor.usageGrant === undefined ? {} : { usageGrant: actor.usageGrant }),
|
|
245
283
|
preserveActiveRun: true,
|
|
246
|
-
...(parent ? { messageOrigin: origin(parent) } : {}),
|
|
284
|
+
...(parent ? { messageOrigin: origin(parent, session.id) } : {}),
|
|
247
285
|
...(message === undefined
|
|
248
286
|
? {}
|
|
249
287
|
: {
|
|
@@ -267,7 +305,7 @@ export function createNodeAgentNetwork(options) {
|
|
|
267
305
|
...(signal ? { [SESSION_ADMISSION_SIGNAL]: signal } : {}),
|
|
268
306
|
sessionId: session.id,
|
|
269
307
|
__suppressNotifications: true,
|
|
270
|
-
...(parent ? { __marctlOrigin: origin(parent) } : {})
|
|
308
|
+
...(parent ? { __marctlOrigin: origin(parent, session.id) } : {})
|
|
271
309
|
}));
|
|
272
310
|
return publicReceipt(result);
|
|
273
311
|
},
|
|
@@ -313,80 +351,18 @@ export function createNodeAgentNetwork(options) {
|
|
|
313
351
|
const workspaceId = terminal?.workspaceId ?? source?.workspaceId;
|
|
314
352
|
if (!workspaceId)
|
|
315
353
|
throw new Error('SESSION_INVALID');
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const profiles = await options.profiles(actor.secretEnvironment ?? {}, actor.runnerConfiguration?.marAgentModels);
|
|
323
|
-
const profile = profiles.find((item) => item.runner === runner);
|
|
324
|
-
if (!profile)
|
|
325
|
-
throw new Error('RUNNER_PROFILE_NOT_FOUND');
|
|
326
|
-
const models = profile.runner !== 'mar-agent'
|
|
327
|
-
? profile.models
|
|
328
|
-
: profile.models.filter((model) => {
|
|
329
|
-
const configurations = [
|
|
330
|
-
actor.runnerConfiguration?.marAgentModel,
|
|
331
|
-
...(Array.isArray(actor.runnerConfiguration?.marAgentModels)
|
|
332
|
-
? actor.runnerConfiguration.marAgentModels
|
|
333
|
-
: [])
|
|
334
|
-
];
|
|
335
|
-
return configurations.some((value) => isPlainRecord(value) &&
|
|
336
|
-
value.id === model.id &&
|
|
337
|
-
value.enabled === true &&
|
|
338
|
-
value.conversationModel === true &&
|
|
339
|
-
value.subagentModel === true);
|
|
340
|
-
});
|
|
341
|
-
return { current, runner: profile.runner, models, capabilities: profile.capabilities };
|
|
342
|
-
}
|
|
343
|
-
if (scope === 'workspace') {
|
|
344
|
-
const result = await options.listSessions({
|
|
345
|
-
workspaceId,
|
|
346
|
-
view: 'marctl',
|
|
347
|
-
registeredOnly: true,
|
|
348
|
-
...(input.query === undefined ? {} : { query: input.query }),
|
|
349
|
-
...(input.parent === undefined ? {} : { parentSessionId: input.parent }),
|
|
350
|
-
...(input.limit === undefined ? {} : { limit: input.limit }),
|
|
351
|
-
...(input.cursor === undefined ? {} : { cursor: input.cursor })
|
|
352
|
-
});
|
|
353
|
-
if (!isPlainRecord(result))
|
|
354
|
-
throw new Error('SESSION_LIST_INVALID_RESULT');
|
|
355
|
-
return { ...result, current };
|
|
356
|
-
}
|
|
357
|
-
if (scope !== 'children' && scope !== 'descendants')
|
|
358
|
-
throw new Error('AGENT_LIST_INVALID');
|
|
359
|
-
const root = input.agentId === undefined || input.agentId === source?.id
|
|
360
|
-
? source
|
|
361
|
-
: await target(actor, input.agentId);
|
|
362
|
-
if (!root)
|
|
363
|
-
throw new Error('AGENT_TARGET_REQUIRED');
|
|
364
|
-
const database = options.database();
|
|
365
|
-
if (!database)
|
|
366
|
-
throw new Error('DATABASE_UNAVAILABLE');
|
|
367
|
-
const records = scope === 'children'
|
|
368
|
-
? database.listChildSessionRecords(root.workspaceId, root.id)
|
|
369
|
-
: database.sessionTreeRecords(root.workspaceId, root.id).slice(1);
|
|
370
|
-
const sessions = (await Promise.all(records.map((record) => options.runtime.getAgentSession(record.id) ??
|
|
371
|
-
options.projection.resolve(record.id, true))))
|
|
372
|
-
.filter((session) => session !== undefined)
|
|
373
|
-
.filter((session) => !input.query ||
|
|
374
|
-
(session.customTitle ?? session.runnerTitle ?? '')
|
|
375
|
-
.normalize('NFKC')
|
|
376
|
-
.toLocaleLowerCase()
|
|
377
|
-
.includes(input.query.normalize('NFKC').toLocaleLowerCase()));
|
|
378
|
-
const page = sessionPage(sessions, { limit: input.limit, cursor: input.cursor });
|
|
379
|
-
return {
|
|
380
|
-
current,
|
|
381
|
-
sessions: page.sessions.map((session) => marctlSessionSummary(options.runtime, session)),
|
|
382
|
-
nextCursor: page.nextCursor,
|
|
383
|
-
truncated: page.nextCursor !== null
|
|
384
|
-
};
|
|
354
|
+
return listAgentNetworkSessions(source, input, {
|
|
355
|
+
workspaceId,
|
|
356
|
+
runtime: options.runtime,
|
|
357
|
+
database: options.database(),
|
|
358
|
+
projection: options.projection
|
|
359
|
+
});
|
|
385
360
|
},
|
|
386
|
-
view: async (actor, input) => {
|
|
361
|
+
view: async (actor, input, onResolved) => {
|
|
387
362
|
const source = terminalCalls.has(actor) ? undefined : await actorSession(actor);
|
|
388
363
|
const session = input.agentId === source?.id ? source : await target(actor, input.agentId);
|
|
389
364
|
assertSessionControlOwner(options.runtime, session.id, actor.requestUserId);
|
|
365
|
+
await onResolved?.([session]);
|
|
390
366
|
return {
|
|
391
367
|
session: marctlSessionDetail(options.runtime, session, options.runtime.sessionQueue(session.id).itemIds.length),
|
|
392
368
|
goal: publicGoal(options.goals.get(session.id)) ?? null,
|
|
@@ -4,9 +4,9 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { modelUsageGrantSchema } from '@myagentroam/protocol';
|
|
5
5
|
/** Bind each native invocation to the actual live Node Execution and its captured resources. */
|
|
6
6
|
export function agentNetworkInvoke(input) {
|
|
7
|
-
return async (executionId, action, payload, context) => {
|
|
7
|
+
return async (executionId, action, payload, context, onResolved) => {
|
|
8
8
|
const actor = executionAgentActor({ ...input, executionId });
|
|
9
|
-
return invokeAgentNetwork(input.service, actor, action, payload, context);
|
|
9
|
+
return invokeAgentNetwork(input.service, actor, action, payload, context, undefined, onResolved);
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
/** Read actor resources from the current Node-owned Execution, not caller arguments. */
|
|
@@ -51,7 +51,7 @@ export function executionAgentActor(input) {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
/** Shared command dispatch; callers must bind actor authority at their trusted boundary. */
|
|
54
|
-
export async function invokeAgentNetwork(service, actor, action, payload, context, requestSignal) {
|
|
54
|
+
export async function invokeAgentNetwork(service, actor, action, payload, context, requestSignal, onResolved) {
|
|
55
55
|
const signal = context?.signal ?? requestSignal;
|
|
56
56
|
switch (action) {
|
|
57
57
|
case 'start':
|
|
@@ -63,16 +63,16 @@ export async function invokeAgentNetwork(service, actor, action, payload, contex
|
|
|
63
63
|
typeof payload.title === 'string' &&
|
|
64
64
|
!payload.title.trim()) {
|
|
65
65
|
const { title: _title, ...withoutTitle } = payload;
|
|
66
|
-
return service.start(actor, withoutTitle, signal);
|
|
66
|
+
return service.start(actor, withoutTitle, signal, onResolved);
|
|
67
67
|
}
|
|
68
68
|
return service.start(actor, {
|
|
69
69
|
...payload,
|
|
70
70
|
...(typeof payload.title === 'string' ? { title: payload.title.trim() } : {})
|
|
71
|
-
}, signal);
|
|
71
|
+
}, signal, onResolved);
|
|
72
72
|
case 'send':
|
|
73
|
-
return service.send(actor, payload, signal);
|
|
73
|
+
return service.send(actor, payload, signal, onResolved);
|
|
74
74
|
case 'stop':
|
|
75
|
-
return service.stop(actor, payload);
|
|
75
|
+
return service.stop(actor, payload, onResolved);
|
|
76
76
|
case 'wait':
|
|
77
77
|
return service.wait(actor, {
|
|
78
78
|
...payload,
|
|
@@ -81,11 +81,11 @@ export async function invokeAgentNetwork(service, actor, action, payload, contex
|
|
|
81
81
|
: requestSignal
|
|
82
82
|
? { signal: requestSignal }
|
|
83
83
|
: {})
|
|
84
|
-
});
|
|
84
|
+
}, onResolved);
|
|
85
85
|
case 'list':
|
|
86
86
|
return service.list(actor, payload);
|
|
87
87
|
case 'view':
|
|
88
|
-
return service.view(actor, payload);
|
|
88
|
+
return service.view(actor, payload, onResolved);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
/** Node broker injects the caller only after validating the Server-issued Execution credential. */
|
|
@@ -145,8 +145,7 @@ export function agentNetworkExecutionCommand(input) {
|
|
|
145
145
|
const session = input.runtime.getAgentSession(actor.sessionId);
|
|
146
146
|
if (actor.requestUserId !== data.__requestUserId || session?.workspaceId !== data.workspaceId)
|
|
147
147
|
throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
|
|
148
|
-
const
|
|
149
|
-
const resources = (['start', 'send'].includes(String(data.action)) || profileQuery) &&
|
|
148
|
+
const resources = ['start', 'send'].includes(String(data.action)) &&
|
|
150
149
|
isPlainRecord(data.__marctlExecutionResources)
|
|
151
150
|
? data.__marctlExecutionResources
|
|
152
151
|
: undefined;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { marctlSessionSummary } from './session-recent-activity.js';
|
|
2
|
+
/** Read only registered relationships and live Node facts; never discover or resume a Runner. */
|
|
3
|
+
export async function listAgentNetworkSessions(source, input, options) {
|
|
4
|
+
if (Object.keys(input).some((key) => !['scope', 'query', 'limit'].includes(key)) ||
|
|
5
|
+
(input.scope !== undefined && !['tree', 'active'].includes(input.scope)) ||
|
|
6
|
+
(input.query !== undefined && (typeof input.query !== 'string' || input.query.length > 160)) ||
|
|
7
|
+
(input.limit !== undefined &&
|
|
8
|
+
(!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100)))
|
|
9
|
+
throw Object.assign(new Error('Invalid Agent list input.'), { code: 'AGENT_LIST_INVALID' });
|
|
10
|
+
const scope = input.scope ?? (source ? 'tree' : 'active');
|
|
11
|
+
const known = new Map(options.runtime
|
|
12
|
+
.listAgentSessions()
|
|
13
|
+
.filter((session) => session.workspaceId === options.workspaceId)
|
|
14
|
+
.map((session) => [session.id, session]));
|
|
15
|
+
if (source)
|
|
16
|
+
known.set(source.id, source);
|
|
17
|
+
let sessions;
|
|
18
|
+
if (scope === 'tree') {
|
|
19
|
+
if (!source)
|
|
20
|
+
throw new Error('AGENT_TARGET_REQUIRED');
|
|
21
|
+
if (!options.database)
|
|
22
|
+
throw new Error('DATABASE_UNAVAILABLE');
|
|
23
|
+
const parents = new Map(options.database
|
|
24
|
+
.listSessionRecords(options.workspaceId)
|
|
25
|
+
.map((record) => [record.id, record.metadata.parentSessionId ?? null]));
|
|
26
|
+
for (const session of known.values())
|
|
27
|
+
parents.set(session.id, session.parentSessionId ?? null);
|
|
28
|
+
let root = source.id;
|
|
29
|
+
const ancestors = new Set([root]);
|
|
30
|
+
while (true) {
|
|
31
|
+
const parent = parents.get(root);
|
|
32
|
+
if (!parent || !parents.has(parent))
|
|
33
|
+
break;
|
|
34
|
+
if (ancestors.has(parent))
|
|
35
|
+
throw Object.assign(new Error('Session ancestry contains a cycle.'), {
|
|
36
|
+
code: 'SESSION_RELATION_INVALID'
|
|
37
|
+
});
|
|
38
|
+
ancestors.add(parent);
|
|
39
|
+
root = parent;
|
|
40
|
+
}
|
|
41
|
+
const children = new Map();
|
|
42
|
+
for (const [id, parent] of parents) {
|
|
43
|
+
if (parent === null)
|
|
44
|
+
continue;
|
|
45
|
+
const siblings = children.get(parent) ?? [];
|
|
46
|
+
siblings.push(id);
|
|
47
|
+
children.set(parent, siblings);
|
|
48
|
+
}
|
|
49
|
+
const ids = [root];
|
|
50
|
+
for (let i = 0; i < ids.length; i++)
|
|
51
|
+
ids.push(...(children.get(ids[i]) ?? []).sort());
|
|
52
|
+
sessions = (await Promise.all(ids.map((id) => known.get(id) ?? options.projection.resolve(id, true)))).filter((session) => session !== undefined && session.workspaceId === options.workspaceId);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
sessions = [...known.values()].sort((left, right) => right.lastActivityAt - left.lastActivityAt || left.id.localeCompare(right.id));
|
|
56
|
+
}
|
|
57
|
+
const query = input.query?.trim().normalize('NFKC').toLocaleLowerCase() ?? '';
|
|
58
|
+
const matches = sessions
|
|
59
|
+
.map((session) => {
|
|
60
|
+
const { id, title, runner, status } = marctlSessionSummary(options.runtime, session);
|
|
61
|
+
return { id, parentSessionId: session.parentSessionId ?? null, title, runner, status };
|
|
62
|
+
})
|
|
63
|
+
.filter((session) => (scope !== 'active' || session.status !== 'IDLE') &&
|
|
64
|
+
(!query || session.title.normalize('NFKC').toLocaleLowerCase().includes(query)));
|
|
65
|
+
const limit = input.limit ?? 100;
|
|
66
|
+
return {
|
|
67
|
+
currentAgentId: source?.id ?? null,
|
|
68
|
+
sessions: matches.slice(0, limit),
|
|
69
|
+
truncated: matches.length > limit
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -12,10 +12,10 @@ export class AgentNetworkService {
|
|
|
12
12
|
list(actor, input = {}) {
|
|
13
13
|
return this.options.list(actor, input);
|
|
14
14
|
}
|
|
15
|
-
view(actor, input) {
|
|
16
|
-
return this.options.view(actor, input);
|
|
15
|
+
view(actor, input, onResolved) {
|
|
16
|
+
return this.options.view(actor, input, onResolved);
|
|
17
17
|
}
|
|
18
|
-
async start(actor, input, signal) {
|
|
18
|
+
async start(actor, input, signal, onResolved) {
|
|
19
19
|
signal?.throwIfAborted();
|
|
20
20
|
const goal = parseAgentGoal(input.goal);
|
|
21
21
|
const content = input.content ?? '';
|
|
@@ -41,6 +41,7 @@ export class AgentNetworkService {
|
|
|
41
41
|
clientMessageId: input.clientMessageId ?? randomUUID(),
|
|
42
42
|
goal
|
|
43
43
|
}, signal);
|
|
44
|
+
await onResolved?.([created.session]);
|
|
44
45
|
try {
|
|
45
46
|
if (created.session.parentSessionId === actor.sessionId)
|
|
46
47
|
this.options.observe(actor, created.session);
|
|
@@ -54,7 +55,7 @@ export class AgentNetworkService {
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
|
-
async send(actor, input, signal) {
|
|
58
|
+
async send(actor, input, signal, onResolved) {
|
|
58
59
|
signal?.throwIfAborted();
|
|
59
60
|
const goal = parseAgentGoal(input.goal);
|
|
60
61
|
const content = input.content ?? '';
|
|
@@ -89,9 +90,12 @@ export class AgentNetworkService {
|
|
|
89
90
|
await this.options.authorize(actor, session, 'send');
|
|
90
91
|
if (goal.kind !== 'unchanged')
|
|
91
92
|
await this.options.authorize(actor, session, 'goal');
|
|
93
|
+
if (hasMessage || goal.kind === 'set')
|
|
94
|
+
await this.options.validateSend(actor, session);
|
|
92
95
|
if (input.approval !== undefined)
|
|
93
96
|
await this.options.validateApproval(actor, session, input.approval);
|
|
94
97
|
signal?.throwIfAborted();
|
|
98
|
+
await onResolved?.([session]);
|
|
95
99
|
if (session.parentSessionId === actor.sessionId)
|
|
96
100
|
this.options.observe(actor, session);
|
|
97
101
|
const approvalResult = input.approval === undefined
|
|
@@ -191,7 +195,7 @@ export class AgentNetworkService {
|
|
|
191
195
|
};
|
|
192
196
|
}
|
|
193
197
|
}
|
|
194
|
-
async stop(actor, input) {
|
|
198
|
+
async stop(actor, input, onResolved) {
|
|
195
199
|
if (typeof input.agentId !== 'string' ||
|
|
196
200
|
!input.agentId ||
|
|
197
201
|
typeof input.executionId !== 'string' ||
|
|
@@ -199,6 +203,7 @@ export class AgentNetworkService {
|
|
|
199
203
|
throw new Error('AGENT_STOP_INVALID');
|
|
200
204
|
const session = await this.options.target(actor, input.agentId);
|
|
201
205
|
await this.options.authorize(actor, session, 'stop');
|
|
206
|
+
await onResolved?.([session]);
|
|
202
207
|
const goalBefore = this.options.goal(session.id);
|
|
203
208
|
try {
|
|
204
209
|
const result = this.options.stop(actor, session, input.executionId);
|
|
@@ -224,7 +229,7 @@ export class AgentNetworkService {
|
|
|
224
229
|
};
|
|
225
230
|
}
|
|
226
231
|
}
|
|
227
|
-
async wait(actor, input) {
|
|
232
|
+
async wait(actor, input, onResolved) {
|
|
228
233
|
validateAgentWaitOptions(input);
|
|
229
234
|
if ((input.mode !== undefined && input.agentIds === undefined) ||
|
|
230
235
|
(input.agentId !== undefined && input.agentIds !== undefined) ||
|
|
@@ -268,6 +273,7 @@ export class AgentNetworkService {
|
|
|
268
273
|
if (!targets.length)
|
|
269
274
|
throw new Error('AGENT_EXECUTION_NOT_FOUND');
|
|
270
275
|
const selected = new Set(targets.map((target) => target.agentId));
|
|
276
|
+
await onResolved?.(targets.map((target) => sessions.find((session) => session.id === target.agentId)));
|
|
271
277
|
const directChildren = new Set();
|
|
272
278
|
for (const session of sessions)
|
|
273
279
|
if (selected.has(session.id) && session.parentSessionId === actor.sessionId) {
|
|
@@ -11,7 +11,11 @@ export function withSessionMessageOrigin(input, origin, parentSessionId) {
|
|
|
11
11
|
? parsed
|
|
12
12
|
: {
|
|
13
13
|
...parsed,
|
|
14
|
-
relation: parsed.sessionId === parentSessionId
|
|
14
|
+
relation: parsed.sessionId === parentSessionId
|
|
15
|
+
? 'parent'
|
|
16
|
+
: parsed.relation === 'child' || parsed.relation === 'descendant'
|
|
17
|
+
? parsed.relation
|
|
18
|
+
: 'other'
|
|
15
19
|
};
|
|
16
20
|
const tag = serializeComposerTag('mar_session_origin', snapshot);
|
|
17
21
|
const parts = parseComposerTags(input);
|
|
@@ -45,10 +49,12 @@ export function prepareComposerResourceInput(input, cwd, workspacePath) {
|
|
|
45
49
|
if (part.kind !== 'mar_references')
|
|
46
50
|
return part.raw;
|
|
47
51
|
return serializeComposerTag('mar_references', {
|
|
48
|
-
references: part.payload.references.map((reference) =>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
references: part.payload.references.map((reference) => reference.type === 'session'
|
|
53
|
+
? reference
|
|
54
|
+
: {
|
|
55
|
+
...reference,
|
|
56
|
+
runnerPath: relative(cwd, resolve(workspacePath, reference.path)).replaceAll('\\', '/')
|
|
57
|
+
})
|
|
52
58
|
});
|
|
53
59
|
})
|
|
54
60
|
.join('');
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runnerDefaultConfigurationSchema } from '@myagentroam/protocol';
|
|
2
2
|
import { parseSecretEnvironment } from '../mcp/secret-environment.js';
|
|
3
|
+
import { isPlainRecord } from '../util/records.js';
|
|
3
4
|
export class RunnerService {
|
|
4
5
|
database;
|
|
5
6
|
capabilities;
|
|
@@ -85,7 +86,10 @@ export class RunnerService {
|
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
88
|
saveDefaults(data) {
|
|
88
|
-
|
|
89
|
+
if (!isPlainRecord(data))
|
|
90
|
+
throw new Error('RUNNER_DEFAULT_INVALID');
|
|
91
|
+
const { __workspaceWatchSessionHash: _sessionHash, __requestUserId: _userId, __usageGrant: _usageGrant, ...configuration } = data;
|
|
92
|
+
const parsed = runnerDefaultConfigurationSchema.safeParse(configuration);
|
|
89
93
|
if (!parsed.success)
|
|
90
94
|
throw new Error('RUNNER_DEFAULT_INVALID');
|
|
91
95
|
if (parsed.data.runner === 'mar-agent' &&
|
|
@@ -29,11 +29,19 @@ export class NativeSessionProjectionService {
|
|
|
29
29
|
return accepted === undefined ? [] : [accepted];
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
|
+
const candidates = new Map(ordered.map((session) => [session.id, session]));
|
|
32
33
|
return ordered.flatMap((session) => {
|
|
33
34
|
const record = database.getSessionRecord(session.id);
|
|
34
|
-
if (record === undefined && session
|
|
35
|
+
if (record === undefined && !this.validParent(session, candidates))
|
|
35
36
|
return [];
|
|
36
|
-
return [
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
...session,
|
|
40
|
+
parentSessionId: record === undefined
|
|
41
|
+
? (session.parentSessionId ?? null)
|
|
42
|
+
: (record.metadata.parentSessionId ?? null)
|
|
43
|
+
}
|
|
44
|
+
];
|
|
37
45
|
});
|
|
38
46
|
}
|
|
39
47
|
async discoverCodex(workspaceId) {
|
|
@@ -237,29 +245,30 @@ export class NativeSessionProjectionService {
|
|
|
237
245
|
createdAt: timestamp
|
|
238
246
|
};
|
|
239
247
|
}
|
|
248
|
+
validParent(session, candidates) {
|
|
249
|
+
const database = this.options.database();
|
|
250
|
+
const visited = new Set([session.id]);
|
|
251
|
+
let parentId = session.parentSessionId ?? null;
|
|
252
|
+
let depth = 1;
|
|
253
|
+
while (parentId !== null) {
|
|
254
|
+
if (visited.has(parentId) || ++depth > 3)
|
|
255
|
+
return false;
|
|
256
|
+
visited.add(parentId);
|
|
257
|
+
const stored = database?.getSessionRecord(parentId);
|
|
258
|
+
const candidate = stored === undefined ? candidates.get(parentId) : undefined;
|
|
259
|
+
if ((stored?.workspaceId ?? candidate?.workspaceId) !== session.workspaceId)
|
|
260
|
+
return false;
|
|
261
|
+
parentId =
|
|
262
|
+
stored === undefined
|
|
263
|
+
? (candidate?.parentSessionId ?? null)
|
|
264
|
+
: (stored.metadata.parentSessionId ?? null);
|
|
265
|
+
}
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
240
268
|
remember(sessions) {
|
|
241
269
|
const database = this.options.database();
|
|
242
270
|
const candidates = new Map(sessions.map((session) => [session.id, session]));
|
|
243
|
-
const
|
|
244
|
-
const visited = new Set([session.id]);
|
|
245
|
-
let parentId = session.parentSessionId ?? null;
|
|
246
|
-
let depth = 1;
|
|
247
|
-
while (parentId !== null) {
|
|
248
|
-
if (visited.has(parentId) || ++depth > 3)
|
|
249
|
-
return false;
|
|
250
|
-
visited.add(parentId);
|
|
251
|
-
const stored = database?.getSessionRecord(parentId);
|
|
252
|
-
const candidate = stored === undefined ? candidates.get(parentId) : undefined;
|
|
253
|
-
if ((stored?.workspaceId ?? candidate?.workspaceId) !== session.workspaceId)
|
|
254
|
-
return false;
|
|
255
|
-
parentId =
|
|
256
|
-
stored === undefined
|
|
257
|
-
? (candidate?.parentSessionId ?? null)
|
|
258
|
-
: (stored.metadata.parentSessionId ?? null);
|
|
259
|
-
}
|
|
260
|
-
return true;
|
|
261
|
-
};
|
|
262
|
-
const valid = new Map(sessions.map((session) => [session.id, validParent(session)]));
|
|
271
|
+
const valid = new Map(sessions.map((session) => [session.id, this.validParent(session, candidates)]));
|
|
263
272
|
const accepted = sessions.flatMap((proposed) => {
|
|
264
273
|
const metadata = database === undefined || proposed.externalSessionId === null
|
|
265
274
|
? undefined
|
|
@@ -322,7 +322,7 @@ export class SessionCatalogService {
|
|
|
322
322
|
this.options.invalidateWorkspaceSessions(session.workspaceId);
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
|
-
async removeTree(root) {
|
|
325
|
+
async removeTree(root, cleanup) {
|
|
326
326
|
if (root.externalSessionId === null)
|
|
327
327
|
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
328
328
|
const database = this.options.database();
|
|
@@ -340,13 +340,20 @@ export class SessionCatalogService {
|
|
|
340
340
|
const ids = [root.externalSessionId];
|
|
341
341
|
const seen = new Set(ids);
|
|
342
342
|
const parents = new Map();
|
|
343
|
+
const settle = (session, deleted) => {
|
|
344
|
+
if (cleanup === undefined)
|
|
345
|
+
return;
|
|
346
|
+
database.deleteSessionRecord(session.id);
|
|
347
|
+
this.options.forget(session, this.options.nativeIds(session));
|
|
348
|
+
cleanup.settled(session, deleted);
|
|
349
|
+
};
|
|
343
350
|
const releases = [];
|
|
344
351
|
const locked = new Set();
|
|
345
352
|
const lock = (id) => {
|
|
346
353
|
const actual = this.options.runtime.getAgentSession(id)?.id ?? id;
|
|
347
354
|
if (locked.has(actual))
|
|
348
355
|
return;
|
|
349
|
-
releases.push(this.options.runtime.beginSessionHistoryMutation(actual, { allowQueued: true }));
|
|
356
|
+
releases.push(this.options.runtime.beginSessionHistoryMutation(actual, cleanup !== undefined && actual === root.id ? undefined : { allowQueued: true }));
|
|
350
357
|
locked.add(actual);
|
|
351
358
|
};
|
|
352
359
|
try {
|
|
@@ -355,6 +362,9 @@ export class SessionCatalogService {
|
|
|
355
362
|
const parent = ids[index];
|
|
356
363
|
const children = [
|
|
357
364
|
...database.listChildSessionRecords(root.workspaceId, parent).map((record) => record.id),
|
|
365
|
+
...(cleanup?.children.get(parent) ?? [])
|
|
366
|
+
.filter((session) => cleanup?.candidates.has(session.id))
|
|
367
|
+
.map((session) => session.externalSessionId ?? session.id),
|
|
358
368
|
...(liveChildren.get(parent) ?? [])
|
|
359
369
|
];
|
|
360
370
|
for (const child of children) {
|
|
@@ -371,35 +381,64 @@ export class SessionCatalogService {
|
|
|
371
381
|
}
|
|
372
382
|
const sessions = [];
|
|
373
383
|
for (const id of ids) {
|
|
374
|
-
const session =
|
|
384
|
+
const session = cleanup?.candidates.get(id) ??
|
|
385
|
+
this.options.runtime.getAgentSession(id) ??
|
|
375
386
|
(await this.options.projection.resolve(id, true));
|
|
376
387
|
if (session === undefined || session.workspaceId !== root.workspaceId)
|
|
377
388
|
throw new Error('SESSION_NOT_FOUND');
|
|
378
389
|
sessions.push(session);
|
|
379
390
|
}
|
|
380
391
|
// Validate every member before performing any irreversible native deletion.
|
|
381
|
-
for (const session of sessions)
|
|
382
|
-
|
|
383
|
-
|
|
392
|
+
for (const session of sessions) {
|
|
393
|
+
const missing = cleanup !== undefined && session.externalDiscovered === false;
|
|
394
|
+
if (missing && !this.options.runnerAvailable(session.runner))
|
|
395
|
+
throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
|
|
396
|
+
await this.assertRemovable(session, cleanup?.budget, missing);
|
|
397
|
+
}
|
|
398
|
+
if (cleanup !== undefined && this.cleanupRootProtected(root, cleanup.cutoff)) {
|
|
399
|
+
throw new Error('SESSION_WATCHING');
|
|
400
|
+
}
|
|
401
|
+
let batch = [];
|
|
402
|
+
const flushBatch = async () => {
|
|
403
|
+
if (batch.length === 0)
|
|
404
|
+
return;
|
|
405
|
+
cleanup?.budget.check();
|
|
406
|
+
await this.options.runners.require(batch[0].runner).removeNativeMembers(batch);
|
|
407
|
+
for (const session of batch)
|
|
408
|
+
settle(session, true);
|
|
409
|
+
batch = [];
|
|
410
|
+
};
|
|
384
411
|
for (const session of [...sessions].reverse()) {
|
|
412
|
+
if (cleanup !== undefined &&
|
|
413
|
+
(session.externalSessionId === null || session.externalDiscovered === false)) {
|
|
414
|
+
await flushBatch();
|
|
415
|
+
cleanup.budget.check();
|
|
416
|
+
settle(session, true);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
385
419
|
if (session.externalSessionId === null)
|
|
386
420
|
continue;
|
|
387
421
|
const runner = this.options.runners.require(session.runner);
|
|
388
422
|
if (runner.supportsNativeBatchRemoval?.() === true) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
423
|
+
if (batch.length > 0 && batch[0].runner !== session.runner)
|
|
424
|
+
await flushBatch();
|
|
425
|
+
batch.push(session);
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
// Preserve descendant-first order across batch and non-batch native owners.
|
|
429
|
+
await flushBatch();
|
|
430
|
+
cleanup?.budget.check();
|
|
431
|
+
const deleted = await this.removeNativeHistories(session, this.options.nativeIds(session));
|
|
432
|
+
settle(session, deleted);
|
|
392
433
|
}
|
|
393
|
-
else
|
|
394
|
-
await this.removeNativeHistories(session, this.options.nativeIds(session));
|
|
395
434
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
435
|
+
await flushBatch();
|
|
436
|
+
// Manual deletion preserves all relationships if any native owner fails.
|
|
437
|
+
if (cleanup === undefined) {
|
|
438
|
+
for (const session of sessions.reverse()) {
|
|
439
|
+
this.options.database().deleteSessionRecord(session.id);
|
|
440
|
+
this.options.forget(session, this.options.nativeIds(session));
|
|
441
|
+
}
|
|
403
442
|
}
|
|
404
443
|
}
|
|
405
444
|
finally {
|
|
@@ -443,6 +482,7 @@ export class SessionCatalogService {
|
|
|
443
482
|
let deleted = 0;
|
|
444
483
|
let skipped = 0;
|
|
445
484
|
let failed = 0;
|
|
485
|
+
const outcomes = new Map();
|
|
446
486
|
const deletedByRunner = {};
|
|
447
487
|
let candidateCount;
|
|
448
488
|
let timedOut = false;
|
|
@@ -458,7 +498,14 @@ export class SessionCatalogService {
|
|
|
458
498
|
candidates.set(session.id, session);
|
|
459
499
|
}
|
|
460
500
|
this.mergePersisted(candidates, workspaceId, false);
|
|
461
|
-
const
|
|
501
|
+
const children = new Map();
|
|
502
|
+
for (const candidate of candidates.values()) {
|
|
503
|
+
if (candidate.parentSessionId == null)
|
|
504
|
+
continue;
|
|
505
|
+
const siblings = children.get(candidate.parentSessionId) ?? [];
|
|
506
|
+
siblings.push(candidate);
|
|
507
|
+
children.set(candidate.parentSessionId, siblings);
|
|
508
|
+
}
|
|
462
509
|
candidateCount = candidates.size;
|
|
463
510
|
for (const session of candidates.values()) {
|
|
464
511
|
budget.check();
|
|
@@ -472,14 +519,6 @@ export class SessionCatalogService {
|
|
|
472
519
|
session.lastActivityAt > 0 &&
|
|
473
520
|
session.lastActivityAt >= cutoff) ||
|
|
474
521
|
(session.pinOrder !== null && session.pinOrder !== undefined) ||
|
|
475
|
-
(session.externalSessionId !== null &&
|
|
476
|
-
(this.options.database().listChildSessionRecords(workspaceId, session.externalSessionId)
|
|
477
|
-
.length > 0 ||
|
|
478
|
-
candidateParents.has(session.externalSessionId) ||
|
|
479
|
-
this.options.runtime
|
|
480
|
-
.listAgentSessions()
|
|
481
|
-
.some((child) => child.workspaceId === workspaceId &&
|
|
482
|
-
child.parentSessionId === session.externalSessionId))) ||
|
|
483
522
|
this.options.isWatching(session.id) ||
|
|
484
523
|
this.options.isRecovering?.(session.id) ||
|
|
485
524
|
this.options.runtime.sessionQueue(session.id).itemIds.length > 0 ||
|
|
@@ -487,24 +526,31 @@ export class SessionCatalogService {
|
|
|
487
526
|
.listRunsForSession(session.id)
|
|
488
527
|
.some((run) => !isTerminalRunStatus(run.status)) ||
|
|
489
528
|
this.options.externalActivity(session.id) === 'BUSY') {
|
|
490
|
-
skipped
|
|
529
|
+
outcomes.set(session.id, { session, status: 'skipped' });
|
|
491
530
|
continue;
|
|
492
531
|
}
|
|
493
532
|
try {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
533
|
+
await this.removeTree(session, {
|
|
534
|
+
candidates,
|
|
535
|
+
children,
|
|
536
|
+
cutoff,
|
|
537
|
+
budget,
|
|
538
|
+
settled: (member, wasDeleted) => {
|
|
539
|
+
candidates.delete(member.id);
|
|
540
|
+
outcomes.set(member.id, {
|
|
541
|
+
session: member,
|
|
542
|
+
status: wasDeleted ? 'deleted' : 'skipped'
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
});
|
|
500
546
|
}
|
|
501
547
|
catch (error) {
|
|
502
548
|
if (error instanceof WorkspaceSessionCleanupDeadline)
|
|
503
549
|
throw error;
|
|
504
550
|
if (isCleanupSkip(error))
|
|
505
|
-
skipped
|
|
551
|
+
outcomes.set(session.id, { session, status: 'skipped' });
|
|
506
552
|
else {
|
|
507
|
-
failed
|
|
553
|
+
outcomes.set(session.id, { session, status: 'failed' });
|
|
508
554
|
nodeLog('workspace.session.cleanup.failed', {
|
|
509
555
|
workspaceId,
|
|
510
556
|
sessionId: session.id,
|
|
@@ -514,6 +560,7 @@ export class SessionCatalogService {
|
|
|
514
560
|
}
|
|
515
561
|
}
|
|
516
562
|
}
|
|
563
|
+
budget.check();
|
|
517
564
|
}
|
|
518
565
|
catch (error) {
|
|
519
566
|
if (!(error instanceof WorkspaceSessionCleanupDeadline))
|
|
@@ -525,6 +572,16 @@ export class SessionCatalogService {
|
|
|
525
572
|
finally {
|
|
526
573
|
this.options.invalidateWorkspaceSessions(workspaceId);
|
|
527
574
|
}
|
|
575
|
+
for (const { session, status } of outcomes.values()) {
|
|
576
|
+
if (status === 'deleted') {
|
|
577
|
+
deleted += 1;
|
|
578
|
+
deletedByRunner[session.runner] = (deletedByRunner[session.runner] ?? 0) + 1;
|
|
579
|
+
}
|
|
580
|
+
else if (status === 'skipped')
|
|
581
|
+
skipped += 1;
|
|
582
|
+
else
|
|
583
|
+
failed += 1;
|
|
584
|
+
}
|
|
528
585
|
const result = {
|
|
529
586
|
deleted,
|
|
530
587
|
skipped,
|
|
@@ -535,7 +592,7 @@ export class SessionCatalogService {
|
|
|
535
592
|
timedOut: true,
|
|
536
593
|
...(candidateCount === undefined
|
|
537
594
|
? {}
|
|
538
|
-
: { remaining: candidateCount -
|
|
595
|
+
: { remaining: Math.max(0, candidateCount - outcomes.size) })
|
|
539
596
|
}
|
|
540
597
|
: {})
|
|
541
598
|
};
|
|
@@ -559,15 +616,16 @@ export class SessionCatalogService {
|
|
|
559
616
|
lastActivityAt: latestSessionActivityAt(projected.lastActivityAt, native.lastActivityAt)
|
|
560
617
|
};
|
|
561
618
|
}
|
|
562
|
-
async assertRemovable(session) {
|
|
619
|
+
async assertRemovable(session, budget, missing = false) {
|
|
563
620
|
if (this.options.isRecovering?.(session.id))
|
|
564
621
|
throw new Error('SESSION_RESTART_PENDING');
|
|
565
622
|
if (this.options.runtime
|
|
566
623
|
.listRunsForSession(session.id)
|
|
567
624
|
.some((run) => !isTerminalRunStatus(run.status)))
|
|
568
625
|
throw new Error('SESSION_ACTIVE');
|
|
569
|
-
if (session.externalSessionId !== null && session.nativeControl === 'EXTERNAL') {
|
|
570
|
-
|
|
626
|
+
if (!missing && session.externalSessionId !== null && session.nativeControl === 'EXTERNAL') {
|
|
627
|
+
const refresh = () => this.options.refreshActivity(session);
|
|
628
|
+
await (budget ? budget.read(refresh) : refresh());
|
|
571
629
|
if (this.options.externalActivity(session.id) === 'BUSY')
|
|
572
630
|
throw new Error('SESSION_ACTIVE');
|
|
573
631
|
}
|
|
@@ -592,55 +650,22 @@ export class SessionCatalogService {
|
|
|
592
650
|
}
|
|
593
651
|
return deleted;
|
|
594
652
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
653
|
+
cleanupRootProtected(session, cutoff) {
|
|
654
|
+
// Pin updates and Watch admission do not share the history mutation queue.
|
|
655
|
+
const pinOrder = session.externalSessionId === null
|
|
656
|
+
? undefined
|
|
657
|
+
: this.options
|
|
658
|
+
.database()
|
|
659
|
+
.getNativeSessionMetadata(session.runner, session.externalSessionId, session.workspaceId)?.pinOrder;
|
|
660
|
+
return ((session.externalDiscovered !== false &&
|
|
661
|
+
latestSessionActivityAt(session.lastActivityAt, this.options.runtime.getAgentSession(session.id)?.lastActivityAt ?? 0) >= cutoff) ||
|
|
662
|
+
(pinOrder !== undefined && pinOrder !== null) ||
|
|
663
|
+
this.options.isWatching(session.id) ||
|
|
664
|
+
this.options.isRecovering?.(session.id) === true ||
|
|
665
|
+
this.options.runtime.sessionQueue(session.id).itemIds.length > 0 ||
|
|
666
|
+
this.options.runtime
|
|
604
667
|
.listRunsForSession(session.id)
|
|
605
|
-
.some((run) => !isTerminalRunStatus(run.status)))
|
|
606
|
-
throw new Error('SESSION_ACTIVE');
|
|
607
|
-
if (session.nativeControl === 'EXTERNAL' && !missingCandidate) {
|
|
608
|
-
const refresh = () => this.options.refreshActivity(session);
|
|
609
|
-
await (budget ? budget.read(refresh) : refresh());
|
|
610
|
-
if (this.options.externalActivity(session.id) === 'BUSY')
|
|
611
|
-
throw new Error('SESSION_ACTIVE');
|
|
612
|
-
}
|
|
613
|
-
if (requireCleanupSafety) {
|
|
614
|
-
if (this.options.isWatching(session.id))
|
|
615
|
-
throw new Error('SESSION_WATCHING');
|
|
616
|
-
if (this.options.runtime.sessionQueue(session.id).itemIds.length > 0)
|
|
617
|
-
throw new Error('SESSION_QUEUE_NOT_EMPTY');
|
|
618
|
-
// Pin updates do not share the cleanup mutation queue.
|
|
619
|
-
const pinOrder = this.options
|
|
620
|
-
.database()
|
|
621
|
-
.getNativeSessionMetadata(session.runner, session.externalSessionId, session.workspaceId)?.pinOrder;
|
|
622
|
-
if (pinOrder !== undefined && pinOrder !== null)
|
|
623
|
-
return 'skipped';
|
|
624
|
-
if (this.options.runtime
|
|
625
|
-
.listRunsForSession(session.id)
|
|
626
|
-
.some((run) => !isTerminalRunStatus(run.status)))
|
|
627
|
-
throw new Error('SESSION_ACTIVE');
|
|
628
|
-
}
|
|
629
|
-
budget?.check();
|
|
630
|
-
const nativeIds = this.options.nativeIds(session);
|
|
631
|
-
if (missingCandidate) {
|
|
632
|
-
this.options.database().deleteSessionRecord(session.id);
|
|
633
|
-
this.options.forget(session, nativeIds);
|
|
634
|
-
return 'deleted';
|
|
635
|
-
}
|
|
636
|
-
const deleted = await this.removeNativeHistories(session, nativeIds);
|
|
637
|
-
this.options.database().deleteSessionRecord(session.id);
|
|
638
|
-
this.options.forget(session, nativeIds);
|
|
639
|
-
return deleted ? 'deleted' : 'skipped';
|
|
640
|
-
}
|
|
641
|
-
finally {
|
|
642
|
-
releaseMutation();
|
|
643
|
-
}
|
|
668
|
+
.some((run) => !isTerminalRunStatus(run.status)));
|
|
644
669
|
}
|
|
645
670
|
}
|
|
646
671
|
export function rewindTitle(sourceTitle, existingTitles = []) {
|
|
@@ -674,6 +699,7 @@ function deduplicateNativeSessions(sessions) {
|
|
|
674
699
|
function isCleanupSkip(error) {
|
|
675
700
|
return (error instanceof Error &&
|
|
676
701
|
(error.message === 'SESSION_ACTIVE' ||
|
|
702
|
+
error.message === 'SESSION_RESTART_PENDING' ||
|
|
677
703
|
error.message === 'SESSION_NOT_FOUND' ||
|
|
678
704
|
error.message === 'SESSION_QUEUE_NOT_EMPTY' ||
|
|
679
705
|
error.message === 'SESSION_WATCHING' ||
|
package/dist/workspace/git.js
CHANGED
|
@@ -107,6 +107,18 @@ export async function readRestrictedDiff(workspacePath, fromRef, toRef) {
|
|
|
107
107
|
}
|
|
108
108
|
return { binary: false, text: result.text, truncated: result.truncated };
|
|
109
109
|
}
|
|
110
|
+
/** Returns a bounded, deterministic refs snapshot, or undefined when it cannot be compared. */
|
|
111
|
+
export async function readGitRefsSnapshot(workspacePath, timeoutMs) {
|
|
112
|
+
if (timeoutMs <= 0)
|
|
113
|
+
return undefined;
|
|
114
|
+
try {
|
|
115
|
+
const result = await runReadOnlyGit(workspacePath, ['for-each-ref', '--sort=refname', '--format=%(refname)%00%(objectname)%00%(symref)'], { timeoutMs: Math.min(timeoutMs, GIT_TIMEOUT_MS), outputLimit: GIT_LARGE_OUTPUT_LIMIT });
|
|
116
|
+
return result.truncated ? undefined : result.output;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
110
122
|
/** Fetches refs only; failures never expose remote URLs or authentication diagnostics. */
|
|
111
123
|
export async function fetchGitRepository(workspacePath, timeoutMs) {
|
|
112
124
|
if (timeoutMs <= 0)
|
|
@@ -2,7 +2,7 @@ import { lstat } from 'node:fs/promises';
|
|
|
2
2
|
import { basename, posix, resolve } from 'node:path';
|
|
3
3
|
import { publicErrorMessage } from '@myagentroam/protocol';
|
|
4
4
|
import { nodeLog } from '../operational.js';
|
|
5
|
-
import { GitCommandError, fetchGitRepository, isWorkspaceChangeVisible, listGitWorktrees, listGitSubmodules, readCurrentChangesSummary, resolveGitRepository, resolveWorkspaceGitRoot } from './git.js';
|
|
5
|
+
import { GitCommandError, fetchGitRepository, readGitRefsSnapshot, isWorkspaceChangeVisible, listGitWorktrees, listGitSubmodules, readCurrentChangesSummary, resolveGitRepository, resolveWorkspaceGitRoot } from './git.js';
|
|
6
6
|
const CACHE_TTL_MS = 5_000;
|
|
7
7
|
const REPOSITORY_READ_CONCURRENCY = 4;
|
|
8
8
|
const WORKTREE_READ_CONCURRENCY = 4;
|
|
@@ -37,7 +37,6 @@ export class WorkspaceChangeService {
|
|
|
37
37
|
if (selected !== undefined)
|
|
38
38
|
paths = [repositoryPath];
|
|
39
39
|
else {
|
|
40
|
-
this.invalidate(workspace.id);
|
|
41
40
|
const current = (await this.current(workspace));
|
|
42
41
|
// Linked worktrees share the root's refs and must not fetch them twice.
|
|
43
42
|
paths = current.repositories
|
|
@@ -49,15 +48,25 @@ export class WorkspaceChangeService {
|
|
|
49
48
|
const results = await mapWithConcurrency(paths, REPOSITORY_READ_CONCURRENCY, async (path) => {
|
|
50
49
|
try {
|
|
51
50
|
if (performance.now() >= deadline)
|
|
52
|
-
return false;
|
|
51
|
+
return { success: false, updated: false };
|
|
53
52
|
const repository = selected ?? (await resolveGitRepository(root, path));
|
|
54
|
-
|
|
53
|
+
const remaining = () => Math.max(0, Math.floor(deadline - performance.now()));
|
|
54
|
+
const before = await readGitRefsSnapshot(repository, remaining());
|
|
55
|
+
const success = await fetchGitRepository(repository, remaining());
|
|
56
|
+
const after = await readGitRefsSnapshot(repository, remaining());
|
|
57
|
+
return {
|
|
58
|
+
success,
|
|
59
|
+
updated: before === undefined || after === undefined || !before.equals(after)
|
|
60
|
+
};
|
|
55
61
|
}
|
|
56
62
|
catch {
|
|
57
|
-
return false;
|
|
63
|
+
return { success: false, updated: false };
|
|
58
64
|
}
|
|
59
65
|
});
|
|
60
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
fetchFailed: results.some((result) => !result.success),
|
|
68
|
+
updated: results.some((result) => result.updated)
|
|
69
|
+
};
|
|
61
70
|
}
|
|
62
71
|
finally {
|
|
63
72
|
// Fetch is request/user scoped, never coalesced with another user's credentials.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.48",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"sharp": "0.35.5",
|
|
39
39
|
"ws": "^8.21.3",
|
|
40
40
|
"zod": "4.4.3",
|
|
41
|
-
"@myagentroam/agent": "0.11.
|
|
42
|
-
"@myagentroam/
|
|
43
|
-
"@myagentroam/protocol": "0.11.
|
|
44
|
-
"@myagentroam/
|
|
41
|
+
"@myagentroam/agent": "0.11.48",
|
|
42
|
+
"@myagentroam/code-navigation": "0.11.1",
|
|
43
|
+
"@myagentroam/protocol": "0.11.48",
|
|
44
|
+
"@myagentroam/marctl": "0.11.9"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/ws": "^8.18.1"
|