@sagentlab/navarch-runtime 0.1.63 → 0.1.65
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 +5 -0
- package/dist/api.cjs +4 -0
- package/dist/session.cjs +33 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,11 @@ it. Plain Node/TypeScript, no Next.js coupling, and no shared app-code coupling
|
|
|
6
6
|
— this directory is a self-contained package you can `npx` on any fresh
|
|
7
7
|
machine.
|
|
8
8
|
|
|
9
|
+
During a connected run, the runtime reports preparation, agent start, output
|
|
10
|
+
activity, and completion checks to the control plane. Project members can see
|
|
11
|
+
these phase updates in the task view. The activity signal contains no prompt
|
|
12
|
+
or CLI output; the redacted transcript is uploaded when the run finishes.
|
|
13
|
+
|
|
9
14
|
The machine operator selects **Claude Code**, **OpenAI Codex**, **Google
|
|
10
15
|
Gemini CLI**, **OpenCode**, or an **Agent Client Protocol (ACP)** agent when connecting a worker. Any selected agent can
|
|
11
16
|
run any unpinned project task; task eligibility depends on capabilities and
|
package/dist/api.cjs
CHANGED
|
@@ -141,6 +141,10 @@ class NavarchApiClient {
|
|
|
141
141
|
throw new Error("heartbeatLease: empty response from control plane.");
|
|
142
142
|
return result;
|
|
143
143
|
}
|
|
144
|
+
/** Best-effort, lease-scoped activity signal for connected machines. */
|
|
145
|
+
async reportActivity(leaseId, phase) {
|
|
146
|
+
await this.request("POST", `/api/dispatch/${encodeURIComponent(leaseId)}/activity`, { phase }, { allowEmpty: true });
|
|
147
|
+
}
|
|
144
148
|
/** schema-design.md §7 — `POST /api/dispatch/:leaseId/complete {status, report, evidence_urls, cost}`. */
|
|
145
149
|
async completeLease(leaseId, req) {
|
|
146
150
|
await this.request("POST", `/api/dispatch/${encodeURIComponent(leaseId)}/complete`, req, {
|
package/dist/session.cjs
CHANGED
|
@@ -94,7 +94,7 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
94
94
|
await api
|
|
95
95
|
.completeLease(leaseId, {
|
|
96
96
|
status: "failed",
|
|
97
|
-
report:
|
|
97
|
+
report: failureSummary,
|
|
98
98
|
failure_summary: failureSummary,
|
|
99
99
|
failed_before_start: true,
|
|
100
100
|
evidence_urls: [],
|
|
@@ -174,7 +174,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
174
174
|
const failureSummary = `Project ${task.project_id} has no GitHub repository URL. Set it in Project settings before dispatching work.`;
|
|
175
175
|
await api.completeLease(leaseId, {
|
|
176
176
|
status: "failed",
|
|
177
|
-
report:
|
|
177
|
+
report: failureSummary,
|
|
178
178
|
failure_summary: failureSummary,
|
|
179
179
|
failed_before_start: true,
|
|
180
180
|
evidence_urls: [],
|
|
@@ -296,7 +296,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
296
296
|
await api
|
|
297
297
|
.completeLease(leaseId, {
|
|
298
298
|
status: "failed",
|
|
299
|
-
report:
|
|
299
|
+
report: "Docker sandbox unavailable on this machine.",
|
|
300
300
|
failure_summary: "Docker sandbox unavailable on this machine.",
|
|
301
301
|
failed_before_start: true,
|
|
302
302
|
evidence_urls: [],
|
|
@@ -395,6 +395,23 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
395
395
|
// session actually produced instead of zeroing it.
|
|
396
396
|
let lastCompletion = null;
|
|
397
397
|
let adapterStarted = false;
|
|
398
|
+
// Activity is metadata only: no prompt, model output, or secret-bearing CLI
|
|
399
|
+
// text crosses this path. Failures never affect lease ownership or delivery.
|
|
400
|
+
let activityPending = Promise.resolve();
|
|
401
|
+
let outputObservedAt = 0;
|
|
402
|
+
let outputReportedAt = 0;
|
|
403
|
+
let adapterRunning = false;
|
|
404
|
+
const reportActivity = (phase) => {
|
|
405
|
+
activityPending = activityPending.then(() => api.reportActivity(leaseId, phase)).catch((error) => {
|
|
406
|
+
log.warn(`activity update failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(String(error), registry.list())}`);
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
const activityTimer = setInterval(() => {
|
|
410
|
+
if (adapterRunning && outputObservedAt > outputReportedAt && Date.now() - outputReportedAt >= 60_000) {
|
|
411
|
+
outputReportedAt = Date.now();
|
|
412
|
+
reportActivity("output");
|
|
413
|
+
}
|
|
414
|
+
}, 15_000);
|
|
398
415
|
const heartbeatGuard = new lease_heartbeat_cjs_1.LeaseHeartbeatGuard(claimed.expires_at, () => {
|
|
399
416
|
log.warn(`confirmed lease expired for ${leaseId} without renewal — killing session.`);
|
|
400
417
|
leaseLost = true;
|
|
@@ -407,6 +424,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
407
424
|
abortForRuntimeShutdown();
|
|
408
425
|
lifecycle.enteredMainSessionTry = true;
|
|
409
426
|
try {
|
|
427
|
+
reportActivity("preparing");
|
|
410
428
|
await gitWorktree.prepare();
|
|
411
429
|
if (gitWorktree.hostedCheckpointRestoreFailure) {
|
|
412
430
|
log.warn(`hosted checkpoint for ${leaseId} could not be restored: ` +
|
|
@@ -465,6 +483,8 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
465
483
|
nextPrompt = null;
|
|
466
484
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), runPrompt, "utf8");
|
|
467
485
|
adapterStarted = true;
|
|
486
|
+
adapterRunning = true;
|
|
487
|
+
reportActivity("running");
|
|
468
488
|
const remainingRuntimeMs = maxRuntimeMs > 0
|
|
469
489
|
? maxRuntimeMs - (node_perf_hooks_1.performance.now() - agentBudgetStartedAt)
|
|
470
490
|
: 0;
|
|
@@ -488,8 +508,10 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
488
508
|
cwd: gitWorktree.worktreePath,
|
|
489
509
|
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
490
510
|
signal: activeAbortController.signal,
|
|
511
|
+
onActivity: () => { outputObservedAt = Date.now(); },
|
|
491
512
|
}, task.idle_timeout_ms ?? config.sessionIdleTimeoutMs ?? 45 * 60_000, runtime === "opencode" ? config.opencodeStartupTimeoutMs : 0);
|
|
492
513
|
activeAbortController = null;
|
|
514
|
+
adapterRunning = false;
|
|
493
515
|
if (turnResult.timedOut || leaseLost)
|
|
494
516
|
await checkpointForRecovery(turnResult);
|
|
495
517
|
if (turnResult.model) {
|
|
@@ -497,6 +519,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
497
519
|
executionReport = { ...executionReport, model: turnResult.model };
|
|
498
520
|
}
|
|
499
521
|
attempts.push(turnResult);
|
|
522
|
+
reportActivity("finishing");
|
|
500
523
|
const capacityLimit = (0, adapter_capacity_cjs_1.detectAdapterCapacityLimit)(turnResult);
|
|
501
524
|
if (capacityLimit) {
|
|
502
525
|
await checkpointForRecovery(turnResult);
|
|
@@ -634,9 +657,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
634
657
|
const redactedReport = (0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets);
|
|
635
658
|
const completion = {
|
|
636
659
|
status: mapping.leaseOutcome,
|
|
637
|
-
report:
|
|
638
|
-
? verificationFailureReport(task.task_type, redactedReport, mapping.exitStatus)
|
|
639
|
-
: redactedReport,
|
|
660
|
+
report: redactedReport,
|
|
640
661
|
evidence_urls: mapping.evidenceUrls,
|
|
641
662
|
...(deliveryFailureSummary
|
|
642
663
|
? { failure_summary: (0, redact_cjs_1.redactText)(deliveryFailureSummary, knownSecrets) }
|
|
@@ -698,9 +719,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
698
719
|
await api.completeLease(leaseId, {
|
|
699
720
|
...completion,
|
|
700
721
|
status: "failed",
|
|
701
|
-
report:
|
|
722
|
+
report: remediable
|
|
702
723
|
? redactedRejection
|
|
703
|
-
: `${redactedRejection}\n\n---\n\n${completion.report}`,
|
|
724
|
+
: `${redactedRejection}\n\n---\n\n${completion.report}`,
|
|
704
725
|
failure_summary: redactedRejection,
|
|
705
726
|
exit_status: "failed",
|
|
706
727
|
});
|
|
@@ -732,9 +753,9 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
732
753
|
await api
|
|
733
754
|
.completeLease(leaseId, {
|
|
734
755
|
status: "failed",
|
|
735
|
-
report:
|
|
756
|
+
report: lastCompletion
|
|
736
757
|
? `${lastCompletion.report}\n\n---\n\n${failureSummary}`
|
|
737
|
-
: failureSummary,
|
|
758
|
+
: failureSummary,
|
|
738
759
|
failure_summary: failureSummary,
|
|
739
760
|
failed_before_start: !adapterStarted,
|
|
740
761
|
evidence_urls: lastCompletion?.evidence_urls ?? [],
|
|
@@ -752,6 +773,7 @@ async function runClaimedSession(deps, claimed, sessionId, lifecycle) {
|
|
|
752
773
|
deps.shutdownSignal?.removeEventListener("abort", abortForRuntimeShutdown);
|
|
753
774
|
sessionClosing = true;
|
|
754
775
|
clearInterval(heartbeatTimer);
|
|
776
|
+
clearInterval(activityTimer);
|
|
755
777
|
heartbeatGuard.stop();
|
|
756
778
|
activeAbortController?.abort();
|
|
757
779
|
// Drain only an already-started filesystem update, never a stalled network
|
|
@@ -815,33 +837,6 @@ function sumReportedUsage(attempts, key) {
|
|
|
815
837
|
});
|
|
816
838
|
return reported.length > 0 ? reported.reduce((sum, value) => sum + value, 0) : undefined;
|
|
817
839
|
}
|
|
818
|
-
/**
|
|
819
|
-
* The control plane refuses to release a verify lease whose failed completion
|
|
820
|
-
* does not open with `Verification verdict: FAIL` or `BLOCKED`
|
|
821
|
-
* (verification_verdict_invalid), and that gate applies to runtime-authored
|
|
822
|
-
* failure reports too. When the report carries no usable verdict — the agent
|
|
823
|
-
* exited non-zero, or opened with PASS despite the failed outcome — label it
|
|
824
|
-
* BLOCKED: the failure prevented an acceptance decision. Without this, the
|
|
825
|
-
* failure completion is itself rejected, the session crashes, and the lease
|
|
826
|
-
* dangles until expiry.
|
|
827
|
-
*
|
|
828
|
-
* A `killed` or `crashed` exit is left unlabelled: the control plane accepts
|
|
829
|
-
* those verdict-less as the runtime's own failure and routes them through
|
|
830
|
-
* failure attribution (an infrastructure retry). A BLOCKED label there would
|
|
831
|
-
* misreport a transient timeout as a verifier verdict and escalate it to a
|
|
832
|
-
* human; the control plane discards such a label, so posting it only muddies
|
|
833
|
-
* the session report.
|
|
834
|
-
*/
|
|
835
|
-
function verificationFailureReport(taskType, report, exitStatus) {
|
|
836
|
-
if (taskType !== "verify")
|
|
837
|
-
return report;
|
|
838
|
-
if (exitStatus === "killed" || exitStatus === "crashed")
|
|
839
|
-
return report;
|
|
840
|
-
const verdict = (0, exit_conditions_cjs_1.leadingVerificationVerdict)(report);
|
|
841
|
-
if (verdict === "fail" || verdict === "blocked")
|
|
842
|
-
return report;
|
|
843
|
-
return `Verification verdict: BLOCKED — the session ended before verification reached an acceptance decision.\n\n${report}`;
|
|
844
|
-
}
|
|
845
840
|
/** Rejection codes another agent turn in the same worktree can plausibly fix. */
|
|
846
841
|
const REMEDIABLE_REJECTION_CODES = new Set([
|
|
847
842
|
"review_fix_required",
|
|
@@ -871,7 +866,7 @@ const REMEDIABLE_REJECTION_CODES = new Set([
|
|
|
871
866
|
// `Verification verdict:` line only has to restate its report; the
|
|
872
867
|
// rejection prose spells out the exact format. Failed outcomes never reach
|
|
873
868
|
// remediation (see the leaseOutcome === "completed" gate) — their reports
|
|
874
|
-
// are
|
|
869
|
+
// are recorded as session failures when the verifier supplied no verdict.
|
|
875
870
|
"verification_verdict_invalid",
|
|
876
871
|
]);
|
|
877
872
|
/**
|
package/package.json
CHANGED