@fitchmultz/pi-subagents 0.0.0-stage → 0.44.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 +1645 -0
- package/LICENSE +21 -0
- package/README.md +1529 -2
- package/agents/context-builder.md +97 -0
- package/agents/debugger.md +51 -0
- package/agents/delegate.md +13 -0
- package/agents/fixer.md +37 -0
- package/agents/oracle.md +71 -0
- package/agents/planner.md +72 -0
- package/agents/researcher.md +51 -0
- package/agents/reviewer-claude.md +69 -0
- package/agents/reviewer-gpt.md +68 -0
- package/agents/reviewer-ponytail.md +82 -0
- package/agents/reviewer-security.md +81 -0
- package/agents/reviewer.md +69 -0
- package/agents/scout.md +66 -0
- package/agents/ui-designer.md +51 -0
- package/agents/watcher.md +26 -0
- package/agents/worker.md +46 -0
- package/agents/writer.md +36 -0
- package/dist/agents/agent-management.js +923 -0
- package/dist/agents/agent-scope.js +7 -0
- package/dist/agents/agent-selection.js +20 -0
- package/dist/agents/agent-serializer.js +100 -0
- package/dist/agents/agents.js +752 -0
- package/dist/agents/chain-serializer.js +367 -0
- package/dist/agents/frontmatter.js +21 -0
- package/dist/agents/identity.js +29 -0
- package/dist/agents/skills.js +570 -0
- package/dist/extension/build-info.js +1 -0
- package/dist/extension/compact-tools.js +112 -0
- package/dist/extension/config.js +22 -0
- package/dist/extension/control-notices.js +29 -0
- package/dist/extension/doctor.js +143 -0
- package/dist/extension/fanout-child.js +326 -0
- package/dist/extension/index.js +577 -0
- package/dist/extension/schemas.js +416 -0
- package/dist/extension/tool-input.js +20 -0
- package/dist/extension/tool-result.js +14 -0
- package/dist/history/canonical-result.js +81 -0
- package/dist/history/database.js +72 -0
- package/dist/history/index.js +194 -0
- package/dist/history/ingest.js +447 -0
- package/dist/history/queries.js +350 -0
- package/dist/history/store.js +216 -0
- package/dist/history/types.js +4 -0
- package/dist/history/worker.js +461 -0
- package/dist/intercom/intercom-bridge.js +112 -0
- package/dist/intercom/live-intercom.js +94 -0
- package/dist/intercom/result-intercom.js +373 -0
- package/dist/pi-intercom/agent-dir.js +10 -0
- package/dist/pi-intercom/broker/broker.js +426 -0
- package/dist/pi-intercom/broker/client.js +492 -0
- package/dist/pi-intercom/broker/framing.js +65 -0
- package/dist/pi-intercom/broker/paths.js +42 -0
- package/dist/pi-intercom/broker/pid.js +66 -0
- package/dist/pi-intercom/broker/spawn.js +221 -0
- package/dist/pi-intercom/config.js +102 -0
- package/dist/pi-intercom/index.js +2942 -0
- package/dist/pi-intercom/reply-tracker.js +182 -0
- package/dist/pi-intercom/session-targets.js +128 -0
- package/dist/pi-intercom/subagent-live-events.js +132 -0
- package/dist/pi-intercom/topics.js +157 -0
- package/dist/pi-intercom/types.js +163 -0
- package/dist/pi-intercom/ui/compose.js +269 -0
- package/dist/pi-intercom/ui/inline-message.js +80 -0
- package/dist/pi-intercom/ui/session-list.js +124 -0
- package/dist/runs/background/async-control.js +95 -0
- package/dist/runs/background/async-execution.js +676 -0
- package/dist/runs/background/async-job-tracker.js +383 -0
- package/dist/runs/background/async-result-file.js +58 -0
- package/dist/runs/background/async-resume.js +305 -0
- package/dist/runs/background/async-status.js +442 -0
- package/dist/runs/background/completion-dedupe.js +69 -0
- package/dist/runs/background/completion-delivery.js +245 -0
- package/dist/runs/background/notify.js +71 -0
- package/dist/runs/background/parallel-groups.js +55 -0
- package/dist/runs/background/result-watcher.js +426 -0
- package/dist/runs/background/run-id-resolver.js +64 -0
- package/dist/runs/background/run-status.js +382 -0
- package/dist/runs/background/stale-run-reconciler.js +335 -0
- package/dist/runs/background/subagent-runner-launcher.js +55 -0
- package/dist/runs/background/subagent-runner.js +1872 -0
- package/dist/runs/background/top-level-async.js +5 -0
- package/dist/runs/foreground/chain-clarify.js +1152 -0
- package/dist/runs/foreground/clarify-invocation.js +72 -0
- package/dist/runs/foreground/execution-input.js +423 -0
- package/dist/runs/foreground/foreground-control.js +817 -0
- package/dist/runs/foreground/question-control.js +77 -0
- package/dist/runs/foreground/run-async-path.js +205 -0
- package/dist/runs/foreground/subagent-executor.js +442 -0
- package/dist/runs/foreground/subagent-params.js +165 -0
- package/dist/runs/foreground/wait-run.js +139 -0
- package/dist/runs/shared/acceptance-contract.js +253 -0
- package/dist/runs/shared/acceptance-evaluation.js +256 -0
- package/dist/runs/shared/acceptance-finalization.js +280 -0
- package/dist/runs/shared/acceptance-reports.js +147 -0
- package/dist/runs/shared/acceptance.js +4 -0
- package/dist/runs/shared/chain-outputs.js +110 -0
- package/dist/runs/shared/child-attempt.js +693 -0
- package/dist/runs/shared/child-execution-cwd.js +96 -0
- package/dist/runs/shared/child-observations.js +114 -0
- package/dist/runs/shared/claude-code.js +199 -0
- package/dist/runs/shared/completion-guard.js +31 -0
- package/dist/runs/shared/dynamic-fanout.js +274 -0
- package/dist/runs/shared/history-index.js +72 -0
- package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
- package/dist/runs/shared/model-fallback.js +185 -0
- package/dist/runs/shared/mutating-tool-guard.js +170 -0
- package/dist/runs/shared/native-finalization.js +101 -0
- package/dist/runs/shared/native-usage.js +153 -0
- package/dist/runs/shared/nested-events.js +700 -0
- package/dist/runs/shared/nested-path.js +48 -0
- package/dist/runs/shared/nested-render.js +108 -0
- package/dist/runs/shared/parallel-utils.js +61 -0
- package/dist/runs/shared/parent-receipts.js +68 -0
- package/dist/runs/shared/parent-usage.js +121 -0
- package/dist/runs/shared/pi-args.js +310 -0
- package/dist/runs/shared/pi-spawn.js +52 -0
- package/dist/runs/shared/run-history-queries.js +45 -0
- package/dist/runs/shared/run-history.js +103 -0
- package/dist/runs/shared/run-id-ambiguity.js +4 -0
- package/dist/runs/shared/run-records.js +679 -0
- package/dist/runs/shared/session-cwd-preload.js +33 -0
- package/dist/runs/shared/single-output.js +222 -0
- package/dist/runs/shared/streaming-text.js +15 -0
- package/dist/runs/shared/structured-output.js +55 -0
- package/dist/runs/shared/subagent-control.js +179 -0
- package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
- package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
- package/dist/runs/shared/supervisor-questions.js +320 -0
- package/dist/runs/shared/workflow-graph.js +214 -0
- package/dist/runs/shared/workflow-policy.js +84 -0
- package/dist/runs/shared/worktree.js +525 -0
- package/dist/shared/agent-context-policy.js +126 -0
- package/dist/shared/artifacts.js +96 -0
- package/dist/shared/atomic-json.js +13 -0
- package/dist/shared/cooperative.js +20 -0
- package/dist/shared/execution-cwd.js +14 -0
- package/dist/shared/file-coalescer.js +21 -0
- package/dist/shared/fork-context.js +49 -0
- package/dist/shared/formatters.js +85 -0
- package/dist/shared/journal-reader.js +407 -0
- package/dist/shared/lazy-tools.js +32 -0
- package/dist/shared/model-info.js +68 -0
- package/dist/shared/native-session.js +15 -0
- package/dist/shared/native-tui.js +18 -0
- package/dist/shared/native-typebox.js +20 -0
- package/dist/shared/post-exit-stdio-guard.js +197 -0
- package/dist/shared/prompt-sections.js +8 -0
- package/dist/shared/session-entries.js +52 -0
- package/dist/shared/session-identity.js +14 -0
- package/dist/shared/settings.js +254 -0
- package/dist/shared/status-format.js +111 -0
- package/dist/shared/temp-root.js +114 -0
- package/dist/shared/types.js +207 -0
- package/dist/shared/ui-mode.js +3 -0
- package/dist/shared/utils.js +334 -0
- package/dist/slash/prompt-template-bridge.js +298 -0
- package/dist/slash/slash-bridge.js +116 -0
- package/dist/slash/slash-commands.js +607 -0
- package/dist/slash/slash-live-state.js +246 -0
- package/dist/tui/action-hints.js +75 -0
- package/dist/tui/agent-history.js +197 -0
- package/dist/tui/agent-view.js +1656 -0
- package/dist/tui/render.js +1515 -0
- package/docs/intercom.md +579 -0
- package/docs/performance.md +121 -0
- package/package.json +101 -4
- package/prompts/gather-context-and-clarify.md +14 -0
- package/prompts/parallel-cleanup.md +62 -0
- package/prompts/parallel-context-build.md +56 -0
- package/prompts/parallel-handoff-plan.md +62 -0
- package/prompts/parallel-research.md +51 -0
- package/prompts/parallel-review.md +59 -0
- package/prompts/review-loop.md +42 -0
- package/scripts/build.mjs +134 -0
- package/scripts/check-compat.mjs +48 -0
- package/scripts/compat-host.mjs +19 -0
- package/scripts/linux-smoke.sh +51 -0
- package/scripts/local-install-smoke.mjs +66 -0
- package/scripts/native-package-smoke.mjs +82 -0
- package/scripts/package-smoke.mjs +205 -0
- package/scripts/prepare.mjs +67 -0
- package/scripts/real-pi-smoke.mjs +415 -0
- package/scripts/run-tests.mjs +149 -0
- package/skills/pi-intercom/SKILL.md +113 -0
- package/skills/pi-intercom/evals/evals.json +29 -0
- package/skills/pi-intercom/evals/trigger-evals.json +47 -0
- package/skills/pi-intercom/references/peer-sessions.md +69 -0
- package/skills/pi-subagents/SKILL.md +76 -0
- package/skills/pi-subagents/evals/evals.json +29 -0
- package/skills/pi-subagents/evals/trigger-evals.json +42 -0
- package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
- package/src/agents/agent-management.ts +813 -0
- package/src/agents/agent-scope.ts +7 -0
- package/src/agents/agent-selection.ts +23 -0
- package/src/agents/agent-serializer.ts +99 -0
- package/src/agents/agents.ts +916 -0
- package/src/agents/chain-serializer.ts +329 -0
- package/src/agents/frontmatter.ts +21 -0
- package/src/agents/identity.ts +30 -0
- package/src/agents/skills.ts +637 -0
- package/src/extension/build-info.ts +2 -0
- package/src/extension/compact-tools.ts +114 -0
- package/src/extension/config.ts +23 -0
- package/src/extension/control-notices.ts +47 -0
- package/src/extension/doctor.ts +171 -0
- package/src/extension/fanout-child.ts +312 -0
- package/src/extension/index.ts +649 -0
- package/src/extension/schemas.ts +444 -0
- package/src/extension/tool-input.ts +19 -0
- package/src/extension/tool-result.ts +16 -0
- package/src/history/canonical-result.ts +62 -0
- package/src/history/database.ts +59 -0
- package/src/history/index.ts +152 -0
- package/src/history/ingest.ts +332 -0
- package/src/history/queries.ts +241 -0
- package/src/history/store.ts +203 -0
- package/src/history/types.ts +166 -0
- package/src/history/worker.ts +317 -0
- package/src/intercom/intercom-bridge.ts +123 -0
- package/src/intercom/live-intercom.ts +104 -0
- package/src/intercom/result-intercom.ts +429 -0
- package/src/pi-intercom/agent-dir.ts +9 -0
- package/src/pi-intercom/broker/broker.ts +463 -0
- package/src/pi-intercom/broker/client.ts +558 -0
- package/src/pi-intercom/broker/framing.ts +77 -0
- package/src/pi-intercom/broker/paths.ts +47 -0
- package/src/pi-intercom/broker/pid.ts +59 -0
- package/src/pi-intercom/broker/spawn.ts +248 -0
- package/src/pi-intercom/config.ts +142 -0
- package/src/pi-intercom/index.ts +2929 -0
- package/src/pi-intercom/reply-tracker.ts +211 -0
- package/src/pi-intercom/session-targets.ts +172 -0
- package/src/pi-intercom/subagent-live-events.ts +133 -0
- package/src/pi-intercom/topics.ts +125 -0
- package/src/pi-intercom/types.ts +271 -0
- package/src/pi-intercom/ui/compose.ts +289 -0
- package/src/pi-intercom/ui/inline-message.ts +92 -0
- package/src/pi-intercom/ui/session-list.ts +139 -0
- package/src/runs/background/async-control.ts +89 -0
- package/src/runs/background/async-execution.ts +850 -0
- package/src/runs/background/async-job-tracker.ts +385 -0
- package/src/runs/background/async-result-file.ts +62 -0
- package/src/runs/background/async-resume.ts +333 -0
- package/src/runs/background/async-status.ts +481 -0
- package/src/runs/background/completion-dedupe.ts +79 -0
- package/src/runs/background/completion-delivery.ts +195 -0
- package/src/runs/background/notify.ts +123 -0
- package/src/runs/background/parallel-groups.ts +53 -0
- package/src/runs/background/result-watcher.ts +423 -0
- package/src/runs/background/run-id-resolver.ts +69 -0
- package/src/runs/background/run-status.ts +384 -0
- package/src/runs/background/stale-run-reconciler.ts +369 -0
- package/src/runs/background/subagent-runner-launcher.ts +56 -0
- package/src/runs/background/subagent-runner.ts +2053 -0
- package/src/runs/background/top-level-async.ts +13 -0
- package/src/runs/foreground/chain-clarify.ts +1340 -0
- package/src/runs/foreground/clarify-invocation.ts +77 -0
- package/src/runs/foreground/execution-input.ts +467 -0
- package/src/runs/foreground/foreground-control.ts +821 -0
- package/src/runs/foreground/question-control.ts +70 -0
- package/src/runs/foreground/run-async-path.ts +243 -0
- package/src/runs/foreground/subagent-executor.ts +501 -0
- package/src/runs/foreground/subagent-params.ts +320 -0
- package/src/runs/foreground/wait-run.ts +121 -0
- package/src/runs/shared/acceptance-contract.ts +257 -0
- package/src/runs/shared/acceptance-evaluation.ts +275 -0
- package/src/runs/shared/acceptance-finalization.ts +353 -0
- package/src/runs/shared/acceptance-reports.ts +152 -0
- package/src/runs/shared/acceptance.ts +33 -0
- package/src/runs/shared/chain-outputs.ts +114 -0
- package/src/runs/shared/child-attempt.ts +623 -0
- package/src/runs/shared/child-execution-cwd.ts +91 -0
- package/src/runs/shared/child-observations.ts +99 -0
- package/src/runs/shared/claude-code.ts +243 -0
- package/src/runs/shared/completion-guard.ts +35 -0
- package/src/runs/shared/dynamic-fanout.ts +295 -0
- package/src/runs/shared/history-index.ts +69 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
- package/src/runs/shared/model-fallback.ts +219 -0
- package/src/runs/shared/mutating-tool-guard.ts +194 -0
- package/src/runs/shared/native-finalization.ts +128 -0
- package/src/runs/shared/native-usage.ts +113 -0
- package/src/runs/shared/nested-events.ts +752 -0
- package/src/runs/shared/nested-path.ts +52 -0
- package/src/runs/shared/nested-render.ts +119 -0
- package/src/runs/shared/parallel-utils.ts +153 -0
- package/src/runs/shared/parent-receipts.ts +59 -0
- package/src/runs/shared/parent-usage.ts +121 -0
- package/src/runs/shared/pi-args.ts +350 -0
- package/src/runs/shared/pi-spawn.ts +52 -0
- package/src/runs/shared/run-history-queries.ts +44 -0
- package/src/runs/shared/run-history.ts +93 -0
- package/src/runs/shared/run-id-ambiguity.ts +4 -0
- package/src/runs/shared/run-records.ts +607 -0
- package/src/runs/shared/session-cwd-preload.ts +27 -0
- package/src/runs/shared/single-output.ts +267 -0
- package/src/runs/shared/streaming-text.ts +18 -0
- package/src/runs/shared/structured-output.ts +68 -0
- package/src/runs/shared/subagent-control.ts +219 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
- package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
- package/src/runs/shared/supervisor-questions.ts +372 -0
- package/src/runs/shared/workflow-graph.ts +220 -0
- package/src/runs/shared/workflow-policy.ts +125 -0
- package/src/runs/shared/worktree.ts +635 -0
- package/src/shared/agent-context-policy.ts +184 -0
- package/src/shared/artifacts.ts +95 -0
- package/src/shared/atomic-json.ts +16 -0
- package/src/shared/cooperative.ts +21 -0
- package/src/shared/execution-cwd.ts +14 -0
- package/src/shared/file-coalescer.ts +29 -0
- package/src/shared/fork-context.ts +68 -0
- package/src/shared/formatters.ts +84 -0
- package/src/shared/journal-reader.ts +333 -0
- package/src/shared/lazy-tools.ts +36 -0
- package/src/shared/model-info.ts +81 -0
- package/src/shared/native-session.ts +8 -0
- package/src/shared/native-tui.ts +10 -0
- package/src/shared/native-typebox.ts +14 -0
- package/src/shared/post-exit-stdio-guard.ts +204 -0
- package/src/shared/prompt-sections.ts +10 -0
- package/src/shared/session-entries.ts +49 -0
- package/src/shared/session-identity.ts +18 -0
- package/src/shared/settings.ts +384 -0
- package/src/shared/status-format.ts +123 -0
- package/src/shared/temp-root.ts +107 -0
- package/src/shared/types.ts +1259 -0
- package/src/shared/ui-mode.ts +5 -0
- package/src/shared/utils.ts +342 -0
- package/src/slash/prompt-template-bridge.ts +408 -0
- package/src/slash/slash-bridge.ts +178 -0
- package/src/slash/slash-commands.ts +617 -0
- package/src/slash/slash-live-state.ts +292 -0
- package/src/tui/action-hints.ts +69 -0
- package/src/tui/agent-history.ts +210 -0
- package/src/tui/agent-view.ts +1323 -0
- package/src/tui/render.ts +1550 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
import net from "net";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
import { writeMessage, createMessageReader } from "./framing.ts";
|
|
5
|
+
import { getBrokerSocketPath, getLegacyBrokerSocketPath, isOwnedBrokerSocket } from "./paths.ts";
|
|
6
|
+
import { compactTopicMessage, normalizeMessage, normalizeSessionInfo } from "../types.ts";
|
|
7
|
+
import type { SessionInfo, Message, Attachment, MessageDelivery, QueueMode, HumanMessageOrigin, TopicUpdate, TopicChange, SessionSnapshot, SendResult } from "../types.ts";
|
|
8
|
+
export type { SendResult } from "../types.ts";
|
|
9
|
+
|
|
10
|
+
/** Default delivery-ack timeout for `send` (broker acknowledges quickly). */
|
|
11
|
+
const DEFAULT_SEND_TIMEOUT_MS = 8000;
|
|
12
|
+
/** Default timeout for `listSessions` responses. */
|
|
13
|
+
const DEFAULT_LIST_TIMEOUT_MS = 5000;
|
|
14
|
+
|
|
15
|
+
export interface IntercomClientOptions {
|
|
16
|
+
/** Timeout (ms) for the broker to acknowledge message delivery. */
|
|
17
|
+
sendTimeoutMs?: number;
|
|
18
|
+
/** Timeout (ms) for a session list response. */
|
|
19
|
+
listTimeoutMs?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SendOptions {
|
|
23
|
+
text: string;
|
|
24
|
+
attachments?: Attachment[];
|
|
25
|
+
replyTo?: string;
|
|
26
|
+
expectsReply?: boolean;
|
|
27
|
+
delivery?: MessageDelivery;
|
|
28
|
+
queueMode?: QueueMode;
|
|
29
|
+
threadId?: string;
|
|
30
|
+
passive?: boolean;
|
|
31
|
+
messageId?: string;
|
|
32
|
+
human?: HumanMessageOrigin;
|
|
33
|
+
topic?: TopicUpdate;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toError(error: unknown): Error {
|
|
37
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function connectSocket(socketPath: string, timeoutMs = 500): Promise<net.Socket> {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const socket = net.connect(socketPath);
|
|
43
|
+
const finish = (error?: Error) => {
|
|
44
|
+
clearTimeout(timeout);
|
|
45
|
+
socket.off("connect", onConnect);
|
|
46
|
+
socket.off("error", onError);
|
|
47
|
+
if (error) {
|
|
48
|
+
socket.destroy();
|
|
49
|
+
reject(error);
|
|
50
|
+
} else {
|
|
51
|
+
resolve(socket);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const onConnect = () => finish();
|
|
55
|
+
const onError = (error: Error) => finish(error);
|
|
56
|
+
socket.once("connect", onConnect);
|
|
57
|
+
socket.once("error", onError);
|
|
58
|
+
const timeout = setTimeout(() => finish(new Error(`Connection timeout: ${socketPath}`)), timeoutMs);
|
|
59
|
+
timeout.unref?.();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function connectBrokerSocket(): Promise<net.Socket> {
|
|
64
|
+
const preferred = getBrokerSocketPath();
|
|
65
|
+
const legacy = getLegacyBrokerSocketPath();
|
|
66
|
+
const candidates = [preferred, ...(legacy !== preferred ? [legacy] : [])];
|
|
67
|
+
let lastError: Error | undefined;
|
|
68
|
+
for (const candidate of candidates) {
|
|
69
|
+
if (!isOwnedBrokerSocket(candidate)) continue;
|
|
70
|
+
try {
|
|
71
|
+
return await connectSocket(candidate);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
lastError = toError(error);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw lastError ?? new Error(`Intercom broker socket is unavailable: ${preferred}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class IntercomClient extends EventEmitter {
|
|
80
|
+
private socket: net.Socket | null = null;
|
|
81
|
+
private _sessionId: string | null = null;
|
|
82
|
+
private _topicsSupported = false;
|
|
83
|
+
private pendingSends = new Map<string, { resolve: (r: SendResult) => void; reject: (e: Error) => void }>();
|
|
84
|
+
private pendingLists = new Map<string, { resolve: (snapshot: SessionSnapshot) => void; reject: (e: Error) => void; sessions: Map<string, SessionInfo>; receipts: SessionSnapshot["receipts"] }>();
|
|
85
|
+
private connecting = false;
|
|
86
|
+
private disconnecting = false;
|
|
87
|
+
private disconnectError: Error | null = null;
|
|
88
|
+
private readonly sendTimeoutMs: number;
|
|
89
|
+
private readonly listTimeoutMs: number;
|
|
90
|
+
|
|
91
|
+
constructor(options: IntercomClientOptions = {}) {
|
|
92
|
+
super();
|
|
93
|
+
this.sendTimeoutMs = options.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS;
|
|
94
|
+
this.listTimeoutMs = options.listTimeoutMs ?? DEFAULT_LIST_TIMEOUT_MS;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private failPending(error: Error): void {
|
|
98
|
+
for (const pending of this.pendingSends.values()) {
|
|
99
|
+
pending.reject(error);
|
|
100
|
+
}
|
|
101
|
+
this.pendingSends.clear();
|
|
102
|
+
for (const pending of this.pendingLists.values()) {
|
|
103
|
+
pending.reject(error);
|
|
104
|
+
}
|
|
105
|
+
this.pendingLists.clear();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get sessionId(): string | null {
|
|
109
|
+
return this._sessionId;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get supportsTopics(): boolean { return this.isConnected() && this._topicsSupported; }
|
|
113
|
+
isConnected(): boolean {
|
|
114
|
+
const socket = this.socket;
|
|
115
|
+
return Boolean(socket && this._sessionId && !this.disconnecting && !socket.destroyed && !socket.writableEnded && socket.writable);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private requireActiveSocket(): net.Socket {
|
|
119
|
+
if (this.disconnecting) {
|
|
120
|
+
throw new Error("Client disconnecting");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const socket = this.socket;
|
|
124
|
+
if (!socket || !this._sessionId) {
|
|
125
|
+
throw new Error("Not connected");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (socket.destroyed || socket.writableEnded || !socket.writable) {
|
|
129
|
+
throw new Error("Client disconnected");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return socket;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async connect(session: Omit<SessionInfo, "id">, requestedId?: string): Promise<void> {
|
|
136
|
+
if (this.socket || this.connecting) {
|
|
137
|
+
throw new Error("Already connected");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.connecting = true;
|
|
141
|
+
let socket: net.Socket;
|
|
142
|
+
try {
|
|
143
|
+
socket = await connectBrokerSocket();
|
|
144
|
+
} finally {
|
|
145
|
+
this.connecting = false;
|
|
146
|
+
}
|
|
147
|
+
const { topics, subscriptions, ...identity } = session;
|
|
148
|
+
await new Promise<void>((resolve, reject) => {
|
|
149
|
+
this.socket = socket;
|
|
150
|
+
this.disconnectError = null;
|
|
151
|
+
let settled = false;
|
|
152
|
+
const timeout = setTimeout(() => {
|
|
153
|
+
if (!this._sessionId) {
|
|
154
|
+
cleanupConnectionAttempt();
|
|
155
|
+
cleanupSocketListeners();
|
|
156
|
+
if (this.socket === socket) {
|
|
157
|
+
this.socket = null;
|
|
158
|
+
}
|
|
159
|
+
socket.destroy();
|
|
160
|
+
reject(new Error("Connection timeout"));
|
|
161
|
+
}
|
|
162
|
+
}, 10000);
|
|
163
|
+
timeout.unref?.();
|
|
164
|
+
|
|
165
|
+
let connectionEstablished = false;
|
|
166
|
+
|
|
167
|
+
const onRegistered = () => {
|
|
168
|
+
settled = true;
|
|
169
|
+
connectionEstablished = true;
|
|
170
|
+
cleanupConnectionAttempt();
|
|
171
|
+
resolve();
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const onError = (err: Error) => {
|
|
175
|
+
settled = true;
|
|
176
|
+
cleanupConnectionAttempt();
|
|
177
|
+
cleanupSocketListeners();
|
|
178
|
+
if (this.socket === socket) {
|
|
179
|
+
this.socket = null;
|
|
180
|
+
}
|
|
181
|
+
socket.destroy();
|
|
182
|
+
reject(err);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const onClose = () => {
|
|
186
|
+
const wasConnecting = !settled && !this._sessionId;
|
|
187
|
+
const wasDisconnecting = this.disconnecting;
|
|
188
|
+
const disconnectError = this.disconnectError ?? new Error("Client disconnected");
|
|
189
|
+
this.disconnecting = false;
|
|
190
|
+
cleanupConnectionAttempt();
|
|
191
|
+
cleanupSocketListeners();
|
|
192
|
+
this.failPending(disconnectError);
|
|
193
|
+
if (this.socket === socket) {
|
|
194
|
+
this.socket = null;
|
|
195
|
+
}
|
|
196
|
+
this._sessionId = null;
|
|
197
|
+
this.disconnectError = null;
|
|
198
|
+
if (connectionEstablished && !wasDisconnecting) {
|
|
199
|
+
this.emit("disconnected", disconnectError);
|
|
200
|
+
}
|
|
201
|
+
if (wasConnecting) {
|
|
202
|
+
reject(new Error("Connection closed before registration"));
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const onSocketError = (err: Error) => {
|
|
207
|
+
if (connectionEstablished) {
|
|
208
|
+
this.disconnectError = err;
|
|
209
|
+
this.emit("error", err);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const onReaderError = (error: Error) => {
|
|
214
|
+
const protocolError = new Error(`Intercom protocol error: ${error.message}`, { cause: error });
|
|
215
|
+
if (!connectionEstablished) {
|
|
216
|
+
onError(protocolError);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
this.disconnectError = protocolError;
|
|
220
|
+
this.emit("error", protocolError);
|
|
221
|
+
socket.destroy();
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const reader = createMessageReader((msg) => {
|
|
225
|
+
this.handleBrokerMessage(msg);
|
|
226
|
+
}, onReaderError);
|
|
227
|
+
|
|
228
|
+
const cleanupConnectionAttempt = () => {
|
|
229
|
+
this.off("_registered", onRegistered);
|
|
230
|
+
socket.off("error", onError);
|
|
231
|
+
clearTimeout(timeout);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const cleanupSocketListeners = () => {
|
|
235
|
+
socket.off("data", reader);
|
|
236
|
+
socket.off("error", onSocketError);
|
|
237
|
+
socket.off("close", onClose);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
socket.on("data", reader);
|
|
241
|
+
socket.on("error", onError);
|
|
242
|
+
socket.on("close", onClose);
|
|
243
|
+
|
|
244
|
+
socket.on("error", onSocketError);
|
|
245
|
+
this.once("_registered", onRegistered);
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
writeMessage(socket, { type: "register", session: identity, requestedId, topicFrames: true });
|
|
249
|
+
} catch (error) {
|
|
250
|
+
cleanupConnectionAttempt();
|
|
251
|
+
cleanupSocketListeners();
|
|
252
|
+
if (this.socket === socket) {
|
|
253
|
+
this.socket = null;
|
|
254
|
+
}
|
|
255
|
+
socket.destroy();
|
|
256
|
+
reject(toError(error));
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
if (this.supportsTopics) {
|
|
260
|
+
for (const topic of topics ?? []) await this.updateTopics({ action: "restore", topic });
|
|
261
|
+
for (const subscription of subscriptions ?? []) await this.updateTopics({ action: "restore", subscription });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private handleBrokerMessage(msg: unknown): void {
|
|
266
|
+
if (typeof msg !== "object" || msg === null || !("type" in msg) || typeof msg.type !== "string") {
|
|
267
|
+
throw new Error("Invalid broker message");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const brokerMessage = msg as { type: string } & Record<string, unknown>;
|
|
271
|
+
|
|
272
|
+
if (this._sessionId === null && brokerMessage.type !== "registered") {
|
|
273
|
+
throw new Error(`Received ${brokerMessage.type} before registered`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
switch (brokerMessage.type) {
|
|
277
|
+
case "registered": {
|
|
278
|
+
if (typeof brokerMessage.sessionId !== "string") {
|
|
279
|
+
throw new Error("Invalid registered message");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (this._sessionId !== null) {
|
|
283
|
+
throw new Error("Received duplicate registered message");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
this._sessionId = brokerMessage.sessionId;
|
|
287
|
+
this._topicsSupported = brokerMessage.topicsSupported === true && brokerMessage.topicFrames === true;
|
|
288
|
+
this.emit("_registered", { type: "registered", sessionId: brokerMessage.sessionId });
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
case "sessions": {
|
|
293
|
+
const { requestId, sessions, receipts, more, error } = brokerMessage;
|
|
294
|
+
if (typeof requestId !== "string" || !Array.isArray(sessions) || (more !== undefined && typeof more !== "boolean")) throw new Error("Invalid sessions message");
|
|
295
|
+
const pending = this.pendingLists.get(requestId);
|
|
296
|
+
if (!pending) return;
|
|
297
|
+
if (typeof error === "string") {
|
|
298
|
+
this.pendingLists.delete(requestId);
|
|
299
|
+
pending.reject(new Error(error));
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
for (const row of sessions) {
|
|
303
|
+
if (!row || typeof row.id !== "string" || (row.topics !== undefined && !Array.isArray(row.topics)) || (row.subscriptions !== undefined && !Array.isArray(row.subscriptions))) throw new Error("Invalid session snapshot row");
|
|
304
|
+
const previous = pending.sessions.get(row.id);
|
|
305
|
+
const info = normalizeSessionInfo({ ...previous, ...row,
|
|
306
|
+
...(row.topics ? { topics: [...(previous?.topics ?? []), ...row.topics] } : {}),
|
|
307
|
+
...(row.subscriptions ? { subscriptions: [...(previous?.subscriptions ?? []), ...row.subscriptions] } : {}),
|
|
308
|
+
});
|
|
309
|
+
if (!info) throw new Error("Invalid session snapshot");
|
|
310
|
+
pending.sessions.set(info.id, info);
|
|
311
|
+
}
|
|
312
|
+
if (receipts !== undefined) {
|
|
313
|
+
if (!Array.isArray(receipts) || receipts.some((receipt) => !receipt || typeof receipt.to !== "string" || typeof receipt.id !== "string" || typeof receipt.accepted !== "boolean" || typeof receipt.delivered !== "boolean")) throw new Error("Invalid topic delivery receipts");
|
|
314
|
+
pending.receipts.push(...receipts);
|
|
315
|
+
}
|
|
316
|
+
if (more !== true) {
|
|
317
|
+
this.pendingLists.delete(requestId);
|
|
318
|
+
pending.resolve({ sessions: [...pending.sessions.values()], receipts: pending.receipts });
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
case "message": {
|
|
324
|
+
const { from } = brokerMessage;
|
|
325
|
+
const message = normalizeMessage(brokerMessage.message);
|
|
326
|
+
const normalizedFrom = normalizeSessionInfo(from);
|
|
327
|
+
if (!normalizedFrom || !message) {
|
|
328
|
+
throw new Error("Invalid message event");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
this.emit("message", normalizedFrom, message);
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
case "delivered": {
|
|
336
|
+
const { messageId } = brokerMessage;
|
|
337
|
+
if (typeof messageId !== "string") {
|
|
338
|
+
throw new Error("Invalid delivered message");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const pending = this.pendingSends.get(messageId);
|
|
342
|
+
if (!pending) {
|
|
343
|
+
// Late send responses are harmless once the caller has already timed out.
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
this.pendingSends.delete(messageId);
|
|
348
|
+
pending.resolve({ id: messageId, accepted: true, delivered: true });
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
case "delivery_queued": {
|
|
353
|
+
const { messageId, reason } = brokerMessage;
|
|
354
|
+
if (typeof messageId !== "string" || typeof reason !== "string") {
|
|
355
|
+
throw new Error("Invalid delivery_queued message");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const pending = this.pendingSends.get(messageId);
|
|
359
|
+
if (!pending) {
|
|
360
|
+
// Late send responses are harmless once the caller has already timed out.
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
this.pendingSends.delete(messageId);
|
|
365
|
+
pending.resolve({ id: messageId, accepted: true, delivered: false, queued: true, reason });
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case "delivery_failed": {
|
|
370
|
+
const { messageId, reason } = brokerMessage;
|
|
371
|
+
if (typeof messageId !== "string" || typeof reason !== "string") {
|
|
372
|
+
throw new Error("Invalid delivery_failed message");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const pending = this.pendingSends.get(messageId);
|
|
376
|
+
if (!pending) {
|
|
377
|
+
// Late send responses are harmless once the caller has already timed out.
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.pendingSends.delete(messageId);
|
|
382
|
+
pending.resolve({ id: messageId, accepted: false, delivered: false, reason });
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
case "session_left": {
|
|
387
|
+
if (typeof brokerMessage.sessionId !== "string") {
|
|
388
|
+
throw new Error("Invalid session_left message");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.emit("session_left", brokerMessage.sessionId);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
default:
|
|
396
|
+
throw new Error(`Unknown broker message type: ${brokerMessage.type}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async disconnect(): Promise<void> {
|
|
401
|
+
const socket = this.socket;
|
|
402
|
+
if (!socket) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.disconnecting = true;
|
|
407
|
+
this.disconnectError = null;
|
|
408
|
+
this.failPending(new Error("Client disconnected"));
|
|
409
|
+
|
|
410
|
+
await new Promise<void>((resolve) => {
|
|
411
|
+
let settled = false;
|
|
412
|
+
const finish = () => {
|
|
413
|
+
if (settled) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
settled = true;
|
|
417
|
+
clearTimeout(timeout);
|
|
418
|
+
socket.off("close", onClose);
|
|
419
|
+
socket.off("error", onError);
|
|
420
|
+
resolve();
|
|
421
|
+
};
|
|
422
|
+
const onClose = () => finish();
|
|
423
|
+
const onError = () => {
|
|
424
|
+
socket.destroy();
|
|
425
|
+
};
|
|
426
|
+
const timeout = setTimeout(() => {
|
|
427
|
+
socket.destroy();
|
|
428
|
+
}, 2000);
|
|
429
|
+
|
|
430
|
+
socket.once("close", onClose);
|
|
431
|
+
socket.once("error", onError);
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
writeMessage(socket, { type: "unregister" });
|
|
435
|
+
socket.end();
|
|
436
|
+
} catch {
|
|
437
|
+
// Disconnect should still finish even if the unregister write fails.
|
|
438
|
+
socket.destroy();
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
listSessions(): Promise<SessionInfo[]> {
|
|
444
|
+
return this.requestSessions().then((snapshot) => snapshot.sessions);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
updateTopics(change: TopicChange, onAccepted?: (snapshot: SessionSnapshot) => void): Promise<SessionSnapshot> {
|
|
448
|
+
if (!this.supportsTopics) return Promise.reject(new Error("This broker does not support topic snapshots."));
|
|
449
|
+
return this.requestSessions(change, onAccepted);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private requestSessions(change?: TopicChange, onAccepted?: (snapshot: SessionSnapshot) => void): Promise<SessionSnapshot> {
|
|
453
|
+
let socket: net.Socket;
|
|
454
|
+
try {
|
|
455
|
+
socket = this.requireActiveSocket();
|
|
456
|
+
} catch (error) {
|
|
457
|
+
return Promise.reject(toError(error));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return new Promise((resolve, reject) => {
|
|
461
|
+
const requestId = randomUUID();
|
|
462
|
+
const wrappedResolve = (snapshot: SessionSnapshot) => {
|
|
463
|
+
clearTimeout(timeout);
|
|
464
|
+
try {
|
|
465
|
+
// Commit confirmed subscriptions before handling a following message in the same read.
|
|
466
|
+
onAccepted?.(snapshot);
|
|
467
|
+
resolve(snapshot);
|
|
468
|
+
} catch (error) { reject(toError(error)); }
|
|
469
|
+
};
|
|
470
|
+
const wrappedReject = (error: Error) => {
|
|
471
|
+
clearTimeout(timeout);
|
|
472
|
+
reject(error);
|
|
473
|
+
};
|
|
474
|
+
const timeout = setTimeout(() => {
|
|
475
|
+
if (this.pendingLists.has(requestId)) {
|
|
476
|
+
this.pendingLists.delete(requestId);
|
|
477
|
+
wrappedReject(new Error("List sessions timeout"));
|
|
478
|
+
}
|
|
479
|
+
}, this.listTimeoutMs);
|
|
480
|
+
timeout.unref?.();
|
|
481
|
+
this.pendingLists.set(requestId, { resolve: wrappedResolve, reject: wrappedReject, sessions: new Map(), receipts: [] });
|
|
482
|
+
try {
|
|
483
|
+
writeMessage(socket, { type: "list", requestId, stream: true, ...(change ? { change } : {}) });
|
|
484
|
+
} catch (error) {
|
|
485
|
+
clearTimeout(timeout);
|
|
486
|
+
this.pendingLists.delete(requestId);
|
|
487
|
+
reject(toError(error));
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
send(to: string, options: SendOptions): Promise<SendResult> {
|
|
493
|
+
let socket: net.Socket;
|
|
494
|
+
try {
|
|
495
|
+
socket = this.requireActiveSocket();
|
|
496
|
+
} catch (error) {
|
|
497
|
+
return Promise.reject(toError(error));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const messageId = options.messageId ?? randomUUID();
|
|
501
|
+
const message: Message = {
|
|
502
|
+
id: messageId,
|
|
503
|
+
timestamp: Date.now(),
|
|
504
|
+
replyTo: options.replyTo,
|
|
505
|
+
expectsReply: options.expectsReply,
|
|
506
|
+
...(options.human ? { human: options.human } : {}),
|
|
507
|
+
...(options.topic ? { topic: options.topic } : {}),
|
|
508
|
+
delivery: options.delivery ?? (options.passive === true ? "passive" : options.expectsReply === true ? undefined : "steer"),
|
|
509
|
+
queueMode: options.queueMode,
|
|
510
|
+
threadId: options.threadId,
|
|
511
|
+
passive: options.passive,
|
|
512
|
+
content: {
|
|
513
|
+
text: options.text,
|
|
514
|
+
attachments: options.attachments,
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
return new Promise((resolve, reject) => {
|
|
519
|
+
const wrappedResolve = (result: SendResult) => {
|
|
520
|
+
clearTimeout(timeout);
|
|
521
|
+
resolve(result);
|
|
522
|
+
};
|
|
523
|
+
const wrappedReject = (error: Error) => {
|
|
524
|
+
clearTimeout(timeout);
|
|
525
|
+
reject(error);
|
|
526
|
+
};
|
|
527
|
+
const timeout = setTimeout(() => {
|
|
528
|
+
if (this.pendingSends.has(messageId)) {
|
|
529
|
+
this.pendingSends.delete(messageId);
|
|
530
|
+
wrappedReject(new Error("Send timeout"));
|
|
531
|
+
}
|
|
532
|
+
}, this.sendTimeoutMs);
|
|
533
|
+
timeout.unref?.();
|
|
534
|
+
this.pendingSends.set(messageId, { resolve: wrappedResolve, reject: wrappedReject });
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
writeMessage(socket, { type: "send", to, message: this._topicsSupported ? compactTopicMessage(message) : message });
|
|
538
|
+
} catch (error) {
|
|
539
|
+
clearTimeout(timeout);
|
|
540
|
+
this.pendingSends.delete(messageId);
|
|
541
|
+
reject(toError(error));
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
updatePresence(updates: { name?: string; status?: string; model?: string; pendingAsks?: number; acceptsAsks?: boolean; lastIntercomActivity?: number; subscriptions?: SessionInfo["subscriptions"]; topics?: TopicUpdate[] }): void {
|
|
547
|
+
if (this.disconnecting) {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const socket = this.socket;
|
|
552
|
+
if (!socket || !this._sessionId || socket.destroyed || socket.writableEnded || !socket.writable) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
writeMessage(socket, { type: "presence", ...updates });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Socket } from "net";
|
|
2
|
+
|
|
3
|
+
export const MAX_FRAME_SIZE_BYTES = 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
export function intercomMessageSizeBytes(msg: unknown): number {
|
|
6
|
+
return Buffer.byteLength(JSON.stringify(msg), "utf-8");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function validateIntercomMessageSize(msg: unknown): Error | null {
|
|
10
|
+
const size = intercomMessageSizeBytes(msg);
|
|
11
|
+
return size > MAX_FRAME_SIZE_BYTES
|
|
12
|
+
? new Error(`Intercom message too large (${size} bytes; max ${MAX_FRAME_SIZE_BYTES})`)
|
|
13
|
+
: null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Write a length-prefixed message to a socket.
|
|
18
|
+
* Format: 4-byte big-endian length + JSON payload
|
|
19
|
+
*/
|
|
20
|
+
export function writeMessage(socket: Socket, msg: unknown): void {
|
|
21
|
+
const tooLarge = validateIntercomMessageSize(msg);
|
|
22
|
+
if (tooLarge) throw tooLarge;
|
|
23
|
+
const json = JSON.stringify(msg);
|
|
24
|
+
const payload = Buffer.from(json, "utf-8");
|
|
25
|
+
const header = Buffer.alloc(4);
|
|
26
|
+
header.writeUInt32BE(payload.length, 0);
|
|
27
|
+
socket.write(Buffer.concat([header, payload]));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a message reader that handles partial reads.
|
|
32
|
+
* Calls onMessage for each complete message received.
|
|
33
|
+
* Protocol or handler errors are reported to onError so the caller can close the socket.
|
|
34
|
+
*/
|
|
35
|
+
export function createMessageReader(
|
|
36
|
+
onMessage: (msg: unknown) => void,
|
|
37
|
+
onError: (error: Error) => void,
|
|
38
|
+
) {
|
|
39
|
+
let buffer = Buffer.alloc(0);
|
|
40
|
+
|
|
41
|
+
return (data: Buffer) => {
|
|
42
|
+
buffer = Buffer.concat([buffer, data]);
|
|
43
|
+
|
|
44
|
+
while (buffer.length >= 4) {
|
|
45
|
+
const length = buffer.readUInt32BE(0);
|
|
46
|
+
if (length > MAX_FRAME_SIZE_BYTES) {
|
|
47
|
+
onError(new Error(`Intercom frame too large (${length} bytes; max ${MAX_FRAME_SIZE_BYTES})`));
|
|
48
|
+
buffer = Buffer.alloc(0);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (buffer.length < 4 + length) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const payload = buffer.subarray(4, 4 + length);
|
|
57
|
+
buffer = buffer.subarray(4 + length);
|
|
58
|
+
|
|
59
|
+
let msg: unknown;
|
|
60
|
+
try {
|
|
61
|
+
msg = JSON.parse(payload.toString("utf-8"));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
64
|
+
onError(new Error(`Failed to parse intercom message: ${message}`, { cause: error }));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
onMessage(msg);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
onError(new Error(`Failed to handle intercom message: ${message}`, { cause: error }));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { getPiAgentDir } from "../agent-dir.ts";
|
|
6
|
+
|
|
7
|
+
const MAX_UNIX_SOCKET_PATH_BYTES = 100;
|
|
8
|
+
|
|
9
|
+
function agentDigest(agentDir: string, uid?: number): string {
|
|
10
|
+
return createHash("sha256").update(uid === undefined ? agentDir : `${uid}:${agentDir}`).digest("hex").slice(0, 16);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getLegacyBrokerSocketPath(agentDir: string = getPiAgentDir()): string {
|
|
14
|
+
return join(tmpdir(), `pi-intercom-${agentDigest(agentDir)}.sock`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isOwnedBrokerSocket(socketPath: string): boolean {
|
|
18
|
+
try {
|
|
19
|
+
const stat = fs.lstatSync(socketPath);
|
|
20
|
+
const uid = process.getuid?.();
|
|
21
|
+
return stat.isSocket() && (uid === undefined || stat.uid === uid);
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getBrokerSocketPath(agentDir: string = getPiAgentDir()): string {
|
|
28
|
+
const uid = process.getuid?.();
|
|
29
|
+
const suffix = `pi-intercom-${agentDigest(agentDir, uid)}`;
|
|
30
|
+
const preferred = join(tmpdir(), suffix, "broker.sock");
|
|
31
|
+
return Buffer.byteLength(preferred) <= MAX_UNIX_SOCKET_PATH_BYTES
|
|
32
|
+
? preferred
|
|
33
|
+
: join("/tmp", suffix, "broker.sock");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function prepareBrokerSocketPath(): string {
|
|
37
|
+
const socketPath = getBrokerSocketPath();
|
|
38
|
+
const brokerDir = dirname(socketPath);
|
|
39
|
+
fs.mkdirSync(brokerDir, { recursive: true, mode: 0o700 });
|
|
40
|
+
const stat = fs.lstatSync(brokerDir);
|
|
41
|
+
const uid = process.getuid?.();
|
|
42
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (uid !== undefined && stat.uid !== uid)) {
|
|
43
|
+
throw new Error(`Unsafe intercom socket directory: ${brokerDir}`);
|
|
44
|
+
}
|
|
45
|
+
fs.chmodSync(brokerDir, 0o700);
|
|
46
|
+
return socketPath;
|
|
47
|
+
}
|