@myagentroam/node 0.11.43 → 0.11.45
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/application/node-lifecycle.js +30 -7
- package/dist/connector.js +5 -1
- package/dist/main.js +10 -3
- package/dist/runner/claude/managed-run-controller.js +3 -10
- package/dist/runner/claude/native-transcript.js +1 -1
- package/dist/runner/claude/tool-normalizer.js +16 -11
- package/dist/runner/codex/conversation-parser.js +29 -38
- package/dist/runner/codex/managed-run-controller.js +4 -4
- package/dist/runner/codex/native-transcript.js +5 -3
- package/dist/runner/codex/tool-normalizer.js +13 -17
- package/dist/runner/execution-presentation.js +8 -51
- package/dist/runner/mar-agent/agent-network-extension.js +43 -13
- package/dist/runner/mar-agent/agent-operation-presentation.js +167 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +118 -107
- package/dist/runner/mar-agent/session-runtime-registry.js +22 -12
- package/dist/runner/mar-agent/session-runtime.js +16 -11
- package/dist/runner/normalization.js +5 -0
- package/dist/runner/opencode/tool-normalizer.js +10 -10
- package/dist/service/agent-network-bindings.js +10 -49
- package/dist/service/agent-network-invoke.js +4 -19
- package/dist/service/agent-network-service.js +0 -16
- package/dist/service/marctl-caller.js +17 -9
- package/dist/session/runtime/conversation-projection.js +10 -3
- package/dist/workspace/git.js +44 -1
- package/dist/workspace/workspace-change-service.js +39 -1
- package/dist/workspace/workspace-workbench-service.js +8 -0
- package/package.json +4 -4
|
@@ -3,10 +3,8 @@ import { resolvedPersonalInstructions } from '../runner/personal-instructions.js
|
|
|
3
3
|
import { AgentNetworkService } from './agent-network-service.js';
|
|
4
4
|
import { isPlainRecord } from '../util/records.js';
|
|
5
5
|
import { safeErrorCode } from '../util/safe-error.js';
|
|
6
|
-
import { withSessionMessageOrigin } from './composer-message-input.js';
|
|
7
6
|
import { SESSION_ADMISSION_SIGNAL } from '../session/operation-admission.js';
|
|
8
7
|
import { assertSessionControlOwner } from './run-ownership-policy.js';
|
|
9
|
-
const approvedCalls = new WeakMap();
|
|
10
8
|
const terminalCalls = new WeakMap();
|
|
11
9
|
/** Bind a terminal actor only after the Node request boundary has authenticated its caller. */
|
|
12
10
|
export function actorForAuthenticatedTerminal(actor, workspaceId) {
|
|
@@ -14,14 +12,6 @@ export function actorForAuthenticatedTerminal(actor, workspaceId) {
|
|
|
14
12
|
terminalCalls.set(authenticated, { workspaceId });
|
|
15
13
|
return authenticated;
|
|
16
14
|
}
|
|
17
|
-
/** Bind a Host-approved invocation to its exact target and domain operations. */
|
|
18
|
-
export function actorForApprovedAgentCall(actor, context, agentIds, operations) {
|
|
19
|
-
if (context.permission?.decision !== 'approved')
|
|
20
|
-
return actor;
|
|
21
|
-
const approved = Object.freeze({ ...actor });
|
|
22
|
-
approvedCalls.set(approved, { agentIds: [...agentIds], operations: [...operations] });
|
|
23
|
-
return approved;
|
|
24
|
-
}
|
|
25
15
|
import { sessionPage } from '../session/pagination.js';
|
|
26
16
|
import { marctlSessionSummary, marctlSessionDetail } from './session-recent-activity.js';
|
|
27
17
|
/** Bind the collaboration use case to the existing Session, Run and Goal owners. */
|
|
@@ -50,7 +40,7 @@ export function createNodeAgentNetwork(options) {
|
|
|
50
40
|
const parent = terminal ? undefined : await actorSession(actor);
|
|
51
41
|
const workspaceId = terminal?.workspaceId ?? parent?.workspaceId;
|
|
52
42
|
const session = options.runtime.getAgentSession(id) ?? (await options.projection.resolve(id, true));
|
|
53
|
-
if (!session || session.workspaceId !== workspaceId
|
|
43
|
+
if (!session || session.workspaceId !== workspaceId)
|
|
54
44
|
throw new Error('SESSION_NOT_FOUND');
|
|
55
45
|
return session;
|
|
56
46
|
}
|
|
@@ -93,11 +83,8 @@ export function createNodeAgentNetwork(options) {
|
|
|
93
83
|
return result;
|
|
94
84
|
}
|
|
95
85
|
async function validateApproval(actor, session, input) {
|
|
96
|
-
if (!terminalCalls.has(actor))
|
|
97
|
-
|
|
98
|
-
if (session.parentSessionId !== parent.id)
|
|
99
|
-
throw new Error('AGENT_TARGET_NOT_AUTHORIZED');
|
|
100
|
-
}
|
|
86
|
+
if (!terminalCalls.has(actor))
|
|
87
|
+
await actorSession(actor);
|
|
101
88
|
assertSessionControlOwner(options.runtime, session.id, actor.requestUserId);
|
|
102
89
|
const approval = options.runtime.getApproval(input.approvalId);
|
|
103
90
|
if (!approval)
|
|
@@ -118,19 +105,10 @@ export function createNodeAgentNetwork(options) {
|
|
|
118
105
|
return new AgentNetworkService({
|
|
119
106
|
observations: options.observations,
|
|
120
107
|
target,
|
|
121
|
-
authorize: async (actor, session
|
|
108
|
+
authorize: async (actor, session) => {
|
|
109
|
+
if (!terminalCalls.has(actor))
|
|
110
|
+
await actorSession(actor);
|
|
122
111
|
assertSessionControlOwner(options.runtime, session.id, actor.requestUserId);
|
|
123
|
-
if (terminalCalls.has(actor))
|
|
124
|
-
return;
|
|
125
|
-
const source = await actorSession(actor);
|
|
126
|
-
if (session.parentSessionId === source.id)
|
|
127
|
-
return;
|
|
128
|
-
if (source.parentSessionId === session.id && operation === 'send')
|
|
129
|
-
return;
|
|
130
|
-
const approved = approvedCalls.get(actor);
|
|
131
|
-
if (approved?.agentIds.includes(session.id) && approved.operations.includes(operation))
|
|
132
|
-
return;
|
|
133
|
-
throw new Error('AGENT_TARGET_NOT_AUTHORIZED');
|
|
134
112
|
},
|
|
135
113
|
children: async (actor) => {
|
|
136
114
|
if (terminalCalls.has(actor))
|
|
@@ -144,27 +122,6 @@ export function createNodeAgentNetwork(options) {
|
|
|
144
122
|
observe: (actor) => {
|
|
145
123
|
options.ensureObservations(actor);
|
|
146
124
|
},
|
|
147
|
-
isUpward: async (actor, session) => !terminalCalls.has(actor) && (await actorSession(actor)).parentSessionId === session.id,
|
|
148
|
-
reportUpward: async (actor, session, input, signal) => {
|
|
149
|
-
const child = await actorSession(actor);
|
|
150
|
-
const runId = actor.runId;
|
|
151
|
-
const executionId = actor.executionId;
|
|
152
|
-
if (!runId ||
|
|
153
|
-
!executionId ||
|
|
154
|
-
options.runtime.currentExecutionId(runId) !== executionId ||
|
|
155
|
-
child.parentSessionId !== session.id)
|
|
156
|
-
throw new Error('AGENT_ACTOR_NOT_ACTIVE');
|
|
157
|
-
signal?.throwIfAborted();
|
|
158
|
-
const parent = options.observations.get(session.id);
|
|
159
|
-
if (!parent?.report({
|
|
160
|
-
childId: child.id,
|
|
161
|
-
runId,
|
|
162
|
-
executionId,
|
|
163
|
-
value: withSessionMessageOrigin(input.content, origin(child), session.parentSessionId)
|
|
164
|
-
}))
|
|
165
|
-
throw new Error('AGENT_PARENT_NOT_WARM');
|
|
166
|
-
return { delivery: 'reported', clientMessageId: input.clientMessageId };
|
|
167
|
-
},
|
|
168
125
|
freezeExecution: (session) => {
|
|
169
126
|
const snapshot = options.runtime.latestSessionExecution(session.id);
|
|
170
127
|
const run = options.runtime.activeSessionRun(session.id);
|
|
@@ -222,6 +179,7 @@ export function createNodeAgentNetwork(options) {
|
|
|
222
179
|
cwd: parent?.cwd ?? workspace.path,
|
|
223
180
|
content: input.content,
|
|
224
181
|
clientMessageId: input.clientMessageId,
|
|
182
|
+
__suppressNotifications: true,
|
|
225
183
|
...(!terminal && parent ? { __marctlOrigin: origin(parent) } : {}),
|
|
226
184
|
...(input.title === undefined ? {} : { title: input.title }),
|
|
227
185
|
...(model ? { model } : {}),
|
|
@@ -237,6 +195,7 @@ export function createNodeAgentNetwork(options) {
|
|
|
237
195
|
objective: input.goal.kind === 'set' ? input.goal.objective : '',
|
|
238
196
|
secretEnvironment: childEnvironment(actor),
|
|
239
197
|
messageContent: input.content,
|
|
198
|
+
suppressNotifications: true,
|
|
240
199
|
...(!terminal && parent ? { messageOrigin: origin(parent) } : {}),
|
|
241
200
|
clientMessageId: input.clientMessageId,
|
|
242
201
|
...(input.attachments === undefined ? {} : { attachments: input.attachments }),
|
|
@@ -276,6 +235,7 @@ export function createNodeAgentNetwork(options) {
|
|
|
276
235
|
session: current,
|
|
277
236
|
objective: change.objective,
|
|
278
237
|
executionContext,
|
|
238
|
+
suppressNotifications: true,
|
|
279
239
|
secretEnvironment: childEnvironment(actor),
|
|
280
240
|
...(actor.requestUserId === undefined ? {} : { initiatedByUserId: actor.requestUserId }),
|
|
281
241
|
...(actor.personalInstructions === undefined
|
|
@@ -306,6 +266,7 @@ export function createNodeAgentNetwork(options) {
|
|
|
306
266
|
...input,
|
|
307
267
|
...(signal ? { [SESSION_ADMISSION_SIGNAL]: signal } : {}),
|
|
308
268
|
sessionId: session.id,
|
|
269
|
+
__suppressNotifications: true,
|
|
309
270
|
...(parent ? { __marctlOrigin: origin(parent) } : {})
|
|
310
271
|
}));
|
|
311
272
|
return publicReceipt(result);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { actorForAuthenticatedTerminal } from './agent-network-bindings.js';
|
|
2
2
|
import { isPlainRecord } from '../util/records.js';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { modelUsageGrantSchema } from '@myagentroam/protocol';
|
|
@@ -52,21 +52,6 @@ export function executionAgentActor(input) {
|
|
|
52
52
|
}
|
|
53
53
|
/** Shared command dispatch; callers must bind actor authority at their trusted boundary. */
|
|
54
54
|
export async function invokeAgentNetwork(service, actor, action, payload, context, requestSignal) {
|
|
55
|
-
const targets = typeof payload.agentId === 'string'
|
|
56
|
-
? [payload.agentId]
|
|
57
|
-
: Array.isArray(payload.agentIds)
|
|
58
|
-
? payload.agentIds.filter((id) => typeof id === 'string')
|
|
59
|
-
: [];
|
|
60
|
-
const operations = action === 'send'
|
|
61
|
-
? ['send', 'goal']
|
|
62
|
-
: action === 'stop'
|
|
63
|
-
? ['stop']
|
|
64
|
-
: action === 'wait'
|
|
65
|
-
? ['wait']
|
|
66
|
-
: [];
|
|
67
|
-
const authorized = context
|
|
68
|
-
? actorForApprovedAgentCall(actor, context, targets, operations)
|
|
69
|
-
: actor;
|
|
70
55
|
const signal = context?.signal ?? requestSignal;
|
|
71
56
|
switch (action) {
|
|
72
57
|
case 'start':
|
|
@@ -85,11 +70,11 @@ export async function invokeAgentNetwork(service, actor, action, payload, contex
|
|
|
85
70
|
...(typeof payload.title === 'string' ? { title: payload.title.trim() } : {})
|
|
86
71
|
}, signal);
|
|
87
72
|
case 'send':
|
|
88
|
-
return service.send(
|
|
73
|
+
return service.send(actor, payload, signal);
|
|
89
74
|
case 'stop':
|
|
90
|
-
return service.stop(
|
|
75
|
+
return service.stop(actor, payload);
|
|
91
76
|
case 'wait':
|
|
92
|
-
return service.wait(
|
|
77
|
+
return service.wait(actor, {
|
|
93
78
|
...payload,
|
|
94
79
|
...(context
|
|
95
80
|
? { signal: context.signal, interrupt: context.waitInterrupt }
|
|
@@ -91,22 +91,6 @@ export class AgentNetworkService {
|
|
|
91
91
|
if (input.approval !== undefined)
|
|
92
92
|
await this.options.validateApproval(actor, session, input.approval);
|
|
93
93
|
signal?.throwIfAborted();
|
|
94
|
-
if (hasMessage && (await this.options.isUpward(actor, session))) {
|
|
95
|
-
signal?.throwIfAborted();
|
|
96
|
-
if (goal.kind !== 'unchanged' ||
|
|
97
|
-
input.approval !== undefined ||
|
|
98
|
-
input.deliveryIntent === 'REPLACE_CURRENT' ||
|
|
99
|
-
input.deliveryIntent === 'QUEUE' ||
|
|
100
|
-
input.attachments?.length ||
|
|
101
|
-
input.attachmentIds?.length)
|
|
102
|
-
throw new Error('AGENT_UPWARD_REPORT_INVALID');
|
|
103
|
-
const result = await this.options.reportUpward(actor, session, {
|
|
104
|
-
clientMessageId: input.clientMessageId ?? randomUUID(),
|
|
105
|
-
content
|
|
106
|
-
}, signal);
|
|
107
|
-
return { agentId: session.id, message: { status: 'accepted', result } };
|
|
108
|
-
}
|
|
109
|
-
signal?.throwIfAborted();
|
|
110
94
|
if (session.parentSessionId === actor.sessionId)
|
|
111
95
|
this.options.observe(actor, session);
|
|
112
96
|
const approvalResult = input.approval === undefined
|
|
@@ -21,7 +21,7 @@ const executionManagementOperations = new Set([
|
|
|
21
21
|
'session.config.update',
|
|
22
22
|
'session.schedule.agent.save'
|
|
23
23
|
]);
|
|
24
|
-
/**
|
|
24
|
+
/** Keep execution management within its authenticated Workspace and Run ownership. */
|
|
25
25
|
export async function authorizeMarctlExecutionOperation(input) {
|
|
26
26
|
if (!isPlainRecord(input.caller))
|
|
27
27
|
throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
|
|
@@ -31,19 +31,27 @@ export async function authorizeMarctlExecutionOperation(input) {
|
|
|
31
31
|
throw new Error('MARCTL_EXECUTION_CREDENTIAL_INVALID');
|
|
32
32
|
if (executionReadOperations.has(input.operation))
|
|
33
33
|
return;
|
|
34
|
-
if (!
|
|
35
|
-
!isPlainRecord(input.data) ||
|
|
36
|
-
typeof input.data.sessionId !== 'string' ||
|
|
37
|
-
typeof input.data.workspaceId !== 'string')
|
|
34
|
+
if (!isPlainRecord(input.data) || typeof input.data.workspaceId !== 'string')
|
|
38
35
|
throw new Error('MARCTL_AGENT_NETWORK_REQUIRED');
|
|
36
|
+
let sessionId;
|
|
37
|
+
if (input.operation === 'run.approval.decide') {
|
|
38
|
+
const run = typeof input.data.runId === 'string' ? input.runtime.getRun(input.data.runId) : undefined;
|
|
39
|
+
if (!run)
|
|
40
|
+
throw new Error('RUN_NOT_FOUND');
|
|
41
|
+
sessionId = run.sessionId;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
if (!executionManagementOperations.has(input.operation) ||
|
|
45
|
+
typeof input.data.sessionId !== 'string')
|
|
46
|
+
throw new Error('MARCTL_AGENT_NETWORK_REQUIRED');
|
|
47
|
+
sessionId = input.data.sessionId;
|
|
48
|
+
}
|
|
39
49
|
const source = input.runtime.getAgentSession(input.caller.sessionId);
|
|
40
|
-
const target = input.runtime.getAgentSession(input.
|
|
41
|
-
(await input.resolveSession(input.data.sessionId));
|
|
50
|
+
const target = input.runtime.getAgentSession(sessionId) ?? (await input.resolveSession(sessionId));
|
|
42
51
|
if (!source ||
|
|
43
52
|
!target ||
|
|
44
53
|
source.workspaceId !== input.data.workspaceId ||
|
|
45
|
-
target.workspaceId !== source.workspaceId
|
|
46
|
-
(target.id !== source.id && target.parentSessionId !== source.id))
|
|
54
|
+
target.workspaceId !== source.workspaceId)
|
|
47
55
|
throw new Error('AGENT_TARGET_NOT_AUTHORIZED');
|
|
48
56
|
assertSessionControlOwner(input.runtime, target.id, input.userId);
|
|
49
57
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { appendToolOutput, retainExecutionInputTruncation, toolOutputSchema, permissionStopSchema } from '@myagentroam/protocol';
|
|
2
2
|
import { projectConversationWorkProcesses } from './work-process.js';
|
|
3
3
|
function isRecord(value) {
|
|
4
4
|
return typeof value === 'object' && value !== null;
|
|
@@ -292,9 +292,16 @@ export function mergeConversationItemPayload(existing, next) {
|
|
|
292
292
|
}
|
|
293
293
|
if (next.append === 'output' &&
|
|
294
294
|
isRecord(next.payload.output) &&
|
|
295
|
-
|
|
295
|
+
Array.isArray(next.payload.output.blocks)) {
|
|
296
|
+
const update = toolOutputSchema.parse(next.payload.output);
|
|
297
|
+
const prior = base.output === undefined ? undefined : toolOutputSchema.parse(base.output);
|
|
298
|
+
value.output = appendToolOutput(prior, update);
|
|
296
299
|
if (next.payload.toolPresentation !== undefined)
|
|
297
|
-
value.toolPresentation =
|
|
300
|
+
value.toolPresentation = retainExecutionInputTruncation(base.toolPresentation, next.payload.toolPresentation);
|
|
301
|
+
}
|
|
302
|
+
if (next.append === 'output' &&
|
|
303
|
+
isRecord(next.payload.output) &&
|
|
304
|
+
typeof next.payload.output.text === 'string') {
|
|
298
305
|
const prior = isRecord(base.output) && typeof base.output.text === 'string' ? base.output : undefined;
|
|
299
306
|
const text = `${prior?.text ?? ''}${next.payload.output.text}`;
|
|
300
307
|
value.output = {
|
package/dist/workspace/git.js
CHANGED
|
@@ -107,6 +107,23 @@ export async function readRestrictedDiff(workspacePath, fromRef, toRef) {
|
|
|
107
107
|
}
|
|
108
108
|
return { binary: false, text: result.text, truncated: result.truncated };
|
|
109
109
|
}
|
|
110
|
+
/** Fetches refs only; failures never expose remote URLs or authentication diagnostics. */
|
|
111
|
+
export async function fetchGitRepository(workspacePath, timeoutMs) {
|
|
112
|
+
if (timeoutMs <= 0)
|
|
113
|
+
return false;
|
|
114
|
+
try {
|
|
115
|
+
const result = await runGitAllowFailure(workspacePath, ['fetch', '--all', '--quiet', '--no-recurse-submodules', '--no-write-fetch-head'], {
|
|
116
|
+
timeoutMs,
|
|
117
|
+
processGroup: true,
|
|
118
|
+
includeStderrInTimeout: false,
|
|
119
|
+
environment: { GIT_TERMINAL_PROMPT: '0', GCM_INTERACTIVE: 'never' }
|
|
120
|
+
});
|
|
121
|
+
return result.code === 0;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
110
127
|
function currentBranchFromStatusHeader(header) {
|
|
111
128
|
const value = header.slice(3);
|
|
112
129
|
if (value.startsWith('HEAD '))
|
|
@@ -898,10 +915,12 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
|
|
|
898
915
|
cwd: workspacePath,
|
|
899
916
|
shell: false,
|
|
900
917
|
windowsHide: true,
|
|
918
|
+
detached: options.processGroup === true && process.platform !== 'win32',
|
|
901
919
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
902
920
|
env: {
|
|
903
921
|
...process.env,
|
|
904
922
|
...gitOperationEnvironment.getStore(),
|
|
923
|
+
...options.environment,
|
|
905
924
|
...(options.readOnly === true ? { GIT_OPTIONAL_LOCKS: '0' } : {})
|
|
906
925
|
}
|
|
907
926
|
});
|
|
@@ -911,6 +930,7 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
|
|
|
911
930
|
let errorSize = 0;
|
|
912
931
|
let truncated = false;
|
|
913
932
|
let timedOut = false;
|
|
933
|
+
let forcedTerminationTimer;
|
|
914
934
|
const timer = setTimeout(() => {
|
|
915
935
|
timedOut = true;
|
|
916
936
|
if (process.platform === 'win32') {
|
|
@@ -932,6 +952,26 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
|
|
|
932
952
|
});
|
|
933
953
|
return;
|
|
934
954
|
}
|
|
955
|
+
if (options.processGroup === true && child.pid !== undefined) {
|
|
956
|
+
const pid = child.pid;
|
|
957
|
+
try {
|
|
958
|
+
// Give Git a chance to release transaction locks before bounded escalation.
|
|
959
|
+
process.kill(-pid, 'SIGTERM');
|
|
960
|
+
forcedTerminationTimer = setTimeout(() => {
|
|
961
|
+
try {
|
|
962
|
+
process.kill(-pid, 'SIGKILL');
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
// The process group may have completed its cleanup already.
|
|
966
|
+
}
|
|
967
|
+
}, 250);
|
|
968
|
+
forcedTerminationTimer.unref();
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
catch {
|
|
972
|
+
// The group may have exited before the timeout fired.
|
|
973
|
+
}
|
|
974
|
+
}
|
|
935
975
|
child.kill('SIGKILL');
|
|
936
976
|
}, options.timeoutMs ?? GIT_TIMEOUT_MS);
|
|
937
977
|
child.stdout.on('data', (chunk) => {
|
|
@@ -953,6 +993,7 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
|
|
|
953
993
|
});
|
|
954
994
|
child.once('error', (error) => {
|
|
955
995
|
clearTimeout(timer);
|
|
996
|
+
clearTimeout(forcedTerminationTimer);
|
|
956
997
|
logNodeCommandFailure(`git ${args[0] ?? 'unknown'}`, {
|
|
957
998
|
reason: error.code ?? 'START_FAILED'
|
|
958
999
|
});
|
|
@@ -960,9 +1001,11 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
|
|
|
960
1001
|
});
|
|
961
1002
|
child.once('close', (code) => {
|
|
962
1003
|
clearTimeout(timer);
|
|
1004
|
+
clearTimeout(forcedTerminationTimer);
|
|
963
1005
|
const stderr = Buffer.concat(errors).toString('utf8');
|
|
964
1006
|
if (timedOut) {
|
|
965
|
-
|
|
1007
|
+
const details = options.includeStderrInTimeout === false ? '' : stderr;
|
|
1008
|
+
reject(gitCommandFailure(code === null || code === 0 ? 1 : code, `Git command timed out after ${options.timeoutMs ?? GIT_TIMEOUT_MS} ms.${details ? `\n${details}` : ''}`, args[0] ?? 'unknown', Math.round(performance.now() - startedAt), 'TIMEOUT'));
|
|
966
1009
|
return;
|
|
967
1010
|
}
|
|
968
1011
|
const output = Buffer.concat(chunks);
|
|
@@ -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, isWorkspaceChangeVisible, listGitWorktrees, listGitSubmodules, readCurrentChangesSummary, resolveGitRepository, resolveWorkspaceGitRoot } from './git.js';
|
|
5
|
+
import { GitCommandError, fetchGitRepository, 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;
|
|
@@ -27,6 +27,44 @@ export class WorkspaceChangeService {
|
|
|
27
27
|
this.changesCache.clear();
|
|
28
28
|
this.changesVersions.clear();
|
|
29
29
|
}
|
|
30
|
+
async fetch(workspace, repositoryPath) {
|
|
31
|
+
if (workspace.kind !== 'GIT_WORKSPACE')
|
|
32
|
+
throw new Error('WORKSPACE_NOT_GIT');
|
|
33
|
+
const root = await resolveWorkspaceGitRoot(workspace.path, workspace.includeParentRepository ?? true);
|
|
34
|
+
// A selected History repository is validated before any network operation.
|
|
35
|
+
const selected = repositoryPath === undefined ? undefined : await resolveGitRepository(root, repositoryPath);
|
|
36
|
+
let paths;
|
|
37
|
+
if (selected !== undefined)
|
|
38
|
+
paths = [repositoryPath];
|
|
39
|
+
else {
|
|
40
|
+
this.invalidate(workspace.id);
|
|
41
|
+
const current = (await this.current(workspace));
|
|
42
|
+
// Linked worktrees share the root's refs and must not fetch them twice.
|
|
43
|
+
paths = current.repositories
|
|
44
|
+
.filter((entry) => entry.source !== 'WORKTREE' && entry.status !== 'UNAVAILABLE')
|
|
45
|
+
.map((entry) => entry.repositoryPath);
|
|
46
|
+
}
|
|
47
|
+
const deadline = performance.now() + 10_000;
|
|
48
|
+
try {
|
|
49
|
+
const results = await mapWithConcurrency(paths, REPOSITORY_READ_CONCURRENCY, async (path) => {
|
|
50
|
+
try {
|
|
51
|
+
if (performance.now() >= deadline)
|
|
52
|
+
return false;
|
|
53
|
+
const repository = selected ?? (await resolveGitRepository(root, path));
|
|
54
|
+
return await fetchGitRepository(repository, Math.max(0, Math.floor(deadline - performance.now())));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return { fetchFailed: results.some((success) => !success) };
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
// Fetch is request/user scoped, never coalesced with another user's credentials.
|
|
64
|
+
// Invalidate even after partial failure: some remotes may already have advanced.
|
|
65
|
+
this.invalidate(workspace.id);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
30
68
|
async current(workspace) {
|
|
31
69
|
if (workspace.kind !== 'GIT_WORKSPACE')
|
|
32
70
|
throw new Error('WORKSPACE_NOT_GIT');
|
|
@@ -58,6 +58,7 @@ export class WorkspaceWorkbenchService {
|
|
|
58
58
|
'workspace.files.upload.complete': (data) => this.completeUpload(record(data)),
|
|
59
59
|
'workspace.files.upload.cancel': (data) => this.cancelUpload(record(data)),
|
|
60
60
|
'workspace.changes.current': (data) => this.currentChanges(record(data)),
|
|
61
|
+
'workspace.git.fetch': (data) => this.gitFetch(record(data)),
|
|
61
62
|
'workspace.change.diff': (data) => this.changeDiff(record(data)),
|
|
62
63
|
'workspace.change.restore': (data) => this.restoreChange(record(data)),
|
|
63
64
|
'workspace.git.refs.list': (data) => this.gitRefs(record(data)),
|
|
@@ -239,6 +240,13 @@ export class WorkspaceWorkbenchService {
|
|
|
239
240
|
async gitRefs(input) {
|
|
240
241
|
return { refs: await listGitHistoryRefs(await this.gitRepository(input)) };
|
|
241
242
|
}
|
|
243
|
+
async gitFetch(input) {
|
|
244
|
+
if (typeof input.workspaceId !== 'string')
|
|
245
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
246
|
+
if (input.repositoryPath !== undefined && typeof input.repositoryPath !== 'string')
|
|
247
|
+
throw new Error('GIT_REPOSITORY_INVALID');
|
|
248
|
+
return this.options.changes.fetch(this.gitWorkspace(input.workspaceId), input.repositoryPath);
|
|
249
|
+
}
|
|
242
250
|
async gitHistory(input) {
|
|
243
251
|
const refs = this.refs(input);
|
|
244
252
|
if (input.cursor !== undefined && typeof input.cursor !== 'string')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.45",
|
|
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/marctl": "0.11.6",
|
|
41
|
+
"@myagentroam/agent": "0.11.45",
|
|
43
42
|
"@myagentroam/code-navigation": "0.11.1",
|
|
44
|
-
"@myagentroam/
|
|
43
|
+
"@myagentroam/marctl": "0.11.6",
|
|
44
|
+
"@myagentroam/protocol": "0.11.45"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/ws": "^8.18.1"
|