@kingcrab/pi-imessage 1.1.0 → 1.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 +4 -6
- package/dist/agent/behavior.js +7 -13
- package/dist/agent/chats.js +7 -8
- package/dist/agent/commands.js +16 -11
- package/dist/agent/harness.js +6 -0
- package/dist/agent/progress.js +30 -4
- package/dist/agent/prompt.js +41 -24
- package/dist/agent/run.js +5 -4
- package/dist/agent/scheduling.js +11 -34
- package/dist/cli.js +42 -5
- package/dist/config/settings.js +4 -0
- package/dist/extensions/browser.js +7 -3
- package/dist/extensions/codemode.js +16 -0
- package/dist/extensions/final-text.js +12 -0
- package/dist/extensions/schedules.js +1 -1
- package/dist/extensions/subagent.js +3 -12
- package/dist/extensions/workspace.js +6 -2
- package/dist/main.js +38 -34
- package/dist/transport/watch.js +7 -1
- package/dist/web/app.js +10 -3
- package/dist/web/server.js +7 -3
- package/docs/adr/{0004-native-extensions.md → 0003-native-extensions.md} +1 -1
- package/docs/adr/{0006-jsonl-storage.md → 0004-jsonl-storage.md} +1 -1
- package/docs/adr/{0007-images-by-path.md → 0005-images-by-path.md} +1 -1
- package/docs/adr/{0010-test-budget.md → 0006-test-budget.md} +1 -1
- package/docs/adr/{0011-reconcile-unsent-answers.md → 0007-reconcile-unsent-answers.md} +2 -2
- package/docs/adr/{0012-run-in-owned-conversation.md → 0008-run-in-owned-conversation.md} +5 -5
- package/docs/adr/{0032-native-background-schedules.md → 0009-native-background-schedules.md} +1 -1
- package/docs/adr/0010-framework-and-workspace.md +27 -0
- package/docs/adr/README.md +24 -47
- package/docs/api.md +16 -8
- package/docs/architecture.md +19 -8
- package/docs/browser.md +7 -7
- package/docs/install.md +109 -0
- package/docs/workspace-extensions.md +8 -4
- package/package.json +4 -5
- package/dist/agent/compaction.js +0 -51
- package/dist/extensions/restart.js +0 -100
- package/docs/adr/0003-replies-at-most-once.md +0 -11
- package/docs/adr/0005-history-migration.md +0 -11
- package/docs/adr/0008-compaction-and-run.md +0 -11
- package/docs/adr/0009-one-shot-cutover.md +0 -11
- package/docs/adr/0013-thin-adapters-around-durable.md +0 -18
- package/docs/adr/0014-ephemeral-private-storage.md +0 -13
- package/docs/adr/0015-ordinary-stop-keeps-native-inbox.md +0 -21
- package/docs/adr/0016-process-http-transport.md +0 -21
- package/docs/adr/0017-browser-cli-owned-scope.md +0 -22
- package/docs/adr/0018-tasks-without-prompt-api.md +0 -19
- package/docs/adr/0019-remove-scheduled-services.md +0 -41
- package/docs/adr/0020-chat-task-progress.md +0 -41
- package/docs/adr/0021-readable-progress-without-link-previews.md +0 -33
- package/docs/adr/0022-progress-admission-and-settlement.md +0 -35
- package/docs/adr/0023-retain-progress-type-prefixes.md +0 -18
- package/docs/adr/0024-status-marker-before-type-prefix.md +0 -13
- package/docs/adr/0025-reusable-progress-and-command-preview.md +0 -49
- package/docs/adr/0026-inline-command-only-requests.md +0 -16
- package/docs/adr/0027-command-preview-width.md +0 -10
- package/docs/adr/0028-command-preview-120.md +0 -8
- package/docs/adr/0030-shared-web-read-cache.md +0 -33
- package/docs/adr/0031-quiet-compaction-progress.md +0 -32
- package/docs/adr/0033-operator-scheduled-run.md +0 -23
- package/docs/adr/0034-machine-local-schedule-time.md +0 -36
- package/docs/adr/0035-external-business-task-extensions.md +0 -56
- package/docs/adr/0036-workspace-policy-and-neutral-service-id.md +0 -37
- package/docs/adr/0037-restart-during-in-flight-work.md +0 -30
- package/docs/adr/0038-restart-any-time.md +0 -25
- package/docs/adr/0039-rerun-interrupted-tools.md +0 -24
- package/docs/adr/0040-chat-progress-notices.md +0 -44
- package/docs/adr/0041-restart-and-recovery.md +0 -28
- package/docs/adr/0042-background-schedules-and-workspace-extensions.md +0 -44
- package/docs/adr/0043-remove-legacy-migration.md +0 -23
- package/docs/adr/0044-message-channels.md +0 -32
- package/docs/adr/0045-chat-scheduling-tools.md +0 -51
- package/docs/adr/0046-selective-command-redaction.md +0 -31
- package/docs/adr/0047-one-scheduling-path.md +0 -158
- package/docs/adr/0048-quiet-attachment-reads.md +0 -11
- package/docs/adr/0049-per-chat-progress-notices.md +0 -21
- package/docs/adr/0052-host-owned-self-restart.md +0 -53
- package/ops/README.md +0 -250
- package/ops/preflight.mjs +0 -28
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/** Text written next to tool calls in one assistant message: the model's interim update. */
|
|
2
|
+
export function interimText(message) {
|
|
3
|
+
if (message?.role !== "assistant" || message.stopReason !== "toolUse")
|
|
4
|
+
return;
|
|
5
|
+
if (!message.content.some((part) => part.type === "toolCall"))
|
|
6
|
+
return;
|
|
7
|
+
const text = message.content
|
|
8
|
+
.map((part) => (part.type === "text" ? part.text : ""))
|
|
9
|
+
.join("\n")
|
|
10
|
+
.trim();
|
|
11
|
+
return text || undefined;
|
|
12
|
+
}
|
|
1
13
|
/** The same final-only projection is used for transport and the model's delivery facts. */
|
|
2
14
|
export function finalReplyText(message) {
|
|
3
15
|
if (message?.role !== "assistant" || !["stop", "length"].includes(message.stopReason))
|
|
@@ -8,7 +8,7 @@ export const ScheduledOutbox = defineDoc({
|
|
|
8
8
|
checkpointWhen: () => false,
|
|
9
9
|
});
|
|
10
10
|
export function validateSchedules(extensions) {
|
|
11
|
-
const ids = new Set(
|
|
11
|
+
const ids = new Set();
|
|
12
12
|
const requestIds = new Set();
|
|
13
13
|
for (const extension of extensions) {
|
|
14
14
|
for (const schedule of extension.schedules ?? []) {
|
|
@@ -6,28 +6,19 @@ export const Subagent = defineExtension({
|
|
|
6
6
|
tools: [
|
|
7
7
|
defineTool({
|
|
8
8
|
name: "subagent",
|
|
9
|
-
description: "Run a foreground
|
|
9
|
+
description: "Run a foreground subagent with a separate context and the same tools as you. Returns its result here.",
|
|
10
10
|
parameters: Type.Object({ task: Type.String() }),
|
|
11
11
|
replay: "safe",
|
|
12
12
|
async execute(args, api, context) {
|
|
13
|
-
const parent = await api.agent(context);
|
|
14
13
|
const childId = await api.commit(async (tx) => {
|
|
15
14
|
const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
16
15
|
if (existing)
|
|
17
16
|
return existing.id;
|
|
18
17
|
const child = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
19
|
-
|
|
20
|
-
"read",
|
|
21
|
-
"search_memory",
|
|
22
|
-
"load_memory",
|
|
23
|
-
"web_search",
|
|
24
|
-
"fetch_content",
|
|
25
|
-
"get_search_results",
|
|
26
|
-
];
|
|
18
|
+
// The child has every tool its parent has, deferred ones included; only nesting another subagent is left out.
|
|
27
19
|
await configure(tx, child.id, {
|
|
28
20
|
extensions: { remove: [Subagent] },
|
|
29
|
-
|
|
30
|
-
instructions: "Complete only the delegated task. You are read-only. Do not send messages or launch background work.",
|
|
21
|
+
instructions: "Complete only the delegated task and return its result; do not take on other work.",
|
|
31
22
|
});
|
|
32
23
|
return child.id;
|
|
33
24
|
}, context);
|
|
@@ -2,7 +2,8 @@ import { readdir, readFile, realpath } from "node:fs/promises";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { join, sep } from "node:path";
|
|
4
4
|
import { Type } from "@earendil-works/pi-ai";
|
|
5
|
-
import
|
|
5
|
+
import * as durable from "@earendil-works/pi-durable";
|
|
6
|
+
import { configure, defineDoc, defineExtension, defineTask, defineTool, GenerationTask, hook, section, } from "@earendil-works/pi-durable";
|
|
6
7
|
import { ScheduledOutbox, validateSchedules, } from "./schedules.js";
|
|
7
8
|
const loadModule = createRequire(import.meta.url);
|
|
8
9
|
/** A chat assistant can apply its own workspace changes through the existing host owner. */
|
|
@@ -22,7 +23,7 @@ export function workspaceExtension(reload) {
|
|
|
22
23
|
],
|
|
23
24
|
});
|
|
24
25
|
}
|
|
25
|
-
/** Scan only direct workspace extension directories; never skills or attachments
|
|
26
|
+
/** Scan only direct workspace extension directories; never skills or attachments. */
|
|
26
27
|
export async function loadWorkspaceExtensions(workingDir) {
|
|
27
28
|
const extensions = [];
|
|
28
29
|
const root = join(workingDir, "extensions");
|
|
@@ -69,6 +70,9 @@ export async function loadWorkspaceExtensions(workingDir) {
|
|
|
69
70
|
configure,
|
|
70
71
|
ScheduledOutbox,
|
|
71
72
|
section,
|
|
73
|
+
hook,
|
|
74
|
+
GenerationTask,
|
|
75
|
+
durable,
|
|
72
76
|
});
|
|
73
77
|
if (!extension ||
|
|
74
78
|
typeof extension !== "object" ||
|
package/dist/main.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
2
|
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { createRegistry } from "@earendil-works/pi-durable";
|
|
4
|
+
import { createToolSearchExtension } from "@earendil-works/pi-durable/tool-search";
|
|
4
5
|
import { CodingTools } from "@earendil-works/pi-durable/tools";
|
|
5
6
|
import { Chats, chatConversation, submitMessage, WatchCursor, } from "./agent/chats.js";
|
|
6
7
|
import { compactionReply, isCommand, runCommand } from "./agent/commands.js";
|
|
7
|
-
import { compactChats } from "./agent/compaction.js";
|
|
8
8
|
import { deliverReplies, recoverSending } from "./agent/deliver.js";
|
|
9
9
|
import { deliverDirect } from "./agent/direct-send.js";
|
|
10
10
|
import { openHarness } from "./agent/harness.js";
|
|
@@ -16,10 +16,10 @@ import { Runs, runRecord } from "./agent/run.js";
|
|
|
16
16
|
import { deliverScheduled, EXECUTION_KIND, Schedules, schedulingExtension } from "./agent/scheduling.js";
|
|
17
17
|
import { isReplyEnabled, readSettings } from "./config/settings.js";
|
|
18
18
|
import { createBrowser } from "./extensions/browser.js";
|
|
19
|
+
import { createCodemode } from "./extensions/codemode.js";
|
|
19
20
|
import { memoryExtension } from "./extensions/memory.js";
|
|
20
21
|
import { messageExtension } from "./extensions/message.js";
|
|
21
22
|
import { ImageRead } from "./extensions/read-image.js";
|
|
22
|
-
import { launchInTerminal, restartExtension } from "./extensions/restart.js";
|
|
23
23
|
import { validateSchedules } from "./extensions/schedules.js";
|
|
24
24
|
import { Subagent } from "./extensions/subagent.js";
|
|
25
25
|
import { webExtension } from "./extensions/web.js";
|
|
@@ -28,6 +28,29 @@ import { archiveAttachments } from "./transport/attachments.js";
|
|
|
28
28
|
import { openHttpTransport } from "./transport/http.js";
|
|
29
29
|
import { createWatcher } from "./transport/watch.js";
|
|
30
30
|
import { startWeb } from "./web/server.js";
|
|
31
|
+
/** Tools offered upfront; every other built-in or workspace tool waits for tool_search. */
|
|
32
|
+
const OFFERED_TOOLS = new Set([
|
|
33
|
+
"read",
|
|
34
|
+
"bash",
|
|
35
|
+
"edit",
|
|
36
|
+
"write",
|
|
37
|
+
"send_message",
|
|
38
|
+
"search_memory",
|
|
39
|
+
"load_memory",
|
|
40
|
+
"save_memory",
|
|
41
|
+
"web_search",
|
|
42
|
+
"fetch_content",
|
|
43
|
+
"tool_search",
|
|
44
|
+
"codemode",
|
|
45
|
+
]);
|
|
46
|
+
function deferRare(extension) {
|
|
47
|
+
if (!extension.tools)
|
|
48
|
+
return extension;
|
|
49
|
+
return {
|
|
50
|
+
...extension,
|
|
51
|
+
tools: extension.tools.map((tool) => OFFERED_TOOLS.has(tool.name) ? tool : { ...tool, deferred: true }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
31
54
|
/** Explicit startup; pass an isolated runtime in tests instead of opening installed auth. */
|
|
32
55
|
export async function startService(options,
|
|
33
56
|
/** The application owns process HTTP resources; standalone callers retain their transport. */
|
|
@@ -45,6 +68,11 @@ reloadHttpTransport) {
|
|
|
45
68
|
}
|
|
46
69
|
const defaults = { ...runtime.defaults, model: { ...runtime.defaults.model } };
|
|
47
70
|
const browser = await createBrowser(options.workingDir, options.agentDir);
|
|
71
|
+
const codemode = createCodemode();
|
|
72
|
+
const closeResources = async () => {
|
|
73
|
+
await codemode.close();
|
|
74
|
+
await browser.close();
|
|
75
|
+
};
|
|
48
76
|
if (!runtime.models.getModel(defaults.model.provider, defaults.model.modelId))
|
|
49
77
|
throw new Error(`Model is unavailable: ${defaults.model.provider}/${defaults.model.modelId}`);
|
|
50
78
|
async function loadExtensions() {
|
|
@@ -71,21 +99,16 @@ reloadHttpTransport) {
|
|
|
71
99
|
: [
|
|
72
100
|
CodingTools,
|
|
73
101
|
ImageRead,
|
|
102
|
+
// Durable's own tool_search loads the deferred tools.
|
|
103
|
+
createToolSearchExtension(),
|
|
74
104
|
await memoryExtension(options.workingDir),
|
|
75
105
|
webExtension(),
|
|
76
106
|
browser.extension,
|
|
107
|
+
codemode.extension,
|
|
77
108
|
Subagent,
|
|
78
109
|
workspaceExtension(reload),
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
workingDir: options.workingDir,
|
|
82
|
-
currentRelease: process.cwd(),
|
|
83
|
-
deployRoot: "/tmp",
|
|
84
|
-
settled: (taskId) => harness.waitForTask(taskId, BACKGROUND_CONTEXT),
|
|
85
|
-
launch: launchInTerminal,
|
|
86
|
-
}),
|
|
87
|
-
]),
|
|
88
|
-
...(await loadWorkspaceExtensions(options.workingDir)),
|
|
110
|
+
].map(deferRare)),
|
|
111
|
+
...(await loadWorkspaceExtensions(options.workingDir)).map(deferRare),
|
|
89
112
|
];
|
|
90
113
|
validateSchedules(extensions);
|
|
91
114
|
const names = new Set();
|
|
@@ -94,7 +117,7 @@ reloadHttpTransport) {
|
|
|
94
117
|
throw new Error(`Duplicate extension name: ${extension.name}`);
|
|
95
118
|
names.add(extension.name);
|
|
96
119
|
}
|
|
97
|
-
//
|
|
120
|
+
// An interrupted tool reruns on recovery unless it declares `unsafe`.
|
|
98
121
|
return extensions.map((extension) => extension.tools
|
|
99
122
|
? { ...extension, tools: extension.tools.map((tool) => ({ ...tool, replay: tool.replay ?? "safe" })) }
|
|
100
123
|
: extension);
|
|
@@ -127,7 +150,6 @@ reloadHttpTransport) {
|
|
|
127
150
|
const jobId = input && typeof input === "object" && !Array.isArray(input) ? input.jobId : undefined;
|
|
128
151
|
const saved = savedSchedules?.items.find((schedule) => schedule.id === jobId);
|
|
129
152
|
if (saved &&
|
|
130
|
-
saved.id !== "compact-chats" &&
|
|
131
153
|
!activeExtensions.some((extension) => extension.schedules?.some((schedule) => schedule.id === saved.id)))
|
|
132
154
|
throw new Error(`Missing extension for unfinished schedule: ${saved.id}`);
|
|
133
155
|
}
|
|
@@ -139,7 +161,7 @@ reloadHttpTransport) {
|
|
|
139
161
|
});
|
|
140
162
|
}
|
|
141
163
|
catch (error) {
|
|
142
|
-
await owner.close(
|
|
164
|
+
await owner.close(closeResources);
|
|
143
165
|
throw error;
|
|
144
166
|
}
|
|
145
167
|
let reloading = Promise.resolve();
|
|
@@ -360,24 +382,6 @@ reloadHttpTransport) {
|
|
|
360
382
|
return receipt;
|
|
361
383
|
});
|
|
362
384
|
},
|
|
363
|
-
/** Admit quiet native compactions; observing their results must not hold shutdown open. */
|
|
364
|
-
compact() {
|
|
365
|
-
return track(async () => {
|
|
366
|
-
const tasks = await compactChats(harness);
|
|
367
|
-
for (const task of tasks)
|
|
368
|
-
void harness
|
|
369
|
-
.waitForTask(task, BACKGROUND_CONTEXT)
|
|
370
|
-
.then((settled) => {
|
|
371
|
-
if (settled.state.outcome.status !== "completed")
|
|
372
|
-
console.warn("Scheduled compaction ended", task, settled.state.outcome);
|
|
373
|
-
})
|
|
374
|
-
.catch((error) => {
|
|
375
|
-
if (!closed)
|
|
376
|
-
console.warn("Scheduled compaction observation failed", task, error);
|
|
377
|
-
});
|
|
378
|
-
return tasks;
|
|
379
|
-
});
|
|
380
|
-
},
|
|
381
385
|
// Explicit host boundary; no model loop, timer or transport runs on import.
|
|
382
386
|
deliver() {
|
|
383
387
|
return track(async () => {
|
|
@@ -394,7 +398,7 @@ reloadHttpTransport) {
|
|
|
394
398
|
closing ??= (async () => {
|
|
395
399
|
await Promise.allSettled([...operations]);
|
|
396
400
|
await progress.close();
|
|
397
|
-
await owner.close(
|
|
401
|
+
await owner.close(closeResources);
|
|
398
402
|
})();
|
|
399
403
|
return closing;
|
|
400
404
|
},
|
package/dist/transport/watch.js
CHANGED
|
@@ -62,7 +62,13 @@ export function createWatcher(options) {
|
|
|
62
62
|
const attachments = db
|
|
63
63
|
.prepare(`SELECT a.filename FROM attachment a
|
|
64
64
|
JOIN message_attachment_join j ON j.attachment_id=a.ROWID WHERE j.message_id=?`)
|
|
65
|
-
.all(row.rowid).flatMap(({ filename }) =>
|
|
65
|
+
.all(row.rowid).flatMap(({ filename }) =>
|
|
66
|
+
// Link-preview and iMessage app payload plists carry nothing a model can read; the text already
|
|
67
|
+
// holds the URL. Messages may record one in a transient transcoder directory and delete the
|
|
68
|
+
// file, so archiving it would hold this row forever.
|
|
69
|
+
filename && !filename.endsWith(".pluginPayloadAttachment")
|
|
70
|
+
? [filename.replace(/^~(?=\/)/, homedir())]
|
|
71
|
+
: []);
|
|
66
72
|
const reply = row.replyGuid
|
|
67
73
|
? db.prepare("SELECT text,attributedBody FROM message WHERE guid=?").get(row.replyGuid)
|
|
68
74
|
: undefined;
|
package/dist/web/app.js
CHANGED
|
@@ -427,15 +427,18 @@ function renderSettings() {
|
|
|
427
427
|
|
|
428
428
|
function renderChatSettings() {
|
|
429
429
|
const chats = data.chats || [];
|
|
430
|
-
const overrides = chats.filter(
|
|
430
|
+
const overrides = chats.filter(
|
|
431
|
+
(chat) => chat.verbose.chat !== undefined || chat.interim.chat !== undefined,
|
|
432
|
+
).length;
|
|
431
433
|
view.replaceChildren(
|
|
432
434
|
element(
|
|
433
435
|
"p",
|
|
434
436
|
[
|
|
435
437
|
`${chats.length} chats`,
|
|
436
438
|
`default verbose ${data.defaults?.progressMessages ? "on" : "off"}`,
|
|
439
|
+
`default interim ${data.defaults?.interimMessages ? "on" : "off"}`,
|
|
437
440
|
`${overrides} ${overrides === 1 ? "override" : "overrides"}`,
|
|
438
|
-
"read-only: /verbose and /thinking change a chat, settings.json the default",
|
|
441
|
+
"read-only: /verbose, /interim and /thinking change a chat, settings.json the default",
|
|
439
442
|
].join(" · "),
|
|
440
443
|
"muted",
|
|
441
444
|
),
|
|
@@ -455,7 +458,7 @@ function renderChatSettings() {
|
|
|
455
458
|
card.append(header);
|
|
456
459
|
const table = element("table", undefined, "chat-table");
|
|
457
460
|
const heading = table.createTHead().insertRow();
|
|
458
|
-
for (const label of ["Chat", "Replies", "Verbose", "Model", "Thinking", "Last activity"])
|
|
461
|
+
for (const label of ["Chat", "Replies", "Verbose", "Interim", "Model", "Thinking", "Last activity"])
|
|
459
462
|
heading.append(element("th", label));
|
|
460
463
|
const body = table.createTBody();
|
|
461
464
|
for (const chat of rows) {
|
|
@@ -476,6 +479,10 @@ function renderChatSettings() {
|
|
|
476
479
|
chat.verbose.effective ? "on" : "off",
|
|
477
480
|
chat.verbose.chat === undefined ? "default" : "this chat",
|
|
478
481
|
),
|
|
482
|
+
stacked(
|
|
483
|
+
chat.interim.effective ? "on" : "off",
|
|
484
|
+
chat.interim.chat === undefined ? "default" : "this chat",
|
|
485
|
+
),
|
|
479
486
|
stacked(modelId.join("/") || "-", chat.model ? provider : undefined),
|
|
480
487
|
stacked(chat.thinking || "-"),
|
|
481
488
|
stacked(activity || "-"),
|
package/dist/web/server.js
CHANGED
|
@@ -6,7 +6,7 @@ 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, progressEnabled } from "../agent/chats.js";
|
|
9
|
+
import { Chats, interimEnabled, 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";
|
|
@@ -113,7 +113,7 @@ export async function startWeb(options) {
|
|
|
113
113
|
settings: { chatAllowlist: configuration.chatAllowlist },
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
-
/** One row per chat: reply eligibility, the /verbose
|
|
116
|
+
/** One row per chat: reply eligibility, the /verbose and /interim choices and the conversation's agent settings. */
|
|
117
117
|
async function chatSettings() {
|
|
118
118
|
const [chats, configuration] = await Promise.all([
|
|
119
119
|
options.agent.harness.snapshot(Chats, BACKGROUND_CONTEXT),
|
|
@@ -132,6 +132,7 @@ export async function startWeb(options) {
|
|
|
132
132
|
updatedAt: entries ? historyActivity(entries) : undefined,
|
|
133
133
|
replies: isReplyEnabled(configuration, chat.chatGuid),
|
|
134
134
|
verbose: { chat: chat.progress, effective: progressEnabled(configuration, records, chat.chatGuid) },
|
|
135
|
+
interim: { chat: chat.interim, effective: interimEnabled(configuration, records, chat.chatGuid) },
|
|
135
136
|
model: agent?.model ? `${agent.model.provider}/${agent.model.modelId}` : undefined,
|
|
136
137
|
thinking: agent?.thinkingLevel,
|
|
137
138
|
};
|
|
@@ -139,7 +140,10 @@ export async function startWeb(options) {
|
|
|
139
140
|
rows.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0) || Number(b.conversationId) - Number(a.conversationId));
|
|
140
141
|
return {
|
|
141
142
|
chats: rows,
|
|
142
|
-
defaults: {
|
|
143
|
+
defaults: {
|
|
144
|
+
progressMessages: configuration.progressMessages,
|
|
145
|
+
interimMessages: configuration.interimMessages,
|
|
146
|
+
},
|
|
143
147
|
revision: viewRevision(),
|
|
144
148
|
};
|
|
145
149
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 0007. Reconcile unsent answers after intervening input
|
|
2
2
|
|
|
3
3
|
Status: accepted (2026-10-04; owner chose reconciliation and explicitly included different senders)
|
|
4
4
|
|
|
5
|
-
**Context.** Native steer places all selected messages after a tool round when configured with `steeringMode: "all"`. At a final-answer boundary, however, Durable first records the answer and settles that run's inputs; selected new messages start another run. The earlier answer remains eligible for delivery even when a newer message corrects its assumptions. An `onYield` continuation does not change this: it is ignored when the boundary selects user input. See [the state ownership decision](0001-durable-owns-state.md)
|
|
5
|
+
**Context.** Native steer places all selected messages after a tool round when configured with `steeringMode: "all"`. At a final-answer boundary, however, Durable first records the answer and settles that run's inputs; selected new messages start another run. The earlier answer remains eligible for delivery even when a newer message corrects its assumptions. An `onYield` continuation does not change this: it is ignored when the boundary selects user input. See [the state ownership decision](0001-durable-owns-state.md).
|
|
6
6
|
|
|
7
7
|
**Example.** The model is finishing a two-day itinerary when the user says “three days instead.” Sending both itineraries is confusing. Conversely, blindly dropping the first answer when the new message asks an unrelated question can lose useful work.
|
|
8
8
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 0008. `/run` works in a conversation owned by one task
|
|
2
2
|
|
|
3
|
-
Status: accepted (2026-10-04; owner accepted 2026-10-05).
|
|
3
|
+
Status: accepted (2026-10-04; owner accepted 2026-10-05).
|
|
4
4
|
|
|
5
|
-
**Context.**
|
|
5
|
+
**Context.** Before this decision, `/run` worked inside the chat's own conversation. Stopping it then means telling run work apart from ordinary work: `Conversation.abort()` also withdraws ordinary queued messages, so the draft tracked submission IDs and command receipts by hand. Durable already scopes abort by ownership: aborting a task aborts the conversations and work it owns.
|
|
6
6
|
|
|
7
7
|
**Example.** The user sends `/run 30m plan a two-day trip`, then "make it three days". The correction must reach the run. A later `/stop` must end the run without touching anything else.
|
|
8
8
|
|
|
@@ -12,9 +12,9 @@ Status: accepted (2026-10-04; owner accepted 2026-10-05). Replaces the `/run` pa
|
|
|
12
12
|
- `/run` starts only when the chat conversation is idle. Otherwise it replies that the chat is busy and does nothing: no queueing, no moving of running work. A repeated `/run` during a run updates the deadline; a task text in it is sent to the run like an ordinary message.
|
|
13
13
|
- While a run is active, all ordinary messages of the chat go to the run conversation as steers, with the message GUID as request ID. Commands do not.
|
|
14
14
|
- An `onYield` hook continues the run until the model calls `end_run` (done, or the user is needed) or the deadline passes. The deadline only stops continuation: the current turn finishes, so a run can overrun. Ordinary messages do not extend it. Without unfinished work, the model ends the run instead of inventing a task.
|
|
15
|
-
- Final answers of the run conversation are delivered as they are committed, including those between continuations,
|
|
15
|
+
- Final answers of the run conversation are delivered as they are committed, including those between continuations, at most once and under [0007](0007-reconcile-unsent-answers.md).
|
|
16
16
|
- `/stop` aborts the run task. Durable aborts the run's work and withdraws its queued messages; they stay in history, unanswered, and are not resubmitted anywhere. Without an active run, `/stop` replies that nothing is running. There is no `/run-stop`. `/new` also stops the run.
|
|
17
|
-
- A restart does not stop a run. `resume()` continues the run task like any other pending work, with the deadline from our run document; if it passed during the downtime, the current turn finishes and the run ends.
|
|
17
|
+
- A restart does not stop a run. `resume()` continues the run task like any other pending work, with the deadline from our run document; if it passed during the downtime, the current turn finishes and the run ends. A reply interrupted mid-send becomes `unknown` and is not resent.
|
|
18
18
|
- The chat conversation keeps the record: the run's user messages and sent replies are written to it as passive entries, plus how the run ended. Tool calls and results stay in the run conversation, searchable under `durable/` like other history.
|
|
19
19
|
|
|
20
20
|
**Consequences.** Stopping needs no submission bookkeeping and no command receipts. During a run, the chat has one place where work happens. Messages still queued in the run when `/stop` arrives are dropped, a narrower promise than before. After the run, the chat conversation knows what was said, not how; the model searches the run conversation when it needs details.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 0010. The framework integrates iMessage and Durable; the workspace owns behavior
|
|
2
|
+
|
|
3
|
+
Status: accepted (2026-10-31, owner, grilling session)
|
|
4
|
+
|
|
5
|
+
**Problem.** Personal behavior, preferences and operations crept into the
|
|
6
|
+
framework: a hard-coded idle reset, chat etiquette in the product prompt,
|
|
7
|
+
claw-specific Git, deployment and restart procedures in the repository.
|
|
8
|
+
|
|
9
|
+
**Decision.**
|
|
10
|
+
|
|
11
|
+
- The *framework* is this repository and the service it runs. The *workspace*
|
|
12
|
+
is `WORKING_DIR` (`settings.json`, `AGENTS.md`, `extensions/`, `skills/`).
|
|
13
|
+
- The framework does only the integration of iMessage and Durable: transport,
|
|
14
|
+
chat-to-conversation mapping, delivery and receipts, scheduling machinery,
|
|
15
|
+
the extension loader and read-only views. It keeps general tools (web,
|
|
16
|
+
browser, subagent, memory, images, messages).
|
|
17
|
+
- Every other behavior lives in the workspace: personal tasks, chat etiquette,
|
|
18
|
+
thresholds such as idle reset, and operations (Git flow, deployment, restart).
|
|
19
|
+
The framework keeps no default or example for it.
|
|
20
|
+
- The product prompt states only facts of the integration (interim and delivery
|
|
21
|
+
semantics, different senders are different people). How to behave belongs in
|
|
22
|
+
workspace `AGENTS.md`.
|
|
23
|
+
- Workspace code is a plain Durable extension built with the framework's pinned
|
|
24
|
+
Durable SDK; the framework adds no API of its own for it.
|
|
25
|
+
|
|
26
|
+
**Consequences.** Another deployment gets the integration and nothing personal.
|
|
27
|
+
Changing behavior is a workspace edit and `/reload`, not a release.
|
package/docs/adr/README.md
CHANGED
|
@@ -1,54 +1,31 @@
|
|
|
1
1
|
# Architecture decision records
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
"Active" means the decision is in force. A replaced ADR points to its successor.
|
|
3
|
+
These are the decisions in force. Accepted ADR files are never edited.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
| ADR | Decision |
|
|
6
|
+
|---|---|
|
|
7
|
+
| [0001](0001-durable-owns-state.md) | Pi Durable owns all chat state |
|
|
8
|
+
| [0002](0002-module-layering.md) | Modules in strict layers |
|
|
9
|
+
| [0003](0003-native-extensions.md) | Native Durable extensions, not an SDK adapter |
|
|
10
|
+
| [0004](0004-jsonl-storage.md) | One JSONL storage, backed up by hand |
|
|
11
|
+
| [0005](0005-images-by-path.md) | Store images as file paths |
|
|
12
|
+
| [0006](0006-test-budget.md) | About 40 tests |
|
|
13
|
+
| [0007](0007-reconcile-unsent-answers.md) | Reconcile unsent answers |
|
|
14
|
+
| [0008](0008-run-in-owned-conversation.md) | `/run` in an owned conversation |
|
|
15
|
+
| [0009](0009-native-background-schedules.md) | Native background schedules and read-only execution history |
|
|
16
|
+
| [0010](0010-framework-and-workspace.md) | The framework integrates iMessage and Durable; the workspace owns behavior |
|
|
7
17
|
|
|
8
|
-
|
|
9
|
-
|---|---|---|
|
|
10
|
-
| [0001](0001-durable-owns-state.md) | Pi Durable owns all chat state | Active |
|
|
11
|
-
| [0002](0002-module-layering.md) | Modules in strict layers | Active |
|
|
12
|
-
| [0003](0003-replies-at-most-once.md) | Replies are sent at most once | Active |
|
|
13
|
-
| [0004](0004-native-extensions.md) | Native Durable extensions, not an SDK adapter | Active; Codex tier clause replaced by 0036 |
|
|
14
|
-
| [0006](0006-jsonl-storage.md) | One JSONL storage, backed up by hand | Active |
|
|
15
|
-
| [0007](0007-images-by-path.md) | Store images as file paths | Active |
|
|
16
|
-
| [0008](0008-compaction-and-run.md) | Six-hour compaction | Active; `/run` clause replaced by 0012 |
|
|
17
|
-
| [0010](0010-test-budget.md) | About 40 tests | Active |
|
|
18
|
-
| [0011](0011-reconcile-unsent-answers.md) | Reconcile unsent answers | Active |
|
|
19
|
-
| [0012](0012-run-in-owned-conversation.md) | `/run` in an owned conversation | Active |
|
|
20
|
-
| [0013](0013-thin-adapters-around-durable.md) | Thin adapters around Durable | Active; scheduler clause replaced by 0042 |
|
|
21
|
-
| [0016](0016-process-http-transport.md) | One process-owned HTTP transport | Active |
|
|
22
|
-
| [0017](0017-browser-cli-owned-scope.md) | Conversation-scoped browser CLI tool | Accepted, to be implemented |
|
|
23
|
-
| [0018](0018-tasks-without-prompt-api.md) | Ordinary `/stop`; no HTTP `/prompt` or `/send` | Active |
|
|
24
|
-
| [0036](0036-workspace-policy-and-neutral-service-id.md) | Workspace policy and neutral service ID | Active |
|
|
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; self-restart path added by 0052 |
|
|
27
|
-
| [0042](0042-background-schedules-and-workspace-extensions.md) | Background schedules and workspace extensions | Active; operator-run clause replaced by 0047 |
|
|
28
|
-
| [0043](0043-remove-legacy-migration.md) | Remove the legacy migration code | Active; archived at tag `archive/legacy-migration` |
|
|
29
|
-
| [0044](0044-message-channels.md) | Organize message sources as channels | Proposed |
|
|
30
|
-
| [0045](0045-chat-scheduling-tools.md) | Native chat scheduling tools | Proposed |
|
|
18
|
+
## Adding an ADR
|
|
31
19
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
Do not add an ADR on your own initiative. A new ADR, including one that
|
|
21
|
+
reverses a decision above, is written only during a grilling session (the
|
|
22
|
+
`grilling` skill), to record what the owner settled there. Outside such a
|
|
23
|
+
session, describe the change in the GitHub issue, `docs/architecture.md` or
|
|
24
|
+
the code.
|
|
37
25
|
|
|
38
|
-
##
|
|
26
|
+
## Removed ADRs
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
| [0014](0014-ephemeral-private-storage.md) | Ephemeral private storage | 0018 |
|
|
45
|
-
| [0015](0015-ordinary-stop-keeps-native-inbox.md) | `/stop` keeps the native inbox | 0018 |
|
|
46
|
-
| [0030](0030-shared-web-read-cache.md) | Shared web read cache | Rejected; removed |
|
|
47
|
-
| [0019](0019-remove-scheduled-services.md) | Remove scheduled services | 0042 |
|
|
48
|
-
| [0020](0020-chat-task-progress.md)–[0028](0028-command-preview-120.md) | Progress notice format | 0040 |
|
|
49
|
-
| [0031](0031-quiet-compaction-progress.md) | Quiet six-hour compaction progress | 0040 |
|
|
50
|
-
| [0032](0032-native-background-schedules.md)–[0035](0035-external-business-task-extensions.md) | Native schedules, operator run, local time, workspace extensions | 0042 |
|
|
51
|
-
| [0037](0037-restart-during-in-flight-work.md)–[0039](0039-rerun-interrupted-tools.md) | Restart and tool rerun | 0041 |
|
|
52
|
-
|
|
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.
|
|
28
|
+
All other ADRs were removed on 2026-10-31, together with every reference to
|
|
29
|
+
them, and the rest renumbered without gaps. The files are kept, under their old
|
|
30
|
+
numbers, at the annotated tag `archive/adr-pre-prune`
|
|
31
|
+
(`git show archive/adr-pre-prune:docs/adr/<file>`).
|
package/docs/api.md
CHANGED
|
@@ -7,7 +7,7 @@ The server has no authentication; do not expose it to untrusted networks.
|
|
|
7
7
|
|
|
8
8
|
The pages `/`, `/chat`, `/chats`, `/tasks`, `/scheduled`, `/settings`, `/memory` and `/logs` are read-only.
|
|
9
9
|
The Chats tab (`/chats`, `/chats/data`) lists groups and direct chats, one row
|
|
10
|
-
each: reply eligibility, the `/verbose`
|
|
10
|
+
each: reply eligibility, the `/verbose` and `/interim` choices with their effective values, the
|
|
11
11
|
conversation's model and thinking level, and last activity. Chats that do not
|
|
12
12
|
reply are dimmed. It never shows message text. The message history page in the
|
|
13
13
|
navigation is labelled Messages.
|
|
@@ -47,8 +47,7 @@ requests; detail history and its older-page cursors are not reduced to 15
|
|
|
47
47
|
messages. Existing `/chat/data` and conversation detail responses without these
|
|
48
48
|
view parameters retain their native state shapes.
|
|
49
49
|
|
|
50
|
-
Every request reads current state directly; there is no server-side read cache
|
|
51
|
-
(ADR 0030 was rejected).
|
|
50
|
+
Every request reads current state directly; there is no server-side read cache.
|
|
52
51
|
|
|
53
52
|
Pages load once and refresh only through the Refresh button, memory filtering/
|
|
54
53
|
pagination or explicit navigation. There is no browser SSE connection, periodic
|
|
@@ -69,10 +68,10 @@ that arbitrary user-written secrets can be identified.
|
|
|
69
68
|
|
|
70
69
|
The agent sends files and extra messages with its immediate `send_message`
|
|
71
70
|
tool. HTTP `/prompt` and `/send` remain retired. Legacy cron management,
|
|
72
|
-
reminders and PT Automation routes remain removed and return 404
|
|
71
|
+
reminders and PT Automation routes remain removed and return 404.
|
|
73
72
|
|
|
74
73
|
`GET /scheduled` and `GET /scheduled/data` expose configured native background
|
|
75
|
-
jobs
|
|
74
|
+
jobs, not the retained legacy cron files. The JSON view contains `jobs`
|
|
76
75
|
with cadence, enabled state, native scheduler task/conversation IDs, phase and
|
|
77
76
|
`nextAt`, and each job's latest ten execution tasks. `recent` contains the latest
|
|
78
77
|
ten executions across all jobs. Results separate native task outcome from the
|
|
@@ -82,7 +81,7 @@ run, pause or resend buttons. Scheduling configuration takes effect at startup.
|
|
|
82
81
|
Daily slots and learning dates use the service machine's local timezone (or an
|
|
83
82
|
explicit process `TZ`). Each job includes `timezone`; Web deadlines and history
|
|
84
83
|
use it even when the browser is in another timezone. Saved absolute deadlines
|
|
85
|
-
remain unchanged on restart
|
|
84
|
+
remain unchanged on restart.
|
|
86
85
|
|
|
87
86
|
| Endpoint | Description | Example |
|
|
88
87
|
|---|---|---|
|
|
@@ -99,6 +98,15 @@ previews. Raw outputs, commands, prompts and document contents stay local.
|
|
|
99
98
|
Known credential indicators are redacted; arbitrary user-authored secrets cannot
|
|
100
99
|
be guaranteed detectable. Missing or false keeps final-only delivery. The setting
|
|
101
100
|
is reread for each notice; old tasks and unknown sends are not replayed
|
|
102
|
-
|
|
101
|
+
. `/verbose on|off` in a chat
|
|
103
102
|
overrides this default for that chat only and `/verbose default` removes the
|
|
104
|
-
override
|
|
103
|
+
override.
|
|
104
|
+
|
|
105
|
+
Text the model writes next to a tool call is sent at once as an interim message
|
|
106
|
+
in reply-enabled chats; subagent text stays private. `interimMessages: false` in
|
|
107
|
+
`settings.json` turns this off by default, and `/interim on|off|default` sets it
|
|
108
|
+
for one chat.
|
|
109
|
+
|
|
110
|
+
`hiddenSkills` in `settings.json` is a list of skill names left out of the
|
|
111
|
+
prompt's skill catalog; their files stay readable by path. It is read at startup
|
|
112
|
+
and on `/reload`.
|
package/docs/architecture.md
CHANGED
|
@@ -45,15 +45,27 @@ extensions. Source and configuration reload without interrupting current calls.
|
|
|
45
45
|
Failed reloads retain old definitions and schedule configuration. Removed task
|
|
46
46
|
code stays in memory for calls that can still admit children; missing required
|
|
47
47
|
code fails explicitly on restart. The assistant's `reload_extensions` tool uses
|
|
48
|
-
the same service owner. See [workspace extensions](workspace-extensions.md)
|
|
49
|
-
ADR 0035 and ADR 0036.
|
|
48
|
+
the same service owner. See [workspace extensions](workspace-extensions.md).
|
|
50
49
|
|
|
51
50
|
The built-in native `browser` extension wraps the pinned pi-browser CLI with
|
|
52
51
|
host-derived conversation scopes. Browser children get private HOME/cache,
|
|
53
52
|
profile/output and cwd; other coding tools are unchanged. The host owns scoped
|
|
54
53
|
stop/reset/shutdown/restart cleanup, while Durable owns invocation state and
|
|
55
54
|
unsafe replay policy. Read-only contexts do not offer the tool. This is not an
|
|
56
|
-
OS sandbox; see [browser operations and retention](browser.md)
|
|
55
|
+
OS sandbox; see [browser operations and retention](browser.md).
|
|
56
|
+
|
|
57
|
+
The model is offered a fixed core of tools; every other built-in or workspace tool
|
|
58
|
+
is deferred and loaded on demand with Durable's `tool_search`, the same in every
|
|
59
|
+
conversation. The core includes Durable's `codemode` (`extensions/codemode.ts`):
|
|
60
|
+
the model writes a script that calls the conversation's tools in a QuickJS
|
|
61
|
+
sandbox and only the script's output enters the context; `models.*` is off, so
|
|
62
|
+
scripts cannot start model calls, and the prompt section lists callable tools
|
|
63
|
+
within a small token budget.
|
|
64
|
+
|
|
65
|
+
`/run <duration>` (ADR 0008) treats the duration as time to use: after the
|
|
66
|
+
stated task is done the run keeps verifying and improving within its scope
|
|
67
|
+
until the deadline, and `end_run` ends it early only when blocked, when the
|
|
68
|
+
user is needed or when nothing in scope remains.
|
|
57
69
|
|
|
58
70
|
|
|
59
71
|
### Native schedules
|
|
@@ -62,15 +74,14 @@ OS sandbox; see [browser operations and retention](browser.md) and ADR 0017.
|
|
|
62
74
|
job's dedicated conversation owns a background scheduler, whose checkpoint saves
|
|
63
75
|
the next deadline. Each occurrence is a child execution task delegating to an
|
|
64
76
|
extension's native business task; terminal receipts supply Web history, not a
|
|
65
|
-
parallel UI log. The framework owns daily/interval cadence, non-overlap,
|
|
66
|
-
|
|
77
|
+
parallel UI log. The framework owns daily/interval cadence, non-overlap and recovery, and
|
|
78
|
+
defines no schedule of its own: chat context is bounded by Durable's thresholds
|
|
79
|
+
against the window each model advertises. Workspace tasks own business policy and
|
|
67
80
|
documents, and can commit their state and the generic outgoing item atomically.
|
|
68
81
|
The host delivery poll uses existing direct-send receipts and passive chat records.
|
|
69
82
|
`web/schedules.ts` projects these native records, including latest-ten history.
|
|
70
83
|
Daily slots and learning dates use the service machine's local timezone; the Web
|
|
71
|
-
view uses the same timezone. Saved absolute deadlines survive restart.
|
|
72
|
-
[ADR 0032](adr/0032-native-background-schedules.md) and
|
|
73
|
-
[ADR 0034](adr/0034-machine-local-schedule-time.md).
|
|
84
|
+
view uses the same timezone. Saved absolute deadlines survive restart.
|
|
74
85
|
|
|
75
86
|
## 2. Workflow
|
|
76
87
|
|