@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 +6 -3
- package/dist/delivery.cjs +24 -4
- package/dist/exit-conditions.cjs +4 -1
- package/dist/prompt.cjs +1 -1
- package/dist/session.cjs +45 -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/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
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"and
|
|
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
|
-
"
|
|
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
|
}
|
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/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
|
-
|
|
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
|
-
|
|
708
|
-
|
|
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
|
|
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:
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
failure_summary:
|
|
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