pullfrog 0.1.19 → 0.1.20

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/cli.mjs CHANGED
@@ -101235,6 +101235,111 @@ function formatMcpToolRef(agentId, toolName) {
101235
101235
 
101236
101236
  // utils/activity.ts
101237
101237
  import { performance as performance2 } from "node:perf_hooks";
101238
+ function isMonitorDebugEnabled() {
101239
+ return process.env.ACTIONS_STEP_DEBUG === "true" || process.env.RUNNER_DEBUG === "1" || process.env.LOG_LEVEL === "debug";
101240
+ }
101241
+ var DEFAULT_ACTIVITY_TIMEOUT_MS = 3e5;
101242
+ var AGENT_ACTIVITY_TIMEOUT_MS = 9e5;
101243
+ var DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5e3;
101244
+ var DEBUG_TS_PREFIX = /^(?:\[\d{4}-\d{2}-\d{2}T[^\]]+\]\s+)?/.source;
101245
+ var ACTIVITY_NOISE_PATTERNS = [
101246
+ new RegExp(`${DEBUG_TS_PREFIX}\\[mcp-proxy\\]`),
101247
+ new RegExp(`${DEBUG_TS_PREFIX}\xBB provider error detected`),
101248
+ new RegExp(`${DEBUG_TS_PREFIX}\\[DEBUG\\]\\s+(?:spawn|process) activity `),
101249
+ /^::debug::(?:spawn|process) activity /
101250
+ ];
101251
+ function isActivityNoise(chunk) {
101252
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
101253
+ if (!text.trim()) return true;
101254
+ return text.split("\n").every((line) => {
101255
+ const trimmed = line.trim();
101256
+ if (!trimmed) return true;
101257
+ return ACTIVITY_NOISE_PATTERNS.some((pattern) => pattern.test(trimmed));
101258
+ });
101259
+ }
101260
+ var _lastActivity = performance2.now();
101261
+ function markActivity() {
101262
+ _lastActivity = performance2.now();
101263
+ }
101264
+ function getIdleMs() {
101265
+ return Math.round(performance2.now() - _lastActivity);
101266
+ }
101267
+ function wrapWrite(original, onActivity) {
101268
+ const wrapped = (chunk, encodingOrCb, cb) => {
101269
+ if (!isActivityNoise(chunk)) {
101270
+ onActivity();
101271
+ }
101272
+ if (typeof encodingOrCb === "function") {
101273
+ return original(chunk, encodingOrCb);
101274
+ }
101275
+ return original(chunk, encodingOrCb, cb);
101276
+ };
101277
+ return wrapped;
101278
+ }
101279
+ function startProcessOutputMonitor(ctx) {
101280
+ let timedOut = false;
101281
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
101282
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
101283
+ process.stdout.write = wrapWrite(originalStdoutWrite, markActivity);
101284
+ process.stderr.write = wrapWrite(originalStderrWrite, markActivity);
101285
+ const debugBypass = (msg) => {
101286
+ if (!isMonitorDebugEnabled()) return;
101287
+ originalStdoutWrite(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] ${msg}
101288
+ `);
101289
+ };
101290
+ debugBypass(`process activity monitor started: timeout=${ctx.timeoutMs}ms`);
101291
+ const intervalId = setInterval(() => {
101292
+ const idleMs = getIdleMs();
101293
+ debugBypass(`process activity check: idle=${idleMs}ms / ${ctx.timeoutMs}ms`);
101294
+ if (timedOut || idleMs <= ctx.timeoutMs) return;
101295
+ timedOut = true;
101296
+ ctx.onTimeout(idleMs);
101297
+ }, ctx.checkIntervalMs);
101298
+ function stop() {
101299
+ clearInterval(intervalId);
101300
+ process.stdout.write = originalStdoutWrite;
101301
+ process.stderr.write = originalStderrWrite;
101302
+ }
101303
+ return { stop };
101304
+ }
101305
+ function createProcessOutputActivityTimeout(ctx) {
101306
+ markActivity();
101307
+ let rejectFn = null;
101308
+ const promise2 = new Promise((_2, reject) => {
101309
+ rejectFn = reject;
101310
+ });
101311
+ let monitor = null;
101312
+ monitor = startProcessOutputMonitor({
101313
+ timeoutMs: ctx.timeoutMs,
101314
+ checkIntervalMs: ctx.checkIntervalMs,
101315
+ onTimeout: (idleMs) => {
101316
+ if (!rejectFn) return;
101317
+ const idleSec = Math.round(idleMs / 1e3);
101318
+ if (monitor) {
101319
+ monitor.stop();
101320
+ }
101321
+ const reject = rejectFn;
101322
+ rejectFn = null;
101323
+ reject(new Error(`activity timeout: no output for ${idleSec}s`));
101324
+ }
101325
+ });
101326
+ return {
101327
+ promise: promise2,
101328
+ // stop() also disarms forceReject so a late safety-net fire can't reject
101329
+ // the promise after the run has already succeeded.
101330
+ stop: () => {
101331
+ monitor?.stop();
101332
+ rejectFn = null;
101333
+ },
101334
+ forceReject: (reason) => {
101335
+ if (!rejectFn) return;
101336
+ monitor?.stop();
101337
+ const reject = rejectFn;
101338
+ rejectFn = null;
101339
+ reject(new Error(reason));
101340
+ }
101341
+ };
101342
+ }
101238
101343
 
101239
101344
  // utils/log.ts
101240
101345
  var core = __toESM(require_core(), 1);
@@ -101550,137 +101655,6 @@ function formatUsageSummary(entries) {
101550
101655
  ].join("\n");
101551
101656
  }
101552
101657
 
101553
- // utils/activity.ts
101554
- function isMonitorDebugEnabled() {
101555
- return process.env.ACTIONS_STEP_DEBUG === "true" || process.env.RUNNER_DEBUG === "1" || process.env.LOG_LEVEL === "debug";
101556
- }
101557
- var DEFAULT_ACTIVITY_TIMEOUT_MS = 3e5;
101558
- var DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5e3;
101559
- var DEBUG_TS_PREFIX = /^(?:\[\d{4}-\d{2}-\d{2}T[^\]]+\]\s+)?/.source;
101560
- var ACTIVITY_NOISE_PATTERNS = [
101561
- new RegExp(`${DEBUG_TS_PREFIX}\\[mcp-proxy\\]`),
101562
- new RegExp(`${DEBUG_TS_PREFIX}\xBB provider error detected`),
101563
- new RegExp(`${DEBUG_TS_PREFIX}\\[DEBUG\\]\\s+(?:spawn|process) activity `),
101564
- /^::debug::(?:spawn|process) activity /
101565
- ];
101566
- function isActivityNoise(chunk) {
101567
- const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
101568
- if (!text.trim()) return true;
101569
- return text.split("\n").every((line) => {
101570
- const trimmed = line.trim();
101571
- if (!trimmed) return true;
101572
- return ACTIVITY_NOISE_PATTERNS.some((pattern) => pattern.test(trimmed));
101573
- });
101574
- }
101575
- var _lastActivity = performance2.now();
101576
- var MAX_TOOL_CALL_SUSPENSION_MS = 15 * 60 * 1e3;
101577
- var _suspendedAt = null;
101578
- var _suspensionTimer = null;
101579
- function markActivity() {
101580
- _lastActivity = performance2.now();
101581
- }
101582
- function getIdleMs() {
101583
- if (_suspendedAt !== null) return 0;
101584
- return Math.round(performance2.now() - _lastActivity);
101585
- }
101586
- function suspendActivity(maxMs = MAX_TOOL_CALL_SUSPENSION_MS) {
101587
- if (_suspendedAt !== null) return;
101588
- _suspendedAt = performance2.now();
101589
- _suspensionTimer = setTimeout(() => {
101590
- log.warning(`activity watchdog suspended >${Math.round(maxMs / 1e3)}s \u2014 auto-resuming`);
101591
- resumeActivity();
101592
- }, maxMs);
101593
- _suspensionTimer.unref?.();
101594
- }
101595
- function resumeActivity() {
101596
- if (_suspendedAt === null) return;
101597
- _suspendedAt = null;
101598
- if (_suspensionTimer) {
101599
- clearTimeout(_suspensionTimer);
101600
- _suspensionTimer = null;
101601
- }
101602
- _lastActivity = performance2.now();
101603
- }
101604
- function isActivitySuspended() {
101605
- return _suspendedAt !== null;
101606
- }
101607
- function wrapWrite(original, onActivity) {
101608
- const wrapped = (chunk, encodingOrCb, cb) => {
101609
- if (!isActivityNoise(chunk)) {
101610
- onActivity();
101611
- }
101612
- if (typeof encodingOrCb === "function") {
101613
- return original(chunk, encodingOrCb);
101614
- }
101615
- return original(chunk, encodingOrCb, cb);
101616
- };
101617
- return wrapped;
101618
- }
101619
- function startProcessOutputMonitor(ctx) {
101620
- let timedOut = false;
101621
- const originalStdoutWrite = process.stdout.write.bind(process.stdout);
101622
- const originalStderrWrite = process.stderr.write.bind(process.stderr);
101623
- process.stdout.write = wrapWrite(originalStdoutWrite, markActivity);
101624
- process.stderr.write = wrapWrite(originalStderrWrite, markActivity);
101625
- const debugBypass = (msg) => {
101626
- if (!isMonitorDebugEnabled()) return;
101627
- originalStdoutWrite(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] ${msg}
101628
- `);
101629
- };
101630
- debugBypass(`process activity monitor started: timeout=${ctx.timeoutMs}ms`);
101631
- const intervalId = setInterval(() => {
101632
- const idleMs = getIdleMs();
101633
- debugBypass(`process activity check: idle=${idleMs}ms / ${ctx.timeoutMs}ms`);
101634
- if (timedOut || idleMs <= ctx.timeoutMs) return;
101635
- timedOut = true;
101636
- ctx.onTimeout(idleMs);
101637
- }, ctx.checkIntervalMs);
101638
- function stop() {
101639
- clearInterval(intervalId);
101640
- process.stdout.write = originalStdoutWrite;
101641
- process.stderr.write = originalStderrWrite;
101642
- }
101643
- return { stop };
101644
- }
101645
- function createProcessOutputActivityTimeout(ctx) {
101646
- markActivity();
101647
- let rejectFn = null;
101648
- const promise2 = new Promise((_2, reject) => {
101649
- rejectFn = reject;
101650
- });
101651
- let monitor = null;
101652
- monitor = startProcessOutputMonitor({
101653
- timeoutMs: ctx.timeoutMs,
101654
- checkIntervalMs: ctx.checkIntervalMs,
101655
- onTimeout: (idleMs) => {
101656
- if (!rejectFn) return;
101657
- const idleSec = Math.round(idleMs / 1e3);
101658
- if (monitor) {
101659
- monitor.stop();
101660
- }
101661
- const reject = rejectFn;
101662
- rejectFn = null;
101663
- reject(new Error(`activity timeout: no output for ${idleSec}s`));
101664
- }
101665
- });
101666
- return {
101667
- promise: promise2,
101668
- // stop() also disarms forceReject so a late safety-net fire can't reject
101669
- // the promise after the run has already succeeded.
101670
- stop: () => {
101671
- monitor?.stop();
101672
- rejectFn = null;
101673
- },
101674
- forceReject: (reason) => {
101675
- if (!rejectFn) return;
101676
- monitor?.stop();
101677
- const reject = rejectFn;
101678
- rejectFn = null;
101679
- reject(new Error(reason));
101680
- }
101681
- };
101682
- }
101683
-
101684
101658
  // utils/install.ts
