@timqi/pier 0.0.29 → 0.1.1
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 +58 -125
- package/dist/agent/config.js +24 -11
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +43 -62
- package/dist/agent/listing.js +113 -68
- package/dist/agent/pi.js +204 -211
- package/dist/boards/boards.js +19 -29
- package/dist/channels/attach.js +14 -42
- package/dist/channels/chains.js +33 -37
- package/dist/channels/chunk.js +8 -28
- package/dist/channels/commands.js +3 -14
- package/dist/channels/config.js +33 -52
- package/dist/channels/control.js +4 -13
- package/dist/channels/conversations.js +8 -25
- package/dist/channels/dedup.js +8 -17
- package/dist/channels/gatekeeper.js +13 -23
- package/dist/channels/lark-api.js +23 -63
- package/dist/channels/lark-outbound.js +12 -44
- package/dist/channels/lark-panel.js +8 -24
- package/dist/channels/lark-render.js +18 -62
- package/dist/channels/lark.js +52 -141
- package/dist/channels/lines.js +13 -15
- package/dist/channels/panel.js +16 -36
- package/dist/channels/receipts.js +29 -52
- package/dist/channels/routes.js +3 -9
- package/dist/channels/runtime.js +12 -23
- package/dist/channels/slack-api.js +34 -86
- package/dist/channels/slack-directory.js +7 -23
- package/dist/channels/slack-outbound.js +12 -56
- package/dist/channels/slack-panel.js +4 -13
- package/dist/channels/slack-render.js +23 -91
- package/dist/channels/slack-tool.js +48 -171
- package/dist/channels/slack.js +73 -239
- package/dist/channels/telegram-api.js +8 -20
- package/dist/channels/telegram-panel.js +5 -21
- package/dist/channels/telegram-render.js +13 -40
- package/dist/channels/telegram.js +54 -146
- package/dist/channels/types.js +5 -16
- package/dist/cli.js +17 -41
- package/dist/config-sync.js +87 -4
- package/dist/core/hub.js +7 -20
- package/dist/core/identity.js +20 -59
- package/dist/core/inbound-file.js +15 -49
- package/dist/core/inbox.js +13 -35
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -260
- package/dist/core/types.js +17 -1
- package/dist/db.js +98 -252
- package/dist/drain.js +58 -51
- package/dist/extensions/index.js +3 -11
- package/dist/extensions/web/anthropic.js +3 -9
- package/dist/extensions/web/artifacts.js +2 -5
- package/dist/extensions/web/content.js +6 -14
- package/dist/extensions/web/http.js +2 -6
- package/dist/extensions/web/language.js +8 -18
- package/dist/extensions/web/openai.js +1 -1
- package/dist/extensions/web/provider.js +5 -18
- package/dist/extensions/web/tools.js +19 -63
- package/dist/lock.js +98 -0
- package/dist/log.js +9 -26
- package/dist/main.js +87 -179
- package/dist/paths.js +10 -26
- package/dist/secrets.js +19 -46
- package/dist/service.js +33 -75
- package/dist/settings.js +42 -65
- package/dist/tasks/agent.js +129 -114
- package/dist/tasks/callbacks.js +9 -19
- package/dist/tasks/command.js +29 -14
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +46 -42
- package/dist/tasks/groups.js +41 -35
- package/dist/tasks/messages.js +121 -182
- package/dist/tasks/outbox.js +61 -55
- package/dist/tasks/routes.js +5 -11
- package/dist/tasks/runs.js +14 -13
- package/dist/tasks/service.js +53 -54
- package/dist/tasks/store.js +53 -30
- package/dist/tasks/tool.js +132 -61
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +100 -327
- package/dist/update.js +21 -44
- package/dist/web/auth.js +118 -179
- package/dist/web/config-sync.js +2 -2
- package/dist/web/config.js +3 -7
- package/dist/web/explorer.js +10 -21
- package/dist/web/fs.js +20 -42
- package/dist/web/instance.js +45 -85
- package/dist/web/providers.js +14 -13
- package/dist/web/public/assets/{activity-D3m4L2IL.js → activity-B89_hH7q.js} +2 -2
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js +1 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +4 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +85 -0
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +2 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js +1 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js +5 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js +3 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +4 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
- package/dist/web/public/index.html +100 -130
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +2 -2
- package/dist/web/public/manifest.webmanifest.br +0 -0
- package/dist/web/public/manifest.webmanifest.gz +0 -0
- package/dist/web/public/sw.js +14 -2
- package/dist/web/public/sw.js.br +0 -0
- package/dist/web/public/sw.js.gz +0 -0
- package/dist/web/push.js +55 -77
- package/dist/web/route.js +3 -7
- package/dist/web/server.js +131 -180
- package/dist/web/session-state.js +14 -54
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +81 -160
- package/skills/pier-help/SKILL.md +23 -20
- package/skills/pier-slack/SKILL.md +2 -2
- package/skills/pier-tasks/SKILL.md +153 -160
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-D3m4L2IL.js.br +0 -0
- package/dist/web/public/assets/activity-D3m4L2IL.js.gz +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js +0 -1
- package/dist/web/public/assets/boards-BIObcQeX.js.br +0 -0
- package/dist/web/public/assets/boards-BIObcQeX.js.gz +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js +0 -4
- package/dist/web/public/assets/explorer-C_rSWPNB.js.br +0 -0
- package/dist/web/public/assets/explorer-C_rSWPNB.js.gz +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css +0 -2
- package/dist/web/public/assets/index-CX3fYZY5.css.br +0 -0
- package/dist/web/public/assets/index-CX3fYZY5.css.gz +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js +0 -85
- package/dist/web/public/assets/index-uFsZkKOQ.js.br +0 -0
- package/dist/web/public/assets/index-uFsZkKOQ.js.gz +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js +0 -1
- package/dist/web/public/assets/runs-Ch6DZq6O.js.br +0 -0
- package/dist/web/public/assets/runs-Ch6DZq6O.js.gz +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js +0 -5
- package/dist/web/public/assets/settings-BWcEIEcv.js.br +0 -0
- package/dist/web/public/assets/settings-BWcEIEcv.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js +0 -3
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.br +0 -0
- package/dist/web/public/assets/task-runs-DPkwv2UE.js.gz +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js +0 -4
- package/dist/web/public/assets/tasks-DTiCi2mH.js.br +0 -0
- package/dist/web/public/assets/tasks-DTiCi2mH.js.gz +0 -0
package/dist/tasks/messages.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
// What a parent and a child say to each other while a run is going
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// finished — an undelivered message is retried, expired and *said*, never
|
|
6
|
-
// dropped (§5b).
|
|
7
|
-
import { EventHub } from "../core/hub.js";
|
|
8
|
-
import { Router } from "../core/router.js";
|
|
9
|
-
import { logger } from "../log.js";
|
|
1
|
+
// What a parent and a child say to each other while a run is going. Every
|
|
2
|
+
// message is a durable row before it is a delivery: either end may be mid-turn
|
|
3
|
+
// or gone, and an undelivered message is retried, expired and said, never
|
|
4
|
+
// dropped (§5). Delivery itself belongs to outbox.ts.
|
|
10
5
|
import { runSource } from "./callbacks.js";
|
|
11
6
|
import { newId } from "./definitions.js";
|
|
12
|
-
import {
|
|
13
|
-
import { isTerminal
|
|
14
|
-
const log = logger("tasks");
|
|
7
|
+
import { Outbox } from "./outbox.js";
|
|
8
|
+
import { isTerminal } from "./types.js";
|
|
15
9
|
const MAX_MESSAGE_LENGTH = 16 * 1024;
|
|
10
|
+
/** `answered` is a delivery that was read; `expired` is one given up on. */
|
|
11
|
+
const ENGINE_STATE = {
|
|
12
|
+
pending: "pending", failed: "failed", delivered: "delivered", answered: "delivered", expired: "abandoned",
|
|
13
|
+
};
|
|
16
14
|
function bounded(content) {
|
|
17
15
|
const text = content.trim();
|
|
18
16
|
if (!text)
|
|
@@ -23,20 +21,56 @@ function bounded(content) {
|
|
|
23
21
|
}
|
|
24
22
|
export class TaskMessenger {
|
|
25
23
|
store;
|
|
26
|
-
router;
|
|
27
24
|
hub;
|
|
28
|
-
|
|
25
|
+
prepareResume;
|
|
29
26
|
unreachable;
|
|
27
|
+
startRun;
|
|
28
|
+
outbox;
|
|
30
29
|
constructor(store, router, hub,
|
|
31
|
-
/**
|
|
32
|
-
|
|
30
|
+
/** Prepares a continuation; it starts only after the reply commits. */
|
|
31
|
+
prepareResume,
|
|
33
32
|
/** Reports a delivery nobody can complete (service.ts owns the surfaces). */
|
|
34
|
-
unreachable) {
|
|
33
|
+
unreachable, startRun) {
|
|
35
34
|
this.store = store;
|
|
36
|
-
this.router = router;
|
|
37
35
|
this.hub = hub;
|
|
38
|
-
this.
|
|
36
|
+
this.prepareResume = prepareResume;
|
|
39
37
|
this.unreachable = unreachable;
|
|
38
|
+
this.startRun = startRun;
|
|
39
|
+
this.outbox = new Outbox(router, {
|
|
40
|
+
id: ({ message }) => message.id,
|
|
41
|
+
reload: (id) => {
|
|
42
|
+
const message = this.store.getMessage(id);
|
|
43
|
+
return message && this.carry(message);
|
|
44
|
+
},
|
|
45
|
+
save: ({ message, callbackState, callbackAttempts, callbackError, callbackNextAttemptAt }) => {
|
|
46
|
+
message.attempts = callbackAttempts;
|
|
47
|
+
message.error = callbackError;
|
|
48
|
+
message.nextAttemptAt = callbackNextAttemptAt;
|
|
49
|
+
if (callbackState === "delivered") {
|
|
50
|
+
message.state = "delivered";
|
|
51
|
+
message.deliveredAt = Date.now();
|
|
52
|
+
}
|
|
53
|
+
else if (callbackState === "abandoned") {
|
|
54
|
+
message.state = "expired";
|
|
55
|
+
message.answeredAt = Date.now();
|
|
56
|
+
}
|
|
57
|
+
else
|
|
58
|
+
message.state = callbackState ?? "pending";
|
|
59
|
+
this.store.saveMessage(message);
|
|
60
|
+
},
|
|
61
|
+
changed: ({ message }) => this.changed(message),
|
|
62
|
+
input: (records) => {
|
|
63
|
+
const message = records[0].message;
|
|
64
|
+
const run = this.store.getRun(message.runId);
|
|
65
|
+
if (!run)
|
|
66
|
+
throw new Error(`unknown task run: ${message.runId}`);
|
|
67
|
+
return { text: this.format(message, run), origin: this.origin(message, run) };
|
|
68
|
+
},
|
|
69
|
+
abandoned: ({ message }, _sessionId, why) => this.told(message, why),
|
|
70
|
+
// A follow-up joins the recipient's queue now: the run it guides may end
|
|
71
|
+
// with the turn it would otherwise wait out.
|
|
72
|
+
queues: true,
|
|
73
|
+
});
|
|
40
74
|
}
|
|
41
75
|
expirePending() {
|
|
42
76
|
for (const message of this.store.expirePendingMessages())
|
|
@@ -49,6 +83,7 @@ export class TaskMessenger {
|
|
|
49
83
|
/** A manual continuation supersedes an unanswered decision (design 04):
|
|
50
84
|
* one continuation per run, never two racing ones. */
|
|
51
85
|
expireDecisions(runId, reason) {
|
|
86
|
+
const expired = [];
|
|
52
87
|
for (const message of this.store.listMessages(runId)) {
|
|
53
88
|
if (message.kind !== "decision" || (message.state !== "pending" && message.state !== "delivered"))
|
|
54
89
|
continue;
|
|
@@ -56,8 +91,9 @@ export class TaskMessenger {
|
|
|
56
91
|
message.error = reason;
|
|
57
92
|
message.answeredAt = Date.now();
|
|
58
93
|
this.store.saveMessage(message);
|
|
59
|
-
|
|
94
|
+
expired.push(message);
|
|
60
95
|
}
|
|
96
|
+
return expired;
|
|
61
97
|
}
|
|
62
98
|
list(runId) {
|
|
63
99
|
return this.store.listMessages(runId);
|
|
@@ -67,8 +103,9 @@ export class TaskMessenger {
|
|
|
67
103
|
}
|
|
68
104
|
async control(run, fromSessionId, kind, content) {
|
|
69
105
|
const message = this.create(run, kind, fromSessionId, run.targetSessionId ?? "", content, null);
|
|
106
|
+
this.changed(message);
|
|
70
107
|
if (run.targetSessionId)
|
|
71
|
-
this.deliver(message, run
|
|
108
|
+
this.deliver(message, run.targetSessionId);
|
|
72
109
|
return this.require(message.id);
|
|
73
110
|
}
|
|
74
111
|
deliverPendingControls(run) {
|
|
@@ -77,12 +114,11 @@ export class TaskMessenger {
|
|
|
77
114
|
for (const message of this.store.listMessages(run.id)) {
|
|
78
115
|
if (message.state !== "pending" || (message.kind !== "steer" && message.kind !== "follow_up"))
|
|
79
116
|
continue;
|
|
80
|
-
this.deliver(message, run
|
|
117
|
+
this.deliver(message, run.targetSessionId);
|
|
81
118
|
}
|
|
82
119
|
}
|
|
83
|
-
/**
|
|
84
|
-
*
|
|
85
|
-
* deliver. Controls aimed at a finished run are dead and expire here. */
|
|
120
|
+
/** Delivery is fire-and-forget, so this sweep is what closes a failed one.
|
|
121
|
+
* Controls aimed at a finished run are dead and expire here. */
|
|
86
122
|
retryUndelivered(now = Date.now()) {
|
|
87
123
|
for (const { message, run } of this.store.listUndeliveredMessages()) {
|
|
88
124
|
if (!run)
|
|
@@ -95,26 +131,19 @@ export class TaskMessenger {
|
|
|
95
131
|
this.changed(message);
|
|
96
132
|
continue;
|
|
97
133
|
}
|
|
98
|
-
// A reply that resumed a terminal run
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// continuation is the proof instead. `startedAt` is written one statement
|
|
102
|
-
// before the prompt reaches the session (agent.ts), so `delivered` here
|
|
103
|
-
// means "handed to the run that reports for it" — a `systemInput` that
|
|
104
|
-
// then throws fails *that* run, and the failure reaches the replier
|
|
105
|
-
// through its callback.
|
|
134
|
+
// A reply that resumed a terminal run is the continuation's prompt, not a
|
|
135
|
+
// system input: the transcript read would re-inject it and start a turn no
|
|
136
|
+
// run owns. The continuation's `startedAt` is the proof instead.
|
|
106
137
|
if (message.kind === "reply" && isTerminal(run.state)) {
|
|
107
138
|
const resumed = message.resumeRunId ? this.store.getRun(message.resumeRunId) : undefined;
|
|
108
139
|
if (resumed?.startedAt)
|
|
109
|
-
this.
|
|
140
|
+
this.delivered(message);
|
|
110
141
|
else if (!resumed)
|
|
111
142
|
this.abandon(message, `continuation ${message.resumeRunId ?? "(none)"} is gone`);
|
|
112
143
|
else if (isTerminal(resumed.state)) {
|
|
113
|
-
// Never started
|
|
114
|
-
// ceiling
|
|
115
|
-
//
|
|
116
|
-
// continuation's own cancelled callback, and a duplicate beats a
|
|
117
|
-
// special case that could suppress the only report either gets.
|
|
144
|
+
// Never started and never will: said now, not four minutes later at
|
|
145
|
+
// the ceiling. A duplicate report beats a special case that could
|
|
146
|
+
// suppress the only one.
|
|
118
147
|
this.abandon(message, `continuation ${resumed.id} ${resumed.state} before it started`);
|
|
119
148
|
}
|
|
120
149
|
continue;
|
|
@@ -123,15 +152,12 @@ export class TaskMessenger {
|
|
|
123
152
|
continue;
|
|
124
153
|
const target = message.toSessionId || run.targetSessionId;
|
|
125
154
|
if (target)
|
|
126
|
-
this.deliver(message,
|
|
155
|
+
this.deliver(message, target);
|
|
127
156
|
}
|
|
128
157
|
}
|
|
129
|
-
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* A decision steers the supervisor: a follow-up only lands once the
|
|
133
|
-
* supervisor has no tool calls left, so a blocked child would wait out the
|
|
134
|
-
* whole turn. Progress stays a follow-up — nobody waits on it. */
|
|
158
|
+
/** Returns the receipt immediately; the reply arrives as a follow-up (active
|
|
159
|
+
* run) or resumes the session (terminal run). A decision steers the
|
|
160
|
+
* supervisor, or a blocked child would wait out its whole turn. */
|
|
135
161
|
async contact(run, fromSessionId, reason, content) {
|
|
136
162
|
if (!run.invokedBySessionId)
|
|
137
163
|
throw new Error("run has no supervisor session");
|
|
@@ -139,7 +165,8 @@ export class TaskMessenger {
|
|
|
139
165
|
throw new Error("run already has a pending supervisor decision");
|
|
140
166
|
}
|
|
141
167
|
const message = this.create(run, reason, fromSessionId, run.invokedBySessionId, content, null);
|
|
142
|
-
this.
|
|
168
|
+
this.changed(message);
|
|
169
|
+
this.deliver(message, run.invokedBySessionId);
|
|
143
170
|
return this.require(message.id);
|
|
144
171
|
}
|
|
145
172
|
async reply(questionId, fromSessionId, content) {
|
|
@@ -161,25 +188,28 @@ export class TaskMessenger {
|
|
|
161
188
|
const run = this.store.getRun(question.runId);
|
|
162
189
|
if (!run)
|
|
163
190
|
throw new Error(`unknown task run: ${question.runId}`);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
191
|
+
// A rejected continuation must leave the question answerable. Persist the
|
|
192
|
+
// answer and its queued run together, before either publishes or executes.
|
|
193
|
+
const { reply, continuation } = this.store.transact(() => {
|
|
194
|
+
const reply = this.create(run, "reply", fromSessionId, question.fromSessionId, text, question.id);
|
|
195
|
+
question.state = "answered";
|
|
196
|
+
question.answeredAt = Date.now();
|
|
197
|
+
this.store.saveMessage(question);
|
|
198
|
+
const continuation = isTerminal(run.state)
|
|
199
|
+
? this.prepareResume(run.id, this.format(reply, run), fromSessionId)
|
|
200
|
+
: undefined;
|
|
201
|
+
if (continuation) {
|
|
202
|
+
reply.resumeRunId = continuation.id;
|
|
203
|
+
this.store.saveMessage(reply);
|
|
204
|
+
}
|
|
205
|
+
return { reply, continuation };
|
|
206
|
+
});
|
|
168
207
|
this.changed(question);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
else if (isTerminal(run.state)) {
|
|
175
|
-
// Creating the continuation is not delivering the reply: the run is
|
|
176
|
-
// queued, and a restart or a cancel before it starts loses the prompt
|
|
177
|
-
// that carries the text. So the id is recorded and the sweep below
|
|
178
|
-
// settles it against the continuation's own `startedAt`.
|
|
179
|
-
reply.resumeRunId = this.resumeRun(run.id, this.format(reply, run), fromSessionId).id;
|
|
180
|
-
this.store.saveMessage(reply);
|
|
181
|
-
this.changed(reply);
|
|
182
|
-
}
|
|
208
|
+
this.changed(reply);
|
|
209
|
+
if (continuation)
|
|
210
|
+
this.startRun(continuation);
|
|
211
|
+
else if (run.targetSessionId)
|
|
212
|
+
this.deliver(reply, run.targetSessionId);
|
|
183
213
|
return this.require(reply.id);
|
|
184
214
|
}
|
|
185
215
|
create(run, kind, fromSessionId, toSessionId, content, replyTo) {
|
|
@@ -200,33 +230,30 @@ export class TaskMessenger {
|
|
|
200
230
|
nextAttemptAt: null,
|
|
201
231
|
};
|
|
202
232
|
this.store.saveMessage(message);
|
|
203
|
-
this.changed(message);
|
|
204
233
|
return message;
|
|
205
234
|
}
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
* So `delivered` is written by `confirmed`, against the one proof that
|
|
210
|
-
* survives an abort or a restart: the message visible in the recipient's own
|
|
211
|
-
* transcript. Until then it stays pending and the sweep tries again. */
|
|
212
|
-
deliver(candidate, run, targetSessionId) {
|
|
213
|
-
const message = this.require(candidate.id);
|
|
214
|
-
if (message.state !== "pending" && message.state !== "failed")
|
|
215
|
-
return;
|
|
216
|
-
if (message.toSessionId !== targetSessionId)
|
|
235
|
+
/** A control created before its run had a session is aimed once it has one. */
|
|
236
|
+
deliver(message, targetSessionId) {
|
|
237
|
+
if (message.toSessionId !== targetSessionId) {
|
|
217
238
|
message.toSessionId = targetSessionId;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
this.
|
|
221
|
-
this.changed(message);
|
|
222
|
-
void this.inject(message, run, targetSessionId, this.mode(message))
|
|
223
|
-
.catch((error) => log.error(`message ${message.id} delivery collapsed`, error));
|
|
239
|
+
this.store.saveMessage(message);
|
|
240
|
+
}
|
|
241
|
+
void this.outbox.deliver(targetSessionId, [this.carry(message)]);
|
|
224
242
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
243
|
+
carry(message) {
|
|
244
|
+
return {
|
|
245
|
+
message,
|
|
246
|
+
callbackState: ENGINE_STATE[message.state],
|
|
247
|
+
callbackAttempts: message.attempts,
|
|
248
|
+
callbackError: message.error,
|
|
249
|
+
callbackNextAttemptAt: message.nextAttemptAt,
|
|
250
|
+
// Steer whatever someone is blocked on: a follow-up lands only once the
|
|
251
|
+
// recipient runs out of tool calls. Progress is a follow-up because nobody waits.
|
|
252
|
+
...(message.kind === "follow_up" || message.kind === "progress" ? {} : { callbackMode: "steer" }),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Proven by something other than the recipient's transcript. */
|
|
256
|
+
delivered(message) {
|
|
230
257
|
message.state = "delivered";
|
|
231
258
|
message.deliveredAt = Date.now();
|
|
232
259
|
message.error = null;
|
|
@@ -234,37 +261,6 @@ export class TaskMessenger {
|
|
|
234
261
|
this.store.saveMessage(message);
|
|
235
262
|
this.changed(message);
|
|
236
263
|
}
|
|
237
|
-
/** Waiting is not a failed attempt: the message is in flight or the turn it
|
|
238
|
-
* has to wait for is still running, so it is tried again shortly and the
|
|
239
|
-
* ceiling is left for the deliveries that actually failed (the rule
|
|
240
|
-
* outbox.ts keeps for callbacks). */
|
|
241
|
-
defer(id) {
|
|
242
|
-
const message = this.store.getMessage(id);
|
|
243
|
-
if (!message || message.state !== "pending")
|
|
244
|
-
return;
|
|
245
|
-
message.nextAttemptAt = Date.now() + 1000;
|
|
246
|
-
this.store.saveMessage(message);
|
|
247
|
-
}
|
|
248
|
-
/** Counts one hand-off and says whether it may happen: a recipient that
|
|
249
|
-
* never records the message must not be re-sent once a second forever.
|
|
250
|
-
* False means the ceiling was reached and the message is now expired. */
|
|
251
|
-
spend(id) {
|
|
252
|
-
const message = this.store.getMessage(id);
|
|
253
|
-
if (!message || message.state !== "pending")
|
|
254
|
-
return false;
|
|
255
|
-
message.attempts += 1;
|
|
256
|
-
message.nextAttemptAt = Date.now() + retryDelay(message.attempts);
|
|
257
|
-
if (message.attempts > MAX_DELIVERY_ATTEMPTS) {
|
|
258
|
-
this.abandon(message, undeliverable(message.attempts - 1, message.error));
|
|
259
|
-
return false;
|
|
260
|
-
}
|
|
261
|
-
this.store.saveMessage(message);
|
|
262
|
-
return true;
|
|
263
|
-
}
|
|
264
|
-
/** Out of attempts. Both ends are told — the recipient that was owed it and
|
|
265
|
-
* the sender waiting on the answer — and a decision that expires here stops
|
|
266
|
-
* suppressing its run's completion callback, which `execution.ts` decided
|
|
267
|
-
* once, at the end of the run, and never revisits. */
|
|
268
264
|
abandon(message, why) {
|
|
269
265
|
message.state = "expired";
|
|
270
266
|
message.error = why;
|
|
@@ -272,6 +268,11 @@ export class TaskMessenger {
|
|
|
272
268
|
message.nextAttemptAt = null;
|
|
273
269
|
this.store.saveMessage(message);
|
|
274
270
|
this.changed(message);
|
|
271
|
+
this.told(message, why);
|
|
272
|
+
}
|
|
273
|
+
/** Both ends are told, and an expired decision stops suppressing its run's
|
|
274
|
+
* completion callback, which `execution.ts` decided once and never revisits. */
|
|
275
|
+
told(message, why) {
|
|
275
276
|
this.unreachable(message.toSessionId, `a ${message.kind} from run ${message.runId}`, why);
|
|
276
277
|
if (message.fromSessionId && message.fromSessionId !== message.toSessionId) {
|
|
277
278
|
this.unreachable(message.fromSessionId, `your ${message.kind} on run ${message.runId}`, why);
|
|
@@ -284,69 +285,6 @@ export class TaskMessenger {
|
|
|
284
285
|
this.store.saveRun(run);
|
|
285
286
|
}
|
|
286
287
|
}
|
|
287
|
-
/** Steer whatever someone is blocked on: a decision, and the reply that
|
|
288
|
-
* answers it — a follow-up lands only once the recipient runs out of tool
|
|
289
|
-
* calls, so each would wait out a whole turn (the reply waited 3 minutes
|
|
290
|
-
* behind one on 2026-09-07). Progress is a follow-up because nobody waits. */
|
|
291
|
-
mode(message) {
|
|
292
|
-
return message.kind === "follow_up" || message.kind === "progress" ? "follow_up" : "steer";
|
|
293
|
-
}
|
|
294
|
-
failed(id, error, spent) {
|
|
295
|
-
const message = this.store.getMessage(id);
|
|
296
|
-
// Only a still-pending message can fail: a late rejection must not undo a
|
|
297
|
-
// delivery a newer attempt already proved, nor revive an expired one.
|
|
298
|
-
if (message?.state !== "pending")
|
|
299
|
-
return;
|
|
300
|
-
// Both ends are waiting on this one: the sender for an answer, the
|
|
301
|
-
// recipient for a message it never got told about.
|
|
302
|
-
log.warn(`${message.kind} ${id} to session ${message.toSessionId} failed`, error);
|
|
303
|
-
message.state = "failed";
|
|
304
|
-
message.error = String(error);
|
|
305
|
-
// A pass that died before the send never spent its attempt — an
|
|
306
|
-
// unresolvable target dies there every time, and would retry forever.
|
|
307
|
-
if (!spent)
|
|
308
|
-
message.attempts += 1;
|
|
309
|
-
message.nextAttemptAt = Date.now() + retryDelay(message.attempts);
|
|
310
|
-
this.store.saveMessage(message);
|
|
311
|
-
this.changed(message);
|
|
312
|
-
if (message.attempts > MAX_DELIVERY_ATTEMPTS) {
|
|
313
|
-
this.abandon(message, undeliverable(message.attempts - 1, message.error));
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
/** One pass at getting the message into the recipient: the dedupe on a retry
|
|
317
|
-
* and the proof of delivery are the same transcript read. Owns its own
|
|
318
|
-
* failure, so an attempt is counted exactly once whether the pass died
|
|
319
|
-
* before the send or the send itself was refused. */
|
|
320
|
-
async inject(message, run, targetSessionId, mode) {
|
|
321
|
-
let spent = false;
|
|
322
|
-
try {
|
|
323
|
-
const session = await this.router.ensure({ channelId: "task", conversationId: targetSessionId });
|
|
324
|
-
const recorded = async () => (await session.history()).some((turn) => turn.role === "system" && turn.origin?.kind === "task-message" && turn.origin.messageId === message.id);
|
|
325
|
-
if (await recorded())
|
|
326
|
-
return this.confirmed(message.id);
|
|
327
|
-
// Accepted and waiting in the recipient's queue for the running turn to
|
|
328
|
-
// drain it: not recorded yet, and sending again would deliver the same
|
|
329
|
-
// guidance twice. Unlike a callback this is not deferred on a busy
|
|
330
|
-
// target — a steer's whole point is to reach the turn already running —
|
|
331
|
-
// so the queue is where it sits, and waiting for it costs no attempt.
|
|
332
|
-
if (await this.queued(session, message.id))
|
|
333
|
-
return this.defer(message.id);
|
|
334
|
-
spent = this.spend(message.id);
|
|
335
|
-
if (!spent)
|
|
336
|
-
return;
|
|
337
|
-
await session.systemInput(this.format(message, run), this.origin(message, run), mode === "follow_up" ? "followUp" : mode);
|
|
338
|
-
if (await recorded())
|
|
339
|
-
this.confirmed(message.id);
|
|
340
|
-
}
|
|
341
|
-
catch (error) {
|
|
342
|
-
this.failed(message.id, error, spent);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
/** In the recipient's queue, by the id its origin carries — the transcript
|
|
346
|
-
* read above answers "landed", this answers "handed over and waiting". */
|
|
347
|
-
async queued(session, id) {
|
|
348
|
-
return (await session.pendingSystemInputs()).some((origin) => origin.kind === "task-message" && origin.messageId === id);
|
|
349
|
-
}
|
|
350
288
|
format(message, run) {
|
|
351
289
|
const title = run.context.definition.name;
|
|
352
290
|
if (message.kind === "progress") {
|
|
@@ -377,6 +315,7 @@ export class TaskMessenger {
|
|
|
377
315
|
throw new Error(`unknown task message: ${id}`);
|
|
378
316
|
return message;
|
|
379
317
|
}
|
|
318
|
+
/** Publish only after any transaction changing this message has committed. */
|
|
380
319
|
changed(message) {
|
|
381
320
|
this.hub.emitWorkspace({ type: "task-message-changed", runId: message.runId, messageId: message.id });
|
|
382
321
|
}
|
package/dist/tasks/outbox.js
CHANGED
|
@@ -1,40 +1,35 @@
|
|
|
1
|
-
// One delivery engine for everything that
|
|
2
|
-
//
|
|
3
|
-
// this logic was written twice and the two copies drifted — the ceiling was
|
|
4
|
-
// checked in one order here and another there, and only one of them counted a
|
|
5
|
-
// failed attempt.
|
|
6
|
-
import { Router } from "../core/router.js";
|
|
1
|
+
// One delivery engine for everything that reaches a session as a system input:
|
|
2
|
+
// run callbacks, group callbacks and run messages.
|
|
7
3
|
import { logger } from "../log.js";
|
|
8
4
|
import { MAX_DELIVERY_ATTEMPTS, retryDelay, undeliverable } from "./types.js";
|
|
9
5
|
const log = logger("tasks");
|
|
6
|
+
/** The records a delivery names, from either side of the seam: a batch names
|
|
7
|
+
* every one of them, a group names itself, a message names its id. */
|
|
8
|
+
const recordIds = (origin) => {
|
|
9
|
+
if (origin?.kind === "task-callback")
|
|
10
|
+
return origin.runIds ?? [origin.runId];
|
|
11
|
+
if (origin?.kind === "task-message")
|
|
12
|
+
return [origin.messageId];
|
|
13
|
+
return [];
|
|
14
|
+
};
|
|
10
15
|
export class Outbox {
|
|
11
16
|
router;
|
|
12
17
|
kind;
|
|
13
|
-
unreachable;
|
|
14
18
|
delivering = new Set();
|
|
15
|
-
constructor(router, kind
|
|
16
|
-
/** Reports a delivery nobody can complete (service.ts owns the surfaces). */
|
|
17
|
-
unreachable) {
|
|
19
|
+
constructor(router, kind) {
|
|
18
20
|
this.router = router;
|
|
19
21
|
this.kind = kind;
|
|
20
|
-
this.unreachable = unreachable;
|
|
21
22
|
}
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* `delivered` is written only against proof — the input visible in the
|
|
27
|
-
* recipient's own transcript. Pi's queues are memory, so an abort or a
|
|
28
|
-
* restart drops an accepted input and `systemInput` resolving proves
|
|
29
|
-
* nothing; a delivery reported on a resolved send is how a delegating agent
|
|
30
|
-
* ends up waiting forever on a result that was never read.
|
|
31
|
-
*/
|
|
23
|
+
/** One model turn drains the batch instead of one per record. `delivered` is
|
|
24
|
+
* written only against the input visible in the recipient's transcript: Pi's
|
|
25
|
+
* queues are memory, so a resolved `systemInput` proves nothing. */
|
|
32
26
|
async deliver(sessionId, batch) {
|
|
33
27
|
const mine = batch.filter((record) => !this.delivering.has(this.kind.id(record)));
|
|
34
28
|
if (mine.length === 0)
|
|
35
29
|
return;
|
|
36
30
|
for (const record of mine)
|
|
37
31
|
this.delivering.add(this.kind.id(record));
|
|
32
|
+
const counted = new Set();
|
|
38
33
|
try {
|
|
39
34
|
const session = await this.router.ensure({ channelId: "task", conversationId: sessionId });
|
|
40
35
|
// The transcript read is both the crash-window dedupe and the proof.
|
|
@@ -44,42 +39,56 @@ export class Outbox {
|
|
|
44
39
|
const live = unproven.filter((record) => !this.spent(record, sessionId));
|
|
45
40
|
if (live.length === 0)
|
|
46
41
|
return;
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
//
|
|
42
|
+
let sending = live;
|
|
43
|
+
// Waiting for a busy target is not an attempt, or the ceiling arrives in
|
|
44
|
+
// seconds. A `steer` record joins the running turn instead, but only once:
|
|
45
|
+
// handed over, it sits in Pi's in-memory queue, invisible in the transcript.
|
|
50
46
|
if (session.state === "streaming") {
|
|
47
|
+
const handedOver = await this.queued(session);
|
|
48
|
+
const sendNow = (record) => (record.callbackMode === "steer" || this.kind.queues === true) && !handedOver.has(this.kind.id(record));
|
|
49
|
+
sending = live.filter(sendNow);
|
|
51
50
|
for (const record of live)
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
if (!sendNow(record))
|
|
52
|
+
this.defer(record);
|
|
53
|
+
if (sending.length === 0)
|
|
54
|
+
return;
|
|
54
55
|
}
|
|
55
|
-
for (const record of
|
|
56
|
+
for (const record of sending) {
|
|
56
57
|
this.sent(record);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
await this.settle(
|
|
58
|
+
counted.add(this.kind.id(record));
|
|
59
|
+
}
|
|
60
|
+
const { text, origin } = this.kind.input(sending);
|
|
61
|
+
const mode = sending.every((record) => record.callbackMode === "steer") ? "steer" : "followUp";
|
|
62
|
+
log.debug(`callback for ${sending.map((r) => this.kind.id(r)).join(", ")} → session ${sessionId}`);
|
|
63
|
+
// Not awaited: `systemInput` settles with the recipient's whole turn.
|
|
64
|
+
session.systemInput(text, origin, mode)
|
|
65
|
+
.catch((error) => this.retry(sessionId, sending, error, counted));
|
|
66
|
+
// Pi records the input as it starts the turn; the tick sweep is the backstop.
|
|
67
|
+
await this.settle(sending, session);
|
|
67
68
|
}
|
|
68
69
|
catch (error) {
|
|
69
|
-
this.retry(sessionId, mine, error);
|
|
70
|
+
this.retry(sessionId, mine, error, counted);
|
|
70
71
|
}
|
|
71
72
|
finally {
|
|
72
73
|
for (const record of mine)
|
|
73
74
|
this.delivering.delete(this.kind.id(record));
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
/** What the transcript cannot answer yet; empty unless the session is streaming. */
|
|
78
|
+
async queued(session) {
|
|
79
|
+
const ids = new Set();
|
|
80
|
+
for (const origin of await session.pendingSystemInputs())
|
|
81
|
+
for (const id of recordIds(origin))
|
|
82
|
+
ids.add(id);
|
|
83
|
+
return ids;
|
|
84
|
+
}
|
|
76
85
|
/** Marks every record the transcript proves; returns the ones it does not. */
|
|
77
86
|
async settle(records, session) {
|
|
78
87
|
const seen = new Set();
|
|
79
88
|
for (const turn of await session.history()) {
|
|
80
|
-
if (turn.role !== "system"
|
|
89
|
+
if (turn.role !== "system")
|
|
81
90
|
continue;
|
|
82
|
-
for (const id of turn.origin
|
|
91
|
+
for (const id of recordIds(turn.origin))
|
|
83
92
|
seen.add(id);
|
|
84
93
|
}
|
|
85
94
|
const unproven = [];
|
|
@@ -92,22 +101,20 @@ export class Outbox {
|
|
|
92
101
|
}
|
|
93
102
|
return unproven;
|
|
94
103
|
}
|
|
95
|
-
/** The
|
|
96
|
-
|
|
97
|
-
* ceiling is what ends the retrying out loud. */
|
|
98
|
-
retry(sessionId, batch, error) {
|
|
104
|
+
/** The retry itself is silent, so this line is the only sign of it. */
|
|
105
|
+
retry(sessionId, batch, error, counted) {
|
|
99
106
|
log.warn(`callback to session ${sessionId} failed, will retry`, error);
|
|
100
107
|
for (const stale of batch) {
|
|
101
108
|
const record = this.kind.reload(this.kind.id(stale));
|
|
102
109
|
// Already proven delivered, or already given up on: not a failure.
|
|
103
110
|
if (!record || (record.callbackState !== "pending" && record.callbackState !== "failed"))
|
|
104
111
|
continue;
|
|
105
|
-
this.failed(record, error);
|
|
112
|
+
this.failed(record, error, counted.has(this.kind.id(record)));
|
|
113
|
+
counted.add(this.kind.id(record));
|
|
106
114
|
this.spent(record, sessionId);
|
|
107
115
|
}
|
|
108
116
|
}
|
|
109
|
-
/**
|
|
110
|
-
* result it will never get must not be left waiting on silence. */
|
|
117
|
+
/** An agent waiting on a result it will never get must not wait on silence. */
|
|
111
118
|
spent(record, sessionId) {
|
|
112
119
|
if (record.callbackAttempts < MAX_DELIVERY_ATTEMPTS)
|
|
113
120
|
return false;
|
|
@@ -116,7 +123,7 @@ export class Outbox {
|
|
|
116
123
|
record.callbackError = undeliverable(record.callbackAttempts, record.callbackError);
|
|
117
124
|
record.callbackNextAttemptAt = null;
|
|
118
125
|
this.write(record);
|
|
119
|
-
this.
|
|
126
|
+
this.kind.abandoned(record, sessionId, record.callbackError);
|
|
120
127
|
}
|
|
121
128
|
return true;
|
|
122
129
|
}
|
|
@@ -125,8 +132,8 @@ export class Outbox {
|
|
|
125
132
|
record.callbackNextAttemptAt = Date.now() + 1000;
|
|
126
133
|
this.kind.save(record);
|
|
127
134
|
}
|
|
128
|
-
/**
|
|
129
|
-
*
|
|
135
|
+
/** Backs off like a failure, so an input never recorded is re-sent on a
|
|
136
|
+
* curve, not once a second. */
|
|
130
137
|
sent(record) {
|
|
131
138
|
record.callbackAttempts += 1;
|
|
132
139
|
record.callbackState = "pending";
|
|
@@ -140,11 +147,10 @@ export class Outbox {
|
|
|
140
147
|
record.callbackNextAttemptAt = null;
|
|
141
148
|
this.write(record);
|
|
142
149
|
}
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
record.callbackAttempts += 1;
|
|
150
|
+
/** A failed pass costs one attempt, whether it died before or after send. */
|
|
151
|
+
failed(record, error, counted) {
|
|
152
|
+
if (!counted)
|
|
153
|
+
record.callbackAttempts += 1;
|
|
148
154
|
record.callbackState = "failed";
|
|
149
155
|
record.callbackError = String(error);
|
|
150
156
|
record.callbackNextAttemptAt = Date.now() + retryDelay(record.callbackAttempts);
|