pi-harness-runtime 1.1.459 → 1.1.461

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.
Files changed (46) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/harness/pending-countdown.ts +348 -0
  3. package/harness/pending-wait.ts +69 -0
  4. package/index.ts +248 -48
  5. package/package.json +1 -1
  6. package/packages/agent-policy/package.json +1 -1
  7. package/packages/auth/package.json +1 -1
  8. package/packages/autonomous-runtime/package.json +1 -1
  9. package/packages/checkpoint/package.json +1 -1
  10. package/packages/clipboard/package.json +1 -1
  11. package/packages/codex-watcher/package.json +1 -1
  12. package/packages/config-capture/package.json +1 -1
  13. package/packages/contracts/package.json +1 -1
  14. package/packages/cookie-sanitizer/package.json +1 -1
  15. package/packages/evaluation-engine/package.json +1 -1
  16. package/packages/event-bus/package.json +1 -1
  17. package/packages/event-store/package.json +1 -1
  18. package/packages/experience-replay/package.json +1 -1
  19. package/packages/file-copy-helper/package.json +1 -1
  20. package/packages/intent-analyzer/package.json +1 -1
  21. package/packages/jev-judge/package.json +1 -1
  22. package/packages/knowledge-retrieval/package.json +1 -1
  23. package/packages/learning-engine/package.json +1 -1
  24. package/packages/logging/package.json +1 -1
  25. package/packages/memory-engine/package.json +1 -1
  26. package/packages/notification/package.json +1 -1
  27. package/packages/paperclip-integration/package.json +1 -1
  28. package/packages/performance-optimizer/package.json +1 -1
  29. package/packages/privilege-broker/package.json +1 -1
  30. package/packages/prompt-compiler/package.json +1 -1
  31. package/packages/providers/package.json +1 -1
  32. package/packages/qdrant-skills/package.json +1 -1
  33. package/packages/quota-manager/package.json +1 -1
  34. package/packages/relay/package.json +1 -1
  35. package/packages/skill-mcp-client/package.json +1 -1
  36. package/packages/skills/package.json +1 -1
  37. package/packages/todo-bd-sync/package.json +1 -1
  38. package/packages/token-estimation/package.json +1 -1
  39. package/packages/tooling-preference/package.json +1 -1
  40. package/packages/trajectory/package.json +1 -1
  41. package/packages/tui/package.json +1 -1
  42. package/packages/two-phase-review/package.json +1 -1
  43. package/packages/types/package.json +1 -1
  44. package/packages/webmcp-tester/package.json +1 -1
  45. package/packages/wiki/package.json +1 -1
  46. package/packages/write-review/package.json +1 -1
package/index.ts CHANGED
@@ -42,6 +42,11 @@ import {
42
42
  relayShouldBeReported,
43
43
  } from "./harness/startup-report.ts";
44
44
  import { localSkillsReport, skillsReport } from "./harness/skills-startup.ts";
45
+ import {
46
+ PendingQuestionCountdown,
47
+ type CountdownView,
48
+ } from "./harness/pending-countdown.ts";
49
+ import { endPendingWait } from "./harness/pending-wait.ts";
45
50
  import { isChannelDisabled, setChannelDisabled } from "./harness/notification-settings.ts";
46
51
  // The Honcho contract lives with the Honcho integration. A type-only import
47
52
  // is erased at runtime, so the startup keeps loading the module lazily.
@@ -292,6 +297,9 @@ const sharedState = {
292
297
  validationLedger: null as unknown,
293
298
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
294
299
  updateAutoContinueStatus: null as any,
300
+ // The live countdown of the pending question. The channels stop it when
301
+ // a human answer arrives.
302
+ pendingCountdown: null as PendingQuestionCountdown | null,
295
303
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
296
304
  pqController: null as any,
297
305
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -335,6 +343,8 @@ function applyChannelApproval(
335
343
  // cleared after a successful resolution.
336
344
  sharedState.waitingForUserSince = null;
337
345
  sharedState.updateAutoContinueStatus?.("IDLE");
346
+ // The countdown of the answered question is over with it.
347
+ sharedState.pendingCountdown?.stop();
338
348
 
339
349
  if (result.resolution === "yes") {
340
350
  (sharedState.todoContinuationController as any)?.resume?.();
@@ -932,6 +942,12 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
932
942
  let lastUserMessage: Date | null = null;
933
943
  let autoContinueTimer: NodeJS.Timeout | null = null;
934
944
  let _inferenceInProgress = false; // Track for overlap prevention
945
+ // The timing of the wait. The countdown in the status row and the
946
+ // decision of the judge use the same constants, so the displayed
947
+ // deadline and the decision can never drift apart.
948
+ const AUTO_CONTINUE_INTERVAL_MS = 60 * 1000; // Fallback poll: every minute
949
+ const MIN_WAIT_MINUTES = 5; // Minimum wait before auto-continue
950
+ const MAX_WAIT_MINUTES = 30; // After this the work pauses instead of waiting
935
951
  // A continuation was dispatched and its turn has not settled yet.
936
952
  // The gate owns that state: the status stays "RUN" until the turn
937
953
  // settles, and no timer may clear it in the meantime.
@@ -1023,6 +1039,15 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1023
1039
  }
1024
1040
  });
