@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,59 @@
|
|
|
1
|
+
import { closeSync, openSync, readFileSync, readlinkSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
// Keep the first numeric line readable by older brokers/spawn guards.
|
|
4
|
+
// starttime is /proc/<pid>/stat field 22, in clock ticks; never round it to Number.
|
|
5
|
+
const IDENTITY = /^linux-v1 ([0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}) (pid:\[\d+\]) (time:\[\d+\]) (\d+)$/;
|
|
6
|
+
|
|
7
|
+
export function readLinuxProcess(pid: number): { tgid: number; identity?: string } | undefined {
|
|
8
|
+
if (process.platform !== "linux") return undefined;
|
|
9
|
+
let fd: number | undefined;
|
|
10
|
+
try {
|
|
11
|
+
// A procfs mounted from an ancestor PID namespace has different PID numbers
|
|
12
|
+
// from kill(2)/process.pid. Do not use that view to disprove broker identity.
|
|
13
|
+
const self = readFileSync("/proc/self/status", "utf8");
|
|
14
|
+
if (!new RegExp(`^NStgid:[ \\t]+${process.pid}$`, "m").test(self)) return undefined;
|
|
15
|
+
// Pin this proc directory so separate metadata reads cannot cross PID reuse.
|
|
16
|
+
fd = openSync(`/proc/${pid}`, "r");
|
|
17
|
+
const base = `/proc/self/fd/${fd}`;
|
|
18
|
+
const status = readFileSync(`${base}/status`, "utf8");
|
|
19
|
+
const tgid = Number(/^Tgid:[ \t]+(\d+)$/m.exec(status)?.[1]);
|
|
20
|
+
if (!Number.isSafeInteger(tgid) || tgid <= 0
|
|
21
|
+
|| Number(/^Pid:[ \t]+(\d+)$/m.exec(status)?.[1]) !== pid) return undefined;
|
|
22
|
+
// Node writes process.pid (the thread-group leader), never a worker TID.
|
|
23
|
+
if (tgid !== pid) return { tgid };
|
|
24
|
+
const stat = readFileSync(`${base}/stat`, "utf8");
|
|
25
|
+
// comm can contain spaces, newlines and ')'; fields after its final ')' are fixed.
|
|
26
|
+
if (!stat.startsWith(`${pid} (`)) return undefined;
|
|
27
|
+
const startTicks = stat.slice(stat.lastIndexOf(")") + 2).split(/\s+/)[19];
|
|
28
|
+
const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
29
|
+
const pidNamespace = readlinkSync(`${base}/ns/pid`);
|
|
30
|
+
// Linux applies the *reader's* time-namespace offset to stat.starttime.
|
|
31
|
+
const timeNamespace = readlinkSync("/proc/self/ns/time");
|
|
32
|
+
const identity = `linux-v1 ${bootId} ${pidNamespace} ${timeNamespace} ${startTicks}`;
|
|
33
|
+
return IDENTITY.test(identity) ? { tgid, identity } : undefined;
|
|
34
|
+
} catch {
|
|
35
|
+
// Unavailable procfs, permissions or incomplete observations prove nothing.
|
|
36
|
+
return undefined;
|
|
37
|
+
} finally {
|
|
38
|
+
if (fd !== undefined) closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function brokerPidRecord(): string {
|
|
43
|
+
const identity = readLinuxProcess(process.pid)?.identity;
|
|
44
|
+
return `${process.pid}\n${identity ? `${identity}\n` : ""}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isBrokerPidReused(pid: number, record: string): boolean {
|
|
48
|
+
const current = readLinuxProcess(pid);
|
|
49
|
+
if (!current) return false;
|
|
50
|
+
if (current.tgid !== pid) return true;
|
|
51
|
+
const saved = IDENTITY.exec(record.slice(record.indexOf("\n") + 1).trim());
|
|
52
|
+
const actual = IDENTITY.exec(current.identity ?? "");
|
|
53
|
+
if (!saved || !actual) return false;
|
|
54
|
+
const [, boot, namespace, clock, start] = saved;
|
|
55
|
+
const [, actualBoot, actualNamespace, actualClock, actualStart] = actual;
|
|
56
|
+
// A clock-namespace change alone is not evidence that the process changed.
|
|
57
|
+
return boot !== actualBoot || namespace !== actualNamespace
|
|
58
|
+
|| (clock === actualClock && start !== actualStart);
|
|
59
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { mkdir } from "fs/promises";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import net from "net";
|
|
7
|
+
import { getPiAgentDir } from "../agent-dir.ts";
|
|
8
|
+
import { getBrokerSocketPath, getLegacyBrokerSocketPath, isOwnedBrokerSocket } from "./paths.ts";
|
|
9
|
+
import { isBrokerPidReused } from "./pid.ts";
|
|
10
|
+
|
|
11
|
+
const INTERCOM_DIR = join(getPiAgentDir(), "intercom");
|
|
12
|
+
const EXTENSION_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
13
|
+
const BROKER_PID = join(INTERCOM_DIR, "broker.pid");
|
|
14
|
+
const BROKER_SPAWN_LOCK = join(INTERCOM_DIR, "broker.spawn.lock");
|
|
15
|
+
|
|
16
|
+
function sleep(ms: number): Promise<void> {
|
|
17
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function usesLegacyTsxDefault(brokerCommand: string, brokerArgs: string[]): boolean {
|
|
21
|
+
return brokerCommand === "npx"
|
|
22
|
+
&& brokerArgs.length === 2
|
|
23
|
+
&& brokerArgs[0] === "--no-install"
|
|
24
|
+
&& brokerArgs[1] === "tsx";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getBrokerLaunchSpec(
|
|
28
|
+
brokerPath: string,
|
|
29
|
+
brokerCommand: string,
|
|
30
|
+
brokerArgs: string[],
|
|
31
|
+
): { command: string; args: string[] } {
|
|
32
|
+
if (usesLegacyTsxDefault(brokerCommand, brokerArgs)) {
|
|
33
|
+
return {
|
|
34
|
+
command: process.execPath,
|
|
35
|
+
args: [brokerPath],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
command: brokerCommand,
|
|
41
|
+
args: [...brokerArgs, brokerPath],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getBrokerSpawnOptions(): {
|
|
46
|
+
detached: true;
|
|
47
|
+
stdio: "ignore";
|
|
48
|
+
cwd: string;
|
|
49
|
+
env: NodeJS.ProcessEnv;
|
|
50
|
+
} {
|
|
51
|
+
return {
|
|
52
|
+
detached: true,
|
|
53
|
+
stdio: "ignore",
|
|
54
|
+
cwd: EXTENSION_DIR,
|
|
55
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function toError(error: unknown): Error {
|
|
60
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function spawnBrokerIfNeeded(brokerCommand: string, brokerArgs: string[]): Promise<void> {
|
|
64
|
+
await mkdir(INTERCOM_DIR, { recursive: true });
|
|
65
|
+
|
|
66
|
+
if (await isBrokerRunning()) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ownsLock = acquireSpawnLock();
|
|
71
|
+
if (!ownsLock) {
|
|
72
|
+
await waitForBroker();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
if (await isBrokerRunning()) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await stopUnhealthyBrokerBeforeSpawn();
|
|
81
|
+
|
|
82
|
+
const brokerPath = join(
|
|
83
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
84
|
+
import.meta.url.endsWith(".ts") ? "broker.ts" : "broker.js",
|
|
85
|
+
);
|
|
86
|
+
const launch = getBrokerLaunchSpec(brokerPath, brokerCommand, brokerArgs);
|
|
87
|
+
const child = spawn(launch.command, launch.args, getBrokerSpawnOptions());
|
|
88
|
+
child.unref();
|
|
89
|
+
|
|
90
|
+
await new Promise<void>((resolve, reject) => {
|
|
91
|
+
const cleanup = () => {
|
|
92
|
+
child.off("error", onError);
|
|
93
|
+
child.off("exit", onExit);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const onError = (error: Error) => {
|
|
97
|
+
cleanup();
|
|
98
|
+
reject(new Error(`Failed to spawn intercom broker: ${error.message}`, { cause: error }));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
102
|
+
cleanup();
|
|
103
|
+
if (signal) {
|
|
104
|
+
reject(new Error(`Intercom broker exited before startup with signal ${signal}`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
reject(new Error(`Intercom broker exited before startup with code ${code ?? "unknown"}`));
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
child.once("error", onError);
|
|
111
|
+
child.once("exit", onExit);
|
|
112
|
+
waitForBroker().then(() => {
|
|
113
|
+
cleanup();
|
|
114
|
+
resolve();
|
|
115
|
+
}, (error) => {
|
|
116
|
+
cleanup();
|
|
117
|
+
reject(toError(error));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
} finally {
|
|
121
|
+
releaseSpawnLock();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function isBrokerRunning(): Promise<boolean> {
|
|
126
|
+
return checkSocketConnectable();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readLivePid(): number | null {
|
|
130
|
+
if (!existsSync(BROKER_PID)) return null;
|
|
131
|
+
let raw: string;
|
|
132
|
+
try {
|
|
133
|
+
raw = readFileSync(BROKER_PID, "utf-8");
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const pid = parseInt(raw.trim(), 10);
|
|
138
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
139
|
+
try {
|
|
140
|
+
process.kill(pid, 0);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return (error as NodeJS.ErrnoException).code === "EPERM" ? pid : null;
|
|
143
|
+
}
|
|
144
|
+
return isBrokerPidReused(pid, raw) ? null : pid;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function stopUnhealthyBrokerBeforeSpawn(): Promise<void> {
|
|
148
|
+
if (await checkSocketConnectable()) return;
|
|
149
|
+
const pid = readLivePid();
|
|
150
|
+
if (pid === null) return;
|
|
151
|
+
throw new Error(`Intercom broker PID ${pid} may still own accepted work but its socket is unhealthy; refusing to spawn a second broker. Let the broker exit normally, then retry.`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function checkSocketConnectable(): Promise<boolean> {
|
|
155
|
+
const preferred = getBrokerSocketPath();
|
|
156
|
+
if (isOwnedBrokerSocket(preferred) && await checkOneSocket(preferred)) return true;
|
|
157
|
+
const legacy = getLegacyBrokerSocketPath();
|
|
158
|
+
return legacy !== preferred && isOwnedBrokerSocket(legacy) && await checkOneSocket(legacy);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function checkOneSocket(brokerSocket: string): Promise<boolean> {
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
const socket = net.connect(brokerSocket);
|
|
164
|
+
const finish = (isConnected: boolean) => {
|
|
165
|
+
clearTimeout(timeout);
|
|
166
|
+
socket.off("connect", onConnect);
|
|
167
|
+
socket.off("error", onError);
|
|
168
|
+
socket.destroy();
|
|
169
|
+
resolve(isConnected);
|
|
170
|
+
};
|
|
171
|
+
const onConnect = () => finish(true);
|
|
172
|
+
const onError = () => finish(false);
|
|
173
|
+
socket.on("connect", onConnect);
|
|
174
|
+
socket.on("error", onError);
|
|
175
|
+
const timeout = setTimeout(() => finish(false), 1000);
|
|
176
|
+
timeout.unref?.();
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function acquireSpawnLock(): boolean {
|
|
181
|
+
const maxRetries = 5;
|
|
182
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
183
|
+
try {
|
|
184
|
+
writeFileSync(BROKER_SPAWN_LOCK, `${process.pid}\n${Date.now()}\n`, { flag: "wx" });
|
|
185
|
+
return true;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
if (isSpawnLockStale()) {
|
|
191
|
+
try {
|
|
192
|
+
unlinkSync(BROKER_SPAWN_LOCK);
|
|
193
|
+
} catch {
|
|
194
|
+
// If we can't delete the stale lock, retry a few times before giving up
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isSpawnLockStale(): boolean {
|
|
205
|
+
if (!existsSync(BROKER_SPAWN_LOCK)) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const [pidLine = "", createdAtLine = "0"] = readFileSync(BROKER_SPAWN_LOCK, "utf-8").trim().split("\n");
|
|
211
|
+
const pid = Number.parseInt(pidLine, 10);
|
|
212
|
+
const createdAt = Number.parseInt(createdAtLine, 10);
|
|
213
|
+
const ageMs = Date.now() - createdAt;
|
|
214
|
+
|
|
215
|
+
if (Number.isFinite(pid)) {
|
|
216
|
+
try {
|
|
217
|
+
process.kill(pid, 0);
|
|
218
|
+
} catch {
|
|
219
|
+
// The process that created the lock is gone.
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return !Number.isFinite(createdAt) || ageMs > 10_000;
|
|
225
|
+
} catch {
|
|
226
|
+
// Unreadable lock contents are treated as stale so a new broker can start.
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function releaseSpawnLock(): void {
|
|
232
|
+
try {
|
|
233
|
+
unlinkSync(BROKER_SPAWN_LOCK);
|
|
234
|
+
} catch {
|
|
235
|
+
// Another cleanup path may already have removed the lock.
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function waitForBroker(timeoutMs = 5000): Promise<void> {
|
|
240
|
+
const start = Date.now();
|
|
241
|
+
while (Date.now() - start < timeoutMs) {
|
|
242
|
+
if (await checkSocketConnectable()) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
await sleep(100);
|
|
246
|
+
}
|
|
247
|
+
throw new Error("Broker failed to start within timeout");
|
|
248
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { Key, type KeyId } from "@earendil-works/pi-tui";
|
|
4
|
+
import { getPiAgentDir } from "./agent-dir.ts";
|
|
5
|
+
|
|
6
|
+
export interface IntercomConfig {
|
|
7
|
+
/** Toggle owned agents, or open peer messaging when the agent view is unavailable. */
|
|
8
|
+
shortcut: KeyId;
|
|
9
|
+
|
|
10
|
+
/** Broker command used to spawn the broker process (for example, Node or Bun) */
|
|
11
|
+
brokerCommand: string;
|
|
12
|
+
|
|
13
|
+
/** Arguments passed to the broker command before the broker script path */
|
|
14
|
+
brokerArgs: string[];
|
|
15
|
+
|
|
16
|
+
/** Require confirmation before non-reply sends from interactive sessions */
|
|
17
|
+
confirmSend: boolean;
|
|
18
|
+
|
|
19
|
+
/** Optional custom status suffix shown after automatic lifecycle status */
|
|
20
|
+
status?: string;
|
|
21
|
+
|
|
22
|
+
/** Show reply hint in incoming messages (default: true) */
|
|
23
|
+
replyHint: boolean;
|
|
24
|
+
|
|
25
|
+
/** How long ordinary intercom `ask` waits for a reply before giving up, in ms (default: 120000 = 2 minutes). */
|
|
26
|
+
askTimeoutMs: number;
|
|
27
|
+
|
|
28
|
+
/** How long the client waits for the broker to acknowledge message delivery, in ms (default: 8000). */
|
|
29
|
+
sendTimeoutMs: number;
|
|
30
|
+
|
|
31
|
+
/** How long the client waits for a session list response, in ms (default: 5000). */
|
|
32
|
+
listTimeoutMs: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getConfigPath(): string {
|
|
36
|
+
return join(getPiAgentDir(), "intercom", "config.json");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const defaults: IntercomConfig = {
|
|
40
|
+
shortcut: Key.altShift("m"),
|
|
41
|
+
brokerCommand: process.execPath,
|
|
42
|
+
brokerArgs: [],
|
|
43
|
+
confirmSend: false,
|
|
44
|
+
replyHint: true,
|
|
45
|
+
askTimeoutMs: 2 * 60 * 1000,
|
|
46
|
+
sendTimeoutMs: 8000,
|
|
47
|
+
listTimeoutMs: 5000,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const namedKeys = new Set<string>(Object.values(Key).filter((key) => typeof key === "string"));
|
|
51
|
+
function isShortcut(value: string): value is KeyId {
|
|
52
|
+
const prefix = value.match(/^(?:(?:ctrl|shift|alt|super)\+)+/)?.[0] ?? "";
|
|
53
|
+
const modifiers = prefix.split("+").filter(Boolean);
|
|
54
|
+
const key = value.slice(prefix.length);
|
|
55
|
+
return new Set(modifiers).size === modifiers.length && (namedKeys.has(key) || /^[a-z0-9]$/.test(key));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function loadConfig(): IntercomConfig {
|
|
59
|
+
const configPath = getConfigPath();
|
|
60
|
+
if (!existsSync(configPath)) {
|
|
61
|
+
return { ...defaults };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
66
|
+
const parsed: unknown = JSON.parse(raw);
|
|
67
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
68
|
+
throw new Error("Config must be a JSON object");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const parsedConfig = parsed as Record<string, unknown>;
|
|
72
|
+
const config: IntercomConfig = { ...defaults };
|
|
73
|
+
|
|
74
|
+
if (Object.hasOwn(parsedConfig, "shortcut")) {
|
|
75
|
+
if (typeof parsedConfig.shortcut !== "string") throw new Error('"shortcut" must be a native key identifier');
|
|
76
|
+
const shortcut = parsedConfig.shortcut.trim();
|
|
77
|
+
if (!isShortcut(shortcut)) throw new Error('"shortcut" must be a native key identifier, for example alt+shift+m');
|
|
78
|
+
config.shortcut = shortcut;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (Object.hasOwn(parsedConfig, "brokerCommand")) {
|
|
82
|
+
if (typeof parsedConfig.brokerCommand !== "string") {
|
|
83
|
+
throw new Error(`"brokerCommand" must be a string`);
|
|
84
|
+
}
|
|
85
|
+
const brokerCommand = parsedConfig.brokerCommand.trim();
|
|
86
|
+
if (!brokerCommand) {
|
|
87
|
+
throw new Error(`"brokerCommand" must not be empty`);
|
|
88
|
+
}
|
|
89
|
+
config.brokerCommand = brokerCommand;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (Object.hasOwn(parsedConfig, "brokerArgs")) {
|
|
93
|
+
if (!Array.isArray(parsedConfig.brokerArgs)) {
|
|
94
|
+
throw new Error(`"brokerArgs" must be an array`);
|
|
95
|
+
}
|
|
96
|
+
const brokerArgs: string[] = [];
|
|
97
|
+
for (const arg of parsedConfig.brokerArgs) {
|
|
98
|
+
if (typeof arg !== "string") {
|
|
99
|
+
throw new Error(`"brokerArgs" items must be strings`);
|
|
100
|
+
}
|
|
101
|
+
brokerArgs.push(arg);
|
|
102
|
+
}
|
|
103
|
+
config.brokerArgs = brokerArgs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (Object.hasOwn(parsedConfig, "confirmSend")) {
|
|
107
|
+
if (typeof parsedConfig.confirmSend !== "boolean") {
|
|
108
|
+
throw new Error(`"confirmSend" must be a boolean`);
|
|
109
|
+
}
|
|
110
|
+
config.confirmSend = parsedConfig.confirmSend;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (Object.hasOwn(parsedConfig, "replyHint")) {
|
|
114
|
+
if (typeof parsedConfig.replyHint !== "boolean") {
|
|
115
|
+
throw new Error(`"replyHint" must be a boolean`);
|
|
116
|
+
}
|
|
117
|
+
config.replyHint = parsedConfig.replyHint;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (Object.hasOwn(parsedConfig, "status")) {
|
|
121
|
+
if (typeof parsedConfig.status !== "string") {
|
|
122
|
+
throw new Error(`"status" must be a string`);
|
|
123
|
+
}
|
|
124
|
+
config.status = parsedConfig.status;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const [key, min] of [["askTimeoutMs", 1000], ["sendTimeoutMs", 500], ["listTimeoutMs", 500]] as const) {
|
|
128
|
+
if (Object.hasOwn(parsedConfig, key)) {
|
|
129
|
+
const value = parsedConfig[key];
|
|
130
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min) {
|
|
131
|
+
throw new Error(`"${key}" must be a finite number >= ${min}`);
|
|
132
|
+
}
|
|
133
|
+
config[key] = value;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return config;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error(`Failed to load intercom config at ${configPath}:`, error);
|
|
140
|
+
return { ...defaults };
|
|
141
|
+
}
|
|
142
|
+
}
|