@comput/pi-intercom 0.1.1 → 0.1.3
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 +58 -23
- package/package.json +2 -2
- package/references/implementation-blocker.md +2 -2
- package/references/v1-specification.md +14 -12
- package/src/config.ts +34 -15
- package/src/index.ts +16 -4
- package/src/launcher.ts +9 -7
- package/src/runtime.ts +61 -17
package/README.md
CHANGED
|
@@ -19,11 +19,21 @@ Alternatively install from npm:
|
|
|
19
19
|
pi install npm:@comput/pi-intercom
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Then start a new Pi session or run `/reload`.
|
|
22
|
+
Then start a new Pi session or run Pi `/reload`. `pi install` records the package in user settings by default; add `-l` for project settings. The Intercom extension itself does not change Pi settings. Load only one copy: do not enable the installed package alongside an explicit source checkout, or load both source and built output.
|
|
23
|
+
|
|
24
|
+
### Updating the loaded copy
|
|
25
|
+
|
|
26
|
+
- **npm installation:** update the installed package, not this checkout: `pi update npm:@comput/pi-intercom`. A pinned version must be explicitly changed (for example, `pi install npm:@comput/pi-intercom@0.1.2`, using the same settings scope as the original installation). `pi list` shows configured package sources; inspect that installation's `package.json` to confirm its version.
|
|
27
|
+
- **Source loading (`pi -e .../src/index.ts`):** update that checkout. `npm run build` produces test output; it does not update a separate npm installation.
|
|
28
|
+
- After obtaining the intended code, start a new session or use Pi `/reload` in each affected running host. **`intercom_reload_worker` only reads shared configuration; it cannot load extension-code fixes.** Workers launched by a coordinator receive the coordinator extension's actual file path. Updating a different copy does not change that path, and existing workers do not automatically reload.
|
|
29
|
+
|
|
30
|
+
The 0.1.2 update/reload and subsequent worker creation were observed live; comprehensive reload/session-replacement lifecycle coverage remains pending.
|
|
31
|
+
|
|
32
|
+
### Startup and launchers
|
|
23
33
|
|
|
24
34
|
Without shared config, the current working directory becomes the coordinator root. Startup creates `.pi-intercom/config.json`, opens a loopback listener, synchronizes the name `Coordinator`, and waits for user input (no model turn). Existing config is found by walking upward. Invalid/unreadable config is an error and is not overwritten. Unknown session IDs, including forks, are workers, never replacement coordinators.
|
|
25
35
|
|
|
26
|
-
Default launcher `herdr` requires the coordinator to be inside Herdr (`HERDR_ENV=1` and workspace context), with `herdr` on PATH. Each worker gets **one tab, one pane, no split, no focus change**. The `none` launcher requires `pi.cmd` and Windows PowerShell on PATH and opens a separate visible PowerShell terminal. Missing launcher is an error, not fallback.
|
|
36
|
+
Default launcher `herdr` requires the coordinator to be inside Herdr (`HERDR_ENV=1` and workspace context), with `herdr` on PATH and `powershell.exe` plus `pi.ps1` available in the worker pane. Each worker gets **one tab, one pane, no split, no focus change**. The `none` launcher requires `pi.cmd` and Windows PowerShell on PATH and opens a separate visible PowerShell terminal. Missing launcher is an error, not fallback. Launch acknowledgment means command submission/terminal creation, not Pi readiness. A failed launch can leave a tab or process behind; no automatic cleanup occurs. See the scoped live evidence below.
|
|
27
37
|
|
|
28
38
|
## Explicit workflow
|
|
29
39
|
|
|
@@ -34,25 +44,25 @@ Default launcher `herdr` requires the coordinator to be inside Herdr (`HERDR_ENV
|
|
|
34
44
|
5. Coordinator sends explicit work using `intercom_send({to:"Builder",message:"..."})`. Workers can communicate directly with configured peers. Findings do not authorize unrelated implementation. Progress questions ask for reporting and continuation, not cancellation.
|
|
35
45
|
6. Workers report as instructed and wait, without autonomous exit. Coordinator unavailability does not close the worker or cancel its assignment.
|
|
36
46
|
|
|
37
|
-
Existing workers restore responsibility by session ID, bind their saved port or an OS-selected fallback, and report their actual port. Coordinator updates existing ports as bookkeeping. Status from removed/unknown sessions notifies Pi but creates no entry. Resume uses the configured project directory and full session ID, with a saved-session lookup first; a never-used Pi session might not yet exist on disk. Failure leaves config unchanged, without fabricated history or forced model turns.
|
|
47
|
+
Existing workers restore responsibility by session ID, bind their saved port or, if it is already in use, an OS-selected fallback, and report their actual port. Coordinator updates existing ports as bookkeeping. Status from removed/unknown sessions notifies Pi but creates no entry. Resume uses the configured project directory and full session ID, with a saved-session lookup first; a never-used Pi session might not yet exist on disk. Failure leaves config unchanged, without fabricated history or forced model turns.
|
|
38
48
|
|
|
39
49
|
## Tools and permissions
|
|
40
50
|
|
|
41
51
|
Every tool rereads current config and checks the live Pi session ID. Names are case-insensitive for lookup/uniqueness; display capitalization is preserved. `Coordinator` is reserved.
|
|
42
52
|
|
|
43
|
-
| Tool | Permission / effect |
|
|
44
|
-
|
|
45
|
-
| `intercom_create_worker` | Coordinator; launch anonymous worker
|
|
46
|
-
| `intercom_configure_worker` | Coordinator;
|
|
47
|
-
| `intercom_reload_worker` | Coordinator;
|
|
48
|
-
| `intercom_send` | Configured, responsibility-loaded sessions;
|
|
49
|
-
| `intercom_list` | Configured sessions; saved state only, never live health |
|
|
50
|
-
| `intercom_request_status` |
|
|
51
|
-
| `intercom_report_status` | Workers including anonymous; shared report function, never registration |
|
|
52
|
-
| `intercom_stop_worker`, `intercom_close_worker` | Coordinator role checked, then **unsupported-host error**, no side effects |
|
|
53
|
-
| `intercom_resume_worker` | Coordinator;
|
|
54
|
-
| `intercom_remove_worker` | Coordinator; config entry only, no shutdown/session deletion |
|
|
55
|
-
| `intercom_set_multiplexer` |
|
|
53
|
+
| Tool | Arguments | Permission / effect |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `intercom_create_worker` | `projectDirectory?` (default `.`) | Coordinator; launch anonymous worker |
|
|
56
|
+
| `intercom_configure_worker` | `sessionId`, `port`, `projectDirectory`, `name`, `description` | Coordinator; all five explicit fields, writes only |
|
|
57
|
+
| `intercom_reload_worker` | `to` (worker name) | Coordinator; passive config/name reload |
|
|
58
|
+
| `intercom_send` | `to` (name), `message` | Configured, responsibility-loaded sessions; asynchronous message |
|
|
59
|
+
| `intercom_list` | None | Configured sessions; saved state only, never live health |
|
|
60
|
+
| `intercom_request_status` | `to` (worker name) | Coordinator; independent extension report |
|
|
61
|
+
| `intercom_report_status` | None | Workers including anonymous; shared report function, never registration |
|
|
62
|
+
| `intercom_stop_worker`, `intercom_close_worker` | `to` (worker name) | Coordinator role checked, then **unsupported-host error**, no side effects |
|
|
63
|
+
| `intercom_resume_worker` | `to` (worker name) | Coordinator; saved session, configured launcher/directory |
|
|
64
|
+
| `intercom_remove_worker` | `to` (worker name) | Coordinator; config entry only, no shutdown/session deletion |
|
|
65
|
+
| `intercom_set_multiplexer` | `multiplexer`: `herdr` or `none` | Coordinator; future launches only |
|
|
56
66
|
|
|
57
67
|
**Removal precondition:** explicitly close a running worker before removing its entry. Since remote close is disabled, arrange explicit user closure in its terminal first. Intercom does not infer liveness from saved IDs/ports and cannot certify closure. No hidden probing or force-kill is performed.
|
|
58
68
|
|
|
@@ -89,12 +99,14 @@ HTTP binds **127.0.0.1 only**, `POST /intercom`, UTF-8 JSON, at most 64 KiB. Env
|
|
|
89
99
|
|
|
90
100
|
Kinds: `message`, `registration` (`port`, `projectDirectory`), `status` (`port`, `busy`), `request_status`, `reload`, `stop`, `close` (empty control payload). Sender session identity is the envelope `from`; registration/status agent notifications include an explicit `sessionId` field. Agent messages require configured sender and recipient; controls require the current coordinator's sender ID and a worker recipient. Registration/unknown status are intentional exceptions to configured-sender checks, accepted only by coordinator. All receivers verify expected recipient ID, protecting against stale ports reaching another session. Local processes are trusted: this is role validation, **not authentication**. No remote networking/proxies/redirects or credentials.
|
|
91
101
|
|
|
92
|
-
`202 {"accepted":true}` means extension receipt/acceptance,
|
|
102
|
+
`202 {"accepted":true}` means Intercom extension receipt/dispatch, **not guaranteed Pi input acceptance, queueing, or model completion**. In Pi 0.84.4, `sendUserMessage` can reject during manual compaction after Intercom has acknowledged receipt: its void extension API reports the asynchronous rejection only as a local host error. Intercom has no delivery queue or retry to repair this gap; inspect the recipient's local error and arrange an explicit resend after compaction if needed. Replacing it with `sendMessage` is not a safe workaround because that path can start a concurrent run. Invalid/unsupported requests return an error; excessive bodies return 413. Request-status sends its report separately after accepting control. Delivery is normal when idle and steering when busy, sampled at arrival. It does not hard-interrupt an executing tool. Each send resolves the recipient again from config; 5-second receipt deadline, **no retries**. Timeout means unknown outcome, not proof of nondelivery. No durable inbox, deduplication or replay log in V1.
|
|
93
103
|
|
|
94
104
|
Tool text output is bounded to 50 KiB/2000 lines. Full list data remains in the shared config file. Name sync errors are surfaced; startup retains the useful endpoint, and explicit reload reports failure (Pi name/responsibility may already have changed before a Herdr tab rename fails).
|
|
95
105
|
|
|
96
106
|
## Development and validation
|
|
97
107
|
|
|
108
|
+
Run these commands from a source checkout; tests and workflows are not shipped in the npm package. The [original implementation validation snapshot](https://github.com/mbundgaard/PiIntercom/blob/main/references/implementation-progress.md) is historical, not the current live-test inventory.
|
|
109
|
+
|
|
98
110
|
```sh
|
|
99
111
|
npm ci
|
|
100
112
|
npm run typecheck
|
|
@@ -108,18 +120,41 @@ Production source runs through Pi's TypeScript loader; `npm run build` generates
|
|
|
108
120
|
|
|
109
121
|
## Publishing
|
|
110
122
|
|
|
111
|
-
`.github/workflows/publish.yml` publishes through npm Trusted Publishing (GitHub OIDC), without an npm token secret. Configure the npm trusted publisher as owner `mbundgaard`, repository `PiIntercom`, workflow `publish.yml`, with no environment.
|
|
123
|
+
[`.github/workflows/publish.yml`](https://github.com/mbundgaard/PiIntercom/blob/main/.github/workflows/publish.yml) publishes through npm Trusted Publishing (GitHub OIDC), without an npm token secret. Configure the npm trusted publisher as owner `mbundgaard`, repository `PiIntercom`, workflow `publish.yml`, with no environment.
|
|
112
124
|
|
|
113
125
|
The workflow validates on Windows, then publishes with provenance from a GitHub-hosted Ubuntu runner. It runs when a GitHub release is published or when manually dispatched. Release tags must be `v<package.json version>`.
|
|
114
126
|
|
|
115
|
-
|
|
127
|
+
Published versions are immutable. For a new release, bump package and lockfile versions, commit/push, then publish a matching GitHub release. Manual dispatch publishes the selected ref and is **not a dry run**; the release-tag check only applies when a release tag is present. Do not dispatch publishing for an already published version. CI validates pushes to `main` and pull requests separately without publishing.
|
|
128
|
+
|
|
129
|
+
### Release evidence
|
|
130
|
+
|
|
131
|
+
- [v0.1.1](https://github.com/mbundgaard/PiIntercom/releases/tag/v0.1.1): successful npm OIDC [run 35466923379](https://github.com/mbundgaard/PiIntercom/actions/runs/35466923379).
|
|
132
|
+
- [v0.1.2](https://github.com/mbundgaard/PiIntercom/releases/tag/v0.1.2): successful npm OIDC [run 35467586052](https://github.com/mbundgaard/PiIntercom/actions/runs/35467586052). Fixes Windows Herdr launching by using `herdr pane run` on the returned pane ID with encoded PowerShell invoking `pi.ps1`, instead of the failing `Start-Process pi` wrapper. No Herdr agent alias is needed.
|
|
133
|
+
|
|
134
|
+
### 0.1.3 changes and verification
|
|
135
|
+
|
|
136
|
+
Version 0.1.3 includes these fixes (not present in 0.1.2):
|
|
137
|
+
|
|
138
|
+
- Lifecycle generation/session checks reject stale queued or in-flight operations before submitting new launches, sends, deliveries or config publication. This includes delayed listener startup, anonymous registration and deferred status reporting. An OS link/rename or external action already submitted before invalidation is **not cancelled or rolled back**; there is no guarantee that in-flight external actions are drained.
|
|
139
|
+
- Failed temporary-file write, sync or close attempts best-effort close/unlink cleanup while preserving the primary error. Filesystem cleanup failures can still leave temporary files.
|
|
140
|
+
- Resume preflight now follows the child session-storage context: `PI_CODING_AGENT_SESSION_DIR`, then target-project/global `sessionDir` settings, then Pi's default. Relative storage paths resolve against the target working directory. Launchers do not propagate the coordinator's CLI `--session-dir`; no machine-specific session path is stored in shared config. A static persisted-session fixture and mocked launcher validate lookup, not live resumed-worker startup.
|
|
141
|
+
|
|
142
|
+
Coordinator validation of this source snapshot: `npm run typecheck` PASS; `npm test` **36/36**, no failures; `npm run check:package` PASS; `git diff --check` PASS (line-ending warnings only). Quality independently confirmed typecheck and all 36 tests, with no remaining actionable regression identified in these fixes. Tests include actual extension-adapter event handling against a mocked Pi API/context and runtime-to-runtime loopback HTTP; they do not establish real Pi queue or compaction behavior.
|
|
143
|
+
|
|
144
|
+
Tests use temporary directories, local test-only HTTP servers, static session fixtures and mocked Pi hosts/launch executors. They never start agents, tabs, terminals, contact models, services or bridges, or access credentials.
|
|
145
|
+
|
|
146
|
+
### Observed live behavior
|
|
147
|
+
|
|
148
|
+
The release/session evidence reported during the 0.1.2 validation establishes these specific observations, not blanket lifecycle compliance:
|
|
116
149
|
|
|
117
|
-
|
|
150
|
+
- Encoded PowerShell invoking `pi.ps1` recovered an existing failed Herdr tab; Pi started and the worker registered.
|
|
151
|
+
- After package update and Pi reload, coordinator `intercom_create_worker` launched three workers; each registered, then was explicitly configured and separately reloaded. Integration's configured name was applied through configure/reload.
|
|
152
|
+
- An asynchronous Core → Coordinator message was relayed with `telegram_send` and receipt was confirmed by the owner. Telegram is a separate integration, not an Intercom transport or automatic forwarding feature.
|
|
118
153
|
|
|
119
|
-
###
|
|
154
|
+
### Remaining limitations / not comprehensively live-tested
|
|
120
155
|
|
|
121
|
-
-
|
|
122
|
-
-
|
|
156
|
+
- Idle/steering queue edge cases, busy snapshots, session-name persistence across restarts, trust UI and session replacement/reload lifecycle. The specific manual-compaction rejection gap described above remains unresolved; successful ordinary messaging does not verify that case.
|
|
157
|
+
- The observations above do not establish all Herdr failure paths, tab-association edge cases or readiness guarantees.
|
|
123
158
|
- Windows visible terminal/Pi process startup. Success only proves terminal creation, **not Pi readiness**; startup failures remain visible in that terminal. No live readiness handshake was added.
|
|
124
159
|
- Actual persisted-session lookup and resumed worker startup; never-used session failure.
|
|
125
160
|
- Cross-process/network-drive atomicity and Windows ACL/sharing failures (same-process temp-filesystem races are tested).
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/mbundgaard/PiIntercom/issues"
|
|
14
14
|
},
|
|
15
|
-
"version": "0.1.
|
|
15
|
+
"version": "0.1.3",
|
|
16
16
|
"description": "Explicit local communication between Pi sessions; Pi orchestrates.",
|
|
17
17
|
"type": "module",
|
|
18
18
|
"keywords": [
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc",
|
|
47
47
|
"typecheck": "tsc --noEmit",
|
|
48
|
-
"test": "npm run build && node --test tests/core.test.mjs tests/integration.test.mjs tests/launcher.test.mjs tests/extension.test.mjs",
|
|
48
|
+
"test": "npm run build && node --test tests/core.test.mjs tests/integration.test.mjs tests/launcher.test.mjs tests/extension.test.mjs tests/runtime-http.test.mjs tests/resume-storage.test.mjs",
|
|
49
49
|
"test:host": "node --test tests/pi-cancellation-probe.test.mjs",
|
|
50
50
|
"check:package": "npm pack --dry-run --ignore-scripts"
|
|
51
51
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# V1 cancellation capability blocked: Pi cancellation contract
|
|
2
2
|
|
|
3
|
-
> **Current status:** The user subsequently approved implementing independent features. The supported core now exists; see
|
|
3
|
+
> **Current status:** The user subsequently approved implementing independent features. The supported core now exists; see the [historical implementation snapshot](https://github.com/mbundgaard/PiIntercom/blob/main/references/implementation-progress.md) and [current README](../README.md). Stop/close remain disabled, failing before cancellation/shutdown; README also records the separate manual-compaction input-acceptance gap. The historical feasibility report below is preserved as evidence; its statements that no implementation exists describe the earlier run, not current project status.
|
|
4
4
|
|
|
5
5
|
## Historical outcome
|
|
6
6
|
|
|
@@ -40,7 +40,7 @@ No private-runtime access, repeated abort polling, synthetic Escape input, setti
|
|
|
40
40
|
|
|
41
41
|
Added `tests/pi-cancellation-probe.test.mjs`, a dependency-free compatibility probe. It extracts selected installed methods and executes them with mocks; it does not construct a Pi session, read credentials, contact a model, bind a server, launch a process, or modify Herdr.
|
|
42
42
|
|
|
43
|
-
Git Bash invocation:
|
|
43
|
+
Git Bash invocation from a source checkout (the probe is not shipped in the npm package):
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
PI_INTERCOM_PI_ROOT='C:/Users/martin/AppData/Roaming/npm/node_modules/@earendil-works/pi-coding-agent' \
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# PiIntercom V1 Specification
|
|
2
2
|
|
|
3
|
-
This is the consolidated
|
|
3
|
+
This is the consolidated **target design**, authoritative over conflicting wording in the original proposal and historical discussion notes. It is not a claim that every capability is currently supported. See [README](../README.md) for current behavior, release evidence and validation limits.
|
|
4
|
+
|
|
5
|
+
**Current capability exception:** `intercom_stop_worker` and `intercom_close_worker` are registered but disabled on every host, before cancellation/shutdown or outgoing control. Their intended guarantees below remain unchanged; see the [cancellation blocker](implementation-blocker.md). Manual-compaction delivery also has a known acceptance gap: HTTP receipt does not guarantee Pi accepted the input. Unresolved contract questions are not silently approved decisions.
|
|
4
6
|
|
|
5
7
|
## 1. Scope and principles
|
|
6
8
|
|
|
@@ -34,7 +36,7 @@ Required configuration information:
|
|
|
34
36
|
| Agent | Pi session ID, name, coordinator flag, responsibility description, port |
|
|
35
37
|
| Worker | Project directory relative to coordinator root |
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
The implemented schema is version 1; its fields and validation are documented in [README: Config and wire contracts](../README.md#config-and-wire-contracts). Do not treat a saved endpoint or session ID as proof that its process exists.
|
|
38
40
|
|
|
39
41
|
## 3. Startup and registration
|
|
40
42
|
|
|
@@ -122,12 +124,12 @@ Every invocation reads the current Pi session ID and shared config, determines r
|
|
|
122
124
|
| `intercom_create_worker` | Coordinator | `projectDirectory?` (default `.`) | Launch new anonymous worker with extension loaded; report launch success/failure without awaiting registration |
|
|
123
125
|
| `intercom_configure_worker` | Coordinator | `sessionId`, `port`, `projectDirectory`, `name`, `description` | Write the explicitly supplied worker configuration; no notification |
|
|
124
126
|
| `intercom_reload_worker` | Coordinator | `to` (name) | Send config-read control only |
|
|
125
|
-
| `intercom_send` |
|
|
126
|
-
| `intercom_list` |
|
|
127
|
+
| `intercom_send` | Configured, responsibility-loaded sessions | `to` (name), `message` | Deliver agent prompt/steering; return send result, not agent reply |
|
|
128
|
+
| `intercom_list` | Configured sessions | None | Reread config; list names, responsibilities, IDs, roles, ports, without live probing |
|
|
127
129
|
| `intercom_request_status` | Coordinator | `to` (name) | Ask worker extension to send independent runtime status report |
|
|
128
|
-
| `intercom_report_status` | Worker | None | Invoke shared reporting function; reject on coordinator |
|
|
129
|
-
| `intercom_stop_worker` | Coordinator | `to` (name) |
|
|
130
|
-
| `intercom_close_worker` | Coordinator | `to` (name) |
|
|
130
|
+
| `intercom_report_status` | Worker, including anonymous | None | Invoke shared reporting function; reject on coordinator |
|
|
131
|
+
| `intercom_stop_worker` | Coordinator | `to` (name) | Target: cancel current work; leave Pi and extension running. Currently disabled; unsupported-host error |
|
|
132
|
+
| `intercom_close_worker` | Coordinator | `to` (name) | Target: cancel work, gracefully exit Pi, preserve session/config. Currently disabled; unsupported-host error |
|
|
131
133
|
| `intercom_resume_worker` | Coordinator | `to` (name) | Launch saved session using configured directory and launcher; failures leave config unchanged |
|
|
132
134
|
| `intercom_remove_worker` | Coordinator | `to` (name) | Remove config entry only; no process stop or session-file deletion |
|
|
133
135
|
| `intercom_set_multiplexer` | Coordinator | `multiplexer` | Set `herdr` or `none` for future launches, including resume |
|
|
@@ -137,7 +139,7 @@ Every invocation reads the current Pi session ID and shared config, determines r
|
|
|
137
139
|
- Configure rejects duplicate names, including `Coordinator`, case-insensitively. Keeping a session's own name is allowed.
|
|
138
140
|
- Configure and reload are deliberately separate. Reload is not Pi's extension-code reload, a process restart, cancellation, or a work assignment. Explain this in tool and user documentation.
|
|
139
141
|
- Reload synchronizes Pi session and applicable Herdr tab names to config.
|
|
140
|
-
-
|
|
142
|
+
- Target contract: stop/close use Pi cancellation and graceful shutdown; no force-kill, automatic commit, or rollback. Close does not require a separate stop call. **Currently both fail closed; no stop or shutdown is performed.**
|
|
141
143
|
- Close success at transport level is not proof that process exit has completed.
|
|
142
144
|
- Remove a running worker only after explicitly closing it; removal itself never closes it.
|
|
143
145
|
- Resume failure must be surfaced; no implicit removal, replacement, or reassignment.
|
|
@@ -163,14 +165,14 @@ Every invocation reads the current Pi session ID and shared config, determines r
|
|
|
163
165
|
|
|
164
166
|
## 9. Remaining implementation gaps — not new approved behavior
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
Original design-gap inventory with current implementation annotations. Avoid reopening settled user flows. Implemented mechanisms are not proof of comprehensive live-host validation.
|
|
167
169
|
|
|
168
170
|
1. **Registration handoff — resolved:** deliver reported session ID, port, and project directory into the coordinator Pi context. The coordinator agent decides what to do and supplies all values explicitly to configure_worker. No hidden pending-registration map, placeholder entry, or automatic configuration/reload workflow.
|
|
169
171
|
2. **Retrying anonymous registration:** status reports do not create registrations. Define an explicit way to repeat a failed initial registration without conflating it with known-worker status.
|
|
170
|
-
3. **Wire/config schemas:**
|
|
172
|
+
3. **Wire/config schemas — implemented:** schema version 1, documented fields/control kinds, validation, 64 KiB body bound and extension-receipt acknowledgment are described in README. No message IDs, deduplication or synchronous agent replies. Receipt is not proof of Pi input acceptance or model completion.
|
|
171
173
|
4. **Pi lifecycle APIs:** verify extension loading, session-ID resume lookup, cancellation, queued-message treatment on stop/close, busy detection, and session-switch cleanup. Stop must not unexpectedly restart queued work; exact API feasibility needs checking.
|
|
172
174
|
5. **Launch integration:** verify installed Herdr operations and a Windows visible-terminal implementation, including readiness/launch-failure observability and tab association. A successful process launch is not worker registration success.
|
|
173
|
-
6. **Configuration writes
|
|
175
|
+
6. **Configuration writes — implemented, validation limits remain:** serialized atomic updates and complete-file no-replace initial publication; invalid config is not overwritten. See README for filesystem assumptions and unverified cross-process/network-drive cases.
|
|
174
176
|
7. **Missing coordinator session:** workers report connection failure and remain reachable. Explicit coordinator takeover is not specified or approved; do not invent it.
|
|
175
177
|
8. **Unconfigured worker permissions:** explicitly define allowed worker-side tools before its entry exists, while retaining registration/reporting ability and forbidding coordinator operations.
|
|
176
178
|
9. **Protocol control checks:** distinguish initial registration from configured-sender validation. Reload must reach the anonymous worker after the coordinator has created its entry.
|
|
@@ -180,4 +182,4 @@ Consolidate these before coding; avoid reopening settled user flows.
|
|
|
180
182
|
- Read installed Pi documentation and relevant Herdr skills before implementation.
|
|
181
183
|
- Document tool permissions, startup/registration flow, passive responsibility loading, separate configure/reload calls, and failure semantics.
|
|
182
184
|
- Test role gates, port fallback, recipient mismatch, duplicate names, config errors, explicit recovery, and asynchronous steering/status paths.
|
|
183
|
-
-
|
|
185
|
+
- The original design discussion produced no implementation code. A supported-core implementation and releases now exist; current evidence and limitations are maintained in README.
|
package/src/config.ts
CHANGED
|
@@ -74,41 +74,60 @@ export class ConfigStore {
|
|
|
74
74
|
const parent = path.dirname(at); if (parent === at) return undefined; at = parent;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
protected openTemporary(file: string) { return open(file, 'wx'); }
|
|
77
78
|
private async temporary(config: Config): Promise<string> {
|
|
78
79
|
validateConfig(config);
|
|
79
80
|
await mkdir(path.dirname(this.file), { recursive: true });
|
|
80
81
|
const temp = `${this.file}.${randomUUID()}.tmp`;
|
|
81
|
-
const handle = await
|
|
82
|
-
try {
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
const handle = await this.openTemporary(temp);
|
|
83
|
+
try {
|
|
84
|
+
await handle.writeFile(JSON.stringify(config, null, 2) + '\n');
|
|
85
|
+
await handle.sync();
|
|
86
|
+
await handle.close();
|
|
87
|
+
return temp;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// Cleanup failures must not replace the original write/sync/close error.
|
|
90
|
+
await handle.close().catch(() => {});
|
|
91
|
+
await unlink(temp).catch(() => {});
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
85
94
|
}
|
|
86
|
-
async initialize(id: string, boundPort: number): Promise<boolean> {
|
|
95
|
+
async initialize(id: string, boundPort: number, assertValid: () => void = () => {}): Promise<boolean> {
|
|
87
96
|
// Publish an already complete file using an atomic, no-replace hard link.
|
|
88
97
|
// Losers only ever see complete JSON; unsupported filesystems fail explicitly.
|
|
98
|
+
assertValid();
|
|
89
99
|
const temp = await this.temporary({ version: 1, multiplexer: 'herdr', agents: [{ sessionId: id, name: 'Coordinator', coordinator: true, description: DEFAULT_DESCRIPTION, port: boundPort, projectDirectory: '.' }] });
|
|
90
|
-
try {
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
try {
|
|
101
|
+
assertValid();
|
|
102
|
+
try { await link(temp, this.file); }
|
|
103
|
+
catch (e) {
|
|
104
|
+
if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e;
|
|
105
|
+
await this.read(); await unlink(temp); return false;
|
|
106
|
+
}
|
|
107
|
+
await unlink(temp); return true;
|
|
108
|
+
} catch (error) { await unlink(temp).catch(() => {}); throw error; }
|
|
93
109
|
}
|
|
94
|
-
async update(id: string, mutate: (config: Config) => void | Promise<void
|
|
110
|
+
async update(id: string, mutate: (config: Config) => void | Promise<void>, assertValid: () => void = () => {}): Promise<Config> {
|
|
95
111
|
const operation = this.tail.then(async () => {
|
|
96
|
-
|
|
97
|
-
await
|
|
112
|
+
assertValid();
|
|
113
|
+
const c = await this.read(); assertValid(); requireCoordinator(c, id);
|
|
114
|
+
await mutate(c); assertValid(); validateConfig(c);
|
|
98
115
|
const temp = await this.temporary(c);
|
|
99
|
-
try {
|
|
100
|
-
|
|
116
|
+
try {
|
|
117
|
+
// Last check before publication; an already submitted OS rename cannot be undone.
|
|
118
|
+
assertValid(); await rename(temp, this.file);
|
|
119
|
+
} catch (error) { await unlink(temp).catch(() => {}); throw error; }
|
|
101
120
|
return c;
|
|
102
121
|
});
|
|
103
122
|
this.tail = operation.catch(() => {}); return operation;
|
|
104
123
|
}
|
|
105
|
-
async configure(id: string, values: Omit<Agent, 'coordinator'
|
|
124
|
+
async configure(id: string, values: Omit<Agent, 'coordinator'>, assertValid: () => void = () => {}): Promise<Config> {
|
|
106
125
|
return this.update(id, async c => {
|
|
107
126
|
if (values.sessionId === coordinator(c).sessionId) fail('cannot configure coordinator as worker');
|
|
108
127
|
const projectDirectory = await directory(this.root, values.projectDirectory);
|
|
109
128
|
const a: Agent = { sessionId: values.sessionId, name: values.name, description: values.description, port: values.port, projectDirectory, coordinator: false };
|
|
110
129
|
const index = c.agents.findIndex(old => old.sessionId === a.sessionId);
|
|
111
130
|
if (index < 0) c.agents.push(a); else c.agents[index] = a;
|
|
112
|
-
});
|
|
131
|
+
}, assertValid);
|
|
113
132
|
}
|
|
114
133
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { Type, type TSchema } from 'typebox';
|
|
3
|
-
import { SessionManager, truncateHead, withFileMutationQueue, type ExtensionAPI, type ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
4
|
+
import { getAgentDir, SessionManager, SettingsManager, truncateHead, withFileMutationQueue, type ExtensionAPI, type ExtensionContext } from '@earendil-works/pi-coding-agent';
|
|
4
5
|
import { ConfigStore } from './config.js';
|
|
5
6
|
import { Intercom, UNSUPPORTED_CANCELLATION } from './runtime.js';
|
|
6
7
|
import { launchers } from './launcher.js';
|
|
@@ -21,10 +22,21 @@ const tools: [string, string, TSchema][] = [
|
|
|
21
22
|
['set_multiplexer', 'Coordinator only. Set herdr (default) or none (separate visible Windows terminals) for future launches. Never move/restart existing workers or fall back.', Type.Object({ multiplexer: Type.String({ enum: ['herdr', 'none'] }) })],
|
|
22
23
|
];
|
|
23
24
|
|
|
25
|
+
// Launchers pass no --session-dir. Match child Pi's env > per-cwd settings >
|
|
26
|
+
// default lookup, resolving relative paths against the child's cwd, not ours.
|
|
27
|
+
// SettingsManager's public getter applies Pi's own path/tilde normalization.
|
|
28
|
+
export function resumeSessionDirectory(cwd: string, envSessionDir = process.env.PI_CODING_AGENT_SESSION_DIR, agentDir = getAgentDir()): string | undefined {
|
|
29
|
+
const settings = envSessionDir
|
|
30
|
+
? SettingsManager.inMemory({ sessionDir: envSessionDir })
|
|
31
|
+
: SettingsManager.create(cwd, path.resolve(cwd, agentDir));
|
|
32
|
+
const sessionDir = settings.getSessionDir();
|
|
33
|
+
return sessionDir ? path.resolve(cwd, sessionDir) : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
// Participate in Pi's own per-file mutation queue as well as Intercom serialization.
|
|
25
37
|
class PiConfigStore extends ConfigStore {
|
|
26
|
-
override update(id: string, mutate: Parameters<ConfigStore['update']>[1]) {
|
|
27
|
-
return withFileMutationQueue(this.file, () => super.update(id, mutate));
|
|
38
|
+
override update(id: string, mutate: Parameters<ConfigStore['update']>[1], assertValid?: () => void) {
|
|
39
|
+
return withFileMutationQueue(this.file, () => super.update(id, mutate, assertValid));
|
|
28
40
|
}
|
|
29
41
|
}
|
|
30
42
|
export default function intercomExtension(pi: ExtensionAPI): void {
|
|
@@ -32,7 +44,7 @@ export default function intercomExtension(pi: ExtensionAPI): void {
|
|
|
32
44
|
let context: ExtensionContext | undefined;
|
|
33
45
|
const launcher = launchers({
|
|
34
46
|
extension: fileURLToPath(import.meta.url),
|
|
35
|
-
sessionExists: async (cwd, id) => (await SessionManager.list(cwd,
|
|
47
|
+
sessionExists: async (cwd, id) => (await SessionManager.list(cwd, resumeSessionDirectory(cwd))).some(s => s.id === id),
|
|
36
48
|
});
|
|
37
49
|
pi.on('session_start', async (_event, ctx) => {
|
|
38
50
|
await runtime?.close(); context = ctx;
|
package/src/launcher.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { randomBytes } from 'node:crypto';
|
|
3
2
|
import { promisify } from 'node:util';
|
|
4
3
|
import { fail } from './config.js';
|
|
5
4
|
import type { LaunchRequest } from './runtime.js';
|
|
@@ -31,12 +30,15 @@ export function launchers(options: LauncherOptions) {
|
|
|
31
30
|
catch (e) { fail(`Herdr tab launch failed: ${String(e)}; no fallback or cleanup`); }
|
|
32
31
|
const pane = created.result?.root_pane?.pane_id, tab = created.result?.tab?.tab_id;
|
|
33
32
|
if (!pane || !tab) fail('Herdr creation response missing returned pane/tab IDs; no guessed targeting or cleanup');
|
|
34
|
-
// Herdr
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
// Herdr's Windows agent-start wrapper uses Start-Process on `pi`, which
|
|
34
|
+
// can resolve to a PowerShell shim and fail with invalid Win32 application.
|
|
35
|
+
// Run an explicit PowerShell command in the returned shell pane instead.
|
|
36
|
+
// This acknowledges command submission only, not Pi readiness/registration.
|
|
37
|
+
const script = `$ErrorActionPreference='Stop'; Set-Location -LiteralPath ${psQuote(request.cwd)}; & (Get-Command pi.ps1 -ErrorAction Stop).Source ${args.map(psQuote).join(' ')}; if ($LASTEXITCODE -ne 0) { Write-Error ('Pi exited with code ' + $LASTEXITCODE) }`;
|
|
38
|
+
const command = `powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encoded(script)}`;
|
|
39
|
+
try { await exec('herdr', ['pane', 'run', pane, command]); }
|
|
40
|
+
catch (e) { fail(`Herdr Pi command submission failed in tab ${tab}, pane ${pane}: ${String(e)}. Tab/process may remain; no automatic cleanup/retry/replacement`); }
|
|
41
|
+
return { commandSubmitted: true, tab, pane, piReadiness: 'not observed; inspect pane for startup failures', registrationAwaited: false };
|
|
40
42
|
}
|
|
41
43
|
if (request.multiplexer !== 'none') fail('unsupported multiplexer');
|
|
42
44
|
// Use encoded PowerShell rather than constructing cmd.exe command strings.
|
package/src/runtime.ts
CHANGED
|
@@ -22,6 +22,7 @@ export class Intercom {
|
|
|
22
22
|
store!: ConfigStore;
|
|
23
23
|
endpoint?: Endpoint;
|
|
24
24
|
private active = false;
|
|
25
|
+
private generation = 0;
|
|
25
26
|
private initialId = '';
|
|
26
27
|
responsibility?: Agent;
|
|
27
28
|
constructor(readonly host: Host, readonly options: RuntimeOptions) {}
|
|
@@ -30,67 +31,100 @@ export class Intercom {
|
|
|
30
31
|
if (!this.active || id !== this.initialId) fail('session is inactive or replaced');
|
|
31
32
|
return id;
|
|
32
33
|
}
|
|
34
|
+
private validity(): () => void {
|
|
35
|
+
const generation = this.generation;
|
|
36
|
+
this.id();
|
|
37
|
+
return () => {
|
|
38
|
+
this.id();
|
|
39
|
+
if (generation !== this.generation) fail('runtime lifecycle replaced');
|
|
40
|
+
};
|
|
41
|
+
}
|
|
33
42
|
async start(): Promise<void> {
|
|
43
|
+
if (this.active) fail('runtime already active');
|
|
34
44
|
this.initialId = this.host.sessionId(); this.active = true;
|
|
45
|
+
const generation = ++this.generation, assertValid = this.validity();
|
|
35
46
|
try {
|
|
36
47
|
const discovered = await ConfigStore.discover(this.host.cwd);
|
|
48
|
+
assertValid();
|
|
37
49
|
this.store = this.options.store?.(discovered?.root ?? this.host.cwd) ?? discovered ?? new ConfigStore(this.host.cwd);
|
|
38
50
|
const previous = discovered ? await this.store.read() : undefined;
|
|
51
|
+
assertValid();
|
|
39
52
|
const own = previous?.agents.find(a => a.sessionId === this.id());
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
const endpoint = await (this.options.listen ?? listen)(own?.port, m => { assertValid(); return this.receive(m); });
|
|
54
|
+
try { assertValid(); } catch (error) { await endpoint.close(); throw error; }
|
|
55
|
+
this.endpoint = endpoint;
|
|
56
|
+
if (!previous) await this.store.initialize(this.id(), this.endpoint.port, assertValid);
|
|
42
57
|
const config = await this.store.read();
|
|
58
|
+
assertValid();
|
|
43
59
|
const me = config.agents.find(a => a.sessionId === this.id());
|
|
44
60
|
if (me?.coordinator) {
|
|
45
|
-
await this.store.update(this.id(), c => { requireCoordinator(c, this.id()).port = this.endpoint!.port; });
|
|
46
|
-
|
|
61
|
+
await this.store.update(this.id(), c => { requireCoordinator(c, this.id()).port = this.endpoint!.port; }, assertValid);
|
|
62
|
+
assertValid();
|
|
63
|
+
try { await this.reload(); } catch (e) { assertValid(); this.host.notify(`Name/responsibility synchronization failed: ${String(e)}`); }
|
|
64
|
+
assertValid();
|
|
47
65
|
this.host.notify(`Intercom Coordinator ready at 127.0.0.1:${this.endpoint.port}; waiting for user input.`);
|
|
48
66
|
} else {
|
|
49
|
-
if (me) { try { await this.reload(); } catch (e) { this.host.notify(`Name/responsibility synchronization failed: ${String(e)}`); } }
|
|
67
|
+
if (me) { try { await this.reload(); } catch (e) { assertValid(); this.host.notify(`Name/responsibility synchronization failed: ${String(e)}`); } }
|
|
68
|
+
assertValid();
|
|
50
69
|
try {
|
|
51
70
|
if (me) await this.report();
|
|
52
|
-
else
|
|
53
|
-
|
|
71
|
+
else {
|
|
72
|
+
const projectDirectory = await directory(this.store.root, this.host.cwd);
|
|
73
|
+
assertValid();
|
|
74
|
+
await this.transmit(coordinator(config).name, 'registration', { port: this.endpoint.port, projectDirectory });
|
|
75
|
+
}
|
|
76
|
+
} catch (e) { assertValid(); this.host.notify(`Coordinator unreachable/registration failed; worker remains reachable: ${String(e)}. No retry. Anonymous registration repeat is an unresolved contract; use explicit Pi /reload to restart this extension, not report_status.`); }
|
|
77
|
+
assertValid();
|
|
54
78
|
this.host.notify(`Intercom worker ready at 127.0.0.1:${this.endpoint.port}; waiting for explicit work.`);
|
|
55
79
|
}
|
|
56
|
-
} catch (e) { await this.close(); throw e; }
|
|
80
|
+
} catch (e) { if (generation === this.generation) await this.close(); throw e; }
|
|
57
81
|
}
|
|
58
82
|
async close(): Promise<void> {
|
|
59
|
-
this.active = false;
|
|
83
|
+
this.active = false; this.generation++;
|
|
60
84
|
const endpoint = this.endpoint; this.endpoint = undefined;
|
|
61
|
-
if (endpoint) await endpoint.close();
|
|
62
85
|
this.responsibility = undefined;
|
|
86
|
+
if (endpoint) await endpoint.close();
|
|
63
87
|
}
|
|
64
88
|
async state(): Promise<{ id: string; config: Config; me?: Agent }> {
|
|
89
|
+
const assertValid = this.validity();
|
|
65
90
|
const id = this.id(), config = await this.store.read();
|
|
66
|
-
|
|
91
|
+
assertValid();
|
|
67
92
|
return { id, config, me: config.agents.find(a => a.sessionId === id) };
|
|
68
93
|
}
|
|
69
94
|
async reload(): Promise<void> {
|
|
95
|
+
const assertValid = this.validity();
|
|
70
96
|
const { me } = await this.state();
|
|
97
|
+
assertValid();
|
|
71
98
|
if (!me) fail('worker not configured; configure_worker must run before reload_worker');
|
|
72
99
|
this.responsibility = { ...me };
|
|
73
100
|
await this.host.setName(me.name);
|
|
101
|
+
assertValid();
|
|
74
102
|
this.host.notify(`Intercom responsibility loaded for ${me.name}; no work turn started.`);
|
|
75
103
|
}
|
|
76
104
|
async transmit(to: string, kind: Kind, payload: Record<string, unknown> = {}): Promise<void> {
|
|
105
|
+
const assertValid = this.validity();
|
|
77
106
|
const { id, config } = await this.state();
|
|
107
|
+
assertValid();
|
|
78
108
|
const target = named(config, to);
|
|
79
109
|
await (this.options.send ?? send)(target.port, { version: 1, from: id, to: target.sessionId, kind, payload });
|
|
80
110
|
}
|
|
81
111
|
async report(): Promise<void> {
|
|
112
|
+
const assertValid = this.validity();
|
|
82
113
|
const { config, me } = await this.state();
|
|
114
|
+
assertValid();
|
|
83
115
|
if (me?.coordinator) fail('report_status is worker-only');
|
|
84
116
|
if (!this.endpoint) fail('endpoint unavailable');
|
|
85
117
|
await this.transmit(coordinator(config).name, 'status', { port: this.endpoint.port, busy: this.host.busy() });
|
|
86
118
|
}
|
|
87
119
|
async receive(message: Envelope): Promise<void> {
|
|
120
|
+
const assertValid = this.validity();
|
|
88
121
|
const { id, config, me } = await this.state();
|
|
122
|
+
assertValid();
|
|
89
123
|
if (message.to !== id) fail('recipient session ID mismatch (stale endpoint)');
|
|
90
124
|
const sender = config.agents.find(a => a.sessionId === message.from);
|
|
91
125
|
const busy = this.host.busy();
|
|
92
126
|
const notify = (purpose: string, content: string) => {
|
|
93
|
-
|
|
127
|
+
assertValid();
|
|
94
128
|
this.host.deliver(`[Intercom ${purpose} from ${sender?.name ?? 'unconfigured worker'} (${message.from})]\n${content}`, busy);
|
|
95
129
|
};
|
|
96
130
|
if (message.kind === 'registration' || message.kind === 'status') {
|
|
@@ -104,7 +138,7 @@ export class Intercom {
|
|
|
104
138
|
if (sender) await this.store.update(id, c => {
|
|
105
139
|
const entry = c.agents.find(a => a.sessionId === message.from && !a.coordinator);
|
|
106
140
|
if (entry) entry.port = message.payload.port as number;
|
|
107
|
-
});
|
|
141
|
+
}, assertValid);
|
|
108
142
|
notify('status', JSON.stringify({ sessionId: message.from, port: message.payload.port, busy: message.payload.busy, configured: !!sender }));
|
|
109
143
|
}
|
|
110
144
|
return;
|
|
@@ -120,13 +154,21 @@ export class Intercom {
|
|
|
120
154
|
if (message.kind === 'reload') { await this.reload(); return; }
|
|
121
155
|
if (message.kind === 'request_status') {
|
|
122
156
|
// Independent one-way report, never a synchronous status in the HTTP response.
|
|
123
|
-
setImmediate(() => {
|
|
157
|
+
setImmediate(() => {
|
|
158
|
+
try { assertValid(); } catch { return; } // Closed/replaced requests cannot report for a new lifecycle.
|
|
159
|
+
void this.report().catch(e => {
|
|
160
|
+
try { assertValid(); } catch { return; }
|
|
161
|
+
this.host.notify(`Status report failed: ${String(e)}`);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
124
164
|
return;
|
|
125
165
|
}
|
|
126
166
|
fail('unsupported control');
|
|
127
167
|
}
|
|
128
168
|
async tool(operation: string, args: Record<string, unknown>): Promise<unknown> {
|
|
169
|
+
const assertValid = this.validity();
|
|
129
170
|
const { id, config, me } = await this.state();
|
|
171
|
+
assertValid();
|
|
130
172
|
if (operation === 'list') {
|
|
131
173
|
if (!me) fail('anonymous intercom_list permissions unresolved (TODO); no coordinator privileges');
|
|
132
174
|
return config;
|
|
@@ -139,14 +181,15 @@ export class Intercom {
|
|
|
139
181
|
requireCoordinator(config, id);
|
|
140
182
|
if (operation === 'stop_worker' || operation === 'close_worker') fail(UNSUPPORTED_CANCELLATION);
|
|
141
183
|
if (operation === 'configure_worker') {
|
|
142
|
-
await this.store.configure(id, args as unknown as Omit<Agent, 'coordinator'
|
|
184
|
+
await this.store.configure(id, args as unknown as Omit<Agent, 'coordinator'>, assertValid);
|
|
143
185
|
return { written: true, reloaded: false };
|
|
144
186
|
}
|
|
145
187
|
if (operation === 'set_multiplexer') {
|
|
146
|
-
await this.store.update(id, c => { c.multiplexer = args.multiplexer as Config['multiplexer']; }); return { written: true };
|
|
188
|
+
await this.store.update(id, c => { c.multiplexer = args.multiplexer as Config['multiplexer']; }, assertValid); return { written: true };
|
|
147
189
|
}
|
|
148
190
|
if (operation === 'create_worker') {
|
|
149
191
|
const relative = await directory(this.store.root, (args.projectDirectory as string | undefined) ?? '.');
|
|
192
|
+
assertValid();
|
|
150
193
|
return this.options.launch({ multiplexer: config.multiplexer, cwd: path.resolve(this.store.root, relative) });
|
|
151
194
|
}
|
|
152
195
|
const target = named(config, args.to as string);
|
|
@@ -157,11 +200,12 @@ export class Intercom {
|
|
|
157
200
|
}
|
|
158
201
|
if (operation === 'resume_worker') {
|
|
159
202
|
const relative = await directory(this.store.root, target.projectDirectory);
|
|
203
|
+
assertValid();
|
|
160
204
|
return this.options.launch({ multiplexer: config.multiplexer, cwd: path.resolve(this.store.root, relative), sessionId: target.sessionId });
|
|
161
205
|
}
|
|
162
206
|
if (operation === 'remove_worker') {
|
|
163
207
|
// No live process claim: the caller must explicitly ensure closure first.
|
|
164
|
-
await this.store.update(id, c => { c.agents = c.agents.filter(a => a.sessionId !== target.sessionId); });
|
|
208
|
+
await this.store.update(id, c => { c.agents = c.agents.filter(a => a.sessionId !== target.sessionId); }, assertValid);
|
|
165
209
|
return { removed: true, processStopped: false, sessionDeleted: false };
|
|
166
210
|
}
|
|
167
211
|
fail(`unknown operation ${operation}`);
|