1025
1041
  let _sessionId = Date.now().toString(36); // Track session for cancellation
1042
+ // The session of the Pi runtime. A countdown belongs to the session
1043
+ // that asked the question.
1044
+ let _piSessionId = "";
1045
+ // The session the running countdown belongs to. A countdown of a
1046
+ // session that is gone must not draw its status any more.
1047
+ let _countdownSessionId = "";
1048
+ // The request the row of the footer belongs to. A countdown that
1049
+ // stops must not clear the row of a newer question.
1050
+ let _countdownRowRequestId: string | null = null;
1026
1051
  let _lastNotifiedQuestion = ""; // Track last question to prevent repeat notifications
1027
1052
  let _completedNotifiedFor: string | null = null; // Track completion notification deduplication
1028
1053
 
@@ -1048,36 +1073,191 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1048
1073
  ERROR: "Auto-continue unavailable",
1049
1074
  } as const;
1050
1075
 
1076
+ // The live countdown of the pending question.
1077
+ //
1078
+ // It owns the deadline of the wait: it shows the remaining time in
1079
+ // the status row once per second and starts the decision of the
1080
+ // judge exactly at the deadline. The fallback poll only takes over
1081
+ // when there is no countdown, so a decision is never started twice.
1082
+ const pendingCountdown = new PendingQuestionCountdown({
1083
+ // The two policies of the wait belong to the countdown, so the
1084
+ // displayed deadline and the deadline of the judge cannot drift
1085
+ // apart.
1086
+ waitMinutes: MIN_WAIT_MINUTES,
1087
+ recheckIntervalMs: AUTO_CONTINUE_INTERVAL_MS,
1088
+ render: (view: CountdownView) => {
1089
+ _countdownRowRequestId = view.requestId;
1090
+ updateAutoContinueStatus(
1091
+ "WAITING",
1092
+ view.requestId.slice(0, 8),
1093
+ view.mode === "recheck"
1094
+ ? `Jev will recheck in ${view.remaining}`
1095
+ : `Jev decision in ${view.remaining}`,
1096
+ );
1097
+ },
1098
+ onDeadline: (requestId: string) => {
1099
+ logToFile("[auto-continue]", "The decision deadline was reached:", requestId);
1100
+ checkAndAutoContinue("countdown")
1101
+ .then(() => {
1102
+ // The decision did not start - another one was still in
1103
+ // flight, or the wait was gone before it ran. The
1104
+ // deadline is not lost: the countdown is armed again
1105
+ // and the decision follows.
1106
+ if (pendingCountdown.fired && pendingCountdown.requestId === requestId) {
1107
+ pendingCountdown.startRecheck(requestId);
1108
+ }
1109
+ })
1110
+ .catch((error) => {
1111
+ logToFile("[auto-continue]", "The decision failed:", oneLineError(error));
1112
+ // The decision did not run: the countdown releases the
1113
+ // wait, so the fallback poll can take over again.
1114
+ pendingCountdown.stop();
1115
+ });
1116
+ },
1117
+ isPending: (requestId: string) => {
1118
+ // The wait must still belong to the request: a late tick of
1119
+ // an answered or replaced question must not draw its status
1120
+ // and must not trigger a decision.
1121
+ if (!waitingForUserSince) return false;
1122
+ if (_countdownSessionId !== _piSessionId) return false;
1123
+ const current = sharedState.pqController?.getCurrentQuestion?.();
1124
+ if (!current) return false;
1125
+ return (
1126
+ current.requestId === requestId &&
1127
+ (current.state === "pending" || current.state === "evaluating")
1128
+ );
1129
+ },
1130
+ onStop: (requestId: string | null) => {
1131
+ // The countdown is over: the row is cleared with it, so no
1132
+ // row claims a decision that is not due any more. Only the row
1133
+ // of that countdown is cleared: a row of another state - the
1134
+ // judge continues, for example - belongs to that state, and
1135
+ // the row of a newer question belongs to that question.
1136
+ if (_countdownRowRequestId && requestId && _countdownRowRequestId !== requestId) {
1137
+ return;
1138
+ }
1139
+ _countdownRowRequestId = null;
1140
+ if (_currentAutoContinueState.startsWith("[WAIT]")) {
1141
+ _currentAutoContinueState = "";
1142
+ _setAutoContinueStatus?.(undefined);
1143
+ }
1144
+ },
1145
+ onError: (error: unknown) => {
1146
+ logToFile("[auto-continue]", "The countdown failed:", oneLineError(error));
1147
+ },
1148
+ });
1149
+ sharedState.pendingCountdown = pendingCountdown;
1150
+
1051
1151
  // Wire shared state for callback handlers
