pullfrog 0.1.59 → 0.1.61

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.
@@ -322,6 +322,13 @@ export interface WriteablePayload {
322
322
  } | undefined;
323
323
  /** when true, seed the PR summary tmpfile + persist edits at run end */
324
324
  generateSummary?: boolean | undefined;
325
+ /**
326
+ * the codex canary assigned this run to the EXPERIMENTAL codex harness. rolled
327
+ * once server-side at reservation and stamped on the run row, so the row and the
328
+ * run always agree on which arm this was. ORed with the account opt-in in
329
+ * `resolveAgent` — it widens who reaches codex, never narrows it.
330
+ */
331
+ codexArm?: boolean | undefined;
325
332
  }
326
333
  export type Payload = Readonly<WriteablePayload>;
327
334
  /**
package/dist/index.js CHANGED
@@ -104420,6 +104420,11 @@ var DEFAULT_ACTIVITY_TIMEOUT_MS = 3e5;
104420
104420
  var AGENT_ACTIVITY_TIMEOUT_MS = 9e5;
104421
104421
  var AGENT_FIRST_EVENT_TIMEOUT_MS = 12e4;
104422
104422
  var DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5e3;
104423
+ function watchdogBudgetMs(compiled, envVar) {
104424
+ const raw2 = Number(process.env[envVar]);
104425
+ if (!Number.isFinite(raw2) || raw2 <= 0) return compiled;
104426
+ return Math.min(compiled, raw2);
104427
+ }
104423
104428
  var DEBUG_TS_PREFIX = /^(?:\[\d{4}-\d{2}-\d{2}T[^\]]+\]\s+)?/.source;
104424
104429
  var ACTIVITY_NOISE_PATTERNS = [
104425
104430
  new RegExp(`${DEBUG_TS_PREFIX}\\[mcp-proxy\\]`),
@@ -105183,7 +105188,7 @@ var import_semver = __toESM(require_semver2(), 1);
105183
105188
  // package.json
105184
105189
  var package_default = {
105185
105190
  name: "pullfrog",
105186
- version: "0.1.59",
105191
+ version: "0.1.61",
105187
105192
  type: "module",
105188
105193
  bin: {
105189
105194
  pullfrog: "dist/cli.mjs",
@@ -107292,7 +107297,7 @@ var claude = agent({
107292
107297
  if ((isBedrockRoute || isVertexRoute2) && specifier && effort.alias?.effort) {
107293
107298
  applyHostedEffortCapabilities({ env: env2, modelId: specifier, levels: effort.alias.effort });
107294
107299
  }
107295
- if (env2.CLAUDE_CODE_OAUTH_TOKEN && !isBedrockRoute && env2.ANTHROPIC_API_KEY) {
107300
+ if (env2.CLAUDE_CODE_OAUTH_TOKEN && !isBedrockRoute && !isVertexRoute2 && !env2.ANTHROPIC_BASE_URL && !env2.ANTHROPIC_AUTH_TOKEN && env2.ANTHROPIC_API_KEY) {
107296
107301
  const preflight = await preflightClaudeSubscription({
107297
107302
  token: env2.CLAUDE_CODE_OAUTH_TOKEN,
107298
107303
  model
@@ -107309,6 +107314,19 @@ var claude = agent({
107309
107314
  delete env2.CLAUDE_CODE_OAUTH_TOKEN;
107310
107315
  }
107311
107316
  }
107317
+ if (env2.CLAUDE_CODE_USE_BEDROCK) {
107318
+ ctx.toolState.credential = "bedrock";
107319
+ } else if (env2.CLAUDE_CODE_USE_VERTEX) {
107320
+ ctx.toolState.credential = "vertex";
107321
+ } else if (env2.CLAUDE_CODE_USE_FOUNDRY) {
107322
+ ctx.toolState.credential = "foundry";
107323
+ } else if (env2.ANTHROPIC_AUTH_TOKEN) {
107324
+ ctx.toolState.credential = "gateway";
107325
+ } else if (env2.ANTHROPIC_API_KEY) {
107326
+ ctx.toolState.credential = "api_key";
107327
+ } else if (env2.CLAUDE_CODE_OAUTH_TOKEN) {
107328
+ ctx.toolState.credential = "subscription";
107329
+ }
107312
107330
  const effortEnvOverride = env2[CLAUDE_EFFORT_ENV]?.trim();
107313
107331
  if (effortEnvOverride) {
107314
107332
  log.warning(
@@ -107818,6 +107836,18 @@ function toTodoWriteInput(item) {
107818
107836
  }))
107819
107837
  };
107820
107838
  }
107839
+ var CODEX_MODEL_PRICING = {
107840
+ "gpt-5.6-sol": { input: 5, cacheRead: 0.5, cacheWrite: 6.25, output: 30 },
107841
+ "gpt-5.6-luna": { input: 0.2, cacheRead: 0.02, cacheWrite: 0.25, output: 1.2 },
107842
+ "gpt-5.6-terra": { input: 2, cacheRead: 0.2, cacheWrite: 2.5, output: 12 }
107843
+ };
107844
+ function codexCostUsd(params) {
107845
+ const price = params.model ? CODEX_MODEL_PRICING[params.model] : void 0;
107846
+ if (!price) return void 0;
107847
+ const fresh = Math.max(0, params.input - params.cacheRead - params.cacheWrite);
107848
+ const usd = (fresh * price.input + params.cacheRead * price.cacheRead + params.cacheWrite * price.cacheWrite + params.output * price.output) / 1e6;
107849
+ return usd > 0 ? usd : void 0;
107850
+ }
107821
107851
  async function runCodex(params) {
107822
107852
  const startTime = performance6.now();
107823
107853
  const thinkingTimer = new ThinkingTimer();
@@ -107829,14 +107859,15 @@ async function runCodex(params) {
107829
107859
  let turnError = null;
107830
107860
  let lastProviderError = null;
107831
107861
  let stdoutBuffer = "";
107832
- const tokens = { input: 0, cacheRead: 0, output: 0 };
107862
+ const tokens = { input: 0, cacheRead: 0, cacheWrite: 0, output: 0 };
107833
107863
  function buildUsage2() {
107834
107864
  if (tokens.input === 0 && tokens.output === 0) return void 0;
107835
107865
  return {
107836
107866
  agent: "codex",
107837
107867
  inputTokens: tokens.input,
107838
107868
  outputTokens: tokens.output,
107839
- cacheReadTokens: tokens.cacheRead || void 0
107869
+ cacheReadTokens: tokens.cacheRead || void 0,
107870
+ costUsd: codexCostUsd({ model: params.model, ...tokens })
107840
107871
  };
107841
107872
  }
107842
107873
  function onItem(item, phase) {
@@ -107901,6 +107932,7 @@ async function runCodex(params) {
107901
107932
  case "turn.completed":
107902
107933
  tokens.input += event.usage.input_tokens ?? 0;
107903
107934
  tokens.cacheRead += event.usage.cached_input_tokens ?? 0;
107935
+ tokens.cacheWrite += event.usage.cache_write_input_tokens ?? 0;
107904
107936
  tokens.output += event.usage.output_tokens ?? 0;
107905
107937
  return;
107906
107938
  case "turn.failed":
@@ -108061,6 +108093,11 @@ var codex = agent({
108061
108093
  } else if (process.env.OPENAI_API_KEY) {
108062
108094
  env2.CODEX_API_KEY = process.env.OPENAI_API_KEY;
108063
108095
  }
108096
+ if (codexHomeAuth) {
108097
+ ctx.toolState.credential = "subscription";
108098
+ } else if (env2.CODEX_API_KEY) {
108099
+ ctx.toolState.credential = "api_key";
108100
+ }
108064
108101
  const securityFlags = securityOverrideFlags({
108065
108102
  ctx,
108066
108103
  sandboxMode: ctx.payload.push === "disabled" ? "read-only" : "workspace-write",
@@ -108069,7 +108106,13 @@ var codex = agent({
108069
108106
  model: resolveCodexModel(ctx),
108070
108107
  effortRung: effort.rung && CODEX_EFFORTS.includes(effort.rung) ? effort.rung : void 0
108071
108108
  });
108072
- const runnerArgs = { cliPath, cwd: process.cwd(), env: env2, todoTracker: ctx.todoTracker };
108109
+ const runnerArgs = {
108110
+ cliPath,
108111
+ cwd: process.cwd(),
108112
+ env: env2,
108113
+ todoTracker: ctx.todoTracker,
108114
+ model: resolveCodexModel(ctx)
108115
+ };
108073
108116
  const initial = await runCodex({
108074
108117
  ...runnerArgs,
108075
108118
  args: [...securityFlags, "exec", "--json", ctx.instructions.full],
@@ -114157,6 +114200,7 @@ function processTerminalToolPart(ctx, part, label, isOrchestrator) {
114157
114200
  const callLine = inputFormatted !== "{}" ? `\xBB ${toolName}(${inputFormatted})` : `\xBB ${toolName}()`;
114158
114201
  log.info(withLabel(label, callLine));
114159
114202
  if (isOrchestrator) ctx.loggedToolCallIDs.add(toolId);
114203
+ if (isOrchestrator && toolName.startsWith("pullfrog_")) ctx.mcpToolCalls++;
114160
114204
  if (state.status === "completed") {
114161
114205
  log.debug(withLabel(label, ` output: ${state.output}`));
114162
114206
  } else {
@@ -114395,21 +114439,28 @@ function formatConfigError(name, data) {
114395
114439
  ).join("");
114396
114440
  return `${name}${detail}${bullets}`;
114397
114441
  }
114442
+ var WATCHDOG_SALVAGE_PROMPT = "Your previous turn was cut off mid-response by a provider stall. Nothing you had not already submitted through a tool was saved. Continue from where you stopped and submit your work now \u2014 do not restart your analysis.";
114398
114443
  function startInnerActivityWatchdog(params) {
114399
114444
  let fired = false;
114445
+ let everFired = false;
114400
114446
  const id = setInterval(() => {
114401
114447
  if (fired) return;
114402
114448
  const idleMs = performance7.now() - params.ctx.lastEventAt;
114403
- const budgetMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
114449
+ const compiledMs = params.ctx.sawModelOutput ? params.timeoutMs : AGENT_FIRST_EVENT_TIMEOUT_MS;
114450
+ const budgetMs = everFired ? compiledMs : watchdogBudgetMs(
114451
+ compiledMs,
114452
+ params.ctx.sawModelOutput ? "PULLFROG_E2E_ACTIVITY_TIMEOUT_MS" : "PULLFROG_E2E_FIRST_EVENT_TIMEOUT_MS"
114453
+ );
114404
114454
  if (idleMs <= budgetMs) return;
114405
114455
  fired = true;
114456
+ everFired = true;
114406
114457
  const idleSec = Math.round(idleMs / 1e3);
114407
114458
  params.ctx.diagnostic.idleSec = idleSec;
114408
114459
  params.ctx.diagnostic.sawModelOutput = params.ctx.sawModelOutput;
114409
114460
  log.info(
114410
114461
  params.ctx.sawModelOutput ? `\xBB no opencode events for ${idleSec}s \u2014 aborting in-flight prompt and notifying harness` : `\xBB no opencode events for ${idleSec}s \u2014 the provider never returned a first token; aborting in-flight prompt and notifying harness`
114411
114462
  );
114412
- params.abortController.abort();
114463
+ params.abortTurn();
114413
114464
  try {
114414
114465
  params.ctx.onActivityTimeout?.();
114415
114466
  } catch (err) {
@@ -114419,7 +114470,22 @@ function startInnerActivityWatchdog(params) {
114419
114470
  }
114420
114471
  }, 5e3);
114421
114472
  id.unref?.();
114422
- return { stop: () => clearInterval(id) };
114473
+ return {
114474
+ stop: () => clearInterval(id),
114475
+ /**
114476
+ * Start a turn's idle budget from now. The clock must be reset with the
114477
+ * latch: `lastEventAt` only advances on model output, so a turn following a
114478
+ * stall would inherit the full stalled interval and be aborted on the very
114479
+ * next 5s tick — killing the salvage before the provider could answer. It
114480
+ * is the right semantics for an ordinary resume too, whose budget should
114481
+ * not be pre-spent by the post-run gate checks that ran between turns.
114482
+ */
114483
+ armForTurn: () => {
114484
+ fired = false;
114485
+ params.ctx.lastEventAt = performance7.now();
114486
+ },
114487
+ firedThisTurn: () => fired
114488
+ };
114423
114489
  }
