@selesai/code 0.13.39 → 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/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-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 +4 -2
- 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
|
@@ -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
|
|
|
@@ -143,6 +143,19 @@ See auth.ts:142-156.
|
|
|
143
143
|
|
|
144
144
|
The reply hint (enabled by default) points to `intercom({ action: "reply", ... })`, so recipients do not need raw sender or `replyTo` IDs. Idle recipients get a new turn immediately; busy interactive recipients receive the message through Selesai's steering queue at the next safe model boundary without aborting the active turn. Attachment content is included in the agent-visible body, and messages are rendered inline and stored in Selesai session history.
|
|
145
145
|
|
|
146
|
+
## Desktop / RPC clients
|
|
147
|
+
|
|
148
|
+
RPC clients can use typed `intercom_status`, `intercom_list`, `intercom_send`, `intercom_ask`, `intercom_reply`, and `intercom_pending` commands directly, without a model turn or the TUI-only `/intercom` overlay. They reuse this extension's broker connection, reply tracker, scope, and consent policy. Ordinary/inferred sends honor `confirmSend`; explicit replies and asks skip confirmation as in the tool. Missing/disabled extensions fail explicitly.
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{"id":"peers","type":"intercom_list"}
|
|
152
|
+
{"id":"send","type":"intercom_send","to":"peer-full-id","message":"Build finished."}
|
|
153
|
+
{"id":"ask","type":"intercom_ask","to":"peer-full-id","message":"Approve?","timeoutMs":60000}
|
|
154
|
+
{"id":"reply","type":"intercom_reply","replyTo":"question-id","message":"Approved."}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Structured connection, peer join/leave, presence, incoming message, delivery, and receipt events stream while idle or busy. Command `id` correlates responses; Intercom `messageId`/`replyTo` correlate messages and receipts. An ask timeout does not cancel delivered work. See [the RPC protocol](../../../docs/rpc.md#intercom-desktop--headless-clients) for response shapes, event examples, timeout limits, confirmation dialogs, and typed client helpers.
|
|
158
|
+
|
|
146
159
|
## Workflow: Planner-Worker Coordination
|
|
147
160
|
|
|
148
161
|
The most natural use of pi-intercom is splitting a task between two sessions — one holds the big picture, the other does the hands-on work. When the worker hits an ambiguity ("should I optimize for readability or performance here?"), they ask without losing context.
|
|
@@ -610,3 +623,5 @@ Use pi-messenger for multi-agent swarms working on a shared task. Use pi-interco
|
|
|
610
623
|
- **No attachments UI** — `file`, `snippet`, and `context` attachments are supported in the protocol, but not in the compose overlay
|
|
611
624
|
- **Only connected sessions appear** — The list shows Selesai sessions that have loaded `pi-intercom` and successfully registered with the broker, not every open Selesai process on the machine
|
|
612
625
|
- **Broker lifecycle** — The broker auto-spawns on first use and exits when idle; sessions reconnect automatically if the broker restarts
|
|
626
|
+
|
|
627
|
+
All Intercom events and `intercom_status.data` carry `scope: {version:1,sessionId,epoch}`. `scope.sessionId` is the host parent SDK session ID, not a path or broker endpoint. Existing top-level `sessionId` still identifies broker self (status/connection) or departing peer (leave). The epoch is shared with subagent snapshots and rotates on rebind/reload; discard obsolete scope pairs. Broker reconnect within a binding keeps the epoch.
|