@timqi/pier 0.2.0 → 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.
- package/dist/agent/listing.js +14 -10
- package/dist/agent/pi.js +2 -2
- package/dist/agent/roles.js +4 -3
- package/dist/channels/conversations.js +4 -4
- package/dist/channels/lark-outbound.js +5 -0
- package/dist/channels/lark.js +25 -5
- package/dist/channels/runtime.js +47 -2
- package/dist/channels/slack-outbound.js +7 -0
- package/dist/channels/slack-thread.js +65 -0
- package/dist/channels/slack.js +27 -72
- package/dist/cli.js +7 -2
- package/dist/core/chain.js +23 -4
- package/dist/core/identity.js +2 -2
- package/dist/core/inbound-file.js +9 -3
- package/dist/core/reply.js +17 -3
- package/dist/core/search.js +65 -0
- package/dist/core/types.js +2 -0
- package/dist/db.js +8 -0
- package/dist/drain.js +1 -1
- package/dist/main.js +21 -5
- package/dist/settings.js +1 -1
- package/dist/socket.js +1 -0
- package/dist/tasks/execution.js +9 -3
- package/dist/tasks/open-items.js +53 -35
- package/dist/tasks/service.js +22 -1
- package/dist/tasks/store.js +5 -2
- package/dist/tasks/types.js +0 -2
- package/dist/tools.js +10 -3
- package/dist/web/public/assets/{code-BZwvPani.js → code-CWT27KH1.js} +1 -1
- package/dist/web/public/assets/code-CWT27KH1.js.br +0 -0
- package/dist/web/public/assets/code-CWT27KH1.js.gz +0 -0
- package/dist/web/public/assets/{explorer-DDoVzMLQ.js → explorer-C4DhXFCm.js} +4 -4
- package/dist/web/public/assets/explorer-C4DhXFCm.js.br +0 -0
- package/dist/web/public/assets/explorer-C4DhXFCm.js.gz +0 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js +72 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js.br +0 -0
- package/dist/web/public/assets/highlight-CDSk2hRJ.js.gz +0 -0
- package/dist/web/public/assets/index-D0ZYdweB.js +14 -0
- package/dist/web/public/assets/index-D0ZYdweB.js.br +0 -0
- package/dist/web/public/assets/index-D0ZYdweB.js.gz +0 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css +2 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css.br +0 -0
- package/dist/web/public/assets/index-JN0Ic8K5.css.gz +0 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js +9 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js.br +0 -0
- package/dist/web/public/assets/settings-DSTeuIyc.js.gz +0 -0
- package/dist/web/public/index.html +3 -4
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/push.js +2 -2
- package/dist/web/server.js +2 -4
- package/dist/web/session-state.js +1 -2
- package/package.json +1 -1
- package/skills/pier-help/SKILL.md +6 -1
- package/skills/pier-search/SKILL.md +27 -0
- package/skills/pier-tasks/SKILL.md +10 -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.br +0 -0
- package/dist/web/public/assets/explorer-DDoVzMLQ.js.gz +0 -0
- package/dist/web/public/assets/highlight-5Bl8zZ8q.js +0 -72
- 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/index-BpFzCwlo.js +0 -7
- 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 +0 -2
- 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 +0 -8
- 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 +0 -9
- package/dist/web/public/assets/settings-Cv7rbeqf.js.br +0 -0
- package/dist/web/public/assets/settings-Cv7rbeqf.js.gz +0 -0
package/dist/core/reply.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// emphasis repair. Web chat and every IM adapter render these, so the wording
|
|
4
4
|
// and units live here.
|
|
5
5
|
import { replaceOutsideCode } from "./inbound-file.js";
|
|
6
|
-
import { TASK_RUN_STATES } from "./types.js";
|
|
6
|
+
import { NOT_IN_LEDGER, TASK_RUN_STATES } from "./types.js";
|
|
7
7
|
// The syntax is told in agent/roles.ts; this file parses it back.
|
|
8
8
|
/** Where a system input came from; wording every surface must spell the same. */
|
|
9
9
|
export function originLabel(origin) {
|
|
@@ -30,7 +30,7 @@ const LIFTABLE = /[\u201c\u201d"\u2018\u2019'()\uff08\uff09\u300c\u300d\u300e\u3
|
|
|
30
30
|
* `**“怎么做”**:` never closes (a spec hole, not a platform bug, and constant
|
|
31
31
|
* in model output). Moving the punctuation outside — `“**怎么做**”:` — lands
|
|
32
32
|
* the `**` against a letter. Fences and code spans are content, never touched. */
|
|
33
|
-
|
|
33
|
+
function cjkFriendly(markdown) {
|
|
34
34
|
return replaceOutsideCode(markdown, /\*\*(\S|\S[\s\S]*?\S)\*\*/g, ([whole, inner = ""]) => {
|
|
35
35
|
let lead = "";
|
|
36
36
|
let trail = "";
|
|
@@ -75,6 +75,11 @@ export const agoLabel = (ts, now) => {
|
|
|
75
75
|
export const runStatus = (r, now) => r.finishedAt === null ? `${r.state} ${relTime(r.queuedAt, now)}` : `${r.state} ${agoLabel(r.finishedAt, now)}`;
|
|
76
76
|
/** A lead's launches by state, in the states' own order: "2 running, 1 failed". */
|
|
77
77
|
export const workerCounts = (workers) => TASK_RUN_STATES.filter((s) => workers[s] > 0).map((s) => `${String(workers[s])} ${s}`).join(", ") || "none";
|
|
78
|
+
/** A run behind an open item, `run <id8>… <state> <age>`, a lead's with its workers;
|
|
79
|
+
* web/ui/turn-activity.ts `linkRuns` reads the token back. */
|
|
80
|
+
export const openRunText = (r, now) => r.state === NOT_IN_LEDGER
|
|
81
|
+
? `run ${r.runId} — ${NOT_IN_LEDGER}`
|
|
82
|
+
: `run ${r.runId.length > 8 ? `${r.runId.slice(0, 8)}…` : r.runId} ${runStatus(r, now)}${r.workers ? ` · workers: ${workerCounts(r.workers)}` : ""}`;
|
|
78
83
|
/** 1200 → "1.2K", 12_000 → "12K" — absolute token counts read badly inline. */
|
|
79
84
|
export const compact = (n) => {
|
|
80
85
|
if (n < 1000)
|
|
@@ -170,7 +175,16 @@ export function splitReply(rawMarkdown, meta) {
|
|
|
170
175
|
}
|
|
171
176
|
/** For rendering mid-turn: everything `splitReply` repairs, minus the
|
|
172
177
|
* next-step block, which is only one at the very end of a turn. */
|
|
173
|
-
export const streamBody = (markdown) => cjkFriendly(
|
|
178
|
+
export const streamBody = (markdown) => cjkFriendly(unmarked(markdown));
|
|
179
|
+
const unmarked = (markdown) => replaceOutsideCode(markdown.replace(SILENT, ""), MARKER, () => "").trim();
|
|
180
|
+
/** What a message says with Pier's markup off — `<silent>`, the open-item
|
|
181
|
+
* markers, the next-step block — and otherwise untouched: search cuts its
|
|
182
|
+
* snippets from this. */
|
|
183
|
+
export function saidText(markdown) {
|
|
184
|
+
const body = unmarked(markdown);
|
|
185
|
+
const m = BLOCK.exec(body);
|
|
186
|
+
return m?.[1] ? body.slice(0, m.index).trimEnd() : body;
|
|
187
|
+
}
|
|
174
188
|
/** Lines a blank line does not necessarily separate (a loose list is still one
|
|
175
189
|
* list). Over-matching is fine: one boundary too few costs only a repaint. */
|
|
176
190
|
const CONTINUES = /^(?:\s|[-*+>]|\d+[.)])/;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// `pier search`: the `/search` route's host and the verb's argv, on the search
|
|
2
|
+
// seam core declares (`AgentFactory.search`); root keeps only the dispatch, as
|
|
3
|
+
// for `pier task` and `pier web`. Argv shape is checked here, the host validates
|
|
4
|
+
// the params, and its text reaches the shell verbatim (08 §Failure lines).
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { readableTitle } from "./identity.js";
|
|
7
|
+
const USAGE = "usage: pier search <q...> [--limit N] [--json]";
|
|
8
|
+
// The match marks are the palette's to paint; a line is one line.
|
|
9
|
+
const flat = (text) => text.replaceAll("\u0001", "").replaceAll("\u0002", "").replace(/\s+/g, " ").trim();
|
|
10
|
+
// A title may be a whole first message; the snippet is what the line is for.
|
|
11
|
+
const TITLE_CHARS = 30;
|
|
12
|
+
const clip = (text) => {
|
|
13
|
+
const chars = [...text];
|
|
14
|
+
return chars.length > TITLE_CHARS ? `${chars.slice(0, TITLE_CHARS - 1).join("").trimEnd()}…` : text;
|
|
15
|
+
};
|
|
16
|
+
// sv-SE spells local time as YYYY-MM-DD HH:MM.
|
|
17
|
+
const when = (at) => new Date(at).toLocaleString("sv-SE", { dateStyle: "short", timeStyle: "short" });
|
|
18
|
+
const processIo = {
|
|
19
|
+
stdout: (line) => process.stdout.write(`${line}\n`),
|
|
20
|
+
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
21
|
+
};
|
|
22
|
+
/** `/search`'s params to hits, each titled with its session's name as it is
|
|
23
|
+
* now; a session gone from disk is named by its id. */
|
|
24
|
+
export const searchSessions = (factory) => async (params) => {
|
|
25
|
+
const { q, limit = 20 } = (params ?? {});
|
|
26
|
+
const query = typeof q === "string" ? q.trim() : "";
|
|
27
|
+
if (!query)
|
|
28
|
+
throw new Error("q must be non-empty words to search for");
|
|
29
|
+
if (!Number.isInteger(limit))
|
|
30
|
+
throw new Error("limit must be an integer");
|
|
31
|
+
const hits = await factory.search(query, Math.min(50, Math.max(1, limit)));
|
|
32
|
+
return {
|
|
33
|
+
hits: await Promise.all(hits.map(async (hit) => ({ ...hit, title: readableTitle((await factory.find(hit.sessionId))?.title) ?? hit.sessionId }))),
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
/** One line per hit, or the answer's object with `--json`. */
|
|
37
|
+
export async function runSearchCli(argv, post, io = processIo) {
|
|
38
|
+
let v = {};
|
|
39
|
+
let words = [];
|
|
40
|
+
try {
|
|
41
|
+
({ values: v, positionals: words } = parseArgs({ args: argv, allowPositionals: true, strict: true, options: { limit: { type: "string" }, json: { type: "boolean" } } }));
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
io.stderr(`search: ${err instanceof Error ? err.message : String(err)}\n${USAGE}`);
|
|
45
|
+
return 2;
|
|
46
|
+
}
|
|
47
|
+
if (!words.length) {
|
|
48
|
+
io.stderr(`search: say what to search for\n${USAGE}`);
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
const { status, body } = await post({ q: words.join(" "), ...(v.limit === undefined ? {} : { limit: Number(v.limit) }) });
|
|
52
|
+
if (status !== 200 || !body.result) {
|
|
53
|
+
io.stderr(`search: ${body.error ?? `socket answered ${String(status)}`}`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
const { hits } = body.result;
|
|
57
|
+
if (v.json)
|
|
58
|
+
io.stdout(JSON.stringify(body.result));
|
|
59
|
+
else if (!hits.length)
|
|
60
|
+
io.stdout("no hits");
|
|
61
|
+
else
|
|
62
|
+
for (const hit of hits)
|
|
63
|
+
io.stdout(`${hit.sessionId} · ${clip(flat(hit.title))} · ${hit.role} · ${when(hit.at)}: ${flat(hit.snippet)}`);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
package/dist/core/types.js
CHANGED
|
@@ -26,6 +26,8 @@ export const CHAIN_FULL_TOKENS = 60_000;
|
|
|
26
26
|
/** Declared here, not in tasks/, because `BackgroundRun`, the `task-status` event core carries, names it.
|
|
27
27
|
* In order: the Console lists them so, and a worker count reads so. */
|
|
28
28
|
export const TASK_RUN_STATES = ["queued", "running", "succeeded", "failed", "cancelled", "interrupted", "skipped"];
|
|
29
|
+
/** The `state` of a run an open item names that the ledger no longer holds; its `name` is the id. */
|
|
30
|
+
export const NOT_IN_LEDGER = "not in the ledger";
|
|
29
31
|
/** How much of a tool result any surface ever shows. A transcript replay
|
|
30
32
|
* carries no more than that: a session's tool output is most of its history
|
|
31
33
|
* payload, and the bytes past this point were downloaded to be sliced off. */
|
package/dist/db.js
CHANGED
|
@@ -348,6 +348,14 @@ const MIGRATIONS = [
|
|
|
348
348
|
`
|
|
349
349
|
ALTER TABLE session_state DROP COLUMN sort;
|
|
350
350
|
ALTER TABLE session_state DROP COLUMN closed;
|
|
351
|
+
`,
|
|
352
|
+
// 33 — columns written and never read.
|
|
353
|
+
`
|
|
354
|
+
ALTER TABLE session_state DROP COLUMN cwd;
|
|
355
|
+
ALTER TABLE session_state DROP COLUMN project_sort;
|
|
356
|
+
ALTER TABLE conversations DROP COLUMN updated_at;
|
|
357
|
+
ALTER TABLE restart_ledger DROP COLUMN created_at;
|
|
358
|
+
ALTER TABLE push_identity DROP COLUMN created_at;
|
|
351
359
|
`,
|
|
352
360
|
];
|
|
353
361
|
/** `BEGIN IMMEDIATE`: taking the write lock up front turns a race with another
|
package/dist/drain.js
CHANGED
|
@@ -15,7 +15,7 @@ export class RestartLedger {
|
|
|
15
15
|
this.db = db;
|
|
16
16
|
}
|
|
17
17
|
record(entry) {
|
|
18
|
-
this.db.prepare("INSERT INTO restart_ledger (channel_id, conversation_id, note
|
|
18
|
+
this.db.prepare("INSERT INTO restart_ledger (channel_id, conversation_id, note) VALUES (?, ?, ?)").run(entry.channelId, entry.conversationId, entry.note);
|
|
19
19
|
}
|
|
20
20
|
list() {
|
|
21
21
|
const rows = this.db.prepare("SELECT id, channel_id, conversation_id, note FROM restart_ledger ORDER BY id").all();
|
package/dist/main.js
CHANGED
|
@@ -20,13 +20,16 @@ import { ChannelRuntime } from "./channels/runtime.js";
|
|
|
20
20
|
import { MainChain } from "./core/chain.js";
|
|
21
21
|
import { EventHub } from "./core/hub.js";
|
|
22
22
|
import { splitSpeaker } from "./core/identity.js";
|
|
23
|
+
import { saidText } from "./core/reply.js";
|
|
23
24
|
import { pierDb } from "./db.js";
|
|
24
25
|
import { deliverLedger, drainForRestart, RestartLedger } from "./drain.js";
|
|
25
26
|
import { surfacePrompt } from "./agent/roles.js";
|
|
26
27
|
import { Router } from "./core/router.js";
|
|
28
|
+
import { searchSessions } from "./core/search.js";
|
|
27
29
|
import { acquireInstanceLock } from "./lock.js";
|
|
28
30
|
import { parseWebParams, runWeb } from "./websearch/run.js";
|
|
29
31
|
import { logger } from "./log.js";
|
|
32
|
+
import { runResultText, runSource } from "./tasks/callbacks.js";
|
|
30
33
|
import { openItemsStatus } from "./tasks/open-items.js";
|
|
31
34
|
import { registerTaskRoutes } from "./tasks/routes.js";
|
|
32
35
|
import { TaskService } from "./tasks/service.js";
|
|
@@ -100,8 +103,9 @@ const factory = new PiAgentFactory(
|
|
|
100
103
|
() => surfacePrompt({ boardsDir: defaultBoardsDir(), publicUrl: settings.get().publicUrl }),
|
|
101
104
|
// Documents Pier's own tools, so it loads only inside a Pier session.
|
|
102
105
|
[skillsDir], new CredentialStore(db, secrets), piConfig, () => settings.get().modelMenu, () => settings.get(), () => settings.get().titleModel,
|
|
103
|
-
// Transcripts carry the speaker header core wrote for the model
|
|
104
|
-
|
|
106
|
+
// Transcripts carry the speaker header core wrote for the model, and replies
|
|
107
|
+
// the chat markup core strips before anyone sees them.
|
|
108
|
+
new IndexedListing(undefined, undefined, (text) => splitSpeaker(text).text, saidText), (id) => taskStore.roleOf(id));
|
|
105
109
|
const hub = new EventHub();
|
|
106
110
|
const router = new Router(hub, (key) => {
|
|
107
111
|
// Web conversation ids are session ids; an IM id is a chat, resolved through
|
|
@@ -132,6 +136,17 @@ tasks = new TaskService(taskStore, factory, router, hub, {
|
|
|
132
136
|
},
|
|
133
137
|
},
|
|
134
138
|
continuous: chain,
|
|
139
|
+
// `channels` is assigned below, before any run can settle.
|
|
140
|
+
designLead: (run, state) => {
|
|
141
|
+
if (!run.targetSessionId)
|
|
142
|
+
return;
|
|
143
|
+
void channels.designLead({
|
|
144
|
+
sessionId: run.targetSessionId,
|
|
145
|
+
name: run.context.definition.name,
|
|
146
|
+
origin: { kind: "task-callback", taskId: run.taskId, runId: run.id, sourceSessionId: run.targetSessionId, source: runSource(run), state: run.state },
|
|
147
|
+
text: state === "failed" ? run.error ?? run.state : runResultText(run),
|
|
148
|
+
}, state).catch((err) => log.error(`lead ${run.targetSessionId ?? "?"}: its ${state} state did not reach the home chat`, err));
|
|
149
|
+
},
|
|
135
150
|
});
|
|
136
151
|
const configurationSync = configSyncTask(tasks, configSync);
|
|
137
152
|
const managedTools = new ManagedTools();
|
|
@@ -149,7 +164,7 @@ const control = createControl({
|
|
|
149
164
|
router, factory, conversations, store: channelStore,
|
|
150
165
|
modelMenu: () => settings.get().modelMenu,
|
|
151
166
|
});
|
|
152
|
-
const channels = new ChannelRuntime(channelStore, router, chain, control);
|
|
167
|
+
const channels = new ChannelRuntime(channelStore, router, chain, control, conversations);
|
|
153
168
|
/** The head answers in the home chat while its adapter runs (docs/design/11), else on
|
|
154
169
|
* the web with Web Push; an older member gets no home key, or an adapter's start
|
|
155
170
|
* would re-key it over the head. Any other session: its conversations row. */
|
|
@@ -348,6 +363,7 @@ servePier({
|
|
|
348
363
|
note(`done ${JSON.stringify(result.details)}`);
|
|
349
364
|
return result;
|
|
350
365
|
},
|
|
366
|
+
search: searchSessions(factory),
|
|
351
367
|
// Live in the router, or on disk: the same two places a callback target is looked for.
|
|
352
368
|
knows: async (id) => router.stateOf(id) !== undefined || (await factory.find(id)) !== undefined,
|
|
353
369
|
login: () => `${settings.get().publicUrl || `http://127.0.0.1:${String(port)}`}/login/${auth.mintLink()}`,
|
|
@@ -383,8 +399,8 @@ const shutdown = (stopTasks = true) => {
|
|
|
383
399
|
// 90-second wait for SIGKILL.
|
|
384
400
|
setTimeout(() => process.exit(0), 3000).unref();
|
|
385
401
|
stopEviction();
|
|
386
|
-
//
|
|
387
|
-
//
|
|
402
|
+
// Aborted runs end interrupted, as boot recovery records them; the drain path
|
|
403
|
+
// leaves the ones it outlasted to that recovery.
|
|
388
404
|
if (stopTasks)
|
|
389
405
|
tasks.stop();
|
|
390
406
|
void channels.stop().finally(() => {
|
package/dist/settings.js
CHANGED
|
@@ -92,7 +92,7 @@ export function normalizeModelRef(raw) {
|
|
|
92
92
|
}
|
|
93
93
|
/** Shape only: tools.ts owns the catalog, and an unknown name is ignored
|
|
94
94
|
* there, so a downgrade cannot lose a setting it cannot explain. */
|
|
95
|
-
|
|
95
|
+
function normalizeTools(raw) {
|
|
96
96
|
return normalizeNames(raw);
|
|
97
97
|
}
|
|
98
98
|
function normalizeNames(raw) {
|
package/dist/socket.js
CHANGED
package/dist/tasks/execution.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import { logger } from "../log.js";
|
|
4
4
|
import { settleCallback } from "./callbacks.js";
|
|
5
5
|
import { runBash } from "./command.js";
|
|
6
|
+
import { INTERRUPTED } from "./store.js";
|
|
6
7
|
const log = logger("tasks");
|
|
8
|
+
const SHUTDOWN = Symbol("shutdown");
|
|
7
9
|
export class TaskExecution {
|
|
8
10
|
store;
|
|
9
11
|
definitions;
|
|
@@ -21,9 +23,11 @@ export class TaskExecution {
|
|
|
21
23
|
start(run) {
|
|
22
24
|
void this.execute(run);
|
|
23
25
|
}
|
|
26
|
+
/** Shutdown: the abort still kills each detached bash group, which boot
|
|
27
|
+
* recovery could not, but the run ends as that recovery would record it. */
|
|
24
28
|
stop() {
|
|
25
29
|
for (const controller of this.controllers.values())
|
|
26
|
-
controller.abort();
|
|
30
|
+
controller.abort(SHUTDOWN);
|
|
27
31
|
}
|
|
28
32
|
cancel(id) {
|
|
29
33
|
log.info(`run ${id} cancel requested`);
|
|
@@ -75,9 +79,11 @@ export class TaskExecution {
|
|
|
75
79
|
}
|
|
76
80
|
catch (error) {
|
|
77
81
|
// A killed child reports `exited null`; report why we aborted instead.
|
|
82
|
+
// The first abort's reason wins: a shutdown after a user cancel stays cancelled.
|
|
78
83
|
const aborted = controller.signal.aborted;
|
|
79
|
-
|
|
80
|
-
run.
|
|
84
|
+
const interrupted = aborted && !timedOut && controller.signal.reason === SHUTDOWN;
|
|
85
|
+
run.state = aborted ? (timedOut ? "failed" : interrupted ? "interrupted" : "cancelled") : "failed";
|
|
86
|
+
run.error = timedOut ? "task timed out" : interrupted ? INTERRUPTED : aborted ? "cancelled" : String(error);
|
|
81
87
|
cause = timedOut ? run.error : error;
|
|
82
88
|
}
|
|
83
89
|
finally {
|
package/dist/tasks/open-items.js
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
// The continuous conversation's open items in task-run words: main's markers
|
|
2
2
|
// joined to the run ledger by session, a lead's workers counted by state, and
|
|
3
3
|
// the one text every surface shows (docs/design/10-continuous-session.md#open-items).
|
|
4
|
-
import { openItemMarkers,
|
|
5
|
-
import { LEDGER_WINDOW_MS, TASK_RUN_STATES } from "../core/types.js";
|
|
4
|
+
import { openItemMarkers, openRunText } from "../core/reply.js";
|
|
5
|
+
import { LEDGER_WINDOW_MS, NOT_IN_LEDGER, TASK_RUN_STATES } from "../core/types.js";
|
|
6
6
|
import { logger } from "../log.js";
|
|
7
|
-
import { NOT_IN_LEDGER } from "./types.js";
|
|
8
7
|
const log = logger("tasks");
|
|
9
8
|
const IN_FLIGHT = new Set(["queued", "running"]);
|
|
10
9
|
/** `members`: the chain, newest first; `designs`: the open designs of sessions not closed. */
|
|
11
10
|
export function openItems(tasks, router, members, designs, now) {
|
|
12
11
|
const since = now - LEDGER_WINDOW_MS;
|
|
13
12
|
const runs = members.length ? tasks.ledger(members, since) : [];
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
workers[w.state] += 1;
|
|
21
|
-
}
|
|
22
|
-
return { ...r, workers };
|
|
23
|
-
};
|
|
13
|
+
const awaiting = new Set(designs.flatMap((d) => (d.targetSessionId ? [d.targetSessionId] : [])));
|
|
14
|
+
const statusOf = (named) => named.some((r) => IN_FLIGHT.has(r.state) || (r.targetSessionId && router.stateOf(r.targetSessionId) === "streaming"))
|
|
15
|
+
? "running"
|
|
16
|
+
: named.some((r) => r.targetSessionId && awaiting.has(r.targetSessionId))
|
|
17
|
+
? "waiting on you"
|
|
18
|
+
: named.length && named.every((r) => r.state === "succeeded") ? "pending release" : "waiting on you";
|
|
24
19
|
// An item names a session through any of its runs: the session's newest run stands for it.
|
|
25
20
|
const tracked = new Set();
|
|
26
21
|
const items = tasks.store.openItems().map((row) => {
|
|
@@ -28,15 +23,36 @@ export function openItems(tasks, router, members, designs, now) {
|
|
|
28
23
|
const session = runs.find((r) => r.runId === id)?.targetSessionId ?? tasks.store.getRun(id)?.targetSessionId ?? null;
|
|
29
24
|
tracked.add(session ?? id);
|
|
30
25
|
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 };
|
|
26
|
+
return run ?? { runId: id, name: id, state: NOT_IN_LEDGER, targetSessionId: session, cwd: null, queuedAt: 0, finishedAt: null };
|
|
34
27
|
}).filter((r, i, all) => all.findIndex((o) => o.runId === r.runId) === i);
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
return { problem: row.problem, stage: row.stage, runs: named, status: statusOf(named) };
|
|
29
|
+
});
|
|
30
|
+
const unlisted = runs.filter((r) => IN_FLIGHT.has(r.state) && !tracked.has(r.targetSessionId ?? r.runId));
|
|
31
|
+
for (const r of unlisted)
|
|
32
|
+
tracked.add(r.targetSessionId ?? r.runId);
|
|
33
|
+
const unheld = designs.filter((d) => !tracked.has(d.targetSessionId ?? d.runId)).map((d) => {
|
|
34
|
+
const named = [runs.find((r) => d.targetSessionId && r.targetSessionId === d.targetSessionId) ?? d];
|
|
35
|
+
return { problem: d.name, stage: "", runs: named, status: statusOf(named) };
|
|
37
36
|
});
|
|
38
|
-
const
|
|
39
|
-
|
|
37
|
+
const all = [...items, ...unheld];
|
|
38
|
+
// Every shown lead's workers in one read: a ledger call per lead grows with the list.
|
|
39
|
+
const leads = tasks.store.leads();
|
|
40
|
+
const leadOf = (r) => (r.state !== NOT_IN_LEDGER && r.targetSessionId && leads.has(r.targetSessionId) ? r.targetSessionId : null);
|
|
41
|
+
const shown = new Set([...all.flatMap((i) => i.runs), ...unlisted].flatMap((r) => leadOf(r) ?? []));
|
|
42
|
+
const counts = new Map([...shown].map((id) => [id, Object.fromEntries(TASK_RUN_STATES.map((s) => [s, 0]))]));
|
|
43
|
+
for (const w of shown.size ? tasks.store.ledgerRuns([...shown], since) : []) {
|
|
44
|
+
const c = w.invokedBySessionId ? counts.get(w.invokedBySessionId) : undefined;
|
|
45
|
+
if (c)
|
|
46
|
+
c[w.state] += 1;
|
|
47
|
+
}
|
|
48
|
+
const withWorkers = (r) => {
|
|
49
|
+
const workers = counts.get(leadOf(r) ?? "");
|
|
50
|
+
return workers ? { ...r, workers: { ...workers } } : r;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
items: all.map((i) => ({ ...i, runs: i.runs.map(withWorkers) })),
|
|
54
|
+
unlisted: unlisted.map(withWorkers),
|
|
55
|
+
};
|
|
40
56
|
}
|
|
41
57
|
/** The head's reply's markers, written; answers whether a row changed. */
|
|
42
58
|
export function recordOpenItems(store, text, now) {
|
|
@@ -45,26 +61,28 @@ export function recordOpenItems(store, text, now) {
|
|
|
45
61
|
log.warn(`open items: dropped a marker with no problem text: ${marker}`);
|
|
46
62
|
return markers.length > 0 && store.markOpenItems(markers, now) > 0;
|
|
47
63
|
}
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/** The one string every surface shows for the open items: `/status` and the seed.
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
/** An `unlisted` run's stage: main named it in no item. */
|
|
65
|
+
const UNLISTED = "not on the list";
|
|
66
|
+
/** `status` is an item's, or `queued` for an unlisted run not started, as the panel tags it. */
|
|
67
|
+
const itemLine = (i, now) => `- ${i.problem}${i.stage ? ` — ${i.stage}` : ""} (${i.status})${i.runs.map((r) => ` · ${openRunText(r, now)}`).join("")}`;
|
|
68
|
+
/** The one string every surface shows for the open items: `/status` and the seed.
|
|
69
|
+
* Grouped as the status panel groups them (web/ui/drawer.ts): what waits on the user first. */
|
|
70
|
+
function renderOpenItems({ items, unlisted }, now) {
|
|
71
|
+
if (!items.length && !unlisted.length)
|
|
55
72
|
return "Nothing open.";
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
73
|
+
const waiting = items.filter((i) => i.status !== "running").map((i) => itemLine(i, now));
|
|
74
|
+
const running = [
|
|
75
|
+
...items.filter((i) => i.status === "running").map((i) => itemLine(i, now)),
|
|
76
|
+
...unlisted.map((r) => itemLine({ problem: r.name, stage: UNLISTED, runs: [r], status: r.state === "queued" ? "queued" : "running" }, now)),
|
|
77
|
+
];
|
|
59
78
|
return [
|
|
60
|
-
...(
|
|
61
|
-
...(
|
|
62
|
-
...(decide.length ? ["Designs for you to finalize", ...decide] : []),
|
|
79
|
+
...(waiting.length ? ["Waiting on you", ...waiting] : []),
|
|
80
|
+
...(running.length ? ["In progress", ...running] : []),
|
|
63
81
|
].join("\n");
|
|
64
82
|
}
|
|
65
|
-
/** `/status`'s card: the text, and run id → session id for every
|
|
83
|
+
/** `/status`'s card: the text, and run id → session id for every run it names that has one. */
|
|
66
84
|
export function openItemsStatus(open, now) {
|
|
67
|
-
const sessions = Object.fromEntries([...open.items.flatMap((i) => i.runs), ...open.
|
|
85
|
+
const sessions = Object.fromEntries([...open.items.flatMap((i) => i.runs), ...open.unlisted]
|
|
68
86
|
.flatMap((r) => (r.targetSessionId ? [[r.runId, r.targetSessionId]] : [])));
|
|
69
87
|
return { text: renderOpenItems(open, now), sessions };
|
|
70
88
|
}
|
package/dist/tasks/service.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
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 { DESIGN_FINAL, MILESTONE, runCwd, settleCallback, 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";
|
|
@@ -138,6 +138,26 @@ export class TaskService {
|
|
|
138
138
|
else
|
|
139
139
|
log.info(`lead ${sessionId}: Design final: outside any run, recorded as run ${run.id} for ${run.callbackSessionId}`);
|
|
140
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
|
+
}
|
|
141
161
|
}
|
|
142
162
|
start(tickMs = 1000) {
|
|
143
163
|
if (this.timer)
|
|
@@ -445,6 +465,7 @@ export class TaskService {
|
|
|
445
465
|
resolve(run);
|
|
446
466
|
this.waiters.delete(run.id);
|
|
447
467
|
this.groups.onSettled(run);
|
|
468
|
+
this.designLead(run);
|
|
448
469
|
}
|
|
449
470
|
backgroundRun(run) {
|
|
450
471
|
return {
|
package/dist/tasks/store.js
CHANGED
|
@@ -6,6 +6,8 @@ const clamp = (limit, cap) => Math.min(Math.max(limit, 1), cap);
|
|
|
6
6
|
/** Kept unmatched probes per watch: one page of history, which is all any
|
|
7
7
|
* surface lists of a probe that found nothing. */
|
|
8
8
|
const KEPT_PROBES = 50;
|
|
9
|
+
/** The error of a run a restart or shutdown stopped: not a result, not a cancel. */
|
|
10
|
+
export const INTERRUPTED = "Pier restarted while the run was active";
|
|
9
11
|
export class TaskStore {
|
|
10
12
|
db;
|
|
11
13
|
/** Every query below is a fixed string, so each is compiled once. */
|
|
@@ -194,8 +196,9 @@ export class TaskStore {
|
|
|
194
196
|
}
|
|
195
197
|
/** What a milestone resumes, and whose supervisor it reports to. */
|
|
196
198
|
latestRunForTarget(sessionId) {
|
|
199
|
+
// Two runs queued in one millisecond are common (a turn and the run it records); ids are random, rowid is insertion order.
|
|
197
200
|
return this.#one(`
|
|
198
|
-
SELECT json FROM task_runs WHERE json_extract(json, '$.targetSessionId') = ? ORDER BY queued_at DESC,
|
|
201
|
+
SELECT json FROM task_runs WHERE json_extract(json, '$.targetSessionId') = ? ORDER BY queued_at DESC, rowid DESC LIMIT 1
|
|
199
202
|
`, sessionId);
|
|
200
203
|
}
|
|
201
204
|
/** Runs in flight whose result is owed to the session, their own or their
|
|
@@ -316,7 +319,7 @@ export class TaskStore {
|
|
|
316
319
|
interruptRunning(now = Date.now()) {
|
|
317
320
|
return this.#many("SELECT json FROM task_runs WHERE state IN ('queued', 'running')").map((run) => {
|
|
318
321
|
run.state = "interrupted";
|
|
319
|
-
run.error =
|
|
322
|
+
run.error = INTERRUPTED;
|
|
320
323
|
run.finishedAt = now;
|
|
321
324
|
if (run.callbackSessionId)
|
|
322
325
|
run.callbackState = "pending";
|
package/dist/tasks/types.js
CHANGED
|
@@ -17,5 +17,3 @@ export const isTerminal = (state) => state === "succeeded" ||
|
|
|
17
17
|
/** The role a fresh run's session is created with and keeps: a lead's, or a
|
|
18
18
|
* worker's when a session launched it; a cron or watch run's has none. */
|
|
19
19
|
export const createdRole = ({ context: { definition: { action } }, invokedBySessionId }) => action.type === "agent" && action.launch?.role === "lead" ? "lead" : invokedBySessionId !== null ? "worker" : undefined;
|
|
20
|
-
/** The `state` of a run an item names that the ledger no longer holds; its `name` is the id. */
|
|
21
|
-
export const NOT_IN_LEDGER = "not in the ledger";
|
package/dist/tools.js
CHANGED
|
@@ -392,6 +392,8 @@ export class ManagedTools {
|
|
|
392
392
|
#fetch;
|
|
393
393
|
#root;
|
|
394
394
|
#lock;
|
|
395
|
+
/** Why a tool is absent lives only here: `ubix list` never names one that failed to install. */
|
|
396
|
+
#lastSync;
|
|
395
397
|
#db;
|
|
396
398
|
constructor(options = {}) {
|
|
397
399
|
this.#exec = options.exec ?? spawnExec;
|
|
@@ -459,7 +461,8 @@ export class ManagedTools {
|
|
|
459
461
|
// The only thing that changes what `list` answers; drop the memo both sides.
|
|
460
462
|
this.#listed = undefined;
|
|
461
463
|
try {
|
|
462
|
-
|
|
464
|
+
this.#lastSync = await this.#converge(tools, customTools, fence);
|
|
465
|
+
return this.#lastSync;
|
|
463
466
|
}
|
|
464
467
|
finally {
|
|
465
468
|
this.#listed = undefined;
|
|
@@ -527,8 +530,12 @@ export class ManagedTools {
|
|
|
527
530
|
}
|
|
528
531
|
return base.map((entry) => {
|
|
529
532
|
const state = states.find((s) => s.name === entry.name);
|
|
530
|
-
if (!state)
|
|
531
|
-
|
|
533
|
+
if (!state) {
|
|
534
|
+
if (!entry.enabled)
|
|
535
|
+
return entry;
|
|
536
|
+
const synced = this.#lastSync?.entries.find((e) => e.name === entry.name);
|
|
537
|
+
return withBinary(entry, { error: synced?.error ?? "not installed — the last sync did not reach it" });
|
|
538
|
+
}
|
|
532
539
|
const gone = state.installed === true && state.exists === false;
|
|
533
540
|
// `npm:` lands in fnm's node prefix and `pixi:` in its own: installed,
|
|
534
541
|
// but not where Pier's PATH points, and the row has to say so.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{J as e,r as t}from"./highlight-
|
|
1
|
+
import{J as e,r as t}from"./highlight-CDSk2hRJ.js";var n=2e4;function r(e){let t=e.split(`
|
|
2
2
|
`);return t.at(-1)===``&&t.pop(),t.map((e,t)=>({nums:[t+1],text:e,tone:``}))}function i(t,r){let i=t.length<=n?r:null,o=Math.max(2,...t.map(e=>e.nums.reduce((e,t)=>Math.max(e,String(t).length),0))),s=e(`div`,`py-2 font-mono text-[12px] leading-[1.5]`);for(let n of t){let t=e(`div`,`flex pl-2 pr-4 ${n.tone===`add`?`bg-emerald-50`:n.tone===`del`?`bg-red-50`:``}`);for(let r of n.nums){let n=e(`span`,`flex-none select-none pr-1.5 text-right text-neutral-300`,r===``?``:String(r));n.style.width=`calc(${o}ch + 0.375rem)`,t.append(n)}let r=n.nums.at(-1);typeof r==`number`&&(t.dataset.line=String(r)),t.append(a(n,i)),s.append(t)}return s}function a(n,r){let i=`min-w-0 flex-1 whitespace-pre-wrap [overflow-wrap:anywhere]`;if(!n.mark||n.mark[0]>=n.mark[1]){let e=t(n.text,r);return e.className=i,e}let[a,o]=n.mark,s=t(n.text.slice(a,o),r);return s.className=`rounded-xs ${n.tone===`add`?`bg-emerald-200/80`:`bg-red-200/70`}`,e(`span`,i,t(n.text.slice(0,a),r),s,t(n.text.slice(o),r))}export{r as n,i as t};
|
|
Binary file
|
|
Binary file
|