@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.
Files changed (80) 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 +38 -34
  19. package/dist/transport/watch.js +7 -1
  20. package/dist/web/app.js +10 -3
  21. package/dist/web/server.js +7 -3
  22. package/docs/adr/{0004-native-extensions.md → 0003-native-extensions.md} +1 -1
  23. package/docs/adr/{0006-jsonl-storage.md → 0004-jsonl-storage.md} +1 -1
  24. package/docs/adr/{0007-images-by-path.md → 0005-images-by-path.md} +1 -1
  25. package/docs/adr/{0010-test-budget.md → 0006-test-budget.md} +1 -1
  26. package/docs/adr/{0011-reconcile-unsent-answers.md → 0007-reconcile-unsent-answers.md} +2 -2
  27. package/docs/adr/{0012-run-in-owned-conversation.md → 0008-run-in-owned-conversation.md} +5 -5
  28. package/docs/adr/{0032-native-background-schedules.md → 0009-native-background-schedules.md} +1 -1
  29. package/docs/adr/0010-framework-and-workspace.md +27 -0
  30. package/docs/adr/README.md +24 -47
  31. package/docs/api.md +16 -8
  32. package/docs/architecture.md +19 -8
  33. package/docs/browser.md +7 -7
  34. package/docs/install.md +109 -0
  35. package/docs/workspace-extensions.md +8 -4
  36. package/package.json +4 -5
  37. package/dist/agent/compaction.js +0 -51
  38. package/dist/extensions/restart.js +0 -100
  39. package/docs/adr/0003-replies-at-most-once.md +0 -11
  40. package/docs/adr/0005-history-migration.md +0 -11
  41. package/docs/adr/0008-compaction-and-run.md +0 -11
  42. package/docs/adr/0009-one-shot-cutover.md +0 -11
  43. package/docs/adr/0013-thin-adapters-around-durable.md +0 -18
  44. package/docs/adr/0014-ephemeral-private-storage.md +0 -13
  45. package/docs/adr/0015-ordinary-stop-keeps-native-inbox.md +0 -21
  46. package/docs/adr/0016-process-http-transport.md +0 -21
  47. package/docs/adr/0017-browser-cli-owned-scope.md +0 -22
  48. package/docs/adr/0018-tasks-without-prompt-api.md +0 -19
  49. package/docs/adr/0019-remove-scheduled-services.md +0 -41
  50. package/docs/adr/0020-chat-task-progress.md +0 -41
  51. package/docs/adr/0021-readable-progress-without-link-previews.md +0 -33
  52. package/docs/adr/0022-progress-admission-and-settlement.md +0 -35
  53. package/docs/adr/0023-retain-progress-type-prefixes.md +0 -18
  54. package/docs/adr/0024-status-marker-before-type-prefix.md +0 -13
  55. package/docs/adr/0025-reusable-progress-and-command-preview.md +0 -49
  56. package/docs/adr/0026-inline-command-only-requests.md +0 -16
  57. package/docs/adr/0027-command-preview-width.md +0 -10
  58. package/docs/adr/0028-command-preview-120.md +0 -8
  59. package/docs/adr/0030-shared-web-read-cache.md +0 -33
  60. package/docs/adr/0031-quiet-compaction-progress.md +0 -32
  61. package/docs/adr/0033-operator-scheduled-run.md +0 -23
  62. package/docs/adr/0034-machine-local-schedule-time.md +0 -36
  63. package/docs/adr/0035-external-business-task-extensions.md +0 -56
  64. package/docs/adr/0036-workspace-policy-and-neutral-service-id.md +0 -37
  65. package/docs/adr/0037-restart-during-in-flight-work.md +0 -30
  66. package/docs/adr/0038-restart-any-time.md +0 -25
  67. package/docs/adr/0039-rerun-interrupted-tools.md +0 -24
  68. package/docs/adr/0040-chat-progress-notices.md +0 -44
  69. package/docs/adr/0041-restart-and-recovery.md +0 -28
  70. package/docs/adr/0042-background-schedules-and-workspace-extensions.md +0 -44
  71. package/docs/adr/0043-remove-legacy-migration.md +0 -23
  72. package/docs/adr/0044-message-channels.md +0 -32
  73. package/docs/adr/0045-chat-scheduling-tools.md +0 -51
  74. package/docs/adr/0046-selective-command-redaction.md +0 -31
  75. package/docs/adr/0047-one-scheduling-path.md +0 -158
  76. package/docs/adr/0048-quiet-attachment-reads.md +0 -11
  77. package/docs/adr/0049-per-chat-progress-notices.md +0 -21
  78. package/docs/adr/0052-host-owned-self-restart.md +0 -53
  79. package/ops/README.md +0 -250
  80. package/ops/preflight.mjs +0 -28
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.
@@ -6,9 +6,9 @@ Business code belongs in `WORKING_DIR/extensions/<name>/index.ts` with its own
6
6
  host code, not a sandbox or an SDK `ExtensionAPI` adapter. Back up the extension
