@selesai/code 0.13.2 → 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 +20 -0
- package/dist/defaults/models.json +85 -13
- package/dist/extensions/cost-reconcile.test.ts +200 -4
- package/dist/extensions/cost-reconcile.ts +88 -102
- 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/dist/extensions/tokenin-onboarding.ts +302 -0
- package/package.json +1 -1
|
@@ -9,19 +9,19 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Extensions cannot hook the response body through the event API
|
|
11
11
|
* (`after_provider_response` exposes only status/headers), but provider SDKs
|
|
12
|
-
* fall back to `globalThis.fetch`. This extension installs a
|
|
13
|
-
* fetch once per process: LLM API responses are scanned for a
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `message_end
|
|
12
|
+
* fall back to `globalThis.fetch`. This extension installs a stream-wrapping
|
|
13
|
+
* fetch once per process: LLM API responses are scanned for a provider-reported
|
|
14
|
+
* cost (LiteLLM `x-litellm-response-cost` header, OpenRouter `usage.cost`,
|
|
15
|
+
* `total_cost`, plus object-shaped `cost.total`) and response id. The wrapper
|
|
16
|
+
* finishes capture before the provider SDK finalizes its assistant message, so
|
|
17
|
+
* `message_end` can replace `usage.cost.total` before session persistence.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
|
24
|
+
* compatibility.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import type { ExtensionAPI } from "@selesai/code";
|
|
@@ -31,11 +31,10 @@ const ENTRY_VERSION = 1 as const;
|
|
|
31
31
|
|
|
32
32
|
// Guard against pathological bodies; real usage chunks arrive well under 1MB.
|
|
33
33
|
const MAX_CAPTURE_BYTES = 4 * 1024 * 1024;
|
|
34
|
-
// Only LLM API paths are
|
|
35
|
-
//
|
|
34
|
+
// Only LLM API paths are scanned; unrelated fetches (web fetch tool, model
|
|
35
|
+
// catalog refreshes) pass through untouched.
|
|
36
36
|
const LLM_PATH_RE = /(\/chat\/completions|\/responses|\/messages\b|:streamGenerateContent)/;
|
|
37
37
|
const MAX_CAPTURED_IDS = 50;
|
|
38
|
-
const MAX_PENDING_LOOKUPS = 200;
|
|
39
38
|
const MAX_SESSION_ENTRIES = 2_000;
|
|
40
39
|
|
|
41
40
|
type ReconcileEntry = {
|
|
@@ -49,46 +48,34 @@ type ReconcileEntry = {
|
|
|
49
48
|
source: "payload";
|
|
50
49
|
};
|
|
51
50
|
|
|
52
|
-
/**
|
|
53
|
-
* Scan a captured SSE/JSON body for provider-reported cost and response ids.
|
|
54
|
-
* Exported for tests.
|
|
55
|
-
*
|
|
56
|
-
* In streams, usage lands in the final chunk, so the LAST cost match wins.
|
|
57
|
-
*/
|
|
58
51
|
type MessageInfo = {
|
|
59
52
|
provider: string;
|
|
60
53
|
model: string;
|
|
61
54
|
responseId: string;
|
|
62
55
|
};
|
|
63
56
|
|
|
64
|
-
// Process-wide capture state. The fetch patch is installed once per process
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
const processCaptured = new Map<string, number>();
|
|
68
|
-
const processWaiting = new Map<
|
|
69
|
-
string,
|
|
70
|
-
{ info: MessageInfo; write: (info: MessageInfo, cost: number) => void }
|
|
71
|
-
>();
|
|
57
|
+
// Process-wide capture state. The fetch patch is installed once per process;
|
|
58
|
+
// per-session message_end handlers consume the matching response id. A duplicate
|
|
59
|
+
// capture for the same id is marked ambiguous rather than assigning either bill.
|
|
60
|
+
const processCaptured = new Map<string, number | null>();
|
|
72
61
|
|
|
73
|
-
/**
|
|
74
|
-
* Scan a captured SSE/JSON body for provider-reported cost and response ids.
|
|
75
|
-
* Pure function; exported for tests.
|
|
76
|
-
*
|
|
77
|
-
* In streams, usage lands in the final chunk, so the LAST cost match wins.
|
|
78
|
-
*/
|
|
79
62
|
/**
|
|
80
63
|
* Parse a LiteLLM `x-litellm-response-cost` header value (a USD float,
|
|
81
64
|
* possibly in scientific notation). Exported for tests.
|
|
82
65
|
*/
|
|
83
66
|
export function parseCostHeader(value: string | null): number | undefined {
|
|
84
|
-
|
|
85
|
-
|
|
67
|
+
const trimmed = value?.trim();
|
|
68
|
+
if (!trimmed) return undefined;
|
|
69
|
+
const cost = Number(trimmed);
|
|
86
70
|
return Number.isFinite(cost) && cost >= 0 ? cost : undefined;
|
|
87
71
|
}
|
|
88
72
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Scan a captured SSE/JSON body for provider-reported cost and response ids.
|
|
75
|
+
* Exported for tests. In streams, usage lands in the final chunk, so the LAST
|
|
76
|
+
* valid cost match wins.
|
|
77
|
+
*/
|
|
78
|
+
export function extractCosts(text: string): { ids: string[]; cost?: number } {
|
|
92
79
|
const ids = new Set<string>();
|
|
93
80
|
for (const match of text.matchAll(/"id"\s*:\s*"([^"]{1,200})"/g)) {
|
|
94
81
|
ids.add(match[1]);
|
|
@@ -151,25 +138,25 @@ export default function costReconcileExtension(pi: ExtensionAPI): void {
|
|
|
151
138
|
try {
|
|
152
139
|
pi.appendEntry(ENTRY_TYPE, entry);
|
|
153
140
|
} catch {
|
|
154
|
-
//
|
|
141
|
+
// Transcript persistence failure must not break the session.
|
|
155
142
|
}
|
|
156
143
|
};
|
|
157
144
|
|
|
158
145
|
const finishCapture = (rawBody: string, headerCost?: number): void => {
|
|
159
|
-
//
|
|
160
|
-
//
|
|
146
|
+
// Ponytail: one naive regex pass over the whole body; a streaming parser
|
|
147
|
+
// only matters if bodies grow past the 4MB cap.
|
|
161
148
|
const { ids, cost } = extractCosts(rawBody);
|
|
162
|
-
// LiteLLM's header is the amount the gateway billed; prefer it over
|
|
163
|
-
//
|
|
149
|
+
// LiteLLM's header is the amount the gateway billed; prefer it over any
|
|
150
|
+
// cost the upstream payload reports.
|
|
164
151
|
const effectiveCost = headerCost ?? cost;
|
|
165
|
-
if (effectiveCost === undefined) return;
|
|
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.
|
|
166
158
|
for (const responseId of ids) {
|
|
167
|
-
processCaptured.set(responseId, effectiveCost);
|
|
168
|
-
const waiting = processWaiting.get(responseId);
|
|
169
|
-
if (waiting) {
|
|
170
|
-
processWaiting.delete(responseId);
|
|
171
|
-
waiting.write(waiting.info, effectiveCost);
|
|
172
|
-
}
|
|
159
|
+
processCaptured.set(responseId, processCaptured.has(responseId) ? null : effectiveCost);
|
|
173
160
|
}
|
|
174
161
|
if (processCaptured.size > 500) {
|
|
175
162
|
for (const key of [...processCaptured.keys()].slice(0, processCaptured.size - 500))
|
|
@@ -191,7 +178,7 @@ export default function costReconcileExtension(pi: ExtensionAPI): void {
|
|
|
191
178
|
if (typeof responseId === "string") seen.add(responseId);
|
|
192
179
|
}
|
|
193
180
|
} catch {
|
|
194
|
-
//
|
|
181
|
+
// Best effort only.
|
|
195
182
|
}
|
|
196
183
|
|
|
197
184
|
const globalFetch = globalThis as typeof globalThis & { [FETCH_PATCHED]?: boolean };
|
|
@@ -205,26 +192,36 @@ export default function costReconcileExtension(pi: ExtensionAPI): void {
|
|
|
205
192
|
const contentType = response.headers.get("content-type") ?? "";
|
|
206
193
|
if (!contentType.includes("json") && !contentType.includes("event-stream")) return response;
|
|
207
194
|
if (!response.body) return response;
|
|
195
|
+
|
|
208
196
|
const headerCost = parseCostHeader(response.headers.get("x-litellm-response-cost"));
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
raw
|
|
219
|
-
|
|
197
|
+
let raw = "";
|
|
198
|
+
let captureFailed = false;
|
|
199
|
+
const decoder = new TextDecoder();
|
|
200
|
+
const capture = new TransformStream<Uint8Array, Uint8Array>({
|
|
201
|
+
transform(chunk, controller) {
|
|
202
|
+
controller.enqueue(chunk);
|
|
203
|
+
if (captureFailed) return;
|
|
204
|
+
try {
|
|
205
|
+
raw += decoder.decode(chunk, { stream: true });
|
|
206
|
+
if (raw.length > MAX_CAPTURE_BYTES) captureFailed = true;
|
|
207
|
+
} catch {
|
|
208
|
+
captureFailed = true;
|
|
220
209
|
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
210
|
+
},
|
|
211
|
+
flush() {
|
|
212
|
+
if (captureFailed) return;
|
|
213
|
+
try {
|
|
214
|
+
raw += decoder.decode();
|
|
215
|
+
finishCapture(raw, headerCost);
|
|
216
|
+
} catch {
|
|
217
|
+
// Capture is best-effort; the response stream is unaffected.
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
// flush() runs before the SDK observes EOF, so message_end can safely
|
|
222
|
+
// replace the finalized message usage without waiting on another fetch.
|
|
223
|
+
const body = response.body.pipeThrough(capture);
|
|
224
|
+
return new Response(body, {
|
|
228
225
|
status: response.status,
|
|
229
226
|
statusText: response.statusText,
|
|
230
227
|
headers: response.headers,
|
|
@@ -236,43 +233,32 @@ export default function costReconcileExtension(pi: ExtensionAPI): void {
|
|
|
236
233
|
});
|
|
237
234
|
|
|
238
235
|
pi.on("message_end", (event) => {
|
|
239
|
-
const message = event.message
|
|
240
|
-
|
|
241
|
-
role?: string;
|
|
242
|
-
provider?: string;
|
|
243
|
-
model?: string;
|
|
244
|
-
responseId?: string;
|
|
245
|
-
stopReason?: string;
|
|
246
|
-
}
|
|
247
|
-
| undefined;
|
|
248
|
-
if (!message || message.role !== "assistant") return;
|
|
236
|
+
const message = event.message;
|
|
237
|
+
if (message.role !== "assistant") return;
|
|
249
238
|
if (message.stopReason === "error" || message.stopReason === "aborted") return;
|
|
250
239
|
const responseId = message.responseId;
|
|
251
240
|
if (typeof responseId !== "string" || responseId.length === 0) return;
|
|
252
241
|
|
|
253
|
-
const info: MessageInfo = {
|
|
254
|
-
provider: message.provider ?? "",
|
|
255
|
-
model: message.responseModel ?? message.model ?? "",
|
|
256
|
-
responseId,
|
|
257
|
-
};
|
|
258
242
|
const cost = processCaptured.get(responseId);
|
|
259
|
-
if (cost
|
|
260
|
-
|
|
243
|
+
if (cost === undefined || cost === null || !message.usage?.cost) {
|
|
244
|
+
processCaptured.delete(responseId);
|
|
261
245
|
return;
|
|
262
246
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
247
|
+
processCaptured.delete(responseId);
|
|
248
|
+
|
|
249
|
+
writeEntry(
|
|
250
|
+
{
|
|
251
|
+
provider: message.provider ?? "",
|
|
252
|
+
model: message.responseModel ?? message.model ?? "",
|
|
253
|
+
responseId,
|
|
254
|
+
},
|
|
255
|
+
cost,
|
|
256
|
+
);
|
|
257
|
+
return {
|
|
258
|
+
message: {
|
|
259
|
+
...message,
|
|
260
|
+
usage: { ...message.usage, cost: { ...message.usage.cost, total: cost } },
|
|
261
|
+
},
|
|
262
|
+
};
|
|
269
263
|
});
|
|
270
264
|
}
|
|
271
|
-
|
|
272
|
-
// Trim oldest waiting entries so unclaimed captures cannot grow unbounded.
|
|
273
|
-
function waitingLimitGuard(): void {
|
|
274
|
-
if (processWaiting.size >= MAX_PENDING_LOOKUPS) {
|
|
275
|
-
for (const key of [...processWaiting.keys()].slice(0, processWaiting.size - MAX_PENDING_LOOKUPS + 1))
|
|
276
|
-
processWaiting.delete(key);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
@@ -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 },
|