@timqi/pier 0.1.14 → 0.3.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.
Files changed (130) hide show
  1. package/README.md +44 -22
  2. package/dist/agent/config.js +2 -2
  3. package/dist/agent/events.js +32 -3
  4. package/dist/agent/listing.js +14 -10
  5. package/dist/agent/packages.js +6 -10
  6. package/dist/agent/pi.js +113 -70
  7. package/dist/agent/roles.js +104 -0
  8. package/dist/agent/types.js +78 -0
  9. package/dist/boards/boards.js +42 -32
  10. package/dist/channels/chains.js +0 -4
  11. package/dist/channels/commands.js +7 -12
  12. package/dist/channels/config.js +27 -1
  13. package/dist/channels/control.js +5 -16
  14. package/dist/channels/conversations.js +4 -9
  15. package/dist/channels/lark-api.js +20 -26
  16. package/dist/channels/lark-outbound.js +21 -29
  17. package/dist/channels/lark-panel.js +1 -1
  18. package/dist/channels/lark.js +81 -39
  19. package/dist/channels/lines.js +33 -1
  20. package/dist/channels/panel.js +17 -107
  21. package/dist/channels/routes.js +11 -21
  22. package/dist/channels/runtime.js +79 -25
  23. package/dist/channels/slack-outbound.js +12 -14
  24. package/dist/channels/slack-panel.js +1 -1
  25. package/dist/channels/slack-render.js +1 -1
  26. package/dist/channels/slack-thread.js +65 -0
  27. package/dist/channels/slack.js +85 -124
  28. package/dist/channels/types.js +1 -1
  29. package/dist/cli.js +7 -2
  30. package/dist/config-sync.js +42 -37
  31. package/dist/core/chain.js +221 -0
  32. package/dist/core/identity.js +36 -13
  33. package/dist/core/inbound-file.js +11 -4
  34. package/dist/core/reply.js +111 -81
  35. package/dist/core/router.js +111 -118
  36. package/dist/core/search.js +65 -0
  37. package/dist/core/types.js +38 -77
  38. package/dist/db.js +41 -2
  39. package/dist/drain.js +1 -1
  40. package/dist/main.js +65 -40
  41. package/dist/settings.js +26 -19
  42. package/dist/socket.js +1 -0
  43. package/dist/tasks/agent.js +11 -5
  44. package/dist/tasks/callbacks.js +55 -5
  45. package/dist/tasks/cli.js +22 -11
  46. package/dist/tasks/definitions.js +30 -17
  47. package/dist/tasks/execution.js +11 -5
  48. package/dist/tasks/groups.js +5 -2
  49. package/dist/tasks/messages.js +7 -7
  50. package/dist/tasks/open-items.js +88 -0
  51. package/dist/tasks/operations.js +76 -39
  52. package/dist/tasks/outbox.js +19 -6
  53. package/dist/tasks/routes.js +19 -249
  54. package/dist/tasks/service.js +166 -34
  55. package/dist/tasks/store.js +123 -57
  56. package/dist/tasks/types.js +3 -0
  57. package/dist/tools.js +12 -5
  58. package/dist/web/auth.js +2 -2
  59. package/dist/web/config-sync.js +4 -5
  60. package/dist/web/config.js +9 -2
  61. package/dist/web/explorer.js +2 -2
  62. package/dist/web/fs.js +16 -1
  63. package/dist/web/instance.js +8 -5
  64. package/dist/web/packages.js +1 -1
  65. package/dist/web/passkeys.js +3 -3
  66. package/dist/web/providers.js +3 -3
  67. package/dist/web/public/assets/code-CWT27KH1.js +2 -0
  68. package/dist/web/public/assets/code-CWT27KH1.js.br +0 -0
  69. package/dist/web/public/assets/code-CWT27KH1.js.gz +0 -0
  70. package/dist/web/public/assets/explorer-C4DhXFCm.js +5 -0
  71. package/dist/web/public/assets/explorer-C4DhXFCm.js.br +0 -0
  72. package/dist/web/public/assets/explorer-C4DhXFCm.js.gz +0 -0
  73. package/dist/web/public/assets/highlight-CDSk2hRJ.js +72 -0
  74. package/dist/web/public/assets/highlight-CDSk2hRJ.js.br +0 -0
  75. package/dist/web/public/assets/highlight-CDSk2hRJ.js.gz +0 -0
  76. package/dist/web/public/assets/{hljs-tWqyD59G.js → hljs-BRz6a8Dq.js} +2 -2
  77. package/dist/web/public/assets/hljs-BRz6a8Dq.js.br +0 -0
  78. package/dist/web/public/assets/hljs-BRz6a8Dq.js.gz +0 -0
  79. package/dist/web/public/assets/index-D0ZYdweB.js +14 -0
  80. package/dist/web/public/assets/index-D0ZYdweB.js.br +0 -0
  81. package/dist/web/public/assets/index-D0ZYdweB.js.gz +0 -0
  82. package/dist/web/public/assets/index-JN0Ic8K5.css +2 -0
  83. package/dist/web/public/assets/index-JN0Ic8K5.css.br +0 -0
  84. package/dist/web/public/assets/index-JN0Ic8K5.css.gz +0 -0
  85. package/dist/web/public/assets/settings-DSTeuIyc.js +9 -0
  86. package/dist/web/public/assets/settings-DSTeuIyc.js.br +0 -0
  87. package/dist/web/public/assets/settings-DSTeuIyc.js.gz +0 -0
  88. package/dist/web/public/index.html +39 -109
  89. package/dist/web/public/index.html.br +0 -0
  90. package/dist/web/public/index.html.gz +0 -0
  91. package/dist/web/public/manifest.webmanifest +0 -2
  92. package/dist/web/push.js +3 -3
  93. package/dist/web/server.js +89 -116
  94. package/dist/web/session-state.js +4 -40
  95. package/docs/deploy.md +5 -3
  96. package/package.json +2 -1
  97. package/skills/pier-boards/SKILL.md +14 -4
  98. package/skills/pier-help/SKILL.md +62 -23
  99. package/skills/pier-search/SKILL.md +27 -0
  100. package/skills/pier-tasks/SKILL.md +68 -18
  101. package/dist/channels/handoff.js +0 -94
  102. package/dist/web/public/assets/activity-Ds6fCHnb.js +0 -5
  103. package/dist/web/public/assets/activity-Ds6fCHnb.js.br +0 -0
  104. package/dist/web/public/assets/activity-Ds6fCHnb.js.gz +0 -0
  105. package/dist/web/public/assets/boards-BKCj6EwK.js +0 -1
  106. package/dist/web/public/assets/boards-BKCj6EwK.js.br +0 -0
  107. package/dist/web/public/assets/boards-BKCj6EwK.js.gz +0 -0
  108. package/dist/web/public/assets/explorer-DnTm975c.js +0 -4
  109. package/dist/web/public/assets/explorer-DnTm975c.js.br +0 -0
  110. package/dist/web/public/assets/explorer-DnTm975c.js.gz +0 -0
  111. package/dist/web/public/assets/hljs-tWqyD59G.js.br +0 -0
  112. package/dist/web/public/assets/hljs-tWqyD59G.js.gz +0 -0
  113. package/dist/web/public/assets/index-BQo-haPN.js +0 -85
  114. package/dist/web/public/assets/index-BQo-haPN.js.br +0 -0
  115. package/dist/web/public/assets/index-BQo-haPN.js.gz +0 -0
  116. package/dist/web/public/assets/index-DiHj0w1i.css +0 -2
  117. package/dist/web/public/assets/index-DiHj0w1i.css.br +0 -0
  118. package/dist/web/public/assets/index-DiHj0w1i.css.gz +0 -0
  119. package/dist/web/public/assets/runs-C_AthWcW.js +0 -1
  120. package/dist/web/public/assets/runs-C_AthWcW.js.br +0 -0
  121. package/dist/web/public/assets/runs-C_AthWcW.js.gz +0 -0
  122. package/dist/web/public/assets/settings-DDaAtFlc.js +0 -5
  123. package/dist/web/public/assets/settings-DDaAtFlc.js.br +0 -0
  124. package/dist/web/public/assets/settings-DDaAtFlc.js.gz +0 -0
  125. package/dist/web/public/assets/task-runs-0pvdITiV.js +0 -3
  126. package/dist/web/public/assets/task-runs-0pvdITiV.js.br +0 -0
  127. package/dist/web/public/assets/task-runs-0pvdITiV.js.gz +0 -0
  128. package/dist/web/public/assets/tasks-Bz29caHJ.js +0 -4
  129. package/dist/web/public/assets/tasks-Bz29caHJ.js.br +0 -0
  130. package/dist/web/public/assets/tasks-Bz29caHJ.js.gz +0 -0
