@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/tasks/messages.js
CHANGED
|
@@ -23,13 +23,17 @@ export class TaskMessenger {
|
|
|
23
23
|
store;
|
|
24
24
|
hub;
|
|
25
25
|
unreachable;
|
|
26
|
+
runChanged;
|
|
26
27
|
outbox;
|
|
27
28
|
constructor(store, router, hub,
|
|
28
29
|
/** Reports a delivery nobody can complete (service.ts owns the surfaces). */
|
|
29
|
-
unreachable
|
|
30
|
+
unreachable,
|
|
31
|
+
/** A follow-up's run card counts it while it is pending (service.ts). */
|
|
32
|
+
runChanged) {
|
|
30
33
|
this.store = store;
|
|
31
34
|
this.hub = hub;
|
|
32
35
|
this.unreachable = unreachable;
|
|
36
|
+
this.runChanged = runChanged;
|
|
33
37
|
this.outbox = new Outbox(router, {
|
|
34
38
|
id: ({ message }) => message.id,
|
|
35
39
|
reload: (id) => {
|
|
@@ -68,12 +72,6 @@ export class TaskMessenger {
|
|
|
68
72
|
for (const message of this.store.expirePendingMessages())
|
|
69
73
|
this.changed(message);
|
|
70
74
|
}
|
|
71
|
-
list(runId) {
|
|
72
|
-
return this.store.listMessages(runId);
|
|
73
|
-
}
|
|
74
|
-
recent(since) {
|
|
75
|
-
return this.store.listRecentMessages(since);
|
|
76
|
-
}
|
|
77
75
|
async control(run, fromSessionId, kind, content) {
|
|
78
76
|
const message = this.create(run, kind, fromSessionId, run.targetSessionId ?? "", content);
|
|
79
77
|
this.changed(message);
|
|
@@ -178,5 +176,7 @@ export class TaskMessenger {
|
|
|
178
176
|
/** Publish only after any transaction changing this message has committed. */
|
|
179
177
|
changed(message) {
|
|
180
178
|
this.hub.emitWorkspace({ type: "task-message-changed", runId: message.runId, messageId: message.id });
|
|
179
|
+
if (message.kind === "follow_up")
|
|
180
|
+
this.runChanged(message.runId);
|
|
181
181
|
}
|
|
182
182
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The continuous conversation's open items in task-run words: main's markers
|
|
2
|
+
// joined to the run ledger by session, a lead's workers counted by state, and
|
|
3
|
+
// the one text every surface shows (docs/design/10-continuous-session.md#open-items).
|
|
4
|
+
import { openItemMarkers, runStatus, workerCounts } from "../core/reply.js";
|
|
5
|
+
import { LEDGER_WINDOW_MS, TASK_RUN_STATES } from "../core/types.js";
|
|
6
|
+
import { logger } from "../log.js";
|
|
7
|
+
import { NOT_IN_LEDGER } from "./types.js";
|
|
8
|
+
const log = logger("tasks");
|
|
9
|
+
const IN_FLIGHT = new Set(["queued", "running"]);
|
|
10
|
+
/** `members`: the chain, newest first; `designs`: the open designs of sessions not closed. */
|
|
11
|
+
export function openItems(tasks, router, members, designs, now) {
|
|
12
|
+
const since = now - LEDGER_WINDOW_MS;
|
|
13
|
+
const runs = members.length ? tasks.ledger(members, since) : [];
|
|
14
|
+
const withWorkers = (r) => {
|
|
15
|
+
if (!r.targetSessionId || tasks.store.roleOf(r.targetSessionId) !== "lead")
|
|
16
|
+
return r;
|
|
17
|
+
const workers = Object.fromEntries(TASK_RUN_STATES.map((s) => [s, 0]));
|
|
18
|
+
for (const w of tasks.ledger([r.targetSessionId], since)) {
|
|
19
|
+
if (w.state in workers)
|
|
20
|
+
workers[w.state] += 1;
|
|
21
|
+
}
|
|
22
|
+
return { ...r, workers };
|
|
23
|
+
};
|
|
24
|
+
// An item names a session through any of its runs: the session's newest run stands for it.
|
|
25
|
+
const tracked = new Set();
|
|
26
|
+
const items = tasks.store.openItems().map((row) => {
|
|
27
|
+
const named = row.runIds.map((id) => {
|
|
28
|
+
const session = runs.find((r) => r.runId === id)?.targetSessionId ?? tasks.store.getRun(id)?.targetSessionId ?? null;
|
|
29
|
+
tracked.add(session ?? id);
|
|
30
|
+
const run = runs.find((r) => (session ? r.targetSessionId === session : r.runId === id));
|
|
31
|
+
return run
|
|
32
|
+
? withWorkers(run)
|
|
33
|
+
: { runId: id, name: id, state: NOT_IN_LEDGER, targetSessionId: session, cwd: null, queuedAt: 0, finishedAt: null };
|
|
34
|
+
}).filter((r, i, all) => all.findIndex((o) => o.runId === r.runId) === i);
|
|
35
|
+
const busy = named.some((r) => IN_FLIGHT.has(r.state) || (r.targetSessionId && router.stateOf(r.targetSessionId) === "streaming"));
|
|
36
|
+
return { problem: row.problem, stage: row.stage, runs: named, ...(named.length ? { live: busy ? "running" : "idle" } : {}) };
|
|
37
|
+
});
|
|
38
|
+
const unlisted = runs.filter((r) => IN_FLIGHT.has(r.state) && !tracked.has(r.targetSessionId ?? r.runId)).map(withWorkers);
|
|
39
|
+
return { items, unlisted, designs };
|
|
40
|
+
}
|
|
41
|
+
/** The head's reply's markers, written; answers whether a row changed. */
|
|
42
|
+
export function recordOpenItems(store, text, now) {
|
|
43
|
+
const { markers, dropped } = openItemMarkers(text);
|
|
44
|
+
for (const marker of dropped)
|
|
45
|
+
log.warn(`open items: dropped a marker with no problem text: ${marker}`);
|
|
46
|
+
return markers.length > 0 && store.markOpenItems(markers, now) > 0;
|
|
47
|
+
}
|
|
48
|
+
const workersText = (workers) => workers ? ` · workers: ${workerCounts(workers)}` : "";
|
|
49
|
+
const runText = (r, now) => r.state === NOT_IN_LEDGER
|
|
50
|
+
? `run ${r.runId} — ${NOT_IN_LEDGER}`
|
|
51
|
+
: `run ${r.runId.length > 8 ? `${r.runId.slice(0, 8)}…` : r.runId} ${runStatus(r, now)}${workersText(r.workers)}`;
|
|
52
|
+
/** The one string every surface shows for the open items: `/status` and the seed. */
|
|
53
|
+
export function renderOpenItems({ items, unlisted, designs }, now) {
|
|
54
|
+
if (!items.length && !unlisted.length && !designs.length)
|
|
55
|
+
return "Nothing open.";
|
|
56
|
+
const open = items.map((i) => `- ${i.problem}${i.stage ? ` — ${i.stage}` : ""}${i.live ? ` (${i.live})` : ""}${i.runs.map((r) => ` · ${runText(r, now)}`).join("")}`);
|
|
57
|
+
const rest = unlisted.map((r) => `- ${r.name} — ${runStatus(r, now)}${workersText(r.workers)}`);
|
|
58
|
+
const decide = designs.map((r) => `- ${r.name} · ${runText(r, now)}`);
|
|
59
|
+
return [
|
|
60
|
+
...(open.length ? ["Open", ...open] : []),
|
|
61
|
+
...(rest.length ? ["Not on the list", ...rest] : []),
|
|
62
|
+
...(decide.length ? ["Designs for you to finalize", ...decide] : []),
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
/** `/status`'s card: the text, and run id → session id for every named run and listed design that has one. */
|
|
66
|
+
export function openItemsStatus(open, now) {
|
|
67
|
+
const sessions = Object.fromEntries([...open.items.flatMap((i) => i.runs), ...open.designs]
|
|
68
|
+
.flatMap((r) => (r.targetSessionId ? [[r.runId, r.targetSessionId]] : [])));
|
|
69
|
+
return { text: renderOpenItems(open, now), sessions };
|
|
70
|
+
}
|
package/dist/tasks/operations.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// The `/task` socket route: every `pier task` operation — run, message, save,
|
|
2
|
-
// list, cancel, recover — validated here once, answered
|
|
3
|
-
// model reads. Scheduling and delivery stay in the
|
|
4
|
-
// who may ask for what.
|
|
2
|
+
// list, pause, resume, archive, cancel, recover — validated here once, answered
|
|
3
|
+
// with the summaries a model reads. Scheduling and delivery stay in the
|
|
4
|
+
// service; this file decides who may ask for what.
|
|
5
5
|
import { isAbsolute, resolve } from "node:path";
|
|
6
|
+
import { isModelTier, LEDGER_WINDOW_MS } from "../core/types.js";
|
|
6
7
|
import { logger } from "../log.js";
|
|
7
8
|
import { record, requiredString } from "./definitions.js";
|
|
8
9
|
import { isTerminal } from "./types.js";
|
|
@@ -104,35 +105,67 @@ function cancelledOrCurrent(host, id, ms) {
|
|
|
104
105
|
new Promise((resolve) => setTimeout(() => resolve(host.getRun(id)), ms).unref()),
|
|
105
106
|
]);
|
|
106
107
|
}
|
|
107
|
-
export async function handleTask(host, definitions, store, raw, callerSessionId) {
|
|
108
|
+
export async function handleTask(host, definitions, store, raw, callerSessionId, chain) {
|
|
109
|
+
// The launching session controls a run, and so does the run's own; every
|
|
110
|
+
// member of the continuous conversation counts as the one that launched it.
|
|
111
|
+
const launchers = () => chain.chainOf(callerSessionId) ?? [callerSessionId];
|
|
112
|
+
const assertOwns = (target) => {
|
|
113
|
+
if (!launchers().includes(target.invokedBySessionId ?? "") && target.targetSessionId !== callerSessionId) {
|
|
114
|
+
throw new Error("session does not own this run");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
108
117
|
const input = record(raw);
|
|
109
118
|
if (!input)
|
|
110
119
|
throw new Error("task parameters required");
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
120
|
+
// Depth stays 2 (docs/design/09-tasks-cli.md §Two levels, no tree); a queued
|
|
121
|
+
// run has not taken the session's turn, so it gates nothing yet.
|
|
122
|
+
const role = store.roleOf(callerSessionId);
|
|
123
|
+
const lead = role === "lead";
|
|
114
124
|
const active = store.findActiveRunForTarget(callerSessionId);
|
|
115
|
-
if (active?.state === "running" && store.supervised(active))
|
|
125
|
+
if (active?.state === "running" && store.supervised(active) && !lead)
|
|
116
126
|
throw new Error("a delegated run cannot delegate; ask in your result and let your supervisor run it");
|
|
127
|
+
if (role === "worker")
|
|
128
|
+
throw new Error("a worker's session never delegates, in a run or after it; say what needs another agent and let the session that launched it run it");
|
|
129
|
+
// Read off a draft before it is filed; a saved definition has the same shape under `action`.
|
|
130
|
+
const notLead = (draft) => {
|
|
131
|
+
const role = record(record(draft)?.launch)?.role ?? record(record(record(draft)?.action)?.launch)?.role;
|
|
132
|
+
if (lead && role === "lead")
|
|
133
|
+
throw new Error("a feature lead cannot launch a lead; a build lead is your supervisor's to launch, from your milestone");
|
|
134
|
+
return draft;
|
|
135
|
+
};
|
|
117
136
|
const menu = () => host.models().then((listed) => listed.models);
|
|
118
|
-
if (input.operation === "list")
|
|
119
|
-
|
|
137
|
+
if (input.operation === "list") {
|
|
138
|
+
// What the schedule answer needs: the next occurrence and the last run's state, not the run.
|
|
139
|
+
return definitions.list().filter((task) => task.kind !== "subagent").map(({ nextRunAt, ...task }) => {
|
|
140
|
+
const last = host.listRuns(task.id, 1)[0];
|
|
141
|
+
return { ...task, nextRun: nextRunAt, lastRun: last ? defined({ runId: last.id, state: last.state, startedAt: last.startedAt, finishedAt: last.finishedAt }) : null };
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (input.operation === "runs")
|
|
145
|
+
return host.ledger(launchers(), Date.now() - LEDGER_WINDOW_MS);
|
|
146
|
+
// A one-shot's hidden definition is a run's record, not a task anyone filed.
|
|
147
|
+
const filed = () => {
|
|
148
|
+
const id = requiredString(input.task_id, "task_id");
|
|
149
|
+
if (definitions.get(id).kind === "subagent")
|
|
150
|
+
throw new Error(`${id} is a one-shot run's own definition; save without --task-id to file a task`);
|
|
151
|
+
return id;
|
|
152
|
+
};
|
|
120
153
|
if (input.operation === "save") {
|
|
121
154
|
const draft = await expandDraft(definitions, menu, input.task, callerSessionId);
|
|
122
155
|
if (input.task_id === undefined)
|
|
123
156
|
return definitions.create(draft, `session:${callerSessionId}`);
|
|
124
|
-
|
|
125
|
-
// A one-shot's hidden definition is a run's record, not a task anyone filed.
|
|
126
|
-
if (definitions.get(id).kind === "subagent")
|
|
127
|
-
throw new Error(`${id} is a one-shot run's own definition; save without --task-id to file a task`);
|
|
128
|
-
return definitions.update(id, draft);
|
|
157
|
+
return definitions.update(filed(), draft);
|
|
129
158
|
}
|
|
159
|
+
if (input.operation === "pause" || input.operation === "resume")
|
|
160
|
+
return host.setEnabled(filed(), input.operation === "resume");
|
|
161
|
+
if (input.operation === "archive")
|
|
162
|
+
return host.archive(filed());
|
|
130
163
|
if (input.operation === "run") {
|
|
131
164
|
// `--model ?`: the menu instead of a run, the one lookup the common case never pays.
|
|
132
165
|
if (record(input.launch)?.model === "?") {
|
|
133
166
|
const { source, models } = await host.models();
|
|
134
167
|
const head = source === "menu" ? "the operator's menu" : "the live catalog (no model pinned)";
|
|
135
|
-
return `${head} — --model takes a provider/id or a unique substring of one:\n${menuLines(models)}`;
|
|
168
|
+
return `${head} — --model takes a tier, a provider/id or a unique substring of one:\n${menuLines(models)}`;
|
|
136
169
|
}
|
|
137
170
|
const callbackMode = callbackModeOf(input);
|
|
138
171
|
if (Array.isArray(input.tasks)) {
|
|
@@ -149,8 +182,8 @@ export async function handleTask(host, definitions, store, raw, callerSessionId)
|
|
|
149
182
|
if (!entry)
|
|
150
183
|
throw new Error("invalid tasks[] entry");
|
|
151
184
|
resolved.push(entry.task_id === undefined
|
|
152
|
-
? await resolveDraft(definitions, menu, entry, callerSessionId)
|
|
153
|
-
: definitions.get(requiredString(entry.task_id, "task_id")));
|
|
185
|
+
? await resolveDraft(definitions, menu, notLead(entry), callerSessionId)
|
|
186
|
+
: notLead(definitions.get(requiredString(entry.task_id, "task_id"))));
|
|
154
187
|
}
|
|
155
188
|
const groupCallbackSessionId = input.callback === "none" ? null : callerSessionId;
|
|
156
189
|
const { group, runs } = host.runGroup(resolved, oneOf(input, "join", ["all", "first"]) ?? "all", callerSessionId, groupCallbackSessionId, callbackMode);
|
|
@@ -158,8 +191,8 @@ export async function handleTask(host, definitions, store, raw, callerSessionId)
|
|
|
158
191
|
}
|
|
159
192
|
const draft = input.task_id === undefined ? inlineDraft(input) : undefined;
|
|
160
193
|
const task = draft
|
|
161
|
-
? await resolveDraft(definitions, menu, draft, callerSessionId)
|
|
162
|
-
: definitions.get(requiredString(input.task_id, "task_id"));
|
|
194
|
+
? await resolveDraft(definitions, menu, notLead(draft), callerSessionId)
|
|
195
|
+
: notLead(definitions.get(requiredString(input.task_id, "task_id")));
|
|
163
196
|
const callbackSessionId = await callbackTarget(input, definitions, callerSessionId);
|
|
164
197
|
const run = host.run(task.id, null, "agent", null, {
|
|
165
198
|
invokedBySessionId: callerSessionId,
|
|
@@ -205,12 +238,12 @@ export async function handleTask(host, definitions, store, raw, callerSessionId)
|
|
|
205
238
|
if (typeof input.group_id === "string") {
|
|
206
239
|
const { group, members } = host.getGroup(input.group_id);
|
|
207
240
|
for (const member of members)
|
|
208
|
-
assertOwns(
|
|
241
|
+
assertOwns(member);
|
|
209
242
|
const cancelled = host.cancelGroup(group.id);
|
|
210
243
|
return summarizeGroup(cancelled, cancelled.memberRunIds.map((id) => host.getRun(id)));
|
|
211
244
|
}
|
|
212
245
|
const run = host.getRun(requiredString(input.run_id, "run_id"));
|
|
213
|
-
assertOwns(
|
|
246
|
+
assertOwns(run);
|
|
214
247
|
// cancel() only aborts; the row flips ~100 ms later. A receipt still saying
|
|
215
248
|
// `running` reads as "cancel failed", so wait for the flip, briefly.
|
|
216
249
|
host.cancel(run.id);
|
|
@@ -221,7 +254,7 @@ export async function handleTask(host, definitions, store, raw, callerSessionId)
|
|
|
221
254
|
// caller, decides whether the text steers, queues or resumes, so a status
|
|
222
255
|
// query never has to exist.
|
|
223
256
|
const run = host.getRun(requiredString(input.run_id, "run_id"));
|
|
224
|
-
assertOwns(
|
|
257
|
+
assertOwns(run);
|
|
225
258
|
const message = requiredString(input.message, "message");
|
|
226
259
|
if (isTerminal(run.state)) {
|
|
227
260
|
// A resumed run is a new run with its own callback.
|
|
@@ -254,23 +287,33 @@ function nameFrom(text) {
|
|
|
254
287
|
.find(Boolean) ?? "subagent";
|
|
255
288
|
return line.length > 60 ? `${line.slice(0, 59).trimEnd()}…` : line;
|
|
256
289
|
}
|
|
257
|
-
/** `launch.model` by name (docs/design/09-tasks-cli.md §Models)
|
|
258
|
-
*
|
|
259
|
-
* from, so the agent never
|
|
290
|
+
/** `launch.model` by name (docs/design/09-tasks-cli.md §Models): the one pin
|
|
291
|
+
* it names, a tier its first. Anything but a tier, one pin or an unpinned
|
|
292
|
+
* `provider/id` is refused with the lines to pick from, so the agent never
|
|
293
|
+
* guesses an id. A tier is never a substring: "cheap" matching an id would
|
|
294
|
+
* pick a pin the operator did not assign. */
|
|
260
295
|
function resolveModel(name, menu) {
|
|
261
296
|
const needle = name.trim().toLowerCase();
|
|
262
297
|
const full = (pin) => `${pin.provider}/${pin.id}`;
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
298
|
+
const pick = (pin) => ({ model: { provider: pin.provider, id: pin.id }, thinking: pin.thinking });
|
|
299
|
+
if (isModelTier(needle)) {
|
|
300
|
+
const pin = menu.find((p) => p.tier === needle);
|
|
301
|
+
if (pin)
|
|
302
|
+
return pick(pin);
|
|
303
|
+
throw new Error(`model "${name}": tier ${needle} is unassigned — the operator's menu:\n${menuLines(menu)}`);
|
|
304
|
+
}
|
|
305
|
+
const exact = menu.filter((pin) => full(pin).toLowerCase() === needle);
|
|
306
|
+
const hits = exact.length ? exact : menu.filter((pin) => full(pin).toLowerCase().includes(needle));
|
|
307
|
+
// One model pinned at several levels is still one model: its first row, as with a tier.
|
|
308
|
+
if (hits.length && hits.every((pin) => full(pin) === full(hits[0])))
|
|
309
|
+
return pick(hits[0]);
|
|
267
310
|
const slash = name.indexOf("/");
|
|
268
311
|
if (!hits.length && slash > 0 && slash < name.length - 1)
|
|
269
312
|
return { model: { provider: name.slice(0, slash), id: name.slice(slash + 1) } };
|
|
270
313
|
throw new Error(`model "${name}" matches ${String(hits.length)} of the menu:\n${menuLines(hits.length ? hits : menu)}`);
|
|
271
314
|
}
|
|
272
315
|
/** One pin per line, the only shape a menu is ever printed in. */
|
|
273
|
-
const menuLines = (menu) => menu.map((pin) => `${pin.
|
|
316
|
+
const menuLines = (menu) => menu.map((pin) => `${pin.tier ? `${pin.tier} · ` : ""}${pin.provider}/${pin.id}${pin.thinking ? ` · ${pin.thinking}` : ""}`).join("\n")
|
|
274
317
|
|| "(no model is pinned or available)";
|
|
275
318
|
/** A `prompt` shorthand becomes a fresh Agent action in the caller's own
|
|
276
319
|
* directory; everything the caller did spell out passes through to parseDraft. */
|
|
@@ -305,6 +348,7 @@ async function expandDraft(definitions, menu, raw, callerSessionId) {
|
|
|
305
348
|
const launch = record(action?.launch);
|
|
306
349
|
if (typeof launch?.model === "string") {
|
|
307
350
|
const { model, thinking } = resolveModel(launch.model, await menu());
|
|
351
|
+
// A pin's level only where the caller named none.
|
|
308
352
|
const resolved = { ...launch, model, ...(launch.thinking === undefined && thinking ? { thinking } : {}) };
|
|
309
353
|
draft = { ...draft, action: { ...record(draft.action), launch: resolved } };
|
|
310
354
|
}
|
|
@@ -324,12 +368,5 @@ async function resolveDraft(definitions, menu, raw, callerSessionId) {
|
|
|
324
368
|
if (draft.callback !== undefined || draft.callback_session_id !== undefined) {
|
|
325
369
|
throw new Error("an inline task draft cannot set callback; use the top-level callback / callback_session_id");
|
|
326
370
|
}
|
|
327
|
-
return definitions.create({ ...draft, trigger: { type: "manual" } }, `session:${callerSessionId}`, "subagent");
|
|
328
|
-
}
|
|
329
|
-
/** The session that launched a run controls it, and so does the run's own
|
|
330
|
-
* session; a `task` action's child inherits its parent's launcher. */
|
|
331
|
-
function assertOwns(callerSessionId, target) {
|
|
332
|
-
if (target.invokedBySessionId !== callerSessionId && target.targetSessionId !== callerSessionId) {
|
|
333
|
-
throw new Error("session does not own this run");
|
|
334
|
-
}
|
|
371
|
+
return definitions.create({ ...draft, trigger: { type: "manual" }, callback: { type: "none" } }, `session:${callerSessionId}`, "subagent");
|
|
335
372
|
}
|
package/dist/tasks/outbox.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
import { logger } from "../log.js";
|
|
4
4
|
import { MAX_DELIVERY_ATTEMPTS, retryDelay, undeliverable } from "./types.js";
|
|
5
5
|
const log = logger("tasks");
|
|
6
|
+
/** A result waiting on a lead's own running turn is asked again this often, not
|
|
7
|
+
* every tick: a wait spends no attempt, so the attempt backoff does not apply. */
|
|
8
|
+
const MILESTONE_WAIT_MS = 10_000;
|
|
9
|
+
/** The marks a delivered record carries: nothing left to retry. */
|
|
10
|
+
const DELIVERED = { callbackState: "delivered", callbackError: null, callbackNextAttemptAt: null };
|
|
6
11
|
/** The records a delivery names, from either side of the seam: a batch names
|
|
7
12
|
* every one of them, a group names itself, a message names its id. */
|
|
8
13
|
const recordIds = (origin) => {
|
|
@@ -24,6 +29,17 @@ export class Outbox {
|
|
|
24
29
|
* written only against the input visible in the recipient's transcript: Pi's
|
|
25
30
|
* queues are memory, so a resolved `systemInput` proves nothing. */
|
|
26
31
|
async deliver(sessionId, batch) {
|
|
32
|
+
const marked = batch.map((record) => ({ ...record, ...DELIVERED }));
|
|
33
|
+
const taken = this.kind.milestone?.(sessionId, () => this.kind.input(batch).text, () => { for (const record of marked)
|
|
34
|
+
this.kind.save(record); });
|
|
35
|
+
if (taken === "resumed")
|
|
36
|
+
for (const record of marked)
|
|
37
|
+
this.kind.changed(record);
|
|
38
|
+
if (taken === "wait")
|
|
39
|
+
for (const record of batch)
|
|
40
|
+
this.defer(record, MILESTONE_WAIT_MS);
|
|
41
|
+
if (taken === "resumed" || taken === "wait")
|
|
42
|
+
return;
|
|
27
43
|
const mine = batch.filter((record) => !this.delivering.has(this.kind.id(record)));
|
|
28
44
|
if (mine.length === 0)
|
|
29
45
|
return;
|
|
@@ -128,8 +144,8 @@ export class Outbox {
|
|
|
128
144
|
return true;
|
|
129
145
|
}
|
|
130
146
|
/** Busy target: try again shortly, and do not count it. */
|
|
131
|
-
defer(record) {
|
|
132
|
-
record.callbackNextAttemptAt = Date.now() +
|
|
147
|
+
defer(record, ms = 1000) {
|
|
148
|
+
record.callbackNextAttemptAt = Date.now() + ms;
|
|
133
149
|
this.kind.save(record);
|
|
134
150
|
}
|
|
135
151
|
/** Backs off like a failure, so an input never recorded is re-sent on a
|
|
@@ -142,10 +158,7 @@ export class Outbox {
|
|
|
142
158
|
this.kind.save(record);
|
|
143
159
|
}
|
|
144
160
|
delivered(record) {
|
|
145
|
-
record
|
|
146
|
-
record.callbackError = null;
|
|
147
|
-
record.callbackNextAttemptAt = null;
|
|
148
|
-
this.write(record);
|
|
161
|
+
this.write(Object.assign(record, DELIVERED));
|
|
149
162
|
}
|
|
150
163
|
/** A failed pass costs one attempt, whether it died before or after send. */
|
|
151
164
|
failed(record, error, counted) {
|
package/dist/tasks/routes.js
CHANGED
|
@@ -1,258 +1,28 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
}
|