@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.
Files changed (84) hide show
  1. package/README.md +4 -6
  2. package/dist/agent/behavior.js +7 -13
  3. package/dist/agent/chats.js +7 -8
  4. package/dist/agent/commands.js +16 -11
  5. package/dist/agent/harness.js +6 -0
  6. package/dist/agent/progress.js +30 -4
  7. package/dist/agent/prompt.js +41 -24
  8. package/dist/agent/run.js +5 -4
  9. package/dist/agent/scheduling.js +11 -34
  10. package/dist/cli.js +42 -5
  11. package/dist/config/settings.js +4 -0
  12. package/dist/extensions/browser.js +7 -3
  13. package/dist/extensions/codemode.js +16 -0
  14. package/dist/extensions/final-text.js +12 -0
  15. package/dist/extensions/schedules.js +1 -1
  16. package/dist/extensions/subagent.js +3 -12
  17. package/dist/extensions/workspace.js +6 -2
  18. package/dist/main.js +42 -49
  19. package/dist/transport/watch.js +7 -1
  20. package/dist/web/app.js +63 -121
  21. package/dist/web/server.js +7 -14
  22. package/dist/web/style.css +0 -6
  23. package/dist/web/tasks.js +12 -120
  24. package/docs/adr/{0004-native-extensions.md → 0003-native-extensions.md} +1 -1
  25. package/docs/adr/{0006-jsonl-storage.md → 0004-jsonl-storage.md} +1 -1
  26. package/docs/adr/{0007-images-by-path.md → 0005-images-by-path.md} +1 -1
  27. package/docs/adr/{0010-test-budget.md → 0006-test-budget.md} +1 -1
  28. package/docs/adr/{0011-reconcile-unsent-answers.md → 0007-reconcile-unsent-answers.md} +2 -2
  29. package/docs/adr/{0012-run-in-owned-conversation.md → 0008-run-in-owned-conversation.md} +5 -5
  30. package/docs/adr/{0032-native-background-schedules.md → 0009-native-background-schedules.md} +1 -1
  31. package/docs/adr/0010-framework-and-workspace.md +27 -0
  32. package/docs/adr/README.md +24 -48
  33. package/docs/api.md +16 -8
  34. package/docs/architecture.md +19 -8
  35. package/docs/browser.md +7 -7
  36. package/docs/install.md +109 -0
  37. package/docs/workspace-extensions.md +8 -4
  38. package/package.json +4 -5
  39. package/dist/agent/compaction.js +0 -51
  40. package/dist/agent/error-log.js +0 -37
  41. package/dist/extensions/restart.js +0 -100
  42. package/docs/adr/0003-replies-at-most-once.md +0 -11
  43. package/docs/adr/0005-history-migration.md +0 -11
  44. package/docs/adr/0008-compaction-and-run.md +0 -11
  45. package/docs/adr/0009-one-shot-cutover.md +0 -11
  46. package/docs/adr/0013-thin-adapters-around-durable.md +0 -18
  47. package/docs/adr/0014-ephemeral-private-storage.md +0 -13
  48. package/docs/adr/0015-ordinary-stop-keeps-native-inbox.md +0 -21
  49. package/docs/adr/0016-process-http-transport.md +0 -21
  50. package/docs/adr/0017-browser-cli-owned-scope.md +0 -22
  51. package/docs/adr/0018-tasks-without-prompt-api.md +0 -19
  52. package/docs/adr/0019-remove-scheduled-services.md +0 -41
  53. package/docs/adr/0020-chat-task-progress.md +0 -41
  54. package/docs/adr/0021-readable-progress-without-link-previews.md +0 -33
  55. package/docs/adr/0022-progress-admission-and-settlement.md +0 -35
  56. package/docs/adr/0023-retain-progress-type-prefixes.md +0 -18
  57. package/docs/adr/0024-status-marker-before-type-prefix.md +0 -13
  58. package/docs/adr/0025-reusable-progress-and-command-preview.md +0 -49
  59. package/docs/adr/0026-inline-command-only-requests.md +0 -16
  60. package/docs/adr/0027-command-preview-width.md +0 -10
  61. package/docs/adr/0028-command-preview-120.md +0 -8
  62. package/docs/adr/0030-shared-web-read-cache.md +0 -33
  63. package/docs/adr/0031-quiet-compaction-progress.md +0 -32
  64. package/docs/adr/0033-operator-scheduled-run.md +0 -23
  65. package/docs/adr/0034-machine-local-schedule-time.md +0 -36
  66. package/docs/adr/0035-external-business-task-extensions.md +0 -56
  67. package/docs/adr/0036-workspace-policy-and-neutral-service-id.md +0 -37
  68. package/docs/adr/0037-restart-during-in-flight-work.md +0 -30
  69. package/docs/adr/0038-restart-any-time.md +0 -25
  70. package/docs/adr/0039-rerun-interrupted-tools.md +0 -24
  71. package/docs/adr/0040-chat-progress-notices.md +0 -44
  72. package/docs/adr/0041-restart-and-recovery.md +0 -28
  73. package/docs/adr/0042-background-schedules-and-workspace-extensions.md +0 -44
  74. package/docs/adr/0043-remove-legacy-migration.md +0 -23
  75. package/docs/adr/0044-message-channels.md +0 -32
  76. package/docs/adr/0045-chat-scheduling-tools.md +0 -51
  77. package/docs/adr/0046-selective-command-redaction.md +0 -31
  78. package/docs/adr/0047-one-scheduling-path.md +0 -158
  79. package/docs/adr/0048-quiet-attachment-reads.md +0 -11
  80. package/docs/adr/0049-per-chat-progress-notices.md +0 -21
  81. package/docs/adr/0052-host-owned-self-restart.md +0 -53
  82. package/docs/adr/0053-service-log-errors.md +0 -44
  83. package/ops/README.md +0 -250
  84. 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 generations shown per conversation, with every entry and task from the turn of the oldest one. */
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 and their parent chains. Per conversation the window is the newest generations plus every
41
- * entry and task back to the `pi.user` entry that opened the oldest one's turn. Entries and tasks share one
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 id = conversation.id;
57
- // One descending scan: the newest tasks down to the oldest shown generation; a task scan with a
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(page),
23
+ updatedAt: historyActivity(history),
121
24
  groupName: typeof groupName === "string" ? groupName : undefined,
