@rosthq/cli 0.7.101 → 0.7.102

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
@@ -57536,15 +57536,17 @@ ${usage()}
57536
57536
  let heartbeatSucceeded = false;
57537
57537
  let failed = false;
57538
57538
  let activeSessions = 0;
57539
+ const activeTurnExecutionIds = /* @__PURE__ */ new Set();
57539
57540
  do {
57540
57541
  const detectedCapabilities = await detectCapabilities(cliVersion, env, homeDir);
57541
57542
  const capabilities = capabilitiesForConfiguredRuntime(detectedCapabilities, config2.runtime);
57542
57543
  const localRuntime = config2.execute ? effectiveExecutionRuntime(config2.runtime, capabilities) : null;
57543
57544
  try {
57544
- const heartbeat = await post2(fetchImpl, options.appUrl, "/api/runner/heartbeat", {
57545
+ const heartbeat = await post2(fetchImpl, options.appUrl, "/api/runner/heartbeat", buildHeartbeatBody({
57545
57546
  capabilities,
57546
- telemetry: detectTelemetry(capabilities, env, activeSessions, cliVersion)
57547
- }, state.runner_secret);
57547
+ telemetry: detectTelemetry(capabilities, env, activeSessions, cliVersion),
57548
+ activeTurnExecutionIds: [...activeTurnExecutionIds]
57549
+ }), state.runner_secret);
57548
57550
  if (heartbeat.status !== 200) {
57549
57551
  options.io.stderr.write(`heartbeat ${heartbeat.status}: ${redactForLog(JSON.stringify(heartbeat.json))}
57550
57552
  `);
@@ -57563,10 +57565,11 @@ ${usage()}
57563
57565
  intervalMs: config2.heartbeatMs,
57564
57566
  capabilities,
57565
57567
  io: options.io,
57566
- telemetry: () => detectTelemetry(capabilities, env, activeSessions, cliVersion)
57568
+ telemetry: () => detectTelemetry(capabilities, env, activeSessions, cliVersion),
57569
+ activeTurnIds: () => [...activeTurnExecutionIds]
57567
57570
  });
57568
57571
  try {
57569
- await claimAndExecute(fetchImpl, options.appUrl, state, config2, options.io, localRuntime);
57572
+ await claimAndExecute(fetchImpl, options.appUrl, state, config2, options.io, localRuntime, activeTurnExecutionIds);
57570
57573
  } finally {
57571
57574
  stopTurnHeartbeat();
57572
57575
  activeSessions = Math.max(0, activeSessions - 1);
@@ -57579,27 +57582,61 @@ ${usage()}
57579
57582
  }
57580
57583
  beat += 1;
57581
57584
  if (!config2.once) {
57582
- await waitForNextHeartbeat(fetchImpl, options.appUrl, state, config2, options.io, localRuntime);
57585
+ await waitForNextHeartbeat(
57586
+ fetchImpl,
57587
+ options.appUrl,
57588
+ state,
57589
+ config2,
57590
+ options.io,
57591
+ localRuntime,
57592
+ capabilities,
57593
+ () => detectTelemetry(capabilities, env, activeSessions, cliVersion),
57594
+ activeTurnExecutionIds
57595
+ );
57583
57596
  }
57584
57597
  } while (!config2.once);
57585
57598
  return heartbeatSucceeded && !failed ? 0 : 1;
57586
57599
  }