1052
1152
  sharedState.waitingForUserSince = waitingForUserSince;
1053
1153
  sharedState.updateAutoContinueStatus = updateAutoContinueStatus;
1054
1154
 
1055
- // Update auto-continue status in TUI footer
1056
- function updateAutoContinueStatus(state: keyof typeof AC_STATE_MESSAGES, requestId?: string): void {
1057
- if (!_setAutoContinueStatus) return;
1155
+ /**
1156
+ * Start the countdown of a pending question.
1157
+ *
1158
+ * The deadline of the decision is derived from the wait that is
1159
+ * already running, so the displayed deadline and the decision of
1160
+ * the judge can never drift apart.
1161
+ */
1162
+ function startPendingCountdown(requestId: string): void {
1163
+ _countdownSessionId = _piSessionId;
1164
+ const startedAt = waitingForUserSince?.getTime() ?? Date.now();
1165
+ pendingCountdown.start(requestId, startedAt);
1166
+ }
1167
+
1168
+ /**
1169
+ * Start the countdown of the next attempt.
1170
+ *
1171
+ * The judge asked to wait longer: the countdown counts down to the
1172
+ * next attempt instead of to the deadline of the decision.
1173
+ */
1174
+ function startRecheckCountdown(requestId: string): void {
1175
+ _countdownSessionId = _piSessionId;
1176
+ pendingCountdown.startRecheck(requestId);
1177
+ }
1058
1178
 
1179
+ // Update auto-continue status in TUI footer
1180
+ function updateAutoContinueStatus(
1181
+ state: keyof typeof AC_STATE_MESSAGES,
1182
+ requestId?: string,
1183
+ detail?: string,
1184
+ ): void {
1059
1185
  const message = AC_STATE_MESSAGES[state];
1060
1186
  if (state === "IDLE") {
1061
- // Clear status
1062
- _setAutoContinueStatus(undefined);
1063
- _currentAutoContinueState = "";
1064
- } else {
1065
- const statusText = requestId
1066
- ? `[${AC_STATE[state]}] ${message} | ${requestId}`
1067
- : `[${AC_STATE[state]}] ${message}`;
1068
-
1069
- // Only update if state changed (prevent flicker)
1070
- if (statusText !== _currentAutoContinueState) {
1071
- _setAutoContinueStatus(statusText);
1072
- _currentAutoContinueState = statusText;
1187
+ // Only the row is cleared here: the countdown belongs to the
1188
+ // wait, not to the row. A settled continuation must not stop
1189
+ // the countdown of a question that the agent asked in the
1190
+ // same turn, and it must not blank its row either.
1191
+ if (!pendingCountdown.active) {
1192
+ _currentAutoContinueState = "";
1193
+ _setAutoContinueStatus?.(undefined);
1073
1194
  }
1195
+ return;
1196
+ }
1197
+
1198
+ const parts = [`[${AC_STATE[state]}] ${message}`];
1199
+ if (requestId) parts.push(requestId);
1200
+ if (detail) parts.push(detail);
1201
+ const statusText = parts.join(" | ");
1202
+
1203
+ // Only update if state changed (prevent flicker). The state is
1204
+ // kept even without a UI binding: a session that starts without
1205
+ // a TUI shows the current state as soon as the UI is there.
1206
+ if (statusText !== _currentAutoContinueState) {
1207
+ _currentAutoContinueState = statusText;
1208
+ _setAutoContinueStatus?.(statusText);
1074
1209
  }
1075
1210
  }
1076
1211
 
1212
+ /**
1213
+ * End the wait for a pending question.
1214
+ *
1215
+ * The countdown, the wait state and the pending question belong
1216
+ * together: when one of them ends, the others end with it. A wait
1217
+ * that outlives its question would let the judge decide about a
1218
+ * question of a session that is gone.
1219
+ */
1220
+ function endWait(reason: string): void {
1221
+ endPendingWait(reason, {
1222
+ currentQuestion: () => sharedState.pqController?.getCurrentQuestion?.(),
1223
+ expireQuestion: (requestId: string, why: string) =>
1224
+ sharedState.pqController?.expireByTimeout?.(requestId, why) === true,
1225
+ clearWait: () => {
1226
+ waitingForUserSince = null;
1227
+ sharedState.waitingForUserSince = null;
1228
+ _lastNotifiedQuestion = "";
1229
+ },
1230
+ stopCountdown: () => pendingCountdown.stop(),
1231
+ clearStatus: () => updateAutoContinueStatus("IDLE"),
1232
+ logTransition: (requestId: string, outcome: string, why: string) =>
1233
+ logQuestionTransition(requestId, outcome, why),
1234
+ });
1235
+ }
1236
+
1077
1237
  // Capture UI context from session_start
1078
1238
  pi.on("session_start", (_event, ctx) => {
1239
+ // The session of the runtime: the countdown of a question belongs
1240
+ // to the session that asked it.
1241
+ const sessionId = (
1242
+ ctx as { sessionManager?: { getSessionId?: () => string | undefined } }
1243
+ ).sessionManager?.getSessionId?.();
1244
+ if (typeof sessionId === "string" && sessionId) {
1245
+ if (_piSessionId && _piSessionId !== sessionId) {
1246
+ // The session changed: the wait of the old session is
1247
+ // over with it. The question is closed and the wait
1248
+ // state is cleared, so the judge is not asked about a
1249
+ // question that nobody can answer any more.
1250
+ endWait("session-switched");
1251
+ }
1252
+ _piSessionId = sessionId;
1253
+ }
1079
1254
  if (ctx.hasUI && ctx.ui) {
1080
1255
  _setAutoContinueStatus = ctx.ui.setStatus.bind(ctx.ui, "harness-auto-continue");
1256
+ // A session without a TUI collected the state already: it is
1257
+ // shown as soon as the UI is there.
1258
+ if (_currentAutoContinueState) {
1259
+ _setAutoContinueStatus(_currentAutoContinueState);
1260
+ }
1081
1261
  }
1082
1262
 
1083
1263
  });
@@ -1090,34 +1270,21 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1090
1270
  autoContinueTimer = null;
1091
1271
  }
1092
1272
 
1093
- // Clear UI binding and status
1094
- if (_setAutoContinueStatus) {
1095
- _setAutoContinueStatus(undefined);
1096
- _setAutoContinueStatus = null;
1097
- }
1098
- _currentAutoContinueState = "";
1099
-
1100
1273
  // A restart does not survive in memory: the wait state and the
1101
1274
  // pending question are gone with the process. The question is
1102
1275
  // therefore closed explicitly, so the buttons of that request
1103
1276
  // do not stay open and look like a request that is still
1104
1277
  // waiting.
1105
- const pending = sharedState.pqController?.getCurrentQuestion?.();
1106
- if (pending && pending.state === "pending") {
1107
- const expired = sharedState.pqController?.expireByTimeout?.(
1108
- pending.requestId,
1109
- "session-ended",
1110
- ) === true;
1111
- logQuestionTransition(
1112
- pending.requestId,
1113
- expired ? "expired" : "closed",
1114
- "session-ended",
1115
- );
1278
+ endWait("session-ended");
1279
+
1280
+ // Clear UI binding and status
1281
+ if (_setAutoContinueStatus) {
1282
+ _setAutoContinueStatus(undefined);
1283
+ _setAutoContinueStatus = null;
1116
1284
  }
1285
+ _currentAutoContinueState = "";
1117
1286
 
1118
1287
  // Reset state
1119
- waitingForUserSince = null;
1120
- _lastNotifiedQuestion = "";
1121
1288
  _inferenceInProgress = false;
1122
1289
  });