@@ -1,258 +1,28 @@
1
- // The area's HTTP surface. A route names the caller and hands the decision to
2
- // TaskService: policy here would be policy `pier task` does not get.
3
- import { record, requiredString } from "./definitions.js";
4
- /** Reject malformed filters instead of silently widening a global query. */
5
- function runQuery(params) {
6
- const query = {};
7
- const states = ["queued", "running", "succeeded", "failed", "cancelled", "interrupted", "skipped"];
8
- const sources = ["manual", "cron", "watch", "agent", "task"];
9
- if (params.state) {
10
- if (!states.includes(params.state))
11
- throw new Error("invalid state");
12
- query.state = params.state;
13
- }
14
- if (params.source) {
15
- if (!sources.includes(params.source))
16
- throw new Error("invalid source");
17
- query.source = params.source;
18
- }
19
- if (params.taskId)
20
- query.taskId = params.taskId;
21
- for (const key of ["since", "until", "limit"]) {
22
- if (params[key] === undefined)
23
- continue;
24
- const value = Number(params[key]);
25
- if (!params[key] || !Number.isSafeInteger(value) || value < 0 || (key === "limit" && (value < 1 || value > 200)))
26
- throw new Error(`invalid ${key}`);
27
- query[key] = value;
28
- }
29
- if (query.since !== undefined && query.until !== undefined && query.since > query.until)
30
- throw new Error("since exceeds until");
31
- if (params.showUnmatched !== undefined) {
32
- if (!["true", "false"].includes(params.showUnmatched))
33
- throw new Error("invalid showUnmatched");
34
- query.showUnmatched = params.showUnmatched === "true";
35
- }
36
- if (params.cursor) {
37
- const cursor = JSON.parse(params.cursor);
38
- const value = record(cursor);
39
- if (!value || !Number.isSafeInteger(value.queuedAt) || Number(value.queuedAt) < 0 || typeof value.id !== "string" || !value.id)
40
- throw new Error("invalid cursor");
41
- query.cursor = { queuedAt: Number(value.queuedAt), id: value.id };
42
- }
43
- return query;
44
- }
45
- const jsonBody = async (req) => req.json().catch(() => null);
46
- export function registerTaskRoutes(app, tasks, activity) {
47
- if (activity)
48
- app.get("/api/activity", async (c) => {
49
- const now = Date.now();
50
- const recent = c.req.query("scope") === "recent";
51
- const windowStart = now - 24 * 60 * 60 * 1000;
52
- const runs = tasks.activityRuns(recent ? windowStart : undefined);
53
- const listed = await activity.factory.list();
54
- const byId = new Map(listed.map((session) => [session.id, session]));
55
- const linkedIds = new Set();
56
- for (const run of runs) {
57
- for (const id of [run.invokedBySessionId, run.targetSessionId, run.callbackSessionId]) {
58
- if (id)
59
- linkedIds.add(id);
60
- }
61
- }
62
- for (const session of listed) {
63
- if (activity.router.stateOf(session.id) === "streaming")
64
- linkedIds.add(session.id);
65
- }
66
- const messages = tasks.recentMessages(windowStart)
67
- .filter((message) => runs.some((run) => run.id === message.runId));
68
- for (const message of messages) {
69
- if (message.fromSessionId !== "console")
70
- linkedIds.add(message.fromSessionId);
71
- if (message.toSessionId !== "console")
72
- linkedIds.add(message.toSessionId);
73
- }
74
- return c.json({
75
- sessions: [...linkedIds].map((id) => {
76
- const session = byId.get(id);
77
- return {
78
- id,
79
- cwd: session?.cwd ?? "",
80
- createdAt: session?.createdAt ?? null,
81
- title: session?.title,
82
- state: activity.router.stateOf(id) ?? "idle",
83
- stateSince: activity.router.stateSinceOf(id) ?? null,
84
- };
85
- }),
86
- runs,
87
- messages,
88
- });
89
- });
90
- app.get("/api/task-runs", (c) => {
91
- try {
92
- return c.json(tasks.queryRuns(runQuery(c.req.query())));
93
- }
94
- catch (err) {
95
- return c.json({ error: String(err) }, 400);
96
- }
97
- });
98
- app.get("/api/tasks", (c) => {
99
- const trigger = c.req.query("trigger");
100
- const state = c.req.query("state");
101
- const kind = c.req.query("kind");
102
- let rows = tasks.list();
103
- // Subagent one-shots are hidden unless explicitly requested via ?kind=subagent.
104
- rows = rows.filter((task) => task.kind === (kind ?? "task"));
105
- if (trigger)
106
- rows = rows.filter((task) => task.trigger.type === trigger);
107
- if (state === "archived")
108
- rows = rows.filter((task) => task.archived);
109
- else if (state === "active")
110
- rows = rows.filter((task) => !task.archived);
111
- return c.json(rows.map((task) => ({
112
- ...task,
113
- lastRun: tasks.listRuns(task.id, 1)[0] ?? null,
114
- })));
115
- });
116
- app.post("/api/tasks", async (c) => {
117
- const body = await jsonBody(c.req);
118
- const runNow = typeof body === "object" && body !== null && "runNow" in body && body.runNow === true;
119
- const definition = typeof body === "object" && body !== null && "task" in body ? body.task : body;
120
- try {
121
- const task = await tasks.create(definition);
122
- const run = runNow ? tasks.run(task.id) : null;
123
- return c.json({ task, runId: run?.id ?? null }, 201);
124
- }
125
- catch (err) {
126
- return c.json({ error: String(err) }, 400);
127
- }
128
- });
1
+ // Settings → Tasks' HTTP surface: the list, pause/resume, a task's runs. A
2
+ // route names the caller and hands the decision to TaskService: policy here
3
+ // would be policy `pier task` does not get.
4
+ const RUNS_LIMIT = 20;
5
+ export function registerTaskRoutes(app, tasks) {
6
+ // Subagent one-shots and archived tasks are history, not something to manage.
7
+ app.get("/api/tasks", (c) => c.json(tasks.list()
8
+ .filter((task) => task.kind === "task" && !task.archived)
9
+ .map((task) => ({ ...task, lastRun: tasks.listRuns(task.id, 1)[0] ?? null }))));
129
10
  app.get("/api/tasks/:id/runs", (c) => {
130
11
  try {
131
- const limit = Number(c.req.query("limit") ?? 50);
132
- const offset = Number(c.req.query("offset") ?? 0);
133
- return c.json(tasks.listRuns(c.req.param("id"), limit, offset));
12
+ return c.json(tasks.listRuns(tasks.get(c.req.param("id")).id, RUNS_LIMIT));
134
13
  }
135
14
  catch (err) {
136
15
  return c.json({ error: String(err) }, 404);
137
16
  }
138
17
  });
139
- app.get("/api/task-runs/:id", (c) => {
140
- try {
141
- return c.json(tasks.getRunView(c.req.param("id")));
142
- }
143
- catch (err) {
144
- return c.json({ error: String(err) }, 404);
145
- }
146
- });
147
- app.get("/api/task-groups/:id", (c) => {
148
- try {
149
- return c.json(tasks.getGroup(c.req.param("id")));
150
- }
151
- catch (err) {
152
- return c.json({ error: String(err) }, 404);
153
- }
154
- });
155
- app.get("/api/task-runs/:id/messages", (c) => {
156
- try {
157
- return c.json(tasks.listMessages(c.req.param("id")));
158
- }
159
- catch (err) {
160
- return c.json({ error: String(err) }, 404);
161
- }
162
- });
163
- app.post("/api/task-runs/:id/steer", async (c) => {
164
- const body = record(await jsonBody(c.req));
165
- try {
166
- return c.json(await tasks.control(c.req.param("id"), typeof body?.sourceSessionId === "string" ? body.sourceSessionId : "console", body?.mode === "followUp" ? "follow_up" : "steer", requiredString(body?.message, "message")), 202);
167
- }
168
- catch (err) {
169
- return c.json({ error: String(err) }, 400);
170
- }
171
- });
172
- app.post("/api/task-runs/:id/resume", async (c) => {
173
- const body = record(await jsonBody(c.req));
174
- try {
175
- const wait = body?.wait === true;
176
- const source = typeof body?.sourceSessionId === "string" ? body.sourceSessionId : null;
177
- const run = tasks.resume(c.req.param("id"), requiredString(body?.message, "message"), {
178
- invokedBySessionId: source,
179
- callbackSessionId: wait ? null : source,
180
- background: !wait,
181
- });
182
- return c.json(wait ? await tasks.waitForRun(run.id) : run, wait ? 200 : 202);
183
- }
184
- catch (err) {
185
- return c.json({ error: String(err) }, 400);
186
- }
187
- });
188
- app.post("/api/task-runs/:id/cancel", (c) => {
189
- try {
190
- return c.json(tasks.cancel(c.req.param("id")), 202);
191
- }
192
- catch (err) {
193
- return c.json({ error: String(err) }, 404);
194
- }
195
- });
196
- app.get("/api/tasks/:id", (c) => {
197
- try {
198
- return c.json(tasks.get(c.req.param("id")));
199
- }
200
- catch (err) {
201
- return c.json({ error: String(err) }, 404);
202
- }
203
- });
204
- app.patch("/api/tasks/:id", async (c) => {
205
- try {
206
- return c.json(await tasks.update(c.req.param("id"), await jsonBody(c.req)));
207
- }
208
- catch (err) {
209
- return c.json({ error: String(err) }, 400);
210
- }
211
- });
212
- app.post("/api/tasks/:id/run", async (c) => {
213
- const body = record(await jsonBody(c.req));
214
- const input = body && "input" in body ? body.input : null;
215
- try {
216
- // Dropping an unsupported override would run the definition's own policy
217
- // instead — on a reuse definition, work injected into a live session.
218
- if (body?.sessionMode !== undefined && body.sessionMode !== "fresh") {
219
- throw new Error(`unsupported sessionMode: ${String(body.sessionMode)}`);
18
+ for (const [verb, enabled] of [["pause", false], ["resume", true]]) {
19
+ app.post(`/api/tasks/:id/${verb}`, (c) => {
20
+ try {
21
+ return c.json(tasks.setEnabled(c.req.param("id"), enabled));
220
22
  }
221
- const sessionMode = body?.sessionMode;
222
- const sourceSessionId = typeof body?.sourceSessionId === "string" && body.sourceSessionId ? body.sourceSessionId : null;
223
- const task = tasks.get(c.req.param("id"));
224
- return c.json({ runId: tasks.run(task.id, input, "manual", null, {
225
- invokedBySessionId: sourceSessionId,
226
- sourceSessionId,
227
- sessionMode,
228
- }).id }, 202);
229
- }
230
- catch (err) {
231
- return c.json({ error: String(err) }, 400);
232
- }
233
- });
234
- app.post("/api/tasks/:id/pause", (c) => {
235
- try {
236
- return c.json(tasks.setEnabled(c.req.param("id"), false));
237
- }
238
- catch (err) {
239
- return c.json({ error: String(err) }, 400);
240
- }
241
- });
242
- app.post("/api/tasks/:id/resume", (c) => {
243
- try {
244
- return c.json(tasks.setEnabled(c.req.param("id"), true));
245
- }
246
- catch (err) {
247
- return c.json({ error: String(err) }, 400);
248
- }
249
- });
250
- app.post("/api/tasks/:id/archive", (c) => {
251
- try {
252
- return c.json(tasks.archive(c.req.param("id")));
253
- }
254
- catch (err) {
255
- return c.json({ error: String(err) }, 400);
256
- }
257
- });
23
+ catch (err) {
24
+ return c.json({ error: String(err) }, 400);
25
+ }
26
+ });
27
+ }
258
28
  }
@@ -3,11 +3,12 @@
3
3
  // drain needs. Decisions belong to the files beside it.
4
4
  import { logger } from "../log.js";
5
5
  import { AgentTaskRunner } from "./agent.js";
6
- import { TaskCallbacks } from "./callbacks.js";
6
+ import { DESIGN_FINAL, LEAD_TURN, MILESTONE, runCwd, settleCallback, TaskCallbacks } from "./callbacks.js";
7
7
  import { TaskDefinitions, requiredString } from "./definitions.js";
8
8
  import { TaskExecution } from "./execution.js";
9
9
  import { TaskGroups } from "./groups.js";
10
10
  import { TaskMessenger } from "./messages.js";
11
+ import { openItems, recordOpenItems } from "./open-items.js";
11
12
  import { TaskRunQueue } from "./runs.js";
12
13
  import { handleTask } from "./operations.js";
13
14
  import { isTerminal } from "./types.js";
@@ -36,17 +37,23 @@ export class TaskService {
36
37
  runs;
37
38
  execution;
38
39
  constructor(store, factory, router, hub,
39
- /** Structural: tasks/ must not import settings.ts. Absent in bare test rigs. */
40
+ /** Structural: tasks/ must not import settings.ts. */
40
41
  instance) {
41
42
  this.store = store;
42
43
  this.factory = factory;
43
44
  this.router = router;
44
45
  this.hub = hub;
45
46
  this.instance = instance;
47
+ const headOf = (id) => instance.continuous.chainOf(id)?.[0] ?? id;
48
+ const conversation = () => instance.continuous.members()[0]?.sessionId ?? null;
46
49
  const unreachable = (sessionId, what, why) => this.unreachable(sessionId, what, why);
47
- this.messages = new TaskMessenger(store, router, hub, unreachable);
48
- this.definitions = new TaskDefinitions(store, factory, router, hub, instance?.systemActions);
49
- this.callbacks = new TaskCallbacks(store, router, (run) => this.changed(run), unreachable);
50
+ this.messages = new TaskMessenger(store, router, hub, unreachable, (runId) => {
51
+ const run = store.getRun(runId);
52
+ if (run)
53
+ this.status(run);
54
+ });
55
+ this.definitions = new TaskDefinitions(store, factory, router, hub, instance.systemActions);
56
+ this.callbacks = new TaskCallbacks(store, router, (run) => this.changed(run), unreachable, headOf, this.milestone, conversation);
50
57
  this.groups = new TaskGroups(store, router, {
51
58
  getRun: (id) => this.getRun(id),
52
59
  cancel: (id) => { this.cancel(id); },
@@ -58,7 +65,7 @@ export class TaskService {
58
65
  groupId,
59
66
  }),
60
67
  startMember: (run) => this.runs.start(run),
61
- }, (group) => this.hub.emitWorkspace({ type: "task-group-changed", groupId: group.id }), unreachable);
68
+ }, (group) => this.hub.emitWorkspace({ type: "task-group-changed", groupId: group.id }), unreachable, headOf, this.milestone);
62
69
  const agent = new AgentTaskRunner(factory, router, store, this.messages, (run) => this.changed(run));
63
70
  this.execution = new TaskExecution(store, this.definitions, this.callbacks, agent, {
64
71
  runChild: (taskId, parent) => this.run(taskId, parent.input, "task", parent.id, {
@@ -73,6 +80,84 @@ export class TaskService {
73
80
  changed: (run) => this.changed(run),
74
81
  });
75
82
  this.runs = new TaskRunQueue(store, this.callbacks, (run) => this.execution.start(run), (run) => this.changed(run));
83
+ router.onTurnEnd((sessionId, text) => {
84
+ // Only the head's turns write the list.
85
+ if (instance.continuous.members()[0]?.sessionId === sessionId) {
86
+ try {
87
+ if (recordOpenItems(store, text, Date.now()))
88
+ hub.emitWorkspace({ type: "open-items-changed" });
89
+ }
90
+ catch (err) {
91
+ log.warn(`open items: the markers in ${sessionId}'s reply could not be written`, err);
92
+ }
93
+ }
94
+ if (!DESIGN_FINAL.test(text))
95
+ return;
96
+ // On Pi's dispatch stack, which must not unwind.
97
+ try {
98
+ this.designFinal(sessionId, text);
99
+ }
100
+ catch (err) {
101
+ log.error(`lead ${sessionId}: a Design final: outside any run could not be recorded; main was not told`, err);
102
+ }
103
+ });
104
+ }
105
+ /** A design lead finalized in its own session, a turn outside any run (the
106
+ * user confirmed there): recorded as a finished run of the lead whose result
107
+ * is that reply, so it reports to the lead's supervisor, and the session list and
108
+ * `/status` see it, exactly as a run's `Design final:` does. */
109
+ designFinal(sessionId, text) {
110
+ // A run's own turn reports through the run.
111
+ if (this.store.leadPhaseOf(sessionId) !== "design" || this.store.findActiveRunForTarget(sessionId)?.state === "running")
112
+ return;
113
+ const last = this.store.latestRunForTarget(sessionId);
114
+ if (!last)
115
+ return;
116
+ const run = this.store.transact(() => {
117
+ const run = this.runs.prepare(last.context.definition, null, "agent", null, {
118
+ invokedBySessionId: last.invokedBySessionId,
119
+ sourceSessionId: last.invokedBySessionId,
120
+ targetSessionId: sessionId,
121
+ sessionMode: "reuse",
122
+ resumedFromRunId: last.id,
123
+ callbackSessionId: last.callbackSessionId,
124
+ callbackMode: last.callbackMode,
125
+ background: last.background,
126
+ });
127
+ const now = Date.now();
128
+ run.state = "succeeded";
129
+ run.startedAt = run.finishedAt = now;
130
+ run.result = { type: "agent", text, sessionId };
131
+ run.context.sessionId = sessionId;
132
+ settleCallback(run, this.store);
133
+ this.store.saveRun(run);
134
+ return run;
135
+ });
136
+ if (run.callbackSessionId === null)
137
+ log.warn(`lead ${sessionId}: Design final: recorded as run ${run.id}; the lead reports to no one`);
138
+ else
139
+ log.info(`lead ${sessionId}: Design final: outside any run, recorded as run ${run.id} for ${run.callbackSessionId}`);
140
+ this.runs.start(run);
141
+ this.designLead(run);
142
+ }
143
+ /** What a settled run of a design lead means to whoever shows the lead to
144
+ * the user (channels/runtime.ts); a reporter that throws must not unwind the settle. */
145
+ designLead(run) {
146
+ const lead = run.targetSessionId;
147
+ if (!this.instance.designLead || lead === null || this.store.leadPhaseOf(lead) !== "design")
148
+ return;
149
+ const state = run.callbackError === LEAD_TURN ? "waiting"
150
+ : run.state !== "succeeded" ? "failed"
151
+ : run.result?.type === "agent" && DESIGN_FINAL.test(run.result.text) ? "final"
152
+ : undefined;
153
+ if (!state)
154
+ return;
155
+ try {
156
+ this.instance.designLead(run, state);
157
+ }
158
+ catch (err) {
159
+ log.error(`lead ${lead}: its ${state} state could not be reported`, err);
160
+ }
76
161
  }
77
162
  start(tickMs = 1000) {
78
163
  if (this.timer)
@@ -145,7 +230,7 @@ export class TaskService {
145
230
  create(raw, creator = "http") {
146
231
  return this.definitions.create(raw, creator);
147
232
  }
148
- /** `by` is how owning code says so; the routes and `pier task` have none (definitions.ts). */
233
+ /** `by` is how owning code says so; `pier task` has none (definitions.ts). */
149
234
  update(id, raw, by) {
150
235
  return this.definitions.update(id, raw, by);
151
236
  }
@@ -155,9 +240,9 @@ export class TaskService {
155
240
  archive(id, by) {
156
241
  return this.definitions.archive(id, by);
157
242
  }
158
- listRuns(taskId, limit = 50, offset = 0) {
159
- this.get(taskId);
160
- return this.store.listRuns(taskId, Math.min(Math.max(limit, 1), 200), Math.max(offset, 0));
243
+ /** `pier task list`'s last run: the newest `limit` of a definition's runs. */
244
+ listRuns(taskId, limit = 1) {
245
+ return this.store.listRuns(taskId, limit);
161
246
  }
162
247
  getRun(id) {
163
248
  const run = this.store.getRun(id);
@@ -165,24 +250,6 @@ export class TaskService {
165
250
  throw new Error(`unknown task run: ${id}`);
166
251
  return run;
167
252
  }
168
- getRunView(id) {
169
- const run = this.getRun(id);
170
- return { ...run,
171
- groupCallbackState: run.groupId ? this.store.getGroup(run.groupId)?.callbackState ?? null : null };
172
- }
173
- listMessages(runId) {
174
- this.getRun(runId);
175
- return this.messages.list(runId);
176
- }
177
- queryRuns(query = {}) {
178
- return this.store.queryRuns(query);
179
- }
180
- activityRuns(since) {
181
- return this.store.activityRuns(since);
182
- }
183
- recentMessages(since) {
184
- return this.messages.recent(since);
185
- }
186
253
  /** Every run, not the last hour's: the card is a message in the transcript. */
187
254
  backgroundRuns(sessionId) {
188
255
  return this.store.listRunsForSession(sessionId, 200)
@@ -190,6 +257,28 @@ export class TaskService {
190
257
  .reverse()
191
258
  .map((run) => this.backgroundRun(run));
192
259
  }
260
+ /** What the session's queue shows beside Pi's own: follow-ups waiting for it to idle. */
261
+ parkedMessages(sessionId) {
262
+ return this.store.pendingFollowUpsTo(sessionId).map((message) => ({
263
+ messageId: message.id,
264
+ runName: this.store.getRun(message.runId)?.context.definition.name ?? message.runId,
265
+ text: message.content,
266
+ }));
267
+ }
268
+ /** The run ledger: runs any of `sessionIds` launched, in flight or finished since `since`, at most 200. */
269
+ ledger(sessionIds, since) {
270
+ return this.store.ledgerRuns(sessionIds, since).map(ledgerRun);
271
+ }
272
+ /** Design leads no run of which has reported `Design final:`, by their creating run: the user's to finalize. */
273
+ openDesigns() {
274
+ return [...this.store.leads().values()].flatMap((lead) => {
275
+ const run = lead.designOpen ? this.store.getRun(lead.runId) : undefined;
276
+ return run ? [ledgerRun(run)] : [];
277
+ });
278
+ }
279
+ openItems(now = Date.now()) {
280
+ return openItems(this, this.router, this.instance.continuous.members().map((m) => m.sessionId), this.openDesigns(), now);
281
+ }
193
282
  activeBackgroundRunCounts() {
194
283
  return this.store.countActiveBackgroundRunsBySession();
195
284
  }
@@ -262,15 +351,19 @@ export class TaskService {
262
351
  return this.messages.control(run, fromSessionId, mode, message);
263
352
  }
264
353
  resume(id, message, provenance = {}) {
354
+ const run = this.prepareResume(this.getRun(id), message, provenance);
355
+ this.runs.start(run);
356
+ return run;
357
+ }
358
+ prepareResume(prior, message, provenance) {
265
359
  this.refusePaused();
266
- const prior = this.getRun(id);
267
360
  if (!isTerminal(prior.state))
268
361
  throw new Error("run must be terminal before resume");
269
362
  if (prior.context.definition.action.type !== "agent" || !prior.targetSessionId) {
270
363
  throw new Error("only persisted Agent runs can be resumed");
271
364
  }
272
365
  const prompt = requiredString(message, "message");
273
- const run = this.runs.prepare(prior.context.definition, null, "agent", null, {
366
+ return this.runs.prepare(prior.context.definition, null, "agent", null, {
274
367
  ...provenance,
275
368
  sourceSessionId: provenance.invokedBySessionId ?? prior.invokedBySessionId,
276
369
  targetSessionId: prior.targetSessionId,
@@ -278,16 +371,46 @@ export class TaskService {
278
371
  resumedFromRunId: prior.id,
279
372
  resumePrompt: prompt,
280
373
  });
281
- this.runs.start(run);
282
- return run;
283
374
  }
375
+ /** A lead reports once per wave (docs/design/10-continuous-session.md
376
+ * §Feature lead): the result that leaves nothing owed to it in flight
377
+ * resumes its last run, whose own callback reaches its supervisor. */
378
+ milestone = (sessionId, text, settle) => {
379
+ if (this.store.roleOf(sessionId) !== "lead" || this.store.countOwedTo(sessionId) > 0)
380
+ return "plain";
381
+ const last = this.store.latestRunForTarget(sessionId);
382
+ if (!last || last.callbackSessionId === null)
383
+ return "plain";
384
+ // A running turn reports upstream when it ends; a drain leaves the result to the next boot.
385
+ if (!isTerminal(last.state) || this.paused)
386
+ return "wait";
387
+ try {
388
+ // One transaction: a crash between the resume and the marks would resume twice.
389
+ const run = this.store.transact(() => {
390
+ const run = this.prepareResume(last, `${MILESTONE}\n\n${text()}`, {
391
+ invokedBySessionId: last.invokedBySessionId,
392
+ callbackSessionId: last.callbackSessionId,
393
+ callbackMode: last.callbackMode,
394
+ background: last.background,
395
+ });
396
+ settle();
397
+ return run;
398
+ });
399
+ this.runs.start(run);
400
+ return "resumed";
401
+ }
402
+ catch (err) {
403
+ log.error(`lead ${sessionId}: the wave's last result could not resume run ${last.id}; delivering it as a plain callback`, err);
404
+ return "plain";
405
+ }
406
+ };
284
407
  /** What `pier task` asks, under the calling session's identity. */
285
408
  handle(raw, callerSessionId) {
286
- return handleTask(this, this.definitions, this.store, raw, callerSessionId);
409
+ return handleTask(this, this.definitions, this.store, raw, callerSessionId, this.instance.continuous);
287
410
  }
288
411
  /** An agent picks from names that exist right now, never from memory. */
289
412
  async models() {
290
- const menu = this.instance?.modelMenu() ?? [];
413
+ const menu = this.instance.modelMenu();
291
414
  if (menu.length)
292
415
  return { source: "menu", models: menu };
293
416
  return { source: "catalog", models: await this.factory.availableModels() };
@@ -342,6 +465,7 @@ export class TaskService {
342
465
  resolve(run);
343
466
  this.waiters.delete(run.id);
344
467
  this.groups.onSettled(run);
468
+ this.designLead(run);
345
469
  }
346
470
  backgroundRun(run) {
347
471
  return {
@@ -355,12 +479,20 @@ export class TaskService {
355
479
  queuedAt: run.queuedAt,
356
480
  startedAt: run.startedAt,
357
481
  finishedAt: run.finishedAt,
482
+ // A finished run's pending messages only wait for the sweep to expire them.
483
+ queuedMessages: isTerminal(run.state) ? 0 : this.store.countPendingFollowUps(run.id),
358
484
  };
359
485
  }
360
486
  changed(run) {
361
487
  this.hub.emitWorkspace({ type: "task-run-changed", taskId: run.taskId, runId: run.id });
488
+ this.status(run);
489
+ }
490
+ status(run) {
362
491
  if (run.background && run.invokedBySessionId) {
363
492
  this.hub.emit(run.invokedBySessionId, { type: "task-status", run: this.backgroundRun(run) });
364
493
  }
365
494
  }
366
495
  }
496
+ function ledgerRun(run) {
497
+ return { runId: run.id, name: run.context.definition.name, state: run.state, targetSessionId: run.targetSessionId, cwd: runCwd(run), queuedAt: run.queuedAt, finishedAt: run.finishedAt };
498
+ }