@kingcrab/pi-imessage 0.0.43 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -127
- package/dist/agent/behavior.js +79 -0
- package/dist/agent/chats.js +88 -0
- package/dist/agent/commands.js +191 -0
- package/dist/agent/compaction.js +51 -0
- package/dist/agent/deliver.js +139 -0
- package/dist/agent/direct-send.js +69 -0
- package/dist/agent/harness.js +79 -0
- package/dist/agent/health.js +33 -0
- package/dist/agent/models.js +69 -0
- package/dist/agent/progress-format.js +87 -0
- package/dist/agent/progress.js +173 -0
- package/dist/agent/prompt.js +79 -0
- package/dist/agent/replies.js +10 -0
- package/dist/agent/run.js +160 -0
- package/dist/agent/scheduling.js +291 -0
- package/dist/agent/time.js +18 -0
- package/dist/cli.js +88 -191
- package/dist/config/settings.js +60 -0
- package/dist/extensions/browser.js +326 -0
- package/dist/extensions/final-text.js +23 -0
- package/dist/extensions/memory.js +109 -0
- package/dist/extensions/message.js +33 -0
- package/dist/extensions/read-image.js +72 -0
- package/dist/extensions/restart.js +100 -0
- package/dist/extensions/schedules.js +42 -0
- package/dist/extensions/subagent.js +54 -0
- package/dist/extensions/web.js +100 -0
- package/dist/extensions/workspace.js +98 -0
- package/dist/main.js +578 -88
- package/dist/transport/attachments.js +37 -0
- package/dist/transport/http.js +110 -0
- package/dist/{rich-text.js → transport/rich-text.js} +25 -13
- package/dist/{send.js → transport/send.js} +13 -13
- package/dist/transport/watch.js +124 -0
- package/dist/web/app.js +826 -0
- package/dist/web/history.js +87 -0
- package/dist/web/memory.js +81 -0
- package/dist/web/page.js +29 -0
- package/dist/web/schedules.js +63 -0
- package/dist/web/server.js +382 -0
- package/dist/web/style.css +577 -0
- package/dist/web/tasks.js +97 -0
- package/docs/adr/0001-durable-owns-state.md +11 -0
- package/docs/adr/0002-module-layering.md +11 -0
- package/docs/adr/0003-replies-at-most-once.md +11 -0
- package/docs/adr/0004-native-extensions.md +11 -0
- package/docs/adr/0005-history-migration.md +11 -0
- package/docs/adr/0006-jsonl-storage.md +11 -0
- package/docs/adr/0007-images-by-path.md +11 -0
- package/docs/adr/0008-compaction-and-run.md +11 -0
- package/docs/adr/0009-one-shot-cutover.md +11 -0
- package/docs/adr/0010-test-budget.md +11 -0
- package/docs/adr/0011-reconcile-unsent-answers.md +24 -0
- package/docs/adr/0012-run-in-owned-conversation.md +22 -0
- package/docs/adr/0013-thin-adapters-around-durable.md +18 -0
- package/docs/adr/0014-ephemeral-private-storage.md +13 -0
- package/docs/adr/0015-ordinary-stop-keeps-native-inbox.md +21 -0
- package/docs/adr/0016-process-http-transport.md +21 -0
- package/docs/adr/0017-browser-cli-owned-scope.md +22 -0
- package/docs/adr/0018-tasks-without-prompt-api.md +19 -0
- package/docs/adr/0019-remove-scheduled-services.md +41 -0
- package/docs/adr/0020-chat-task-progress.md +41 -0
- package/docs/adr/0021-readable-progress-without-link-previews.md +33 -0
- package/docs/adr/0022-progress-admission-and-settlement.md +35 -0
- package/docs/adr/0023-retain-progress-type-prefixes.md +18 -0
- package/docs/adr/0024-status-marker-before-type-prefix.md +13 -0
- package/docs/adr/0025-reusable-progress-and-command-preview.md +49 -0
- package/docs/adr/0026-inline-command-only-requests.md +16 -0
- package/docs/adr/0027-command-preview-width.md +10 -0
- package/docs/adr/0028-command-preview-120.md +8 -0
- package/docs/adr/0030-shared-web-read-cache.md +33 -0
- package/docs/adr/0031-quiet-compaction-progress.md +32 -0
- package/docs/adr/0032-native-background-schedules.md +58 -0
- package/docs/adr/0033-operator-scheduled-run.md +23 -0
- package/docs/adr/0034-machine-local-schedule-time.md +36 -0
- package/docs/adr/0035-external-business-task-extensions.md +56 -0
- package/docs/adr/0036-workspace-policy-and-neutral-service-id.md +37 -0
- package/docs/adr/0037-restart-during-in-flight-work.md +30 -0
- package/docs/adr/0038-restart-any-time.md +25 -0
- package/docs/adr/0039-rerun-interrupted-tools.md +24 -0
- package/docs/adr/0040-chat-progress-notices.md +44 -0
- package/docs/adr/0041-restart-and-recovery.md +28 -0
- package/docs/adr/0042-background-schedules-and-workspace-extensions.md +44 -0
- package/docs/adr/0043-remove-legacy-migration.md +23 -0
- package/docs/adr/0044-message-channels.md +32 -0
- package/docs/adr/0045-chat-scheduling-tools.md +51 -0
- package/docs/adr/0046-selective-command-redaction.md +31 -0
- package/docs/adr/0047-one-scheduling-path.md +158 -0
- package/docs/adr/0048-quiet-attachment-reads.md +11 -0
- package/docs/adr/0049-per-chat-progress-notices.md +21 -0
- package/docs/adr/0052-host-owned-self-restart.md +53 -0
- package/docs/adr/README.md +54 -0
- package/docs/api.md +104 -0
- package/docs/architecture.md +164 -0
- package/docs/browser.md +93 -0
- package/docs/memory.md +27 -0
- package/docs/screenshot.png +0 -0
- package/docs/workspace-extensions.md +133 -0
- package/examples/workspace-extensions/system-context/index.ts +69 -0
- package/examples/workspace-extensions/system-context/package.json +3 -0
- package/ops/README.md +250 -0
- package/ops/preflight.mjs +28 -0
- package/package.json +36 -37
- package/dist/agent.d.ts +0 -84
- package/dist/agent.d.ts.map +0 -1
- package/dist/agent.js +0 -780
- package/dist/agent.js.map +0 -1
- package/dist/cli.d.ts +0 -15
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/imessage.d.ts +0 -33
- package/dist/imessage.d.ts.map +0 -1
- package/dist/imessage.js +0 -76
- package/dist/imessage.js.map +0 -1
- package/dist/logger.d.ts +0 -33
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -104
- package/dist/logger.js.map +0 -1
- package/dist/main.d.ts +0 -5
- package/dist/main.d.ts.map +0 -1
- package/dist/main.js.map +0 -1
- package/dist/memory.d.ts +0 -53
- package/dist/memory.d.ts.map +0 -1
- package/dist/memory.js +0 -196
- package/dist/memory.js.map +0 -1
- package/dist/model-health.d.ts +0 -11
- package/dist/model-health.d.ts.map +0 -1
- package/dist/model-health.js +0 -69
- package/dist/model-health.js.map +0 -1
- package/dist/pipeline.d.ts +0 -27
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/pipeline.js +0 -59
- package/dist/pipeline.js.map +0 -1
- package/dist/queue.d.ts +0 -23
- package/dist/queue.d.ts.map +0 -1
- package/dist/queue.js +0 -89
- package/dist/queue.js.map +0 -1
- package/dist/reminders.d.ts +0 -42
- package/dist/reminders.d.ts.map +0 -1
- package/dist/reminders.js +0 -207
- package/dist/reminders.js.map +0 -1
- package/dist/rich-text.d.ts +0 -26
- package/dist/rich-text.d.ts.map +0 -1
- package/dist/rich-text.js.map +0 -1
- package/dist/self-echo.d.ts +0 -19
- package/dist/self-echo.d.ts.map +0 -1
- package/dist/self-echo.js +0 -54
- package/dist/self-echo.js.map +0 -1
- package/dist/send.d.ts +0 -24
- package/dist/send.d.ts.map +0 -1
- package/dist/send.js.map +0 -1
- package/dist/settings.d.ts +0 -44
- package/dist/settings.d.ts.map +0 -1
- package/dist/settings.js +0 -80
- package/dist/settings.js.map +0 -1
- package/dist/store.d.ts +0 -57
- package/dist/store.d.ts.map +0 -1
- package/dist/store.js +0 -124
- package/dist/store.js.map +0 -1
- package/dist/tasks.d.ts +0 -95
- package/dist/tasks.d.ts.map +0 -1
- package/dist/tasks.js +0 -384
- package/dist/tasks.js.map +0 -1
- package/dist/types.d.ts +0 -93
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -38
- package/dist/types.js.map +0 -1
- package/dist/watch.d.ts +0 -21
- package/dist/watch.d.ts.map +0 -1
- package/dist/watch.js +0 -219
- package/dist/watch.js.map +0 -1
- package/dist/web/data.d.ts +0 -11
- package/dist/web/data.d.ts.map +0 -1
- package/dist/web/data.js +0 -56
- package/dist/web/data.js.map +0 -1
- package/dist/web/html.d.ts +0 -5
- package/dist/web/html.d.ts.map +0 -1
- package/dist/web/html.js +0 -16
- package/dist/web/html.js.map +0 -1
- package/dist/web/index.d.ts +0 -25
- package/dist/web/index.d.ts.map +0 -1
- package/dist/web/index.js +0 -335
- package/dist/web/index.js.map +0 -1
- package/dist/web/render.d.ts +0 -11
- package/dist/web/render.d.ts.map +0 -1
- package/dist/web/render.js +0 -56
- package/dist/web/render.js.map +0 -1
- package/dist/web/templates/logs.eta +0 -75
- package/dist/web/templates/memory.eta +0 -91
- package/dist/web/templates/page.eta +0 -98
package/docs/browser.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Conversation-owned browser
|
|
2
|
+
|
|
3
|
+
The host's built-in native Durable `browser` extension implements ADR 0017. It is
|
|
4
|
+
registered with the other built-in tools, not loaded as a personal workspace
|
|
5
|
+
business extension. It invokes the installed `PI_CODING_AGENT_DIR/bin/pi-browser`
|
|
6
|
+
wrapper and its pinned Playwright CLI by argv; there is no SDK shim, private CLI
|
|
7
|
+
import or second browser driver.
|
|
8
|
+
|
|
9
|
+
## Operations
|
|
10
|
+
|
|
11
|
+
`browser` takes `action` and only the arguments needed by that action:
|
|
12
|
+
|
|
13
|
+
- `open`, `goto`, `tab-new`: optional `url` (`goto` requires one), HTTP(S) without credentials.
|
|
14
|
+
- `snapshot`, `screenshot`: optional `ref` from the latest snapshot.
|
|
15
|
+
- `click`, `hover`, `check`, `uncheck`: required `ref`.
|
|
16
|
+
- `fill`, `select`: required `ref` and `text`.
|
|
17
|
+
- `press`: required `key`.
|
|
18
|
+
- `tab-select`: required `tab`; `tab-close` accepts an optional index.
|
|
19
|
+
- `go-back`, `go-forward`, `tab-list`, `close`: no further arguments.
|
|
20
|
+
|
|
21
|
+
Open before interacting. A snapshot includes bounded accessibility text with
|
|
22
|
+
current refs. Screenshot/other artifact links resolve from the reported private
|
|
23
|
+
cwd; the result also gives the absolute output directory. Page output is
|
|
24
|
+
untrusted. No attachment, discovery, headed overrides, global cleanup, arbitrary
|
|
25
|
+
profile/config/filename, storage export/import or arbitrary code execution is
|
|
26
|
+
exposed to the model. Positional text follows `--`, so strings resembling flags
|
|
27
|
+
remain literal. Native health checks and read-only subagents do not offer the tool.
|
|
28
|
+
|
|
29
|
+
## Isolation and retention
|
|
30
|
+
|
|
31
|
+
Scope is derived from the canonical Durable storage directory and native
|
|
32
|
+
conversation ID, never a model-selected session. Ordinary chats, run conversations
|
|
33
|
+
and other tool-enabled native conversations receive distinct scope directories:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
WORKING_DIR/browser/<sha256-of-canonical-storage>/<conversation-id>/
|
|
37
|
+
owner.json
|
|
38
|
+
home/ # private HOME/cache/CLI daemon registry
|
|
39
|
+
cwd/.playwright/ # private workspace identity and managed headless config
|
|
40
|
+
state/profiles/owned/ # dedicated non-personal persistent Chrome profile
|
|
41
|
+
state/output/ # snapshots/screenshots
|
|
42
|
+
state/auth.json # private last completed storage-state checkpoint
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Browser child processes alone get the private HOME/cwd and a small environment
|
|
46
|
+
allowlist. Browser target/config/test environment overrides are not inherited.
|
|
47
|
+
Ordinary bash/read keep their original environment. Scope directories are 0700;
|
|
48
|
+
authentication checkpoints are 0600, not returned to the model. Public scoped
|
|
49
|
+
`state-save`/`state-load` preserve cookie/local-storage login state across browser
|
|
50
|
+
restarts alongside the retained profile. Restore before navigating the supplied
|
|
51
|
+
URL, not by repeating the navigation. Before orderly close/reopen, a host-only
|
|
52
|
+
public scoped session query checks the owned profile and refreshes its checkpoint,
|
|
53
|
+
including asynchronous token changes since the last command. Checkpoints publish
|
|
54
|
+
by atomic rename, so an interrupted save cannot truncate the last completed state. On recovery a live
|
|
55
|
+
owned daemon is checkpointed before cleanup; if it already died, only the last
|
|
56
|
+
completed checkpoint is available and may require renewed login. Profiles,
|
|
57
|
+
checkpoints and outputs are not automatically deleted. Real-site login expiration
|
|
58
|
+
and IndexedDB-based workflows are not guaranteed by the synthetic fixture.
|
|
59
|
+
|
|
60
|
+
Operations serialize per conversation, while other conversations remain
|
|
61
|
+
independent. `/stop` and `/new` close the chat's owned browsers, including run/task
|
|
62
|
+
child scopes, after aborting current work and before admitting the next boundary.
|
|
63
|
+
Host shutdown stops native work, closes owned sessions, then releases the native
|
|
64
|
+
owner lock. Startup closes saved owned sessions after acquiring that lock, before
|
|
65
|
+
resuming work.
|
|
66
|
+
Only matching ownership markers and scoped `close` are used; malformed ownership
|
|
67
|
+
or a cleanup failure is an explicit error, never grounds for global killing or
|
|
68
|
+
silent profile deletion.
|
|
69
|
+
|
|
70
|
+
The tool declares `replay: "unsafe"` even though the host otherwise defaults tools
|
|
71
|
+
to safe. A click/press can already have submitted something when its caller is
|
|
72
|
+
interrupted. Cancellation/timeout does not prove the effect was undone; do not
|
|
73
|
+
blindly retry. Browser calls have request-bound deadlines and await CLI child exit
|
|
74
|
+
on cancellation. SIGTERM gets a short grace period, then only that exact owned CLI
|
|
75
|
+
child is terminated; this is never a global Chrome/service kill. Raw subprocess
|
|
76
|
+
argv/errors (which can contain form contents) are not surfaced, and snapshots are
|
|
77
|
+
read with a byte limit.
|
|
78
|
+
|
|
79
|
+
This is a tool boundary, **not an OS sandbox**. Unrestricted coding tools can
|
|
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.
|
|
83
|
+
|
|
84
|
+
## Verification
|
|
85
|
+
|
|
86
|
+
- `test/browser.test.ts` uses a faux model and fake CLI boundary to check native
|
|
87
|
+
registration, forced arguments/environment, forbidden controls, literal flag-like
|
|
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
|
|
91
|
+
same-name sessions, daemon discovery, cookies/local storage, tabs/output files,
|
|
92
|
+
literal arguments, selective cleanup and preserved login checkpoints on reopen.
|
|
93
|
+
It never opens a personal profile, calls a paid model or sends an iMessage.
|
package/docs/memory.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Structured Memory
|
|
2
|
+
|
|
3
|
+
[Back to README](../README.md)
|
|
4
|
+
|
|
5
|
+
Durable memories are stored as namespaced JSONL records under
|
|
6
|
+
`WORKING_DIR/skills/file-memory/namespaces/`. Each record includes a stable ID,
|
|
7
|
+
creation timestamp, factual event date when known, source, kind, subjects,
|
|
8
|
+
importance, confidence, and status. This keeps every memory traceable without
|
|
9
|
+
inventing dates for older facts.
|
|
10
|
+
|
|
11
|
+
The agent uses typed tools to load complete namespaces, search for specific
|
|
12
|
+
records, and append validated memories. Corrections append a new record that
|
|
13
|
+
supersedes the old ID, preserving an auditable history instead of silently
|
|
14
|
+
rewriting it. Legacy global and per-chat `MEMORY.md` files remain read-only
|
|
15
|
+
archives.
|
|
16
|
+
|
|
17
|
+
The service calls the store's existing `memory_cli.py` in that directory using
|
|
18
|
+
Python 3 and argument arrays, not a shell or an embedded backend. The CLI is a
|
|
19
|
+
runtime prerequisite; missing or failed calls return tool errors. Tool
|
|
20
|
+
cancellation terminates the subprocess, but an already completed write is not
|
|
21
|
+
undone. Saves are replay-safe because the store assigns deterministic IDs and
|
|
22
|
+
skips identical records; corrections pass the existing `supersedes` option.
|
|
23
|
+
|
|
24
|
+
Core memory (`core.md`) and the namespace list are loaded at startup and refreshed
|
|
25
|
+
by `/reload`. `load_memory` and `search_memory` always read the current store.
|
|
26
|
+
Explicit extension selection can replace the service's default memory tools for
|
|
27
|
+
isolated callers or restricted execution.
|
|
Binary file
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Workspace extensions
|
|
2
|
+
|
|
3
|
+
Memory, web tools, chat routing and delivery remain built-in capabilities.
|
|
4
|
+
Business code belongs in `WORKING_DIR/extensions/<name>/index.ts` with its own
|
|
5
|
+
`config.json`. Both the operator and chat assistant can create it. This is trusted
|
|
6
|
+
host code, not a sandbox or an SDK `ExtensionAPI` adapter. Back up the extension
|
|
7
|
+
directories with their configuration. The
|
|
8
|
+
[system-context](../examples/workspace-extensions/system-context/) example loads
|
|
9
|
+
an optional workspace summary into the prompt (ADR 0036).
|
|
10
|
+
|
|
11
|
+
Tools without `replay` rerun after an interruption (ADR 0039). Declare
|
|
12
|
+
`replay: "unsafe"` for an effect that must not repeat.
|
|
13
|
+
|
|
14
|
+
## Factory and reload
|
|
15
|
+
|
|
16
|
+
Use a CommonJS factory (`module.exports`) and CommonJS local dependencies
|
|
17
|
+
(`require`); put `{"type":"commonjs"}` in the extension's `package.json`.
|
|
18
|
+
Erasable TypeScript and type-only imports are supported. Runtime ESM modules
|
|
19
|
+
cannot be cache-evicted safely and are not part of this reload contract.
|
|
20
|
+
No dependencies are installed by the loader.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
module.exports = ({ config, defineExtension, defineTask }) => {
|
|
24
|
+
const Tick = defineTask({
|
|
25
|
+
name: "example.tick",
|
|
26
|
+
version: 1,
|
|
27
|
+
initial: () => ({ phase: "work" }),
|
|
28
|
+
phases: {
|
|
29
|
+
async work(task, runtime, context) {
|
|
30
|
+
await runtime.commit(() => ({
|
|
31
|
+
status: "terminal",
|
|
32
|
+
outcome: { status: "completed", result: {
|
|
33
|
+
summary: config.summary,
|
|
34
|
+
finishedAt: runtime.now()
|
|
35
|
+
} }
|
|
36
|
+
}), context);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
async abort(task, runtime, context) {
|
|
40
|
+
await runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), context);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
...defineExtension({ name: "example", tasks: [Tick] }),
|
|
45
|
+
schedules: [{ id: "example-tick", name: "Example tick", enabled: true,
|
|
46
|
+
intervalMs: 3600000, task: Tick.definition.name }]
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
```
|
|
50
|
+
|
|
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.
|
|
54
|
+
Return a native extension with optional `schedules`; tools and multiple tasks
|
|
55
|
+
work without any schedules. Startup and `/reload` scan direct, non-hidden child
|
|
56
|
+
directories only, never skills or attachments. `reload_extensions` lets the chat
|
|
57
|
+
assistant apply its changes through the existing service owner, without sending
|
|
58
|
+
an iMessage or opening a second Harness.
|
|
59
|
+
|
|
60
|
+
Extension names, task names and schedule IDs must be unique. Schedule IDs are
|
|
61
|
+
stable state keys, not display names. Failed validation or initialization keeps
|
|
62
|
+
old definitions and schedule configuration. Reload refreshes CommonJS entry and
|
|
63
|
+
local dependency code plus configuration. Running invocations keep their code;
|
|
64
|
+
subsequent invocations use the replacement. Increment task versions and supply
|
|
65
|
+
native `migrate` handlers for changed saved inputs/checkpoints.
|
|
66
|
+
|
|
67
|
+
## Scheduling and delivery
|
|
68
|
+
|
|
69
|
+
A schedule requires `id`, `name`, `enabled`, and a `task` name belonging to its
|
|
70
|
+
extension. Choose exactly one cadence: `time: "HH:MM"` or positive `intervalMs`.
|
|
71
|
+
Optional fields: `kind` (display metadata), `chatGuid` and JSON `input`.
|
|
72
|
+
An optional `initialize(tx, conversationId)` imports business state within the
|
|
73
|
+
host's atomic schedule commit. It must be replay-safe and must not send messages
|
|
74
|
+
or perform external writes; use native tasks for external actions.
|
|
75
|
+
|
|
76
|
+
The framework creates a dedicated conversation and background scheduler for each
|
|
77
|
+
schedule. Each occurrence delegates to the business task with:
|
|
78
|
+
|
|
79
|
+
- `jobId`, `date` (machine-local), `dueAt`, `startedAt`;
|
|
80
|
+
- `chatGuid` or null, and `config` containing the schedule's JSON `input` or null.
|
|
81
|
+
|
|
82
|
+
Successful business tasks return `{ summary, finishedAt, requestId? }`.
|
|
83
|
+
The wrapper's native receipt supplies read-only `/scheduled` history.
|
|
84
|
+
Daily schedules process only today's slot, without missed-date backfill;
|
|
85
|
+
interval schedules skip missed slots. Automatic and manual occurrences do not
|
|
86
|
+
overlap. Reload never resets a saved absolute deadline: new cadence applies
|
|
87
|
+
when computing the following slot. An explicitly aborted scheduler is not
|
|
88
|
+
silently recreated.
|
|
89
|
+
|
|
90
|
+
Commit business changes and `ScheduledOutbox.items` together, using a stable
|
|
91
|
+
`requestId`. The host reuses its direct-send receipts and passive chat records.
|
|
92
|
+
The destination must be reply-enabled. `sending`/`unknown` effects are never
|
|
93
|
+
replayed automatically; a queued item is not proof of delivery. Keep credentials
|
|
94
|
+
out of `settings.json`; the schedule view does not show extension configuration.
|
|
95
|
+
|
|
96
|
+
## One-off runs through the same scheduler
|
|
97
|
+
|
|
98
|
+
For an existing factory that does not declare `runRequests`, the loader takes
|
|
99
|
+
that field directly from `config.json`; business code need not change. A factory
|
|
100
|
+
can instead explicitly supply its own `runRequests` declarations alongside its
|
|
101
|
+
`schedules`. For example, the extension's `config.json` can include:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"runRequests": [
|
|
106
|
+
{ "id": "example-trial-1", "scheduleId": "example-tick", "at": "now" }
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Apply the configuration with `reload_extensions` (or the existing `/reload`).
|
|
112
|
+
The target must be an enabled schedule belonging to that extension. `at` is
|
|
113
|
+
`"now"` or an absolute epoch-millisecond timestamp. There is no configuration
|
|
114
|
+
watcher or separate CLI/HTTP execution entry point.
|
|
115
|
+
|
|
116
|
+
The owner persists a background `imessage.schedule` task for the request, then
|
|
117
|
+
uses the same occurrence wrapper and business conversation as recurring runs.
|
|
118
|
+
A busy target delays the one-off until its active occurrence settles. The request
|
|
119
|
+
ID and resolved deadline survive reload/restart, including after completion.
|
|
120
|
+
Reuse the same declaration to recover it; use a new ID for a new intended run.
|
|
121
|
+
Conflicting reuse fails rather than changing an existing request.
|
|
122
|
+
|
|
123
|
+
Removing a request prevents future admission, not an already admitted business
|
|
124
|
+
task. Re-adding a consumed ID never runs it again. Business per-date dedupe and
|
|
125
|
+
delivery receipts still apply; a trial does not force another English card.
|
|
126
|
+
Regular task IDs and deadlines do not move. Historical operator runs remain
|
|
127
|
+
stored, but startup `--run-scheduled` flags are no longer accepted.
|
|
128
|
+
|
|
129
|
+
Removing an extension disables future schedule admission and removes its tools
|
|
130
|
+
and prompt sections, not current calls or historical records. Removed task code
|
|
131
|
+
is conservatively retained in memory until restart, because an old running call
|
|
132
|
+
can still admit children. Restart fails explicitly if unfinished work needs
|
|
133
|
+
missing task code or a missing schedule extension.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type * as FileSystem from "node:fs/promises";
|
|
2
|
+
import type * as Paths from "node:path";
|
|
3
|
+
import type { defineExtension as DefineExtension, section as Section } from "@earendil-works/pi-durable";
|
|
4
|
+
|
|
5
|
+
const { constants, open } = require("node:fs/promises") as typeof FileSystem;
|
|
6
|
+
const { resolve } = require("node:path") as typeof Paths;
|
|
7
|
+
/** Per-file byte budget for System summary; dated history is never loaded here. */
|
|
8
|
+
|
|
9
|
+
async function readSummary(path: string, maxBytes: number, boundary: string) {
|
|
10
|
+
let file: Awaited<ReturnType<typeof open>> | undefined;
|
|
11
|
+
try {
|
|
12
|
+
file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
13
|
+
if (!(await file.stat()).isFile())
|
|
14
|
+
return "[System summary unavailable: expected a regular summary file.]";
|
|
15
|
+
const buffer = Buffer.alloc(maxBytes + 1);
|
|
16
|
+
let bytesRead = 0;
|
|
17
|
+
while (bytesRead < buffer.length) {
|
|
18
|
+
const result = await file.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
|
|
19
|
+
if (!result.bytesRead) break;
|
|
20
|
+
bytesRead += result.bytesRead;
|
|
21
|
+
}
|
|
22
|
+
const truncated = bytesRead > maxBytes;
|
|
23
|
+
// A streaming decode drops only an incomplete trailing UTF-8 character at the byte boundary.
|
|
24
|
+
const text = new TextDecoder("utf-8", { fatal: true })
|
|
25
|
+
.decode(buffer.subarray(0, Math.min(bytesRead, maxBytes)), { stream: truncated })
|
|
26
|
+
.trim();
|
|
27
|
+
const end = boundary ? text.indexOf(boundary) : -1;
|
|
28
|
+
if (end >= 0) return text.slice(0, end).trim();
|
|
29
|
+
return truncated
|
|
30
|
+
? `${text}\n\n[System summary exceeded the ${maxBytes}-byte summary limit; the rest was not loaded. Read the full file on demand.]`
|
|
31
|
+
: text;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return "";
|
|
34
|
+
return "[System summary summary unavailable.]";
|
|
35
|
+
} finally {
|
|
36
|
+
await file?.close();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = async ({
|
|
41
|
+
workingDir,
|
|
42
|
+
config,
|
|
43
|
+
defineExtension,
|
|
44
|
+
section,
|
|
45
|
+
}: {
|
|
46
|
+
workingDir: string;
|
|
47
|
+
config: Record<string, unknown>;
|
|
48
|
+
defineExtension: typeof DefineExtension;
|
|
49
|
+
section: typeof Section;
|
|
50
|
+
}) => {
|
|
51
|
+
if (
|
|
52
|
+
typeof config.summaryFile !== "string" ||
|
|
53
|
+
!config.summaryFile ||
|
|
54
|
+
!Number.isSafeInteger(config.maxBytes) ||
|
|
55
|
+
(config.maxBytes as number) <= 0 ||
|
|
56
|
+
(config.maxBytes as number) > 65536 ||
|
|
57
|
+
typeof config.boundary !== "string"
|
|
58
|
+
)
|
|
59
|
+
throw new Error("System context requires summaryFile, maxBytes (1..65536) and boundary");
|
|
60
|
+
const summary = await readSummary(
|
|
61
|
+
resolve(workingDir, config.summaryFile),
|
|
62
|
+
config.maxBytes as number,
|
|
63
|
+
config.boundary,
|
|
64
|
+
);
|
|
65
|
+
return defineExtension({
|
|
66
|
+
name: "system-context",
|
|
67
|
+
sections: [section("system_configuration", () => summary || undefined)],
|
|
68
|
+
});
|
|
69
|
+
};
|
package/ops/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Operations: installation, deployment and restart
|
|
2
|
+
|
|
3
|
+
The owner has authorized deployment after each completed change, including
|
|
4
|
+
verification, plus committing and pushing each completed intended project change
|
|
5
|
+
after the required checks. Do not ask again for routine deployment or commit/push
|
|
6
|
+
approval in that scope. Stage explicit intended paths; exclude unrelated drafts,
|
|
7
|
+
temporary files and credentials. Verify the pushed remote revision; stop on a
|
|
8
|
+
rejected push, never force push. This does not authorize paid probes, test messages,
|
|
9
|
+
data cleanup or unrelated service changes. Stop for a new material safety or
|
|
10
|
+
data-integrity risk.
|
|
11
|
+
Never run old/new pipelines concurrently or use production data in tests.
|
|
12
|
+
Implementation readiness and open decisions are tracked in GitHub #33.
|
|
13
|
+
|
|
14
|
+
For ordinary updates, use [Routine deployment and restart](#routine-deployment-and-restart).
|
|
15
|
+
The one-time legacy migration is archived at tag `archive/legacy-migration` (ADR 0043).
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- macOS, Node >=22.22.2, Python 3 and authenticated Pi models.
|
|
20
|
+
- Terminal/Node Full Disk Access for Messages' SQLite database; Automation and
|
|
21
|
+
Accessibility permissions for Messages.app/rich-text sends; `/usr/bin/sips`.
|
|
22
|
+
- The existing memory CLI at `WORKING_DIR/skills/file-memory/memory_cli.py`.
|
|
23
|
+
- The installed `pi-browser` CLI and its pinned Playwright runtime. Run
|
|
24
|
+
`pi-browser setup` when needed and authorized; preflight never installs it. Preflight
|
|
25
|
+
resolves the wrapper in Pi's agent `bin/`, or a preflight-only
|
|
26
|
+
`PI_BROWSER_CLI_PATH` override, and runs `--version` with temporary browser state
|
|
27
|
+
that is removed afterward. A wrapper whose `doctor` reports MISSING is not ready.
|
|
28
|
+
Version readiness does not prove browser privacy, login or per-chat isolation.
|
|
29
|
+
- Reviewed native SQLite and image dependencies. Install dependencies with
|
|
30
|
+
`npm ci --ignore-scripts`; if native binaries are missing, review and approve
|
|
31
|
+
their build/download separately rather than silently enabling install scripts.
|
|
32
|
+
|
|
33
|
+
Safe local checks (no live models/messages/workspace):
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm run check
|
|
37
|
+
npm test
|
|
38
|
+
node --experimental-strip-types ops/preflight.mjs
|
|
39
|
+
node --experimental-strip-types src/cli.ts --help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For changes to the built-in browser, also run
|
|
43
|
+
`node --experimental-strip-types ops/browser-smoke.mjs`. This uses real headless
|
|
44
|
+
Chrome and the installed pinned CLI with temporary HOME/profiles, loopback pages
|
|
45
|
+
and synthetic login state; it never opens a personal profile or sends a message.
|
|
46
|
+
See [browser isolation and retention](../docs/browser.md).
|
|
47
|
+
|
|
48
|
+
`WORKING_DIR`, `WEB_HOST`, `WEB_PORT`, `WEB_ENABLED` and `MESSAGES_DB_PATH`
|
|
49
|
+
are explicit host configuration. `npm start`, the
|
|
50
|
+
executable CLI and the generated launchd job enable Node's environment proxy
|
|
51
|
+
support. `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY` (and their lowercase forms) and
|
|
52
|
+
`NODE_OPTIONS` are retained when generating the job; review them before install.
|
|
53
|
+
CLI execution loads the current directory's `.env` using Node's parser; existing
|
|
54
|
+
environment values take precedence and a missing file is allowed. Set
|
|
55
|
+
`DOTENV_CONFIG_PATH` for another file. Module imports and `--help` do not load it.
|
|
56
|
+
Other read errors stop startup without printing file contents. `.env` is trusted
|
|
57
|
+
operator configuration, not a message attachment. Keep it private and out of Git.
|
|
58
|
+
Installation pins its current directory as the job's working directory, so the
|
|
59
|
+
same configuration path is used at launch. Selected environment values in the
|
|
60
|
+
plist are a snapshot; regenerate a reviewed job to change those overrides.
|
|
61
|
+
The job also pins the selected Pi agent directory as an absolute
|
|
62
|
+
`PI_CODING_AGENT_DIR` (including the default). Relative paths and `~` are resolved
|
|
63
|
+
at installation, after `.env` loading, so daemon HOME/cwd changes do not select a
|
|
64
|
+
different auth.json, models.json, settings.json or skill root. This selects a
|
|
65
|
+
configuration location; it does not copy credentials or grant access to it.
|
|
66
|
+
|
|
67
|
+
No proxy credentials are printed by installation. Application startup configures
|
|
68
|
+
one process-owned Undici transport after `.env` loading and before model/auth
|
|
69
|
+
refresh. Global Pi `httpProxy` supplies HTTP/HTTPS defaults; explicit environment
|
|
70
|
+
values win, lowercase proxy/bypass values take precedence, and `NO_PROXY` is
|
|
71
|
+
honored. Project-local `httpProxy` is ignored. `/reload` applies changed settings
|
|
72
|
+
for subsequent requests while old clients drain active requests. Shutdown stops
|
|
73
|
+
messaging and Harness work before releasing owned HTTP resources. Standalone
|
|
74
|
+
callers with injected runtimes retain transport control; deliberate fetch
|
|
75
|
+
overrides and dispatchers owned by other callers are not replaced on cleanup.
|
|
76
|
+
|
|
77
|
+
Pi settings in the agent directory and `WORKING_DIR/.pi/settings.json` supply
|
|
78
|
+
request policy; `/reload` refreshes it. `retry.provider.timeoutMs` overrides the
|
|
79
|
+
provider request timeout, otherwise `httpIdleTimeoutMs` supplies that default
|
|
80
|
+
(300000 ms; 0 uses Pi's effectively unlimited value). This forwards a request
|
|
81
|
+
option to supporting providers. Separately, application HTTP transport uses
|
|
82
|
+
`httpIdleTimeoutMs` for native header/body inactivity timers; 0 disables those
|
|
83
|
+
timers. An explicit provider request timeout can still limit a request when
|
|
84
|
+
socket-idle timers are disabled. Provider retry limits/delay caps and `retry`
|
|
85
|
+
generation policy are executed by Pi/Durable; transport remains SSE. These do
|
|
86
|
+
not replace the old host activity timer, prompt maximum-duration or
|
|
87
|
+
compaction-deadline behavior.
|
|
88
|
+
|
|
89
|
+
Web defaults to localhost:7750 and has no authentication. Never expose it to
|
|
90
|
+
untrusted networks.
|
|
91
|
+
|
|
92
|
+
Lifecycle logs use ISO timestamps on stdout/stderr: application startup (PID,
|
|
93
|
+
Node version and workspace), agent readiness/reload, the actual Web bind address
|
|
94
|
+
or disabled state, messaging readiness (cursor and poll/compaction intervals),
|
|
95
|
+
shutdown signal, resource cleanup and failures. Readiness is logged only after
|
|
96
|
+
the corresponding initialization succeeds; it is not a model or delivery health
|
|
97
|
+
check. Idle polls do not produce log lines. These lifecycle entries do not include
|
|
98
|
+
message text, tool arguments or proxy/auth configuration.
|
|
99
|
+
The generated launchd job redirects both streams to `WORKING_DIR/service.log`.
|
|
100
|
+
Foreground runs use the terminal or the controller's selected redirection; the
|
|
101
|
+
application does not open a second log file.
|
|
102
|
+
|
|
103
|
+
## npm publishing
|
|
104
|
+
|
|
105
|
+
The public package contains compiled JavaScript and Web assets, not a TypeScript
|
|
106
|
+
executable under `node_modules`. `npm pack` builds through `prepack`;
|
|
107
|
+
`npm run test:package` verifies the packed executable and inert installer in a
|
|
108
|
+
temporary HOME/workspace without models or Messages.
|
|
109
|
+
|
|
110
|
+
`.github/workflows/publish.yml` uses the existing npm trusted publisher with
|
|
111
|
+
GitHub OIDC, Node 24 and `id-token: write`; no local npm login or token is needed.
|
|
112
|
+
It runs the full checks, tests and package smoke before publishing with provenance.
|
|
113
|
+
Future releases push a matching `v<package-version>` tag. To publish an existing
|
|
114
|
+
version after fixing packaging, dispatch the workflow on main with that exact
|
|
115
|
+
version; never move an already published GitHub tag.
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
gh workflow run publish.yml --ref main -f version=1.0.0
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
An installed npm service runs `dist/cli.js`; source deployments continue to use
|
|
122
|
+
`src/cli.ts`. Package installation does not start or reload a service.
|
|
123
|
+
|
|
124
|
+
## Service identity
|
|
125
|
+
|
|
126
|
+
New `install` output uses `org.pi-imessage.service` and
|
|
127
|
+
`org.pi-imessage.service.plist`. For an installed
|
|
128
|
+
`me.changchen.pi-imessage-durable` job, privately preserve its plist/environment,
|
|
129
|
+
gracefully stop and unload that exact controller, verify PID/lock/port release,
|
|
130
|
+
then write and load the reviewed neutral job. Do not leave the old KeepAlive
|
|
131
|
+
controller loaded or start a Terminal copy alongside launchd. A foreground
|
|
132
|
+
Terminal service with no old-label job needs no controller change. Never rename
|
|
133
|
+
unrelated legacy jobs as part of this migration.
|
|
134
|
+
|
|
135
|
+
## Routine deployment and restart
|
|
136
|
+
|
|
137
|
+
### Choose the smallest operation
|
|
138
|
+
|
|
139
|
+
| Change | Action |
|
|
140
|
+
|---|---|
|
|
141
|
+
| Server, agent, transport or imported TypeScript | Restart from a new release directory; verify the change. |
|
|
142
|
+
| Web `app.js` / `style.css` only | Verify the served assets; no restart (the file cache checks metadata). |
|
|
143
|
+
| Documentation only | Commit and push; no reload or restart. |
|
|
144
|
+
| Configuration supported by `/reload` | Reload; restart only for startup-only configuration. Never send an iMessage to trigger it. |
|
|
145
|
+
|
|
146
|
+
Deploying means the change is live and verified, then committed and pushed.
|
|
147
|
+
|
|
148
|
+
### Release directories
|
|
149
|
+
|
|
150
|
+
The service runs a fixed commit, never the main checkout:
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
SHA=$(git -C /Users/clawbot/pi-imessage-next rev-parse origin/main)
|
|
154
|
+
RELEASE=/Users/clawbot/pi-imessage-releases/$SHA
|
|
155
|
+
git -C /Users/clawbot/pi-imessage-next worktree add --detach "$RELEASE" "$SHA"
|
|
156
|
+
(cd "$RELEASE" && npm ci --ignore-scripts && node --experimental-strip-types ops/preflight.mjs)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Never edit a release directory; rollback is a routine restart from the previous
|
|
160
|
+
one. Keep the latest three; remove older ones with `git worktree remove` (no
|
|
161
|
+
`--force`) when no process or plist uses them. A release directory has no `.env`:
|
|
162
|
+
set `DOTENV_CONFIG_PATH` to the main checkout's `.env`.
|
|
163
|
+
|
|
164
|
+
### 1. Identify the running service
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
launchctl list | grep -Ei 'pi-imessage|pi-web' || true
|
|
168
|
+
lsof -nP -iTCP:7750 -sTCP:LISTEN # take PID from here, never from notes
|
|
169
|
+
ps -p "$PID" -o pid,ppid,lstart,command
|
|
170
|
+
lsof -a -p "$PID" -d cwd,1,2 -Fn
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The claw baseline is a **Terminal foreground process** (not launchd) running from
|
|
174
|
+
a release directory, with `WORKING_DIR=~/.pi/imessage-next`,
|
|
175
|
+
`PI_CODING_AGENT_DIR=~/.pi/agent`, Node 22, logs in `WORKING_DIR/service.log`, and
|
|
176
|
+
the owner-selected unauthenticated bind `0.0.0.0:7750` (trusted networks only).
|
|
177
|
+
A similarly named Pi web job does not control this service. Do not switch
|
|
178
|
+
controllers, install launchd or upgrade Node during a routine restart.
|
|
179
|
+
|
|
180
|
+
### 2. Record the baseline (ADR 0038)
|
|
181
|
+
|
|
182
|
+
Restart at any time; no running work defers it. Before stopping, record the
|
|
183
|
+
counts of `sending` / `unknown` receipts from `/chat/data` (counts only, no chat
|
|
184
|
+
text). Never reset cursors, delete state or replay uncertain sends. Do not use
|
|
185
|
+
`/health/model`: it is a paid request.
|
|
186
|
+
|
|
187
|
+
### 3. Run one private controller
|
|
188
|
+
|
|
189
|
+
From a chat, the assistant calls `restart_service` with the full commit hash of
|
|
190
|
+
the prepared release directory (ADR 0052). The host writes the controller
|
|
191
|
+
directory below itself and opens the controller only after that tool call has
|
|
192
|
+
settled; the call returns the directory and the next call verifies (step 4).
|
|
193
|
+
The manual procedure below is for operators and for a cold start when no
|
|
194
|
+
service is running.
|
|
195
|
+
|
|
196
|
+
In a `mktemp -d /tmp/pi-imessage-restart.XXXXXX` directory with `umask 077`:
|
|
197
|
+
|
|
198
|
+
- `environment.sh`: shell-quoted exports of the running process's `WORKING_DIR`,
|
|
199
|
+
`PI_CODING_AGENT_DIR`, `DOTENV_CONFIG_PATH`, Web/DB overrides, proxy variables
|
|
200
|
+
(both cases), `NODE_OPTIONS`, search keys, HOME, locale, `TZ` and PATH (keep
|
|
201
|
+
`~/.pi/agent/bin` and the Node directory). Write it directly; never print
|
|
202
|
+
values, `ps eww` output or credentials.
|
|
203
|
+
- `control.sh`: shell-quoted `OLD_PID`, `OLD_STARTED` (exact `ps -o lstart=`),
|
|
204
|
+
`EXPECTED_COMMAND`, `REPO` (the release directory), `NODE`, `WORKING_DIR`,
|
|
205
|
+
`WEB_PORT` and `LOG_FILE`.
|
|
206
|
+
- `restart.command`: a copy of [`ops/restart.command`](restart.command) from the
|
|
207
|
+
release directory. It checks the old PID's start time and command, sends
|
|
208
|
+
SIGTERM, waits up to 30 s, refuses to start while the lock or port is held,
|
|
209
|
+
writes `controller.log` and `new.pid`, deletes `environment.sh` and then
|
|
210
|
+
`exec`s the new service process.
|
|
211
|
+
|
|
212
|
+
Launch it once with `chmod 700` and `open -a Terminal "$DEPLOY_DIR/restart.command"`
|
|
213
|
+
(`osascript` `do script` times out on claw). The separate Terminal keeps the
|
|
214
|
+
controller alive when the initiating agent's own turn is interrupted. For a
|
|
215
|
+
launchd controller, use that job's lifecycle instead; never `launchctl kickstart -k`
|
|
216
|
+
from inside the service.
|
|
217
|
+
|
|
218
|
+
A chat that writes its own controller through `bash` instead of
|
|
219
|
+
`restart_service` must end that call right after `open`: no `sleep`, polling or
|
|
220
|
+
verification in the same call. Tools are replay-safe by default (ADR 0041), so a
|
|
221
|
+
call still running when the controller stops the service reruns on every start,
|
|
222
|
+
launches another controller and loops (#39). Verify in a later call.
|
|
223
|
+
|
|
224
|
+
### 4. Verify
|
|
225
|
+
|
|
226
|
+
In a new tool call, read `controller.log`, `new.pid`, the service log and the
|
|
227
|
+
listener first. Never launch the controller twice before the first outcome is
|
|
228
|
+
known. Require:
|
|
229
|
+
|
|
230
|
+
1. Old PID gone; exactly one new process and listener, cwd in the release
|
|
231
|
+
directory.
|
|
232
|
+
2. Fresh `Application starting`, `Agent ready`, `Web server listening`,
|
|
233
|
+
`Messaging ready` and `Application started`; no startup error.
|
|
234
|
+
3. Root and changed endpoints return 200; check UI changes in a browser.
|
|
235
|
+
4. Interrupted work continues; scheduler deadlines are unchanged; receipt
|
|
236
|
+
counts change only by new sends and sends interrupted by the stop. After
|
|
237
|
+
`restart_service`, the old log shows `Restart requested` with the
|
|
238
|
+
controller directory.
|
|
239
|
+
|
|
240
|
+
On a timeout, lock, occupied port or startup failure, stop and report it. Do not
|
|
241
|
+
force-kill, steal the lock, reinstall dependencies or restore a workspace snapshot.
|
|
242
|
+
|
|
243
|
+
Routine deployments do not run the live smoke. Only when the owner authorizes a
|
|
244
|
+
paid model request and a real message, run
|
|
245
|
+
`node --experimental-strip-types ops/smoke.ts --live --chat CHAT_GUID`: it checks
|
|
246
|
+
`/health/model`, waits for the operator's message from the device and reports
|
|
247
|
+
the new reply's receipt. Never resend a reply that is not `sent`.
|
|
248
|
+
Record the deployment (time, commit, PID, checks, unverified limits) with
|
|
249
|
+
`pi-memory`, delete the `/tmp` controller directory, and report **deployed and
|
|
250
|
+
verified** or the blocker. Release gates and evidence live in GitHub #33.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import Database from "better-sqlite3";
|
|
9
|
+
import sharp from "sharp";
|
|
10
|
+
|
|
11
|
+
assert.equal(process.platform, "darwin", "Messages transport requires macOS");
|
|
12
|
+
const version = process.versions.node.split(".").map(Number);
|
|
13
|
+
assert.ok(version[0] > 22 || (version[0] === 22 && (version[1] > 22 || (version[1] === 22 && version[2] >= 2))), "Node 22.22.2 or newer is required");
|
|
14
|
+
const database = new Database(":memory:");
|
|
15
|
+
try { assert.equal(database.prepare("SELECT 1 AS value").get().value, 1); } finally { database.close(); }
|
|
16
|
+
assert.equal((await sharp({ create: { width: 1, height: 1, channels: 3, background: "white" } }).jpeg().toBuffer()).length > 0, true);
|
|
17
|
+
const browserState = await mkdtemp(join(tmpdir(), "imessage-browser-preflight-"));
|
|
18
|
+
try {
|
|
19
|
+
// doctor can report MISSING with exit 0; --version must reach the installed runtime.
|
|
20
|
+
await promisify(execFile)(process.env.PI_BROWSER_CLI_PATH ?? join(getAgentDir(), "bin", "pi-browser"), ["--version"], {
|
|
21
|
+
env: { ...process.env, PI_BROWSER_HOME: browserState }, timeout: 10000, maxBuffer: 65536,
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error("Browser CLI readiness failed; install the pinned runtime with pi-browser setup", { cause: error });
|
|
25
|
+
} finally {
|
|
26
|
+
await rm(browserState, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
console.log("Preflight passed: macOS, Node, native SQLite/image dependencies and browser CLI runtime. No model, Messages or production workspace was accessed.");
|