1123
1290
 
@@ -1148,6 +1315,8 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1148
1315
  waitingForUserSince = null;
1149
1316
  sharedState.waitingForUserSince = null;
1150
1317
  _lastNotifiedQuestion = "";
1318
+ // The question is answered: the countdown of the wait is over.
1319
+ pendingCountdown.stop();
1151
1320
  updateAutoContinueStatus("IDLE");
1152
1321
  // Dispatch answer to Pi
1153
1322
  const message = answer === "yes" ? "yes" : "not yet";
@@ -1224,8 +1393,10 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1224
1393
  // Use question.requestId as the jobId for callback matching
1225
1394
  const questionJobId = question.requestId;
1226
1395
 
1227
- // Update TUI status - use short ID for display
1228
- updateAutoContinueStatus("WAITING", questionJobId.slice(0, 8));
1396
+ // The status row counts down to the decision of the judge.
1397
+ // The countdown is bound to this request, so a newer
1398
+ // question replaces it.
1399
+ startPendingCountdown(questionJobId);
1229
1400
 
1230
1401
  // Get task context for additional info
1231
1402
  const taskContext = await getTaskContext().catch(() => ({ total: 0, completed: 0, remaining: [] as string[] }));
@@ -1388,6 +1559,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1388
1559
  if (waitingForUserSince) {
1389
1560
  waitingForUserSince = null;
1390
1561
  _lastNotifiedQuestion = "";
1562
+ // The user answered in Pi: the countdown of the wait is
1563
+ // over with it.
1564
+ pendingCountdown.stop();
1391
1565
  updateAutoContinueStatus("IDLE");
1392
1566
  // Cancel pending question on user response
1393
1567
  // (User answered via Pi instead of button)
@@ -1397,18 +1571,22 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1397
1571
  });
