@myagentroam/node 0.9.106 → 0.9.108

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.
@@ -301,7 +301,8 @@ export async function probeClaudeCodeAsync(command = 'claude') {
301
301
  const authInvocation = platformCliInvocation(command, ['auth', 'status', '--json']);
302
302
  const authResult = await runRunnerProbe(authInvocation.command, authInvocation.args, {
303
303
  env: minimalRunnerEnvironment(),
304
- timeoutMs: 3_000
304
+ timeoutMs: 3_000,
305
+ logStderr: false
305
306
  });
306
307
  let authenticated = false;
307
308
  if (authResult.status === 0) {
@@ -1,8 +1,10 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { basename } from 'node:path';
2
3
  import { writeNodeLog } from './rotating-log.js';
3
4
  const secretKey = /(?:password|credential|token|cookie|authorization|secret|api[_-]?key)/i;
4
5
  export function runRunnerProbe(command, args, options) {
5
6
  return new Promise((resolve) => {
7
+ const startedAt = performance.now();
6
8
  const child = spawn(command, args, {
7
9
  windowsHide: true,
8
10
  env: options.env,
@@ -11,22 +13,29 @@ export function runRunnerProbe(command, args, options) {
11
13
  let stdout = '';
12
14
  let stderr = '';
13
15
  let settled = false;
14
- const finish = (status) => {
16
+ const finish = (status, reason) => {
15
17
  if (settled)
16
18
  return;
17
19
  settled = true;
18
20
  clearTimeout(timer);
21
+ if (status !== 0)
22
+ logNodeCommandFailure(basename(command), {
23
+ ...(status === null ? {} : { exitCode: status }),
24
+ ...(options.logStderr === false ? {} : { stderr }),
25
+ ...(reason === undefined ? {} : { reason }),
26
+ durationMs: Math.round(performance.now() - startedAt)
27
+ });
19
28
  resolve({ status, stdout, stderr });
20
29
  };
21
30
  const timer = setTimeout(() => {
22
- finish(null);
31
+ finish(null, 'TIMEOUT');
23
32
  child.kill();
24
33
  }, options.timeoutMs);
25
34
  child.stdout.setEncoding('utf8');
26
35
  child.stderr.setEncoding('utf8');
27
36
  child.stdout.on('data', (value) => (stdout += value));
28
37
  child.stderr.on('data', (value) => (stderr += value));
29
- child.once('error', () => finish(null));
38
+ child.once('error', (error) => finish(null, error.code ?? 'START_FAILED'));
30
39
  child.once('close', (status) => finish(status));
31
40
  });
32
41
  }
@@ -120,3 +129,14 @@ export function nodeLog(event, fields = {}) {
120
129
  process.stdout.write(line);
121
130
  writeNodeLog(line);
122
131
  }
132
+ /** Logs Node-owned subprocess failures without copying argv, stdout or environment values. */
133
+ export function logNodeCommandFailure(command, result) {
134
+ const details = result.stderr?.trim().slice(0, 1_000);
135
+ nodeLog('node.command.failed', {
136
+ command,
137
+ ...(result.exitCode === undefined ? {} : { exitCode: result.exitCode }),
138
+ ...(result.reason === undefined ? {} : { reason: result.reason }),
139
+ ...(result.durationMs === undefined ? {} : { durationMs: result.durationMs }),
140
+ ...(details ? { errorDetails: details } : {})
141
+ });
142
+ }
@@ -134,8 +134,10 @@ export class MarAgentConversationNormalizer {
134
134
  if (action === 'start' && typeof input.command === 'string') {
135
135
  const command = {
136
136
  itemId: ownItemId,
137
+ turnKey: event.turnId ?? event.executionId,
137
138
  command: compactRunnerText(input.command, 10_000),
138
139
  cwd: typeof input.cwd === 'string' ? compactRunnerText(input.cwd, 4_096) : null,
140
+ status: 'IN_PROGRESS',
139
141
  outputPreview: '',
140
142
  exitCode: null,
141
143
  truncated: false
@@ -154,6 +156,29 @@ export class MarAgentConversationNormalizer {
154
156
  typeof input.processId === 'string') {
155
157
  const origin = this.#processOrigins.get(input.processId);
156
158
  if (origin !== undefined) {
159
+ const previous = this.#commands.get(origin);
160
+ const sameTurn = previous?.turnKey === (event.turnId ?? event.executionId);
161
+ if (previous !== undefined && !sameTurn) {
162
+ const command = {
163
+ itemId: ownItemId,
164
+ turnKey: event.turnId ?? event.executionId,
165
+ command: previous.command,
166
+ cwd: previous.cwd,
167
+ status: 'IN_PROGRESS',
168
+ outputPreview: '',
169
+ exitCode: null,
170
+ truncated: false
171
+ };
172
+ this.#commands.set(event.callId, command);
173
+ this.#tools.set(event.callId, {
174
+ toolName: event.toolName,
175
+ input: event.input,
176
+ itemId: ownItemId,
177
+ structured: true,
178
+ commandOrigin: event.callId
179
+ });
180
+ return [];
181
+ }
157
182
  this.#tools.set(event.callId, {
158
183
  toolName: event.toolName,
159
184
  input: event.input,
@@ -201,6 +226,7 @@ export class MarAgentConversationNormalizer {
201
226
  : result.status === 'completed' && result.exitCode === 0
202
227
  ? 'COMPLETED'
203
228
  : 'FAILED';
229
+ command.status = status;
204
230
  return [commandItem(command, status)];
205
231
  }
206
232
  }
@@ -256,8 +282,15 @@ export class MarAgentConversationNormalizer {
256
282
  const state = this.#tools.get(event.callId);
257
283
  if (event.toolName === 'exec' && state?.commandOrigin !== undefined) {
258
284
  const command = this.#commands.get(state.commandOrigin);
259
- if (command !== undefined)
285
+ const input = record(state.input);
286
+ if (command !== undefined &&
287
+ command.status === 'IN_PROGRESS' &&
288
+ (state.itemId !== command.itemId ||
289
+ input?.action === 'start' ||
290
+ (input?.action == null && typeof input?.command === 'string'))) {
291
+ command.status = 'FAILED';
260
292
  return [commandItem(command, 'FAILED')];
293
+ }
261
294
  }
262
295
  if (event.toolName === 'apply_patch' && state?.structured === true)
263
296
  return [fileChangeItem(state?.itemId ?? itemId('tool', event.callId), 'FAILED', [])];
@@ -361,7 +394,9 @@ function commandItem(command, status) {
361
394
  function applyCommandResult(command, result) {
362
395
  const output = [result.stdout, result.stderr].filter(Boolean).join('\n');
363
396
  if (output.length > 0) {
364
- const combined = [command.outputPreview, output].filter(Boolean).join('\n');
397
+ const combined = result.cached
398
+ ? output
399
+ : [command.outputPreview, output].filter(Boolean).join('\n');
365
400
  command.truncated ||= combined.length > 10_000;
366
401
  command.outputPreview = compactRunnerText(combined, 10_000);
367
402
  }
@@ -443,6 +478,7 @@ function execResult(value, summary, eventTruncated = false) {
443
478
  stdout,
444
479
  stderr,
445
480
  ...(exitCode === undefined ? {} : { exitCode }),
481
+ ...(data?.cached === true || summaryData?.cached === true ? { cached: true } : {}),
446
482
  truncated: eventTruncated ||
447
483
  data?.truncated === true ||
448
484
  summaryData?.truncated === true ||
@@ -511,6 +511,7 @@ export class MarAgentManagedRunController {
511
511
  agent,
512
512
  session,
513
513
  host: localHost,
514
+ normalizer: new MarAgentConversationNormalizer(),
514
515
  mcpLease,
515
516
  dynamicCredentials,
516
517
  models,
@@ -639,7 +640,7 @@ export class MarAgentManagedRunController {
639
640
  itemId: boundedConversationItemId('mar-agent-message', event.itemId)
640
641
  });
641
642
  else {
642
- let normalizer = this.#normalizers.get(runId);
643
+ let normalizer = this.#normalizers.get(runId) ?? this.#sessionRuntimes.get(event.sessionId)?.normalizer;
643
644
  if (normalizer === undefined) {
644
645
  normalizer = new MarAgentConversationNormalizer();
645
646
  this.#normalizers.set(runId, normalizer);
@@ -4,6 +4,7 @@ import { createReadStream, createWriteStream } from 'node:fs';
4
4
  import { lstat, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
5
5
  import { relative, resolve, sep } from 'node:path';
6
6
  import { createInterface } from 'node:readline';
7
+ import { logNodeCommandFailure } from '../../operational.js';
7
8
  import { compareTagNames } from './ctags-query.js';
8
9
  import { sourceFileFingerprint } from './navigation-target-validator.js';
9
10
  const BUILD_TIMEOUT_MS = 120_000;
@@ -175,6 +176,15 @@ export async function runCtags(executable, cwd, fileList, destination) {
175
176
  windowsHide: true,
176
177
  stdio: ['ignore', 'ignore', 'pipe']
177
178
  });
179
+ const errors = [];
180
+ let errorBytes = 0;
181
+ const stderr = () => Buffer.concat(errors).toString('utf8');
182
+ const reportFailure = (reason, exitCode) => logNodeCommandFailure('ctags', {
183
+ reason,
184
+ ...(exitCode === undefined ? {} : { exitCode }),
185
+ stderr: stderr(),
186
+ durationMs: Date.now() - startedAt
187
+ });
178
188
  let settled = false;
179
189
  const settle = (callback) => {
180
190
  if (settled)
@@ -197,12 +207,32 @@ export async function runCtags(executable, cwd, fileList, destination) {
197
207
  };
198
208
  const timer = setTimeout(() => {
199
209
  terminate();
200
- settle(() => reject(new Error('SYMBOL_INDEX_BUILD_FAILED')));
210
+ settle(() => {
211
+ reportFailure('TIMEOUT');
212
+ reject(new Error('SYMBOL_INDEX_BUILD_FAILED'));
213
+ });
201
214
  }, BUILD_TIMEOUT_MS);
202
215
  timer.unref();
203
- child.stderr.on('data', () => undefined);
204
- child.once('error', () => settle(() => reject(new Error('SYMBOL_INDEX_BUILD_FAILED'))));
205
- child.once('exit', (code) => settle(() => (code === 0 ? resolvePromise() : reject(new Error('SYMBOL_INDEX_BUILD_FAILED')))));
216
+ child.stderr.on('data', (chunk) => {
217
+ const remaining = Math.max(0, 1_000 - errorBytes);
218
+ if (remaining === 0)
219
+ return;
220
+ const retained = chunk.subarray(0, remaining);
221
+ errors.push(retained);
222
+ errorBytes += retained.length;
223
+ });
224
+ child.once('error', (error) => settle(() => {
225
+ reportFailure(error.code ?? 'START_FAILED');
226
+ reject(new Error('SYMBOL_INDEX_BUILD_FAILED'));
227
+ }));
228
+ child.once('close', (code) => settle(() => {
229
+ if (code === 0)
230
+ resolvePromise();
231
+ else {
232
+ reportFailure('EXIT', code ?? undefined);
233
+ reject(new Error('SYMBOL_INDEX_BUILD_FAILED'));
234
+ }
235
+ }));
206
236
  });
207
237
  return Date.now() - startedAt;
208
238
  }
@@ -18,6 +18,30 @@ export function convergeConversationWatchPage(previous, previousObserved, next,
18
18
  page: composeConversationRuntimePage(previous, next),
19
19
  authority: authority ?? next.source
20
20
  };
21
+ // A Runner history read can lag a Run that the Watch has already delivered. Keep the
22
+ // unconfirmed Runtime tail until the history includes that Turn; source priority alone
23
+ // does not establish that the returned window contains the latest conversation.
24
+ if (previous?.source === 'runtime' &&
25
+ previous.turns.length > 0 &&
26
+ next.source !== 'runtime' &&
27
+ (authority === undefined || sourceRank(next.source) >= sourceRank(authority))) {
28
+ const boundary = previous.turns.findLastIndex((turn) => next.turns.some((candidate) => sameConversationTurn(candidate, turn)));
29
+ if (boundary < 0)
30
+ return { page: previous, authority: next.source };
31
+ const tail = previous.turns
32
+ .slice(boundary + 1)
33
+ .filter((turn) => !next.turns.some((candidate) => sameConversationTurn(candidate, turn)));
34
+ if (tail.length > 0)
35
+ return {
36
+ page: {
37
+ ...next,
38
+ source: 'runtime',
39
+ snapshotSequence: previous.snapshotSequence,
40
+ turns: [...next.turns, ...tail]
41
+ },
42
+ authority: next.source
43
+ };
44
+ }
21
45
  if (previous === undefined ||
22
46
  authority === undefined ||
23
47
  sourceRank(next.source) >= sourceRank(authority))
@@ -249,8 +249,20 @@ export class NativeSessionWatchService {
249
249
  const page = await (readHistoricalPage
250
250
  ? this.options.readHistoryPage(session, limit)
251
251
  : this.options.readPage(session, limit));
252
- if (mode === 'observation' && revisionBeforeRead !== watch.eventRevision)
253
- return watch.page;
252
+ if (revisionBeforeRead !== watch.eventRevision) {
253
+ // Initial still needs a full page for its new lease, whose pending events will
254
+ // replay above that page's sequence. Never publish this invalidated read to
255
+ // existing leases or replace their shared observation state.
256
+ if (mode !== 'initial')
257
+ return watch.page;
258
+ const initial = this.options.convergePage(watch.page, watch.observedPage, page, watch.authority).page;
259
+ if (watch.page === undefined && watch.signature === undefined)
260
+ watch.signature = JSON.stringify({
261
+ source: initial.source,
262
+ turns: initial.turns.slice(-(EMITTED_TURN_LIMIT + 1))
263
+ });
264
+ return initial;
265
+ }
254
266
  stage = 'present_session';
255
267
  const presented = this.options.present(session);
256
268
  const sessionSignature = JSON.stringify(presented);
@@ -265,7 +277,7 @@ export class NativeSessionWatchService {
265
277
  : page.turns.slice(-EMITTED_TURN_LIMIT);
266
278
  const observed = { ...page, turns };
267
279
  stage = 'converge_page';
268
- const convergence = this.options.convergePage(watch.page, watch.observedPage, observed, watch.authority);
280
+ const convergence = this.options.convergePage(watch.page, watch.observedPage, page, watch.authority);
269
281
  const converged = convergence.page;
270
282
  const accepted = converged.turns.length <= EMITTED_TURN_LIMIT + 1
271
283
  ? converged
@@ -285,8 +297,9 @@ export class NativeSessionWatchService {
285
297
  const changed = signature !== watch.signature;
286
298
  if (changed) {
287
299
  watch.signature = signature;
288
- watch.page = accepted;
289
300
  }
301
+ if (changed || watch.page === undefined)
302
+ watch.page = accepted;
290
303
  stage = 'emit_page';
291
304
  if (completingConvergence && accepted.source === 'runtime')
292
305
  this.releasePendingEvents(session.id);
@@ -294,7 +307,8 @@ export class NativeSessionWatchService {
294
307
  this.emitSnapshotToLeases(session, accepted, true);
295
308
  else if (changed)
296
309
  this.emitObservedPage(session, accepted);
297
- return mode === 'initial' ? page : accepted;
310
+ // Initial uses the full result of the same authority decision as observation.
311
+ return mode === 'initial' ? converged : accepted;
298
312
  }
299
313
  catch (error) {
300
314
  watch.failures += 1;
@@ -42,7 +42,7 @@ export class WorkspaceChangeService {
42
42
  ...(error instanceof GitCommandError
43
43
  ? {
44
44
  parameters: { gitExitCode: error.code },
45
- details: `Git command exited with status ${error.code}.`
45
+ details: error.publicDetails
46
46
  }
47
47
  : {})
48
48
  });
@@ -53,6 +53,7 @@ export class WorkspaceChangeService {
53
53
  groupPath: path,
54
54
  errorCode: error.code,
55
55
  ...error.parameters,
56
+ ...(error.details === undefined ? {} : { errorDetails: error.details }),
56
57
  durationMs: Math.round(performance.now() - startedAt)
57
58
  });
58
59
  };
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { createReadStream } from 'node:fs';
3
3
  import { lstat, realpath } from 'node:fs/promises';
4
4
  import { isAbsolute, join, relative, sep } from 'node:path';
5
+ import { logNodeCommandFailure } from '../operational.js';
5
6
  import { bundledRipgrepPath } from '../ripgrep-binary.js';
6
7
  const RESULT_LIMIT = 200;
7
8
  const PER_FILE_LIMIT = 20;
@@ -113,10 +114,14 @@ export class WorkspaceContentSearchService {
113
114
  async validateNativeRegex(root, search, deadline) {
114
115
  const command = commandFor('RIPGREP', [], search, this.ripgrep ?? undefined);
115
116
  const result = await runCommand(command.file, [...command.args, '-'], root, deadline);
116
- if (result.code === 2)
117
+ if (result.code === 2) {
118
+ logNodeCommandFailure('rg', { exitCode: result.code, reason: 'INVALID_QUERY' });
117
119
  throw new Error('FILE_CONTENT_SEARCH_INVALID');
118
- if (!command.validCodes.includes(result.code))
120
+ }
121
+ if (!command.validCodes.includes(result.code)) {
122
+ logNodeCommandFailure('rg', { exitCode: result.code, stderr: result.stderr });
119
123
  throw new Error('CONTENT_SEARCH_COMMAND_FAILED');
124
+ }
120
125
  }
121
126
  async candidates(workspace, deadline) {
122
127
  const root = await realpath(workspace.path);
@@ -167,8 +172,11 @@ export class WorkspaceContentSearchService {
167
172
  const command = commandFor(engine, batch, search, this.ripgrep ?? undefined);
168
173
  const result = await runCommand(command.file, command.args, root, deadline, engine === 'RIPGREP');
169
174
  if (!command.validCodes.includes(result.code)) {
170
- if (search.regex && result.code === 2)
175
+ if (search.regex && result.code === 2) {
176
+ logNodeCommandFailure('rg', { exitCode: result.code, reason: 'INVALID_QUERY' });
171
177
  throw new Error('FILE_CONTENT_SEARCH_INVALID');
178
+ }
179
+ logNodeCommandFailure('rg', { exitCode: result.code, stderr: result.stderr });
172
180
  throw new Error('CONTENT_SEARCH_COMMAND_FAILED');
173
181
  }
174
182
  const paths = new Map(batch.flatMap((file) => [
@@ -268,21 +276,42 @@ function resolveRipgrep() {
268
276
  }
269
277
  async function runCommand(file, args, cwd, deadline, stopAfterResultLimit = false) {
270
278
  return new Promise((resolve, reject) => {
279
+ const startedAt = performance.now();
271
280
  const child = spawn(file, args, {
272
281
  cwd,
273
282
  windowsHide: true,
274
- stdio: ['ignore', 'pipe', 'ignore']
283
+ stdio: ['ignore', 'pipe', 'pipe']
275
284
  });
276
285
  const chunks = [];
286
+ const errors = [];
277
287
  let pending = Buffer.alloc(0);
278
288
  let bytes = 0;
289
+ let errorBytes = 0;
279
290
  let matches = 0;
280
291
  let stopped = false;
281
- const timer = setTimeout(() => child.kill(), Math.max(1, deadline - Date.now()));
292
+ let failed = false;
293
+ let timedOut = false;
294
+ const timer = setTimeout(() => {
295
+ timedOut = true;
296
+ child.kill();
297
+ }, Math.max(1, deadline - Date.now()));
282
298
  child.once('error', (error) => {
283
299
  clearTimeout(timer);
300
+ failed = true;
301
+ logNodeCommandFailure('rg', {
302
+ reason: error.code ?? 'START_FAILED',
303
+ durationMs: Math.round(performance.now() - startedAt)
304
+ });
284
305
  reject(error);
285
306
  });
307
+ child.stderr.on('data', (chunk) => {
308
+ const remaining = Math.max(0, 1_000 - errorBytes);
309
+ if (remaining === 0)
310
+ return;
311
+ const retained = chunk.subarray(0, remaining);
312
+ errors.push(retained);
313
+ errorBytes += retained.length;
314
+ });
286
315
  child.stdout.on('data', (chunk) => {
287
316
  bytes += chunk.length;
288
317
  if (bytes > OUTPUT_BYTE_LIMIT) {
@@ -311,14 +340,23 @@ async function runCommand(file, args, cwd, deadline, stopAfterResultLimit = fals
311
340
  });
312
341
  child.once('close', (code, signal) => {
313
342
  clearTimeout(timer);
343
+ if (failed)
344
+ return;
345
+ const stderr = Buffer.concat(errors).toString('utf8');
314
346
  if (stopped)
315
- resolve({ code: 0, stdout: Buffer.concat(chunks) });
316
- else if (signal !== null || bytes > OUTPUT_BYTE_LIMIT)
347
+ resolve({ code: 0, stdout: Buffer.concat(chunks), stderr });
348
+ else if (timedOut || signal !== null || bytes > OUTPUT_BYTE_LIMIT) {
349
+ logNodeCommandFailure('rg', {
350
+ reason: timedOut ? 'TIMEOUT' : bytes > OUTPUT_BYTE_LIMIT ? 'OUTPUT_LIMIT' : 'SIGNAL',
351
+ stderr,
352
+ durationMs: Math.round(performance.now() - startedAt)
353
+ });
317
354
  reject(new Error('CONTENT_SEARCH_COMMAND_LIMIT'));
355
+ }
318
356
  else {
319
357
  if (stopAfterResultLimit && pending.length > 0)
320
358
  chunks.push(pending);
321
- resolve({ code: code ?? -1, stdout: Buffer.concat(chunks) });
359
+ resolve({ code: code ?? -1, stdout: Buffer.concat(chunks), stderr });
322
360
  }
323
361
  });
324
362
  });
package/dist/workspace.js CHANGED
@@ -4,6 +4,7 @@ import { homedir, platform as hostPlatform } from 'node:os';
4
4
  import { basename, dirname, isAbsolute, posix, relative, resolve, sep, win32 } from 'node:path';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { minimatch } from 'minimatch';
7
+ import { logNodeCommandFailure } from './operational.js';
7
8
  import { terminateProcessTree } from './process-tree.js';
8
9
  import { gitOperationEnvironment } from './service/git-operation-service.js';
9
10
  const GIT_TIMEOUT_MS = 5_000;
@@ -1116,7 +1117,7 @@ export async function listGitWorktrees(workspacePath) {
1116
1117
  if (nulDelimited.code === 0)
1117
1118
  return parseGitWorktreeList(workspacePath, nulDelimited, true);
1118
1119
  if (!canRetryGitWorktreeListWithoutNullOutput(nulDelimited))
1119
- throw new GitCommandError(nulDelimited.code, nulDelimited.stderr);
1120
+ throw gitCommandFailure(nulDelimited.code, nulDelimited.stderr, 'worktree');
1120
1121
  const lineDelimited = await runReadOnlyGit(workspacePath, ['worktree', 'list', '--porcelain'], options);
1121
1122
  return parseGitWorktreeList(workspacePath, lineDelimited, false);
1122
1123
  }
@@ -1570,7 +1571,7 @@ export async function readGitHistoryFileContentRange(repository, commit, request
1570
1571
  timeoutMs: GIT_HISTORY_TIMEOUT_MS
1571
1572
  });
1572
1573
  if (result.code !== 0)
1573
- throw new GitCommandError(result.code, result.stderr);
1574
+ throw gitCommandFailure(result.code, result.stderr, 'cat-file');
1574
1575
  const content = result.output.subarray(offset, Math.min(size, offset + limit));
1575
1576
  const nextOffset = offset + content.length;
1576
1577
  return {
@@ -1633,7 +1634,7 @@ export async function openGitHistoryFileContent(repository, commit, requestedPat
1633
1634
  yield new Uint8Array(chunk);
1634
1635
  const result = await exit;
1635
1636
  if (result.code !== 0)
1636
- throw new GitCommandError(result.code, result.stderr);
1637
+ throw gitCommandFailure(result.code, result.stderr, 'cat-file');
1637
1638
  }
1638
1639
  finally {
1639
1640
  if (!closed)
@@ -1686,7 +1687,7 @@ async function readCurrentChangeGitDiff(workspacePath, path) {
1686
1687
  path
1687
1688
  ]);
1688
1689
  if (result.code !== 0 && result.code !== 1)
1689
- throw new GitCommandError(result.code, result.stderr);
1690
+ throw gitCommandFailure(result.code, result.stderr, 'diff');
1690
1691
  return result;
1691
1692
  }
1692
1693
  return runReadOnlyGit(workspacePath, [
@@ -1816,12 +1817,25 @@ function gitChange(status) {
1816
1817
  export class GitCommandError extends Error {
1817
1818
  code;
1818
1819
  stderr;
1820
+ publicDetails;
1821
+ publicParameters;
1819
1822
  constructor(code, stderr) {
1820
1823
  super('GIT_COMMAND_FAILED');
1821
1824
  this.code = code;
1822
1825
  this.stderr = stderr;
1826
+ this.publicParameters = { gitExitCode: code };
1827
+ this.publicDetails = stderr.trim().slice(0, 1_000) || `Git exited with status ${code}.`;
1823
1828
  }
1824
1829
  }
1830
+ function gitCommandFailure(code, stderr, subcommand, durationMs, reason) {
1831
+ logNodeCommandFailure(`git ${subcommand}`, {
1832
+ exitCode: code,
1833
+ stderr,
1834
+ ...(durationMs === undefined ? {} : { durationMs }),
1835
+ ...(reason === undefined ? {} : { reason })
1836
+ });
1837
+ return new GitCommandError(code, stderr);
1838
+ }
1825
1839
  async function runReadOnlyGit(workspacePath, args, options = {}) {
1826
1840
  return runGit(workspacePath, args, { ...options, readOnly: true });
1827
1841
  }
@@ -1829,14 +1843,16 @@ async function runReadOnlyGitAllowFailure(workspacePath, args, options = {}) {
1829
1843
  return runGitAllowFailure(workspacePath, args, { ...options, readOnly: true });
1830
1844
  }
1831
1845
  async function runGit(workspacePath, args, options = {}) {
1846
+ const startedAt = performance.now();
1832
1847
  const result = await runGitAllowFailure(workspacePath, args, options);
1833
1848
  if (result.code !== 0) {
1834
- throw new GitCommandError(result.code, result.stderr);
1849
+ throw gitCommandFailure(result.code, result.stderr, args[0] ?? 'unknown', Math.round(performance.now() - startedAt));
1835
1850
  }
1836
1851
  return result;
1837
1852
  }
1838
1853
  async function runGitAllowFailure(workspacePath, args, options = {}) {
1839
1854
  return new Promise((resolve, reject) => {
1855
+ const startedAt = performance.now();
1840
1856
  const outputLimit = options.outputLimit ?? GIT_OUTPUT_LIMIT;
1841
1857
  const child = spawn('git', [...args], {
1842
1858
  cwd: workspacePath,
@@ -1854,7 +1870,9 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
1854
1870
  let size = 0;
1855
1871
  let errorSize = 0;
1856
1872
  let truncated = false;
1873
+ let timedOut = false;
1857
1874
  const timer = setTimeout(() => {
1875
+ timedOut = true;
1858
1876
  if (process.platform === 'win32') {
1859
1877
  const pid = child.pid;
1860
1878
  if (pid === undefined) {
@@ -1895,16 +1913,24 @@ async function runGitAllowFailure(workspacePath, args, options = {}) {
1895
1913
  });
1896
1914
  child.once('error', (error) => {
1897
1915
  clearTimeout(timer);
1916
+ logNodeCommandFailure(`git ${args[0] ?? 'unknown'}`, {
1917
+ reason: error.code ?? 'START_FAILED'
1918
+ });
1898
1919
  reject(error);
1899
1920
  });
1900
1921
  child.once('close', (code) => {
1901
1922
  clearTimeout(timer);
1923
+ const stderr = Buffer.concat(errors).toString('utf8');
1924
+ if (timedOut) {
1925
+ reject(gitCommandFailure(code === null || code === 0 ? 1 : code, `Git command timed out after ${options.timeoutMs ?? GIT_TIMEOUT_MS} ms.${stderr ? `\n${stderr}` : ''}`, args[0] ?? 'unknown', Math.round(performance.now() - startedAt), 'TIMEOUT'));
1926
+ return;
1927
+ }
1902
1928
  const output = Buffer.concat(chunks);
1903
1929
  resolve({
1904
1930
  code: code ?? 1,
1905
1931
  output,
1906
1932
  text: output.toString('utf8'),
1907
- stderr: Buffer.concat(errors).toString('utf8'),
1933
+ stderr,
1908
1934
  truncated
1909
1935
  });
1910
1936
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.106",
3
+ "version": "0.9.108",
4
4
  "description": "MyAgentRoam Node runtime CLI.",
5
5
  "license": "Apache-2.0 AND GPL-2.0-or-later",
6
6
  "type": "module",
@@ -30,9 +30,9 @@
30
30
  "npm": "11.12.1",
31
31
  "ws": "^8.21.3",
32
32
  "zod": "4.4.3",
33
- "@myagentroam/agent": "0.9.106",
34
- "@myagentroam/code-navigation": "0.9.106",
35
- "@myagentroam/protocol": "0.9.106"
33
+ "@myagentroam/agent": "0.9.108",
34
+ "@myagentroam/protocol": "0.9.108",
35
+ "@myagentroam/code-navigation": "0.9.108"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/ws": "^8.18.1"