@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 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: verificationFailureReport(task.task_type, failureSummary, "crashed"),
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: verificationFailureReport(task.task_type, failureSummary, "crashed"),
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: verificationFailureReport(task.task_type, "Docker sandbox unavailable on this machine.", "crashed"),
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: mapping.leaseOutcome === "failed"
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: verificationFailureReport(task.task_type, remediable
722
+ report: remediable
702
723
  ? redactedRejection
703
- : `${redactedRejection}\n\n---\n\n${completion.report}`, "failed"),
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: verificationFailureReport(task.task_type, lastCompletion
756
+ report: lastCompletion
736
757
  ? `${lastCompletion.report}\n\n---\n\n${failureSummary}`
737
- : failureSummary, "crashed"),
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 verdict-labelled by verificationFailureReport before posting.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sagentlab/navarch-runtime",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
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",