@sgtbeatdown/pi-tasks 0.1.9 → 0.1.10

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/src/extension.ts CHANGED
@@ -6,7 +6,8 @@ import { Type } from "typebox";
6
6
  import { abortableSleep } from "./abortable-sleep";
7
7
  import { deliverTaskInbox, incorporatedTaskEvents } from "./task-inbox";
8
8
  import { TaskDeliveryEvidenceState, TaskDeliveryStage, TaskOutboxDeliveryError, TaskProtocolError } from "./task-protocol";
9
- import { createWolfpackTaskCore } from "./wolfpack-task-relay";
9
+ import { createConfiguredTaskCore } from "./configured-task-core";
10
+ import type { OwnedTaskCore } from "./configured-task-core";
10
11
  import type { SubmitIntentInput, SubmitIntentOutcome, TaskCore } from "./task-core";
11
12
  import type { TaskEndpoint, TaskSnapshot } from "./task-protocol";
12
13
 
@@ -41,51 +42,87 @@ const DoneParams = Type.Object({
41
42
  artifacts: Type.Optional(Type.Array(Type.Object({ path: Type.String({ minLength: 1, description: "artifact paths are receiver-project-relative regular files" }), mimeType: Type.Optional(Type.String()), description: Type.Optional(Type.String()) }))),
42
43
  });
43
44
 
44
- export type ConfiguredCoreFactory = (signal?: AbortSignal) => Promise<TaskCore>;
45
+ export type ConfiguredCoreFactory = (signal?: AbortSignal, rebind?: boolean) => Promise<TaskCore & Partial<Pick<OwnedTaskCore, "close">>>;
46
+ export interface ConfiguredCoreLoader extends ConfiguredCoreFactory {
47
+ close(): Promise<void>;
48
+ start(): Promise<void>;
49
+ }
50
+ const defaultCoreFactory: ConfiguredCoreFactory = (signal, rebind) => createConfiguredTaskCore({ rebind }, signal);
45
51
 
