gentle-pi 4.0.0 → 4.2.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/README.md +157 -18
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +29 -25
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +708 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +399 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +90 -33
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +127 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +310 -47
- package/docs/render-cache-performance.md +28 -0
- package/docs/session-profile-format.md +39 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +11 -10
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +718 -87
- package/extensions/gentle-ai.ts +991 -263
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +271 -80
- package/extensions/gentle-todo.ts +123 -18
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +459 -0
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +484 -0
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +104 -28
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +535 -0
- package/lib/gentle-shell-launcher.ts +75 -24
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase.ts +1 -1
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +22 -7
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +261 -0
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +88 -71
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +81 -26
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +27 -5
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +5 -5
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +536 -0
- package/runtime/gentle-shell-launcher.mjs +74 -23
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +7 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +79 -32
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/pi-update.mjs +223 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +276 -0
- package/tests/auto-mode-sdk.test.ts +84 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +634 -0
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +454 -0
- package/tests/autonomous-guard.test.ts +86 -16
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +52 -1
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +316 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1895 -76
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +263 -19
- package/tests/gentle-shell-bundled.test.ts +1036 -0
- package/tests/gentle-shell-launcher.test.ts +251 -25
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +611 -100
- package/tests/gentle-todo.test.ts +317 -14
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2159 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +395 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +29 -6
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +6 -5
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/package-manifest.test.ts +105 -21
- package/tests/packed-runner-owned-path.test.ts +35 -1
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/pi-update-launcher.test.ts +87 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/rdd-status-line.test.ts +52 -0
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +5 -4
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-risk-assessment.test.ts +109 -33
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +65 -5
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +394 -0
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +79 -62
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +27 -2
- package/tests/shell-todo.test.ts +132 -11
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +27 -1
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter.test.ts +47 -21
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +38 -5
- package/tests/yolo-session-policy.test.ts +1 -1
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { agentsViewKey, agentsCollapseKey, agentsStopKey } from "../lib/agents-keys.ts";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { Type } from "typebox";
|
|
3
4
|
import { recordReviewMutation } from "../lib/review-reminder-receipt.ts";
|
|
4
5
|
import { SESSION_CHANGE_RELAY } from "../lib/session-changes.ts";
|
|
5
6
|
import { publishForeignSessionChange } from "../lib/session-change-capture.ts";
|
|
6
|
-
import { SessionWorktreeRegistry, resolveSessionWorktree, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
|
|
7
|
+
import { SESSION_WORKTREE_ENTRY, SESSION_WORKTREE_CHANGED, SessionWorktreeRegistry, resolveSessionWorktree, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
|
|
8
|
+
import { canonicalWriterRoot } from "../lib/writer-surfaces.ts";
|
|
7
9
|
import { ForeignTargetGrants } from "../lib/foreign-target-grants.ts";
|
|
8
10
|
import { MESSAGING_REASON_MAX_UTF8_BYTES, MESSAGING_REASON_MIN_CHARACTERS, normalizeMessagingReason, SessionMessagingGrants } from "../lib/session-messaging-grants.ts";
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
10
12
|
import { randomUUID } from "node:crypto";
|
|
11
13
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
12
14
|
import os from "node:os";
|
|
13
15
|
import { join, resolve, isAbsolute } from "node:path";
|
|
14
16
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { keyHint, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { createLocalBashOperations, keyHint, SettingsManager, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { createJobRegistry, isStandaloneSleep, type JobExecOperations, type JobRecord } from "../lib/background-jobs.ts";
|
|
19
|
+
import { JOB_GLYPH, JOB_NOTICE_TYPE, jobDetails, jobNoticeText, monitorEventsText, monitorStoppedText, registerBackgroundJobTools } from "../lib/background-jobs-tools.ts";
|
|
20
|
+
import { createMonitorController, mergeMonitorBatches, type MonitorNotice } from "../lib/background-monitor.ts";
|
|
21
|
+
import { JobsView } from "../lib/jobs-view.ts";
|
|
22
|
+
import { JOBS_SIDEBAR_EVENT, JOBS_STATUS_KEY } from "../lib/jobs-sidebar-state.ts";
|
|
16
23
|
import { Text, type TUI } from "@earendil-works/pi-tui";
|
|
17
24
|
import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
|
|
18
25
|
import { VISUAL_SETTINGS_CHANGED } from "../lib/shell-sidebar.ts";
|
|
@@ -20,10 +27,12 @@ import { resolveVisualSettings } from "../lib/visual-customization-policy.ts";
|
|
|
20
27
|
import { createCompletionQueue } from "../lib/agents-completion-delivery.ts";
|
|
21
28
|
import { createAgentMessageQueue, type PendingAgentMessage } from "../lib/agents-message-delivery.ts";
|
|
22
29
|
import { AGENT_MODE, discoverAgents, formatModelRef, loadAgentsConfig, resolveAgentProfile, withPinnedModelProfiles, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
|
|
30
|
+
import { freezeSessionProfileAtStartup, inheritedProfileDriftNotice, resolveSessionProfile, sessionProfileRoutingAt } from "../lib/session-profile-freeze.ts";
|
|
23
31
|
import { resolveBackgroundSubagentsPolicy } from "../lib/background-subagents-policy.ts";
|
|
24
32
|
import { installBackgroundCacheWarming } from "../lib/background-cache-warming.ts";
|
|
25
|
-
import { isFinished, TASK_EVENT, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
26
|
-
import { AgentRunner, piCommand, abortReasonText, type AskAnswer, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
|
|
33
|
+
import { isFinished, MISSING_TOOLS_NOTE_PREFIX, TASK_EVENT, TASK_STATUS, TaskStore, type AskRequest, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
34
|
+
import { AgentRunner, piCommand, abortReasonText, REQUESTED_TOOLS_ENV, type AskAnswer, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
|
|
35
|
+
import { parseChildPackageInjection } from "../lib/child-package-injection.ts";
|
|
27
36
|
import { ChildMessenger, type IpcEndpoint } from "../lib/agents-messaging.ts";
|
|
28
37
|
import { ActiveSessionClient, ActiveSessionListener, SessionPresenceRegistry, type PresenceRecord, type ReceivedNotification, type SentNotification, type SessionPresenceCandidate } from "../lib/agents-session-transport.ts";
|
|
29
38
|
import { WindowsActiveSessionClient, WindowsActiveSessionListener, WindowsSessionPresenceRegistry, type WindowsSessionRegistryPhaseObserver } from "../lib/windows-session-transport.ts";
|
|
@@ -33,18 +42,29 @@ import { historyDir, loadHistory, loadStoredTask, pruneHistory, saveTask } from
|
|
|
33
42
|
import { sessionToMarkdown } from "../lib/agents-transcript.ts";
|
|
34
43
|
import { AgentsView } from "../lib/agents-view.ts";
|
|
35
44
|
import { withOverlayRepaint } from "../lib/overlay-repaint.ts";
|
|
36
|
-
import { PresencePublisher } from "../lib/orchestrator-presence.ts";
|
|
45
|
+
import { PresencePublisher, sanitizeDisplayLabel, listPresence, sessionHash } from "../lib/orchestrator-presence.ts";
|
|
46
|
+
import { incomingMessageCard, outgoingMessageCall, outgoingMessageResult, type OrchestratorMessageDetails } from "../lib/orchestrator-message-card.ts";
|
|
47
|
+
import { orchestratorToolRenderers } from "../lib/orchestrator-tool-card.ts";
|
|
48
|
+
import { discoverOrchestrators } from "../lib/orchestrator-discovery.ts";
|
|
49
|
+
import { consultPublishedMetadata, unavailableMetadata, type MetadataReceipt } from "../lib/orchestrator-consultation.ts";
|
|
50
|
+
import { HelperCostPermission } from "../lib/orchestrator-helper-consent.ts";
|
|
51
|
+
import { OrchestratorStateCache } from "../lib/orchestrator-state.ts";
|
|
52
|
+
import { normalizeTaskSubject } from "../lib/orchestrator-alias.ts";
|
|
53
|
+
import { decodeWorkDescriptor } from "../lib/orchestrator-work.ts";
|
|
54
|
+
import { validateWorkFilter, searchPublishedWork } from "../lib/orchestrator-work-search.ts";
|
|
55
|
+
import { OrchestratorScopeCache, type RepositoryFact } from "../lib/orchestrator-scope.ts";
|
|
37
56
|
import { createRpcActivityPublisher, type RpcActivityPublisher } from "../lib/agents-rpc-publisher.ts";
|
|
38
57
|
import { isInteractiveRpcHost } from "../lib/rpc-host.ts";
|
|
39
58
|
import { createNativeFullscreenInteraction } from "../lib/native-fullscreen-interaction.ts";
|
|
40
59
|
import { AGENTS_GLYPH, renderAgentsCard, widgetExpiryMs, widgetRows } from "../lib/agents-widget.ts";
|
|
41
60
|
import { CARD_TONE, renderCard } from "../lib/shell-card.ts";
|
|
42
61
|
import { openInExternalEditor } from "./gentle-shell.ts";
|
|
43
|
-
import {
|
|
44
|
-
import {
|
|
45
|
-
import { allowedEditSurfaces, isDevelopmentSurface, isGenericBoundedWriter, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sessionRepositoryAuthority } from "../lib/bounded-writer-admission.ts";
|
|
62
|
+
import { gentlePiConfigHome } from "../lib/agent-home.ts";
|
|
63
|
+
import { resolveAgentHomeDirectory } from "../lib/agent-model-resolution.ts";
|
|
64
|
+
import { allowedEditSurfaces, inheritAllowedEditSurfaces, isBoundedWriter, isDevelopmentSurface, isGenericBoundedWriter, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sessionRepositoryAuthority } from "../lib/bounded-writer-admission.ts";
|
|
46
65
|
import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, childEvent, launchSelection, type LaunchSelection } from "../lib/runtime-metrics-children.ts";
|
|
47
66
|
import { runtimeMetricsEnvAllows, type RuntimeMetricsPolicyDeps } from "../lib/runtime-metrics-policy.ts";
|
|
67
|
+
import { publishTaskStatusChanges } from "../lib/notification-events.ts";
|
|
48
68
|
|
|
49
69
|
// Gentle Agents: subagents as isolated `pi --mode rpc` children, a task
|
|
50
70
|
// store that notifies per task, and a Gentle Shell card above the editor.
|
|
@@ -53,19 +73,46 @@ import { runtimeMetricsEnvAllows, type RuntimeMetricsPolicyDeps } from "../lib/r
|
|
|
53
73
|
|
|
54
74
|
export const AGENTS_WIDGET_KEY = "gentle-agents";
|
|
55
75
|
export const AGENTS_COMMAND_NAME = "gentle:agents";
|
|
76
|
+
export const JOBS_COMMAND_NAME = "gentle:jobs";
|
|
77
|
+
// How long session shutdown waits for stopped jobs to close their logs.
|
|
78
|
+
const JOBS_SHUTDOWN_GRACE_MS = 2000;
|
|
56
79
|
export const AGENTS_RESULT_TYPE = "gentle-agents.result";
|
|
57
80
|
export const AGENTS_MESSAGE_TYPE = "gentle-agents.message";
|
|
58
81
|
export const AGENTS_ORCHESTRATOR_MESSAGE_TYPE = "gentle-agents.orchestrator-message";
|
|
82
|
+
|
|
83
|
+
// Display labels are best-effort local snapshots, never routing or authority.
|
|
84
|
+
function messagePeerLabel(profile: string, sessionId: string): string | undefined {
|
|
85
|
+
try {
|
|
86
|
+
const page = listPresence(profile);
|
|
87
|
+
if (page.unavailable || page.overflow || page.rejected) return undefined;
|
|
88
|
+
const matches = page.entries.filter(header => header.sessionHash === sessionHash(sessionId));
|
|
89
|
+
return matches.length === 1 && matches[0].recent ? sanitizeDisplayLabel(matches[0].label) || undefined : undefined;
|
|
90
|
+
} catch { return undefined; }
|
|
91
|
+
}
|
|
59
92
|
export const AGENTS_STALE_RESULT_TYPE = "gentle-agents.stale-result";
|
|
93
|
+
export const AGENTS_STALE_NOTICE_TYPE = "gentle-agents.stale-notice";
|
|
60
94
|
const RENDER_COALESCE_MS = 400;
|
|
61
95
|
const CLOCK_TICK_MS = 1000;
|
|
62
96
|
const TOOL_PREFIX = "subagent_";
|
|
63
97
|
// Wakes an idle parent after child content was stored as a custom message.
|
|
64
98
|
// It names itself as automated so the model never attributes it to the human.
|
|
65
99
|
const PARENT_WAKE_TEXT = "[System-generated Gentle Agents notification, not written by the user] Subagent output was delivered to this session above. Review it and continue.";
|
|
100
|
+
const PARENT_WAKE_TYPE = "gentle-agents.wake";
|
|
101
|
+
const BRIDGE_WAKE_IDENTITY_TYPE = "gentle-agents.wake-identity";
|
|
102
|
+
interface BridgeWakeIdentity {
|
|
103
|
+
sessionId: string;
|
|
104
|
+
nonce: string;
|
|
105
|
+
text: string;
|
|
106
|
+
}
|
|
107
|
+
const bridgeWakeText = (nonce: string): string => `${PARENT_WAKE_TEXT} [gentle-agents wake: ${nonce}]`;
|
|
108
|
+
const NATIVE_PARENT_WAKE_TEXT = "Review the delivered subagent output and continue.";
|
|
66
109
|
// How long a dispatched wake may take to start a parent run before a later
|
|
67
110
|
// delivery may send another one.
|
|
68
111
|
export const PARENT_WAKE_GRACE_MS = 30_000;
|
|
112
|
+
// A delivery or wake the host rejected while the parent was idle is retried
|
|
113
|
+
// after the grace at most this many times in a row, so a permanently broken
|
|
114
|
+
// runtime never retries forever.
|
|
115
|
+
const PARENT_DELIVERY_RETRY_LIMIT = 3;
|
|
69
116
|
|
|
70
117
|
const retiredSddAgent = (name: string): boolean => /^sdd(?:-|$)/.test(name);
|
|
71
118
|
|
|
@@ -76,6 +123,7 @@ export interface SessionTransportRegistry {
|
|
|
76
123
|
}
|
|
77
124
|
|
|
78
125
|
export interface SessionTransportListener {
|
|
126
|
+
readonly record?: PresenceRecord;
|
|
79
127
|
readonly registry: SessionTransportRegistry;
|
|
80
128
|
readonly closesRegistry?: boolean;
|
|
81
129
|
start(): Promise<void>;
|
|
@@ -105,15 +153,22 @@ export interface AgentsDeps extends RunnerDeps {
|
|
|
105
153
|
metricsSchedule?: RunnerDeps["schedule"];
|
|
106
154
|
// Extensions every child loads with --extension (gentle-shell#1587).
|
|
107
155
|
childExtensionPaths?: string[];
|
|
156
|
+
/** Shell for background jobs; defaults to Pi's configured Bash. */
|
|
157
|
+
jobShell?: (cwd: string) => { operations: JobExecOperations; commandPrefix?: string };
|
|
158
|
+
/** Directory for background job output; defaults to a private temp dir. */
|
|
159
|
+
jobOutputDir?: () => string;
|
|
108
160
|
}
|
|
109
161
|
|
|
110
|
-
// gentle-shell#1587:
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
162
|
+
// gentle-shell#1587: fallback for a parent without the launcher's package
|
|
163
|
+
// injection signal (#1690), e.g. a manual `pi -e <package>` launch. Children
|
|
164
|
+
// then get context filtering, destructive-command safety, and the nan
|
|
165
|
+
// provider (gentle-shell#1731 T32: a model routed to nan/* is not found
|
|
166
|
+
// without it). When the signal is present, children load the whole package
|
|
167
|
+
// instead. This list is frozen: new child-facing behavior ships inside the
|
|
168
|
+
// package, never here. Missing files are omitted.
|
|
114
169
|
export function childContextExtensionPaths(exists: (path: string) => boolean = existsSync): string[] {
|
|
115
170
|
try {
|
|
116
|
-
return ["./child-context.ts", "./child-safety.ts"]
|
|
171
|
+
return ["./child-context.ts", "./child-safety.ts", "./nan-provider.ts"]
|
|
117
172
|
.map((path) => fileURLToPath(new URL(path, import.meta.url)))
|
|
118
173
|
.filter(exists);
|
|
119
174
|
} catch {
|
|
@@ -121,11 +176,37 @@ export function childContextExtensionPaths(exists: (path: string) => boolean = e
|
|
|
121
176
|
}
|
|
122
177
|
}
|
|
123
178
|
|
|
179
|
+
// #1690: when the gentle-shell launcher injected the package with -e, the
|
|
180
|
+
// host env carries that exact set and every child loads it too, which already
|
|
181
|
+
// includes the curated entries. Without a valid signal (declared package,
|
|
182
|
+
// regular gentle-pi, malformed value) children keep the curated entries.
|
|
183
|
+
function childExtensionRequest(env: NodeJS.ProcessEnv, curated: string[] | undefined): Pick<TaskRequest, "extensionPaths" | "noExtensions"> {
|
|
184
|
+
const injection = parseChildPackageInjection(env);
|
|
185
|
+
if (injection) return { extensionPaths: [...injection.extensionPaths], ...(injection.noExtensions ? { noExtensions: true } : {}) };
|
|
186
|
+
return curated && curated.length > 0 ? { extensionPaths: [...curated] } : {};
|
|
187
|
+
}
|
|
188
|
+
|
|
124
189
|
export function agentRuntimePaths(home: string, agentHome = join(home, ".pi", "agent")): { sessions: string; transcripts: string } {
|
|
125
190
|
const root = join(agentHome, "gentle-agents");
|
|
126
191
|
return { sessions: join(root, "sessions"), transcripts: join(root, "transcripts") };
|
|
127
192
|
}
|
|
128
193
|
|
|
194
|
+
const workDescriptorSchema = {
|
|
195
|
+
type: "object", additionalProperties: false,
|
|
196
|
+
description: "Explicit non-authoritative classification, not child context. Topic requires area; limits are UTF-8 bytes. No nested tasks.",
|
|
197
|
+
properties: {
|
|
198
|
+
area: { type: "string", maxLength: 64 }, topic: { type: "string", maxLength: 64 },
|
|
199
|
+
tags: { type: "array", maxItems: 8, uniqueItems: true, items: { type: "string", maxLength: 64 } },
|
|
200
|
+
refs: { type: "array", maxItems: 8, uniqueItems: true, items: {
|
|
201
|
+
type: "object", additionalProperties: false, required: ["kind", "repository", "id"], properties: {
|
|
202
|
+
kind: { type: "string", enum: ["issue", "pr", "task"] },
|
|
203
|
+
repository: { type: "string", maxLength: 256, description: "Explicit public host/owner/repo; not a URL or inferred identity." },
|
|
204
|
+
id: { type: "string", maxLength: 256, description: "Canonical positive decimal issue/PR ID or opaque historical task ID; never a peer route." },
|
|
205
|
+
},
|
|
206
|
+
} },
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
129
210
|
interface ToolText {
|
|
130
211
|
content: Array<{ type: "text"; text: string }>;
|
|
131
212
|
details: Record<string, unknown>;
|
|
@@ -190,6 +271,43 @@ function ownedChildIpc(env: NodeJS.ProcessEnv, candidate: IpcEndpoint | undefine
|
|
|
190
271
|
return candidate;
|
|
191
272
|
}
|
|
192
273
|
|
|
274
|
+
// Catalog declarations are routing hints, not proof of child availability.
|
|
275
|
+
// Filter non-name entries such as `"*": false`, but do not claim that a
|
|
276
|
+
// syntactically valid name is registered. Empty declarations omit --tools and
|
|
277
|
+
// use Pi defaults. The runner adds parent messaging separately.
|
|
278
|
+
const TOOL_NAME = /^[A-Za-z0-9_.:-]+$/;
|
|
279
|
+
|
|
280
|
+
function declaredToolInventory(agent: AgentDefinition): string {
|
|
281
|
+
const usable = agent.tools.filter((name) => TOOL_NAME.test(name));
|
|
282
|
+
if (usable.length > 0) return `[declared tools (not verified): ${usable.join(", ")}]`;
|
|
283
|
+
return agent.tools.length === 0
|
|
284
|
+
? "[declared tools: Pi defaults (no allowlist)]"
|
|
285
|
+
: "[declared tools: no tool names declared]";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Pi drops unknown --tools names without a diagnostic (#1690), so the child
|
|
289
|
+
// reports them once through the one notify the parent keeps. The check runs
|
|
290
|
+
// at the first before_agent_start, not session_start: Pi runs session_start
|
|
291
|
+
// handlers in extension load order, so an extension loaded after this one may
|
|
292
|
+
// still register its tools there. MCP tools register later still, and entries
|
|
293
|
+
// that cannot be tool names (the `"*": false` frontmatter entry) are not
|
|
294
|
+
// checkable.
|
|
295
|
+
function registerMissingToolsCheck(pi: ExtensionAPI, env: NodeJS.ProcessEnv): void {
|
|
296
|
+
const requested = (env[REQUESTED_TOOLS_ENV] ?? "").split(",").map((name) => name.trim())
|
|
297
|
+
.filter((name) => /^[A-Za-z0-9_.:-]+$/.test(name) && !name.startsWith("mcp__"));
|
|
298
|
+
if (requested.length === 0) return;
|
|
299
|
+
let reported = false;
|
|
300
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
301
|
+
if (reported) return;
|
|
302
|
+
reported = true;
|
|
303
|
+
try {
|
|
304
|
+
const available = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
305
|
+
const missing = [...new Set(requested.filter((name) => !available.has(name)))];
|
|
306
|
+
if (missing.length > 0) ctx.ui.notify(`${MISSING_TOOLS_NOTE_PREFIX} ${missing.join(", ")}`, "warning");
|
|
307
|
+
} catch { /* A diagnostic must never break the child's first prompt. */ }
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
193
311
|
function registerChildMessaging(pi: ExtensionAPI, ipc: IpcEndpoint): void {
|
|
194
312
|
const messenger = new ChildMessenger(ipc);
|
|
195
313
|
pi.registerTool({
|
|
@@ -197,7 +315,7 @@ function registerChildMessaging(pi: ExtensionAPI, ipc: IpcEndpoint): void {
|
|
|
197
315
|
label: "Agent parent message",
|
|
198
316
|
description: "Send a bounded notification or correlated query to this subagent's parent.",
|
|
199
317
|
parameters: { type: "object", additionalProperties: false, required: ["message"], properties: { kind: { type: "string", enum: ["notification", "query"] }, message: { type: "string" } } } as never,
|
|
200
|
-
async execute(_id, params) {
|
|
318
|
+
async execute(_id, params): Promise<ToolText> {
|
|
201
319
|
const input = params as { kind?: unknown; message?: unknown };
|
|
202
320
|
if (typeof input.message !== "string") throw new Error("parent messages require text");
|
|
203
321
|
if (input.kind === undefined || input.kind === "notification") {
|
|
@@ -272,18 +390,27 @@ function expandHint(expanded: boolean): string {
|
|
|
272
390
|
}
|
|
273
391
|
}
|
|
274
392
|
|
|
393
|
+
// pi runs `-p` and `--mode json` through one one-shot runner that disposes
|
|
394
|
+
// the runtime as soon as the prompt returns, so a background result has no
|
|
395
|
+
// parent session left to reach (gentle-shell#1731 T18).
|
|
396
|
+
export function isSingleShotMode(mode: string | undefined): boolean {
|
|
397
|
+
return mode === "print" || mode === "json";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const SINGLE_SHOT_BACKGROUND_ERROR = "Background subagents are unavailable in single-shot modes: pi -p and pi --mode json exit before a parent session can receive results. Use task mode, RPC mode, or interactive Pi.";
|
|
401
|
+
|
|
275
402
|
// The default mode for a subagent_run request that named neither an explicit
|
|
276
403
|
// mode nor an agent-defined one. Background is a runtime default only when
|
|
277
404
|
// the background-subagents policy is on AND the parent can receive results:
|
|
278
|
-
//
|
|
279
|
-
// the
|
|
280
|
-
//
|
|
405
|
+
// single-shot modes exit before a parent session exists to deliver them to
|
|
406
|
+
// (see the guard in `launch` below), so they keep the configured default
|
|
407
|
+
// (normally task) even when the policy is on.
|
|
281
408
|
export function resolveDefaultSubagentMode(input: {
|
|
282
409
|
configuredDefault: AgentMode;
|
|
283
410
|
policy: "on" | "off";
|
|
284
411
|
parentMode: string | undefined;
|
|
285
412
|
}): AgentMode {
|
|
286
|
-
return input.policy === "on" && input.parentMode
|
|
413
|
+
return input.policy === "on" && !isSingleShotMode(input.parentMode)
|
|
287
414
|
? AGENT_MODE.BACKGROUND
|
|
288
415
|
: input.configuredDefault;
|
|
289
416
|
}
|
|
@@ -342,19 +469,15 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
342
469
|
return;
|
|
343
470
|
}
|
|
344
471
|
if (childIpc) registerChildMessaging(pi, childIpc);
|
|
472
|
+
registerMissingToolsCheck(pi, env);
|
|
345
473
|
return;
|
|
346
474
|
}
|
|
347
475
|
if (!agentsEnabled(env)) return;
|
|
348
476
|
const deps: AgentsDeps = { ...defaultDeps(env), ...overrides };
|
|
349
477
|
// An explicitly injected pi command wins over the default per-spawn resolver.
|
|
350
478
|
if (overrides?.pi && !overrides.resolvePi) delete deps.resolvePi;
|
|
351
|
-
const selectedHome = overrides.agentHome ?? (overrides.home === undefined ? resolveGentlePiAgentHome(deps.env) : join(deps.home, ".pi", "agent"));
|
|
352
|
-
// Expand environment tildes like Pi, but leave explicit path APIs literal.
|
|
353
|
-
const environmentHome = overrides.agentHome === undefined && overrides.home === undefined;
|
|
354
|
-
const expandedHome = environmentHome && selectedHome === "~" ? deps.home
|
|
355
|
-
: environmentHome && (selectedHome.startsWith("~/") || (process.platform === "win32" && selectedHome.startsWith("~\\"))) ? join(deps.home, selectedHome.slice(2)) : selectedHome;
|
|
356
479
|
// Freeze the host's root before a child uses a different session cwd.
|
|
357
|
-
const agentHome =
|
|
480
|
+
const agentHome = resolveAgentHomeDirectory({ env: deps.env, home: deps.home, agentHome: overrides.agentHome, homeOverridden: overrides.home !== undefined });
|
|
358
481
|
const sessionTransport = deps.sessionTransport ?? createDefaultSessionTransport();
|
|
359
482
|
if (legacySubagentsInstalledAt(agentHome)) {
|
|
360
483
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -366,6 +489,10 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
366
489
|
const viewKey = agentsViewKey(env);
|
|
367
490
|
const stopKey = agentsStopKey(env);
|
|
368
491
|
const store = new TaskStore();
|
|
492
|
+
// Attach before any live add/start; restore and unchanged updates are silent.
|
|
493
|
+
// TaskStore contains bus listener failures so notifications cannot fail tools.
|
|
494
|
+
const stopStatusNotifications = publishTaskStatusChanges(store, pi.events, () => performance.now());
|
|
495
|
+
pi.on("session_shutdown", () => { stopStatusNotifications(); });
|
|
369
496
|
const restoredTaskIds = new Set<string>();
|
|
370
497
|
const tasksDir = historyDir(deps.home, agentHome);
|
|
371
498
|
let ui: ExtensionContext["ui"] | undefined;
|
|
@@ -376,6 +503,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
376
503
|
let stopVisualUpdates: (() => void) | undefined;
|
|
377
504
|
let sessions: ExtensionContext["sessionManager"] | undefined;
|
|
378
505
|
let presence: PresencePublisher | undefined;
|
|
506
|
+
const scopeCache = new OrchestratorScopeCache(deps.resolveWorktree);
|
|
507
|
+
const stateCache = new OrchestratorStateCache();
|
|
379
508
|
let rpcActivityPublisher: RpcActivityPublisher | undefined;
|
|
380
509
|
// Messages already surfaced to the user this session through the RPC
|
|
381
510
|
// activity publisher's `onError`, so a recurring push failure (the
|
|
@@ -383,16 +512,43 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
383
512
|
// session instead of flooding the UI. Reset on every `session_start`.
|
|
384
513
|
let notifiedRpcActivityErrors: Set<string> | undefined;
|
|
385
514
|
const overlays = new Set<AgentsView>();
|
|
515
|
+
const startPresence = (manager: ExtensionContext["sessionManager"]) => {
|
|
516
|
+
scopeCache.clear();
|
|
517
|
+
const sessionId = manager.getSessionId() ?? "";
|
|
518
|
+
const labelSource = () => {
|
|
519
|
+
if (sessions !== manager || (manager.getSessionId() ?? "") !== sessionId) throw new Error("stale-session");
|
|
520
|
+
return manager.getSessionName?.() || manager.getCwd().split(/[\\/]/).pop() || "Orchestrator";
|
|
521
|
+
};
|
|
522
|
+
return PresencePublisher.start({ profile: agentHome, sessionId, label: labelSource(), labelSource, activity: [] });
|
|
523
|
+
};
|
|
386
524
|
const publishActivity = () => {
|
|
387
525
|
if (!sessions) return;
|
|
388
526
|
try {
|
|
389
527
|
if (!presence || presence.error) {
|
|
390
|
-
presence
|
|
391
|
-
|
|
528
|
+
presence?.dispose();
|
|
529
|
+
presence = startPresence(sessions);
|
|
530
|
+
}
|
|
531
|
+
const tasks = store.list(activeSessionId()).filter((task) => ownedTaskIds.has(task.id) && !isFinished(task.status) && !restoredTaskIds.has(task.id));
|
|
532
|
+
presence?.update(tasks.map((task) => ({ task, thread: store.thread(task.id) })));
|
|
533
|
+
const transport = activeSessionTransport;
|
|
534
|
+
if (transport?.sessionManager === sessions && transport.sessionId === activeSessionId() && transport.listener.record) {
|
|
535
|
+
// Read the existing durable registry, without roots()'s repeated Git validation.
|
|
536
|
+
// These are recorded contexts, never authority for admission or messaging.
|
|
537
|
+
const registered = sessions.getEntries().flatMap(entry => {
|
|
538
|
+
if (entry.type !== "custom" || entry.customType !== SESSION_WORKTREE_ENTRY) return [];
|
|
539
|
+
const data = entry.data as { sessionId?: string; root?: string; evidence?: string } | undefined;
|
|
540
|
+
return data?.sessionId === activeSessionId() && typeof data.root === "string" && typeof data.evidence === "string" ? [data.root] : [];
|
|
541
|
+
});
|
|
542
|
+
presence?.updateDiscovery(transport.listener.record, { workspace: sessions.getCwd(), tasks, registered,
|
|
543
|
+
scope: scopeCache.project(sessions.getCwd(), tasks, registered), state: stateCache.get(sessions) });
|
|
392
544
|
}
|
|
393
|
-
presence?.update(store.list(activeSessionId()).filter((task) => !isFinished(task.status) && !restoredTaskIds.has(task.id)).map((task) => ({ task, thread: store.thread(task.id) })));
|
|
394
545
|
} catch { presence?.dispose(); presence = undefined; }
|
|
395
546
|
};
|
|
547
|
+
const unsubscribeScope = pi.events.on(SESSION_WORKTREE_CHANGED, (event) => {
|
|
548
|
+
if ((event as { sessionId?: string } | undefined)?.sessionId !== sessions?.getSessionId()) return;
|
|
549
|
+
scopeCache.clear();
|
|
550
|
+
publishActivity();
|
|
551
|
+
});
|
|
396
552
|
let worktrees: SessionWorktreeRegistry | undefined;
|
|
397
553
|
let worktreeManager: ExtensionContext["sessionManager"] | undefined;
|
|
398
554
|
let worktreeAuthority: (() => boolean) | undefined;
|
|
@@ -437,6 +593,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
437
593
|
pi.on("session_shutdown", () => {
|
|
438
594
|
clearTaskMetrics();
|
|
439
595
|
unsubscribeMetrics();
|
|
596
|
+
unsubscribeScope();
|
|
597
|
+
scopeCache.clear();
|
|
440
598
|
});
|
|
441
599
|
let stopAllConfirmation: Promise<void> | undefined;
|
|
442
600
|
|
|
@@ -498,7 +656,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
498
656
|
listener = sessionTransport.createListener(registry, sessionId, async (notification) => {
|
|
499
657
|
const active = activeSessionTransport;
|
|
500
658
|
if (!active || active.generation !== generation || active.sessionManager !== sessionManager || active.sessionId !== sessionId || sessions !== sessionManager || activeSessionId() !== sessionId) throw new Error("stale session transport");
|
|
501
|
-
pi.sendMessage({ customType: AGENTS_ORCHESTRATOR_MESSAGE_TYPE, content: `Session message from ${notification.senderSessionId} (correlation ${notification.id}): ${notification.message}`, display: true, details: { gentleAgents: { senderSessionId: notification.senderSessionId, recipientSessionId: sessionId, correlationId: notification.id, direction: "incoming" } } }, { deliverAs: "followUp", triggerTurn: true });
|
|
659
|
+
pi.sendMessage({ customType: AGENTS_ORCHESTRATOR_MESSAGE_TYPE, content: `Session message from ${notification.senderSessionId} (correlation ${notification.id}): ${notification.message}`, display: true, details: { gentleAgents: { senderSessionId: notification.senderSessionId, recipientSessionId: sessionId, correlationId: notification.id, direction: "incoming", message: notification.message, senderLabel: messagePeerLabel(agentHome, notification.senderSessionId), recipientLabel: sanitizeDisplayLabel(sessionManager.getSessionName?.() ?? "") } } }, { deliverAs: "followUp", triggerTurn: true });
|
|
502
660
|
});
|
|
503
661
|
client = sessionTransport.createClient(registry, sessionId);
|
|
504
662
|
if (sessions !== sessionManager || generation !== transportGeneration || activeSessionId() !== sessionId) {
|
|
@@ -515,6 +673,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
515
673
|
await closeStartupTransport();
|
|
516
674
|
return;
|
|
517
675
|
}
|
|
676
|
+
publishActivity();
|
|
518
677
|
} catch {
|
|
519
678
|
if (activeSessionTransport?.generation === generation) activeSessionTransport = undefined;
|
|
520
679
|
await closeStartupTransport();
|
|
@@ -589,12 +748,102 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
589
748
|
// conversation.
|
|
590
749
|
const completions = createCompletionQueue<TaskRecord>();
|
|
591
750
|
const messages = createAgentMessageQueue();
|
|
751
|
+
// A job's exit, or a monitor's line events and harness stop, waiting for
|
|
752
|
+
// the parent delivery router.
|
|
753
|
+
type JobNotice = { job: JobRecord; kind: "exit" } | MonitorNotice;
|
|
754
|
+
const jobNotices: JobNotice[] = [];
|
|
755
|
+
let jobOutputDir: string | undefined;
|
|
756
|
+
const jobs = createJobRegistry({
|
|
757
|
+
outputDir: deps.jobOutputDir ?? (() => (jobOutputDir ??= mkdtempSync(join(os.tmpdir(), "gentle-jobs-")))),
|
|
758
|
+
now: deps.now,
|
|
759
|
+
shell: deps.jobShell ?? ((cwd) => {
|
|
760
|
+
const settings = SettingsManager.create(cwd, agentHome);
|
|
761
|
+
return { operations: createLocalBashOperations({ shellPath: settings.getShellPath() }), commandPrefix: settings.getShellCommandPrefix() };
|
|
762
|
+
}),
|
|
763
|
+
onSettled: (job) => settleJob(job),
|
|
764
|
+
});
|
|
765
|
+
const monitors = createMonitorController({
|
|
766
|
+
registry: jobs,
|
|
767
|
+
schedule: (fn, ms) => deps.schedule(fn, ms),
|
|
768
|
+
now: deps.now,
|
|
769
|
+
deliver: (notice) => queueJobNotice(notice),
|
|
770
|
+
});
|
|
771
|
+
// A parent that sleeps while background work runs only burns the turn: each
|
|
772
|
+
// completion already arrives as a session message (#1903). bash_background
|
|
773
|
+
// and monitor stay allowed because they are the sanctioned way to wait.
|
|
774
|
+
pi.on("tool_call", (event) => {
|
|
775
|
+
if (event.toolName !== "bash") return undefined;
|
|
776
|
+
const command = (event.input as { command?: unknown }).command;
|
|
777
|
+
if (typeof command !== "string" || !isStandaloneSleep(command)) return undefined;
|
|
778
|
+
const sessionId = activeSessionId();
|
|
779
|
+
// Same active-task predicate as publishActivity.
|
|
780
|
+
const tasks = store.list(sessionId).filter((task) => ownedTaskIds.has(task.id) && !isFinished(task.status) && !restoredTaskIds.has(task.id)).length;
|
|
781
|
+
const running = jobs.list(sessionId ?? "").filter((job) => job.status === "running").length;
|
|
782
|
+
if (tasks + running === 0) return undefined;
|
|
783
|
+
return {
|
|
784
|
+
block: true,
|
|
785
|
+
reason: `Standalone sleep is blocked while background work is active (${tasks} task(s), ${running} job(s)). End the turn now; each completion arrives automatically as a session message and starts a new turn.`,
|
|
786
|
+
};
|
|
787
|
+
});
|
|
592
788
|
let activeAgentRuns = 0;
|
|
593
789
|
// ExtensionAPI has no idle probe; ctx.isIdle() is the only one. It is live
|
|
594
790
|
// and also reports compaction, which activeAgentRuns cannot see. The
|
|
595
791
|
// session_start context is kept for it and dropped at shutdown; a stale
|
|
596
792
|
// context throws instead of answering, so delivery fails closed.
|
|
597
793
|
let parentCtx: ExtensionContext | undefined;
|
|
794
|
+
const helperPermission = new HelperCostPermission(() => parentCtx && activeTransportFor(parentCtx) ? parentCtx : undefined);
|
|
795
|
+
pi.on("session_before_switch", () => helperPermission.clear());
|
|
796
|
+
pi.on("session_before_fork", () => helperPermission.clear());
|
|
797
|
+
pi.on("session_before_tree", () => helperPermission.clear());
|
|
798
|
+
pi.on("model_select", () => helperPermission.clear());
|
|
799
|
+
pi.on("resources_discover", () => helperPermission.clear());
|
|
800
|
+
let bridgeWakeIdentity: BridgeWakeIdentity | undefined;
|
|
801
|
+
let wakeVisibilityWarning = false;
|
|
802
|
+
const restoreBridgeWakeIdentity = (ctx: ExtensionContext | undefined) => {
|
|
803
|
+
bridgeWakeIdentity = undefined;
|
|
804
|
+
if (!ctx) return;
|
|
805
|
+
// Rendering is session-wide, not model/branch state: an identity on an
|
|
806
|
+
// abandoned branch still owns its generated bubbles in the session tree.
|
|
807
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
808
|
+
if (entry.type !== "custom" || entry.customType !== BRIDGE_WAKE_IDENTITY_TYPE) continue;
|
|
809
|
+
const data = entry.data as Partial<BridgeWakeIdentity> | undefined;
|
|
810
|
+
if (data?.sessionId === ctx.sessionManager.getSessionId() && typeof data.nonce === "string"
|
|
811
|
+
&& /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(data.nonce)
|
|
812
|
+
&& data.text === bridgeWakeText(data.nonce)) {
|
|
813
|
+
bridgeWakeIdentity = data as BridgeWakeIdentity;
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
const hasWakeTransformer = typeof pi.registerMarkdownTransformer === "function";
|
|
819
|
+
if (hasWakeTransformer) {
|
|
820
|
+
// Register once in this runtime. Reload replaces the runtime; session
|
|
821
|
+
// replacement changes the identity, never the transformer registration.
|
|
822
|
+
pi.registerMarkdownTransformer((markdown, context) =>
|
|
823
|
+
context.messageType === "user" && bridgeWakeIdentity?.sessionId === parentCtx?.sessionManager.getSessionId()
|
|
824
|
+
&& markdown === bridgeWakeIdentity?.text ? "" : markdown);
|
|
825
|
+
}
|
|
826
|
+
const bridgeWake = (): string => {
|
|
827
|
+
if (!parentCtx) return PARENT_WAKE_TEXT;
|
|
828
|
+
try {
|
|
829
|
+
if (!hasWakeTransformer) throw new Error("Markdown transformer API unavailable");
|
|
830
|
+
if (!bridgeWakeIdentity || bridgeWakeIdentity.sessionId !== parentCtx.sessionManager.getSessionId()) {
|
|
831
|
+
const nonce = randomUUID();
|
|
832
|
+
const identity = { sessionId: parentCtx.sessionManager.getSessionId(), nonce, text: bridgeWakeText(nonce) };
|
|
833
|
+
// Persist before sending so a restart can reconstruct exact ownership.
|
|
834
|
+
pi.appendEntry(BRIDGE_WAKE_IDENTITY_TYPE, identity);
|
|
835
|
+
bridgeWakeIdentity = identity;
|
|
836
|
+
}
|
|
837
|
+
return bridgeWakeIdentity.text;
|
|
838
|
+
} catch {
|
|
839
|
+
// Compatibility fallback preserves continuation, not invisibility.
|
|
840
|
+
if (!wakeVisibilityWarning) {
|
|
841
|
+
wakeVisibilityWarning = true;
|
|
842
|
+
try { parentCtx.ui.notify("Gentle Agents cannot hide Claude Bridge continuation on this runtime; the generated user wake remains visible.", "warning"); } catch { /* UI failure must not drop continuation. */ }
|
|
843
|
+
}
|
|
844
|
+
return PARENT_WAKE_TEXT;
|
|
845
|
+
}
|
|
846
|
+
};
|
|
598
847
|
// Mirrors the host's agent run, which spans agent_start through
|
|
599
848
|
// agent_settled, including post-run retries and in-run compaction. Unlike
|
|
600
849
|
// activeAgentRuns it stays set between agent_end and agent_settled, where
|
|
@@ -614,6 +863,22 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
614
863
|
let promptStarting = false;
|
|
615
864
|
let cancelPromptGrace: (() => void) | undefined;
|
|
616
865
|
let cancelBoundaryFlush: (() => void) | undefined;
|
|
866
|
+
// An idle parent has no turn boundary coming, so a failed idle delivery or
|
|
867
|
+
// wake is retried by one bounded timer that runs a full flush.
|
|
868
|
+
let cancelDeliveryRetry: (() => void) | undefined;
|
|
869
|
+
let deliveryRetries = 0;
|
|
870
|
+
const endDeliveryRetry = () => {
|
|
871
|
+
cancelDeliveryRetry?.();
|
|
872
|
+
cancelDeliveryRetry = undefined;
|
|
873
|
+
};
|
|
874
|
+
const armDeliveryRetry = () => {
|
|
875
|
+
if (cancelDeliveryRetry || deliveryRetries >= PARENT_DELIVERY_RETRY_LIMIT) return;
|
|
876
|
+
deliveryRetries += 1;
|
|
877
|
+
cancelDeliveryRetry = deps.schedule(() => {
|
|
878
|
+
cancelDeliveryRetry = undefined;
|
|
879
|
+
flushAll();
|
|
880
|
+
}, PARENT_WAKE_GRACE_MS);
|
|
881
|
+
};
|
|
617
882
|
|
|
618
883
|
const isTaskLive = (id: string): boolean => {
|
|
619
884
|
const task = store.get(id);
|
|
@@ -629,13 +894,10 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
629
894
|
// branch, so a parent that keeps calling tools would see the content only
|
|
630
895
|
// when the whole run ends — the original #867 delay.
|
|
631
896
|
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
// message is stored durably without a turn instead, and a short
|
|
637
|
-
// system-generated user message wakes the parent through the normal prompt
|
|
638
|
-
// path. The wake never repeats child content, so the model sees it once.
|
|
897
|
+
// Idle child content is stored durably without a turn, then a separate
|
|
898
|
+
// coalesced wake requests continuation without repeating that content.
|
|
899
|
+
// Claude Bridge needs a user wake through the prompt lifecycle for capture;
|
|
900
|
+
// native providers can use a hidden custom-message turn instead.
|
|
639
901
|
//
|
|
640
902
|
// A parent that is busy without a run (compaction, or a prompt's pre-run
|
|
641
903
|
// compaction) is not streaming, so steer + triggerTurn would also start a
|
|
@@ -684,14 +946,31 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
684
946
|
// without a run keeps the wake owed until a later boundary flush.
|
|
685
947
|
if (route !== "idle") return;
|
|
686
948
|
wakeOwed = false;
|
|
949
|
+
endDeliveryRetry();
|
|
687
950
|
beginPromptStart();
|
|
688
951
|
try {
|
|
689
952
|
// "steer" matters only when a run started in between: the wake is then
|
|
690
953
|
// queued into it instead of being rejected as a concurrent prompt.
|
|
691
|
-
|
|
954
|
+
// Read the live selection at dispatch, not when child content arrived.
|
|
955
|
+
// Only Claude Bridge is currently evidenced to require prompt capture;
|
|
956
|
+
// registering a custom provider alone does not make it a bridge.
|
|
957
|
+
if (parentCtx?.model?.provider === "claude-bridge") {
|
|
958
|
+
pi.sendUserMessage(bridgeWake(), { deliverAs: "steer" });
|
|
959
|
+
} else {
|
|
960
|
+
pi.sendMessage({
|
|
961
|
+
customType: PARENT_WAKE_TYPE,
|
|
962
|
+
content: NATIVE_PARENT_WAKE_TEXT,
|
|
963
|
+
display: false,
|
|
964
|
+
}, { deliverAs: "steer", triggerTurn: true });
|
|
965
|
+
}
|
|
966
|
+
deliveryRetries = 0;
|
|
692
967
|
} catch {
|
|
693
968
|
// A stale runtime fails closed instead of throwing from a microtask.
|
|
969
|
+
// The stored content still needs a turn, so the wake stays owed and is
|
|
970
|
+
// retried once per grace, a bounded number of times in a row.
|
|
694
971
|
endPromptStart();
|
|
972
|
+
wakeOwed = true;
|
|
973
|
+
armDeliveryRetry();
|
|
695
974
|
}
|
|
696
975
|
};
|
|
697
976
|
|
|
@@ -712,12 +991,21 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
712
991
|
sendToParent({ customType: AGENTS_RESULT_TYPE, content: completionText(task), display: true, details: taskDetails(task) }, route);
|
|
713
992
|
};
|
|
714
993
|
|
|
715
|
-
// A stale
|
|
716
|
-
//
|
|
717
|
-
|
|
994
|
+
// A stale report must not re-enter the LLM conversation, but the parent is
|
|
995
|
+
// told never to poll: it gets a compact model-facing notice naming the task,
|
|
996
|
+
// and the human gets durable TUI-only content.
|
|
997
|
+
const deliverStale = (task: TaskRecord, settledAt: number, route: Exclude<ParentRoute, "hold">) => {
|
|
718
998
|
if (activeSessionId() !== task.parentSessionId) return;
|
|
719
999
|
const ageSeconds = Math.max(0, Math.round((deps.now() - settledAt) / 1000));
|
|
720
|
-
|
|
1000
|
+
const status = task.status.replace("_", " ");
|
|
1001
|
+
sendToParent({
|
|
1002
|
+
customType: AGENTS_STALE_NOTICE_TYPE,
|
|
1003
|
+
content: `Subagent ${task.agent} (task ${task.id}, "${task.label}") ${status} about ${ageSeconds}s ago while you were busy. Its report was not replayed here; call subagent_result with task_id ${task.id} to read it.`,
|
|
1004
|
+
display: false,
|
|
1005
|
+
details: taskDetails(task),
|
|
1006
|
+
}, route);
|
|
1007
|
+
// The notice already reached the parent; a failed card must not resend it.
|
|
1008
|
+
try { pi.appendEntry(AGENTS_STALE_RESULT_TYPE, { taskId: task.id, agent: task.agent, label: task.label, status: task.status, ageSeconds }); } catch { /* TUI-only content. */ }
|
|
721
1009
|
};
|
|
722
1010
|
|
|
723
1011
|
const deliverMessage = (msg: PendingAgentMessage, route: Exclude<ParentRoute, "hold">) => {
|
|
@@ -754,17 +1042,48 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
754
1042
|
const flushCompletions = () => {
|
|
755
1043
|
const route = deliveryRoute(false);
|
|
756
1044
|
if (!route) return;
|
|
757
|
-
for (const
|
|
1045
|
+
for (const entry of completions.takeDeliverable(deps.now())) {
|
|
1046
|
+
try {
|
|
1047
|
+
if (entry.stale) deliverStale(entry.task, entry.settledAt, route);
|
|
1048
|
+
else deliver(entry.task, route);
|
|
1049
|
+
} catch {
|
|
1050
|
+
// The parent is told never to poll, so a failed forward must not
|
|
1051
|
+
// lose the completion: it waits for the next boundary instead, and
|
|
1052
|
+
// an idle parent, which has none coming, gets a bounded retry.
|
|
1053
|
+
completions.requeue(entry);
|
|
1054
|
+
if (route === "idle") armDeliveryRetry();
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
// A background job's exit notice rides the same router as a completion. It
|
|
1060
|
+
// is compact (no full output), so it has no stale form; a failed forward
|
|
1061
|
+
// requeues it like a completion does.
|
|
1062
|
+
const flushJobNotices = () => {
|
|
1063
|
+
const route = deliveryRoute(false);
|
|
1064
|
+
if (!route) return;
|
|
1065
|
+
const failed: JobNotice[] = [];
|
|
1066
|
+
for (const notice of jobNotices.splice(0)) {
|
|
1067
|
+
const { job } = notice;
|
|
1068
|
+
if (activeSessionId() !== job.ownerSessionId) continue;
|
|
1069
|
+
const content = notice.kind === "events" ? monitorEventsText(job, notice.batch)
|
|
1070
|
+
: notice.kind === "stopped" ? monitorStoppedText(job, notice.reason) : jobNoticeText(job);
|
|
1071
|
+
const event = notice.kind === "events" ? "events" : notice.kind === "stopped" ? notice.reason : undefined;
|
|
758
1072
|
try {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1073
|
+
sendToParent({ customType: JOB_NOTICE_TYPE, content, display: true, details: jobDetails(job, event) }, route);
|
|
1074
|
+
} catch {
|
|
1075
|
+
failed.push(notice);
|
|
1076
|
+
}
|
|
762
1077
|
}
|
|
1078
|
+
if (failed.length === 0) return;
|
|
1079
|
+
jobNotices.unshift(...failed);
|
|
1080
|
+
if (route === "idle") armDeliveryRetry();
|
|
763
1081
|
};
|
|
764
1082
|
|
|
765
1083
|
const flushAll = () => {
|
|
766
1084
|
flushMessages();
|
|
767
1085
|
flushCompletions();
|
|
1086
|
+
flushJobNotices();
|
|
768
1087
|
// A wake left owed by an earlier held or expired attempt is retried here.
|
|
769
1088
|
requestWake();
|
|
770
1089
|
};
|
|
@@ -783,12 +1102,52 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
783
1102
|
|
|
784
1103
|
// Session changes discard every pending wake and boundary flush.
|
|
785
1104
|
const resetParentDelivery = (ctx: ExtensionContext | undefined) => {
|
|
1105
|
+
helperPermission.clear(); // Revoke before dropping old callback authority.
|
|
786
1106
|
parentCtx = ctx;
|
|
1107
|
+
restoreBridgeWakeIdentity(ctx);
|
|
1108
|
+
wakeVisibilityWarning = false;
|
|
787
1109
|
parentRunActive = false;
|
|
788
1110
|
wakeOwed = false;
|
|
789
1111
|
endPromptStart();
|
|
790
1112
|
cancelBoundaryFlush?.();
|
|
791
1113
|
cancelBoundaryFlush = undefined;
|
|
1114
|
+
endDeliveryRetry();
|
|
1115
|
+
deliveryRetries = 0;
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// Structured descriptions feed Status; native hosts keep the compact count.
|
|
1119
|
+
const refreshJobStatus = () => {
|
|
1120
|
+
const sessionId = activeSessionId();
|
|
1121
|
+
const running = jobs.list(sessionId ?? "").filter((job) => job.status === "running")
|
|
1122
|
+
.sort((a, b) => b.startedAt - a.startedAt);
|
|
1123
|
+
try { parentCtx?.ui.setStatus(JOBS_STATUS_KEY, running.length === 0 ? undefined : `${JOB_GLYPH} ${running.length} job${running.length === 1 ? "" : "s"}`); } catch { /* Status is cosmetic. */ }
|
|
1124
|
+
if (sessionId) {
|
|
1125
|
+
try { pi.events.emit(JOBS_SIDEBAR_EVENT, { sessionId, snapshot: { jobs: running.map(({ id, label, startedAt }) => ({ id, label, startedAt })) } }); } catch { /* Display only. */ }
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
// A job exit settles like a completion below: flushed at once for an idle
|
|
1130
|
+
// parent, at the next turn boundary for a busy one.
|
|
1131
|
+
// Line events still waiting for a busy parent coalesce per monitor.
|
|
1132
|
+
const queueJobNotice = (notice: JobNotice) => {
|
|
1133
|
+
// Merge into this monitor's latest pending notice when that is still
|
|
1134
|
+
// an events notice, so events never move past its own end.
|
|
1135
|
+
let index = -1;
|
|
1136
|
+
for (let i = jobNotices.length - 1; i >= 0; i--) if (jobNotices[i]!.job === notice.job) { index = i; break; }
|
|
1137
|
+
const pending = index >= 0 ? jobNotices[index]! : undefined;
|
|
1138
|
+
if (notice.kind === "events" && pending?.kind === "events") {
|
|
1139
|
+
jobNotices[index] = { ...pending, batch: mergeMonitorBatches(pending.batch, notice.batch) };
|
|
1140
|
+
} else {
|
|
1141
|
+
jobNotices.push(notice);
|
|
1142
|
+
}
|
|
1143
|
+
refreshJobStatus();
|
|
1144
|
+
requestRender();
|
|
1145
|
+
if (activeAgentRuns === 0) flushAll();
|
|
1146
|
+
};
|
|
1147
|
+
const settleJob = (job: JobRecord) => {
|
|
1148
|
+
// A monitor's pending lines are delivered before its end.
|
|
1149
|
+
if (job.kind === "monitor") monitors.finish(job);
|
|
1150
|
+
queueJobNotice({ job, kind: "exit" });
|
|
792
1151
|
};
|
|
793
1152
|
|
|
794
1153
|
// A completion settles into our queue. An idle parent flushes right away so
|
|
@@ -821,6 +1180,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
821
1180
|
parentRunActive = true;
|
|
822
1181
|
// The run carries everything stored while the parent was idle.
|
|
823
1182
|
wakeOwed = false;
|
|
1183
|
+
endDeliveryRetry();
|
|
1184
|
+
deliveryRetries = 0;
|
|
824
1185
|
endPromptStart();
|
|
825
1186
|
});
|
|
826
1187
|
pi.on("agent_end", () => {
|
|
@@ -936,6 +1297,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
936
1297
|
},
|
|
937
1298
|
});
|
|
938
1299
|
|
|
1300
|
+
registerBackgroundJobTools(pi, { registry: jobs, monitors, sessionId: (ctx) => ctx.sessionManager.getSessionId() ?? "", now: deps.now, onChange: () => { refreshJobStatus(); requestRender(); } });
|
|
1301
|
+
|
|
939
1302
|
pi.registerMessageRenderer(AGENTS_MESSAGE_TYPE, (message, options, theme) => {
|
|
940
1303
|
const details = (message.details as { gentleAgents?: { taskId?: unknown; agent?: unknown } } | undefined)?.gentleAgents;
|
|
941
1304
|
const taskId = typeof details?.taskId === "string" ? details.taskId : "unknown";
|
|
@@ -946,11 +1309,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
946
1309
|
});
|
|
947
1310
|
|
|
948
1311
|
pi.registerMessageRenderer(AGENTS_ORCHESTRATOR_MESSAGE_TYPE, (message, options, theme) => {
|
|
949
|
-
const details = (message.details as { gentleAgents?:
|
|
950
|
-
|
|
951
|
-
const heading = `⇄ Orchestrator message · Received · From ${sanitizeTerminalText(sender)}`;
|
|
952
|
-
const body = sanitizeTerminalText(messageText(message.content));
|
|
953
|
-
return new Text(`${theme.fg("customMessageLabel", heading)}\n${theme.fg("customMessageText", body)}`, options.outputPad, 0);
|
|
1312
|
+
const details = (message.details as { gentleAgents?: OrchestratorMessageDetails } | undefined)?.gentleAgents ?? {};
|
|
1313
|
+
return incomingMessageCard(details, messageText(message.content), options.expanded, theme, expandHint(options.expanded));
|
|
954
1314
|
});
|
|
955
1315
|
|
|
956
1316
|
pi.registerMessageRenderer(AGENTS_RESULT_TYPE, (message, options, theme) => {
|
|
@@ -1051,6 +1411,14 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1051
1411
|
return stored?.task;
|
|
1052
1412
|
};
|
|
1053
1413
|
|
|
1414
|
+
// A guessed id ("1") leads back to real ids instead of a dead end, so the
|
|
1415
|
+
// parent retries the same call rather than re-summarizing it (gentle-shell#1713).
|
|
1416
|
+
const unknownTask = (id: unknown, ctx: ExtensionContext | undefined) => {
|
|
1417
|
+
const recent = [...store.list(ctx?.sessionManager?.getSessionId() ?? "")].sort((a, b) => b.createdAt - a.createdAt).slice(0, 5);
|
|
1418
|
+
const hint = recent.length ? ` Recent task ids: ${recent.map((task) => `${task.id} (${task.agent})`).join(", ")}.` : "";
|
|
1419
|
+
return text(`Error: no task ${String(id)}.${hint}`, { error: "unknown task" });
|
|
1420
|
+
};
|
|
1421
|
+
|
|
1054
1422
|
// Restores this exact session's own finished subagents as visible history
|
|
1055
1423
|
// on an explicit resume, or on startup into a session that already has
|
|
1056
1424
|
// entries -- never on new, fork, or reload (see the session_start handler's
|
|
@@ -1065,12 +1433,12 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1065
1433
|
} catch {
|
|
1066
1434
|
return;
|
|
1067
1435
|
}
|
|
1068
|
-
//
|
|
1069
|
-
// must never
|
|
1070
|
-
if (ctx.sessionManager.getSessionId() !== sessionId) return;
|
|
1071
|
-
// Fire-and-forget from session_start: a throwing summary subscriber must
|
|
1072
|
-
// never surface as an unhandled rejection. History is best-effort.
|
|
1436
|
+
// Fire-and-forget from session_start: a stale SDK context or throwing
|
|
1437
|
+
// summary subscriber must never surface as an unhandled rejection.
|
|
1073
1438
|
try {
|
|
1439
|
+
// The session may have moved on while disk was read; a stale restore
|
|
1440
|
+
// must never land in the wrong session's store.
|
|
1441
|
+
if (ctx.sessionManager.getSessionId() !== sessionId) return;
|
|
1074
1442
|
for (const { task, thread } of history) {
|
|
1075
1443
|
if (task.parentSessionId !== sessionId) continue;
|
|
1076
1444
|
if (store.restore(task, thread)) restoredTaskIds.add(task.id);
|
|
@@ -1188,7 +1556,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1188
1556
|
const buildRequest = async (ctx: ExtensionContext, agent: AgentDefinition, prompt: string, label: string | undefined, context: string | undefined, mode: AgentMode, resume?: string, workspaceRoot?: string, signal?: AbortSignal, repositoryRoot?: string): Promise<TaskRequest> => {
|
|
1189
1557
|
if (retiredSddAgent(agent.name)) throw new Error("Retired SDD agents cannot be dispatched.");
|
|
1190
1558
|
if (![AGENT_MODE.TASK, AGENT_MODE.BACKGROUND].includes(mode)) throw new Error("Subagent mode must be task or background.");
|
|
1191
|
-
if (ctx.mode
|
|
1559
|
+
if (isSingleShotMode(ctx.mode) && mode === AGENT_MODE.BACKGROUND) throw new Error(SINGLE_SHOT_BACKGROUND_ERROR);
|
|
1192
1560
|
const scopeDenied = rejectUnscopedBoundedWriterDispatch({ agent: agent.name, task: prompt, context });
|
|
1193
1561
|
if (scopeDenied) throw new Error(scopeDenied.reason);
|
|
1194
1562
|
if (signal?.aborted) throw new Error("Subagent launch aborted before authorization.");
|
|
@@ -1201,10 +1569,27 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1201
1569
|
if (isGenericBoundedWriter(agent.name) && !current()) throw new Error("Writer session Git authority changed before admission.");
|
|
1202
1570
|
let admittedModel: string | undefined;
|
|
1203
1571
|
const surfaces = allowedEditSurfaces(prompt, context);
|
|
1572
|
+
// gentle-shell#1064 slice 2: the session profile is resolved once per task
|
|
1573
|
+
// request, before admission, so the admitted model and the launch routing
|
|
1574
|
+
// resolve the same session layer and can never disagree about it (#1558: a
|
|
1575
|
+
// bound session used to kill its own non-git writer mid-preparation because
|
|
1576
|
+
// admission still read only the pin/global layers). Slice 3b-ii: the same
|
|
1577
|
+
// read covers the frozen inherited profile and `follow` mode.
|
|
1578
|
+
const profileConfigHome = gentlePiConfigHome(deps.env);
|
|
1579
|
+
const sessionProfile = resolveSessionProfile({
|
|
1580
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
1581
|
+
cwd: originalCwd,
|
|
1582
|
+
configHome: profileConfigHome,
|
|
1583
|
+
resolveWorktree: deps.resolveWorktree,
|
|
1584
|
+
env: deps.env,
|
|
1585
|
+
});
|
|
1204
1586
|
if (!resume && isGenericBoundedWriter(agent.name) && surfaces?.some(isDevelopmentSurface) && !deps.resolveWorktree(originalCwd, originalCwd) && repositoryRoot === undefined) {
|
|
1205
1587
|
const root = safeBootstrapDirectory(originalCwd);
|
|
1206
1588
|
if (!root || (workspaceRoot !== undefined && (!isAbsolute(workspaceRoot) || safeBootstrapDirectory(workspaceRoot) !== root))) throw new Error("Writer bootstrap requires the original safe project root.");
|
|
1207
|
-
const config = withPinnedModelProfiles(
|
|
1589
|
+
const config = withPinnedModelProfiles(
|
|
1590
|
+
loadAgentsConfig(roots(ctx)),
|
|
1591
|
+
sessionProfileRoutingAt(sessionProfile, { cwd: root, configHome: profileConfigHome, resolveWorktree: deps.resolveWorktree }),
|
|
1592
|
+
);
|
|
1208
1593
|
const model = resolveAgentProfile(agent, config).model ?? ctx.model;
|
|
1209
1594
|
const catalogModel = model?.provider ? ctx.modelRegistry?.find(model.provider, model.id) : ctx.modelRegistry?.getAll().find(candidate => candidate.id === model?.id);
|
|
1210
1595
|
if (!catalogModel) throw new Error("Writer bootstrap requires a valid effective model in this session's catalog.");
|
|
@@ -1249,14 +1634,34 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1249
1634
|
const pinIdentity: WorktreeResolver = foreign ? resolveSessionWorktree : target !== undefined && parentIdentity !== undefined && target === parentIdentity.root
|
|
1250
1635
|
? () => parentIdentity
|
|
1251
1636
|
: deps.resolveWorktree;
|
|
1637
|
+
// gentle-shell#1064: the session profile outranks the shared defaults for
|
|
1638
|
+
// launches from that session (`session → frozen p → P → G`), with the same
|
|
1639
|
+
// wholesale-replacement contract as the pin. Explicit and frozen profiles
|
|
1640
|
+
// belong to the session and apply to every target, a foreign repository
|
|
1641
|
+
// included; only `follow` reads the defaults of the target. The profile is
|
|
1642
|
+
// resolved here, at task-request creation, so queued and running children
|
|
1643
|
+
// keep the routing frozen into their requests even if the session rebinds.
|
|
1252
1644
|
const config = withPinnedModelProfiles(
|
|
1253
1645
|
loadAgentsConfig(roots(ctx)),
|
|
1254
|
-
|
|
1646
|
+
sessionProfileRoutingAt(sessionProfile, {
|
|
1255
1647
|
cwd: target ?? parentCwd,
|
|
1256
|
-
configHome:
|
|
1648
|
+
configHome: profileConfigHome,
|
|
1257
1649
|
resolveWorktree: pinIdentity,
|
|
1258
|
-
|
|
1650
|
+
foreignRepository: foreign,
|
|
1651
|
+
}),
|
|
1259
1652
|
);
|
|
1653
|
+
// Drift is measured against the defaults of the session's own directory,
|
|
1654
|
+
// where the profile was frozen, never against a delegated target.
|
|
1655
|
+
if (ctx.hasUI) {
|
|
1656
|
+
const drift = inheritedProfileDriftNotice({
|
|
1657
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
1658
|
+
cwd: parentCwd,
|
|
1659
|
+
configHome: profileConfigHome,
|
|
1660
|
+
resolveWorktree: deps.resolveWorktree,
|
|
1661
|
+
env: deps.env,
|
|
1662
|
+
});
|
|
1663
|
+
if (drift !== undefined) ctx.ui.notify(drift, "info");
|
|
1664
|
+
}
|
|
1260
1665
|
const profile = resolveAgentProfile(agent, config);
|
|
1261
1666
|
if (admittedModel !== undefined) {
|
|
1262
1667
|
const model = profile.model ?? ctx.model;
|
|
@@ -1301,7 +1706,10 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1301
1706
|
thinking: profile.thinking,
|
|
1302
1707
|
sessionDir,
|
|
1303
1708
|
resumeSessionPath: resume,
|
|
1304
|
-
...(deps.
|
|
1709
|
+
...childExtensionRequest(deps.env, deps.childExtensionPaths),
|
|
1710
|
+
// A writer claims its surfaces in its canonical worktree root for its queued
|
|
1711
|
+
// and running lifetime; a continuation is a new task and claims them again.
|
|
1712
|
+
...(isBoundedWriter(agent.name) && surfaces ? { writerSurfaces: surfaces, writerRoot: canonicalWriterRoot(target ?? parentWorktreeRoot, foreign ? resolveSessionWorktree : deps.resolveWorktree) } : {}),
|
|
1305
1713
|
env: childEnv,
|
|
1306
1714
|
...(foreign || parentRepositoryIdentity === undefined ? {} : {
|
|
1307
1715
|
authorizeParentStandingReviewPermission: (repositoryIdentity: string) => {
|
|
@@ -1325,10 +1733,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1325
1733
|
return request;
|
|
1326
1734
|
};
|
|
1327
1735
|
|
|
1328
|
-
const launch = async (ctx: ExtensionContext, request: TaskRequest, signal?: AbortSignal): Promise<ToolText> => {
|
|
1329
|
-
if (ctx.mode
|
|
1330
|
-
throw new Error("Background subagents are unavailable in print mode: pi -p exits before a parent session can receive results. Use task mode, RPC mode, or interactive Pi.");
|
|
1331
|
-
}
|
|
1736
|
+
const launch = async (ctx: ExtensionContext, request: TaskRequest, signal?: AbortSignal, publishWork?: (id: string) => void): Promise<ToolText> => {
|
|
1737
|
+
if (isSingleShotMode(ctx.mode) && request.mode === AGENT_MODE.BACKGROUND) throw new Error(SINGLE_SHOT_BACKGROUND_ERROR);
|
|
1332
1738
|
if (retiredSddAgent(request.agent.name)) throw new Error("Retired SDD agents cannot be dispatched.");
|
|
1333
1739
|
|
|
1334
1740
|
// Bounded live observation only. Native send owns the fresh policy decision;
|
|
@@ -1362,6 +1768,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1362
1768
|
const foreignRequest = foreignRequests.get(request);
|
|
1363
1769
|
if (launched && foreignRequest) foreignTasks.set(task.id, foreignRequest);
|
|
1364
1770
|
ownedTaskIds.add(task.id);
|
|
1771
|
+
publishWork?.(task.id);
|
|
1772
|
+
publishActivity(); // Admission's summary notification precedes runtime ownership.
|
|
1365
1773
|
store.subscribe(task.id, () => { publishActivity(); requestRender(); });
|
|
1366
1774
|
if (request.mode === AGENT_MODE.BACKGROUND) return text(`Started ${task.agent} in the background as task ${task.id}. Retain that id; completion is pushed automatically. Never sleep or periodically poll subagent_status/subagent_result for completion or cache maintenance. Inspect status only at a real orchestration decision boundary; never relaunch equivalent queued/running work.`, taskDetails(task));
|
|
1367
1775
|
// A tool call aborted by the host (a human interrupting the turn, a timeout)
|
|
@@ -1393,7 +1801,9 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1393
1801
|
const finished = await runner.waitFor(task.id);
|
|
1394
1802
|
completions.consume(finished.id);
|
|
1395
1803
|
messages.invalidateTask(finished.id);
|
|
1396
|
-
|
|
1804
|
+
// The id rides in the text too: details never reach the model, which
|
|
1805
|
+
// otherwise guesses ordinal ids for subagent_continue (#1731 T19).
|
|
1806
|
+
return text(completionText(finished), taskDetails(finished));
|
|
1397
1807
|
} finally {
|
|
1398
1808
|
signal?.removeEventListener("abort", onAbort);
|
|
1399
1809
|
}
|
|
@@ -1419,7 +1829,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1419
1829
|
return { render: () => [], invalidate() {} };
|
|
1420
1830
|
}
|
|
1421
1831
|
const body = result.content.map((part) => (part.type === "text" ? part.text : "")).join("\n");
|
|
1422
|
-
const visibleBody = options.expanded ? body : theme.fg("muted", body.split("\n")[0] ?? "");
|
|
1832
|
+
const visibleBody = options.expanded ? body : theme.fg("muted", agentResultPreview(body).split("\n")[0] ?? "");
|
|
1423
1833
|
const title = `${AGENTS_GLYPH} agent result${task?.taskId ? ` · ${sanitizeTerminalText(task.taskId)}` : ""}`;
|
|
1424
1834
|
return new Text(name === "result" ? `${theme.fg("toolTitle", title)}\n${visibleBody}` : visibleBody, 0, 0);
|
|
1425
1835
|
},
|
|
@@ -1432,25 +1842,150 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1432
1842
|
pi.registerTool({
|
|
1433
1843
|
name: "orchestrator_session_id",
|
|
1434
1844
|
label: "Orchestrator session ID",
|
|
1435
|
-
description: "Return this host session's
|
|
1436
|
-
|
|
1437
|
-
|
|
1845
|
+
description: "Return this host session's stable routing ID, Initial alias (FIRST TASK), Current alias (CURRENT TASK), and separate human Session name. Explicit subjects update Current even when named; Initial stays the first declared branch topic, unknown for legacy history. Before delegation or cross-session coordination, declare a short recognizable subject here. Do not require a subject declaration for small direct tasks; do not query all peers. Names never authenticate. Existing Pi names and human renames are preserved. Use a concise non-sensitive label, not a prompt. For work beyond small direct tasks, publish subject and current/next status here; refresh at task changes/completion. Owner-curated state (2048 UTF-8 bytes total): null withdraws, omission leaves unchanged. Never include credentials, internal instructions, or raw prompts. Historical notes are not consent or an owner reply.",
|
|
1846
|
+
...orchestratorToolRenderers("session", expandHint),
|
|
1847
|
+
parameters: { type: "object", additionalProperties: false, properties: {
|
|
1848
|
+
subject: { type: "string", maxLength: 120, description: "Optional short task subject; updates task aliases independently of human names. Also names an unnamed Pi session." },
|
|
1849
|
+
state: { anyOf: [{ type: "null" }, { type: "object", additionalProperties: false, properties: {
|
|
1850
|
+
objective: { type: "string" }, progress: { type: "string" }, decisions: { type: "string" }, blockers: { type: "string" },
|
|
1851
|
+
work: { type: "object", additionalProperties: false,
|
|
1852
|
+
description: "Optional historical, non-authoritative classification; topic requires area. Text plus work JSON fits 2048 UTF-8 bytes. Exact duplicate tags/refs are rejected; no ref resolution or routing.",
|
|
1853
|
+
properties: {
|
|
1854
|
+
...workDescriptorSchema.properties,
|
|
1855
|
+
tasks: { type: "object", maxProperties: 8, propertyNames: { type: "string", maxLength: 256 },
|
|
1856
|
+
additionalProperties: workDescriptorSchema,
|
|
1857
|
+
description: "Exact actual owner-declared task IDs, at most 256 UTF-8 bytes; controls, surrogates and __proto__/prototype/constructor rejected. Root-only; nonempty tasks alone are valid. Replacement omitting tasks clears annotations." },
|
|
1858
|
+
},
|
|
1859
|
+
},
|
|
1860
|
+
} }] },
|
|
1861
|
+
} } as never,
|
|
1862
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1438
1863
|
const transport = activeTransportFor(ctx);
|
|
1439
|
-
|
|
1864
|
+
if (!transport) return text("Error: session messaging is not ready.", { error: "not ready" });
|
|
1865
|
+
const { subject, state } = params as { subject?: unknown; state?: unknown };
|
|
1866
|
+
const declared = normalizeTaskSubject(subject);
|
|
1867
|
+
if (state !== undefined || declared) stateCache.publish(ctx.sessionManager, state, (type, data) => pi.appendEntry(type, data), Date.now(), declared);
|
|
1868
|
+
if (activeTransportFor(ctx) !== transport) throw new Error("stale-published-state");
|
|
1869
|
+
if (declared && !ctx.sessionManager.getSessionName?.()) pi.setSessionName(declared);
|
|
1870
|
+
const alias = sanitizeDisplayLabel(ctx.sessionManager.getSessionName?.() ?? "");
|
|
1871
|
+
const aliases = stateCache.get(ctx.sessionManager)?.aliases;
|
|
1872
|
+
if (state !== undefined || declared) publishActivity();
|
|
1873
|
+
presence?.refreshLabel();
|
|
1874
|
+
return text(`Active session ID: ${transport.sessionId}\nCurrent alias (CURRENT TASK): ${aliases?.currentAlias ?? "unknown"}\nInitial alias (FIRST TASK): ${aliases?.initialAlias ?? "unknown"}\nSession name: ${alias || "unnamed"}`,
|
|
1875
|
+
{ gentleAgents: { senderSessionId: transport.sessionId, alias, sessionName: alias, initialAlias: aliases?.initialAlias ?? null, currentAlias: aliases?.currentAlias ?? null } });
|
|
1876
|
+
},
|
|
1877
|
+
});
|
|
1878
|
+
pi.registerTool({
|
|
1879
|
+
name: "orchestrator_consult",
|
|
1880
|
+
label: "Consult published context",
|
|
1881
|
+
...orchestratorToolRenderers("consult", expandHint),
|
|
1882
|
+
description: "Read published metadata (default), request bounded helper reasoning with explicit UI model-cost permission and a question, or revoke-reasoning for an exact target. Never an owner reply, consent or private context access.",
|
|
1883
|
+
parameters: { type: "object", additionalProperties: false, required: ["recipient_session_id"], properties: {
|
|
1884
|
+
kind: { type: "string", enum: ["metadata", "reasoning", "revoke-reasoning"] },
|
|
1885
|
+
question: { type: "string", maxLength: 1024 },
|
|
1886
|
+
recipient_session_id: { type: "string", pattern: "^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$" },
|
|
1887
|
+
cursor: { type: "string", maxLength: 1024 },
|
|
1888
|
+
} } as never,
|
|
1889
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
1890
|
+
const input = params as { kind?: unknown; recipient_session_id?: unknown; cursor?: unknown; question?: unknown };
|
|
1891
|
+
const kind = input?.kind === undefined ? "metadata" : input.kind;
|
|
1892
|
+
if (!input || Object.keys(input).some(k => !["kind", "recipient_session_id", "cursor", "question"].includes(k))
|
|
1893
|
+
|| !["metadata", "reasoning", "revoke-reasoning"].includes(kind as string) || !validTransportSessionId(input.recipient_session_id)
|
|
1894
|
+
|| (kind === "revoke-reasoning" && input.cursor !== undefined)
|
|
1895
|
+
|| (kind !== "reasoning" && input.question !== undefined)
|
|
1896
|
+
|| (kind === "reasoning" && (typeof input.question !== "string" || !input.question.trim()
|
|
1897
|
+
|| Buffer.byteLength(input.question) > 1024 || /[\p{Cc}\p{Cf}\p{Cs}]/u.test(input.question)))
|
|
1898
|
+
|| (input.cursor !== undefined && (typeof input.cursor !== "string" || input.cursor.length > 1024))) throw new Error("Invalid metadata consultation parameters.");
|
|
1899
|
+
const selection = { recipientSessionId: input.recipient_session_id, cursor: input.cursor as string | undefined };
|
|
1900
|
+
const transport = activeTransportFor(ctx);
|
|
1901
|
+
const result = (receipt: MetadataReceipt) => {
|
|
1902
|
+
const output = kind === "metadata" ? receipt : { status: "unavailable", code: receipt.unknowns[0], source: "helper_advice", ownerReply: false, authority: "none" };
|
|
1903
|
+
return text(JSON.stringify(output), { gentleAgents: { senderSessionId: transport?.sessionId, receipt: output } });
|
|
1904
|
+
};
|
|
1905
|
+
if (!transport) return result(unavailableMetadata(selection.recipientSessionId, "not-ready"));
|
|
1906
|
+
const reply = (receipt: unknown) => text(JSON.stringify(receipt), { gentleAgents: { senderSessionId: transport.sessionId, receipt } });
|
|
1907
|
+
if (kind === "revoke-reasoning") {
|
|
1908
|
+
helperPermission.revoke(selection.recipientSessionId);
|
|
1909
|
+
return reply({ status: "revoked", source: "helper_advice", ownerReply: false, authority: "none" });
|
|
1910
|
+
}
|
|
1911
|
+
try {
|
|
1912
|
+
const activations = await transport.listener.registry.listActivations(transport.sessionId);
|
|
1913
|
+
if (activeTransportFor(ctx) !== transport) return result(unavailableMetadata(selection.recipientSessionId, "source-session-changed"));
|
|
1914
|
+
const receipt = consultPublishedMetadata(agentHome, activations, selection);
|
|
1915
|
+
if (kind === "metadata") return result(receipt);
|
|
1916
|
+
const current = () => activeTransportFor(ctx) === transport && parentCtx?.sessionManager === ctx.sessionManager;
|
|
1917
|
+
const readSource = async () => {
|
|
1918
|
+
const peers = await transport.listener.registry.listActivations(transport.sessionId);
|
|
1919
|
+
return current() ? consultPublishedMetadata(agentHome, peers, selection)
|
|
1920
|
+
: unavailableMetadata(selection.recipientSessionId, "source-session-changed");
|
|
1921
|
+
};
|
|
1922
|
+
const advice = await helperPermission.run({ receipt, question: input.question as string, signal: _signal,
|
|
1923
|
+
readSource, isSourceCurrent: () => current() && consultPublishedMetadata(agentHome, activations, selection).digest === receipt.digest });
|
|
1924
|
+
return reply(advice);
|
|
1925
|
+
} catch { return result(unavailableMetadata(selection.recipientSessionId, "discovery-unavailable")); }
|
|
1440
1926
|
},
|
|
1441
1927
|
});
|
|
1442
1928
|
pi.registerTool({
|
|
1443
1929
|
name: "orchestrator_list",
|
|
1444
1930
|
label: "List orchestrators",
|
|
1445
|
-
|
|
1446
|
-
|
|
1931
|
+
...orchestratorToolRenderers("list", expandHint),
|
|
1932
|
+
description: "List other sessions advertised by the trusted local profile. Optional filter searches only published classified work with bounded, non-exhaustive coverage, no authority and unknown reachability. Omit filter for the existing session list.",
|
|
1933
|
+
parameters: Type.Object({
|
|
1934
|
+
recipient_session_id: Type.Optional(Type.String({ description: "Exact routing ID of the peer to inspect." })),
|
|
1935
|
+
cursor: Type.Optional(Type.String({ description: "Opaque catalog continuation from that peer; requires recipient_session_id." })),
|
|
1936
|
+
filter: Type.Optional(Type.Object({
|
|
1937
|
+
area: Type.Optional(Type.String()),
|
|
1938
|
+
topic: Type.Optional(Type.String({ description: "Requires area." })),
|
|
1939
|
+
tag: Type.Optional(Type.String()),
|
|
1940
|
+
text: Type.Optional(Type.String({ description: "Literal label/descriptor search, never state prose or history." })),
|
|
1941
|
+
ref: Type.Optional(Type.Object({
|
|
1942
|
+
kind: Type.Union([Type.Literal("issue"), Type.Literal("pr"), Type.Literal("task")]),
|
|
1943
|
+
repository: Type.String({ description: "Exact public host/owner/repo scope." }),
|
|
1944
|
+
id: Type.String(),
|
|
1945
|
+
}, { additionalProperties: false })),
|
|
1946
|
+
repository_root: Type.Optional(Type.String({ description: "Recorded absolute Git root; no new Git probe." })),
|
|
1947
|
+
related_to: Type.Optional(Type.Object({
|
|
1948
|
+
session_id: Type.String({ description: "Exact stable source owner session ID." }),
|
|
1949
|
+
task_id: Type.Optional(Type.String({ description: "Actual task ID on the current catalog page, not child session ID." })),
|
|
1950
|
+
}, { additionalProperties: false })),
|
|
1951
|
+
}, { additionalProperties: false, description: "Explicit {} indexes classified work; criteria combine with AND." })),
|
|
1952
|
+
}, { additionalProperties: false }),
|
|
1447
1953
|
async execute(_id, _params, _signal, _onUpdate, ctx) {
|
|
1954
|
+
const params = _params as { recipient_session_id?: string; cursor?: string; filter?: unknown };
|
|
1955
|
+
let filter;
|
|
1956
|
+
try {
|
|
1957
|
+
if (!params || typeof params !== "object" || Array.isArray(params)
|
|
1958
|
+
|| Object.keys(params).some(key => !["recipient_session_id", "cursor", "filter"].includes(key))
|
|
1959
|
+
|| (params.recipient_session_id !== undefined && !validTransportSessionId(params.recipient_session_id))
|
|
1960
|
+
|| (params.cursor !== undefined && (typeof params.cursor !== "string" || params.cursor.length > 1024))) throw new Error();
|
|
1961
|
+
if (Object.hasOwn(params, "filter")) filter = validateWorkFilter(params.filter);
|
|
1962
|
+
} catch { throw new Error("Invalid orchestrator list parameters."); }
|
|
1963
|
+
if (params.cursor !== undefined && !params.recipient_session_id) return text("Error: cursor requires recipient_session_id.", { error: "invalid-cursor" });
|
|
1448
1964
|
const transport = activeTransportFor(ctx);
|
|
1449
1965
|
if (!transport) return text("Error: session discovery is not ready.", { error: "not ready" });
|
|
1450
1966
|
try {
|
|
1451
|
-
const
|
|
1967
|
+
const activations = await transport.listener.registry.listActivations(transport.sessionId);
|
|
1452
1968
|
if (activeTransportFor(ctx) !== transport) return text("Error: session discovery became unavailable before results were confirmed.", { error: "stale" });
|
|
1453
|
-
|
|
1969
|
+
if (filter !== undefined) {
|
|
1970
|
+
const workSearch = searchPublishedWork(agentHome, activations, filter, params.recipient_session_id
|
|
1971
|
+
? { recipientSessionId: params.recipient_session_id, cursor: params.cursor } : undefined);
|
|
1972
|
+
return text(JSON.stringify(workSearch), { gentleAgents: { workSearch } });
|
|
1973
|
+
}
|
|
1974
|
+
const peers = discoverOrchestrators(agentHome, activations, Date.now(), params.recipient_session_id
|
|
1975
|
+
? { recipientSessionId: params.recipient_session_id, cursor: params.cursor } : undefined);
|
|
1976
|
+
const repository = (fact?: RepositoryFact) => fact?.root
|
|
1977
|
+
? `repository: ${fact.root} · clone: ${fact.cloneHash} · Git resolved at: ${fact.resolvedAt} (${fact.source})`
|
|
1978
|
+
: "repository: unknown";
|
|
1979
|
+
const rows = peers.map(peer => {
|
|
1980
|
+
const context = peer.freshness === "recent" ? ` · ${peer.aliases?.currentAlias || peer.label || "unnamed"} · Session name: ${peer.label || "unnamed"} · Current alias (CURRENT TASK): ${peer.aliases?.currentAlias ?? "unknown"} · Initial alias (FIRST TASK): ${peer.aliases?.initialAlias ?? "unknown"} · recorded workspace: ${peer.workspace || "unknown"}` : " · context: unknown";
|
|
1981
|
+
const tasks = peer.tasks?.map(task => `\n - ${task.label || task.id} [${task.status}] · launch workspace: ${task.workspace || "unknown"} · ${repository(peer.scope?.tasks.find(t => t.id === task.id)?.repository)}`).join("") ?? "";
|
|
1982
|
+
const registered = peer.scope?.registered.map(fact => `\n registered: ${repository(fact)}`).join("") ?? "";
|
|
1983
|
+
const gaps = peer.scope && !peer.scope.complete ? `\n (scope incomplete: ${peer.scope.omittedTasks} tasks, ${peer.scope.omittedRegistered} registered roots omitted)` : "";
|
|
1984
|
+
const catalog = peer.catalog ? `\n recorded catalog (not Git identity): ${JSON.stringify(peer.catalog)}` : "\n recorded catalog: unknown";
|
|
1985
|
+
const note = params.recipient_session_id ? `\n owner-curated recorded state (not consent or owner reply; authority none): ${peer.state ? JSON.stringify(peer.state) : "unknown"}` : "";
|
|
1986
|
+
return `- ${peer.sessionId}${context} · ${repository(peer.scope?.host)} · metadata: ${peer.freshness}${tasks}${registered}${gaps}${catalog}${note}${peer.omitted ? `\n (${peer.omitted} more tasks omitted)` : ""}`;
|
|
1987
|
+
});
|
|
1988
|
+
return peers.length === 0 ? text("No other sessions are currently advertised. Advertisements have unknown reachability and do not guarantee a live session.") : text(`Advertised sessions (reachability is unknown):\n${rows.join("\n")}`, { gentleAgents: { candidates: peers } });
|
|
1454
1989
|
} catch {
|
|
1455
1990
|
return text("Error: session discovery is unavailable.", { error: "unavailable" });
|
|
1456
1991
|
}
|
|
@@ -1459,6 +1994,13 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1459
1994
|
pi.registerTool({
|
|
1460
1995
|
name: "orchestrator_send_message",
|
|
1461
1996
|
label: "Send orchestrator message",
|
|
1997
|
+
renderShell: "self",
|
|
1998
|
+
renderCall(args, theme, context) {
|
|
1999
|
+
return outgoingMessageCall(args as { message?: unknown }, theme, context, expandHint(context.expanded));
|
|
2000
|
+
},
|
|
2001
|
+
renderResult(result, options, theme, context) {
|
|
2002
|
+
return outgoingMessageResult(result, options.expanded, options.isPartial, theme, context);
|
|
2003
|
+
},
|
|
1462
2004
|
description: "Send a notification to another active session in this trusted local profile. If recipient_session_id is omitted, the sole peer is selected or the user selects one. Acceptance means enqueued, not read or completed.",
|
|
1463
2005
|
parameters: {
|
|
1464
2006
|
type: "object",
|
|
@@ -1517,16 +2059,16 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1517
2059
|
}
|
|
1518
2060
|
try {
|
|
1519
2061
|
const accepted = await transport.client.sendNotification(recipient, message, { signal, expectedActivation: activation, beforeConnect: () => activeTransportFor(ctx) === transport });
|
|
1520
|
-
return activeTransportFor(ctx) === transport ? text(`Message ${accepted.id} from ${transport.sessionId} to ${recipient} accepted for delivery; it is not a delivery or read receipt.`, { gentleAgents: { messageId: accepted.id, senderSessionId: transport.sessionId, recipientSessionId: recipient, state: "accepted" } }) : text("Error: session messaging is not ready.", { error: "stale" });
|
|
2062
|
+
return activeTransportFor(ctx) === transport ? text(`Message ${accepted.id} from ${transport.sessionId} to ${recipient} accepted for delivery; it is not a delivery or read receipt.`, { gentleAgents: { messageId: accepted.id, senderSessionId: transport.sessionId, recipientSessionId: recipient, state: "accepted", message, reason, senderLabel: sanitizeDisplayLabel(ctx.sessionManager.getSessionName?.() ?? ""), recipientLabel: messagePeerLabel(agentHome, recipient) } }) : text("Error: session messaging is not ready.", { error: "stale" });
|
|
1521
2063
|
} catch {
|
|
1522
2064
|
return text("Error: session message was not accepted.", { error: "not accepted" });
|
|
1523
2065
|
}
|
|
1524
2066
|
},
|
|
1525
2067
|
});
|
|
1526
2068
|
|
|
1527
|
-
tool("list_agents", "List the subagents defined for this project and user, with their descriptions.", { properties: {} }, async (_params, ctx) => {
|
|
2069
|
+
tool("list_agents", "List the subagents defined for this project and user, with their descriptions and declared tool inventory.", { properties: {} }, async (_params, ctx) => {
|
|
1528
2070
|
const { agents, errors } = discoverAgents(roots(ctx));
|
|
1529
|
-
const lines = agents.map((agent) => `- ${agent.name} (${agent.scope}): ${agent.description || "no description"}`);
|
|
2071
|
+
const lines = agents.map((agent) => `- ${agent.name} (${agent.scope}): ${agent.description || "no description"} ${declaredToolInventory(agent)}`);
|
|
1530
2072
|
const problems = errors.map((error) => `! ${error}`);
|
|
1531
2073
|
return text(lines.length === 0 ? "No subagents defined." : [...lines, ...problems].join("\n"));
|
|
1532
2074
|
});
|
|
@@ -1541,12 +2083,15 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1541
2083
|
task: { type: "string", description: "What the subagent must do, self-contained." },
|
|
1542
2084
|
label: { type: "string", description: "Three to six words naming the work, shown on the agents card, e.g. 'map footer data sources'." },
|
|
1543
2085
|
context: { type: "string", description: "Optional extra context appended to the task." },
|
|
2086
|
+
work: workDescriptorSchema,
|
|
1544
2087
|
workspace_root: { type: "string", description: "Optional canonical main or linked Git worktree within the parent's same clone only; mutually exclusive with repository_root." },
|
|
1545
2088
|
repository_root: { type: "string", description: "Optional canonical independent Git repository; requires direct interactive session-scoped consent before queueing; mutually exclusive with workspace_root." },
|
|
1546
2089
|
mode: { type: "string", enum: ["task", "background"], description: "task waits for the result (default); background returns immediately." },
|
|
1547
2090
|
},
|
|
1548
2091
|
},
|
|
1549
2092
|
async (params, ctx, signal) => {
|
|
2093
|
+
const work = Object.hasOwn(params, "work") ? decodeWorkDescriptor(params.work) : undefined;
|
|
2094
|
+
const manager = ctx.sessionManager, sessionId = manager.getSessionId();
|
|
1550
2095
|
if (typeof params.agent !== "string" || !params.agent.trim() || typeof params.task !== "string" || !params.task.trim() || (params.context !== undefined && typeof params.context !== "string") || (params.label !== undefined && typeof params.label !== "string")) throw new Error("Subagent dispatch requires a named agent, non-empty task and string context/label.");
|
|
1551
2096
|
if (params.mode !== undefined && params.mode !== AGENT_MODE.TASK && params.mode !== AGENT_MODE.BACKGROUND) throw new Error("Subagent mode must be task or background.");
|
|
1552
2097
|
// An empty selector names no destination: only real roots are mutually
|
|
@@ -1566,18 +2111,33 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1566
2111
|
});
|
|
1567
2112
|
const workspaceRoot = typeof params.workspace_root === "string" && params.workspace_root !== "" ? params.workspace_root : undefined;
|
|
1568
2113
|
const repositoryRoot = typeof params.repository_root === "string" && params.repository_root !== "" ? params.repository_root : undefined;
|
|
1569
|
-
|
|
2114
|
+
const publication: { status: "recorded" | "unavailable" } = { status: "unavailable" };
|
|
2115
|
+
const current = () => ctx.sessionManager === manager && manager.getSessionId() === sessionId && !!activeTransportFor(ctx);
|
|
2116
|
+
const result = await launch(ctx, await buildRequest(ctx, agent, String(params.task ?? ""), typeof params.label === "string" ? params.label : undefined, typeof params.context === "string" ? params.context : undefined, mode, undefined, workspaceRoot, signal, repositoryRoot), signal, work ? id => {
|
|
2117
|
+
try {
|
|
2118
|
+
if (!current()) return;
|
|
2119
|
+
const state = stateCache.get(manager)?.state ?? {};
|
|
2120
|
+
stateCache.publish(manager, { ...state, work: { ...state.work, tasks: { ...state.work?.tasks, [id]: work } } }, (type, data) => pi.appendEntry(type, data));
|
|
2121
|
+
if (current()) publication.status = "recorded";
|
|
2122
|
+
} catch { /* Optional metadata cannot invalidate an allocated task. */ }
|
|
2123
|
+
} : undefined);
|
|
2124
|
+
if (!work) return result;
|
|
2125
|
+
if (!current()) publication.status = "unavailable";
|
|
2126
|
+
const note = publication.status === "recorded"
|
|
2127
|
+
? "Work recorded in local curated state; peer advertisement is best-effort."
|
|
2128
|
+
: "Work publication unavailable/unknown. Do not relaunch this allocated task. Use orchestrator_session_id with a bounded replacement state and this actual task ID when the owner session is active.";
|
|
2129
|
+
return { ...result, content: [...result.content, { type: "text", text: note }], details: { ...result.details, workPublication: publication } };
|
|
1570
2130
|
},
|
|
1571
2131
|
);
|
|
1572
2132
|
|
|
1573
|
-
tool("status", "Report the status of one subagent task.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params) => {
|
|
2133
|
+
tool("status", "Report the status of one subagent task.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params, ctx) => {
|
|
1574
2134
|
const task = await resolveTask(String(params.task_id));
|
|
1575
|
-
return task ? text(describeTask(task), taskDetails(task)) :
|
|
2135
|
+
return task ? text(describeTask(task), taskDetails(task)) : unknownTask(params.task_id, ctx);
|
|
1576
2136
|
});
|
|
1577
2137
|
|
|
1578
|
-
tool("result", "Return the final answer of a finished subagent task, or its current state if it is still running.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params) => {
|
|
2138
|
+
tool("result", "Return the final answer of a finished subagent task, or its current state if it is still running.", { required: ["task_id"], properties: { task_id: { type: "string" } } }, async (params, ctx) => {
|
|
1579
2139
|
const task = await resolveTask(String(params.task_id));
|
|
1580
|
-
if (!task) return
|
|
2140
|
+
if (!task) return unknownTask(params.task_id, ctx);
|
|
1581
2141
|
// The parent just pulled a finished result; its pending completion must
|
|
1582
2142
|
// never be replayed on top of it.
|
|
1583
2143
|
if (isFinished(task.status)) {
|
|
@@ -1617,7 +2177,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1617
2177
|
async (params, ctx, signal) => {
|
|
1618
2178
|
if (Object.hasOwn(params, "sdd_change") || Object.hasOwn(params, "remediation") || Object.hasOwn(params, "research_selection")) return text("Error: retired SDD delegation is not supported.", { error: "retired SDD delegation" });
|
|
1619
2179
|
const previous = await resolveTask(String(params.task_id));
|
|
1620
|
-
if (!previous) return
|
|
2180
|
+
if (!previous) return unknownTask(params.task_id, ctx);
|
|
1621
2181
|
if (retiredSddAgent(previous.agent)) return text("Error: retired SDD agents cannot be continued.", { error: "retired SDD delegation" });
|
|
1622
2182
|
if (!isFinished(previous.status) || !previous.sessionPath) return text(`Error: task ${previous.id} cannot be continued yet (${previous.status}).`, { error: "not continuable" });
|
|
1623
2183
|
// Continuing acts on the previous result, so any pending completion for
|
|
@@ -1628,7 +2188,8 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1628
2188
|
if (!agent) return text(`Error: subagent "${previous.agent}" is no longer defined.`, { error: "unknown agent" });
|
|
1629
2189
|
const mode = (params.mode as AgentMode | undefined) ?? (previous.mode as AgentMode);
|
|
1630
2190
|
const foreignContinuation = foreignTasks.has(previous.id);
|
|
1631
|
-
|
|
2191
|
+
const prompt = inheritAllowedEditSurfaces(previous.agent, String(params.prompt ?? ""), params.context, previous.prompt);
|
|
2192
|
+
return launch(ctx, await buildRequest(ctx, agent, prompt, typeof params.label === "string" ? params.label : undefined, typeof params.context === "string" ? params.context : undefined, mode, previous.sessionPath, foreignContinuation ? undefined : previous.cwd, signal, foreignContinuation ? previous.cwd : undefined), signal);
|
|
1632
2193
|
},
|
|
1633
2194
|
);
|
|
1634
2195
|
|
|
@@ -1643,6 +2204,37 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1643
2204
|
});
|
|
1644
2205
|
}
|
|
1645
2206
|
|
|
2207
|
+
pi.registerCommand(JOBS_COMMAND_NAME, {
|
|
2208
|
+
description: "Show background jobs: running first, newest first within each group; status, command, output tail; s stops the selected job.",
|
|
2209
|
+
handler: async (_args, ctx) => {
|
|
2210
|
+
if (!ctx.hasUI) return;
|
|
2211
|
+
if (ctx.mode !== "tui") {
|
|
2212
|
+
ctx.ui.notify("The jobs overlay requires TUI mode.", "warning");
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
let view: JobsView | undefined;
|
|
2216
|
+
await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
|
|
2217
|
+
const close = withOverlayRepaint(tui, done);
|
|
2218
|
+
view = new JobsView({
|
|
2219
|
+
theme,
|
|
2220
|
+
rows: () => Math.max(0, tui.terminal.rows),
|
|
2221
|
+
jobs: () => jobs.list(ctx.sessionManager.getSessionId() ?? ""),
|
|
2222
|
+
now: () => deps.now(),
|
|
2223
|
+
// The agent was promised a notice, so a human stop is reported;
|
|
2224
|
+
// a job_stop needs none because the agent itself stopped it.
|
|
2225
|
+
onStop: (job) => {
|
|
2226
|
+
// Lines already seen are delivered before the stop notice.
|
|
2227
|
+
monitors.finish(job);
|
|
2228
|
+
const stopped = jobs.stop(job.id);
|
|
2229
|
+
if (stopped) queueJobNotice({ job: stopped, kind: "exit" });
|
|
2230
|
+
},
|
|
2231
|
+
onClose: () => close(null),
|
|
2232
|
+
requestRender: () => tui.requestRender(),
|
|
2233
|
+
});
|
|
2234
|
+
return view;
|
|
2235
|
+
}, { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", margin: 0, anchor: "center" } }).finally(() => view?.dispose());
|
|
2236
|
+
},
|
|
2237
|
+
});
|
|
1646
2238
|
pi.registerCommand(AGENTS_COMMAND_NAME, {
|
|
1647
2239
|
description: "Show this session's active subagents; a lists open orchestrators in this profile. Peer threads are read-only; o opens a local task's transcript in $EDITOR.",
|
|
1648
2240
|
handler: async (_args, ctx) => openOverlay(ctx),
|
|
@@ -1660,11 +2252,34 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1660
2252
|
});
|
|
1661
2253
|
}
|
|
1662
2254
|
|
|
2255
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
2256
|
+
helperPermission.clear();
|
|
2257
|
+
if (sessions !== ctx.sessionManager) return;
|
|
2258
|
+
stateCache.load(ctx.sessionManager);
|
|
2259
|
+
publishActivity();
|
|
2260
|
+
});
|
|
1663
2261
|
pi.on("session_start", async (event, ctx) => {
|
|
2262
|
+
// gentle-shell#1064 slice 3b-ii: a parent session that has no explicit
|
|
2263
|
+
// profile binding freezes `p → P → G` now. This handler is parent-only (a
|
|
2264
|
+
// child returns before registering it) and runs for every reason Pi
|
|
2265
|
+
// reports, including `startup` for --resume/--continue/--session. The
|
|
2266
|
+
// freeze is in-memory and idempotent per session id: /reload keeps it, a
|
|
2267
|
+
// new process freezes again until slice 3b-i persists it as an entry.
|
|
2268
|
+
try {
|
|
2269
|
+
freezeSessionProfileAtStartup({
|
|
2270
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
2271
|
+
cwd: ctx.sessionManager.getCwd(),
|
|
2272
|
+
configHome: gentlePiConfigHome(deps.env),
|
|
2273
|
+
resolveWorktree: deps.resolveWorktree,
|
|
2274
|
+
env: deps.env,
|
|
2275
|
+
});
|
|
2276
|
+
} catch { /* A launch freezes lazily through the same resolver. */ }
|
|
2277
|
+
stateCache.load(ctx.sessionManager);
|
|
1664
2278
|
// A resumed, reloaded, or replaced session starts with an empty completion
|
|
1665
2279
|
// queue so nothing pending from another session can replay here.
|
|
1666
2280
|
completions.dropAll();
|
|
1667
2281
|
messages.dropAll();
|
|
2282
|
+
jobNotices.length = 0;
|
|
1668
2283
|
resetParentDelivery(ctx);
|
|
1669
2284
|
presence?.dispose();
|
|
1670
2285
|
registryFor(ctx);
|
|
@@ -1682,8 +2297,7 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1682
2297
|
const preexisting = event.reason === "resume" || (event.reason === "startup" && ctx.sessionManager.getEntries().length > 0);
|
|
1683
2298
|
if (preexisting && sessionId) void restoreSessionHistory(ctx, sessionId);
|
|
1684
2299
|
try {
|
|
1685
|
-
presence =
|
|
1686
|
-
label: ctx.sessionManager.getSessionName?.() || ctx.sessionManager.getCwd().split(/[\\/]/).pop() || "Orchestrator", activity: [] });
|
|
2300
|
+
presence = startPresence(ctx.sessionManager);
|
|
1687
2301
|
publishActivity();
|
|
1688
2302
|
} catch { presence = undefined; }
|
|
1689
2303
|
void startSessionTransport(ctx);
|
|
@@ -1714,10 +2328,16 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1714
2328
|
pi.on("session_shutdown", async () => {
|
|
1715
2329
|
completions.dropAll();
|
|
1716
2330
|
messages.dropAll();
|
|
2331
|
+
// Jobs never outlive their session: a stopped job sends no notice.
|
|
2332
|
+
monitors.cancelAll();
|
|
2333
|
+
const jobsStopped = jobs.stopAll();
|
|
2334
|
+
jobNotices.length = 0;
|
|
2335
|
+
refreshJobStatus();
|
|
1717
2336
|
activeAgentRuns = 0;
|
|
1718
2337
|
resetParentDelivery(undefined);
|
|
1719
2338
|
presence?.dispose();
|
|
1720
2339
|
presence = undefined;
|
|
2340
|
+
stateCache.clear();
|
|
1721
2341
|
rpcActivityPublisher?.stop();
|
|
1722
2342
|
rpcActivityPublisher = undefined;
|
|
1723
2343
|
cancelClock?.();
|
|
@@ -1733,5 +2353,16 @@ export default function gentleAgents(pi: ExtensionAPI, env: NodeJS.ProcessEnv =
|
|
|
1733
2353
|
const stopped = shutdownSessionTransport();
|
|
1734
2354
|
runner.cancelAll("cancelled: parent session shut down");
|
|
1735
2355
|
await stopped;
|
|
2356
|
+
// The default log directory goes with the session once every log is
|
|
2357
|
+
// closed, but a process that ignores its stop must not hang shutdown.
|
|
2358
|
+
await new Promise<void>((done) => {
|
|
2359
|
+
const cancel = deps.schedule(done, JOBS_SHUTDOWN_GRACE_MS);
|
|
2360
|
+
void jobsStopped.then(() => { cancel(); done(); });
|
|
2361
|
+
});
|
|
2362
|
+
if (jobOutputDir) {
|
|
2363
|
+
const dir = jobOutputDir;
|
|
2364
|
+
jobOutputDir = undefined;
|
|
2365
|
+
try { rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }); } catch { /* A leftover temp directory is harmless. */ }
|
|
2366
|
+
}
|
|
1736
2367
|
});
|
|
1737
2368
|
}
|