@sagentlab/navarch-runtime 0.1.93 → 0.1.96

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/README.md CHANGED
@@ -330,13 +330,14 @@ own budgets. An absolute limit covers all guidance/remediation turns within the
330
330
  claim, rather than resetting with each turn. These are runtime limits, not
331
331
  provider spending caps.
332
332
 
333
- After a watchdog stop or lease loss during a turn, before worktree cleanup, the runtime attempts a private
333
+ After a watchdog stop, lease loss during a turn, or exhausted delivery correction
334
+ turns, before worktree cleanup, the runtime attempts a private
334
335
  local recovery checkpoint at
335
336
  `<workspaceRoot>/checkpoints/<project>/<task>/<session>/`. It contains a Git
336
337
  bundle of committed history, a binary patch for dirty tracked files, untracked
337
338
  non-ignored files, and recovery instructions. The bundle excludes repository
338
339
  configuration and broker credentials. Checkpoints are not pushed or uploaded;
339
- timeout reports and runtime logs name their location. They survive session cleanup and need
340
+ failure reports and runtime logs name their location. They survive session cleanup and need
340
341
  manual recovery and removal on that machine. Ignored files, agent conversation
341
342
  state, and checkpoints across machine/disk loss are not provided.
342
343
 
@@ -504,7 +505,9 @@ as an explicit read-only mount.
504
505
  - **Codex:** the runtime passes a one-off native permission profile with
505
506
  `approval_policy="on-request"` and `approvals_reviewer="auto_review"`.
506
507
  Codex's OS sandbox grants read/write access only to the allowed roots and
507
- denies the surrounding multi-session workspace. Its network policy allows
508
+ denies the surrounding multi-session workspace. After preparing the worktree,
509
+ the runtime explicitly grants its resolved linked Git metadata directory write
510
+ access so staging and committing work inside the sandbox. Its network policy allows
508
511
  GitHub and GitHub-hosted content so authenticated `gh` and git operations
509
512
  required by the task stay inside the sandbox. Other destinations remain
510
513
  blocked and eligible escalations are decided by the automatic reviewer
