@kingcrab/pi-imessage 1.0.0 → 1.1.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 +38 -46
- package/dist/agent/chats.js +21 -0
- package/dist/agent/commands.js +66 -38
- package/dist/agent/direct-send.js +1 -0
- package/dist/agent/health.js +1 -0
- package/dist/agent/progress.js +43 -5
- package/dist/agent/replies.js +1 -0
- package/dist/agent/run.js +2 -1
- package/dist/agent/scheduling.js +8 -7
- package/dist/extensions/browser.js +1 -1
- package/dist/extensions/memory.js +1 -1
- package/dist/extensions/message.js +1 -1
- package/dist/extensions/read-image.js +1 -1
- package/dist/extensions/restart.js +100 -0
- package/dist/extensions/schedules.js +1 -0
- package/dist/extensions/subagent.js +1 -1
- package/dist/extensions/web.js +4 -3
- package/dist/extensions/workspace.js +1 -1
- package/dist/main.js +10 -0
- package/dist/web/app.js +81 -3
- package/dist/web/page.js +3 -2
- package/dist/web/schedules.js +4 -15
- package/dist/web/server.js +37 -3
- package/dist/web/style.css +34 -0
- package/dist/web/tasks.js +72 -38
- package/docs/adr/0048-quiet-attachment-reads.md +11 -0
- package/docs/adr/0049-per-chat-progress-notices.md +21 -0
- package/docs/adr/0052-host-owned-self-restart.md +53 -0
- package/docs/adr/README.md +6 -2
- package/docs/api.md +16 -4
- package/ops/README.md +24 -41
- package/package.json +5 -5
package/dist/extensions/web.js
CHANGED
|
@@ -9,6 +9,7 @@ export const WebResults = defineDoc({
|
|
|
9
9
|
history: "rewindable",
|
|
10
10
|
fork: "asOf",
|
|
11
11
|
initial: () => ({ queries: {} }),
|
|
12
|
+
checkpointWhen: () => false,
|
|
12
13
|
});
|
|
13
14
|
async function fetchBounded(url, options = {}) {
|
|
14
15
|
const parsed = new URL(url);
|
|
@@ -65,7 +66,7 @@ export function webExtension(apiKey = process.env.BRAVE_API_KEY ?? process.env.B
|
|
|
65
66
|
for (const key of keys.slice(0, Math.max(0, keys.length - 20)))
|
|
66
67
|
delete results.queries[key];
|
|
67
68
|
}, context);
|
|
68
|
-
return {
|
|
69
|
+
return { output: [{ type: "text", text }] };
|
|
69
70
|
},
|
|
70
71
|
}),
|
|
71
72
|
defineTool({
|
|
@@ -81,7 +82,7 @@ export function webExtension(apiKey = process.env.BRAVE_API_KEY ?? process.env.B
|
|
|
81
82
|
? (await Defuddle(result.bytes.toString(), result.url, { markdown: true, useAsync: false }))
|
|
82
83
|
.content
|
|
83
84
|
: result.bytes.toString();
|
|
84
|
-
return {
|
|
85
|
+
return { output: [{ type: "text", text: text.slice(0, 50000) }] };
|
|
85
86
|
},
|
|
86
87
|
}),
|
|
87
88
|
defineTool({
|
|
@@ -91,7 +92,7 @@ export function webExtension(apiKey = process.env.BRAVE_API_KEY ?? process.env.B
|
|
|
91
92
|
replay: "safe",
|
|
92
93
|
async execute(args, api, context) {
|
|
93
94
|
const data = await api.snapshot(WebResults, api.conversationId, context);
|
|
94
|
-
return {
|
|
95
|
+
return { output: [{ type: "text", text: data?.queries[args.query] ?? "No saved result" }] };
|
|
95
96
|
},
|
|
96
97
|
}),
|
|
97
98
|
],
|
|
@@ -16,7 +16,7 @@ export function workspaceExtension(reload) {
|
|
|
16
16
|
parameters: Type.Object({}),
|
|
17
17
|
async execute() {
|
|
18
18
|
await reload();
|
|
19
|
-
return {
|
|
19
|
+
return { output: [{ type: "text", text: "Workspace extensions and configuration reloaded." }] };
|
|
20
20
|
},
|
|
21
21
|
}),
|
|
22
22
|
],
|
package/dist/main.js
CHANGED
|
@@ -19,6 +19,7 @@ import { createBrowser } from "./extensions/browser.js";
|
|
|
19
19
|
import { memoryExtension } from "./extensions/memory.js";
|
|
20
20
|
import { messageExtension } from "./extensions/message.js";
|
|
21
21
|
import { ImageRead } from "./extensions/read-image.js";
|
|
22
|
+
import { launchInTerminal, restartExtension } from "./extensions/restart.js";
|
|
22
23
|
import { validateSchedules } from "./extensions/schedules.js";
|
|
23
24
|
import { Subagent } from "./extensions/subagent.js";
|
|
24
25
|
import { webExtension } from "./extensions/web.js";
|
|
@@ -75,6 +76,14 @@ reloadHttpTransport) {
|
|
|
75
76
|
browser.extension,
|
|
76
77
|
Subagent,
|
|
77
78
|
workspaceExtension(reload),
|
|
79
|
+
// ADR 0052: the service runs from a release directory; the controller starts after the call settles.
|
|
80
|
+
restartExtension({
|
|
81
|
+
workingDir: options.workingDir,
|
|
82
|
+
currentRelease: process.cwd(),
|
|
83
|
+
deployRoot: "/tmp",
|
|
84
|
+
settled: (taskId) => harness.waitForTask(taskId, BACKGROUND_CONTEXT),
|
|
85
|
+
launch: launchInTerminal,
|
|
86
|
+
}),
|
|
78
87
|
]),
|
|
79
88
|
...(await loadWorkspaceExtensions(options.workingDir)),
|
|
80
89
|
];
|
|
@@ -271,6 +280,7 @@ reloadHttpTransport) {
|
|
|
271
280
|
models: runtime.models,
|
|
272
281
|
defaults,
|
|
273
282
|
reload,
|
|
283
|
+
workingDir: options.workingDir,
|
|
274
284
|
chatGuid: snapshot.chatGuid,
|
|
275
285
|
guid: snapshot.guid,
|
|
276
286
|
text: snapshot.text,
|
package/dist/web/app.js
CHANGED
|
@@ -16,7 +16,8 @@ document.body.classList.add(`page-${page.slice(1)}`);
|
|
|
16
16
|
if (conversationId) document.body.classList.add("conversation-detail");
|
|
17
17
|
document.getElementById("title").textContent =
|
|
18
18
|
{
|
|
19
|
-
"/chat": "
|
|
19
|
+
"/chat": "Messages",
|
|
20
|
+
"/chats": "Chats",
|
|
20
21
|
"/tasks": "Tasks",
|
|
21
22
|
"/scheduled": "Scheduled tasks",
|
|
22
23
|
"/logs": "Logs",
|
|
@@ -424,6 +425,80 @@ function renderSettings() {
|
|
|
424
425
|
}
|
|
425
426
|
}
|
|
426
427
|
|
|
428
|
+
function renderChatSettings() {
|
|
429
|
+
const chats = data.chats || [];
|
|
430
|
+
const overrides = chats.filter((chat) => chat.verbose.chat !== undefined).length;
|
|
431
|
+
view.replaceChildren(
|
|
432
|
+
element(
|
|
433
|
+
"p",
|
|
434
|
+
[
|
|
435
|
+
`${chats.length} chats`,
|
|
436
|
+
`default verbose ${data.defaults?.progressMessages ? "on" : "off"}`,
|
|
437
|
+
`${overrides} ${overrides === 1 ? "override" : "overrides"}`,
|
|
438
|
+
"read-only: /verbose and /thinking change a chat, settings.json the default",
|
|
439
|
+
].join(" · "),
|
|
440
|
+
"muted",
|
|
441
|
+
),
|
|
442
|
+
);
|
|
443
|
+
// A main value with its source or provider underneath keeps each column scannable.
|
|
444
|
+
function stacked(main, detail) {
|
|
445
|
+
const cell = element("td", undefined, "c-fix");
|
|
446
|
+
cell.append(element("div", main));
|
|
447
|
+
if (detail) cell.append(element("div", detail, "meta"));
|
|
448
|
+
return cell;
|
|
449
|
+
}
|
|
450
|
+
function section(id, title, rows) {
|
|
451
|
+
const card = element("section", undefined, "card");
|
|
452
|
+
card.id = `chats-${id}`;
|
|
453
|
+
const header = element("header", undefined, "card-header");
|
|
454
|
+
header.append(element("span", title), element("span", String(rows.length), "meta"));
|
|
455
|
+
card.append(header);
|
|
456
|
+
const table = element("table", undefined, "chat-table");
|
|
457
|
+
const heading = table.createTHead().insertRow();
|
|
458
|
+
for (const label of ["Chat", "Replies", "Verbose", "Model", "Thinking", "Last activity"])
|
|
459
|
+
heading.append(element("th", label));
|
|
460
|
+
const body = table.createTBody();
|
|
461
|
+
for (const chat of rows) {
|
|
462
|
+
const row = body.insertRow();
|
|
463
|
+
if (!chat.replies) row.className = "chat-silent";
|
|
464
|
+
const name = row.insertCell();
|
|
465
|
+
// Group name, or the handle of a direct chat; the raw GUID stays in the native state only.
|
|
466
|
+
const handle = chat.chatGuid.includes(";+;") ? "unnamed group" : chat.chatGuid.split(";").at(-1);
|
|
467
|
+
name.append(link(chat.groupName || handle, `/chat?conversationId=${chat.conversationId}`));
|
|
468
|
+
const [provider, ...modelId] = (chat.model || "").split("/");
|
|
469
|
+
const activity =
|
|
470
|
+
typeof chat.updatedAt === "number"
|
|
471
|
+
? new Date(chat.updatedAt).toLocaleString("sv-SE").slice(0, 16)
|
|
472
|
+
: "";
|
|
473
|
+
row.append(
|
|
474
|
+
stacked(chat.replies ? "on" : "off"),
|
|
475
|
+
stacked(
|
|
476
|
+
chat.verbose.effective ? "on" : "off",
|
|
477
|
+
chat.verbose.chat === undefined ? "default" : "this chat",
|
|
478
|
+
),
|
|
479
|
+
stacked(modelId.join("/") || "-", chat.model ? provider : undefined),
|
|
480
|
+
stacked(chat.thinking || "-"),
|
|
481
|
+
stacked(activity || "-"),
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
card.append(table);
|
|
485
|
+
if (!rows.length) card.append(element("p", "None.", "empty chat-empty"));
|
|
486
|
+
return card;
|
|
487
|
+
}
|
|
488
|
+
view.append(
|
|
489
|
+
section(
|
|
490
|
+
"groups",
|
|
491
|
+
"Groups",
|
|
492
|
+
chats.filter((chat) => chat.chatGuid.includes(";+;")),
|
|
493
|
+
),
|
|
494
|
+
section(
|
|
495
|
+
"direct",
|
|
496
|
+
"Direct",
|
|
497
|
+
chats.filter((chat) => !chat.chatGuid.includes(";+;")),
|
|
498
|
+
),
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
427
502
|
function renderLogs() {
|
|
428
503
|
const scrollPositions = Array.from(view.querySelectorAll(".card-content"), (node) => ({
|
|
429
504
|
top: node.scrollTop,
|
|
@@ -615,9 +690,11 @@ function renderTasks() {
|
|
|
615
690
|
item.append(detail);
|
|
616
691
|
}
|
|
617
692
|
const descendants = children.get(key) || [];
|
|
618
|
-
|
|
693
|
+
const more = node.type === "conversation" && data.more?.[node.id];
|
|
694
|
+
if (descendants.length || more) {
|
|
619
695
|
const list = element("ul", undefined, "task-tree");
|
|
620
696
|
for (const child of descendants) list.append(renderNode(child));
|
|
697
|
+
if (more) list.append(element("li", "older tasks not listed", "meta"));
|
|
621
698
|
item.append(list);
|
|
622
699
|
}
|
|
623
700
|
return item;
|
|
@@ -643,7 +720,7 @@ function setupToolbar() {
|
|
|
643
720
|
if (page === "/chat" || page === "/tasks") {
|
|
644
721
|
if (conversationId) {
|
|
645
722
|
toolbar.className = "toolbar";
|
|
646
|
-
toolbar.append(page === "/chat" ? link("<
|
|
723
|
+
toolbar.append(page === "/chat" ? link("< Messages", "/") : link("< All conversations", "/tasks"));
|
|
647
724
|
}
|
|
648
725
|
} else if (page === "/memory") {
|
|
649
726
|
const records = Array.isArray(data) ? memoryRows() : data.records;
|
|
@@ -702,6 +779,7 @@ async function render() {
|
|
|
702
779
|
if (page === "/chat") await loadChats();
|
|
703
780
|
else if (page === "/memory") renderMemory();
|
|
704
781
|
else if (page === "/settings") renderSettings();
|
|
782
|
+
else if (page === "/chats") renderChatSettings();
|
|
705
783
|
else if (page === "/logs") renderLogs();
|
|
706
784
|
else if (page === "/tasks") renderTasks();
|
|
707
785
|
else if (page === "/scheduled") renderScheduled();
|
package/dist/web/page.js
CHANGED
|
@@ -5,7 +5,8 @@ function escapeHtml(text) {
|
|
|
5
5
|
export function renderPage(page, state) {
|
|
6
6
|
const active = page === "/" || page === "/chat" ? "/" : page;
|
|
7
7
|
const navigation = [
|
|
8
|
-
["/", "
|
|
8
|
+
["/", "Messages"],
|
|
9
|
+
["/chats", "Chats"],
|
|
9
10
|
["/tasks", "Tasks"],
|
|
10
11
|
["/scheduled", "Scheduled"],
|
|
11
12
|
["/logs", "Logs"],
|
|
@@ -19,7 +20,7 @@ export function renderPage(page, state) {
|
|
|
19
20
|
<title>blue — pi-imessage</title><link rel="stylesheet" href="/assets/style.css">
|
|
20
21
|
<script src="/assets/app.js" defer></script></head>
|
|
21
22
|
<body><nav class="app-nav" aria-label="Main navigation">${navigation}</nav>
|
|
22
|
-
<main class="content" id="main"><header class="page-header"><h1 id="title">
|
|
23
|
+
<main class="content" id="main"><header class="page-header"><h1 id="title">Messages</h1>
|
|
23
24
|
<div class="connection"><button id="refresh" type="button">Refresh</button></div></header>
|
|
24
25
|
<div id="toolbar"></div><p id="notice" role="status" hidden></p><div id="view" aria-busy="true"></div>
|
|
25
26
|
<details class="native-state"><summary>Native state</summary><pre id="state">${escapeHtml(state)}</pre></details></main>
|
package/dist/web/schedules.js
CHANGED
|
@@ -11,18 +11,9 @@ export function readSchedules(harness) {
|
|
|
11
11
|
const task = await tx.task(schedule.taskId);
|
|
12
12
|
const checkpoint = task?.state.checkpoint;
|
|
13
13
|
const state = checkpoint && typeof checkpoint === "object" && !Array.isArray(checkpoint) ? checkpoint : undefined;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const page = await tx.scanTasks({ conversationId: schedule.conversationId, kind: EXECUTION_KIND }, 100, cursor);
|
|
18
|
-
executions.push(...page.items);
|
|
19
|
-
cursor = page.next;
|
|
20
|
-
} while (cursor);
|
|
21
|
-
// Native scans merge live/terminal records; their order is not execution chronology.
|
|
22
|
-
const runs = executions
|
|
23
|
-
.sort((a, b) => b.id - a.id)
|
|
24
|
-
.slice(0, 10)
|
|
25
|
-
.map((run) => {
|
|
14
|
+
// Occurrence IDs grow with admission time; the newest ten are the history shown.
|
|
15
|
+
const executions = await tx.scanTasks({ conversationId: schedule.conversationId, kind: EXECUTION_KIND, order: "descending" }, 10);
|
|
16
|
+
const runs = executions.items.map((run) => {
|
|
26
17
|
const outcome = run.state.outcome;
|
|
27
18
|
const result = outcome?.status === "completed" &&
|
|
28
19
|
outcome.result &&
|
|
@@ -38,9 +29,7 @@ export function readSchedules(harness) {
|
|
|
38
29
|
input: run.input,
|
|
39
30
|
status: outcome?.status ?? run.state.status,
|
|
40
31
|
result,
|
|
41
|
-
error: outcome?.status === "failed" || outcome?.status === "faulted"
|
|
42
|
-
? outcome.error.message
|
|
43
|
-
: undefined,
|
|
32
|
+
error: outcome?.status === "failed" || outcome?.status === "faulted" ? outcome.error.message : undefined,
|
|
44
33
|
delivery: receipt?.textStatus ?? (result?.requestId ? "queued" : undefined),
|
|
45
34
|
};
|
|
46
35
|
});
|
package/dist/web/server.js
CHANGED
|
@@ -6,12 +6,12 @@ import { promisify } from "node:util";
|
|
|
6
6
|
import { gzip } from "node:zlib";
|
|
7
7
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
8
8
|
import { LiveDoc, } from "@earendil-works/pi-durable";
|
|
9
|
-
import { Chats } from "../agent/chats.js";
|
|
9
|
+
import { Chats, progressEnabled } from "../agent/chats.js";
|
|
10
10
|
import { DirectSends } from "../agent/direct-send.js";
|
|
11
11
|
import { Sessions } from "../agent/health.js";
|
|
12
12
|
import { Deliveries } from "../agent/replies.js";
|
|
13
13
|
import { Schedules } from "../agent/scheduling.js";
|
|
14
|
-
import { readSettings } from "../config/settings.js";
|
|
14
|
+
import { isReplyEnabled, readSettings } from "../config/settings.js";
|
|
15
15
|
import { displayHistory, historyActivity } from "./history.js";
|
|
16
16
|
import { memoryPage, parseMemoryFile } from "./memory.js";
|
|
17
17
|
import { renderPage } from "./page.js";
|
|
@@ -113,6 +113,36 @@ export async function startWeb(options) {
|
|
|
113
113
|
settings: { chatAllowlist: configuration.chatAllowlist },
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
+
/** One row per chat: reply eligibility, the /verbose choice and the conversation's agent settings. */
|
|
117
|
+
async function chatSettings() {
|
|
118
|
+
const [chats, configuration] = await Promise.all([
|
|
119
|
+
options.agent.harness.snapshot(Chats, BACKGROUND_CONTEXT),
|
|
120
|
+
settings(),
|
|
121
|
+
]);
|
|
122
|
+
const records = chats?.items ?? [];
|
|
123
|
+
const rows = await Promise.all(records.map(async (chat) => {
|
|
124
|
+
const conversation = await options.agent.harness.conversation(chat.conversationId, BACKGROUND_CONTEXT);
|
|
125
|
+
const [agent, entries] = conversation
|
|
126
|
+
? await Promise.all([conversation.agent(BACKGROUND_CONTEXT), history(chat.conversationId)])
|
|
127
|
+
: [undefined, undefined];
|
|
128
|
+
return {
|
|
129
|
+
chatGuid: chat.chatGuid,
|
|
130
|
+
conversationId: chat.conversationId,
|
|
131
|
+
groupName: entries ? displayHistory(entries, 15).groupName : undefined,
|
|
132
|
+
updatedAt: entries ? historyActivity(entries) : undefined,
|
|
133
|
+
replies: isReplyEnabled(configuration, chat.chatGuid),
|
|
134
|
+
verbose: { chat: chat.progress, effective: progressEnabled(configuration, records, chat.chatGuid) },
|
|
135
|
+
model: agent?.model ? `${agent.model.provider}/${agent.model.modelId}` : undefined,
|
|
136
|
+
thinking: agent?.thinkingLevel,
|
|
137
|
+
};
|
|
138
|
+
}));
|
|
139
|
+
rows.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0) || Number(b.conversationId) - Number(a.conversationId));
|
|
140
|
+
return {
|
|
141
|
+
chats: rows,
|
|
142
|
+
defaults: { progressMessages: configuration.progressMessages },
|
|
143
|
+
revision: viewRevision(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
116
146
|
const server = createServer((request, response) => {
|
|
117
147
|
void handle(request, response).catch((error) => {
|
|
118
148
|
if (!response.headersSent)
|
|
@@ -241,6 +271,10 @@ export async function startWeb(options) {
|
|
|
241
271
|
value = await readSchedules(options.agent.harness);
|
|
242
272
|
compact = true;
|
|
243
273
|
}
|
|
274
|
+
else if (page === "/chats") {
|
|
275
|
+
value = await chatSettings();
|
|
276
|
+
compact = true;
|
|
277
|
+
}
|
|
244
278
|
else if (page === "/tasks") {
|
|
245
279
|
value = {
|
|
246
280
|
...(await readTaskTree(options.agent.harness)),
|
|
@@ -265,7 +299,7 @@ export async function startWeb(options) {
|
|
|
265
299
|
value = await settings();
|
|
266
300
|
else if (page === "/logs") {
|
|
267
301
|
const cursor = readCursor(url.searchParams.get("cursor"));
|
|
268
|
-
const tasks = await options.agent.harness.commit((tx) => tx.scanTasks({}, 100, cursor), BACKGROUND_CONTEXT);
|
|
302
|
+
const tasks = await options.agent.harness.commit((tx) => tx.scanTasks(cursor ? {} : { order: "descending" }, 100, cursor), BACKGROUND_CONTEXT);
|
|
269
303
|
let hostLog = "";
|
|
270
304
|
try {
|
|
271
305
|
const file = await open(join(options.workingDir, "service.log"), "r");
|
package/dist/web/style.css
CHANGED
|
@@ -541,3 +541,37 @@ body.page-logs .native-state {
|
|
|
541
541
|
width: 100px;
|
|
542
542
|
}
|
|
543
543
|
}
|
|
544
|
+
|
|
545
|
+
.chat-table th,
|
|
546
|
+
.chat-table td {
|
|
547
|
+
padding: 8px 16px;
|
|
548
|
+
border-bottom: 1px solid var(--border);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.chat-table th {
|
|
552
|
+
opacity: 0.55;
|
|
553
|
+
white-space: nowrap;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.chat-table td:first-child {
|
|
557
|
+
width: 100%;
|
|
558
|
+
word-break: break-all;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.chat-table tbody tr:last-child td {
|
|
562
|
+
border-bottom: 0;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.chat-silent {
|
|
566
|
+
opacity: 0.55;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.chat-empty {
|
|
570
|
+
padding: 12px 16px;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/* The Chats tables grow with their rows; the page itself scrolls. */
|
|
574
|
+
body.page-chats .card {
|
|
575
|
+
max-height: none;
|
|
576
|
+
overflow: visible;
|
|
577
|
+
}
|
package/dist/web/tasks.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
2
|
import { displayHistory, historyActivity } from "./history.js";
|
|
3
|
-
/**
|
|
3
|
+
/** Newest tasks listed per conversation; older finished ones are only flagged. */
|
|
4
|
+
export const RECENT_TASKS_PER_CONVERSATION = 20;
|
|
5
|
+
/**
|
|
6
|
+
* Read one consistent tree, newest first: all conversations, every unfinished task, every task that owns a
|
|
7
|
+
* conversation, their parent chains and the newest tasks of each conversation. Descending scans stop at the
|
|
8
|
+
* window instead of reading the whole task history (earendil-works/pi#10546).
|
|
9
|
+
*/
|
|
4
10
|
export function readTaskTree(harness) {
|
|
5
11
|
return harness.commit(async (tx) => {
|
|
6
12
|
const conversations = [];
|
|
13
|
+
const kept = new Map();
|
|
14
|
+
const more = {};
|
|
7
15
|
let cursor;
|
|
8
16
|
do {
|
|
9
17
|
const page = await tx.scanConversations({}, 100, cursor);
|
|
@@ -15,49 +23,75 @@ export function readTaskTree(harness) {
|
|
|
15
23
|
updatedAt: historyActivity(history),
|
|
16
24
|
groupName: typeof groupName === "string" ? groupName : undefined,
|
|
17
25
|
});
|
|
26
|
+
const newest = await tx.scanTasks({ conversationId: conversation.id, order: "descending" }, RECENT_TASKS_PER_CONVERSATION + 1);
|
|
27
|
+
for (const task of newest.items.slice(0, RECENT_TASKS_PER_CONVERSATION))
|
|
28
|
+
kept.set(task.id, task);
|
|
29
|
+
if (newest.items.length > RECENT_TASKS_PER_CONVERSATION)
|
|
30
|
+
more[conversation.id] = true;
|
|
31
|
+
if (conversation.owner && !kept.has(conversation.owner.taskId)) {
|
|
32
|
+
const owner = await tx.task(conversation.owner.taskId);
|
|
33
|
+
if (owner)
|
|
34
|
+
kept.set(owner.id, owner);
|
|
35
|
+
}
|
|
18
36
|
}
|
|
19
37
|
cursor = page.next;
|
|
20
38
|
} while (cursor);
|
|
21
|
-
|
|
39
|
+
// Unfinished tasks can be old (sleeping schedulers); status scans are small.
|
|
40
|
+
for (const status of ["pending", "running", "waiting", "sleeping"]) {
|
|
41
|
+
cursor = undefined;
|
|
42
|
+
do {
|
|
43
|
+
const page = await tx.scanTasks({ status, order: "descending" }, 100, cursor);
|
|
44
|
+
for (const task of page.items)
|
|
45
|
+
kept.set(task.id, task);
|
|
46
|
+
cursor = page.next;
|
|
47
|
+
} while (cursor);
|
|
48
|
+
}
|
|
49
|
+
// A kept child needs its parent chain so the tree can place it.
|
|
50
|
+
for (const task of [...kept.values()]) {
|
|
51
|
+
for (let owner = task.owner; owner !== undefined && !kept.has(owner); owner = kept.get(owner)?.owner) {
|
|
52
|
+
const parent = await tx.task(owner);
|
|
53
|
+
if (!parent)
|
|
54
|
+
break;
|
|
55
|
+
kept.set(parent.id, parent);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
22
58
|
const assistants = new Map();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
59
|
+
const tasks = [];
|
|
60
|
+
for (const task of [...kept.values()].sort((a, b) => Number(b.id) - Number(a.id))) {
|
|
61
|
+
let name;
|
|
62
|
+
const input = task.input;
|
|
63
|
+
if (task.kind === "pi.tool" && input && typeof input === "object" && !Array.isArray(input)) {
|
|
64
|
+
const call = input;
|
|
65
|
+
if (typeof call.assistant === "number" && typeof call.callId === "string") {
|
|
66
|
+
const id = call.assistant;
|
|
67
|
+
if (!assistants.has(id))
|
|
68
|
+
assistants.set(id, await tx.entry(id));
|
|
69
|
+
for (const message of assistants.get(id)?.model ?? []) {
|
|
70
|
+
if (message.role !== "assistant")
|
|
71
|
+
continue;
|
|
72
|
+
const block = message.content.find((item) => item.type === "toolCall" && item.id === call.callId);
|
|
73
|
+
if (block?.type === "toolCall")
|
|
74
|
+
name = block.name;
|
|
40
75
|
}
|
|
41
76
|
}
|
|
42
|
-
const state = task.state;
|
|
43
|
-
tasks.push({
|
|
44
|
-
id: task.id,
|
|
45
|
-
conversationId: task.conversationId,
|
|
46
|
-
owner: task.owner,
|
|
47
|
-
kind: task.kind,
|
|
48
|
-
name,
|
|
49
|
-
background: task.background,
|
|
50
|
-
abortRequested: task.abortRequested,
|
|
51
|
-
status: state.status,
|
|
52
|
-
phase: state.checkpoint && typeof state.checkpoint === "object" && !Array.isArray(state.checkpoint)
|
|
53
|
-
? state.checkpoint.phase
|
|
54
|
-
: undefined,
|
|
55
|
-
on: state.status === "waiting" ? state.on : undefined,
|
|
56
|
-
outcome: state.outcome?.status,
|
|
57
|
-
});
|
|
58
77
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
const state = task.state;
|
|
79
|
+
tasks.push({
|
|
80
|
+
id: task.id,
|
|
81
|
+
conversationId: task.conversationId,
|
|
82
|
+
owner: task.owner,
|
|
83
|
+
kind: task.kind,
|
|
84
|
+
name,
|
|
85
|
+
background: task.background,
|
|
86
|
+
abortRequested: task.abortRequested,
|
|
87
|
+
status: state.status,
|
|
88
|
+
phase: state.checkpoint && typeof state.checkpoint === "object" && !Array.isArray(state.checkpoint)
|
|
89
|
+
? state.checkpoint.phase
|
|
90
|
+
: undefined,
|
|
91
|
+
on: state.status === "waiting" ? state.on : undefined,
|
|
92
|
+
outcome: state.outcome?.status,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return { conversations, tasks, more };
|
|
62
96
|
}, BACKGROUND_CONTEXT);
|
|
63
97
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 0048. Quiet attachment reads
|
|
2
|
+
|
|
3
|
+
Status: proposed (owner-requested behavior in #38, 2026-10-07)
|
|
4
|
+
|
|
5
|
+
**Problem.** Reading an archived attachment adds noisy tool notices containing long private archive paths.
|
|
6
|
+
|
|
7
|
+
**Example.** A native `read` of `WORKING_DIR/attachments/<chat>/<file>` produces no request or completion notice. An ordinary file read still shows the existing progress pair.
|
|
8
|
+
|
|
9
|
+
**Decision.** Add a narrow exception to ADR 0040's all-tool progress rule: suppress both admission and settlement notices for native `read` calls under the active workspace attachment root, including owned child calls and failed reads. Normalize relative, home, @ and file-URL paths and resolve filesystem aliases using metadata only. Match directory boundaries, not a generic attachment substring. Other reads, tools, task progress, manual compaction and final replies remain unchanged.
|
|
10
|
+
|
|
11
|
+
The existing native observer filters before assigning notice routing or making direct-send receipts. Execution, result/error history, receipt/no-replay rules and the single pipeline are unchanged. No new poller, sender, permission boundary, data removal or credential policy is introduced.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 0049. Per-chat progress notices
|
|
2
|
+
|
|
3
|
+
Status: proposed (owner-requested implementation, 2026-10-24)
|
|
4
|
+
|
|
5
|
+
**Problem.** `progressMessages` in `settings.json` turns tool progress notices on or off for every chat at once. A busy group wants only the final messages while an admin chat wants to watch the tools.
|
|
6
|
+
|
|
7
|
+
**Example.**
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
/verbose -> Verbose: on (default)
|
|
11
|
+
/verbose off -> Verbose: off (this chat only)
|
|
12
|
+
/verbose default -> Verbose: on (default)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Decision.**
|
|
16
|
+
|
|
17
|
+
- Each chat record in the native `imessage.chats` document gets an optional `progress` boolean. `/verbose on|off` sets it and `/verbose default` removes it; `/verbose` alone reports the effective state. The record version is unchanged; old records have no field.
|
|
18
|
+
- The progress observer (ADR 0040) resolves the owning chat first and then gates both the admission and the settlement notice by `chat.progress ?? settings.progressMessages`. Reply-enabled allowlisting, formatting, screening, receipts and no-replay rules are unchanged.
|
|
19
|
+
- `settings.json` stays the default and is never rewritten by a command; the per-chat choice lives only in Durable.
|
|
20
|
+
|
|
21
|
+
**Consequences.** A `/verbose off` chat still runs tools and records their results; it only stops seeing the two notice lines. Child notices follow the owning chat's choice. Changing the global default does not touch chats with an explicit choice.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# 0052. The host owns its own restart
|
|
2
|
+
|
|
3
|
+
Status: accepted (owner accepted on 2026-10-24)
|
|
4
|
+
|
|
5
|
+
**Problem.** A chat assistant restarts the service by writing a controller
|
|
6
|
+
script and running it from a `bash` tool call. The host cannot tell that this
|
|
7
|
+
call is about to stop it. When the call is still running at the stop, ADR 0041
|
|
8
|
+
reruns it on the next start, which launches another controller and stops the
|
|
9
|
+
service again (#39: about 70 restarts in eleven minutes). A host-side stop
|
|
10
|
+
heuristic that aborts a tool interrupted twice in a row was tried the same
|
|
11
|
+
day; it limits the damage after the fact, does not remove the cause, and
|
|
12
|
+
aborts any long tool that two deployments happen to interrupt in a row.
|
|
13
|
+
|
|
14
|
+
**Example.**
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
restart_service({ release: "d627232…" })
|
|
18
|
+
-> Restart into /Users/clawbot/pi-imessage-releases/d627232… is scheduled;
|
|
19
|
+
the service stops after this call settles. Controller directory:
|
|
20
|
+
/tmp/pi-imessage-restart.Xy12ab. Verify in a later call.
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The call settles, the host opens the controller in Terminal, the controller
|
|
24
|
+
stops the old process and becomes the new one. The next turn verifies the
|
|
25
|
+
listener, the log and the running release directory.
|
|
26
|
+
|
|
27
|
+
**Decision.**
|
|
28
|
+
|
|
29
|
+
- A built-in `restart_service` tool takes the full commit hash of a prepared
|
|
30
|
+
release directory, a sibling of the running one. It refuses a hash that is not
|
|
31
|
+
a directory with `src/cli.ts`, `node_modules` and `ops/restart.command`, and
|
|
32
|
+
a second request while one is scheduled.
|
|
33
|
+
- The tool writes the private controller directory of ops/README.md step 3
|
|
34
|
+
itself: `environment.sh` from the process environment, `control.sh` from the
|
|
35
|
+
process's own PID, start time and command line, and a copy of
|
|
36
|
+
`ops/restart.command` from the target release. The controller deletes
|
|
37
|
+
`environment.sh` after sourcing it.
|
|
38
|
+
- The tool returns at once. The host launches the controller only after the
|
|
39
|
+
tool task has settled, so the stop can never interrupt the call that asked
|
|
40
|
+
for it.
|
|
41
|
+
- The tool is `replay: "unsafe"`. If it is interrupted anyway, the model gets
|
|
42
|
+
an interrupted result and checks what is running instead of a rerun
|
|
43
|
+
launching a second controller.
|
|
44
|
+
- Ordinary tools keep the rerun default of ADR 0041. Operators keep the manual
|
|
45
|
+
procedure for a cold start when no service is running.
|
|
46
|
+
|
|
47
|
+
**Consequences.** A self-restart costs one restart and no extra model turn.
|
|
48
|
+
The stop still happens at a tool boundary of the requesting chat, so that chat's
|
|
49
|
+
next model request is repeated after the restart, as ADR 0041 accepts. The
|
|
50
|
+
restart loop of #39 cannot recur through this tool, so the stop heuristic is
|
|
51
|
+
removed and ADR 0041 applies unchanged to every other tool. A chat that writes
|
|
52
|
+
its own controller through `bash` is still subject to the ops rule of ending
|
|
53
|
+
the call right after `open`.
|
package/docs/adr/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Accepted ADR files are never edited; this index records which ones are current.
|
|
|
23
23
|
| [0018](0018-tasks-without-prompt-api.md) | Ordinary `/stop`; no HTTP `/prompt` or `/send` | Active |
|
|
24
24
|
| [0036](0036-workspace-policy-and-neutral-service-id.md) | Workspace policy and neutral service ID | Active |
|
|
25
25
|
| [0040](0040-chat-progress-notices.md) | Chat progress notices | Active; consolidates 0020–0028, 0031 |
|
|
26
|
-
| [0041](0041-restart-and-recovery.md) | Restart and recovery | Active; consolidates 0037–0039 |
|
|
26
|
+
| [0041](0041-restart-and-recovery.md) | Restart and recovery | Active; consolidates 0037–0039; self-restart path added by 0052 |
|
|
27
27
|
| [0042](0042-background-schedules-and-workspace-extensions.md) | Background schedules and workspace extensions | Active; operator-run clause replaced by 0047 |
|
|
28
28
|
| [0043](0043-remove-legacy-migration.md) | Remove the legacy migration code | Active; archived at tag `archive/legacy-migration` |
|
|
29
29
|
| [0044](0044-message-channels.md) | Organize message sources as channels | Proposed |
|
|
@@ -31,6 +31,9 @@ Accepted ADR files are never edited; this index records which ones are current.
|
|
|
31
31
|
|
|
32
32
|
| [0046](0046-selective-command-redaction.md) | Selective command-preview redaction | Proposed; owner-requested implementation in #36 |
|
|
33
33
|
| [0047](0047-one-scheduling-path.md) | One scheduling path for recurring and one-off work | Active |
|
|
34
|
+
| [0048](0048-quiet-attachment-reads.md) | Quiet attachment reads | Proposed; owner-requested implementation in #38 |
|
|
35
|
+
| [0049](0049-per-chat-progress-notices.md) | Per-chat progress notices via `/verbose` | Proposed; owner-requested implementation |
|
|
36
|
+
| [0052](0052-host-owned-self-restart.md) | The host owns its own restart (`restart_service`) | Active; owner accepted on 2026-10-24 (#39) |
|
|
34
37
|
|
|
35
38
|
## Replaced or rejected
|
|
36
39
|
|
|
@@ -47,4 +50,5 @@ Accepted ADR files are never edited; this index records which ones are current.
|
|
|
47
50
|
| [0032](0032-native-background-schedules.md)–[0035](0035-external-business-task-extensions.md) | Native schedules, operator run, local time, workspace extensions | 0042 |
|
|
48
51
|
| [0037](0037-restart-during-in-flight-work.md)–[0039](0039-rerun-interrupted-tools.md) | Restart and tool rerun | 0041 |
|
|
49
52
|
|
|
50
|
-
|
|
53
|
+
Numbers 0029, 0050 and 0051 are not used: 0050 and 0051 were interim stop
|
|
54
|
+
heuristics for #39 that the owner did not accept; 0052 replaced them.
|
package/docs/api.md
CHANGED
|
@@ -5,17 +5,27 @@
|
|
|
5
5
|
Default address: `http://localhost:7750` (configured by `WEB_HOST` and `WEB_PORT`).
|
|
6
6
|
The server has no authentication; do not expose it to untrusted networks.
|
|
7
7
|
|
|
8
|
-
The pages `/`, `/chat`, `/tasks`, `/scheduled`, `/settings`, `/memory` and `/logs` are read-only.
|
|
8
|
+
The pages `/`, `/chat`, `/chats`, `/tasks`, `/scheduled`, `/settings`, `/memory` and `/logs` are read-only.
|
|
9
|
+
The Chats tab (`/chats`, `/chats/data`) lists groups and direct chats, one row
|
|
10
|
+
each: reply eligibility, the `/verbose` choice with its effective value, the
|
|
11
|
+
conversation's model and thinking level, and last activity. Chats that do not
|
|
12
|
+
reply are dimmed. It never shows message text. The message history page in the
|
|
13
|
+
navigation is labelled Messages.
|
|
9
14
|
The Tasks tab shows real chat and native schedule ownership trees, including
|
|
10
15
|
subagent child conversations and terminal tasks. Internal-only conversations such as model-health
|
|
11
16
|
are hidden, including when selected by URL; their stored data is not deleted.
|
|
12
17
|
`/tasks/data` retains all conversation records for native diagnostics
|
|
13
|
-
and lightweight task summaries,
|
|
18
|
+
and lightweight task summaries, newest first. It lists every unfinished task,
|
|
19
|
+
every task that owns a conversation, their parent chains and the 20 newest
|
|
20
|
+
tasks per conversation, read with descending scans so the full task history is
|
|
21
|
+
not scanned; a conversation with older tasks is flagged in `more` and shows one
|
|
22
|
+
line. The Scheduled history and the Logs task list use the same descending
|
|
23
|
+
order, so "Older tasks" pages go back in time. Input and payloads
|
|
14
24
|
are fetched only when a task is expanded, through `/tasks/task/data?taskId=ID`.
|
|
15
25
|
`/tasks?conversationId=ID` focuses one conversation and its descendants. The tree
|
|
16
26
|
and expanded tasks update only on manual refresh. Conversation lists and sidebars
|
|
17
27
|
use newest message activity first, with conversation ID as a descending tie-break;
|
|
18
|
-
Tasks uses the same activity clock as
|
|
28
|
+
Tasks uses the same activity clock as Messages, not conversation creation order.
|
|
19
29
|
Conversation windows have a maximum height of `min(560px, 70dvh)` and scroll inside. These are latest task states,
|
|
20
30
|
not checkpoint history: terminal tasks retain input and outcome, but no checkpoint.
|
|
21
31
|
Existing redaction and large-string truncation apply to task details too.
|
|
@@ -89,4 +99,6 @@ previews. Raw outputs, commands, prompts and document contents stay local.
|
|
|
89
99
|
Known credential indicators are redacted; arbitrary user-authored secrets cannot
|
|
90
100
|
be guaranteed detectable. Missing or false keeps final-only delivery. The setting
|
|
91
101
|
is reread for each notice; old tasks and unknown sends are not replayed
|
|
92
|
-
([ADR
|
|
102
|
+
([ADR 0040](adr/0040-chat-progress-notices.md)). `/verbose on|off` in a chat
|
|
103
|
+
overrides this default for that chat only and `/verbose default` removes the
|
|
104
|
+
override ([ADR 0049](adr/0049-per-chat-progress-notices.md)).
|