1398
1572
 
1399
1573
  // Periodic check for auto-continue decision
1400
- const AUTO_CONTINUE_INTERVAL_MS = 60 * 1000; // Check every 1 minute
1401
- const MIN_WAIT_MINUTES = 5; // Minimum wait before auto-continue
1402
- const MAX_WAIT_MINUTES = 30; // After this the work pauses instead of waiting
1403
1574
 
1404
- const checkAndAutoContinue = async () => {
1575
+ const checkAndAutoContinue = async (source: "countdown" | "poll" = "poll") => {
1405
1576
  // Prevent overlap if inference is already in progress
1406
1577
  if (_inferenceInProgress) {
1578
+ // A deadline that was blocked is armed again by the caller of
1579
+ // the countdown, so the decision is not lost.
1407
1580
  return;
1408
1581
  }
1409
1582
 
1410
1583
  if (!waitingForUserSince) return;
1411
1584
 
1585
+ // The countdown owns the wait while it runs: the fallback poll
1586
+ // only takes over when there is no countdown, so the decision is
1587
+ // never started twice.
1588
+ if (source === "poll" && pendingCountdown.active) return;
1589
+
1412
1590
  // Capture session ID at check time for post-inference validation
1413
1591
  const checkSessionId = _sessionId;
1414
1592
  // Use same hash algorithm as detection
@@ -1470,6 +1648,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1470
1648
  if (taskContext.total === 0 && !hasPendingQuestion) {
1471
1649
  logToFile("[auto-continue]", "No tasks and no pending questions, skipping auto-continue check");
1472
1650
  _inferenceInProgress = false;
1651
+ // The countdown is over: the fallback poll takes over the
1652
+ // next attempt.
1653
+ pendingCountdown.stop();
1473
1654
  return;
1474
1655
  }
1475
1656
 
@@ -1624,15 +1805,20 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1624
1805
  if (waitBelongsToRequest(questionContext?.requestId)) {
1625
1806
  waitingForUserSince = null;
1626
1807
  sharedState.waitingForUserSince = null;
1808
+ // The request expired: the countdown of the wait
1809
+ // is over with it.
1810
+ pendingCountdown.stop();
1627
1811
  updateAutoContinueStatus("IDLE");
1628
1812
  }
1629
1813
  } else {
1630
- // The request stays open: the next poll asks again
1631
- // until the deadline closes it.
1632
- updateAutoContinueStatus(
1633
- "WAITING",
1634
- questionContext?.requestId.slice(0, 8),
1635
- );
1814
+ // The request stays open: the countdown counts down to
1815
+ // the next check, so the retry does not wait for the
1816
+ // fallback poll.
1817
+ if (questionContext?.requestId) {
1818
+ startRecheckCountdown(questionContext.requestId);
1819
+ } else {
1820
+ updateAutoContinueStatus("WAITING");
1821
+ }
1636
1822
  }
1637
1823
  } else {
1638
1824
  // Close the question with the Jev decision. The buttons of
@@ -1669,6 +1855,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1669
1855
  waitingForUserSince = null;
1670
1856
  sharedState.waitingForUserSince = null;
1671
1857
  _lastNotifiedQuestion = "";
1858
+ // The stale decision is discarded: the countdown
1859
+ // of the wait is over with it.
1860
+ pendingCountdown.stop();
1672
1861
  updateAutoContinueStatus("IDLE");
1673
1862
  } else {
1674
1863
  logToFile(
@@ -1692,6 +1881,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1692
1881
  waitingForUserSince = null;
1693
1882
  sharedState.waitingForUserSince = null;
1694
1883
  _lastNotifiedQuestion = "";
1884
+ // The work pauses for a human: the countdown of
1885
+ // the wait is over with it.
1886
+ pendingCountdown.stop();
1695
1887
  updateAutoContinueStatus("IDLE");
1696
1888
  }
1697
1889
  return;
@@ -1725,6 +1917,8 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1725
1917
  waitingForUserSince = null;
1726
1918
  sharedState.waitingForUserSince = null;
1727
1919
  _lastNotifiedQuestion = "";
1920
+ // The question is closed: the countdown of the wait is over.
1921
+ pendingCountdown.stop();
1728
1922
 
1729
1923
  // The status stays "RUN" until the turn that the steer
1730
1924
  // started has settled. A timer would clear it while the
@@ -1735,6 +1929,9 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
1735
1929
  _inferenceInProgress = false;
1736
1930
  } catch {
1737
1931
  _inferenceInProgress = false;
1932
+ // The countdown is over: the fallback poll takes over the next
1933
+ // attempt.
1934
+ pendingCountdown.stop();
1738
1935
  updateAutoContinueStatus("ERROR");
1739
1936
  addHarnessMessage("Jev", "auto-continue evaluation failed", "err", [
1740
1937
  "How to fix: Check logs at /tmp/pi-harness.log for API errors",
@@ -2108,7 +2305,10 @@ async function initAutoContinue(pi: ExtensionAPI): Promise<void> {
2108
2305
  });
2109
2306
 
2110
2307
  // Start periodic check
2111
- autoContinueTimer = setInterval(checkAndAutoContinue, AUTO_CONTINUE_INTERVAL_MS);
2308
+ autoContinueTimer = setInterval(
2309
+ () => checkAndAutoContinue("poll"),
2310
+ AUTO_CONTINUE_INTERVAL_MS,
2311
+ );
2112
2312
  if (jevKeyMissing) {
2113
2313
  // Without a provider the health check cannot run. The missing-key
2114
2314
  // message (with its fix commands) stays the visible one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/agent-policy",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Agent policy runtime for pi-harness - enforces mandatory rule delivery and mutation gating",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/auth",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "pi-harness-runtime — auth",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-runtime",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Autonomous Operations Runtime — task inbox, lease management, and worker lifecycle (RFC-0101)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/checkpoint",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Enhanced checkpoint system with incremental snapshots, diff-based storage, and recovery strategies",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/clipboard",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/codex-watcher",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Watches Codex CLI sessions and emits visible messages for PING-PONG monitoring",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/config-capture",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Auto-capture API-fetched configuration values to AGENTS.md",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/contracts",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "type": "module",
5
5
  "description": "RPC schema definitions for pi-harness runtime",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/cookie-sanitizer",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "pi-harness-runtime — cookie sanitizer (provider-quota cookie file normaliser)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/evaluation-engine",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Evaluation engine for pi-harness runtime (RFC-0057)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/event-bus",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/event-store",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/experience-replay",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Experience replay for debugging and reproducibility (RFC-0059)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/file-copy-helper",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Injects cp/sudo cp rule when user asks to mimic/copy files",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/intent-analyzer",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/jev-judge",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Jev Judge - Structured decision model for agent routing, E2E testing, and automated decisions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/knowledge-retrieval",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Knowledge retrieval via TencentDB-Agent-Memory MCP (RFC-0105)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/learning-engine",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Learning engine for extracting lessons from runtime execution (RFC-0058)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/logging",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "type": "module",
5
5
  "description": "Centralized structured application logging with buffered Cloudflare R2 and webhook transports",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/memory-engine",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Memory engine with OKF integration for durable knowledge management (RFC-0060)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/notification",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "pi-harness-runtime — notification",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/paperclip-integration",
3
- "version": "1.1.459",
3
+ "version": "1.1.461",
4
4
  "description": "Paperclip API integration: poll agent inbox, route tasks to pi.dev, Telegram alerts",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",