@kingcrab/pi-imessage 1.1.1 → 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 +42 -49
- package/dist/transport/watch.js +7 -1
- package/dist/web/app.js +63 -121
- package/dist/web/server.js +7 -14
- package/dist/web/style.css +0 -6
- package/dist/web/tasks.js +12 -120
- 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 -48
- 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/agent/error-log.js +0 -37
- 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/docs/adr/0053-service-log-errors.md +0 -44
- package/ops/README.md +0 -250
- package/ops/preflight.mjs +0 -28
package/dist/web/tasks.js
CHANGED
|
@@ -1,139 +1,33 @@
|
|
|
1
1
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
2
|
-
import { UserEntry, } from "@earendil-works/pi-durable";
|
|
3
2
|
import { displayHistory, historyActivity } from "./history.js";
|
|
4
|
-
/** Newest
|
|
5
|
-
export const RECENT_GENERATIONS_PER_CONVERSATION = 10;
|
|
6
|
-
/** Newest tasks shown for a conversation that never generated (schedulers). */
|
|
3
|
+
/** Newest tasks listed per conversation; older finished ones are only flagged. */
|
|
7
4
|
export const RECENT_TASKS_PER_CONVERSATION = 20;
|
|
8
|
-
/** Pages of 5 entries read back from the oldest generation to find the user message that opened its turn. */
|
|
9
|
-
const TURN_LOOKBACK_PAGES = 4;
|
|
10
|
-
function firstLine(text, limit = 80) {
|
|
11
|
-
return text
|
|
12
|
-
.replace(/^\[(?:Group '[^']*'|DM|SMS) from [^\]]+\] /, "")
|
|
13
|
-
.split("\n")
|
|
14
|
-
.find((line) => line.trim())
|
|
15
|
-
?.trim()
|
|
16
|
-
.slice(0, limit);
|
|
17
|
-
}
|
|
18
|
-
/** One line per entry: the first user or assistant message, tool calls by name only, tool results by text. */
|
|
19
|
-
function summarizeEntry(entry) {
|
|
20
|
-
const message = entry.model?.[0];
|
|
21
|
-
if (!message)
|
|
22
|
-
return { role: undefined, text: undefined };
|
|
23
|
-
const content = message.content;
|
|
24
|
-
const text = typeof content === "string"
|
|
25
|
-
? content
|
|
26
|
-
: content
|
|
27
|
-
.map((block) => block.type === "text"
|
|
28
|
-
? block.text
|
|
29
|
-
: block.type === "toolCall"
|
|
30
|
-
? `[toolCall ${block.name}]`
|
|
31
|
-
: block.type === "thinking"
|
|
32
|
-
? "[thinking]"
|
|
33
|
-
: `[${block.type}]`)
|
|
34
|
-
.filter(Boolean)
|
|
35
|
-
.join(" ");
|
|
36
|
-
return { role: message.role, text: firstLine(text) };
|
|
37
|
-
}
|
|
38
5
|
/**
|
|
39
6
|
* Read one consistent tree, newest first: all conversations, every unfinished task, every task that owns a
|
|
40
|
-
* conversation
|
|
41
|
-
*
|
|
42
|
-
* id sequence, so the window is a plain id range and the browser nests records by `owner` and `byTaskId`
|
|
43
|
-
* without any inference. Descending scans stop at the window instead of reading the whole history
|
|
44
|
-
* (earendil-works/pi#10546).
|
|
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).
|
|
45
9
|
*/
|
|
46
10
|
export function readTaskTree(harness) {
|
|
47
11
|
return harness.commit(async (tx) => {
|
|
48
12
|
const conversations = [];
|
|
49
13
|
const kept = new Map();
|
|
50
|
-
const entries = [];
|
|
51
14
|
const more = {};
|
|
52
15
|
let cursor;
|
|
53
16
|
do {
|
|
54
17
|
const page = await tx.scanConversations({}, 100, cursor);
|
|
55
18
|
for (const conversation of page.items) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
// conversation filter walks the global task order, so it must not run twice per conversation.
|
|
59
|
-
const recent = [];
|
|
60
|
-
let oldest;
|
|
61
|
-
let generations = 0;
|
|
62
|
-
let next;
|
|
63
|
-
scan: do {
|
|
64
|
-
const tasks = await tx.scanTasks({ conversationId: id, order: "descending" }, 100, next);
|
|
65
|
-
for (const task of tasks.items) {
|
|
66
|
-
if (task.kind === "pi.generation") {
|
|
67
|
-
if (++generations > RECENT_GENERATIONS_PER_CONVERSATION) {
|
|
68
|
-
more[id] = true;
|
|
69
|
-
break scan;
|
|
70
|
-
}
|
|
71
|
-
oldest = task;
|
|
72
|
-
}
|
|
73
|
-
else if (!generations && recent.length >= RECENT_TASKS_PER_CONVERSATION) {
|
|
74
|
-
more[id] = true;
|
|
75
|
-
break scan;
|
|
76
|
-
}
|
|
77
|
-
recent.push(task);
|
|
78
|
-
}
|
|
79
|
-
next = tasks.next;
|
|
80
|
-
} while (next);
|
|
81
|
-
let history;
|
|
82
|
-
let floor;
|
|
83
|
-
if (oldest) {
|
|
84
|
-
history = [];
|
|
85
|
-
let next;
|
|
86
|
-
do {
|
|
87
|
-
const window = await tx.scanEntries({ conversationId: id, minEntryId: oldest.id }, 100, next);
|
|
88
|
-
history.push(...window.items);
|
|
89
|
-
next = window.next;
|
|
90
|
-
} while (next);
|
|
91
|
-
// Walk back to the user message that opened the oldest generation's turn.
|
|
92
|
-
let max = oldest.id - 1;
|
|
93
|
-
for (let pages = 0; pages < TURN_LOOKBACK_PAGES; pages++) {
|
|
94
|
-
const older = await tx.scanEntries({ conversationId: id, maxEntryId: max }, 5);
|
|
95
|
-
const user = older.items.findIndex((entry) => UserEntry.is(entry));
|
|
96
|
-
history.push(...(user === -1 ? older.items : older.items.slice(0, user + 1)));
|
|
97
|
-
if (user !== -1 || !older.items.length)
|
|
98
|
-
break;
|
|
99
|
-
max = older.items[older.items.length - 1].id - 1;
|
|
100
|
-
}
|
|
101
|
-
const turn = history.find((entry) => UserEntry.is(entry) && entry.id < oldest.id);
|
|
102
|
-
floor = turn ? turn.id : oldest.id;
|
|
103
|
-
history = history.filter((entry) => entry.id >= floor);
|
|
104
|
-
// Tasks between the turn's user message and its first generation sit on the next page.
|
|
105
|
-
while (next && (recent.at(-1)?.id ?? 0) >= floor) {
|
|
106
|
-
const tasks = await tx.scanTasks({ conversationId: id, order: "descending" }, 100, next);
|
|
107
|
-
recent.push(...tasks.items.filter((task) => task.id >= floor));
|
|
108
|
-
next = tasks.next;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
// Names and activity still come from the newest 100 entries, as on the Messages page.
|
|
113
|
-
history = [...(await tx.scanEntries({ conversationId: id }, 100)).items];
|
|
114
|
-
floor = history[RECENT_TASKS_PER_CONVERSATION - 1]?.id ?? 0;
|
|
115
|
-
}
|
|
116
|
-
const page = { items: history, next: undefined };
|
|
117
|
-
const { groupName } = displayHistory(page, 1);
|
|
19
|
+
const history = await tx.scanEntries({ conversationId: conversation.id }, 100);
|
|
20
|
+
const { groupName } = displayHistory(history, 1);
|
|
118
21
|
conversations.push({
|
|
119
22
|
...conversation,
|
|
120
|
-
updatedAt: historyActivity(
|
|
23
|
+
updatedAt: historyActivity(history),
|
|
121
24
|
groupName: typeof groupName === "string" ? groupName : undefined,
|
|
122
25
|
});
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
conversationId: id,
|
|
129
|
-
kind: entry.kind,
|
|
130
|
-
byTaskId: entry.byTaskId,
|
|
131
|
-
...summarizeEntry(entry),
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
for (const task of recent)
|
|
135
|
-
if (!oldest || task.id >= floor)
|
|
136
|
-
kept.set(task.id, task);
|
|
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;
|
|
137
31
|
if (conversation.owner && !kept.has(conversation.owner.taskId)) {
|
|
138
32
|
const owner = await tx.task(conversation.owner.taskId);
|
|
139
33
|
if (owner)
|
|
@@ -191,15 +85,13 @@ export function readTaskTree(harness) {
|
|
|
191
85
|
background: task.background,
|
|
192
86
|
abortRequested: task.abortRequested,
|
|
193
87
|
status: state.status,
|
|
194
|
-
// Phases exist only while a task runs; a terminal record keeps its outcome and drops the checkpoint.
|
|
195
88
|
phase: state.checkpoint && typeof state.checkpoint === "object" && !Array.isArray(state.checkpoint)
|
|
196
89
|
? state.checkpoint.phase
|
|
197
90
|
: undefined,
|
|
198
91
|
on: state.status === "waiting" ? state.on : undefined,
|
|
199
|
-
until: state.status === "sleeping" ? state.until : undefined,
|
|
200
92
|
outcome: state.outcome?.status,
|
|
201
93
|
});
|
|
202
94
|
}
|
|
203
|
-
return { conversations, tasks,
|
|
95
|
+
return { conversations, tasks, more };
|
|
204
96
|
}, BACKGROUND_CONTEXT);
|
|
205
97
|
}
|
|
@@ -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,55 +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
|
-
|
|
37
|
-
| [0053](0053-service-log-errors.md) | Service-log errors for failures that break normal operation | Proposed; first kind (model call failed) implemented |
|
|
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.
|
|
38
25
|
|
|
39
|
-
##
|
|
26
|
+
## Removed ADRs
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
| [0014](0014-ephemeral-private-storage.md) | Ephemeral private storage | 0018 |
|
|
46
|
-
| [0015](0015-ordinary-stop-keeps-native-inbox.md) | `/stop` keeps the native inbox | 0018 |
|
|
47
|
-
| [0030](0030-shared-web-read-cache.md) | Shared web read cache | Rejected; removed |
|
|
48
|
-
| [0019](0019-remove-scheduled-services.md) | Remove scheduled services | 0042 |
|
|
49
|
-
| [0020](0020-chat-task-progress.md)–[0028](0028-command-preview-120.md) | Progress notice format | 0040 |
|
|
50
|
-
| [0031](0031-quiet-compaction-progress.md) | Quiet six-hour compaction progress | 0040 |
|
|
51
|
-
| [0032](0032-native-background-schedules.md)–[0035](0035-external-business-task-extensions.md) | Native schedules, operator run, local time, workspace extensions | 0042 |
|
|
52
|
-
| [0037](0037-restart-during-in-flight-work.md)–[0039](0039-rerun-interrupted-tools.md) | Restart and tool rerun | 0041 |
|
|
53
|
-
|
|
54
|
-
Numbers 0029, 0050 and 0051 are not used: 0050 and 0051 were interim stop
|
|
55
|
-
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
|
|
package/docs/browser.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Conversation-owned browser
|
|
2
2
|
|
|
3
|
-
The host's built-in native Durable `browser` extension
|
|
3
|
+
The host's built-in native Durable `browser` extension scopes browser work to its conversation. It is
|
|
4
4
|
registered with the other built-in tools, not loaded as a personal workspace
|
|
5
5
|
business extension. It invokes the installed `PI_CODING_AGENT_DIR/bin/pi-browser`
|
|
6
6
|
wrapper and its pinned Playwright CLI by argv; there is no SDK shim, private CLI
|
|
@@ -21,7 +21,7 @@ import or second browser driver.
|
|
|
21
21
|
Open before interacting. A snapshot includes bounded accessibility text with
|
|
22
22
|
current refs. Screenshot/other artifact links resolve from the reported private
|
|
23
23
|
cwd; the result also gives the absolute output directory. Page output is
|
|
24
|
-
untrusted. No attachment, discovery,
|
|
24
|
+
untrusted. No attachment, discovery, global cleanup, arbitrary
|
|
25
25
|
profile/config/filename, storage export/import or arbitrary code execution is
|
|
26
26
|
exposed to the model. Positional text follows `--`, so strings resembling flags
|
|
27
27
|
remain literal. Native health checks and read-only subagents do not offer the tool.
|
|
@@ -36,7 +36,7 @@ and other tool-enabled native conversations receive distinct scope directories:
|
|
|
36
36
|
WORKING_DIR/browser/<sha256-of-canonical-storage>/<conversation-id>/
|
|
37
37
|
owner.json
|
|
38
38
|
home/ # private HOME/cache/CLI daemon registry
|
|
39
|
-
cwd/.playwright/ # private workspace identity and managed
|
|
39
|
+
cwd/.playwright/ # private workspace identity and managed headed config
|
|
40
40
|
state/profiles/owned/ # dedicated non-personal persistent Chrome profile
|
|
41
41
|
state/output/ # snapshots/screenshots
|
|
42
42
|
state/auth.json # private last completed storage-state checkpoint
|
|
@@ -78,16 +78,16 @@ read with a byte limit.
|
|
|
78
78
|
|
|
79
79
|
This is a tool boundary, **not an OS sandbox**. Unrestricted coding tools can
|
|
80
80
|
still access host files or invoke another browser process. Stronger containment
|
|
81
|
-
requires a separate decision. Never
|
|
82
|
-
|
|
81
|
+
requires a separate decision. Never bypass a CAPTCHA/2FA or perform an
|
|
82
|
+
unapproved irreversible action.
|
|
83
83
|
|
|
84
84
|
## Verification
|
|
85
85
|
|
|
86
86
|
- `test/browser.test.ts` uses a faux model and fake CLI boundary to check native
|
|
87
87
|
registration, forced arguments/environment, forbidden controls, literal flag-like
|
|
88
88
|
form text, owned stop/reset/restart, retention, cancellation and unsafe replay.
|
|
89
|
-
- `node --experimental-strip-types
|
|
90
|
-
CLI and
|
|
89
|
+
- `node --experimental-strip-types scripts/browser-smoke.mjs` uses the real installed
|
|
90
|
+
CLI and visible Chrome with temporary profiles and loopback pages. It checks
|
|
91
91
|
same-name sessions, daemon discovery, cookies/local storage, tabs/output files,
|
|
92
92
|
literal arguments, selective cleanup and preserved login checkpoints on reopen.
|
|
93
93
|
It never opens a personal profile, calls a paid model or sends an iMessage.
|
package/docs/install.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- macOS, Node >=22.22.2, Python 3 and authenticated Pi models.
|
|
6
|
+
- Terminal/Node Full Disk Access for Messages' SQLite database; Automation and
|
|
7
|
+
Accessibility permissions for Messages.app/rich-text sends; `/usr/bin/sips`.
|
|
8
|
+
- The existing memory CLI at `WORKING_DIR/skills/file-memory/memory_cli.py`.
|
|
9
|
+
- The installed `pi-browser` CLI and its pinned Playwright runtime. Run
|
|
10
|
+
`pi-browser setup` when needed and authorized; `pi-imessage preflight` never
|
|
11
|
+
installs it. Preflight
|
|
12
|
+
resolves the wrapper in Pi's agent `bin/`, or a preflight-only
|
|
13
|
+
`PI_BROWSER_CLI_PATH` override, and runs `--version` with temporary browser state
|
|
14
|
+
that is removed afterward. A wrapper whose `doctor` reports MISSING is not ready.
|
|
15
|
+
Version readiness does not prove browser privacy, login or per-chat isolation.
|
|
16
|
+
- Reviewed native SQLite and image dependencies. Install dependencies with
|
|
17
|
+
`npm ci --ignore-scripts`; if native binaries are missing, review and approve
|
|
18
|
+
their build/download separately rather than silently enabling install scripts.
|
|
19
|
+
|
|
20
|
+
Safe local checks (no live models/messages/workspace):
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm run check
|
|
24
|
+
npm test
|
|
25
|
+
node --experimental-strip-types src/cli.ts preflight
|
|
26
|
+
node --experimental-strip-types src/cli.ts --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
For changes to the built-in browser, also run
|
|
30
|
+
`node --experimental-strip-types scripts/browser-smoke.mjs`. This uses real visible
|
|
31
|
+
Chrome and the installed pinned CLI with temporary HOME/profiles, loopback pages
|
|
32
|
+
and synthetic login state; it never opens a personal profile or sends a message.
|
|
33
|
+
See [browser isolation and retention](browser.md).
|
|
34
|
+
|
|
35
|
+
`WORKING_DIR`, `WEB_HOST`, `WEB_PORT`, `WEB_ENABLED` and `MESSAGES_DB_PATH`
|
|
36
|
+
are explicit host configuration. `npm start`, the
|
|
37
|
+
executable CLI and the generated launchd job enable Node's environment proxy
|
|
38
|
+
support. `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` (and their lowercase forms) and
|
|
39
|
+
`NODE_OPTIONS` are retained when generating the job; review them before install.
|
|
40
|
+
CLI execution loads the current directory's `.env` using Node's parser; existing
|
|
41
|
+
environment values take precedence and a missing file is allowed. Set
|
|
42
|
+
`DOTENV_CONFIG_PATH` for another file. Module imports and `--help` do not load it.
|
|
43
|
+
Other read errors stop startup without printing file contents. `.env` is trusted
|
|
44
|
+
operator configuration, not a message attachment. Keep it private and out of Git.
|
|
45
|
+
Installation pins its current directory as the job's working directory, so the
|
|
46
|
+
same configuration path is used at launch. Selected environment values in the
|
|
47
|
+
plist are a snapshot; regenerate a reviewed job to change those overrides.
|
|
48
|
+
The job also pins the selected Pi agent directory as an absolute
|
|
49
|
+
`PI_CODING_AGENT_DIR` (including the default). Relative paths and `~` are resolved
|
|
50
|
+
at installation, after `.env` loading, so daemon HOME/cwd changes do not select a
|
|
51
|
+
different auth.json, models.json, settings.json or skill root. This selects a
|
|
52
|
+
configuration location; it does not copy credentials or grant access to it.
|
|
53
|
+
|
|
54
|
+
No proxy credentials are printed by installation. Application startup configures
|
|
55
|
+
one process-owned Undici transport after `.env` loading and before model/auth
|
|
56
|
+
refresh. Global Pi `httpProxy` supplies HTTP/HTTPS defaults; explicit environment
|
|
57
|
+
values win, lowercase proxy/bypass values take precedence, and `NO_PROXY` is
|
|
58
|
+
honored. Project-local `httpProxy` is ignored. `/reload` applies changed settings
|
|
59
|
+
for subsequent requests while old clients drain active requests. Shutdown stops
|
|
60
|
+
messaging and Harness work before releasing owned HTTP resources. Standalone
|
|
61
|
+
callers with injected runtimes retain transport control; deliberate fetch
|
|
62
|
+
overrides and dispatchers owned by other callers are not replaced on cleanup.
|
|
63
|
+
|
|
64
|
+
Pi settings in the agent directory and `WORKING_DIR/.pi/settings.json` supply
|
|
65
|
+
request policy; `/reload` refreshes it. `retry.provider.timeoutMs` overrides the
|
|
66
|
+
provider request timeout, otherwise `httpIdleTimeoutMs` supplies that default
|
|
67
|
+
(300000 ms; 0 uses Pi's effectively unlimited value). This forwards a request
|
|
68
|
+
option to supporting providers. Separately, application HTTP transport uses
|
|
69
|
+
`httpIdleTimeoutMs` for native header/body inactivity timers; 0 disables those
|
|
70
|
+
timers. An explicit provider request timeout can still limit a request when
|
|
71
|
+
socket-idle timers are disabled. Provider retry limits/delay caps and `retry`
|
|
72
|
+
generation policy are executed by Pi/Durable; transport remains SSE. These do
|
|
73
|
+
not replace the old host activity timer, prompt maximum-duration or
|
|
74
|
+
compaction-deadline behavior.
|
|
75
|
+
|
|
76
|
+
Web defaults to localhost:7750 and has no authentication. Never expose it to
|
|
77
|
+
untrusted networks.
|
|
78
|
+
|
|
79
|
+
Lifecycle logs use ISO timestamps on stdout/stderr: application startup (PID,
|
|
80
|
+
Node version and workspace), agent readiness/reload, the actual Web bind address
|
|
81
|
+
or disabled state, messaging readiness (cursor and poll/compaction intervals),
|
|
82
|
+
shutdown signal, resource cleanup and failures. Readiness is logged only after
|
|
83
|
+
the corresponding initialization succeeds; it is not a model or delivery health
|
|
84
|
+
check. Idle polls do not produce log lines. These lifecycle entries do not include
|
|
85
|
+
message text, tool arguments or proxy/auth configuration.
|
|
86
|
+
The generated launchd job redirects both streams to `WORKING_DIR/service.log`.
|
|
87
|
+
Foreground runs use the terminal or the controller's selected redirection; the
|
|
88
|
+
application does not open a second log file.
|
|
89
|
+
|
|
90
|
+
## npm publishing
|
|
91
|
+
|
|
92
|
+
The public package contains compiled JavaScript and Web assets, not a TypeScript
|
|
93
|
+
executable under `node_modules`. `npm pack` builds through `prepack`;
|
|
94
|
+
`npm run test:package` verifies the packed executable and inert installer in a
|
|
95
|
+
temporary HOME/workspace without models or Messages.
|
|
96
|
+
|
|
97
|
+
`.github/workflows/publish.yml` uses the existing npm trusted publisher with
|
|
98
|
+
GitHub OIDC, Node 24 and `id-token: write`; no local npm login or token is needed.
|
|
99
|
+
It runs the full checks, tests and package smoke before publishing with provenance.
|
|
100
|
+
Future releases push a matching `v<package-version>` tag. To publish an existing
|
|
101
|
+
version after fixing packaging, dispatch the workflow on main with that exact
|
|
102
|
+
version; never move an already published GitHub tag.
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
gh workflow run publish.yml --ref main -f version=1.0.0
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
An installed npm service runs `dist/cli.js`; source deployments continue to use
|
|
109
|
+
`src/cli.ts`. Package installation does not start or reload a service.
|