@selesai/code 0.13.3 → 0.13.4
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/CHANGELOG.md +11 -0
- package/dist/defaults/models.json +17 -1
- package/dist/extensions/cost-reconcile.test.ts +24 -5
- package/dist/extensions/cost-reconcile.ts +14 -7
- package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
- package/dist/extensions/pi-intercom/README.md +4 -5
- package/dist/extensions/pi-intercom/config.test.ts +3 -31
- package/dist/extensions/pi-intercom/config.ts +0 -15
- package/dist/extensions/pi-intercom/index.ts +9 -46
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +49 -57
- package/dist/extensions/pi-intercom/package.json +1 -1
- package/dist/extensions/pi-intercom/reply-tracker.test.ts +20 -0
- package/dist/extensions/pi-intercom/reply-tracker.ts +8 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +27 -0
- package/dist/extensions/pi-subagents/docs/tool-reference.md +4 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +2 -2
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +48 -243
- package/dist/extensions/pi-subagents/skills/council-mode/references/pass-contracts.md +150 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +87 -37
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +29 -233
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +49 -8
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +34 -27
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +157 -28
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +1 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +45 -2
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +3 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +13 -2
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +5 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +10 -2
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +11 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +98 -1
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +138 -4
- package/dist/extensions/pi-subagents/src/runs/shared/background-process-options.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +52 -3
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +47 -1
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +45 -18
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +21 -2
- package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +15 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +34 -1
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +11 -3
- package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +31 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +597 -112
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +40 -1
- package/dist/extensions/pi-subagents/src/workflows/host-command.ts +6 -1
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +53 -2
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +55 -6
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +111 -1
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +206 -38
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +522 -31
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +123 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +57 -1
- package/dist/extensions/pi-subagents/test/unit/background-process-options.test.ts +17 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +44 -4
- package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +0 -1
- package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +20 -3
- package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +93 -17
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +103 -26
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +58 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +22 -2
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +13 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +54 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +48 -1
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +21 -6
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +11 -0
- package/dist/extensions/pi-web-agent/CHANGELOG.md +410 -0
- package/dist/extensions/pi-web-agent/README.md +131 -0
- package/dist/extensions/pi-web-agent/package.json +4 -2
- package/dist/extensions/pi-web-agent/src/backends/config.ts +62 -5
- package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +96 -6
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +183 -47
- package/dist/extensions/pi-web-agent/src/extension.ts +62 -25
- package/dist/extensions/pi-web-agent/src/extract/readability.ts +19 -11
- package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +5 -4
- package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +2 -25
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +5 -2
- package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +2 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +72 -7
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +8 -1
- package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +28 -3
- package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +4 -0
- package/dist/extensions/pi-web-agent/src/orchestration/url.ts +35 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +18 -6
- package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +14 -2
- package/dist/extensions/pi-web-agent/src/readers/github-reader.ts +150 -0
- package/dist/extensions/pi-web-agent/src/readers/limits.ts +3 -0
- package/dist/extensions/pi-web-agent/src/readers/pdf-reader.ts +87 -0
- package/dist/extensions/pi-web-agent/src/readers/resolver.ts +25 -0
- package/dist/extensions/pi-web-agent/src/readers/types.ts +11 -0
- package/dist/extensions/pi-web-agent/src/readers/youtube-reader.ts +79 -0
- package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +32 -5
- package/dist/extensions/pi-web-agent/src/search/exa.ts +109 -0
- package/dist/extensions/pi-web-agent/src/search/fanout.ts +154 -0
- package/dist/extensions/pi-web-agent/src/search/tavily.ts +113 -0
- package/dist/extensions/pi-web-agent/src/search/youcom.ts +109 -0
- package/dist/extensions/pi-web-agent/src/tools/web-search.ts +22 -9
- package/dist/extensions/pi-web-agent/src/types.ts +19 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.4] - 2026-08-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Bundled `pi-subagents` 0.60.0.** `subagent({ action: "list", capabilities: true })` returns a compact prompt-free capability catalog (`details.catalog`: agent name, description, source, runner, tools/model/execution/output/extensions snapshot, executable and restriction status), and `list` results stay human text. Forked children now stamp an OpenAI-compatible `prompt_cache_key` (`pi-fork:<sha256>`) on provider requests for prompt-cache reuse; timeout recovery evidence (report status, changed tracked files, dirty-worktree classification) is projected into async status and `subagent_wait` completions; `runs.lanes` lane plans (stages, phase, label, structured output) are emitted and rendered as workflow-graph stages; `runs.host` rejects per-step `cwd` with a hint; watchdog repo signatures skip untracked scans for home-root and entry-less repos; single-output snapshots propagate non-ENOENT stat errors; background processes use `windowsHide` and detached-only-on-unix.
|
|
9
|
+
- **Bundled `pi-web-agent` 1.10.0.** Search backends: You.com (`YDC_API_KEY`), Exa (`EXA_API_KEY`), Tavily (`TAVILY_API_KEY`) in addition to DuckDuckGo, SearXNG and Brave; search fanout across configured providers with dedupe/agreement ranking (`backends.search.fanout` with `off`/`on`/`auto` modes and provider selection); keyless Tavily fallback when DuckDuckGo is bot-blocked (disable with `PI_WEB_AGENT_DISABLE_KEYLESS_FALLBACK=1`); DuckDuckGo hardened against bot-walls (browser headers + one retry); GitHub/PDF/YouTube direct readers wired into the fetch path; canonical URL normalization (tracking-param stripping); the model tool result now carries the full synthesized findings/sources/caveats regardless of terminal presentation.
|
|
10
|
+
- **GLM-5.3 models in the default catalog.** Added `glm-5.3` (GLM-5.3) to the bundled `tokenin` provider and upgraded `glm-5.3-flash` to text+image input with a full `thinkingLevelMap`.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Cost reconciliation keys every streamed response id.** Streamed bodies repeat the response id across chunks and carry tool-call ids (`call_*`), so the fetch wrapper now keys the billed cost by every id found and `message_end` consumes only the one matching the finalized message's `responseId`; a duplicate capture (retry) is marked ambiguous instead of assigning either bill.
|
|
14
|
+
- **Bundled `pi-intercom` 0.12.1.** During a turn triggered by an inbound ask, a non-`reply` `send` to a different target is refused so guessed parent/root CWD cannot receive an accidental reply; the `toolVisibility` setting and `after-first-use` reveal path were removed (the generic `intercom` tool stays in the active tool set for prompt-cache stability; obsolete `toolVisibility` config keys are ignored).
|
|
15
|
+
|
|
5
16
|
## [0.13.3] - 2026-08-31
|
|
6
17
|
|
|
7
18
|
### Added
|
|
@@ -224,7 +224,23 @@
|
|
|
224
224
|
"id": "glm-5.3-flash",
|
|
225
225
|
"name": "GLM-5.3 Flash",
|
|
226
226
|
"reasoning": true,
|
|
227
|
-
"input": ["text"],
|
|
227
|
+
"input": ["text", "image"],
|
|
228
|
+
"contextWindow": 1000000,
|
|
229
|
+
"maxTokens": 64000,
|
|
230
|
+
"thinkingLevelMap": {
|
|
231
|
+
"minimal": null,
|
|
232
|
+
"low": "low",
|
|
233
|
+
"medium": null,
|
|
234
|
+
"high": "high",
|
|
235
|
+
"xhigh": null,
|
|
236
|
+
"max": "max"
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"id": "glm-5.3",
|
|
241
|
+
"name": "GLM-5.3",
|
|
242
|
+
"reasoning": true,
|
|
243
|
+
"input": ["text", "image"],
|
|
228
244
|
"contextWindow": 1000000,
|
|
229
245
|
"maxTokens": 64000,
|
|
230
246
|
"thinkingLevelMap": {
|
|
@@ -180,17 +180,36 @@ describe("live assistant usage reconciliation", () => {
|
|
|
180
180
|
expect(result.message.usage.cost.total).toBe(0);
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
-
it("
|
|
184
|
-
const responseId = "live-
|
|
185
|
-
|
|
186
|
-
`data: {"id":"${responseId}","
|
|
187
|
-
|
|
183
|
+
it("reconciles a tool-call stream: keys cost by every id, message_end consumes the matching one", async () => {
|
|
184
|
+
const responseId = "live-toolcall-3";
|
|
185
|
+
const body =
|
|
186
|
+
`data: {"id":"${responseId}","choices":[{"delta":{"tool_calls":[{"id":"call_abc123","function":{"name":"bash","arguments":""}}]}}]}\n\n` +
|
|
187
|
+
`data: {"id":"${responseId}","usage":{"cost":0.0123}}\n\n` +
|
|
188
|
+
"data: [DONE]\n\n";
|
|
189
|
+
installFetch(body);
|
|
188
190
|
const { appendEntry, handlers } = createHarness();
|
|
189
191
|
await startSession(handlers);
|
|
190
192
|
|
|
191
193
|
const response = await globalThis.fetch("https://gateway.example/v1/chat/completions");
|
|
192
194
|
await response.text();
|
|
193
195
|
const message = assistantMessage(responseId);
|
|
196
|
+
const result = await handlers.get("message_end")!({ message }, {});
|
|
197
|
+
expect(result.message.usage.cost.total).toBe(0.0123);
|
|
198
|
+
expect(appendEntry).toHaveBeenCalledWith(
|
|
199
|
+
"cost-reconcile",
|
|
200
|
+
expect.objectContaining({ provider: "openrouter", responseId, cost: 0.0123, source: "payload" }),
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("does not apply a captured cost to a message whose id was not in the body", async () => {
|
|
205
|
+
const body = `data: {"id":"other-response","usage":{"cost":0.0123}}\n\ndata: [DONE]\n\n`;
|
|
206
|
+
installFetch(body);
|
|
207
|
+
const { appendEntry, handlers } = createHarness();
|
|
208
|
+
await startSession(handlers);
|
|
209
|
+
|
|
210
|
+
const response = await globalThis.fetch("https://gateway.example/v1/chat/completions");
|
|
211
|
+
await response.text();
|
|
212
|
+
const message = assistantMessage("unrelated-message");
|
|
194
213
|
expect(await handlers.get("message_end")!({ message }, {})).toBeUndefined();
|
|
195
214
|
expect(message.usage.cost.total).toBe(0.75);
|
|
196
215
|
expect(appendEntry).not.toHaveBeenCalled();
|
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
* finishes capture before the provider SDK finalizes its assistant message, so
|
|
17
17
|
* `message_end` can replace `usage.cost.total` before session persistence.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Streamed bodies repeat the response id across chunks and also carry
|
|
20
|
+
* tool-call ids (`call_*`), so the billed cost is keyed by every id found and
|
|
21
|
+
* `message_end` consumes only the one matching the finalized message's
|
|
22
|
+
* `responseId`. Providers whose payloads carry no cost retain their rate-card
|
|
23
|
+
* estimate. Reconciled entries remain persisted for zentui and existing-session
|
|
21
24
|
* compatibility.
|
|
22
25
|
*/
|
|
23
26
|
|
|
@@ -146,11 +149,15 @@ export default function costReconcileExtension(pi: ExtensionAPI): void {
|
|
|
146
149
|
// LiteLLM's header is the amount the gateway billed; prefer it over any
|
|
147
150
|
// cost the upstream payload reports.
|
|
148
151
|
const effectiveCost = headerCost ?? cost;
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
if (effectiveCost === undefined || ids.length === 0) return;
|
|
153
|
+
// A streamed response repeats its own id across chunks while tool-call
|
|
154
|
+
// ids (`call_*`) appear once, so a body legitimately carries several ids.
|
|
155
|
+
// Key the billed cost by every id; message_end consumes only the one
|
|
156
|
+
// matching the finalized message's responseId. A duplicate capture of the
|
|
157
|
+
// same id (retry) is marked ambiguous rather than assigning either bill.
|
|
158
|
+
for (const responseId of ids) {
|
|
159
|
+
processCaptured.set(responseId, processCaptured.has(responseId) ? null : effectiveCost);
|
|
160
|
+
}
|
|
154
161
|
if (processCaptured.size > 500) {
|
|
155
162
|
for (const key of [...processCaptured.keys()].slice(0, processCaptured.size - 500))
|
|
156
163
|
processCaptured.delete(key);
|
|
@@ -4,6 +4,19 @@ All notable changes to the `pi-intercom` extension will be documented in this fi
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.12.1] - 2026-08-29
|
|
8
|
+
|
|
9
|
+
### Highlights
|
|
10
|
+
- Replies to inbound asks are now harder to send to the wrong local session by mistake.
|
|
11
|
+
- The `intercom` tool now stays in the active tool set, which avoids a late-session prompt-cache reset when intercom first becomes useful.
|
|
12
|
+
- Existing configs that still mention `toolVisibility` keep loading; the old setting is simply ignored.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Refuse non-reply `send` calls to a different target during a turn triggered by an inbound ask, preventing CWD hierarchy or roster guesses from misdirecting replies. Thanks to [@yceachan](https://github.com/yceachan) for issue #117.
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
- Removed `toolVisibility` and the `after-first-use` reveal path. The generic `intercom` schema and prompt snippet now stay stable for provider prompt caches, and existing `toolVisibility` config keys are ignored. Thanks to [@XWIlluDelu](https://github.com/XWIlluDelu) for issue #118.
|
|
19
|
+
|
|
7
20
|
## [0.12.0] - 2026-08-22
|
|
8
21
|
|
|
9
22
|
### Highlights
|
|
@@ -218,7 +218,7 @@ This matters because the agent receiving the message doesn't need to reconstruct
|
|
|
218
218
|
|
|
219
219
|
### `send` vs `ask`
|
|
220
220
|
|
|
221
|
-
`send` is fire-and-forget — the tool returns immediately after delivery. When the destination has exactly one pending inbound ask, `send` infers that it is the answer, attaches the ask's `replyTo`, and reports `Reply sent to <target> (inferred from pending ask)`. With zero or multiple matching asks, it remains an ordinary unthreaded send. An inferred answer still uses the `confirmSend` dialog when configured; only a caller-supplied `replyTo` skips confirmation.
|
|
221
|
+
`send` is fire-and-forget — the tool returns immediately after delivery. When the destination has exactly one pending inbound ask, `send` infers that it is the answer, attaches the ask's `replyTo`, and reports `Reply sent to <target> (inferred from pending ask)`. During a turn triggered by an inbound ask, `send` refuses a different non-reply target instead of treating CWD, roster position, or project hierarchy as reply authority. With zero or multiple matching asks, it remains an ordinary unthreaded send. An inferred answer still uses the `confirmSend` dialog when configured; only a caller-supplied `replyTo` skips confirmation.
|
|
222
222
|
|
|
223
223
|
`ask` requires a currently connected recipient, then blocks until it responds (10-minute timeout by default; set `PI_INTERCOM_ASK_TIMEOUT_MS` to a positive millisecond value to change it). If the target is disconnected, `ask` fails immediately; use `send` when queued, non-blocking mailbox delivery is appropriate. The reply comes back as the tool result, so the agent continues in the same turn with full context. No confirmation dialog — if you're asking and waiting, the intent is clear.
|
|
224
224
|
|
|
@@ -366,7 +366,7 @@ Only registered in sessions where `pi-subagents` supplied the required child bri
|
|
|
366
366
|
|
|
367
367
|
**`list`** — Returns the current session plus other active intercom-connected sessions with name, short ID, working directory, model, and live status. Status is derived automatically from Selesai lifecycle events: `idle`, `thinking`, or `tool:<name>`.
|
|
368
368
|
|
|
369
|
-
**`send`** — Sends a message to the specified session and returns immediately after delivery. If the destination has exactly one pending inbound ask, `send` infers the message is its answer and returns `Reply sent to <target> (inferred from pending ask)
|
|
369
|
+
**`send`** — Sends a message to the specified session and returns immediately after delivery. If the destination has exactly one pending inbound ask, `send` infers the message is its answer and returns `Reply sent to <target> (inferred from pending ask)`. During a turn triggered by an inbound ask, a non-reply `send` to a different target is rejected so a guessed parent/root CWD cannot receive an accidental reply. Zero or multiple pending-ask matches remain unthreaded sends outside the active ask turn. Set `confirmSend: true` to confirm ordinary and inferred sends. A caller-supplied `replyTo` skips confirmation. `to` alone resolves globally across all live sessions. `cwd` alone targets the sole live peer in that directory. `to` plus `cwd` requires that peer to be in the directory. With `openProjectPaneIfMissing: true`, pi-intercom opens a visible Herdr project pane, starts Selesai there, waits for that session to register, then delivers the message through normal intercom routing.
|
|
370
370
|
|
|
371
371
|
**`ask`** — Requires a currently connected recipient, sends a message, and waits for the recipient to reply (10-minute timeout by default; configurable with `PI_INTERCOM_ASK_TIMEOUT_MS`). A disconnected target fails immediately rather than queueing a blocking request. The reply is returned as the tool result. No confirmation dialog. Only one pending `ask` is allowed per session at a time. Use this when the agent needs the answer to continue working. The same `to`, `cwd`, and `openProjectPaneIfMissing` targeting rules apply.
|
|
372
372
|
|
|
@@ -397,7 +397,6 @@ Create `~/.selesai/agent/intercom/config.json`:
|
|
|
397
397
|
"brokerArgs": ["--no-install", "tsx"],
|
|
398
398
|
"confirmSend": false,
|
|
399
399
|
"inboundTrigger": "always",
|
|
400
|
-
"toolVisibility": "always",
|
|
401
400
|
"enabled": true,
|
|
402
401
|
"replyHint": true,
|
|
403
402
|
"status": "researching"
|
|
@@ -410,12 +409,12 @@ Create `~/.selesai/agent/intercom/config.json`:
|
|
|
410
409
|
| `brokerArgs` | `["--no-install", "tsx"]` | Advanced trusted arguments passed to custom `brokerCommand` before the broker script path |
|
|
411
410
|
| `confirmSend` | false | Show a confirmation dialog before ordinary or inferred sends from an interactive session with UI; caller-supplied `replyTo` skips it |
|
|
412
411
|
| `inboundTrigger` | `"always"` | Auto-trigger policy for inbound broker messages: `"always"`, `"replies"`, or `"never"`. Local in-process subagent relay events still trigger the addressed session. |
|
|
413
|
-
| `toolVisibility` | `"always"` | When the generic `intercom` tool enters the active model tool set: `"always"` or `"after-first-use"`. Lazy visibility reveals it after an inbound broker message, a successful overlay send, or loading the bundled skill. It does not hide the child-only `contact_supervisor` tool. |
|
|
414
412
|
| `enabled` | true | Enable/disable intercom entirely |
|
|
415
413
|
| `replyHint` | true | Include reply instruction in incoming messages |
|
|
416
414
|
| `status` | — | Optional custom status suffix shown after the automatic lifecycle status, for example `thinking · researching` |
|
|
417
415
|
|
|
418
416
|
If `config.json` cannot be parsed or contains an invalid value, pi-intercom logs the error and fails closed for inbound broker auto-triggering by using `inboundTrigger: "never"` until the config is fixed.
|
|
417
|
+
Obsolete `toolVisibility` values are ignored; the generic `intercom` tool remains stable in the active tool set for prompt-cache friendliness.
|
|
419
418
|
|
|
420
419
|
Custom broker commands are trusted local configuration: anyone who can edit this config can choose the executable used for future broker auto-spawns. For example, if you have Bun installed and want it to start the broker directly, use:
|
|
421
420
|
|
|
@@ -542,7 +541,7 @@ Runtime files live at `~/.selesai/agent/intercom/` by default, or `$SELESAI_CODI
|
|
|
542
541
|
- `broker.port.json` — Dynamic localhost TCP endpoint, only when Windows TCP transport is explicitly enabled
|
|
543
542
|
- `config.json` — User configuration
|
|
544
543
|
|
|
545
|
-
Supported `config.json` keys include `stableId` for restart-stable addressing, `status` for a custom status suffix, `inboundTrigger` (`always`, `replies`, or `never`), `
|
|
544
|
+
Supported `config.json` keys include `stableId` for restart-stable addressing, `status` for a custom status suffix, `inboundTrigger` (`always`, `replies`, or `never`), `replyHint`, `confirmSend`, and advanced broker launch overrides.
|
|
546
545
|
|
|
547
546
|
## Design Decisions
|
|
548
547
|
|
|
@@ -60,24 +60,13 @@ test("loadConfig accepts inboundTrigger replies policy", async () => {
|
|
|
60
60
|
}
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
test("loadConfig
|
|
64
|
-
const root = mkdtempSync(join(tmpdir(), "pi-intercom-config-"));
|
|
65
|
-
try {
|
|
66
|
-
await withAgentDir(root, () => {
|
|
67
|
-
assert.equal(loadConfig().toolVisibility, "always");
|
|
68
|
-
});
|
|
69
|
-
} finally {
|
|
70
|
-
rmSync(root, { recursive: true, force: true });
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("loadConfig accepts lazy intercom tool visibility", async () => {
|
|
63
|
+
test("loadConfig ignores obsolete toolVisibility values", async () => {
|
|
75
64
|
const root = mkdtempSync(join(tmpdir(), "pi-intercom-config-"));
|
|
76
65
|
try {
|
|
77
66
|
mkdirSync(join(root, "intercom"), { recursive: true });
|
|
78
|
-
writeFileSync(join(root, "intercom", "config.json"), JSON.stringify({ toolVisibility: "
|
|
67
|
+
writeFileSync(join(root, "intercom", "config.json"), JSON.stringify({ toolVisibility: "lazy", replyHint: false }));
|
|
79
68
|
await withAgentDir(root, () => {
|
|
80
|
-
assert.equal(loadConfig().
|
|
69
|
+
assert.equal(loadConfig().replyHint, false);
|
|
81
70
|
});
|
|
82
71
|
} finally {
|
|
83
72
|
rmSync(root, { recursive: true, force: true });
|
|
@@ -97,23 +86,6 @@ test("loadConfig accepts a restart-stable intercom id", async () => {
|
|
|
97
86
|
}
|
|
98
87
|
});
|
|
99
88
|
|
|
100
|
-
test("loadConfig rejects invalid toolVisibility values", async () => {
|
|
101
|
-
const root = mkdtempSync(join(tmpdir(), "pi-intercom-config-"));
|
|
102
|
-
try {
|
|
103
|
-
mkdirSync(join(root, "intercom"), { recursive: true });
|
|
104
|
-
writeFileSync(join(root, "intercom", "config.json"), JSON.stringify({ toolVisibility: "lazy" }));
|
|
105
|
-
|
|
106
|
-
await withAgentDir(root, () => {
|
|
107
|
-
assert.throws(
|
|
108
|
-
() => loadConfig(),
|
|
109
|
-
/Failed to load intercom config.*"toolVisibility" must be "always" or "after-first-use"/,
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
} finally {
|
|
113
|
-
rmSync(root, { recursive: true, force: true });
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
89
|
test("loadConfig rejects invalid inboundTrigger values", async () => {
|
|
118
90
|
const root = mkdtempSync(join(tmpdir(), "pi-intercom-config-"));
|
|
119
91
|
try {
|
|
@@ -24,7 +24,6 @@ export function getIntercomScopeId(env: NodeJS.ProcessEnv = process.env): string
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export type InboundTriggerPolicy = "always" | "replies" | "never";
|
|
27
|
-
export type IntercomToolVisibility = "always" | "after-first-use";
|
|
28
27
|
|
|
29
28
|
export interface IntercomConfig {
|
|
30
29
|
/** Broker command used to spawn the broker process (e.g. "npx" or "bun") */
|
|
@@ -39,9 +38,6 @@ export interface IntercomConfig {
|
|
|
39
38
|
/** Controls whether inbound broker messages may automatically trigger a model turn */
|
|
40
39
|
inboundTrigger: InboundTriggerPolicy;
|
|
41
40
|
|
|
42
|
-
/** Controls when the intercom tool enters the active model tool set */
|
|
43
|
-
toolVisibility: IntercomToolVisibility;
|
|
44
|
-
|
|
45
41
|
/** Optional custom status suffix shown after automatic lifecycle status */
|
|
46
42
|
status?: string;
|
|
47
43
|
|
|
@@ -64,7 +60,6 @@ const defaults: IntercomConfig = {
|
|
|
64
60
|
brokerArgs: ["--no-install", "tsx"],
|
|
65
61
|
confirmSend: false,
|
|
66
62
|
inboundTrigger: "always",
|
|
67
|
-
toolVisibility: "always",
|
|
68
63
|
enabled: true,
|
|
69
64
|
replyHint: true,
|
|
70
65
|
};
|
|
@@ -135,16 +130,6 @@ export function loadConfig(): IntercomConfig {
|
|
|
135
130
|
config.inboundTrigger = parsedConfig.inboundTrigger;
|
|
136
131
|
}
|
|
137
132
|
|
|
138
|
-
if (Object.hasOwn(parsedConfig, "toolVisibility")) {
|
|
139
|
-
if (
|
|
140
|
-
parsedConfig.toolVisibility !== "always"
|
|
141
|
-
&& parsedConfig.toolVisibility !== "after-first-use"
|
|
142
|
-
) {
|
|
143
|
-
throw new Error(`"toolVisibility" must be "always" or "after-first-use"`);
|
|
144
|
-
}
|
|
145
|
-
config.toolVisibility = parsedConfig.toolVisibility;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
133
|
if (Object.hasOwn(parsedConfig, "replyHint")) {
|
|
149
134
|
if (typeof parsedConfig.replyHint !== "boolean") {
|
|
150
135
|
throw new Error(`"replyHint" must be a boolean`);
|
|
@@ -27,15 +27,12 @@ import {
|
|
|
27
27
|
type IntercomOutboxResultV1,
|
|
28
28
|
} from "./extension-api.ts";
|
|
29
29
|
import { ReplyTracker } from "./reply-tracker.ts";
|
|
30
|
-
import { realpathSync } from "node:fs";
|
|
31
30
|
import { resolve as resolvePath } from "node:path";
|
|
32
|
-
import { fileURLToPath } from "node:url";
|
|
33
31
|
import { sameCwd } from "./cwd.ts";
|
|
34
32
|
import { formatContextUsage } from "./format-context.ts";
|
|
35
33
|
import { openProjectPane, resolveTargetInCwd, waitForProjectSession, type ProjectPaneLaunch } from "./project-agent.ts";
|
|
36
34
|
|
|
37
35
|
const INTERCOM_TOOL_NAME = "intercom";
|
|
38
|
-
const INTERCOM_SKILL_PATH = realpathSync(fileURLToPath(new URL("./skills/pi-intercom/SKILL.md", import.meta.url)));
|
|
39
36
|
const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
|
|
40
37
|
const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
|
|
41
38
|
const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
|
|
@@ -116,18 +113,6 @@ function getErrorMessage(error: unknown): string {
|
|
|
116
113
|
return error instanceof Error ? error.message : String(error);
|
|
117
114
|
}
|
|
118
115
|
|
|
119
|
-
function isIntercomSkillRead(input: unknown, cwd: string): boolean {
|
|
120
|
-
if (!input || typeof input !== "object") return false;
|
|
121
|
-
const inputPath = Reflect.get(input, "path");
|
|
122
|
-
if (typeof inputPath !== "string") return false;
|
|
123
|
-
const normalizedPath = inputPath.startsWith("@") ? inputPath.slice(1) : inputPath;
|
|
124
|
-
try {
|
|
125
|
-
return realpathSync(resolvePath(cwd, normalizedPath)) === INTERCOM_SKILL_PATH;
|
|
126
|
-
} catch {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
116
|
function deliveryDetails(result: SendResult): Record<string, unknown> {
|
|
132
117
|
return {
|
|
133
118
|
messageId: result.id,
|
|
@@ -622,23 +607,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
622
607
|
let runtimeGeneration = 0;
|
|
623
608
|
let agentRunning = false;
|
|
624
609
|
const activeTools = new Map<string, string>();
|
|
625
|
-
let intercomToolHiddenByPolicy = false;
|
|
626
610
|
const replyTracker = new ReplyTracker();
|
|
627
|
-
function hideIntercomTool(): void {
|
|
628
|
-
if (config.toolVisibility !== "after-first-use") return;
|
|
629
|
-
const activeToolNames = pi.getActiveTools();
|
|
630
|
-
if (!activeToolNames.includes(INTERCOM_TOOL_NAME)) return;
|
|
631
|
-
pi.setActiveTools(activeToolNames.filter((name) => name !== INTERCOM_TOOL_NAME));
|
|
632
|
-
intercomToolHiddenByPolicy = true;
|
|
633
|
-
}
|
|
634
|
-
function activateIntercomTool(): void {
|
|
635
|
-
if (!intercomToolHiddenByPolicy) return;
|
|
636
|
-
const activeToolNames = pi.getActiveTools();
|
|
637
|
-
if (!activeToolNames.includes(INTERCOM_TOOL_NAME)) {
|
|
638
|
-
pi.setActiveTools([...activeToolNames, INTERCOM_TOOL_NAME]);
|
|
639
|
-
}
|
|
640
|
-
intercomToolHiddenByPolicy = false;
|
|
641
|
-
}
|
|
642
611
|
|
|
643
612
|
const seenInboundMessages = new Map<string, number>();
|
|
644
613
|
const latestOutboundReceipts = new Map<string, { status: MessageReceiptStatus; timestamp: number; detail?: string }>();
|
|
@@ -1221,7 +1190,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1221
1190
|
);
|
|
1222
1191
|
}
|
|
1223
1192
|
function sendIncomingBrokerMessage(entry: InboundMessageEntry, delivery: "trigger" | "steer", generation = runtimeGeneration): void {
|
|
1224
|
-
activateIntercomTool();
|
|
1225
1193
|
sendIncomingMessage(entry, delivery, generation);
|
|
1226
1194
|
}
|
|
1227
1195
|
function handleIncomingMessage(ctx: ExtensionContext, from: SessionInfo, message: Message): void {
|
|
@@ -1572,7 +1540,6 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1572
1540
|
});
|
|
1573
1541
|
}
|
|
1574
1542
|
function startSessionRuntime(ctx: ExtensionContext): void {
|
|
1575
|
-
hideIntercomTool();
|
|
1576
1543
|
const previousClient = client;
|
|
1577
1544
|
failPendingOutboxRequests(runtimeGeneration, "session_ended", "Session replaced");
|
|
1578
1545
|
shuttingDown = false;
|
|
@@ -1819,17 +1786,7 @@ export default function piIntercomExtension(pi: ExtensionAPI) {
|
|
|
1819
1786
|
return new InlineMessageComponent(details.from, details.message, theme, details.replyCommand, details.bodyText, !options.expanded);
|
|
1820
1787
|
});
|
|
1821
1788
|
|
|
1822
|
-
pi.on("
|
|
1823
|
-
if (/^\/skill: pi-intercom(?:\s|$)/u.test(event.text.trimStart())) {
|
|
1824
|
-
activateIntercomTool();
|
|
1825
|
-
}
|
|
1826
|
-
});
|
|
1827
|
-
|
|
1828
|
-
pi.on("tool_result", (event, ctx) => {
|
|
1829
|
-
if (event.toolName === "read" && event.isError !== true && isIntercomSkillRead(event.input, ctx.cwd)) {
|
|
1830
|
-
activateIntercomTool();
|
|
1831
|
-
return;
|
|
1832
|
-
}
|
|
1789
|
+
pi.on("tool_result", (event) => {
|
|
1833
1790
|
if (event.toolName !== "intercom" && event.toolName !== "contact_supervisor") {
|
|
1834
1791
|
return;
|
|
1835
1792
|
}
|
|
@@ -2181,7 +2138,6 @@ Usage:
|
|
|
2181
2138
|
}),
|
|
2182
2139
|
|
|
2183
2140
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2184
|
-
activateIntercomTool();
|
|
2185
2141
|
let connectedClient: IntercomClient;
|
|
2186
2142
|
try {
|
|
2187
2143
|
connectedClient = await ensureConnected("tool");
|
|
@@ -2350,6 +2306,14 @@ Usage:
|
|
|
2350
2306
|
details: { error: true },
|
|
2351
2307
|
};
|
|
2352
2308
|
}
|
|
2309
|
+
const activeReplyMismatch = replyTo ? null : replyTracker.findActiveReplyTargetMismatch(sendTo);
|
|
2310
|
+
if (activeReplyMismatch) {
|
|
2311
|
+
const senderLabel = activeReplyMismatch.from.name || activeReplyMismatch.from.id;
|
|
2312
|
+
return {
|
|
2313
|
+
content: [{ type: "text", text: `This turn is responding to an intercom ask from "${senderLabel}". Use intercom({ action: "reply", message: "..." }) or set replyTo: "${activeReplyMismatch.message.id}". Refusing non-reply send to "${targetDisplay}" to avoid a misdirected reply.` }],
|
|
2314
|
+
details: { error: true, replyTo: activeReplyMismatch.message.id },
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2353
2317
|
const inferredAsk = replyTo ? null : replyTracker.findUniquePendingAskFrom(sendTo);
|
|
2354
2318
|
const effectiveReplyTo = replyTo ?? inferredAsk?.message.id;
|
|
2355
2319
|
if (confirmSend && !(cwd && openProjectPaneIfMissing)) {
|
|
@@ -2761,7 +2725,6 @@ Usage:
|
|
|
2761
2725
|
).catch(() => undefined);
|
|
2762
2726
|
|
|
2763
2727
|
if (result?.sent && result.messageId && result.text && getLiveContext(ctx, overlayGeneration)) {
|
|
2764
|
-
activateIntercomTool();
|
|
2765
2728
|
pi.appendEntry("intercom_sent", {
|
|
2766
2729
|
to: selectedSession.name || selectedSession.id,
|
|
2767
2730
|
message: { text: result.text },
|
|
@@ -1672,59 +1672,7 @@ test("intercom tool result hook marks failed details as errors", async () => {
|
|
|
1672
1672
|
assert.deepEqual(okResults.filter(Boolean), []);
|
|
1673
1673
|
});
|
|
1674
1674
|
|
|
1675
|
-
test("
|
|
1676
|
-
const { default: piIntercomExtension } = await import("./index.ts");
|
|
1677
|
-
|
|
1678
|
-
await withIntercomConfig({ toolVisibility: "after-first-use" }, () => withChildOrchestratorEnv({
|
|
1679
|
-
orchestratorTarget: "orchestrator",
|
|
1680
|
-
runId: "78f659a3",
|
|
1681
|
-
agent: "worker",
|
|
1682
|
-
index: "0",
|
|
1683
|
-
}, async () => {
|
|
1684
|
-
const harness = createExtensionHarness("lazy-skill-worker", { activeTools: ["read", "bash"] });
|
|
1685
|
-
piIntercomExtension(harness.pi as never);
|
|
1686
|
-
|
|
1687
|
-
try {
|
|
1688
|
-
await harness.emitLifecycle("session_start");
|
|
1689
|
-
assert.deepEqual(harness.getActiveTools(), ["read", "bash", "contact_supervisor"]);
|
|
1690
|
-
|
|
1691
|
-
harness.pi.events.emit("subagent:control-intercom", {
|
|
1692
|
-
to: "session-child-test",
|
|
1693
|
-
message: "Local relay traffic should not reveal the generic tool.",
|
|
1694
|
-
});
|
|
1695
|
-
assert.equal(harness.sentMessages.at(-1)?.activeTools.includes("intercom"), false);
|
|
1696
|
-
assert.equal(harness.getActiveTools().includes("intercom"), false);
|
|
1697
|
-
|
|
1698
|
-
await harness.emitLifecycle("tool_result", {
|
|
1699
|
-
toolName: "read",
|
|
1700
|
-
input: { path: path.join(repoDir, "README.md") },
|
|
1701
|
-
isError: false,
|
|
1702
|
-
});
|
|
1703
|
-
await harness.emitLifecycle("tool_result", {
|
|
1704
|
-
toolName: "read",
|
|
1705
|
-
input: { path: path.join(repoDir, "skills", "pi-intercom", "SKILL.md") },
|
|
1706
|
-
isError: true,
|
|
1707
|
-
});
|
|
1708
|
-
assert.equal(harness.getActiveTools().includes("intercom"), false);
|
|
1709
|
-
|
|
1710
|
-
await harness.emitLifecycle("tool_result", {
|
|
1711
|
-
toolName: "read",
|
|
1712
|
-
input: { path: path.join(repoDir, "skills", "pi-intercom", "SKILL.md") },
|
|
1713
|
-
isError: false,
|
|
1714
|
-
});
|
|
1715
|
-
assert.deepEqual(harness.getActiveTools(), ["read", "bash", "contact_supervisor", "intercom"]);
|
|
1716
|
-
|
|
1717
|
-
await harness.emitLifecycle("session_start");
|
|
1718
|
-
assert.equal(harness.getActiveTools().includes("intercom"), false);
|
|
1719
|
-
await harness.emitLifecycle("input", { text: " /skill: pi-intercom", source: "user" });
|
|
1720
|
-
assert.equal(harness.getActiveTools().includes("intercom"), true);
|
|
1721
|
-
} finally {
|
|
1722
|
-
await harness.emitLifecycle("session_shutdown");
|
|
1723
|
-
}
|
|
1724
|
-
}));
|
|
1725
|
-
});
|
|
1726
|
-
|
|
1727
|
-
test("lazy tool visibility reveals intercom before broker injection and after an overlay send", { concurrency: false }, async () => {
|
|
1675
|
+
test("obsolete toolVisibility config never hides or reveals the intercom tool", { concurrency: false }, async () => {
|
|
1728
1676
|
const { default: piIntercomExtension } = await import("./index.ts");
|
|
1729
1677
|
|
|
1730
1678
|
await withIntercomConfig({ toolVisibility: "after-first-use" }, async () => {
|
|
@@ -1754,14 +1702,14 @@ test("lazy tool visibility reveals intercom before broker injection and after an
|
|
|
1754
1702
|
piIntercomExtension(inboundHarness.pi as never);
|
|
1755
1703
|
await overlayHarness.emitLifecycle("session_start");
|
|
1756
1704
|
await inboundHarness.emitLifecycle("session_start");
|
|
1757
|
-
assert.equal(overlayHarness.getActiveTools().includes("intercom"),
|
|
1758
|
-
assert.equal(inboundHarness.getActiveTools().includes("intercom"),
|
|
1705
|
+
assert.equal(overlayHarness.getActiveTools().includes("intercom"), true);
|
|
1706
|
+
assert.equal(inboundHarness.getActiveTools().includes("intercom"), true);
|
|
1759
1707
|
|
|
1760
1708
|
selectedSession = await waitForSessionByName(planner, "planner");
|
|
1761
1709
|
const inboundSession = await waitForSessionByName(planner, "lazy-inbound-worker");
|
|
1762
1710
|
const delivered = await planner.send(inboundSession.id, {
|
|
1763
|
-
messageId: "
|
|
1764
|
-
text: "
|
|
1711
|
+
messageId: "stable-inbound-message",
|
|
1712
|
+
text: "Keep intercom active before injecting this message.",
|
|
1765
1713
|
});
|
|
1766
1714
|
assert.equal(delivered.delivered, true);
|
|
1767
1715
|
const deadline = Date.now() + 1000;
|
|
@@ -1770,6 +1718,13 @@ test("lazy tool visibility reveals intercom before broker injection and after an
|
|
|
1770
1718
|
}
|
|
1771
1719
|
assert.equal(inboundHarness.sentMessages[0]?.activeTools.includes("intercom"), true);
|
|
1772
1720
|
|
|
1721
|
+
await inboundHarness.emitLifecycle("tool_result", {
|
|
1722
|
+
toolName: "read",
|
|
1723
|
+
input: { path: path.join(repoDir, "skills", "pi-intercom", "SKILL.md") },
|
|
1724
|
+
isError: false,
|
|
1725
|
+
});
|
|
1726
|
+
assert.equal(inboundHarness.getActiveTools().includes("intercom"), true);
|
|
1727
|
+
|
|
1773
1728
|
await overlayHarness.commands.get("intercom")!("", overlayHarness.ctx);
|
|
1774
1729
|
assert.equal(overlayStep, 2);
|
|
1775
1730
|
assert.equal(overlayHarness.getActiveTools().includes("intercom"), true);
|
|
@@ -3148,6 +3103,43 @@ test("intercom reply sends attachments", { concurrency: false }, async () => {
|
|
|
3148
3103
|
}
|
|
3149
3104
|
});
|
|
3150
3105
|
|
|
3106
|
+
test("intercom send refuses a different target during an active inbound ask turn", { concurrency: false }, async () => {
|
|
3107
|
+
const { planner, orchestrator, cleanup } = await setupClients();
|
|
3108
|
+
const { default: piIntercomExtension } = await import("./index.ts");
|
|
3109
|
+
const harness = createExtensionHarness("cwd-reply-worker");
|
|
3110
|
+
|
|
3111
|
+
try {
|
|
3112
|
+
piIntercomExtension(harness.pi as never);
|
|
3113
|
+
await harness.emitLifecycle("session_start");
|
|
3114
|
+
const worker = await waitForSessionByName(planner, "cwd-reply-worker");
|
|
3115
|
+
|
|
3116
|
+
assert.equal((await planner.send(worker.id, {
|
|
3117
|
+
messageId: "cwd-hierarchy-ask",
|
|
3118
|
+
text: "Please answer me, not the repo-root session.",
|
|
3119
|
+
expectsReply: true,
|
|
3120
|
+
})).delivered, true);
|
|
3121
|
+
const deadline = Date.now() + 1000;
|
|
3122
|
+
while (harness.sentMessages.length === 0 && Date.now() < deadline) {
|
|
3123
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
3124
|
+
}
|
|
3125
|
+
await harness.emitLifecycle("turn_start");
|
|
3126
|
+
|
|
3127
|
+
const intercomTool = harness.tools.find((tool) => tool.name === "intercom")!;
|
|
3128
|
+
const result = await intercomTool.execute("misdirected-send", {
|
|
3129
|
+
action: "send",
|
|
3130
|
+
to: "orchestrator",
|
|
3131
|
+
message: "This was meant as the ask answer.",
|
|
3132
|
+
}, new AbortController().signal, undefined, harness.ctx);
|
|
3133
|
+
|
|
3134
|
+
assert.equal(result.details?.error, true);
|
|
3135
|
+
assert.equal(result.details?.replyTo, "cwd-hierarchy-ask");
|
|
3136
|
+
assert.match(result.content[0]?.text ?? "", /Refusing non-reply send to "orchestrator"/);
|
|
3137
|
+
} finally {
|
|
3138
|
+
await harness.emitLifecycle("session_shutdown");
|
|
3139
|
+
await cleanup();
|
|
3140
|
+
}
|
|
3141
|
+
});
|
|
3142
|
+
|
|
3151
3143
|
test("intercom reply targets one of multiple pending asks by short session ID", { concurrency: false }, async () => {
|
|
3152
3144
|
const { planner, orchestrator, cleanup } = await setupClients();
|
|
3153
3145
|
const { default: piIntercomExtension } = await import("./index.ts");
|
|
@@ -93,6 +93,26 @@ test("explicit to overrides the current turn context", () => {
|
|
|
93
93
|
assert.throws(() => tracker.resolveReplyTarget({ to: "missing" }, 1003), /No pending ask from/);
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
test("active ask context flags non-reply sends to a different target", () => {
|
|
97
|
+
const tracker = new ReplyTracker();
|
|
98
|
+
const current = tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "Need a reply"), 1000);
|
|
99
|
+
tracker.queueTurnContext(current);
|
|
100
|
+
tracker.beginTurn(1001);
|
|
101
|
+
|
|
102
|
+
assert.equal(tracker.findActiveReplyTargetMismatch("planner-id", 1002), null);
|
|
103
|
+
assert.equal(tracker.findActiveReplyTargetMismatch("planner", 1002)?.message.id, "ask-1");
|
|
104
|
+
assert.equal(tracker.findActiveReplyTargetMismatch("repo-root", 1002)?.message.id, "ask-1");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("active ask context does not trust sender names as destination identity", () => {
|
|
108
|
+
const tracker = new ReplyTracker();
|
|
109
|
+
const current = tracker.recordIncomingMessage(createSession("asker-session", "root-session"), createMessage("ask-1", "Need a reply"), 1000);
|
|
110
|
+
tracker.queueTurnContext(current);
|
|
111
|
+
tracker.beginTurn(1001);
|
|
112
|
+
|
|
113
|
+
assert.equal(tracker.findActiveReplyTargetMismatch("root-session", 1002)?.message.id, "ask-1");
|
|
114
|
+
});
|
|
115
|
+
|
|
96
116
|
test("replyTo resolves the exact pending ask", () => {
|
|
97
117
|
const tracker = new ReplyTracker();
|
|
98
118
|
tracker.recordIncomingMessage(createSession("planner-id", "planner"), createMessage("ask-1", "First"), 1000);
|
|
@@ -121,6 +121,14 @@ export class ReplyTracker {
|
|
|
121
121
|
return candidates.length === 1 ? candidates[0]! : null;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
findActiveReplyTargetMismatch(to: string, now = Date.now()): IntercomContext | null {
|
|
125
|
+
this.pruneExpired(now);
|
|
126
|
+
if (!this.currentTurnContext?.message.expectsReply) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return this.currentTurnContext.from.id === to ? null : this.currentTurnContext;
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
markReplied(replyTo: string): void {
|
|
125
133
|
this.dismissPendingAsk(replyTo);
|
|
126
134
|
}
|
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.60.0] - 2026-08-31
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
- Ask for the runtime delegation catalog as clean rows: `subagent({ action: "list", capabilities: true })` returns compact, prompt-free capability rows and machine metadata.
|
|
9
|
+
- Timeout recovery is explainable: async status and `subagent_wait` completions report the missing/written report and changed tracked files for timed-out children with dirty worktrees.
|
|
10
|
+
- Forked children reuse prompt caches: fork-context launches stamp an OpenAI-compatible `prompt_cache_key` on provider requests.
|
|
11
|
+
- `runs.lanes` plans show up as workflow-graph stages in status and TUI views.
|
|
12
|
+
- Watchdog and single-output evidence are cheaper and kinder: home-root/entry-less repos skip untracked scans, and stat errors propagate instead of being swallowed.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Add `capabilities` option to management `list`: returns executable/restricted status, restriction sources, aliases, runner (pi / external-cli / external-job with capabilities), tools (ambient, mcp-direct, mutation), model (value/fallbacks/thinking), execution (defaultAsync, timeoutMs), output, and extensions snapshot per agent, plus total restricted count and capability-ceiling sources, as versioned `details.agentCapabilities` (`details.catalog`).
|
|
16
|
+
- Emit `lanePlan` metadata from `runs.lanes` (stages, phase, label, agent, output name, structured flag) and render workflow-graph stage nodes in async status and TUI views.
|
|
17
|
+
- Stamp `prompt_cache_key` (`pi-fork:<sha256>`) on supported provider requests for fork-context children via `before_provider_request`.
|
|
18
|
+
- Project timeout-recovery evidence (termination, changed files, report status, dirty-worktree reason) into async status steps and `subagent_wait` completion output.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- `runs.host` rejects per-step `cwd` with an actionable hint (use outer request cwd or a trusted `cd` in the command).
|
|
22
|
+
- Watchdog repo-change signatures skip `--untracked-files=all` for home-repo roots and repositories with no tracked entries, keeping status probes cheap and bounded.
|
|
23
|
+
- Single-output snapshots propagate non-ENOENT stat errors instead of treating every failure as "file missing".
|
|
24
|
+
- Background spawns use `windowsHide` consistently and `detached` only on non-Windows.
|
|
25
|
+
- `workflowScript` guidance documents the portability rules (no nested async helpers) and `runs.host` cwd semantics.
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- "Running" spinners in the TUI advance on wall-clock time instead of only on model activity.
|
|
29
|
+
- Noisy repeated single-step status rows are suppressed in workflow status rendering.
|
|
30
|
+
- Timeout recovery summaries no longer claim a written report when the required output is missing.
|
|
31
|
+
|
|
5
32
|
## [0.59.0] - 2026-08-28
|
|
6
33
|
|
|
7
34
|
### Highlights
|