122
25
  });
123
- for (const entry of history) {
124
- if (entry.id < floor)
125
- continue;
126
- entries.push({
127
- id: entry.id,
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, entries, more };
95
+ return { conversations, tasks, more };
204
96
  }, BACKGROUND_CONTEXT);
205
97
  }
@@ -1,4 +1,4 @@
1
- # 0004. Native Durable extensions, not an SDK adapter
1
+ # 0003. Native Durable extensions, not an SDK adapter
2
2
 
3
3
  Status: accepted (2026-10-04; owner accepted 2026-10-05)
4
4
 
@@ -1,4 +1,4 @@
1
- # 0006. One JSONL storage, backed up by hand
1
+ # 0004. One JSONL storage, backed up by hand
2
2
 
3
3
  Status: accepted (2026-10-04; owner accepted 2026-10-05)
4
4
 
@@ -1,4 +1,4 @@
1
- # 0007. Store images as file paths
1
+ # 0005. Store images as file paths
2
2
 
3
3
  Status: accepted (2026-10-04; owner accepted 2026-10-05)
4
4
 
@@ -1,4 +1,4 @@
1
- # 0010. About 40 tests, only for our code
1
+ # 0006. About 40 tests, only for our code
2
2
 
3
3
  Status: accepted (2026-10-04; owner accepted 2026-10-05)
4
4
 
@@ -1,8 +1,8 @@
1
- # 0011. Reconcile unsent answers after intervening input
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) and [at-most-once replies](0003-replies-at-most-once.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
- # 0012. `/run` works in a conversation owned by one task
1
+ # 0008. `/run` works in a conversation owned by one task
2
2
 
3
- Status: accepted (2026-10-04; owner accepted 2026-10-05). Replaces the `/run` part of [0008](0008-compaction-and-run.md); its compaction part stays.
3
+ Status: accepted (2026-10-04; owner accepted 2026-10-05).
4
4
 
5
- **Context.** In 0008, `/run` works 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.
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, under [0003](0003-replies-at-most-once.md) and [0011](0011-reconcile-unsent-answers.md).
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. Replies interrupted mid-send follow [0003](0003-replies-at-most-once.md).
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.
@@ -1,4 +1,4 @@
1
- # 0032. Native background schedules and read-only execution history
1
+ # 0009. Native background schedules and read-only execution history
2
2
 
3
3
  Status: proposed (owner requested native scheduling, compact migration and Web history)
4
4
 
@@ -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.
@@ -1,55 +1,31 @@
1
1
  # Architecture decision records
2
2
 
3
- Accepted ADR files are never edited; this index records which ones are current.
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
- ## Current
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
- | ADR | Decision | Status |
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
- | [0046](0046-selective-command-redaction.md) | Selective command-preview redaction | Proposed; owner-requested implementation in #36 |
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) |
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
- ## Replaced or rejected
26
+ ## Removed ADRs
40
27
 
41
- | ADR | Decision | Replaced by |
42
- |---|---|---|
43
- | [0005](0005-history-migration.md) | Import old history once | 0043 (done) |
44
- | [0009](0009-one-shot-cutover.md) | Switch over in one step | 0043 (done) |
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` choice with its effective value, the
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 (ADR 0019).
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 (ADR 0032), not the retained legacy cron files. The JSON view contains `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 (ADR 0034).
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
- ([ADR 0040](adr/0040-chat-progress-notices.md)). `/verbose on|off` in a chat
101
+ . `/verbose on|off` in a chat
103
102
  overrides this default for that chat only and `/verbose default` removes the
104
- override ([ADR 0049](adr/0049-per-chat-progress-notices.md)).
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`.
@@ -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) and ADR 0017.
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, recovery
66
- and quiet six-hour native compaction. Workspace tasks own business policy and
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. See
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 implements ADR 0017. It is
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, headed overrides, global cleanup, arbitrary
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 headless config
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 copy a personal Chrome profile, bypass a
82
- CAPTCHA/2FA or perform an unapproved irreversible action.
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 ops/browser-smoke.mjs` uses the real installed
90
- CLI and headless Chrome with temporary profiles and loopback pages. It checks
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.
@@ -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.