agents 0.29.0 → 0.30.0

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 (35) hide show
  1. package/dist/browser/tools/pi.d.ts +6 -2
  2. package/dist/browser/tools/pi.js +6 -2
  3. package/dist/browser/tools/pi.js.map +1 -1
  4. package/dist/chat-sdk/index.js +1 -1
  5. package/dist/cli.js +46 -45
  6. package/dist/experimental/channels/web/index.js +1 -1
  7. package/dist/harness/container/index.js +1 -1
  8. package/dist/harness/opencode/index.js +1 -1
  9. package/dist/harness/pi/index.d.ts +57 -35
  10. package/dist/harness/pi/index.js +261 -119
  11. package/dist/harness/pi/index.js.map +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/lifecycle/index.js +1 -1
  14. package/dist/{lifecycle-ClT4DmF2.js → lifecycle-v1rKHV2C.js} +57 -17
  15. package/dist/lifecycle-v1rKHV2C.js.map +1 -0
  16. package/dist/mcp/index.js +1 -1
  17. package/dist/models/pi-ai/index.js +2 -2
  18. package/dist/models/pi-ai/index.js.map +1 -1
  19. package/dist/{src-6lr7MtM_.js → src-DfLzdwlK.js} +3 -3
  20. package/dist/{src-6lr7MtM_.js.map → src-DfLzdwlK.js.map} +1 -1
  21. package/dist/web/fetch/tools/pi.d.ts +10 -6
  22. package/dist/web/fetch/tools/pi.js +10 -6
  23. package/dist/web/fetch/tools/pi.js.map +1 -1
  24. package/dist/web/search/tools/pi.d.ts +10 -6
  25. package/dist/web/search/tools/pi.js +10 -6
  26. package/dist/web/search/tools/pi.js.map +1 -1
  27. package/dist/websockets/index.js +1 -1
  28. package/dist/{websockets-CqxnntmE.js → websockets-CyoeosdO.js} +2 -2
  29. package/dist/{websockets-CqxnntmE.js.map → websockets-CyoeosdO.js.map} +1 -1
  30. package/dist/workflows.js +1 -1
  31. package/docs/harnesses/pi.md +62 -28
  32. package/docs/lifecycle.md +7 -1
  33. package/docs/models-pi-ai.md +5 -1
  34. package/package.json +11 -6
  35. package/dist/lifecycle-ClT4DmF2.js.map +0 -1
@@ -1,30 +1,12 @@
1
1
  import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "../../classPrivateFieldGet2-DZBYAB34.js";
2
2
  import { t as _classPrivateMethodInitSpec } from "../../classPrivateMethodInitSpec-qMjJ6sHQ.js";
3
3
  import { t as LifecycleCapability } from "../../capability-DBAKc6UZ.js";
4
- import "../../lifecycle-ClT4DmF2.js";
4
+ import "../../lifecycle-v1rKHV2C.js";
5
+ import { isDurableObjectMemoryLimitReset } from "../../retries.js";
5
6
  import { Type } from "@earendil-works/pi-ai";
6
7
  import { LiveDoc, ROOT_CONVERSATION_ID, watchEvents } from "@earendil-works/pi-durable";