46
- /** Reuses a successful configured core while allowing the next lifecycle operation to retry a failed setup. */
47
- export function createConfiguredCoreLoader(factory: ConfiguredCoreFactory): ConfiguredCoreFactory {
48
- let corePromise: Promise<TaskCore> | undefined;
49
- return async (signal?: AbortSignal): Promise<TaskCore> => {
52
+ /** One lifecycle owns setup and its core. Late setup cannot resurrect a closed lifecycle. */
53
+ export function createConfiguredCoreLoader(factory: ConfiguredCoreFactory): ConfiguredCoreLoader {
54
+ let corePromise: ReturnType<ConfiguredCoreFactory> | undefined;
55
+ let controller = new AbortController();
56
+ let epoch = 0, closed = false;
57
+ let closing: Promise<void> = Promise.resolve();
58
+ const load: ConfiguredCoreLoader = async (signal, rebind) => {
59
+ if (closed) throw new TaskProtocolError("RELAY_CLOSED", "task lifecycle is closed", { retryable: false });
50
60
  if (corePromise) return corePromise;
51
- const candidate = Promise.resolve().then(() => factory(signal));
61
+ const current = epoch;
62
+ const lifetimeSignal = controller.signal;
63
+ const combined = signal ? AbortSignal.any([signal, lifetimeSignal]) : lifetimeSignal;
64
+ const candidate = Promise.resolve().then(async () => {
65
+ if (combined.aborted) throw new TaskProtocolError("ABORTED", "task setup was cancelled");
66
+ const value = await factory(combined, rebind);
67
+ if (current !== epoch || closed || combined.aborted) {
68
+ await value.close?.();
69
+ throw new TaskProtocolError("RELAY_CLOSED", "task lifecycle was replaced", { retryable: false });
70
+ }
71
+ return value;
72
+ });
52
73
  corePromise = candidate;
53
- try {
54
- return await candidate;
55
- } catch (error) {
56
- if (corePromise === candidate) corePromise = undefined;
57
- throw error;
58
- }
74
+ try { return await candidate; }
75
+ catch (error) { if (corePromise === candidate) corePromise = undefined; throw error; }
76
+ };
77
+ load.start = async () => {
78
+ const current = epoch;
79
+ await closing;
80
+ if (current !== epoch) throw new TaskProtocolError("RELAY_CLOSED", "task lifecycle was stopped during startup", { retryable: false });
81
+ if (closed) { closed = false; controller = new AbortController(); }
59
82
  };
83
+ load.close = () => {
84
+ closed = true; epoch++; controller.abort();
85
+ const prior = corePromise; corePromise = undefined;
86
+ const dispose = async () => {
87
+ let value: Awaited<ReturnType<ConfiguredCoreFactory>> | undefined;
88
+ try { value = await prior; } catch { /* Setup failure already owns cleanup. */ }
89
+ await value?.close?.();
90
+ };
91
+ closing = Promise.all([closing, dispose()]).then(() => undefined);
92
+ return closing;
93
+ };
94
+ return load;
60
95
  }
61
96
 
62
- /** Registers v2 endpoint-owned tools using a configured durable Wolfpack relay by default. */
63
- export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefined = undefined, createCore: ConfiguredCoreFactory = (signal) => createWolfpackTaskCore({}, signal)): void {
97
+ /** Registers endpoint-owned tools using the memory-owned Wolfpack transport by default. */
98
+ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefined = undefined, createCore: ConfiguredCoreFactory = defaultCoreFactory): void {
64
99
  let inboxContext: ExtensionContext | undefined;
65
100
  let backgroundTimer: ReturnType<typeof setInterval> | undefined;
66
101
  let lifecycleEpoch = 0;
67
102
  const closingTaskIds = new Set<string>();
68
103
  const workerGateEnabled = process.env.PI_TASK_WORKER === "1";
69
- const configuredCore: ConfiguredCoreFactory = core === undefined ? createConfiguredCoreLoader(createCore) : async (): Promise<TaskCore> => core;
104
+ const ownedLoader = core === undefined ? createConfiguredCoreLoader(createCore) : undefined;
105
+ const configuredCore: ConfiguredCoreFactory = ownedLoader ?? (async (): Promise<TaskCore> => core!);
70
106
  const refreshInbox = createSingleFlightInboxRefresh(async (signal) => {
71
107
  const context = inboxContext;
108
+ const epoch = lifecycleEpoch;
109
+ const isCurrent = (): boolean => inboxContext === context && lifecycleEpoch === epoch;
72
110
  if (!context) return undefined;
73
111
  const activeCore = await configuredCore(signal);
74
- if (inboxContext !== context) return undefined;
112
+ if (!isCurrent()) return undefined;
75
113
  let outboxError: unknown;
76
114
  try {
77
115
  await activeCore.flushOutbox(signal);
78
116
  } catch (error) {
79
117
  outboxError = error;
80
118
  }
81
- if (inboxContext !== context) return undefined;
119
+ if (!isCurrent()) return undefined;
82
120
  try {
83
121
  await activeCore.evaluateTimeouts(signal);
84
122
  } catch (error) {
85
123
  outboxError ??= error;
86
124
  }
87
- if (inboxContext !== context) return undefined;
88
- const isCurrent = (): boolean => inboxContext === context;
125
+ if (!isCurrent()) return undefined;
89
126
  const guardedCore: TaskCore = {
90
127
  ...activeCore,
91
128
  async receive(receiveSignal) {
@@ -159,6 +196,8 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
159
196
  pi.on("session_start", async (_event, context) => {
160
197
  closingTaskIds.clear();
161
198
  const epoch = ++lifecycleEpoch;
199
+ await ownedLoader?.start();
200
+ if (epoch !== lifecycleEpoch) return;
162
201
  inboxContext = context;
163
202
  if (backgroundTimer) clearInterval(backgroundTimer);
164
203
  backgroundTimer = setInterval(() => {
@@ -168,25 +207,58 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
168
207
  await refreshLifecycle(context, epoch);
169
208
  });
170
209
  pi.on("agent_end", async (_event, context) => {
210
+ if (!inboxContext) return;
171
211
  const epoch = lifecycleEpoch;
172
212
  inboxContext = context;
173
213
  await refreshLifecycle(context, epoch);
174
214
  });
175
215
  pi.on("agent_settled", async (_event, context) => {
216
+ if (!inboxContext) return;
176
217
  const epoch = lifecycleEpoch;
177
218
  inboxContext = context;
178
219
  await refreshLifecycle(context, epoch);
179
220
  });
180
- pi.on("session_shutdown", () => {
221
+ pi.on("session_shutdown", async () => {
181
222
  closingTaskIds.clear();
182
223
  lifecycleEpoch += 1;
183
224
  if (backgroundTimer) clearInterval(backgroundTimer);
184
225
  backgroundTimer = undefined;
185
226
  inboxContext = undefined;
227
+ await ownedLoader?.close();
228
+ });
229
+
230
+ if (ownedLoader && createCore === defaultCoreFactory) pi.registerCommand("task-relay-rebind", {
231
+ description: "Accept relay loss, discard active task state and bind a fresh endpoint",
232
+ async handler(args, context) {
233
+ if (args.trim() !== "--accept-relay-loss") {
234
+ context.ui.notify("Relay restart can lose accepted mail. Rebind discards this lifetime's task state; Pi session history remains, but is never replayed. Run /task-relay-rebind --accept-relay-loss to continue with a fresh endpoint.", "warning");
235
+ return;
236
+ }
237
+ const epoch = ++lifecycleEpoch;
238
+ inboxContext = undefined;
239
+ if (backgroundTimer) clearInterval(backgroundTimer);
240
+ backgroundTimer = undefined;
241
+ await ownedLoader.close();
242
+ if (epoch !== lifecycleEpoch) return;
243
+ try {
244
+ await ownedLoader.start();
245
+ if (epoch !== lifecycleEpoch) return;
246
+ await ownedLoader(undefined, true);
247
+ if (epoch !== lifecycleEpoch) return;
248
+ closingTaskIds.clear();
249
+ context.ui.notify("Fresh task endpoint bound with empty state. Prior tasks remain only in session history.", "info");
250
+ } catch (error) {
251
+ if (epoch === lifecycleEpoch) context.ui.notify(outboxFailureStatus(error), "error");
252
+ }
253
+ if (epoch !== lifecycleEpoch) return;
254
+ inboxContext = context;
255
+ backgroundTimer = setInterval(() => { if (inboxContext) void refreshLifecycle(inboxContext, epoch); }, BACKGROUND_POLL_MS);
256
+ await refreshLifecycle(context, epoch);
257
+ },
186
258
  });
187
259
 
188
260
  pi.registerTool({
189
- name: "agent_task_send", label: "Send Agent Task", description: "Persist an endpoint-owned task and submit its opaque assignment envelope to a relay.", parameters: SendParams,
261
+ name: "agent_task_send", label: "Send Agent Task", description: "Create a task in this endpoint's RAM and submit its assignment; restart loses active state.", parameters: SendParams,
190
262
  async execute(_id, params, signal) {
191
263
  try {
192
264
  const sent = await (await configuredCore(signal)).createTask({ target: params.to, task: params.task, timeoutMs: params.timeoutMs ?? DEFAULT_TASK_TIMEOUT_MS }, signal);
@@ -201,7 +273,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
201
273
  try {
202
274
  const activeCore = await configuredCore(signal);
203
275
  const task = activeCore.getTask(params.taskId);
204
- if (!task) return taskError(new Error("unknown local task"));
276
+ if (!task) return taskError(new TaskProtocolError("UNKNOWN_TASK", `unknown task: ${params.taskId}`, { retryable: false }));
205
277
  const deliveryEvidence = taskDeliveryEvidence(task);
206
278
  return toolResult({ ...task, deliveryEvidence }, `## task status\n- task: \`${task.taskId}\`\n- status: ${task.status}${receiverAssignment(activeCore, task, context)}${deliveryEvidenceText(deliveryEvidence)}`);
207
279
  } catch (error) { return taskError(error); }
@@ -215,7 +287,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
215
287
  for (;;) {
216
288
  await refreshInbox(signal);
217
289
  const task = (await configuredCore(signal)).getTask(params.taskId);
218
- if (!task) return taskError(new Error("unknown local task"));
290
+ if (!task) return taskError(new TaskProtocolError("UNKNOWN_TASK", `unknown task: ${params.taskId}`, { retryable: false }));
219
291
  if (terminal(task.status)) return toolResult(task, `## task status\n- task: \`${task.taskId}\`\n- status: ${task.status}`);
220
292
  if (Date.now() >= deadline) return toolResult({ taskId: params.taskId, status: task.status }, `## task wait\n- task: \`${params.taskId}\`\n- status: ${task.status}\n- wait timed out`);
221
293
  onUpdate?.({ content: [{ type: "text", text: `waiting for ${params.taskId}...` }], details: {} });
@@ -237,12 +309,12 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
237
309
  renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
238
310
  });
239
311
  pi.registerTool({
240
- name: "agent_task_message", label: "Message Agent Task", description: "Persist a message intent before relay submission.", parameters: MessageParams,
312
+ name: "agent_task_message", label: "Message Agent Task", description: "Record a message intent in this lifetime before relay submission.", parameters: MessageParams,
241
313
  async execute(_id, params, signal) { try { await (await configuredCore(signal)).submitIntent({ taskId: params.taskId, type: `task.${params.type}`, payload: { message: params.message } }, signal); return toolResult({ taskId: params.taskId }, `## task ${params.type}\n- task: \`${params.taskId}\``); } catch (error) { return taskError(error); } },
242
314
  renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
243
315
  });
244
316
  pi.registerTool({
245
- name: "agent_task_cancel", label: "Cancel Agent Task", description: "Persist cancellation before relay submission.", parameters: TaskIdParams,
317
+ name: "agent_task_cancel", label: "Cancel Agent Task", description: "Record cancellation in this lifetime before relay submission.", parameters: TaskIdParams,
246
318
  async execute(_id, params, signal) {
247
319
  try {
248
320
  const activeCore = await configuredCore(signal);
@@ -262,7 +334,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
262
334
  renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
263
335
  });
264
336
  pi.registerTool({
265
- name: "agent_task_done", label: "Complete Agent Task", description: "Persist a terminal task intent before relay submission.", parameters: DoneParams,
337
+ name: "agent_task_done", label: "Complete Agent Task", description: "Record a terminal intent in this lifetime before relay submission.", parameters: DoneParams,
266
338
  async execute(_id, params, signal) {
267
339
  try {
268
340
  const activeCore = await configuredCore(signal);
@@ -275,7 +347,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
275
347
  return blockedDoneResult(activeCore, params.taskId, params.status, error) ?? taskError(error);
276
348
  }
277
349
  const task = activeCore.getTask(params.taskId);
278
- if (!task) return taskError(new Error("unknown local task"));
350
+ if (!task) return taskError(new TaskProtocolError("UNKNOWN_TASK", `unknown task: ${params.taskId}`, { retryable: false }));
279
351
  if (submission?.authority === "origin" && submission.canonicalEvent.type === "task.late_terminal") {
280
352
  return {
281
353
  ...toolResult({ taskId: params.taskId, requestedStatus: params.status, observedCanonicalStatus: task.status, canonicalEvent: submission.canonicalEvent }, `## canonical late terminal recorded\n- task: \`${params.taskId}\`\n- requested status: ${params.status}\n- canonical status remains: ${task.status}\n- ${params.summary}`),
@@ -313,34 +385,34 @@ export default function piTasks(pi: ExtensionAPI): void {
313
385
  }
314
386
 
315
387
  interface TaskDeliveryEvidence {
316
- readonly receiverPersistence: "not_confirmed" | "confirmed";
388
+ readonly receiverReceipt: "not_confirmed" | "confirmed";
317
389
  readonly piInsertion: "not_confirmed" | "blocked" | "confirmed";
318
390
  readonly wakeAcceptance: "not_confirmed" | "pending" | "confirmed";
319
391
  readonly modelExecution: "not_evidenced";
320
392
  }
321
393
 
322
394
  function taskDeliveryEvidence(task: TaskSnapshot): TaskDeliveryEvidence {
323
- let receiverPersistence: TaskDeliveryEvidence["receiverPersistence"] = "not_confirmed";
395
+ let receiverReceipt: TaskDeliveryEvidence["receiverReceipt"] = "not_confirmed";
324
396
  let piInsertion: TaskDeliveryEvidence["piInsertion"] = "not_confirmed";
325
397
  let wakeAcceptance: TaskDeliveryEvidence["wakeAcceptance"] = "not_confirmed";
326
398
  const assignmentEventId = task.events.find((event) => event.type === "task.created")?.eventId;
327
399
  for (const event of task.events) {
328
400
  if (event.type !== "task.delivery_receipt" || event.payload.eventId !== assignmentEventId) continue;
329
- if (event.payload.stage === TaskDeliveryStage.receiverPersisted) receiverPersistence = "confirmed";
401
+ if (event.payload.stage === TaskDeliveryStage.receiverRecorded) receiverReceipt = "confirmed";
330
402
  if (event.payload.stage === TaskDeliveryStage.piInsertion && event.payload.state === TaskDeliveryEvidenceState.blocked) piInsertion = "blocked";
331
403
  if (event.payload.stage === TaskDeliveryStage.piInserted || event.payload.stage === undefined) {
332
- receiverPersistence = "confirmed";
404
+ receiverReceipt = "confirmed";
333
405
  piInsertion = "confirmed";
334
406
  }
335
407
  if (event.payload.stage === TaskDeliveryStage.wakeRequested) wakeAcceptance = "pending";
336
408
  if (event.payload.stage === TaskDeliveryStage.wakeAccepted) wakeAcceptance = "confirmed";
337
409
  }
338
- return { receiverPersistence, piInsertion, wakeAcceptance, modelExecution: "not_evidenced" };
410
+ return { receiverReceipt, piInsertion, wakeAcceptance, modelExecution: "not_evidenced" };
339
411
  }
340
412
 
341
413
  function deliveryEvidenceText(evidence: TaskDeliveryEvidence): string {
342
414
  const insertion = evidence.piInsertion === "blocked" ? "blocked; retryable" : evidence.piInsertion;
343
- return `\n- receiver persistence: ${evidence.receiverPersistence}\n- Pi insertion: ${insertion}\n- wake acceptance: ${evidence.wakeAcceptance}\n- model execution: ${evidence.modelExecution}`;
415
+ return `\n- receiver receipt (RAM): ${evidence.receiverReceipt}\n- Pi insertion: ${insertion}\n- wake acceptance: ${evidence.wakeAcceptance}\n- model execution: ${evidence.modelExecution}`;
344
416
  }
345
417
 
346
418
  function toolResult(details: unknown, markdown: string): AgentToolResult<unknown> {
@@ -393,7 +465,12 @@ function blockedDeliveryWarning(error: TaskOutboxDeliveryError, target: TaskEndp
393
465
  return { code: error.code, message: error.message, retryable: false, delivery: "blocked", target, details };
394
466
  }
395
467
 
396
- function outboxFailureStatus(error: unknown): "tasks: relay unavailable" | "tasks: outbox degraded" {
468
+ function outboxFailureStatus(error: unknown): string {
469
+ if (error instanceof TaskProtocolError) {
470
+ if (["RELAY_RESET", "RELAY_REBIND_REQUIRED"].includes(error.code)) return "tasks: relay reset; /task-relay-rebind";
471
+ if (error.code === "RELAY_PROFILE_REQUIRED") return "tasks: compatible memory-owned relay required";
472
+ if (error.code === "RELAY_AUTH_REQUIRED") return "tasks: Wolfpack authentication required";
473
+ }
397
474
  return error instanceof TaskOutboxDeliveryError && error.code === TARGET_NOT_REGISTERED_CODE ? "tasks: outbox degraded" : "tasks: relay unavailable";
398
475
  }
399
476
 
package/src/index.ts CHANGED
@@ -1,13 +1,11 @@
1
1
  export { createInMemoryTaskRelay, InMemoryTaskRelay } from "./in-memory-task-relay";
2
2
  export { createTaskCore } from "./task-core";
3
+ export { createConfiguredTaskCore } from "./configured-task-core";
4
+ export type { ConfiguredTaskCoreOptions, OwnedTaskCore } from "./configured-task-core";
3
5
  export { createTaskStore } from "./task-store";
4
- export {
5
- createWolfpackTaskCore,
6
- createWolfpackTaskRelay,
7
- WOLFPACK_TASK_RELAY_ID,
8
- WOLFPACK_TASK_RELAY_LEASE_MS,
9
- WOLFPACK_TASK_RELAY_PROTOCOL_VERSION,
10
- } from "./wolfpack-task-relay";
6
+ export { createVolatileTaskSession, VOLATILE_PROFILE } from "./volatile-task-session";
7
+ export type { VolatileTaskSession, VolatileTaskSessionOptions } from "./volatile-task-session";
8
+ export { WOLFPACK_TASK_RELAY_ID } from "./wolfpack-endpoint-view";
11
9
  export { runTaskRelayConformance } from "./task-relay-conformance";
12
10
  export {
13
11
  MAX_RELAY_PAYLOAD_BYTES,
@@ -25,6 +23,7 @@ export type {
25
23
  RelayInboxPage,
26
24
  RelayReceiveRequest,
27
25
  RelayTargetReference,
26
+ RelayTransportBinding,
28
27
  TaskEndpoint,
29
28
  TaskEvent,
30
29
  TaskIntent,
package/src/task-core.ts CHANGED
@@ -25,7 +25,7 @@ import type { TaskStore } from "./task-store";
25
25
 
26
26
  const RECEIVE_PAGE_SIZE = 100;
27
27
  const DELIVERY_EVIDENCE_OPERATION = "delivery_evidence";
28
- const TARGET_NOT_REGISTERED_CODE = "TARGET_NOT_REGISTERED";
28
+ const TERMINAL_DELIVERY_CODES = new Set(["TARGET_NOT_REGISTERED", INVALID_RELAY_METADATA, "DELIVERY_UNCONFIRMED", "ENVELOPE_EXPIRED", "ENVELOPE_CONFLICT", "CROSS_RELAY_ENDPOINT"]);
29
29
  const TERMINAL_STATUSES = new Set<TaskRecord["status"]>(["completed", "failed", "cancelled", "timed_out"]);
30
30
  const TERMINAL_EVENTS = new Set(["task.completed", "task.failed", "task.cancelled", "task.timed_out"]);
31
31
  const CANONICAL_EVENTS = new Set([
@@ -301,6 +301,7 @@ interface PersistedIntent {
301
301
  }
302
302
 
303
303
  function persistIntent(options: TaskCoreOptions, now: () => number, ids: () => string, task: TaskRecord, input: SubmitIntentInput, reservedOperation?: string): PersistedIntent {
304
+ if (!sameEndpoint(options.endpoint, task.origin) && !sameEndpoint(options.endpoint, task.target)) throw new TaskProtocolError("NOT_PARTICIPANT", "historical task belongs to a different endpoint", { retryable: false });
304
305
  if (sameEndpoint(options.endpoint, task.origin)) {
305
306
  const operation = input.type === "task.cancelled" ? ORIGIN_CANCELLATION_OPERATION : reservedOperation;
306
307
  const canonical = canonicalize(options, now, ids, task, { intentId: ids(), taskId: input.taskId, type: input.type, payload: input.payload }, input.type, operation);
@@ -346,7 +347,7 @@ function deliveryEvidence(input: SubmitIntentInput): DeliveryEvidence | undefine
346
347
  if (stage === TaskDeliveryStage.piInsertion && state === TaskDeliveryEvidenceState.blocked && input.payload.retryable === true) {
347
348
  return { eventId: input.payload.eventId, stage, state, retryable: true };
348
349
  }
349
- if ((stage === TaskDeliveryStage.receiverPersisted || stage === TaskDeliveryStage.piInserted || stage === TaskDeliveryStage.wakeRequested || stage === TaskDeliveryStage.wakeAccepted)
350
+ if ((stage === TaskDeliveryStage.receiverRecorded || stage === TaskDeliveryStage.piInserted || stage === TaskDeliveryStage.wakeRequested || stage === TaskDeliveryStage.wakeAccepted)
350
351
  && state === TaskDeliveryEvidenceState.confirmed) {
351
352
  return { eventId: input.payload.eventId, stage, state };
352
353
  }
@@ -400,7 +401,7 @@ async function flush(options: TaskCoreOptions, signal: AbortSignal | undefined,
400
401
  options.store.transaction(() => { options.store.markOutboxAccepted(record.envelope.envelopeId); });
401
402
  } catch (error) {
402
403
  if (signal?.aborted || (error instanceof TaskProtocolError && error.code === "ABORTED")) throw error;
403
- if (error instanceof TaskProtocolError && (error.code === TARGET_NOT_REGISTERED_CODE || error.code === INVALID_RELAY_METADATA) && !error.retryable) {
404
+ if (error instanceof TaskProtocolError && TERMINAL_DELIVERY_CODES.has(error.code) && !error.retryable) {
404
405
  options.store.transaction(() => {
405
406
  options.store.quarantineOutbox(record.envelope.envelopeId, {
406
407
  errorCode: error.code,
@@ -482,7 +483,7 @@ function throwIfTerminalDeliveryBlocked(store: TaskStore, taskId: string): void
482
483
 
483
484
  function requiredTask(store: TaskStore, taskId: string): TaskSnapshot {
484
485
  const task = store.getTask(taskId);
485
- if (!task) throw new TaskProtocolError("UNKNOWN_TASK", `unknown task: ${taskId}`);
486
+ if (!task) throw new TaskProtocolError("UNKNOWN_TASK", `unknown task: ${taskId}`, { retryable: false });
486
487
  return task;
487
488
  }
488
489
 
package/src/task-inbox.ts CHANGED
@@ -5,6 +5,7 @@ import type { RelayDelivery, TaskEvent } from "./task-protocol";
5
5
  const TASK_EVENT_CUSTOM_TYPE = "pi-tasks-event";
6
6
  const TASK_WAKE_CUSTOM_TYPE = "pi-tasks-wake";
7
7
  const TASK_CURSOR_CUSTOM_TYPE = "pi-tasks-relay-cursor";
8
+ const TASK_RECORD_CUSTOM_TYPE = "pi-tasks-event-record";
8
9
 
9
10
  interface InboxContext {
10
11
  readonly isIdle: () => boolean;
@@ -17,16 +18,18 @@ interface InboxPi {
17
18
  message: { readonly customType: string; readonly content: string; readonly display: boolean; readonly details: TaskEventDetails },
18
19
  options: { readonly triggerTurn: false } | { readonly triggerTurn: true; readonly deliverAs: "followUp" },
19
20
  ): void;
20
- appendEntry(customType: string, data: { readonly cursor: string }): void;
21
+ appendEntry(customType: string, data: { readonly cursor: string } | TaskEventDetails): void;
21
22
  }
22
23
 
23
24
  export interface TaskEventDetails {
24
25
  readonly taskId: string;
25
26
  readonly eventId: string;
27
+ /** Historical evidence only; never used to rebuild active task state. */
28
+ readonly event?: TaskEvent;
26
29
  }
27
30
 
28
31
  type DeliveryEvidencePayload =
29
- | { readonly stage: typeof TaskDeliveryStage.receiverPersisted | typeof TaskDeliveryStage.wakeRequested | typeof TaskDeliveryStage.wakeAccepted; readonly state: typeof TaskDeliveryEvidenceState.confirmed }
32
+ | { readonly stage: typeof TaskDeliveryStage.receiverRecorded | typeof TaskDeliveryStage.wakeRequested | typeof TaskDeliveryStage.wakeAccepted; readonly state: typeof TaskDeliveryEvidenceState.confirmed }
30
33
  | { readonly stage: typeof TaskDeliveryStage.piInsertion; readonly state: typeof TaskDeliveryEvidenceState.blocked; readonly retryable: true };
31
34
 
32
35
  /** Persists model-visible Pi evidence before advancing the relay cursor, then starts one separate turn. */
@@ -44,8 +47,13 @@ export async function deliverTaskInbox(pi: InboxPi, core: TaskCore, context: Inb
44
47
  if (context.hasPendingMessages()) return;
45
48
  const eventDetails = { taskId: event.taskId, eventId: event.eventId };
46
49
  const eventKey = key(event.taskId, event.eventId);
50
+ if (!isModelVisible(event.type) && !recordedEventKeys(context.sessionManager.getEntries()).has(eventKey)) {
51
+ // Archive receipts/parent ACK/late-terminal facts without waking the model.
52
+ // This is evidence only: no task state or delivery queue is reconstructed.
53
+ pi.appendEntry(TASK_RECORD_CUSTOM_TYPE, { ...eventDetails, event });
54
+ }
47
55
  if (event.type === "task.created") {
48
- await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.receiverPersisted, state: TaskDeliveryEvidenceState.confirmed }, signal);
56
+ await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.receiverRecorded, state: TaskDeliveryEvidenceState.confirmed }, signal);
49
57
  }
50
58
  let incorporated = incorporatedEvents(context.sessionManager.getEntries()).has(eventKey);
51
59
  if (!incorporated && isModelVisible(event.type)) {
@@ -54,7 +62,7 @@ export async function deliverTaskInbox(pi: InboxPi, core: TaskCore, context: Inb
54
62
  customType: TASK_EVENT_CUSTOM_TYPE,
55
63
  content: renderTaskEvent(event),
56
64
  display: true,
57
- details: eventDetails,
65
+ details: { ...eventDetails, event },
58
66
  }, { triggerTurn: false });
59
67
  incorporated = incorporatedEvents(context.sessionManager.getEntries()).has(eventKey);
60
68
  if (!incorporated) {
@@ -126,6 +134,12 @@ function taskMessageDetails(entries: readonly unknown[], customType: string): re
126
134
  return events;
127
135
  }
128
136
 
137
+ function recordedEventKeys(entries: readonly unknown[]): Set<string> {
138
+ return new Set(entries.flatMap(entry => isRecord(entry) && entry.type === "custom" && entry.customType === TASK_RECORD_CUSTOM_TYPE
139
+ && isRecord(entry.data) && typeof entry.data.taskId === "string" && typeof entry.data.eventId === "string"
140
+ ? [key(entry.data.taskId, entry.data.eventId)] : []));
141
+ }
142
+
129
143
  function taskMessageKeys(entries: readonly unknown[], customType: string): Set<string> {
130
144
  return new Set(taskMessageDetails(entries, customType).map((event) => key(event.taskId, event.eventId)));
131
145
  }
@@ -6,7 +6,7 @@ export const PARENT_ACKNOWLEDGMENT_OPERATION = "parent_acknowledgment";
6
6
  export const TERMINAL_INTENT_OPERATION = "terminal_intent";
7
7
 
8
8
  export const TaskDeliveryStage = {
9
- receiverPersisted: "receiver_persisted",
9
+ receiverRecorded: "receiver_recorded",
10
10
  piInsertion: "pi_insertion",
11
11
  piInserted: "pi_inserted",
12
12
  wakeRequested: "wake_requested",
@@ -23,6 +23,17 @@ export interface TaskEndpoint {
23
23
  readonly id: string;
24
24
  }
25
25
 
26
+ /** Endpoint-owned transport binding; not task lifecycle or relay replay state. */
27
+ export interface RelayTransportBinding {
28
+ readonly profile: "volatile-v1";
29
+ readonly epoch: string;
30
+ readonly endpoint: TaskEndpoint;
31
+ readonly generation: string;
32
+ readonly callerSession: string;
33
+ readonly url: string;
34
+ readonly reset?: true;
35
+ }
36
+
26
37
  /** A provider-defined reference which core resolves without interpreting. */
27
38
  export interface RelayTargetReference {
28
39
  readonly relay: string;