@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
|
@@ -9,47 +9,25 @@ import { isThinkingLevel } from "../core/types.js";
|
|
|
9
9
|
import { EventHub } from "../core/hub.js";
|
|
10
10
|
import { Router } from "../core/router.js";
|
|
11
11
|
import { TaskStore } from "./store.js";
|
|
12
|
-
|
|
12
|
+
// An agent run that implements, tests and commits one concern takes 15–60
|
|
13
|
+
// minutes; the default is the runaway ceiling, not the pace of normal work.
|
|
14
|
+
const DEFAULT_TIMEOUT = 3600;
|
|
13
15
|
const MIN_WATCH_SECONDS = 5;
|
|
14
|
-
/** Crockford's base32, lowercased: i, l, o and u are gone
|
|
15
|
-
*
|
|
16
|
-
* case throughout, so a model re-types an id it read verbatim. */
|
|
16
|
+
/** Crockford's base32, lowercased: i, l, o and u are gone, and one case
|
|
17
|
+
* throughout, so a model re-types an id it read verbatim. */
|
|
17
18
|
const ID_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
18
|
-
/**
|
|
19
|
-
* values: a repeated or omitted character in the alphabet above biases every
|
|
20
|
-
* id ever minted, and no sample of finished ids can show it. */
|
|
19
|
+
/** Exported only so a test can walk all 256 byte values for alphabet bias. */
|
|
21
20
|
export const idSymbol = (byte) => ID_ALPHABET.charAt(byte & 31);
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* every comparison in the area is string equality and every listing orders by
|
|
27
|
-
* a timestamp column — so rows minted as UUIDs before this keep working
|
|
28
|
-
* untouched; there is nothing to migrate. It lives here rather than in
|
|
29
|
-
* types.ts because the browser type-checks that file and it stays node-free.
|
|
30
|
-
*
|
|
31
|
-
* Sixteen characters, 80 bits (`& 31` is unbiased because 256 is a multiple
|
|
32
|
-
* of 32). Sixty would have read the same and cost a token less, but a watch
|
|
33
|
-
* task on a 5-second interval mints ~6M runs a year, and a collision here is
|
|
34
|
-
* not an error: `saveRun`'s ON CONFLICT DO UPDATE would quietly overwrite the
|
|
35
|
-
* older run with the newer one. Four more characters buy ~16 million times
|
|
36
|
-
* the headroom for four bytes — cheaper than the alternative fix, which is a
|
|
37
|
-
* strict INSERT and a retry loop on every one of the four mint sites. */
|
|
21
|
+
/** Every task record's id. These ride through model context constantly, and a
|
|
22
|
+
* UUID spends ~12 tokens where this spends ~6. Sixteen characters (80 bits;
|
|
23
|
+
* `& 31` is unbiased): a watch task mints ~6M runs a year, and a collision is
|
|
24
|
+
* not an error — `saveRun`'s ON CONFLICT DO UPDATE would quietly overwrite. */
|
|
38
25
|
export const newId = () => Array.from(randomBytes(16), idSymbol).join("");
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* Anything else is an instance-layer owner — today the tools update task
|
|
45
|
-
* (src/tools-task.ts), whose script a switch in Settings runs on demand and
|
|
46
|
-
* cron runs nightly. Both
|
|
47
|
-
* public surfaces could rename it, point it at another script, pause it or
|
|
48
|
-
* archive it, and the switch would go on claiming Pier keeps the tools current
|
|
49
|
-
* while the run did something else entirely. The owner names itself in `by`;
|
|
50
|
-
* neither the routes nor the tool has a `by` to pass, so this one function
|
|
51
|
-
* closes both.
|
|
52
|
-
*/
|
|
26
|
+
/** A definition Pier's own code created is reconciled by that code and edited
|
|
27
|
+
* by nobody: `creator` is `"http"` (Console) or `session:<id>` (task tool);
|
|
28
|
+
* anything else is an instance-layer owner, which names itself in `by`.
|
|
29
|
+
* Otherwise a public surface could repoint the tools task while its switch
|
|
30
|
+
* went on claiming Pier keeps the tools current. */
|
|
53
31
|
const ownerOf = (task) => task.creator === "http" || task.creator.startsWith("session:") ? null : task.creator;
|
|
54
32
|
export const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
55
33
|
? value
|
|
@@ -143,9 +121,8 @@ export class TaskDefinitions {
|
|
|
143
121
|
return task;
|
|
144
122
|
}
|
|
145
123
|
async create(raw, creator = "http", kind = "task") {
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// draft that forgot its trigger must not silently unschedule it.
|
|
124
|
+
// A trigger-less new definition means manual. Update keeps requiring it:
|
|
125
|
+
// a draft that forgot its trigger must not silently unschedule a cron task.
|
|
149
126
|
const value = record(raw);
|
|
150
127
|
const draft = await this.parseDraft(value && value.trigger === undefined ? { ...value, trigger: { type: "manual" } } : raw, creator);
|
|
151
128
|
const now = Date.now();
|
|
@@ -176,19 +153,24 @@ export class TaskDefinitions {
|
|
|
176
153
|
async update(id, raw, by) {
|
|
177
154
|
const old = this.get(id);
|
|
178
155
|
this.assertOwner(old, by, "edited");
|
|
179
|
-
if (old.archived)
|
|
180
|
-
throw new Error("archived tasks cannot be edited");
|
|
181
156
|
const draft = await this.parseDraft(raw, ownerOf(old) ? by : undefined);
|
|
157
|
+
// Validation awaits, so the record read above is a guess by now: archive()
|
|
158
|
+
// or another edit may have landed, and the spread below would restore it.
|
|
159
|
+
const current = this.get(id);
|
|
160
|
+
if (current.archived)
|
|
161
|
+
throw new Error("archived tasks cannot be edited");
|
|
162
|
+
if (current.revision !== old.revision)
|
|
163
|
+
throw new Error("task was edited while this draft was validated");
|
|
182
164
|
const now = Date.now();
|
|
183
165
|
const task = {
|
|
184
|
-
...
|
|
166
|
+
...current,
|
|
185
167
|
name: draft.name,
|
|
186
168
|
description: draft.description ?? "",
|
|
187
|
-
enabled: draft.enabled ??
|
|
188
|
-
revision:
|
|
169
|
+
enabled: draft.enabled ?? current.enabled,
|
|
170
|
+
revision: current.revision + 1,
|
|
189
171
|
trigger: draft.trigger,
|
|
190
172
|
action: draft.action,
|
|
191
|
-
callback: draft.callback ??
|
|
173
|
+
callback: draft.callback ?? current.callback,
|
|
192
174
|
timeoutSeconds: draft.timeoutSeconds ?? DEFAULT_TIMEOUT,
|
|
193
175
|
nextRunAt: null,
|
|
194
176
|
updatedAt: now,
|
|
@@ -228,20 +210,16 @@ export class TaskDefinitions {
|
|
|
228
210
|
this.store.saveTask(task);
|
|
229
211
|
}
|
|
230
212
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
if (due.length)
|
|
243
|
-
this.changed();
|
|
244
|
-
return due;
|
|
213
|
+
/** The store narrows by index; the JSON is still the record, so its fields decide. */
|
|
214
|
+
due(now) {
|
|
215
|
+
return this.store.listDueTasks(now)
|
|
216
|
+
.filter((task) => task.enabled && !task.archived && task.nextRunAt !== null && task.nextRunAt <= now);
|
|
217
|
+
}
|
|
218
|
+
/** Committed with the run this occurrence queues: a failed enqueue rolls the
|
|
219
|
+
* advance back, so the occurrence stays due instead of being skipped. */
|
|
220
|
+
advance(task, now) {
|
|
221
|
+
task.nextRunAt = nextRunAt(task.trigger, now);
|
|
222
|
+
this.store.saveTask(task);
|
|
245
223
|
}
|
|
246
224
|
async sessionExists(sessionId) {
|
|
247
225
|
return this.router.stateOf(sessionId) !== undefined ||
|
|
@@ -332,8 +310,7 @@ export class TaskDefinitions {
|
|
|
332
310
|
session = { mode: "fresh", cwd };
|
|
333
311
|
}
|
|
334
312
|
else {
|
|
335
|
-
// Validation never mutates: a
|
|
336
|
-
// (POST /api/sessions) and then referenced with mode:"reuse".
|
|
313
|
+
// Validation never mutates: a session is created explicitly and then reused.
|
|
337
314
|
throw new Error('agent session policy required, e.g. {"mode":"fresh","cwd":"/abs/path"} or {"mode":"reuse","sessionId":"..."}');
|
|
338
315
|
}
|
|
339
316
|
if (session.mode === "reuse" && launch)
|
package/dist/tasks/execution.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
// One queued run carried to a result: dispatched by action kind
|
|
2
|
-
//
|
|
3
|
-
// while it goes, settled exactly once. Command and agent work lives in
|
|
4
|
-
// command.ts and agent.ts; this file owns the lifecycle they share.
|
|
1
|
+
// One queued run carried to a result: dispatched by action kind, abortable,
|
|
2
|
+
// settled exactly once. The lifecycle command.ts and agent.ts share.
|
|
5
3
|
import { logger } from "../log.js";
|
|
6
|
-
import { AgentTaskRunner } from "./agent.js";
|
|
7
|
-
import { TaskCallbacks } from "./callbacks.js";
|
|
8
4
|
import { runBash } from "./command.js";
|
|
9
|
-
import { TaskDefinitions } from "./definitions.js";
|
|
10
|
-
import { TaskStore } from "./store.js";
|
|
11
5
|
const log = logger("tasks");
|
|
12
6
|
export class TaskExecution {
|
|
13
7
|
store;
|
|
@@ -39,15 +33,27 @@ export class TaskExecution {
|
|
|
39
33
|
this.controllers.set(run.id, controller);
|
|
40
34
|
let timedOut = false;
|
|
41
35
|
let cause;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
let timeout;
|
|
37
|
+
// The budget is the run's own: waiting for an agent slot or a busy session
|
|
38
|
+
// (agent.ts) is bounded by cancellation and restart, never by the timeout.
|
|
39
|
+
const start = () => {
|
|
40
|
+
timeout ??= setTimeout(() => {
|
|
41
|
+
if (controller.signal.aborted)
|
|
42
|
+
return;
|
|
43
|
+
timedOut = true;
|
|
44
|
+
controller.abort();
|
|
45
|
+
}, run.context.definition.timeoutSeconds * 1000).unref();
|
|
46
|
+
if (run.state === "running")
|
|
47
|
+
return;
|
|
48
|
+
run.state = "running";
|
|
49
|
+
run.startedAt = Date.now();
|
|
50
|
+
this.store.saveRun(run);
|
|
51
|
+
this.host.changed(run);
|
|
52
|
+
};
|
|
47
53
|
try {
|
|
48
54
|
const { definition } = run.context;
|
|
49
55
|
if (definition.trigger.type === "watch" && !run.resumedFromRunId) {
|
|
50
|
-
|
|
56
|
+
start();
|
|
51
57
|
run.probe = await runBash(definition.trigger.script, definition.trigger.cwd, run.input, controller.signal);
|
|
52
58
|
run.matched = run.probe.exitCode === 0;
|
|
53
59
|
this.store.saveRun(run);
|
|
@@ -57,19 +63,17 @@ export class TaskExecution {
|
|
|
57
63
|
throw new Error(`watch probe exited ${String(run.probe.exitCode)}`);
|
|
58
64
|
}
|
|
59
65
|
if (run.matched !== false)
|
|
60
|
-
run.result = await this.executeAction(run, controller.signal);
|
|
66
|
+
run.result = await this.executeAction(run, controller.signal, start);
|
|
67
|
+
controller.signal.throwIfAborted();
|
|
61
68
|
run.state = "succeeded";
|
|
62
69
|
if (definition.trigger.type === "watch" && !run.resumedFromRunId && definition.trigger.mode === "once" && run.matched) {
|
|
63
|
-
// As the definition's own creator
|
|
64
|
-
//
|
|
65
|
-
// somebody's task, and the owner guard (definitions.ts) would
|
|
66
|
-
// otherwise fail the run that had just succeeded.
|
|
70
|
+
// As the definition's own creator, or the owner guard (definitions.ts)
|
|
71
|
+
// would fail the run that just succeeded.
|
|
67
72
|
this.definitions.setEnabled(definition.id, false, definition.creator);
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
catch (error) {
|
|
71
|
-
// A killed child reports `exited null
|
|
72
|
-
// aborted instead of how the corpse looked.
|
|
76
|
+
// A killed child reports `exited null`; report why we aborted instead.
|
|
73
77
|
const aborted = controller.signal.aborted;
|
|
74
78
|
run.state = aborted ? (timedOut ? "failed" : "cancelled") : "failed";
|
|
75
79
|
run.error = timedOut ? "task timed out" : aborted ? "cancelled" : String(error);
|
|
@@ -80,10 +84,8 @@ export class TaskExecution {
|
|
|
80
84
|
run.finishedAt = Date.now();
|
|
81
85
|
const seconds = ((run.finishedAt - (run.startedAt ?? run.queuedAt)) / 1000).toFixed(1);
|
|
82
86
|
const settled = `run ${run.id} (${run.context.definition.name}) ${run.state} in ${seconds}s`;
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// A watch probe that did not match is the opposite case — it fires on
|
|
86
|
-
// every interval and at info would be most of the journal.
|
|
87
|
+
// Nobody opens the Console to find out a scheduled run failed. A watch
|
|
88
|
+
// probe that did not match fires every interval and would be most of the journal.
|
|
87
89
|
if (run.state === "failed")
|
|
88
90
|
log.error(settled, cause);
|
|
89
91
|
else if (run.matched === false)
|
|
@@ -99,22 +101,30 @@ export class TaskExecution {
|
|
|
99
101
|
this.store.saveRun(run);
|
|
100
102
|
}
|
|
101
103
|
catch (err) {
|
|
102
|
-
// Waiters settle from the in-memory run
|
|
103
|
-
//
|
|
104
|
-
// sweep re-marks it and delivers then. Named here so that delay has
|
|
105
|
-
// an explanation.
|
|
104
|
+
// Waiters settle from the in-memory run; the callback reads the stale
|
|
105
|
+
// row and waits for the next boot's interrupt sweep.
|
|
106
106
|
log.error(`run ${run.id} final save failed — callback/join deferred to next boot`, err);
|
|
107
107
|
}
|
|
108
|
+
if (run.matched === false) {
|
|
109
|
+
try {
|
|
110
|
+
this.store.pruneUnmatchedProbes(run.taskId);
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
// Housekeeping: a waiter on this run is owed its settlement regardless.
|
|
114
|
+
log.warn(`run ${run.id} probe retention failed`, err);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
108
117
|
this.host.changed(run);
|
|
109
118
|
this.host.settled(run);
|
|
110
119
|
if (run.callbackState === "pending")
|
|
111
120
|
void this.callbacks.deliver(run);
|
|
112
121
|
}
|
|
113
122
|
}
|
|
114
|
-
async executeAction(run, signal) {
|
|
123
|
+
async executeAction(run, signal, start) {
|
|
124
|
+
signal.throwIfAborted();
|
|
115
125
|
const action = run.context.definition.action;
|
|
116
126
|
if (action.type === "bash") {
|
|
117
|
-
|
|
127
|
+
start();
|
|
118
128
|
run.context.cwd = action.cwd;
|
|
119
129
|
this.store.saveRun(run);
|
|
120
130
|
const result = await runBash(action.script, action.cwd, run.input, signal);
|
|
@@ -126,7 +136,7 @@ export class TaskExecution {
|
|
|
126
136
|
return output;
|
|
127
137
|
}
|
|
128
138
|
if (action.type === "system") {
|
|
129
|
-
|
|
139
|
+
start();
|
|
130
140
|
const handler = this.definitions.systemAction(action.name, run.context.definition.creator);
|
|
131
141
|
signal.throwIfAborted();
|
|
132
142
|
const text = await handler(signal);
|
|
@@ -134,7 +144,7 @@ export class TaskExecution {
|
|
|
134
144
|
return { type: "system", text };
|
|
135
145
|
}
|
|
136
146
|
if (action.type === "task") {
|
|
137
|
-
|
|
147
|
+
start();
|
|
138
148
|
const child = this.host.runChild(action.taskId, run);
|
|
139
149
|
let rejectWait = (reason) => { void reason; };
|
|
140
150
|
const aborted = new Promise((_, reject) => { rejectWait = reject; });
|
|
@@ -143,6 +153,8 @@ export class TaskExecution {
|
|
|
143
153
|
rejectWait(new Error("cancelled"));
|
|
144
154
|
};
|
|
145
155
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
156
|
+
if (signal.aborted)
|
|
157
|
+
onAbort();
|
|
146
158
|
try {
|
|
147
159
|
const done = await Promise.race([this.host.waitForRun(child.id), aborted]);
|
|
148
160
|
if (done.state !== "succeeded")
|
|
@@ -153,14 +165,6 @@ export class TaskExecution {
|
|
|
153
165
|
signal.removeEventListener("abort", onAbort);
|
|
154
166
|
}
|
|
155
167
|
}
|
|
156
|
-
return this.agent.execute(run, action, signal,
|
|
157
|
-
}
|
|
158
|
-
markRunning(run) {
|
|
159
|
-
if (run.state === "running")
|
|
160
|
-
return;
|
|
161
|
-
run.state = "running";
|
|
162
|
-
run.startedAt = Date.now();
|
|
163
|
-
this.store.saveRun(run);
|
|
164
|
-
this.host.changed(run);
|
|
168
|
+
return this.agent.execute(run, action, signal, start);
|
|
165
169
|
}
|
|
166
170
|
}
|
package/dist/tasks/groups.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
// The fan-out join:
|
|
2
|
-
//
|
|
3
|
-
// members are cancelled, and the single aggregated callback that goes back.
|
|
4
|
-
// Delivering it is the outbox's job; deciding it is this file's.
|
|
5
|
-
import { Router } from "../core/router.js";
|
|
1
|
+
// The fan-out join: when the join condition is met, which members are
|
|
2
|
+
// cancelled, and the single aggregated callback. Delivering it is the outbox's job.
|
|
6
3
|
import { logger } from "../log.js";
|
|
7
4
|
import { runRef, runResultText } from "./callbacks.js";
|
|
8
5
|
import { newId } from "./definitions.js";
|
|
9
6
|
import { Outbox } from "./outbox.js";
|
|
10
|
-
import { TaskStore } from "./store.js";
|
|
11
7
|
import { isTerminal } from "./types.js";
|
|
12
8
|
const log = logger("tasks");
|
|
13
9
|
/** Core-owned fan-out join: members run detached, the group delivers one
|
|
@@ -16,43 +12,44 @@ export class TaskGroups {
|
|
|
16
12
|
store;
|
|
17
13
|
host;
|
|
18
14
|
changed;
|
|
15
|
+
unreachable;
|
|
19
16
|
outbox;
|
|
20
17
|
constructor(store, router, host, changed, unreachable) {
|
|
21
18
|
this.store = store;
|
|
22
19
|
this.host = host;
|
|
23
20
|
this.changed = changed;
|
|
21
|
+
this.unreachable = unreachable;
|
|
24
22
|
this.outbox = new Outbox(router, {
|
|
25
23
|
id: (group) => group.id,
|
|
26
24
|
reload: (id) => this.store.getGroup(id),
|
|
27
25
|
save: (group) => { this.store.saveGroup(group); },
|
|
28
26
|
changed,
|
|
29
|
-
// A group names itself
|
|
30
|
-
//
|
|
31
|
-
// resolves that id to the group's members (web/ui/task-runs.ts).
|
|
27
|
+
// A group names itself where a run names its run id, so the engine's
|
|
28
|
+
// transcript proof works unchanged.
|
|
32
29
|
input: (groups) => ({
|
|
33
30
|
text: this.text(groups[0]),
|
|
34
31
|
origin: { kind: "task-callback", taskId: groups[0].id, runId: groups[0].id, sourceSessionId: null },
|
|
35
32
|
}),
|
|
36
|
-
|
|
37
|
-
}
|
|
33
|
+
abandoned: (group, sessionId, why) => unreachable(sessionId, `the result of a ${String(group.memberRunIds.length)}-run group`, why),
|
|
34
|
+
});
|
|
38
35
|
}
|
|
39
36
|
/** Enqueues every member or none: a partially started group is worse than
|
|
40
37
|
* a rejected one. */
|
|
41
|
-
runAll(definitions, join, callerSessionId, parentRunId, callbackSessionId) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.
|
|
38
|
+
runAll(definitions, join, callerSessionId, parentRunId, callbackSessionId, callbackMode) {
|
|
39
|
+
if (definitions.length < 2)
|
|
40
|
+
throw new Error("a task group needs at least 2 members");
|
|
41
|
+
const { group, runs } = this.store.transact(() => {
|
|
42
|
+
const group = this.create(join, callerSessionId, callbackSessionId, callbackMode);
|
|
43
|
+
const runs = definitions.map((definition) => this.host.prepareMember(definition.id, group.id, callerSessionId, parentRunId));
|
|
44
|
+
group.memberRunIds = runs.map((run) => run.id);
|
|
45
|
+
this.store.saveGroup(group);
|
|
46
|
+
return { group, runs };
|
|
47
|
+
});
|
|
48
|
+
this.changed(group);
|
|
49
|
+
for (const run of runs)
|
|
50
|
+
this.host.startMember(run);
|
|
51
|
+
// Skipped members do not pass through execution's settled hook.
|
|
52
|
+
this.evaluate(this.get(group.id));
|
|
56
53
|
return { group: this.get(group.id), runs };
|
|
57
54
|
}
|
|
58
55
|
members(id) {
|
|
@@ -64,12 +61,13 @@ export class TaskGroups {
|
|
|
64
61
|
this.host.cancel(runId);
|
|
65
62
|
return this.get(id);
|
|
66
63
|
}
|
|
67
|
-
create(join, invokedBySessionId, callbackSessionId) {
|
|
68
|
-
|
|
64
|
+
create(join, invokedBySessionId, callbackSessionId, callbackMode) {
|
|
65
|
+
return {
|
|
69
66
|
id: newId(),
|
|
70
67
|
join,
|
|
71
68
|
invokedBySessionId,
|
|
72
69
|
callbackSessionId,
|
|
70
|
+
...(callbackMode === "steer" ? { callbackMode } : {}),
|
|
73
71
|
memberRunIds: [],
|
|
74
72
|
winnerRunId: null,
|
|
75
73
|
callbackState: null,
|
|
@@ -79,13 +77,6 @@ export class TaskGroups {
|
|
|
79
77
|
createdAt: Date.now(),
|
|
80
78
|
finishedAt: null,
|
|
81
79
|
};
|
|
82
|
-
this.store.saveGroup(group);
|
|
83
|
-
return group;
|
|
84
|
-
}
|
|
85
|
-
setMembers(group, runIds) {
|
|
86
|
-
group.memberRunIds = runIds;
|
|
87
|
-
this.store.saveGroup(group);
|
|
88
|
-
this.changed(group);
|
|
89
80
|
}
|
|
90
81
|
get(id) {
|
|
91
82
|
const group = this.store.getGroup(id);
|
|
@@ -109,6 +100,21 @@ export class TaskGroups {
|
|
|
109
100
|
}
|
|
110
101
|
}
|
|
111
102
|
evaluate(group) {
|
|
103
|
+
if (group.finishedAt !== null)
|
|
104
|
+
return;
|
|
105
|
+
// An empty group is a broken record; retire it without inventing a
|
|
106
|
+
// successful zero-member join.
|
|
107
|
+
if (group.memberRunIds.length === 0) {
|
|
108
|
+
group.finishedAt = Date.now();
|
|
109
|
+
group.callbackState = group.callbackSessionId ? "abandoned" : null;
|
|
110
|
+
group.callbackError = "group admission failed before its members were recorded";
|
|
111
|
+
this.store.saveGroup(group);
|
|
112
|
+
this.changed(group);
|
|
113
|
+
log.error(`group ${group.id}: ${group.callbackError}`);
|
|
114
|
+
if (group.callbackSessionId)
|
|
115
|
+
this.unreachable(group.callbackSessionId, `task group ${group.id}`, group.callbackError);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
112
118
|
const members = group.memberRunIds.map((id) => this.host.getRun(id));
|
|
113
119
|
if (group.join === "first") {
|
|
114
120
|
const winner = members.find((run) => isTerminal(run.state));
|