@sgtbeatdown/pi-tasks 0.1.11 → 0.1.12
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/package.json +1 -1
- package/src/extension.ts +4 -4
- package/src/task-core.ts +2 -3
- package/src/task-inbox.ts +32 -15
- package/src/task-protocol.ts +13 -0
- package/src/volatile-task-session.ts +6 -1
package/package.json
CHANGED
package/src/extension.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Type } from "typebox";
|
|
|
5
5
|
|
|
6
6
|
import { abortableSleep } from "./abortable-sleep";
|
|
7
7
|
import { deliverTaskInbox, incorporatedTaskEvents } from "./task-inbox";
|
|
8
|
-
import { TaskDeliveryEvidenceState, TaskDeliveryStage, TaskOutboxDeliveryError, TaskProtocolError } from "./task-protocol";
|
|
8
|
+
import { TaskDeliveryEvidenceState, TaskDeliveryStage, TaskOutboxDeliveryError, TaskProtocolError, isBlockedOutboxDeliveryCode } from "./task-protocol";
|
|
9
9
|
import { createConfiguredTaskCore } from "./configured-task-core";
|
|
10
10
|
import type { OwnedTaskCore } from "./configured-task-core";
|
|
11
11
|
import type { SubmitIntentInput, SubmitIntentOutcome, TaskCore } from "./task-core";
|
|
@@ -143,7 +143,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
143
143
|
if (isCurrent()) await activeCore.recordInsertion(input, insertionSignal);
|
|
144
144
|
},
|
|
145
145
|
};
|
|
146
|
-
await deliverTaskInbox({
|
|
146
|
+
const inboxOutboxError = await deliverTaskInbox({
|
|
147
147
|
sendMessage(message, options) { if (isCurrent()) pi.sendMessage(message, options); },
|
|
148
148
|
appendEntry(customType, data) { if (isCurrent()) pi.appendEntry(customType, data); },
|
|
149
149
|
}, guardedCore, {
|
|
@@ -151,7 +151,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
151
151
|
hasPendingMessages: (): boolean => !isCurrent() || context.hasPendingMessages(),
|
|
152
152
|
sessionManager: context.sessionManager,
|
|
153
153
|
}, signal);
|
|
154
|
-
return outboxError;
|
|
154
|
+
return outboxError ?? inboxOutboxError;
|
|
155
155
|
});
|
|
156
156
|
const startBackgroundRefresh = (context: ExtensionContext, epoch: number): void => {
|
|
157
157
|
inboxContext = context;
|
|
@@ -502,7 +502,7 @@ function outboxFailureStatus(error: unknown): string {
|
|
|
502
502
|
if (error.code === "RELAY_PROFILE_REQUIRED") return "tasks: compatible memory-owned relay required";
|
|
503
503
|
if (error.code === "RELAY_AUTH_REQUIRED") return "tasks: Wolfpack authentication required";
|
|
504
504
|
}
|
|
505
|
-
return error instanceof TaskOutboxDeliveryError && error.code
|
|
505
|
+
return error instanceof TaskOutboxDeliveryError && isBlockedOutboxDeliveryCode(error.code) ? "tasks: outbox degraded" : "tasks: relay unavailable";
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
function isRelayLoss(error: unknown): error is TaskProtocolError {
|
package/src/task-core.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MAX_RELAY_PAYLOAD_BYTES,
|
|
3
|
-
INVALID_RELAY_METADATA,
|
|
4
3
|
ORIGIN_CANCELLATION_OPERATION,
|
|
5
4
|
PARENT_ACKNOWLEDGMENT_OPERATION,
|
|
6
5
|
TASK_PROTOCOL_VERSION,
|
|
@@ -10,6 +9,7 @@ import {
|
|
|
10
9
|
TaskEnvelopeKind,
|
|
11
10
|
TaskOutboxDeliveryError,
|
|
12
11
|
TaskProtocolError,
|
|
12
|
+
isBlockedOutboxDeliveryCode,
|
|
13
13
|
} from "./task-protocol";
|
|
14
14
|
import type {
|
|
15
15
|
RelayDelivery,
|
|
@@ -25,7 +25,6 @@ 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 TERMINAL_DELIVERY_CODES = new Set(["TARGET_NOT_REGISTERED", INVALID_RELAY_METADATA, "DELIVERY_UNCONFIRMED", "ENVELOPE_EXPIRED", "ENVELOPE_CONFLICT", "CROSS_RELAY_ENDPOINT"]);
|
|
29
28
|
const TERMINAL_STATUSES = new Set<TaskRecord["status"]>(["completed", "failed", "cancelled", "timed_out"]);
|
|
30
29
|
const TERMINAL_EVENTS = new Set(["task.completed", "task.failed", "task.cancelled", "task.timed_out"]);
|
|
31
30
|
const CANONICAL_EVENTS = new Set([
|
|
@@ -401,7 +400,7 @@ async function flush(options: TaskCoreOptions, signal: AbortSignal | undefined,
|
|
|
401
400
|
options.store.transaction(() => { options.store.markOutboxAccepted(record.envelope.envelopeId); });
|
|
402
401
|
} catch (error) {
|
|
403
402
|
if (signal?.aborted || (error instanceof TaskProtocolError && error.code === "ABORTED")) throw error;
|
|
404
|
-
if (error instanceof TaskProtocolError &&
|
|
403
|
+
if (error instanceof TaskProtocolError && isBlockedOutboxDeliveryCode(error.code) && !error.retryable) {
|
|
405
404
|
options.store.transaction(() => {
|
|
406
405
|
options.store.quarantineOutbox(record.envelope.envelopeId, {
|
|
407
406
|
errorCode: error.code,
|
package/src/task-inbox.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TaskCore } from "./task-core";
|
|
2
|
-
import { TaskDeliveryEvidenceState, TaskDeliveryStage, TaskEnvelopeKind, TaskProtocolError } from "./task-protocol";
|
|
2
|
+
import { TaskDeliveryEvidenceState, TaskDeliveryStage, TaskEnvelopeKind, TaskOutboxDeliveryError, TaskProtocolError, isBlockedOutboxDeliveryCode } from "./task-protocol";
|
|
3
3
|
import type { RelayDelivery, TaskEvent } from "./task-protocol";
|
|
4
4
|
|
|
5
5
|
const TASK_EVENT_CUSTOM_TYPE = "pi-tasks-event";
|
|
@@ -33,9 +33,10 @@ type DeliveryEvidencePayload =
|
|
|
33
33
|
| { readonly stage: typeof TaskDeliveryStage.piInsertion; readonly state: typeof TaskDeliveryEvidenceState.blocked; readonly retryable: true };
|
|
34
34
|
|
|
35
35
|
/** Persists model-visible Pi evidence before advancing the relay cursor, then starts one separate turn. */
|
|
36
|
-
export async function deliverTaskInbox(pi: InboxPi, core: TaskCore, context: InboxContext, signal?: AbortSignal): Promise<
|
|
37
|
-
if (context.hasPendingMessages()) return;
|
|
36
|
+
export async function deliverTaskInbox(pi: InboxPi, core: TaskCore, context: InboxContext, signal?: AbortSignal): Promise<TaskOutboxDeliveryError | undefined> {
|
|
37
|
+
if (context.hasPendingMessages()) return undefined;
|
|
38
38
|
const deliveries = await core.receive(signal);
|
|
39
|
+
let outboxError: TaskOutboxDeliveryError | undefined;
|
|
39
40
|
for (const delivery of deliveries) {
|
|
40
41
|
if (delivery.envelope.kind === TaskEnvelopeKind.intent) {
|
|
41
42
|
await core.acknowledgeRelayDelivery(delivery.cursor, signal);
|
|
@@ -44,7 +45,7 @@ export async function deliverTaskInbox(pi: InboxPi, core: TaskCore, context: Inb
|
|
|
44
45
|
}
|
|
45
46
|
const event = inboxEvent(delivery);
|
|
46
47
|
if (!isKnownEvent(event.type)) throw new TaskProtocolError("UNKNOWN_EVENT", `unknown task inbox event type: ${event.type}`);
|
|
47
|
-
if (context.hasPendingMessages()) return;
|
|
48
|
+
if (context.hasPendingMessages()) return outboxError;
|
|
48
49
|
const eventDetails = { taskId: event.taskId, eventId: event.eventId };
|
|
49
50
|
const eventKey = key(event.taskId, event.eventId);
|
|
50
51
|
if (!isModelVisible(event.type) && !recordedEventKeys(context.sessionManager.getEntries()).has(eventKey)) {
|
|
@@ -53,11 +54,12 @@ export async function deliverTaskInbox(pi: InboxPi, core: TaskCore, context: Inb
|
|
|
53
54
|
pi.appendEntry(TASK_RECORD_CUSTOM_TYPE, { ...eventDetails, event });
|
|
54
55
|
}
|
|
55
56
|
if (event.type === "task.created") {
|
|
56
|
-
await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.receiverRecorded, state: TaskDeliveryEvidenceState.confirmed }, signal);
|
|
57
|
+
const evidenceError = await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.receiverRecorded, state: TaskDeliveryEvidenceState.confirmed }, signal);
|
|
58
|
+
outboxError ??= evidenceError;
|
|
57
59
|
}
|
|
58
60
|
let incorporated = incorporatedEvents(context.sessionManager.getEntries()).has(eventKey);
|
|
59
61
|
if (!incorporated && isModelVisible(event.type)) {
|
|
60
|
-
if (!context.isIdle()) return;
|
|
62
|
+
if (!context.isIdle()) return outboxError;
|
|
61
63
|
pi.sendMessage({
|
|
62
64
|
customType: TASK_EVENT_CUSTOM_TYPE,
|
|
63
65
|
content: renderTaskEvent(event),
|
|
@@ -66,16 +68,19 @@ export async function deliverTaskInbox(pi: InboxPi, core: TaskCore, context: Inb
|
|
|
66
68
|
}, { triggerTurn: false });
|
|
67
69
|
incorporated = incorporatedEvents(context.sessionManager.getEntries()).has(eventKey);
|
|
68
70
|
if (!incorporated) {
|
|
69
|
-
await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.piInsertion, state: TaskDeliveryEvidenceState.blocked, retryable: true }, signal);
|
|
70
|
-
|
|
71
|
+
const evidenceError = await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.piInsertion, state: TaskDeliveryEvidenceState.blocked, retryable: true }, signal);
|
|
72
|
+
outboxError ??= evidenceError;
|
|
73
|
+
return outboxError;
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
if (isModelVisible(event.type)) {
|
|
74
|
-
await core.recordInsertion(eventDetails, signal);
|
|
77
|
+
const insertionError = await settleDeliveryEvidence(() => core.recordInsertion(eventDetails, signal));
|
|
78
|
+
outboxError ??= insertionError;
|
|
75
79
|
let wakeAccepted = taskMessageKeys(context.sessionManager.getEntries(), TASK_WAKE_CUSTOM_TYPE).has(eventKey);
|
|
76
80
|
if (!wakeAccepted) {
|
|
77
|
-
if (!context.isIdle()) return;
|
|
78
|
-
await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.wakeRequested, state: TaskDeliveryEvidenceState.confirmed }, signal);
|
|
81
|
+
if (!context.isIdle()) return outboxError;
|
|
82
|
+
const wakeRequestError = await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.wakeRequested, state: TaskDeliveryEvidenceState.confirmed }, signal);
|
|
83
|
+
outboxError ??= wakeRequestError;
|
|
79
84
|
pi.sendMessage({
|
|
80
85
|
customType: TASK_WAKE_CUSTOM_TYPE,
|
|
81
86
|
content: "Process the pending Pi task event.",
|
|
@@ -83,17 +88,29 @@ export async function deliverTaskInbox(pi: InboxPi, core: TaskCore, context: Inb
|
|
|
83
88
|
details: eventDetails,
|
|
84
89
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
85
90
|
wakeAccepted = taskMessageKeys(context.sessionManager.getEntries(), TASK_WAKE_CUSTOM_TYPE).has(eventKey);
|
|
86
|
-
if (!wakeAccepted) return;
|
|
91
|
+
if (!wakeAccepted) return outboxError;
|
|
87
92
|
}
|
|
88
|
-
await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.wakeAccepted, state: TaskDeliveryEvidenceState.confirmed }, signal);
|
|
93
|
+
const wakeAcceptanceError = await recordDeliveryEvidence(core, eventDetails, { stage: TaskDeliveryStage.wakeAccepted, state: TaskDeliveryEvidenceState.confirmed }, signal);
|
|
94
|
+
outboxError ??= wakeAcceptanceError;
|
|
89
95
|
}
|
|
90
96
|
await core.acknowledgeRelayDelivery(delivery.cursor, signal);
|
|
91
97
|
pi.appendEntry(TASK_CURSOR_CUSTOM_TYPE, { cursor: delivery.cursor });
|
|
92
98
|
}
|
|
99
|
+
return outboxError;
|
|
93
100
|
}
|
|
94
101
|
|
|
95
|
-
async function recordDeliveryEvidence(core: TaskCore, event: TaskEventDetails, evidence: DeliveryEvidencePayload, signal?: AbortSignal): Promise<
|
|
96
|
-
|
|
102
|
+
async function recordDeliveryEvidence(core: TaskCore, event: TaskEventDetails, evidence: DeliveryEvidencePayload, signal?: AbortSignal): Promise<TaskOutboxDeliveryError | undefined> {
|
|
103
|
+
return settleDeliveryEvidence(() => core.submitIntent({ taskId: event.taskId, type: "task.delivery_receipt", payload: { eventId: event.eventId, ...evidence } }, signal));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function settleDeliveryEvidence(operation: () => Promise<void>): Promise<TaskOutboxDeliveryError | undefined> {
|
|
107
|
+
try {
|
|
108
|
+
await operation();
|
|
109
|
+
return undefined;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof TaskOutboxDeliveryError && !error.retryable && isBlockedOutboxDeliveryCode(error.code)) return error;
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
function inboxEvent(delivery: RelayDelivery): TaskEvent {
|
package/src/task-protocol.ts
CHANGED
|
@@ -5,6 +5,19 @@ export const ORIGIN_CANCELLATION_OPERATION = "origin_cancellation";
|
|
|
5
5
|
export const PARENT_ACKNOWLEDGMENT_OPERATION = "parent_acknowledgment";
|
|
6
6
|
export const TERMINAL_INTENT_OPERATION = "terminal_intent";
|
|
7
7
|
|
|
8
|
+
const BLOCKED_OUTBOX_DELIVERY_CODES: ReadonlySet<string> = new Set([
|
|
9
|
+
"TARGET_NOT_REGISTERED",
|
|
10
|
+
INVALID_RELAY_METADATA,
|
|
11
|
+
"DELIVERY_UNCONFIRMED",
|
|
12
|
+
"ENVELOPE_EXPIRED",
|
|
13
|
+
"ENVELOPE_CONFLICT",
|
|
14
|
+
"CROSS_RELAY_ENDPOINT",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function isBlockedOutboxDeliveryCode(code: string): boolean {
|
|
18
|
+
return BLOCKED_OUTBOX_DELIVERY_CODES.has(code);
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
export const TaskDeliveryStage = {
|
|
9
22
|
receiverRecorded: "receiver_recorded",
|
|
10
23
|
piInsertion: "pi_insertion",
|
|
@@ -223,7 +223,12 @@ class Transport {
|
|
|
223
223
|
if (controller.signal.aborted) throw abortError();
|
|
224
224
|
const response = await this.fetcher(this.url, { method: "POST", headers: { "content-type": "application/json" }, body: encoded, redirect: "error", signal: controller.signal });
|
|
225
225
|
if (controller.signal.aborted) { void response.body?.cancel().catch(() => undefined); throw abortError(); }
|
|
226
|
-
|
|
226
|
+
const rateLimited = response.status === 429;
|
|
227
|
+
if (response.status === 401 || rateLimited) {
|
|
228
|
+
// Admission failures need no relay profile. Best-effort cleanup must not mask their status.
|
|
229
|
+
void response.body?.cancel().catch(() => undefined);
|
|
230
|
+
throw failure(rateLimited ? "RELAY_CAPACITY" : "RELAY_AUTH_REQUIRED", rateLimited);
|
|
231
|
+
}
|
|
227
232
|
const reader = response.body?.getReader();
|
|
228
233
|
if (!reader) throw failure("INVALID_RESPONSE", true);
|
|
229
234
|
const cancel = () => { void reader.cancel().catch(() => undefined); };
|