@fitchmultz/pi-subagents 0.44.7 → 0.46.0
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 +264 -7
- package/LICENSE +1 -0
- package/README.md +58 -1483
- package/agents/context-builder.md +19 -1
- package/agents/debugger.md +10 -3
- package/agents/delegate.md +1 -0
- package/agents/fixer.md +4 -1
- package/agents/oracle.md +13 -2
- package/agents/planner.md +14 -3
- package/agents/researcher.md +11 -2
- package/agents/reviewer-claude.md +11 -2
- package/agents/reviewer-gpt.md +12 -2
- package/agents/reviewer-ponytail.md +11 -1
- package/agents/reviewer-security.md +13 -2
- package/agents/reviewer.md +12 -3
- package/agents/scout.md +12 -1
- package/agents/ui-designer.md +10 -3
- package/agents/watcher.md +1 -0
- package/agents/worker.md +8 -2
- package/agents/writer.md +6 -3
- package/dist/agents/agent-defaults.js +9 -0
- package/dist/agents/agent-definition.js +196 -0
- package/dist/agents/agent-management.js +64 -905
- package/dist/agents/agent-scope.js +4 -2
- package/dist/agents/agent-selection.js +10 -5
- package/dist/agents/agent-serializer.js +55 -67
- package/dist/agents/agents.js +75 -716
- package/dist/agents/builtin-overrides.js +218 -0
- package/dist/agents/chain-serializer.js +3 -367
- package/dist/agents/config-values.js +52 -0
- package/dist/agents/discovery-paths.js +91 -0
- package/dist/agents/frontmatter.js +15 -5
- package/dist/agents/identity.js +25 -8
- package/dist/agents/json-chain.js +254 -0
- package/dist/agents/management-config.js +332 -0
- package/dist/agents/management-format.js +231 -0
- package/dist/agents/management-persistence.js +287 -0
- package/dist/agents/management-targets.js +136 -0
- package/dist/agents/management-warnings.js +81 -0
- package/dist/agents/markdown-chain.js +210 -0
- package/dist/agents/package-agents.js +154 -0
- package/dist/agents/skill-files.js +157 -0
- package/dist/agents/skill-search-paths.js +246 -0
- package/dist/agents/skill-types.js +1 -0
- package/dist/agents/skills.js +54 -499
- package/dist/extension/acceptance-schema.js +55 -0
- package/dist/extension/build-info.js +1 -1
- package/dist/extension/chain-schema.js +196 -0
- package/dist/extension/child-runtime.js +237 -0
- package/dist/extension/compact-tools.js +122 -51
- package/dist/extension/config-schema.js +29 -0
- package/dist/extension/config.js +27 -11
- package/dist/extension/control-notice-schema.js +37 -0
- package/dist/extension/control-notices.js +13 -6
- package/dist/extension/delegation-bridges.js +43 -0
- package/dist/extension/doctor.js +48 -25
- package/dist/extension/everyday-schemas.js +207 -0
- package/dist/extension/fanout-child.js +18 -319
- package/dist/extension/index.js +8 -563
- package/dist/extension/message-renderers.js +225 -0
- package/dist/extension/nested-control-inbox.js +154 -0
- package/dist/extension/parent-runtime.js +339 -0
- package/dist/extension/primary-tool.js +69 -0
- package/dist/extension/result-snapshot.js +22 -0
- package/dist/extension/runtime-reload.js +62 -0
- package/dist/extension/runtime-storage.js +43 -0
- package/dist/extension/schema-overrides.js +75 -0
- package/dist/extension/schemas.js +265 -336
- package/dist/extension/tool-input.js +27 -12
- package/dist/extension/tool-result.js +19 -4
- package/dist/history/answer-texts.js +57 -0
- package/dist/history/canonical-result.js +76 -43
- package/dist/history/canonical-selection.js +40 -0
- package/dist/history/database-generation.js +82 -0
- package/dist/history/database.js +57 -33
- package/dist/history/entry-ingestion.js +61 -0
- package/dist/history/index.js +242 -97
- package/dist/history/ingest.js +168 -331
- package/dist/history/output-digests.js +40 -0
- package/dist/history/preview.js +210 -0
- package/dist/history/prune.js +33 -0
- package/dist/history/queries.js +60 -349
- package/dist/history/query-scope.js +190 -0
- package/dist/history/rows.js +77 -0
- package/dist/history/run-projection.js +137 -0
- package/dist/history/run-queries.js +153 -0
- package/dist/history/schema.js +31 -0
- package/dist/history/search-queries.js +163 -0
- package/dist/history/selected-record.js +108 -0
- package/dist/history/source-file.js +45 -0
- package/dist/history/source-watches.js +141 -0
- package/dist/history/store.js +164 -153
- package/dist/history/text-chunks.js +169 -0
- package/dist/history/text.js +35 -0
- package/dist/history/transcript-queries.js +177 -0
- package/dist/history/types.js +5 -1
- package/dist/history/values.js +36 -0
- package/dist/history/wire-inputs.js +122 -0
- package/dist/history/wire-models.js +38 -0
- package/dist/history/worker-runtime.js +441 -0
- package/dist/history/worker.js +66 -462
- package/dist/intercom/intercom-bridge.js +48 -20
- package/dist/intercom/live-intercom.js +80 -23
- package/dist/intercom/nested-result-tree.js +163 -0
- package/dist/intercom/result-delivery.js +40 -0
- package/dist/intercom/result-intercom.js +178 -261
- package/dist/pi-intercom/agent-dir.js +7 -3
- package/dist/pi-intercom/async.js +27 -0
- package/dist/pi-intercom/bridge-ask.js +57 -0
- package/dist/pi-intercom/bridge-config.js +89 -0
- package/dist/pi-intercom/bridge-http.js +86 -0
- package/dist/pi-intercom/bridge-launch-policy.js +88 -0
- package/dist/pi-intercom/bridge-launch.js +217 -0
- package/dist/pi-intercom/bridge-protocol.js +106 -0
- package/dist/pi-intercom/bridge-routes.js +72 -0
- package/dist/pi-intercom/bridge-rpc-process.js +264 -0
- package/dist/pi-intercom/bridge-session.js +420 -0
- package/dist/pi-intercom/bridge.js +221 -606
- package/dist/pi-intercom/broker/broker.js +236 -244
- package/dist/pi-intercom/broker/client.js +189 -304
- package/dist/pi-intercom/broker/connection.js +165 -0
- package/dist/pi-intercom/broker/framing.js +5 -3
- package/dist/pi-intercom/broker/paths.js +4 -1
- package/dist/pi-intercom/broker/pid.js +53 -25
- package/dist/pi-intercom/broker/snapshot-reader.js +78 -0
- package/dist/pi-intercom/broker/snapshots.js +188 -0
- package/dist/pi-intercom/broker/spawn.js +36 -25
- package/dist/pi-intercom/config.js +85 -71
- package/dist/pi-intercom/connection.js +297 -0
- package/dist/pi-intercom/identity.js +58 -0
- package/dist/pi-intercom/inbound-delivery.js +356 -0
- package/dist/pi-intercom/inbound-journal.js +303 -0
- package/dist/pi-intercom/inbound-record.js +113 -0
- package/dist/pi-intercom/inbound.js +179 -0
- package/dist/pi-intercom/index.js +79 -2933
- package/dist/pi-intercom/inspection.js +62 -0
- package/dist/pi-intercom/interview.js +236 -0
- package/dist/pi-intercom/lifecycle.js +168 -0
- package/dist/pi-intercom/message-format.js +69 -0
- package/dist/pi-intercom/outbound.js +304 -0
- package/dist/pi-intercom/peer-awareness.js +66 -0
- package/dist/pi-intercom/reply-tracker.js +62 -43
- package/dist/pi-intercom/reply-wait.js +201 -0
- package/dist/pi-intercom/runtime-types.js +3 -0
- package/dist/pi-intercom/session-events.js +180 -0
- package/dist/pi-intercom/session-targets.js +42 -34
- package/dist/pi-intercom/subagent-events.js +238 -0
- package/dist/pi-intercom/subagent-live-events.js +129 -86
- package/dist/pi-intercom/supervisor.js +282 -0
- package/dist/pi-intercom/tool-arguments.js +74 -0
- package/dist/pi-intercom/tool-registration.js +95 -0
- package/dist/pi-intercom/tool-results.js +38 -0
- package/dist/pi-intercom/tool-schemas.js +82 -0
- package/dist/pi-intercom/tools.js +51 -0
- package/dist/pi-intercom/topic-actions.js +177 -0
- package/dist/pi-intercom/topics.js +177 -92
- package/dist/pi-intercom/transport.js +1 -0
- package/dist/pi-intercom/types.js +151 -114
- package/dist/pi-intercom/ui/compose-paste.js +93 -0
- package/dist/pi-intercom/ui/compose.js +121 -143
- package/dist/pi-intercom/ui/inline-message.js +59 -59
- package/dist/pi-intercom/ui/message-renderers.js +147 -0
- package/dist/pi-intercom/ui/overlay.js +181 -0
- package/dist/pi-intercom/ui/session-format.js +124 -0
- package/dist/pi-intercom/ui/session-list.js +76 -43
- package/dist/pi-intercom/ui/tool-renderers.js +110 -0
- package/dist/pi-intercom/ui/topic-view.js +88 -0
- package/dist/pi-intercom/validation.js +59 -0
- package/dist/runs/background/async-chain-plan.js +265 -0
- package/dist/runs/background/async-control.js +134 -58
- package/dist/runs/background/async-execution.js +136 -633
- package/dist/runs/background/async-job-admission.js +40 -0
- package/dist/runs/background/async-job-projection.js +169 -0
- package/dist/runs/background/async-job-tracker.js +297 -327
- package/dist/runs/background/async-launch.js +216 -0
- package/dist/runs/background/async-plan.js +116 -0
- package/dist/runs/background/async-result-file.js +47 -23
- package/dist/runs/background/async-resume.js +195 -253
- package/dist/runs/background/async-run-discovery.js +213 -0
- package/dist/runs/background/async-run-location.js +135 -0
- package/dist/runs/background/async-run-record.js +1 -0
- package/dist/runs/background/async-run-repair.js +223 -0
- package/dist/runs/background/async-run-summary.js +1 -0
- package/dist/runs/background/async-single-plan.js +105 -0
- package/dist/runs/background/async-status-projection.js +155 -0
- package/dist/runs/background/async-status-render.js +117 -0
- package/dist/runs/background/async-status-validation.js +181 -0
- package/dist/runs/background/async-status.js +3 -442
- package/dist/runs/background/async-value.js +21 -0
- package/dist/runs/background/completion-dedupe.js +36 -12
- package/dist/runs/background/completion-delivery.js +211 -205
- package/dist/runs/background/completion-receipts.js +237 -0
- package/dist/runs/background/legacy-status-acceptance.js +34 -0
- package/dist/runs/background/notify.js +96 -48
- package/dist/runs/background/parallel-groups.js +36 -19
- package/dist/runs/background/result-payload.js +178 -0
- package/dist/runs/background/result-watcher.js +348 -339
- package/dist/runs/background/run-id-resolver.js +53 -33
- package/dist/runs/background/run-schemas.js +189 -0
- package/dist/runs/background/run-status-contracts.js +14 -0
- package/dist/runs/background/run-status-render.js +270 -0
- package/dist/runs/background/run-status.js +231 -339
- package/dist/runs/background/runner-attempt-output.js +155 -0
- package/dist/runs/background/runner-attempt.js +263 -0
- package/dist/runs/background/runner-child-executor.js +166 -0
- package/dist/runs/background/runner-child-observer.js +192 -0
- package/dist/runs/background/runner-completion-summary.js +71 -0
- package/dist/runs/background/runner-completion.js +211 -0
- package/dist/runs/background/runner-config.js +1 -0
- package/dist/runs/background/runner-contract.js +1 -0
- package/dist/runs/background/runner-dynamic.js +121 -0
- package/dist/runs/background/runner-finalization.js +149 -0
- package/dist/runs/background/runner-graph.js +68 -0
- package/dist/runs/background/runner-initial-status.js +60 -0
- package/dist/runs/background/runner-lifecycle.js +142 -0
- package/dist/runs/background/runner-monitor.js +309 -0
- package/dist/runs/background/runner-parallel.js +204 -0
- package/dist/runs/background/runner-prepare-step.js +86 -0
- package/dist/runs/background/runner-reporting.js +153 -0
- package/dist/runs/background/runner-status.js +148 -0
- package/dist/runs/background/runner-step-output.js +92 -0
- package/dist/runs/background/runner-step-record.js +45 -0
- package/dist/runs/background/runner-step.js +112 -0
- package/dist/runs/background/runner-streaming.js +173 -0
- package/dist/runs/background/runner-workflow.js +213 -0
- package/dist/runs/background/stale-run-reconciler.js +155 -292
- package/dist/runs/background/subagent-runner-launcher.js +33 -13
- package/dist/runs/background/subagent-runner.js +51 -1842
- package/dist/runs/background/top-level-async.js +2 -1
- package/dist/runs/foreground/chain-clarify.js +293 -1017
- package/dist/runs/foreground/clarification-draft.js +87 -0
- package/dist/runs/foreground/clarify-contracts.js +1 -0
- package/dist/runs/foreground/clarify-invocation.js +129 -38
- package/dist/runs/foreground/clarify-text-editor.js +210 -0
- package/dist/runs/foreground/clarify-view.js +167 -0
- package/dist/runs/foreground/execution-input.js +226 -353
- package/dist/runs/foreground/execution-routing.js +33 -0
- package/dist/runs/foreground/execution-timeout.js +81 -0
- package/dist/runs/foreground/foreground-control.js +222 -763
- package/dist/runs/foreground/foreground-memory.js +99 -0
- package/dist/runs/foreground/invocation-context.js +55 -0
- package/dist/runs/foreground/invocation-execution.js +331 -0
- package/dist/runs/foreground/invocation-expansion.js +48 -0
- package/dist/runs/foreground/management-actions.js +295 -0
- package/dist/runs/foreground/nested-control.js +179 -0
- package/dist/runs/foreground/question-continuation.js +78 -0
- package/dist/runs/foreground/question-control.js +155 -50
- package/dist/runs/foreground/resume-target.js +221 -0
- package/dist/runs/foreground/revival-launch-policy.js +58 -0
- package/dist/runs/foreground/run-async-path.js +190 -178
- package/dist/runs/foreground/run-interrupt.js +164 -0
- package/dist/runs/foreground/run-nudge.js +278 -0
- package/dist/runs/foreground/saved-revival.js +251 -0
- package/dist/runs/foreground/subagent-executor.js +135 -424
- package/dist/runs/foreground/subagent-params.js +67 -30
- package/dist/runs/foreground/wait-observation.js +104 -0
- package/dist/runs/foreground/wait-registration.js +17 -0
- package/dist/runs/foreground/wait-run.js +283 -123
- package/dist/runs/shared/acceptance-contract.js +193 -99
- package/dist/runs/shared/acceptance-defaults.js +1 -0
- package/dist/runs/shared/acceptance-evaluation.js +201 -181
- package/dist/runs/shared/acceptance-finalization.js +256 -180
- package/dist/runs/shared/acceptance-outcome.js +53 -0
- package/dist/runs/shared/acceptance-reports.js +137 -72
- package/dist/runs/shared/acceptance-submission.js +167 -0
- package/dist/runs/shared/acceptance-verification.js +100 -0
- package/dist/runs/shared/chain-outputs.js +80 -52
- package/dist/runs/shared/child-attempt-types.js +1 -0
- package/dist/runs/shared/child-attempt.js +314 -667
- package/dist/runs/shared/child-boundaries.js +173 -0
- package/dist/runs/shared/child-context-filter.js +44 -0
- package/dist/runs/shared/child-event-observer.js +390 -0
- package/dist/runs/shared/child-execution-cwd.js +73 -43
- package/dist/runs/shared/child-execution-outcome.js +72 -0
- package/dist/runs/shared/child-invocation.js +54 -0
- package/dist/runs/shared/child-message-validation.js +215 -0
- package/dist/runs/shared/child-native-accounting.js +106 -0
- package/dist/runs/shared/child-native-observation.js +69 -0
- package/dist/runs/shared/child-observation-spool.js +164 -0
- package/dist/runs/shared/child-observations.js +157 -76
- package/dist/runs/shared/child-presence.js +3 -0
- package/dist/runs/shared/child-route-env.js +119 -0
- package/dist/runs/shared/child-stream-observer.js +221 -0
- package/dist/runs/shared/child-stream-projection.js +137 -0
- package/dist/runs/shared/child-structured-tool.js +27 -0
- package/dist/runs/shared/claude-code.js +121 -158
- package/dist/runs/shared/claude-model.js +42 -0
- package/dist/runs/shared/claude-session.js +83 -0
- package/dist/runs/shared/claude-types.js +1 -0
- package/dist/runs/shared/completion-guard.js +18 -18
- package/dist/runs/shared/control-event-policy.js +8 -0
- package/dist/runs/shared/control-notification-owner.js +13 -0
- package/dist/runs/shared/dynamic-fanout.js +188 -173
- package/dist/runs/shared/dynamic-item.js +102 -0
- package/dist/runs/shared/history-index.js +62 -36
- package/dist/runs/shared/legacy-async-recovery.js +114 -0
- package/dist/runs/shared/legacy-output-recovery.js +148 -0
- package/dist/runs/shared/mcp-config-schema.js +36 -0
- package/dist/runs/shared/mcp-direct-tool-allowlist.js +147 -116
- package/dist/runs/shared/model-fallback.js +97 -38
- package/dist/runs/shared/mutating-tool-guard.js +128 -88
- package/dist/runs/shared/native-finalization-types.js +1 -0
- package/dist/runs/shared/native-finalization.js +155 -76
- package/dist/runs/shared/native-usage.js +238 -98
- package/dist/runs/shared/nested-control-records.js +128 -0
- package/dist/runs/shared/nested-events.js +8 -700
- package/dist/runs/shared/nested-path.js +32 -28
- package/dist/runs/shared/nested-projection.js +155 -0
- package/dist/runs/shared/nested-protocol.js +18 -0
- package/dist/runs/shared/nested-registry.js +191 -0
- package/dist/runs/shared/nested-render.js +106 -58
- package/dist/runs/shared/nested-route.js +220 -0
- package/dist/runs/shared/nested-validation.js +216 -0
- package/dist/runs/shared/owned-child-evidence.js +125 -0
- package/dist/runs/shared/owned-run-list.js +146 -0
- package/dist/runs/shared/owned-run-observation.js +267 -0
- package/dist/runs/shared/owned-run-progress.js +109 -0
- package/dist/runs/shared/owned-run-read-state.js +1 -0
- package/dist/runs/shared/owned-run-results.js +202 -0
- package/dist/runs/shared/owned-run-status.js +180 -0
- package/dist/runs/shared/owned-run-view.js +51 -0
- package/dist/runs/shared/owned-workflow-details.js +17 -0
- package/dist/runs/shared/owner-output.js +43 -0
- package/dist/runs/shared/parallel-utils.js +36 -28
- package/dist/runs/shared/parent-receipts.js +90 -32
- package/dist/runs/shared/parent-usage.js +169 -57
- package/dist/runs/shared/pi-args.js +108 -201
- package/dist/runs/shared/pi-launch-input.js +1 -0
- package/dist/runs/shared/pi-session-launch.js +86 -0
- package/dist/runs/shared/pi-spawn.js +35 -21
- package/dist/runs/shared/run-history-queries.js +126 -35
- package/dist/runs/shared/run-history.js +101 -35
- package/dist/runs/shared/run-metadata-paths.js +14 -0
- package/dist/runs/shared/run-metadata.js +158 -0
- package/dist/runs/shared/run-persistence.js +131 -0
- package/dist/runs/shared/run-records.js +9 -679
- package/dist/runs/shared/run-restoration.js +338 -0
- package/dist/runs/shared/run-state-owner.js +26 -0
- package/dist/runs/shared/session-cwd-preload.js +27 -18
- package/dist/runs/shared/single-output.js +148 -70
- package/dist/runs/shared/stable-value.js +15 -0
- package/dist/runs/shared/streaming-text.js +23 -12
- package/dist/runs/shared/structured-output.js +25 -13
- package/dist/runs/shared/subagent-control.js +167 -100
- package/dist/runs/shared/subagent-prompt-runtime.js +27 -130
- package/dist/runs/shared/subagent-tool-loop-guard.js +81 -52
- package/dist/runs/shared/supervisor-question-state.js +145 -0
- package/dist/runs/shared/supervisor-questions.js +185 -254
- package/dist/runs/shared/workflow-graph.js +259 -167
- package/dist/runs/shared/workflow-policy.js +120 -48
- package/dist/runs/shared/worktree-contract.js +1 -0
- package/dist/runs/shared/worktree-hook.js +138 -0
- package/dist/runs/shared/worktree-location.js +47 -0
- package/dist/runs/shared/worktree-preservation.js +8 -0
- package/dist/runs/shared/worktree-process.js +107 -0
- package/dist/runs/shared/worktree.js +183 -322
- package/dist/shared/agent-context-policy.js +34 -44
- package/dist/shared/agent-dir.js +14 -0
- package/dist/shared/artifacts.js +30 -12
- package/dist/shared/atomic-json.js +4 -1
- package/dist/shared/chain-files.js +72 -0
- package/dist/shared/concurrency.js +18 -0
- package/dist/shared/content-preview.js +115 -0
- package/dist/shared/cooperative.js +5 -2
- package/dist/shared/execution-cwd.js +13 -5
- package/dist/shared/file-coalescer.js +2 -1
- package/dist/shared/fork-context.js +10 -12
- package/dist/shared/formatters.js +45 -19
- package/dist/shared/journal-frames.js +164 -0
- package/dist/shared/journal-projections.js +121 -0
- package/dist/shared/journal-reader.js +6 -407
- package/dist/shared/json-projection.js +191 -0
- package/dist/shared/lazy-tools.js +19 -8
- package/dist/shared/message-output.js +116 -0
- package/dist/shared/model-info.js +31 -14
- package/dist/shared/native-import.js +20 -0
- package/dist/shared/native-journal.js +140 -0
- package/dist/shared/native-session-loader.js +17 -0
- package/dist/shared/native-session.js +2 -13
- package/dist/shared/native-tui.js +7 -2
- package/dist/shared/native-typebox.js +15 -3
- package/dist/shared/output-page.js +63 -0
- package/dist/shared/post-exit-stdio-guard.js +128 -88
- package/dist/shared/prompt-sections.js +2 -0
- package/dist/shared/session-entries.js +70 -35
- package/dist/shared/session-identity.js +5 -3
- package/dist/shared/settings.js +62 -192
- package/dist/shared/status-format.js +97 -37
- package/dist/shared/task-file-policy.js +24 -0
- package/dist/shared/temp-root.js +44 -15
- package/dist/shared/types/acceptance.js +1 -0
- package/dist/shared/types/async.js +1 -0
- package/dist/shared/types/config.js +1 -0
- package/dist/shared/types/details.js +1 -0
- package/dist/shared/types/history.js +1 -0
- package/dist/shared/types/inputs.js +1 -0
- package/dist/shared/types/intercom.js +1 -0
- package/dist/shared/types/launch.js +1 -0
- package/dist/shared/types/messages.js +1 -0
- package/dist/shared/types/nested.js +1 -0
- package/dist/shared/types/owned-runs.js +1 -0
- package/dist/shared/types/progress.js +1 -0
- package/dist/shared/types/questions.js +1 -0
- package/dist/shared/types/results.js +1 -0
- package/dist/shared/types/state.js +1 -0
- package/dist/shared/types/usage.js +1 -0
- package/dist/shared/types/workflow.js +1 -0
- package/dist/shared/types.js +75 -30
- package/dist/shared/unknown.js +52 -0
- package/dist/shared/utils.js +54 -260
- package/dist/slash/command-discovery.js +82 -0
- package/dist/slash/command-input.js +190 -0
- package/dist/slash/prompt-template-bridge.js +94 -270
- package/dist/slash/prompt-template-contract.js +77 -0
- package/dist/slash/prompt-template-results.js +125 -0
- package/dist/slash/request-protocol.js +68 -0
- package/dist/slash/result-delivery.js +237 -0
- package/dist/slash/saved-chain.js +78 -0
- package/dist/slash/slash-bridge.js +123 -89
- package/dist/slash/slash-commands.js +101 -572
- package/dist/slash/slash-live-state.js +68 -47
- package/dist/tui/action-hints.js +50 -22
- package/dist/tui/activity-lines.js +32 -0
- package/dist/tui/agent-browser.js +290 -0
- package/dist/tui/agent-controls.js +289 -0
- package/dist/tui/agent-conversation.js +444 -0
- package/dist/tui/agent-dock.js +171 -0
- package/dist/tui/agent-history.js +50 -183
- package/dist/tui/agent-model.js +62 -0
- package/dist/tui/agent-picker.js +262 -0
- package/dist/tui/agent-view.js +232 -1564
- package/dist/tui/compact-results.js +193 -0
- package/dist/tui/conversation-details.js +190 -0
- package/dist/tui/conversation-frame.js +135 -0
- package/dist/tui/conversation-history.js +201 -0
- package/dist/tui/conversation-menu.js +78 -0
- package/dist/tui/conversation-notices.js +79 -0
- package/dist/tui/conversation-selection.js +58 -0
- package/dist/tui/conversation-viewport.js +129 -0
- package/dist/tui/direction-message.js +27 -0
- package/dist/tui/display.js +282 -0
- package/dist/tui/editor-viewport.js +18 -0
- package/dist/tui/expanded-content.js +87 -0
- package/dist/tui/expanded-multi.js +170 -0
- package/dist/tui/expanded-single.js +65 -0
- package/dist/tui/history-cards.js +216 -0
- package/dist/tui/history-display.js +238 -0
- package/dist/tui/history-json.js +16 -0
- package/dist/tui/history-metadata.js +152 -0
- package/dist/tui/history-record.js +251 -0
- package/dist/tui/history-text.js +34 -0
- package/dist/tui/indexed-history.js +116 -0
- package/dist/tui/nested-widget.js +120 -0
- package/dist/tui/picker-page.js +59 -0
- package/dist/tui/render.js +58 -1501
- package/dist/tui/result-facts.js +64 -0
- package/dist/tui/task-observation.js +59 -0
- package/dist/tui/task-store.js +366 -0
- package/dist/tui/text-values.js +16 -0
- package/dist/tui/view-connections.js +47 -0
- package/dist/tui/view-model.js +94 -0
- package/dist/tui/view-persistence.js +51 -0
- package/dist/tui/view-ports.js +1 -0
- package/dist/tui/view-session.js +37 -0
- package/dist/tui/widget-details.js +144 -0
- package/dist/tui/widget-status.js +245 -0
- package/dist/tui/widget.js +145 -0
- package/dist/tui/workflow-labels.js +223 -0
- package/dist/tui/workflow-outcomes.js +124 -0
- package/docs/code-quality.md +38 -0
- package/docs/development.md +99 -0
- package/docs/intercom-bridge.md +77 -22
- package/docs/intercom.md +132 -106
- package/docs/performance.md +19 -17
- package/docs/reference.md +1472 -0
- package/generated/RunValidators.d.ts +28 -0
- package/generated/RunValidators.js +2 -0
- package/package.json +27 -6
- package/prompts/parallel-cleanup.md +3 -0
- package/prompts/parallel-context-build.md +3 -0
- package/prompts/parallel-handoff-plan.md +6 -0
- package/prompts/parallel-research.md +4 -0
- package/prompts/parallel-review.md +2 -0
- package/prompts/review-loop.md +2 -0
- package/scripts/build.mjs +129 -85
- package/scripts/check-compat.mjs +48 -11
- package/scripts/compat-host.mjs +35 -7
- package/scripts/generate-run-schemas.mjs +210 -0
- package/scripts/intercom-bridge-credentials.mjs +158 -29
- package/scripts/intercom-remote.mjs +213 -52
- package/scripts/local-install-smoke.mjs +47 -37
- package/scripts/native-package-smoke.mjs +135 -25
- package/scripts/package-smoke.mjs +360 -140
- package/scripts/prepare.mjs +54 -34
- package/scripts/real-pi-processes.mjs +261 -0
- package/scripts/real-pi-smoke.mjs +432 -351
- package/scripts/run-tests.mjs +29 -15
- package/skills/pi-intercom/SKILL.md +11 -11
- package/skills/pi-intercom/references/peer-sessions.md +7 -2
- package/skills/pi-subagents/SKILL.md +1 -1
- package/skills/pi-subagents/references/full-orchestration-guide.md +254 -112
- package/src/agents/agent-defaults.ts +13 -0
- package/src/agents/agent-definition.ts +253 -0
- package/src/agents/agent-management.ts +111 -808
- package/src/agents/agent-scope.ts +7 -3
- package/src/agents/agent-selection.ts +26 -16
- package/src/agents/agent-serializer.ts +84 -93
- package/src/agents/agents.ts +199 -906
- package/src/agents/builtin-overrides.ts +297 -0
- package/src/agents/chain-serializer.ts +3 -329
- package/src/agents/config-values.ts +57 -0
- package/src/agents/discovery-paths.ts +134 -0
- package/src/agents/frontmatter.ts +35 -17
- package/src/agents/identity.ts +49 -18
- package/src/agents/json-chain.ts +305 -0
- package/src/agents/management-config.ts +342 -0
- package/src/agents/management-format.ts +243 -0
- package/src/agents/management-persistence.ts +408 -0
- package/src/agents/management-targets.ts +260 -0
- package/src/agents/management-warnings.ts +115 -0
- package/src/agents/markdown-chain.ts +237 -0
- package/src/agents/package-agents.ts +231 -0
- package/src/agents/skill-files.ts +182 -0
- package/src/agents/skill-search-paths.ts +289 -0
- package/src/agents/skill-types.ts +31 -0
- package/src/agents/skills.ts +152 -610
- package/src/extension/acceptance-schema.ts +88 -0
- package/src/extension/chain-schema.ts +276 -0
- package/src/extension/child-runtime.ts +302 -0
- package/src/extension/compact-tools.ts +259 -102
- package/src/extension/config-schema.ts +59 -0
- package/src/extension/config.ts +39 -17
- package/src/extension/control-notice-schema.ts +55 -0
- package/src/extension/control-notices.ts +57 -33
- package/src/extension/delegation-bridges.ts +59 -0
- package/src/extension/doctor.ts +171 -135
- package/src/extension/everyday-schemas.ts +252 -0
- package/src/extension/fanout-child.ts +29 -308
- package/src/extension/index.ts +17 -642
- package/src/extension/message-renderers.ts +334 -0
- package/src/extension/nested-control-inbox.ts +196 -0
- package/src/extension/parent-runtime.ts +405 -0
- package/src/extension/primary-tool.ts +130 -0
- package/src/extension/result-snapshot.ts +35 -0
- package/src/extension/runtime-reload.ts +72 -0
- package/src/extension/runtime-storage.ts +49 -0
- package/src/extension/schema-overrides.ts +114 -0
- package/src/extension/schemas.ts +451 -418
- package/src/extension/tool-input.ts +40 -13
- package/src/extension/tool-result.ts +45 -14
- package/src/history/answer-texts.ts +74 -0
- package/src/history/canonical-result.ts +127 -51
- package/src/history/canonical-selection.ts +54 -0
- package/src/history/database-generation.ts +86 -0
- package/src/history/database.ts +86 -48
- package/src/history/entry-ingestion.ts +129 -0
- package/src/history/index.ts +426 -148
- package/src/history/ingest.ts +372 -342
- package/src/history/output-digests.ts +41 -0
- package/src/history/preview.ts +235 -0
- package/src/history/prune.ts +48 -0
- package/src/history/queries.ts +122 -256
- package/src/history/query-scope.ts +278 -0
- package/src/history/rows.ts +118 -0
- package/src/history/run-projection.ts +164 -0
- package/src/history/run-queries.ts +199 -0
- package/src/history/schema.ts +34 -0
- package/src/history/search-queries.ts +216 -0
- package/src/history/selected-record.ts +140 -0
- package/src/history/source-file.ts +51 -0
- package/src/history/source-watches.ts +156 -0
- package/src/history/store.ts +310 -197
- package/src/history/text-chunks.ts +181 -0
- package/src/history/text.ts +43 -0
- package/src/history/transcript-queries.ts +251 -0
- package/src/history/types.ts +27 -164
- package/src/history/values.ts +36 -0
- package/src/history/wire-inputs.ts +141 -0
- package/src/history/wire-models.ts +51 -0
- package/src/history/worker-runtime.ts +502 -0
- package/src/history/worker.ts +94 -336
- package/src/intercom/intercom-bridge.ts +136 -69
- package/src/intercom/live-intercom.ts +197 -92
- package/src/intercom/nested-result-tree.ts +213 -0
- package/src/intercom/result-delivery.ts +56 -0
- package/src/intercom/result-intercom.ts +364 -382
- package/src/pi-intercom/agent-dir.ts +9 -3
- package/src/pi-intercom/async.ts +33 -0
- package/src/pi-intercom/bridge-ask.ts +64 -0
- package/src/pi-intercom/bridge-config.ts +120 -0
- package/src/pi-intercom/bridge-http.ts +97 -0
- package/src/pi-intercom/bridge-launch-policy.ts +109 -0
- package/src/pi-intercom/bridge-launch.ts +317 -0
- package/src/pi-intercom/bridge-protocol.ts +123 -0
- package/src/pi-intercom/bridge-routes.ts +105 -0
- package/src/pi-intercom/bridge-rpc-process.ts +293 -0
- package/src/pi-intercom/bridge-session.ts +510 -0
- package/src/pi-intercom/bridge.ts +305 -549
- package/src/pi-intercom/broker/broker.ts +326 -263
- package/src/pi-intercom/broker/client.ts +273 -354
- package/src/pi-intercom/broker/connection.ts +192 -0
- package/src/pi-intercom/broker/framing.ts +11 -6
- package/src/pi-intercom/broker/paths.ts +28 -25
- package/src/pi-intercom/broker/pid.ts +68 -24
- package/src/pi-intercom/broker/snapshot-reader.ts +91 -0
- package/src/pi-intercom/broker/snapshots.ts +241 -0
- package/src/pi-intercom/broker/spawn.ts +58 -34
- package/src/pi-intercom/config.ts +113 -108
- package/src/pi-intercom/connection.ts +346 -0
- package/src/pi-intercom/identity.ts +71 -0
- package/src/pi-intercom/inbound-delivery.ts +450 -0
- package/src/pi-intercom/inbound-journal.ts +350 -0
- package/src/pi-intercom/inbound-record.ts +141 -0
- package/src/pi-intercom/inbound.ts +237 -0
- package/src/pi-intercom/index.ts +86 -2923
- package/src/pi-intercom/inspection.ts +82 -0
- package/src/pi-intercom/interview.ts +295 -0
- package/src/pi-intercom/lifecycle.ts +183 -0
- package/src/pi-intercom/message-format.ts +77 -0
- package/src/pi-intercom/outbound.ts +418 -0
- package/src/pi-intercom/peer-awareness.ts +84 -0
- package/src/pi-intercom/reply-tracker.ts +121 -59
- package/src/pi-intercom/reply-wait.ts +247 -0
- package/src/pi-intercom/runtime-types.ts +97 -0
- package/src/pi-intercom/session-events.ts +247 -0
- package/src/pi-intercom/session-targets.ts +124 -72
- package/src/pi-intercom/subagent-events.ts +299 -0
- package/src/pi-intercom/subagent-live-events.ts +212 -109
- package/src/pi-intercom/supervisor.ts +362 -0
- package/src/pi-intercom/tool-arguments.ts +120 -0
- package/src/pi-intercom/tool-registration.ts +121 -0
- package/src/pi-intercom/tool-results.ts +54 -0
- package/src/pi-intercom/tool-schemas.ts +134 -0
- package/src/pi-intercom/tools.ts +80 -0
- package/src/pi-intercom/topic-actions.ts +243 -0
- package/src/pi-intercom/topics.ts +267 -121
- package/src/pi-intercom/transport.ts +14 -0
- package/src/pi-intercom/types.ts +288 -190
- package/src/pi-intercom/ui/compose-paste.ts +109 -0
- package/src/pi-intercom/ui/compose.ts +173 -157
- package/src/pi-intercom/ui/inline-message.ts +80 -62
- package/src/pi-intercom/ui/message-renderers.ts +182 -0
- package/src/pi-intercom/ui/overlay.ts +238 -0
- package/src/pi-intercom/ui/session-format.ts +146 -0
- package/src/pi-intercom/ui/session-list.ts +145 -62
- package/src/pi-intercom/ui/tool-renderers.ts +148 -0
- package/src/pi-intercom/ui/topic-view.ts +107 -0
- package/src/pi-intercom/validation.ts +69 -0
- package/src/runs/background/async-chain-plan.ts +451 -0
- package/src/runs/background/async-control.ts +201 -71
- package/src/runs/background/async-execution.ts +206 -829
- package/src/runs/background/async-job-admission.ts +54 -0
- package/src/runs/background/async-job-projection.ts +225 -0
- package/src/runs/background/async-job-tracker.ts +416 -360
- package/src/runs/background/async-launch.ts +319 -0
- package/src/runs/background/async-plan.ts +285 -0
- package/src/runs/background/async-result-file.ts +79 -37
- package/src/runs/background/async-resume.ts +338 -285
- package/src/runs/background/async-run-discovery.ts +270 -0
- package/src/runs/background/async-run-location.ts +194 -0
- package/src/runs/background/async-run-record.ts +14 -0
- package/src/runs/background/async-run-repair.ts +307 -0
- package/src/runs/background/async-run-summary.ts +71 -0
- package/src/runs/background/async-single-plan.ts +168 -0
- package/src/runs/background/async-status-projection.ts +188 -0
- package/src/runs/background/async-status-render.ts +164 -0
- package/src/runs/background/async-status-validation.ts +238 -0
- package/src/runs/background/async-status.ts +9 -481
- package/src/runs/background/async-value.ts +29 -0
- package/src/runs/background/completion-dedupe.ts +103 -54
- package/src/runs/background/completion-delivery.ts +313 -187
- package/src/runs/background/completion-receipts.ts +309 -0
- package/src/runs/background/legacy-status-acceptance.ts +38 -0
- package/src/runs/background/notify.ts +167 -106
- package/src/runs/background/parallel-groups.ts +95 -46
- package/src/runs/background/result-payload.ts +268 -0
- package/src/runs/background/result-watcher.ts +484 -387
- package/src/runs/background/run-id-resolver.ts +126 -57
- package/src/runs/background/run-schemas.ts +240 -0
- package/src/runs/background/run-status-contracts.ts +46 -0
- package/src/runs/background/run-status-render.ts +364 -0
- package/src/runs/background/run-status.ts +338 -356
- package/src/runs/background/runner-attempt-output.ts +281 -0
- package/src/runs/background/runner-attempt.ts +354 -0
- package/src/runs/background/runner-child-executor.ts +232 -0
- package/src/runs/background/runner-child-observer.ts +270 -0
- package/src/runs/background/runner-completion-summary.ts +106 -0
- package/src/runs/background/runner-completion.ts +274 -0
- package/src/runs/background/runner-config.ts +52 -0
- package/src/runs/background/runner-contract.ts +51 -0
- package/src/runs/background/runner-dynamic.ts +185 -0
- package/src/runs/background/runner-finalization.ts +203 -0
- package/src/runs/background/runner-graph.ts +91 -0
- package/src/runs/background/runner-initial-status.ts +72 -0
- package/src/runs/background/runner-lifecycle.ts +159 -0
- package/src/runs/background/runner-monitor.ts +399 -0
- package/src/runs/background/runner-parallel.ts +280 -0
- package/src/runs/background/runner-prepare-step.ts +128 -0
- package/src/runs/background/runner-reporting.ts +192 -0
- package/src/runs/background/runner-status.ts +220 -0
- package/src/runs/background/runner-step-output.ts +158 -0
- package/src/runs/background/runner-step-record.ts +76 -0
- package/src/runs/background/runner-step.ts +150 -0
- package/src/runs/background/runner-streaming.ts +220 -0
- package/src/runs/background/runner-workflow.ts +284 -0
- package/src/runs/background/stale-run-reconciler.ts +279 -336
- package/src/runs/background/subagent-runner-launcher.ts +67 -35
- package/src/runs/background/subagent-runner.ts +80 -2048
- package/src/runs/background/top-level-async.ts +9 -7
- package/src/runs/foreground/chain-clarify.ts +499 -1334
- package/src/runs/foreground/clarification-draft.ts +92 -0
- package/src/runs/foreground/clarify-contracts.ts +33 -0
- package/src/runs/foreground/clarify-invocation.ts +192 -65
- package/src/runs/foreground/clarify-text-editor.ts +229 -0
- package/src/runs/foreground/clarify-view.ts +222 -0
- package/src/runs/foreground/execution-input.ts +358 -424
- package/src/runs/foreground/execution-routing.ts +41 -0
- package/src/runs/foreground/execution-timeout.ts +92 -0
- package/src/runs/foreground/foreground-control.ts +333 -812
- package/src/runs/foreground/foreground-memory.ts +132 -0
- package/src/runs/foreground/invocation-context.ts +88 -0
- package/src/runs/foreground/invocation-execution.ts +465 -0
- package/src/runs/foreground/invocation-expansion.ts +58 -0
- package/src/runs/foreground/management-actions.ts +360 -0
- package/src/runs/foreground/nested-control.ts +248 -0
- package/src/runs/foreground/question-continuation.ts +112 -0
- package/src/runs/foreground/question-control.ts +297 -70
- package/src/runs/foreground/resume-target.ts +330 -0
- package/src/runs/foreground/revival-launch-policy.ts +112 -0
- package/src/runs/foreground/run-async-path.ts +307 -223
- package/src/runs/foreground/run-interrupt.ts +202 -0
- package/src/runs/foreground/run-nudge.ts +355 -0
- package/src/runs/foreground/saved-revival.ts +356 -0
- package/src/runs/foreground/subagent-executor.ts +195 -493
- package/src/runs/foreground/subagent-params.ts +314 -242
- package/src/runs/foreground/wait-observation.ts +160 -0
- package/src/runs/foreground/wait-registration.ts +18 -0
- package/src/runs/foreground/wait-run.ts +361 -119
- package/src/runs/shared/acceptance-contract.ts +410 -209
- package/src/runs/shared/acceptance-defaults.ts +1 -0
- package/src/runs/shared/acceptance-evaluation.ts +314 -240
- package/src/runs/shared/acceptance-finalization.ts +447 -305
- package/src/runs/shared/acceptance-outcome.ts +69 -0
- package/src/runs/shared/acceptance-reports.ts +220 -126
- package/src/runs/shared/acceptance-submission.ts +251 -0
- package/src/runs/shared/acceptance-verification.ts +118 -0
- package/src/runs/shared/acceptance.ts +25 -25
- package/src/runs/shared/chain-outputs.ts +190 -98
- package/src/runs/shared/child-attempt-types.ts +118 -0
- package/src/runs/shared/child-attempt.ts +372 -608
- package/src/runs/shared/child-boundaries.ts +217 -0
- package/src/runs/shared/child-context-filter.ts +53 -0
- package/src/runs/shared/child-event-observer.ts +496 -0
- package/src/runs/shared/child-execution-cwd.ts +139 -68
- package/src/runs/shared/child-execution-outcome.ts +103 -0
- package/src/runs/shared/child-invocation.ts +74 -0
- package/src/runs/shared/child-message-validation.ts +233 -0
- package/src/runs/shared/child-native-accounting.ts +154 -0
- package/src/runs/shared/child-native-observation.ts +79 -0
- package/src/runs/shared/child-observation-spool.ts +188 -0
- package/src/runs/shared/child-observations.ts +219 -89
- package/src/runs/shared/child-presence.ts +6 -0
- package/src/runs/shared/child-route-env.ts +142 -0
- package/src/runs/shared/child-stream-observer.ts +302 -0
- package/src/runs/shared/child-stream-projection.ts +160 -0
- package/src/runs/shared/child-structured-tool.ts +38 -0
- package/src/runs/shared/claude-code.ts +201 -228
- package/src/runs/shared/claude-model.ts +67 -0
- package/src/runs/shared/claude-session.ts +124 -0
- package/src/runs/shared/claude-types.ts +31 -0
- package/src/runs/shared/completion-guard.ts +35 -26
- package/src/runs/shared/control-event-policy.ts +15 -0
- package/src/runs/shared/control-notification-owner.ts +20 -0
- package/src/runs/shared/dynamic-fanout.ts +402 -238
- package/src/runs/shared/dynamic-item.ts +114 -0
- package/src/runs/shared/history-index.ts +112 -51
- package/src/runs/shared/legacy-async-recovery.ts +153 -0
- package/src/runs/shared/legacy-output-recovery.ts +214 -0
- package/src/runs/shared/mcp-config-schema.ts +46 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +396 -307
- package/src/runs/shared/model-fallback.ts +302 -180
- package/src/runs/shared/mutating-tool-guard.ts +259 -158
- package/src/runs/shared/native-finalization-types.ts +29 -0
- package/src/runs/shared/native-finalization.ts +233 -109
- package/src/runs/shared/native-usage.ts +379 -96
- package/src/runs/shared/nested-control-records.ts +178 -0
- package/src/runs/shared/nested-events.ts +45 -752
- package/src/runs/shared/nested-path.ts +47 -34
- package/src/runs/shared/nested-projection.ts +230 -0
- package/src/runs/shared/nested-protocol.ts +67 -0
- package/src/runs/shared/nested-registry.ts +273 -0
- package/src/runs/shared/nested-render.ts +198 -100
- package/src/runs/shared/nested-route.ts +292 -0
- package/src/runs/shared/nested-validation.ts +291 -0
- package/src/runs/shared/owned-child-evidence.ts +184 -0
- package/src/runs/shared/owned-run-list.ts +187 -0
- package/src/runs/shared/owned-run-observation.ts +368 -0
- package/src/runs/shared/owned-run-progress.ts +172 -0
- package/src/runs/shared/owned-run-read-state.ts +7 -0
- package/src/runs/shared/owned-run-results.ts +289 -0
- package/src/runs/shared/owned-run-status.ts +239 -0
- package/src/runs/shared/owned-run-view.ts +65 -0
- package/src/runs/shared/owned-workflow-details.ts +24 -0
- package/src/runs/shared/owner-output.ts +73 -0
- package/src/runs/shared/parallel-utils.ts +93 -129
- package/src/runs/shared/parent-receipts.ts +177 -50
- package/src/runs/shared/parent-usage.ts +314 -91
- package/src/runs/shared/pi-args.ts +241 -311
- package/src/runs/shared/pi-launch-input.ts +48 -0
- package/src/runs/shared/pi-session-launch.ts +94 -0
- package/src/runs/shared/pi-spawn.ts +57 -33
- package/src/runs/shared/run-history-queries.ts +189 -38
- package/src/runs/shared/run-history.ts +197 -76
- package/src/runs/shared/run-id-ambiguity.ts +7 -3
- package/src/runs/shared/run-metadata-paths.ts +17 -0
- package/src/runs/shared/run-metadata.ts +240 -0
- package/src/runs/shared/run-persistence.ts +181 -0
- package/src/runs/shared/run-records.ts +19 -607
- package/src/runs/shared/run-restoration.ts +455 -0
- package/src/runs/shared/run-state-owner.ts +31 -0
- package/src/runs/shared/session-cwd-preload.ts +39 -19
- package/src/runs/shared/single-output.ts +358 -205
- package/src/runs/shared/stable-value.ts +15 -0
- package/src/runs/shared/streaming-text.ts +39 -12
- package/src/runs/shared/structured-output.ts +76 -46
- package/src/runs/shared/subagent-control.ts +330 -197
- package/src/runs/shared/subagent-prompt-runtime.ts +92 -174
- package/src/runs/shared/subagent-tool-loop-guard.ts +118 -68
- package/src/runs/shared/supervisor-question-state.ts +221 -0
- package/src/runs/shared/supervisor-questions.ts +396 -401
- package/src/runs/shared/workflow-graph.ts +400 -198
- package/src/runs/shared/workflow-policy.ts +245 -110
- package/src/runs/shared/worktree-contract.ts +29 -0
- package/src/runs/shared/worktree-hook.ts +181 -0
- package/src/runs/shared/worktree-location.ts +61 -0
- package/src/runs/shared/worktree-preservation.ts +10 -0
- package/src/runs/shared/worktree-process.ts +132 -0
- package/src/runs/shared/worktree.ts +459 -587
- package/src/shared/agent-context-policy.ts +176 -145
- package/src/shared/agent-dir.ts +15 -0
- package/src/shared/artifacts.ts +99 -68
- package/src/shared/atomic-json.ts +14 -11
- package/src/shared/chain-files.ts +72 -0
- package/src/shared/concurrency.ts +27 -0
- package/src/shared/content-preview.ts +117 -0
- package/src/shared/cooperative.ts +17 -13
- package/src/shared/execution-cwd.ts +34 -9
- package/src/shared/file-coalescer.ts +24 -22
- package/src/shared/fork-context.ts +52 -48
- package/src/shared/formatters.ts +88 -47
- package/src/shared/journal-frames.ts +197 -0
- package/src/shared/journal-projections.ts +133 -0
- package/src/shared/journal-reader.ts +18 -333
- package/src/shared/json-projection.ts +224 -0
- package/src/shared/lazy-tools.ts +76 -27
- package/src/shared/message-output.ts +146 -0
- package/src/shared/model-info.ts +94 -58
- package/src/shared/native-import.ts +25 -0
- package/src/shared/native-journal.ts +172 -0
- package/src/shared/native-session-loader.ts +13 -0
- package/src/shared/native-session.ts +3 -5
- package/src/shared/native-tui.ts +13 -3
- package/src/shared/native-typebox.ts +18 -3
- package/src/shared/output-page.ts +76 -0
- package/src/shared/post-exit-stdio-guard.ts +232 -172
- package/src/shared/prompt-sections.ts +13 -7
- package/src/shared/session-entries.ts +103 -44
- package/src/shared/session-identity.ts +17 -10
- package/src/shared/settings.ts +182 -367
- package/src/shared/status-format.ts +207 -98
- package/src/shared/stream-json.d.ts +10 -0
- package/src/shared/task-file-policy.ts +35 -0
- package/src/shared/temp-root.ts +122 -71
- package/src/shared/types/acceptance.ts +149 -0
- package/src/shared/types/async.ts +247 -0
- package/src/shared/types/config.ts +139 -0
- package/src/shared/types/details.ts +104 -0
- package/src/shared/types/history.ts +217 -0
- package/src/shared/types/inputs.ts +4 -0
- package/src/shared/types/intercom.ts +57 -0
- package/src/shared/types/launch.ts +39 -0
- package/src/shared/types/messages.ts +104 -0
- package/src/shared/types/nested.ts +153 -0
- package/src/shared/types/owned-runs.ts +148 -0
- package/src/shared/types/progress.ts +106 -0
- package/src/shared/types/questions.ts +103 -0
- package/src/shared/types/results.ts +119 -0
- package/src/shared/types/state.ts +56 -0
- package/src/shared/types/usage.ts +36 -0
- package/src/shared/types/workflow.ts +187 -0
- package/src/shared/types.ts +222 -1161
- package/src/shared/ui-mode.ts +1 -1
- package/src/shared/unknown.ts +59 -0
- package/src/shared/utils.ts +111 -305
- package/src/slash/command-discovery.ts +109 -0
- package/src/slash/command-input.ts +232 -0
- package/src/slash/prompt-template-bridge.ts +153 -400
- package/src/slash/prompt-template-contract.ts +152 -0
- package/src/slash/prompt-template-results.ts +189 -0
- package/src/slash/request-protocol.ts +87 -0
- package/src/slash/result-delivery.ts +312 -0
- package/src/slash/saved-chain.ts +115 -0
- package/src/slash/slash-bridge.ts +202 -156
- package/src/slash/slash-commands.ts +176 -598
- package/src/slash/slash-live-state.ts +287 -229
- package/src/tui/action-hints.ts +147 -61
- package/src/tui/activity-lines.ts +40 -0
- package/src/tui/agent-browser.ts +354 -0
- package/src/tui/agent-controls.ts +355 -0
- package/src/tui/agent-conversation.ts +514 -0
- package/src/tui/agent-dock.ts +250 -0
- package/src/tui/agent-history.ts +78 -203
- package/src/tui/agent-model.ts +78 -0
- package/src/tui/agent-picker.ts +325 -0
- package/src/tui/agent-view.ts +391 -1333
- package/src/tui/compact-results.ts +342 -0
- package/src/tui/conversation-details.ts +206 -0
- package/src/tui/conversation-frame.ts +184 -0
- package/src/tui/conversation-history.ts +246 -0
- package/src/tui/conversation-menu.ts +119 -0
- package/src/tui/conversation-notices.ts +116 -0
- package/src/tui/conversation-selection.ts +66 -0
- package/src/tui/conversation-viewport.ts +169 -0
- package/src/tui/direction-message.ts +35 -0
- package/src/tui/display.ts +409 -0
- package/src/tui/editor-viewport.ts +19 -0
- package/src/tui/expanded-content.ts +134 -0
- package/src/tui/expanded-multi.ts +264 -0
- package/src/tui/expanded-single.ts +92 -0
- package/src/tui/history-cards.ts +316 -0
- package/src/tui/history-display.ts +334 -0
- package/src/tui/history-json.ts +17 -0
- package/src/tui/history-metadata.ts +177 -0
- package/src/tui/history-record.ts +321 -0
- package/src/tui/history-text.ts +34 -0
- package/src/tui/indexed-history.ts +149 -0
- package/src/tui/nested-widget.ts +192 -0
- package/src/tui/picker-page.ts +80 -0
- package/src/tui/render.ts +100 -1546
- package/src/tui/result-facts.ts +99 -0
- package/src/tui/task-observation.ts +96 -0
- package/src/tui/task-store.ts +432 -0
- package/src/tui/text-values.ts +20 -0
- package/src/tui/view-connections.ts +64 -0
- package/src/tui/view-model.ts +181 -0
- package/src/tui/view-persistence.ts +57 -0
- package/src/tui/view-ports.ts +55 -0
- package/src/tui/view-session.ts +42 -0
- package/src/tui/widget-details.ts +220 -0
- package/src/tui/widget-status.ts +331 -0
- package/src/tui/widget.ts +208 -0
- package/src/tui/workflow-labels.ts +340 -0
- package/src/tui/workflow-outcomes.ts +150 -0
- package/tsconfig.build.json +11 -10
- package/tsconfig.json +30 -17
|
@@ -1,2942 +1,88 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { randomUUID } from "crypto";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { StringEnum } from "@earendil-works/pi-ai";
|
|
7
|
-
import { Type } from "../shared/native-typebox.js";
|
|
8
|
-
import { MouseRegion, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
-
import { IntercomClient } from "./broker/client.js";
|
|
10
|
-
import { isBrokerRunning, spawnBrokerIfNeeded } from "./broker/spawn.js";
|
|
11
|
-
import { SessionListOverlay } from "./ui/session-list.js";
|
|
12
|
-
import { ComposeOverlay } from "./ui/compose.js";
|
|
13
|
-
import { InlineMessageComponent } from "./ui/inline-message.js";
|
|
14
1
|
import { loadConfig } from "./config.js";
|
|
15
|
-
import {
|
|
2
|
+
import { readChildOrchestratorMetadata } from "./identity.js";
|
|
3
|
+
import { ReplyTracker } from "./reply-tracker.js";
|
|
16
4
|
import { IntercomTopics } from "./topics.js";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const INTERCOM_DETACH_RESPONSE_TIMEOUT_MS = 500;
|
|
35
|
-
const INBOUND_FLUSH_DELAY_MS = 200;
|
|
36
|
-
const INBOUND_IDLE_RETRY_MS = 500;
|
|
37
|
-
const NON_UI_REPLACE_FLUSH_DELAY_MS = 1_600;
|
|
38
|
-
const PEER_AWARENESS_LIST_TIMEOUT_MS = 75;
|
|
39
|
-
const DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX = "subagent-chat";
|
|
40
|
-
const RECIPIENT_TURN_FAILED_PREFIX = "Recipient turn failed:";
|
|
41
|
-
const RECIPIENT_TURN_FAILED_ATTACHMENT = "pi-intercom-recipient-turn-failure";
|
|
42
|
-
const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "PI_SUBAGENT_ORCHESTRATOR_TARGET";
|
|
43
|
-
const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID";
|
|
44
|
-
const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT";
|
|
45
|
-
const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX";
|
|
46
|
-
const SUBAGENT_INTERCOM_SESSION_NAME_ENV = "PI_SUBAGENT_INTERCOM_SESSION_NAME";
|
|
47
|
-
const PACKAGE_ROOT = fileURLToPath(new URL("../../", import.meta.url));
|
|
48
|
-
const TOPICS_UNAVAILABLE = "Intercom topics are unavailable while an older broker is still running. Direct messaging and active sessions are unchanged. Let its sessions close normally; after it exits, reconnect with the updated package and retry. Nothing was stopped or restarted.";
|
|
49
|
-
function getErrorMessage(error) {
|
|
50
|
-
return error instanceof Error ? error.message : String(error);
|
|
51
|
-
}
|
|
52
|
-
function toError(error) {
|
|
53
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
54
|
-
}
|
|
55
|
-
function failureDetails(reasonCode, nextActions, details = {}) {
|
|
56
|
-
return { ...details, reasonCode, nextActions };
|
|
57
|
-
}
|
|
58
|
-
function replyFailureReason(message) {
|
|
59
|
-
if (message.startsWith("No active intercom context") || message.startsWith("No pending ask"))
|
|
60
|
-
return "no_pending_reply";
|
|
61
|
-
if (message.startsWith("Multiple pending asks") || message.includes("too short"))
|
|
62
|
-
return "ambiguous_reply_target";
|
|
63
|
-
return "reply_failed";
|
|
64
|
-
}
|
|
65
|
-
function inboundIdFromCustomMessage(message) {
|
|
66
|
-
if (typeof message !== "object" || message === null)
|
|
67
|
-
return undefined;
|
|
68
|
-
const record = message;
|
|
69
|
-
if (record.customType !== "intercom_message" && record.customType !== "subagent-human-message")
|
|
70
|
-
return undefined;
|
|
71
|
-
const details = record.details;
|
|
72
|
-
if (typeof details !== "object" || details === null)
|
|
73
|
-
return undefined;
|
|
74
|
-
const inboundId = details.message?.id;
|
|
75
|
-
return typeof inboundId === "string" ? inboundId : undefined;
|
|
76
|
-
}
|
|
77
|
-
function getAssistantErrorMessage(message) {
|
|
78
|
-
if (typeof message !== "object" || message === null) {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
const record = message;
|
|
82
|
-
if (record.role !== "assistant") {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
const errorMessage = typeof record.errorMessage === "string" && record.errorMessage.trim()
|
|
86
|
-
? record.errorMessage.trim()
|
|
87
|
-
: undefined;
|
|
88
|
-
if (record.stopReason === "error") {
|
|
89
|
-
return errorMessage ?? "assistant turn failed";
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
function formatAttachments(attachments) {
|
|
94
|
-
let text = "";
|
|
95
|
-
for (const att of attachments) {
|
|
96
|
-
if (att.language) {
|
|
97
|
-
text += `\n\n---\n📎 ${att.name}\n~~~${att.language}\n${att.content}\n~~~`;
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
text += `\n\n---\n📎 ${att.name}\n${att.content}`;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return text;
|
|
104
|
-
}
|
|
105
|
-
function readChildOrchestratorMetadata() {
|
|
106
|
-
const orchestratorTarget = process.env[SUBAGENT_ORCHESTRATOR_TARGET_ENV]?.trim();
|
|
107
|
-
const runId = process.env[SUBAGENT_RUN_ID_ENV]?.trim();
|
|
108
|
-
const agent = process.env[SUBAGENT_CHILD_AGENT_ENV]?.trim();
|
|
109
|
-
const index = process.env[SUBAGENT_CHILD_INDEX_ENV]?.trim();
|
|
110
|
-
if (!orchestratorTarget || !runId || !agent || !index) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
const sessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
|
|
114
|
-
return {
|
|
115
|
-
orchestratorTarget,
|
|
116
|
-
runId,
|
|
117
|
-
agent,
|
|
118
|
-
index,
|
|
119
|
-
...(sessionName ? { sessionName } : {}),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
function formatChildOrchestratorMessage(kind, metadata, message) {
|
|
123
|
-
const heading = kind === "ask"
|
|
124
|
-
? "Subagent needs a supervisor decision."
|
|
125
|
-
: kind === "interview"
|
|
126
|
-
? "Subagent requests a structured supervisor interview."
|
|
127
|
-
: "Subagent progress update.";
|
|
128
|
-
return [
|
|
129
|
-
heading,
|
|
130
|
-
`Run: ${metadata.runId}`,
|
|
131
|
-
`Agent: ${metadata.agent}`,
|
|
132
|
-
`Child index: ${metadata.index}`,
|
|
133
|
-
metadata.sessionName ? `Child intercom target: ${metadata.sessionName}` : undefined,
|
|
134
|
-
"",
|
|
135
|
-
message,
|
|
136
|
-
].filter((line) => line !== undefined).join("\n");
|
|
137
|
-
}
|
|
138
|
-
function validateSupervisorInterviewRequest(input) {
|
|
139
|
-
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
140
|
-
return { ok: false, error: "interview must be an object with a questions array" };
|
|
141
|
-
}
|
|
142
|
-
const raw = input;
|
|
143
|
-
if (raw.title !== undefined && typeof raw.title !== "string") {
|
|
144
|
-
return { ok: false, error: "interview.title must be a string when provided" };
|
|
145
|
-
}
|
|
146
|
-
if (raw.description !== undefined && typeof raw.description !== "string") {
|
|
147
|
-
return { ok: false, error: "interview.description must be a string when provided" };
|
|
148
|
-
}
|
|
149
|
-
if (!Array.isArray(raw.questions) || raw.questions.length === 0) {
|
|
150
|
-
return { ok: false, error: "interview.questions must be a non-empty array" };
|
|
151
|
-
}
|
|
152
|
-
const validTypes = new Set(["single", "multi", "text", "image", "info"]);
|
|
153
|
-
const ids = new Set();
|
|
154
|
-
const questions = [];
|
|
155
|
-
for (let index = 0; index < raw.questions.length; index++) {
|
|
156
|
-
const questionInput = raw.questions[index];
|
|
157
|
-
if (!questionInput || typeof questionInput !== "object" || Array.isArray(questionInput)) {
|
|
158
|
-
return { ok: false, error: `interview.questions[${index}] must be an object` };
|
|
159
|
-
}
|
|
160
|
-
const question = questionInput;
|
|
161
|
-
if (typeof question.id !== "string" || question.id.trim() === "") {
|
|
162
|
-
return { ok: false, error: `interview.questions[${index}].id must be a non-empty string` };
|
|
163
|
-
}
|
|
164
|
-
const id = question.id.trim();
|
|
165
|
-
if (ids.has(id)) {
|
|
166
|
-
return { ok: false, error: `interview question id must be unique: ${id}` };
|
|
167
|
-
}
|
|
168
|
-
ids.add(id);
|
|
169
|
-
if (typeof question.type !== "string" || !validTypes.has(question.type)) {
|
|
170
|
-
return { ok: false, error: `interview.questions[${index}].type must be one of: single, multi, text, image, info` };
|
|
171
|
-
}
|
|
172
|
-
if (typeof question.question !== "string" || question.question.trim() === "") {
|
|
173
|
-
return { ok: false, error: `interview.questions[${index}].question must be a non-empty string` };
|
|
174
|
-
}
|
|
175
|
-
if (question.context !== undefined && typeof question.context !== "string") {
|
|
176
|
-
return { ok: false, error: `interview.questions[${index}].context must be a string when provided` };
|
|
177
|
-
}
|
|
178
|
-
let options;
|
|
179
|
-
if (question.options !== undefined) {
|
|
180
|
-
if (!Array.isArray(question.options)) {
|
|
181
|
-
return { ok: false, error: `interview.questions[${index}].options must be an array when provided` };
|
|
182
|
-
}
|
|
183
|
-
options = [];
|
|
184
|
-
for (let optionIndex = 0; optionIndex < question.options.length; optionIndex++) {
|
|
185
|
-
const option = question.options[optionIndex];
|
|
186
|
-
if (typeof option === "string") {
|
|
187
|
-
const label = option.trim();
|
|
188
|
-
if (!label) {
|
|
189
|
-
return { ok: false, error: `interview.questions[${index}].options[${optionIndex}] must not be empty` };
|
|
190
|
-
}
|
|
191
|
-
options.push(label);
|
|
192
|
-
}
|
|
193
|
-
else if (!option || typeof option !== "object" || Array.isArray(option) || typeof option.label !== "string" || option.label.trim() === "") {
|
|
194
|
-
return { ok: false, error: `interview.questions[${index}].options[${optionIndex}] must be a non-empty string or an object with a non-empty label` };
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
options.push({ ...option, label: option.label.trim() });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if ((question.type === "single" || question.type === "multi") && (!options || options.length === 0)) {
|
|
202
|
-
return { ok: false, error: `interview.questions[${index}].options must be a non-empty array for ${question.type} questions` };
|
|
203
|
-
}
|
|
204
|
-
if (question.type !== "single" && question.type !== "multi" && options) {
|
|
205
|
-
return { ok: false, error: `interview.questions[${index}].options is only valid for single and multi questions` };
|
|
206
|
-
}
|
|
207
|
-
questions.push({
|
|
208
|
-
...question,
|
|
209
|
-
id,
|
|
210
|
-
type: question.type,
|
|
211
|
-
question: question.question.trim(),
|
|
212
|
-
...(options ? { options } : {}),
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
return {
|
|
216
|
-
ok: true,
|
|
217
|
-
interview: {
|
|
218
|
-
...raw,
|
|
219
|
-
...(typeof raw.title === "string" ? { title: raw.title.trim() } : {}),
|
|
220
|
-
...(typeof raw.description === "string" ? { description: raw.description.trim() } : {}),
|
|
221
|
-
questions,
|
|
222
|
-
},
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
function interviewOptionLabel(option) {
|
|
226
|
-
return typeof option === "string" ? option : option.label;
|
|
227
|
-
}
|
|
228
|
-
function interviewExampleValue(question) {
|
|
229
|
-
if (question.type === "multi") {
|
|
230
|
-
return question.options?.slice(0, 2).map(interviewOptionLabel) ?? [];
|
|
231
|
-
}
|
|
232
|
-
if (question.type === "single") {
|
|
233
|
-
return question.options?.[0] !== undefined ? interviewOptionLabel(question.options[0]) : "option label";
|
|
234
|
-
}
|
|
235
|
-
if (question.type === "image") {
|
|
236
|
-
return "image/file reference or description";
|
|
237
|
-
}
|
|
238
|
-
return "answer text";
|
|
239
|
-
}
|
|
240
|
-
function formatSupervisorInterviewRequest(interview, message) {
|
|
241
|
-
const lines = [];
|
|
242
|
-
const title = interview.title?.trim();
|
|
243
|
-
if (title)
|
|
244
|
-
lines.push(`Interview: ${title}`);
|
|
245
|
-
const description = interview.description?.trim();
|
|
246
|
-
if (description)
|
|
247
|
-
lines.push(description);
|
|
248
|
-
const note = message?.trim();
|
|
249
|
-
if (note)
|
|
250
|
-
lines.push(`Child note: ${note}`);
|
|
251
|
-
if (lines.length > 0)
|
|
252
|
-
lines.push("");
|
|
253
|
-
lines.push("Questions:");
|
|
254
|
-
interview.questions.forEach((question, index) => {
|
|
255
|
-
lines.push(`${index + 1}. [${question.id}] (${question.type}) ${question.question}`);
|
|
256
|
-
if (typeof question.context === "string" && question.context.trim()) {
|
|
257
|
-
lines.push(` Context: ${question.context.trim()}`);
|
|
258
|
-
}
|
|
259
|
-
if (question.options?.length) {
|
|
260
|
-
lines.push(" Options:");
|
|
261
|
-
for (const option of question.options) {
|
|
262
|
-
lines.push(` - ${interviewOptionLabel(option)}`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
const responseExample = {
|
|
267
|
-
responses: interview.questions
|
|
268
|
-
.filter((question) => question.type !== "info")
|
|
269
|
-
.map((question) => ({
|
|
270
|
-
id: question.id,
|
|
271
|
-
value: interviewExampleValue(question),
|
|
272
|
-
})),
|
|
273
|
-
};
|
|
274
|
-
lines.push("", "Supervisor reply instructions:", "Reply with plain JSON or a fenced ```json block using this stable shape. Use the question ids exactly. Info questions are context-only and do not need responses. For single questions, value is one option label. For multi questions, value is an array of option labels. For text/image questions, value is a string unless the question asks otherwise.", "", "```json", JSON.stringify(responseExample, null, 2), "```");
|
|
275
|
-
return lines.join("\n");
|
|
276
|
-
}
|
|
277
|
-
function validateSupervisorInterviewReply(value, interview) {
|
|
278
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
279
|
-
throw new Error("reply JSON must be an object with a responses array");
|
|
280
|
-
}
|
|
281
|
-
const responsesInput = value.responses;
|
|
282
|
-
if (!Array.isArray(responsesInput)) {
|
|
283
|
-
throw new Error("reply JSON must include a responses array");
|
|
284
|
-
}
|
|
285
|
-
const questionById = new Map(interview.questions
|
|
286
|
-
.filter((question) => question.type !== "info")
|
|
287
|
-
.map((question) => [question.id, question]));
|
|
288
|
-
const seenIds = new Set();
|
|
289
|
-
const responses = [];
|
|
290
|
-
for (let index = 0; index < responsesInput.length; index++) {
|
|
291
|
-
const response = responsesInput[index];
|
|
292
|
-
if (!response || typeof response !== "object" || Array.isArray(response)) {
|
|
293
|
-
throw new Error(`responses[${index}] must be an object`);
|
|
294
|
-
}
|
|
295
|
-
const raw = response;
|
|
296
|
-
if (typeof raw.id !== "string" || raw.id.trim() === "") {
|
|
297
|
-
throw new Error(`responses[${index}].id must be a non-empty string`);
|
|
298
|
-
}
|
|
299
|
-
const id = raw.id.trim();
|
|
300
|
-
const question = questionById.get(id);
|
|
301
|
-
if (!question) {
|
|
302
|
-
throw new Error(`responses[${index}].id must match a non-info interview question id`);
|
|
303
|
-
}
|
|
304
|
-
if (seenIds.has(id)) {
|
|
305
|
-
throw new Error(`responses[${index}].id is duplicated: ${id}`);
|
|
306
|
-
}
|
|
307
|
-
seenIds.add(id);
|
|
308
|
-
if (!Object.hasOwn(raw, "value")) {
|
|
309
|
-
throw new Error(`responses[${index}].value is required`);
|
|
310
|
-
}
|
|
311
|
-
const value = raw.value;
|
|
312
|
-
if (question.type === "single") {
|
|
313
|
-
if (typeof value !== "string")
|
|
314
|
-
throw new Error(`responses[${index}].value must be a string for single questions`);
|
|
315
|
-
const optionLabels = new Set(question.options?.map(interviewOptionLabel));
|
|
316
|
-
if (!optionLabels.has(value.trim()))
|
|
317
|
-
throw new Error(`responses[${index}].value must match one of the question options`);
|
|
318
|
-
responses.push({ id, value: value.trim() });
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
if (question.type === "multi") {
|
|
322
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
323
|
-
throw new Error(`responses[${index}].value must be an array of strings for multi questions`);
|
|
324
|
-
}
|
|
325
|
-
const optionLabels = new Set(question.options?.map(interviewOptionLabel));
|
|
326
|
-
const selected = value.map((item) => item.trim());
|
|
327
|
-
const invalid = selected.find((item) => !optionLabels.has(item));
|
|
328
|
-
if (invalid)
|
|
329
|
-
throw new Error(`responses[${index}].value contains an option that is not in the question options: ${invalid}`);
|
|
330
|
-
responses.push({ id, value: selected });
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
333
|
-
if (typeof value !== "string") {
|
|
334
|
-
throw new Error(`responses[${index}].value must be a string for ${question.type} questions`);
|
|
335
|
-
}
|
|
336
|
-
responses.push({ id, value });
|
|
337
|
-
}
|
|
338
|
-
return { responses };
|
|
339
|
-
}
|
|
340
|
-
function parseStructuredSupervisorReply(text, interview) {
|
|
341
|
-
const fencedMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
342
|
-
const candidate = (fencedMatch?.[1] ?? text).trim();
|
|
343
|
-
if (!candidate.startsWith("{") && !candidate.startsWith("[")) {
|
|
344
|
-
return undefined;
|
|
345
|
-
}
|
|
346
|
-
try {
|
|
347
|
-
return { value: validateSupervisorInterviewReply(JSON.parse(candidate), interview) };
|
|
348
|
-
}
|
|
349
|
-
catch (error) {
|
|
350
|
-
return { error: getErrorMessage(error) };
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
function duplicateSessionNames(sessions) {
|
|
354
|
-
return new Set(sessions
|
|
355
|
-
.map(s => s.name?.toLowerCase())
|
|
356
|
-
.filter((name) => Boolean(name))
|
|
357
|
-
.filter((name, index, names) => names.indexOf(name) !== index));
|
|
358
|
-
}
|
|
359
|
-
function parseSubagentIntercomPayload(payload) {
|
|
360
|
-
if (typeof payload !== "object" || payload === null) {
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
const record = payload;
|
|
364
|
-
if (typeof record.to !== "string" || typeof record.message !== "string") {
|
|
365
|
-
return null;
|
|
366
|
-
}
|
|
367
|
-
const requestId = typeof record.requestId === "string" ? record.requestId : undefined;
|
|
368
|
-
const source = record.source === "foreground" || record.source === "async" ? record.source : undefined;
|
|
369
|
-
const children = Array.isArray(record.children) ? record.children.flatMap((value) => {
|
|
370
|
-
if (!value || typeof value !== "object")
|
|
371
|
-
return [];
|
|
372
|
-
const child = value;
|
|
373
|
-
return typeof child.agent === "string" && typeof child.index === "number" && Number.isSafeInteger(child.index) && child.index >= 0
|
|
374
|
-
&& typeof child.intercomTarget === "string" && child.intercomTarget.length > 0 && typeof child.status === "string"
|
|
375
|
-
&& ["completed", "failed", "blocked", "paused", "timed-out", "detached"].includes(child.status)
|
|
376
|
-
? [{ agent: child.agent, index: child.index, status: child.status, intercomTarget: child.intercomTarget }] : [];
|
|
377
|
-
}) : [];
|
|
378
|
-
const completion = source && typeof record.runId === "string" && typeof record.status === "string" && children.length
|
|
379
|
-
? { runId: record.runId, status: record.status, children, ...(typeof record.completionId === "string" ? { completionId: record.completionId } : {}) } : undefined;
|
|
380
|
-
return { to: record.to, message: record.message, ...(requestId ? { requestId } : {}), ...(source ? { source } : {}), ...(completion ? { completion } : {}) };
|
|
381
|
-
}
|
|
382
|
-
function resolveIntercomPresenceName(sessionName, sessionId) {
|
|
383
|
-
const trimmedName = sessionName?.trim();
|
|
384
|
-
if (trimmedName) {
|
|
385
|
-
return trimmedName;
|
|
386
|
-
}
|
|
387
|
-
const normalizedSessionId = sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId;
|
|
388
|
-
return `${DEFAULT_UNNAMED_SESSION_ALIAS_PREFIX}-${normalizedSessionId.slice(0, 8)}`;
|
|
389
|
-
}
|
|
390
|
-
function buildPresenceIdentity(pi, sessionId) {
|
|
391
|
-
const subagentIntercomSessionName = process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV]?.trim();
|
|
392
|
-
return {
|
|
393
|
-
name: subagentIntercomSessionName || resolveIntercomPresenceName(pi.getSessionName(), sessionId),
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
function formatDuration(seconds) {
|
|
397
|
-
if (seconds < 60)
|
|
398
|
-
return `${seconds}s`;
|
|
399
|
-
if (seconds < 3600)
|
|
400
|
-
return `${Math.floor(seconds / 60)}m`;
|
|
401
|
-
return `${Math.floor(seconds / 3600)}h`;
|
|
402
|
-
}
|
|
403
|
-
function sessionBusyState(session) {
|
|
404
|
-
if (session.acceptsAsks === true)
|
|
405
|
-
return "idle";
|
|
406
|
-
if (session.acceptsAsks === false)
|
|
407
|
-
return "busy";
|
|
408
|
-
const status = session.status ?? "";
|
|
409
|
-
if (status === "idle" || status.startsWith("idle ") || status.startsWith("idle ·"))
|
|
410
|
-
return "idle";
|
|
411
|
-
if (status === "thinking" || status.startsWith("thinking ") || status.startsWith("thinking ·") || status.startsWith("tool:"))
|
|
412
|
-
return "busy";
|
|
413
|
-
return "unknown";
|
|
414
|
-
}
|
|
415
|
-
function formatIntercomAge(timestamp, now) {
|
|
416
|
-
return typeof timestamp === "number" && timestamp > 0
|
|
417
|
-
? `${formatDuration(Math.max(0, Math.floor((now - timestamp) / 1000)))} ago`
|
|
418
|
-
: "none";
|
|
419
|
-
}
|
|
420
|
-
function sessionDeliveryGuidance(session, isSelf) {
|
|
421
|
-
if (isSelf)
|
|
422
|
-
return "self target unavailable; choose a peer from Other sessions; use pending/reply for inbound asks";
|
|
423
|
-
const state = sessionBusyState(session);
|
|
424
|
-
if (state === "idle")
|
|
425
|
-
return "send defaults to steer and wakes; ask only if sender must stay alive for a required reply; queue only for intentional delay; passive discouraged";
|
|
426
|
-
if (session.acceptsAsks === false)
|
|
427
|
-
return "send defaults to steer; ask only if sender must stay alive for a required reply (default sends without waiting when peer is busy); queue only for intentional delay; passive discouraged";
|
|
428
|
-
if (state === "busy")
|
|
429
|
-
return "send defaults to steer at the next tool boundary; ask only if sender must stay alive for a required reply; queue only for intentional delay; passive discouraged";
|
|
430
|
-
return "state unknown; target is valid; send defaults to steer; ask only if sender must stay alive for a required reply; queue only for intentional delay; passive discouraged";
|
|
431
|
-
}
|
|
432
|
-
function formatSessionListRow(session, currentCwd, isSelf, duplicates = new Set(), allSessions = [session], now = Date.now()) {
|
|
433
|
-
const name = session.name || "Unnamed session";
|
|
434
|
-
const duplicateName = Boolean(session.name && duplicates.has(session.name.toLowerCase()));
|
|
435
|
-
const healthTags = [];
|
|
436
|
-
healthTags.push(`state:${sessionBusyState(session)}`);
|
|
437
|
-
healthTags.push(`accepts_asks:${session.acceptsAsks === undefined ? "unknown" : session.acceptsAsks ? "true" : "false"}`);
|
|
438
|
-
healthTags.push(`pending_asks:${typeof session.pendingAsks === "number" ? session.pendingAsks : "unknown"}`);
|
|
439
|
-
healthTags.push(`last_intercom_activity:${formatIntercomAge(session.lastIntercomActivity, now)}`);
|
|
440
|
-
if (typeof session.lastSeen === "number") {
|
|
441
|
-
healthTags.push(`last_seen:${formatDuration(Math.max(0, Math.floor((now - session.lastSeen) / 1000)))} ago`);
|
|
442
|
-
}
|
|
443
|
-
const tags = [
|
|
444
|
-
isSelf ? "self" : session.cwd === currentCwd ? "same native session cwd" : undefined,
|
|
445
|
-
session.status,
|
|
446
|
-
duplicateName ? `target:${formatSessionTarget(session, allSessions)}` : undefined,
|
|
447
|
-
...healthTags,
|
|
448
|
-
].filter((tag) => Boolean(tag));
|
|
449
|
-
const target = formatSessionTarget(session, allSessions);
|
|
450
|
-
const suffix = tags.length ? ` [${tags.join(", ")}]` : "";
|
|
451
|
-
return `• ${name} (${target}) — Native session cwd: ${session.cwd} (${session.model})${suffix}\n ↳ ${sessionDeliveryGuidance(session, isSelf)}`;
|
|
452
|
-
}
|
|
453
|
-
function sessionsForScope(sessions, currentSessionId, scope) {
|
|
454
|
-
return scope === "all" ? sessions : filterProjectSessions(sessions, currentSessionId);
|
|
455
|
-
}
|
|
456
|
-
function formatSessionListSections(allSessions, currentSessionId, scope = "project") {
|
|
457
|
-
const currentSession = allSessions.find(s => s.id === currentSessionId);
|
|
458
|
-
if (!currentSession) {
|
|
459
|
-
throw new Error("Current session is missing from intercom session list.");
|
|
460
|
-
}
|
|
461
|
-
const sessions = sessionsForScope(allSessions, currentSessionId, scope);
|
|
462
|
-
const hiddenSessionCount = allSessions.length - sessions.length;
|
|
463
|
-
const duplicates = duplicateSessionNames(allSessions);
|
|
464
|
-
const otherSessions = sessions.filter(s => s.id !== currentSessionId);
|
|
465
|
-
const currentSection = `**Current session:**\n${formatSessionListRow(currentSession, currentSession.cwd, true, duplicates, allSessions)}`;
|
|
466
|
-
const otherSection = otherSessions.length === 0
|
|
467
|
-
? `**Other sessions:**\n${hiddenSessionCount > 0
|
|
468
|
-
? "No other sessions connected in this project."
|
|
469
|
-
: `No other sessions connected. Start another intercom-enabled session with \`pi --name worker\`, then run \`intercom({ action: "list" })\` again. If you are dogfooding this local fork without installing it, start the peer with \`${localForkStartCommand()}\`.`}`
|
|
470
|
-
: `**Other sessions:**\n${otherSessions.map(s => formatSessionListRow(s, currentSession.cwd, false, duplicates, allSessions)).join("\n")}`;
|
|
471
|
-
const scopeHint = hiddenSessionCount > 0
|
|
472
|
-
? `\n\n${hiddenSessionCount} session${hiddenSessionCount === 1 ? "" : "s"} in other projects hidden. Use \`intercom({ action: "list", scope: "all" })\` to show every connected session.`
|
|
473
|
-
: "";
|
|
474
|
-
return `${currentSection}\n\n${otherSection}${scopeHint}`;
|
|
475
|
-
}
|
|
476
|
-
function previewText(value, maxLength = 72) {
|
|
477
|
-
if (typeof value !== "string") {
|
|
478
|
-
return undefined;
|
|
479
|
-
}
|
|
480
|
-
const normalized = value.replace(/\s+/g, " ").trim();
|
|
481
|
-
if (!normalized) {
|
|
482
|
-
return undefined;
|
|
483
|
-
}
|
|
484
|
-
return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1)}…` : normalized;
|
|
485
|
-
}
|
|
486
|
-
function pendingAskPreview(message) {
|
|
487
|
-
const text = message.content.text;
|
|
488
|
-
const normalized = text.replace(/\s+/g, " ").trim();
|
|
489
|
-
if (text.startsWith("Subagent needs a supervisor decision.") || text.startsWith("Subagent requests a structured supervisor interview.")) {
|
|
490
|
-
const run = text.match(/^Run:\s*(.+)$/m)?.[1]?.trim();
|
|
491
|
-
const agent = text.match(/^Agent:\s*(.+)$/m)?.[1]?.trim();
|
|
492
|
-
const childTarget = text.match(/^Child intercom target:\s*(.+)$/m)?.[1]?.trim();
|
|
493
|
-
const body = text.split(/\n\s*\n/).slice(1).join(" ").replace(/\s+/g, " ").trim();
|
|
494
|
-
return [
|
|
495
|
-
text.startsWith("Subagent requests") ? "structured supervisor interview" : "supervisor decision",
|
|
496
|
-
run ? `run=${run}` : undefined,
|
|
497
|
-
agent ? `agent=${agent}` : undefined,
|
|
498
|
-
childTarget ? `target=${childTarget}` : undefined,
|
|
499
|
-
body ? `question=${previewText(body, 180)}` : undefined,
|
|
500
|
-
].filter((part) => Boolean(part)).join(" · ");
|
|
501
|
-
}
|
|
502
|
-
return previewText(normalized, 180) ?? normalized;
|
|
503
|
-
}
|
|
504
|
-
function firstTextContent(result) {
|
|
505
|
-
return result.content?.find((item) => item.type === "text" && typeof item.text === "string")?.text?.replace(/\*\*/g, "") ?? "";
|
|
506
|
-
}
|
|
507
|
-
class ToolExecutionFailure extends Error {
|
|
508
|
-
details;
|
|
509
|
-
constructor(message, details) {
|
|
510
|
-
super(message);
|
|
511
|
-
this.details = details;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
class AskDeliveryError extends Error {
|
|
515
|
-
result;
|
|
516
|
-
constructor(result) {
|
|
517
|
-
super(result.reason ?? "Session may not exist or has disconnected.");
|
|
518
|
-
this.result = result;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
function throwIfToolError(result) {
|
|
522
|
-
if (!result.isError)
|
|
523
|
-
return result;
|
|
524
|
-
throw new ToolExecutionFailure(firstTextContent(result) || "Tool failed", result.details);
|
|
525
|
-
}
|
|
526
|
-
function shellQuote(value) {
|
|
527
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
528
|
-
}
|
|
529
|
-
function localForkStartCommand() {
|
|
530
|
-
return `pi --name worker --extension ${shellQuote(fileURLToPath(import.meta.url))} --skill ${shellQuote(path.join(PACKAGE_ROOT, "skills", "pi-intercom"))}`;
|
|
531
|
-
}
|
|
532
|
-
async function settleWithin(operation, timeoutMs) {
|
|
533
|
-
return await new Promise((resolve) => {
|
|
534
|
-
let settled = false;
|
|
535
|
-
const finish = (value) => {
|
|
536
|
-
if (settled)
|
|
537
|
-
return;
|
|
538
|
-
settled = true;
|
|
539
|
-
clearTimeout(timer);
|
|
540
|
-
resolve(value);
|
|
541
|
-
};
|
|
542
|
-
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
543
|
-
timer.unref?.();
|
|
544
|
-
void Promise.resolve().then(operation).then((value) => finish(value), () => finish(null));
|
|
545
|
-
});
|
|
546
|
-
}
|
|
5
|
+
import { IntercomLifecycle } from "./lifecycle.js";
|
|
6
|
+
import { InboundJournal } from "./inbound-journal.js";
|
|
7
|
+
import { InboundDeliveryOwner } from "./inbound-delivery.js";
|
|
8
|
+
import { ReplyWait } from "./reply-wait.js";
|
|
9
|
+
import { IntercomConnection } from "./connection.js";
|
|
10
|
+
import { IntercomInbound } from "./inbound.js";
|
|
11
|
+
import { IntercomOutbound } from "./outbound.js";
|
|
12
|
+
import { IntercomInspection } from "./inspection.js";
|
|
13
|
+
import { IntercomTopicActions } from "./topic-actions.js";
|
|
14
|
+
import { IntercomSupervisor } from "./supervisor.js";
|
|
15
|
+
import { IntercomTools } from "./tools.js";
|
|
16
|
+
import { IntercomOverlay } from "./ui/overlay.js";
|
|
17
|
+
import { SubagentEventBridges } from "./subagent-events.js";
|
|
18
|
+
import { IntercomSessionEvents } from "./session-events.js";
|
|
19
|
+
import { PeerAwareness } from "./peer-awareness.js";
|
|
20
|
+
import { registerMessageRenderers } from "./ui/message-renderers.js";
|
|
21
|
+
import { registerIntercomTools } from "./tool-registration.js";
|
|
547
22
|
export default function piIntercomExtension(pi) {
|
|
548
|
-
let client = null;
|
|
549
23
|
const config = loadConfig();
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
// round trip. This keeps the system prompt byte-stable across fleet churn
|
|
565
|
-
// (session spawns/exits, subagent panels), which keeps the provider prompt
|
|
566
|
-
// cache valid; the hint text is written so it stays true after peers exit.
|
|
567
|
-
let peerAwarenessHintPinned = false;
|
|
568
|
-
let agentRunning = false;
|
|
569
|
-
let lastIntercomActivity = 0;
|
|
570
|
-
const activeTools = new Map();
|
|
571
|
-
const replyTracker = new ReplyTracker(config.askTimeoutMs);
|
|
572
|
-
const topics = new IntercomTopics(pi, () => getLiveContext());
|
|
573
|
-
let topicSyncError;
|
|
574
|
-
const pendingInbound = new Map();
|
|
575
|
-
const nativePendingIds = new Set();
|
|
576
|
-
const consumedInboundIds = new Set();
|
|
577
|
-
const completedChildren = new Map();
|
|
578
|
-
let reconciledLeafId = null;
|
|
579
|
-
let inboundFlushTimer = null;
|
|
580
|
-
let inboundRestore;
|
|
581
|
-
let inboundBatch;
|
|
582
|
-
let replyWaiter = null;
|
|
583
|
-
function waitForReply(from, replyTo, signal, question) {
|
|
584
|
-
if (replyWaiter) {
|
|
585
|
-
throw new Error("Already waiting for a reply");
|
|
586
|
-
}
|
|
587
|
-
if (signal?.aborted) {
|
|
588
|
-
throw new Error("Cancelled");
|
|
589
|
-
}
|
|
590
|
-
return new Promise((resolve, reject) => {
|
|
591
|
-
const timeout = question ? undefined : setTimeout(() => {
|
|
592
|
-
rejectReplyWaiter(new Error(`No reply from "${from}" within ${Math.max(1, Math.round(config.askTimeoutMs / 60000))} minute(s)`));
|
|
593
|
-
}, config.askTimeoutMs);
|
|
594
|
-
timeout?.unref?.();
|
|
595
|
-
const poll = question ? setInterval(() => {
|
|
596
|
-
try {
|
|
597
|
-
const saved = readQuestionState(question);
|
|
598
|
-
if (saved.state === "cancelled") {
|
|
599
|
-
getLiveContext()?.abort();
|
|
600
|
-
return rejectReplyWaiter(new Error("Cancelled"));
|
|
601
|
-
}
|
|
602
|
-
if (saved.answer)
|
|
603
|
-
replyWaiter?.resolve({ id: replyTo, replyTo, timestamp: saved.answer.answeredAt, content: { text: saved.answer.message } });
|
|
604
|
-
else
|
|
605
|
-
replyWaiter?.retryNotification?.();
|
|
606
|
-
}
|
|
607
|
-
catch (error) {
|
|
608
|
-
rejectReplyWaiter(toError(error));
|
|
609
|
-
}
|
|
610
|
-
}, 250) : undefined;
|
|
611
|
-
const cleanup = () => {
|
|
612
|
-
clearTimeout(timeout);
|
|
613
|
-
clearInterval(poll);
|
|
614
|
-
signal?.removeEventListener("abort", onAbort);
|
|
615
|
-
if (replyWaiter?.replyTo === replyTo) {
|
|
616
|
-
replyWaiter = null;
|
|
617
|
-
}
|
|
618
|
-
};
|
|
619
|
-
const onAbort = () => {
|
|
620
|
-
try {
|
|
621
|
-
if (question)
|
|
622
|
-
cancelSupervisorQuestion(question);
|
|
623
|
-
}
|
|
624
|
-
finally {
|
|
625
|
-
cleanup();
|
|
626
|
-
reject(new Error("Cancelled"));
|
|
627
|
-
}
|
|
628
|
-
};
|
|
629
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
630
|
-
replyWaiter = {
|
|
631
|
-
from,
|
|
632
|
-
replyTo,
|
|
633
|
-
question,
|
|
634
|
-
resolve: (message) => {
|
|
635
|
-
if (question && message.content.attachments?.some((attachment) => attachment.name === RECIPIENT_TURN_FAILED_ATTACHMENT)) {
|
|
636
|
-
pi.appendEntry("intercom_question_notification_error", { questionId: question.questionId, error: message.content.text });
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
try {
|
|
640
|
-
if (question)
|
|
641
|
-
saveQuestionAnswer(question, message.content.text);
|
|
642
|
-
cleanup();
|
|
643
|
-
resolve(message);
|
|
644
|
-
}
|
|
645
|
-
catch (error) {
|
|
646
|
-
cleanup();
|
|
647
|
-
reject(toError(error));
|
|
648
|
-
}
|
|
649
|
-
},
|
|
650
|
-
reject: (error) => {
|
|
651
|
-
cleanup();
|
|
652
|
-
reject(error);
|
|
653
|
-
},
|
|
654
|
-
};
|
|
655
|
-
});
|
|
656
|
-
}
|
|
657
|
-
function currentReplyWaiter() { return replyWaiter; }
|
|
658
|
-
function rejectReplyWaiter(error) {
|
|
659
|
-
replyWaiter?.reject(error);
|
|
660
|
-
}
|
|
661
|
-
function rejectReplyWaiterForPeer(sessionId) {
|
|
662
|
-
if (replyWaiter?.from === sessionId && !replyWaiter.question) {
|
|
663
|
-
rejectReplyWaiter(new Error(`Reply peer disconnected before answering: ${sessionId}`));
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
async function sendAskTransaction(activeClient, to, questionId, options, signal, onSent) {
|
|
667
|
-
const replyPromise = waitForReply(to, questionId, signal);
|
|
668
|
-
replyPromise.catch(() => undefined);
|
|
669
|
-
try {
|
|
670
|
-
if (signal?.aborted)
|
|
671
|
-
throw new Error("Cancelled");
|
|
672
|
-
const result = await activeClient.send(to, { ...options, messageId: questionId, expectsReply: true });
|
|
673
|
-
if (!result.accepted)
|
|
674
|
-
throw new AskDeliveryError(result);
|
|
675
|
-
markIntercomActivity();
|
|
676
|
-
syncPresenceStatus();
|
|
677
|
-
onSent(result);
|
|
678
|
-
return await replyPromise;
|
|
679
|
-
}
|
|
680
|
-
catch (error) {
|
|
681
|
-
if (replyWaiter?.replyTo === questionId)
|
|
682
|
-
rejectReplyWaiter(toError(error));
|
|
683
|
-
try {
|
|
684
|
-
await replyPromise;
|
|
685
|
-
}
|
|
686
|
-
catch {
|
|
687
|
-
// Cleanup only; preserve the transaction failure.
|
|
688
|
-
}
|
|
689
|
-
throw error;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
function clearReconnectTimer() {
|
|
693
|
-
if (!reconnectTimer) {
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
clearTimeout(reconnectTimer);
|
|
697
|
-
reconnectTimer = null;
|
|
698
|
-
}
|
|
699
|
-
function clearStartupConnectTimer() {
|
|
700
|
-
if (!startupConnectTimer) {
|
|
701
|
-
return;
|
|
702
|
-
}
|
|
703
|
-
clearTimeout(startupConnectTimer);
|
|
704
|
-
startupConnectTimer = null;
|
|
705
|
-
}
|
|
706
|
-
function scheduleStartupConnection(ctx, generation) {
|
|
707
|
-
clearStartupConnectTimer();
|
|
708
|
-
startupConnectTimer = setTimeout(() => {
|
|
709
|
-
startupConnectTimer = null;
|
|
710
|
-
if (!getLiveContext(ctx, generation))
|
|
711
|
-
return;
|
|
712
|
-
void ensureConnected("startup").catch(() => {
|
|
713
|
-
if (!getLiveContext(ctx, generation))
|
|
714
|
-
return;
|
|
715
|
-
client = null;
|
|
716
|
-
scheduleReconnect();
|
|
717
|
-
});
|
|
718
|
-
}, 0);
|
|
719
|
-
startupConnectTimer.unref?.();
|
|
720
|
-
}
|
|
721
|
-
function clearInboundFlushTimer() {
|
|
722
|
-
if (!inboundFlushTimer) {
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
clearTimeout(inboundFlushTimer);
|
|
726
|
-
inboundFlushTimer = null;
|
|
727
|
-
}
|
|
728
|
-
function getLiveContext(ctx = runtimeContext, generation = runtimeGeneration) {
|
|
729
|
-
if (disposed || generation !== runtimeGeneration || !ctx) {
|
|
730
|
-
return null;
|
|
731
|
-
}
|
|
732
|
-
try {
|
|
733
|
-
if (currentSessionId && ctx.sessionManager.getSessionId() !== currentSessionId) {
|
|
734
|
-
return null;
|
|
735
|
-
}
|
|
736
|
-
void ctx.hasUI;
|
|
737
|
-
return ctx;
|
|
738
|
-
}
|
|
739
|
-
catch {
|
|
740
|
-
// A context that throws while reading session/UI state is no longer usable.
|
|
741
|
-
return null;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
function notifyIfLive(ctx, message, level, generation = runtimeGeneration) {
|
|
745
|
-
const liveContext = getLiveContext(ctx, generation);
|
|
746
|
-
if (!liveContext?.hasUI) {
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
try {
|
|
750
|
-
liveContext.ui.notify(message, level);
|
|
751
|
-
}
|
|
752
|
-
catch {
|
|
753
|
-
// The UI can disappear during session shutdown/reload while async overlay work is settling.
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
function getReconnectDelayMs() {
|
|
757
|
-
const backoffMs = [1000, 2000, 5000, 10000, 30000];
|
|
758
|
-
return backoffMs[Math.min(reconnectAttempt, backoffMs.length - 1)];
|
|
759
|
-
}
|
|
760
|
-
function currentStatus() {
|
|
761
|
-
const activeToolName = activeTools.values().next().value;
|
|
762
|
-
const lifecycleStatus = activeToolName ? `tool:${activeToolName}` : agentRunning ? "thinking" : "idle";
|
|
763
|
-
return config.status ? `${lifecycleStatus} · ${config.status}` : lifecycleStatus;
|
|
764
|
-
}
|
|
765
|
-
async function buildRegistration() {
|
|
766
|
-
const liveContext = getLiveContext();
|
|
767
|
-
if (!liveContext || !currentSessionId) {
|
|
768
|
-
throw new Error("Intercom runtime not initialized");
|
|
769
|
-
}
|
|
770
|
-
const identity = buildPresenceIdentity(pi, currentSessionId);
|
|
771
|
-
const cwd = liveContext.cwd ?? process.cwd();
|
|
772
|
-
return {
|
|
773
|
-
name: identity.name,
|
|
774
|
-
cwd,
|
|
775
|
-
model: currentModel,
|
|
776
|
-
projectId: await resolveSessionProjectId(cwd),
|
|
777
|
-
lastSeen: Date.now(),
|
|
778
|
-
status: currentStatus(),
|
|
779
|
-
...buildPresenceHealth(),
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
function isRecipientIdle(ctx) {
|
|
783
|
-
// ponytail: Pi 1.0 exposes no busy signal during foreign prompt preparation;
|
|
784
|
-
// custom delivery may start a separate legitimate run. Use a public admission
|
|
785
|
-
// signal if the host adds one, rather than reserving rejected input here.
|
|
786
|
-
if (agentRunning || activeTools.size > 0)
|
|
787
|
-
return false;
|
|
788
|
-
try {
|
|
789
|
-
return ctx.isIdle();
|
|
790
|
-
}
|
|
791
|
-
catch {
|
|
792
|
-
return false;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
function canAcceptAsks() {
|
|
796
|
-
const liveContext = getLiveContext();
|
|
797
|
-
return liveContext ? isRecipientIdle(liveContext) : false;
|
|
798
|
-
}
|
|
799
|
-
/** Publish whether the recipient can accept a blocking ask. */
|
|
800
|
-
function buildPresenceHealth() {
|
|
801
|
-
return {
|
|
802
|
-
pendingAsks: replyTracker.listPending().length,
|
|
803
|
-
acceptsAsks: canAcceptAsks(),
|
|
804
|
-
lastIntercomActivity,
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
function markIntercomActivity() {
|
|
808
|
-
lastIntercomActivity = Date.now();
|
|
809
|
-
}
|
|
810
|
-
function syncPresenceIdentity(sessionId) {
|
|
811
|
-
if (!client || !getLiveContext()) {
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
client.updatePresence({ ...buildPresenceIdentity(pi, sessionId), status: currentStatus(), ...buildPresenceHealth() });
|
|
815
|
-
}
|
|
816
|
-
function syncPresenceStatus() {
|
|
817
|
-
if (!client || !currentSessionId || !getLiveContext()) {
|
|
818
|
-
return;
|
|
819
|
-
}
|
|
820
|
-
client.updatePresence({ status: currentStatus(), ...buildPresenceHealth() });
|
|
821
|
-
}
|
|
822
|
-
function requestedDelivery(message) {
|
|
823
|
-
if (message.passive === true || message.delivery === "passive")
|
|
824
|
-
return "passive";
|
|
825
|
-
return message.delivery ?? (message.expectsReply === true ? "auto" : "steer");
|
|
826
|
-
}
|
|
827
|
-
function shouldTriggerTurn(message) {
|
|
828
|
-
return requestedDelivery(message) !== "passive";
|
|
829
|
-
}
|
|
830
|
-
function queuedTriggerIndex(entries) {
|
|
831
|
-
const canTrigger = (entry) => entry.flushDelivery !== "passive" && shouldTriggerTurn(entry.message);
|
|
832
|
-
const askIndex = entries.findIndex((entry) => canTrigger(entry) && entry.message.expectsReply === true);
|
|
833
|
-
return askIndex === -1 ? entries.findIndex(canTrigger) : askIndex;
|
|
834
|
-
}
|
|
835
|
-
function runInboundBatch(deliver, generation = runtimeGeneration) {
|
|
836
|
-
if (inboundBatch)
|
|
837
|
-
return inboundBatch;
|
|
838
|
-
// Native appends emit subscriber callbacks synchronously; publish ownership first.
|
|
839
|
-
const batch = Promise.resolve().then(deliver);
|
|
840
|
-
inboundBatch = batch;
|
|
841
|
-
const finish = () => {
|
|
842
|
-
if (inboundBatch !== batch)
|
|
843
|
-
return;
|
|
844
|
-
inboundBatch = undefined;
|
|
845
|
-
if (getLiveContext(runtimeContext, generation) && queuedInbound().length)
|
|
846
|
-
scheduleInboundFlush(0);
|
|
847
|
-
};
|
|
848
|
-
void batch.then(finish, finish);
|
|
849
|
-
return batch;
|
|
850
|
-
}
|
|
851
|
-
function* sendTriggerLast(entries, generation = runtimeGeneration) {
|
|
852
|
-
const passives = [];
|
|
853
|
-
const rest = [];
|
|
854
|
-
for (const entry of entries) {
|
|
855
|
-
if (entry.flushDelivery === "passive")
|
|
856
|
-
passives.push(entry);
|
|
857
|
-
else
|
|
858
|
-
rest.push(entry);
|
|
859
|
-
}
|
|
860
|
-
for (const entry of passives) {
|
|
861
|
-
if (!getLiveContext(runtimeContext, generation))
|
|
862
|
-
return;
|
|
863
|
-
sendIncomingMessage(entry, "passive", generation);
|
|
864
|
-
yield;
|
|
865
|
-
}
|
|
866
|
-
if (!getLiveContext(runtimeContext, generation))
|
|
867
|
-
return;
|
|
868
|
-
const triggerIndex = queuedTriggerIndex(rest);
|
|
869
|
-
const trigger = triggerIndex === -1 ? undefined : rest.splice(triggerIndex, 1)[0];
|
|
870
|
-
if (trigger && pendingInbound.get(trigger.message.id) === trigger)
|
|
871
|
-
replyTracker.queueTurnContext({ ...trigger, receivedAt: Date.now() });
|
|
872
|
-
for (const entry of rest) {
|
|
873
|
-
if (!getLiveContext(runtimeContext, generation))
|
|
874
|
-
return;
|
|
875
|
-
sendIncomingMessage(entry, entry.flushDelivery === "steer" ? "steer" : "followUp", generation);
|
|
876
|
-
yield;
|
|
877
|
-
}
|
|
878
|
-
if (trigger)
|
|
879
|
-
sendIncomingMessage(trigger, "trigger", generation);
|
|
880
|
-
}
|
|
881
|
-
function currentSessionTargetMatches(to, resolvedTo, activeClient) {
|
|
882
|
-
const targets = new Set();
|
|
883
|
-
const addTarget = (target) => {
|
|
884
|
-
const trimmed = target?.trim();
|
|
885
|
-
if (trimmed)
|
|
886
|
-
targets.add(trimmed.toLowerCase());
|
|
887
|
-
};
|
|
888
|
-
addTarget(currentSessionId);
|
|
889
|
-
addTarget(activeClient?.sessionId);
|
|
890
|
-
addTarget(pi.getSessionName());
|
|
891
|
-
if (currentSessionId)
|
|
892
|
-
addTarget(buildPresenceIdentity(pi, currentSessionId).name);
|
|
893
|
-
return Boolean(resolvedTo && activeClient?.sessionId && resolvedTo === activeClient.sessionId)
|
|
894
|
-
|| targets.has(to.trim().toLowerCase());
|
|
895
|
-
}
|
|
896
|
-
function checkpointInbound(update) {
|
|
897
|
-
if (currentSessionId)
|
|
898
|
-
pi.appendEntry(INBOUND_CHECKPOINT_TYPE, { sessionId: currentSessionId, ...update });
|
|
899
|
-
}
|
|
900
|
-
function retireReply(messageId, stage = "reply-retired") {
|
|
901
|
-
replyTracker.markReplied(messageId);
|
|
902
|
-
checkpointInbound({ messageId, stage });
|
|
903
|
-
}
|
|
904
|
-
function keepInbound(entry) {
|
|
905
|
-
if (entry.stage === "queued" && entry.message.queueMode === "replace" && entry.message.threadId) {
|
|
906
|
-
for (const pending of pendingInbound.values()) {
|
|
907
|
-
if (pending.stage === "queued" && pending.from.id === entry.from.id && pending.message.threadId === entry.message.threadId) {
|
|
908
|
-
pendingInbound.delete(pending.message.id);
|
|
909
|
-
replyTracker.markReplied(pending.message.id);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
pendingInbound.set(entry.message.id, entry);
|
|
914
|
-
}
|
|
915
|
-
function setInboundStage(entry, stage) {
|
|
916
|
-
if (entry.stage === stage)
|
|
917
|
-
return;
|
|
918
|
-
pendingInbound.set(entry.message.id, { ...entry, stage });
|
|
919
|
-
checkpointInbound({ messageId: entry.message.id, stage });
|
|
920
|
-
}
|
|
921
|
-
function rememberInbound(entry, stage, flushDelivery) {
|
|
922
|
-
const pending = pendingInbound.get(entry.message.id);
|
|
923
|
-
if (pending) {
|
|
924
|
-
setInboundStage(pending, stage);
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
const saved = { ...entry, stage, flushDelivery, receivedAt: Date.now() };
|
|
928
|
-
keepInbound(saved);
|
|
929
|
-
checkpointInbound({ entry: saved });
|
|
930
|
-
}
|
|
931
|
-
function queuedInbound() {
|
|
932
|
-
return [...pendingInbound.values()].filter((entry) => entry.stage === "queued");
|
|
933
|
-
}
|
|
934
|
-
function rememberCompletedChildren(entry) {
|
|
935
|
-
const completion = entry.subagentCompletion;
|
|
936
|
-
if (entry.from.id !== "subagent-result" || completion?.ownerSessionId !== currentSessionId)
|
|
937
|
-
return;
|
|
938
|
-
for (const child of completion.children) {
|
|
939
|
-
const thread = `subagent-progress:${completion.runId}:${child.agent}:${child.index}`;
|
|
940
|
-
if (child.status === "detached")
|
|
941
|
-
completedChildren.delete(thread);
|
|
942
|
-
else
|
|
943
|
-
completedChildren.set(thread, child);
|
|
944
|
-
}
|
|
945
|
-
for (const pending of queuedInbound())
|
|
946
|
-
discardObsoleteProgress(pending);
|
|
947
|
-
}
|
|
948
|
-
function discardObsoleteProgress(entry) {
|
|
949
|
-
const message = entry.message;
|
|
950
|
-
if (message.delivery !== "queue" || message.queueMode !== "replace" || !message.threadId || message.expectsReply || message.replyTo)
|
|
951
|
-
return false;
|
|
952
|
-
const child = completedChildren.get(message.threadId);
|
|
953
|
-
if (!child || (entry.from.id !== child.intercomTarget && entry.from.name !== child.intercomTarget))
|
|
954
|
-
return false;
|
|
955
|
-
// Keep the original checkpoint as history; obsolete progress is not another model turn.
|
|
956
|
-
if (!pendingInbound.has(message.id))
|
|
957
|
-
rememberInbound(entry, "queued", "auto");
|
|
958
|
-
pendingInbound.delete(message.id);
|
|
959
|
-
replyTracker.markReplied(message.id);
|
|
960
|
-
checkpointInbound({ messageId: message.id, stage: "discarded" });
|
|
961
|
-
return true;
|
|
962
|
-
}
|
|
963
|
-
function needsIntercom(entry) {
|
|
964
|
-
return entry.message.human === undefined && shouldTriggerTurn(entry.message)
|
|
965
|
-
&& entry.from.id !== "subagent-result" && entry.from.id !== "subagent-control";
|
|
966
|
-
}
|
|
967
|
-
function reconcileIntercom(ctx) {
|
|
968
|
-
if ([...pendingInbound.values()].some(needsIntercom) || replyTracker.listPending().some(needsIntercom))
|
|
969
|
-
activateTools(pi, ["intercom"]);
|
|
970
|
-
}
|
|
971
|
-
function* restoreInboundEntries(ctx, generation) {
|
|
972
|
-
consumedInboundIds.clear();
|
|
973
|
-
completedChildren.clear();
|
|
974
|
-
const leafId = ctx.sessionManager.getLeafId();
|
|
975
|
-
for (const metadata of ctx.sessionManager.getEntries()) {
|
|
976
|
-
yield;
|
|
977
|
-
if (!getLiveContext(ctx, generation))
|
|
978
|
-
return;
|
|
979
|
-
if (!(metadata.type === "custom_message" && ["intercom_message", "subagent-human-message"].includes(metadata.customType)
|
|
980
|
-
|| metadata.type === "custom" && [INBOUND_CHECKPOINT_TYPE, "intercom_sent"].includes(metadata.customType)))
|
|
981
|
-
continue;
|
|
982
|
-
const item = ctx.sessionManager.getEntry(metadata.id) ?? metadata;
|
|
983
|
-
if (item.type === "custom_message") {
|
|
984
|
-
const receipt = item.details;
|
|
985
|
-
if (receipt?.subagentCompletion)
|
|
986
|
-
rememberCompletedChildren(receipt);
|
|
987
|
-
const id = receipt && typeof receipt === "object" ? receipt.message?.id : undefined;
|
|
988
|
-
if (typeof id === "string") {
|
|
989
|
-
consumedInboundIds.add(id);
|
|
990
|
-
pendingInbound.delete(id);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
else if (item.type === "custom" && item.customType === INBOUND_CHECKPOINT_TYPE) {
|
|
994
|
-
const data = item.data;
|
|
995
|
-
// Forks inherit entries, but pending delivery belongs to the saved session UUID.
|
|
996
|
-
if (data?.sessionId !== currentSessionId)
|
|
997
|
-
continue;
|
|
998
|
-
if ("entry" in data) {
|
|
999
|
-
rememberCompletedChildren(data.entry);
|
|
1000
|
-
replyTracker.recordIncomingMessage(data.entry.from, data.entry.message, data.entry.receivedAt);
|
|
1001
|
-
if (!consumedInboundIds.has(data.entry.message.id))
|
|
1002
|
-
keepInbound({ ...data.entry });
|
|
1003
|
-
}
|
|
1004
|
-
else if (data.stage === "reply-retired") {
|
|
1005
|
-
replyTracker.markReplied(data.messageId);
|
|
1006
|
-
}
|
|
1007
|
-
else if (data.stage === "discarded") {
|
|
1008
|
-
pendingInbound.delete(data.messageId);
|
|
1009
|
-
replyTracker.markReplied(data.messageId);
|
|
1010
|
-
}
|
|
1011
|
-
else {
|
|
1012
|
-
const pending = pendingInbound.get(data.messageId);
|
|
1013
|
-
if (pending)
|
|
1014
|
-
pendingInbound.set(data.messageId, { ...pending, stage: data.stage });
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
else if (item.type === "custom" && item.customType === "intercom_sent") {
|
|
1018
|
-
const sent = item.data;
|
|
1019
|
-
if (sent?.message?.replyTo)
|
|
1020
|
-
replyTracker.markReplied(sent.message.replyTo);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
if (!getLiveContext(ctx, generation))
|
|
1024
|
-
return;
|
|
1025
|
-
reconciledLeafId = leafId;
|
|
1026
|
-
for (const entry of pendingInbound.values()) {
|
|
1027
|
-
discardObsoleteProgress(entry);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
async function restoreInbound(ctx, generation) {
|
|
1031
|
-
await runCooperatively(restoreInboundEntries(ctx, generation));
|
|
1032
|
-
if (!getLiveContext(ctx, generation))
|
|
1033
|
-
return;
|
|
1034
|
-
if (replyTracker.hasReplyContext) {
|
|
1035
|
-
const questions = await listSupervisorQuestionsAsync(ctx.sessionManager.getSessionId());
|
|
1036
|
-
if (!getLiveContext(ctx, generation))
|
|
1037
|
-
return;
|
|
1038
|
-
for (const question of questions) {
|
|
1039
|
-
if (question.answer || question.state === "cancelled")
|
|
1040
|
-
replyTracker.markReplied(question.questionId);
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
reconcileConsumedInbound(ctx);
|
|
1044
|
-
reconcileIntercom(ctx);
|
|
1045
|
-
}
|
|
1046
|
-
function sendIncomingMessage(entry, delivery, generation = runtimeGeneration) {
|
|
1047
|
-
if (runtimeStarted && !getLiveContext(runtimeContext, generation)) {
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
if ("stage" in entry && pendingInbound.get(entry.message.id) !== entry)
|
|
1051
|
-
return;
|
|
1052
|
-
if (discardObsoleteProgress(entry))
|
|
1053
|
-
return;
|
|
1054
|
-
rememberInbound(entry, "native", delivery === "passive" ? "passive" : "auto");
|
|
1055
|
-
if (delivery === "steer" || delivery === "followUp")
|
|
1056
|
-
nativePendingIds.add(entry.message.id);
|
|
1057
|
-
if (delivery !== "passive") {
|
|
1058
|
-
replyTracker.queueTurnContext({ from: entry.from, message: entry.message, receivedAt: Date.now() });
|
|
1059
|
-
}
|
|
1060
|
-
if (delivery !== "passive" && needsIntercom(entry))
|
|
1061
|
-
activateTools(pi, ["intercom"]);
|
|
1062
|
-
const senderDisplay = entry.from.name || entry.from.id.slice(0, 8);
|
|
1063
|
-
const bodyText = entry.bodyText;
|
|
1064
|
-
const replyInstruction = entry.replyCommand ? `\n\nTo reply, use the intercom tool: ${entry.replyCommand}` : "";
|
|
1065
|
-
const human = entry.message.human !== undefined;
|
|
1066
|
-
const options = delivery === "trigger"
|
|
1067
|
-
? { triggerTurn: true }
|
|
1068
|
-
: delivery === "followUp" || delivery === "steer"
|
|
1069
|
-
? { deliverAs: delivery }
|
|
1070
|
-
: { triggerTurn: false };
|
|
1071
|
-
pi.sendMessage({
|
|
1072
|
-
customType: human ? "subagent-human-message" : "intercom_message",
|
|
1073
|
-
content: human
|
|
1074
|
-
? `Direct user message to this agent (human origin, not peer advice). Respond in this conversation; the user sees it directly. Do not ask the parent to relay or approve it.\n\n${bodyText}`
|
|
1075
|
-
: `**📨 From ${senderDisplay}** (Native session cwd: ${entry.from.cwd})${replyInstruction}\n\n${bodyText}`,
|
|
1076
|
-
display: true,
|
|
1077
|
-
details: { ...entry, bodyText },
|
|
1078
|
-
}, options);
|
|
1079
|
-
if (delivery === "passive" && runtimeContext)
|
|
1080
|
-
reconcileConsumedInbound(runtimeContext);
|
|
1081
|
-
}
|
|
1082
|
-
function isBlockingSubagentSupervisorMessage(entry) {
|
|
1083
|
-
if (!entry.message.expectsReply)
|
|
1084
|
-
return false;
|
|
1085
|
-
const text = entry.bodyText.trimStart();
|
|
1086
|
-
return text.startsWith("Subagent needs a supervisor decision.")
|
|
1087
|
-
|| text.startsWith("Subagent requests a structured supervisor interview.");
|
|
1088
|
-
}
|
|
1089
|
-
async function requestSubagentDetachForBlockingSupervisorMessage(entry, attention = false) {
|
|
1090
|
-
if (!attention && !isBlockingSubagentSupervisorMessage(entry))
|
|
1091
|
-
return false;
|
|
1092
|
-
const requestId = randomUUID();
|
|
1093
|
-
return await new Promise((resolve) => {
|
|
1094
|
-
let settled = false;
|
|
1095
|
-
let unsubscribe;
|
|
1096
|
-
const finish = (accepted) => {
|
|
1097
|
-
if (settled)
|
|
1098
|
-
return;
|
|
1099
|
-
settled = true;
|
|
1100
|
-
clearTimeout(timer);
|
|
1101
|
-
unsubscribe?.();
|
|
1102
|
-
resolve(accepted);
|
|
1103
|
-
};
|
|
1104
|
-
const timer = setTimeout(() => finish(false), INTERCOM_DETACH_RESPONSE_TIMEOUT_MS);
|
|
1105
|
-
timer.unref?.();
|
|
1106
|
-
unsubscribe = pi.events.on(INTERCOM_DETACH_RESPONSE_EVENT, (payload) => {
|
|
1107
|
-
if (!payload || typeof payload !== "object")
|
|
1108
|
-
return;
|
|
1109
|
-
const response = payload;
|
|
1110
|
-
if (response.requestId !== requestId)
|
|
1111
|
-
return;
|
|
1112
|
-
finish(response.accepted === true);
|
|
1113
|
-
});
|
|
1114
|
-
try {
|
|
1115
|
-
pi.events.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId, ...(attention ? { reason: "attention" } : {}) });
|
|
1116
|
-
// Owned waits answer ordinary attention synchronously, so an unanswered probe can finish now.
|
|
1117
|
-
if (attention)
|
|
1118
|
-
finish(false);
|
|
1119
|
-
}
|
|
1120
|
-
catch {
|
|
1121
|
-
finish(false);
|
|
1122
|
-
}
|
|
1123
|
-
});
|
|
1124
|
-
}
|
|
1125
|
-
function scheduleInboundFlush(delayMs = INBOUND_FLUSH_DELAY_MS) {
|
|
1126
|
-
if (!getLiveContext()) {
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
const scheduledGeneration = runtimeGeneration;
|
|
1130
|
-
clearInboundFlushTimer();
|
|
1131
|
-
inboundFlushTimer = setTimeout(() => {
|
|
1132
|
-
inboundFlushTimer = null;
|
|
1133
|
-
void flushIdleMessages(scheduledGeneration)?.catch((error) => console.error("Intercom inbound delivery failed:", error));
|
|
1134
|
-
}, delayMs);
|
|
1135
|
-
inboundFlushTimer.unref?.();
|
|
1136
|
-
}
|
|
1137
|
-
function flushIdleMessages(generation = runtimeGeneration) {
|
|
1138
|
-
if (inboundBatch)
|
|
1139
|
-
return inboundBatch;
|
|
1140
|
-
const entries = queuedInbound();
|
|
1141
|
-
if (entries.length === 0)
|
|
1142
|
-
return;
|
|
1143
|
-
const ctx = getLiveContext(runtimeContext, generation);
|
|
1144
|
-
if (!ctx)
|
|
1145
|
-
return;
|
|
1146
|
-
if (!isRecipientIdle(ctx)) {
|
|
1147
|
-
const steers = entries.filter((entry) => entry.flushDelivery === "steer"
|
|
1148
|
-
&& (requestedDelivery(entry.message) === "steer" || !ctx.hasUI && activeTools.size === 0));
|
|
1149
|
-
if (!steers.length) {
|
|
1150
|
-
scheduleInboundFlush(INBOUND_IDLE_RETRY_MS);
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
return runInboundBatch(async () => {
|
|
1154
|
-
const detachments = [];
|
|
1155
|
-
await runCooperatively((function* () {
|
|
1156
|
-
for (const entry of steers) {
|
|
1157
|
-
if (!getLiveContext(runtimeContext, generation))
|
|
1158
|
-
return;
|
|
1159
|
-
if (pendingInbound.get(entry.message.id) !== entry)
|
|
1160
|
-
continue;
|
|
1161
|
-
sendIncomingMessage(entry, "steer", generation);
|
|
1162
|
-
if (requestedDelivery(entry.message) === "steer" && entry.from.id !== "subagent-result" && entry.from.id !== "subagent-control") {
|
|
1163
|
-
detachments.push(requestSubagentDetachForBlockingSupervisorMessage(entry, !isBlockingSubagentSupervisorMessage(entry)));
|
|
1164
|
-
}
|
|
1165
|
-
yield;
|
|
1166
|
-
}
|
|
1167
|
-
})());
|
|
1168
|
-
await Promise.all(detachments);
|
|
1169
|
-
}, generation);
|
|
1170
|
-
}
|
|
1171
|
-
return runInboundBatch(() => runCooperatively(sendTriggerLast(entries, generation)), generation);
|
|
1172
|
-
}
|
|
1173
|
-
function queueIdleMessage(entry, flushDelivery = "auto", delayMs = INBOUND_FLUSH_DELAY_MS) {
|
|
1174
|
-
rememberInbound(entry, "queued", flushDelivery);
|
|
1175
|
-
syncPresenceStatus();
|
|
1176
|
-
scheduleInboundFlush(delayMs);
|
|
1177
|
-
}
|
|
1178
|
-
function reconcileConsumedInbound(ctx, fullScan = false) {
|
|
1179
|
-
const leafId = ctx.sessionManager.getLeafId();
|
|
1180
|
-
if (pendingInbound.size > 0) {
|
|
1181
|
-
const recent = [];
|
|
1182
|
-
let id = leafId;
|
|
1183
|
-
// Passive appends emit SDK events, not extension message_end. Walk only
|
|
1184
|
-
// new entries; a moved/missing branch boundary still needs full history.
|
|
1185
|
-
while (!fullScan && id !== reconciledLeafId) {
|
|
1186
|
-
const entry = id ? ctx.sessionManager.getEntry(id) : undefined;
|
|
1187
|
-
if (!entry) {
|
|
1188
|
-
fullScan = true;
|
|
1189
|
-
break;
|
|
1190
|
-
}
|
|
1191
|
-
recent.push(entry);
|
|
1192
|
-
id = entry.parentId;
|
|
1193
|
-
}
|
|
1194
|
-
for (const metadata of fullScan ? ctx.sessionManager.getEntries() : recent) {
|
|
1195
|
-
if (metadata.type !== "custom_message" || !["intercom_message", "subagent-human-message"].includes(metadata.customType))
|
|
1196
|
-
continue;
|
|
1197
|
-
const entry = ctx.sessionManager.getEntry(metadata.id) ?? metadata;
|
|
1198
|
-
const inboundId = inboundIdFromCustomMessage(entry);
|
|
1199
|
-
if (inboundId)
|
|
1200
|
-
consumedInboundIds.add(inboundId);
|
|
1201
|
-
}
|
|
1202
|
-
for (const pendingId of pendingInbound.keys()) {
|
|
1203
|
-
if (consumedInboundIds.has(pendingId)) {
|
|
1204
|
-
pendingInbound.delete(pendingId);
|
|
1205
|
-
nativePendingIds.delete(pendingId);
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
reconciledLeafId = leafId;
|
|
1210
|
-
}
|
|
1211
|
-
async function redeliverUnconsumedInbound(ctx, generation) {
|
|
1212
|
-
if (inboundBatch)
|
|
1213
|
-
await inboundBatch;
|
|
1214
|
-
if (!getLiveContext(ctx, generation))
|
|
1215
|
-
return;
|
|
1216
|
-
reconcileConsumedInbound(ctx);
|
|
1217
|
-
// Public coarse occupancy excludes custom queues on Pi 1.0; the existing
|
|
1218
|
-
// turn boundary's occupancy distinguishes retained from fully cleared work.
|
|
1219
|
-
if (pendingInbound.size === 0 || ctx.hasPendingMessages())
|
|
1220
|
-
return;
|
|
1221
|
-
const entries = [...pendingInbound.values()].filter((entry) => entry.stage === "native" && !nativePendingIds.has(entry.message.id));
|
|
1222
|
-
await runInboundBatch(() => runCooperatively(sendTriggerLast(entries, generation)), generation);
|
|
1223
|
-
}
|
|
1224
|
-
function isOwnedHumanMessage(from, message) {
|
|
1225
|
-
const origin = message.human;
|
|
1226
|
-
if (!origin || !childOrchestratorMetadata || origin.runId !== childOrchestratorMetadata.runId || origin.index !== Number(childOrchestratorMetadata.index))
|
|
1227
|
-
return false;
|
|
1228
|
-
const owner = readRunJson(path.join(getRunMetadataDir(origin.runId), "question-owner.json"));
|
|
1229
|
-
return owner?.sessionId === origin.ownerSessionId && from.id === `pi-${createHash("sha256").update(origin.ownerSessionId).digest("hex").slice(0, 32)}`;
|
|
1230
|
-
}
|
|
1231
|
-
async function handleIncomingMessage(ctx, from, message) {
|
|
1232
|
-
const messageGeneration = runtimeGeneration;
|
|
1233
|
-
if (inboundRestore)
|
|
1234
|
-
await inboundRestore;
|
|
1235
|
-
const liveContext = getLiveContext(ctx, messageGeneration);
|
|
1236
|
-
if (!liveContext) {
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
if (topics.receive(from, message))
|
|
1240
|
-
return;
|
|
1241
|
-
if (message.topic)
|
|
1242
|
-
message = { ...message, content: { ...message.content, text: `Topic ${message.topic.topic} · ${message.topic.event}\n${message.content.text}` } };
|
|
1243
|
-
if (replyWaiter) {
|
|
1244
|
-
const senderTarget = from.name || from.id;
|
|
1245
|
-
const fromMatches = senderTarget.toLowerCase() === replyWaiter.from.toLowerCase()
|
|
1246
|
-
|| from.id === replyWaiter.from;
|
|
1247
|
-
const replyMatches = message.replyTo === replyWaiter.replyTo;
|
|
1248
|
-
if (fromMatches && replyMatches) {
|
|
1249
|
-
markIntercomActivity();
|
|
1250
|
-
syncPresenceStatus();
|
|
1251
|
-
replyWaiter.resolve(message);
|
|
1252
|
-
return;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
if (message.human && !isOwnedHumanMessage(from, message))
|
|
1256
|
-
message = { ...message, human: undefined };
|
|
1257
|
-
const attachmentText = message.content.attachments?.length
|
|
1258
|
-
? formatAttachments(message.content.attachments)
|
|
1259
|
-
: "";
|
|
1260
|
-
const bodyText = `${message.content.text}${attachmentText}`;
|
|
1261
|
-
const replyCommand = config.replyHint && message.expectsReply
|
|
1262
|
-
? `intercom({ action: "reply", message: "..." })`
|
|
1263
|
-
: undefined;
|
|
1264
|
-
replyTracker.recordIncomingMessage(from, message);
|
|
1265
|
-
markIntercomActivity();
|
|
1266
|
-
syncPresenceStatus();
|
|
1267
|
-
const entry = { from, message, replyCommand, bodyText };
|
|
1268
|
-
if (discardObsoleteProgress(entry))
|
|
1269
|
-
return;
|
|
1270
|
-
const activeContext = getLiveContext(liveContext, messageGeneration);
|
|
1271
|
-
if (!activeContext) {
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
const delivery = requestedDelivery(message);
|
|
1275
|
-
if (inboundBatch) {
|
|
1276
|
-
queueIdleMessage(entry, delivery === "passive" ? "passive" : delivery === "steer" ? "steer" : "auto");
|
|
1277
|
-
return;
|
|
1278
|
-
}
|
|
1279
|
-
if (delivery === "queue" && message.queueMode === "replace") {
|
|
1280
|
-
if (!isRecipientIdle(activeContext) && !activeContext.hasUI) {
|
|
1281
|
-
queueIdleMessage(entry, "steer", NON_UI_REPLACE_FLUSH_DELAY_MS);
|
|
1282
|
-
return;
|
|
1283
|
-
}
|
|
1284
|
-
queueIdleMessage(entry, "auto");
|
|
1285
|
-
return;
|
|
1286
|
-
}
|
|
1287
|
-
if (!isRecipientIdle(activeContext)) {
|
|
1288
|
-
if (isBlockingSubagentSupervisorMessage(entry)) {
|
|
1289
|
-
const willDeliver = activeContext.hasUI || delivery !== "auto";
|
|
1290
|
-
if (willDeliver)
|
|
1291
|
-
rememberInbound(entry, "queued", "auto");
|
|
1292
|
-
await requestSubagentDetachForBlockingSupervisorMessage(entry);
|
|
1293
|
-
if (!getLiveContext(liveContext, messageGeneration) || (willDeliver && pendingInbound.get(message.id)?.stage !== "queued")) {
|
|
1294
|
-
return;
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
if (delivery === "steer") {
|
|
1298
|
-
// Queue first, then release a delegated foreground wait so native Pi can consume the steer.
|
|
1299
|
-
sendIncomingMessage(entry, "steer", messageGeneration);
|
|
1300
|
-
if (!isBlockingSubagentSupervisorMessage(entry))
|
|
1301
|
-
await requestSubagentDetachForBlockingSupervisorMessage(entry, true);
|
|
1302
|
-
return;
|
|
1303
|
-
}
|
|
1304
|
-
if (delivery === "queue" && message.queueMode !== "replace") {
|
|
1305
|
-
sendIncomingMessage(entry, "followUp", messageGeneration);
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
if (delivery === "passive") {
|
|
1309
|
-
queueIdleMessage(entry, "passive");
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
if (!activeContext.hasUI) {
|
|
1313
|
-
if (!message.expectsReply) {
|
|
1314
|
-
sendIncomingMessage(entry, "steer", messageGeneration);
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
const activeClient = client;
|
|
1318
|
-
if (!message.replyTo && activeClient?.isConnected()) {
|
|
1319
|
-
try {
|
|
1320
|
-
const result = await activeClient.send(from.id, {
|
|
1321
|
-
text: "This agent is running in non-interactive mode and cannot respond to intercom messages while it is working. It will continue its current task and exit when done.",
|
|
1322
|
-
replyTo: message.id,
|
|
1323
|
-
});
|
|
1324
|
-
if (result.delivered && getLiveContext(liveContext, messageGeneration)) {
|
|
1325
|
-
replyTracker.markReplied(message.id);
|
|
1326
|
-
markIntercomActivity();
|
|
1327
|
-
syncPresenceStatus();
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
catch {
|
|
1331
|
-
// Best-effort reply; keep the busy non-interactive session running either way.
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
return;
|
|
1335
|
-
}
|
|
1336
|
-
queueIdleMessage(entry, "auto");
|
|
1337
|
-
return;
|
|
1338
|
-
}
|
|
1339
|
-
if (getLiveContext(liveContext, messageGeneration)) {
|
|
1340
|
-
sendIncomingMessage(entry, delivery === "passive" ? "passive" : "trigger", messageGeneration);
|
|
1341
|
-
}
|
|
1342
|
-
}
|
|
1343
|
-
function attachClientHandlers(nextClient) {
|
|
1344
|
-
nextClient.on("message", (from, message) => {
|
|
1345
|
-
const liveContext = getLiveContext();
|
|
1346
|
-
if (client !== nextClient || !liveContext) {
|
|
1347
|
-
return;
|
|
1348
|
-
}
|
|
1349
|
-
void handleIncomingMessage(liveContext, from, message).catch((error) => {
|
|
1350
|
-
console.error("Intercom inbound delivery failed:", error);
|
|
1351
|
-
});
|
|
1352
|
-
});
|
|
1353
|
-
nextClient.on("session_left", (sessionId) => {
|
|
1354
|
-
if (client !== nextClient) {
|
|
1355
|
-
return;
|
|
1356
|
-
}
|
|
1357
|
-
topics.disconnected(sessionId);
|
|
1358
|
-
rejectReplyWaiterForPeer(sessionId);
|
|
1359
|
-
for (const pending of replyTracker.listPending()) {
|
|
1360
|
-
if (pending.from.id === sessionId && !isDurableSupervisorQuestion(pending.message)
|
|
1361
|
-
&& pendingInbound.get(pending.message.id)?.stage !== "queued")
|
|
1362
|
-
retireReply(pending.message.id);
|
|
1363
|
-
}
|
|
1364
|
-
replyTracker.expireSender(sessionId);
|
|
1365
|
-
for (const pending of queuedInbound()) {
|
|
1366
|
-
if (pending.from.id === sessionId && pending.message.expectsReply && !isDurableSupervisorQuestion(pending.message)) {
|
|
1367
|
-
pendingInbound.delete(pending.message.id);
|
|
1368
|
-
checkpointInbound({ messageId: pending.message.id, stage: "discarded" });
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
});
|
|
1372
|
-
nextClient.on("disconnected", (error) => {
|
|
1373
|
-
if (client !== nextClient) {
|
|
1374
|
-
return;
|
|
1375
|
-
}
|
|
1376
|
-
if (!replyWaiter?.question)
|
|
1377
|
-
rejectReplyWaiter(new Error(`Disconnected while waiting for reply: ${error.message}`, { cause: error }));
|
|
1378
|
-
client = null;
|
|
1379
|
-
topics.disconnected();
|
|
1380
|
-
if (!disposed) {
|
|
1381
|
-
clearReconnectTimer();
|
|
1382
|
-
scheduleReconnect();
|
|
1383
|
-
}
|
|
1384
|
-
});
|
|
1385
|
-
nextClient.on("error", () => {
|
|
1386
|
-
// Keep broker/socket noise out of the TUI. Reconnect logic runs from the disconnect path.
|
|
1387
|
-
});
|
|
1388
|
-
}
|
|
1389
|
-
function scheduleReconnect() {
|
|
1390
|
-
if (disposed || reconnectTimer || reconnectPromise || !getLiveContext()) {
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
const scheduledGeneration = runtimeGeneration;
|
|
1394
|
-
reconnectTimer = setTimeout(() => {
|
|
1395
|
-
reconnectTimer = null;
|
|
1396
|
-
if (scheduledGeneration !== runtimeGeneration || !getLiveContext()) {
|
|
1397
|
-
return;
|
|
1398
|
-
}
|
|
1399
|
-
reconnectAttempt += 1;
|
|
1400
|
-
void ensureConnected("background").catch(() => {
|
|
1401
|
-
// ensureConnected("background") already queued the next retry.
|
|
1402
|
-
});
|
|
1403
|
-
}, getReconnectDelayMs());
|
|
1404
|
-
reconnectTimer.unref?.();
|
|
1405
|
-
}
|
|
1406
|
-
async function restoreTopics(activeClient) {
|
|
1407
|
-
const changes = [
|
|
1408
|
-
...[...topics.published.values()].map((topic) => ({ action: "restore", topic })),
|
|
1409
|
-
...[...topics.subscriptions.values()].map((subscription) => ({ action: "restore", subscription })),
|
|
1410
|
-
];
|
|
1411
|
-
let failure;
|
|
1412
|
-
for (const change of changes) {
|
|
1413
|
-
try {
|
|
1414
|
-
await activeClient.updateTopics(change);
|
|
1415
|
-
}
|
|
1416
|
-
catch (error) {
|
|
1417
|
-
if (!activeClient.isConnected())
|
|
1418
|
-
throw error;
|
|
1419
|
-
failure ??= getErrorMessage(error);
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1422
|
-
return failure;
|
|
1423
|
-
}
|
|
1424
|
-
async function ensureConnected(reason) {
|
|
1425
|
-
const generationAtStart = runtimeGeneration;
|
|
1426
|
-
if (inboundRestore)
|
|
1427
|
-
await inboundRestore;
|
|
1428
|
-
if (disposed || generationAtStart !== runtimeGeneration) {
|
|
1429
|
-
throw new Error("Intercom shutting down");
|
|
1430
|
-
}
|
|
1431
|
-
if (reconnectPromise && reconnectPromiseGeneration === runtimeGeneration)
|
|
1432
|
-
return reconnectPromise;
|
|
1433
|
-
if (client && client.isConnected()) {
|
|
1434
|
-
return client;
|
|
1435
|
-
}
|
|
1436
|
-
const contextAtStart = getLiveContext();
|
|
1437
|
-
if (!contextAtStart || !currentSessionId) {
|
|
1438
|
-
throw new Error("Intercom runtime not initialized");
|
|
1439
|
-
}
|
|
1440
|
-
clearReconnectTimer();
|
|
1441
|
-
if (reconnectPromise && reconnectPromiseGeneration === generationAtStart) {
|
|
1442
|
-
return reconnectPromise;
|
|
1443
|
-
}
|
|
1444
|
-
let nextReconnectPromise;
|
|
1445
|
-
let retryAfterSettle = false;
|
|
1446
|
-
nextReconnectPromise = (async () => {
|
|
1447
|
-
const nextClient = new IntercomClient({ sendTimeoutMs: config.sendTimeoutMs, listTimeoutMs: config.listTimeoutMs });
|
|
1448
|
-
client = nextClient;
|
|
1449
|
-
attachClientHandlers(nextClient);
|
|
1450
|
-
try {
|
|
1451
|
-
if (reason !== "peer-awareness") {
|
|
1452
|
-
await spawnBrokerIfNeeded(config.brokerCommand, config.brokerArgs);
|
|
1453
|
-
}
|
|
1454
|
-
await nextClient.connect(await buildRegistration(), `pi-${createHash("sha256").update(currentSessionId).digest("hex").slice(0, 32)}`);
|
|
1455
|
-
if (!getLiveContext(contextAtStart, generationAtStart)) {
|
|
1456
|
-
await nextClient.disconnect();
|
|
1457
|
-
throw new Error("Intercom runtime no longer active");
|
|
1458
|
-
}
|
|
1459
|
-
client = nextClient;
|
|
1460
|
-
// Startup can change the name before registration accepts presence updates.
|
|
1461
|
-
syncPresenceIdentity(contextAtStart.sessionManager.getSessionId());
|
|
1462
|
-
reconnectAttempt = 0;
|
|
1463
|
-
if (nextClient.supportsTopics) {
|
|
1464
|
-
const failure = await restoreTopics(nextClient);
|
|
1465
|
-
const sessions = await nextClient.listSessions();
|
|
1466
|
-
if (getLiveContext(contextAtStart, generationAtStart)) {
|
|
1467
|
-
topicSyncError = failure;
|
|
1468
|
-
topics.refresh(sessions);
|
|
1469
|
-
if (failure)
|
|
1470
|
-
topics.disconnected(nextClient.sessionId);
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
else
|
|
1474
|
-
topics.disconnected();
|
|
1475
|
-
return nextClient;
|
|
1476
|
-
}
|
|
1477
|
-
catch (error) {
|
|
1478
|
-
if (client === nextClient) {
|
|
1479
|
-
client = null;
|
|
1480
|
-
}
|
|
1481
|
-
// Retry intent must not depend on the caller: a foreground tool or
|
|
1482
|
-
// overlay attempt clears any pending reconnect timer on entry, so a
|
|
1483
|
-
// failed attempt that does not re-arm would silently end recovery
|
|
1484
|
-
// (background reasons coalescing behind a foreground promise lose
|
|
1485
|
-
// their reason too). scheduleReconnect() self-guards against
|
|
1486
|
-
// duplicates via the timer, promise, and liveness checks.
|
|
1487
|
-
retryAfterSettle = Boolean(getLiveContext(contextAtStart, generationAtStart));
|
|
1488
|
-
throw toError(error);
|
|
1489
|
-
}
|
|
1490
|
-
finally {
|
|
1491
|
-
if (reconnectPromise === nextReconnectPromise) {
|
|
1492
|
-
reconnectPromise = null;
|
|
1493
|
-
reconnectPromiseGeneration = null;
|
|
1494
|
-
}
|
|
1495
|
-
// scheduleReconnect() refuses to schedule while reconnectPromise is
|
|
1496
|
-
// set, so a failed background attempt must queue its next retry only
|
|
1497
|
-
// after the promise slot clears. Scheduling from the catch block ran
|
|
1498
|
-
// before that and silently killed the retry chain on first failure.
|
|
1499
|
-
if (retryAfterSettle) {
|
|
1500
|
-
scheduleReconnect();
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
})();
|
|
1504
|
-
reconnectPromise = nextReconnectPromise;
|
|
1505
|
-
reconnectPromiseGeneration = generationAtStart;
|
|
1506
|
-
return nextReconnectPromise;
|
|
1507
|
-
}
|
|
1508
|
-
async function resolveSessionTarget(activeClient, nameOrId) {
|
|
1509
|
-
const sessions = await activeClient.listSessions();
|
|
1510
|
-
const resolution = resolveSessionTargetValue(sessions, nameOrId);
|
|
1511
|
-
if (resolution.status === "found") {
|
|
1512
|
-
return resolution.target.id;
|
|
1513
|
-
}
|
|
1514
|
-
if (resolution.status === "ambiguous") {
|
|
1515
|
-
throw new Error(`Target "${nameOrId}" matches multiple sessions. Use one of these targets: ${formatTargetOptions(resolution.matches, sessions)}.`);
|
|
1516
|
-
}
|
|
1517
|
-
if (resolution.status === "prefix_too_short") {
|
|
1518
|
-
throw new Error(`Target "${nameOrId}" is too short. Use the displayed target from intercom list, such as ${formatTargetOptions(resolution.matches, sessions)}.`);
|
|
1519
|
-
}
|
|
1520
|
-
return null;
|
|
1521
|
-
}
|
|
1522
|
-
/** Look up a connected peer's latest published health by session id. Returns null if unavailable. */
|
|
1523
|
-
async function resolvePeerHealth(activeClient, sessionId) {
|
|
1524
|
-
try {
|
|
1525
|
-
const sessions = await activeClient.listSessions();
|
|
1526
|
-
return sessions.find((session) => session.id === sessionId) ?? null;
|
|
1527
|
-
}
|
|
1528
|
-
catch {
|
|
1529
|
-
// If health cannot be resolved, callers keep the normal reply wait.
|
|
1530
|
-
return null;
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
/** Only an explicit refusal skips the default reply wait. */
|
|
1534
|
-
function peerDeclinesAsks(health) {
|
|
1535
|
-
return health?.acceptsAsks === false;
|
|
1536
|
-
}
|
|
1537
|
-
function deliverLocalSubagentRelayMessage(sender, status, messageText, completion) {
|
|
1538
|
-
const now = Date.now();
|
|
1539
|
-
const entry = {
|
|
1540
|
-
from: {
|
|
1541
|
-
id: sender,
|
|
1542
|
-
name: sender,
|
|
1543
|
-
cwd: runtimeContext?.cwd ?? process.cwd(),
|
|
1544
|
-
model: sender,
|
|
1545
|
-
status,
|
|
1546
|
-
},
|
|
1547
|
-
message: {
|
|
1548
|
-
id: completion?.completionId ? `subagent-completion:${completion.completionId}` : randomUUID(),
|
|
1549
|
-
timestamp: now,
|
|
1550
|
-
content: { text: messageText },
|
|
1551
|
-
},
|
|
1552
|
-
bodyText: messageText,
|
|
1553
|
-
...(sender === "subagent-result" && completion && currentSessionId ? { subagentCompletion: { ...completion, ownerSessionId: currentSessionId } } : {}),
|
|
1554
|
-
};
|
|
1555
|
-
if (consumedInboundIds.has(entry.message.id) || pendingInbound.has(entry.message.id))
|
|
1556
|
-
return;
|
|
1557
|
-
rememberCompletedChildren(entry);
|
|
1558
|
-
if (inboundBatch)
|
|
1559
|
-
queueIdleMessage(entry, "steer");
|
|
1560
|
-
else
|
|
1561
|
-
sendIncomingMessage(entry, "trigger");
|
|
1562
|
-
}
|
|
1563
|
-
function recordSubagentDeliveryError(entryType, to, message, error) {
|
|
1564
|
-
pi.appendEntry(entryType, {
|
|
1565
|
-
to,
|
|
1566
|
-
message,
|
|
1567
|
-
error: getErrorMessage(error),
|
|
1568
|
-
timestamp: Date.now(),
|
|
1569
|
-
});
|
|
1570
|
-
}
|
|
1571
|
-
function emitResultDelivery(requestId, delivered, error) {
|
|
1572
|
-
if (!requestId)
|
|
1573
|
-
return;
|
|
1574
|
-
pi.events.emit(SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT, {
|
|
1575
|
-
requestId,
|
|
1576
|
-
delivered,
|
|
1577
|
-
...(error ? { error: getErrorMessage(error) } : {}),
|
|
1578
|
-
});
|
|
1579
|
-
}
|
|
1580
|
-
async function relaySubagentIntercomPayload(payload, options) {
|
|
1581
|
-
const parsed = parseSubagentIntercomPayload(payload);
|
|
1582
|
-
if (!parsed)
|
|
1583
|
-
return;
|
|
1584
|
-
const relayGeneration = runtimeGeneration;
|
|
1585
|
-
const relayStillLive = () => !runtimeStarted || Boolean(getLiveContext(runtimeContext, relayGeneration));
|
|
1586
|
-
try {
|
|
1587
|
-
if (inboundRestore)
|
|
1588
|
-
await inboundRestore;
|
|
1589
|
-
if (!relayStillLive())
|
|
1590
|
-
return;
|
|
1591
|
-
if (!currentSessionTargetMatches(parsed.to)) {
|
|
1592
|
-
const activeClient = await ensureConnected("background");
|
|
1593
|
-
const target = await resolveSessionTarget(activeClient, parsed.to) ?? parsed.to;
|
|
1594
|
-
if (!relayStillLive())
|
|
1595
|
-
return;
|
|
1596
|
-
if (!currentSessionTargetMatches(parsed.to, target, activeClient)) {
|
|
1597
|
-
const result = await activeClient.send(target, { text: parsed.message });
|
|
1598
|
-
if (!relayStillLive())
|
|
1599
|
-
return;
|
|
1600
|
-
if (!result.accepted)
|
|
1601
|
-
throw new Error(result.reason ?? "Session may not exist or has disconnected.");
|
|
1602
|
-
if (options.acknowledge)
|
|
1603
|
-
emitResultDelivery(parsed.requestId, true);
|
|
1604
|
-
return;
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
if (!(options.sender === "subagent-control" && (parsed.source === "foreground" || parsed.source === "async"))) {
|
|
1608
|
-
deliverLocalSubagentRelayMessage(options.sender, options.status, parsed.message, parsed.completion);
|
|
1609
|
-
}
|
|
1610
|
-
if (options.acknowledge)
|
|
1611
|
-
emitResultDelivery(parsed.requestId, true);
|
|
1612
|
-
}
|
|
1613
|
-
catch (error) {
|
|
1614
|
-
if (!relayStillLive())
|
|
1615
|
-
return;
|
|
1616
|
-
recordSubagentDeliveryError(options.errorEntryType, parsed.to, parsed.message, error);
|
|
1617
|
-
if (options.acknowledge)
|
|
1618
|
-
emitResultDelivery(parsed.requestId, false, error);
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
// Subagent event bridges (live/health/control/result) are torn down on session_shutdown.
|
|
1622
|
-
// Re-register them on session_start so they survive an in-process restart; the guard skips
|
|
1623
|
-
// the first start (already registered at load) and only restores after a shutdown cleared them.
|
|
1624
|
-
function registerSubagentEventBridges() {
|
|
1625
|
-
return [
|
|
1626
|
-
...registerSubagentLiveEventHandlers({
|
|
1627
|
-
events: pi.events,
|
|
1628
|
-
ensureConnected: () => ensureConnected("background"),
|
|
1629
|
-
getConnection: () => ({ client, connecting: Boolean(startupConnectTimer || reconnectPromise), started: runtimeStarted }),
|
|
1630
|
-
resolveSessionTarget,
|
|
1631
|
-
currentSessionTargetMatches,
|
|
1632
|
-
getLivenessCheck: () => {
|
|
1633
|
-
const generation = runtimeGeneration;
|
|
1634
|
-
return () => !runtimeStarted || Boolean(getLiveContext(runtimeContext, generation));
|
|
1635
|
-
},
|
|
1636
|
-
}),
|
|
1637
|
-
pi.events.on(SUBAGENT_SUPERVISOR_QUESTION_RESOLVED_EVENT, (payload) => {
|
|
1638
|
-
const questionId = payload && typeof payload === "object" ? payload.questionId : undefined;
|
|
1639
|
-
if (typeof questionId !== "string")
|
|
1640
|
-
return;
|
|
1641
|
-
retireReply(questionId, pendingInbound.delete(questionId) ? "discarded" : "reply-retired");
|
|
1642
|
-
syncPresenceStatus();
|
|
1643
|
-
}),
|
|
1644
|
-
pi.events.on("intercom:open", () => {
|
|
1645
|
-
const ctx = getLiveContext();
|
|
1646
|
-
if (ctx?.mode === "tui")
|
|
1647
|
-
return openIntercomOverlay(ctx, "all");
|
|
1648
|
-
}),
|
|
1649
|
-
pi.events.on(SUBAGENT_INTERCOM_IDENTITY_REQUEST_EVENT, (payload) => {
|
|
1650
|
-
const requestId = payload && typeof payload === "object" ? payload.requestId : undefined;
|
|
1651
|
-
if (typeof requestId === "string" && client?.isConnected() && client.sessionId) {
|
|
1652
|
-
pi.events.emit(SUBAGENT_INTERCOM_IDENTITY_RESPONSE_EVENT, { requestId, sessionId: client.sessionId });
|
|
1653
|
-
}
|
|
1654
|
-
}),
|
|
1655
|
-
pi.events.on(SUBAGENT_CONTROL_INTERCOM_EVENT, (payload) => {
|
|
1656
|
-
return relaySubagentIntercomPayload(payload, {
|
|
1657
|
-
sender: "subagent-control",
|
|
1658
|
-
status: "needs_attention",
|
|
1659
|
-
errorEntryType: "intercom_control_error",
|
|
1660
|
-
});
|
|
1661
|
-
}),
|
|
1662
|
-
pi.events.on(SUBAGENT_RESULT_INTERCOM_EVENT, (payload) => {
|
|
1663
|
-
return relaySubagentIntercomPayload(payload, {
|
|
1664
|
-
sender: "subagent-result",
|
|
1665
|
-
status: "result",
|
|
1666
|
-
errorEntryType: "intercom_result_error",
|
|
1667
|
-
acknowledge: true,
|
|
1668
|
-
});
|
|
1669
|
-
}),
|
|
1670
|
-
];
|
|
1671
|
-
}
|
|
1672
|
-
let eventUnsubscribes = registerSubagentEventBridges();
|
|
1673
|
-
let eventBridgesActive = true;
|
|
1674
|
-
pi.on("session_start", async (event, ctx) => {
|
|
1675
|
-
if (!eventBridgesActive) {
|
|
1676
|
-
eventUnsubscribes = registerSubagentEventBridges();
|
|
1677
|
-
eventBridgesActive = true;
|
|
1678
|
-
}
|
|
1679
|
-
disposed = false;
|
|
1680
|
-
runtimeStarted = true;
|
|
1681
|
-
runtimeGeneration += 1;
|
|
1682
|
-
const generation = runtimeGeneration;
|
|
1683
|
-
// A session boundary starts from a cold prompt cache, so re-detect peers
|
|
1684
|
-
// instead of carrying a pin from a previous session in this process.
|
|
1685
|
-
peerAwarenessHintPinned = false;
|
|
1686
|
-
reconnectAttempt = 0;
|
|
1687
|
-
clearReconnectTimer();
|
|
1688
|
-
clearStartupConnectTimer();
|
|
1689
|
-
runtimeContext = ctx;
|
|
1690
|
-
currentSessionId = ctx.sessionManager.getSessionId();
|
|
1691
|
-
topics.start(ctx);
|
|
1692
|
-
topicSyncError = undefined;
|
|
1693
|
-
currentModel = ctx.model?.id ?? "unknown";
|
|
1694
|
-
agentRunning = false;
|
|
1695
|
-
lastIntercomActivity = 0;
|
|
1696
|
-
activeTools.clear();
|
|
1697
|
-
pendingInbound.clear();
|
|
1698
|
-
nativePendingIds.clear();
|
|
1699
|
-
replyTracker.reset();
|
|
1700
|
-
if (event.reason !== "reload")
|
|
1701
|
-
restoreLazyTools(pi, ctx, "load_intercom", ["intercom"]);
|
|
1702
|
-
const restoration = Promise.resolve().then(() => restoreInbound(ctx, generation));
|
|
1703
|
-
inboundRestore = restoration;
|
|
1704
|
-
await restoration;
|
|
1705
|
-
if (!getLiveContext(ctx, generation))
|
|
1706
|
-
return;
|
|
1707
|
-
if (inboundRestore === restoration)
|
|
1708
|
-
inboundRestore = undefined;
|
|
1709
|
-
if (event.reason === "reload") {
|
|
1710
|
-
for (const entry of pendingInbound.values())
|
|
1711
|
-
if (entry.stage === "native")
|
|
1712
|
-
nativePendingIds.add(entry.message.id);
|
|
1713
|
-
}
|
|
1714
|
-
// A fresh runtime has no inherited native queues. Reload keeps those queues
|
|
1715
|
-
// and in-flight prompts, so only its intercom-staged entries are flushed here.
|
|
1716
|
-
if (event.reason !== "reload" && !ctx.signal && !ctx.hasPendingMessages()) {
|
|
1717
|
-
for (const entry of [...pendingInbound.values()])
|
|
1718
|
-
setInboundStage(entry, "queued");
|
|
1719
|
-
}
|
|
1720
|
-
scheduleStartupConnection(ctx, generation);
|
|
1721
|
-
if (queuedInbound().length > 0)
|
|
1722
|
-
scheduleInboundFlush();
|
|
1723
|
-
});
|
|
1724
|
-
pi.on("session_shutdown", async () => {
|
|
1725
|
-
for (const unsubscribe of eventUnsubscribes) {
|
|
1726
|
-
try {
|
|
1727
|
-
unsubscribe();
|
|
1728
|
-
}
|
|
1729
|
-
catch {
|
|
1730
|
-
// Best effort cleanup for reload/session replacement.
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
eventUnsubscribes = [];
|
|
1734
|
-
eventBridgesActive = false;
|
|
1735
|
-
clearStartupConnectTimer();
|
|
1736
|
-
clearReconnectTimer();
|
|
1737
|
-
clearInboundFlushTimer();
|
|
1738
|
-
rejectReplyWaiter(new Error("Session shutting down"));
|
|
1739
|
-
// Native reload keeps this runner valid until shutdown completes. Finish the
|
|
1740
|
-
// owned handoff before invalidating it so cleared native messages cannot strand.
|
|
1741
|
-
await Promise.allSettled([inboundRestore, inboundBatch]);
|
|
1742
|
-
disposed = true;
|
|
1743
|
-
runtimeGeneration += 1;
|
|
1744
|
-
// ponytail: joins the bounded attempt only; overdue custom launchers need lifecycle ownership in broker/spawn.ts.
|
|
1745
|
-
await Promise.allSettled([reconnectPromise]);
|
|
1746
|
-
inboundRestore = undefined;
|
|
1747
|
-
inboundBatch = undefined;
|
|
1748
|
-
replyTracker.reset();
|
|
1749
|
-
pendingInbound.clear();
|
|
1750
|
-
nativePendingIds.clear();
|
|
1751
|
-
consumedInboundIds.clear();
|
|
1752
|
-
completedChildren.clear();
|
|
1753
|
-
reconciledLeafId = null;
|
|
1754
|
-
clearInboundFlushTimer();
|
|
1755
|
-
agentRunning = false;
|
|
1756
|
-
activeTools.clear();
|
|
1757
|
-
if (client) {
|
|
1758
|
-
await client.disconnect();
|
|
1759
|
-
client = null;
|
|
1760
|
-
}
|
|
1761
|
-
runtimeContext = null;
|
|
1762
|
-
currentSessionId = null;
|
|
1763
|
-
});
|
|
1764
|
-
pi.on("session_info_changed", (_event, ctx) => {
|
|
1765
|
-
if (getLiveContext(ctx) && currentSessionId)
|
|
1766
|
-
syncPresenceIdentity(currentSessionId);
|
|
1767
|
-
});
|
|
1768
|
-
pi.on("session_tree", (_event, ctx) => {
|
|
1769
|
-
if (getLiveContext(ctx))
|
|
1770
|
-
reconcileConsumedInbound(ctx, true);
|
|
1771
|
-
reconcileIntercom(ctx);
|
|
1772
|
-
});
|
|
1773
|
-
pi.on("session_compact", (_event, ctx) => reconcileIntercom(ctx));
|
|
1774
|
-
pi.on("turn_end", (event) => {
|
|
1775
|
-
if (!getLiveContext()) {
|
|
1776
|
-
return;
|
|
1777
|
-
}
|
|
1778
|
-
// Pi exposes the next queued batch, not every custom queue item. Empty
|
|
1779
|
-
// proves both queues cleared; a nonempty batch must retain all admitted IDs.
|
|
1780
|
-
// ponytail: selective removal while other items remain is not observable;
|
|
1781
|
-
// use a complete public queue snapshot if the host adds one.
|
|
1782
|
-
if (event.context.pendingMessages.length === 0)
|
|
1783
|
-
nativePendingIds.clear();
|
|
1784
|
-
replyTracker.endTurn();
|
|
1785
|
-
scheduleInboundFlush(0);
|
|
1786
|
-
});
|
|
1787
|
-
pi.on("message_end", (event) => {
|
|
1788
|
-
const message = event.message;
|
|
1789
|
-
const inboundId = inboundIdFromCustomMessage(message);
|
|
1790
|
-
if (inboundId) {
|
|
1791
|
-
consumedInboundIds.add(inboundId);
|
|
1792
|
-
pendingInbound.delete(inboundId);
|
|
1793
|
-
nativePendingIds.delete(inboundId);
|
|
1794
|
-
}
|
|
1795
|
-
const activeClient = client;
|
|
1796
|
-
const context = replyTracker.currentTurn();
|
|
1797
|
-
const errorMessage = getAssistantErrorMessage(message);
|
|
1798
|
-
if (!activeClient?.isConnected() || !context?.message.expectsReply || !errorMessage) {
|
|
1799
|
-
return;
|
|
1800
|
-
}
|
|
1801
|
-
const replyTo = context.message.id;
|
|
1802
|
-
return activeClient.send(context.from.id, {
|
|
1803
|
-
text: `${RECIPIENT_TURN_FAILED_PREFIX} ${errorMessage}`,
|
|
1804
|
-
replyTo,
|
|
1805
|
-
attachments: [{
|
|
1806
|
-
type: "context",
|
|
1807
|
-
name: RECIPIENT_TURN_FAILED_ATTACHMENT,
|
|
1808
|
-
content: errorMessage,
|
|
1809
|
-
}],
|
|
1810
|
-
}).then((result) => {
|
|
1811
|
-
if (result.delivered && !isDurableSupervisorQuestion(context.message)) {
|
|
1812
|
-
retireReply(replyTo);
|
|
1813
|
-
}
|
|
1814
|
-
}).catch(() => {
|
|
1815
|
-
// Best-effort failure propagation; the local error remains visible in the recipient session.
|
|
1816
|
-
});
|
|
1817
|
-
});
|
|
1818
|
-
pi.on("agent_start", () => {
|
|
1819
|
-
if (!getLiveContext()) {
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
|
-
agentRunning = true;
|
|
1823
|
-
activeTools.clear();
|
|
1824
|
-
syncPresenceStatus();
|
|
1825
|
-
return flushIdleMessages();
|
|
1826
|
-
});
|
|
1827
|
-
pi.on("tool_execution_start", (event) => {
|
|
1828
|
-
if (!getLiveContext()) {
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
activeTools.set(event.toolCallId, event.toolName);
|
|
1832
|
-
syncPresenceStatus();
|
|
1833
|
-
});
|
|
1834
|
-
pi.on("tool_execution_end", (event) => {
|
|
1835
|
-
if (!getLiveContext()) {
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
activeTools.delete(event.toolCallId);
|
|
1839
|
-
syncPresenceStatus();
|
|
1840
|
-
return flushIdleMessages();
|
|
1841
|
-
});
|
|
1842
|
-
pi.on("agent_settled", async (_event, ctx) => {
|
|
1843
|
-
// A rejected concurrent prompt may settle while the original run is active.
|
|
1844
|
-
if (!getLiveContext(ctx) || ctx.signal) {
|
|
1845
|
-
return;
|
|
1846
|
-
}
|
|
1847
|
-
const generation = runtimeGeneration;
|
|
1848
|
-
agentRunning = false;
|
|
1849
|
-
activeTools.clear();
|
|
1850
|
-
replyTracker.endAgent();
|
|
1851
|
-
syncPresenceStatus();
|
|
1852
|
-
await redeliverUnconsumedInbound(ctx, generation);
|
|
1853
|
-
if (getLiveContext(ctx, generation))
|
|
1854
|
-
scheduleInboundFlush(0);
|
|
1855
|
-
});
|
|
1856
|
-
pi.on("turn_start", (_event, ctx) => {
|
|
1857
|
-
if (!getLiveContext(ctx)) {
|
|
1858
|
-
return;
|
|
1859
|
-
}
|
|
1860
|
-
currentSessionId = ctx.sessionManager.getSessionId();
|
|
1861
|
-
syncPresenceIdentity(ctx.sessionManager.getSessionId());
|
|
1862
|
-
replyTracker.beginTurn();
|
|
1863
|
-
});
|
|
1864
|
-
pi.on("model_select", (event, ctx) => {
|
|
1865
|
-
if (!getLiveContext(ctx)) {
|
|
1866
|
-
return;
|
|
1867
|
-
}
|
|
1868
|
-
currentModel = event.model.id;
|
|
1869
|
-
if (client) {
|
|
1870
|
-
client.updatePresence({
|
|
1871
|
-
...buildPresenceIdentity(pi, ctx.sessionManager.getSessionId()),
|
|
1872
|
-
model: event.model.id,
|
|
1873
|
-
status: currentStatus(),
|
|
1874
|
-
});
|
|
1875
|
-
}
|
|
1876
|
-
});
|
|
1877
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
1878
|
-
if (childOrchestratorMetadata)
|
|
1879
|
-
return;
|
|
1880
|
-
const generation = runtimeGeneration;
|
|
1881
|
-
if (!getLiveContext(ctx, generation))
|
|
1882
|
-
return;
|
|
1883
|
-
if (peerAwarenessHintPinned) {
|
|
1884
|
-
setPromptSection(event.systemPromptOptions, "intercom_peers", PEER_AWARENESS_HINT);
|
|
1885
|
-
return;
|
|
1886
|
-
}
|
|
1887
|
-
const deadline = Date.now() + PEER_AWARENESS_LIST_TIMEOUT_MS;
|
|
1888
|
-
let activeClient = client?.isConnected() ? client : null;
|
|
1889
|
-
if (!activeClient) {
|
|
1890
|
-
clearStartupConnectTimer();
|
|
1891
|
-
const brokerAvailable = await settleWithin(() => isBrokerRunning(), PEER_AWARENESS_LIST_TIMEOUT_MS);
|
|
1892
|
-
const connectBudgetMs = deadline - Date.now();
|
|
1893
|
-
if (brokerAvailable && connectBudgetMs > 0) {
|
|
1894
|
-
activeClient = await settleWithin(() => ensureConnected("peer-awareness"), connectBudgetMs);
|
|
1895
|
-
}
|
|
1896
|
-
if (!activeClient) {
|
|
1897
|
-
if (getLiveContext(ctx, generation))
|
|
1898
|
-
scheduleStartupConnection(ctx, generation);
|
|
1899
|
-
return;
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
const currentBrokerSessionId = activeClient.sessionId;
|
|
1903
|
-
const remainingMs = deadline - Date.now();
|
|
1904
|
-
if (!currentBrokerSessionId || remainingMs <= 0 || !getLiveContext(ctx, generation))
|
|
1905
|
-
return;
|
|
1906
|
-
const sessions = await settleWithin(() => activeClient.listSessions(), remainingMs);
|
|
1907
|
-
if (!sessions || client !== activeClient || !getLiveContext(ctx, generation))
|
|
1908
|
-
return;
|
|
1909
|
-
const hint = formatPeerAwarenessHint(sessions, currentBrokerSessionId);
|
|
1910
|
-
if (!hint)
|
|
1911
|
-
return;
|
|
1912
|
-
peerAwarenessHintPinned = true;
|
|
1913
|
-
setPromptSection(event.systemPromptOptions, "intercom_peers", hint);
|
|
1914
|
-
});
|
|
1915
|
-
pi.registerMessageRenderer("subagent-human-message", (message, _options, theme) => {
|
|
1916
|
-
const details = message.details;
|
|
1917
|
-
return new Text(`${theme.fg("accent", theme.bold("User → this agent"))}\n${details?.bodyText ?? message.content}`, 0, 0);
|
|
1918
|
-
});
|
|
1919
|
-
const messageExpansion = new WeakMap();
|
|
1920
|
-
pi.registerMessageRenderer("intercom_message", (message, options, theme) => {
|
|
1921
|
-
const details = message.details;
|
|
1922
|
-
if (!details)
|
|
1923
|
-
return undefined;
|
|
1924
|
-
const compact = "compactView" in options && options.compactView === true;
|
|
1925
|
-
const attention = details.from.id === "subagent-control" || details.from.status === "needs_attention" || details.message.expectsReply === true || Boolean(details.replyCommand);
|
|
1926
|
-
const renderContent = (expanded) => {
|
|
1927
|
-
if (compact && !expanded && !attention) {
|
|
1928
|
-
const sender = details.from.name || details.from.id.slice(0, 8);
|
|
1929
|
-
const body = details.bodyText || details.message.content.text;
|
|
1930
|
-
const summary = details.subagentCompletion
|
|
1931
|
-
? `${details.subagentCompletion.status} [${details.subagentCompletion.runId.slice(0, 8)}]`
|
|
1932
|
-
: details.from.id === "subagent-result"
|
|
1933
|
-
? body.split("\n").slice(2, 6).join(" · ")
|
|
1934
|
-
: body.split("\n").find((line) => line.trim()) || "(no text)";
|
|
1935
|
-
const preview = `${sender}: ${summary}`.replace(/\s+/g, " ").trim();
|
|
1936
|
-
let cachedWidth;
|
|
1937
|
-
let cachedKey;
|
|
1938
|
-
let cachedLines;
|
|
1939
|
-
return {
|
|
1940
|
-
render(width) {
|
|
1941
|
-
const key = keyText("app.tools.expand");
|
|
1942
|
-
if (cachedLines && cachedWidth === width && cachedKey === key)
|
|
1943
|
-
return cachedLines;
|
|
1944
|
-
const hint = key ? theme.fg("dim", ` · ${key}`) : "";
|
|
1945
|
-
const text = `${" ".repeat(options.outputPad)}${theme.fg("accent", "📨 ")}${theme.fg("muted", preview)}`;
|
|
1946
|
-
cachedWidth = width;
|
|
1947
|
-
cachedKey = key;
|
|
1948
|
-
cachedLines = [truncateToWidth(truncateToWidth(text, width - visibleWidth(hint)) + hint, width)];
|
|
1949
|
-
return cachedLines;
|
|
1950
|
-
},
|
|
1951
|
-
invalidate() { cachedLines = undefined; },
|
|
1952
|
-
};
|
|
1953
|
-
}
|
|
1954
|
-
return new InlineMessageComponent(details.from, details.message, theme, details.replyCommand, details.bodyText, expanded || details.from.id !== "subagent-result" || attention);
|
|
1955
|
-
};
|
|
1956
|
-
if (attention || (!compact && details.from.id !== "subagent-result"))
|
|
1957
|
-
return renderContent(options.expanded);
|
|
1958
|
-
let state = messageExpansion.get(message);
|
|
1959
|
-
if (!state || state.globalExpanded !== options.expanded) {
|
|
1960
|
-
state = { expanded: options.expanded, globalExpanded: options.expanded };
|
|
1961
|
-
messageExpansion.set(message, state);
|
|
1962
|
-
}
|
|
1963
|
-
const expansion = state;
|
|
1964
|
-
let component = renderContent(expansion.expanded);
|
|
1965
|
-
return new MouseRegion({ render: (width) => component.render(width), invalidate: () => component.invalidate() }, (event) => {
|
|
1966
|
-
if (event.type !== "click" || event.button !== "left")
|
|
1967
|
-
return;
|
|
1968
|
-
expansion.expanded = !expansion.expanded;
|
|
1969
|
-
component = renderContent(expansion.expanded);
|
|
1970
|
-
return { handled: true };
|
|
1971
|
-
});
|
|
1972
|
-
});
|
|
1973
|
-
async function requestSupervisorDecision(reason, message, interview, signal, ctx) {
|
|
1974
|
-
if (replyWaiter)
|
|
1975
|
-
throw new Error("Already waiting for a reply");
|
|
1976
|
-
if (signal?.aborted)
|
|
1977
|
-
throw new Error("Cancelled");
|
|
1978
|
-
const metadata = childOrchestratorMetadata;
|
|
1979
|
-
const body = interview ? formatSupervisorInterviewRequest(interview, message) : message;
|
|
1980
|
-
const question = createSupervisorQuestion({
|
|
1981
|
-
runId: metadata.runId,
|
|
1982
|
-
ownerTarget: metadata.orchestratorTarget,
|
|
1983
|
-
agent: metadata.agent,
|
|
1984
|
-
index: Number(metadata.index),
|
|
1985
|
-
childSessionId: ctx.sessionManager.getSessionId(),
|
|
1986
|
-
childTarget: metadata.sessionName ?? buildPresenceIdentity(pi, ctx.sessionManager.getSessionId()).name,
|
|
1987
|
-
sessionFile: ctx.sessionManager.getSessionFile() ?? "",
|
|
1988
|
-
cwd: ctx.cwd,
|
|
1989
|
-
pid: process.pid,
|
|
1990
|
-
reason,
|
|
1991
|
-
message: body,
|
|
1992
|
-
...(interview ? { interview } : {}),
|
|
1993
|
-
});
|
|
1994
|
-
const requestText = formatChildOrchestratorMessage(interview ? "interview" : "ask", metadata, [
|
|
1995
|
-
`Question ID: ${question.questionId}`,
|
|
1996
|
-
`Answer after reconnect/reload: ${formatRunAction("answer", question.runId, { questionId: question.questionId, message: "..." }, Number(process.env.PI_SUBAGENT_DEPTH) > 1)}`,
|
|
1997
|
-
body,
|
|
1998
|
-
].join("\n"));
|
|
1999
|
-
const replyPromise = waitForReply(metadata.orchestratorTarget, question.questionId, signal, question);
|
|
2000
|
-
// Persistence is the delivery path; intercom only wakes the supervisor. Going offline does not end the wait.
|
|
2001
|
-
let retryAfter = 0;
|
|
2002
|
-
let notificationFailureLogged = false;
|
|
2003
|
-
const notifySupervisor = async () => {
|
|
2004
|
-
const waiter = currentReplyWaiter();
|
|
2005
|
-
if (waiter?.replyTo !== question.questionId || Date.now() < retryAfter)
|
|
2006
|
-
return;
|
|
2007
|
-
waiter.retryNotification = undefined;
|
|
2008
|
-
let retry = true;
|
|
2009
|
-
try {
|
|
2010
|
-
const connectedClient = await ensureConnected("tool");
|
|
2011
|
-
if (currentReplyWaiter() !== waiter)
|
|
2012
|
-
return;
|
|
2013
|
-
const to = await resolveSessionTarget(connectedClient, metadata.orchestratorTarget) ?? metadata.orchestratorTarget;
|
|
2014
|
-
if (currentReplyWaiter() !== waiter)
|
|
2015
|
-
return;
|
|
2016
|
-
waiter.from = to;
|
|
2017
|
-
// Once dispatched, an exception may mean a lost acknowledgement. Only explicit rejection is safe to replay.
|
|
2018
|
-
retry = false;
|
|
2019
|
-
const sent = await connectedClient.send(to, { text: requestText, messageId: question.questionId, expectsReply: true, delivery: "steer" });
|
|
2020
|
-
if (currentReplyWaiter() !== waiter)
|
|
2021
|
-
return;
|
|
2022
|
-
retry = !sent.accepted;
|
|
2023
|
-
if (sent.accepted || !notificationFailureLogged) {
|
|
2024
|
-
pi.appendEntry("intercom_sent", { to, messageId: question.questionId, message: { text: requestText, reason }, accepted: sent.accepted, timestamp: Date.now() });
|
|
2025
|
-
}
|
|
2026
|
-
notificationFailureLogged ||= !sent.accepted;
|
|
2027
|
-
}
|
|
2028
|
-
catch (error) {
|
|
2029
|
-
if (currentReplyWaiter() === waiter && !notificationFailureLogged) {
|
|
2030
|
-
pi.appendEntry("intercom_question_notification_error", { questionId: question.questionId, error: getErrorMessage(error) });
|
|
2031
|
-
notificationFailureLogged = true;
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
finally {
|
|
2035
|
-
if (retry && currentReplyWaiter() === waiter) {
|
|
2036
|
-
retryAfter = Date.now() + 1000;
|
|
2037
|
-
waiter.retryNotification = () => { void notifySupervisor(); };
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
};
|
|
2041
|
-
void notifySupervisor();
|
|
2042
|
-
const replyMessage = await replyPromise;
|
|
2043
|
-
const replyText = replyMessage.content.text;
|
|
2044
|
-
const replyAttachments = replyMessage.content.attachments?.length ? formatAttachments(replyMessage.content.attachments) : "";
|
|
2045
|
-
const structuredReply = interview ? parseStructuredSupervisorReply(replyText, interview) : undefined;
|
|
2046
|
-
pi.appendEntry("intercom_received", { from: metadata.orchestratorTarget, questionId: question.questionId, message: { text: replyText, attachments: replyMessage.content.attachments }, messageId: replyMessage.id, timestamp: replyMessage.timestamp });
|
|
2047
|
-
recordQuestionDelivery(question, { kind: "live", runId: question.runId, deliveredAt: Date.now() });
|
|
2048
|
-
return {
|
|
2049
|
-
content: [{ type: "text", text: `**${readQuestionState(question).answer?.origin === "human" ? "Direct user answer (human origin)" : "Reply from supervisor"}:**\n${replyText}${replyAttachments}` }],
|
|
2050
|
-
isError: false,
|
|
2051
|
-
details: { questionId: question.questionId, ...(structuredReply?.value ? { structuredReply: structuredReply.value } : structuredReply?.error ? { structuredReplyParseError: structuredReply.error } : {}) },
|
|
2052
|
-
};
|
|
2053
|
-
}
|
|
2054
|
-
if (childOrchestratorMetadata) {
|
|
2055
|
-
pi.registerTool({
|
|
2056
|
-
name: "contact_supervisor",
|
|
2057
|
-
label: "Contact Supervisor",
|
|
2058
|
-
description: "Subagent-only tool for contacting the supervisor agent that delegated this task. Use need_decision only when this child cannot safely continue without a decision, approval, or product/API/scope clarification; this steers the supervisor at its next tool boundary and keeps the child alive for the reply. Use interview_request only when multiple structured answers are all required before safe progress; this also steers and waits. Use progress_update only for a discovery or change the supervisor needs while working; it steers at the next tool boundary without waiting for a reply. Skip starts, redundant status, and routine completion; retain material findings in the final result.",
|
|
2059
|
-
promptSnippet: "Subagent-only: steer the supervisor for required decisions, structured interviews, or material discoveries needed during active work. Skip routine status and completion messages.",
|
|
2060
|
-
promptGuidelines: [
|
|
2061
|
-
"Use contact_supervisor with reason='need_decision' when a subagent cannot safely continue without a decision, approval, or product/API/scope clarification; it steers the supervisor and waits for the reply.",
|
|
2062
|
-
"Use contact_supervisor with reason='interview_request' only when the child cannot safely continue until it receives multiple structured answers in one blocking steered exchange.",
|
|
2063
|
-
"Use contact_supervisor with reason='progress_update' only for a discovery or change the supervisor needs while working. It steers at the next tool boundary without waiting. Skip starts, redundant narration, and routine completion; keep material findings in the final result.",
|
|
2064
|
-
"Do not use contact_supervisor for routine completion handoffs; return the final subagent result normally.",
|
|
2065
|
-
],
|
|
2066
|
-
parameters: Type.Object({
|
|
2067
|
-
reason: StringEnum(["need_decision", "progress_update", "interview_request"], {
|
|
2068
|
-
description: "Contact reason: 'need_decision' and 'interview_request' steer the supervisor and wait for a reply; 'progress_update' steers a material discovery without waiting for a reply",
|
|
2069
|
-
}),
|
|
2070
|
-
message: Type.Optional(Type.String({
|
|
2071
|
-
description: "Decision request, optional interview note, or meaningful progress update for the supervisor",
|
|
2072
|
-
})),
|
|
2073
|
-
interview: Type.Optional(Type.Object({
|
|
2074
|
-
title: Type.Optional(Type.String()),
|
|
2075
|
-
description: Type.Optional(Type.String()),
|
|
2076
|
-
questions: Type.Array(Type.Object({
|
|
2077
|
-
id: Type.String(),
|
|
2078
|
-
type: StringEnum(["single", "multi", "text", "image", "info"], { description: "Question type: single, multi, text, image, or info" }),
|
|
2079
|
-
question: Type.String(),
|
|
2080
|
-
options: Type.Optional(Type.Array(Type.Any())),
|
|
2081
|
-
context: Type.Optional(Type.String()),
|
|
2082
|
-
})),
|
|
2083
|
-
}, { description: "Structured interview request for reason='interview_request'" })),
|
|
2084
|
-
}),
|
|
2085
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
2086
|
-
return throwIfToolError(await (async () => {
|
|
2087
|
-
const reason = params.reason;
|
|
2088
|
-
if (reason !== "need_decision" && reason !== "progress_update" && reason !== "interview_request") {
|
|
2089
|
-
return {
|
|
2090
|
-
content: [{ type: "text", text: "Invalid reason. Use 'need_decision', 'interview_request', or 'progress_update'." }],
|
|
2091
|
-
isError: true,
|
|
2092
|
-
details: { error: true },
|
|
2093
|
-
};
|
|
2094
|
-
}
|
|
2095
|
-
if ((reason === "need_decision" || reason === "progress_update") && typeof params.message !== "string") {
|
|
2096
|
-
return {
|
|
2097
|
-
content: [{ type: "text", text: `Missing 'message' parameter for reason '${reason}'.` }],
|
|
2098
|
-
isError: true,
|
|
2099
|
-
details: { error: true },
|
|
2100
|
-
};
|
|
2101
|
-
}
|
|
2102
|
-
const interviewValidation = reason === "interview_request"
|
|
2103
|
-
? validateSupervisorInterviewRequest(params.interview)
|
|
2104
|
-
: undefined;
|
|
2105
|
-
if (interviewValidation?.ok === false) {
|
|
2106
|
-
return {
|
|
2107
|
-
content: [{ type: "text", text: `Invalid interview request: ${interviewValidation.error}` }],
|
|
2108
|
-
isError: true,
|
|
2109
|
-
details: { error: true },
|
|
2110
|
-
};
|
|
2111
|
-
}
|
|
2112
|
-
const supervisorInterview = interviewValidation?.ok === true ? interviewValidation.interview : undefined;
|
|
2113
|
-
if (reason !== "progress_update") {
|
|
2114
|
-
return await requestSupervisorDecision(reason, params.message, supervisorInterview, signal, ctx);
|
|
2115
|
-
}
|
|
2116
|
-
let connectedClient;
|
|
2117
|
-
try {
|
|
2118
|
-
connectedClient = await ensureConnected("tool");
|
|
2119
|
-
}
|
|
2120
|
-
catch (error) {
|
|
2121
|
-
return {
|
|
2122
|
-
content: [{ type: "text", text: `Intercom not connected: ${getErrorMessage(error)}` }],
|
|
2123
|
-
isError: true,
|
|
2124
|
-
details: { error: true },
|
|
2125
|
-
};
|
|
2126
|
-
}
|
|
2127
|
-
syncPresenceIdentity(ctx.sessionManager.getSessionId());
|
|
2128
|
-
if (signal?.aborted) {
|
|
2129
|
-
return {
|
|
2130
|
-
content: [{ type: "text", text: "Cancelled" }],
|
|
2131
|
-
isError: true,
|
|
2132
|
-
details: { error: true },
|
|
2133
|
-
};
|
|
2134
|
-
}
|
|
2135
|
-
const metadata = childOrchestratorMetadata;
|
|
2136
|
-
let sendTo;
|
|
2137
|
-
try {
|
|
2138
|
-
sendTo = await resolveSessionTarget(connectedClient, metadata.orchestratorTarget) ?? metadata.orchestratorTarget;
|
|
2139
|
-
}
|
|
2140
|
-
catch (error) {
|
|
2141
|
-
return {
|
|
2142
|
-
content: [{ type: "text", text: `Failed to resolve supervisor target: ${getErrorMessage(error)}` }],
|
|
2143
|
-
isError: true,
|
|
2144
|
-
details: { error: true },
|
|
2145
|
-
};
|
|
2146
|
-
}
|
|
2147
|
-
if (signal?.aborted) {
|
|
2148
|
-
return {
|
|
2149
|
-
content: [{ type: "text", text: "Cancelled" }],
|
|
2150
|
-
isError: true,
|
|
2151
|
-
details: { error: true },
|
|
2152
|
-
};
|
|
2153
|
-
}
|
|
2154
|
-
if (sendTo === connectedClient.sessionId) {
|
|
2155
|
-
return {
|
|
2156
|
-
content: [{ type: "text", text: "Cannot message the current session" }],
|
|
2157
|
-
isError: true,
|
|
2158
|
-
details: { error: true },
|
|
2159
|
-
};
|
|
2160
|
-
}
|
|
2161
|
-
const message = params.message;
|
|
2162
|
-
try {
|
|
2163
|
-
const result = await connectedClient.send(sendTo, {
|
|
2164
|
-
text: formatChildOrchestratorMessage("update", metadata, message),
|
|
2165
|
-
delivery: "steer",
|
|
2166
|
-
});
|
|
2167
|
-
if (!result.accepted) {
|
|
2168
|
-
const errorText = result.reason ?? "Session may not exist or has disconnected.";
|
|
2169
|
-
return {
|
|
2170
|
-
content: [{ type: "text", text: `Message to "${metadata.orchestratorTarget}" was not delivered: ${errorText}` }],
|
|
2171
|
-
isError: true,
|
|
2172
|
-
details: { messageId: result.id, accepted: result.accepted, delivered: false, reason: result.reason },
|
|
2173
|
-
};
|
|
2174
|
-
}
|
|
2175
|
-
markIntercomActivity();
|
|
2176
|
-
syncPresenceStatus();
|
|
2177
|
-
pi.appendEntry("intercom_sent", {
|
|
2178
|
-
to: metadata.orchestratorTarget,
|
|
2179
|
-
message: { text: message, reason },
|
|
2180
|
-
messageId: result.id,
|
|
2181
|
-
timestamp: Date.now(),
|
|
2182
|
-
subagent: { runId: metadata.runId, agent: metadata.agent, index: metadata.index },
|
|
2183
|
-
});
|
|
2184
|
-
return {
|
|
2185
|
-
content: [{ type: "text", text: `Progress update accepted for supervisor ${metadata.orchestratorTarget}. Queued for the next tool boundary; broker acceptance does not confirm the supervisor has read or acted on it.` }],
|
|
2186
|
-
isError: false,
|
|
2187
|
-
details: { messageId: result.id, accepted: result.accepted, delivered: result.delivered, queued: result.queued === true },
|
|
2188
|
-
};
|
|
2189
|
-
}
|
|
2190
|
-
catch (error) {
|
|
2191
|
-
return {
|
|
2192
|
-
content: [{ type: "text", text: `Failed to send progress update: ${getErrorMessage(error)}` }],
|
|
2193
|
-
isError: true,
|
|
2194
|
-
details: { error: true },
|
|
2195
|
-
};
|
|
2196
|
-
}
|
|
2197
|
-
})());
|
|
2198
|
-
},
|
|
2199
|
-
renderCall(args, theme) {
|
|
2200
|
-
const reason = typeof args.reason === "string" ? args.reason : "contact";
|
|
2201
|
-
const messagePreview = previewText(args.message, 96);
|
|
2202
|
-
const interview = args.interview && typeof args.interview === "object" ? args.interview : undefined;
|
|
2203
|
-
let text = theme.fg("toolTitle", theme.bold("contact_supervisor "));
|
|
2204
|
-
text += theme.fg(reason === "need_decision" ? "warning" : reason === "progress_update" ? "muted" : "accent", reason);
|
|
2205
|
-
if (typeof interview?.title === "string" && interview.title.trim()) {
|
|
2206
|
-
text += " " + theme.fg("accent", interview.title.trim());
|
|
2207
|
-
}
|
|
2208
|
-
if (messagePreview) {
|
|
2209
|
-
text += "\n " + theme.fg("dim", messagePreview);
|
|
2210
|
-
}
|
|
2211
|
-
return new Text(text, 0, 0);
|
|
2212
|
-
},
|
|
2213
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
2214
|
-
if (isPartial) {
|
|
2215
|
-
return new Text(theme.fg("warning", "Waiting for supervisor..."), 0, 0);
|
|
2216
|
-
}
|
|
2217
|
-
const details = result.details;
|
|
2218
|
-
const textContent = firstTextContent(result);
|
|
2219
|
-
const failed = Boolean(context.isError || details?.error === true || details?.accepted === false);
|
|
2220
|
-
const parseWarning = typeof details?.structuredReplyParseError === "string";
|
|
2221
|
-
let text = failed
|
|
2222
|
-
? theme.fg("error", "✗ ")
|
|
2223
|
-
: parseWarning
|
|
2224
|
-
? theme.fg("warning", "⚠ ")
|
|
2225
|
-
: theme.fg("success", "✓ ");
|
|
2226
|
-
text += theme.fg(failed ? "error" : "text", textContent);
|
|
2227
|
-
if (parseWarning) {
|
|
2228
|
-
text += "\n" + theme.fg("warning", `Structured reply parse issue: ${details.structuredReplyParseError}`);
|
|
2229
|
-
}
|
|
2230
|
-
return new Text(text, 0, 0);
|
|
2231
|
-
},
|
|
2232
|
-
});
|
|
2233
|
-
}
|
|
2234
|
-
pi.registerTool({
|
|
2235
|
-
name: "load_intercom",
|
|
2236
|
-
label: "Load Intercom",
|
|
2237
|
-
description: "Enable local peer coordination: list/check sessions before shared-state work, send or reply to messages, and inspect/publish topics. Does not send a message or check peers itself.",
|
|
2238
|
-
parameters: Type.Object({}),
|
|
2239
|
-
async execute() {
|
|
2240
|
-
if (!pi.getAllTools().some((tool) => tool.name === "intercom" && !("namespace" in tool && tool.namespace)))
|
|
2241
|
-
throw new Error("Intercom is excluded from this session.");
|
|
2242
|
-
activateTools(pi, ["intercom"]);
|
|
2243
|
-
return { content: [{ type: "text", text: "Intercom enabled. Use intercom({action:'list'}) to check peers, or status, pending, reply, send, ask and topics for coordination." }], details: {} };
|
|
2244
|
-
},
|
|
2245
|
-
});
|
|
2246
|
-
pi.registerTool({
|
|
2247
|
-
name: "intercom",
|
|
2248
|
-
defaultActive: false,
|
|
2249
|
-
label: "Intercom",
|
|
2250
|
-
description: `Send a message to another pi session running on this machine.
|
|
2251
|
-
Use this to communicate findings, request help, or coordinate work with other sessions.
|
|
2252
|
-
Non-blocking send defaults to steer for guidance, answers, corrections, or blockers that may affect active work. Use queue only when delay is intentional; use ask only when this process must remain alive waiting for the reply.
|
|
2253
|
-
|
|
2254
|
-
Usage:
|
|
2255
|
-
intercom({ action: "list" }) → List sessions in this repository and its worktrees
|
|
2256
|
-
intercom({ action: "list", scope: "all" }) → List connected sessions across every project
|
|
2257
|
-
intercom({ action: "send", to: "session-name", message: "..." }) → Send live coordination (defaults to steer)
|
|
2258
|
-
intercom({ action: "ask", to: "session-name", delivery: "steer", message: "..." }) → Blocking wait only when sender must stay alive
|
|
2259
|
-
intercom({ action: "reply", message: "..." }) → Reply to the active/single pending ask
|
|
2260
|
-
intercom({ action: "pending" }) → List unresolved inbound asks
|
|
2261
|
-
intercom({ action: "status" }) → Show connection status
|
|
2262
|
-
intercom({ action: "subscribe", topic: "project/work", awaitRelease: true }) → Opt into a topic
|
|
2263
|
-
intercom({ action: "publish", topic: "project/work", message: "Current self-contained state" }) → Quiet latest state
|
|
2264
|
-
intercom({ action: "topics" }) → Inspect topics and current resource owners`,
|
|
2265
|
-
promptSnippet: "Coordinate with local Pi sessions. Non-blocking send defaults to steer for live agent guidance; queue only for intentional delay and ask only for a required blocking reply.",
|
|
2266
|
-
promptGuidelines: [
|
|
2267
|
-
"Intercom list/status defaults to the current Git repository and its worktrees. Use scope='all' only when intentionally discovering sessions in other projects.",
|
|
2268
|
-
"Action='send' defaults to delivery='steer' for agent-to-agent guidance, answers, corrections, blockers, or other context that may affect active work.",
|
|
2269
|
-
"Use delivery='queue' only when delay is intentional. For routine status, explicitly subscribe/publish topics: updates replace quiet inspectable state outside model context, not passive conversation messages. Topic blockers/decisions and awaited ownership releases interrupt; direct messages always bypass subscriptions.",
|
|
2270
|
-
"Treat inbound steered messages as supplemental coordination within the active task: incorporate relevant context and continue; replace the task only when the message explicitly says so.",
|
|
2271
|
-
"Use action='reply' for an active inbound ask. Otherwise respond with send plus steer; use blocking ask only when this process must stay alive and cannot safely continue without the answer.",
|
|
2272
|
-
],
|
|
2273
|
-
parameters: Type.Object({
|
|
2274
|
-
action: StringEnum(["list", "send", "ask", "reply", "pending", "status", "subscribe", "unsubscribe", "publish", "topics"], {
|
|
2275
|
-
description: "Direct messages: list/send/ask/reply/pending/status. Quiet state: subscribe/unsubscribe/publish/topics.",
|
|
2276
|
-
}),
|
|
2277
|
-
scope: Type.Optional(StringEnum(["project", "all"], {
|
|
2278
|
-
description: "For list/status: 'project' (default) shows this Git repository and its worktrees; 'all' includes sessions in other projects.",
|
|
2279
|
-
})),
|
|
2280
|
-
to: Type.Optional(Type.String({
|
|
2281
|
-
description: "Target session name or ID (for 'send', 'ask', or disambiguating 'reply')",
|
|
2282
|
-
})),
|
|
2283
|
-
topic: Type.Optional(Type.String({ description: "Exact opt-in topic for subscribe/unsubscribe/publish/topics; no automatic subscriptions." })),
|
|
2284
|
-
event: Type.Optional(StringEnum(["update", "blocker", "decision", "release"], { description: "Publish: update is quiet current state. Blockers/decisions interrupt subscribers; release interrupts only those awaiting release." })),
|
|
2285
|
-
resource: Type.Optional(Type.String({ description: "Publish: resource this session is using (advisory ownership, not a lock)." })),
|
|
2286
|
-
ownership: Type.Optional(StringEnum(["held", "released"], { description: "Publish: this session's declared resource state. Disconnect never implies released." })),
|
|
2287
|
-
awaitRelease: Type.Optional(Type.Boolean({ description: "Subscribe: interrupt when this topic explicitly releases ownership." })),
|
|
2288
|
-
message: Type.Optional(Type.String({
|
|
2289
|
-
description: "Message for send/ask/reply, or self-contained latest state for publish",
|
|
2290
|
-
})),
|
|
2291
|
-
attachments: Type.Optional(Type.Array(Type.Object({
|
|
2292
|
-
type: StringEnum(["file", "snippet", "context"]),
|
|
2293
|
-
name: Type.String(),
|
|
2294
|
-
content: Type.String(),
|
|
2295
|
-
language: Type.Optional(Type.String()),
|
|
2296
|
-
}))),
|
|
2297
|
-
replyTo: Type.Optional(Type.String({
|
|
2298
|
-
description: "Message ID to reply to (for threading or responding to an 'ask')",
|
|
2299
|
-
})),
|
|
2300
|
-
delivery: Type.Optional(StringEnum(["queue", "steer", "passive"], {
|
|
2301
|
-
description: "Delivery mode. Omitted send delivery defaults to 'steer', which injects after the current tool call. Use 'queue' only to intentionally wait behind active work; 'passive' does not wake the recipient model.",
|
|
2302
|
-
})),
|
|
2303
|
-
queueMode: Type.Optional(StringEnum(["stack", "replace"], {
|
|
2304
|
-
description: "For delivery='queue': 'stack' keeps all messages; 'replace' keeps only the latest undelivered message for the same threadId.",
|
|
2305
|
-
})),
|
|
2306
|
-
threadId: Type.Optional(Type.String({
|
|
2307
|
-
description: "Stable topic key for queueMode='replace'.",
|
|
2308
|
-
})),
|
|
2309
|
-
passive: Type.Optional(Type.Boolean({
|
|
2310
|
-
description: "For action='send' only: legacy alias for delivery='passive'. Discouraged for agent-to-agent messages.",
|
|
2311
|
-
})),
|
|
2312
|
-
}),
|
|
2313
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2314
|
-
return throwIfToolError(await (async () => {
|
|
2315
|
-
let connectedClient;
|
|
2316
|
-
try {
|
|
2317
|
-
connectedClient = await ensureConnected("tool");
|
|
2318
|
-
}
|
|
2319
|
-
catch (error) {
|
|
2320
|
-
if (params.action === "topics") {
|
|
2321
|
-
topics.disconnected();
|
|
2322
|
-
return { content: [{ type: "text", text: `Broker unavailable; last saved records follow.\n${topics.inspect(params.topic)}` }], details: { connection: "disconnected" } };
|
|
2323
|
-
}
|
|
2324
|
-
return {
|
|
2325
|
-
content: [{ type: "text", text: `Intercom not connected: ${getErrorMessage(error)}` }],
|
|
2326
|
-
isError: true,
|
|
2327
|
-
details: { error: true },
|
|
2328
|
-
};
|
|
2329
|
-
}
|
|
2330
|
-
syncPresenceIdentity(ctx.sessionManager.getSessionId());
|
|
2331
|
-
const { action, scope = "project", to, message, attachments, replyTo, delivery, queueMode, threadId, passive } = params;
|
|
2332
|
-
if (["subscribe", "unsubscribe", "publish", "topics"].includes(action)) {
|
|
2333
|
-
if (!connectedClient.supportsTopics) {
|
|
2334
|
-
topics.disconnected();
|
|
2335
|
-
return { content: [{ type: "text", text: `${TOPICS_UNAVAILABLE}${action === "topics" ? `\n\nLast saved records:\n${topics.inspect(params.topic)}` : ""}` }], isError: true, details: { topicsSupported: false } };
|
|
2336
|
-
}
|
|
2337
|
-
const generation = runtimeGeneration;
|
|
2338
|
-
const topic = params.topic?.trim();
|
|
2339
|
-
if (topic && !isTopicSubscription({ topic, awaitRelease: params.awaitRelease }))
|
|
2340
|
-
return { content: [{ type: "text", text: "Topic must be a plain nonempty label; awaitRelease must be boolean." }], isError: true, details: {} };
|
|
2341
|
-
if (action !== "topics" && !topic)
|
|
2342
|
-
return { content: [{ type: "text", text: `${action} requires an exact topic.` }], isError: true, details: {} };
|
|
2343
|
-
let change;
|
|
2344
|
-
if (action === "subscribe")
|
|
2345
|
-
change = { action: "subscribe", subscription: { topic: topic, ...(params.awaitRelease ? { awaitRelease: true } : {}) } };
|
|
2346
|
-
if (action === "unsubscribe")
|
|
2347
|
-
change = { action: "unsubscribe", topic: topic };
|
|
2348
|
-
if (action === "publish") {
|
|
2349
|
-
const event = params.event ?? (params.ownership === "released" ? "release" : "update");
|
|
2350
|
-
const update = { topic: topic, text: message ?? "", event, resource: params.resource, ownership: params.ownership,
|
|
2351
|
-
revision: (topics.published.get(topic)?.revision ?? 0) + 1, updatedAt: Date.now() };
|
|
2352
|
-
if (!update.text.trim() || !isTopicUpdate(update) || (event === "release" && (!update.resource || update.ownership !== "released")))
|
|
2353
|
-
return { content: [{ type: "text", text: "Publish requires self-contained message text; ownership needs resource, and release needs ownership:'released'." }], isError: true, details: {} };
|
|
2354
|
-
change = { action: "publish", topic: update };
|
|
2355
|
-
}
|
|
2356
|
-
const commit = (snapshot) => {
|
|
2357
|
-
if (!getLiveContext(ctx, generation))
|
|
2358
|
-
throw new Error("Session changed; no topic state saved in this session.");
|
|
2359
|
-
if (action === "subscribe")
|
|
2360
|
-
topics.subscribe(topic, params.awaitRelease);
|
|
2361
|
-
if (action === "unsubscribe")
|
|
2362
|
-
topics.unsubscribe(topic);
|
|
2363
|
-
if (action === "publish") {
|
|
2364
|
-
const update = snapshot.sessions.find((session) => session.id === connectedClient.sessionId)?.topics?.find((entry) => entry.topic === topic);
|
|
2365
|
-
if (!update)
|
|
2366
|
-
throw new Error("Broker did not confirm the topic publication; prior saved state is unchanged.");
|
|
2367
|
-
topics.publish(update, { id: connectedClient.sessionId, name: pi.getSessionName() });
|
|
2368
|
-
}
|
|
2369
|
-
topics.refresh(snapshot.sessions);
|
|
2370
|
-
};
|
|
2371
|
-
const snapshot = change ? await connectedClient.updateTopics(change, commit) : { sessions: await connectedClient.listSessions(), receipts: [] };
|
|
2372
|
-
if (!getLiveContext(ctx, generation))
|
|
2373
|
-
return { content: [{ type: "text", text: "Session changed; no topic state saved in this session." }], isError: true, details: {} };
|
|
2374
|
-
if (!change)
|
|
2375
|
-
topics.refresh(snapshot.sessions);
|
|
2376
|
-
if (topicSyncError) {
|
|
2377
|
-
const own = snapshot.sessions.find((session) => session.id === connectedClient.sessionId);
|
|
2378
|
-
const published = new Map(own?.topics?.map((update) => [update.topic, update]));
|
|
2379
|
-
const subscribed = new Map(own?.subscriptions?.map((entry) => [entry.topic, entry]));
|
|
2380
|
-
if (own && published.size === topics.published.size && subscribed.size === topics.subscriptions.size
|
|
2381
|
-
&& [...topics.published].every(([key, update]) => JSON.stringify(published.get(key)) === JSON.stringify(update))
|
|
2382
|
-
&& [...topics.subscriptions].every(([key, entry]) => subscribed.has(key) && Boolean(subscribed.get(key)?.awaitRelease) === Boolean(entry.awaitRelease)))
|
|
2383
|
-
topicSyncError = undefined;
|
|
2384
|
-
else
|
|
2385
|
-
topics.disconnected(connectedClient.sessionId);
|
|
2386
|
-
}
|
|
2387
|
-
if (action === "publish") {
|
|
2388
|
-
const receipts = snapshot.receipts;
|
|
2389
|
-
return { content: [{ type: "text", text: `Current state saved for ${topic}. ${receipts.filter((receipt) => receipt.accepted).length}/${receipts.length} subscribed deliveries accepted; this does not confirm reading or action. Routine updates stay outside conversation context.` }], details: { topic, receipts } };
|
|
2390
|
-
}
|
|
2391
|
-
return { content: [{ type: "text", text: `${topicSyncError ? `Some saved topic state could not be restored: ${topicSyncError}\nOrdinary messaging remains available. Correct the saved publication or subscription.\n\n` : ""}${action === "topics" ? "" : `${action === "subscribe" ? "Subscribed to" : "Unsubscribed from"} ${topic}.\n`}${topics.inspect(topic)}` }], details: { topic } };
|
|
2392
|
-
}
|
|
2393
|
-
if (params.scope !== undefined && action !== "list" && action !== "status") {
|
|
2394
|
-
return {
|
|
2395
|
-
content: [{ type: "text", text: "'scope' is only valid for action='list' or action='status'" }],
|
|
2396
|
-
isError: true,
|
|
2397
|
-
details: { error: true },
|
|
2398
|
-
};
|
|
2399
|
-
}
|
|
2400
|
-
if (passive !== undefined && action !== "send") {
|
|
2401
|
-
return {
|
|
2402
|
-
content: [{ type: "text", text: "'passive' is only valid for action='send'" }],
|
|
2403
|
-
isError: true,
|
|
2404
|
-
details: { error: true },
|
|
2405
|
-
};
|
|
2406
|
-
}
|
|
2407
|
-
if (delivery === "passive" && action !== "send") {
|
|
2408
|
-
return {
|
|
2409
|
-
content: [{ type: "text", text: "delivery='passive' is only valid for action='send'. Passive delivery is for human-visible breadcrumbs and is discouraged for agent-to-agent coordination; normal send defaults to steer. Use ask with delivery='steer' only when the sender must stay alive and cannot safely continue without the reply." }],
|
|
2410
|
-
isError: true,
|
|
2411
|
-
details: { error: true },
|
|
2412
|
-
};
|
|
2413
|
-
}
|
|
2414
|
-
if ((delivery !== undefined || queueMode !== undefined || threadId !== undefined) && action !== "send" && action !== "ask") {
|
|
2415
|
-
return {
|
|
2416
|
-
content: [{ type: "text", text: "'delivery', 'queueMode', and 'threadId' are only valid for action='send' or action='ask'" }],
|
|
2417
|
-
isError: true,
|
|
2418
|
-
details: failureDetails(queueMode !== undefined || delivery === "queue" || threadId !== undefined ? "invalid_queue_arguments" : "invalid_delivery_arguments", [{ action: "send", guidance: "Use delivery options only with send or ask." }], { error: true }),
|
|
2419
|
-
};
|
|
2420
|
-
}
|
|
2421
|
-
if (passive === true && delivery !== undefined && delivery !== "passive") {
|
|
2422
|
-
return {
|
|
2423
|
-
content: [{ type: "text", text: "'passive' cannot be combined with a non-passive delivery mode" }],
|
|
2424
|
-
isError: true,
|
|
2425
|
-
details: { error: true },
|
|
2426
|
-
};
|
|
2427
|
-
}
|
|
2428
|
-
if (threadId !== undefined && queueMode !== "replace") {
|
|
2429
|
-
return {
|
|
2430
|
-
content: [{ type: "text", text: "'threadId' is only valid with queueMode='replace'" }],
|
|
2431
|
-
isError: true,
|
|
2432
|
-
details: failureDetails("invalid_queue_arguments", [{ action: "send", guidance: "Use threadId only with delivery='queue' and queueMode='replace'." }], { error: true }),
|
|
2433
|
-
};
|
|
2434
|
-
}
|
|
2435
|
-
if (queueMode === "replace" && (!threadId || !threadId.trim())) {
|
|
2436
|
-
return {
|
|
2437
|
-
content: [{ type: "text", text: "queueMode='replace' requires a non-empty threadId" }],
|
|
2438
|
-
isError: true,
|
|
2439
|
-
details: failureDetails("invalid_queue_arguments", [{ action: "send", guidance: "Provide a non-empty threadId with delivery='queue' and queueMode='replace'." }], { error: true }),
|
|
2440
|
-
};
|
|
2441
|
-
}
|
|
2442
|
-
const deliveryMode = (passive === true ? "passive" : delivery);
|
|
2443
|
-
const cleanedThreadId = typeof threadId === "string" ? threadId.trim() : undefined;
|
|
2444
|
-
if (queueMode !== undefined && deliveryMode !== "queue") {
|
|
2445
|
-
return {
|
|
2446
|
-
content: [{ type: "text", text: "'queueMode' is only valid with delivery='queue'. Use queue only for intentionally deferred work; otherwise omit queueMode and use default-steered send for live agent coordination. Avoid passive for agent-to-agent coordination." }],
|
|
2447
|
-
isError: true,
|
|
2448
|
-
details: failureDetails("invalid_queue_arguments", [{ action: "send", guidance: "Set delivery='queue', or omit queueMode and threadId." }], { error: true }),
|
|
2449
|
-
};
|
|
2450
|
-
}
|
|
2451
|
-
switch (action) {
|
|
2452
|
-
case "list": {
|
|
2453
|
-
try {
|
|
2454
|
-
const mySessionId = connectedClient.sessionId;
|
|
2455
|
-
if (!mySessionId)
|
|
2456
|
-
throw new Error("Current intercom session id is unavailable.");
|
|
2457
|
-
const allSessions = await connectedClient.listSessions();
|
|
2458
|
-
const sessions = sessionsForScope(allSessions, mySessionId, scope);
|
|
2459
|
-
return {
|
|
2460
|
-
content: [{ type: "text", text: formatSessionListSections(allSessions, mySessionId, scope) }],
|
|
2461
|
-
isError: false,
|
|
2462
|
-
details: { sessionCount: sessions.length },
|
|
2463
|
-
};
|
|
2464
|
-
}
|
|
2465
|
-
catch (error) {
|
|
2466
|
-
return {
|
|
2467
|
-
content: [{ type: "text", text: `Failed to list sessions: ${getErrorMessage(error)}` }],
|
|
2468
|
-
isError: true,
|
|
2469
|
-
details: { error: true },
|
|
2470
|
-
};
|
|
2471
|
-
}
|
|
2472
|
-
}
|
|
2473
|
-
case "send": {
|
|
2474
|
-
if (!to || !message) {
|
|
2475
|
-
return {
|
|
2476
|
-
content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }],
|
|
2477
|
-
isError: true,
|
|
2478
|
-
details: { error: true },
|
|
2479
|
-
};
|
|
2480
|
-
}
|
|
2481
|
-
try {
|
|
2482
|
-
const sendTo = await resolveSessionTarget(connectedClient, to) ?? to;
|
|
2483
|
-
if (sendTo === connectedClient.sessionId) {
|
|
2484
|
-
return {
|
|
2485
|
-
content: [{ type: "text", text: "Cannot message the current session" }],
|
|
2486
|
-
isError: true,
|
|
2487
|
-
details: { error: true },
|
|
2488
|
-
};
|
|
2489
|
-
}
|
|
2490
|
-
if (!replyTo && config.confirmSend && ctx.hasUI) {
|
|
2491
|
-
const attachmentText = attachments?.length ? formatAttachments(attachments) : "";
|
|
2492
|
-
const confirmed = await ctx.ui.confirm("Send Message", `Send to "${to}":\n\n${message}${attachmentText}`);
|
|
2493
|
-
if (!confirmed) {
|
|
2494
|
-
return {
|
|
2495
|
-
content: [{ type: "text", text: "Message cancelled by user" }],
|
|
2496
|
-
isError: false,
|
|
2497
|
-
};
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
const result = await connectedClient.send(sendTo, {
|
|
2501
|
-
text: message,
|
|
2502
|
-
attachments,
|
|
2503
|
-
replyTo,
|
|
2504
|
-
delivery: deliveryMode,
|
|
2505
|
-
queueMode: queueMode,
|
|
2506
|
-
threadId: cleanedThreadId,
|
|
2507
|
-
passive: passive === true,
|
|
2508
|
-
});
|
|
2509
|
-
if (!result.accepted) {
|
|
2510
|
-
const errorText = result.reason ?? "Session may not exist or has disconnected.";
|
|
2511
|
-
return {
|
|
2512
|
-
content: [{ type: "text", text: `Message to "${to}" was not delivered: ${errorText}` }],
|
|
2513
|
-
isError: true,
|
|
2514
|
-
details: failureDetails("delivery_failed", [{ action: "list" }, { action: "send", guidance: "Retry with an exact active recipient target." }], { messageId: result.id, accepted: result.accepted, delivered: false, reason: result.reason }),
|
|
2515
|
-
};
|
|
2516
|
-
}
|
|
2517
|
-
markIntercomActivity();
|
|
2518
|
-
pi.appendEntry("intercom_sent", {
|
|
2519
|
-
to,
|
|
2520
|
-
message: { text: message, attachments, replyTo, delivery: deliveryMode, queueMode, threadId: cleanedThreadId, passive: passive === true },
|
|
2521
|
-
messageId: result.id,
|
|
2522
|
-
timestamp: Date.now(),
|
|
2523
|
-
});
|
|
2524
|
-
if (replyTo) {
|
|
2525
|
-
replyTracker.markReplied(replyTo);
|
|
2526
|
-
}
|
|
2527
|
-
syncPresenceStatus();
|
|
2528
|
-
const replyModeHint = replyTo
|
|
2529
|
-
? ""
|
|
2530
|
-
: deliveryMode === "passive"
|
|
2531
|
-
? " (passive; recipient model was not woken)"
|
|
2532
|
-
: deliveryMode === "steer"
|
|
2533
|
-
? " (steers active recipient after the current tool call)"
|
|
2534
|
-
: deliveryMode === "queue"
|
|
2535
|
-
? " (intentionally deferred behind active recipient work)"
|
|
2536
|
-
: " (defaults to steer; wakes idle recipients and steers active recipients after the current tool call)";
|
|
2537
|
-
return {
|
|
2538
|
-
content: [{ type: "text", text: result.queued ? `Message queued for ${to} (${result.reason ?? "queued"})` : `Message sent to ${to}${replyModeHint}` }],
|
|
2539
|
-
isError: false,
|
|
2540
|
-
details: { messageId: result.id, accepted: result.accepted, delivered: result.delivered, queued: result.queued === true, reasonCode: result.queued ? "message_queued" : "message_accepted" },
|
|
2541
|
-
};
|
|
2542
|
-
}
|
|
2543
|
-
catch (error) {
|
|
2544
|
-
return {
|
|
2545
|
-
content: [{ type: "text", text: `Failed to send: ${getErrorMessage(error)}` }],
|
|
2546
|
-
isError: true,
|
|
2547
|
-
details: failureDetails(getErrorMessage(error).startsWith("Target ") ? "ambiguous_target" : "send_failed", [{ action: "list" }, { action: "send", guidance: "Retry with an exact active recipient target." }], { error: true }),
|
|
2548
|
-
};
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
case "ask": {
|
|
2552
|
-
if (!to || !message) {
|
|
2553
|
-
return {
|
|
2554
|
-
content: [{ type: "text", text: "Missing 'to' or 'message' parameter" }],
|
|
2555
|
-
isError: true,
|
|
2556
|
-
details: { error: true },
|
|
2557
|
-
};
|
|
2558
|
-
}
|
|
2559
|
-
if (replyWaiter) {
|
|
2560
|
-
return {
|
|
2561
|
-
content: [{ type: "text", text: "Already waiting for a reply" }],
|
|
2562
|
-
isError: true,
|
|
2563
|
-
details: { error: true },
|
|
2564
|
-
};
|
|
2565
|
-
}
|
|
2566
|
-
if (_signal?.aborted) {
|
|
2567
|
-
return {
|
|
2568
|
-
content: [{ type: "text", text: "Cancelled" }],
|
|
2569
|
-
isError: true,
|
|
2570
|
-
details: { error: true },
|
|
2571
|
-
};
|
|
2572
|
-
}
|
|
2573
|
-
let questionId;
|
|
2574
|
-
try {
|
|
2575
|
-
const sendTo = await resolveSessionTarget(connectedClient, to) ?? to;
|
|
2576
|
-
if (_signal?.aborted) {
|
|
2577
|
-
return {
|
|
2578
|
-
content: [{ type: "text", text: "Cancelled" }],
|
|
2579
|
-
isError: true,
|
|
2580
|
-
details: { error: true },
|
|
2581
|
-
};
|
|
2582
|
-
}
|
|
2583
|
-
if (sendTo === connectedClient.sessionId) {
|
|
2584
|
-
return {
|
|
2585
|
-
content: [{ type: "text", text: "Cannot message the current session" }],
|
|
2586
|
-
isError: true,
|
|
2587
|
-
details: { error: true },
|
|
2588
|
-
};
|
|
2589
|
-
}
|
|
2590
|
-
const peerHealth = await resolvePeerHealth(connectedClient, sendTo);
|
|
2591
|
-
const skipReplyWait = peerDeclinesAsks(peerHealth) && deliveryMode === undefined;
|
|
2592
|
-
if (_signal?.aborted) {
|
|
2593
|
-
return {
|
|
2594
|
-
content: [{ type: "text", text: "Cancelled" }],
|
|
2595
|
-
isError: true,
|
|
2596
|
-
details: { error: true },
|
|
2597
|
-
};
|
|
2598
|
-
}
|
|
2599
|
-
questionId = randomUUID();
|
|
2600
|
-
const sendOptions = {
|
|
2601
|
-
text: message,
|
|
2602
|
-
attachments,
|
|
2603
|
-
replyTo,
|
|
2604
|
-
delivery: deliveryMode,
|
|
2605
|
-
queueMode: queueMode,
|
|
2606
|
-
threadId: cleanedThreadId,
|
|
2607
|
-
};
|
|
2608
|
-
const recordSent = (sendResult) => pi.appendEntry("intercom_sent", {
|
|
2609
|
-
to,
|
|
2610
|
-
message: { text: message, attachments, replyTo, delivery: deliveryMode, queueMode, threadId: cleanedThreadId },
|
|
2611
|
-
messageId: sendResult.id,
|
|
2612
|
-
timestamp: Date.now(),
|
|
2613
|
-
});
|
|
2614
|
-
let replyMessage;
|
|
2615
|
-
if (skipReplyWait) {
|
|
2616
|
-
const sendResult = await connectedClient.send(sendTo, { ...sendOptions, messageId: questionId, expectsReply: true });
|
|
2617
|
-
if (!sendResult.accepted)
|
|
2618
|
-
throw new AskDeliveryError(sendResult);
|
|
2619
|
-
markIntercomActivity();
|
|
2620
|
-
syncPresenceStatus();
|
|
2621
|
-
recordSent(sendResult);
|
|
2622
|
-
return {
|
|
2623
|
-
content: [{ type: "text", text: `${sendResult.delivered ? "Delivered" : "Queued"} ask to ${to}; peer is busy and not accepting blocking asks (peer_busy). Not waiting for a reply; delivery does not mean the question was consumed.` }],
|
|
2624
|
-
isError: false,
|
|
2625
|
-
details: { messageId: sendResult.id, accepted: sendResult.accepted, delivered: sendResult.delivered, queued: sendResult.queued === true, replied: false, reason: "peer_busy", reasonCode: "recipient_not_accepting_asks", nextActions: [{ action: "send", guidance: "Use default-steered send for non-blocking live coordination; queue only when delay is intentional." }] },
|
|
2626
|
-
};
|
|
2627
|
-
}
|
|
2628
|
-
replyMessage = await sendAskTransaction(connectedClient, sendTo, questionId, sendOptions, _signal, recordSent);
|
|
2629
|
-
const replyText = replyMessage.content.text;
|
|
2630
|
-
const replyAttachments = replyMessage.content.attachments?.length
|
|
2631
|
-
? formatAttachments(replyMessage.content.attachments)
|
|
2632
|
-
: "";
|
|
2633
|
-
pi.appendEntry("intercom_received", {
|
|
2634
|
-
from: to,
|
|
2635
|
-
message: { text: replyText, attachments: replyMessage.content.attachments },
|
|
2636
|
-
messageId: replyMessage.id,
|
|
2637
|
-
timestamp: replyMessage.timestamp,
|
|
2638
|
-
});
|
|
2639
|
-
const recipientTurnFailure = replyMessage.content.attachments?.some((attachment) => attachment.name === RECIPIENT_TURN_FAILED_ATTACHMENT);
|
|
2640
|
-
if (recipientTurnFailure) {
|
|
2641
|
-
return {
|
|
2642
|
-
content: [{ type: "text", text: replyText }],
|
|
2643
|
-
isError: true,
|
|
2644
|
-
details: failureDetails("recipient_turn_failed", [{ action: "status" }, { action: "send", guidance: "Send recovery context after the recipient is healthy." }], { error: true, recipientTurnFailed: true, messageId: replyMessage.id, replyTo: questionId }),
|
|
2645
|
-
};
|
|
2646
|
-
}
|
|
2647
|
-
return {
|
|
2648
|
-
content: [{ type: "text", text: `**Reply from ${to}:**\n${replyText}${replyAttachments}` }],
|
|
2649
|
-
isError: false,
|
|
2650
|
-
};
|
|
2651
|
-
}
|
|
2652
|
-
catch (error) {
|
|
2653
|
-
if (error instanceof AskDeliveryError) {
|
|
2654
|
-
return {
|
|
2655
|
-
content: [{ type: "text", text: `Message to "${to}" was not delivered: ${error.message}` }],
|
|
2656
|
-
isError: true,
|
|
2657
|
-
details: failureDetails("delivery_failed", [{ action: "list" }, { action: "send", guidance: "Retry with an exact active recipient target." }], { error: true, messageId: error.result.id, accepted: false, delivered: false, reason: error.result.reason }),
|
|
2658
|
-
};
|
|
2659
|
-
}
|
|
2660
|
-
const errorMessage = getErrorMessage(error);
|
|
2661
|
-
const timedOut = errorMessage.startsWith("No reply from");
|
|
2662
|
-
return {
|
|
2663
|
-
content: [{ type: "text", text: `Failed: ${errorMessage}` }],
|
|
2664
|
-
isError: true,
|
|
2665
|
-
details: failureDetails(timedOut ? "reply_timeout" : errorMessage.startsWith("Target ") ? "ambiguous_target" : "ask_failed", [{ action: "status" }, { action: "list" }, { action: "send", guidance: "Check the recipient before retrying; the original ask may still be seen." }], { error: true, ...(questionId ? { messageId: questionId } : {}) }),
|
|
2666
|
-
};
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
case "reply": {
|
|
2670
|
-
if (!message) {
|
|
2671
|
-
return {
|
|
2672
|
-
content: [{ type: "text", text: "Missing 'message' parameter" }],
|
|
2673
|
-
isError: true,
|
|
2674
|
-
details: { error: true },
|
|
2675
|
-
};
|
|
2676
|
-
}
|
|
2677
|
-
try {
|
|
2678
|
-
const target = replyTracker.resolveReplyTarget({ to, replyTo });
|
|
2679
|
-
if (target.from.id === connectedClient.sessionId) {
|
|
2680
|
-
return {
|
|
2681
|
-
content: [{ type: "text", text: "Cannot message the current session" }],
|
|
2682
|
-
isError: true,
|
|
2683
|
-
details: { error: true },
|
|
2684
|
-
};
|
|
2685
|
-
}
|
|
2686
|
-
const result = await connectedClient.send(target.from.id, {
|
|
2687
|
-
text: message,
|
|
2688
|
-
attachments,
|
|
2689
|
-
replyTo: target.message.id,
|
|
2690
|
-
});
|
|
2691
|
-
if (!result.accepted) {
|
|
2692
|
-
const errorText = result.reason ?? "Session may not exist or has disconnected.";
|
|
2693
|
-
return {
|
|
2694
|
-
content: [{ type: "text", text: `Reply to "${target.from.name || target.from.id}" was not delivered: ${errorText}` }],
|
|
2695
|
-
isError: true,
|
|
2696
|
-
details: failureDetails("delivery_failed", [{ action: "pending" }, { action: "send", guidance: "Send a new message if the original sender is no longer available." }], { messageId: result.id, accepted: result.accepted, delivered: false, queued: result.queued === true, reason: result.reason, replyTo: target.message.id }),
|
|
2697
|
-
};
|
|
2698
|
-
}
|
|
2699
|
-
replyTracker.markReplied(target.message.id);
|
|
2700
|
-
markIntercomActivity();
|
|
2701
|
-
syncPresenceStatus();
|
|
2702
|
-
pi.appendEntry("intercom_sent", {
|
|
2703
|
-
to: target.from.name || target.from.id,
|
|
2704
|
-
message: { text: message, replyTo: target.message.id },
|
|
2705
|
-
messageId: result.id,
|
|
2706
|
-
timestamp: Date.now(),
|
|
2707
|
-
});
|
|
2708
|
-
return {
|
|
2709
|
-
content: [{ type: "text", text: `Reply sent to ${target.from.name || target.from.id}` }],
|
|
2710
|
-
isError: false,
|
|
2711
|
-
details: { messageId: result.id, delivered: true, replyTo: target.message.id, reasonCode: "reply_sent" },
|
|
2712
|
-
};
|
|
2713
|
-
}
|
|
2714
|
-
catch (error) {
|
|
2715
|
-
const errorMessage = getErrorMessage(error);
|
|
2716
|
-
const reasonCode = replyFailureReason(errorMessage);
|
|
2717
|
-
return {
|
|
2718
|
-
content: [{ type: "text", text: `Failed to reply: ${errorMessage}` }],
|
|
2719
|
-
isError: true,
|
|
2720
|
-
details: failureDetails(reasonCode, reasonCode === "no_pending_reply"
|
|
2721
|
-
? [{ action: "pending" }, { action: "send", guidance: "Use send with an explicit recipient when there is no inbound ask to reply to." }]
|
|
2722
|
-
: [{ action: "pending" }, { action: "list" }], { error: true, ...(replyTo ? { replyTo } : {}) }),
|
|
2723
|
-
};
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
case "pending": {
|
|
2727
|
-
const pendingAsks = replyTracker.listPending();
|
|
2728
|
-
if (pendingAsks.length === 0) {
|
|
2729
|
-
return {
|
|
2730
|
-
content: [{ type: "text", text: "No unresolved inbound asks." }],
|
|
2731
|
-
isError: false,
|
|
2732
|
-
};
|
|
2733
|
-
}
|
|
2734
|
-
const now = Date.now();
|
|
2735
|
-
const lines = pendingAsks.map(({ from, message, receivedAt }) => {
|
|
2736
|
-
const preview = pendingAskPreview(message);
|
|
2737
|
-
const elapsedSeconds = Math.max(0, Math.floor((now - receivedAt) / 1000));
|
|
2738
|
-
const sender = from.name ? `${from.name} (${formatSessionTarget(from, pendingAsks.map((ask) => ask.from))})` : from.id;
|
|
2739
|
-
const replyTo = JSON.stringify(message.id);
|
|
2740
|
-
return `- ${sender} · replyTo: ${replyTo} · ${elapsedSeconds}s ago · ${preview}\n Reply: intercom({ action: "reply", replyTo: ${replyTo}, message: "..." })`;
|
|
2741
|
-
});
|
|
2742
|
-
return {
|
|
2743
|
-
content: [{ type: "text", text: `**Pending asks:**\n${lines.join("\n")}` }],
|
|
2744
|
-
isError: false,
|
|
2745
|
-
};
|
|
2746
|
-
}
|
|
2747
|
-
case "status": {
|
|
2748
|
-
try {
|
|
2749
|
-
const mySessionId = connectedClient.sessionId;
|
|
2750
|
-
if (!mySessionId)
|
|
2751
|
-
throw new Error("Current intercom session id is unavailable.");
|
|
2752
|
-
const allSessions = await connectedClient.listSessions();
|
|
2753
|
-
const sessions = sessionsForScope(allSessions, mySessionId, scope);
|
|
2754
|
-
reconcileConsumedInbound(ctx);
|
|
2755
|
-
const pending = [...pendingInbound.values()].map((entry) => ({
|
|
2756
|
-
entry,
|
|
2757
|
-
state: entry.stage === "queued" ? "queued; not yet delivered to model" : "delivered to model queue; not yet consumed",
|
|
2758
|
-
}));
|
|
2759
|
-
const pendingText = pending.length
|
|
2760
|
-
? `\n\nPending inbound messages: ${pending.length}\n${pending.map(({ entry, state }) => `- ${entry.from.name || entry.from.id} [${entry.message.id}] (${state})\n${entry.bodyText}`).join("\n\n")}`
|
|
2761
|
-
: "\n\nPending inbound messages: 0";
|
|
2762
|
-
return {
|
|
2763
|
-
content: [{
|
|
2764
|
-
type: "text",
|
|
2765
|
-
text: `**Intercom Status:**\nConnected: Yes\nSession ID: ${mySessionId}\nConnected sessions in scope: ${sessions.length}\n\n${formatSessionListSections(allSessions, mySessionId, scope)}${pendingText}`,
|
|
2766
|
-
}],
|
|
2767
|
-
isError: false,
|
|
2768
|
-
};
|
|
2769
|
-
}
|
|
2770
|
-
catch (error) {
|
|
2771
|
-
return {
|
|
2772
|
-
content: [{ type: "text", text: `Failed to get status: ${getErrorMessage(error)}` }],
|
|
2773
|
-
isError: true,
|
|
2774
|
-
details: { error: true },
|
|
2775
|
-
};
|
|
2776
|
-
}
|
|
2777
|
-
}
|
|
2778
|
-
default:
|
|
2779
|
-
return {
|
|
2780
|
-
content: [{ type: "text", text: `Unknown action: ${action}` }],
|
|
2781
|
-
isError: true,
|
|
2782
|
-
details: { error: true },
|
|
2783
|
-
};
|
|
2784
|
-
}
|
|
2785
|
-
})());
|
|
24
|
+
const child = readChildOrchestratorMetadata();
|
|
25
|
+
const lifecycle = new IntercomLifecycle(pi, config);
|
|
26
|
+
const replies = new ReplyTracker(config.askTimeoutMs);
|
|
27
|
+
const topics = new IntercomTopics(pi, () => lifecycle.live());
|
|
28
|
+
const journal = new InboundJournal(pi, lifecycle, replies);
|
|
29
|
+
const wait = new ReplyWait(pi, lifecycle, config.askTimeoutMs);
|
|
30
|
+
// Hooks are invoked only after construction, at a native session or event boundary.
|
|
31
|
+
const connection = new IntercomConnection(lifecycle, config, topics, {
|
|
32
|
+
restoring: () => delivery.restoring,
|
|
33
|
+
pendingAsks: () => replies.listPending().length,
|
|
34
|
+
incoming: (ctx, from, message) => inbound.receive(ctx, from, message),
|
|
35
|
+
peerLeft: (id) => {
|
|
36
|
+
wait.peerDisconnected(id);
|
|
37
|
+
journal.peerLeft(id);
|
|
2786
38
|
},
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
const messagePreview = previewText(args.message, 96);
|
|
2791
|
-
const attachmentCount = Array.isArray(args.attachments) ? args.attachments.length : 0;
|
|
2792
|
-
let text = theme.fg("toolTitle", theme.bold("intercom "));
|
|
2793
|
-
text += theme.fg(action === "ask" ? "warning" : action === "reply" ? "success" : "accent", action);
|
|
2794
|
-
if (target) {
|
|
2795
|
-
text += " " + theme.fg("muted", "→") + " " + theme.fg("accent", target);
|
|
39
|
+
disconnected: (error) => {
|
|
40
|
+
if (!wait.durable) {
|
|
41
|
+
wait.reject(new Error(`Disconnected while waiting for reply: ${error.message}`, { cause: error }));
|
|
2796
42
|
}
|
|
2797
|
-
if (attachmentCount > 0) {
|
|
2798
|
-
text += " " + theme.fg("dim", `(${attachmentCount} attachment${attachmentCount === 1 ? "" : "s"})`);
|
|
2799
|
-
}
|
|
2800
|
-
if (messagePreview) {
|
|
2801
|
-
text += "\n " + theme.fg("dim", messagePreview);
|
|
2802
|
-
}
|
|
2803
|
-
return new Text(text, 0, 0);
|
|
2804
|
-
},
|
|
2805
|
-
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
2806
|
-
if (isPartial) {
|
|
2807
|
-
return new Text(theme.fg("warning", "Intercom working..."), 0, 0);
|
|
2808
|
-
}
|
|
2809
|
-
const details = result.details;
|
|
2810
|
-
const failed = Boolean(context.isError || details?.error === true || details?.accepted === false);
|
|
2811
|
-
const action = context.args && typeof context.args === "object" && "action" in context.args
|
|
2812
|
-
? context.args.action
|
|
2813
|
-
: undefined;
|
|
2814
|
-
if (!failed && action === "list" && !expanded) {
|
|
2815
|
-
const summary = details?.sessionCount === undefined
|
|
2816
|
-
? "Sessions listed"
|
|
2817
|
-
: `${details.sessionCount} session${details.sessionCount === 1 ? "" : "s"}`;
|
|
2818
|
-
const key = keyText("app.tools.expand");
|
|
2819
|
-
return new Text(`${theme.fg("success", "✓ ")}${theme.fg("text", summary)}${key ? ` ${theme.fg("dim", `(${key} to expand)`)}` : ""}`, 0, 0);
|
|
2820
|
-
}
|
|
2821
|
-
let text = failed ? theme.fg("error", "✗ ") : theme.fg("success", "✓ ");
|
|
2822
|
-
text += theme.fg(failed ? "error" : "text", firstTextContent(result));
|
|
2823
|
-
if (details?.messageId && !expanded) {
|
|
2824
|
-
text += theme.fg("dim", ` (${details.messageId.slice(0, 8)})`);
|
|
2825
|
-
}
|
|
2826
|
-
if (details?.reason && expanded) {
|
|
2827
|
-
text += "\n" + theme.fg("dim", `Reason: ${details.reason}`);
|
|
2828
|
-
}
|
|
2829
|
-
return new Text(text, 0, 0);
|
|
2830
|
-
},
|
|
2831
|
-
});
|
|
2832
|
-
async function openIntercomOverlay(ctx, scope = "project") {
|
|
2833
|
-
const overlayGeneration = runtimeGeneration;
|
|
2834
|
-
const liveContext = getLiveContext(ctx, overlayGeneration);
|
|
2835
|
-
if (!liveContext?.hasUI)
|
|
2836
|
-
return;
|
|
2837
|
-
let overlayClient;
|
|
2838
|
-
try {
|
|
2839
|
-
overlayClient = await ensureConnected("overlay");
|
|
2840
|
-
}
|
|
2841
|
-
catch (error) {
|
|
2842
|
-
notifyIfLive(ctx, `Intercom unavailable: ${getErrorMessage(error)}`, "error", overlayGeneration);
|
|
2843
|
-
return;
|
|
2844
|
-
}
|
|
2845
|
-
if (!getLiveContext(ctx, overlayGeneration))
|
|
2846
|
-
return;
|
|
2847
|
-
syncPresenceIdentity(ctx.sessionManager.getSessionId());
|
|
2848
|
-
let currentSession;
|
|
2849
|
-
let sessions;
|
|
2850
|
-
let allSessions;
|
|
2851
|
-
let hiddenSessionCount = 0;
|
|
2852
|
-
try {
|
|
2853
|
-
const mySessionId = overlayClient.sessionId;
|
|
2854
|
-
if (!mySessionId)
|
|
2855
|
-
throw new Error("Current intercom session id is unavailable.");
|
|
2856
|
-
allSessions = await overlayClient.listSessions();
|
|
2857
|
-
if (!getLiveContext(ctx, overlayGeneration))
|
|
2858
|
-
return;
|
|
2859
|
-
const foundCurrentSession = allSessions.find(s => s.id === mySessionId);
|
|
2860
|
-
if (!foundCurrentSession) {
|
|
2861
|
-
notifyIfLive(ctx, "Current session is missing from intercom session list", "error", overlayGeneration);
|
|
2862
|
-
return;
|
|
2863
|
-
}
|
|
2864
|
-
currentSession = foundCurrentSession;
|
|
2865
|
-
const scopedSessions = sessionsForScope(allSessions, mySessionId, scope);
|
|
2866
|
-
hiddenSessionCount = allSessions.length - scopedSessions.length;
|
|
2867
|
-
sessions = scopedSessions.filter(s => s.id !== mySessionId);
|
|
2868
|
-
}
|
|
2869
|
-
catch (error) {
|
|
2870
|
-
notifyIfLive(ctx, `Failed to list sessions: ${getErrorMessage(error)}`, "error", overlayGeneration);
|
|
2871
|
-
return;
|
|
2872
|
-
}
|
|
2873
|
-
const selectedSession = await ctx.ui.custom((tui, theme, keybindings, done) => new SessionListOverlay(tui, theme, keybindings, currentSession, sessions, done, hiddenSessionCount, allSessions), { overlay: true }).catch(() => undefined);
|
|
2874
|
-
if (!selectedSession || !getLiveContext(ctx, overlayGeneration))
|
|
2875
|
-
return;
|
|
2876
|
-
try {
|
|
2877
|
-
overlayClient = await ensureConnected("overlay");
|
|
2878
|
-
}
|
|
2879
|
-
catch (error) {
|
|
2880
|
-
notifyIfLive(ctx, `Intercom unavailable: ${getErrorMessage(error)}`, "error", overlayGeneration);
|
|
2881
|
-
return;
|
|
2882
|
-
}
|
|
2883
|
-
if (!getLiveContext(ctx, overlayGeneration))
|
|
2884
|
-
return;
|
|
2885
|
-
const targetLabel = targetDisplayName(selectedSession, allSessions);
|
|
2886
|
-
const result = await ctx.ui.custom((tui, theme, keybindings, done) => new ComposeOverlay(tui, theme, keybindings, selectedSession, targetLabel, overlayClient, done), { overlay: true }).catch(() => undefined);
|
|
2887
|
-
if (result?.sent && result.messageId && result.text && getLiveContext(ctx, overlayGeneration)) {
|
|
2888
|
-
pi.appendEntry("intercom_sent", {
|
|
2889
|
-
to: selectedSession.name || selectedSession.id,
|
|
2890
|
-
message: { text: result.text, expectsReply: result.expectsReply },
|
|
2891
|
-
messageId: result.messageId,
|
|
2892
|
-
timestamp: Date.now(),
|
|
2893
|
-
});
|
|
2894
|
-
notifyIfLive(ctx, `${result.expectsReply ? "Ask sent" : "Message sent"} to ${targetLabel}`, "info", overlayGeneration);
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
pi.registerCommand("intercom", {
|
|
2898
|
-
description: "Open peer messaging; 'all' includes other projects, 'topics' inspects quiet topic/owner state",
|
|
2899
|
-
handler: async (args, ctx) => {
|
|
2900
|
-
const requestedScope = args.trim();
|
|
2901
|
-
if (requestedScope === "topics") {
|
|
2902
|
-
const generation = runtimeGeneration;
|
|
2903
|
-
let notice;
|
|
2904
|
-
try {
|
|
2905
|
-
const active = await ensureConnected("overlay");
|
|
2906
|
-
if (!active.supportsTopics) {
|
|
2907
|
-
notice = TOPICS_UNAVAILABLE;
|
|
2908
|
-
topics.disconnected();
|
|
2909
|
-
}
|
|
2910
|
-
else {
|
|
2911
|
-
const sessions = await active.listSessions();
|
|
2912
|
-
if (getLiveContext(ctx, generation))
|
|
2913
|
-
topics.refresh(sessions);
|
|
2914
|
-
}
|
|
2915
|
-
}
|
|
2916
|
-
catch {
|
|
2917
|
-
if (getLiveContext(ctx, generation)) {
|
|
2918
|
-
topics.disconnected();
|
|
2919
|
-
notice = "Broker unavailable; last saved topic records follow.";
|
|
2920
|
-
}
|
|
2921
|
-
}
|
|
2922
|
-
if (getLiveContext(ctx, generation))
|
|
2923
|
-
await topics.open(ctx, notice);
|
|
2924
|
-
return;
|
|
2925
|
-
}
|
|
2926
|
-
if (requestedScope && requestedScope !== "all") {
|
|
2927
|
-
ctx.ui.notify("Usage: /intercom [all|topics]", "warning");
|
|
2928
|
-
return;
|
|
2929
|
-
}
|
|
2930
|
-
await openIntercomOverlay(ctx, requestedScope === "all" ? "all" : "project");
|
|
2931
|
-
},
|
|
2932
|
-
});
|
|
2933
|
-
pi.registerShortcut(config.shortcut, {
|
|
2934
|
-
description: "Toggle your agents, or open connected sessions",
|
|
2935
|
-
handler: async (ctx) => {
|
|
2936
|
-
const request = { ctx, handled: false };
|
|
2937
|
-
pi.events.emit("subagent:open-agents", request);
|
|
2938
|
-
if (!request.handled)
|
|
2939
|
-
await openIntercomOverlay(ctx);
|
|
2940
43
|
},
|
|
2941
44
|
});
|
|
45
|
+
const delivery = new InboundDeliveryOwner(pi, lifecycle, journal, {
|
|
46
|
+
replies,
|
|
47
|
+
syncStatus: () => connection.syncStatus(),
|
|
48
|
+
});
|
|
49
|
+
const inbound = new IntercomInbound(lifecycle, config, {
|
|
50
|
+
journal,
|
|
51
|
+
delivery,
|
|
52
|
+
replies,
|
|
53
|
+
wait,
|
|
54
|
+
connection,
|
|
55
|
+
topics,
|
|
56
|
+
child,
|
|
57
|
+
});
|
|
58
|
+
const outbound = new IntercomOutbound({ pi, lifecycle, connection, replies, wait }, config);
|
|
59
|
+
const inspection = new IntercomInspection(journal, replies);
|
|
60
|
+
const topicActions = new IntercomTopicActions({ pi, lifecycle, connection, topics });
|
|
61
|
+
const tools = new IntercomTools({ connection, outbound, topicActions, topics, inspection });
|
|
62
|
+
const supervisor = child
|
|
63
|
+
? new IntercomSupervisor({ pi, lifecycle, connection, wait }, child)
|
|
64
|
+
: null;
|
|
65
|
+
const overlay = new IntercomOverlay({ pi, lifecycle, connection, topics });
|
|
66
|
+
const bridges = new SubagentEventBridges(lifecycle, connection, delivery, {
|
|
67
|
+
pi,
|
|
68
|
+
journal,
|
|
69
|
+
open: (ctx, scope) => overlay.open(ctx, scope),
|
|
70
|
+
});
|
|
71
|
+
const awareness = child ? null : new PeerAwareness(connection, lifecycle);
|
|
72
|
+
bridges.start();
|
|
73
|
+
new IntercomSessionEvents(pi, {
|
|
74
|
+
lifecycle,
|
|
75
|
+
connection,
|
|
76
|
+
delivery,
|
|
77
|
+
journal,
|
|
78
|
+
replies,
|
|
79
|
+
wait,
|
|
80
|
+
topics,
|
|
81
|
+
bridges,
|
|
82
|
+
supervisor,
|
|
83
|
+
awareness,
|
|
84
|
+
}).register();
|
|
85
|
+
registerMessageRenderers(pi);
|
|
86
|
+
registerIntercomTools(pi, tools, supervisor);
|
|
87
|
+
overlay.register(config.shortcut);
|
|
2942
88
|
}
|