101685
101659
  import { spawnSync } from "node:child_process";
101686
101660
  import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync } from "node:fs";
@@ -101880,7 +101854,7 @@ var import_semver = __toESM(require_semver2(), 1);
101880
101854
  // package.json
101881
101855
  var package_default = {
101882
101856
  name: "pullfrog",
101883
- version: "0.1.19",
101857
+ version: "0.1.20",
101884
101858
  type: "module",
101885
101859
  bin: {
101886
101860
  pullfrog: "dist/cli.mjs",
@@ -102218,11 +102192,6 @@ async function spawn3(options) {
102218
102192
  `spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
102219
102193
  );
102220
102194
  activityCheckIntervalId = setInterval(() => {
102221
- if (options.isPausedExternally?.()) {
102222
- lastActivityTime = performance3.now();
102223
- log.debug(`spawn activity check: pid=${child.pid} paused externally`);
102224
- return;
102225
- }
102226
102195
  const idleMs = performance3.now() - lastActivityTime;
102227
102196
  log.debug(
102228
102197
  `spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
@@ -103690,7 +103659,6 @@ async function runClaude(params) {
103690
103659
  }
103691
103660
  } else if (block.type === "tool_use") {
103692
103661
  const toolName = block.name || "unknown";
103693
- suspendActivity();
103694
103662
  if (params.onToolUse) {
103695
103663
  params.onToolUse({
103696
103664
  toolName,
@@ -103735,7 +103703,6 @@ async function runClaude(params) {
103735
103703
  for (const block of content) {
103736
103704
  if (typeof block === "string") continue;
103737
103705
  if (block.type === "tool_result") {
103738
- resumeActivity();
103739
103706
  timerFor(label).markToolResult();
103740
103707
  const outputContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
103741
103708
  (entry) => typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && "text" in entry ? String(entry.text) : JSON.stringify(entry)
@@ -103822,9 +103789,10 @@ async function runClaude(params) {
103822
103789
  args: params.args,
103823
103790
  cwd: params.cwd,
103824
103791
  env: params.env,
103825
- activityTimeout: 3e5,
103792
+ // flat agent idle budget — long synchronous MCP tool calls (issue #760)
103793
+ // sit well under it, so no per-toolcall suspend bracketing is needed.
103794
+ activityTimeout: AGENT_ACTIVITY_TIMEOUT_MS,
103826
103795
  onActivityTimeout: params.onActivityTimeout,
103827
- isPausedExternally: isActivitySuspended,
103828
103796
  stdio: ["ignore", "pipe", "pipe"],
103829
103797
  // run claude in its own process group so SIGKILL on activity timeout /
103830
103798
  // outer cancellation reaches any subprocesses it spawns (rg, file
@@ -108179,13 +108147,6 @@ async function onToolPart(ctx, part, label, isOrchestrator) {
108179
108147
  const status = part.state.status;
108180
108148
  const toolName = part.tool;
108181
108149
  const toolId = part.callID;
108182
- if (toolName !== "task") {
108183
- if (status === "completed" || status === "error") {
108184
- resumeActivity();
108185
- } else {
108186
- suspendActivity();
108187
- }
108188
- }
108189
108150
  if (toolName === "task" && status === "running" && isOrchestrator && !ctx.taskDispatchByCallID.has(toolId)) {
108190
108151
  const input = part.state.input ?? {};
108191
108152
  const dispatched = ctx.labeler.recordTaskDispatch(input);
@@ -108433,7 +108394,6 @@ function startInnerActivityWatchdog(params) {
108433
108394
  let fired = false;
108434
108395
  const id = setInterval(() => {
108435
108396
  if (fired) return;
108436
- if (isActivitySuspended()) return;
108437
108397
  const idleMs = performance6.now() - params.ctx.lastEventAt;
108438
108398
  if (idleMs <= params.timeoutMs) return;
108439
108399
  fired = true;
@@ -108589,13 +108549,13 @@ var opencode = agent({
108589
108549
  const watchdog = startInnerActivityWatchdog({
108590
108550
  ctx: runnerCtx,
108591
108551
  // model-stall budget: how long the orchestrator may stream NO progress
108592
- // (no token/tool part.updated) before we tear the turn down. tool
108593
- // execution is excluded via suspend/resumeActivity, so this only bounds
108594
- // a genuinely silent provider. 120s comfortably exceeds normal inter-
108595
- // token gaps (incl. extended-thinking pauses) while catching the
108596
- // observed stalls (model goes quiet ~3-4min in) far sooner than the old
108597
- // 300s undici cap that this fix removes.
108598
- timeoutMs: 12e4,
108552
+ // (no token/tool part.updated) before we tear the turn down. opencode's
108553
+ // keepalive/lifecycle events keep the outer process-output monitor
108554
+ // alive even while the model is silent, so this inner timer is the only
108555
+ // stall detector for the v2 SSE path. it shares the flat idle budget so
108556
+ // a long synchronous tool call (no part.updated while it runs) can't
108557
+ // false-positive it.
108558
+ timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
108599
108559
  abortController
108600
108560
  });
108601
108561
  const sdkModel = parseModel2(model);
@@ -161974,7 +161934,7 @@ ${instructions.user}` : null,
161974
161934
  }
161975
161935
  }
161976
161936
  activityTimeout = createProcessOutputActivityTimeout({
161977
- timeoutMs: DEFAULT_ACTIVITY_TIMEOUT_MS,
161937
+ timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
161978
161938
  checkIntervalMs: DEFAULT_ACTIVITY_CHECK_INTERVAL_MS
161979
161939
  });
161980
161940
  activityTimeout.promise.catch(() => {
@@ -163019,7 +162979,7 @@ async function run2() {
163019
162979
  }
163020
162980
 
163021
162981
  // cli.ts
163022
- var VERSION10 = "0.1.19";
162982
+ var VERSION10 = "0.1.20";
163023
162983
  var bin = basename2(process.argv[1] || "");
163024
162984
  var PROG = bin === "pf" || bin === "pullfrog" ? bin : "pullfrog";
163025
162985
  var rawArgs = process.argv.slice(2);
package/dist/index.js CHANGED
@@ -99435,6 +99435,111 @@ function formatMcpToolRef(agentId, toolName) {
99435
99435
 
99436
99436
  // utils/activity.ts
99437
99437
  import { performance as performance2 } from "node:perf_hooks";
99438
+ function isMonitorDebugEnabled() {
99439
+ return process.env.ACTIONS_STEP_DEBUG === "true" || process.env.RUNNER_DEBUG === "1" || process.env.LOG_LEVEL === "debug";
99440
+ }
99441
+ var DEFAULT_ACTIVITY_TIMEOUT_MS = 3e5;
99442
+ var AGENT_ACTIVITY_TIMEOUT_MS = 9e5;
99443
+ var DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5e3;
99444
+ var DEBUG_TS_PREFIX = /^(?:\[\d{4}-\d{2}-\d{2}T[^\]]+\]\s+)?/.source;
99445
+ var ACTIVITY_NOISE_PATTERNS = [
99446
+ new RegExp(`${DEBUG_TS_PREFIX}\\[mcp-proxy\\]`),
99447
+ new RegExp(`${DEBUG_TS_PREFIX}\xBB provider error detected`),
99448
+ new RegExp(`${DEBUG_TS_PREFIX}\\[DEBUG\\]\\s+(?:spawn|process) activity `),
99449
+ /^::debug::(?:spawn|process) activity /
99450
+ ];
99451
+ function isActivityNoise(chunk) {
99452
+ const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
99453
+ if (!text.trim()) return true;
99454
+ return text.split("\n").every((line) => {
99455
+ const trimmed = line.trim();
99456
+ if (!trimmed) return true;
99457
+ return ACTIVITY_NOISE_PATTERNS.some((pattern) => pattern.test(trimmed));
99458
+ });
99459
+ }
99460
+ var _lastActivity = performance2.now();
99461
+ function markActivity() {
99462
+ _lastActivity = performance2.now();
99463
+ }
99464
+ function getIdleMs() {
99465
+ return Math.round(performance2.now() - _lastActivity);
99466
+ }
99467
+ function wrapWrite(original, onActivity) {
99468
+ const wrapped = (chunk, encodingOrCb, cb) => {
99469
+ if (!isActivityNoise(chunk)) {
99470
+ onActivity();
99471
+ }
99472
+ if (typeof encodingOrCb === "function") {
99473
+ return original(chunk, encodingOrCb);
99474
+ }
99475
+ return original(chunk, encodingOrCb, cb);
99476
+ };
99477
+ return wrapped;
99478
+ }
99479
+ function startProcessOutputMonitor(ctx) {
99480
+ let timedOut = false;
99481
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
99482
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
99483
+ process.stdout.write = wrapWrite(originalStdoutWrite, markActivity);
99484
+ process.stderr.write = wrapWrite(originalStderrWrite, markActivity);
99485
+ const debugBypass = (msg) => {
99486
+ if (!isMonitorDebugEnabled()) return;
99487
+ originalStdoutWrite(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] ${msg}
99488
+ `);
99489
+ };
99490
+ debugBypass(`process activity monitor started: timeout=${ctx.timeoutMs}ms`);
99491
+ const intervalId = setInterval(() => {
99492
+ const idleMs = getIdleMs();
99493
+ debugBypass(`process activity check: idle=${idleMs}ms / ${ctx.timeoutMs}ms`);
99494
+ if (timedOut || idleMs <= ctx.timeoutMs) return;
99495
+ timedOut = true;
99496
+ ctx.onTimeout(idleMs);
99497
+ }, ctx.checkIntervalMs);
99498
+ function stop() {
99499
+ clearInterval(intervalId);
99500
+ process.stdout.write = originalStdoutWrite;
99501
+ process.stderr.write = originalStderrWrite;
99502
+ }
99503
+ return { stop };
99504
+ }
99505
+ function createProcessOutputActivityTimeout(ctx) {
99506
+ markActivity();
99507
+ let rejectFn = null;
99508
+ const promise2 = new Promise((_, reject) => {
99509
+ rejectFn = reject;
99510
+ });
99511
+ let monitor = null;
99512
+ monitor = startProcessOutputMonitor({
99513
+ timeoutMs: ctx.timeoutMs,
99514
+ checkIntervalMs: ctx.checkIntervalMs,
99515
+ onTimeout: (idleMs) => {
99516
+ if (!rejectFn) return;
99517
+ const idleSec = Math.round(idleMs / 1e3);
99518
+ if (monitor) {
99519
+ monitor.stop();
99520
+ }
99521
+ const reject = rejectFn;
99522
+ rejectFn = null;
99523
+ reject(new Error(`activity timeout: no output for ${idleSec}s`));
99524
+ }
99525
+ });
99526
+ return {
99527
+ promise: promise2,
99528
+ // stop() also disarms forceReject so a late safety-net fire can't reject
99529
+ // the promise after the run has already succeeded.
99530
+ stop: () => {
99531
+ monitor?.stop();
99532
+ rejectFn = null;
99533
+ },
99534
+ forceReject: (reason) => {
99535
+ if (!rejectFn) return;
99536
+ monitor?.stop();
99537
+ const reject = rejectFn;
99538
+ rejectFn = null;
99539
+ reject(new Error(reason));
99540
+ }
99541
+ };
99542
+ }
99438
99543
 
99439
99544
  // utils/log.ts
99440
99545
  var core = __toESM(require_core(), 1);
@@ -99750,137 +99855,6 @@ function formatUsageSummary(entries) {
99750
99855
  ].join("\n");
99751
99856
  }
99752
99857
 
99753
- // utils/activity.ts
99754
- function isMonitorDebugEnabled() {
99755
- return process.env.ACTIONS_STEP_DEBUG === "true" || process.env.RUNNER_DEBUG === "1" || process.env.LOG_LEVEL === "debug";
99756
- }
99757
- var DEFAULT_ACTIVITY_TIMEOUT_MS = 3e5;
99758
- var DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5e3;
99759
- var DEBUG_TS_PREFIX = /^(?:\[\d{4}-\d{2}-\d{2}T[^\]]+\]\s+)?/.source;
99760
- var ACTIVITY_NOISE_PATTERNS = [
99761
- new RegExp(`${DEBUG_TS_PREFIX}\\[mcp-proxy\\]`),
99762
- new RegExp(`${DEBUG_TS_PREFIX}\xBB provider error detected`),
99763
- new RegExp(`${DEBUG_TS_PREFIX}\\[DEBUG\\]\\s+(?:spawn|process) activity `),
99764
- /^::debug::(?:spawn|process) activity /
99765
- ];
99766
- function isActivityNoise(chunk) {
99767
- const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
99768
- if (!text.trim()) return true;
99769
- return text.split("\n").every((line) => {
99770
- const trimmed = line.trim();
99771
- if (!trimmed) return true;
99772
- return ACTIVITY_NOISE_PATTERNS.some((pattern) => pattern.test(trimmed));
99773
- });
99774
- }
99775
- var _lastActivity = performance2.now();
99776
- var MAX_TOOL_CALL_SUSPENSION_MS = 15 * 60 * 1e3;
99777
- var _suspendedAt = null;
99778
- var _suspensionTimer = null;
99779
- function markActivity() {
99780
- _lastActivity = performance2.now();
99781
- }
99782
- function getIdleMs() {
99783
- if (_suspendedAt !== null) return 0;
99784
- return Math.round(performance2.now() - _lastActivity);
99785
- }
99786
- function suspendActivity(maxMs = MAX_TOOL_CALL_SUSPENSION_MS) {
99787
- if (_suspendedAt !== null) return;
99788
- _suspendedAt = performance2.now();
99789
- _suspensionTimer = setTimeout(() => {
99790
- log.warning(`activity watchdog suspended >${Math.round(maxMs / 1e3)}s \u2014 auto-resuming`);
99791
- resumeActivity();
99792
- }, maxMs);
99793
- _suspensionTimer.unref?.();
99794
- }
99795
- function resumeActivity() {
99796
- if (_suspendedAt === null) return;
99797
- _suspendedAt = null;
99798
- if (_suspensionTimer) {
99799
- clearTimeout(_suspensionTimer);
99800
- _suspensionTimer = null;
99801
- }
99802
- _lastActivity = performance2.now();
99803
- }
99804
- function isActivitySuspended() {
99805
- return _suspendedAt !== null;
99806
- }
99807
- function wrapWrite(original, onActivity) {
99808
- const wrapped = (chunk, encodingOrCb, cb) => {
99809
- if (!isActivityNoise(chunk)) {
99810
- onActivity();
99811
- }
99812
- if (typeof encodingOrCb === "function") {
99813
- return original(chunk, encodingOrCb);
99814
- }
99815
- return original(chunk, encodingOrCb, cb);
99816
- };
99817
- return wrapped;
99818
- }
99819
- function startProcessOutputMonitor(ctx) {
99820
- let timedOut = false;
99821
- const originalStdoutWrite = process.stdout.write.bind(process.stdout);
99822
- const originalStderrWrite = process.stderr.write.bind(process.stderr);
99823
- process.stdout.write = wrapWrite(originalStdoutWrite, markActivity);
99824
- process.stderr.write = wrapWrite(originalStderrWrite, markActivity);
99825
- const debugBypass = (msg) => {
99826
- if (!isMonitorDebugEnabled()) return;
99827
- originalStdoutWrite(`[${(/* @__PURE__ */ new Date()).toISOString()}] [DEBUG] ${msg}
99828
- `);
99829
- };
99830
- debugBypass(`process activity monitor started: timeout=${ctx.timeoutMs}ms`);
99831
- const intervalId = setInterval(() => {
99832
- const idleMs = getIdleMs();
99833
- debugBypass(`process activity check: idle=${idleMs}ms / ${ctx.timeoutMs}ms`);
99834
- if (timedOut || idleMs <= ctx.timeoutMs) return;
99835
- timedOut = true;
99836
- ctx.onTimeout(idleMs);
99837
- }, ctx.checkIntervalMs);
99838
- function stop() {
99839
- clearInterval(intervalId);
99840
- process.stdout.write = originalStdoutWrite;
99841
- process.stderr.write = originalStderrWrite;
99842
- }
99843
- return { stop };
99844
- }
99845
- function createProcessOutputActivityTimeout(ctx) {
99846
- markActivity();
99847
- let rejectFn = null;
99848
- const promise2 = new Promise((_, reject) => {
99849
- rejectFn = reject;
99850
- });
99851
- let monitor = null;
99852
- monitor = startProcessOutputMonitor({
99853
- timeoutMs: ctx.timeoutMs,
99854
- checkIntervalMs: ctx.checkIntervalMs,
99855
- onTimeout: (idleMs) => {
99856
- if (!rejectFn) return;
99857
- const idleSec = Math.round(idleMs / 1e3);
99858
- if (monitor) {
99859
- monitor.stop();
99860
- }
99861
- const reject = rejectFn;
99862
- rejectFn = null;
99863
- reject(new Error(`activity timeout: no output for ${idleSec}s`));
99864
- }
99865
- });
99866
- return {
99867
- promise: promise2,
99868
- // stop() also disarms forceReject so a late safety-net fire can't reject
99869
- // the promise after the run has already succeeded.
99870
- stop: () => {
99871
- monitor?.stop();
99872
- rejectFn = null;
99873
- },
99874
- forceReject: (reason) => {
99875
- if (!rejectFn) return;
99876
- monitor?.stop();
99877
- const reject = rejectFn;
99878
- rejectFn = null;
99879
- reject(new Error(reason));
99880
- }
99881
- };
99882
- }
99883
-
99884
99858
  // utils/install.ts
99885
99859
  import { spawnSync } from "node:child_process";
99886
99860
  import { chmodSync, createWriteStream, existsSync as existsSync2, mkdirSync } from "node:fs";
@@ -100080,7 +100054,7 @@ var import_semver = __toESM(require_semver2(), 1);
100080
100054
  // package.json
100081
100055
  var package_default = {
100082
100056
  name: "pullfrog",
100083
- version: "0.1.19",
100057
+ version: "0.1.20",
100084
100058
  type: "module",
100085
100059
  bin: {
100086
100060
  pullfrog: "dist/cli.mjs",
@@ -100418,11 +100392,6 @@ async function spawn(options) {
100418
100392
  `spawn activity timer: pid=${child.pid} cmd=${options.cmd} timeout=${activityTimeoutMs}ms`
100419
100393
  );
100420
100394
  activityCheckIntervalId = setInterval(() => {
100421
- if (options.isPausedExternally?.()) {
100422
- lastActivityTime = performance3.now();
100423
- log.debug(`spawn activity check: pid=${child.pid} paused externally`);
100424
- return;
100425
- }
100426
100395
  const idleMs = performance3.now() - lastActivityTime;
100427
100396
  log.debug(
100428
100397
  `spawn activity check: pid=${child.pid} idle=${Math.round(idleMs)}ms / ${activityTimeoutMs}ms`
@@ -101890,7 +101859,6 @@ async function runClaude(params) {
101890
101859
  }
101891
101860
  } else if (block.type === "tool_use") {
101892
101861
  const toolName = block.name || "unknown";
101893
- suspendActivity();
101894
101862
  if (params.onToolUse) {
101895
101863
  params.onToolUse({
101896
101864
  toolName,
@@ -101935,7 +101903,6 @@ async function runClaude(params) {
101935
101903
  for (const block of content) {
101936
101904
  if (typeof block === "string") continue;
101937
101905
  if (block.type === "tool_result") {
101938
- resumeActivity();
101939
101906
  timerFor(label).markToolResult();
101940
101907
  const outputContent = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map(
101941
101908
  (entry) => typeof entry === "string" ? entry : typeof entry === "object" && entry !== null && "text" in entry ? String(entry.text) : JSON.stringify(entry)
@@ -102022,9 +101989,10 @@ async function runClaude(params) {
102022
101989
  args: params.args,
102023
101990
  cwd: params.cwd,
102024
101991
  env: params.env,
102025
- activityTimeout: 3e5,
101992
+ // flat agent idle budget — long synchronous MCP tool calls (issue #760)
101993
+ // sit well under it, so no per-toolcall suspend bracketing is needed.
101994
+ activityTimeout: AGENT_ACTIVITY_TIMEOUT_MS,
102026
101995
  onActivityTimeout: params.onActivityTimeout,
102027
- isPausedExternally: isActivitySuspended,
102028
101996
  stdio: ["ignore", "pipe", "pipe"],
102029
101997
  // run claude in its own process group so SIGKILL on activity timeout /
102030
101998
  // outer cancellation reaches any subprocesses it spawns (rg, file
@@ -106421,13 +106389,6 @@ async function onToolPart(ctx, part, label, isOrchestrator) {
106421
106389
  const status = part.state.status;
106422
106390
  const toolName = part.tool;
106423
106391
  const toolId = part.callID;
106424
- if (toolName !== "task") {
106425
- if (status === "completed" || status === "error") {
106426
- resumeActivity();
106427
- } else {
106428
- suspendActivity();
106429
- }
106430
- }
106431
106392
  if (toolName === "task" && status === "running" && isOrchestrator && !ctx.taskDispatchByCallID.has(toolId)) {
106432
106393
  const input = part.state.input ?? {};
106433
106394
  const dispatched = ctx.labeler.recordTaskDispatch(input);
@@ -106675,7 +106636,6 @@ function startInnerActivityWatchdog(params) {
106675
106636
  let fired = false;
106676
106637
  const id = setInterval(() => {
106677
106638
  if (fired) return;
106678
- if (isActivitySuspended()) return;
106679
106639
  const idleMs = performance6.now() - params.ctx.lastEventAt;
106680
106640
  if (idleMs <= params.timeoutMs) return;
106681
106641
  fired = true;
@@ -106831,13 +106791,13 @@ var opencode = agent({
106831
106791
  const watchdog = startInnerActivityWatchdog({
106832
106792
  ctx: runnerCtx,
106833
106793
  // model-stall budget: how long the orchestrator may stream NO progress
106834
- // (no token/tool part.updated) before we tear the turn down. tool
106835
- // execution is excluded via suspend/resumeActivity, so this only bounds
106836
- // a genuinely silent provider. 120s comfortably exceeds normal inter-
106837
- // token gaps (incl. extended-thinking pauses) while catching the
106838
- // observed stalls (model goes quiet ~3-4min in) far sooner than the old
106839
- // 300s undici cap that this fix removes.
106840
- timeoutMs: 12e4,
106794
+ // (no token/tool part.updated) before we tear the turn down. opencode's
106795
+ // keepalive/lifecycle events keep the outer process-output monitor
106796
+ // alive even while the model is silent, so this inner timer is the only
106797
+ // stall detector for the v2 SSE path. it shares the flat idle budget so
106798
+ // a long synchronous tool call (no part.updated while it runs) can't
106799
+ // false-positive it.
106800
+ timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
106841
106801
  abortController
106842
106802
  });
106843
106803
  const sdkModel = parseModel2(model);
@@ -160216,7 +160176,7 @@ ${instructions.user}` : null,
160216
160176
  }
160217
160177
  }
160218
160178
  activityTimeout = createProcessOutputActivityTimeout({
160219
- timeoutMs: DEFAULT_ACTIVITY_TIMEOUT_MS,
160179
+ timeoutMs: AGENT_ACTIVITY_TIMEOUT_MS,
160220
160180
  checkIntervalMs: DEFAULT_ACTIVITY_CHECK_INTERVAL_MS
160221
160181
  });
160222
160182
  activityTimeout.promise.catch(() => {
@@ -1,4 +1,21 @@
1
+ /**
2
+ * generic `spawn()` idle default for ordinary short-lived subprocesses (prep
3
+ * probes, package-manager invocations, dependency installs). these should be
4
+ * producing output steadily, so a comparatively tight budget catches a wedged
5
+ * command promptly. the long-silent-tool tolerance the agent harnesses need
6
+ * lives in AGENT_ACTIVITY_TIMEOUT_MS, applied explicitly at those sites.
7
+ */
1
8
  export declare const DEFAULT_ACTIVITY_TIMEOUT_MS = 300000;
9
+ /**
10
+ * flat idle budget for the agent activity watchdog (the outer process-output
11
+ * monitor, the v1 harness spawns, and the v2 inner event-silence watchdog).
12
+ * sized to exceed the worst-case legitimate silent tool window (issue #760:
13
+ * `checkout_pr` git fetch+deepen on a large monorepo, ~4-5min) with generous
14
+ * headroom, so no single in-flight tool call can be mistaken for a stall. a
15
+ * timeout this generous needs no suspend/resume bracketing — the cost of a
16
+ * genuinely hung run is only GitHub Actions minutes, not tokens.
17
+ */
18
+ export declare const AGENT_ACTIVITY_TIMEOUT_MS = 900000;
2
19
  export declare const DEFAULT_ACTIVITY_CHECK_INTERVAL_MS = 5000;
3
20
  export declare const ACTIVITY_NOISE_PATTERNS: readonly RegExp[];
4
21
  export declare function isActivityNoise(chunk: string | Uint8Array): boolean;
@@ -12,44 +29,12 @@ export type ActivityTimeout = {
12
29
  /** force the timeout to reject immediately with a custom reason */
13
30
  forceReject: (reason: string) => void;
14
31
  };
15
- /**
16
- * upper bound on how long a single tool call can suspend the activity
17
- * watchdog. matched against the typical worst-case `checkout_pr`
18
- * fetch+deepen on a large monorepo (issue #760: 4-5min) plus generous
19
- * headroom for slower MCP tools, while still bounding the worst case if
20
- * a tool genuinely hangs and `tool_result` never arrives — auto-resume
21
- * fires here and the normal idle clock takes over from a fresh baseline.
22
- */
23
- export declare const MAX_TOOL_CALL_SUSPENSION_MS: number;
24
32
  /**
25
33
  * mark activity to reset the no-output timeout.
26
34
  * call this whenever the agent emits any event, even if it isn't logged to stdout.
27
35
  */
28
36
  export declare function markActivity(): void;
29
- /**
30
- * get the time since last activity in milliseconds.
31
- * returns 0 while the watchdog is suspended (issue #760).
32
- */
37
+ /** get the time since last activity in milliseconds. */
33
38
  export declare function getIdleMs(): number;
34
- /**
35
- * suspend the activity watchdog while a long-running, in-flight unit of
36
- * work is happening (e.g. an MCP `tools/call` that synchronously awaits
37
- * a multi-minute git fetch). bracket calls with `resumeActivity()` from
38
- * the agent harness's `tool_use` / `tool_result` event handlers.
39
- *
40
- * - idempotent: nested suspends are no-ops; the first resume wins.
41
- * - bounded: auto-resumes after `maxMs` so a buggy tool that never
42
- * produces a `tool_result` can't pin the watchdog open forever.
43
- * - safe: only the *agent harness* (claude.ts / opencode.ts) on explicit,
44
- * paired CLI events should call this. NEVER blanket-suspend on internal
45
- * noise — that would resurrect issue #12 zombie runs.
46
- */
47
- export declare function suspendActivity(maxMs?: number): void;
48
- /**
49
- * resume the activity watchdog. resets the idle baseline so a stale
50
- * idle window before the suspend can't immediately re-fire.
51
- */
52
- export declare function resumeActivity(): void;
53
- export declare function isActivitySuspended(): boolean;
54
39
  export declare function createProcessOutputActivityTimeout(ctx: ActivityTimeoutContext): ActivityTimeout;
55
40
  export {};
@@ -43,7 +43,6 @@ export interface SpawnOptions {
43
43
  timeout?: number;
44
44
  activityTimeout?: number;
45
45
  onActivityTimeout?: (() => void) | undefined;
46
- isPausedExternally?: () => boolean;
47
46
  cwd?: string;
48
47
  stdio?: ("pipe" | "ignore" | "inherit")[];
49
48
  onStdout?: (chunk: string) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pullfrog",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "pullfrog": "dist/cli.mjs",