pi-harness-runtime 0.9.14 → 0.9.16

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 (175) hide show
  1. package/harness/auto-quota-resume.ts +129 -0
  2. package/harness/e2e/minimax-quota-scraper.ts +10 -4
  3. package/harness/index.ts +8 -0
  4. package/harness/loop-runtime.ts +57 -0
  5. package/index.ts +47 -11
  6. package/mirror.ts +40 -18
  7. package/package.json +1 -1
  8. package/packages/a2a-adapter/package.json +1 -1
  9. package/packages/auth/package.json +1 -1
  10. package/packages/autonomous-refactor/package.json +1 -1
  11. package/packages/autonomous-runtime/README.md +48 -0
  12. package/packages/autonomous-runtime/dist/inbox.d.ts +75 -0
  13. package/packages/autonomous-runtime/dist/inbox.d.ts.map +1 -0
  14. package/packages/autonomous-runtime/dist/inbox.js +212 -0
  15. package/packages/autonomous-runtime/dist/inbox.js.map +1 -0
  16. package/packages/autonomous-runtime/dist/index.d.ts +17 -0
  17. package/packages/autonomous-runtime/dist/index.d.ts.map +1 -0
  18. package/packages/autonomous-runtime/dist/index.js +17 -0
  19. package/packages/autonomous-runtime/dist/index.js.map +1 -0
  20. package/packages/autonomous-runtime/dist/lease.d.ts +78 -0
  21. package/packages/autonomous-runtime/dist/lease.d.ts.map +1 -0
  22. package/packages/autonomous-runtime/dist/lease.js +273 -0
  23. package/packages/autonomous-runtime/dist/lease.js.map +1 -0
  24. package/packages/autonomous-runtime/dist/types.d.ts +160 -0
  25. package/packages/autonomous-runtime/dist/types.d.ts.map +1 -0
  26. package/packages/autonomous-runtime/dist/types.js +31 -0
  27. package/packages/autonomous-runtime/dist/types.js.map +1 -0
  28. package/packages/autonomous-runtime/dist/worker.d.ts +63 -0
  29. package/packages/autonomous-runtime/dist/worker.d.ts.map +1 -0
  30. package/packages/autonomous-runtime/dist/worker.js +133 -0
  31. package/packages/autonomous-runtime/dist/worker.js.map +1 -0
  32. package/packages/autonomous-runtime/package.json +34 -0
  33. package/packages/autonomous-runtime/src/inbox.ts +266 -0
  34. package/packages/autonomous-runtime/src/index.ts +22 -0
  35. package/packages/autonomous-runtime/src/lease.ts +318 -0
  36. package/packages/autonomous-runtime/src/types.ts +257 -0
  37. package/packages/autonomous-runtime/test/autonomous-runtime.test.ts +194 -0
  38. package/packages/autonomous-runtime/test/inbox.test.mjs +266 -0
  39. package/packages/autonomous-runtime/test/inbox.test.ts +80 -0
  40. package/packages/autonomous-runtime/test/types.test.ts +68 -0
  41. package/packages/autonomous-runtime/tsconfig.json +22 -0
  42. package/packages/autonomous-runtime/tsconfig.tsbuildinfo +1 -0
  43. package/packages/cache-strategy/package.json +1 -1
  44. package/packages/capability-registry/package.json +1 -1
  45. package/packages/checkpoint/package.json +1 -1
  46. package/packages/cli-plugin-sdk/package.json +1 -1
  47. package/packages/code-generation/package.json +1 -1
  48. package/packages/code-review/package.json +1 -1
  49. package/packages/codex-adapter/package.json +1 -1
  50. package/packages/context-compiler/package.json +1 -1
  51. package/packages/context-manager/package.json +1 -1
  52. package/packages/cookie-sanitizer/package.json +1 -1
  53. package/packages/cost-optimizer/package.json +1 -1
  54. package/packages/django-plugin/package.json +1 -1
  55. package/packages/doc-generator/package.json +1 -1
  56. package/packages/evaluation-engine/package.json +1 -1
  57. package/packages/evaluation-runner/package.json +1 -1
  58. package/packages/experience-replay/package.json +1 -1
  59. package/packages/feedback-collector/package.json +1 -1
  60. package/packages/framework-detector/package.json +1 -1
  61. package/packages/framework-plugin-sdk/package.json +1 -1
  62. package/packages/frappe-plugin/package.json +1 -1
  63. package/packages/generic-web-plugin/package.json +1 -1
  64. package/packages/health-monitor/package.json +1 -1
  65. package/packages/laravel-plugin/package.json +1 -1
  66. package/packages/learning-engine/package.json +1 -1
  67. package/packages/mcp-adapter/package.json +1 -1
  68. package/packages/memory-engine/package.json +1 -1
  69. package/packages/model-registry/package.json +1 -1
  70. package/packages/nextjs-plugin/package.json +1 -1
  71. package/packages/notification/package.json +1 -1
  72. package/packages/observability/package.json +1 -1
  73. package/packages/performance-optimizer/package.json +1 -1
  74. package/packages/privilege-broker/README.md +45 -0
  75. package/packages/privilege-broker/config/privileges.yaml.example +172 -0
  76. package/packages/privilege-broker/dist/approval.d.ts +23 -0
  77. package/packages/privilege-broker/dist/approval.d.ts.map +1 -0
  78. package/packages/privilege-broker/dist/approval.js +45 -0
  79. package/packages/privilege-broker/dist/approval.js.map +1 -0
  80. package/packages/privilege-broker/dist/audit.d.ts +38 -0
  81. package/packages/privilege-broker/dist/audit.d.ts.map +1 -0
  82. package/packages/privilege-broker/dist/audit.js +125 -0
  83. package/packages/privilege-broker/dist/audit.js.map +1 -0
  84. package/packages/privilege-broker/dist/executor.d.ts +25 -0
  85. package/packages/privilege-broker/dist/executor.d.ts.map +1 -0
  86. package/packages/privilege-broker/dist/executor.js +83 -0
  87. package/packages/privilege-broker/dist/executor.js.map +1 -0
  88. package/packages/privilege-broker/dist/index.d.ts +30 -0
  89. package/packages/privilege-broker/dist/index.d.ts.map +1 -0
  90. package/packages/privilege-broker/dist/index.js +5 -0
  91. package/packages/privilege-broker/dist/index.js.map +1 -0
  92. package/packages/privilege-broker/dist/registry.d.ts +15 -0
  93. package/packages/privilege-broker/dist/registry.d.ts.map +1 -0
  94. package/packages/privilege-broker/dist/registry.js +76 -0
  95. package/packages/privilege-broker/dist/registry.js.map +1 -0
  96. package/packages/privilege-broker/dist/types.d.ts +77 -0
  97. package/packages/privilege-broker/dist/types.d.ts.map +1 -0
  98. package/packages/privilege-broker/dist/types.js +11 -0
  99. package/packages/privilege-broker/dist/types.js.map +1 -0
  100. package/packages/privilege-broker/package.json +35 -0
  101. package/packages/privilege-broker/src/approval.ts +70 -0
  102. package/packages/privilege-broker/src/audit.ts +142 -0
  103. package/packages/privilege-broker/src/executor.ts +110 -0
  104. package/packages/privilege-broker/src/index.ts +46 -0
  105. package/packages/privilege-broker/src/registry.ts +98 -0
  106. package/packages/privilege-broker/src/types.ts +105 -0
  107. package/packages/privilege-broker/tsconfig.json +22 -0
  108. package/packages/privilege-broker/tsconfig.tsbuildinfo +1 -0
  109. package/packages/project-analyzer/package.json +1 -1
  110. package/packages/prompt-compiler/package.json +1 -1
  111. package/packages/prompt-versioning/package.json +1 -1
  112. package/packages/provider-adapter-sdk/package.json +1 -1
  113. package/packages/provider-router/package.json +1 -1
  114. package/packages/provider-selector/package.json +1 -1
  115. package/packages/providers/dist/provider-id.d.ts +2 -0
  116. package/packages/providers/dist/provider-id.d.ts.map +1 -1
  117. package/packages/providers/dist/provider-id.js +9 -0
  118. package/packages/providers/dist/provider-id.js.map +1 -1
  119. package/packages/providers/package.json +1 -1
  120. package/packages/providers/src/provider-id.ts +12 -0
  121. package/packages/quota-manager/package.json +1 -1
  122. package/packages/rate-limiter/package.json +1 -1
  123. package/packages/react-vite-plugin/package.json +1 -1
  124. package/packages/release-manager/package.json +1 -1
  125. package/packages/requirement-compiler/package.json +1 -1
  126. package/packages/runtime/package.json +1 -1
  127. package/packages/scheduler/package.json +1 -1
  128. package/packages/scheduler-adapter/README.md +36 -0
  129. package/packages/scheduler-adapter/dist/cron.d.ts +21 -0
  130. package/packages/scheduler-adapter/dist/cron.d.ts.map +1 -0
  131. package/packages/scheduler-adapter/dist/cron.js +116 -0
  132. package/packages/scheduler-adapter/dist/cron.js.map +1 -0
  133. package/packages/scheduler-adapter/dist/index.d.ts +9 -0
  134. package/packages/scheduler-adapter/dist/index.d.ts.map +1 -0
  135. package/packages/scheduler-adapter/dist/index.js +8 -0
  136. package/packages/scheduler-adapter/dist/index.js.map +1 -0
  137. package/packages/scheduler-adapter/dist/interface.d.ts +61 -0
  138. package/packages/scheduler-adapter/dist/interface.d.ts.map +1 -0
  139. package/packages/scheduler-adapter/dist/interface.js +2 -0
  140. package/packages/scheduler-adapter/dist/interface.js.map +1 -0
  141. package/packages/scheduler-adapter/dist/internal.d.ts +30 -0
  142. package/packages/scheduler-adapter/dist/internal.d.ts.map +1 -0
  143. package/packages/scheduler-adapter/dist/internal.js +82 -0
  144. package/packages/scheduler-adapter/dist/internal.js.map +1 -0
  145. package/packages/scheduler-adapter/dist/systemd.d.ts +31 -0
  146. package/packages/scheduler-adapter/dist/systemd.d.ts.map +1 -0
  147. package/packages/scheduler-adapter/dist/systemd.js +203 -0
  148. package/packages/scheduler-adapter/dist/systemd.js.map +1 -0
  149. package/packages/scheduler-adapter/package.json +32 -0
  150. package/packages/scheduler-adapter/src/cron.ts +123 -0
  151. package/packages/scheduler-adapter/src/index.ts +9 -0
  152. package/packages/scheduler-adapter/src/interface.ts +61 -0
  153. package/packages/scheduler-adapter/src/internal.ts +99 -0
  154. package/packages/scheduler-adapter/src/systemd.ts +221 -0
  155. package/packages/scheduler-adapter/tsconfig.json +22 -0
  156. package/packages/scheduler-adapter/tsconfig.tsbuildinfo +1 -0
  157. package/packages/session/package.json +1 -1
  158. package/packages/shared-context/package.json +1 -1
  159. package/packages/skill-registry/package.json +1 -1
  160. package/packages/sprint-planner/package.json +1 -1
  161. package/packages/task-compiler/package.json +1 -1
  162. package/packages/test-data-generator/package.json +1 -1
  163. package/packages/test-generator/package.json +1 -1
  164. package/packages/token-estimation/package.json +1 -1
  165. package/packages/token-optimizer/package.json +1 -1
  166. package/packages/tui/package.json +1 -1
  167. package/packages/types/dist/runtime-types.d.ts +6 -0
  168. package/packages/types/dist/runtime-types.d.ts.map +1 -1
  169. package/packages/types/dist/runtime-types.js +2 -1
  170. package/packages/types/dist/runtime-types.js.map +1 -1
  171. package/packages/types/package.json +1 -1
  172. package/packages/types/src/ai-providers.ts +73 -0
  173. package/packages/types/src/runtime-types.ts +15 -0
  174. package/packages/types/tsconfig.tsbuildinfo +1 -1
  175. package/packages/worktree/package.json +1 -1
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Auto Quota Resume — schedules automatic job resume when quota resets.
3
+ *
4
+ * Flow:
5
+ * quota exhaustion detected
6
+ * → read h5_resets_at_epoch from mirror
7
+ * → compute resumeAt ISO timestamp
8
+ * → call machine.setResumeTime(resumeAt)
9
+ * → schedule setTimeout for auto-resume
10
+ * → at timeout: machine.transition("running")
11
+ *
12
+ * Only the 5h window is auto-resumed (provider-reported, predictable).
13
+ * Weekly/monthly quota exhaustion requires human monitoring.
14
+ */
15
+
16
+ import type { MirrorStore } from "../mirror.js";
17
+ import type { JobStateMachine } from "./job-state-machine.js";
18
+
19
+ /** Small buffer (ms) before the exact reset time to resume. */
20
+ const RESUME_BUFFER_MS = 10_000; // 10 seconds — resume just before reset
21
+
22
+ /** Minimum time to wait before auto-resuming (avoid immediate re-trigger). */
23
+ const MIN_RESUME_DELAY_MS = 5_000; // 5 seconds
24
+
25
+ /** Map of jobId → active auto-resume timer handle. */
26
+ const activeTimers = new Map<
27
+ string,
28
+ { timeout: ReturnType<typeof setTimeout>; resumeAt: number }
29
+ >();
30
+
31
+ /**
32
+ * Schedule an auto-resume for a job that was paused due to quota exhaustion.
33
+ *
34
+ * Reads the reset epoch from the mirror store for the given provider,
35
+ * computes the resume time, sets it on the checkpoint, and schedules
36
+ * a setTimeout to transition back to "running".
37
+ *
38
+ * Returns the scheduled resumeAt ISO string, or null if no epoch data
39
+ * is available (requires human resume).
40
+ */
41
+ export function scheduleAutoResume(
42
+ provider: string,
43
+ machine: JobStateMachine,
44
+ mirrorStore: MirrorStore,
45
+ ): string | null {
46
+ const jobId = machine.getCheckpoint()?.jobId;
47
+ if (!jobId) return null;
48
+
49
+ // Cancel any existing timer for this job
50
+ cancelAutoResume(jobId);
51
+
52
+ const mirror = mirrorStore.readAll();
53
+ const record = mirror?.[provider];
54
+ if (!record) return null;
55
+
56
+ // Only auto-resume on 5h quota exhaustion (has precise epoch)
57
+ if (typeof record.h5_resets_at_epoch !== "number") return null;
58
+
59
+ const resetEpoch = record.h5_resets_at_epoch;
60
+ const now = Date.now();
61
+ const delayMs = Math.max(resetEpoch - now - RESUME_BUFFER_MS, MIN_RESUME_DELAY_MS);
62
+ const resumeAt = now + delayMs;
63
+ const resumeAtIso = new Date(resumeAt).toISOString();
64
+
65
+ // Persist to checkpoint so resumeAt survives worker restart
66
+ machine.setResumeTime(resumeAtIso);
67
+
68
+ console.log(
69
+ `[auto-quota-resume] ${provider} 5h quota exhausted.` +
70
+ ` Auto-resume scheduled at ${resumeAtIso} (in ${Math.round(delayMs / 1000)}s)`,
71
+ );
72
+
73
+ const timeout = setTimeout(async () => {
74
+ activeTimers.delete(jobId);
75
+
76
+ const checkpoint = machine.getCheckpoint();
77
+ if (!checkpoint || checkpoint.status !== "paused_quota") {
78
+ // Job was manually resumed or cancelled
79
+ return;
80
+ }
81
+
82
+ // Check the mirror — if quota is still exhausted, shift the timer
83
+ const updated = mirrorStore.readAll()?.[provider];
84
+ if (updated?.h5_resets_at_epoch) {
85
+ const stillExhausted = Date.now() < updated.h5_resets_at_epoch;
86
+ if (stillExhausted) {
87
+ console.log(
88
+ `[auto-quota-resume] ${provider} still exhausted at scheduled time.` +
89
+ ` Re-scheduling...`,
90
+ );
91
+ scheduleAutoResume(provider, machine, mirrorStore);
92
+ return;
93
+ }
94
+ }
95
+
96
+ const result = await machine.transition("running");
97
+ if (result.success) {
98
+ console.log(
99
+ `[auto-quota-resume] Job ${jobId} auto-resumed after ${provider} 5h quota reset.`,
100
+ );
101
+ } else {
102
+ console.error(
103
+ `[auto-quota-resume] Auto-resume failed for ${jobId}: ${result.error}`,
104
+ );
105
+ }
106
+ }, delayMs);
107
+
108
+ activeTimers.set(jobId, { timeout, resumeAt });
109
+ return resumeAtIso;
110
+ }
111
+
112
+ /**
113
+ * Cancel the auto-resume timer for a job.
114
+ * Call this when the job is manually resumed or cancelled.
115
+ */
116
+ export function cancelAutoResume(jobId: string): void {
117
+ const existing = activeTimers.get(jobId);
118
+ if (existing) {
119
+ clearTimeout(existing.timeout);
120
+ activeTimers.delete(jobId);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get the scheduled resume time for a job, or null if not scheduled.
126
+ */
127
+ export function getScheduledResume(jobId: string): number | null {
128
+ return activeTimers.get(jobId)?.resumeAt ?? null;
129
+ }
@@ -21,10 +21,14 @@ export interface MiniMaxQuotaData {
21
21
  h5UsedPct: number;
22
22
  /** When the 5h window resets (e.g., "in 4 hr 56 min") */
23
23
  h5ResetsAt?: string;
24
+ /** Epoch ms when the 5h window resets — precise UTC timestamp. */
25
+ h5ResetsAtEpoch?: number;
24
26
  /** Weekly usage percentage (0-100) */
25
27
  weeklyUsedPct: number;
26
28
  /** When the weekly window resets */
27
29
  weeklyResetsAt?: string;
30
+ /** Epoch ms when the weekly window resets — precise UTC timestamp. */
31
+ weeklyResetsAtEpoch?: number;
28
32
  /** Monthly usage percentage (0-100) */
29
33
  monthlyUsedPct?: number;
30
34
  /** When the monthly window resets */
@@ -301,10 +305,10 @@ export class MiniMaxQuotaScraper {
301
305
  const weeklyUsedPct = parsePctStr(
302
306
  general.current_weekly_used_percent as unknown,
303
307
  );
304
- const h5ResetsAt = formatRemainsMs(general.end_time as number | undefined);
305
- const weeklyResetsAt = formatRemainsMs(
306
- general.weekly_end_time as number | undefined,
307
- );
308
+ const h5ResetsAtEpoch = general.end_time as number | undefined;
309
+ const h5ResetsAt = formatRemainsMs(h5ResetsAtEpoch);
310
+ const weeklyResetsAtEpoch = general.weekly_end_time as number | undefined;
311
+ const weeklyResetsAt = formatRemainsMs(weeklyResetsAtEpoch);
308
312
 
309
313
  // Best-effort: token usage summary
310
314
  let tokenUsage: MiniMaxQuotaData["tokenUsage"];
@@ -357,8 +361,10 @@ export class MiniMaxQuotaScraper {
357
361
  provider: "minimax",
358
362
  h5UsedPct: h5UsedPct ?? 0,
359
363
  h5ResetsAt,
364
+ h5ResetsAtEpoch,
360
365
  weeklyUsedPct: weeklyUsedPct ?? 0,
361
366
  weeklyResetsAt,
367
+ weeklyResetsAtEpoch,
362
368
  creditBalance,
363
369
  tokenUsage,
364
370
  apiEndpoints: [
package/harness/index.ts CHANGED
@@ -54,6 +54,14 @@ export type { LoopResult, LoopCallbacks } from "./loop-runtime.js";
54
54
  export { RepairEngine } from "./repair-engine.js";
55
55
  export type { RepairResult } from "./repair-engine.js";
56
56
 
57
+ // Auto Quota Resume (5h window auto-resume)
58
+ export {
59
+ scheduleAutoResume,
60
+ cancelAutoResume,
61
+ getScheduledResume,
62
+ } from "./auto-quota-resume.js";
63
+ export type {} from "./auto-quota-resume.js";
64
+
57
65
  // Master Planner
58
66
  export { MasterPlanner } from "./master-planner.js";
59
67
  export type { PlanResult } from "./master-planner.js";
@@ -26,6 +26,7 @@ import type {
26
26
  CompactResult,
27
27
  RuntimeEvent,
28
28
  } from "../packages/types/src/runtime-types.js";
29
+ import { MirrorStore } from "../mirror.js";
29
30
  import {
30
31
  JobStateMachine,
31
32
  type CheckpointManager,
@@ -80,6 +81,14 @@ export interface LoopCallbacks {
80
81
  event: "paused" | "resumed" | "exhausted",
81
82
  provider?: string,
82
83
  ) => Promise<void>;
84
+ /** Get current checkpoint (used by auto-resume to read resumeAt) */
85
+ onGetCheckpoint?: () => Promise<RuntimeCheckpoint | null>;
86
+ /** Check mirror for a provider (used by auto-resume to detect early reset) */
87
+ onCheckMirror?: (
88
+ provider: string,
89
+ ) => Promise<{ h5_used_pct?: number } | null>;
90
+ /** Generic notify (used by auto-resume to tell user wait time) */
91
+ onNotify?: (msg: string, type?: string) => Promise<void>;
83
92
  /** Save checkpoint (call persistence layer) */
84
93
  onCheckpoint?: (checkpoint: RuntimeCheckpoint) => Promise<void>;
85
94
  /** Notify each loop iteration */
@@ -106,6 +115,7 @@ export class LoopRuntime {
106
115
  private readonly config: Required<LoopConfig>;
107
116
  private readonly state: LoopState;
108
117
  private readonly jobState: JobStateMachine;
118
+ private readonly mirrorStore: MirrorStore;
109
119
  private readonly callbacks: LoopCallbacks;
110
120
  private running = false;
111
121
  private paused = false;
@@ -172,6 +182,7 @@ export class LoopRuntime {
172
182
  },
173
183
  },
174
184
  });
185
+ this.mirrorStore = new MirrorStore();
175
186
 
176
187
  // Initialize session memory
177
188
  this.sessionMemory = createSessionMemoryManager(config.jobId);
@@ -214,6 +225,35 @@ export class LoopRuntime {
214
225
 
215
226
  async run(): Promise<LoopResult> {
216
227
  this.running = true;
228
+
229
+ // ─── Auto-resume from quota pause ──────────────────────────────────
230
+ const checkpoint = await this.callbacks.onGetCheckpoint?.();
231
+ if (checkpoint?.status === "paused_quota" && checkpoint.resumeAt) {
232
+ const resumeMs = new Date(checkpoint.resumeAt).getTime();
233
+ const now = Date.now();
234
+ const waitMs = resumeMs - now;
235
+ if (waitMs > 0) {
236
+ const waitMin = Math.ceil(waitMs / 60_000);
237
+ const notify = this.callbacks.onNotify ?? (() => {});
238
+ await notify(
239
+ `Quota reset in ${waitMin} min — auto-resuming at ${checkpoint.resumeAt}`,
240
+ "info",
241
+ );
242
+ // Wait in 5-minute chunks so we don't block forever if quota resets early
243
+ while (this.running && Date.now() < resumeMs) {
244
+ await new Promise((r) => setTimeout(r, 5 * 60_000)); // 5 min
245
+ // Re-check mirror — quota may have reset early
246
+ const fresh = await this.callbacks.onCheckMirror?.("minimax");
247
+ if (fresh && fresh.h5_used_pct !== undefined && fresh.h5_used_pct < 100) {
248
+ break; // quota reset early, resume now
249
+ }
250
+ }
251
+ // Transition back to running
252
+ await this.jobState.transition("running");
253
+ this.callbacks.onQuotaEvent?.("resumed");
254
+ }
255
+ }
256
+
217
257
  this.state.status = "running";
218
258
 
219
259
  try {
@@ -652,6 +692,23 @@ export class LoopRuntime {
652
692
  this.paused = true;
653
693
  this.state.status = "paused_quota";
654
694
  this.jobState.transition("paused_quota");
695
+
696
+ // Set resumeAt from the mirror's known reset epoch so the auto-resume
697
+ // timer knows when to wake this job up.
698
+ const mirror = this.mirrorStore.readProvider("minimax");
699
+ const epoch = mirror?.h5_resets_at_epoch;
700
+ if (epoch) {
701
+ const resumeAt = new Date(epoch).toISOString();
702
+ await this.jobState.setResumeTime(resumeAt);
703
+ console.log(
704
+ `[LoopRuntime] Paused for 5h quota. Auto-resume at ${resumeAt}`,
705
+ );
706
+ } else {
707
+ console.log(
708
+ "[LoopRuntime] Paused for quota but no reset time known yet.",
709
+ );
710
+ }
711
+
655
712
  await this.callbacks.onQuotaEvent?.("paused");
656
713
  await this.saveCheckpoint();
657
714
  }
package/index.ts CHANGED
@@ -62,6 +62,10 @@ import {
62
62
  type SharedBlackboard,
63
63
  createBlackboard,
64
64
  } from "./harness/blackboard.ts";
65
+ import {
66
+ scheduleAutoResume,
67
+ cancelAutoResume,
68
+ } from "./harness/index.js";
65
69
  import { homedir } from "node:os";
66
70
  import { existsSync, mkdirSync } from "node:fs";
67
71
  import { join } from "node:path";
@@ -469,8 +473,10 @@ export default function (pi: ExtensionAPI) {
469
473
  source: "scrape",
470
474
  h5_used_pct: data.h5UsedPct,
471
475
  h5_resets_at: data.h5ResetsAt,
476
+ h5_resets_at_epoch: data.h5ResetsAtEpoch,
472
477
  weekly_used_pct: data.weeklyUsedPct,
473
478
  weekly_resets_at: data.weeklyResetsAt,
479
+ weekly_resets_at_epoch: data.weeklyResetsAtEpoch,
474
480
  });
475
481
  return true;
476
482
  } catch (error) {
@@ -794,25 +800,51 @@ export default function (pi: ExtensionAPI) {
794
800
  return;
795
801
  }
796
802
 
797
- const checkpoint = currentSession.machine.getCheckpoint();
798
- if (!checkpoint) {
799
- ctx.ui.notify("Failed to get checkpoint.", "error");
800
- return;
801
- }
803
+ const checkpoint = currentSession.machine.getCheckpoint();
804
+ if (!checkpoint) {
805
+ ctx.ui.notify("Failed to get checkpoint.", "error");
806
+ return;
807
+ }
802
808
 
803
- const result = await currentSession.machine.transition("paused_quota");
804
- if (!result.success) {
805
- ctx.ui.notify(`Failed to pause: ${result.error}`, "error");
806
- return;
807
- }
809
+ // Read 5h reset epoch from mirror so we can schedule auto-resume
810
+ const mirror = mirrorStore.readProvider("minimax");
811
+ const resumeAtIso = mirror?.h5_resets_at_epoch
812
+ ? new Date(mirror.h5_resets_at_epoch).toISOString()
813
+ : undefined;
814
+
815
+ const result = await currentSession.machine.transition("paused_quota");
816
+ if (!result.success) {
817
+ ctx.ui.notify(`Failed to pause: ${result.error}`, "error");
818
+ return;
819
+ }
808
820
 
821
+ // Persist resumeAt so auto-resume survives worker restart
822
+ if (resumeAtIso) {
823
+ await currentSession.machine.setResumeTime(resumeAtIso);
824
+ }
825
+
826
+ // Schedule auto-resume for 5h quota
827
+ if (resumeAtIso) {
828
+ const scheduled = scheduleAutoResume(
829
+ "minimax",
830
+ currentSession.machine,
831
+ mirrorStore,
832
+ );
833
+ ctx.ui.notify(
834
+ `Job paused.\n` +
835
+ `Auto-resume at ${scheduled ?? resumeAtIso} (5h quota exhausted).\n` +
836
+ `Run /harness cancel to abort.`,
837
+ "info",
838
+ );
839
+ } else {
809
840
  ctx.ui.notify(
810
841
  `Job ${currentSession.jobId} paused.\n` +
811
842
  `Current status: paused_quota\n` +
812
843
  `Run /harness resume to continue.`,
813
844
  "info",
814
845
  );
815
- },
846
+ }
847
+ }
816
848
  });
817
849
 
818
850
  // ─── /harness resume — Resume the harness job ───────────────────────
@@ -839,6 +871,9 @@ export default function (pi: ExtensionAPI) {
839
871
  return;
840
872
  }
841
873
 
874
+ // Cancel any scheduled auto-resume for this job
875
+ cancelAutoResume(currentSession.jobId);
876
+
842
877
  ctx.ui.notify(
843
878
  `Job ${currentSession.jobId} resumed.\n` +
844
879
  `Current status: running\n` +
@@ -873,6 +908,7 @@ export default function (pi: ExtensionAPI) {
873
908
  return;
874
909
  }
875
910
 
911
+ cancelAutoResume(currentSession.jobId);
876
912
  const sessionJobId = currentSession.jobId;
877
913
  currentSession = null;
878
914
 
package/mirror.ts CHANGED
@@ -32,6 +32,27 @@
32
32
 
33
33
  import { getMirrorPath, readJson, writeJson } from "./cli.ts";
34
34
 
35
+ /**
36
+ * Canonical lowercase provider ids — mirrors the KNOWN_AI_PROVIDERS from
37
+ * packages/types/src/ai-providers.ts. Duplicated here to avoid import path
38
+ * resolution issues in the root workspace.
39
+ */
40
+ const KNOWN_PROVIDER_IDS = [
41
+ "minimax",
42
+ "openai",
43
+ "anthropic",
44
+ "glm",
45
+ "openrouter",
46
+ "openai-codex",
47
+ "deepseek",
48
+ "gemini",
49
+ "kimi",
50
+ ] as const;
51
+
52
+ function isKnownProvider(id: string): boolean {
53
+ return (KNOWN_PROVIDER_IDS as readonly string[]).includes(id);
54
+ }
55
+
35
56
  /** Where a piece of data came from. */
36
57
  export type MirrorSource = "scrape" | "tui-signal" | "manual";
37
58
 
@@ -49,10 +70,14 @@ export interface ProviderMirrorRecord {
49
70
  h5_used_pct?: number;
50
71
  /** When the 5h window resets — human-readable or ISO. */
51
72
  h5_resets_at?: string;
73
+ /** Epoch ms when the 5h window resets — precise UTC timestamp. */
74
+ h5_resets_at_epoch?: number;
52
75
  /** Weekly used percent. */
53
76
  weekly_used_pct?: number;
54
77
  /** When the weekly window resets. */
55
78
  weekly_resets_at?: string;
79
+ /** Epoch ms when the weekly window resets — precise UTC timestamp. */
80
+ weekly_resets_at_epoch?: number;
56
81
  /** Set when a limit was hit and we have the exhaustion signal. */
57
82
  exhausted?: boolean;
58
83
  /** Limit type from TUI signal: tokens, context_window, rate_limit, unknown. */
@@ -81,15 +106,7 @@ export interface MirrorRecord {
81
106
  weekly_resets_at?: string;
82
107
  }
83
108
 
84
- /** Known provider ids used as keys in the per-provider map. */
85
- export type KnownProviderId =
86
- | "minimax"
87
- | "openai"
88
- | "openai-codex"
89
- | "glm"
90
- | "anthropic"
91
- | "openrouter"
92
- | (string & {}); // allow extension
109
+
93
110
 
94
111
  const STALE_WARN_MS = 30 * 60 * 1000; // 30 min → orange
95
112
  const STALE_ERROR_MS = 2 * 60 * 60 * 1000; // 2 h → red
@@ -112,11 +129,12 @@ function isLegacyShape(raw: unknown): raw is MirrorRecord & {
112
129
  }
113
130
 
114
131
  /** Convert a legacy single-row record to per-provider shape. */
115
- function upgradeLegacyRecord(legacy: MirrorRecord): PerProviderMirror {
132
+ function upgradeLegacyRecord(legacy: MirrorRecord): PerProviderMirror | null {
116
133
  const provider = legacy.provider ?? "minimax";
134
+ const known = isKnownProvider(provider) ? provider : "minimax";
117
135
  const rec: ProviderMirrorRecord = {
118
136
  synced_at: legacy.synced_at ?? new Date().toISOString(),
119
- provider,
137
+ provider: known,
120
138
  source: "scrape", // legacy was always scrape
121
139
  model: legacy.model,
122
140
  h5_used_pct: legacy.h5_used_pct,
@@ -124,7 +142,7 @@ function upgradeLegacyRecord(legacy: MirrorRecord): PerProviderMirror {
124
142
  weekly_used_pct: legacy.weekly_used_pct,
125
143
  weekly_resets_at: legacy.weekly_resets_at,
126
144
  };
127
- return { [provider]: rec };
145
+ return { [known]: rec };
128
146
  }
129
147
 
130
148
  export class MirrorStore {
@@ -152,13 +170,17 @@ export class MirrorStore {
152
170
 
153
171
  if (isLegacyShape(raw)) {
154
172
  const upgraded = upgradeLegacyRecord(raw as MirrorRecord);
155
- // Persist the upgrade so subsequent reads are fast.
156
- try {
157
- writeJson(this.path, upgraded);
158
- } catch {
159
- // best-effort
173
+ if (upgraded) {
174
+ // Persist the upgrade so subsequent reads are fast.
175
+ try {
176
+ writeJson(this.path, upgraded);
177
+ } catch {
178
+ // best-effort
179
+ }
180
+ return upgraded;
160
181
  }
161
- return upgraded;
182
+ // Unknown legacy provider — treat as absent.
183
+ return null;
162
184
  }
163
185
 
164
186
  // Already per-provider shape.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "0.9.14",
3
+ "version": "0.9.16",
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/a2a-adapter",
3
- "version": "0.9.11",
3
+ "version": "0.9.15",
4
4
  "type": "module",
5
5
  "description": "Google Agent-to-Agent (A2A) protocol adapter for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/auth",
3
- "version": "0.9.11",
3
+ "version": "0.9.15",
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-refactor",
3
- "version": "0.9.11",
3
+ "version": "0.9.15",
4
4
  "type": "module",
5
5
  "description": "Autonomous refactoring engine",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,48 @@
1
+ # Autonomous Runtime — RFC-0101
2
+
3
+ Task Inbox, Task Lease, and worker supervision for autonomous post-session execution.
4
+
5
+ ## Package
6
+
7
+ `@pi-harness/autonomous-runtime`
8
+
9
+ ## Concepts
10
+
11
+ ### Task Inbox
12
+
13
+ Durable queue of tasks submitted by the harness master planner. Survives worker crashes, host reboots, and provider quota exhaustion.
14
+
15
+ ### Task Lease
16
+
17
+ Atomic exclusive claim on a task. Guarantees **single-execution** under crash recovery. Other workers skip a leased task.
18
+
19
+ ### Worker
20
+
21
+ Supervised process that reads from the inbox, claims tasks via leases, executes, and reports results.
22
+
23
+ ## Usage
24
+
25
+ ```ts
26
+ import { TaskInbox, TaskLeaseManager, WORKER_ROOT } from "@pi-harness/autonomous-runtime";
27
+
28
+ const inbox = new TaskInbox();
29
+ const leaseMgr = new TaskLeaseManager();
30
+
31
+ // Submit a task
32
+ await inbox.submit({
33
+ title: "Fix login bug",
34
+ description: "Users cannot log in with SSO",
35
+ capability: "write",
36
+ });
37
+
38
+ // Worker: claim and work
39
+ const task = await inbox.claimNext("worker-1", leaseMgr);
40
+ if (task) {
41
+ // ... do work ...
42
+ await inbox.complete(task.id, { success: true });
43
+ }
44
+ ```
45
+
46
+ ## Status
47
+
48
+ **Phase 1 done.** Phases 2–10 pending implementation.
@@ -0,0 +1,75 @@
1
+ import type { TaskRecord, TaskStatus, TaskEvent } from "./types.js";
2
+ export declare class InboxError extends Error {
3
+ constructor(msg: string);
4
+ }
5
+ /**
6
+ * Manages the append-only task inbox at `~/.pi/harness/inbox/tasks.jsonl`.
7
+ *
8
+ * Storage layout:
9
+ * ```
10
+ * ~/.pi/harness/inbox/
11
+ * ├── tasks.jsonl # append-only log (one JSON line per task)
12
+ * ├── claimed/ # active leases (one JSON file per task)
13
+ * └── BACKLOG.md # human-friendly authoring surface (optional)
14
+ * ```
15
+ *
16
+ * Operations are:
17
+ * - **append** — append a new task to tasks.jsonl
18
+ * - **list** — read all tasks, filterable by status
19
+ * - **transition** — update a task's status in-place (rewrite)
20
+ * - **get** — read one task by id
21
+ * - **reindex** — compact tasks.jsonl by rewriting it from in-memory state
22
+ */
23
+ export declare class TaskInbox {
24
+ private readonly tasksPath;
25
+ private readonly inboxDir;
26
+ private _cache;
27
+ constructor(options?: {
28
+ tasksPath?: string;
29
+ inboxDir?: string;
30
+ });
31
+ /** Ensure the inbox directory exists. Call before any write. */
32
+ ensureDir(): void;
33
+ /**
34
+ * Append a new task to tasks.jsonl.
35
+ * The task is validated before writing. Duplicate ids are rejected.
36
+ *
37
+ * @throws {InboxError} if the task id already exists or validation fails
38
+ */
39
+ append(task: TaskRecord): void;
40
+ /**
41
+ * Read all tasks from tasks.jsonl.
42
+ * Results are sorted by priority ASC then createdAt ASC (oldest first).
43
+ *
44
+ * @param filter Optionally filter by status. If omitted, returns all.
45
+ */
46
+ list(filter?: {
47
+ status?: TaskStatus | TaskStatus[];
48
+ }): TaskRecord[];
49
+ /** Retrieve a single task by id, or null if not found. */
50
+ get(id: string): TaskRecord | null;
51
+ /**
52
+ * Update a task's status in-place inside tasks.jsonl.
53
+ *
54
+ * Uses read→parse→update→rewrite to avoid corrupting the JSONL file.
55
+ * The rewrite is atomic (write-tmp-then-rename).
56
+ *
57
+ * @param taskId The id of the task to transition
58
+ * @param newStatus The target status
59
+ * @param extra Optional additional fields to merge into the task record
60
+ * @param event Optional TaskEvent to append to the history
61
+ * @throws {InboxError} if the task does not exist
62
+ */
63
+ transition(taskId: string, newStatus: TaskStatus, extra?: Partial<TaskRecord>, event?: TaskEvent): TaskRecord;
64
+ /** Convenience: mark a task as completed with a result. */
65
+ complete(taskId: string, result: TaskRecord["result"]): TaskRecord;
66
+ /** Convenience: mark a task as failed with a reason. */
67
+ fail(taskId: string, reason: string): TaskRecord;
68
+ /** Rewrite tasks.jsonl from the given task list. */
69
+ private _rewriteAll;
70
+ /** Force a full reindex. Call after manual edits to tasks.jsonl. */
71
+ reindex(): void;
72
+ /** Return counts of tasks grouped by status. */
73
+ stats(): Record<TaskStatus, number>;
74
+ }
75
+ //# sourceMappingURL=inbox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbox.d.ts","sourceRoot":"","sources":["../src/inbox.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAKpE,qBAAa,UAAW,SAAQ,KAAK;IACpC,YAAY,GAAG,EAAE,MAAM,EAGtB;CACD;AAID;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,SAAS;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,MAAM,CAA6B;IAE3C,YAAY,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,EAGlE;IAED,gEAAgE;IAChE,SAAS,IAAI,IAAI,CAIhB;IAID;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAc7B;IAID;;;;;OAKG;IACH,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,UAAU,GAAG,UAAU,EAAE,CAAA;KAAE,GAAG,UAAU,EAAE,CAoClE;IAID,0DAA0D;IAC1D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAGjC;IAID;;;;;;;;;;;OAWG;IACH,UAAU,CACT,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,UAAU,EACrB,KAAK,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,EAC3B,KAAK,CAAC,EAAE,SAAS,GACf,UAAU,CAqCZ;IAID,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,UAAU,CAWjE;IAED,wDAAwD;IACxD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAe/C;IAID,oDAAoD;IACpD,OAAO,CAAC,WAAW;IAUnB,oEAAoE;IACpE,OAAO,IAAI,IAAI,CAKd;IAID,gDAAgD;IAChD,KAAK,IAAI,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAOlC;CACD"}