@hachej/boring-agent 0.4.27 → 0.4.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -460,7 +460,7 @@ const delegates = [
460
460
  ];
461
461
  ```
462
462
 
463
- **Schedule.** `@hachej/boring-agent/schedule` exports `createScheduleExtension({ now?, onDeadline?, maxDelaySeconds? })`, one native extension with `schedule({ instruction, delaySeconds | at, operationId? })`, `list_schedules()` and `cancel_schedule({ id })`, plus the delivery task `ScheduledDelivery`. A schedule is a native Pi task owned by the conversation as background work: it sleeps on the Harness clock (`runtime.sleep`) and then submits the instruction to its conversation as a follow-up input (entry kind `boring.schedule.delivery`, request id = schedule id, so a replayed delivery never submits twice). Pi's scheduler runs it and a restart resumes it; there is no second scheduler. Pending schedules live in the conversation document `ConversationSchedules`, so a conversation lists and cancels only its own; a cancelled schedule's task wakes and ends without delivering. `schedule` is idempotent on `operationId` (default: the tool task, so a replayed call returns the schedule it made). Hosts schedule outside a tool with `addSchedule(tx, conversationId, { operationId, instruction, runAt })`, `cancelSchedule(tx, conversationId, id)` and `listSchedules(harness, conversationId, context)`. Pass the Harness's own clock as `now`. A host with one wake-up timer (a Durable Object alarm) computes the earliest deadline from `harness.inspect()` with `scheduledDeadline(task)` and moves the timer earlier from `onDeadline`; its alarm handler only reopens or resumes the Harness, and Pi delivers what is due.
463
+ **Schedule.** `@hachej/boring-agent/schedule` exports `createScheduleExtension({ now?, onDeadline?, maxDelaySeconds? })`, one native extension with `schedule({ instruction, delaySeconds | at, operationId? })`, `list_schedules()` and `cancel_schedule({ id })`, plus the delivery task `ScheduledDelivery`. A schedule is a native Pi task owned by the conversation as background work: it waits on the Harness clock (`runtime.setTimer`; Pi 1.4 removed `runtime.sleep`) and then submits the instruction to its conversation as a follow-up input (entry kind `boring.schedule.delivery`, request id = schedule id, so a replayed delivery never submits twice). Pi's scheduler runs it and a restart resumes it; there is no second scheduler. Pending schedules live in the conversation document `ConversationSchedules`, so a conversation lists and cancels only its own; a cancelled schedule's task wakes and ends without delivering. `schedule` is idempotent on `operationId` (default: the tool task, so a replayed call returns the schedule it made). Hosts schedule outside a tool with `addSchedule(tx, conversationId, { operationId, instruction, runAt })`, `cancelSchedule(tx, conversationId, id)` and `listSchedules(harness, conversationId, context)`. Pass the Harness's own clock as `now`. A host with one wake-up timer (a Durable Object alarm) computes the earliest deadline from `harness.inspect()` with `scheduledDeadline(task)` and moves the timer earlier from `onDeadline`; its alarm handler only reopens or resumes the Harness, and Pi delivers what is due.
464
464
 
465
465
  **Storage conformance.** Boring ships no Pi `Storage` implementation. `test/packages/pi-storage-conformance.test.mjs` runs Pi's own suite (`createStorageConformance`, what `registerStorageConformance` registers with Vitest) on every storage the repository configures: `openPiStorage` (shared/pi-storage.mjs) on the local-disk and network-file-system presets, Pi's `SqliteStorage` over `openBrowserSqlite`, and Pi's `openDurableObjectSqliteStorage` on a SQLite-backed Durable Object in workerd through miniflare (the version the pinned wrangler resolves).
466
466
 
@@ -551,3 +551,7 @@ Reply placeholders such as `{note.title}` accept actual strings, finite numbers
551
551
  Hosts must explicitly configure existing conversations when adopting this virtual model; deployment alone does not change their persisted selection. Keep the action's tool and result contract compatible while native work is pending. Changing definitions during an outstanding action is not a version-pinning guarantee.
552
552
 
553
553
  Evidence: `node --test test/packages/agent-actions.test.mjs` runs the public package through a native Harness, SQLite reopen, native tool gates and scripted models. `npm run test:definitions-consumer -- --actions` repeats these tests and checks strict installed declarations. Real-model speed and decision accuracy remain consumer evals, not claims of these scripted tests.
554
+
555
+ ### Deferred action context
556
+
557
+ `withActionModels(models, { resolve, prepareFullContext })` optionally prepares context only before a full-model fallback. The asynchronous callback receives the native model context and request options, including cancellation. Successful extraction and automatic replies never invoke it. A preparation failure fails the generation without sending unprepared context. Hosts must install required guards before routing and preserve their native request/task ownership when deferring memory; this callback does not create background work or persist anything.
package/dist/actions.d.ts CHANGED
@@ -26,6 +26,8 @@ export interface StructuredAction {
26
26
  export interface ActionModelsOptions {
27
27
  /** Resolve trusted configuration; this never registers tools or grants permissions. */
28
28
  readonly resolve: (id: string) => StructuredAction | undefined;
29
+ /** Prepare memory only when a real full-model request is needed; errors fail closed. */
30
+ readonly prepareFullContext?: (context: Parameters<Models['streamSimple']>[1], options: Parameters<Models['streamSimple']>[2]) => Promise<Parameters<Models['streamSimple']>[1]>;
29
31
  readonly now?: () => number;
30
32
  }
31
33
  /** Substitute actual scalar results only. Missing values cannot become invented confirmations. */
package/dist/actions.js CHANGED
@@ -72,8 +72,12 @@ export function withActionModels(models, options) {
72
72
  const action = resolve(model.id);
73
73
  const original = required(action.model);
74
74
  const out = createAssistantMessageEventStream();
75
- const full = async () => { for await (const event of models.streamSimple(original, context, requestOptions))
76
- out.push(event); };
75
+ const full = async () => {
76
+ const prepared = options.prepareFullContext ? await options.prepareFullContext(context, requestOptions) : context;
77
+ requestOptions?.signal?.throwIfAborted();
78
+ for await (const event of models.streamSimple(original, prepared, requestOptions))
79
+ out.push(event);
80
+ };
77
81
  const emit = (message) => {
78
82
  if (message.stopReason === 'error' || message.stopReason === 'aborted')
79
83
  out.push({ type: 'error', reason: message.stopReason, error: message });
package/dist/ask-user.js CHANGED
@@ -6,7 +6,7 @@ export const ASK_USER_TOOL = 'ask_user';
6
6
  export const ASK_USER_MAX_ANSWER = 2000;
7
7
  /** One member per assistant entry and tool call, in the asking conversation. */
8
8
  const documents = defineDocFamily({
9
- kind: 'boring.ask-user.question', version: 1, scope: 'conversation', history: 'latest', fork: 'initial', family: true,
9
+ kind: 'boring.ask-user.question', checkpointWhen: () => false, version: 1, scope: 'conversation', history: 'latest', fork: 'initial', family: true,
10
10
  initial: () => ({ question: null }),
11
11
  });
12
12
  /** Bind an answer to the exact assistant entry that asked it. */
@@ -11,7 +11,7 @@ import { AmbiguousCall, UnknownCall, callBinding, callBindingId, ownBindingKey,
11
11
  export const BROWSER_TASK_MAX_ANSWER = 16_384;
12
12
  /** One member per assistant entry and tool call, in the calling conversation. */
13
13
  const documents = defineDocFamily({
14
- kind: 'boring.browser-task', version: 1, scope: 'conversation', history: 'latest', fork: 'initial', family: true,
14
+ kind: 'boring.browser-task', checkpointWhen: () => false, version: 1, scope: 'conversation', history: 'latest', fork: 'initial', family: true,
15
15
  initial: () => ({ task: null }),
16
16
  });
17
17
  /** The answer ID of a browser task call: the same `[assistantEntryId, callId]` binding as `askUserQuestionId`. */
@@ -3,7 +3,7 @@ import { RequestGuardError, hasJsonContentType, readJsonBody } from '@hachej/bor
3
3
  import { MENTION_FILE_PREFIX } from './mentions.js';
4
4
  /** The native document. `fork: 'initial'`: a fork made by `fork()` gets its own metadata in the creating commit; any other fork starts unmanaged. */
5
5
  export const conversationMetadata = defineDoc({
6
- kind: 'boring.conversation.metadata', version: 1, scope: 'conversation', history: 'latest', fork: 'initial',
6
+ kind: 'boring.conversation.metadata', checkpointWhen: () => false, version: 1, scope: 'conversation', history: 'latest', fork: 'initial',
7
7
  initial: () => ({ owner: '', title: null, createdAt: 0, updatedAt: 0, lastMessage: null, archived: false, deletedAt: null }),
8
8
  });
9
9
  const managed = (meta, owner) => meta !== undefined && meta.owner !== '' && meta.deletedAt === null && (owner === undefined || meta.owner === owner);
@@ -4,7 +4,7 @@ import { createEditTool, createReadTool, createWriteTool } from '@earendil-works
4
4
  import { asWorkspaceResolver, decorate, workspaceFor } from './workspaces.js';
5
5
  /** Conversation document: workspace and path (relative to the workspace root; see `baselineKey`) to the revision this conversation last read or wrote. */
6
6
  export const lastReadRevisions = defineDoc({
7
- kind: 'boring.files.last-read', version: 1, scope: 'conversation', history: 'latest', fork: 'initial',
7
+ kind: 'boring.files.last-read', checkpointWhen: () => false, version: 1, scope: 'conversation', history: 'latest', fork: 'initial',
8
8
  initial: () => ({ revisions: {} }),
9
9
  });
10
10
  /** The baseline key of `path` in `workspace` (its `id`, or its provider id): `<workspace>:<path>`. */
@@ -15,26 +15,26 @@ export { DEFAULTS as OPTCHAT_DEFAULTS, PLACEHOLDER as OPTCHAT_PLACEHOLDER, compa
15
15
  export { OptChatLog, OptChatTree, advanceFold, emptyFold, leavesOf, renderView, zoomText, dateText } from './optchat-core.js';
16
16
  /** Summaries of one conversation, in shards of 64 nodes per level (`<level>.<shard>`). A fork starts with the current ones; fingerprints reject any that no longer match. */
17
17
  export const OptChatNodes = defineDocFamily({
18
- kind: 'boring.memory.optchat.nodes', version: 1, family: true, scope: 'conversation', history: 'latest', fork: 'current', initial: () => ({ nodes: {} }),
18
+ kind: 'boring.memory.optchat.nodes', checkpointWhen: () => false, version: 1, family: true, scope: 'conversation', history: 'latest', fork: 'current', initial: () => ({ nodes: {} }),
19
19
  });
20
20
  /** Seed memories, stored once per conversation and inherited by forks. They precede the Pi transcript in the memory only. */
21
21
  export const OptChatNotes = defineDoc({
22
- kind: 'boring.memory.optchat.notes', version: 1, scope: 'conversation', history: 'latest', fork: 'current',
22
+ kind: 'boring.memory.optchat.notes', checkpointWhen: () => false, version: 1, scope: 'conversation', history: 'latest', fork: 'current',
23
23
  initial: () => ({ seeded: false, notes: [] }),
24
24
  });
25
25
  /** Memory-only tombstones. Values are leaf counts, never the forgotten content. Pi's transcript is unchanged. */
26
26
  export const OptChatEdits = defineDoc({
27
- kind: 'boring.memory.optchat.edits', version: 1, scope: 'conversation', history: 'latest', fork: 'current',
27
+ kind: 'boring.memory.optchat.edits', checkpointWhen: () => false, version: 1, scope: 'conversation', history: 'latest', fork: 'current',
28
28
  initial: () => ({ revision: 0, forgotten: {} }),
29
29
  });
30
30
  /** Only the emitted tiling and fingerprints are retained, never another copy of message or summary text. */
31
31
  const OptChatFold = defineDocFamily({
32
- kind: 'boring.memory.optchat.fold', version: 1, family: true, scope: 'conversation', history: 'latest', fork: 'current',
32
+ kind: 'boring.memory.optchat.fold', checkpointWhen: () => false, version: 1, family: true, scope: 'conversation', history: 'latest', fork: 'current',
33
33
  initial: () => ({ configuration: '', revision: -1, fingerprints: [], fold: null }),
34
34
  });
35
35
  /** Compactor bookkeeping of one conversation: calls, spend and the last failure of each node, including its retry streak. */
36
36
  export const OptChatState = defineDoc({
37
- kind: 'boring.memory.optchat.state', version: 1, scope: 'conversation', history: 'latest', fork: 'initial',
37
+ kind: 'boring.memory.optchat.state', checkpointWhen: () => false, version: 1, scope: 'conversation', history: 'latest', fork: 'initial',
38
38
  initial: () => ({ calls: 0, usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 }, failures: {} }),
39
39
  });
40
40
  const SHARD = 64;
@@ -3,7 +3,7 @@
3
3
  * native shapes produced them. New records take the current release; records written under an earlier reviewed release stay
4
4
  * readable, because the native conversation and tool shapes they refer to did not change between them.
5
5
  */
6
- export declare const NATIVE_VERSION = "pi-durable@1.2.0";
6
+ export declare const NATIVE_VERSION = "pi-durable@1.4.0";
7
7
  export type NativeVersion = 'pi-durable@1.0.1' | 'pi-durable@1.1.0' | typeof NATIVE_VERSION;
8
8
  export declare const isNativeVersion: (value: unknown) => value is NativeVersion;
9
9
  //# sourceMappingURL=native-version.d.ts.map
@@ -3,5 +3,5 @@
3
3
  * native shapes produced them. New records take the current release; records written under an earlier reviewed release stay
4
4
  * readable, because the native conversation and tool shapes they refer to did not change between them.
5
5
  */
6
- export const NATIVE_VERSION = 'pi-durable@1.2.0';
6
+ export const NATIVE_VERSION = 'pi-durable@1.4.0';
7
7
  export const isNativeVersion = (value) => value === NATIVE_VERSION || value === 'pi-durable@1.1.0' || value === 'pi-durable@1.0.1';
package/dist/questions.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ReadAfterWrite, defineDocFamily, defineExtension, defineTask } from '@earendil-works/pi-durable';
2
+ import { waitUntil } from './wait-until.js';
2
3
  function sameRef(left, right) {
3
4
  return left.runtimeId === right.runtimeId && left.conversationId === right.conversationId
4
5
  && left.taskId === right.taskId && left.questionId === right.questionId && left.scopeId === right.scopeId
@@ -11,7 +12,7 @@ export function createQuestions(options) {
11
12
  throw new TypeError('A stable runtime identity is required');
12
13
  const now = options.now ?? Date.now;
13
14
  const documents = defineDocFamily({
14
- kind: 'boring.questions.record', version: 1, scope: 'session', family: true,
15
+ kind: 'boring.questions.record', checkpointWhen: () => false, version: 1, scope: 'session', family: true,
15
16
  initial: () => ({ question: null }),
16
17
  });
17
18
  const key = (ref) => JSON.stringify([runtimeId, ref.scopeId, ref.questionId]);
@@ -40,11 +41,17 @@ export function createQuestions(options) {
40
41
  watch.start(async (value) => { if (value?.question?.state.kind !== 'pending')
41
42
  resolve(); });
42
43
  });
43
- await Promise.race([
44
- changed,
45
- runtime.sleep(Date.parse(watch.value.question.ref.expiresAt), context),
46
- watch.closed.then(() => { throw new Error('Question observation closed'); }),
47
- ]);
44
+ const expiry = waitUntil(runtime, Date.parse(watch.value.question.ref.expiresAt), context);
45
+ try {
46
+ await Promise.race([
47
+ changed,
48
+ expiry.done,
49
+ watch.closed.then(() => { throw new Error('Question observation closed'); }),
50
+ ]);
51
+ }
52
+ finally {
53
+ expiry.disarm();
54
+ }
48
55
  }
49
56
  await runtime.commit(async (tx) => {
50
57
  const doc = await tx.doc(documents, running.input.key, null);
package/dist/room.js CHANGED
@@ -118,8 +118,8 @@ export function wakeFor(entry, room) {
118
118
  return room.people.length === 1 && room.people[0] === author && room.agents.includes('agent:pm') ? [wake('agent:pm')] : [];
119
119
  }
120
120
  /** Conversation names to native ids, and each card's first entry (an action's target). */
121
- const RoomIndex = defineDoc({ kind: 'boring.room.index', version: 1, scope: 'session', initial: () => ({ conversations: {}, cards: {} }) });
122
- const RoomWakes = defineDoc({ kind: 'boring.room.wakes', version: 1, scope: 'session', initial: () => ({ open: [] }) });
121
+ const RoomIndex = defineDoc({ kind: 'boring.room.index', checkpointWhen: () => false, version: 1, scope: 'session', initial: () => ({ conversations: {}, cards: {} }) });
122
+ const RoomWakes = defineDoc({ kind: 'boring.room.wakes', checkpointWhen: () => false, version: 1, scope: 'session', initial: () => ({ open: [] }) });
123
123
  const REFUSED = Object.freeze({ refused: 'no-open-wake' });
124
124
  const copyWake = (wake) => ({ ...wake });
125
125
  /** The room over one Harness that never runs a model. Membership, roles and rates are the host's, before it calls. */
@@ -264,7 +264,7 @@ const framesOf = (entries) => entries.flatMap(entry => (entry.model ?? []).flatM
264
264
  return [];
265
265
  return message.content.flatMap((part) => part.type === 'text' && part.text ? [{ type: 'text', text: part.text }] : part.type === 'toolCall' ? [{ type: 'step', label: part.name, status: 'running' }] : []);
266
266
  }));
267
- const Turns = defineDoc({ kind: 'boring.room.turns', version: 1, scope: 'conversation', history: 'latest', fork: 'initial', initial: () => ({ owed: [] }) });
267
+ const Turns = defineDoc({ kind: 'boring.room.turns', checkpointWhen: () => false, version: 1, scope: 'conversation', history: 'latest', fork: 'initial', initial: () => ({ owed: [] }) });
268
268
  /** The agent's side of a room, over its own conversation in its own home. Delivery to the port is at least once. */
269
269
  export function createRoomTurn(options) {
270
270
  const { conversation, port } = options;
@@ -9,7 +9,7 @@ const INTERRUPTED = new Set(['aborted', 'reset', 'stale']);
9
9
  * Defined without `history` and `fork`, exactly as the homes that ran this protocol before persisted it: pi-durable refuses a token
10
10
  * whose semantics differ from the stored record, so adding them here would break every existing run store.
11
11
  */
12
- export const ResultBudget = defineDoc({ kind: 'boring.result-budget', version: 1, scope: 'conversation', initial: () => ({ misses: 0 }) });
12
+ export const ResultBudget = defineDoc({ kind: 'boring.result-budget', checkpointWhen: () => false, version: 1, scope: 'conversation', initial: () => ({ misses: 0 }) });
13
13
  /** Parse `result:` of a card. `legacy` is the card's `tool.json`, whose input is the default schema. Throws a plain Error naming the agent. */
14
14
  export function resultConfig(value, legacy, name) {
15
15
  if (value !== undefined && (!value || typeof value !== 'object' || Array.isArray(value)))
@@ -33,7 +33,7 @@ export declare const ConversationSchedules: import("@earendil-works/pi-durable")
33
33
  next: number;
34
34
  }>;
35
35
  /**
36
- * The native durable task behind one schedule: it sleeps on the Harness clock (`runtime.sleep`), then delivers the instruction to
36
+ * The native durable task behind one schedule: it waits on the Harness clock (`runtime.setTimer`), then delivers the instruction to
37
37
  * its conversation as a follow-up input whose request id is the schedule id, so a replayed delivery never submits twice. Owned by
38
38
  * the conversation as background work: a person stopping the current turn does not cancel it. No second scheduler: Pi's own
39
39
  * scheduler runs it, and a restart resumes it.
package/dist/schedule.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { defineDoc, defineExtension, defineTask, defineTool } from '@earendil-works/pi-durable';
2
2
  import { Type } from '@earendil-works/pi-ai';
3
3
  import { withAbortSignal } from '@earendil-works/chord/context';
4
+ import { waitUntil } from './wait-until.js';
4
5
  export const SCHEDULE_TOOL = 'schedule';
5
6
  export const LIST_SCHEDULES_TOOL = 'list_schedules';
6
7
  export const CANCEL_SCHEDULE_TOOL = 'cancel_schedule';
@@ -11,7 +12,7 @@ const MAX_INSTRUCTION = 4000;
11
12
  const TIMEZONE_SUFFIX = /(?:Z|[+-]\d{2}:\d{2})$/i;
12
13
  /** Pending schedules of one conversation. Forks start empty: a schedule belongs to the conversation that made it. */
13
14
  export const ConversationSchedules = defineDoc({
14
- kind: 'boring.schedules', version: 1, scope: 'conversation', history: 'latest', fork: 'initial', initial: () => ({ items: [], next: 1 }),
15
+ kind: 'boring.schedules', checkpointWhen: () => false, version: 1, scope: 'conversation', history: 'latest', fork: 'initial', initial: () => ({ items: [], next: 1 }),
15
16
  });
16
17
  const remove = (items, id) => { const index = items.findIndex(item => item.id === id); if (index !== -1)
17
18
  items.splice(index, 1); };
@@ -27,7 +28,13 @@ async function sleepUnlessCancelled(runtime, input, context) {
27
28
  return true;
28
29
  watch.start(async (value) => { if (cancelledIn(value))
29
30
  cancel.abort(); });
30
- await runtime.sleep(input.runAt, withAbortSignal(cancel.signal, context));
31
+ const wait = waitUntil(runtime, input.runAt, withAbortSignal(cancel.signal, context));
32
+ try {
33
+ await wait.done;
34
+ }
35
+ finally {
36
+ wait.disarm();
37
+ }
31
38
  return false;
32
39
  }
33
40
  catch (error) {
@@ -40,7 +47,7 @@ async function sleepUnlessCancelled(runtime, input, context) {
40
47
  }
41
48
  }
42
49
  /**
43
- * The native durable task behind one schedule: it sleeps on the Harness clock (`runtime.sleep`), then delivers the instruction to
50
+ * The native durable task behind one schedule: it waits on the Harness clock (`runtime.setTimer`), then delivers the instruction to
44
51
  * its conversation as a follow-up input whose request id is the schedule id, so a replayed delivery never submits twice. Owned by
45
52
  * the conversation as background work: a person stopping the current turn does not cancel it. No second scheduler: Pi's own
46
53
  * scheduler runs it, and a restart resumes it.
package/dist/view-open.js CHANGED
@@ -12,7 +12,7 @@ export const VIEW_OPEN_TIMEOUT_MS = 15_000;
12
12
  export const PRESENT_TOOL = 'present';
13
13
  /** One member per assistant entry and tool call, in the calling conversation. */
14
14
  const documents = defineDocFamily({
15
- kind: 'boring.view-open', version: 1, scope: 'conversation', history: 'latest', fork: 'initial', family: true,
15
+ kind: 'boring.view-open', checkpointWhen: () => false, version: 1, scope: 'conversation', history: 'latest', fork: 'initial', family: true,
16
16
  initial: () => ({ view: null }),
17
17
  });
18
18
  const MAX_REASON = 200;
@@ -0,0 +1,12 @@
1
+ import type { TaskRuntime } from '@earendil-works/pi-durable';
2
+ import type { Context } from '@earendil-works/chord';
3
+ /**
4
+ * Wait inside the running phase until the Harness clock reaches `at`, on the Harness's own timer (`TaskRuntime.setTimer`, the
5
+ * 1.4 replacement for the removed `TaskRuntime.sleep`). It rejects when `context` is cancelled first. `disarm` frees the timer
6
+ * when something else ends the wait. It is not durable: a restart reruns the phase, which reads its absolute deadline again.
7
+ */
8
+ export declare function waitUntil(runtime: Pick<TaskRuntime<never, never, never, never>, 'now' | 'setTimer'>, at: number, context: Context): {
9
+ readonly done: Promise<void>;
10
+ readonly disarm: () => void;
11
+ };
12
+ //# sourceMappingURL=wait-until.d.ts.map
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Wait inside the running phase until the Harness clock reaches `at`, on the Harness's own timer (`TaskRuntime.setTimer`, the
3
+ * 1.4 replacement for the removed `TaskRuntime.sleep`). It rejects when `context` is cancelled first. `disarm` frees the timer
4
+ * when something else ends the wait. It is not durable: a restart reruns the phase, which reads its absolute deadline again.
5
+ */
6
+ export function waitUntil(runtime, at, context) {
7
+ let disarm = () => { };
8
+ const done = new Promise((resolve, reject) => {
9
+ const signal = context.abortSignal;
10
+ if (signal?.aborted) {
11
+ reject(signal.reason ?? new Error('Wait cancelled'));
12
+ return;
13
+ }
14
+ const onAbort = () => { disarm(); reject(signal.reason ?? new Error('Wait cancelled')); };
15
+ const disarmTimer = runtime.setTimer(() => { signal?.removeEventListener('abort', onAbort); resolve(); }, Math.max(0, at - runtime.now()));
16
+ signal?.addEventListener('abort', onAbort, { once: true });
17
+ disarm = () => { signal?.removeEventListener('abort', onAbort); disarmTimer(); };
18
+ });
19
+ return { done, disarm: () => disarm() };
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-agent",
3
- "version": "0.4.27",
3
+ "version": "0.4.29",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "type": "module",
@@ -177,11 +177,11 @@
177
177
  "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
178
178
  },
179
179
  "peerDependencies": {
180
- "@earendil-works/pi-durable": "~1.2.0",
181
- "@earendil-works/pi-ai": "~1.2.0",
182
- "@earendil-works/chord": "~1.2.0",
183
- "@hachej/boring-files": "^0.4.27",
184
- "@hachej/boring-ui-kit": "^0.4.27",
180
+ "@earendil-works/pi-durable": "~1.4.0",
181
+ "@earendil-works/pi-ai": "~1.4.0",
182
+ "@earendil-works/chord": "~1.4.0",
183
+ "@hachej/boring-files": "^0.4.29",
184
+ "@hachej/boring-ui-kit": "^0.4.29",
185
185
  "typebox": "1.3.27",
186
186
  "xlsx": "^0.20.3"
187
187
  },