@sgtbeatdown/pi-tasks 0.1.9 → 0.1.11
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 +48 -100
- package/package.json +2 -2
- package/skills/wolfpack-pi-task-delegation/SKILL.md +6 -4
- package/src/configured-task-core.ts +63 -0
- package/src/extension.ts +154 -42
- package/src/index.ts +6 -7
- package/src/task-core.ts +5 -4
- package/src/task-inbox.ts +18 -4
- package/src/task-protocol.ts +12 -1
- package/src/task-store.ts +177 -426
- package/src/volatile-task-session.ts +283 -0
- package/src/wolfpack-local-auth.ts +20 -0
- package/src/wolfpack-task-relay.ts +35 -358
- package/src/sqlite-database.ts +0 -68
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 {
|
|
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
|
|
|
@@ -15,6 +16,9 @@ const MIN_TASK_TIMEOUT_MS = 1_000;
|
|
|
15
16
|
const MAX_TASK_TIMEOUT_MS = 86_400_000;
|
|
16
17
|
const BACKGROUND_POLL_MS = 5_000;
|
|
17
18
|
const TARGET_NOT_REGISTERED_CODE = "TARGET_NOT_REGISTERED";
|
|
19
|
+
const RELAY_STATE_LOST_CODE = "RELAY_STATE_LOST";
|
|
20
|
+
const RELAY_LOSS_CUSTOM_TYPE = "pi-tasks-relay-loss";
|
|
21
|
+
const RELAY_LOSS_CODES = new Set(["RELAY_RESET", "RELAY_REBIND_REQUIRED"]);
|
|
18
22
|
const WAIT_POLL_MS = 250;
|
|
19
23
|
const SUMMARY_MAX_CHARS = 1_200;
|
|
20
24
|
const PRE_ASSIGNMENT_TOOLS = new Set(["agent_task_inbox", "agent_task_status", "agent_task_wait"]);
|
|
@@ -41,51 +45,91 @@ const DoneParams = Type.Object({
|
|
|
41
45
|
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
46
|
});
|
|
43
47
|
|
|
44
|
-
export type ConfiguredCoreFactory = (signal?: AbortSignal) => Promise<TaskCore
|
|
48
|
+
export type ConfiguredCoreFactory = (signal?: AbortSignal, rebind?: boolean) => Promise<TaskCore & Partial<Pick<OwnedTaskCore, "close">>>;
|
|
49
|
+
export interface ConfiguredCoreLoader extends ConfiguredCoreFactory {
|
|
50
|
+
close(): Promise<void>;
|
|
51
|
+
start(): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
const defaultCoreFactory: ConfiguredCoreFactory = (signal, rebind) => createConfiguredTaskCore({ rebind }, signal);
|
|
45
54
|
|
|
46
|
-
/**
|
|
47
|
-
export function createConfiguredCoreLoader(factory: ConfiguredCoreFactory):
|
|
48
|
-
let corePromise:
|
|
49
|
-
|
|
55
|
+
/** One lifecycle owns setup and its core. Late setup cannot resurrect a closed lifecycle. */
|
|
56
|
+
export function createConfiguredCoreLoader(factory: ConfiguredCoreFactory): ConfiguredCoreLoader {
|
|
57
|
+
let corePromise: ReturnType<ConfiguredCoreFactory> | undefined;
|
|
58
|
+
let controller = new AbortController();
|
|
59
|
+
let epoch = 0, closed = false;
|
|
60
|
+
let closing: Promise<void> = Promise.resolve();
|
|
61
|
+
const load: ConfiguredCoreLoader = async (signal, rebind) => {
|
|
62
|
+
if (closed) throw new TaskProtocolError("RELAY_CLOSED", "task lifecycle is closed", { retryable: false });
|
|
50
63
|
if (corePromise) return corePromise;
|
|
51
|
-
const
|
|
64
|
+
const current = epoch;
|
|
65
|
+
const lifetimeSignal = controller.signal;
|
|
66
|
+
const combined = signal ? AbortSignal.any([signal, lifetimeSignal]) : lifetimeSignal;
|
|
67
|
+
const candidate = Promise.resolve().then(async () => {
|
|
68
|
+
if (combined.aborted) throw new TaskProtocolError("ABORTED", "task setup was cancelled");
|
|
69
|
+
const value = await factory(combined, rebind);
|
|
70
|
+
if (current !== epoch || closed || combined.aborted) {
|
|
71
|
+
await value.close?.();
|
|
72
|
+
throw new TaskProtocolError("RELAY_CLOSED", "task lifecycle was replaced", { retryable: false });
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
});
|
|
52
76
|
corePromise = candidate;
|
|
53
|
-
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
try { return await candidate; }
|
|
78
|
+
catch (error) { if (corePromise === candidate) corePromise = undefined; throw error; }
|
|
79
|
+
};
|
|
80
|
+
load.start = async () => {
|
|
81
|
+
const current = epoch;
|
|
82
|
+
await closing;
|
|
83
|
+
if (current !== epoch) throw new TaskProtocolError("RELAY_CLOSED", "task lifecycle was stopped during startup", { retryable: false });
|
|
84
|
+
if (closed) { closed = false; controller = new AbortController(); }
|
|
85
|
+
};
|
|
86
|
+
load.close = () => {
|
|
87
|
+
closed = true; epoch++; controller.abort();
|
|
88
|
+
const prior = corePromise; corePromise = undefined;
|
|
89
|
+
const dispose = async () => {
|
|
90
|
+
let value: Awaited<ReturnType<ConfiguredCoreFactory>> | undefined;
|
|
91
|
+
try { value = await prior; } catch { /* Setup failure already owns cleanup. */ }
|
|
92
|
+
await value?.close?.();
|
|
93
|
+
};
|
|
94
|
+
closing = Promise.all([closing, dispose()]).then(() => undefined);
|
|
95
|
+
return closing;
|
|
59
96
|
};
|
|
97
|
+
return load;
|
|
60
98
|
}
|
|
61
99
|
|
|
62
|
-
/** Registers
|
|
63
|
-
export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefined = undefined, createCore: ConfiguredCoreFactory =
|
|
100
|
+
/** Registers endpoint-owned tools using the memory-owned Wolfpack transport by default. */
|
|
101
|
+
export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefined = undefined, createCore: ConfiguredCoreFactory = defaultCoreFactory): void {
|
|
64
102
|
let inboxContext: ExtensionContext | undefined;
|
|
65
103
|
let backgroundTimer: ReturnType<typeof setInterval> | undefined;
|
|
66
104
|
let lifecycleEpoch = 0;
|
|
67
105
|
const closingTaskIds = new Set<string>();
|
|
68
106
|
const workerGateEnabled = process.env.PI_TASK_WORKER === "1";
|
|
69
|
-
const
|
|
107
|
+
const ownedLoader = core === undefined ? createConfiguredCoreLoader(createCore) : undefined;
|
|
108
|
+
const configuredCore: ConfiguredCoreFactory = ownedLoader ?? (async (): Promise<TaskCore> => core!);
|
|
109
|
+
let relayLossRecovery: Promise<void> | undefined;
|
|
110
|
+
let pendingRelayLossCount: number | undefined;
|
|
70
111
|
const refreshInbox = createSingleFlightInboxRefresh(async (signal) => {
|
|
71
112
|
const context = inboxContext;
|
|
113
|
+
const epoch = lifecycleEpoch;
|
|
114
|
+
const isCurrent = (): boolean => inboxContext === context && lifecycleEpoch === epoch;
|
|
72
115
|
if (!context) return undefined;
|
|
73
116
|
const activeCore = await configuredCore(signal);
|
|
74
|
-
if (
|
|
117
|
+
if (!isCurrent()) return undefined;
|
|
75
118
|
let outboxError: unknown;
|
|
76
119
|
try {
|
|
77
120
|
await activeCore.flushOutbox(signal);
|
|
78
121
|
} catch (error) {
|
|
122
|
+
if (isRelayLoss(error)) return error;
|
|
79
123
|
outboxError = error;
|
|
80
124
|
}
|
|
81
|
-
if (
|
|
125
|
+
if (!isCurrent()) return undefined;
|
|
82
126
|
try {
|
|
83
127
|
await activeCore.evaluateTimeouts(signal);
|
|
84
128
|
} catch (error) {
|
|
129
|
+
if (isRelayLoss(error)) return error;
|
|
85
130
|
outboxError ??= error;
|
|
86
131
|
}
|
|
87
|
-
if (
|
|
88
|
-
const isCurrent = (): boolean => inboxContext === context;
|
|
132
|
+
if (!isCurrent()) return undefined;
|
|
89
133
|
const guardedCore: TaskCore = {
|
|
90
134
|
...activeCore,
|
|
91
135
|
async receive(receiveSignal) {
|
|
@@ -109,10 +153,64 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
109
153
|
}, signal);
|
|
110
154
|
return outboxError;
|
|
111
155
|
});
|
|
156
|
+
const startBackgroundRefresh = (context: ExtensionContext, epoch: number): void => {
|
|
157
|
+
inboxContext = context;
|
|
158
|
+
if (backgroundTimer) clearInterval(backgroundTimer);
|
|
159
|
+
backgroundTimer = setInterval(() => {
|
|
160
|
+
const currentContext = inboxContext;
|
|
161
|
+
if (currentContext) void refreshLifecycle(currentContext, epoch);
|
|
162
|
+
}, BACKGROUND_POLL_MS);
|
|
163
|
+
};
|
|
164
|
+
const announceFreshBinding = (context: ExtensionContext): void => {
|
|
165
|
+
if (pendingRelayLossCount === undefined) return;
|
|
166
|
+
const count = pendingRelayLossCount;
|
|
167
|
+
pendingRelayLossCount = undefined;
|
|
168
|
+
const taskSummary = count === 0 ? "No active tasks required local failure records." : `${count} active ${count === 1 ? "task was" : "tasks were"} marked locally failed with unknown remote outcome.`;
|
|
169
|
+
context.ui.notify(`Relay state was lost; automatically bound a fresh endpoint. ${taskSummary}`, "warning");
|
|
170
|
+
};
|
|
171
|
+
const recoverRelayLoss = async (context: ExtensionContext, epoch: number, activeCore: TaskCore | undefined): Promise<boolean> => {
|
|
172
|
+
if (!ownedLoader || lifecycleEpoch !== epoch || inboxContext !== context) return false;
|
|
173
|
+
if (relayLossRecovery) { await relayLossRecovery; return true; }
|
|
174
|
+
const pending = (async (): Promise<void> => {
|
|
175
|
+
const recoveryEpoch = ++lifecycleEpoch;
|
|
176
|
+
const activeTasks = activeCore?.listTasks().filter((task) => task.status === "active") ?? [];
|
|
177
|
+
inboxContext = undefined;
|
|
178
|
+
if (backgroundTimer) clearInterval(backgroundTimer);
|
|
179
|
+
backgroundTimer = undefined;
|
|
180
|
+
await ownedLoader.close();
|
|
181
|
+
if (recoveryEpoch !== lifecycleEpoch) return;
|
|
182
|
+
for (const task of activeTasks) {
|
|
183
|
+
pi.sendMessage({
|
|
184
|
+
customType: RELAY_LOSS_CUSTOM_TYPE,
|
|
185
|
+
content: `## task failed locally\ntask: \`${task.taskId}\`\n\nRelay state was lost; the remote outcome is unknown.`,
|
|
186
|
+
display: true,
|
|
187
|
+
details: { taskId: task.taskId, status: "failed", error: { code: RELAY_STATE_LOST_CODE, message: "relay state was lost; remote outcome is unknown", retryable: false }, remoteOutcome: "unknown" },
|
|
188
|
+
}, { triggerTurn: false });
|
|
189
|
+
}
|
|
190
|
+
pendingRelayLossCount = (pendingRelayLossCount ?? 0) + activeTasks.length;
|
|
191
|
+
closingTaskIds.clear();
|
|
192
|
+
try {
|
|
193
|
+
await ownedLoader.start();
|
|
194
|
+
if (recoveryEpoch !== lifecycleEpoch) return;
|
|
195
|
+
await ownedLoader(undefined, true);
|
|
196
|
+
if (recoveryEpoch !== lifecycleEpoch) return;
|
|
197
|
+
announceFreshBinding(context);
|
|
198
|
+
context.ui.setStatus("pi-tasks", undefined);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (recoveryEpoch !== lifecycleEpoch) return;
|
|
201
|
+
context.ui.setStatus("pi-tasks", context.ui.theme.fg("warning", outboxFailureStatus(error)));
|
|
202
|
+
}
|
|
203
|
+
if (recoveryEpoch === lifecycleEpoch) startBackgroundRefresh(context, recoveryEpoch);
|
|
204
|
+
})();
|
|
205
|
+
relayLossRecovery = pending;
|
|
206
|
+
try { await pending; } finally { if (relayLossRecovery === pending) relayLossRecovery = undefined; }
|
|
207
|
+
return true;
|
|
208
|
+
};
|
|
112
209
|
const refreshLifecycle = async (context: ExtensionContext, epoch: number): Promise<void> => {
|
|
113
210
|
const isCurrent = (): boolean => lifecycleEpoch === epoch && inboxContext === context;
|
|
211
|
+
let activeCore: (TaskCore & Partial<Pick<OwnedTaskCore, "close">>) | undefined;
|
|
114
212
|
try {
|
|
115
|
-
|
|
213
|
+
activeCore = await configuredCore();
|
|
116
214
|
let connectionError: unknown;
|
|
117
215
|
try {
|
|
118
216
|
await activeCore.connect();
|
|
@@ -120,12 +218,16 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
120
218
|
connectionError = error;
|
|
121
219
|
}
|
|
122
220
|
if (!isCurrent()) return;
|
|
221
|
+
if (isRelayLoss(connectionError) && await recoverRelayLoss(context, epoch, activeCore)) return;
|
|
123
222
|
const outboxError = await refreshInbox();
|
|
124
223
|
if (!isCurrent()) return;
|
|
224
|
+
if (isRelayLoss(outboxError) && await recoverRelayLoss(context, epoch, activeCore)) return;
|
|
125
225
|
const lifecycleError = outboxError ?? connectionError;
|
|
126
226
|
const status = lifecycleError === undefined ? undefined : outboxFailureStatus(lifecycleError);
|
|
127
227
|
context.ui.setStatus("pi-tasks", status === undefined ? undefined : context.ui.theme.fg("warning", status));
|
|
228
|
+
if (lifecycleError === undefined) announceFreshBinding(context);
|
|
128
229
|
} catch (error) {
|
|
230
|
+
if (isCurrent() && isRelayLoss(error) && await recoverRelayLoss(context, epoch, activeCore)) return;
|
|
129
231
|
if (isCurrent()) context.ui.setStatus("pi-tasks", context.ui.theme.fg("warning", outboxFailureStatus(error)));
|
|
130
232
|
}
|
|
131
233
|
};
|
|
@@ -159,34 +261,35 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
159
261
|
pi.on("session_start", async (_event, context) => {
|
|
160
262
|
closingTaskIds.clear();
|
|
161
263
|
const epoch = ++lifecycleEpoch;
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
const currentContext = inboxContext;
|
|
166
|
-
if (currentContext) void refreshLifecycle(currentContext, epoch);
|
|
167
|
-
}, BACKGROUND_POLL_MS);
|
|
264
|
+
await ownedLoader?.start();
|
|
265
|
+
if (epoch !== lifecycleEpoch) return;
|
|
266
|
+
startBackgroundRefresh(context, epoch);
|
|
168
267
|
await refreshLifecycle(context, epoch);
|
|
169
268
|
});
|
|
170
269
|
pi.on("agent_end", async (_event, context) => {
|
|
270
|
+
if (!inboxContext) return;
|
|
171
271
|
const epoch = lifecycleEpoch;
|
|
172
272
|
inboxContext = context;
|
|
173
273
|
await refreshLifecycle(context, epoch);
|
|
174
274
|
});
|
|
175
275
|
pi.on("agent_settled", async (_event, context) => {
|
|
276
|
+
if (!inboxContext) return;
|
|
176
277
|
const epoch = lifecycleEpoch;
|
|
177
278
|
inboxContext = context;
|
|
178
279
|
await refreshLifecycle(context, epoch);
|
|
179
280
|
});
|
|
180
|
-
pi.on("session_shutdown", () => {
|
|
281
|
+
pi.on("session_shutdown", async () => {
|
|
181
282
|
closingTaskIds.clear();
|
|
182
283
|
lifecycleEpoch += 1;
|
|
183
284
|
if (backgroundTimer) clearInterval(backgroundTimer);
|
|
184
285
|
backgroundTimer = undefined;
|
|
185
286
|
inboxContext = undefined;
|
|
287
|
+
pendingRelayLossCount = undefined;
|
|
288
|
+
await ownedLoader?.close();
|
|
186
289
|
});
|
|
187
290
|
|
|
188
291
|
pi.registerTool({
|
|
189
|
-
name: "agent_task_send", label: "Send Agent Task", description: "
|
|
292
|
+
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
293
|
async execute(_id, params, signal) {
|
|
191
294
|
try {
|
|
192
295
|
const sent = await (await configuredCore(signal)).createTask({ target: params.to, task: params.task, timeoutMs: params.timeoutMs ?? DEFAULT_TASK_TIMEOUT_MS }, signal);
|
|
@@ -201,7 +304,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
201
304
|
try {
|
|
202
305
|
const activeCore = await configuredCore(signal);
|
|
203
306
|
const task = activeCore.getTask(params.taskId);
|
|
204
|
-
if (!task) return taskError(new
|
|
307
|
+
if (!task) return taskError(new TaskProtocolError("UNKNOWN_TASK", `unknown task: ${params.taskId}`, { retryable: false }));
|
|
205
308
|
const deliveryEvidence = taskDeliveryEvidence(task);
|
|
206
309
|
return toolResult({ ...task, deliveryEvidence }, `## task status\n- task: \`${task.taskId}\`\n- status: ${task.status}${receiverAssignment(activeCore, task, context)}${deliveryEvidenceText(deliveryEvidence)}`);
|
|
207
310
|
} catch (error) { return taskError(error); }
|
|
@@ -215,7 +318,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
215
318
|
for (;;) {
|
|
216
319
|
await refreshInbox(signal);
|
|
217
320
|
const task = (await configuredCore(signal)).getTask(params.taskId);
|
|
218
|
-
if (!task) return taskError(new
|
|
321
|
+
if (!task) return taskError(new TaskProtocolError("UNKNOWN_TASK", `unknown task: ${params.taskId}`, { retryable: false }));
|
|
219
322
|
if (terminal(task.status)) return toolResult(task, `## task status\n- task: \`${task.taskId}\`\n- status: ${task.status}`);
|
|
220
323
|
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
324
|
onUpdate?.({ content: [{ type: "text", text: `waiting for ${params.taskId}...` }], details: {} });
|
|
@@ -237,12 +340,12 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
237
340
|
renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
|
|
238
341
|
});
|
|
239
342
|
pi.registerTool({
|
|
240
|
-
name: "agent_task_message", label: "Message Agent Task", description: "
|
|
343
|
+
name: "agent_task_message", label: "Message Agent Task", description: "Record a message intent in this lifetime before relay submission.", parameters: MessageParams,
|
|
241
344
|
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
345
|
renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
|
|
243
346
|
});
|
|
244
347
|
pi.registerTool({
|
|
245
|
-
name: "agent_task_cancel", label: "Cancel Agent Task", description: "
|
|
348
|
+
name: "agent_task_cancel", label: "Cancel Agent Task", description: "Record cancellation in this lifetime before relay submission.", parameters: TaskIdParams,
|
|
246
349
|
async execute(_id, params, signal) {
|
|
247
350
|
try {
|
|
248
351
|
const activeCore = await configuredCore(signal);
|
|
@@ -262,7 +365,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
262
365
|
renderResult(result, _options, theme) { return new Text(theme.fg("accent", text(result))); },
|
|
263
366
|
});
|
|
264
367
|
pi.registerTool({
|
|
265
|
-
name: "agent_task_done", label: "Complete Agent Task", description: "
|
|
368
|
+
name: "agent_task_done", label: "Complete Agent Task", description: "Record a terminal intent in this lifetime before relay submission.", parameters: DoneParams,
|
|
266
369
|
async execute(_id, params, signal) {
|
|
267
370
|
try {
|
|
268
371
|
const activeCore = await configuredCore(signal);
|
|
@@ -275,7 +378,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
275
378
|
return blockedDoneResult(activeCore, params.taskId, params.status, error) ?? taskError(error);
|
|
276
379
|
}
|
|
277
380
|
const task = activeCore.getTask(params.taskId);
|
|
278
|
-
if (!task) return taskError(new
|
|
381
|
+
if (!task) return taskError(new TaskProtocolError("UNKNOWN_TASK", `unknown task: ${params.taskId}`, { retryable: false }));
|
|
279
382
|
if (submission?.authority === "origin" && submission.canonicalEvent.type === "task.late_terminal") {
|
|
280
383
|
return {
|
|
281
384
|
...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 +416,34 @@ export default function piTasks(pi: ExtensionAPI): void {
|
|
|
313
416
|
}
|
|
314
417
|
|
|
315
418
|
interface TaskDeliveryEvidence {
|
|
316
|
-
readonly
|
|
419
|
+
readonly receiverReceipt: "not_confirmed" | "confirmed";
|
|
317
420
|
readonly piInsertion: "not_confirmed" | "blocked" | "confirmed";
|
|
318
421
|
readonly wakeAcceptance: "not_confirmed" | "pending" | "confirmed";
|
|
319
422
|
readonly modelExecution: "not_evidenced";
|
|
320
423
|
}
|
|
321
424
|
|
|
322
425
|
function taskDeliveryEvidence(task: TaskSnapshot): TaskDeliveryEvidence {
|
|
323
|
-
let
|
|
426
|
+
let receiverReceipt: TaskDeliveryEvidence["receiverReceipt"] = "not_confirmed";
|
|
324
427
|
let piInsertion: TaskDeliveryEvidence["piInsertion"] = "not_confirmed";
|
|
325
428
|
let wakeAcceptance: TaskDeliveryEvidence["wakeAcceptance"] = "not_confirmed";
|
|
326
429
|
const assignmentEventId = task.events.find((event) => event.type === "task.created")?.eventId;
|
|
327
430
|
for (const event of task.events) {
|
|
328
431
|
if (event.type !== "task.delivery_receipt" || event.payload.eventId !== assignmentEventId) continue;
|
|
329
|
-
if (event.payload.stage === TaskDeliveryStage.
|
|
432
|
+
if (event.payload.stage === TaskDeliveryStage.receiverRecorded) receiverReceipt = "confirmed";
|
|
330
433
|
if (event.payload.stage === TaskDeliveryStage.piInsertion && event.payload.state === TaskDeliveryEvidenceState.blocked) piInsertion = "blocked";
|
|
331
434
|
if (event.payload.stage === TaskDeliveryStage.piInserted || event.payload.stage === undefined) {
|
|
332
|
-
|
|
435
|
+
receiverReceipt = "confirmed";
|
|
333
436
|
piInsertion = "confirmed";
|
|
334
437
|
}
|
|
335
438
|
if (event.payload.stage === TaskDeliveryStage.wakeRequested) wakeAcceptance = "pending";
|
|
336
439
|
if (event.payload.stage === TaskDeliveryStage.wakeAccepted) wakeAcceptance = "confirmed";
|
|
337
440
|
}
|
|
338
|
-
return {
|
|
441
|
+
return { receiverReceipt, piInsertion, wakeAcceptance, modelExecution: "not_evidenced" };
|
|
339
442
|
}
|
|
340
443
|
|
|
341
444
|
function deliveryEvidenceText(evidence: TaskDeliveryEvidence): string {
|
|
342
445
|
const insertion = evidence.piInsertion === "blocked" ? "blocked; retryable" : evidence.piInsertion;
|
|
343
|
-
return `\n- receiver
|
|
446
|
+
return `\n- receiver receipt (RAM): ${evidence.receiverReceipt}\n- Pi insertion: ${insertion}\n- wake acceptance: ${evidence.wakeAcceptance}\n- model execution: ${evidence.modelExecution}`;
|
|
344
447
|
}
|
|
345
448
|
|
|
346
449
|
function toolResult(details: unknown, markdown: string): AgentToolResult<unknown> {
|
|
@@ -393,10 +496,19 @@ function blockedDeliveryWarning(error: TaskOutboxDeliveryError, target: TaskEndp
|
|
|
393
496
|
return { code: error.code, message: error.message, retryable: false, delivery: "blocked", target, details };
|
|
394
497
|
}
|
|
395
498
|
|
|
396
|
-
function outboxFailureStatus(error: unknown):
|
|
499
|
+
function outboxFailureStatus(error: unknown): string {
|
|
500
|
+
if (error instanceof TaskProtocolError) {
|
|
501
|
+
if (RELAY_LOSS_CODES.has(error.code)) return "tasks: relay state lost; rebinding automatically";
|
|
502
|
+
if (error.code === "RELAY_PROFILE_REQUIRED") return "tasks: compatible memory-owned relay required";
|
|
503
|
+
if (error.code === "RELAY_AUTH_REQUIRED") return "tasks: Wolfpack authentication required";
|
|
504
|
+
}
|
|
397
505
|
return error instanceof TaskOutboxDeliveryError && error.code === TARGET_NOT_REGISTERED_CODE ? "tasks: outbox degraded" : "tasks: relay unavailable";
|
|
398
506
|
}
|
|
399
507
|
|
|
508
|
+
function isRelayLoss(error: unknown): error is TaskProtocolError {
|
|
509
|
+
return error instanceof TaskProtocolError && RELAY_LOSS_CODES.has(error.code);
|
|
510
|
+
}
|
|
511
|
+
|
|
400
512
|
function terminal(status: string): boolean {
|
|
401
513
|
return ["completed", "failed", "cancelled", "timed_out"].includes(status);
|
|
402
514
|
}
|
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
|
-
|
|
6
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
}
|
package/src/task-protocol.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|