@sagentlab/navarch-runtime 0.1.9 → 0.1.10

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.
@@ -136,7 +136,11 @@ function tomlKey(value) {
136
136
  function safeEnvSegment(value) {
137
137
  return value.toUpperCase().replace(/[^A-Z0-9_]/g, "_");
138
138
  }
139
- /** Parses stdout for `codex exec --json` usage/final-message events and folds them onto the raw result (best-effort; leaves tokensIn/tokensOut/costUsd/reportText unset when nothing parses — see exit-conditions.cts#parseCodexJsonEvents). */
139
+ /**
140
+ * Parses stdout for `codex exec --json` usage/final-message events and folds
141
+ * them onto the raw result. Codex currently reports token counts but no USD
142
+ * cost, so costUsd remains absent unless the stream explicitly provides one.
143
+ */
140
144
  function attachUsage(result) {
141
145
  const events = (0, exit_conditions_cjs_1.parseCodexJsonEvents)(result.stdout);
142
146
  if (events.length === 0)
@@ -147,7 +151,7 @@ function attachUsage(result) {
147
151
  ...result,
148
152
  tokensIn: usage.tokensIn,
149
153
  tokensOut: usage.tokensOut,
150
- costUsd: usage.costUsd,
154
+ ...(usage.costUsd !== undefined ? { costUsd: usage.costUsd } : {}),
151
155
  ...(reportText !== undefined ? { reportText } : {}),
152
156
  };
153
157
  }
package/dist/cli.cjs CHANGED
@@ -19,6 +19,19 @@ const update_coordinator_cjs_1 = require("./update-coordinator.cjs");
19
19
  const supervisor_cjs_1 = require("./supervisor.cjs");
20
20
  const log = (0, logger_cjs_1.createLogger)("cli");
21
21
  const PACKAGE_NAME = "@sagentlab/navarch-runtime";
22
+ /** Include a control-plane response's safe error detail in top-level CLI failures. */
23
+ function describeCliError(err) {
24
+ const message = err instanceof Error ? err.message : String(err);
25
+ if (!err || typeof err !== "object" || !("body" in err))
26
+ return message;
27
+ const body = err.body;
28
+ const detail = body && typeof body === "object" && "error" in body && typeof body.error === "string"
29
+ ? body.error
30
+ : typeof body === "string"
31
+ ? body
32
+ : undefined;
33
+ return detail && !message.includes(detail) ? `${message}: ${detail}` : message;
34
+ }
22
35
  /**
23
36
  * How to tell the user to re-invoke this CLI, matching however THEY launched
24
37
  * it. The bare `navarch-runtime` bin only exists on PATH after a global install
@@ -305,7 +318,7 @@ async function main(argv = process.argv.slice(2)) {
305
318
  }
306
319
  }
307
320
  catch (err) {
308
- log.error(err instanceof Error ? err.message : String(err));
321
+ log.error(describeCliError(err));
309
322
  process.exitCode = 1;
310
323
  }
311
324
  }
@@ -95,7 +95,7 @@ function parseCodexJsonEvents(stdout) {
95
95
  function extractUsageFromCodexEvents(events) {
96
96
  let tokensIn = 0;
97
97
  let tokensOut = 0;
98
- let costUsd = 0;
98
+ let costUsd;
99
99
  for (const event of events) {
100
100
  if (event.type === "turn.completed" && event.usage) {
101
101
  tokensIn = event.usage.input_tokens ?? 0;
@@ -104,8 +104,22 @@ function extractUsageFromCodexEvents(events) {
104
104
  costUsd = event.usage.total_cost_usd;
105
105
  }
106
106
  if (event.msg?.type === "token_count") {
107
- tokensIn = (event.msg.input_tokens ?? 0) + (event.msg.cached_input_tokens ?? 0);
108
- tokensOut = event.msg.output_tokens ?? 0;
107
+ const nested = event.msg.info?.total_token_usage;
108
+ tokensIn = nested
109
+ ? (nested.input_tokens ?? 0)
110
+ : (event.msg.input_tokens ?? 0) + (event.msg.cached_input_tokens ?? 0);
111
+ tokensOut = nested?.output_tokens ?? event.msg.output_tokens ?? 0;
112
+ if (typeof nested?.total_cost_usd === "number")
113
+ costUsd = nested.total_cost_usd;
114
+ }
115
+ if (event.type === "event_msg" && event.payload?.type === "token_count") {
116
+ const total = event.payload.info?.total_token_usage;
117
+ if (total) {
118
+ tokensIn = total.input_tokens ?? 0;
119
+ tokensOut = total.output_tokens ?? 0;
120
+ if (typeof total.total_cost_usd === "number")
121
+ costUsd = total.total_cost_usd;
122
+ }
109
123
  }
110
124
  if (typeof event.msg?.total_cost_usd === "number") {
111
125
  costUsd = event.msg.total_cost_usd;
package/dist/session.cjs CHANGED
@@ -7,6 +7,7 @@ exports.runSession = runSession;
7
7
  exports.toEnvMap = toEnvMap;
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const node_fs_1 = require("node:fs");
10
+ const api_cjs_1 = require("./api.cjs");
10
11
  const sandbox_cjs_1 = require("./sandbox.cjs");
11
12
  const index_cjs_1 = require("./adapters/index.cjs");
12
13
  const exit_conditions_cjs_1 = require("./exit-conditions.cjs");
@@ -21,6 +22,7 @@ const worktree_guard_cjs_1 = require("./worktree-guard.cjs");
21
22
  /** Filename the generated platform MCP config is written under inside the session metadata directory. */
22
23
  const MCP_CONFIG_FILENAME = "mcp-config.json";
23
24
  const GIT_CREDENTIAL_HELPER_FILENAME = "git-credential-navarch.cjs";
25
+ const PR_REQUIRED_COMPLETION_RETRIES = 2;
24
26
  const log = (0, logger_cjs_1.createLogger)("session");
25
27
  /**
26
28
  * Runs one claimed task end to end (implementation-plan.md WP-07):
@@ -250,7 +252,8 @@ async function runSession(deps, claimed, sessionId) {
250
252
  const bin = config.agentType === "codex" ? config.codexBin : config.claudeBin;
251
253
  const extraArgs = config.agentType === "codex" ? config.codexExtraArgs : config.claudeExtraArgs;
252
254
  const attempts = [];
253
- let result;
255
+ let nextPrompt = null;
256
+ let prRequiredCompletionRetries = 0;
254
257
  while (true) {
255
258
  // Guidance can arrive while the worktree/sandbox is being prepared.
256
259
  // It is already included in deliveredGuidance, so clear the pending
@@ -259,9 +262,11 @@ async function runSession(deps, claimed, sessionId) {
259
262
  activeAbortController = new AbortController();
260
263
  if (leaseLost)
261
264
  activeAbortController.abort();
262
- const runPrompt = deliveredGuidance.length > (bundle.guidance?.length ?? 0)
263
- ? (0, prompt_cjs_1.renderGuidanceCorrectionPrompt)(promptText, deliveredGuidance)
264
- : promptText;
265
+ const runPrompt = nextPrompt ??
266
+ (deliveredGuidance.length > (bundle.guidance?.length ?? 0)
267
+ ? (0, prompt_cjs_1.renderGuidanceCorrectionPrompt)(promptText, deliveredGuidance)
268
+ : promptText);
269
+ nextPrompt = null;
265
270
  await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), runPrompt, "utf8");
266
271
  const turnResult = await adapter.run({
267
272
  prompt: runPrompt,
@@ -286,66 +291,93 @@ async function runSession(deps, claimed, sessionId) {
286
291
  await pollLease();
287
292
  if (!leaseLost && pendingGuidance.length > 0)
288
293
  continue;
289
- result = {
294
+ const result = {
290
295
  ...turnResult,
291
- tokensIn: attempts.reduce((sum, attempt) => sum + (attempt.tokensIn ?? 0), 0),
292
- tokensOut: attempts.reduce((sum, attempt) => sum + (attempt.tokensOut ?? 0), 0),
293
- costUsd: attempts.reduce((sum, attempt) => sum + (attempt.costUsd ?? 0), 0),
296
+ tokensIn: sumReportedUsage(attempts, "tokensIn"),
297
+ tokensOut: sumReportedUsage(attempts, "tokensOut"),
298
+ costUsd: sumReportedUsage(attempts, "costUsd"),
294
299
  };
295
- break;
296
- }
297
- const mapping = (0, exit_conditions_cjs_1.mapExitCondition)({ ...result, killedByLeaseLoss: leaseLost || result.killedByLeaseLoss });
298
- try {
299
- const prUrl = await (0, github_pr_cjs_1.findHeadBranchPullRequestUrl)({
300
- repository: bundle.repository?.full_name ?? task.repo,
301
- headBranch: gitWorktree.branch,
302
- githubToken,
300
+ const mapping = (0, exit_conditions_cjs_1.mapExitCondition)({
301
+ ...result,
302
+ killedByLeaseLoss: leaseLost || result.killedByLeaseLoss,
303
303
  });
304
- if (prUrl)
305
- mapping.evidenceUrls.push(prUrl);
306
- }
307
- catch (err) {
308
- // Evidence discovery is best-effort: a GitHub outage or token scope
309
- // mismatch must not turn an otherwise valid completion into a crash.
310
- log.warn(`head-branch PR lookup failed for ${leaseId}: ${String(err)}`);
311
- }
312
- const knownSecrets = registry.list();
313
- if (mapping.leaseOutcome === "failed") {
314
- log.warn(`adapter failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets)}`);
315
- }
316
- const transcript = attempts
317
- .flatMap((attempt, index) => [
318
- `# agent turn ${index + 1} stdout`,
319
- (0, redact_cjs_1.redactText)(attempt.stdout, knownSecrets),
320
- "",
321
- `# agent turn ${index + 1} stderr`,
322
- (0, redact_cjs_1.redactText)(attempt.stderr, knownSecrets),
323
- "",
324
- ])
325
- .join("\n");
326
- let transcriptUrl;
327
- try {
328
- const { upload_url, public_url } = await api.getTranscriptUploadUrl(leaseId);
329
- await (0, upload_cjs_1.uploadTranscript)(upload_url, transcript);
330
- transcriptUrl = public_url;
331
- }
332
- catch (err) {
333
- log.warn(`transcript upload failed for ${leaseId}: ${String(err)}`);
304
+ try {
305
+ const prUrl = await (0, github_pr_cjs_1.findHeadBranchPullRequestUrl)({
306
+ repository: bundle.repository?.full_name ?? task.repo,
307
+ headBranch: gitWorktree.branch,
308
+ githubToken,
309
+ });
310
+ if (prUrl)
311
+ mapping.evidenceUrls.push(prUrl);
312
+ }
313
+ catch (err) {
314
+ // Evidence discovery is best-effort: a GitHub outage or token scope
315
+ // mismatch must not turn an otherwise valid completion into a crash.
316
+ log.warn(`head-branch PR lookup failed for ${leaseId}: ${String(err)}`);
317
+ }
318
+ const knownSecrets = registry.list();
319
+ if (mapping.leaseOutcome === "failed") {
320
+ log.warn(`adapter failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets)}`);
321
+ }
322
+ const transcript = attempts
323
+ .flatMap((attempt, index) => [
324
+ `# agent turn ${index + 1} stdout`,
325
+ (0, redact_cjs_1.redactText)(attempt.stdout, knownSecrets),
326
+ "",
327
+ `# agent turn ${index + 1} stderr`,
328
+ (0, redact_cjs_1.redactText)(attempt.stderr, knownSecrets),
329
+ "",
330
+ ])
331
+ .join("\n");
332
+ let transcriptUrl;
333
+ try {
334
+ const { upload_url, public_url } = await api.getTranscriptUploadUrl(leaseId);
335
+ await (0, upload_cjs_1.uploadTranscript)(upload_url, transcript);
336
+ transcriptUrl = public_url;
337
+ }
338
+ catch (err) {
339
+ log.warn(`transcript upload failed for ${leaseId}: ${String(err)}`);
340
+ }
341
+ const completion = {
342
+ status: mapping.leaseOutcome,
343
+ report: (0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets),
344
+ evidence_urls: mapping.evidenceUrls,
345
+ cost: {
346
+ ...(result.tokensIn !== undefined ? { tokens_in: result.tokensIn } : {}),
347
+ ...(result.tokensOut !== undefined ? { tokens_out: result.tokensOut } : {}),
348
+ ...(result.costUsd !== undefined ? { cost_usd: result.costUsd } : {}),
349
+ },
350
+ transcript_url: transcriptUrl,
351
+ exit_status: mapping.exitStatus,
352
+ agent_type: config.agentType,
353
+ ...executionReport,
354
+ };
355
+ try {
356
+ await api.completeLease(leaseId, completion);
357
+ break;
358
+ }
359
+ catch (err) {
360
+ const rejection = mapping.leaseOutcome === "completed" ? prRequiredRejectionMessage(err) : null;
361
+ if (!rejection)
362
+ throw err;
363
+ const redactedRejection = (0, redact_cjs_1.redactText)(rejection, knownSecrets);
364
+ if (prRequiredCompletionRetries < PR_REQUIRED_COMPLETION_RETRIES) {
365
+ prRequiredCompletionRetries += 1;
366
+ log.warn(`completion for ${leaseId} requires a pull request; restarting agent turn ${prRequiredCompletionRetries}/${PR_REQUIRED_COMPLETION_RETRIES} in the same worktree.`);
367
+ nextPrompt = redactedRejection;
368
+ continue;
369
+ }
370
+ log.warn(`completion for ${leaseId} still requires a pull request after ${PR_REQUIRED_COMPLETION_RETRIES} retries; failing with the control-plane rejection.`);
371
+ await api.completeLease(leaseId, {
372
+ ...completion,
373
+ status: "failed",
374
+ report: redactedRejection,
375
+ failure_summary: redactedRejection,
376
+ exit_status: "failed",
377
+ });
378
+ break;
379
+ }
334
380
  }
335
- await api.completeLease(leaseId, {
336
- status: mapping.leaseOutcome,
337
- report: (0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets),
338
- evidence_urls: mapping.evidenceUrls,
339
- cost: {
340
- tokens_in: result.tokensIn ?? 0,
341
- tokens_out: result.tokensOut ?? 0,
342
- cost_usd: result.costUsd ?? 0,
343
- },
344
- transcript_url: transcriptUrl,
345
- exit_status: mapping.exitStatus,
346
- agent_type: config.agentType,
347
- ...executionReport,
348
- });
349
381
  }
350
382
  catch (err) {
351
383
  log.error(`session ${leaseId} threw before completing: ${String(err)}`);
@@ -373,6 +405,23 @@ async function runSession(deps, claimed, sessionId) {
373
405
  await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
374
406
  }
375
407
  }
408
+ function sumReportedUsage(attempts, key) {
409
+ const reported = attempts.flatMap((attempt) => {
410
+ const value = attempt[key];
411
+ return value === undefined ? [] : [value];
412
+ });
413
+ return reported.length > 0 ? reported.reduce((sum, value) => sum + value, 0) : undefined;
414
+ }
415
+ function prRequiredRejectionMessage(err) {
416
+ if (!(err instanceof api_cjs_1.NavarchApiError) || err.status !== 409)
417
+ return null;
418
+ if (typeof err.body !== "object" || err.body === null || Array.isArray(err.body))
419
+ return null;
420
+ const body = err.body;
421
+ return body.code === "pr_required" && typeof body.error === "string"
422
+ ? body.error
423
+ : null;
424
+ }
376
425
  /** Uppercases + sanitizes secret names into shell-safe env var names for injectEnv(). */
377
426
  function toEnvMap(secrets, credentialRefreshOrAuthorName, gitAuthorNameOrEmail = "sagentlab", gitAuthorEmail = "z@sagentlab.com") {
378
427
  // Keep the existing `(secrets, authorName, authorEmail)` call shape while
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Navarch machine-side session manager: registers a machine, claims tasks from the control-plane dispatcher, runs them via the Claude Code or Codex adapter, and reports results back.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",