57587
- async function waitForNextHeartbeat(fetchImpl, appUrl2, state, config2, io, runtime) {
57600
+ async function waitForNextHeartbeat(fetchImpl, appUrl2, state, config2, io, runtime, capabilities, telemetry, activeTurnExecutionIds) {
57588
57601
  const deadline = Date.now() + config2.heartbeatMs;
57589
- while (Date.now() < deadline) {
57590
- const intervalMs = Math.min(config2.interactiveClaimMs, deadline - Date.now());
57591
- await sleep3(intervalMs);
57592
- if (!runtime || Date.now() >= deadline) {
57593
- continue;
57594
- }
57595
- try {
57596
- await claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime, { interactiveOnly: true });
57597
- } catch (error51) {
57598
- io.stderr.write(`transient runner error (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
57602
+ const stopWait = runtime ? startTurnHeartbeat({
57603
+ fetchImpl,
57604
+ appUrl: appUrl2,
57605
+ secret: state.runner_secret,
57606
+ intervalMs: config2.heartbeatMs,
57607
+ capabilities,
57608
+ io,
57609
+ telemetry,
57610
+ activeTurnIds: () => [...activeTurnExecutionIds]
57611
+ }) : null;
57612
+ try {
57613
+ while (Date.now() < deadline) {
57614
+ const intervalMs = Math.min(config2.interactiveClaimMs, deadline - Date.now());
57615
+ await sleep3(intervalMs);
57616
+ if (!runtime || Date.now() >= deadline) {
57617
+ continue;
57618
+ }
57619
+ try {
57620
+ await claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime, activeTurnExecutionIds, { interactiveOnly: true });
57621
+ } catch (error51) {
57622
+ io.stderr.write(`transient runner error (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
57599
57623
  `);
57624
+ }
57600
57625
  }
57626
+ } finally {
57627
+ stopWait?.();
57601
57628
  }
57602
57629
  }
57630
+ function buildHeartbeatBody(input) {
57631
+ const body = {
57632
+ capabilities: input.capabilities,
57633
+ telemetry: input.telemetry
57634
+ };
57635
+ if (input.activeTurnExecutionIds.length > 0) {
57636
+ body.active_turn_execution_ids = input.activeTurnExecutionIds;
57637
+ }
57638
+ return body;
57639
+ }
57603
57640
  function startTurnHeartbeat(deps) {
57604
57641
  let stopped = false;
57605
57642
  let inFlight = false;
@@ -57613,10 +57650,11 @@ function startTurnHeartbeat(deps) {
57613
57650
  timeout.unref?.();
57614
57651
  void (async () => {
57615
57652
  try {
57616
- const response = await post2(deps.fetchImpl, deps.appUrl, "/api/runner/heartbeat", {
57653
+ const response = await post2(deps.fetchImpl, deps.appUrl, "/api/runner/heartbeat", buildHeartbeatBody({
57617
57654
  capabilities: deps.capabilities,
57618
- telemetry: deps.telemetry()
57619
- }, deps.secret, controller.signal);
57655
+ telemetry: deps.telemetry(),
57656
+ activeTurnExecutionIds: deps.activeTurnIds()
57657
+ }), deps.secret, controller.signal);
57620
57658
  if (!stopped && response.status !== 200) {
57621
57659
  deps.io.stderr.write(`turn heartbeat ${response.status}: ${redactForLog(JSON.stringify(response.json))}
57622
57660
  `);
@@ -58077,7 +58115,7 @@ async function post2(fetchImpl, appUrl2, pathName, body, token, signal) {
58077
58115
  }
58078
58116
  return { status: response.status, json: json2 };
58079
58117
  }
58080
- async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime, options = {}) {
58118
+ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime, activeTurnIds, options = {}) {
58081
58119
  const claimStartedAt = (/* @__PURE__ */ new Date()).toISOString();
58082
58120
  const claimed = await post2(
58083
58121
  fetchImpl,
@@ -58097,7 +58135,7 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
58097
58135
  });
58098
58136
  const turnExecution = claimed.json.turn_execution;
58099
58137
  if (claimed.status === 200 && turnExecution && typeof turnExecution === "object") {
58100
- await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, {
58138
+ await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, activeTurnIds, {
58101
58139
  kind: "turn_execution",
58102
58140
  idField: "turn_execution_id",
58103
58141
  item: turnExecution,
@@ -58111,7 +58149,7 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
58111
58149
  if (claimed.status !== 200 || !workOrder || typeof workOrder !== "object") {
58112
58150
  return;
58113
58151
  }
58114
- await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, {
58152
+ await executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, activeTurnIds, {
58115
58153
  kind: "work_order",
58116
58154
  idField: "work_order_id",
58117
58155
  item: workOrder,
@@ -58120,7 +58158,7 @@ async function claimAndExecute(fetchImpl, appUrl2, state, config2, io, runtime,
58120
58158
  logRef: (id) => `runner-local:${id}`
58121
58159
  });
58122
58160
  }
58123
- async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, input) {
58161
+ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtime, ledger, activeTurnIds, input) {
58124
58162
  const id = input.item.id;
58125
58163
  if (typeof id !== "string") {
58126
58164
  return;
@@ -58129,79 +58167,88 @@ async function executeClaimedUnit(fetchImpl, appUrl2, state, config2, io, runtim
58129
58167
  if (started.status !== 200) {
58130
58168
  throw new Error(`${claimedUnitLabel(input.kind)} start failed ${started.status}: ${redactForLog(JSON.stringify(started.json))}`);
58131
58169
  }
58132
- const turnRuntime = runtimeForClaimedUnit(input.item, runtime, input.kind);
58133
- const result = turnRuntime ? await runLocalTurn({ fetchImpl, appUrl: appUrl2, state, config: config2, io, ledger }, input.item, turnRuntime, input.kind) : {
58134
- ok: false,
58135
- summary: `Runner runtime ${runtime} cannot execute requested ${String(input.item.brain ?? "unknown")} turn.`
58136
- };
58137
- if (input.kind === "turn_execution" && turnRuntime === "claude") {
58138
- try {
58139
- if (result.ok && result.claudeSessionId) {
58140
- await persistClaudeSessionId(config2.stateFile, state, input.item, result.claudeSessionId);
58141
- } else if (!result.ok && result.resumedFromSessionId) {
58142
- await clearClaudeSessionId(config2.stateFile, state, input.item);
58143
- }
58144
- } catch (error51) {
58145
- io.stderr.write(`resume cache update failed (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
58170
+ if (input.kind === "turn_execution") {
58171
+ activeTurnIds.add(id);
58172
+ }
58173
+ try {
58174
+ const turnRuntime = runtimeForClaimedUnit(input.item, runtime, input.kind);
58175
+ const result = turnRuntime ? await runLocalTurn({ fetchImpl, appUrl: appUrl2, state, config: config2, io, ledger }, input.item, turnRuntime, input.kind) : {
58176
+ ok: false,
58177
+ summary: `Runner runtime ${runtime} cannot execute requested ${String(input.item.brain ?? "unknown")} turn.`
58178
+ };
58179
+ if (input.kind === "turn_execution" && turnRuntime === "claude") {
58180
+ try {
58181
+ if (result.ok && result.claudeSessionId) {
58182
+ await persistClaudeSessionId(config2.stateFile, state, input.item, result.claudeSessionId);
58183
+ } else if (!result.ok && result.resumedFromSessionId) {
58184
+ await clearClaudeSessionId(config2.stateFile, state, input.item);
58185
+ }
58186
+ } catch (error51) {
58187
+ io.stderr.write(`resume cache update failed (continuing): ${redactForLog(error51 instanceof Error ? error51.message : String(error51))}
58146
58188
  `);
58189
+ }
58147
58190
  }
58148
- }
58149
- const reportBody = {
58150
- status: result.ok ? "succeeded" : "failed",
58151
- // Cap to the result route's summary limit (see capResultSummary / RESULT_SUMMARY_MAX). A
58152
- // real turn summary can exceed it; without the cap the server's Zod `.max()` 400s the
58153
- // report, and because the loop treats a throw as "transient (continuing)" the phase
58154
- // zombies in `running` until its lease grace. Truncating keeps a successful turn a success.
58155
- summary: capResultSummary(result.summary),
58156
- log_ref: input.logRef(id),
58157
- model: (turnRuntime ?? runtime) === "codex" ? "codex-cli" : "claude-cli",
58158
- // DER-1915: token usage lifted from the claude JSON envelope (extractClaudeUsage), sent for
58159
- // BOTH kinds when present never defaulted to 0 (see RunnerTurnResult.inputTokens/outputTokens).
58160
- // Both result routes (work-orders and turn-executions) accept these snake_case fields.
58161
- ...result.inputTokens != null ? { input_tokens: result.inputTokens } : {},
58162
- ...result.outputTokens != null ? { output_tokens: result.outputTokens } : {},
58163
- // Artifact-bearing result fields (T1.5) only sent when the harness produced them, so
58164
- // the result route's `.strict()` schema still accepts a plain read-status report.
58165
- ...result.prUrl ? { pr_url: result.prUrl } : {},
58166
- ...result.branch ? { branch: result.branch } : {},
58167
- ...result.commitSha ? { commit_sha: result.commitSha } : {},
58168
- ...result.evidence && result.evidence.length > 0 ? { evidence: result.evidence } : {},
58169
- ...result.changedPaths && result.changedPaths.length > 0 ? { changed_paths: result.changedPaths } : {},
58170
- // DER-1352 (T1.8): the merge-turn postcondition. Only sent when the harness GET-verified the PR
58171
- // merged on GitHub the driver's completion writer marks the changeset merged + writes the
58172
- // decisions row ONLY on this. Gated on result.ok so a failed turn can never carry it.
58173
- ...result.ok && result.merged === true ? { merged: true } : {},
58174
- ...result.ok && result.merged === true && result.mergedCommitSha ? { merged_commit_sha: result.mergedCommitSha } : {},
58175
- // DER-1331 (T1.9): the step ledger + the model's (already redacted, bounded) transcript.
58176
- // Only the work-orders result route accepts these today (turn-executions is a separate,
58177
- // narrower endpoint T1.9 scope); its Zod schema strips unrecognized keys on a non-strict
58178
- // parse either way, so sending them there is harmless, just unused.
58179
- ...input.kind === "work_order" ? { step_ledger: ledger.entries() } : {},
58180
- ...input.kind === "work_order" && result.transcript ? { transcript: result.transcript } : {},
58181
- // DER-1371: the planner's structured decomposition only sent when the model produced one.
58182
- ...result.planArtifact ? { plan_artifact: result.planArtifact } : {}
58183
- };
58184
- const reported = await postResultWithRetries(fetchImpl, appUrl2, input.resultPath(id), reportBody, state.runner_secret);
58185
- if (reported.status !== 200) {
58186
- if (reported.status >= 400 && reported.status < 500) {
58187
- const fallback = await post2(fetchImpl, appUrl2, input.resultPath(id), {
58188
- status: "failed",
58189
- summary: capResultSummary(
58190
- `Runner result rejected by server (${reported.status}). Original outcome: ${result.ok ? "succeeded" : "failed"}. ${redactForLog(JSON.stringify(reported.json))}`
58191
- ),
58192
- log_ref: input.logRef(id),
58193
- model: (turnRuntime ?? runtime) === "codex" ? "codex-cli" : "claude-cli"
58194
- }, state.runner_secret);
58195
- if (fallback.status === 200) {
58196
- io.stdout.write(`${input.idField}=${id} status=failed (result rejected ${reported.status}; reported terminal failure)
58191
+ const reportBody = {
58192
+ status: result.ok ? "succeeded" : "failed",
58193
+ // Cap to the result route's summary limit (see capResultSummary / RESULT_SUMMARY_MAX). A
58194
+ // real turn summary can exceed it; without the cap the server's Zod `.max()` 400s the
58195
+ // report, and because the loop treats a throw as "transient (continuing)" the phase
58196
+ // zombies in `running` until its lease grace. Truncating keeps a successful turn a success.
58197
+ summary: capResultSummary(result.summary),
58198
+ log_ref: input.logRef(id),
58199
+ model: (turnRuntime ?? runtime) === "codex" ? "codex-cli" : "claude-cli",
58200
+ // DER-1915: token usage lifted from the claude JSON envelope (extractClaudeUsage), sent for
58201
+ // BOTH kinds when present never defaulted to 0 (see RunnerTurnResult.inputTokens/outputTokens).
58202
+ // Both result routes (work-orders and turn-executions) accept these snake_case fields.
58203
+ ...result.inputTokens != null ? { input_tokens: result.inputTokens } : {},
58204
+ ...result.outputTokens != null ? { output_tokens: result.outputTokens } : {},
58205
+ // Artifact-bearing result fields (T1.5) only sent when the harness produced them, so
58206
+ // the result route's `.strict()` schema still accepts a plain read-status report.
58207
+ ...result.prUrl ? { pr_url: result.prUrl } : {},
58208
+ ...result.branch ? { branch: result.branch } : {},
58209
+ ...result.commitSha ? { commit_sha: result.commitSha } : {},
58210
+ ...result.evidence && result.evidence.length > 0 ? { evidence: result.evidence } : {},
58211
+ ...result.changedPaths && result.changedPaths.length > 0 ? { changed_paths: result.changedPaths } : {},
58212
+ // DER-1352 (T1.8): the merge-turn postcondition. Only sent when the harness GET-verified the PR
58213
+ // merged on GitHub — the driver's completion writer marks the changeset merged + writes the
58214
+ // decisions row ONLY on this. Gated on result.ok so a failed turn can never carry it.
58215
+ ...result.ok && result.merged === true ? { merged: true } : {},
58216
+ ...result.ok && result.merged === true && result.mergedCommitSha ? { merged_commit_sha: result.mergedCommitSha } : {},
58217
+ // DER-1331 (T1.9): the step ledger + the model's (already redacted, bounded) transcript.
58218
+ // Only the work-orders result route accepts these today (turn-executions is a separate,
58219
+ // narrower endpoint T1.9 scope); its Zod schema strips unrecognized keys on a non-strict
58220
+ // parse either way, so sending them there is harmless, just unused.
58221
+ ...input.kind === "work_order" ? { step_ledger: ledger.entries() } : {},
58222
+ ...input.kind === "work_order" && result.transcript ? { transcript: result.transcript } : {},
58223
+ // DER-1371: the planner's structured decomposition only sent when the model produced one.
58224
+ ...result.planArtifact ? { plan_artifact: result.planArtifact } : {}
58225
+ };
58226
+ const reported = await postResultWithRetries(fetchImpl, appUrl2, input.resultPath(id), reportBody, state.runner_secret);
58227
+ if (reported.status !== 200) {
58228
+ if (reported.status >= 400 && reported.status < 500) {
58229
+ const fallback = await post2(fetchImpl, appUrl2, input.resultPath(id), {
58230
+ status: "failed",
58231
+ summary: capResultSummary(
58232
+ `Runner result rejected by server (${reported.status}). Original outcome: ${result.ok ? "succeeded" : "failed"}. ${redactForLog(JSON.stringify(reported.json))}`
58233
+ ),
58234
+ log_ref: input.logRef(id),
58235
+ model: (turnRuntime ?? runtime) === "codex" ? "codex-cli" : "claude-cli"
58236
+ }, state.runner_secret);
58237
+ if (fallback.status === 200) {
58238
+ io.stdout.write(`${input.idField}=${id} status=failed (result rejected ${reported.status}; reported terminal failure)
58197
58239
  `);
58198
- return;
58240
+ return;
58241
+ }
58199
58242
  }
58243
+ throw new Error(`${claimedUnitLabel(input.kind)} result failed ${reported.status}: ${redactForLog(JSON.stringify(reported.json))}`);
58200
58244
  }
58201
- throw new Error(`${claimedUnitLabel(input.kind)} result failed ${reported.status}: ${redactForLog(JSON.stringify(reported.json))}`);
58202
- }
58203
- io.stdout.write(`${input.idField}=${id} status=${result.ok ? "succeeded" : "failed"}
58245
+ io.stdout.write(`${input.idField}=${id} status=${result.ok ? "succeeded" : "failed"}
58204
58246
  `);
58247
+ } finally {
58248
+ if (input.kind === "turn_execution") {
58249
+ activeTurnIds.delete(id);
58250
+ }
58251
+ }
58205
58252
  }
58206
58253
  var RESULT_REPORT_RETRY_ATTEMPTS = 4;
58207
58254
  var RESULT_REPORT_RETRY_BASE_DELAY_MS = 500;