@sgtbeatdown/pi-tasks 0.1.13 → 0.1.15
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 +146 -58
- package/docs/protocol-reference.md +111 -0
- package/package.json +2 -1
- package/src/extension.ts +23 -19
- package/src/task-core.ts +200 -91
- package/src/task-inbox.ts +75 -62
- package/src/task-store.ts +61 -10
- package/src/task-work-scope.ts +63 -0
package/README.md
CHANGED
|
@@ -1,101 +1,189 @@
|
|
|
1
1
|
# pi-tasks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**give another pi agent a task, exchange messages, and get a structured result back.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
pi-tasks adds coordination tools to [pi](https://pi.dev). a parent agent can hand work to another session, keep working, and receive a follow-up when the worker asks a question or finishes. it works between sessions on one machine and, through wolfpack's trusted tailnet routing, across machines.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
for example: an implementer works on a change while the parent investigates something else. the implementer reports its result; the parent checks the diff and tests, then acknowledges the task.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**pi-tasks coordinates work. it does not create agent sessions or execute the assignment itself.** wolfpack manages sessions and carries messages; the agents do the work.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## how it works
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"id": "target-opaque-endpoint-id"
|
|
20
|
-
},
|
|
21
|
-
"task": "implement the narrow change and run focused tests"
|
|
22
|
-
}
|
|
13
|
+
```text
|
|
14
|
+
parent agent ── assignment ──> worker agent
|
|
15
|
+
<── questions ──
|
|
16
|
+
─── answers ───>
|
|
17
|
+
<─── result ────
|
|
18
|
+
─ acknowledgment >
|
|
23
19
|
```
|
|
24
20
|
|
|
25
|
-
|
|
21
|
+
1. the parent sends instructions to the worker's **task endpoint**: an opaque address for that running session.
|
|
22
|
+
2. the relay carries the assignment. the worker's pi extension queues it as a follow-up, without interrupting an active turn.
|
|
23
|
+
3. the worker does the work, optionally exchanges messages, and submits a structured completion or failure.
|
|
24
|
+
4. the parent receives the result, verifies it, and acknowledges the task.
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
completion ends the **task**, not the **session**. keep a worker for another assignment, or close it through wolfpack when you no longer need it.
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
- Active tasks, outbox intentions, individual ACK checkpoints, deduplication and delivery-blocked evidence are RAM-owned. Each endpoint has lower-only limits of16,384 records/32MiB encoded state. Admission fails before exceeding those bounds; they are not a process-RSS guarantee.
|
|
31
|
-
- `agent_task_send` confirms relay acceptance, **not execution**. `receiver_recorded` confirms receiver RAM receipt, **not durable persistence**. Pi insertion and wake acceptance are separate evidence, never proof of model execution.
|
|
32
|
-
- Session history is not an operational journal. A new process/lifecycle starts empty with a fresh endpoint/generation; it does not reconstruct tasks, replay envelopes, reuse old ACKs or reopen a worker gate from old history.
|
|
33
|
-
- A relay restart can lose even accepted mail. A Pi restart loses that endpoint's active task state. Relay-loss evidence marks affected work failed only in the discarded local lifetime; its remote outcome remains explicitly unknown and is never presented as canonical delivery or completion evidence.
|
|
28
|
+
## use with wolfpack
|
|
34
29
|
|
|
35
|
-
|
|
30
|
+
this is the ready-made integration. you need pi, a compatible wolfpack memory-only server, and this package loaded in every participating pi session.
|
|
36
31
|
|
|
37
|
-
|
|
32
|
+
### 1. install the pi package
|
|
38
33
|
|
|
39
|
-
|
|
34
|
+
```bash
|
|
35
|
+
pi install npm:@sgtbeatdown/pi-tasks
|
|
36
|
+
```
|
|
40
37
|
|
|
41
|
-
|
|
38
|
+
start new pi sessions after installation. wolfpack supplies `WOLFPACK_SESSION_NAME` to its sessions; the extension uses it to register the current process. the local control port defaults to `18790` (`WOLFPACK_PORT` overrides it).
|
|
42
39
|
|
|
43
|
-
|
|
40
|
+
### 2. open a worker
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
from a pi parent running under wolfpack:
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
```bash
|
|
45
|
+
# choose an available pi model explicitly.
|
|
46
|
+
MODEL="openai-codex/gpt-6.1-sol:medium"
|
|
47
|
+
wolfpack agent spawn --project-dir /absolute/worktree \
|
|
48
|
+
--name task-implementation --model "$MODEL" \
|
|
49
|
+
--task-worker --readiness-timeout-ms 30000 --json
|
|
50
|
+
```
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
replace `/absolute/worktree` with the directory the worker should use. this starts a prompt-free worker and returns a stable `sessionId` and a registered `taskEndpoint`. registration means the session can receive mail—not that its model has executed anything.
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
for an existing session, use `wolfpack session status <sessionId> --json`. copy its returned `taskEndpoint` unchanged; never construct an endpoint from a session name.
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
### 3. assign work
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
ask the parent agent to call `agent_task_send` with that endpoint:
|
|
56
59
|
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"to": {
|
|
63
|
+
"relay": "wolfpack-pi-tasks-v2",
|
|
64
|
+
"id": "target-opaque-endpoint-id"
|
|
65
|
+
},
|
|
66
|
+
"task": "implement the narrow change and run focused tests",
|
|
67
|
+
"timeoutMs": 3600000
|
|
68
|
+
}
|
|
60
69
|
```
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
2. Verify exact session ID/root/Pi harness and registered `taskEndpoint`. `TASK_WORKER_PREFLIGHT_FAILED` precedes creation. `TASK_WORKER_NOT_READY` retains `createdSession`/`cleanup`; inspect `unconfirmed` cleanup by exact ID before retry. Registration is not model execution.
|
|
64
|
-
3. Submit the assignment and continue useful work. When none remains, yield to structured follow-up. Do not poll `agent_task_status` or `agent_task_inbox` merely to wait; use `agent_task_wait` only when explicitly asked to block.
|
|
65
|
-
4. Use `agent_task_message` for questions, answers and information. The receiver's last action is `agent_task_done`, not completion prose.
|
|
66
|
-
5. Independently verify, then acknowledge the terminal task and deliberately retain or clean up the parent-owned role.
|
|
71
|
+
replace `to` with the actual returned `{ relay, id }`. put the complete assignment in `task`; do not also give the worker a startup prompt. the timeout is a failure deadline, not a polling interval. allow at least an hour for coding work.
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
### 4. receive and verify the result
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
continue useful work after sending. when none remains, yield the turn: structured follow-ups bring questions and results back to the parent. do not repeatedly poll just to wait.
|
|
71
76
|
|
|
72
|
-
|
|
77
|
+
| tool | purpose |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `agent_task_send` | assign work to an endpoint |
|
|
80
|
+
| `agent_task_message` | send a question, answer, or progress update |
|
|
81
|
+
| `agent_task_done` | worker submits its final status and result |
|
|
82
|
+
| `agent_task_ack` | parent acknowledges a terminal task after verification |
|
|
83
|
+
| `agent_task_status` / `agent_task_inbox` | inspect locally known task state |
|
|
84
|
+
| `agent_task_cancel` | parent cancels its task |
|
|
85
|
+
| `agent_task_wait` | explicitly block for a terminal result when requested |
|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
workers report changed source paths in `result.changedFiles`. artifact declarations point to files for the parent to inspect; they do **not** transfer file contents.
|
|
75
88
|
|
|
76
|
-
|
|
89
|
+
when finished with a session you created, run `wolfpack kill <sessionId> --json`, then verify that exact ID is absent from `wolfpack list --json`.
|
|
77
90
|
|
|
78
|
-
|
|
91
|
+
### across machines
|
|
79
92
|
|
|
80
|
-
|
|
93
|
+
select a configured tailnet peer with `wolfpack --machine <peer>`. remote child spawning requires a parent on that machine; for a worker coordinated from here, use remote top-level creation:
|
|
81
94
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
```bash
|
|
96
|
+
wolfpack --machine <peer> session create --project-dir /absolute/remote/worktree \
|
|
97
|
+
--harness pi --task-worker --readiness-timeout-ms 30000 --json
|
|
98
|
+
```
|
|
85
99
|
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
paths are on the remote host. the CLI qualifies the remote endpoint through your local coordinator and returns a locally routable `taskEndpoint`. use that returned address in the same send flow. if it reports `taskEndpointError`, inspect the retained `sessionId` rather than blindly creating another session. use the same `--machine` selector for cleanup.
|
|
101
|
+
|
|
102
|
+
see the [delegation workflow](skills/wolfpack-pi-task-delegation/SKILL.md) for role reuse, worker restrictions, readiness errors, and complete lifecycle rules.
|
|
103
|
+
|
|
104
|
+
## use without wolfpack
|
|
105
|
+
|
|
106
|
+
**the task core is transport-independent; the default pi extension is not.** you can import the core into your own application and supply a `TaskRelay`. there is no bundled alternative production network relay or configuration switch that makes the default extension wolfpack-free.
|
|
107
|
+
|
|
108
|
+
### try the core in one process
|
|
109
|
+
|
|
110
|
+
with the package available to your bun project, save this as `task-demo.ts` and run `bun task-demo.ts`. it uses the exported in-memory relay fixture, so it needs neither wolfpack nor a model:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import {
|
|
114
|
+
createInMemoryTaskRelay,
|
|
115
|
+
createTaskCore,
|
|
116
|
+
createTaskStore,
|
|
117
|
+
} from "@sgtbeatdown/pi-tasks";
|
|
118
|
+
|
|
119
|
+
const relay = createInMemoryTaskRelay("demo");
|
|
120
|
+
const parentStore = createTaskStore();
|
|
121
|
+
const workerStore = createTaskStore();
|
|
122
|
+
const parent = createTaskCore({
|
|
123
|
+
endpoint: { relay: relay.id, id: "parent" },
|
|
124
|
+
relay,
|
|
125
|
+
store: parentStore,
|
|
126
|
+
});
|
|
127
|
+
const worker = createTaskCore({
|
|
128
|
+
endpoint: { relay: relay.id, id: "worker" },
|
|
129
|
+
relay,
|
|
130
|
+
store: workerStore,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await parent.connect();
|
|
135
|
+
await worker.connect();
|
|
136
|
+
const { taskId } = await parent.createTask({
|
|
137
|
+
target: worker.endpoint,
|
|
138
|
+
task: "summarize the change",
|
|
139
|
+
timeoutMs: 60_000,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// a real application would consume the assignment and do the work here.
|
|
143
|
+
for (const delivery of await worker.receive()) {
|
|
144
|
+
await worker.acknowledgeRelayDelivery(delivery.cursor);
|
|
145
|
+
}
|
|
146
|
+
await worker.submitIntent({
|
|
147
|
+
taskId,
|
|
148
|
+
type: "task.completed",
|
|
149
|
+
payload: { summary: "the change adds task coordination" },
|
|
150
|
+
});
|
|
151
|
+
for (const delivery of await parent.receive()) {
|
|
152
|
+
await parent.acknowledgeRelayDelivery(delivery.cursor);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(parent.getTask(taskId)?.status); // completed
|
|
156
|
+
await parent.acknowledgeParent(taskId);
|
|
157
|
+
} finally {
|
|
158
|
+
parentStore.close();
|
|
159
|
+
workerStore.close();
|
|
160
|
+
}
|
|
88
161
|
```
|
|
89
162
|
|
|
90
|
-
|
|
163
|
+
this demonstrates task coordination, not agent execution. both endpoints share one in-process fixture; it does not connect separate processes or machines.
|
|
164
|
+
|
|
165
|
+
### integrate your own transport
|
|
166
|
+
|
|
167
|
+
implement the exported [`TaskRelay` interface](src/task-protocol.ts) to connect endpoints, resolve targets, send envelopes, receive mail, and acknowledge individual deliveries. provide a separate `createTaskStore()` and `createTaskCore()` for each endpoint. `runTaskRelayConformance` is available to check a relay implementation.
|
|
168
|
+
|
|
169
|
+
your application owns polling, retries, timeout evaluation, delivery acknowledgments, and shutdown. it also owns how assignments reach an agent and how that agent runs them. the core alone does not install pi tools or wake a model.
|
|
170
|
+
|
|
171
|
+
## important limits
|
|
172
|
+
|
|
173
|
+
- **active state lives in ram.** a pi restart loses that endpoint's tasks; a relay restart can lose even accepted mail. pi session history remains readable, but does not restore tasks or authorize workers after restart.
|
|
174
|
+
- **accepted is not executed.** relay acceptance, receiver receipt, pi follow-up insertion, model execution, and task completion are separate stages.
|
|
175
|
+
- **the parent owns task status.** a worker submits a result; the origin processes it before its task becomes terminal. status reads are local snapshots.
|
|
176
|
+
- **use a trusted network.** the wolfpack integration assumes trusted tailnet machines. do not expose owner APIs to the public internet or an untrusted proxy.
|
|
177
|
+
|
|
178
|
+
## reference and development
|
|
179
|
+
|
|
180
|
+
- [protocol and operations reference](docs/protocol-reference.md): memory lifecycle, restart loss, authentication, worker gate, delivery evidence, and acknowledgment details.
|
|
181
|
+
- [wolfpack relay control API](https://github.com/almogdepaz/wolfpack/blob/main/docs/control-api-schema.md#pi-tasks-relay-v2-boundary): transport contract.
|
|
91
182
|
|
|
92
183
|
```bash
|
|
93
184
|
bun install
|
|
94
185
|
bun test
|
|
95
186
|
bun run typecheck
|
|
96
|
-
PI_TASKS_WOLFPACK_SOURCE=/absolute/clean/wolfpack-checkout \
|
|
97
|
-
PI_TASKS_WOLFPACK_REVISION=<full-commit-id> \
|
|
98
|
-
bun test tests/volatile-real-worker.test.ts tests/volatile-extension-worker.test.ts
|
|
99
187
|
```
|
|
100
188
|
|
|
101
|
-
|
|
189
|
+
optional real-worker checks and their prerequisites are documented in the [reference](docs/protocol-reference.md#development-and-verification).
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# protocol and operations reference
|
|
2
|
+
|
|
3
|
+
start with the [readme](../README.md) for an overview, quickstart, and the transport-independent core example. this reference covers the default wolfpack integration.
|
|
4
|
+
|
|
5
|
+
Endpoint-owned task coordination for Pi over Wolfpack's memory-only relay. **Both relay mail and endpoint task bookkeeping live in RAM.** Pi's existing session history is the historical record; there is no SQLite database, task-history directory, migration, disk outbox or startup replay.
|
|
6
|
+
|
|
7
|
+
The [relay control API](https://github.com/almogdepaz/wolfpack/blob/main/docs/control-api-schema.md#pi-tasks-relay-v2-boundary) defines the transport boundary.
|
|
8
|
+
|
|
9
|
+
## endpoint-owned relay
|
|
10
|
+
|
|
11
|
+
The default extension requires a compatible Wolfpack memory-owned server. It negotiates `volatile-v1` at `POST /api/task-relay/volatile-v1`. No transport opt-in flag is needed. The old durable relay adapter is removed, not retained as a compatibility mode.
|
|
12
|
+
|
|
13
|
+
Set `WOLFPACK_SESSION_NAME` for each Pi process. `WOLFPACK_PORT` defaults to `18790`; `WOLFPACK_SESSION_NAME` resolves the active Pi process to its relay endpoint. Read `taskEndpoint` from structured session creation/status and pass its opaque `{ relay, id }` unchanged. Never derive it from a name, terminal output or broker ID.
|
|
14
|
+
|
|
15
|
+
### minimal valid v2 send envelope
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"to": {
|
|
20
|
+
"relay": "wolfpack-pi-tasks-v2",
|
|
21
|
+
"id": "target-opaque-endpoint-id"
|
|
22
|
+
},
|
|
23
|
+
"task": "implement the narrow change and run focused tests"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The `agent_task_send` schema is exactly `to`, `task`, and optional `timeoutMs`. Unsupported fields are rejected before admission: a pre-admission validation rejection creates no task. If an outcome is uncertain, idempotency remains necessary within the current lifetime; inspect the returned task ID rather than blindly creating another assignment.
|
|
28
|
+
|
|
29
|
+
## history versus live state
|
|
30
|
+
|
|
31
|
+
- Pi requests task-processing turns through its safe `deliverAs: "followUp"` queue. Pi session messages and tool calls/results preserve historical context. Non-waking receipts, parent ACKs, late-terminal facts and confirmed receiver-authored conversation echoes are archived as `pi-tasks-event-record` custom entries before transport ACK, without model insertion or wake. Received model-visible task events include the complete structured event in `pi-tasks-event` details, including results—not just a rendered summary.
|
|
32
|
+
- Active tasks, outbox intentions, individual ACK checkpoints, deduplication and delivery-blocked evidence are RAM-owned. Each endpoint has lower-only limits of 16,384 records/32MiB encoded state. Admission fails before exceeding those bounds; they are not a process-RSS guarantee.
|
|
33
|
+
- `agent_task_send` confirms relay acceptance, **not execution**. `receiver_recorded` confirms receiver RAM receipt, **not durable persistence**. Pi insertion and wake acceptance are separate evidence, never proof of model execution.
|
|
34
|
+
- Session history is not an operational journal. A new process/lifecycle starts empty with a fresh endpoint/generation; it does not reconstruct tasks, replay envelopes, reuse old ACKs or reopen a worker gate from old history.
|
|
35
|
+
- A relay restart can lose even accepted mail. A Pi restart loses that endpoint's active task state. Relay-loss evidence marks affected work failed only in the discarded local lifetime; its remote outcome remains explicitly unknown and is never presented as canonical delivery or completion evidence.
|
|
36
|
+
|
|
37
|
+
## memory-owned lifecycle
|
|
38
|
+
|
|
39
|
+
`createConfiguredTaskCore()` owns its transport and `createTaskStore()` RAM state. Startup registers; polling runs every five seconds and at agent settlement. Shutdown fences new work, aborts requests, drains active calls and discards state. Late callbacks cannot mutate a successor lifetime.
|
|
40
|
+
|
|
41
|
+
Background refresh records protocol facts in RAM even while Pi is busy or has queued messages. A valid completion within the fetched receive page can become canonical before its notification is presented. Model-visible insertion and the separate follow-up wake still wait for idle Pi with no queued messages; visible relay mail remains replayable until the required structured insertion/wake evidence is present. Intake alone does not incorporate an assignment or authorize worker tools. Parent acknowledgment before terminal presentation retains the event but suppresses its redundant wake. Origin-authored conversations do not echo to the origin. Receiver-authored conversation copies remain canonical replicas, but local outbound intent identity makes their presentation archive-only; payload author claims are not provenance. Missing or unmatched intent identity leaves peer conversations actionable. This correlation is RAM-only and is never recovered from session history.
|
|
42
|
+
|
|
43
|
+
The normal 5s polling cadence and relay byte limits are unchanged. A single-flight background refresh shares one work scope through lease connection, ACK repair, timeout fan-out, receive and all inbox receipts: at most 56 scoped sends plus 8 rotating old-work sends, 48 ACK attempts and one initial receive page of up to 50 items, with a 2s overall deadline. Failed attempts consume the shared counts; the same send/ACK identity is not attempted twice in that scope. Separate 250ms retry/ACK-repair slices stop admitting another attempt, not an already admitted request: each admitted request gets the remaining background window. These are scheduling bounds, not throughput guarantees. Foreground send/message/done retain the configured transport request deadline (default 12s) and caller cancellation instead of inheriting the background cutoff. Any speculative old-work retry after a foreground send uses a finite 2s background window, starting only when that share is reached.
|
|
44
|
+
|
|
45
|
+
Each refresh deliberately fetches only one initial page, even when `hasMore` is true or the ACK frontier advances. Extra-page saturation draining (#27) is deferred, not fixed or optimized; byte limits can make this page shorter than 50 items, and shared send/ACK counts can defer work within it. The normal refresh reserves initial protocol intake ahead of requested-ACK repair; page intake also precedes its own repair pass. Speculative old-work sends wait until all current-page intake and available presentation/evidence/ACK work completes or deliberately yields. Inbox delivery holds that gate across receive and nested fanout/receipt flushes, releasing it in finalization even on empty, held/busy or failed passes when budget remains. Direct core receive suppresses speculation during page materialization and retains its bounded after-intake retry opportunity; an inbox-owned gate remains closed until presentation yields. After required canonical fan-out acceptance, both new and replayed raw intents stage authorized ACK requests in the existing checkpoint rather than retrying inline. The shared after-intake repair pass uses the same ACK budget, admission slice and overall background deadline; remaining raw ACK work reports local deferral on budget/slice/cooldown exhaustion. Failed or pending required fan-out still stops intake at that delivery. Retryable/slow repair still reports failure and retains uncertain requested IDs, but cannot prevent that initial real receive. A failed repair yields before presentation. Ignored-abort ACK responses cannot later advance local authority. Held visible mail, missing insertion/wake evidence, backpressure or exhaustion yields without reading ahead. A full held prefix can still prevent intake of a newer intent beyond that page; general backlog scanning is not provided.
|
|
46
|
+
|
|
47
|
+
Scoped operations attempt their own immutable IDs and cannot report acceptance for omitted or partial fan-out. Concurrent same-ID sends join existing delivery rather than retransmitting. Pending retry IDs rotate incrementally in RAM on attempts/actual skips, including failures and cooldown skips, without copying payloads or rotating an unattempted suffix past the retry deadline. A foreground packet's capacity refusal does not suppress smaller old packets to the same target; same-cycle target suppression starts only when speculative retry itself encounters capacity. Known capacity defers that envelope/ACK for at least 5s (or the validated longer `retryAfterMs`), without a retry timer. `TASK_DELIVERY_DEFERRED` is a retryable local budget/cooldown outcome, not relay acceptance or permanent quarantine. Raw intent replays retain their canonical fan-out reservation and are not ACKed while required sends remain pending; quarantined required sends report their blocked outcome instead of false acceptance. The exported `TaskStore` contract requires indexed lookup, fair rotation and cooldown capabilities, supplied by `createTaskStore()`; there is no legacy snapshot/fixed-prefix fallback. `TaskCore.receive()` retains its array return and the documented transport seam is unchanged.
|
|
48
|
+
|
|
49
|
+
The default extension automatically replaces a lifetime only after a structured `RELAY_RESET` or `RELAY_REBIND_REQUIRED`. It fences and discards the unusable RAM state, records each active task in session history as locally failed with `RELAY_STATE_LOST` and an explicitly unknown remote outcome, then binds a fresh empty endpoint. The loss record does not wake the model by itself. Transient availability, authentication, capacity, abort, profile and malformed-response failures never trigger replacement. Historical tasks are not adopted into the successor lifetime.
|
|
50
|
+
|
|
51
|
+
`createVolatileTaskSession({ url, callerSession, store })` is the lower-level API for a caller-owned RAM store. Transport reconnect within that same lifetime retains pending immutable retries; `rebind()` clears the store, while `close()` stops transport and the caller then closes its state. Persistence/path options are rejected. Old database files are neither read nor deleted.
|
|
52
|
+
|
|
53
|
+
Full immutable content, including the original creation timestamp, is reused for retries. Destination-confirmed acceptance is required. Conflicting content, exhausted delivery and reset cannot become successful acceptance. Individual ACKs preserve sparse bigint cursors and cannot skip earlier pending mail. Bounds/deadlines apply through complete response bodies, including noncooperative injected transports.
|
|
54
|
+
|
|
55
|
+
## trusted Tailnet and optional authentication
|
|
56
|
+
|
|
57
|
+
The intended deployment trusts its Tailnet machines; Tailscale access control is the network boundary. Do not expose owner APIs through the public internet/Funnel or an untrusted proxy. No additional JWT or peer-signature scheme is required by the task protocol.
|
|
58
|
+
|
|
59
|
+
Existing global Wolfpack JWT enforcement remains **optional**. If the owner configures `WOLFPACK_JWT_SECRET`, the configured Pi factory supports it with short-lived tokens only for HTTP loopback. It never discovers/distributes secrets or automatically forwards them to arbitrary HTTPS URLs. HTTP401 reports `RELAY_AUTH_REQUIRED`, not an epoch reset.
|
|
60
|
+
|
|
61
|
+
Remote task endpoints must be locally resolved aliases. The paired CLI qualifies a selected remote endpoint through the local coordinator; remote lists expose explicit remote metadata. Failed qualification preserves any created session/ID but removes unsafe `taskEndpoint` output and reports `taskEndpointError`.
|
|
62
|
+
|
|
63
|
+
## delegation workflow
|
|
64
|
+
|
|
65
|
+
Configure `WOLFPACK_IMPLEMENTER_MODEL` and `WOLFPACK_REVIEWER_MODEL`; defaults are `openai-codex/gpt-5.6-terra` and `openai-codex/gpt-5.6-sol`. Explicit user or project choices override those defaults.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
IMPLEMENTER_MODEL="${WOLFPACK_IMPLEMENTER_MODEL:-openai-codex/gpt-5.6-terra}"
|
|
69
|
+
REVIEWER_MODEL="${WOLFPACK_REVIEWER_MODEL:-openai-codex/gpt-5.6-sol}"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
1. Use one assignment mode: full-startup instructions, or an endpoint assignment—not both. For a task worker: `wolfpack agent spawn --project-dir /absolute/worktree --name <task-role> --model "$IMPLEMENTER_MODEL" --task-worker --readiness-timeout-ms 30000 --json` (or `"$REVIEWER_MODEL"`). This mode rejects prompts/plans and `--notify-parent`; put all instructions in `agent_task_send.task`.
|
|
73
|
+
2. Verify exact session ID/root/Pi harness and registered `taskEndpoint`. `TASK_WORKER_PREFLIGHT_FAILED` precedes creation. `TASK_WORKER_NOT_READY` retains `createdSession`/`cleanup`; inspect `unconfirmed` cleanup by exact ID before retry. Registration is not model execution.
|
|
74
|
+
3. Submit the assignment and continue useful work. When none remains, yield to structured follow-up. Do not poll `agent_task_status` or `agent_task_inbox` merely to wait; use `agent_task_wait` only when explicitly asked to block.
|
|
75
|
+
4. Use `agent_task_message` for questions, answers and information. The receiver's last action is `agent_task_done`, not completion prose.
|
|
76
|
+
5. Independently verify, then acknowledge the terminal task and deliberately retain or clean up the parent-owned role.
|
|
77
|
+
|
|
78
|
+
Endpoint assignments require terminal completion and one `agent_task_ack`; full-startup children have no task ID, so use explicit completion/block notification and parent verification instead. Close only sessions the parent spawned using `wolfpack kill <stable-session-id> --json`, then verify that ID absent from `wolfpack list --json`. Never use `wolfpack session send`, `/exit`, or `/quit` for cleanup.
|
|
79
|
+
|
|
80
|
+
## worker-only execution gate
|
|
81
|
+
|
|
82
|
+
The exact value `PI_TASK_WORKER=1` enables a fail-closed model tool gate. Before assignment only inbox/status/wait are allowed; other tools receive `PI_TASK_WORKER_ASSIGNMENT_REQUIRED`. Ordinary interactive Pi and explicit user shell commands are outside this gate.
|
|
83
|
+
|
|
84
|
+
`PI_TASK_WORKER=1` sessions are leaf roles. Generic role-orchestration guidance applies only to non-worker coordinators. The spawning coordinator owns each child's complete lifecycle.
|
|
85
|
+
|
|
86
|
+
Workers cannot call `agent_task_send`, `agent_task_cancel`, or `agent_task_ack` (`PI_TASK_WORKER_COORDINATION_FORBIDDEN`). `agent_task_message` must name their eligible incorporated assignment. A structured session event alone is insufficient: its task must also be active in this endpoint's current RAM state. Old session history never authorizes work after restart.
|
|
87
|
+
|
|
88
|
+
Preflighting `agent_task_done` marks that task as closing before sibling calls are preflighted. Ordinary tools remain blocked for a closing, pending-terminal, accepted-terminal, or `delivery_blocked` task; an idempotent `agent_task_done` retry for that same assigned task remains allowed. Another independently active assignment can still authorize work.
|
|
89
|
+
|
|
90
|
+
## terminal delivery and acknowledgment
|
|
91
|
+
|
|
92
|
+
During the active lifetime `terminalDelivery` reports `not_submitted`, `pending`, `accepted`, or `delivery_blocked`. The blocked variant includes stable intent/envelope identities, origin endpoint, timestamp, and structured non-retryable relay error. Canonical task status is never changed to `delivery_blocked`. Retryable failures reuse the original intent; permanent failures are not rearmed. This evidence is lost on endpoint restart.
|
|
93
|
+
|
|
94
|
+
`agent_task_ack` is terminal-only and origin-owned. Sequential/concurrent retries in one lifetime reuse the logical event and envelope IDs. There is no restart recovery. Report changed source paths in `result.changedFiles`; artifacts are receiver-project-relative regular files, not the changed-file list.
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{ "result": { "changedFiles": ["src/extension.ts"] }, "artifacts": [{ "path": "verification/task-2.md" }] }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## development and verification
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
bun install
|
|
104
|
+
bun test
|
|
105
|
+
bun run typecheck
|
|
106
|
+
PI_TASKS_WOLFPACK_SOURCE=/absolute/clean/wolfpack-checkout \
|
|
107
|
+
PI_TASKS_WOLFPACK_REVISION=<full-commit-id> \
|
|
108
|
+
bun test tests/volatile-real-worker.test.ts tests/volatile-extension-worker.test.ts
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Wolfpack worker tests require its supported Bun runtime (currently stable>=1.4.2). Optional source-selected checks use private HTTP/workers, not installed services. They test same-lifetime retry/ACK, automatic default-extension reset recovery, explicit lower-level session replacement, fresh endpoint state and history-only evidence. `createInMemoryTaskRelay` is a deterministic conformance fixture, never the default runtime transport. Physical two-machine qualification, final-path performance, release packaging and live activation remain separate gates.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sgtbeatdown/pi-tasks",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Memory-only endpoint-owned Pi agent delegation through Wolfpack relay v2.",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"exports": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"files": [
|
|
20
20
|
"README.md",
|
|
21
|
+
"docs",
|
|
21
22
|
"src",
|
|
22
23
|
"skills",
|
|
23
24
|
"tsconfig.json"
|
package/src/extension.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { Text } from "@earendil-works/pi-tui";
|
|
|
4
4
|
import { Type } from "typebox";
|
|
5
5
|
|
|
6
6
|
import { abortableSleep } from "./abortable-sleep";
|
|
7
|
+
import { createTaskWorkScope } from "./task-work-scope";
|
|
7
8
|
import { deliverTaskInbox, incorporatedTaskEvents } from "./task-inbox";
|
|
8
9
|
import { TaskDeliveryEvidenceState, TaskDeliveryStage, TaskOutboxDeliveryError, TaskProtocolError, isBlockedOutboxDeliveryCode } from "./task-protocol";
|
|
9
10
|
import { createConfiguredTaskCore } from "./configured-task-core";
|
|
@@ -113,18 +114,21 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
113
114
|
const epoch = lifecycleEpoch;
|
|
114
115
|
const isCurrent = (): boolean => inboxContext === context && lifecycleEpoch === epoch;
|
|
115
116
|
if (!context) return undefined;
|
|
117
|
+
const work = createTaskWorkScope(signal);
|
|
118
|
+
work.retryAllowed = false; // Reserve initial intake ahead of speculative backlog work and ACK repair.
|
|
119
|
+
signal = work.signal;
|
|
116
120
|
const activeCore = await configuredCore(signal);
|
|
117
121
|
if (!isCurrent()) return undefined;
|
|
118
122
|
let outboxError: unknown;
|
|
119
123
|
try {
|
|
120
|
-
await activeCore.
|
|
124
|
+
await activeCore.connect(signal, work);
|
|
121
125
|
} catch (error) {
|
|
122
126
|
if (isRelayLoss(error)) return error;
|
|
123
127
|
outboxError = error;
|
|
124
128
|
}
|
|
125
129
|
if (!isCurrent()) return undefined;
|
|
126
130
|
try {
|
|
127
|
-
await activeCore.evaluateTimeouts(signal);
|
|
131
|
+
await activeCore.evaluateTimeouts(signal, work);
|
|
128
132
|
} catch (error) {
|
|
129
133
|
if (isRelayLoss(error)) return error;
|
|
130
134
|
outboxError ??= error;
|
|
@@ -132,19 +136,26 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
132
136
|
if (!isCurrent()) return undefined;
|
|
133
137
|
const guardedCore: TaskCore = {
|
|
134
138
|
...activeCore,
|
|
135
|
-
async receive(receiveSignal) {
|
|
136
|
-
|
|
139
|
+
async receive(receiveSignal, receiveWork) {
|
|
140
|
+
if (!isCurrent()) return [];
|
|
141
|
+
const deliveries = await activeCore.receive(receiveSignal, receiveWork);
|
|
137
142
|
return isCurrent() ? deliveries : [];
|
|
138
143
|
},
|
|
139
|
-
async acknowledgeRelayDelivery(cursor, acknowledgementSignal) {
|
|
140
|
-
if (isCurrent()) await activeCore.acknowledgeRelayDelivery(cursor, acknowledgementSignal);
|
|
144
|
+
async acknowledgeRelayDelivery(cursor, acknowledgementSignal, acknowledgementWork) {
|
|
145
|
+
if (isCurrent()) await activeCore.acknowledgeRelayDelivery(cursor, acknowledgementSignal, acknowledgementWork);
|
|
141
146
|
},
|
|
142
|
-
async recordInsertion(input, insertionSignal) {
|
|
143
|
-
if (isCurrent()) await activeCore.recordInsertion(input, insertionSignal);
|
|
147
|
+
async recordInsertion(input, insertionSignal, insertionWork) {
|
|
148
|
+
if (isCurrent()) await activeCore.recordInsertion(input, insertionSignal, insertionWork);
|
|
144
149
|
},
|
|
145
|
-
|
|
150
|
+
async submitIntent(input, intentSignal, intentWork) {
|
|
151
|
+
if (isCurrent()) await activeCore.submitIntent(input, intentSignal, intentWork);
|
|
152
|
+
},
|
|
153
|
+
async flushOutbox(flushSignal, flushWork) {
|
|
154
|
+
if (isCurrent()) await activeCore.flushOutbox(flushSignal, flushWork);
|
|
155
|
+
},
|
|
156
|
+
recordWakeRequest(input, wakeWork) {
|
|
146
157
|
if (!isCurrent()) throw new TaskProtocolError("RELAY_CLOSED", "task lifecycle was replaced", { retryable: false });
|
|
147
|
-
const request = activeCore.recordWakeRequest(input);
|
|
158
|
+
const request = activeCore.recordWakeRequest(input, wakeWork);
|
|
148
159
|
return { flush: async (flushSignal?: AbortSignal): Promise<void> => {
|
|
149
160
|
if (!isCurrent()) throw new TaskProtocolError("RELAY_CLOSED", "task lifecycle was replaced", { retryable: false });
|
|
150
161
|
await request.flush(flushSignal);
|
|
@@ -158,7 +169,7 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
158
169
|
isIdle: (): boolean => isCurrent() && context.isIdle(),
|
|
159
170
|
hasPendingMessages: (): boolean => !isCurrent() || context.hasPendingMessages(),
|
|
160
171
|
sessionManager: context.sessionManager,
|
|
161
|
-
}, signal);
|
|
172
|
+
}, signal, work);
|
|
162
173
|
return outboxError ?? inboxOutboxError;
|
|
163
174
|
});
|
|
164
175
|
const startBackgroundRefresh = (context: ExtensionContext, epoch: number): void => {
|
|
@@ -219,18 +230,11 @@ export function registerAgentTaskTools(pi: ExtensionAPI, core: TaskCore | undefi
|
|
|
219
230
|
let activeCore: (TaskCore & Partial<Pick<OwnedTaskCore, "close">>) | undefined;
|
|
220
231
|
try {
|
|
221
232
|
activeCore = await configuredCore();
|
|
222
|
-
let connectionError: unknown;
|
|
223
|
-
try {
|
|
224
|
-
await activeCore.connect();
|
|
225
|
-
} catch (error) {
|
|
226
|
-
connectionError = error;
|
|
227
|
-
}
|
|
228
233
|
if (!isCurrent()) return;
|
|
229
|
-
if (isRelayLoss(connectionError) && await recoverRelayLoss(context, epoch, activeCore)) return;
|
|
230
234
|
const outboxError = await refreshInbox();
|
|
231
235
|
if (!isCurrent()) return;
|
|
232
236
|
if (isRelayLoss(outboxError) && await recoverRelayLoss(context, epoch, activeCore)) return;
|
|
233
|
-
const lifecycleError = outboxError
|
|
237
|
+
const lifecycleError = outboxError;
|
|
234
238
|
const status = lifecycleError === undefined ? undefined : outboxFailureStatus(lifecycleError);
|
|
235
239
|
context.ui.setStatus("pi-tasks", status === undefined ? undefined : context.ui.theme.fg("warning", status));
|
|
236
240
|
if (lifecycleError === undefined) announceFreshBinding(context);
|