package/dist/delivery.cjs CHANGED
@@ -146,6 +146,10 @@ function describeBranch(branch) {
146
146
  * task.
147
147
  */
148
148
  function renderDeliveryCorrectionPrompt(args) {
149
+ const report = args.previousReport.trim();
150
+ const clippedReport = report.length > 8_000
151
+ ? `${report.slice(0, 8_000)}\n\n[... truncated]`
152
+ : report;
149
153
  return [
150
154
  args.originalPrompt.trim(),
151
155
  "",
@@ -157,10 +161,26 @@ function renderDeliveryCorrectionPrompt(args) {
157
161
  "so your earlier edits, commits, and branch are all still here — start with `git status` and",
158
162
  "`git log --oneline` and continue from there rather than redoing the work.",
159
163
  "",
160
- "Finish the delivery chain: commit the work, push the branch, and open the pull request.",
161
- "If part of the task genuinely cannot be completed, still deliver what does work as a pull request",
162
- "and state plainly in the description what is missing and why.",
164
+ "Inspect tracked and untracked changes before deciding what is undelivered work. Preserve required",
165
+ "evidence in task artifacts, remove only temporary files you created (such as `.review-*/` scratch",
166
+ "directories), and restore only tracked edits you made solely for local checks. Do not delete",
167
+ "unfamiliar or pre-existing changes, hide leftovers with ignore rules, or commit scratch files.",
168
+ ...(args.taskType === "review"
169
+ ? [
170
+ "This is a read-only review task. The generic delivery failure does not authorize commits,",
171
+ "pushes, rebases, merges, or a new PR. A dirty worktree may contain only review scratch files.",
172
+ "Check whether the native review already exists for this task and reviewed head. If it does,",
173
+ "reuse that review and its matching attestation; do not file a duplicate or implement findings.",
174
+ "If it does not, finish filing the review required by the original task. Leave a clean worktree.",
175
+ ]
176
+ : [
177
+ "Follow the original task's delivery contract. If it requires a PR, commit the intended work,",
178
+ "push the branch, and open or update the PR. A report-only task does not need a new PR.",
179
+ "If part of a PR task cannot be completed, deliver what does work and describe the gap.",
180
+ ]),
163
181
  "",
164
- "This session's worktree is deleted when the session ends. Work that is not pushed is lost.",
182
+ "Your final report replaces the previous one. Restate it in full in the required format, preserving",
183
+ "the verdict, reviewed head, findings, and validation limitations. Do not claim delivery you cannot verify.",
184
+ ...(clippedReport ? ["", "### Previous turn's final report", "", clippedReport] : []),
165
185
  ].join("\n");
166
186
  }
@@ -359,7 +359,10 @@ function mapExitCondition(result) {
359
359
  // including when that response says the task could not start. Treat an
360
360
  // explicit leading blocked verdict as a failed lease so the dispatcher can
361
361
  // retry/escalate it instead of falsely moving unfinished work to Done.
362
- if (/^blocked(?:\s|:|$)/i.test(reportText.trim())) {
362
+ // Build agents also use an explicit outcome marker. A zero process exit
363
+ // means the conversation finished, not that the requested build succeeded.
364
+ if (/^blocked(?:\s|:|$)/i.test(reportText.trim()) ||
365
+ /^Build outcome:\s*(?:FAILED|BLOCKED)\b/i.test(reportText.trim())) {
363
366
  return {
364
367
  leaseOutcome: "failed",
365
368
  exitStatus: "failed",
package/dist/prompt.cjs CHANGED
@@ -5,7 +5,7 @@ exports.renderGuidanceCorrectionPrompt = renderGuidanceCorrectionPrompt;
5
5
  exports.renderCompletionRemediationPrompt = renderCompletionRemediationPrompt;
6
6
  exports.renderHostedCheckpointNote = renderHostedCheckpointNote;
7
7
  const CODE_GRAPH_GUIDANCE = "For JS/TS dependency inspection, the runtime provides `navarch-runtime code-graph --query <symbol-or-file> --mode search|callers|callees|impact --commit HEAD`. If the runtime is not on PATH, use its installed entry point; do not install another copy mid-task. Results describe committed source and include commit/freshness warnings; inspect current files for uncommitted changes and unresolved dependencies.";
8
- const PROCESS_GUIDANCE = "Process management: this host may run other agent sessions. Keep the PID or tool handle of each preview server you start and stop only that owned process. Never use broad process-name or command-line matching (pkill, killall) to clean up servers: the agent launcher carries the task prompt in its arguments and can match those patterns, terminating your own session. Use a different port when a port is occupied by a process you do not own. Keep scratch files in the current worktree; paths in previous transcripts belong to earlier sessions.";
8
+ const PROCESS_GUIDANCE = "Process management: this host may run other agent sessions. Keep the PID or tool handle of each preview server you start and stop only that owned process. Never use broad process-name or command-line matching (pkill, killall) to clean up servers: the agent launcher carries the task prompt in its arguments and can match those patterns, terminating your own session. Use a different port when a port is occupied by a process you do not own. Keep scratch files in the current worktree; paths in previous transcripts belong to earlier sessions. Before your final report, inspect `git status --short`, including untracked files. Preserve required evidence in task artifacts, then remove only temporary files you created and restore only tracked edits you made solely for local checks. Do not delete unfamiliar or pre-existing changes, hide leftovers with ignore rules, or commit scratch files merely to make the worktree clean. Review tasks deliver a native review and attestation, never commits, pushes, or a new PR.";
9
9
  /** Overrides the playbooks' "default branch" PR base when the project sets a working branch. */
10
10
  function workingBranchNote(workingBranch) {
11
11
  if (!workingBranch)
package/dist/session.cjs CHANGED
@@ -232,13 +232,14 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
232
232
  let retainWorktree = false;
233
233
  let heartbeatInFlight = null;
234
234
  let heartbeatWorkspaceUpdate = null;
235
+ let completingLease = false;
235
236
  const sessionOutcome = {};
236
- const checkpointForRecovery = async (result) => {
237
+ const checkpointForRecovery = async (result, reason) => {
237
238
  if (result.checkpointPath || result.retainedWorktreePath)
238
239
  return;
239
240
  try {
240
241
  result.checkpointPath = await gitWorktree.saveCheckpoint();
241
- log.warn(`session ${leaseId} stopped for ${leaseLost ? "lease loss" : result.capacityError ? "provider capacity" : result.timeoutReason ?? "duration"}; recovery checkpoint: ${result.checkpointPath}`);
242
+ log.warn(`session ${leaseId} stopped for ${reason ?? (leaseLost ? "lease loss" : result.capacityError ? "provider capacity" : result.timeoutReason ?? "duration")}; recovery checkpoint: ${result.checkpointPath}`);
242
243
  }
243
244
  catch (error) {
244
245
  retainWorktree = true;
@@ -285,7 +286,15 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
285
286
  log.warn(`lease heartbeat failed for ${leaseId}: ${String(err)} — retrying on the next heartbeat while the confirmed lease remains valid.`);
286
287
  return;
287
288
  }
288
- log.warn(`lease heartbeat failed for ${leaseId}: ${String(err)} — killing session.`);
289
+ if (completingLease && isTerminalLeaseHeartbeatError(err)) {
290
+ // Completion can release the lease before its HTTP response arrives.
291
+ // Keep awaiting/re-sending that report to resolve the ambiguous write;
292
+ // there is no agent turn to kill during this phase.
293
+ log.info(`lease ${leaseId} is no longer active while completion is pending; awaiting completion acknowledgement.`);
294
+ }
295
+ else {
296
+ log.warn(`lease heartbeat failed for ${leaseId}: ${String(err)} — killing session.`);
297
+ }
289
298
  leaseLost = true;
290
299
  leaseGone = isTerminalLeaseHeartbeatError(err);
291
300
  heartbeatGuard.stop();
@@ -385,14 +394,6 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
385
394
  await node_fs_1.promises.mkdir(tmpDir, { recursive: true, mode: 0o700 });
386
395
  Object.assign(sessionEnv, { TMPDIR: tmpDir, TMP: tmpDir, TEMP: tmpDir });
387
396
  }
388
- codexGuardArgs = (0, worktree_guard_cjs_1.codexWorktreeGuardArgs)({
389
- workDir,
390
- worktreePath: gitWorktree.worktreePath,
391
- repositoryPath: gitWorktree.repositoryPath,
392
- workspaceRoot: config.workspaceRoot,
393
- extraRoots: config.guardExtraRoots,
394
- codexBin: config.codexBin,
395
- });
396
397
  }
397
398
  else if (runtime === "gemini") {
398
399
  geminiGuardMounts = (0, worktree_guard_cjs_1.geminiSandboxMounts)({
@@ -465,6 +466,16 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
465
466
  log.warn(`hosted checkpoint for ${leaseId} could not be restored: ` +
466
467
  `${(0, redact_cjs_1.redactText)(gitWorktree.hostedCheckpointRestoreFailure, registry.list())}; starting without it.`);
467
468
  }
469
+ if (config.sandboxMode === "host" && config.worktreeGuard && runtime === "codex") {
470
+ codexGuardArgs = (0, worktree_guard_cjs_1.codexWorktreeGuardArgs)({
471
+ workDir,
472
+ worktreePath: gitWorktree.worktreePath,
473
+ repositoryPath: gitWorktree.repositoryPath,
474
+ workspaceRoot: config.workspaceRoot,
475
+ extraRoots: config.guardExtraRoots,
476
+ codexBin: config.codexBin,
477
+ });
478
+ }
468
479
  const restoredCheckpoint = gitWorktree.restoredHostedCheckpoint;
469
480
  if (restoredCheckpoint) {
470
481
  log.info(`resumed hosted checkpoint ${restoredCheckpoint.sha.slice(0, 12)} for ${leaseId} ` +
@@ -695,17 +706,24 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
695
706
  `restarting agent turn ${deliveryCorrectionRetries}/${DELIVERY_CORRECTION_RETRIES} in the same worktree.`);
696
707
  nextPrompt = (0, delivery_cjs_1.renderDeliveryCorrectionPrompt)({
697
708
  originalPrompt: runPrompt,
709
+ taskType: task.task_type,
710
+ // The completion summary is capped at 500 characters. Preserve
711
+ // the final prose from adapters that only return stdout as well.
712
+ previousReport: (0, redact_cjs_1.redactText)(result.reportText ?? (0, exit_conditions_cjs_1.parseClaudeJsonResult)(result.stdout)?.result ?? result.stdout, registry.list()),
698
713
  reason: delivery.reason,
699
714
  attempt: deliveryCorrectionRetries,
700
715
  maxAttempts: DELIVERY_CORRECTION_RETRIES,
701
716
  });
702
717
  continue;
703
718
  }
719
+ await checkpointForRecovery(result, "incomplete delivery");
704
720
  deliveryFailureSummary =
705
721
  `Session ended without delivering the task: ${delivery.reason}. ` +
706
722
  `${deliveryCorrectionRetries} correction turn(s) did not recover it. ` +
707
- "The session worktree is deleted at teardown. Pushed commits remain on origin; " +
708
- "uncommitted or unpushed work may be lost. The task needs another attempt.";
723
+ (result.checkpointPath
724
+ ? `Local recovery checkpoint: ${result.checkpointPath}. `
725
+ : `Recovery worktree retained: ${result.retainedWorktreePath}. `) +
726
+ "The task needs another attempt.";
709
727
  log.warn(`failing ${leaseId}: ${deliveryFailureSummary}`);
710
728
  mapping.leaseOutcome = "failed";
711
729
  mapping.exitStatus = "failed";
@@ -757,6 +775,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
757
775
  transcript_url: completion.transcript_url,
758
776
  };
759
777
  try {
778
+ completingLease = true;
760
779
  await (0, completion_retry_cjs_1.completeWithTransientRetry)(() => api.completeLease(leaseId, completion), {
761
780
  onRetry: (attempt, delayMs, err) => log.warn(`completion for ${leaseId} did not reach the control plane (${(0, redact_cjs_1.redactText)(String(err), knownSecrets)}); re-sending in ${delayMs / 1000}s (${attempt}).`),
762
781
  });
@@ -778,10 +797,11 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
778
797
  if (!rejection)
779
798
  throw err;
780
799
  const redactedRejection = (0, redact_cjs_1.redactText)(rejection.message, knownSecrets);
800
+ const rejectionDetail = describeResponseBody(`${(0, redact_cjs_1.redactText)(rejection.code, knownSecrets)}: ${redactedRejection}`);
781
801
  const remediable = mapping.leaseOutcome === "completed" && rejection.remediable;
782
802
  if (remediable && completionRemediationRetries < COMPLETION_REMEDIATION_RETRIES) {
783
803
  completionRemediationRetries += 1;
784
- log.warn(`completion for ${leaseId} needs more work; restarting agent turn ${completionRemediationRetries}/${COMPLETION_REMEDIATION_RETRIES} in the same worktree.`);
804
+ log.warn(`completion for ${leaseId} needs more work (${rejectionDetail}); restarting agent turn ${completionRemediationRetries}/${COMPLETION_REMEDIATION_RETRIES} in the same worktree.`);
785
805
  // A new turn is a fresh agent invocation: the rejection alone would
786
806
  // reach an agent that has never seen the task.
787
807
  nextPrompt = (0, prompt_cjs_1.renderCompletionRemediationPrompt)({
@@ -794,22 +814,24 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
794
814
  continue;
795
815
  }
796
816
  if (remediable) {
797
- log.warn(`completion for ${leaseId} is still not ready after ${COMPLETION_REMEDIATION_RETRIES} retries; failing with the control-plane rejection.`);
817
+ log.warn(`completion for ${leaseId} is still not ready after ${COMPLETION_REMEDIATION_RETRIES} retries (${rejectionDetail}); failing with the control-plane rejection.`);
798
818
  }
799
819
  else {
800
820
  // Not something another turn can fix (e.g. review_evidence_tampered).
801
821
  // Fail the lease once, keeping this attempt's real report, cost and
802
822
  // evidence — this is a rejected attempt, not a crashed session.
803
- log.error(`completion for ${leaseId} was rejected as ${rejection.code}, which is not remediable; failing the lease.`);
823
+ log.error(`completion for ${leaseId} was rejected (${rejectionDetail}), which is not remediable; failing the lease.`);
804
824
  }
805
825
  try {
806
826
  await (0, completion_retry_cjs_1.completeWithTransientRetry)(() => api.completeLease(leaseId, {
807
827
  ...completion,
808
828
  status: "failed",
809
- report: remediable
810
- ? redactedRejection
811
- : `${redactedRejection}\n\n---\n\n${completion.report}`,
812
- failure_summary: redactedRejection,
829
+ report: `${redactedRejection}\n\n---\n\n${completion.report}`,
830
+ // Keep the agent's blocker first: the API bounds failure_summary,
831
+ // and retry attribution must not lose it behind a delivery error.
832
+ failure_summary: remediable
833
+ ? `${completion.report.slice(0, 1000)}\n\nControl-plane rejection:\n${redactedRejection.slice(0, 800)}`
834
+ : redactedRejection,
813
835
  exit_status: "failed",
814
836
  }));
815
837
  }
@@ -824,6 +846,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
824
846
  }
825
847
  break;
826
848
  }
849
+ finally {
850
+ completingLease = false;
851
+ }
827
852
  }
828
853
  }
829
854
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.93",
3
+ "version": "0.1.96",
4
4
  "description": "Navarch machine-side session manager: claims delivery tasks and runs them through local coding agents.",
5
5
  "type": "commonjs",
6
6
  "license": "MIT",