@kynver-app/runtime 0.1.13 → 0.1.16

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/dist/index.js CHANGED
@@ -208,6 +208,18 @@ async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
208
208
  "requires --secret, KYNVER_RUNNER_TOKEN, a scoped runner token (`kynver runner credential`), ~/.kynver/credentials runnerToken, KYNVER_API_KEY with an API base URL to mint one, or (legacy) KYNVER_RUNTIME_SECRET / OPENCLAW_CRON_SECRET"
209
209
  );
210
210
  }
211
+ async function refreshRunnerToken(agentOsId, opts) {
212
+ const apiKey = loadApiKey();
213
+ const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
214
+ if (!apiKey || !agentOsId || !baseUrl) return null;
215
+ try {
216
+ const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
217
+ saveRunnerToken(agentOsId, token);
218
+ return token;
219
+ } catch {
220
+ return null;
221
+ }
222
+ }
211
223
  async function fetchRunnerCredential(agentOsId, opts) {
212
224
  const apiKey = opts?.apiKey || loadApiKey();
213
225
  if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
@@ -1483,6 +1495,34 @@ async function sweepRun(args) {
1483
1495
 
1484
1496
  // src/worker-ops.ts
1485
1497
  import path11 from "node:path";
1498
+ async function postCompletion(url, secret, body) {
1499
+ const res = await fetch(url, {
1500
+ method: "POST",
1501
+ headers: buildHarnessCallbackHeaders(secret),
1502
+ body: JSON.stringify(body)
1503
+ });
1504
+ let parsed = null;
1505
+ try {
1506
+ parsed = await res.json();
1507
+ } catch {
1508
+ parsed = null;
1509
+ }
1510
+ return { ok: res.ok, status: res.status, parsed };
1511
+ }
1512
+ function completionErrorText(parsed) {
1513
+ if (parsed && typeof parsed === "object") {
1514
+ const err = parsed.error;
1515
+ if (typeof err === "string" && err.trim()) return err.trim();
1516
+ }
1517
+ return void 0;
1518
+ }
1519
+ function persistCompletionBlocker(worker, reason) {
1520
+ const current = worker.completionBlocker;
1521
+ if ((current ?? void 0) === (reason ?? void 0)) return;
1522
+ if (reason) worker.completionBlocker = reason;
1523
+ else delete worker.completionBlocker;
1524
+ saveWorker(worker.runId, worker);
1525
+ }
1486
1526
  async function tryCompleteWorker(args) {
1487
1527
  const worker = loadWorker(String(args.run), String(args.name));
1488
1528
  const status = computeWorkerStatus(worker);
@@ -1495,7 +1535,8 @@ async function tryCompleteWorker(args) {
1495
1535
  return { ok: true, skipped: true, reason: "worker-not-finished" };
1496
1536
  }
1497
1537
  const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
1498
- const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
1538
+ const explicitSecret = args.secret ? String(args.secret) : void 0;
1539
+ let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
1499
1540
  const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
1500
1541
  const body = {
1501
1542
  source: "openclaw-harness",
@@ -1507,18 +1548,23 @@ async function tryCompleteWorker(args) {
1507
1548
  finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
1508
1549
  status
1509
1550
  };
1510
- const res = await fetch(url, {
1511
- method: "POST",
1512
- headers: buildHarnessCallbackHeaders(secret),
1513
- body: JSON.stringify(body)
1514
- });
1515
- let parsed = null;
1516
- try {
1517
- parsed = await res.json();
1518
- } catch {
1519
- parsed = null;
1551
+ let result = await postCompletion(url, secret, body);
1552
+ if ((result.status === 401 || result.status === 403) && !explicitSecret) {
1553
+ const refreshed = await refreshRunnerToken(agentOsId, { baseUrl: base });
1554
+ if (refreshed && refreshed !== secret) {
1555
+ secret = refreshed;
1556
+ result = await postCompletion(url, secret, body);
1557
+ }
1520
1558
  }
1521
- return { ok: res.ok, httpStatus: res.status, response: parsed };
1559
+ if (result.ok) {
1560
+ persistCompletionBlocker(worker, void 0);
1561
+ return { ok: true, httpStatus: result.status, response: result.parsed };
1562
+ }
1563
+ const authRejected = result.status === 401 || result.status === 403;
1564
+ const detail = completionErrorText(result.parsed) ?? (authRejected ? "runner token unauthorized" : "non-2xx response");
1565
+ const reason = authRejected ? `completion replay rejected (${result.status}): ${detail}` : `completion replay failed (${result.status}): ${detail}`;
1566
+ persistCompletionBlocker(worker, reason);
1567
+ return { ok: false, httpStatus: result.status, response: result.parsed, completionBlocked: true };
1522
1568
  }
1523
1569
  async function completeWorker(args) {
1524
1570
  try {
@@ -1586,11 +1632,13 @@ function runStatus(args) {
1586
1632
  return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
1587
1633
  }
1588
1634
  const status = computeWorkerStatus(worker, { base: run.base });
1635
+ const rawBlocker = worker.completionBlocker;
1636
+ const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
1589
1637
  return {
1590
1638
  worker: status.worker,
1591
- status: status.status,
1592
- attention: status.attention.state,
1593
- attentionReason: status.attention.reason,
1639
+ status: completionBlocker ? "blocked" : status.status,
1640
+ attention: completionBlocker ? "blocked" : status.attention.state,
1641
+ attentionReason: completionBlocker ?? status.attention.reason,
1594
1642
  pid: status.pid,
1595
1643
  alive: status.alive,
1596
1644
  currentTool: status.currentTool,
@@ -1660,6 +1708,7 @@ function terminalStatusFor(run) {
1660
1708
  if (names.length === 0) return "failed";
1661
1709
  let anyAlive = false;
1662
1710
  let anyResult = false;
1711
+ let anyCompletionBlocked = false;
1663
1712
  for (const name of names) {
1664
1713
  const worker = readJson(
1665
1714
  path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
@@ -1671,9 +1720,13 @@ function terminalStatusFor(run) {
1671
1720
  anyAlive = true;
1672
1721
  break;
1673
1722
  }
1723
+ if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
1724
+ anyCompletionBlocked = true;
1725
+ }
1674
1726
  if (status.finalResult) anyResult = true;
1675
1727
  }
1676
1728
  if (anyAlive) return null;
1729
+ if (anyCompletionBlocked) return null;
1677
1730
  return anyResult ? "completed" : "failed";
1678
1731
  }
1679
1732
  function finalizeStaleRuns() {
@@ -1772,9 +1825,10 @@ async function completeFinishedWorkers(runId, args) {
1772
1825
  path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1773
1826
  void 0
1774
1827
  );
1775
- if (!worker?.dispatched || !worker.taskId) continue;
1828
+ if (!worker?.taskId) continue;
1776
1829
  const status = computeWorkerStatus(worker);
1777
1830
  if (!isFinishedWorkerStatus(status)) continue;
1831
+ if (!worker.dispatched && !status.finalResult) continue;
1778
1832
  const result = await tryCompleteWorker({
1779
1833
  run: runId,
1780
1834
  name,
@@ -1802,8 +1856,8 @@ async function runPipelineTick(args) {
1802
1856
  const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
1803
1857
  const execute = args.execute !== false && args.execute !== "false";
1804
1858
  runStatus({ run: runId });
1805
- const finalizedStaleRuns = finalizeStaleRuns();
1806
1859
  const completedWorkers = await completeFinishedWorkers(runId, args);
1860
+ const finalizedStaleRuns = finalizeStaleRuns();
1807
1861
  const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
1808
1862
  const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
1809
1863
  const resourceGate = observeRunnerResourceGate({