@timqi/pier 0.1.14 → 0.2.0
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 +44 -22
- package/dist/agent/config.js +2 -2
- package/dist/agent/events.js +32 -3
- package/dist/agent/packages.js +6 -10
- package/dist/agent/pi.js +111 -68
- package/dist/agent/roles.js +103 -0
- package/dist/agent/types.js +78 -0
- package/dist/boards/boards.js +42 -32
- package/dist/channels/chains.js +0 -4
- package/dist/channels/commands.js +7 -12
- package/dist/channels/config.js +27 -1
- package/dist/channels/control.js +5 -16
- package/dist/channels/conversations.js +0 -5
- package/dist/channels/lark-api.js +20 -26
- package/dist/channels/lark-outbound.js +16 -29
- package/dist/channels/lark-panel.js +1 -1
- package/dist/channels/lark.js +61 -39
- package/dist/channels/lines.js +33 -1
- package/dist/channels/panel.js +17 -107
- package/dist/channels/routes.js +11 -21
- package/dist/channels/runtime.js +34 -25
- package/dist/channels/slack-outbound.js +6 -15
- package/dist/channels/slack-panel.js +1 -1
- package/dist/channels/slack-render.js +1 -1
- package/dist/channels/slack.js +63 -57
- package/dist/channels/types.js +1 -1
- package/dist/config-sync.js +42 -37
- package/dist/core/chain.js +202 -0
- package/dist/core/identity.js +34 -11
- package/dist/core/inbound-file.js +4 -3
- package/dist/core/reply.js +96 -80
- package/dist/core/router.js +111 -118
- package/dist/core/types.js +36 -77
- package/dist/db.js +33 -2
- package/dist/main.js +46 -37
- package/dist/settings.js +25 -18
- package/dist/tasks/agent.js +11 -5
- package/dist/tasks/callbacks.js +55 -5
- package/dist/tasks/cli.js +22 -11
- package/dist/tasks/definitions.js +30 -17
- package/dist/tasks/execution.js +2 -2
- package/dist/tasks/groups.js +5 -2
- package/dist/tasks/messages.js +7 -7
- package/dist/tasks/open-items.js +70 -0
- package/dist/tasks/operations.js +76 -39
- package/dist/tasks/outbox.js +19 -6
- package/dist/tasks/routes.js +19 -249
- package/dist/tasks/service.js +145 -34
- package/dist/tasks/store.js +119 -56
- package/dist/tasks/types.js +5 -0
- package/dist/tools.js +2 -2
- package/dist/web/auth.js +2 -2
- package/dist/web/config-sync.js +4 -5
- package/dist/web/config.js +9 -2
- package/dist/web/explorer.js +2 -2
- package/dist/web/fs.js +16 -1
- package/dist/web/instance.js +8 -5
- package/dist/web/packages.js +1 -1
- package/dist/web/passkeys.js +3 -3
- package/dist/web/providers.js +3 -3
- package/dist/web/public/assets/code-BZwvPani.js +2 -0
- package/dist/web/public/assets/code-BZwvPani.js.br +0 -0
- package/dist/web/public/assets/code-BZwvPani.js.gz +0 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js +5 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js.br +0 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js.gz +0 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js +72 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js.br +0 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js.gz +0 -0
- package/dist/web/public/assets/{hljs-tWqyD59G.js → hljs-BRz6a8Dq.js} +2 -2
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.br +0 -0
- package/dist/web/public/assets/hljs-BRz6a8Dq.js.gz +0 -0
- package/dist/web/public/assets/index-BpFzCwlo.js +7 -0
- package/dist/web/public/assets/index-BpFzCwlo.js.br +0 -0
- package/dist/web/public/assets/index-BpFzCwlo.js.gz +0 -0
- package/dist/web/public/assets/index-wAGxFFPj.css +2 -0
- package/dist/web/public/assets/index-wAGxFFPj.css.br +0 -0
- package/dist/web/public/assets/index-wAGxFFPj.css.gz +0 -0
- package/dist/web/public/assets/report-zjE8Bt8S.js +8 -0
- package/dist/web/public/assets/report-zjE8Bt8S.js.br +0 -0
- package/dist/web/public/assets/report-zjE8Bt8S.js.gz +0 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js +9 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js.br +0 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js.gz +0 -0
- package/dist/web/public/index.html +40 -109
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/public/manifest.webmanifest +0 -2
- package/dist/web/push.js +1 -1
- package/dist/web/server.js +91 -116
- package/dist/web/session-state.js +5 -40
- package/docs/deploy.md +5 -3
- package/package.json +2 -1
- package/skills/pier-boards/SKILL.md +14 -4
- package/skills/pier-help/SKILL.md +57 -23
- package/skills/pier-tasks/SKILL.md +58 -18
- package/dist/channels/handoff.js +0 -94
- package/dist/web/public/assets/activity-Ds6fCHnb.js +0 -5
- package/dist/web/public/assets/activity-Ds6fCHnb.js.br +0 -0
- package/dist/web/public/assets/activity-Ds6fCHnb.js.gz +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js +0 -1
- package/dist/web/public/assets/boards-BKCj6EwK.js.br +0 -0
- package/dist/web/public/assets/boards-BKCj6EwK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js +0 -4
- package/dist/web/public/assets/explorer-DnTm975c.js.br +0 -0
- package/dist/web/public/assets/explorer-DnTm975c.js.gz +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.br +0 -0
- package/dist/web/public/assets/hljs-tWqyD59G.js.gz +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js +0 -85
- package/dist/web/public/assets/index-BQo-haPN.js.br +0 -0
- package/dist/web/public/assets/index-BQo-haPN.js.gz +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css +0 -2
- package/dist/web/public/assets/index-DiHj0w1i.css.br +0 -0
- package/dist/web/public/assets/index-DiHj0w1i.css.gz +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js +0 -1
- package/dist/web/public/assets/runs-C_AthWcW.js.br +0 -0
- package/dist/web/public/assets/runs-C_AthWcW.js.gz +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js +0 -5
- package/dist/web/public/assets/settings-DDaAtFlc.js.br +0 -0
- package/dist/web/public/assets/settings-DDaAtFlc.js.gz +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js +0 -3
- package/dist/web/public/assets/task-runs-0pvdITiV.js.br +0 -0
- package/dist/web/public/assets/task-runs-0pvdITiV.js.gz +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js +0 -4
- package/dist/web/public/assets/tasks-Bz29caHJ.js.br +0 -0
- package/dist/web/public/assets/tasks-Bz29caHJ.js.gz +0 -0
package/dist/main.js
CHANGED
|
@@ -15,18 +15,19 @@ import { defaultBoardsDir, registerBoardRoutes, rotateBoardViews } from "./board
|
|
|
15
15
|
import { ChannelStore } from "./channels/config.js";
|
|
16
16
|
import { createControl } from "./channels/control.js";
|
|
17
17
|
import { ConversationStore, resolveConversation } from "./channels/conversations.js";
|
|
18
|
-
import { createHandoff } from "./channels/handoff.js";
|
|
19
18
|
import { registerChannelRoutes } from "./channels/routes.js";
|
|
20
19
|
import { ChannelRuntime } from "./channels/runtime.js";
|
|
20
|
+
import { MainChain } from "./core/chain.js";
|
|
21
21
|
import { EventHub } from "./core/hub.js";
|
|
22
22
|
import { splitSpeaker } from "./core/identity.js";
|
|
23
23
|
import { pierDb } from "./db.js";
|
|
24
24
|
import { deliverLedger, drainForRestart, RestartLedger } from "./drain.js";
|
|
25
|
-
import { surfacePrompt } from "./
|
|
25
|
+
import { surfacePrompt } from "./agent/roles.js";
|
|
26
26
|
import { Router } from "./core/router.js";
|
|
27
27
|
import { acquireInstanceLock } from "./lock.js";
|
|
28
28
|
import { parseWebParams, runWeb } from "./websearch/run.js";
|
|
29
29
|
import { logger } from "./log.js";
|
|
30
|
+
import { openItemsStatus } from "./tasks/open-items.js";
|
|
30
31
|
import { registerTaskRoutes } from "./tasks/routes.js";
|
|
31
32
|
import { TaskService } from "./tasks/service.js";
|
|
32
33
|
import { TaskStore } from "./tasks/store.js";
|
|
@@ -92,6 +93,7 @@ const configSync = new ConfigSync({
|
|
|
92
93
|
reload: () => readyForConfigReload ? reloadInstance() : Promise.resolve(),
|
|
93
94
|
});
|
|
94
95
|
const skillsDir = fileURLToPath(new URL("../skills", import.meta.url));
|
|
96
|
+
const taskStore = new TaskStore(db);
|
|
95
97
|
const factory = new PiAgentFactory(
|
|
96
98
|
// Getters, read per session open: a Console change reaches the next session
|
|
97
99
|
// without a restart.
|
|
@@ -99,7 +101,7 @@ const factory = new PiAgentFactory(
|
|
|
99
101
|
// Documents Pier's own tools, so it loads only inside a Pier session.
|
|
100
102
|
[skillsDir], new CredentialStore(db, secrets), piConfig, () => settings.get().modelMenu, () => settings.get(), () => settings.get().titleModel,
|
|
101
103
|
// Transcripts carry the speaker header core wrote for the model.
|
|
102
|
-
new IndexedListing(undefined, undefined, (text) => splitSpeaker(text).text));
|
|
104
|
+
new IndexedListing(undefined, undefined, (text) => splitSpeaker(text).text), (id) => taskStore.roleOf(id));
|
|
103
105
|
const hub = new EventHub();
|
|
104
106
|
const router = new Router(hub, (key) => {
|
|
105
107
|
// Web conversation ids are session ids; an IM id is a chat, resolved through
|
|
@@ -108,11 +110,28 @@ const router = new Router(hub, (key) => {
|
|
|
108
110
|
return factory.resume(key.conversationId);
|
|
109
111
|
}
|
|
110
112
|
return resolveIm(key);
|
|
111
|
-
}, (key) => conversations.get(key), (id) =>
|
|
113
|
+
}, (key) => conversations.get(key), (id) => chatKeyOf(id));
|
|
114
|
+
const chain = new MainChain(db, {
|
|
115
|
+
factory, router, home: pierPath("home"),
|
|
116
|
+
ledger: (ids, since) => tasks.ledger(ids, since),
|
|
117
|
+
status: (now) => openItemsStatus(tasks.openItems(now), now),
|
|
118
|
+
defaults: () => piConfig.readDefaults(),
|
|
119
|
+
});
|
|
112
120
|
const stopEviction = router.startIdleEviction();
|
|
113
|
-
tasks = new TaskService(
|
|
121
|
+
tasks = new TaskService(taskStore, factory, router, hub, {
|
|
114
122
|
modelMenu: () => settings.get().modelMenu,
|
|
115
|
-
systemActions: {
|
|
123
|
+
systemActions: {
|
|
124
|
+
// A schema gap pauses the subscription mid-run; the task's own switch follows it.
|
|
125
|
+
"config-sync": async (signal) => {
|
|
126
|
+
try {
|
|
127
|
+
return await configSync.sync(signal);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await configurationSync.reconcile();
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
continuous: chain,
|
|
116
135
|
});
|
|
117
136
|
const configurationSync = configSyncTask(tasks, configSync);
|
|
118
137
|
const managedTools = new ManagedTools();
|
|
@@ -130,24 +149,17 @@ const control = createControl({
|
|
|
130
149
|
router, factory, conversations, store: channelStore,
|
|
131
150
|
modelMenu: () => settings.get().modelMenu,
|
|
132
151
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
hub,
|
|
145
|
-
publicUrl: () => settings.get().publicUrl,
|
|
146
|
-
taskSessions: () => tasks.taskSessions(),
|
|
147
|
-
workingSet: () => sessionState.flags(),
|
|
148
|
-
log: (m) => logger("channels").info(m),
|
|
149
|
-
});
|
|
150
|
-
const channels = new ChannelRuntime(channelStore, router, control, handoff);
|
|
152
|
+
const channels = new ChannelRuntime(channelStore, router, chain, control);
|
|
153
|
+
/** The head answers in the home chat while its adapter runs (docs/design/11), else on
|
|
154
|
+
* the web with Web Push; an older member gets no home key, or an adapter's start
|
|
155
|
+
* would re-key it over the head. Any other session: its conversations row. */
|
|
156
|
+
function chatKeyOf(sessionId) {
|
|
157
|
+
const home = channelStore.home();
|
|
158
|
+
if (home && channels.live(home.platform) && chain.chainOf(sessionId)?.[0] === sessionId) {
|
|
159
|
+
return { channelId: home.platform, conversationId: home.chatId };
|
|
160
|
+
}
|
|
161
|
+
return conversations.keyOf(sessionId);
|
|
162
|
+
}
|
|
151
163
|
resolveIm = resolveConversation(conversations, factory, control.launchFor, (key, message) => {
|
|
152
164
|
logger("channels").warn(`${key.channelId}:${key.conversationId} ${message}`);
|
|
153
165
|
void channels.notify(key.channelId, key.conversationId, message)
|
|
@@ -169,16 +181,8 @@ const reloadInstance = async (includeWatched = false) => {
|
|
|
169
181
|
await channels.reload();
|
|
170
182
|
return router.evictIdle(0, Date.now(), { includeWatched });
|
|
171
183
|
};
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
// sync() already logged the cause; this line says what the boot did about it.
|
|
175
|
-
try {
|
|
176
|
-
await configSync.sync();
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
log.error("Startup configuration sync failed; using the last local configuration");
|
|
180
|
-
}
|
|
181
|
-
}
|
|
184
|
+
// No sync at boot: a restart is not a reason to replace the configuration;
|
|
185
|
+
// the owned task's next check is.
|
|
182
186
|
await configurationSync.reconcile();
|
|
183
187
|
tasks.start();
|
|
184
188
|
readyForConfigReload = true;
|
|
@@ -278,8 +282,8 @@ registerConfigSyncRoutes(app, {
|
|
|
278
282
|
reconcile: configurationSync.reconcile,
|
|
279
283
|
run: configurationSync.run,
|
|
280
284
|
});
|
|
281
|
-
registerTaskRoutes(app, tasks
|
|
282
|
-
registerChannelRoutes(app, channelStore, channels,
|
|
285
|
+
registerTaskRoutes(app, tasks);
|
|
286
|
+
registerChannelRoutes(app, channelStore, channels, conversations);
|
|
283
287
|
registerVaultRoutes(app, { vault, doctor: () => secrets.doctor() });
|
|
284
288
|
registerBoardRoutes(app);
|
|
285
289
|
registerPushRoutes(app, {
|
|
@@ -299,6 +303,7 @@ app.route("/", createServer({
|
|
|
299
303
|
packages,
|
|
300
304
|
providers: factory,
|
|
301
305
|
settings,
|
|
306
|
+
subscribed: () => configSync.status().enabled,
|
|
302
307
|
// Assembled here: the catalog spawns ubix, which web/ may not.
|
|
303
308
|
catalog: async () => {
|
|
304
309
|
const { tools, customTools } = settings.get();
|
|
@@ -320,8 +325,12 @@ app.route("/", createServer({
|
|
|
320
325
|
reload: () => reloadInstance(true),
|
|
321
326
|
backgroundRuns: (id) => tasks.backgroundRuns(id),
|
|
322
327
|
activeBackgroundRunCounts: () => tasks.activeBackgroundRunCounts(),
|
|
328
|
+
parkedMessages: (id) => tasks.parkedMessages(id),
|
|
323
329
|
taskSessions: () => tasks.taskSessions(),
|
|
324
|
-
|
|
330
|
+
leads: () => taskStore.leads(),
|
|
331
|
+
openItems: () => tasks.openItems(),
|
|
332
|
+
channelOf: (id) => chatKeyOf(id)?.channelId,
|
|
333
|
+
continuous: chain,
|
|
325
334
|
}));
|
|
326
335
|
const port = Number(process.env.PORT ?? 3141);
|
|
327
336
|
const hostname = process.env.HOST ?? "127.0.0.1";
|
package/dist/settings.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Instance settings: the facts about *this* Pier that are neither a credential
|
|
2
2
|
// nor per-session. A key-value table, so the next setting is not the next table.
|
|
3
|
-
import { isThinkingLevel } from "./core/types.js";
|
|
3
|
+
import { isModelTier, isThinkingLevel, MODEL_TIERS, THINKING_LEVELS } from "./core/types.js";
|
|
4
4
|
import { pierDb, transact } from "./db.js";
|
|
5
5
|
import { logger } from "./log.js";
|
|
6
6
|
import { normalizeCustomTools } from "./tools.js";
|
|
@@ -48,29 +48,33 @@ export function normalizePublicUrl(raw) {
|
|
|
48
48
|
return `${url.origin}${url.pathname}`.replace(/\/+$/, "");
|
|
49
49
|
}
|
|
50
50
|
/** Rejecting rather than repairing: a "fixed" entry would advertise a model the
|
|
51
|
-
* operator never picked.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
* operator never picked. The refusal names the row (1-based, as the Console
|
|
52
|
+
* lists them) and the field. A `note` left by an older Pier is dropped. One
|
|
53
|
+
* model may be pinned at several levels; the same model at the same level twice
|
|
54
|
+
* is refused, naming both rows. */
|
|
55
|
+
export function parseModelMenu(raw) {
|
|
56
|
+
if (!Array.isArray(raw))
|
|
57
|
+
return "modelMenu must be a list";
|
|
58
|
+
if (raw.length > 32)
|
|
59
|
+
return `modelMenu has ${String(raw.length)} rows; at most 32`;
|
|
55
60
|
const menu = [];
|
|
56
|
-
for (const item of raw) {
|
|
61
|
+
for (const [i, item] of raw.entries()) {
|
|
62
|
+
const row = `modelMenu row ${String(i + 1)}`;
|
|
57
63
|
const ref = normalizeModelRef(item);
|
|
58
64
|
if (!ref)
|
|
59
|
-
return
|
|
60
|
-
const { thinking,
|
|
65
|
+
return `${row}: provider and id must be non-empty strings`;
|
|
66
|
+
const { thinking, tier } = item;
|
|
61
67
|
// Repaired, not rejected: rows stored before the level was required have
|
|
62
68
|
// none, and dropping the menu over it would lose the pins.
|
|
63
69
|
const level = thinking === undefined ? "medium" : thinking;
|
|
64
70
|
if (!isThinkingLevel(level))
|
|
65
|
-
return
|
|
66
|
-
if (
|
|
67
|
-
return
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
...(cleaned ? { note: cleaned } : {}),
|
|
73
|
-
});
|
|
71
|
+
return `${row} (${ref.provider}/${ref.id}): thinking must be one of ${THINKING_LEVELS.join(", ")}`;
|
|
72
|
+
if (tier !== undefined && !isModelTier(tier))
|
|
73
|
+
return `${row} (${ref.provider}/${ref.id}): tier must be one of ${MODEL_TIERS.join(", ")}, or none`;
|
|
74
|
+
const twin = menu.findIndex((e) => e.provider === ref.provider && e.id === ref.id && e.thinking === level);
|
|
75
|
+
if (twin >= 0)
|
|
76
|
+
return `${row} (${ref.provider}/${ref.id}): already pinned at ${level} by row ${String(twin + 1)}`;
|
|
77
|
+
menu.push({ ...ref, thinking: level, ...(tier !== undefined ? { tier } : {}) });
|
|
74
78
|
}
|
|
75
79
|
return menu;
|
|
76
80
|
}
|
|
@@ -117,7 +121,10 @@ export class SettingsStore {
|
|
|
117
121
|
log.warn("settings.accent is not a preset — ignoring it");
|
|
118
122
|
return {
|
|
119
123
|
publicUrl: this.#value("publicUrl") ?? "",
|
|
120
|
-
modelMenu: this.#json("modelMenu",
|
|
124
|
+
modelMenu: this.#json("modelMenu", (raw) => {
|
|
125
|
+
const menu = parseModelMenu(raw);
|
|
126
|
+
return typeof menu === "string" ? null : menu;
|
|
127
|
+
}, "a valid menu") ?? [],
|
|
121
128
|
...(titleModel ? { titleModel } : {}),
|
|
122
129
|
autoUpdate: this.#value("autoUpdate") === "1",
|
|
123
130
|
skillsOff: this.#json("skillsOff", normalizeNames, "a list of names") ?? [],
|
package/dist/tasks/agent.js
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
import { quietLabel, splitReply } from "../core/reply.js";
|
|
5
5
|
import { logger } from "../log.js";
|
|
6
6
|
import { runSource } from "./callbacks.js";
|
|
7
|
+
import { createdRole } from "./types.js";
|
|
7
8
|
// Agent runs are I/O-bound: the cap is there for API pressure and runaway
|
|
8
9
|
// fan-out, not for this machine's CPU.
|
|
9
10
|
const MAX_ACTIVE_AGENTS = 6;
|
|
10
11
|
const log = logger("tasks");
|
|
11
12
|
/** Every session gets the chat-surface contract, task runs included, so the
|
|
12
13
|
* delegation prompt says which of it does not apply. Skipped on resume. */
|
|
13
|
-
const preamble = (run, supervised) => {
|
|
14
|
+
const preamble = (run, supervised, role) => {
|
|
14
15
|
// A cron/watch task with a session callback is read by an agent too.
|
|
15
16
|
const audience = run.invokedBySessionId
|
|
16
17
|
? "read by the agent that delegated this run"
|
|
@@ -19,8 +20,10 @@ const preamble = (run, supervised) => {
|
|
|
19
20
|
: "read by the operator";
|
|
20
21
|
return `[Pier task run ${run.id} — "${run.context.definition.name}"] ` +
|
|
21
22
|
`Your final reply is recorded verbatim as the run result, ${audience}; ` +
|
|
22
|
-
`next-step buttons and file:// attachments do not render there. A question only that reader can answer is your result: state it and end your turn; the answer resumes this session.` +
|
|
23
|
-
(
|
|
23
|
+
`next-step buttons and file:// attachments do not render there. Return the conclusion and the paths it rests on — no process, no log of attempts; a deliverable longer than a screen goes to a file the result names. A question only that reader can answer is your result: state it and end your turn; the answer resumes this session.` +
|
|
24
|
+
(role === "lead"
|
|
25
|
+
? " You are a feature lead: you may delegate to workers with `pier task run` (never `--role lead`), and their results come back to you."
|
|
26
|
+
: supervised || role === "worker" ? " You cannot delegate from here — `pier task` is refused; if the work needs another agent, say so in your result and your supervisor will run it." : "") +
|
|
24
27
|
"\n\n";
|
|
25
28
|
};
|
|
26
29
|
export class AgentTaskRunner {
|
|
@@ -61,7 +64,8 @@ export class AgentTaskRunner {
|
|
|
61
64
|
const input = run.input === undefined || run.input === null
|
|
62
65
|
? ""
|
|
63
66
|
: `\n\n<task_input>\n${JSON.stringify(run.input).replaceAll("</task_input>", "<\\/task_input>")}\n</task_input>`;
|
|
64
|
-
const prompt = run.context.resumePrompt ??
|
|
67
|
+
const prompt = run.context.resumePrompt ??
|
|
68
|
+
`${preamble(run, this.store.supervised(run), this.store.roleOf(session.id))}${action.prompt}${input}`;
|
|
65
69
|
run.context.sessionId = session.id;
|
|
66
70
|
run.context.model = session.model;
|
|
67
71
|
// The level the session settled on: an unspecified effort inherits the caller's.
|
|
@@ -144,12 +148,14 @@ export class AgentTaskRunner {
|
|
|
144
148
|
throw new Error("could not resolve child working directory");
|
|
145
149
|
const opts = {
|
|
146
150
|
cwd,
|
|
147
|
-
|
|
151
|
+
// The session list's title: the caller's `--name`, else the prompt's first line.
|
|
152
|
+
name: run.context.definition.name,
|
|
148
153
|
// Unspecified model inherits the caller's live model, not the global
|
|
149
154
|
// default; falls back to the default when the caller isn't attached.
|
|
150
155
|
model: action.launch?.model ??
|
|
151
156
|
(run.sourceSessionId ? this.router.modelOf(run.sourceSessionId) : undefined),
|
|
152
157
|
thinking: action.launch?.thinking,
|
|
158
|
+
role: createdRole(run),
|
|
153
159
|
};
|
|
154
160
|
const opening = this.factory.create(opts).then(async (session) => {
|
|
155
161
|
// SDK creation cannot be cancelled; a late session still belongs to this
|
package/dist/tasks/callbacks.js
CHANGED
|
@@ -10,6 +10,45 @@ export const runSource = (run) => ({
|
|
|
10
10
|
...(run.context.model ? { model: run.context.model } : {}),
|
|
11
11
|
...(run.context.thinking ? { thinking: run.context.thinking } : {}),
|
|
12
12
|
});
|
|
13
|
+
/** The directory a run worked in: the one its session opened on, else the one its action names. */
|
|
14
|
+
export const runCwd = (run) => {
|
|
15
|
+
const action = run.context.definition.action;
|
|
16
|
+
return run.context.cwd ??
|
|
17
|
+
(action.type === "bash" ? action.cwd : action.type === "agent" && action.session.mode === "fresh" ? action.session.cwd : null);
|
|
18
|
+
};
|
|
19
|
+
/** What a callback's card says about the one run it carries. */
|
|
20
|
+
const oneRun = (run) => {
|
|
21
|
+
const cwd = runCwd(run);
|
|
22
|
+
return { source: runSource(run), state: run.state, ...(cwd ? { cwd } : {}) };
|
|
23
|
+
};
|
|
24
|
+
/** Heads a milestone resume's prompt: the lead's reply is what its supervisor reads. */
|
|
25
|
+
export const MILESTONE = "[Pier: the last result you were waiting on follows; nothing owed to you is still running. Your reply is the milestone your supervisor reads: what is done, what is next, any decision you need.]";
|
|
26
|
+
/** The line a design lead ends on once the user confirms: its milestone, whether a run's or a turn outside any. */
|
|
27
|
+
export const DESIGN_FINAL = /^Design final:/m;
|
|
28
|
+
/** On the record of a lead run that owed its supervisor nothing, so the run says why. */
|
|
29
|
+
export const LEAD_TURN = "a lead's turn, not a milestone";
|
|
30
|
+
/** Decided once, as the run finishes. A lead reports milestones only: any
|
|
31
|
+
* other turn of its the user reads in its session, and settles as `--callback none`.
|
|
32
|
+
* A build lead's turn that leaves nothing coming to it is its milestone: it
|
|
33
|
+
* declares its own completion, where only the user finalizes a design.
|
|
34
|
+
* A turn that did not succeed is not a turn the user read: it calls back. */
|
|
35
|
+
export function settleCallback(run, store) {
|
|
36
|
+
if (!run.callbackSessionId)
|
|
37
|
+
return;
|
|
38
|
+
// A watch probe that did not match is the interval passing, not a result.
|
|
39
|
+
if (run.matched === false && run.state === "succeeded")
|
|
40
|
+
return;
|
|
41
|
+
// The session's creator fixes its role: a `--session` or `--run` turn on a lead's session is a lead's.
|
|
42
|
+
const lead = run.targetSessionId;
|
|
43
|
+
const phase = lead === null ? undefined : store.leadPhaseOf(lead);
|
|
44
|
+
const milestone = () => run.context.resumePrompt?.startsWith(MILESTONE) === true ||
|
|
45
|
+
(run.result?.type === "agent" && DESIGN_FINAL.test(run.result.text)) ||
|
|
46
|
+
(phase === "build" && lead !== null && !store.awaitsResults(lead));
|
|
47
|
+
if (phase !== undefined && run.state === "succeeded" && !milestone())
|
|
48
|
+
run.callbackError = LEAD_TURN;
|
|
49
|
+
else
|
|
50
|
+
run.callbackState = "pending";
|
|
51
|
+
}
|
|
13
52
|
export function runResultText(run) {
|
|
14
53
|
let result = run.error ?? "No result";
|
|
15
54
|
if (run.result?.type === "agent" || run.result?.type === "system")
|
|
@@ -26,9 +65,17 @@ export function runResultText(run) {
|
|
|
26
65
|
}
|
|
27
66
|
export class TaskCallbacks {
|
|
28
67
|
store;
|
|
68
|
+
headOf;
|
|
69
|
+
conversation;
|
|
29
70
|
outbox;
|
|
30
|
-
constructor(store, router, changed, unreachable
|
|
71
|
+
constructor(store, router, changed, unreachable,
|
|
72
|
+
/** Where a result owed to a session goes now: the continuous conversation's head, for a member. */
|
|
73
|
+
headOf = (id) => id, milestone,
|
|
74
|
+
/** The continuous conversation's head, null before its first message. */
|
|
75
|
+
conversation = () => null) {
|
|
31
76
|
this.store = store;
|
|
77
|
+
this.headOf = headOf;
|
|
78
|
+
this.conversation = conversation;
|
|
32
79
|
this.outbox = new Outbox(router, {
|
|
33
80
|
id: (run) => run.id,
|
|
34
81
|
reload: (id) => this.store.getRun(id),
|
|
@@ -43,11 +90,12 @@ export class TaskCallbacks {
|
|
|
43
90
|
sourceSessionId: runs[0].targetSessionId,
|
|
44
91
|
runIds: runs.map((run) => run.id),
|
|
45
92
|
// Only for one run: a batch's caption would attribute every result
|
|
46
|
-
// to the first run's name and
|
|
47
|
-
...(runs.length === 1 ?
|
|
93
|
+
// to the first run's name, model and directory.
|
|
94
|
+
...(runs.length === 1 ? oneRun(runs[0]) : {}),
|
|
48
95
|
},
|
|
49
96
|
}),
|
|
50
97
|
abandoned: (run, sessionId, why) => unreachable(sessionId, `the result of "${run.context.definition.name}"`, why),
|
|
98
|
+
milestone,
|
|
51
99
|
});
|
|
52
100
|
}
|
|
53
101
|
target(callback, origin) {
|
|
@@ -55,6 +103,8 @@ export class TaskCallbacks {
|
|
|
55
103
|
return callback.sessionId;
|
|
56
104
|
if (callback.type === "origin")
|
|
57
105
|
return origin;
|
|
106
|
+
if (callback.type === "conversation")
|
|
107
|
+
return this.conversation();
|
|
58
108
|
return null;
|
|
59
109
|
}
|
|
60
110
|
recover(now = Date.now()) {
|
|
@@ -67,10 +117,10 @@ export class TaskCallbacks {
|
|
|
67
117
|
const first = this.store.getRun(candidate.id);
|
|
68
118
|
if (!first?.callbackSessionId || (first.callbackState !== "pending" && first.callbackState !== "failed"))
|
|
69
119
|
return;
|
|
70
|
-
const sessionId = first.callbackSessionId;
|
|
120
|
+
const sessionId = this.headOf(first.callbackSessionId);
|
|
71
121
|
// Once one callback is deliverable, everything pending for the session rides along.
|
|
72
122
|
const batch = this.store.listPendingCallbacks(Number.MAX_SAFE_INTEGER)
|
|
73
|
-
.filter((run) => run.callbackSessionId === sessionId);
|
|
123
|
+
.filter((run) => run.callbackSessionId !== null && this.headOf(run.callbackSessionId) === sessionId);
|
|
74
124
|
if (!batch.some((run) => run.id === first.id))
|
|
75
125
|
return;
|
|
76
126
|
await this.outbox.deliver(sessionId, batch);
|
package/dist/tasks/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ const OPTIONS = {
|
|
|
10
10
|
timeout: { type: "string" }, callback: { type: "string" }, "callback-session": { type: "string" }, join: { type: "string" },
|
|
11
11
|
bash: { type: "string" }, cron: { type: "string" }, tz: { type: "string" },
|
|
12
12
|
watch: { type: "string" }, every: { type: "string" }, repeat: { type: "boolean" },
|
|
13
|
-
group: { type: "string" }, reason: { type: "string" },
|
|
13
|
+
group: { type: "string" }, reason: { type: "string" }, role: { type: "string" }, design: { type: "boolean" },
|
|
14
14
|
help: { type: "boolean", short: "h" },
|
|
15
15
|
};
|
|
16
16
|
/** The usage line is the contract (docs/design/09-tasks-cli.md); the flags a
|
|
@@ -18,17 +18,21 @@ const OPTIONS = {
|
|
|
18
18
|
const COMMANDS = {
|
|
19
19
|
run: {
|
|
20
20
|
usage: "run [--prompt <text|-> | --bash <script>] [--run <id> [--after]] [--task-id <id>] [--session <id>]\n" +
|
|
21
|
-
" [--model <name|?>] [--thinking <level>] [--cwd <dir>] [--name <text>] [--timeout <seconds>]\n" +
|
|
21
|
+
" [--model <name|?>] [--thinking <level>] [--role lead [--design]] [--cwd <dir>] [--name <text>] [--timeout <seconds>]\n" +
|
|
22
22
|
" [--callback origin|none|steer] [--callback-session <id>] [--join all|first] [--member <flags…>]…",
|
|
23
23
|
help: "a new run (--prompt | --bash | --task-id | --session … --prompt), a batch (--member), or a prompt on an existing one (--run)",
|
|
24
24
|
},
|
|
25
25
|
save: {
|
|
26
26
|
usage: "save [--task-id <id>] --name <text> (--prompt <text|-> | --bash <script>)\n" +
|
|
27
27
|
" [--cron \"<expr>\" --tz <zone> | --watch <script> --every <seconds> [--repeat]]\n" +
|
|
28
|
-
" [--cwd <dir>] [--timeout <seconds>] [--model <name>] [--thinking <level>] [--callback-session <id>]",
|
|
29
|
-
help: "file a definition the operator sees, or update one by --task-id; no trigger means manual",
|
|
28
|
+
" [--cwd <dir>] [--timeout <seconds>] [--model <name>] [--thinking <level>] [--callback-session <id|none>]",
|
|
29
|
+
help: "file a definition the operator sees, or update one by --task-id; no trigger means manual; results reach the continuous conversation unless --callback-session",
|
|
30
30
|
},
|
|
31
|
-
list: { usage: "list", help: "stored definitions, as JSON" },
|
|
31
|
+
list: { usage: "list", help: "stored definitions with nextRun and lastRun, as JSON" },
|
|
32
|
+
pause: { usage: "pause --task-id <id>", help: "stop a definition's schedule; run --task-id still runs it" },
|
|
33
|
+
resume: { usage: "resume --task-id <id>", help: "restart a paused definition's schedule" },
|
|
34
|
+
archive: { usage: "archive --task-id <id>", help: "retire a definition for good" },
|
|
35
|
+
runs: { usage: "runs", help: "the runs you launched (in the continuous conversation, any of its sessions), in flight and finished in the last 24h, as JSON" },
|
|
32
36
|
cancel: { usage: "cancel (--run <id> | --group <id>)", help: "a run or a group, descendants included" },
|
|
33
37
|
recover: { usage: "recover (--run <id> | --group <id>) --reason <text>", help: "a finished result after its callback settled; never a progress check" },
|
|
34
38
|
};
|
|
@@ -58,7 +62,9 @@ const seconds = (flag, raw) => {
|
|
|
58
62
|
return Number.isInteger(n) ? n : refuse(`--${flag} must be a whole number of seconds`);
|
|
59
63
|
};
|
|
60
64
|
/** `launch` as the server takes it: `model` is a menu name it resolves. */
|
|
61
|
-
const launchOf = (v) => v.model === undefined && v.thinking === undefined
|
|
65
|
+
const launchOf = (v) => v.model === undefined && v.thinking === undefined && v.role === undefined && v.design === undefined
|
|
66
|
+
? undefined
|
|
67
|
+
: compact({ model: v.model, thinking: v.thinking, role: v.role, design: v.design });
|
|
62
68
|
/** Argv is split at each bare `--member`; a value equal to it is unreachable,
|
|
63
69
|
* since parseArgs (strict) refuses option-like values anyway. */
|
|
64
70
|
function segments(argv) {
|
|
@@ -125,8 +131,8 @@ function build(name, parsed, io) {
|
|
|
125
131
|
if (parsed.some((v) => v.model === "?"))
|
|
126
132
|
return { operation: "run", launch: { model: "?" } };
|
|
127
133
|
const [values, ...members] = parsed;
|
|
128
|
-
if (name === "list")
|
|
129
|
-
return { operation:
|
|
134
|
+
if (name === "list" || name === "runs")
|
|
135
|
+
return { operation: name };
|
|
130
136
|
if (name === "cancel" || name === "recover") {
|
|
131
137
|
if ((values.run === undefined) === (values.group === undefined))
|
|
132
138
|
refuse(`${name} takes exactly one of --run or --group`);
|
|
@@ -136,13 +142,16 @@ function build(name, parsed, io) {
|
|
|
136
142
|
}
|
|
137
143
|
if (name === "save")
|
|
138
144
|
return saveParams(values, text);
|
|
145
|
+
if (name === "pause" || name === "resume" || name === "archive") {
|
|
146
|
+
return { operation: name, task_id: values["task-id"] ?? refuse(`${name} needs --task-id`) };
|
|
147
|
+
}
|
|
139
148
|
/** One new run, in the three shapes `tasks[]` accepts. A saved
|
|
140
149
|
* definition runs as is: batch defaults pass it by, its own flags are refused. */
|
|
141
150
|
const entry = (v, own = v) => {
|
|
142
151
|
const launch = launchOf(v);
|
|
143
152
|
const timeoutSeconds = seconds("timeout", v.timeout);
|
|
144
153
|
if (v["task-id"] !== undefined) {
|
|
145
|
-
const extra = flagsOf(own).find((flag) => ["prompt", "bash", "session", "cwd", "model", "thinking", "name", "timeout"].includes(flag));
|
|
154
|
+
const extra = flagsOf(own).find((flag) => ["prompt", "bash", "session", "cwd", "model", "thinking", "role", "design", "name", "timeout"].includes(flag));
|
|
146
155
|
if (extra)
|
|
147
156
|
refuse(`--${extra} does not apply to a saved definition (--task-id)`);
|
|
148
157
|
return { task_id: v["task-id"] };
|
|
@@ -151,7 +160,7 @@ function build(name, parsed, io) {
|
|
|
151
160
|
if (v.prompt !== undefined)
|
|
152
161
|
refuse("a run takes exactly one of --prompt or --bash");
|
|
153
162
|
if (launch)
|
|
154
|
-
refuse("--model/--thinking apply to a prompt, not --bash");
|
|
163
|
+
refuse("--model/--thinking/--role/--design apply to a prompt, not --bash");
|
|
155
164
|
if (v.session !== undefined)
|
|
156
165
|
refuse("--session continues an agent session; --bash is a command, not a turn");
|
|
157
166
|
return compact({ name: v.name, timeoutSeconds, action: compact({ type: "bash", script: v.bash, cwd: v.cwd }) });
|
|
@@ -216,7 +225,9 @@ function saveParams(v, text) {
|
|
|
216
225
|
name: v.name,
|
|
217
226
|
timeoutSeconds: seconds("timeout", v.timeout),
|
|
218
227
|
trigger,
|
|
219
|
-
callback: v["callback-session"] === undefined
|
|
228
|
+
callback: v["callback-session"] === undefined
|
|
229
|
+
? { type: "conversation" }
|
|
230
|
+
: v["callback-session"] === "none" ? { type: "none" } : { type: "session", sessionId: v["callback-session"] },
|
|
220
231
|
...(v.bash === undefined
|
|
221
232
|
? { prompt: text(v.prompt), cwd: v.cwd, launch }
|
|
222
233
|
: { action: compact({ type: "bash", script: v.bash, cwd: v.cwd }) }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// What a task *is* before it ever runs: the id it is minted with, the draft
|
|
2
2
|
// validated into a definition, and when its trigger is next due. Every way a
|
|
3
|
-
// definition can be created —
|
|
3
|
+
// definition can be created — `pier task`, Pier's own owned task —
|
|
4
4
|
// arrives here, so a field is checked in one place or nowhere.
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { stat } from "node:fs/promises";
|
|
@@ -69,13 +69,27 @@ function parseTrigger(raw) {
|
|
|
69
69
|
}
|
|
70
70
|
/** Always computed from `from` (boot recomputes from *now*): cron runs missed
|
|
71
71
|
* while Pier was down are skipped, never caught up — no double fire. */
|
|
72
|
-
|
|
72
|
+
function nextRunAt(trigger, from) {
|
|
73
73
|
if (trigger.type === "manual")
|
|
74
74
|
return null;
|
|
75
75
|
if (trigger.type === "watch")
|
|
76
76
|
return from + trigger.intervalSeconds * 1000;
|
|
77
77
|
return new Cron(trigger.expression, { timezone: trigger.timezone }).nextRun(new Date(from))?.getTime() ?? null;
|
|
78
78
|
}
|
|
79
|
+
function parseModel(raw) {
|
|
80
|
+
const model = record(raw);
|
|
81
|
+
if (!model)
|
|
82
|
+
throw new Error("agent model must be an object");
|
|
83
|
+
return {
|
|
84
|
+
provider: requiredString(model.provider, "model provider"),
|
|
85
|
+
id: requiredString(model.id, "model id"),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function parseThinking(raw) {
|
|
89
|
+
if (!isThinkingLevel(raw))
|
|
90
|
+
throw new Error("invalid agent thinking level");
|
|
91
|
+
return raw;
|
|
92
|
+
}
|
|
79
93
|
function parseLaunch(raw) {
|
|
80
94
|
if (raw === undefined)
|
|
81
95
|
return undefined;
|
|
@@ -83,20 +97,19 @@ function parseLaunch(raw) {
|
|
|
83
97
|
if (!value)
|
|
84
98
|
throw new Error("agent launch policy must be an object");
|
|
85
99
|
const launch = {};
|
|
86
|
-
if (value.model !== undefined)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
if (value.model !== undefined)
|
|
101
|
+
launch.model = parseModel(value.model);
|
|
102
|
+
if (value.thinking !== undefined)
|
|
103
|
+
launch.thinking = parseThinking(value.thinking);
|
|
104
|
+
if (value.role !== undefined) {
|
|
105
|
+
if (value.role !== "lead")
|
|
106
|
+
throw new Error("agent role must be lead");
|
|
107
|
+
launch.role = "lead";
|
|
94
108
|
}
|
|
95
|
-
if (value.
|
|
96
|
-
if (
|
|
97
|
-
throw new Error("
|
|
98
|
-
|
|
99
|
-
launch.thinking = value.thinking;
|
|
109
|
+
if (value.design !== undefined) {
|
|
110
|
+
if (value.design !== true || launch.role !== "lead")
|
|
111
|
+
throw new Error("agent design must be true, on a lead");
|
|
112
|
+
launch.design = true;
|
|
100
113
|
}
|
|
101
114
|
return Object.keys(launch).length ? launch : undefined;
|
|
102
115
|
}
|
|
@@ -136,7 +149,7 @@ export class TaskDefinitions {
|
|
|
136
149
|
revision: 1,
|
|
137
150
|
trigger: draft.trigger,
|
|
138
151
|
action: draft.action,
|
|
139
|
-
callback: draft.callback ?? { type: "
|
|
152
|
+
callback: draft.callback ?? { type: "conversation" },
|
|
140
153
|
timeoutSeconds: draft.timeoutSeconds ?? DEFAULT_TIMEOUT,
|
|
141
154
|
nextRunAt: null,
|
|
142
155
|
creator,
|
|
@@ -321,7 +334,7 @@ export class TaskDefinitions {
|
|
|
321
334
|
if (raw === undefined)
|
|
322
335
|
return undefined;
|
|
323
336
|
const value = record(raw);
|
|
324
|
-
if (!value || (value.type !== "none" && value.type !== "origin" && value.type !== "session")) {
|
|
337
|
+
if (!value || (value.type !== "conversation" && value.type !== "none" && value.type !== "origin" && value.type !== "session")) {
|
|
325
338
|
throw new Error("invalid callback");
|
|
326
339
|
}
|
|
327
340
|
if (value.type !== "session")
|
package/dist/tasks/execution.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// One queued run carried to a result: dispatched by action kind, abortable,
|
|
2
2
|
// settled exactly once. The lifecycle command.ts and agent.ts share.
|
|
3
3
|
import { logger } from "../log.js";
|
|
4
|
+
import { settleCallback } from "./callbacks.js";
|
|
4
5
|
import { runBash } from "./command.js";
|
|
5
6
|
const log = logger("tasks");
|
|
6
7
|
export class TaskExecution {
|
|
@@ -92,8 +93,7 @@ export class TaskExecution {
|
|
|
92
93
|
log.debug(`${settled} (watch did not match)`);
|
|
93
94
|
else
|
|
94
95
|
log.info(settled);
|
|
95
|
-
|
|
96
|
-
run.callbackState = "pending";
|
|
96
|
+
settleCallback(run, this.store);
|
|
97
97
|
this.controllers.delete(run.id);
|
|
98
98
|
try {
|
|
99
99
|
this.store.saveRun(run);
|
package/dist/tasks/groups.js
CHANGED
|
@@ -13,12 +13,14 @@ export class TaskGroups {
|
|
|
13
13
|
host;
|
|
14
14
|
changed;
|
|
15
15
|
unreachable;
|
|
16
|
+
headOf;
|
|
16
17
|
outbox;
|
|
17
|
-
constructor(store, router, host, changed, unreachable) {
|
|
18
|
+
constructor(store, router, host, changed, unreachable, headOf = (id) => id, milestone) {
|
|
18
19
|
this.store = store;
|
|
19
20
|
this.host = host;
|
|
20
21
|
this.changed = changed;
|
|
21
22
|
this.unreachable = unreachable;
|
|
23
|
+
this.headOf = headOf;
|
|
22
24
|
this.outbox = new Outbox(router, {
|
|
23
25
|
id: (group) => group.id,
|
|
24
26
|
reload: (id) => this.store.getGroup(id),
|
|
@@ -31,6 +33,7 @@ export class TaskGroups {
|
|
|
31
33
|
origin: { kind: "task-callback", taskId: groups[0].id, runId: groups[0].id, sourceSessionId: null },
|
|
32
34
|
}),
|
|
33
35
|
abandoned: (group, sessionId, why) => unreachable(sessionId, `the result of a ${String(group.memberRunIds.length)}-run group`, why),
|
|
36
|
+
milestone,
|
|
34
37
|
});
|
|
35
38
|
}
|
|
36
39
|
/** Enqueues every member or none: a partially started group is worse than
|
|
@@ -141,7 +144,7 @@ export class TaskGroups {
|
|
|
141
144
|
const group = this.store.getGroup(candidate.id);
|
|
142
145
|
if (!group?.callbackSessionId || (group.callbackState !== "pending" && group.callbackState !== "failed"))
|
|
143
146
|
return;
|
|
144
|
-
await this.outbox.deliver(group.callbackSessionId, [group]);
|
|
147
|
+
await this.outbox.deliver(this.headOf(group.callbackSessionId), [group]);
|
|
145
148
|
}
|
|
146
149
|
text(group) {
|
|
147
150
|
const members = group.memberRunIds.map((id) => this.host.getRun(id));
|