114424
114490
  var opencode = agent({
114425
114491
  name: "opencode",
@@ -114531,6 +114597,7 @@ var opencode = agent({
114531
114597
  variant: effort.rung,
114532
114598
  todoTracker: ctx.todoTracker,
114533
114599
  onActivityTimeout: ctx.onActivityTimeout,
114600
+ onTurnRecovered: ctx.onTurnRecovered,
114534
114601
  onToolUse: ctx.onToolUse,
114535
114602
  currentTurn: null,
114536
114603
  eventCount: 0,
@@ -114539,6 +114606,7 @@ var opencode = agent({
114539
114606
  promptMessageID: void 0,
114540
114607
  taskDispatchByCallID: /* @__PURE__ */ new Map(),
114541
114608
  loggedToolCallIDs: /* @__PURE__ */ new Set(),
114609
+ mcpToolCalls: 0,
114542
114610
  recentStderr: server.recentStderr,
114543
114611
  diagnostic: {
114544
114612
  label: "Pullfrog",
@@ -114562,9 +114630,14 @@ var opencode = agent({
114562
114630
  }
114563
114631
  }
114564
114632
  });
114565
- const abortController = new AbortController();
114566
- const eventLoopPromise = consumeEvents(runnerCtx, abortController.signal).catch((err) => {
114567
- if (!abortController.signal.aborted) {
114633
+ const runController = new AbortController();
114634
+ let turnController = new AbortController();
114635
+ const nextTurnSignal = () => {
114636
+ turnController = new AbortController();
114637
+ return AbortSignal.any([runController.signal, turnController.signal]);
114638
+ };
114639
+ const eventLoopPromise = consumeEvents(runnerCtx, runController.signal).catch((err) => {
114640
+ if (!runController.signal.aborted) {
114568
114641
  log.warning(
114569
114642
  `\xBB opencode event subscription ended: ${err instanceof Error ? err.message : String(err)}`
114570
114643
  );
@@ -114580,31 +114653,48 @@ var opencode = agent({
114580
114653
  // a long synchronous tool call (no part.updated while it runs) can't
114581
114654
  // false-positive it.
114582
114655
  timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
114583
- abortController
114656
+ abortTurn: () => turnController.abort()
114584
114657
  });
114585
114658
  const sdkModel = parseModel2(model);
114659
+ let salvagesLeft = 1;
114660
+ const runTurn = async (text) => {
114661
+ const attempt = (prompt) => {
114662
+ watchdog.armForTurn();
114663
+ return runTurnGuarded(
114664
+ runnerCtx,
114665
+ () => runPromptTurn(runnerCtx, { text: prompt, model: sdkModel, signal: nextTurnSignal() })
114666
+ );
114667
+ };
114668
+ const standDownIfRecovered = (turn) => {
114669
+ if (watchdog.firedThisTurn() && turn.success) ctx.onTurnRecovered?.();
114670
+ return turn;
114671
+ };
114672
+ const result = await attempt(text);
114673
+ if (!watchdog.firedThisTurn()) return result;
114674
+ if (result.success) return standDownIfRecovered(result);
114675
+ if (salvagesLeft <= 0) return result;
114676
+ salvagesLeft--;
114677
+ ctx.onTurnRecovered?.();
114678
+ log.info("\xBB activity watchdog cut the turn off \u2014 re-prompting once on the same session");
114679
+ const mcpCallsBefore = runnerCtx.mcpToolCalls;
114680
+ const salvaged = await attempt(WATCHDOG_SALVAGE_PROMPT);
114681
+ const usage = mergeAgentUsage(result.usage, salvaged.usage);
114682
+ if (runnerCtx.mcpToolCalls === mcpCallsBefore) {
114683
+ log.info(
114684
+ "\xBB salvage turn reached no Pullfrog tool \u2014 keeping the activity-timeout failure"
114685
+ );
114686
+ return { ...result, usage };
114687
+ }
114688
+ return standDownIfRecovered({ ...salvaged, usage });
114689
+ };
114586
114690
  try {
114587
- const initial = await runTurnGuarded(
114588
- runnerCtx,
114589
- () => runPromptTurn(runnerCtx, {
114590
- text: ctx.instructions.full,
114591
- model: sdkModel,
114592
- signal: abortController.signal
114593
- })
114594
- );
114691
+ const initial = await runTurn(ctx.instructions.full);
114595
114692
  const result = await runPostRunRetryLoop({
114596
114693
  ctx,
114597
114694
  initialResult: initial,
114598
114695
  initialUsage: initial.usage,
114599
114696
  reflectionPrompt: buildReflectionPrompt(ctx.toolState),
114600
- resume: async (c) => runTurnGuarded(
114601
- runnerCtx,
114602
- () => runPromptTurn(runnerCtx, {
114603
- text: c.prompt,
114604
- model: sdkModel,
114605
- signal: abortController.signal
114606
- })
114607
- )
114697
+ resume: async (c) => runTurn(c.prompt)
114608
114698
  });
114609
114699
  if (result.success) {
114610
114700
  await ctx.todoTracker?.flush();
@@ -114614,7 +114704,7 @@ var opencode = agent({
114614
114704
  return result;
114615
114705
  } finally {
114616
114706
  watchdog.stop();
114617
- abortController.abort();
114707
+ runController.abort();
114618
114708
  await eventLoopPromise.catch(() => {
114619
114709
  });
114620
114710
  }
@@ -187244,7 +187334,9 @@ var STRING_KEYS = [
187244
187334
  "reviewNodeId",
187245
187335
  "planCommentNodeId",
187246
187336
  "summarySnapshot",
187247
- "model"
187337
+ "model",
187338
+ "agent",
187339
+ "credential"
187248
187340
  ];
187249
187341
  var NUMBER_KEYS = [
187250
187342
  "inputTokens",
@@ -192405,7 +192497,7 @@ function hasEnvVar2(name) {
192405
192497
  return typeof val === "string" && val.length > 0;
192406
192498
  }
192407
192499
  function hasClaudeCodeAuth() {
192408
- return hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar2("ANTHROPIC_API_KEY");
192500
+ return hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN") || hasEnvVar2("ANTHROPIC_API_KEY") || hasEnvVar2("ANTHROPIC_AUTH_TOKEN");
192409
192501
  }
192410
192502
  function hasCodexAuth() {
192411
192503
  return hasEnvVar2("CODEX_AUTH_JSON") || hasEnvVar2("OPENAI_API_KEY");
@@ -192498,8 +192590,11 @@ function resolveAgent(ctx) {
192498
192590
  } catch {
192499
192591
  }
192500
192592
  }
192501
- if (!ctx.model && ctx.codexAgent && hasCodexAuth() && !hasClaudeCodeAuth()) {
192502
- return agents.codex;
192593
+ if (!ctx.model) {
192594
+ if (hasEnvVar2("ANTHROPIC_AUTH_TOKEN") && !hasEnvVar2("ANTHROPIC_API_KEY") && !hasEnvVar2("CLAUDE_CODE_OAUTH_TOKEN")) {
192595
+ return agents.claude;
192596
+ }
192597
+ if (ctx.codexAgent && hasCodexAuth() && !hasClaudeCodeAuth()) return agents.codex;
192503
192598
  }
192504
192599
  return agents.opencode;
192505
192600
  }
@@ -192685,7 +192780,7 @@ function hasSingleProviderAuth(agentName) {
192685
192780
  if (agentName === "codex") {
192686
192781
  return hasEnvVar3("OPENAI_API_KEY") || hasEnvVar3("CODEX_AUTH_JSON");
192687
192782
  }
192688
- return hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN");
192783
+ return hasEnvVar3("ANTHROPIC_API_KEY") || hasEnvVar3("ANTHROPIC_AUTH_TOKEN") || hasEnvVar3("CLAUDE_CODE_OAUTH_TOKEN");
192689
192784
  }
192690
192785
  function validateAgentApiKey(params) {
192691
192786
  if (params.model) {
@@ -193017,7 +193112,8 @@ var PROBES = {
193017
193112
  request: (value2) => ({
193018
193113
  url: "https://api.anthropic.com/v1/models",
193019
193114
  headers: { "x-api-key": value2, "anthropic-version": "2023-06-01" }
193020
- })
193115
+ }),
193116
+ hostConfigurable: true
193021
193117
  },
193022
193118
  OPENROUTER_API_KEY: {
193023
193119
  request: (value2) => ({
@@ -193066,12 +193162,18 @@ function hasEnvVar4(name) {
193066
193162
  const value2 = process.env[name];
193067
193163
  return typeof value2 === "string" && value2.length > 0;
193068
193164
  }
193165
+ var HOST_OVERRIDES = {
193166
+ ANTHROPIC_API_KEY: "ANTHROPIC_BASE_URL",
193167
+ CLAUDE_CODE_OAUTH_TOKEN: "ANTHROPIC_BASE_URL"
193168
+ };
193069
193169
  function envVarsFor(model) {
193070
193170
  return model.includes("/") ? getModelEnvVars(model) : [];
193071
193171
  }
193072
193172
  async function checkOne(params) {
193073
193173
  const value2 = process.env[params.envVar];
193074
193174
  if (!value2) return null;
193175
+ const hostOverride = HOST_OVERRIDES[params.envVar];
193176
+ if (hostOverride && hasEnvVar4(hostOverride)) return null;
193075
193177
  if (params.envVar === "CLAUDE_CODE_OAUTH_TOKEN") {
193076
193178
  const preflight = await preflightClaudeSubscription({
193077
193179
  token: value2,
@@ -194013,7 +194115,10 @@ var JsonPayload = type({
194013
194115
  // optional so a payload from an older server build (pre-`checkRun`) still parses
194014
194116
  // against a newer action across a rolling deploy.
194015
194117
  "checkRun?": type({ id: "string" }).or("undefined"),
194016
- "generateSummary?": "boolean | undefined"
194118
+ "generateSummary?": "boolean | undefined",
194119
+ // optional so a payload from a pre-canary server build still parses against a
194120
+ // newer action across a rolling deploy.
194121
+ "codexArm?": "boolean | undefined"
194017
194122
  });
194018
194123
  var COLLABORATOR_PERMISSIONS = ["admin", "maintain", "write"];
194019
194124
  function isCollaborator(event) {
@@ -194142,6 +194247,7 @@ function resolvePayload(resolvedPromptInput, repoSettings) {
194142
194247
  progressComment: jsonPayload?.progressComment,
194143
194248
  checkRun: jsonPayload?.checkRun,
194144
194249
  generateSummary: jsonPayload?.generateSummary,
194250
+ codexArm: jsonPayload?.codexArm,
194145
194251
  // permissions: inputs > repoSettings > fallbacks
194146
194252
  push: inputs.push ?? repoSettings.push ?? "restricted",
194147
194253
  shell: resolvedShell,
@@ -196086,8 +196192,12 @@ async function main() {
196086
196192
  const agent2 = resolveAgent({
196087
196193
  model: resolvedModel,
196088
196194
  proxyModel: payload.proxyModel,
196089
- codexAgent: runContext.repoSettings.codexAgent
196195
+ // the account opt-in and the canary arm are both admissions to codex, so
196196
+ // they OR: an account that opted in explicitly always gets it, and the
196197
+ // canary widens the pool without ever demoting a run that already had it.
196198
+ codexAgent: runContext.repoSettings.codexAgent || payload.codexArm === true
196090
196199
  });
196200
+ toolState.agent = agent2.name;
196091
196201
  const effectiveModel = payload.proxyModel ?? resolvedModel ?? payload.model;
196092
196202
  toolState.model = effectiveModel;
196093
196203
  if (!payload.proxyModel) {
@@ -196304,16 +196414,14 @@ ${instructions.user}` : null,
196304
196414
  const onInnerActivityTimeout = () => {
196305
196415
  if (innerTimeoutFired) return;
196306
196416
  innerTimeoutFired = true;
196307
- log.info(
196308
- "\xBB inner activity timeout fired \u2014 stopping MCP server and starting 5min safety-net timer"
196309
- );
196310
- mcpHttpServer[Symbol.asyncDispose]().catch((err) => {
196311
- log.debug(
196312
- `mcp server stop after inner kill failed: ${err instanceof Error ? err.message : String(err)}`
196313
- );
196314
- });
196417
+ log.info("\xBB inner activity timeout fired \u2014 starting 5min safety-net timer");
196315
196418
  safetyNetTimer = setTimeout(
196316
196419
  () => {
196420
+ mcpHttpServer[Symbol.asyncDispose]().catch((err) => {
196421
+ log.debug(
196422
+ `mcp server stop after inner kill failed: ${err instanceof Error ? err.message : String(err)}`
196423
+ );
196424
+ });
196317
196425
  activityTimeout?.forceReject(
196318
196426
  "agent still pending 5min after inner activity kill \u2014 forcing exit"
196319
196427
  );
@@ -196322,6 +196430,13 @@ ${instructions.user}` : null,
196322
196430
  );
196323
196431
  safetyNetTimer.unref?.();
196324
196432
  };
196433
+ const onTurnRecovered = () => {
196434
+ if (!innerTimeoutFired) return;
196435
+ innerTimeoutFired = false;
196436
+ if (safetyNetTimer) clearTimeout(safetyNetTimer);
196437
+ safetyNetTimer = void 0;
196438
+ log.info("\xBB inner activity safety net stood down \u2014 turn recovered");
196439
+ };
196325
196440
  const agentPromise = agent2.run({
196326
196441
  payload,
196327
196442
  resolvedModel,
@@ -196342,6 +196457,7 @@ ${instructions.user}` : null,
196342
196457
  toolState,
196343
196458
  apiToken: runContext.apiToken,
196344
196459
  onActivityTimeout: onInnerActivityTimeout,
196460
+ onTurnRecovered,
196345
196461
  onToolUse: (event) => {
196346
196462
  const wasTracked = recordDiffReadFromToolUse({
196347
196463
  state: primaryRepoState(toolState).diffCoverage,
@@ -196439,6 +196555,8 @@ ${instructions.user}` : null,
196439
196555
  if (toolContext) {
196440
196556
  const patch = aggregateUsage(toolState.usageEntries);
196441
196557
  if (toolState.model) patch.model = toolState.model;
196558
+ if (toolState.agent) patch.agent = toolState.agent;
196559
+ if (toolState.credential) patch.credential = toolState.credential;
196442
196560
  if (Object.keys(patch).length > 0) {
196443
196561
  await patchWorkflowRunFields(toolContext, patch);
196444
196562
  }
package/dist/internal.js CHANGED
@@ -777,9 +777,16 @@ function getModelManagedCredentials(slug) {
777
777
  const providerConfig = providers[parsed.provider];
778
778
  return providerConfig?.managedCredentials?.slice() ?? [];
779
779
  }
780
+ var HARNESS_ONLY_CREDENTIALS = {
781
+ anthropic: ["ANTHROPIC_AUTH_TOKEN"]
782
+ };
780
783
  function modelHasStoredAuth(params) {
781
784
  const slug = resolveDisplayAlias(params.model)?.slug ?? params.model;
782
- const authVars = [...getModelEnvVars(slug), ...getModelManagedCredentials(slug)];
785
+ const authVars = [
786
+ ...getModelEnvVars(slug),
787
+ ...getModelManagedCredentials(slug),
788
+ ...HARNESS_ONLY_CREDENTIALS[getModelProvider(slug)] ?? []
789
+ ];
783
790
  return authVars.some((v) => params.secretNames.includes(v));
784
791
  }
785
792
  var modelAliases = Object.entries(providers).flatMap(
@@ -1542,7 +1549,8 @@ var PROBES = {
1542
1549
  request: (value) => ({
1543
1550
  url: "https://api.anthropic.com/v1/models",
1544
1551
  headers: { "x-api-key": value, "anthropic-version": "2023-06-01" }
1545
- })
1552
+ }),
1553
+ hostConfigurable: true
1546
1554
  },
1547
1555
  OPENROUTER_API_KEY: {
1548
1556
  request: (value) => ({
@@ -1565,7 +1573,9 @@ async function verifyClaudeSubscription(value) {
1565
1573
  return preflight.status === 401 ? "dead" : "alive";
1566
1574
  }
1567
1575
  function isCredentialProbeable(envVar) {
1568
- return envVar === "CLAUDE_CODE_OAUTH_TOKEN" || envVar in PROBES;
1576
+ if (envVar === "CLAUDE_CODE_OAUTH_TOKEN") return true;
1577
+ const probe = PROBES[envVar];
1578
+ return probe !== void 0 && !probe.hostConfigurable;
1569
1579
  }
1570
1580
  async function verifyCredential(params) {
1571
1581
  const value = params.value.trim();
@@ -1,4 +1,4 @@
1
- import type { AgentUsage } from "./agents/shared.ts";
1
+ import type { AgentCredential, AgentUsage } from "./agents/shared.ts";
2
2
  import type { PrepResult } from "./prep/types.ts";
3
3
  import type { AgentDiagnostic } from "./utils/agentHangReport.ts";
4
4
  import type { DiffCoverageState } from "./utils/diffCoverage.ts";
@@ -136,6 +136,8 @@ export interface ToolState {
136
136
  output?: string | undefined;
137
137
  usageEntries: AgentUsage[];
138
138
  model?: string | undefined;
139
+ agent?: string | undefined;
140
+ credential?: AgentCredential | undefined;
139
141
  modelFallback?: {
140
142
  from: string;
141
143
  } | undefined;
@@ -50,6 +50,18 @@ export declare const AGENT_ACTIVITY_TIMEOUT_MS = 900000;
50
50
  */
51
51
  export declare const AGENT_FIRST_EVENT_TIMEOUT_MS = 120000;
52
52
  export declare const DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5000;
53
+ /**
54
+ * E2E affordance: shorten a watchdog budget for one dispatch, so the stall path
55
+ * can be exercised on demand. A real provider stall is not provocable, which is
56
+ * how #1085 shipped three escalations deep with its salvage branch never once
57
+ * run end to end.
58
+ *
59
+ * **Shorten-only, deliberately.** The value is clamped to the compiled budget,
60
+ * so an override can make the watchdog stricter but can never relax or disable
61
+ * it. That keeps this unable to manufacture the zombie run the watchdog exists
62
+ * to prevent, even if a customer sets it in their own workflow env.
63
+ */
64
+ export declare function watchdogBudgetMs(compiled: number, envVar: string): number;
53
65
  export declare const ACTIVITY_NOISE_PATTERNS: readonly RegExp[];
54
66
  export declare function isActivityNoise(chunk: string | Uint8Array): boolean;
55
67
  type ActivityTimeoutContext = {
@@ -7,7 +7,13 @@
7
7
  * provider outage rewrite a working account's configuration.
8
8
  */
9
9
  export type CredentialVerdict = "alive" | "dead" | "unknown";
10
- /** whether asking the provider about this credential would tell us anything. */
10
+ /**
11
+ * Whether asking the provider about this credential would tell us anything —
12
+ * the paste-time gate, where the caller stores a secret and cannot see the run
13
+ * env. A `hostConfigurable` credential is excluded here and stays probeable at
14
+ * run time, because only the runner knows whether a gateway is in play: the
15
+ * base URL usually lives in the workflow file, which the console never reads.
16
+ */
11
17
  export declare function isCredentialProbeable(envVar: string): boolean;
12
18
  /**
13
19
  * Ask the provider whether it still accepts this credential. Used at the two
@@ -10,7 +10,7 @@ import type { ToolContext } from "../mcp/server.ts";
10
10
  * don't parse the audit-only `payload`.
11
11
  * Keep in sync with `STRING_FIELDS` in `app/api/workflow-run/[runId]/route.ts`.
12
12
  */
13
- declare const STRING_KEYS: readonly ["prNodeId", "issueNodeId", "reviewNodeId", "planCommentNodeId", "summarySnapshot", "model"];
13
+ declare const STRING_KEYS: readonly ["prNodeId", "issueNodeId", "reviewNodeId", "planCommentNodeId", "summarySnapshot", "model", "agent", "credential"];
14
14
  /**
15
15
  * Number-valued usage fields — aggregated across all agent calls and PATCHed
16
16
  * once at end-of-run. Token counts are Int4 on the DB side (ample for any
@@ -28,6 +28,7 @@ export declare const JsonPayload: import("arktype/internal/variants/object.ts").
28
28
  id: string;
29
29
  } | undefined;
30
30
  generateSummary?: boolean | undefined;
31
+ codexArm?: boolean | undefined;
31
32
  }, {}>;
32
33
  export declare const Inputs: import("arktype/internal/variants/object.ts").ObjectType<{
33
34
  prompt?: string | undefined;
@@ -75,6 +76,7 @@ export declare function resolvePayload(resolvedPromptInput: ResolvedPromptInput,
75
76
  id: string;
76
77
  } | undefined;
77
78
  generateSummary: boolean | undefined;
79
+ codexArm: boolean | undefined;
78
80
  push: import("../external.ts").PushPermission;
79
81
  shell: import("../external.ts").ShellPermission;
80
82
  runStatusCheck: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",