@peakflo/agent-harness 0.2.11-rc.2 → 0.2.11-rc.4

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.
@@ -634,6 +634,18 @@ class SessionRunner {
634
634
  */
635
635
  approvalWaitMs = 0;
636
636
  approvalWaitStartedAt = null;
637
+ /**
638
+ * Total wall-clock this turn has spent with its browser call HELD because a
639
+ * person controls the chat browser (takeover). Excluded from the idle guard
640
+ * and added back onto the hard ceiling exactly like approval time, but it
641
+ * never opens an approval and the approval timeout never applies: a takeover
642
+ * lasts as long as the person needs (owner decision), and the turn's abort
643
+ * and hard ceiling (minus the pause) still end it.
644
+ */
645
+ browserPauseMs = 0;
646
+ browserPauseStartedAt = null;
647
+ /** When the last browser pause ended; the idle clock restarts from here. */
648
+ browserPauseEndedAt = null;
637
649
  constructor(deps) {
638
650
  this.deps = deps;
639
651
  this.logger = deps.logger ?? console;
@@ -650,6 +662,44 @@ class SessionRunner {
650
662
  isAwaitingApproval() {
651
663
  return this.openApproval !== null || this.openQuestionPartId !== null;
652
664
  }
665
+ /** True while a browser call is held because a person controls the browser. */
666
+ isBrowserPaused() {
667
+ return this.browserPauseStartedAt !== null;
668
+ }
669
+ /**
670
+ * Start or stop the browser-takeover pause clock. Called by the harness from
671
+ * the loopback MCP proxy's hold hook. Returns false when this runner has no
672
+ * live turn for `sessionId` — the caller should then cancel the hold rather
673
+ * than keep an agent call parked for a turn that is gone.
674
+ */
675
+ setBrowserPaused(sessionId, paused) {
676
+ if (paused) {
677
+ if (!this.turnContext || this.turnContext.sessionId !== sessionId) {
678
+ return false;
679
+ }
680
+ if (this.browserPauseStartedAt === null) {
681
+ this.browserPauseStartedAt = Date.now();
682
+ this.deps.onBrowserPauseChange?.(sessionId, true);
683
+ }
684
+ return true;
685
+ }
686
+ this.closeBrowserPause(sessionId);
687
+ return true;
688
+ }
689
+ closeBrowserPause(sessionId) {
690
+ if (this.browserPauseStartedAt === null)
691
+ return;
692
+ const now = Date.now();
693
+ this.browserPauseMs += now - this.browserPauseStartedAt;
694
+ this.browserPauseStartedAt = null;
695
+ this.browserPauseEndedAt = now;
696
+ this.deps.onBrowserPauseChange?.(sessionId, false);
697
+ }
698
+ /** Wall-clock spent in the browser pause we are in RIGHT NOW, if any. */
699
+ liveBrowserPauseMs() {
700
+ const startedAt = this.browserPauseStartedAt;
701
+ return startedAt === null ? 0 : Date.now() - startedAt;
702
+ }
653
703
  /**
654
704
  * Run the leased turn to completion, or until `signal` aborts it.
655
705
  *
@@ -1305,6 +1355,9 @@ class SessionRunner {
1305
1355
  this.turnContext = null;
1306
1356
  this.deferredSteers.length = 0;
1307
1357
  this.closeApproval(session.sessionId);
1358
+ // A held browser call cannot outlive its turn; the harness cancels the
1359
+ // proxy hold on its side too.
1360
+ this.closeBrowserPause(session.sessionId);
1308
1361
  this.deps.knowledge?.host.bindTurn(session.sessionId, null);
1309
1362
  this.closeQuestion(session.sessionId);
1310
1363
  this.emittedApprovals.clear();
@@ -1577,6 +1630,7 @@ class SessionRunner {
1577
1630
  const idleLimitMs = session.limits.maxTurnIdleMs ?? DEFAULT_MAX_TURN_IDLE_MS;
1578
1631
  let lastProgressAt = Date.now();
1579
1632
  let approvalWaitAtLastProgress = this.approvalWaitMs;
1633
+ let browserPauseAtLastProgress = this.browserPauseMs;
1580
1634
  try {
1581
1635
  while (!signal.aborted) {
1582
1636
  const now = Date.now();
@@ -1640,7 +1694,11 @@ class SessionRunner {
1640
1694
  // turn stopped to ask a person something is blocked on them, not
1641
1695
  // running away, and ten minutes is an entirely ordinary time for a
1642
1696
  // person to be away from their desk.
1643
- const pausedMs = this.approvalWaitMs + this.liveApprovalWaitMs();
1697
+ // A browser takeover is the same kind of intentional pause.
1698
+ const pausedMs = this.approvalWaitMs +
1699
+ this.liveApprovalWaitMs() +
1700
+ this.browserPauseMs +
1701
+ this.liveBrowserPauseMs();
1644
1702
  if (hardDeadline !== null && now > hardDeadline + pausedMs) {
1645
1703
  timedOut = true;
1646
1704
  break;
@@ -1743,6 +1801,7 @@ class SessionRunner {
1743
1801
  // here, not double-counting wait time already excluded once.
1744
1802
  lastProgressAt = Date.now();
1745
1803
  approvalWaitAtLastProgress = this.approvalWaitMs;
1804
+ browserPauseAtLastProgress = this.browserPauseMs;
1746
1805
  continue;
1747
1806
  }
1748
1807
  const status = await adapter.getStatus(backendSessionId, config);
@@ -1780,9 +1839,27 @@ class SessionRunner {
1780
1839
  const approvalWaitSinceProgress = this.approvalWaitMs -
1781
1840
  approvalWaitAtLastProgress +
1782
1841
  this.liveApprovalWaitMs();
1783
- const idleElapsed = Date.now() - lastProgressAt - approvalWaitSinceProgress;
1842
+ // A handback restarts the idle clock: the agent gets a full idle
1843
+ // window after the person returns control, as after real progress.
1844
+ if (this.browserPauseEndedAt !== null &&
1845
+ this.browserPauseEndedAt > lastProgressAt) {
1846
+ lastProgressAt = this.browserPauseEndedAt;
1847
+ approvalWaitAtLastProgress = this.approvalWaitMs;
1848
+ browserPauseAtLastProgress = this.browserPauseMs;
1849
+ }
1850
+ // Browser takeover time since the last progress, by the same rule.
1851
+ // Skipped outright while held: the idle clock restarts when the
1852
+ // person hands back, never mid-hold.
1853
+ const browserPauseSinceProgress = this.browserPauseMs -
1854
+ browserPauseAtLastProgress +
1855
+ this.liveBrowserPauseMs();
1856
+ const idleElapsed = Date.now() -
1857
+ lastProgressAt -
1858
+ approvalWaitSinceProgress -
1859
+ browserPauseSinceProgress;
1784
1860
  if (this.approvalWaitStartedAt === null &&
1785
1861
  this.openQuestionPartId === null &&
1862
+ this.browserPauseStartedAt === null &&
1786
1863
  idleElapsed > idleLimitMs) {
1787
1864
  this.logger.warn(`[SessionRunner] session ${session.sessionId} had no progress for ` +
1788
1865
  `${Math.round(idleElapsed / 1000)}s (limit ${Math.round(idleLimitMs / 1000)}s)`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peakflo/agent-harness",
3
- "version": "0.2.11-rc.2",
3
+ "version": "0.2.11-rc.4",
4
4
  "description": "Headless Peakflo coding-agent harness. One codebase, three hosting modes: a Daytona cloud sandbox, a customer VM, and the 20x desktop.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",