@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/routes.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
// The area's HTTP surface
|
|
2
|
-
//
|
|
3
|
-
// its body, names the caller and hands the decision to TaskService — policy
|
|
4
|
-
// that lives here would be policy the task tool does not get.
|
|
1
|
+
// The area's HTTP surface. A route names the caller and hands the decision to
|
|
2
|
+
// TaskService: policy here would be policy the task tool does not get.
|
|
5
3
|
import { record, requiredString } from "./definitions.js";
|
|
6
4
|
/** Reject malformed filters instead of silently widening a global query. */
|
|
7
5
|
function runQuery(params) {
|
|
@@ -51,10 +49,7 @@ export function registerTaskRoutes(app, tasks, activity) {
|
|
|
51
49
|
const now = Date.now();
|
|
52
50
|
const recent = c.req.query("scope") === "recent";
|
|
53
51
|
const windowStart = now - 24 * 60 * 60 * 1000;
|
|
54
|
-
|
|
55
|
-
// last 24h no matter when it was queued, so it never drops a live run.
|
|
56
|
-
const runs = tasks.recentRuns(200).filter((run) => run.state === "queued" || run.state === "running" ||
|
|
57
|
-
(recent && run.queuedAt >= windowStart));
|
|
52
|
+
const runs = tasks.activityRuns(recent ? windowStart : undefined);
|
|
58
53
|
const listed = await activity.factory.list();
|
|
59
54
|
const byId = new Map(listed.map((session) => [session.id, session]));
|
|
60
55
|
const linkedIds = new Set();
|
|
@@ -227,9 +222,8 @@ export function registerTaskRoutes(app, tasks, activity) {
|
|
|
227
222
|
const body = record(await jsonBody(c.req));
|
|
228
223
|
const input = body && "input" in body ? body.input : null;
|
|
229
224
|
try {
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// on a reuse definition, work injected into a live session.
|
|
225
|
+
// Dropping an unsupported override would run the definition's own policy
|
|
226
|
+
// instead — on a reuse definition, work injected into a live session.
|
|
233
227
|
if (body?.sessionMode !== undefined && body.sessionMode !== "fresh") {
|
|
234
228
|
throw new Error(`unsupported sessionMode: ${String(body.sessionMode)}`);
|
|
235
229
|
}
|
package/dist/tasks/runs.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
// A definition plus an input becomes a queued run
|
|
2
|
-
//
|
|
3
|
-
// and which session hears about it. The limits that keep a chain from
|
|
4
|
-
// exploding (depth, children per root) are decided here, once, because every
|
|
5
|
-
// caller — scheduler, tool, HTTP — enqueues through this one door.
|
|
1
|
+
// A definition plus an input becomes a queued run. The depth and per-root
|
|
2
|
+
// limits are decided here because every caller enqueues through this one door.
|
|
6
3
|
import { logger } from "../log.js";
|
|
7
|
-
import { TaskCallbacks } from "./callbacks.js";
|
|
8
4
|
import { newId } from "./definitions.js";
|
|
9
|
-
import { TaskStore } from "./store.js";
|
|
10
5
|
const log = logger("tasks");
|
|
11
6
|
const MAX_DEPTH = 2;
|
|
12
7
|
const MAX_CHILDREN_PER_ROOT = 16;
|
|
@@ -23,7 +18,8 @@ export class TaskRunQueue {
|
|
|
23
18
|
this.execute = execute;
|
|
24
19
|
this.changed = changed;
|
|
25
20
|
}
|
|
26
|
-
|
|
21
|
+
/** Validate and persist only; callers may stage several runs in a transaction. */
|
|
22
|
+
prepare(definition, input, source, parentRunId, provenance) {
|
|
27
23
|
const id = newId();
|
|
28
24
|
const parent = parentRunId ? this.getRun(parentRunId) : null;
|
|
29
25
|
const depth = provenance.depth ?? (parent ? parent.depth + 1 : 0);
|
|
@@ -63,6 +59,7 @@ export class TaskRunQueue {
|
|
|
63
59
|
targetSessionId,
|
|
64
60
|
sessionMode,
|
|
65
61
|
callbackSessionId,
|
|
62
|
+
...(provenance.callbackMode === "steer" ? { callbackMode: "steer" } : {}),
|
|
66
63
|
background: provenance.background ?? false,
|
|
67
64
|
callbackState: overlapped && callbackSessionId ? "pending" : null,
|
|
68
65
|
callbackAttempts: 0,
|
|
@@ -84,11 +81,16 @@ export class TaskRunQueue {
|
|
|
84
81
|
finishedAt: overlapped ? now : null,
|
|
85
82
|
};
|
|
86
83
|
this.store.saveRun(run);
|
|
84
|
+
return run;
|
|
85
|
+
}
|
|
86
|
+
/** Publish and execute only after all records this run relies on committed. */
|
|
87
|
+
start(run) {
|
|
87
88
|
this.changed(run);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
//
|
|
89
|
+
const { id, depth, triggerSource: source } = run;
|
|
90
|
+
const overlapped = run.skipReason === "overlap";
|
|
91
|
+
const definition = run.context.definition;
|
|
92
|
+
// Why a run exists is answerable only here: the row keeps the ids, not the
|
|
93
|
+
// reason. A watch probe queues every interval, so it logs at debug.
|
|
92
94
|
const queued = `run ${id} ${run.state}: ${definition.name} via ${source}` +
|
|
93
95
|
`${overlapped ? " (overlapped)" : ""}${depth > 0 ? ` depth ${String(depth)}` : ""}`;
|
|
94
96
|
if (source === "watch" && !overlapped)
|
|
@@ -99,6 +101,5 @@ export class TaskRunQueue {
|
|
|
99
101
|
this.execute(run);
|
|
100
102
|
else if (run.callbackState === "pending")
|
|
101
103
|
void this.callbacks.deliver(run);
|
|
102
|
-
return run;
|
|
103
104
|
}
|
|
104
105
|
}
|
package/dist/tasks/service.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// like it makes belongs to a file beside it (definitions, runs, execution,
|
|
5
|
-
// groups, messages, callbacks) — what is genuinely here is scheduling and the
|
|
6
|
-
// facade, so the HTTP routes and the task tool cannot drift apart.
|
|
7
|
-
import { EventHub } from "../core/hub.js";
|
|
8
|
-
import { Router } from "../core/router.js";
|
|
1
|
+
// The facade the rest of Pier talks to about tasks, and the clock behind it:
|
|
2
|
+
// the tick, the boot recovery that writes off interrupted runs, and the pause a
|
|
3
|
+
// drain needs. Decisions belong to the files beside it.
|
|
9
4
|
import { logger } from "../log.js";
|
|
10
5
|
import { AgentTaskRunner } from "./agent.js";
|
|
11
6
|
import { TaskCallbacks } from "./callbacks.js";
|
|
@@ -14,13 +9,11 @@ import { TaskExecution } from "./execution.js";
|
|
|
14
9
|
import { TaskGroups } from "./groups.js";
|
|
15
10
|
import { TaskMessenger } from "./messages.js";
|
|
16
11
|
import { TaskRunQueue } from "./runs.js";
|
|
17
|
-
import { TaskStore } from "./store.js";
|
|
18
12
|
import { handleTaskTool } from "./tool.js";
|
|
19
13
|
import { isTerminal } from "./types.js";
|
|
20
14
|
const log = logger("tasks");
|
|
21
|
-
/**
|
|
22
|
-
*
|
|
23
|
-
* preamble and the input wrapper, which the delegating session did not send. */
|
|
15
|
+
/** Not `renderedPrompt`: that carries the preamble and input wrapper, which the
|
|
16
|
+
* delegating session did not send. */
|
|
24
17
|
const runPrompt = (run) => {
|
|
25
18
|
if (run.context.resumePrompt)
|
|
26
19
|
return run.context.resumePrompt;
|
|
@@ -43,8 +36,7 @@ export class TaskService {
|
|
|
43
36
|
runs;
|
|
44
37
|
execution;
|
|
45
38
|
constructor(store, factory, router, hub,
|
|
46
|
-
/** Structural
|
|
47
|
-
* hands in a closure over the store instead. Absent in bare test rigs. */
|
|
39
|
+
/** Structural: tasks/ must not import settings.ts. Absent in bare test rigs. */
|
|
48
40
|
instance) {
|
|
49
41
|
this.store = store;
|
|
50
42
|
this.factory = factory;
|
|
@@ -52,20 +44,21 @@ export class TaskService {
|
|
|
52
44
|
this.hub = hub;
|
|
53
45
|
this.instance = instance;
|
|
54
46
|
const unreachable = (sessionId, what, why) => this.unreachable(sessionId, what, why);
|
|
55
|
-
this.messages = new TaskMessenger(store, router, hub, (runId, prompt, fromSessionId) => this.
|
|
47
|
+
this.messages = new TaskMessenger(store, router, hub, (runId, prompt, fromSessionId) => this.prepareResume(runId, prompt, { invokedBySessionId: fromSessionId, callbackSessionId: fromSessionId, background: true }), unreachable, (run) => this.runs.start(run));
|
|
56
48
|
this.definitions = new TaskDefinitions(store, factory, router, hub, instance?.systemActions);
|
|
57
49
|
this.callbacks = new TaskCallbacks(store, router, (run) => this.changed(run), unreachable);
|
|
58
50
|
this.groups = new TaskGroups(store, router, {
|
|
59
51
|
getRun: (id) => this.getRun(id),
|
|
60
52
|
cancel: (id) => { this.cancel(id); },
|
|
61
53
|
openDecisionId: (runId) => this.messages.openDecisionId(runId),
|
|
62
|
-
|
|
54
|
+
prepareMember: (taskId, groupId, callerSessionId, parentRunId) => this.prepareRun(taskId, null, "agent", parentRunId, {
|
|
63
55
|
invokedBySessionId: callerSessionId,
|
|
64
56
|
sourceSessionId: callerSessionId,
|
|
65
57
|
callbackSessionId: null,
|
|
66
58
|
background: true,
|
|
67
59
|
groupId,
|
|
68
60
|
}),
|
|
61
|
+
startMember: (run) => this.runs.start(run),
|
|
69
62
|
}, (group) => this.hub.emitWorkspace({ type: "task-group-changed", groupId: group.id }), unreachable);
|
|
70
63
|
const agent = new AgentTaskRunner(factory, router, store, this.messages, (run) => this.changed(run));
|
|
71
64
|
this.execution = new TaskExecution(store, this.definitions, this.callbacks, agent, {
|
|
@@ -110,10 +103,8 @@ export class TaskService {
|
|
|
110
103
|
}, tickMs);
|
|
111
104
|
this.timer.unref();
|
|
112
105
|
}
|
|
113
|
-
/**
|
|
114
|
-
*
|
|
115
|
-
* leave the scheduler switched off. Deliberately not `start()`: the boot
|
|
116
|
-
* recovery in there would write off runs this process is still running. */
|
|
106
|
+
/** For a handover that never started. Not `start()`: its boot recovery would
|
|
107
|
+
* write off runs this process is still running. */
|
|
117
108
|
unpause(tickMs = 1000) {
|
|
118
109
|
if (this.timer)
|
|
119
110
|
return;
|
|
@@ -124,11 +115,8 @@ export class TaskService {
|
|
|
124
115
|
this.pause();
|
|
125
116
|
this.execution.stop();
|
|
126
117
|
}
|
|
127
|
-
/**
|
|
128
|
-
*
|
|
129
|
-
* scheduler timer goes, and new root runs are refused; children of a run
|
|
130
|
-
* that is still finishing stay allowed, because refusing them would fail
|
|
131
|
-
* the very work the drain is waiting for. */
|
|
118
|
+
/** New root runs are refused, running ones left for the drain to wait on;
|
|
119
|
+
* children of a finishing run stay allowed, or the drain fails its own work. */
|
|
132
120
|
pause() {
|
|
133
121
|
this.paused = true;
|
|
134
122
|
if (this.timer)
|
|
@@ -145,11 +133,8 @@ export class TaskService {
|
|
|
145
133
|
activeRunCount() {
|
|
146
134
|
return this.store.countActiveRuns();
|
|
147
135
|
}
|
|
148
|
-
/**
|
|
149
|
-
*
|
|
150
|
-
* an overlap needs the same answer to know what to wait for, and scanning
|
|
151
|
-
* run history for it finds nothing once the skipped rows outnumber the
|
|
152
|
-
* window. */
|
|
136
|
+
/** A caller refused as an overlap needs this to know what to wait for;
|
|
137
|
+
* scanning run history finds nothing once skipped rows outnumber the window. */
|
|
153
138
|
activeRun(taskId) {
|
|
154
139
|
return this.store.findActiveRun(taskId);
|
|
155
140
|
}
|
|
@@ -162,8 +147,7 @@ export class TaskService {
|
|
|
162
147
|
create(raw, creator = "http") {
|
|
163
148
|
return this.definitions.create(raw, creator);
|
|
164
149
|
}
|
|
165
|
-
/** `by` is how
|
|
166
|
-
* the task tool have none, which is what closes both (definitions.ts). */
|
|
150
|
+
/** `by` is how owning code says so; the routes and the tool have none (definitions.ts). */
|
|
167
151
|
update(id, raw, by) {
|
|
168
152
|
return this.definitions.update(id, raw, by);
|
|
169
153
|
}
|
|
@@ -198,22 +182,19 @@ export class TaskService {
|
|
|
198
182
|
queryRuns(query = {}) {
|
|
199
183
|
return this.store.queryRuns(query);
|
|
200
184
|
}
|
|
201
|
-
|
|
202
|
-
return this.store.
|
|
185
|
+
activityRuns(since) {
|
|
186
|
+
return this.store.activityRuns(since);
|
|
203
187
|
}
|
|
204
188
|
recentMessages(since) {
|
|
205
189
|
return this.messages.recent(since);
|
|
206
190
|
}
|
|
207
|
-
/** Every run
|
|
208
|
-
* it, and a message does not leave the transcript because the run ended. */
|
|
191
|
+
/** Every run, not the last hour's: the card is a message in the transcript. */
|
|
209
192
|
backgroundRuns(sessionId) {
|
|
210
193
|
return this.store.listRunsForSession(sessionId, 200)
|
|
211
194
|
.filter((run) => run.background)
|
|
212
195
|
.reverse()
|
|
213
196
|
.map((run) => this.backgroundRun(run));
|
|
214
197
|
}
|
|
215
|
-
/** The same runs `backgroundRuns` reports as in flight, counted per session
|
|
216
|
-
* in one query — a list needs the number, not the runs. */
|
|
217
198
|
activeBackgroundRunCounts() {
|
|
218
199
|
return this.store.countActiveBackgroundRunsBySession();
|
|
219
200
|
}
|
|
@@ -222,13 +203,18 @@ export class TaskService {
|
|
|
222
203
|
return this.store.taskOwnedSessionIds();
|
|
223
204
|
}
|
|
224
205
|
run(taskId, input = null, source = "manual", parentRunId = null, provenance = {}) {
|
|
206
|
+
const run = this.prepareRun(taskId, input, source, parentRunId, provenance);
|
|
207
|
+
this.runs.start(run);
|
|
208
|
+
return run;
|
|
209
|
+
}
|
|
210
|
+
prepareRun(taskId, input, source, parentRunId, provenance) {
|
|
225
211
|
this.refusePaused(parentRunId);
|
|
226
212
|
const task = this.get(taskId);
|
|
227
213
|
// `enabled:false` pauses scheduling only; manual and agent triggers still
|
|
228
214
|
// run a paused task on demand. Archiving is the terminal state.
|
|
229
215
|
if (task.archived)
|
|
230
216
|
throw new Error("archived tasks cannot run");
|
|
231
|
-
return this.runs.
|
|
217
|
+
return this.runs.prepare(task, input, source, parentRunId, provenance);
|
|
232
218
|
}
|
|
233
219
|
async waitForRun(id) {
|
|
234
220
|
const current = this.getRun(id);
|
|
@@ -256,9 +242,9 @@ export class TaskService {
|
|
|
256
242
|
getGroup(id) {
|
|
257
243
|
return this.groups.members(id);
|
|
258
244
|
}
|
|
259
|
-
runGroup(definitions, join, callerSessionId, parentRunId, callbackSessionId) {
|
|
245
|
+
runGroup(definitions, join, callerSessionId, parentRunId, callbackSessionId, callbackMode) {
|
|
260
246
|
this.refusePaused(parentRunId);
|
|
261
|
-
return this.groups.runAll(definitions, join, callerSessionId, parentRunId, callbackSessionId);
|
|
247
|
+
return this.groups.runAll(definitions, join, callerSessionId, parentRunId, callbackSessionId, callbackMode);
|
|
262
248
|
}
|
|
263
249
|
descendants(run) {
|
|
264
250
|
const byParent = new Map();
|
|
@@ -291,6 +277,16 @@ export class TaskService {
|
|
|
291
277
|
return this.messages.reply(messageId, fromSessionId, message);
|
|
292
278
|
}
|
|
293
279
|
resume(id, message, provenance = {}) {
|
|
280
|
+
const { run, expired } = this.store.transact(() => ({
|
|
281
|
+
run: this.prepareResume(id, message, provenance),
|
|
282
|
+
expired: this.messages.expireDecisions(id, "superseded by a manual resume"),
|
|
283
|
+
}));
|
|
284
|
+
for (const message of expired)
|
|
285
|
+
this.messages.changed(message);
|
|
286
|
+
this.runs.start(run);
|
|
287
|
+
return run;
|
|
288
|
+
}
|
|
289
|
+
prepareResume(id, message, provenance) {
|
|
294
290
|
this.refusePaused();
|
|
295
291
|
const prior = this.getRun(id);
|
|
296
292
|
if (!isTerminal(prior.state))
|
|
@@ -299,8 +295,7 @@ export class TaskService {
|
|
|
299
295
|
throw new Error("only persisted Agent runs can be resumed");
|
|
300
296
|
}
|
|
301
297
|
const prompt = requiredString(message, "message");
|
|
302
|
-
this.
|
|
303
|
-
return this.runs.enqueue(prior.context.definition, null, "agent", null, {
|
|
298
|
+
return this.runs.prepare(prior.context.definition, null, "agent", null, {
|
|
304
299
|
...provenance,
|
|
305
300
|
sourceSessionId: provenance.invokedBySessionId ?? prior.invokedBySessionId,
|
|
306
301
|
targetSessionId: prior.targetSessionId,
|
|
@@ -314,9 +309,7 @@ export class TaskService {
|
|
|
314
309
|
tool(raw, callerSessionId) {
|
|
315
310
|
return handleTaskTool(this, this.definitions, this.store, this.messages, raw, callerSessionId);
|
|
316
311
|
}
|
|
317
|
-
/**
|
|
318
|
-
* set, the curated live catalog otherwise — an agent picks from names that
|
|
319
|
-
* exist right now, never from memory. */
|
|
312
|
+
/** An agent picks from names that exist right now, never from memory. */
|
|
320
313
|
async models() {
|
|
321
314
|
const menu = this.instance?.modelMenu() ?? [];
|
|
322
315
|
if (menu.length)
|
|
@@ -330,8 +323,17 @@ export class TaskService {
|
|
|
330
323
|
try {
|
|
331
324
|
const now = Date.now();
|
|
332
325
|
this.sweep("schedule", () => {
|
|
333
|
-
|
|
334
|
-
|
|
326
|
+
// Per task: one enqueue that throws must not spend the other due tasks'
|
|
327
|
+
// occurrence, and its own stays due for the next tick.
|
|
328
|
+
for (const task of this.definitions.due(now)) {
|
|
329
|
+
this.sweep(`schedule ${task.name}`, () => {
|
|
330
|
+
const run = this.store.transact(() => {
|
|
331
|
+
this.definitions.advance(task, now);
|
|
332
|
+
return this.prepareRun(task.id, null, task.trigger.type === "watch" ? "watch" : "cron", null, {});
|
|
333
|
+
});
|
|
334
|
+
this.hub.emitWorkspace({ type: "tasks-changed" });
|
|
335
|
+
this.runs.start(run);
|
|
336
|
+
});
|
|
335
337
|
}
|
|
336
338
|
});
|
|
337
339
|
this.sweep("run callbacks", () => this.callbacks.recover(now));
|
|
@@ -342,16 +344,13 @@ export class TaskService {
|
|
|
342
344
|
this.ticking = false;
|
|
343
345
|
}
|
|
344
346
|
}
|
|
345
|
-
/**
|
|
346
|
-
*
|
|
347
|
-
* the operator's log, the record the tool and Console read, and the event
|
|
348
|
-
* stream of the session that was supposed to receive it (§5b). */
|
|
347
|
+
/** Retrying forever costs the same silence as dropping, so it stops and says
|
|
348
|
+
* so on the log, the record and the recipient's event stream (§5). */
|
|
349
349
|
unreachable(sessionId, what, why) {
|
|
350
350
|
log.error(`gave up delivering ${what} to session ${sessionId}: ${why}`);
|
|
351
351
|
this.router.reportTo(sessionId, `${what} could not be delivered — ${why}`);
|
|
352
352
|
}
|
|
353
|
-
/**
|
|
354
|
-
* is gone throws on every pass) must not starve the retries behind it. */
|
|
353
|
+
/** Isolated: one sweep throwing on every pass must not starve the others. */
|
|
355
354
|
sweep(what, run) {
|
|
356
355
|
try {
|
|
357
356
|
run();
|
package/dist/tasks/store.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// Every query this area makes against pier.db, and nothing else
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
// its own handle — the schema is db.ts's migration list.
|
|
5
|
-
import { pierDb, statements } from "../db.js";
|
|
1
|
+
// Every query this area makes against pier.db, and nothing else; the schema is
|
|
2
|
+
// db.ts's migration list.
|
|
3
|
+
import { pierDb, statements, transact } from "../db.js";
|
|
6
4
|
const clamp = (limit, cap) => Math.min(Math.max(limit, 1), cap);
|
|
5
|
+
/** Kept unmatched probes per watch: one page of history, which is all any
|
|
6
|
+
* surface lists of a probe that found nothing. */
|
|
7
|
+
const KEPT_PROBES = 50;
|
|
7
8
|
export class TaskStore {
|
|
8
9
|
db;
|
|
9
10
|
/** Every query below is a fixed string, so each is compiled once. */
|
|
@@ -12,9 +13,11 @@ export class TaskStore {
|
|
|
12
13
|
this.db = db;
|
|
13
14
|
this.sql = statements(db);
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/** Commit related task records before publishing events or starting work. */
|
|
17
|
+
transact(work) {
|
|
18
|
+
return transact(this.db, work);
|
|
19
|
+
}
|
|
20
|
+
// The only readers of the JSON columns, and where a schema change normalizes old rows.
|
|
18
21
|
#one(sql, ...params) {
|
|
19
22
|
const row = this.sql(sql).get(...params);
|
|
20
23
|
return row ? JSON.parse(row.json) : undefined;
|
|
@@ -35,9 +38,7 @@ export class TaskStore {
|
|
|
35
38
|
ON CONFLICT(id) DO UPDATE SET updated_at=excluded.updated_at, json=excluded.json
|
|
36
39
|
`).run(task.id, task.updatedAt, JSON.stringify(task));
|
|
37
40
|
}
|
|
38
|
-
/**
|
|
39
|
-
* document: a task with no next run (disabled, archived, manual) is not in
|
|
40
|
-
* it, so an idle second reads no row. */
|
|
41
|
+
/** Asked of the index, not every document: an idle second reads no row. */
|
|
41
42
|
listDueTasks(now) {
|
|
42
43
|
return this.#many("SELECT json FROM tasks WHERE next_run_at IS NOT NULL AND next_run_at <= ?", now);
|
|
43
44
|
}
|
|
@@ -58,9 +59,28 @@ export class TaskStore {
|
|
|
58
59
|
state=excluded.state, callback_state=excluded.callback_state, json=excluded.json
|
|
59
60
|
`).run(run.id, run.taskId, run.queuedAt, run.state, run.callbackState, JSON.stringify(run));
|
|
60
61
|
}
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
62
|
+
/** A watch at the five-second floor mints 17k rows a day, ~1.6 kB each, that
|
|
63
|
+
* no list shows; only rows nothing can dangle from are dropped — a child
|
|
64
|
+
* run is named in its parent's stored result. */
|
|
65
|
+
pruneUnmatchedProbes(taskId) {
|
|
66
|
+
this.sql(`
|
|
67
|
+
DELETE FROM task_runs WHERE id IN (
|
|
68
|
+
SELECT r.id FROM task_runs r
|
|
69
|
+
WHERE r.task_id = ? AND r.state = 'succeeded' AND json_extract(r.json, '$.matched') IS 0
|
|
70
|
+
AND r.callback_state IS NULL AND json_extract(r.json, '$.groupId') IS NULL
|
|
71
|
+
AND json_extract(r.json, '$.parentRunId') IS NULL
|
|
72
|
+
AND NOT EXISTS (SELECT 1 FROM task_messages m WHERE m.run_id = r.id)
|
|
73
|
+
-- A NULL in this list would make NOT IN unknown for every candidate.
|
|
74
|
+
AND r.id NOT IN (
|
|
75
|
+
SELECT json_extract(c.json, '$.resumedFromRunId') FROM task_runs c
|
|
76
|
+
WHERE c.task_id = ? AND json_extract(c.json, '$.resumedFromRunId') IS NOT NULL
|
|
77
|
+
)
|
|
78
|
+
ORDER BY r.queued_at DESC, r.id DESC LIMIT -1 OFFSET ?
|
|
79
|
+
)
|
|
80
|
+
`).run(taskId, taskId, KEPT_PROBES);
|
|
81
|
+
}
|
|
82
|
+
/** The id breaks timestamp ties so a page boundary never repeats or skips a
|
|
83
|
+
* row. Built per call, not cached: the filter set makes it. */
|
|
64
84
|
queryRuns(query = {}) {
|
|
65
85
|
const where = [];
|
|
66
86
|
const params = [];
|
|
@@ -101,8 +121,18 @@ export class TaskStore {
|
|
|
101
121
|
nextCursor: rows.length > limit && last ? { queuedAt: last.queuedAt, id: last.id } : null,
|
|
102
122
|
};
|
|
103
123
|
}
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
/** Activity never limits live work; only its optional history is bounded. */
|
|
125
|
+
activityRuns(since) {
|
|
126
|
+
const active = this.#many("SELECT json FROM task_runs WHERE state IN ('queued', 'running') ORDER BY queued_at DESC, id DESC");
|
|
127
|
+
if (since === undefined)
|
|
128
|
+
return active;
|
|
129
|
+
const recent = this.#many(`
|
|
130
|
+
SELECT json FROM task_runs
|
|
131
|
+
WHERE state NOT IN ('queued', 'running') AND queued_at >= ?
|
|
132
|
+
AND NOT (state = 'succeeded' AND json_extract(json, '$.matched') IS 0)
|
|
133
|
+
ORDER BY queued_at DESC, id DESC LIMIT 200
|
|
134
|
+
`, since);
|
|
135
|
+
return [...active, ...recent].sort((a, b) => b.queuedAt - a.queuedAt || b.id.localeCompare(a.id));
|
|
106
136
|
}
|
|
107
137
|
listRunsByRoot(rootRunId, limit = 100) {
|
|
108
138
|
return this.#many(`
|
|
@@ -129,9 +159,7 @@ export class TaskStore {
|
|
|
129
159
|
ORDER BY CASE state WHEN 'running' THEN 0 ELSE 1 END, queued_at DESC LIMIT 1
|
|
130
160
|
`, sessionId);
|
|
131
161
|
}
|
|
132
|
-
/**
|
|
133
|
-
* draws one dot per row: the state column narrows the scan to the handful
|
|
134
|
-
* of live runs, and no row's JSON is parsed. */
|
|
162
|
+
/** The state column narrows the scan to live runs; no JSON is parsed. */
|
|
135
163
|
countActiveBackgroundRunsBySession() {
|
|
136
164
|
const rows = this.sql(`
|
|
137
165
|
SELECT json_extract(json, '$.invokedBySessionId') AS session_id, COUNT(*) AS n
|
|
@@ -143,10 +171,8 @@ export class TaskStore {
|
|
|
143
171
|
`).all();
|
|
144
172
|
return new Map(rows.map((row) => [row.session_id, row.n]));
|
|
145
173
|
}
|
|
146
|
-
/**
|
|
147
|
-
*
|
|
148
|
-
* session its `fresh` predecessor already stamped. These are the agent's
|
|
149
|
-
* conversations with itself, which the rail does not list. */
|
|
174
|
+
/** `fresh` is the one mode that makes a session rather than borrowing one;
|
|
175
|
+
* these are the agent's conversations with itself, which the rail does not list. */
|
|
150
176
|
taskOwnedSessionIds() {
|
|
151
177
|
const rows = this.sql(`
|
|
152
178
|
SELECT DISTINCT json_extract(json, '$.context.sessionId') AS id
|
|
@@ -200,10 +226,8 @@ export class TaskStore {
|
|
|
200
226
|
listRecentMessages(since) {
|
|
201
227
|
return this.#many("SELECT json FROM task_messages WHERE created_at >= ? ORDER BY created_at DESC LIMIT 200", since);
|
|
202
228
|
}
|
|
203
|
-
/**
|
|
204
|
-
*
|
|
205
|
-
* sweep cost a query per undelivered message. A message whose run is gone
|
|
206
|
-
* comes back with `run: undefined` — the sweep drops it. */
|
|
229
|
+
/** Each beside its run, so the sweep costs one query. A message whose run is
|
|
230
|
+
* gone comes back with `run: undefined`. */
|
|
207
231
|
listUndeliveredMessages() {
|
|
208
232
|
const rows = this.sql(`
|
|
209
233
|
SELECT m.json AS json, r.json AS run_json
|
|
@@ -223,9 +247,8 @@ export class TaskStore {
|
|
|
223
247
|
WHERE state = 'pending' AND json_extract(json, '$.kind') != 'decision'
|
|
224
248
|
`).map((message) => {
|
|
225
249
|
message.state = "expired";
|
|
226
|
-
// "Confirmed", not "completed": the
|
|
227
|
-
//
|
|
228
|
-
// cannot see, and the run it steered is interrupted by the same restart.
|
|
250
|
+
// "Confirmed", not "completed": the proof lives in a transcript this
|
|
251
|
+
// layer cannot see.
|
|
229
252
|
message.error = "Pier restarted before delivery could be confirmed";
|
|
230
253
|
this.saveMessage(message);
|
|
231
254
|
return message;
|