@timqi/pier 0.1.0 → 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 +6 -15
- package/dist/agent/credentials.js +11 -23
- package/dist/agent/events.js +42 -64
- package/dist/agent/listing.js +39 -92
- package/dist/agent/pi.js +103 -252
- 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 +7 -23
- 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 +12 -34
- package/dist/core/queue.js +3 -5
- package/dist/core/reply.js +41 -142
- package/dist/core/router.js +209 -264
- package/dist/core/types.js +4 -0
- package/dist/db.js +88 -272
- package/dist/drain.js +57 -50
- 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 +4 -12
- 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 +84 -183
- package/dist/paths.js +10 -26
- package/dist/secrets.js +18 -45
- package/dist/service.js +31 -73
- package/dist/settings.js +19 -63
- package/dist/tasks/agent.js +24 -45
- package/dist/tasks/callbacks.js +8 -16
- package/dist/tasks/command.js +2 -6
- package/dist/tasks/definitions.js +39 -62
- package/dist/tasks/execution.js +41 -39
- package/dist/tasks/groups.js +8 -11
- package/dist/tasks/messages.js +88 -155
- package/dist/tasks/outbox.js +33 -54
- package/dist/tasks/routes.js +4 -7
- package/dist/tasks/runs.js +4 -9
- package/dist/tasks/service.js +29 -42
- package/dist/tasks/store.js +36 -27
- package/dist/tasks/tool.js +59 -60
- package/dist/tools-task.js +20 -60
- package/dist/tools.js +98 -325
- package/dist/update.js +20 -43
- 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 +35 -82
- package/dist/web/providers.js +5 -11
- package/dist/web/public/assets/{activity-Bl3vZukb.js → activity-B89_hH7q.js} +1 -1
- 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-DYuf4Mlj.js → boards-BeKW0ZXK.js} +1 -1
- 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-BLJu7EXN.js → runs-Cwy0mN8i.js} +1 -1
- 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-BrdVh-Zi.js → settings-DzZLmujq.js} +1 -1
- 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-CeQS1rxa.js → task-runs-BCakxFk8.js} +1 -1
- 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 +30 -16
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.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 +109 -190
- package/dist/web/session-state.js +13 -53
- package/dist/web/types.js +2 -4
- package/dist/web/webpush.js +10 -25
- package/docs/deploy.md +115 -330
- package/package.json +1 -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 +23 -15
- package/dist/config-sync-fetch.js +0 -84
- package/dist/limits.js +0 -14
- package/dist/web/public/assets/activity-Bl3vZukb.js.br +0 -0
- package/dist/web/public/assets/activity-Bl3vZukb.js.gz +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.br +0 -0
- package/dist/web/public/assets/boards-DYuf4Mlj.js.gz +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js +0 -4
- package/dist/web/public/assets/explorer-qJH_9nTE.js.br +0 -0
- package/dist/web/public/assets/explorer-qJH_9nTE.js.gz +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js +0 -85
- package/dist/web/public/assets/index-Dqdb-Eqt.js.br +0 -0
- package/dist/web/public/assets/index-Dqdb-Eqt.js.gz +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css +0 -2
- package/dist/web/public/assets/index-DzmMzvi_.css.br +0 -0
- package/dist/web/public/assets/index-DzmMzvi_.css.gz +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.br +0 -0
- package/dist/web/public/assets/runs-BLJu7EXN.js.gz +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.br +0 -0
- package/dist/web/public/assets/settings-BrdVh-Zi.js.gz +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.br +0 -0
- package/dist/web/public/assets/task-runs-CeQS1rxa.js.gz +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js +0 -4
- package/dist/web/public/assets/tasks-bcb3fYdK.js.br +0 -0
- package/dist/web/public/assets/tasks-bcb3fYdK.js.gz +0 -0
package/dist/tasks/callbacks.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
// What a finished run says to the session that delegated it. Delivery itself
|
|
2
2
|
// belongs to outbox.ts; this file owns the run vocabulary and the batching.
|
|
3
3
|
import { Outbox } from "./outbox.js";
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
* (`#/session/<id>`), and without this line that costs a second tool call.
|
|
7
|
-
* Shared with the group callback, which has always carried both. */
|
|
4
|
+
/** The run id and the session that did the work: a relayer's next move is a
|
|
5
|
+
* deep link to it, and without this that costs a second tool call. */
|
|
8
6
|
export const runRef = (run) => `Run: ${run.id}${run.targetSessionId ? ` / Session: ${run.targetSessionId}` : ""}`;
|
|
9
|
-
/** What the card in the recipient's transcript says the input came from.
|
|
10
|
-
* of the run vocabulary for the same reason `runRef` is: the callback, the
|
|
11
|
-
* subagent messages and the delegation prompt all name a run, and three
|
|
12
|
-
* spellings of "which task, on what model" would be three cards. */
|
|
7
|
+
/** What the card in the recipient's transcript says the input came from. */
|
|
13
8
|
export const runSource = (run) => ({
|
|
14
9
|
taskName: run.context.definition.name,
|
|
15
10
|
...(run.context.model ? { model: run.context.model } : {}),
|
|
@@ -47,15 +42,13 @@ export class TaskCallbacks {
|
|
|
47
42
|
runId: runs[0].id,
|
|
48
43
|
sourceSessionId: runs[0].targetSessionId,
|
|
49
44
|
runIds: runs.map((run) => run.id),
|
|
50
|
-
// Only
|
|
51
|
-
//
|
|
52
|
-
// caption would attribute every other result to it. The text names
|
|
53
|
-
// each run; the card says nothing rather than something false.
|
|
45
|
+
// Only for one run: a batch's caption would attribute every result
|
|
46
|
+
// to the first run's name and model.
|
|
54
47
|
...(runs.length === 1 ? { source: runSource(runs[0]), state: runs[0].state } : {}),
|
|
55
48
|
},
|
|
56
49
|
}),
|
|
57
|
-
|
|
58
|
-
}
|
|
50
|
+
abandoned: (run, sessionId, why) => unreachable(sessionId, `the result of "${run.context.definition.name}"`, why),
|
|
51
|
+
});
|
|
59
52
|
}
|
|
60
53
|
target(callback, origin) {
|
|
61
54
|
if (callback.type === "session")
|
|
@@ -75,8 +68,7 @@ export class TaskCallbacks {
|
|
|
75
68
|
if (!first?.callbackSessionId || (first.callbackState !== "pending" && first.callbackState !== "failed"))
|
|
76
69
|
return;
|
|
77
70
|
const sessionId = first.callbackSessionId;
|
|
78
|
-
//
|
|
79
|
-
// deliverable, everything pending for the session rides along.
|
|
71
|
+
// Once one callback is deliverable, everything pending for the session rides along.
|
|
80
72
|
const batch = this.store.listPendingCallbacks(Number.MAX_SAFE_INTEGER)
|
|
81
73
|
.filter((run) => run.callbackSessionId === sessionId);
|
|
82
74
|
if (!batch.some((run) => run.id === first.id))
|
package/dist/tasks/command.js
CHANGED
|
@@ -93,12 +93,8 @@ export function runBash(script, cwd, input, signal) {
|
|
|
93
93
|
stderrTruncated: stderr.truncated,
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
|
-
// A script that never reads stdin
|
|
97
|
-
//
|
|
98
|
-
// Unhandled, that is an `error` event on a stream, which is an uncaught
|
|
99
|
-
// exception, which is main.ts exiting the process: one task script could
|
|
100
|
-
// take every session and every other run down with it. The input not being
|
|
101
|
-
// wanted is not a failure of the run — anything else still gets said.
|
|
96
|
+
// A script that never reads stdin raises EPIPE here; unhandled, that is an
|
|
97
|
+
// uncaught exception that exits the process. Not wanting the input is not a failure.
|
|
102
98
|
child.stdin.on("error", (err) => {
|
|
103
99
|
if (err.code !== "EPIPE")
|
|
104
100
|
log.warn(`run input could not be written: ${err.message}`);
|
|
@@ -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,7 +1,5 @@
|
|
|
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
4
|
import { runBash } from "./command.js";
|
|
7
5
|
const log = logger("tasks");
|
|
@@ -35,17 +33,27 @@ export class TaskExecution {
|
|
|
35
33
|
this.controllers.set(run.id, controller);
|
|
36
34
|
let timedOut = false;
|
|
37
35
|
let cause;
|
|
38
|
-
|
|
39
|
-
|
|
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")
|
|
40
47
|
return;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
run.state = "running";
|
|
49
|
+
run.startedAt = Date.now();
|
|
50
|
+
this.store.saveRun(run);
|
|
51
|
+
this.host.changed(run);
|
|
52
|
+
};
|
|
45
53
|
try {
|
|
46
54
|
const { definition } = run.context;
|
|
47
55
|
if (definition.trigger.type === "watch" && !run.resumedFromRunId) {
|
|
48
|
-
|
|
56
|
+
start();
|
|
49
57
|
run.probe = await runBash(definition.trigger.script, definition.trigger.cwd, run.input, controller.signal);
|
|
50
58
|
run.matched = run.probe.exitCode === 0;
|
|
51
59
|
this.store.saveRun(run);
|
|
@@ -55,20 +63,17 @@ export class TaskExecution {
|
|
|
55
63
|
throw new Error(`watch probe exited ${String(run.probe.exitCode)}`);
|
|
56
64
|
}
|
|
57
65
|
if (run.matched !== false)
|
|
58
|
-
run.result = await this.executeAction(run, controller.signal);
|
|
66
|
+
run.result = await this.executeAction(run, controller.signal, start);
|
|
59
67
|
controller.signal.throwIfAborted();
|
|
60
68
|
run.state = "succeeded";
|
|
61
69
|
if (definition.trigger.type === "watch" && !run.resumedFromRunId && definition.trigger.mode === "once" && run.matched) {
|
|
62
|
-
// As the definition's own creator
|
|
63
|
-
//
|
|
64
|
-
// somebody's task, and the owner guard (definitions.ts) would
|
|
65
|
-
// 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.
|
|
66
72
|
this.definitions.setEnabled(definition.id, false, definition.creator);
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
catch (error) {
|
|
70
|
-
// A killed child reports `exited null
|
|
71
|
-
// aborted instead of how the corpse looked.
|
|
76
|
+
// A killed child reports `exited null`; report why we aborted instead.
|
|
72
77
|
const aborted = controller.signal.aborted;
|
|
73
78
|
run.state = aborted ? (timedOut ? "failed" : "cancelled") : "failed";
|
|
74
79
|
run.error = timedOut ? "task timed out" : aborted ? "cancelled" : String(error);
|
|
@@ -79,10 +84,8 @@ export class TaskExecution {
|
|
|
79
84
|
run.finishedAt = Date.now();
|
|
80
85
|
const seconds = ((run.finishedAt - (run.startedAt ?? run.queuedAt)) / 1000).toFixed(1);
|
|
81
86
|
const settled = `run ${run.id} (${run.context.definition.name}) ${run.state} in ${seconds}s`;
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// A watch probe that did not match is the opposite case — it fires on
|
|
85
|
-
// 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.
|
|
86
89
|
if (run.state === "failed")
|
|
87
90
|
log.error(settled, cause);
|
|
88
91
|
else if (run.matched === false)
|
|
@@ -98,23 +101,30 @@ export class TaskExecution {
|
|
|
98
101
|
this.store.saveRun(run);
|
|
99
102
|
}
|
|
100
103
|
catch (err) {
|
|
101
|
-
// Waiters settle from the in-memory run
|
|
102
|
-
//
|
|
103
|
-
// sweep re-marks it and delivers then. Named here so that delay has
|
|
104
|
-
// 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.
|
|
105
106
|
log.error(`run ${run.id} final save failed — callback/join deferred to next boot`, err);
|
|
106
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
|
+
}
|
|
107
117
|
this.host.changed(run);
|
|
108
118
|
this.host.settled(run);
|
|
109
119
|
if (run.callbackState === "pending")
|
|
110
120
|
void this.callbacks.deliver(run);
|
|
111
121
|
}
|
|
112
122
|
}
|
|
113
|
-
async executeAction(run, signal) {
|
|
123
|
+
async executeAction(run, signal, start) {
|
|
114
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; });
|
|
@@ -155,14 +165,6 @@ export class TaskExecution {
|
|
|
155
165
|
signal.removeEventListener("abort", onAbort);
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
|
-
return this.agent.execute(run, action, signal,
|
|
159
|
-
}
|
|
160
|
-
markRunning(run) {
|
|
161
|
-
if (run.state === "running")
|
|
162
|
-
return;
|
|
163
|
-
run.state = "running";
|
|
164
|
-
run.startedAt = Date.now();
|
|
165
|
-
this.store.saveRun(run);
|
|
166
|
-
this.host.changed(run);
|
|
168
|
+
return this.agent.execute(run, action, signal, start);
|
|
167
169
|
}
|
|
168
170
|
}
|
package/dist/tasks/groups.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
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.
|
|
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.
|
|
5
3
|
import { logger } from "../log.js";
|
|
6
4
|
import { runRef, runResultText } from "./callbacks.js";
|
|
7
5
|
import { newId } from "./definitions.js";
|
|
@@ -26,15 +24,14 @@ export class TaskGroups {
|
|
|
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. */
|
|
@@ -105,8 +102,8 @@ export class TaskGroups {
|
|
|
105
102
|
evaluate(group) {
|
|
106
103
|
if (group.finishedAt !== null)
|
|
107
104
|
return;
|
|
108
|
-
//
|
|
109
|
-
//
|
|
105
|
+
// An empty group is a broken record; retire it without inventing a
|
|
106
|
+
// successful zero-member join.
|
|
110
107
|
if (group.memberRunIds.length === 0) {
|
|
111
108
|
group.finishedAt = Date.now();
|
|
112
109
|
group.callbackState = group.callbackSessionId ? "abandoned" : null;
|