@sagentlab/navarch-runtime 0.1.93 → 0.1.95
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 +6 -3
- package/dist/exit-conditions.cjs +4 -1
- package/dist/session.cjs +41 -20
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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.
|
|
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/exit-conditions.cjs
CHANGED
|
@@ -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
|
-
|
|
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/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
|
-
|
|
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} ` +
|
|
@@ -701,11 +712,14 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
701
712
|
});
|
|
702
713
|
continue;
|
|
703
714
|
}
|
|
715
|
+
await checkpointForRecovery(result, "incomplete delivery");
|
|
704
716
|
deliveryFailureSummary =
|
|
705
717
|
`Session ended without delivering the task: ${delivery.reason}. ` +
|
|
706
718
|
`${deliveryCorrectionRetries} correction turn(s) did not recover it. ` +
|
|
707
|
-
|
|
708
|
-
|
|
719
|
+
(result.checkpointPath
|
|
720
|
+
? `Local recovery checkpoint: ${result.checkpointPath}. `
|
|
721
|
+
: `Recovery worktree retained: ${result.retainedWorktreePath}. `) +
|
|
722
|
+
"The task needs another attempt.";
|
|
709
723
|
log.warn(`failing ${leaseId}: ${deliveryFailureSummary}`);
|
|
710
724
|
mapping.leaseOutcome = "failed";
|
|
711
725
|
mapping.exitStatus = "failed";
|
|
@@ -757,6 +771,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
757
771
|
transcript_url: completion.transcript_url,
|
|
758
772
|
};
|
|
759
773
|
try {
|
|
774
|
+
completingLease = true;
|
|
760
775
|
await (0, completion_retry_cjs_1.completeWithTransientRetry)(() => api.completeLease(leaseId, completion), {
|
|
761
776
|
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
777
|
});
|
|
@@ -778,10 +793,11 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
778
793
|
if (!rejection)
|
|
779
794
|
throw err;
|
|
780
795
|
const redactedRejection = (0, redact_cjs_1.redactText)(rejection.message, knownSecrets);
|
|
796
|
+
const rejectionDetail = describeResponseBody(`${(0, redact_cjs_1.redactText)(rejection.code, knownSecrets)}: ${redactedRejection}`);
|
|
781
797
|
const remediable = mapping.leaseOutcome === "completed" && rejection.remediable;
|
|
782
798
|
if (remediable && completionRemediationRetries < COMPLETION_REMEDIATION_RETRIES) {
|
|
783
799
|
completionRemediationRetries += 1;
|
|
784
|
-
log.warn(`completion for ${leaseId} needs more work; restarting agent turn ${completionRemediationRetries}/${COMPLETION_REMEDIATION_RETRIES} in the same worktree.`);
|
|
800
|
+
log.warn(`completion for ${leaseId} needs more work (${rejectionDetail}); restarting agent turn ${completionRemediationRetries}/${COMPLETION_REMEDIATION_RETRIES} in the same worktree.`);
|
|
785
801
|
// A new turn is a fresh agent invocation: the rejection alone would
|
|
786
802
|
// reach an agent that has never seen the task.
|
|
787
803
|
nextPrompt = (0, prompt_cjs_1.renderCompletionRemediationPrompt)({
|
|
@@ -794,22 +810,24 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
794
810
|
continue;
|
|
795
811
|
}
|
|
796
812
|
if (remediable) {
|
|
797
|
-
log.warn(`completion for ${leaseId} is still not ready after ${COMPLETION_REMEDIATION_RETRIES} retries; failing with the control-plane rejection.`);
|
|
813
|
+
log.warn(`completion for ${leaseId} is still not ready after ${COMPLETION_REMEDIATION_RETRIES} retries (${rejectionDetail}); failing with the control-plane rejection.`);
|
|
798
814
|
}
|
|
799
815
|
else {
|
|
800
816
|
// Not something another turn can fix (e.g. review_evidence_tampered).
|
|
801
817
|
// Fail the lease once, keeping this attempt's real report, cost and
|
|
802
818
|
// evidence — this is a rejected attempt, not a crashed session.
|
|
803
|
-
log.error(`completion for ${leaseId} was rejected
|
|
819
|
+
log.error(`completion for ${leaseId} was rejected (${rejectionDetail}), which is not remediable; failing the lease.`);
|
|
804
820
|
}
|
|
805
821
|
try {
|
|
806
822
|
await (0, completion_retry_cjs_1.completeWithTransientRetry)(() => api.completeLease(leaseId, {
|
|
807
823
|
...completion,
|
|
808
824
|
status: "failed",
|
|
809
|
-
report:
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
failure_summary:
|
|
825
|
+
report: `${redactedRejection}\n\n---\n\n${completion.report}`,
|
|
826
|
+
// Keep the agent's blocker first: the API bounds failure_summary,
|
|
827
|
+
// and retry attribution must not lose it behind a delivery error.
|
|
828
|
+
failure_summary: remediable
|
|
829
|
+
? `${completion.report.slice(0, 1000)}\n\nControl-plane rejection:\n${redactedRejection.slice(0, 800)}`
|
|
830
|
+
: redactedRejection,
|
|
813
831
|
exit_status: "failed",
|
|
814
832
|
}));
|
|
815
833
|
}
|
|
@@ -824,6 +842,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
824
842
|
}
|
|
825
843
|
break;
|
|
826
844
|
}
|
|
845
|
+
finally {
|
|
846
|
+
completingLease = false;
|
|
847
|
+
}
|
|
827
848
|
}
|
|
828
849
|
}
|
|
829
850
|
catch (err) {
|
package/package.json
CHANGED