@selesai/code 0.13.38 → 0.13.40
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 +7 -0
- package/dist/core/intercom-rpc.d.ts +135 -0
- package/dist/core/intercom-rpc.js +64 -0
- package/dist/core/release-rpc.d.ts +137 -0
- package/dist/core/release-rpc.js +77 -0
- package/dist/core/resource-loader.d.ts +3 -0
- package/dist/core/resource-loader.js +3 -0
- package/dist/core/subagent-rpc.d.ts +110 -0
- package/dist/core/subagent-rpc.js +60 -0
- package/dist/extensions/agent-browser.ts +9 -0
- package/dist/extensions/pi-graft/cli.test.ts +5 -5
- package/dist/extensions/pi-graft/cli.ts +10 -5
- package/dist/extensions/pi-graft/commands.test.ts +5 -5
- package/dist/extensions/pi-graft/graft.integration.test.ts +1 -1
- package/dist/extensions/pi-graft/lifecycle.test.ts +26 -26
- package/dist/extensions/pi-graft/prompt.test.ts +1 -1
- package/dist/extensions/pi-graft/state.test.ts +9 -9
- package/dist/extensions/pi-graft/test-support.ts +1 -1
- package/dist/extensions/pi-graft/tools.test.ts +1 -1
- package/dist/extensions/pi-intercom/README.md +15 -0
- package/dist/extensions/pi-intercom/broker/broker.ts +277 -13
- package/dist/extensions/pi-intercom/broker/client.ts +6 -2
- package/dist/extensions/pi-intercom/broker/mailbox-persistence.test.ts +523 -0
- package/dist/extensions/pi-intercom/broker/paths.test.ts +4 -4
- package/dist/extensions/pi-intercom/broker/paths.ts +4 -2
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +1 -3
- package/dist/extensions/pi-intercom/extension-api.ts +10 -0
- package/dist/extensions/pi-intercom/index.ts +188 -8
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +280 -0
- package/dist/extensions/pi-intercom/package.json +1 -1
- package/dist/extensions/pi-intercom/release-readiness.test.ts +116 -0
- package/dist/extensions/pi-intercom/release-readiness.ts +93 -0
- package/dist/extensions/pi-subagents/docs/extension-api.md +8 -0
- package/dist/extensions/pi-subagents/docs/observability.md +8 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +51 -1
- package/dist/extensions/pi-subagents/src/extension/observability.ts +164 -0
- package/dist/extensions/pi-subagents/src/extension/release-readiness.ts +367 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +8 -1
- package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +7 -1
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +1 -44
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +6 -0
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +13 -3
- package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +269 -7
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +11 -10
- package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +15 -3
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +64 -0
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -6
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -1
- package/dist/extensions/pi-subagents/src/shared/owner-record.ts +107 -0
- package/dist/extensions/pi-subagents/src/shared/process-identity.ts +87 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +9 -0
- package/dist/extensions/pi-subagents/test/integration/async-attention-clearing.test.ts +191 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/integration/stop-all-background.test.ts +142 -0
- package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/observability.test.ts +198 -0
- package/dist/extensions/pi-subagents/test/unit/process-identity.test.ts +75 -0
- package/dist/extensions/pi-subagents/test/unit/release-readiness.test.ts +715 -0
- package/dist/extensions/pi-subagents/test/unit/result-takeover.test.ts +332 -0
- package/dist/extensions/tokenin-onboarding.ts +142 -3
- package/dist/index.d.ts +6 -1
- package/dist/index.js +7 -0
- package/dist/main.d.ts +6 -0
- package/dist/main.js +1 -1
- package/dist/modes/index.d.ts +1 -1
- package/dist/modes/rpc/intercom-bridge.d.ts +14 -0
- package/dist/modes/rpc/intercom-bridge.js +75 -0
- package/dist/modes/rpc/intercom-bridge.test.d.ts +1 -0
- package/dist/modes/rpc/intercom-bridge.test.js +65 -0
- package/dist/modes/rpc/release-bridge.d.ts +35 -0
- package/dist/modes/rpc/release-bridge.js +159 -0
- package/dist/modes/rpc/release-bridge.test.d.ts +1 -0
- package/dist/modes/rpc/release-bridge.test.js +347 -0
- package/dist/modes/rpc/rpc-client.d.ts +34 -10
- package/dist/modes/rpc/rpc-client.js +45 -2
- package/dist/modes/rpc/rpc-intercom-transport.test.d.ts +1 -0
- package/dist/modes/rpc/rpc-intercom-transport.test.js +85 -0
- package/dist/modes/rpc/rpc-mode.d.ts +1 -13
- package/dist/modes/rpc/rpc-mode.js +239 -8
- package/dist/modes/rpc/rpc-release-transport.test.d.ts +1 -0
- package/dist/modes/rpc/rpc-release-transport.test.js +329 -0
- package/dist/modes/rpc/rpc-subagent-transport.test.d.ts +1 -0
- package/dist/modes/rpc/rpc-subagent-transport.test.js +60 -0
- package/dist/modes/rpc/rpc-types.d.ts +12 -2
- package/dist/modes/rpc/rpc-types.js +0 -6
- package/dist/modes/rpc/subagent-bridge.d.ts +23 -0
- package/dist/modes/rpc/subagent-bridge.js +93 -0
- package/dist/modes/rpc/test-fixtures/intercom-host.d.ts +1 -0
- package/dist/modes/rpc/test-fixtures/intercom-host.js +13 -0
- package/docs/rpc-subagent.schema.json +491 -0
- package/docs/rpc.md +210 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.39] - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **`/tokenin usage` shows the account balance.** It queries `/tokenin/account/me/balance` and shows available credit (plan, PAYG, debt), active plan credit used, and plan expiry; a 404 falls back to the LiteLLM `/key/info` quota view.
|
|
9
|
+
- **The `researcher` subagent uses `web_explore`** from the bundled `pi-web-agent` extension instead of `pi-web-access` tools; `pi-web-agent` registers a builtin-agent augmentation so foreground `researcher` children load it.
|
|
10
|
+
- **Graft CLI floor raised to 0.21** (`MIN_GRAFT_VERSION` 0.21.0, install spec `@nanonets/graft@^0.21`).
|
|
11
|
+
|
|
5
12
|
## [0.13.38] - 2026-10-01
|
|
6
13
|
|
|
7
14
|
### Changed
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { RpcObservationScope } from "./subagent-rpc.ts";
|
|
2
|
+
import type { Attachment, BrokerMessage, DeliveryDetails, Message, SessionInfo } from "../extensions/pi-intercom/types.ts";
|
|
3
|
+
/** Dedicated same-process bridge. These channels are not externally callable RPC bus operations. */
|
|
4
|
+
export declare const INTERCOM_RPC_REQUEST_EVENT = "intercom:rpc-request";
|
|
5
|
+
export declare const INTERCOM_RPC_RESULT_EVENT = "intercom:rpc-result";
|
|
6
|
+
export declare const INTERCOM_RPC_EVENT = "intercom:rpc-event";
|
|
7
|
+
export declare const INTERCOM_RPC_ASK_TIMEOUT_MS = 600000;
|
|
8
|
+
export interface IntercomRpcMessageOptions {
|
|
9
|
+
message: string;
|
|
10
|
+
attachments?: Attachment[];
|
|
11
|
+
replyTo?: string;
|
|
12
|
+
}
|
|
13
|
+
export type IntercomRpcCommand = {
|
|
14
|
+
id?: string;
|
|
15
|
+
type: "intercom_status";
|
|
16
|
+
} | {
|
|
17
|
+
id?: string;
|
|
18
|
+
type: "intercom_list";
|
|
19
|
+
} | {
|
|
20
|
+
id?: string;
|
|
21
|
+
type: "intercom_pending";
|
|
22
|
+
} | ({
|
|
23
|
+
id?: string;
|
|
24
|
+
type: "intercom_send";
|
|
25
|
+
to: string;
|
|
26
|
+
} & IntercomRpcMessageOptions) | ({
|
|
27
|
+
id?: string;
|
|
28
|
+
type: "intercom_ask";
|
|
29
|
+
to: string;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
} & IntercomRpcMessageOptions) | ({
|
|
32
|
+
id?: string;
|
|
33
|
+
type: "intercom_reply";
|
|
34
|
+
to?: string;
|
|
35
|
+
} & IntercomRpcMessageOptions);
|
|
36
|
+
export interface IntercomRpcStatus {
|
|
37
|
+
scope: RpcObservationScope;
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
connected: boolean;
|
|
40
|
+
sessionId: string | null;
|
|
41
|
+
name?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface IntercomRpcDelivery extends DeliveryDetails {
|
|
44
|
+
messageId: string;
|
|
45
|
+
delivered: boolean;
|
|
46
|
+
reason?: string;
|
|
47
|
+
to: string;
|
|
48
|
+
replyTo?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface IntercomRpcDataMap {
|
|
51
|
+
intercom_status: IntercomRpcStatus;
|
|
52
|
+
intercom_list: {
|
|
53
|
+
sessionId: string;
|
|
54
|
+
sessions: SessionInfo[];
|
|
55
|
+
};
|
|
56
|
+
intercom_pending: {
|
|
57
|
+
asks: Array<{
|
|
58
|
+
from: SessionInfo;
|
|
59
|
+
message: Message;
|
|
60
|
+
receivedAt: number;
|
|
61
|
+
}>;
|
|
62
|
+
};
|
|
63
|
+
intercom_send: IntercomRpcDelivery;
|
|
64
|
+
intercom_reply: IntercomRpcDelivery;
|
|
65
|
+
intercom_ask: IntercomRpcDelivery & {
|
|
66
|
+
reply: Message;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export type IntercomRpcResponse = {
|
|
70
|
+
[K in keyof IntercomRpcDataMap]: {
|
|
71
|
+
id?: string;
|
|
72
|
+
type: "response";
|
|
73
|
+
command: K;
|
|
74
|
+
success: true;
|
|
75
|
+
data: IntercomRpcDataMap[K];
|
|
76
|
+
};
|
|
77
|
+
}[keyof IntercomRpcDataMap];
|
|
78
|
+
export type IntercomRpcEvent = IntercomRpcEventData & {
|
|
79
|
+
scope: RpcObservationScope;
|
|
80
|
+
};
|
|
81
|
+
/** Internal event data; the stdio host adds its own binding scope. */
|
|
82
|
+
export type IntercomRpcEventData = {
|
|
83
|
+
type: "intercom_connection";
|
|
84
|
+
enabled: boolean;
|
|
85
|
+
connected: boolean;
|
|
86
|
+
sessionId: string | null;
|
|
87
|
+
reason?: string;
|
|
88
|
+
} | {
|
|
89
|
+
type: "intercom_message";
|
|
90
|
+
from: SessionInfo;
|
|
91
|
+
message: Message;
|
|
92
|
+
} | {
|
|
93
|
+
type: "intercom_presence";
|
|
94
|
+
session: SessionInfo;
|
|
95
|
+
} | {
|
|
96
|
+
type: "intercom_session_joined";
|
|
97
|
+
session: SessionInfo;
|
|
98
|
+
} | {
|
|
99
|
+
type: "intercom_session_left";
|
|
100
|
+
sessionId: string;
|
|
101
|
+
} | {
|
|
102
|
+
type: "intercom_receipt";
|
|
103
|
+
from: SessionInfo;
|
|
104
|
+
receipt: Extract<BrokerMessage, {
|
|
105
|
+
type: "message_receipt";
|
|
106
|
+
}>["receipt"];
|
|
107
|
+
} | {
|
|
108
|
+
type: "intercom_delivery";
|
|
109
|
+
result: Extract<BrokerMessage, {
|
|
110
|
+
type: "delivered" | "delivery_failed";
|
|
111
|
+
}>;
|
|
112
|
+
};
|
|
113
|
+
export interface IntercomRpcRequest {
|
|
114
|
+
requestId: string;
|
|
115
|
+
command: IntercomRpcCommand;
|
|
116
|
+
signal: AbortSignal;
|
|
117
|
+
/** Synchronous acknowledgement allows the host to fail promptly if the extension is missing. */
|
|
118
|
+
accept(): void;
|
|
119
|
+
}
|
|
120
|
+
export type IntercomRpcResult = {
|
|
121
|
+
requestId: string;
|
|
122
|
+
} & ({
|
|
123
|
+
success: true;
|
|
124
|
+
data: IntercomRpcDataMap[keyof IntercomRpcDataMap] | Omit<IntercomRpcStatus, "scope">;
|
|
125
|
+
} | {
|
|
126
|
+
success: false;
|
|
127
|
+
error: string;
|
|
128
|
+
});
|
|
129
|
+
export declare function intercomRpcTimeout(command: IntercomRpcCommand): number;
|
|
130
|
+
export interface IntercomRpcEventEnvelope {
|
|
131
|
+
sessionId: string | null;
|
|
132
|
+
event: IntercomRpcEventData;
|
|
133
|
+
}
|
|
134
|
+
/** Fail closed before forwarding extension-bus payloads to a host client. */
|
|
135
|
+
export declare function isIntercomRpcEvent(value: unknown): value is IntercomRpcEventData;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Dedicated same-process bridge. These channels are not externally callable RPC bus operations. */
|
|
2
|
+
export const INTERCOM_RPC_REQUEST_EVENT = "intercom:rpc-request";
|
|
3
|
+
export const INTERCOM_RPC_RESULT_EVENT = "intercom:rpc-result";
|
|
4
|
+
export const INTERCOM_RPC_EVENT = "intercom:rpc-event";
|
|
5
|
+
export const INTERCOM_RPC_ASK_TIMEOUT_MS = 600_000;
|
|
6
|
+
export function intercomRpcTimeout(command) {
|
|
7
|
+
if (command.type !== "intercom_ask")
|
|
8
|
+
return 30_000;
|
|
9
|
+
const timeout = command.timeoutMs ?? INTERCOM_RPC_ASK_TIMEOUT_MS;
|
|
10
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > INTERCOM_RPC_ASK_TIMEOUT_MS) {
|
|
11
|
+
throw new Error("timeoutMs must be an integer between 1 and 600000");
|
|
12
|
+
}
|
|
13
|
+
return timeout;
|
|
14
|
+
}
|
|
15
|
+
/** Fail closed before forwarding extension-bus payloads to a host client. */
|
|
16
|
+
export function isIntercomRpcEvent(value) {
|
|
17
|
+
if (!value || typeof value !== "object")
|
|
18
|
+
return false;
|
|
19
|
+
const event = value;
|
|
20
|
+
switch (event.type) {
|
|
21
|
+
case "intercom_connection":
|
|
22
|
+
return typeof event.enabled === "boolean" && typeof event.connected === "boolean"
|
|
23
|
+
&& (event.sessionId === null || typeof event.sessionId === "string")
|
|
24
|
+
&& (event.reason === undefined || typeof event.reason === "string");
|
|
25
|
+
case "intercom_message": return isSessionInfo(event.from) && isMessage(event.message);
|
|
26
|
+
case "intercom_presence":
|
|
27
|
+
case "intercom_session_joined": return isSessionInfo(event.session);
|
|
28
|
+
case "intercom_session_left": return typeof event.sessionId === "string";
|
|
29
|
+
case "intercom_receipt": return isSessionInfo(event.from) && isMessageReceipt(event.receipt);
|
|
30
|
+
case "intercom_delivery": {
|
|
31
|
+
if (!event.result || typeof event.result !== "object")
|
|
32
|
+
return false;
|
|
33
|
+
const result = event.result;
|
|
34
|
+
return typeof result.messageId === "string" && typeof result.retryable === "boolean" && typeof result.outcomeKnown === "boolean"
|
|
35
|
+
&& (result.code === undefined || typeof result.code === "string")
|
|
36
|
+
&& (result.type === "delivered" && (result.delivery === "queued" || result.delivery === "socket_delivered")
|
|
37
|
+
|| result.type === "delivery_failed" && typeof result.reason === "string" && (result.delivery === "failed" || result.delivery === "unknown"));
|
|
38
|
+
}
|
|
39
|
+
default: return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function isRecord(value) {
|
|
43
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
44
|
+
}
|
|
45
|
+
function isSessionInfo(value) {
|
|
46
|
+
return isRecord(value) && ["id", "cwd", "model"].every(key => typeof value[key] === "string")
|
|
47
|
+
&& ["pid", "startedAt", "lastActivity"].every(key => typeof value[key] === "number" && Number.isFinite(value[key]));
|
|
48
|
+
}
|
|
49
|
+
function isMessage(value) {
|
|
50
|
+
if (!isRecord(value) || typeof value.id !== "string" || typeof value.timestamp !== "number" || !Number.isFinite(value.timestamp)
|
|
51
|
+
|| !isRecord(value.content) || typeof value.content.text !== "string")
|
|
52
|
+
return false;
|
|
53
|
+
if (value.replyTo !== undefined && typeof value.replyTo !== "string" || value.expectsReply !== undefined && typeof value.expectsReply !== "boolean")
|
|
54
|
+
return false;
|
|
55
|
+
const attachments = value.content.attachments;
|
|
56
|
+
return attachments === undefined || Array.isArray(attachments) && attachments.every(item => isRecord(item)
|
|
57
|
+
&& ["file", "snippet", "context"].includes(String(item.type)) && typeof item.name === "string" && typeof item.content === "string"
|
|
58
|
+
&& (item.language === undefined || typeof item.language === "string"));
|
|
59
|
+
}
|
|
60
|
+
function isMessageReceipt(value) {
|
|
61
|
+
return isRecord(value) && typeof value.messageId === "string" && typeof value.timestamp === "number" && Number.isFinite(value.timestamp)
|
|
62
|
+
&& ["receiver_received", "queued", "injected", "acknowledged", "expired", "cancelled", "superseded", "cancellation_requested"].includes(String(value.status))
|
|
63
|
+
&& (value.detail === undefined || typeof value.detail === "string");
|
|
64
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Release-readiness DTOs. No extension runtime or filesystem dependencies. */
|
|
2
|
+
import type { RpcObservationScope } from "./subagent-rpc.ts";
|
|
3
|
+
export type { RpcObservationScope } from "./subagent-rpc.ts";
|
|
4
|
+
export declare const RELEASE_READINESS_EVENT = "release:readiness:v1";
|
|
5
|
+
export type ReleaseReadinessCommand = {
|
|
6
|
+
type: "release_readiness";
|
|
7
|
+
id?: string;
|
|
8
|
+
version: 1;
|
|
9
|
+
};
|
|
10
|
+
export type ReleaseBlockerSource = "core" | "subagents" | "intercom" | "scheduler" | (string & {});
|
|
11
|
+
export interface ReleaseBlocker {
|
|
12
|
+
source: ReleaseBlockerSource;
|
|
13
|
+
/** Short stable machine token, e.g. streaming, compacting, retrying, queued_messages, pending_tool_calls, pending_bash,
|
|
14
|
+
* extension_dialog, compaction_prompt_queue, async_run, foreground_run, undelivered_result, inbound_ask, outbound_ask,
|
|
15
|
+
* armed_schedule, releasing, contributor_error. */
|
|
16
|
+
kind: string;
|
|
17
|
+
id?: string;
|
|
18
|
+
detail: string;
|
|
19
|
+
/** Epoch milliseconds when the blocking condition began (or, for armed schedules, is next due). */
|
|
20
|
+
since?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface ReleaseReadiness extends RpcObservationScope {
|
|
23
|
+
/** True only when `blockers` is empty. A false negative here means lost work, so uncertainty is reported as a blocker. */
|
|
24
|
+
safe: boolean;
|
|
25
|
+
blockers: ReleaseBlocker[];
|
|
26
|
+
/** Extension contributors that answered. `safe` with an empty list means nobody reported. */
|
|
27
|
+
contributors: string[];
|
|
28
|
+
}
|
|
29
|
+
export type ReleaseReadinessResponse = {
|
|
30
|
+
id?: string;
|
|
31
|
+
type: "response";
|
|
32
|
+
command: "release_readiness";
|
|
33
|
+
success: true;
|
|
34
|
+
data: ReleaseReadiness;
|
|
35
|
+
};
|
|
36
|
+
/** Atomic "release if safe": see `decideRelease`. Validated exactly like `release_readiness`. */
|
|
37
|
+
export type ReleaseCommand = {
|
|
38
|
+
type: "release";
|
|
39
|
+
id?: string;
|
|
40
|
+
version: 1;
|
|
41
|
+
};
|
|
42
|
+
export interface ReleaseResult {
|
|
43
|
+
/** True only when the process committed to a gentle exit (exit code 0, no abort). False: it keeps running normally. */
|
|
44
|
+
released: boolean;
|
|
45
|
+
/** The readiness the decision was based on; when `released` is false it lists what blocked the release. */
|
|
46
|
+
readiness: ReleaseReadiness;
|
|
47
|
+
}
|
|
48
|
+
export type ReleaseResponse = {
|
|
49
|
+
id?: string;
|
|
50
|
+
type: "response";
|
|
51
|
+
command: "release";
|
|
52
|
+
success: true;
|
|
53
|
+
data: ReleaseResult;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Core blocker kind reported by `release_readiness` once a `release` has been committed (the process is about to exit).
|
|
57
|
+
* `kind` is `releasing`.
|
|
58
|
+
*/
|
|
59
|
+
export declare const RELEASING_BLOCKER_KIND = "releasing";
|
|
60
|
+
/**
|
|
61
|
+
* Whether a readiness answer permits an immediate release. "Nobody answered" (`contributors` empty, e.g. extensions not
|
|
62
|
+
* loaded) is NOT safe: the process cannot prove that no background work exists.
|
|
63
|
+
*/
|
|
64
|
+
export declare function decideRelease(readiness: Pick<ReleaseReadiness, "safe" | "blockers" | "contributors">): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Synchronous broadcast on the session EventBus. Every listener calls `contribute` before `emit` returns.
|
|
67
|
+
* Listeners MUST ignore requests whose `sessionId` is not the session they are bound to.
|
|
68
|
+
*/
|
|
69
|
+
export interface ReleaseReadinessRequest {
|
|
70
|
+
version: 1;
|
|
71
|
+
sessionId: string;
|
|
72
|
+
contribute(name: string, blockers: ReleaseBlocker[]): void;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Listener-side helper: gate on session identity, compute blockers, and fail closed.
|
|
76
|
+
* The EventBus swallows listener exceptions, so a contributor that throws would otherwise look like "not loaded"
|
|
77
|
+
* and the host would report safe. Always answer through this helper (or reproduce its try/catch).
|
|
78
|
+
*/
|
|
79
|
+
export declare function answerReleaseReadiness(raw: unknown, name: string, boundSessionId: string | null | undefined, compute: () => ReleaseBlocker[]): void;
|
|
80
|
+
/** Bounded shape check for blockers received from trusted-but-fallible extension contributors. */
|
|
81
|
+
export declare function isReleaseBlocker(v: unknown): v is ReleaseBlocker;
|
|
82
|
+
export declare const STOP_ALL_BACKGROUND_EVENT = "release:stop-all:v1";
|
|
83
|
+
/** Total budget for one `stop_all_background`: core abort plus every extension's (possibly asynchronous) stop. */
|
|
84
|
+
export declare const STOP_ALL_BACKGROUND_TIMEOUT_MS = 5000;
|
|
85
|
+
/**
|
|
86
|
+
* Destructive: aborts the turn and stops every background run, session-only schedule and outbound ask of this session.
|
|
87
|
+
* Validated exactly like `release_readiness` and `release`; refused while a `release` is committed.
|
|
88
|
+
*/
|
|
89
|
+
export type StopAllBackgroundCommand = {
|
|
90
|
+
type: "stop_all_background";
|
|
91
|
+
id?: string;
|
|
92
|
+
version: 1;
|
|
93
|
+
};
|
|
94
|
+
export interface StopAllBackgroundItem {
|
|
95
|
+
source: ReleaseBlockerSource;
|
|
96
|
+
/** Short stable machine token, e.g. turn, retry, compaction, bash, queued_messages, async_run, foreground_run, schedule, outbound_ask, inbound_ask, contributor. */
|
|
97
|
+
kind: string;
|
|
98
|
+
id?: string;
|
|
99
|
+
detail: string;
|
|
100
|
+
/** True when the thing is no longer running/armed/pending. False: it could not be stopped (see `error`). */
|
|
101
|
+
ok: boolean;
|
|
102
|
+
error?: string;
|
|
103
|
+
}
|
|
104
|
+
export interface StopAllBackgroundResult extends RpcObservationScope {
|
|
105
|
+
/** Everything that was running, armed or pending when the command arrived, with the outcome of each stop. Empty on an idle session. */
|
|
106
|
+
stopped: StopAllBackgroundItem[];
|
|
107
|
+
/** Readiness computed AFTER the stop: what is still running or pending (also lists things that cannot be stopped). */
|
|
108
|
+
remaining: ReleaseReadiness;
|
|
109
|
+
}
|
|
110
|
+
export type StopAllBackgroundResponse = {
|
|
111
|
+
id?: string;
|
|
112
|
+
type: "response";
|
|
113
|
+
command: "stop_all_background";
|
|
114
|
+
success: true;
|
|
115
|
+
data: StopAllBackgroundResult;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* Synchronous broadcast on the session EventBus, like `ReleaseReadinessRequest`. Every listener calls `contribute` before
|
|
119
|
+
* `emit` returns; the stop itself should start synchronously. A contributor that needs to wait (e.g. for a runner to
|
|
120
|
+
* acknowledge) passes a promise and must settle by `deadline` (epoch ms) with its still-pending items as `ok:false, error:"timeout"`.
|
|
121
|
+
* Listeners MUST ignore requests whose `sessionId` is not the session they are bound to.
|
|
122
|
+
*/
|
|
123
|
+
export interface StopAllBackgroundRequest {
|
|
124
|
+
version: 1;
|
|
125
|
+
sessionId: string;
|
|
126
|
+
deadline: number;
|
|
127
|
+
contribute(name: string, items: StopAllBackgroundItem[] | PromiseLike<StopAllBackgroundItem[]>): void;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Listener-side helper: gate on session identity and fail closed. A synchronous throw becomes an `ok:false` item, so one
|
|
131
|
+
* extension failing never prevents the others from stopping (the bridge also converts a rejected promise).
|
|
132
|
+
*/
|
|
133
|
+
export declare function answerStopAllBackground(raw: unknown, name: string, boundSessionId: string | null | undefined, compute: (request: Pick<StopAllBackgroundRequest, "deadline">) => StopAllBackgroundItem[] | PromiseLike<StopAllBackgroundItem[]>): void;
|
|
134
|
+
/** The item every failure path uses, so a broken contributor is visible instead of silent. */
|
|
135
|
+
export declare function stopAllContributorError(source: string, error: unknown): StopAllBackgroundItem;
|
|
136
|
+
/** Bounded shape check for items received from trusted-but-fallible extension contributors. */
|
|
137
|
+
export declare function isStopAllBackgroundItem(v: unknown): v is StopAllBackgroundItem;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export const RELEASE_READINESS_EVENT = "release:readiness:v1";
|
|
2
|
+
/**
|
|
3
|
+
* Core blocker kind reported by `release_readiness` once a `release` has been committed (the process is about to exit).
|
|
4
|
+
* `kind` is `releasing`.
|
|
5
|
+
*/
|
|
6
|
+
export const RELEASING_BLOCKER_KIND = "releasing";
|
|
7
|
+
/**
|
|
8
|
+
* Whether a readiness answer permits an immediate release. "Nobody answered" (`contributors` empty, e.g. extensions not
|
|
9
|
+
* loaded) is NOT safe: the process cannot prove that no background work exists.
|
|
10
|
+
*/
|
|
11
|
+
export function decideRelease(readiness) {
|
|
12
|
+
return readiness.safe === true && readiness.blockers.length === 0 && readiness.contributors.length > 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Listener-side helper: gate on session identity, compute blockers, and fail closed.
|
|
16
|
+
* The EventBus swallows listener exceptions, so a contributor that throws would otherwise look like "not loaded"
|
|
17
|
+
* and the host would report safe. Always answer through this helper (or reproduce its try/catch).
|
|
18
|
+
*/
|
|
19
|
+
export function answerReleaseReadiness(raw, name, boundSessionId, compute) {
|
|
20
|
+
const request = raw;
|
|
21
|
+
if (!request || request.version !== 1 || typeof request.contribute !== "function" || !boundSessionId || request.sessionId !== boundSessionId)
|
|
22
|
+
return;
|
|
23
|
+
let blockers;
|
|
24
|
+
try {
|
|
25
|
+
blockers = compute();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
blockers = [{ source: name, kind: "contributor_error", detail: (error instanceof Error ? error.message : String(error)).slice(0, 512) || "contributor failed" }];
|
|
29
|
+
}
|
|
30
|
+
request.contribute(name, blockers);
|
|
31
|
+
}
|
|
32
|
+
const text = (v, max) => typeof v === "string" && v.length > 0 && v.length <= max;
|
|
33
|
+
const record = (v) => Boolean(v && typeof v === "object" && !Array.isArray(v));
|
|
34
|
+
/** Bounded shape check for blockers received from trusted-but-fallible extension contributors. */
|
|
35
|
+
export function isReleaseBlocker(v) {
|
|
36
|
+
return record(v) && Object.keys(v).every(key => ["source", "kind", "id", "detail", "since"].includes(key))
|
|
37
|
+
&& text(v.source, 64) && text(v.kind, 64) && text(v.detail, 512)
|
|
38
|
+
&& (v.id === undefined || text(v.id, 256))
|
|
39
|
+
&& (v.since === undefined || typeof v.since === "number" && Number.isSafeInteger(v.since) && v.since >= 0);
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
42
|
+
// stop_all_background: the kill switch behind the desktop's "Quit anyway".
|
|
43
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
44
|
+
export const STOP_ALL_BACKGROUND_EVENT = "release:stop-all:v1";
|
|
45
|
+
/** Total budget for one `stop_all_background`: core abort plus every extension's (possibly asynchronous) stop. */
|
|
46
|
+
export const STOP_ALL_BACKGROUND_TIMEOUT_MS = 5_000;
|
|
47
|
+
/**
|
|
48
|
+
* Listener-side helper: gate on session identity and fail closed. A synchronous throw becomes an `ok:false` item, so one
|
|
49
|
+
* extension failing never prevents the others from stopping (the bridge also converts a rejected promise).
|
|
50
|
+
*/
|
|
51
|
+
export function answerStopAllBackground(raw, name, boundSessionId, compute) {
|
|
52
|
+
const request = raw;
|
|
53
|
+
if (!request || request.version !== 1 || typeof request.contribute !== "function" || !boundSessionId || request.sessionId !== boundSessionId)
|
|
54
|
+
return;
|
|
55
|
+
const deadline = typeof request.deadline === "number" && Number.isFinite(request.deadline) ? request.deadline : Date.now() + STOP_ALL_BACKGROUND_TIMEOUT_MS;
|
|
56
|
+
let items;
|
|
57
|
+
try {
|
|
58
|
+
items = compute({ deadline });
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
items = [stopAllContributorError(name, error)];
|
|
62
|
+
}
|
|
63
|
+
request.contribute(name, items);
|
|
64
|
+
}
|
|
65
|
+
/** The item every failure path uses, so a broken contributor is visible instead of silent. */
|
|
66
|
+
export function stopAllContributorError(source, error) {
|
|
67
|
+
const detail = (error instanceof Error ? error.message : String(error)).slice(0, 400) || "contributor failed";
|
|
68
|
+
return { source, kind: "contributor", detail: `Stop failed: ${detail}`, ok: false, error: detail.slice(0, 256) };
|
|
69
|
+
}
|
|
70
|
+
/** Bounded shape check for items received from trusted-but-fallible extension contributors. */
|
|
71
|
+
export function isStopAllBackgroundItem(v) {
|
|
72
|
+
return record(v) && Object.keys(v).every(key => ["source", "kind", "id", "detail", "ok", "error"].includes(key))
|
|
73
|
+
&& text(v.source, 64) && text(v.kind, 64) && text(v.detail, 512)
|
|
74
|
+
&& (v.id === undefined || text(v.id, 256))
|
|
75
|
+
&& typeof v.ok === "boolean"
|
|
76
|
+
&& (v.error === undefined || text(v.error, 256));
|
|
77
|
+
}
|
|
@@ -28,6 +28,8 @@ export interface ResourceLoaderReloadOptions {
|
|
|
28
28
|
}) => Promise<boolean>;
|
|
29
29
|
}
|
|
30
30
|
export interface ResourceLoader {
|
|
31
|
+
/** Same-process extension bridge for host integrations; never exposed as an arbitrary RPC command. */
|
|
32
|
+
getEventBus?(): EventBus;
|
|
31
33
|
getExtensions(): LoadExtensionsResult;
|
|
32
34
|
getExtensionDiagnostics(): ResourceDiagnostic[];
|
|
33
35
|
getSkills(): {
|
|
@@ -201,6 +203,7 @@ export declare class DefaultResourceLoader implements ResourceLoader {
|
|
|
201
203
|
private lastThemePaths;
|
|
202
204
|
private loaded;
|
|
203
205
|
constructor(options: DefaultResourceLoaderOptions);
|
|
206
|
+
getEventBus(): EventBus;
|
|
204
207
|
getExtensions(): LoadExtensionsResult;
|
|
205
208
|
getExtensionDiagnostics(): ResourceDiagnostic[];
|
|
206
209
|
getSkills(): {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** Read-only stdio observability DTOs. No extension runtime or filesystem dependencies. */
|
|
2
|
+
export declare const SUBAGENT_OBSERVE_EVENT = "subagent:observe:v1";
|
|
3
|
+
export declare const SUBAGENT_RPC_MAX_BYTES = 65536;
|
|
4
|
+
export interface RpcObservationScope {
|
|
5
|
+
version: 1;
|
|
6
|
+
sessionId: string | null;
|
|
7
|
+
epoch: string;
|
|
8
|
+
}
|
|
9
|
+
export type SubagentRpcCommand = {
|
|
10
|
+
type: "subagent_status";
|
|
11
|
+
id?: string;
|
|
12
|
+
version: 1;
|
|
13
|
+
};
|
|
14
|
+
export interface SubagentRpcTokens {
|
|
15
|
+
input: number;
|
|
16
|
+
output: number;
|
|
17
|
+
total: number;
|
|
18
|
+
window?: number;
|
|
19
|
+
windowPeak?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface SubagentRpcFleet {
|
|
22
|
+
version: 1;
|
|
23
|
+
entries: Array<{
|
|
24
|
+
key: string;
|
|
25
|
+
agent: string;
|
|
26
|
+
role?: string;
|
|
27
|
+
model?: string;
|
|
28
|
+
effort?: string;
|
|
29
|
+
startedAt: number;
|
|
30
|
+
tokens: SubagentRpcTokens;
|
|
31
|
+
goal?: string;
|
|
32
|
+
}>;
|
|
33
|
+
totalActive: number;
|
|
34
|
+
topLevelAsyncCapacity: {
|
|
35
|
+
used: number;
|
|
36
|
+
limit: number;
|
|
37
|
+
};
|
|
38
|
+
omitted: number;
|
|
39
|
+
}
|
|
40
|
+
export type SubagentRpcLifecycle = "queued" | "pending" | "running" | "complete" | "completed" | "failed" | "partial" | "paused" | "stopped" | "rejected" | "detached";
|
|
41
|
+
export interface SubagentRpcNode {
|
|
42
|
+
key: string;
|
|
43
|
+
runId?: string;
|
|
44
|
+
workflowId?: string;
|
|
45
|
+
agent?: string;
|
|
46
|
+
status: SubagentRpcLifecycle;
|
|
47
|
+
activity?: "waiting_supervisor" | "needs_attention";
|
|
48
|
+
startedAt?: number;
|
|
49
|
+
updatedAt?: number;
|
|
50
|
+
endedAt?: number;
|
|
51
|
+
progress?: string;
|
|
52
|
+
currentTool?: string;
|
|
53
|
+
tokens?: SubagentRpcTokens;
|
|
54
|
+
error?: string;
|
|
55
|
+
reason?: string;
|
|
56
|
+
/** Only explicit runtime execution projections; lifecycle is not a task verdict. */
|
|
57
|
+
outcome?: {
|
|
58
|
+
status: SubagentRpcLifecycle;
|
|
59
|
+
success?: boolean;
|
|
60
|
+
exitCode?: number;
|
|
61
|
+
};
|
|
62
|
+
/** Independent point-in-time runner proof, never inferred from outcome. */
|
|
63
|
+
processTerminal?: {
|
|
64
|
+
state: "pending" | "observed" | "unknown" | "not-started";
|
|
65
|
+
observedAt?: number;
|
|
66
|
+
reason?: string;
|
|
67
|
+
};
|
|
68
|
+
children: SubagentRpcNode[];
|
|
69
|
+
omittedChildren: number;
|
|
70
|
+
}
|
|
71
|
+
export interface SubagentRpcProjection {
|
|
72
|
+
fleet: SubagentRpcFleet;
|
|
73
|
+
runs: SubagentRpcNode[];
|
|
74
|
+
omitted: {
|
|
75
|
+
runs: number;
|
|
76
|
+
children: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export interface SubagentRpcSnapshot extends RpcObservationScope, SubagentRpcProjection {
|
|
80
|
+
available: boolean;
|
|
81
|
+
capabilities: {
|
|
82
|
+
snapshot: true;
|
|
83
|
+
events: boolean;
|
|
84
|
+
};
|
|
85
|
+
revision: number;
|
|
86
|
+
}
|
|
87
|
+
export interface SubagentRpcEvent extends RpcObservationScope {
|
|
88
|
+
type: "subagent_event";
|
|
89
|
+
revision: number;
|
|
90
|
+
snapshot: SubagentRpcSnapshot;
|
|
91
|
+
}
|
|
92
|
+
export type SubagentRpcResponse = {
|
|
93
|
+
id?: string;
|
|
94
|
+
type: "response";
|
|
95
|
+
command: "subagent_status";
|
|
96
|
+
success: true;
|
|
97
|
+
data: SubagentRpcSnapshot;
|
|
98
|
+
};
|
|
99
|
+
/** Synchronous, dedicated internal registration. Only native projections, never generic bus invocation. */
|
|
100
|
+
export interface SubagentObserveRequest {
|
|
101
|
+
version: 1;
|
|
102
|
+
sessionId: string;
|
|
103
|
+
accept(source: SubagentObservationSource): void;
|
|
104
|
+
}
|
|
105
|
+
export interface SubagentObservationSource {
|
|
106
|
+
snapshot(): SubagentRpcProjection;
|
|
107
|
+
subscribe(listener: (projection: SubagentRpcProjection | null) => void): () => void;
|
|
108
|
+
}
|
|
109
|
+
/** Reject malformed or oversized trusted-extension payloads before JSONL publication. */
|
|
110
|
+
export declare function isSubagentRpcProjection(v: unknown): v is SubagentRpcProjection;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** Read-only stdio observability DTOs. No extension runtime or filesystem dependencies. */
|
|
2
|
+
export const SUBAGENT_OBSERVE_EVENT = "subagent:observe:v1";
|
|
3
|
+
export const SUBAGENT_RPC_MAX_BYTES = 65_536;
|
|
4
|
+
const statuses = new Set(["queued", "pending", "running", "complete", "completed", "failed", "partial", "paused", "stopped", "rejected", "detached"]);
|
|
5
|
+
const record = (v) => Boolean(v && typeof v === "object" && !Array.isArray(v));
|
|
6
|
+
const count = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
7
|
+
const text = (v, max) => typeof v === "string" && v.length > 0 && v.length <= max;
|
|
8
|
+
const optionalText = (v, max) => v === undefined || text(v, max);
|
|
9
|
+
const identity = (v) => text(v, 128) && !/[\\/\r\n]/.test(v);
|
|
10
|
+
const onlyKeys = (v, keys) => Object.keys(v).every(key => keys.includes(key));
|
|
11
|
+
function tokens(v) {
|
|
12
|
+
return record(v) && onlyKeys(v, ["input", "output", "total", "window", "windowPeak"]) && [v.input, v.output, v.total].every(count) && [v.window, v.windowPeak].every(n => n === undefined || count(n));
|
|
13
|
+
}
|
|
14
|
+
/** Reject malformed or oversized trusted-extension payloads before JSONL publication. */
|
|
15
|
+
export function isSubagentRpcProjection(v) {
|
|
16
|
+
if (!record(v) || !onlyKeys(v, ["fleet", "runs", "omitted"]) || !record(v.fleet) || !record(v.omitted) || !Array.isArray(v.runs) || v.runs.length > 32)
|
|
17
|
+
return false;
|
|
18
|
+
const fleet = v.fleet;
|
|
19
|
+
if (!onlyKeys(fleet, ["version", "entries", "totalActive", "topLevelAsyncCapacity", "omitted"]) || !onlyKeys(v.omitted, ["runs", "children"])
|
|
20
|
+
|| fleet.version !== 1 || !Array.isArray(fleet.entries) || fleet.entries.length > 16 || !count(fleet.totalActive) || !count(fleet.omitted)
|
|
21
|
+
|| fleet.totalActive !== fleet.entries.length + fleet.omitted || !record(fleet.topLevelAsyncCapacity)
|
|
22
|
+
|| !onlyKeys(fleet.topLevelAsyncCapacity, ["used", "limit"]) || !count(fleet.topLevelAsyncCapacity.used) || !count(fleet.topLevelAsyncCapacity.limit) || !count(v.omitted.runs) || !count(v.omitted.children))
|
|
23
|
+
return false;
|
|
24
|
+
if (!fleet.entries.every(e => record(e) && onlyKeys(e, ["key", "agent", "role", "model", "effort", "startedAt", "tokens", "goal"]) && identity(e.key) && text(e.agent, 96) && count(e.startedAt) && tokens(e.tokens)
|
|
25
|
+
&& optionalText(e.role, 96) && optionalText(e.model, 128) && optionalText(e.effort, 128) && optionalText(e.goal, 512)))
|
|
26
|
+
return false;
|
|
27
|
+
let nodes = 0;
|
|
28
|
+
let omittedChildren = 0;
|
|
29
|
+
const keys = new Set();
|
|
30
|
+
function node(n, depth) {
|
|
31
|
+
if (!record(n) || !onlyKeys(n, ["key", "runId", "workflowId", "agent", "status", "activity", "startedAt", "updatedAt", "endedAt", "progress", "currentTool", "tokens", "error", "reason", "outcome", "processTerminal", "children", "omittedChildren"]) || depth > 4 || ++nodes > 64 || !identity(n.key) || keys.has(n.key) || !statuses.has(n.status)
|
|
32
|
+
|| !Array.isArray(n.children) || n.children.length > 16 || !count(n.omittedChildren))
|
|
33
|
+
return false;
|
|
34
|
+
keys.add(n.key);
|
|
35
|
+
omittedChildren += n.omittedChildren;
|
|
36
|
+
if ([n.runId, n.workflowId].some(id => id !== undefined && !identity(id))
|
|
37
|
+
|| !optionalText(n.agent, 96) || !optionalText(n.progress, 256) || !optionalText(n.currentTool, 96)
|
|
38
|
+
|| !optionalText(n.error, 256) || !optionalText(n.reason, 128)
|
|
39
|
+
|| [n.startedAt, n.updatedAt, n.endedAt].some(t => t !== undefined && !count(t))
|
|
40
|
+
|| n.activity !== undefined && n.activity !== "waiting_supervisor" && n.activity !== "needs_attention"
|
|
41
|
+
|| n.tokens !== undefined && !tokens(n.tokens))
|
|
42
|
+
return false;
|
|
43
|
+
if (n.outcome !== undefined && (!record(n.outcome) || !onlyKeys(n.outcome, ["status", "success", "exitCode"]) || !statuses.has(n.outcome.status)
|
|
44
|
+
|| n.outcome.success !== undefined && typeof n.outcome.success !== "boolean"
|
|
45
|
+
|| n.outcome.exitCode !== undefined && (typeof n.outcome.exitCode !== "number" || !Number.isSafeInteger(n.outcome.exitCode))))
|
|
46
|
+
return false;
|
|
47
|
+
if (n.processTerminal !== undefined && (!record(n.processTerminal) || !onlyKeys(n.processTerminal, ["state", "observedAt", "reason"]) || !["pending", "observed", "unknown", "not-started"].includes(n.processTerminal.state)
|
|
48
|
+
|| !optionalText(n.processTerminal.reason, 128) || n.processTerminal.observedAt !== undefined && !count(n.processTerminal.observedAt)))
|
|
49
|
+
return false;
|
|
50
|
+
return n.children.every(child => node(child, depth + 1));
|
|
51
|
+
}
|
|
52
|
+
if (!v.runs.every(n => node(n, 0)) || omittedChildren !== v.omitted.children)
|
|
53
|
+
return false;
|
|
54
|
+
try {
|
|
55
|
+
return Buffer.byteLength(JSON.stringify(v), "utf8") <= SUBAGENT_RPC_MAX_BYTES - 1024;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -146,6 +146,15 @@ export default function agentBrowserSetupExtension(pi: ExtensionAPI): void {
|
|
|
146
146
|
if (event.reason !== "startup") return;
|
|
147
147
|
if (process.env[SKIP_ENV] === "1") return;
|
|
148
148
|
if (isDeclined()) return;
|
|
149
|
+
// Startup activation is awaited before the RPC command loop serves stdin, so a
|
|
150
|
+
// startup confirm can deadlock headless RPC hosts that never render it. Warn once
|
|
151
|
+
// instead; /setup-agent-browser re-runs the interactive flow in any mode.
|
|
152
|
+
if (ctx.mode === "rpc") {
|
|
153
|
+
if (!(await isAgentBrowserInstalled(pi))) {
|
|
154
|
+
notify(ctx, `agent-browser CLI not found; install with: ${INSTALL_COMMAND}`, "warning");
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
149
158
|
await ensureAgentBrowser(pi, ctx);
|
|
150
159
|
});
|
|
151
160
|
|