7
7
  directories with their configuration. The
8
8
  [system-context](../examples/workspace-extensions/system-context/) example loads
9
- an optional workspace summary into the prompt (ADR 0036).
9
+ an optional workspace summary into the prompt.
10
10
 
11
- Tools without `replay` rerun after an interruption (ADR 0039). Declare
11
+ Tools without `replay` rerun after an interruption. Declare
12
12
  `replay: "unsafe"` for an effect that must not repeat.
13
13
 
14
14
  ## Factory and reload
@@ -49,8 +49,12 @@ module.exports = ({ config, defineExtension, defineTask }) => {
49
49
  ```
50
50
 
51
51
  The factory receives `workingDir`, parsed `config`, `Type`, `defineExtension`,
52
- `defineTool`, `defineDoc`, `defineTask`, `configure`, `section`, and the generic
53
- `ScheduledOutbox` document token. All primitives are the host's pinned versions.
52
+ `defineTool`, `defineDoc`, `defineTask`, `configure`, `section`, `hook` with
53
+ `GenerationTask` (generation hooks such as a per-request `beforeRequest` message
54
+ rewrite), the generic `ScheduledOutbox` document token, and `durable`, the whole
55
+ Durable SDK, for anything else a plain Durable extension uses (for example
56
+ `durable.LiveDoc` or `durable.ResetEntry`). All primitives are the host's pinned
57
+ versions.
54
58
  Return a native extension with optional `schedules`; tools and multiple tasks
55
59
  work without any schedules. Startup and `/reload` scan direct, non-hidden child
56
60
  directories only, never skills or attachments. `reload_extensions` lets the chat
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kingcrab/pi-imessage",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A minimal, self-managing iMessage bot powered by Pi Durable",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,9 +10,7 @@
10
10
  "files": [
11
11
  "dist",
12
12
  "docs",
13
- "examples",
14
- "ops/README.md",
15
- "ops/preflight.mjs"
13
+ "examples"
16
14
  ],
17
15
  "publishConfig": {
18
16
  "access": "public",
@@ -28,7 +26,7 @@
28
26
  "scripts": {
29
27
  "build": "tsc -p tsconfig.build.json && cp src/web/app.js src/web/style.css dist/web/",
30
28
  "prepack": "npm run build",
31
- "test:package": "npm run build && node ops/package-smoke.mjs",
29
+ "test:package": "npm run build && node scripts/package-smoke.mjs",
32
30
  "start": "node --use-env-proxy --experimental-strip-types src/cli.ts serve",
33
31
  "check": "tsc --noEmit && biome check .",
34
32
  "test": "vitest run"
@@ -36,6 +34,7 @@
36
34
  "dependencies": {
37
35
  "@earendil-works/chord": "1.4.0",
38
36
  "@earendil-works/pi-ai": "1.4.0",
37
+ "@earendil-works/pi-codemode": "1.4.0",
39
38
  "@earendil-works/pi-coding-agent": "1.4.0",
40
39
  "@earendil-works/pi-durable": "1.4.0",
41
40
  "better-sqlite3": "13.0.3",
@@ -1,51 +0,0 @@
1
- import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
2
- import { CompactionEntry, LiveDoc, ResetEntry } from "@earendil-works/pi-durable";
3
- import { Chats } from "./chats.js";
4
- // Native compact() uses reason "manual" for both a command and a maintenance tick.
5
- // Record exact timer task IDs, not conversation IDs, so concurrent manual work stays visible.
6
- const scheduled = new WeakMap();
7
- /** Resolve admission before classifying: the commit may be observed before compact() returns its ID. */
8
- export async function takeScheduledCompaction(harness, taskId) {
9
- const state = scheduled.get(harness);
10
- if (!state)
11
- return false;
12
- await Promise.allSettled([...state.admissions]);
13
- return state.tasks.delete(taskId);
14
- }
15
- /** Admit native compactions, not another summarization loop; Durable decides whether a cut is needed. */
16
- export async function compactChats(harness, admissionFailed) {
17
- const tasks = [];
18
- const chats = await harness.snapshot(Chats, BACKGROUND_CONTEXT);
19
- for (const chat of chats?.items ?? []) {
20
- try {
21
- if ((await harness.snapshot(LiveDoc, chat.conversationId, BACKGROUND_CONTEXT))?.compactions?.length)
22
- continue;
23
- const conversation = await harness.conversation(chat.conversationId, BACKGROUND_CONTEXT);
24
- const newest = (await conversation?.entries({}, 1, undefined, BACKGROUND_CONTEXT))?.items[0];
25
- if (!conversation || !newest || CompactionEntry.is(newest) || ResetEntry.is(newest))
26
- continue;
27
- let state = scheduled.get(harness);
28
- if (!state) {
29
- state = { admissions: new Set(), tasks: new Set() };
30
- scheduled.set(harness, state);
31
- }
32
- const quiet = state;
33
- const admission = conversation.compact(undefined, BACKGROUND_CONTEXT).then((taskId) => {
34
- quiet.tasks.add(taskId);
35
- return taskId;
36
- });
37
- quiet.admissions.add(admission);
38
- try {
39
- tasks.push(await admission);
40
- }
41
- finally {
42
- quiet.admissions.delete(admission);
43
- }
44
- }
45
- catch (error) {
46
- admissionFailed?.();
47
- console.warn("Scheduled compaction admission failed", chat.chatGuid, error);
48
- }
49
- }
50
- return tasks;
51
- }
@@ -1,100 +0,0 @@
1
- import { execFile } from "node:child_process";
2
- import { access, chmod, copyFile, mkdtemp, writeFile } from "node:fs/promises";
3
- import { dirname, join } from "node:path";
4
- import { promisify } from "node:util";
5
- import { Type } from "@earendil-works/pi-ai";
6
- import { defineExtension, defineTool } from "@earendil-works/pi-durable";
7
- const execFileAsync = promisify(execFile);
8
- /** Variables that describe the old shell session or that the controller sets itself. */
9
- const EXCLUDED_ENVIRONMENT = new Set(["_", "PWD", "OLDPWD", "SHLVL", "TERM_SESSION_ID"]);
10
- function shellQuote(value) {
11
- return `'${value.replaceAll("'", "'\\''")}'`;
12
- }
13
- async function processField(field) {
14
- const { stdout } = await execFileAsync("ps", ["-p", String(process.pid), "-o", `${field}=`]);
15
- // Exactly what the controller's `$(ps ...)` yields: trailing newlines stripped, nothing else.
16
- return stdout.replace(/\n+$/, "");
17
- }
18
- /** Writes the private controller directory of ops/README.md step 3 for the target release; starts nothing. */
19
- export async function prepareRestart(options, release) {
20
- if (!/^[0-9a-f]{40}$/.test(release))
21
- throw new Error("release must be the full commit hash of a prepared release directory");
22
- const target = join(dirname(options.currentRelease), release);
23
- for (const entry of ["src/cli.ts", "node_modules", "ops/restart.command"]) {
24
- await access(join(target, entry)).catch(() => {
25
- throw new Error(`Release directory is not prepared: ${join(target, entry)} is missing`);
26
- });
27
- }
28
- const [started, command] = await Promise.all([processField("lstart"), processField("command")]);
29
- const deployDir = await mkdtemp(join(options.deployRoot, "pi-imessage-restart."));
30
- // The whole process environment, including .env values loaded at startup; the controller deletes the file.
31
- const environment = Object.entries(process.env)
32
- .filter(([name, value]) => value !== undefined && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !EXCLUDED_ENVIRONMENT.has(name))
33
- .map(([name, value]) => `export ${name}=${shellQuote(value ?? "")}`);
34
- await writeFile(join(deployDir, "environment.sh"), `${environment.join("\n")}\n`, { mode: 0o600 });
35
- const control = {
36
- OLD_PID: String(process.pid),
37
- OLD_STARTED: started,
38
- EXPECTED_COMMAND: command,
39
- REPO: target,
40
- NODE: process.execPath,
41
- WORKING_DIR: options.workingDir,
42
- WEB_PORT: process.env.WEB_PORT ?? "7750",
43
- LOG_FILE: join(options.workingDir, "service.log"),
44
- };
45
- await writeFile(join(deployDir, "control.sh"), `${Object.entries(control)
46
- .map(([name, value]) => `${name}=${shellQuote(value)}`)
47
- .join("\n")}\n`, { mode: 0o600 });
48
- const controller = join(deployDir, "restart.command");
49
- await copyFile(join(target, "ops", "restart.command"), controller);
50
- await chmod(controller, 0o700);
51
- return { target, deployDir, controller };
52
- }
53
- /** The claw baseline: a Terminal window keeps the controller alive independently of this process. */
54
- export async function launchInTerminal(controller) {
55
- await execFileAsync("open", ["-a", "Terminal", controller], { timeout: 15_000 });
56
- }
57
- /** The chat assistant restarts the service through the host instead of a bash command that stops its own host. */
58
- export function restartExtension(options) {
59
- let scheduled;
60
- return defineExtension({
61
- name: "restart",
62
- tools: [
63
- defineTool({
64
- name: "restart_service",
65
- description: "Restart this service into a prepared release directory: a sibling of the running one named by its full commit hash, with npm ci and ops/preflight.mjs done. The service stops only after this call has settled; end the call at once and verify in a later call: controller.log and new.pid in the returned directory, then the service log, the listener and the running release directory. Never call it again before the first outcome is known.",
66
- parameters: Type.Object({
67
- release: Type.String({ description: "Full commit hash of the prepared release directory" }),
68
- }),
69
- // A rerun would launch a second controller (#39); an interrupted call is reported to the model instead.
70
- replay: "unsafe",
71
- async execute(args, api) {
72
- if (scheduled)
73
- throw new Error(`A restart is already scheduled from ${scheduled}`);
74
- const prepared = await prepareRestart(options, args.release);
75
- scheduled = prepared.deployDir;
76
- console.log(new Date().toISOString(), "Restart requested", {
77
- release: args.release,
78
- controller: prepared.deployDir,
79
- task: api.taskId,
80
- });
81
- void options
82
- .settled(api.taskId)
83
- .then(() => options.launch(prepared.controller))
84
- .catch((error) => {
85
- scheduled = undefined;
86
- console.error(new Date().toISOString(), "Restart launch failed", error);
87
- });
88
- return {
89
- output: [
90
- {
91
- type: "text",
92
- text: `Restart into ${prepared.target} is scheduled; the service stops after this call settles. Controller directory: ${prepared.deployDir}. Verify in a later call.`,
93
- },
94
- ],
95
- };
96
- },
97
- }),
98
- ],
99
- });
100
- }
@@ -1,11 +0,0 @@
1
- # 0003. Replies are sent at most once
2
-
3
- Status: accepted (2026-10-04; owner accepted 2026-10-05)
4
-
5
- **Context.** Sending through Messages.app and recording it in Durable cannot be one atomic step. A crash in between either loses or repeats a reply.
6
-
7
- **Decision.** For each final answer: record `sending` in the conversation, send, then record `sent`. On restart, `sending` becomes `unknown`: it is logged and shown in the web UI, and never resent automatically; to resend, use `/send`. This covers agent replies; `/send`, cron and reminders report their result to the caller or run log.
8
-
9
- **Consequences.** A crash in that window of a few milliseconds can drop one reply. Nothing is ever sent twice.
10
-
11
- **Rejected.** Send, then record (can send twice). Confirming delivery through `chat.db` GUIDs (complex and unreliable).
@@ -1,11 +0,0 @@
1
- # 0005. Import old history once; start with an empty context
2
-
3
- Status: accepted (2026-10-04; owner accepted 2026-10-05)
4
-
5
- **Context.** Old data per chat: `WORKING_DIR/<chat>/log.jsonl` (about 28,000 lines across 12 chats on claw), plus SDK sessions, goals, subagents and several GB of images.
6
-
7
- **Decision.** `migrate/migrate.ts` runs once at cutover. Each `log.jsonl` becomes user and assistant entries in that chat's conversation, followed by one reset, so the model starts with an empty context. Attachments move to `WORKING_DIR/attachments/<chat>/`, and entries keep the path. The agent searches old messages by grepping `WORKING_DIR/durable/`; the system prompt says where. SDK sessions, goals, subagents and scratch are archived once, then deleted.
8
-
9
- **Consequences.** The model doesn't remember pre-cutover talk until it searches. The web UI shows old and new history the same way.
10
-
11
- **Rejected.** Importing the SDK context (different format). A summary per chat at reset (one model call per chat). A separate archive folder.
@@ -1,11 +0,0 @@
1
- # 0008. Compact every 6 hours; `/run` replaces `/goal`
2
-
3
- Status: accepted (2026-10-04; owner accepted 2026-10-05)
4
-
5
- **Context.** The old bot reset every chat nightly, and used `pi-goal-x` for long tasks, which brought task trees, contracts and reviewers we don't need.
6
-
7
- **Decision.** Every 6 hours, each chat with new entries since its last compaction is compacted in the background; no chat notifications. Durable's automatic compaction near the context limit stays on. `/run <duration> [task]` keeps the run going through an `onYield` hook until the task is done, the user is needed, or the deadline passes. One run per conversation; `/run-stop`, `/stop` and `/new` end it; it does not survive a restart.
8
-
9
- **Consequences.** Context is never wiped on a schedule. `/run` spends tokens until it stops.
10
-
11
- **Rejected.** Nightly reset. Migrating `pi-goal-x`.
@@ -1,11 +0,0 @@
1
- # 0009. Switch over in one step
2
-
3
- Status: accepted (2026-10-04; owner accepted 2026-10-05)
4
-
5
- **Context.** Running old and new pipelines side by side (a per-chat canary) needs guards against processing a message twice. That was much of #31's complexity.
6
-
7
- **Decision.** Build in a new repository, `pi-imessage-next`. At cutover: stop the old service, run the migration (which also saves the current `chat.db` row as the watch cursor), start the new service, then push the new code to `daya0576/pi-imessage` as one commit. Rollback: stop the new service, restore the `WORKING_DIR` backup, start the old release.
8
-
9
- **Consequences.** Minutes of downtime; messages that arrive meanwhile are picked up from the cursor, and losing a few is acceptable. No code for running two pipelines.
10
-
11
- **Rejected.** Per-chat canary.
@@ -1,18 +0,0 @@
1
- # 0013. Use Durable directly; keep adapters thin
2
-
3
- Status: accepted (2026-10-04; owner accepted 2026-10-05)
4
-
5
- **Context.** The drafts grew host state that Durable already keeps (per-command receipts with running/unknown recovery, a reply scan over every settled submission) and copies of other systems (a gzip/base64 Python memory backend inside TypeScript, a vendored 1,000-line scheduler). [0001](0001-durable-owns-state.md) says Durable owns chat state; this records where our code stops.
6
-
7
- **Decision.**
8
-
9
- - **Commands** call Durable APIs directly (`configure`, `compact`, `reset`, task abort). Each command is first written to the chat conversation as a passive entry with the message GUID as request ID, so a repeated row is skipped by Durable's dedupe. An interrupted command is not replayed. No command receipt state.
10
- - **`/reload`** rereads settings, models and auth, AGENTS and skills, and reinstalls our extensions under the same names; Durable's registry replaces them in place. Running calls finish on their old code. Nothing is paused or cancelled.
11
- - **Memory tools** run the store's own CLI, `WORKING_DIR/skills/file-memory/memory_cli.py`, which finds its data next to itself. No embedded or rewritten backend; a missing CLI is a tool error.
12
- - **Scheduler** keeps the clock: cron expressions, timezones, reminders and their files (`cron/jobs.json`, reminders). When a job fires it submits to Durable with a stable request ID, or sends directly with a receipt. After acceptance, Durable owns execution and recovery; the scheduler never retries model work. Durable 1.0.2 has task `sleep` but no cron or reminder service, so it is not used as one.
13
- - **Web pages** are read-only: they show Harness state and watches (history, live state, task graph, usage) and have no buttons. They keep no execution log of their own. The endpoints in [the HTTP API](../api.md) stay; endpoints used only by the old page buttons (per-chat reply toggle, PT task actions) are removed. Replies are enabled through the settings allowlist; cron jobs are run or paused through the API.
14
- - **Our documents** hold only what Durable cannot know: the `chat.db` cursor, the chat-to-conversation map, send receipts and the active run with its deadline.
15
-
16
- **Consequences.** Little host recovery code beyond send receipts. Memory needs Python and the store's CLI at runtime (Python is already a prerequisite). The scheduler stays a larger, separate module, isolated behind one submit or send call.
17
-
18
- **Rejected.** Command receipts and a command state machine. Pausing work around `/reload`. Embedding or reimplementing the memory backend. Rewriting cron and reminders as Durable tasks. A separate execution log for the UI. Page buttons, including a resend button for `unknown` replies.
@@ -1,13 +0,0 @@
1
- # 0014. Keep ephemeral transcripts in disposable private storage
2
-
3
- Status: superseded by [0018](0018-tasks-without-prompt-api.md) (2026-10-05). Was: proposed (2026-10-04)
4
-
5
- **Context.** The retained HTTP `ephemeral:true` contract removes an isolated session after its queued work finishes. Durable 1.0.2 has reset/compaction but no public single-conversation deletion API. Reset hides old context from the model; it does not erase stored prompts, tool results or child conversations.
6
-
7
- **Example.** An automated task reads private files. Its final answer may stay in the destination chat, but its private task transcript must not remain searchable in the ordinary conversation store after cleanup.
8
-
9
- **Decision proposed for owner acceptance.** Give each active ephemeral `(chatGuid, sessionKey)` its own private JSONL storage and native Harness. Let Durable own that storage's inbox, dedupe, steering, tasks and recovery. The main store keeps only opaque identity/lifecycle metadata needed to locate it and external send receipts, not its private prompt/tool transcript. Admission success follows persisted private admission. On restart, reopen recorded private storage and resume its native work; never replay a whole turn in the host. Deliver completed answers with stable main-store receipts. Once the private Harness is idle and its completed answers have an authoritative sent/unknown disposition, close it and remove its directory with restart-safe cleanup. A later batch with the same key starts a fresh private context. Do not remove an active directory or silently lose queued work.
10
-
11
- **Consequences.** Extra storage/lifecycle wiring is necessary for the old privacy promise, but no second model loop or mirrored inbox. Repeated request IDs and uncertain sends need checks across cleanup/restart. HTTP ephemeral admission remains blocked until this design is accepted, implemented and verified. Ordinary API/cron contexts stay in the shared store.
12
-
13
- **Rejected.** Calling reset and claiming deletion. Editing shared append-only storage behind Durable. Retaining private transcripts in the main store. Dropping ephemeral behavior without owner approval.
@@ -1,21 +0,0 @@
1
- # 0015. Ordinary `/stop` keeps the native inbox
2
-
3
- Status: superseded by [0018](0018-tasks-without-prompt-api.md) (2026-10-05). Was: proposed (2026-10-04). Replaces only the no-active-run `/stop` clause of proposed [0012](0012-run-in-owned-conversation.md) if accepted. Does not approve its other behavior changes.
4
-
5
- **Problem.** Ordinary `/stop` must cancel current work and foreground children, suppress unclaimed old replies, and preserve later unprocessed ordinary messages. `Conversation.abort()` in Durable 1.0.2 cancels the conversation's foreground scope but withdraws queued inputs. Looking up `LiveDoc.run.taskId` and calling `abortTask()` preserves the parent inbox, but is not equivalent: generation hands over to a new conversation-owned task between turns. The old task can become terminal between lookup and abort; aborting it then does nothing to the new generation.
6
-
7
- **Example.** A request reads a file and continues into another model turn. A later ordinary message is queued. `/stop` must cancel the ongoing request, even at that turn boundary, without losing or resubmitting the later message.
8
-
9
- **Decision.**
10
-
11
- - Preserve this ordinary-chat behavior; do not silently replace it with “nothing is running” or queue withdrawal.
12
- - Require a supported native conversation-scope cancellation operation that atomically selects current foreground work while retaining the root chat's queued inputs. Cancellation of work owned below that scope must still follow Durable's ownership rules. The operation must define admission during cancellation and behavior across generation handover.
13
- - Use the native operation directly. After cancellation settles, suppress old unclaimed drafts using existing delivery records; never change `sending`, `sent` or `unknown` receipts or claim that a started send was undone.
14
- - If preserved inputs require a new boundary, submit one passive cancellation note through the public conversation API. Durable places existing inbox items; the host never copies their content, creates replacement input submissions, or replays the cancelled request.
15
- - Keep `/run` cancellation and isolated API/cron/background/health sessions separately scoped. This decision does not accept dropping queued run messages or cancelling unrelated sessions.
16
-
17
- **API boundary.** Durable 1.0.2 exposes no retain-inbox option on `Conversation.abort()`, and its public transaction API has no atomic task-abort operation. An upstream API change and dependency upgrade are required before this design can be implemented as specified. Do not patch installed package files or write the built-in live/inbox documents to simulate that operation.
18
-
19
- **Evidence.** The [inbox example](https://github.com/earendil-works/pi/blob/main/packages/durable/test/examples/20-inbox.ts), installed `harness/generation.js` (`createGeneration`, `handOver`), `harness/scheduler.js` (`abort`, `abortConversation`) and public `Tx` declarations establish the distinction. A bounded faux-provider experiment reproduces generation-ID handover and the stale-ID abort no-op; aborting the current generation preserves the queued input, and a passive write places it without a replacement submission. That experiment does not establish a race-free host cancellation implementation.
20
-
21
- **Rejected.** Repeated lookup/abort polling, a host cancellation epoch or mirrored inbox, withdrawing and resubmitting queued inputs, direct edits of Durable internals, and treating a cancelled wait as cancelled work. Also rejected: claiming a single generation-task abort cancels a whole multi-turn request.
@@ -1,21 +0,0 @@
1
- # 0016. Initialize one process-owned HTTP transport
2
-
3
- Status: accepted (2026-10-05; owner accepted 2026-10-05)
4
-
5
- **Problem.** Pi's global `httpProxy` and `httpIdleTimeoutMs` settings need HTTP transport setup, not just request-option forwarding. Coding-agent's experimental Durable startup calls `applyHttpProxySettings()` and `configureHttpDispatcher()`, but neither is a public export of coding-agent 1.0.2. Merely assigning `HTTP_PROXY` and `HTTPS_PROXY` after Node starts does not reconfigure the dispatcher created by `--use-env-proxy`.
6
-
7
- **Example.** An operator sets `httpProxy` in Pi settings without shell proxy variables. The service must use that proxy for authentication and model HTTP requests. Setting the variables at service startup can otherwise leave fetch connecting directly. A provider request timeout also does not replace a socket's inactivity timeout.
8
-
9
- **Decision.**
10
-
11
- - The host owns one process-wide HTTP setup, initialized before opening model/auth runtime. Individual chats and private Harness instances do not install global dispatchers.
12
- - Use public Undici APIs, with an exact direct dependency matching the supported SDK implementation, rather than importing private coding-agent files. Supply the global Pi proxy setting only as environment defaults; retain explicit environment proxy values, lowercase precedence and NO_PROXY behavior. Do not accept project-local proxy overrides where Pi accepts only global configuration.
13
- - Configure native HTTP headers/body idle timeouts from Pi settings, including disabled timeout semantics. Keep transport fetch and dispatcher on the same Undici implementation so compressed responses are decoded consistently. Preserve deliberately supplied fetch implementations. Follow the SDK's connection-family and client-error handling rather than inventing transport retries.
14
- - `/reload` may replace transport for subsequent requests without cancelling active work. Track transport resources in memory only, let active requests finish on their original clients, and retire old clients safely. During shutdown, stop producers and Harness work before releasing transport; restore globals only if they are still ours. Never close a dispatcher owned by another caller.
15
- - Durable/provider retry policy and request timeout options remain unchanged. This setup is not a host activity timer, prompt deadline, compaction deadline, inbox or execution loop.
16
-
17
- **Consequences.** Process-global HTTP setup belongs to application lifecycle, not per-conversation configuration. Standalone callers with an injected runtime/fetch keep control of their transport. The implementation must verify proxy precedence/bypass, compressed responses, idle timeout enable/disable, reload with active requests, cleanup and custom-fetch preservation against loopback fixtures before claiming compatibility.
18
-
19
- **Evidence.** The installed coding-agent `core/http-dispatcher.js` and experimental [harness setup](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/experimental/durable/harness-setup.ts) use Undici EnvHttpProxyAgent and matching fetch globals. A bounded Node 26 loopback experiment confirms late environment defaults leave built-in fetch direct, while public Undici 8.10.2 dispatcher plus matching fetch routes through the configured proxy. This establishes an implementation path, not completed lifecycle/timeout behavior or real-provider connectivity.
20
-
21
- **Rejected.** Environment mutation as proof of effective transport, request timeout as proof of socket-idle enforcement, private SDK imports, patching installed packages, a new HTTP retry algorithm, and relying on newer Node-only proxy-reset APIs while retaining Node >=22.22.2 support.
@@ -1,22 +0,0 @@
1
- # 0017. Invoke browser CLI in a native conversation-owned scope
2
-
3
- Status: accepted (2026-10-05; owner accepted 2026-10-05). Replaces only the browser-through-unrestricted-bash part of proposed [0004](0004-native-extensions.md) if accepted.
4
-
5
- **Problem.** `PI_BROWSER_HOME` scopes the pi-browser wrapper's profiles/output, not Playwright CLI's daemon registry. CLI 0.1.19 keeps that registry in the user's cache directory, derives daemon workspace identity from `.playwright` or the installed package, and enumerates other workspaces. All chats currently share the same shell environment and working directory. A named session or separate profile directory alone therefore does not preserve the old browser adapter's private-session rules.
6
-
7
- **Example.** Two chats both use `-s=task`. Even with different PI_BROWSER_HOME values, they can resolve the same daemon/session and see other registry entries. Giving the model a suggested name is not enforcement.
8
-
9
- **Decision.**
10
-
11
- - Offer a thin native Durable browser tool that invokes the installed, pinned pi-browser CLI by argv. Do not load an SDK browser extension, emulate ExtensionAPI, vendor CLI internals or implement another browser driver.
12
- - Derive browser scope from canonical storage identity plus native conversation identity, not a model-selected name. Ordinary chats, runs and isolated API/cron contexts get distinct scope; disallowed read-only contexts do not get the tool.
13
- - Give only browser child processes a dedicated HOME, profile/output root and scoped working directory. Leave ordinary coding tools' HOME/cwd unchanged. Use public process/CLI controls rather than PWTEST daemon/socket environment variables. Verify daemon discovery and attachment cannot escape the scope, including environment-inherited external browser targets.
14
- - Always select the owned session/profile and headless mode. Deny attachment/discovery, global close/kill, arbitrary profile/config paths and headed overrides, retaining the old service's restrictions. Expose the ordinary browsing operations needed by the product through validated arguments; operation results remain untrusted data.
15
- - Close owned browsers at the agreed stop/shutdown boundaries without touching unrelated sessions. Track only external browser resources and cleanup facts, not a second task state machine. Define restart cleanup and privacy retention before claiming isolation. Do not silently delete saved login profiles or replay uncertain browser mutations.
16
- - Do not mistake this tool boundary for an OS sandbox: unrestricted bash/read can still access host files and invoke other executables. State that limit explicitly; stronger containment requires a separate decision.
17
-
18
- **Consequences.** Native tool registration replaces browser instructions as the enforcement boundary for normal product use. The browser skill can still explain workflows, but cannot be the sole privacy mechanism. Actual Chrome/CLI fixtures must verify same-name sessions in different scopes, registry isolation, profile/config rejection and owned cleanup before release.
19
-
20
- **Evidence.** The installed wrapper source separates PI_BROWSER_HOME from Playwright's registry. In a temporary copy installed with the locked manifests (`npm ci --ignore-scripts`), CLI 0.1.19's `cli-client/registry.js` uses OS HOME/cache, scans every workspace and exposes only a PWTEST-specific daemon-root override. A bounded temporary-HOME experiment confirms different PI_BROWSER_HOME values share the daemon directory and registry enumeration sees another synthetic workspace. No browser or personal profile was opened. The experiment describes the pinned implementation; production must not import those private files.
21
-
22
- **Rejected.** Profile-directory changes or prompt-only session naming as proof of isolation, private PWTEST controls, changing HOME/cwd for all bash tools, SDK extension adaptation, unscoped global browser cleanup and claiming unrestricted coding tools are sandboxed.
@@ -1,19 +0,0 @@
1
- # 0018. Ordinary `/stop`; scheduled tasks without HTTP `/prompt` and `/send`
2
-
3
- Status: accepted (2026-10-05; owner accepted 2026-10-05). Supersedes proposed [0014](0014-ephemeral-private-storage.md) and [0015](0015-ordinary-stop-keeps-native-inbox.md). Replaces the HTTP `/prompt` clause of proposed [0012](0012-run-in-owned-conversation.md) and the `/send` resend hint of [0003](0003-replies-at-most-once.md).
4
-
5
- **Context.** Every production caller of HTTP `/prompt` and `/send` is a script or cron job on the same machine, plus the agent sending files through curl. The work they start runs in a separate context, but its result never reaches the chat's own conversation: when the user replies to a morning summary, the chat does not know what was sent. 0014 and 0015 wait for storage lifecycle work and an upstream Durable API.
6
-
7
- **Example.** At 08:00 a cron job runs a script that builds the newsletter prompt; the agent works in its own conversation and sends five links. The user replies "expand the third one". The chat conversation must already hold the five links, not the prompt or tool calls that produced them.
8
-
9
- **Decision.**
10
-
11
- - **Ordinary `/stop`** (no active `/run`) aborts the generation task that owns the current inputs (`LiveDoc.run`), not the conversation, so later queued messages stay in the inbox. If generation hands the same inputs to a new task meanwhile, abort that one too. Then write a passive `[/stop]` entry so Durable places the queued messages. `/stop` during a run is unchanged.
12
- - **Tasks.** Cron `prompt` jobs, `schedule_task` firings and background summaries each run in a fresh ownerless conversation, keyed by request ID. Nothing carries over between runs; continuity lives in the chat conversation and memory. Transcripts are kept, not deleted. The model health check reuses one conversation.
13
- - **The chat records what was sent.** A delivered task answer, and every service or tool send (reminders, cron `send`, background summaries, Automation, `send_message`), is written to the chat conversation as one passive entry with its delivery status, deduplicated by request ID. No extra model turn; the chat's next turn sees it.
14
- - **No HTTP `/prompt`, `/send` or `ephemeral`.** Cron `send` and `prompt` actions accept an optional `command` (absolute argv). Its stdout replaces the text or prompt; empty output skips the run. Scripts print instead of POSTing, and are scheduled in `cron/jobs.json`. The agent sends files and extra messages with the native `send_message` tool, using the existing direct-send receipts (tool task ID as request ID). Remaining HTTP: read-only pages, reminders, cron management and model health.
15
- - **Cutover.** The old service still needs `/prompt` and `/send`, so production scripts, crontab entries and `jobs.json` change in the same step as the cutover ([0009](0009-one-shot-cutover.md)).
16
-
17
- **Consequences.** One way to start agent work from outside a chat, and one place (the chat conversation) that knows what the user saw. `/stop` uses a bounded lookup/abort retry instead of an atomic native operation. Old `ephemeral` deletion is dropped; private tool output stays searchable under `durable/`. External callers on other machines can no longer prompt the agent; a future multi-entry design decides that. The shared scheduler's `jobs.json` parser gains the optional `command` field.
18
-
19
- **Rejected.** Handing task results to the chat as input (an extra model turn, interrupts current work, rewrites the sent text). Private per-session storage (0014). Waiting for an upstream cancellation API (0015). Keeping `/prompt` and `/send` as thin HTTP entries.
@@ -1,41 +0,0 @@
1
- # 0019. Remove scheduled services from the chat application
2
-
3
- Status: proposed
4
-
5
- **Problem.** The owner wants scheduling removed from this project and will design
6
- it separately later. Keeping empty job files or startup switches still leaves
7
- scheduled effects and their dependencies in the chat application's lifecycle.
8
-
9
- **Example.** Starting messaging must not create reminder databases, register cron
10
- jobs, run PT checks or watch completion files, even when old configuration exists.
11
-
12
- **Decision.**
13
-
14
- - Remove cron, reminders, delayed/repeating prompts, background completion
15
- watchers/summaries and PT Automation from source, tools, routes, pages, startup,
16
- preflight and direct dependencies. Do not replace them with disabled stubs.
17
- - Keep messaging, immediate `send_message`, foreground subagents, `/run`, ordinary
18
- `/stop`, native task inspection and explicit tool-less model health checks.
19
- Keep the six-hour context compaction required by ADR 0008; it is a chat
20
- maintenance timer, not user scheduling.
21
- - Keep existing settings, memory, history, receipts and offline backup/import
22
- data intact. Removal does not authorize deleting stored job data, modifying
23
- installed Pi extensions, or changing the operating system's crontab/launchd.
24
- This application does not manage or stop independent operating-system jobs.
25
- - Removed scheduling and Automation endpoints return 404. There is no
26
- compatibility adapter or automatic migration to another scheduler.
27
- - Retain historical isolated-conversation metadata for read-only inspection,
28
- but do not deliver answers from retired scheduled conversations.
29
-
30
- This proposal supersedes the scheduled-service clauses of ADR 0013 and ADR 0018
31
- only when accepted. Their ordinary stop, immediate sending and receipt decisions
32
- remain unchanged. Accepted ADR files are not edited. Scope and implementation
33
- progress are tracked in GitHub #33, not in this document.
34
-
35
- **Consequences.** A future scheduler needs its own design and approval. Startup
36
- still needs independent old producers to be reviewed, and model/Messages live
37
- acceptance remains separate from removal of scheduled services.
38
-
39
- **Rejected.** Merely clearing job definitions, hiding tools while retaining
40
- workers, reviving HTTP `/prompt` or `/send`, and deleting production state as
41
- part of a source-code change.