8
+ import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
7
9
  import { openDurableObjectSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/cloudflare";
8
- //#region src/harness/pi/context.ts
9
- /** chord's `BACKGROUND_CONTEXT`: no values, never cancelled. */
10
- const BACKGROUND_CONTEXT = Object.freeze({
11
- abortSignal: void 0,
12
- value: () => void 0,
13
- toString: () => "[Context pi-harness background]"
14
- });
15
- /**
16
- * chord's `withAbortSignal`: a context cancelled by either the parent's
17
- * signal or `signal`, carrying the parent's values.
18
- */
19
- function withAbortSignal(signal, parent) {
20
- const abortSignal = parent.abortSignal === void 0 ? signal : AbortSignal.any([parent.abortSignal, signal]);
21
- return Object.freeze({
22
- abortSignal,
23
- value: parent.value.bind(parent),
24
- toString: () => `${parent}.WithAbortSignal`
25
- });
26
- }
27
- //#endregion
28
10
  //#region src/harness/pi/session-store.ts
29
11
  const DEFAULT_PREFIX = "pi_";
30
12
  /** Open pi-durable's storage over this object's SQLite database. */
@@ -39,15 +21,10 @@ const BG = BACKGROUND_CONTEXT;
39
21
  /** The root session's id. */
40
22
  const ROOT_SESSION = String(ROOT_CONVERSATION_ID);
41
23
  /**
42
- * The three wake timings. They suit a real deployment; only tests change
43
- * them, with `setWakeTimingForTests`, so a suite does not sit on the real
24
+ * The wake timings. They suit a real deployment; only tests change them,
25
+ * with `setWakeTimingForTests`, so a suite does not sit on the real
44
26
  * heartbeat.
45
27
  *
46
- * A pi long wait further away than this is handed to the alarm, so the
47
- * alarm, not pi's in-memory timer, is what wakes the object.
48
- */
49
- const SLEEP_THRESHOLD_MS = 6e4;
50
- /**
51
28
  * Longest one wake waits on pi. It waits inside an alarm invocation, which
52
29
  * has a 15 minute wall-time limit, so a longer run is waited on across
53
30
  * several alarms.
@@ -59,13 +36,28 @@ const WAIT_BUDGET_MS = 10 * 6e4;
59
36
  * job is still due and its alarm restarts the object.
60
37
  */
61
38
  const HEARTBEAT_MS = 3e4;
39
+ /**
40
+ * How often a wait re-reads `pi.wake()` when the factory did not pass
41
+ * `onWake`, so nothing tells the harness when it changes.
42
+ */
43
+ const POLL_MS = 1e3;
44
+ /**
45
+ * The longest a wake job whose dispatch keeps failing waits before it tries
46
+ * again. Its delay starts at the heartbeat and doubles up to this.
47
+ */
48
+ const MAX_WAKE_BACKOFF_MS = 10 * 6e4;
49
+ /**
50
+ * Wake dispatches that failed in a row. Stored, so the backoff holds across
51
+ * restarts, and outside the job: a push that saved it in the job's payload
52
+ * could overwrite a submit's due-now push.
53
+ */
54
+ const WAKE_ERRORS_KEY = "pi-harness:wake-errors";
62
55
  const WAKE_FN = "wake";
63
- function wakeJobId(session) {
64
- return `pi-wake:${session}`;
65
- }
66
- function sessionOfJob(payload) {
67
- return typeof payload === "object" && payload !== null && "session" in payload && typeof payload.session === "string" ? payload.session : void 0;
68
- }
56
+ /**
57
+ * The one wake job. Before Pi 1.4 there was one per session, with ids
58
+ * `pi-wake:<session>`; a left-over one hands over to this one.
59
+ */
60
+ const WAKE_JOB_ID = "pi-wake";
69
61
  /** pi-durable's stored reference to a pi-ai model. */
70
62
  function modelRef(model) {
71
63
  return {
@@ -88,13 +80,18 @@ function signalContext(signal) {
88
80
  return signal ? withAbortSignal(signal, BG) : BG;
89
81
  }
90
82
  var _options = /* @__PURE__ */ new WeakMap();
91
- var _waits = /* @__PURE__ */ new WeakMap();
83
+ var _waiting = /* @__PURE__ */ new WeakMap();
84
+ var _changed = /* @__PURE__ */ new WeakMap();
92
85
  var _admitting = /* @__PURE__ */ new WeakMap();
93
- var _sleepThresholdMs = /* @__PURE__ */ new WeakMap();
86
+ var _onWakeWired = /* @__PURE__ */ new WeakMap();
94
87
  var _waitBudgetMs = /* @__PURE__ */ new WeakMap();
95
88
  var _heartbeatMs = /* @__PURE__ */ new WeakMap();
89
+ var _pollMs = /* @__PURE__ */ new WeakMap();
96
90
  var _opening = /* @__PURE__ */ new WeakMap();
91
+ var _lastClose = /* @__PURE__ */ new WeakMap();
92
+ var _closing = /* @__PURE__ */ new WeakMap();
97
93
  var _PiHarness_brand = /* @__PURE__ */ new WeakSet();
94
+ var _onWake = /* @__PURE__ */ new WeakMap();
98
95
  /**
99
96
  * pi-durable hosted in a Durable Object, behind a small harness interface:
100
97
  * `harness.prompt()`, `harness.sessions`, `harness.session(id)`. How a session reaches a
@@ -107,12 +104,14 @@ var _PiHarness_brand = /* @__PURE__ */ new WeakSet();
107
104
  *
108
105
  * What pi cannot do on a Durable Object is wake itself: its scheduler runs
109
106
  * in memory, and an evicted object has no memory. The harness is that wake,
110
- * with one Lifecycle job per session. Input goes to pi once, in `submit()`,
111
- * after the session's job is scheduled. The job waits while pi has live
112
- * tasks in the session, rescheduling itself as a heartbeat, and completes
113
- * when there are none. An eviction mid-run leaves the job due, so its alarm
114
- * restarts the object, pi reopens and resumes its own tasks, and the job
115
- * waits again.
107
+ * with one Lifecycle job driven by `pi.wake()`. Input goes to pi once, in
108
+ * `submit()`, after the job is scheduled. While pi has work running, the
109
+ * job waits on it, rescheduling itself as a heartbeat. While pi's tasks
110
+ * sleep, the job is due when the first one wakes; pi tells the harness
111
+ * through the `onWake` the factory passes to `Harness.open`. When pi has
112
+ * nothing to do, the job completes. An eviction mid-run leaves the job due,
113
+ * so its alarm restarts the object, pi reopens and resumes its own tasks,
114
+ * and the job waits again.
116
115
  *
117
116
  * @beta The API may change between releases.
118
117
  */
@@ -121,25 +120,54 @@ var PiHarness = class extends LifecycleCapability {
121
120
  super("pi-harness");
122
121
  _classPrivateMethodInitSpec(this, _PiHarness_brand);
123
122
  _classPrivateFieldInitSpec(this, _options, void 0);
124
- _classPrivateFieldInitSpec(this, _waits, /* @__PURE__ */ new Map());
125
- _classPrivateFieldInitSpec(this, _admitting, /* @__PURE__ */ new Map());
126
- _classPrivateFieldInitSpec(this, _sleepThresholdMs, SLEEP_THRESHOLD_MS);
123
+ _classPrivateFieldInitSpec(this, _waiting, void 0);
124
+ _classPrivateFieldInitSpec(this, _changed, void 0);
125
+ _classPrivateFieldInitSpec(this, _admitting, 0);
126
+ _classPrivateFieldInitSpec(this, _onWakeWired, false);
127
127
  _classPrivateFieldInitSpec(this, _waitBudgetMs, WAIT_BUDGET_MS);
128
128
  _classPrivateFieldInitSpec(this, _heartbeatMs, HEARTBEAT_MS);
129
+ _classPrivateFieldInitSpec(this, _pollMs, POLL_MS);
129
130
  _classPrivateFieldInitSpec(this, _opening, void 0);
131
+ _classPrivateFieldInitSpec(this, _lastClose, Promise.resolve());
132
+ _classPrivateFieldInitSpec(this, _closing, /* @__PURE__ */ new WeakSet());
133
+ _classPrivateFieldInitSpec(this, _onWake, (wake) => {
134
+ _classPrivateFieldSet2(_onWakeWired, this, true);
135
+ _classPrivateFieldGet2(_changed, this)?.call(this);
136
+ if (_classPrivateFieldGet2(_waiting, this)) return;
137
+ _assertClassBrand(_PiHarness_brand, this, _wakeFor).call(this, wake).catch((error) => _assertClassBrand(_PiHarness_brand, this, _wakeError).call(this, error));
138
+ });
130
139
  _classPrivateFieldSet2(_options, this, options);
131
140
  this.sessions = new PiSessions(this);
132
141
  }
133
142
  async onStart(_context) {
134
- const { pi } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
135
- const inspection = await pi.inspect(BG);
136
- const sessions = new Set(inspection.tasks.map((task) => String(task.record.conversationId)));
137
- for (const session of sessions) await _assertClassBrand(_PiHarness_brand, this, _wake).call(this, session);
143
+ await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
138
144
  }
139
145
  async onJob(context) {
140
146
  if (context.job.fn !== WAKE_FN) return;
141
- const session = sessionOfJob(context.job.payload);
142
- if (session !== void 0) return _assertClassBrand(_PiHarness_brand, this, _wakeStep).call(this, session);
147
+ let outcome;
148
+ if (context.job.id !== WAKE_JOB_ID) {
149
+ await _assertClassBrand(_PiHarness_brand, this, _wake).call(this);
150
+ outcome = void 0;
151
+ } else outcome = await _assertClassBrand(_PiHarness_brand, this, _wakeStep).call(this);
152
+ const kv = this.lifecycle.storage.kv;
153
+ if (kv.get(WAKE_ERRORS_KEY) !== void 0) kv.delete(WAKE_ERRORS_KEY);
154
+ return outcome;
155
+ }
156
+ /**
157
+ * A wake dispatch that failed through Lifecycle's retries, as when pi
158
+ * cannot be opened. Lifecycle would complete the job, and pi's work would
159
+ * never be woken again, so try again later: at the heartbeat, doubling
160
+ * while the failures continue, up to 10 minutes.
161
+ */
162
+ onJobError(context, error) {
163
+ if (context.job.fn !== WAKE_FN) return;
164
+ console.error(`pi wake job "${context.job.id}" failed after ${context.attempt} attempts; retrying with backoff`, error);
165
+ _assertClassBrand(_PiHarness_brand, this, _wakeError).call(this, error);
166
+ const kv = this.lifecycle.storage.kv;
167
+ const errors = (kv.get(WAKE_ERRORS_KEY) ?? 0) + 1;
168
+ kv.put(WAKE_ERRORS_KEY, errors);
169
+ const delay = Math.min(_classPrivateFieldGet2(_heartbeatMs, this) * 2 ** (errors - 1), MAX_WAKE_BACKOFF_MS);
170
+ return { rescheduleAt: Date.now() + delay };
143
171
  }
144
172
  /** Close pi's in-memory resources. Durable state is untouched. */
145
173
  async dispose() {
@@ -163,6 +191,13 @@ var PiHarness = class extends LifecycleCapability {
163
191
  abort(options = {}) {
164
192
  return this.session(options.session).abort(options.operationId);
165
193
  }
194
+ /**
195
+ * Run again the tasks pi's crash-loop guard parked in a session's work.
196
+ * False when nothing was parked.
197
+ */
198
+ retry(options = {}) {
199
+ return this.session(options.session).retry();
200
+ }
166
201
  wait(operationId, options = {}) {
167
202
  return this.session(options.session).wait(operationId, options.signal);
168
203
  }
@@ -174,10 +209,12 @@ var PiHarness = class extends LifecycleCapability {
174
209
  async pending(options = {}) {
175
210
  const { pi } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
176
211
  const id = options.session === void 0 ? void 0 : conversationId(options.session);
177
- return (await pi.inspect(BG)).submissions.filter((record) => record.requestId !== void 0 && (id === void 0 || record.conversationId === id)).map((record) => ({
212
+ const inspection = await pi.inspect(BG);
213
+ const parked = await _assertClassBrand(_PiHarness_brand, this, _parked).call(this, inspection);
214
+ return inspection.submissions.filter((record) => record.requestId !== void 0 && (id === void 0 || record.conversationId === id)).map((record) => ({
178
215
  operationId: record.requestId,
179
216
  session: String(record.conversationId),
180
- status: record.status === "queued" ? "queued" : "running"
217
+ status: record.status === "queued" ? "queued" : parked.some((task) => task.sessions.has(record.conversationId)) ? "parked" : "running"
181
218
  }));
182
219
  }
183
220
  /** The opened pi Harness, for anything the interface does not cover. */
@@ -200,30 +237,33 @@ var PiHarness = class extends LifecycleCapability {
200
237
  const operationId = options.operationId ?? crypto.randomUUID();
201
238
  const { storage } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
202
239
  const conversation = await this.conversation(session);
203
- _classPrivateFieldGet2(_admitting, this).set(session, (_classPrivateFieldGet2(_admitting, this).get(session) ?? 0) + 1);
204
- let accepted;
205
- try {
206
- await _assertClassBrand(_PiHarness_brand, this, _wake).call(this, session);
207
- accepted = await storage.submissionByRequest(conversation.id, operationId, BG) === void 0;
208
- await conversation.submit({
209
- type: "input",
210
- content: input,
211
- whenBusy: options.whenBusy ?? "followUp",
212
- requestId: operationId,
213
- ...options.entry ? { entry: options.entry } : {}
214
- }, BG);
215
- } finally {
216
- const left = (_classPrivateFieldGet2(_admitting, this).get(session) ?? 1) - 1;
217
- if (left === 0) _classPrivateFieldGet2(_admitting, this).delete(session);
218
- else _classPrivateFieldGet2(_admitting, this).set(session, left);
219
- }
220
- await _assertClassBrand(_PiHarness_brand, this, _wake).call(this, session);
221
240
  return {
222
241
  operationId,
223
242
  session,
224
- accepted
243
+ accepted: await _assertClassBrand(_PiHarness_brand, this, _admit).call(this, async () => {
244
+ const accepted = await storage.submissionByRequest(conversation.id, operationId, BG) === void 0;
245
+ await conversation.submit({
246
+ type: "input",
247
+ content: input,
248
+ whenBusy: options.whenBusy ?? "followUp",
249
+ requestId: operationId,
250
+ ...options.entry ? { entry: options.entry } : {}
251
+ }, BG);
252
+ return accepted;
253
+ })
225
254
  };
226
255
  }
256
+ /** @internal Run again the tasks pi's restart limit parked in a session. */
257
+ async retryParked(session) {
258
+ const { pi } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
259
+ const id = conversationId(session);
260
+ const parked = (await _assertClassBrand(_PiHarness_brand, this, _parked).call(this, await pi.inspect(BG))).filter((task) => task.sessions.has(id));
261
+ if (parked.length === 0) return false;
262
+ await _assertClassBrand(_PiHarness_brand, this, _admit).call(this, async () => {
263
+ for (const task of parked) await pi.retryTask(task.id, BG);
264
+ });
265
+ return true;
266
+ }
227
267
  /** @internal Withdraw a queued input, or abort the run it joined. */
228
268
  async withdraw(session, operationId) {
229
269
  const { pi, storage } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
@@ -256,94 +296,188 @@ var PiHarness = class extends LifecycleCapability {
256
296
  };
257
297
  }
258
298
  };
259
- /** Schedule the session's wake job now, or pull it forward. */
260
- function _wake(session, time = Date.now()) {
299
+ /**
300
+ * Hand pi work inside the wake. The wake is scheduled first, so if the
301
+ * object dies after pi has the work, that job restarts it; while `hand`
302
+ * is in flight the step will not complete. The wake steps again after, so
303
+ * it sees the work even if it just completed.
304
+ */
305
+ async function _admit(hand) {
306
+ var _this$admitting;
307
+ _classPrivateFieldSet2(_admitting, this, (_this$admitting = _classPrivateFieldGet2(_admitting, this), _this$admitting++, _this$admitting));
308
+ let result;
309
+ try {
310
+ await _assertClassBrand(_PiHarness_brand, this, _wake).call(this);
311
+ result = await hand();
312
+ } finally {
313
+ var _this$admitting3;
314
+ _classPrivateFieldSet2(_admitting, this, (_this$admitting3 = _classPrivateFieldGet2(_admitting, this), _this$admitting3--, _this$admitting3));
315
+ }
316
+ await _assertClassBrand(_PiHarness_brand, this, _wake).call(this);
317
+ return result;
318
+ }
319
+ /** Schedule the wake job now, or at `time`. */
320
+ function _wake(time = Date.now()) {
261
321
  return this.lifecycle.jobs.push({
262
- id: wakeJobId(session),
322
+ id: WAKE_JOB_ID,
263
323
  fn: WAKE_FN,
264
324
  time,
265
- payload: { session },
266
325
  singleflight: true,
267
326
  recoveryLoop: true
268
327
  });
269
328
  }
329
+ /** Schedule the wake job for what `pi.wake()` says, if pi needs one. */
330
+ async function _wakeFor(wake) {
331
+ if (wake.active) await _assertClassBrand(_PiHarness_brand, this, _wake).call(this);
332
+ else if (wake.at !== void 0) await _assertClassBrand(_PiHarness_brand, this, _wake).call(this, wake.at);
333
+ }
270
334
  /**
271
- * One run of a session's wake job. It never admits or replays anything:
272
- * it waits while pi has live tasks in the session and completes when it
273
- * has none. pi does all the work in between.
335
+ * One run of the wake job. It never admits or replays anything: it reads
336
+ * `pi.wake()`, which is all of pi's sessions at once. Running work is
337
+ * waited on; a sleep reschedules the job for when it ends; no work
338
+ * completes the job. pi does all the work in between.
274
339
  */
275
- async function _wakeStep(session) {
340
+ async function _wakeStep() {
276
341
  const heartbeat = { rescheduleAt: Date.now() + _classPrivateFieldGet2(_heartbeatMs, this) };
277
- if (_classPrivateFieldGet2(_waits, this).has(session)) return heartbeat;
342
+ if (_classPrivateFieldGet2(_waiting, this)) return heartbeat;
278
343
  const { pi } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
279
- const conversation = await pi.conversation(conversationId(session), BG);
280
- if (!conversation) return void 0;
281
- const tasks = (await pi.inspect(BG)).tasks.filter((task) => task.record.conversationId === conversation.id);
282
- if (tasks.length === 0) return _classPrivateFieldGet2(_admitting, this).has(session) ? heartbeat : void 0;
283
- const wakeAt = await _assertClassBrand(_PiHarness_brand, this, _longWait).call(this, pi, conversation.id, BG);
284
- if (wakeAt !== void 0) return { rescheduleAt: wakeAt };
285
- if (tasks.every((task) => task.record.background)) return heartbeat;
286
- const wait = _assertClassBrand(_PiHarness_brand, this, _waitForIdle).call(this, conversation).finally(() => {
287
- _classPrivateFieldGet2(_waits, this).delete(session);
288
- _assertClassBrand(_PiHarness_brand, this, _wake).call(this, session);
289
- });
290
- _classPrivateFieldGet2(_waits, this).set(session, wait);
291
- this.lifecycle.trackAlarmWork(wait);
292
- return heartbeat;
344
+ pi.resume();
345
+ const wake = pi.wake();
346
+ if (wake.active) {
347
+ const wait = _assertClassBrand(_PiHarness_brand, this, _waitWhileActive).call(this, pi).then((ended) => {
348
+ _classPrivateFieldSet2(_waiting, this, void 0);
349
+ if (ended) _assertClassBrand(_PiHarness_brand, this, _wake).call(this);
350
+ }, (error) => {
351
+ _classPrivateFieldSet2(_waiting, this, void 0);
352
+ throw error;
353
+ });
354
+ _classPrivateFieldSet2(_waiting, this, wait);
355
+ if (!this.lifecycle.trackAlarmWork(wait)) wait.catch(() => {});
356
+ return heartbeat;
357
+ }
358
+ if (_classPrivateFieldGet2(_admitting, this) > 0) return heartbeat;
359
+ if (wake.at !== void 0) return { rescheduleAt: wake.at };
293
360
  }
294
- async function _waitForIdle(conversation) {
295
- const budget = new AbortController();
296
- const timer = setTimeout(() => budget.abort(), _classPrivateFieldGet2(_waitBudgetMs, this));
361
+ /**
362
+ * Wait while pi has work running, up to the wait budget. With `onWake`, a
363
+ * change ends each sleep early, and so does pi closing; without it, the
364
+ * sleep is `pollMs`. Whether the wait ended, with pi idle or at the
365
+ * budget, rather than failing. Rejects with a memory-limit reset.
366
+ */
367
+ async function _waitWhileActive(pi) {
368
+ const deadline = Date.now() + _classPrivateFieldGet2(_waitBudgetMs, this);
297
369
  try {
298
- await conversation.waitForIdle(withAbortSignal(budget.signal, BG));
370
+ while (pi.wake().active) {
371
+ const left = deadline - Date.now();
372
+ if (left <= 0) return true;
373
+ await new Promise((resolve) => {
374
+ const timer = setTimeout(resolve, _classPrivateFieldGet2(_onWakeWired, this) ? left : Math.min(left, _classPrivateFieldGet2(_pollMs, this)));
375
+ _classPrivateFieldSet2(_changed, this, () => {
376
+ clearTimeout(timer);
377
+ resolve();
378
+ });
379
+ });
380
+ _classPrivateFieldSet2(_changed, this, void 0);
381
+ }
382
+ return true;
299
383
  } catch (error) {
300
- if (!budget.signal.aborted) this.lifecycle.events.emit("pi:wake_error", {
301
- session: String(conversation.id),
302
- error: error instanceof Error ? error.message : String(error)
303
- });
304
- } finally {
305
- clearTimeout(timer);
384
+ if (!_classPrivateFieldGet2(_closing, this).has(pi)) _assertClassBrand(_PiHarness_brand, this, _wakeError).call(this, error);
385
+ if (isDurableObjectMemoryLimitReset(error)) throw error;
386
+ return false;
306
387
  }
307
388
  }
389
+ function _wakeError(error) {
390
+ this.lifecycle.events.emit("pi:wake_error", { error: error instanceof Error ? error.message : String(error) });
391
+ }
308
392
  /**
309
393
  * pi, opened. Waits for Lifecycle startup first, so pi is only ever
310
394
  * opened inside it: startup holds the input gate (`blockConcurrencyWhile`)
311
395
  * and awaits the open in `onStart`, so an open begun before startup would
312
396
  * be awaited behind a closed gate, with its timers and I/O held back, and
313
397
  * never finish.
398
+ *
399
+ * pi stays open until `dispose()`, or until it closes on its own: any
400
+ * storage error fails pi's Session for good. Then the next call, or the
401
+ * wake job's next step, opens pi again once the failed pi has closed, and
402
+ * pi recovers from what it had committed. Closing waits for running task
403
+ * code to return, so code that ignores its abort signal delays the reopen.
404
+ * An open that fails is not kept either, so the next call tries again.
314
405
  */
315
406
  async function _open() {
316
407
  await this.lifecycle.ready();
317
- _classPrivateFieldGet2(_opening, this) ?? _classPrivateFieldSet2(_opening, this, _assertClassBrand(_PiHarness_brand, this, _doOpen).call(this).catch((error) => {
318
- _classPrivateFieldSet2(_opening, this, void 0);
319
- throw error;
320
- }));
408
+ if (_classPrivateFieldGet2(_opening, this) === void 0) {
409
+ const opening = _assertClassBrand(_PiHarness_brand, this, _doOpen).call(this, (pi, closed) => _assertClassBrand(_PiHarness_brand, this, _piClosing).call(this, opening, pi, closed)).catch((error) => {
410
+ if (_classPrivateFieldGet2(_opening, this) === opening) _classPrivateFieldSet2(_opening, this, void 0);
411
+ throw error;
412
+ });
413
+ _classPrivateFieldSet2(_opening, this, opening);
414
+ }
321
415
  return _classPrivateFieldGet2(_opening, this);
322
416
  }
323
- async function _doOpen() {
417
+ async function _doOpen(onClose) {
418
+ await _classPrivateFieldGet2(_lastClose, this);
324
419
  const storage = await openPiSessionStore(this.lifecycle.storage);
325
420
  const pi = await _classPrivateFieldGet2(_options, this).harness({
326
421
  storage,
327
- context: BG
422
+ context: BG,
423
+ onWake: _classPrivateFieldGet2(_onWake, this)
328
424
  });
329
- await pi.root(BG, { agent: this.agentDefaults() });
330
- pi.resume();
425
+ try {
426
+ const closed = pi.closed;
427
+ pi.subscribeClose(() => onClose(pi, closed));
428
+ await pi.root(BG, { agent: this.agentDefaults() });
429
+ pi.resume();
430
+ await new Promise((resolve) => setTimeout(resolve, 0));
431
+ if (!_classPrivateFieldGet2(_onWakeWired, this)) console.warn("PiHarness: the harness factory did not pass onWake to Harness.open, so pi's timers keep this Durable Object in memory while a task sleeps. Pass it: harness: ({ storage, context, onWake }) => Harness.open(storage, { ...options, onWake }, context)");
432
+ await _assertClassBrand(_PiHarness_brand, this, _wakeFor).call(this, pi.wake());
433
+ } catch (error) {
434
+ pi.close(BG).catch(() => {});
435
+ throw error;
436
+ }
331
437
  return {
332
438
  pi,
333
439
  storage
334
440
  };
335
441
  }
442
+ /**
443
+ * pi began closing: on its own, as a storage error makes it, or by
444
+ * `dispose()`.
445
+ */
446
+ function _piClosing(opening, pi, closed) {
447
+ _classPrivateFieldGet2(_closing, this).add(pi);
448
+ _classPrivateFieldGet2(_changed, this)?.call(this);
449
+ if (_classPrivateFieldGet2(_opening, this) !== opening) return;
450
+ _classPrivateFieldSet2(_opening, this, void 0);
451
+ _classPrivateFieldSet2(_lastClose, this, closed);
452
+ }
453
+ /**
454
+ * Tasks pi's restart limit parked, each with the sessions whose work it
455
+ * is: its own, and every session that owns that one, such as the session
456
+ * of a tool that started a subagent.
457
+ */
458
+ async function _parked(inspection) {
459
+ const { storage } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
460
+ const parked = [];
461
+ for (const { record, state } of inspection.tasks) {
462
+ if (state.kind !== "blocked" || state.reason !== "restart_limit") continue;
463
+ const sessions = /* @__PURE__ */ new Set();
464
+ let id = record.conversationId;
465
+ while (id !== void 0 && !sessions.has(id)) {
466
+ sessions.add(id);
467
+ id = (await storage.conversation(id, BG))?.owner?.conversationId;
468
+ }
469
+ parked.push({
470
+ id: record.id,
471
+ sessions
472
+ });
473
+ }
474
+ return parked;
475
+ }
336
476
  async function _findSubmission(id, operationId, context) {
337
477
  const { pi, storage } = await _assertClassBrand(_PiHarness_brand, this, _open).call(this);
338
478
  const record = await storage.submissionByRequest(id, operationId, context);
339
479
  return record ? pi.submission(record.id, context) : void 0;
340
480
  }
341
- /** When pi's current wait is far enough away to hand to the alarm. */
342
- async function _longWait(pi, id, context) {
343
- const live = await pi.snapshot(LiveDoc, id, context);
344
- const at = live?.generation?.deferred?.pollAt ?? live?.generation?.retry?.at;
345
- return at !== void 0 && at - Date.now() > _classPrivateFieldGet2(_sleepThresholdMs, this) ? at : void 0;
346
- }
347
481
  async function _result(session, operationId, settled, context) {
348
482
  if (settled.status === "unanswered") return {
349
483
  operationId,
@@ -407,6 +541,14 @@ var PiSession = class {
407
541
  await (await _classPrivateFieldGet2(_harness, this).conversation(this.id)).abort(BG);
408
542
  return true;
409
543
  }
544
+ /**
545
+ * Run again the tasks pi's crash-loop guard (`settings.restartLimit`)
546
+ * parked in this session's work, including subagent sessions it owns.
547
+ * False when nothing was parked.
548
+ */
549
+ retry() {
550
+ return _classPrivateFieldGet2(_harness, this).retryParked(this.id);
551
+ }
410
552
  /** Start a new context, optionally from a handoff note. */
411
553
  async reset(handoff) {
412
554
  await (await _classPrivateFieldGet2(_harness, this).conversation(this.id)).reset(handoff, BG);