@selesai/code 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/sdk.d.ts +2 -2
- package/dist/core/sdk.js +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/extensions/ascii.txt +9 -0
- package/dist/extensions/package.json +1 -2
- package/dist/extensions/pi-hermes-memory/CHANGELOG.md +371 -0
- package/dist/extensions/pi-hermes-memory/LICENSE +21 -0
- package/dist/extensions/pi-hermes-memory/README.md +643 -0
- package/dist/extensions/pi-hermes-memory/docs/0.1/TASKS.md +197 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/PLAN.md +290 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/TASKS.md +134 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/TEST-PLAN.md +216 -0
- package/dist/extensions/pi-hermes-memory/docs/0.3/PLAN.md +330 -0
- package/dist/extensions/pi-hermes-memory/docs/0.3/TASKS.md +125 -0
- package/dist/extensions/pi-hermes-memory/docs/0.4/PLAN.md +160 -0
- package/dist/extensions/pi-hermes-memory/docs/0.4/TASKS.md +113 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/CHANGELOG.md +57 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/PLAN.md +202 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/TASKS.md +144 -0
- package/dist/extensions/pi-hermes-memory/docs/0.7/PLAN.md +349 -0
- package/dist/extensions/pi-hermes-memory/docs/0.7/TASKS.md +110 -0
- package/dist/extensions/pi-hermes-memory/docs/PUBLISHING.md +149 -0
- package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +468 -0
- package/dist/extensions/pi-hermes-memory/docs/images/memory-architecture.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi-logo.svg +22 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.png +0 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.svg +19 -0
- package/dist/extensions/pi-hermes-memory/docs/images/security-flow.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/session-lifecycle.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/source-architecture.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/memory-architecture.mmd +49 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/security-flow.mmd +40 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/session-lifecycle.mmd +56 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/source-architecture.mmd +54 -0
- package/dist/extensions/pi-hermes-memory/package-lock.json +3799 -0
- package/dist/extensions/pi-hermes-memory/package.json +66 -0
- package/dist/extensions/pi-hermes-memory/scripts/check-min-sdk.mjs +99 -0
- package/dist/extensions/pi-hermes-memory/scripts/ensure-dev.mjs +57 -0
- package/dist/extensions/pi-hermes-memory/src/auto-consolidation-warning.ts +6 -0
- package/dist/extensions/pi-hermes-memory/src/config.ts +178 -0
- package/dist/extensions/pi-hermes-memory/src/constants.ts +424 -0
- package/dist/extensions/pi-hermes-memory/src/extension-root-migration.ts +639 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +401 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +297 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/child-process-watchdog.mjs +90 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/correction-detector.ts +300 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +86 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/insights.ts +78 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/interview.ts +37 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/learn-memory.ts +186 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/message-parts.ts +27 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/pi-child-process.ts +485 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/preview-context.ts +105 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +506 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +145 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-flush.ts +151 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-live-index.ts +94 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/skills-command.ts +1334 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/standing-pin.ts +111 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/switch-project.ts +75 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +266 -0
- package/dist/extensions/pi-hermes-memory/src/index.ts +378 -0
- package/dist/extensions/pi-hermes-memory/src/lifecycle-timing.ts +54 -0
- package/dist/extensions/pi-hermes-memory/src/paths.ts +61 -0
- package/dist/extensions/pi-hermes-memory/src/project-context.ts +13 -0
- package/dist/extensions/pi-hermes-memory/src/project-memory-migration.ts +93 -0
- package/dist/extensions/pi-hermes-memory/src/project.ts +157 -0
- package/dist/extensions/pi-hermes-memory/src/prompt-context.ts +55 -0
- package/dist/extensions/pi-hermes-memory/src/store/atomic-lock-coordinator.ts +375 -0
- package/dist/extensions/pi-hermes-memory/src/store/canonical-storage-path.ts +54 -0
- package/dist/extensions/pi-hermes-memory/src/store/content-scanner.ts +101 -0
- package/dist/extensions/pi-hermes-memory/src/store/db.ts +1180 -0
- package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +101 -0
- package/dist/extensions/pi-hermes-memory/src/store/markdown-mutation-lock.ts +38 -0
- package/dist/extensions/pi-hermes-memory/src/store/memory-lookup.ts +24 -0
- package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +1191 -0
- package/dist/extensions/pi-hermes-memory/src/store/recovery-maintenance.ts +66 -0
- package/dist/extensions/pi-hermes-memory/src/store/schema.ts +118 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-anchor-search.ts +472 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +511 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-parser.ts +213 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +234 -0
- package/dist/extensions/pi-hermes-memory/src/store/skill-store.ts +950 -0
- package/dist/extensions/pi-hermes-memory/src/store/skill-utils.ts +133 -0
- package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +1002 -0
- package/dist/extensions/pi-hermes-memory/src/store/sqlite-native.ts +241 -0
- package/dist/extensions/pi-hermes-memory/src/store/standing-instructions.ts +247 -0
- package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +95 -0
- package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +469 -0
- package/dist/extensions/pi-hermes-memory/src/tools/session-search-tool.ts +246 -0
- package/dist/extensions/pi-hermes-memory/src/tools/shared-output-view.ts +170 -0
- package/dist/extensions/pi-hermes-memory/src/tools/skill-tool.ts +333 -0
- package/dist/extensions/pi-hermes-memory/src/tools/tool-result-views.ts +105 -0
- package/dist/extensions/pi-hermes-memory/src/types.ts +214 -0
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +434 -0
- package/dist/extensions/pi-hermes-memory/tests/extension-root-migration.test.ts +368 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +984 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +950 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/correction-detector.test.ts +671 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/insights.test.ts +157 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/interview.test.ts +127 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/pi-child-process.test.ts +905 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/preview-context.test.ts +152 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/prompt-context.test.ts +166 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/resources-discover.test.ts +71 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +614 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +192 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-flush.test.ts +677 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-live-index.test.ts +225 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/skills-command.test.ts +827 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/standing-pin.test.ts +134 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/sync-markdown-memories.test.ts +491 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/system-prompt.test.ts +199 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/thinking-max.test.ts +64 -0
- package/dist/extensions/pi-hermes-memory/tests/index.test.ts +30 -0
- package/dist/extensions/pi-hermes-memory/tests/integration/flow.test.ts +126 -0
- package/dist/extensions/pi-hermes-memory/tests/lifecycle-timing.test.ts +76 -0
- package/dist/extensions/pi-hermes-memory/tests/paths.test.ts +23 -0
- package/dist/extensions/pi-hermes-memory/tests/project-memory-migration.test.ts +80 -0
- package/dist/extensions/pi-hermes-memory/tests/project-rebinding.test.ts +115 -0
- package/dist/extensions/pi-hermes-memory/tests/project.test.ts +120 -0
- package/dist/extensions/pi-hermes-memory/tests/run-all-timeout.test.ts +107 -0
- package/dist/extensions/pi-hermes-memory/tests/run-all.sh +45 -0
- package/dist/extensions/pi-hermes-memory/tests/store/atomic-lock-coordinator.test.ts +381 -0
- package/dist/extensions/pi-hermes-memory/tests/store/content-scanner.test.ts +388 -0
- package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +1046 -0
- package/dist/extensions/pi-hermes-memory/tests/store/markdown-mutation-lock.test.ts +34 -0
- package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +2150 -0
- package/dist/extensions/pi-hermes-memory/tests/store/recovery-maintenance.test.ts +170 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-anchor-search.test.ts +210 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +643 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-parser.test.ts +282 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +287 -0
- package/dist/extensions/pi-hermes-memory/tests/store/skill-store.test.ts +678 -0
- package/dist/extensions/pi-hermes-memory/tests/store/skill-utils.test.ts +21 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-lazy-load.test.ts +78 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +543 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +164 -0
- package/dist/extensions/pi-hermes-memory/tests/store/standing-instructions.test.ts +171 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +84 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +709 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/session-search-tool.test.ts +345 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/shared-output-view.test.ts +362 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/skill-tool.test.ts +478 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/tool-result-renderer-wiring.test.ts +40 -0
- package/dist/extensions/pi-hermes-memory/tsconfig.json +19 -0
- package/dist/extensions/pi-subagents/.oxlintrc.json +43 -0
- package/dist/extensions/pi-subagents/AGENTS.md +5 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +157 -0
- package/dist/extensions/pi-subagents/README.md +12 -6
- package/dist/extensions/pi-subagents/VISION.md +81 -0
- package/dist/extensions/pi-subagents/agents/claude-code-writer.md +15 -0
- package/dist/extensions/pi-subagents/agents/claude-code.md +15 -0
- package/dist/extensions/pi-subagents/agents/codex-exec-writer.md +15 -0
- package/dist/extensions/pi-subagents/agents/codex-exec.md +15 -0
- package/dist/extensions/pi-subagents/agents/cursor-agent-writer.md +14 -0
- package/dist/extensions/pi-subagents/agents/cursor-agent.md +14 -0
- package/dist/extensions/pi-subagents/agents/reviewer.md +1 -3
- package/dist/extensions/pi-subagents/docs/agents.md +488 -0
- package/dist/extensions/pi-subagents/docs/configuration.md +46 -9
- package/dist/extensions/pi-subagents/docs/extension-api.md +42 -3
- package/dist/extensions/pi-subagents/docs/models.md +27 -4
- package/dist/extensions/pi-subagents/docs/observability.md +10 -9
- package/dist/extensions/pi-subagents/docs/tool-reference.md +102 -6
- package/dist/extensions/pi-subagents/docs/workflows.md +135 -0
- package/dist/extensions/pi-subagents/package-lock.json +389 -2
- package/dist/extensions/pi-subagents/package.json +3 -1
- package/dist/extensions/pi-subagents/prompts/review-loop.md +2 -2
- package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +3 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +7 -3
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +26 -5
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -22
- package/dist/extensions/pi-subagents/src/agents/agent-refinements.ts +4 -4
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +9 -2
- package/dist/extensions/pi-subagents/src/agents/agents.ts +322 -75
- package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +6 -0
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-events.ts +70 -0
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +27 -20
- package/dist/extensions/pi-subagents/src/api/agents.ts +10 -5
- package/dist/extensions/pi-subagents/src/api/background-work.ts +5 -1
- package/dist/extensions/pi-subagents/src/api/delegation.ts +0 -7
- package/dist/extensions/pi-subagents/src/api/preflight.ts +34 -12
- package/dist/extensions/pi-subagents/src/extension/config.ts +70 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -3
- package/dist/extensions/pi-subagents/src/extension/index.ts +83 -30
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +44 -15
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +55 -19
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +41 -15
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -31
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +2 -1
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/dist/extensions/pi-subagents/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/dist/extensions/pi-subagents/src/integrations/herdr-status.ts +51 -3
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +2 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +5 -6
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +77 -10
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +176 -53
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +19 -12
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +29 -27
- package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +20 -3
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +23 -261
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +73 -7
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +74 -8
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +37 -21
- package/dist/extensions/pi-subagents/src/runs/background/inspect-rpc.ts +8 -8
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +184 -9
- package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +45 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +28 -14
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +38 -15
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +50 -13
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +93 -8
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +10 -4
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -14
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +549 -377
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +58 -10
- package/dist/extensions/pi-subagents/src/runs/background/terminal-run-index.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +26 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +23 -9
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +9 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +204 -129
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +23 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +773 -311
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +140 -115
- package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +119 -0
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +463 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +19 -4
- package/dist/extensions/pi-subagents/src/runs/shared/child-launch-plan.ts +151 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +21 -7
- package/dist/extensions/pi-subagents/src/runs/shared/claude-code-adapter.ts +129 -0
- package/dist/extensions/pi-subagents/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +89 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +5 -4
- package/dist/extensions/pi-subagents/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-contract.ts +167 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +122 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +348 -55
- package/dist/extensions/pi-subagents/src/runs/shared/fast-mode-extension.ts +5 -5
- package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +230 -0
- package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +105 -0
- package/dist/extensions/pi-subagents/src/runs/shared/launch-cwd.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-config-sources.ts +422 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +69 -7
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +47 -6
- package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +7 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +9 -3
- package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +419 -7
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +15 -1
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +81 -8
- package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +13 -0
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +21 -1
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +44 -4
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +98 -13
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +18 -0
- package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +46 -0
- package/dist/extensions/pi-subagents/src/shared/extension-context.ts +24 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/formatters.ts +18 -3
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +5 -1
- package/dist/extensions/pi-subagents/src/shared/pruned-fork.ts +450 -0
- package/dist/extensions/pi-subagents/src/shared/session-file-trust.ts +19 -0
- package/dist/extensions/pi-subagents/src/shared/session-tokens.ts +14 -3
- package/dist/extensions/pi-subagents/src/shared/settings.ts +19 -105
- package/dist/extensions/pi-subagents/src/shared/shortcuts.ts +17 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +359 -40
- package/dist/extensions/pi-subagents/src/shared/utils.ts +41 -84
- package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +116 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +1 -9
- package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +0 -4
- package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -2
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +378 -88
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +22 -11
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +3 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +147 -79
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +11 -5
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +39 -18
- package/dist/extensions/pi-subagents/src/tui/render.ts +369 -46
- package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +1 -1
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +6 -3
- package/dist/extensions/pi-subagents/src/workflows/host-command.ts +230 -0
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +740 -58
- package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +121 -0
- package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +270 -0
- package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +195 -6
- package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +246 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1114 -260
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +150 -3
- package/dist/extensions/pi-subagents/test/integration/claude-code-smoke.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/integration/claude-code-writer-smoke.test.ts +55 -0
- package/dist/extensions/pi-subagents/test/integration/codex-exec-smoke.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/integration/codex-exec-writer-smoke.test.ts +57 -0
- package/dist/extensions/pi-subagents/test/integration/cursor-agent-smoke.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/integration/cursor-agent-writer-smoke.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +57 -1
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +4 -3
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +54 -4
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +482 -4
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +157 -9
- package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +5 -5
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1508 -291
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +423 -1
- package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/support/cursor-smoke-inputs.ts +51 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +2 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +5 -5
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +6 -6
- package/dist/extensions/pi-subagents/test/unit/abort-recovery.test.ts +152 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +184 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +66 -6
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/agent-refinements.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/anti-slop-oxlint.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +101 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +14 -7
- package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +190 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +42 -0
- package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +8 -4
- package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +78 -1
- package/dist/extensions/pi-subagents/test/unit/child-launch-plan.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/child-session-name.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/claude-code-adapter.test.ts +245 -0
- package/dist/extensions/pi-subagents/test/unit/codex-exec-adapter.test.ts +192 -0
- package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +150 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/cursor-agent-adapter.test.ts +259 -0
- package/dist/extensions/pi-subagents/test/unit/cursor-smoke-inputs.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +1 -4
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/extension-context.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +223 -0
- package/dist/extensions/pi-subagents/test/unit/fast-mode-extension.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +208 -2
- package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +110 -1
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
- package/dist/extensions/pi-subagents/test/unit/formatters.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +18 -9
- package/dist/extensions/pi-subagents/test/unit/herdr-session-roots-codec.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/herdr-status-bridge.test.ts +132 -0
- package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +156 -0
- package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +178 -26
- package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +2 -1
- package/dist/extensions/pi-subagents/test/unit/inspect-rpc.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +138 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +72 -7
- package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +20 -2
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +129 -1
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +4 -0
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +358 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +574 -21
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +109 -1
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +71 -1
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/pruned-fork.test.ts +214 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +29 -8
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/unit/result-delivery-ownership.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +101 -3
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +10 -8
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +71 -1
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +41 -7
- package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +136 -2
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +137 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +33 -12
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +504 -1
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +12 -1
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +3 -5
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +5 -6
- package/dist/extensions/pi-subagents/test/unit/subagent-action-recovery.test.ts +0 -8
- package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +5 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +157 -2
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +63 -4
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +27 -23
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +7 -5
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +106 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +484 -9
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +38 -4
- package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +155 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +318 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +365 -0
- package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +349 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +2 -7
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/index.ts +13 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/rules/no-service-constructor-imports.ts +52 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/index.ts +41 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts +77 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-known-value-widening.ts +247 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-module-mocking.ts +91 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-object-parameters.ts +126 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-apply.ts +28 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-get.ts +28 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts +67 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts +39 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts +83 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-returns.ts +115 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts +72 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts +134 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts +366 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts +62 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/dictionary-types.ts +502 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts +60 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/reflect-method.ts +35 -0
- package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +1 -1
- package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +2 -2
- package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +1 -1
- package/dist/modes/interactive/interactive-mode.js +7 -7
- package/dist/skills/workflow/SKILL.md +65 -50
- package/package.json +112 -110
- package/dist/extensions/enable-readonly-tools.test.ts +0 -63
- package/dist/extensions/enable-readonly-tools.ts +0 -24
- package/dist/extensions/model-prompt-injector/config.json +0 -16
- package/dist/extensions/model-prompt-injector/index.test.ts +0 -102
- package/dist/extensions/model-prompt-injector/index.ts +0 -148
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +0 -98
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +0 -250
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
buildDirectReviewUserPrompt,
|
|
7
|
+
buildSubprocessReviewPrompt,
|
|
8
|
+
setupBackgroundReview,
|
|
9
|
+
type BackgroundReviewDeps,
|
|
10
|
+
} from "../../src/handlers/background-review.js";
|
|
11
|
+
import { resolveWatchedChildPiInvocation } from "../../src/handlers/pi-child-process.js";
|
|
12
|
+
import type { DirectReviewResult } from "../../src/handlers/review-memory-ops.js";
|
|
13
|
+
import type { MemoryConfig } from "../../src/types.js";
|
|
14
|
+
|
|
15
|
+
// ─── Mock infrastructure ───
|
|
16
|
+
|
|
17
|
+
interface CallLog {
|
|
18
|
+
handler: string;
|
|
19
|
+
args: any[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let handlers: Record<string, Function[]>;
|
|
23
|
+
let execCalls: any[];
|
|
24
|
+
let directCalls: any[];
|
|
25
|
+
let notifyCalls: any[];
|
|
26
|
+
|
|
27
|
+
// The turn_end handler intentionally does not await its review work
|
|
28
|
+
// (fire-and-forget, so background review never blocks interactive chat —
|
|
29
|
+
// see #10 in CHANGELOG.md). Tests can't just await fireTurnEnd(), so setup()
|
|
30
|
+
// below wires the production-side onReviewSettled test hook (fired once
|
|
31
|
+
// runReview() and its .finally() cleanup fully complete, regardless of
|
|
32
|
+
// outcome) instead of racing exec-call timing or a fixed sleep.
|
|
33
|
+
let reviewSettledSignal: PromiseWithResolvers<void>;
|
|
34
|
+
|
|
35
|
+
function resetReviewSettledSignal(): void {
|
|
36
|
+
reviewSettledSignal = Promise.withResolvers<void>();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setup(
|
|
40
|
+
pi: ExtensionAPI,
|
|
41
|
+
config: MemoryConfig = defaultConfig as MemoryConfig,
|
|
42
|
+
extraDeps: BackgroundReviewDeps = {},
|
|
43
|
+
): void {
|
|
44
|
+
setupBackgroundReview(pi, mockStore, null, config, {
|
|
45
|
+
deps: { onReviewSettled: () => reviewSettledSignal.resolve(), ...extraDeps },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function captureExecArgs(args: any[]): any[] {
|
|
50
|
+
const [command, childArgs, options] = args;
|
|
51
|
+
const capturedArgs = [...childArgs];
|
|
52
|
+
const promptReference = capturedArgs.at(-1);
|
|
53
|
+
if (typeof promptReference === "string" && promptReference.startsWith("@")) {
|
|
54
|
+
capturedArgs[capturedArgs.length - 1] = readFileSync(promptReference.slice(1), "utf-8");
|
|
55
|
+
}
|
|
56
|
+
return [command, capturedArgs, options];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createMockPi(execReturn?: { code: number; stdout: string; stderr: string }) {
|
|
60
|
+
const defaultReturn = { code: 0, stdout: "Saved memory", stderr: "" };
|
|
61
|
+
const ret = execReturn ?? defaultReturn;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
on: (event: string, handler: Function) => {
|
|
65
|
+
handlers[event] = handlers[event] || [];
|
|
66
|
+
handlers[event].push(handler);
|
|
67
|
+
},
|
|
68
|
+
exec: async (...args: any[]) => {
|
|
69
|
+
execCalls.push(captureExecArgs(args));
|
|
70
|
+
return ret;
|
|
71
|
+
},
|
|
72
|
+
registerTool: () => {},
|
|
73
|
+
registerCommand: () => {},
|
|
74
|
+
} as any;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function makeBranch(numMessages: number) {
|
|
78
|
+
return Array.from({ length: numMessages }, (_, i) => ({
|
|
79
|
+
type: "message",
|
|
80
|
+
message: {
|
|
81
|
+
role: i % 2 === 0 ? "user" : "assistant",
|
|
82
|
+
content: [{ type: "text", text: `Message number ${i} with some real content here` }],
|
|
83
|
+
timestamp: i,
|
|
84
|
+
},
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function makeCtx(branch: any[] = [], overrides: Record<string, any> = {}) {
|
|
89
|
+
return {
|
|
90
|
+
sessionManager: { getBranch: () => branch },
|
|
91
|
+
signal: undefined as any,
|
|
92
|
+
ui: {
|
|
93
|
+
notify: (msg: string, level: string) => {
|
|
94
|
+
notifyCalls.push({ msg, level });
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
...overrides,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const defaultConfig = {
|
|
102
|
+
reviewEnabled: true,
|
|
103
|
+
reviewTransport: "subprocess" as const,
|
|
104
|
+
nudgeInterval: 10,
|
|
105
|
+
reviewRecentMessages: 0,
|
|
106
|
+
flushMinTurns: 6,
|
|
107
|
+
flushRecentMessages: 0,
|
|
108
|
+
flushOnCompact: true,
|
|
109
|
+
flushOnShutdown: true,
|
|
110
|
+
memoryCharLimit: 5000,
|
|
111
|
+
userCharLimit: 5000,
|
|
112
|
+
projectCharLimit: 5000,
|
|
113
|
+
autoConsolidate: true,
|
|
114
|
+
correctionDetection: true,
|
|
115
|
+
failureInjectionEnabled: true,
|
|
116
|
+
failureInjectionMaxAgeDays: 7,
|
|
117
|
+
failureInjectionMaxEntries: 5,
|
|
118
|
+
nudgeToolCalls: 15,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const mockStore = {
|
|
122
|
+
getMemoryEntries: () => ["existing memory entry"],
|
|
123
|
+
getUserEntries: () => ["existing user entry"],
|
|
124
|
+
} as any;
|
|
125
|
+
|
|
126
|
+
function fireMessageEnd(role: string) {
|
|
127
|
+
const h = handlers["message_end"];
|
|
128
|
+
if (!h) throw new Error("No message_end handler registered");
|
|
129
|
+
for (const fn of h) {
|
|
130
|
+
fn({ message: { role, content: [{ type: "text", text: "hi" }] } }, makeCtx());
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function fireTurnEnd(branch: any[] = makeBranch(10), ctxOverrides: Record<string, any> = {}) {
|
|
135
|
+
const h = handlers["turn_end"];
|
|
136
|
+
if (!h) throw new Error("No turn_end handler registered");
|
|
137
|
+
const ctx = makeCtx(branch, ctxOverrides);
|
|
138
|
+
// Extract the last assistant message from the branch to pass as event.message
|
|
139
|
+
// (the handler now reads tool calls from event.message, not from the branch)
|
|
140
|
+
let assistantMessage = undefined;
|
|
141
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
142
|
+
if (branch[i]?.message?.role === "assistant") {
|
|
143
|
+
assistantMessage = branch[i].message;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const event = assistantMessage ? { message: assistantMessage } : {};
|
|
148
|
+
for (const fn of h) {
|
|
149
|
+
fn(event, ctx);
|
|
150
|
+
}
|
|
151
|
+
return ctx;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Only for genuinely negative assertions (nothing should have happened),
|
|
155
|
+
// where there is no positive side effect to await instead.
|
|
156
|
+
async function settle(ms = 10): Promise<void> {
|
|
157
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
158
|
+
setTimeout(resolve, ms);
|
|
159
|
+
await promise;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function logicalChildArgs(index = execCalls.length - 1): string[] {
|
|
163
|
+
const [cmd, args] = execCalls[index];
|
|
164
|
+
const underlying = { command: args[3], args: args.slice(4) };
|
|
165
|
+
const expected = resolveWatchedChildPiInvocation(underlying, Number(args[1]), args[2]);
|
|
166
|
+
assert.strictEqual(cmd, expected.command);
|
|
167
|
+
assert.deepStrictEqual(args, expected.args);
|
|
168
|
+
return underlying.command === "selesai" ? underlying.args : underlying.args.slice(1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function reviewPrompt(index = execCalls.length - 1): string {
|
|
172
|
+
const args = logicalChildArgs(index);
|
|
173
|
+
return args[args.length - 1];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Tests ───
|
|
177
|
+
|
|
178
|
+
describe("setupBackgroundReview", () => {
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
handlers = {};
|
|
181
|
+
execCalls = [];
|
|
182
|
+
directCalls = [];
|
|
183
|
+
notifyCalls = [];
|
|
184
|
+
resetReviewSettledSignal();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
function setupWithDirectDeps(
|
|
188
|
+
pi: ExtensionAPI,
|
|
189
|
+
directResult: DirectReviewResult,
|
|
190
|
+
config: MemoryConfig = { ...defaultConfig, reviewTransport: "direct" as const } as MemoryConfig,
|
|
191
|
+
): void {
|
|
192
|
+
setup(pi, config, {
|
|
193
|
+
runDirectReview: async (...args: any[]) => {
|
|
194
|
+
directCalls.push(args);
|
|
195
|
+
return directResult;
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
it("increments user turn count on message_end for user messages", async () => {
|
|
201
|
+
const pi = createMockPi();
|
|
202
|
+
setup(pi, defaultConfig);
|
|
203
|
+
|
|
204
|
+
fireMessageEnd("user");
|
|
205
|
+
fireMessageEnd("user");
|
|
206
|
+
fireMessageEnd("user");
|
|
207
|
+
|
|
208
|
+
// Verify by checking that 3 user turns is enough to allow review
|
|
209
|
+
// (userTurnCount >= 3 check passes after 3 user message_end events)
|
|
210
|
+
// Fire 10 turn_end events — should trigger review since userTurnCount is 3
|
|
211
|
+
for (let i = 0; i < 10; i++) {
|
|
212
|
+
fireTurnEnd();
|
|
213
|
+
}
|
|
214
|
+
await reviewSettledSignal.promise;
|
|
215
|
+
|
|
216
|
+
// exec should have been called since we have 3 user turns and 10 turn_end events
|
|
217
|
+
assert.ok(execCalls.length > 0, "exec should be called with 3 user turns and 10 turn_end events");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("triggers review at nudgeInterval (10) turns", async () => {
|
|
221
|
+
const pi = createMockPi();
|
|
222
|
+
setup(pi, defaultConfig);
|
|
223
|
+
|
|
224
|
+
// Register 3 user messages first
|
|
225
|
+
fireMessageEnd("user");
|
|
226
|
+
fireMessageEnd("user");
|
|
227
|
+
fireMessageEnd("user");
|
|
228
|
+
|
|
229
|
+
// Fire 9 turn_end events — not enough
|
|
230
|
+
for (let i = 0; i < 9; i++) {
|
|
231
|
+
fireTurnEnd();
|
|
232
|
+
}
|
|
233
|
+
assert.strictEqual(execCalls.length, 0, "exec should NOT be called at 9 turns");
|
|
234
|
+
|
|
235
|
+
// 10th turn_end triggers review
|
|
236
|
+
fireTurnEnd();
|
|
237
|
+
await reviewSettledSignal.promise;
|
|
238
|
+
|
|
239
|
+
assert.strictEqual(execCalls.length, 1, "exec should be called once at turn 10");
|
|
240
|
+
// Verify it calls pi.exec with review prompt
|
|
241
|
+
const cmdArgs = logicalChildArgs(0);
|
|
242
|
+
assert.ok(cmdArgs[0] === "-p", "should use -p flag");
|
|
243
|
+
assert.ok(cmdArgs.includes("--no-session"), "should include --no-session");
|
|
244
|
+
const prompt = reviewPrompt(0);
|
|
245
|
+
assert.match(prompt, /Do NOT create or modify skills in this background review/i);
|
|
246
|
+
assert.doesNotMatch(prompt, /save a reusable procedure using the skill tool/i);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("passes child LLM override args to the review subprocess", async () => {
|
|
250
|
+
const pi = createMockPi();
|
|
251
|
+
setup(pi, {
|
|
252
|
+
...defaultConfig,
|
|
253
|
+
llmModelOverride: "openrouter/deepseek/deepseek-v4-flash",
|
|
254
|
+
llmThinkingOverride: "minimal",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
fireMessageEnd("user");
|
|
258
|
+
fireMessageEnd("user");
|
|
259
|
+
fireMessageEnd("user");
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < 10; i++) {
|
|
262
|
+
fireTurnEnd();
|
|
263
|
+
}
|
|
264
|
+
await reviewSettledSignal.promise;
|
|
265
|
+
|
|
266
|
+
const cmdArgs = logicalChildArgs(0);
|
|
267
|
+
assert.deepStrictEqual(
|
|
268
|
+
cmdArgs.slice(0, 6),
|
|
269
|
+
["-p", "--no-session", "--model", "openrouter/deepseek/deepseek-v4-flash", "--thinking", "minimal"],
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("does NOT trigger review when reviewEnabled is false", async () => {
|
|
274
|
+
const config = { ...defaultConfig, reviewEnabled: false };
|
|
275
|
+
const pi = createMockPi();
|
|
276
|
+
setup(pi, config);
|
|
277
|
+
|
|
278
|
+
fireMessageEnd("user");
|
|
279
|
+
fireMessageEnd("user");
|
|
280
|
+
fireMessageEnd("user");
|
|
281
|
+
|
|
282
|
+
for (let i = 0; i < 15; i++) {
|
|
283
|
+
fireTurnEnd();
|
|
284
|
+
}
|
|
285
|
+
await settle();
|
|
286
|
+
|
|
287
|
+
assert.strictEqual(execCalls.length, 0, "exec should NOT be called when reviewEnabled is false");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("does NOT trigger review with fewer than 3 user turns", async () => {
|
|
291
|
+
const pi = createMockPi();
|
|
292
|
+
setup(pi, defaultConfig);
|
|
293
|
+
|
|
294
|
+
// Only 2 user messages
|
|
295
|
+
fireMessageEnd("user");
|
|
296
|
+
fireMessageEnd("user");
|
|
297
|
+
|
|
298
|
+
for (let i = 0; i < 15; i++) {
|
|
299
|
+
fireTurnEnd();
|
|
300
|
+
}
|
|
301
|
+
await settle();
|
|
302
|
+
|
|
303
|
+
assert.strictEqual(execCalls.length, 0, "exec should NOT be called with only 2 user turns");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("reviewInProgress guard prevents double-trigger", async () => {
|
|
307
|
+
// Use a slow exec that never resolves to keep reviewInProgress true
|
|
308
|
+
let resolveExec: () => void;
|
|
309
|
+
const slowPi = {
|
|
310
|
+
on: (event: string, handler: Function) => {
|
|
311
|
+
handlers[event] = handlers[event] || [];
|
|
312
|
+
handlers[event].push(handler);
|
|
313
|
+
},
|
|
314
|
+
exec: async (...args: any[]) => {
|
|
315
|
+
execCalls.push(captureExecArgs(args));
|
|
316
|
+
await new Promise<void>((r) => { resolveExec = r; });
|
|
317
|
+
return { code: 0, stdout: "Saved", stderr: "" };
|
|
318
|
+
},
|
|
319
|
+
registerTool: () => {},
|
|
320
|
+
registerCommand: () => {},
|
|
321
|
+
} as any;
|
|
322
|
+
|
|
323
|
+
setup(slowPi, defaultConfig);
|
|
324
|
+
|
|
325
|
+
fireMessageEnd("user");
|
|
326
|
+
fireMessageEnd("user");
|
|
327
|
+
fireMessageEnd("user");
|
|
328
|
+
|
|
329
|
+
// Fire 10 turn_end events — first triggers review (slow, won't resolve)
|
|
330
|
+
for (let i = 0; i < 10; i++) {
|
|
331
|
+
fireTurnEnd();
|
|
332
|
+
}
|
|
333
|
+
await settle(5);
|
|
334
|
+
|
|
335
|
+
assert.strictEqual(execCalls.length, 1, "exec should be called once for first trigger");
|
|
336
|
+
|
|
337
|
+
// Fire more turn_end events — should be blocked by reviewInProgress
|
|
338
|
+
for (let i = 0; i < 15; i++) {
|
|
339
|
+
fireTurnEnd();
|
|
340
|
+
}
|
|
341
|
+
await settle(5);
|
|
342
|
+
|
|
343
|
+
assert.strictEqual(execCalls.length, 1, "exec should still only be called once — reviewInProgress guard");
|
|
344
|
+
|
|
345
|
+
// Resolve the pending exec to clean up
|
|
346
|
+
resolveExec!();
|
|
347
|
+
await settle();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("does NOT trigger for short conversations (< 4 message parts)", async () => {
|
|
351
|
+
const pi = createMockPi();
|
|
352
|
+
setup(pi, defaultConfig);
|
|
353
|
+
|
|
354
|
+
fireMessageEnd("user");
|
|
355
|
+
fireMessageEnd("user");
|
|
356
|
+
fireMessageEnd("user");
|
|
357
|
+
|
|
358
|
+
// Branch with only 2 message entries (< 4 parts)
|
|
359
|
+
const shortBranch = [
|
|
360
|
+
{ type: "message", message: { role: "user", content: [{ type: "text", text: "hi" }] } },
|
|
361
|
+
{ type: "message", message: { role: "assistant", content: [{ type: "text", text: "hello" }] } },
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
for (let i = 0; i < 10; i++) {
|
|
365
|
+
fireTurnEnd(shortBranch);
|
|
366
|
+
}
|
|
367
|
+
await settle();
|
|
368
|
+
|
|
369
|
+
assert.strictEqual(execCalls.length, 0, "exec should NOT be called for short conversations");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("uses the full conversation by default", async () => {
|
|
373
|
+
const pi = createMockPi();
|
|
374
|
+
setup(pi, defaultConfig);
|
|
375
|
+
|
|
376
|
+
fireMessageEnd("user");
|
|
377
|
+
fireMessageEnd("user");
|
|
378
|
+
fireMessageEnd("user");
|
|
379
|
+
|
|
380
|
+
for (let i = 0; i < 10; i++) {
|
|
381
|
+
fireTurnEnd(makeBranch(10));
|
|
382
|
+
}
|
|
383
|
+
await reviewSettledSignal.promise;
|
|
384
|
+
|
|
385
|
+
const prompt = reviewPrompt();
|
|
386
|
+
assert.ok(prompt.includes("Message number 0"), "default should include older messages");
|
|
387
|
+
assert.ok(prompt.includes("Message number 9"), "default should include latest messages");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("limits background review to recent messages when configured", async () => {
|
|
391
|
+
const config = { ...defaultConfig, reviewRecentMessages: 3 };
|
|
392
|
+
const pi = createMockPi();
|
|
393
|
+
setup(pi, config);
|
|
394
|
+
|
|
395
|
+
fireMessageEnd("user");
|
|
396
|
+
fireMessageEnd("user");
|
|
397
|
+
fireMessageEnd("user");
|
|
398
|
+
|
|
399
|
+
for (let i = 0; i < 10; i++) {
|
|
400
|
+
fireTurnEnd(makeBranch(10));
|
|
401
|
+
}
|
|
402
|
+
await reviewSettledSignal.promise;
|
|
403
|
+
|
|
404
|
+
const prompt = reviewPrompt();
|
|
405
|
+
assert.ok(!prompt.includes("Message number 6"), "window should exclude older messages");
|
|
406
|
+
assert.ok(prompt.includes("Message number 7"));
|
|
407
|
+
assert.ok(prompt.includes("Message number 8"));
|
|
408
|
+
assert.ok(prompt.includes("Message number 9"));
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("does not use the flush recent-message limit for background review", async () => {
|
|
412
|
+
const config = { ...defaultConfig, flushRecentMessages: 2 };
|
|
413
|
+
const pi = createMockPi();
|
|
414
|
+
setup(pi, config);
|
|
415
|
+
|
|
416
|
+
fireMessageEnd("user");
|
|
417
|
+
fireMessageEnd("user");
|
|
418
|
+
fireMessageEnd("user");
|
|
419
|
+
|
|
420
|
+
for (let i = 0; i < 10; i++) {
|
|
421
|
+
fireTurnEnd(makeBranch(10));
|
|
422
|
+
}
|
|
423
|
+
await reviewSettledSignal.promise;
|
|
424
|
+
|
|
425
|
+
assert.ok(reviewPrompt().includes("Message number 0"), "flush limit must not affect review");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("keeps the short conversation guard based on the full conversation", async () => {
|
|
429
|
+
const config = { ...defaultConfig, reviewRecentMessages: 2 };
|
|
430
|
+
const pi = createMockPi();
|
|
431
|
+
setup(pi, config);
|
|
432
|
+
|
|
433
|
+
fireMessageEnd("user");
|
|
434
|
+
fireMessageEnd("user");
|
|
435
|
+
fireMessageEnd("user");
|
|
436
|
+
|
|
437
|
+
for (let i = 0; i < 10; i++) {
|
|
438
|
+
fireTurnEnd(makeBranch(4));
|
|
439
|
+
}
|
|
440
|
+
await reviewSettledSignal.promise;
|
|
441
|
+
|
|
442
|
+
assert.strictEqual(execCalls.length, 1, "full conversation has enough parts to review");
|
|
443
|
+
const prompt = reviewPrompt();
|
|
444
|
+
assert.ok(!prompt.includes("Message number 0"));
|
|
445
|
+
assert.ok(!prompt.includes("Message number 1"));
|
|
446
|
+
assert.ok(prompt.includes("Message number 2"));
|
|
447
|
+
assert.ok(prompt.includes("Message number 3"));
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("resets turn counter after review triggers", async () => {
|
|
451
|
+
const pi = createMockPi();
|
|
452
|
+
setup(pi, defaultConfig);
|
|
453
|
+
|
|
454
|
+
fireMessageEnd("user");
|
|
455
|
+
fireMessageEnd("user");
|
|
456
|
+
fireMessageEnd("user");
|
|
457
|
+
|
|
458
|
+
// Fire 10 turns — triggers review
|
|
459
|
+
for (let i = 0; i < 10; i++) {
|
|
460
|
+
fireTurnEnd();
|
|
461
|
+
}
|
|
462
|
+
await reviewSettledSignal.promise;
|
|
463
|
+
|
|
464
|
+
assert.strictEqual(execCalls.length, 1, "first review triggered");
|
|
465
|
+
|
|
466
|
+
// Fire 10 more turns — should trigger again (counter was reset)
|
|
467
|
+
resetReviewSettledSignal();
|
|
468
|
+
for (let i = 0; i < 10; i++) {
|
|
469
|
+
fireTurnEnd();
|
|
470
|
+
}
|
|
471
|
+
await reviewSettledSignal.promise;
|
|
472
|
+
|
|
473
|
+
assert.strictEqual(execCalls.length, 2, "second review should trigger after counter reset");
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("shows notification only when review saves something", async () => {
|
|
477
|
+
const pi = createMockPi({ code: 0, stdout: "Saved new memory about user preferences", stderr: "" });
|
|
478
|
+
setup(pi, defaultConfig);
|
|
479
|
+
|
|
480
|
+
fireMessageEnd("user");
|
|
481
|
+
fireMessageEnd("user");
|
|
482
|
+
fireMessageEnd("user");
|
|
483
|
+
|
|
484
|
+
for (let i = 0; i < 10; i++) {
|
|
485
|
+
fireTurnEnd();
|
|
486
|
+
}
|
|
487
|
+
await reviewSettledSignal.promise;
|
|
488
|
+
|
|
489
|
+
// 10 diagnostic notifications + 1 auto-review notification
|
|
490
|
+
const reviewNotify = notifyCalls.find(n => n.msg.includes("Memory auto-reviewed"));
|
|
491
|
+
assert.ok(reviewNotify, "should have a 'Memory auto-reviewed' notification");
|
|
492
|
+
|
|
493
|
+
// Reset and test "nothing to save" case
|
|
494
|
+
handlers = {};
|
|
495
|
+
execCalls = [];
|
|
496
|
+
notifyCalls = [];
|
|
497
|
+
resetReviewSettledSignal();
|
|
498
|
+
|
|
499
|
+
const nothingPi = createMockPi({ code: 0, stdout: "Nothing to save.", stderr: "" });
|
|
500
|
+
setup(nothingPi, defaultConfig);
|
|
501
|
+
|
|
502
|
+
fireMessageEnd("user");
|
|
503
|
+
fireMessageEnd("user");
|
|
504
|
+
fireMessageEnd("user");
|
|
505
|
+
|
|
506
|
+
for (let i = 0; i < 10; i++) {
|
|
507
|
+
fireTurnEnd();
|
|
508
|
+
}
|
|
509
|
+
await reviewSettledSignal.promise;
|
|
510
|
+
|
|
511
|
+
const reviewNotify2 = notifyCalls.find(n => n.msg.includes("Memory auto-reviewed"));
|
|
512
|
+
assert.strictEqual(reviewNotify2, undefined, "no 'Memory auto-reviewed' notification for 'nothing to save'");
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it("does NOT crash agent when exec throws", async () => {
|
|
516
|
+
const crashPi = {
|
|
517
|
+
on: (event: string, handler: Function) => {
|
|
518
|
+
handlers[event] = handlers[event] || [];
|
|
519
|
+
handlers[event].push(handler);
|
|
520
|
+
},
|
|
521
|
+
exec: async (...args: any[]) => {
|
|
522
|
+
execCalls.push(captureExecArgs(args));
|
|
523
|
+
throw new Error("exec crashed");
|
|
524
|
+
},
|
|
525
|
+
registerTool: () => {},
|
|
526
|
+
registerCommand: () => {},
|
|
527
|
+
} as any;
|
|
528
|
+
|
|
529
|
+
setup(crashPi, defaultConfig);
|
|
530
|
+
|
|
531
|
+
fireMessageEnd("user");
|
|
532
|
+
fireMessageEnd("user");
|
|
533
|
+
fireMessageEnd("user");
|
|
534
|
+
|
|
535
|
+
// This should NOT throw
|
|
536
|
+
for (let i = 0; i < 10; i++) {
|
|
537
|
+
fireTurnEnd();
|
|
538
|
+
}
|
|
539
|
+
await reviewSettledSignal.promise;
|
|
540
|
+
|
|
541
|
+
assert.strictEqual(execCalls.length, 1, "exec was attempted");
|
|
542
|
+
// If we get here without an unhandled rejection, the error was caught
|
|
543
|
+
assert.ok(true, "background review failure was caught silently");
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("assistant message_end does NOT increment user turn count", async () => {
|
|
547
|
+
const pi = createMockPi();
|
|
548
|
+
setup(pi, defaultConfig);
|
|
549
|
+
|
|
550
|
+
// Only assistant messages — userTurnCount stays 0
|
|
551
|
+
fireMessageEnd("assistant");
|
|
552
|
+
fireMessageEnd("assistant");
|
|
553
|
+
fireMessageEnd("assistant");
|
|
554
|
+
|
|
555
|
+
for (let i = 0; i < 15; i++) {
|
|
556
|
+
fireTurnEnd();
|
|
557
|
+
}
|
|
558
|
+
await settle();
|
|
559
|
+
|
|
560
|
+
assert.strictEqual(execCalls.length, 0, "exec should NOT be called — no user messages");
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// ─── Tool-call-aware nudge tests (Epic 4) ───
|
|
564
|
+
|
|
565
|
+
it("triggers on tool call count threshold even with low turn count", async () => {
|
|
566
|
+
const config = { ...defaultConfig, nudgeToolCalls: 5 };
|
|
567
|
+
const pi = createMockPi();
|
|
568
|
+
setup(pi, config);
|
|
569
|
+
|
|
570
|
+
fireMessageEnd("user");
|
|
571
|
+
fireMessageEnd("user");
|
|
572
|
+
fireMessageEnd("user");
|
|
573
|
+
|
|
574
|
+
// Branch with 5 toolCall blocks (meets tool call threshold)
|
|
575
|
+
const branchWithToolCalls = [
|
|
576
|
+
...makeBranch(4),
|
|
577
|
+
{
|
|
578
|
+
type: "message",
|
|
579
|
+
message: {
|
|
580
|
+
role: "assistant",
|
|
581
|
+
content: [
|
|
582
|
+
{ type: "toolCall", id: "tc1", name: "read", arguments: {} },
|
|
583
|
+
{ type: "toolCall", id: "tc2", name: "bash", arguments: {} },
|
|
584
|
+
{ type: "toolCall", id: "tc3", name: "edit", arguments: {} },
|
|
585
|
+
{ type: "toolCall", id: "tc4", name: "read", arguments: {} },
|
|
586
|
+
{ type: "toolCall", id: "tc5", name: "bash", arguments: {} },
|
|
587
|
+
],
|
|
588
|
+
timestamp: 1,
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
];
|
|
592
|
+
|
|
593
|
+
// Only 2 turn_end events (below turn threshold of 10)
|
|
594
|
+
fireTurnEnd(branchWithToolCalls);
|
|
595
|
+
fireTurnEnd(branchWithToolCalls);
|
|
596
|
+
await reviewSettledSignal.promise;
|
|
597
|
+
|
|
598
|
+
assert.ok(execCalls.length >= 1, "exec should be called due to tool call threshold");
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("triggers when both thresholds are met", async () => {
|
|
602
|
+
const config = { ...defaultConfig, nudgeToolCalls: 5 };
|
|
603
|
+
const pi = createMockPi();
|
|
604
|
+
setup(pi, config);
|
|
605
|
+
|
|
606
|
+
fireMessageEnd("user");
|
|
607
|
+
fireMessageEnd("user");
|
|
608
|
+
fireMessageEnd("user");
|
|
609
|
+
|
|
610
|
+
const branchWithToolCalls = [
|
|
611
|
+
...makeBranch(10),
|
|
612
|
+
{
|
|
613
|
+
type: "message",
|
|
614
|
+
message: {
|
|
615
|
+
role: "assistant",
|
|
616
|
+
content: [
|
|
617
|
+
{ type: "toolCall", id: "tc1", name: "read", arguments: {} },
|
|
618
|
+
{ type: "toolCall", id: "tc2", name: "bash", arguments: {} },
|
|
619
|
+
],
|
|
620
|
+
timestamp: 1,
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
];
|
|
624
|
+
|
|
625
|
+
// Fire 10 turns (meets turn threshold) with tool calls (meets tool threshold)
|
|
626
|
+
for (let i = 0; i < 10; i++) {
|
|
627
|
+
fireTurnEnd(branchWithToolCalls);
|
|
628
|
+
}
|
|
629
|
+
await reviewSettledSignal.promise;
|
|
630
|
+
|
|
631
|
+
assert.ok(execCalls.length >= 1, "exec should be called when either threshold is met");
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("resets both counters after review", async () => {
|
|
635
|
+
const config = { ...defaultConfig, nudgeToolCalls: 3 };
|
|
636
|
+
const pi = createMockPi();
|
|
637
|
+
setup(pi, config);
|
|
638
|
+
|
|
639
|
+
fireMessageEnd("user");
|
|
640
|
+
fireMessageEnd("user");
|
|
641
|
+
fireMessageEnd("user");
|
|
642
|
+
|
|
643
|
+
const branchWithToolCalls = [
|
|
644
|
+
...makeBranch(6),
|
|
645
|
+
{
|
|
646
|
+
type: "message",
|
|
647
|
+
message: {
|
|
648
|
+
role: "assistant",
|
|
649
|
+
content: [
|
|
650
|
+
{ type: "toolCall", id: "tc1", name: "read", arguments: {} },
|
|
651
|
+
{ type: "toolCall", id: "tc2", name: "bash", arguments: {} },
|
|
652
|
+
{ type: "toolCall", id: "tc3", name: "edit", arguments: {} },
|
|
653
|
+
],
|
|
654
|
+
timestamp: 1,
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
];
|
|
658
|
+
|
|
659
|
+
// Trigger first review via tool calls
|
|
660
|
+
fireTurnEnd(branchWithToolCalls);
|
|
661
|
+
await reviewSettledSignal.promise;
|
|
662
|
+
assert.strictEqual(execCalls.length, 1, "first review triggered");
|
|
663
|
+
|
|
664
|
+
// Trigger second review via turn count
|
|
665
|
+
resetReviewSettledSignal();
|
|
666
|
+
for (let i = 0; i < 10; i++) {
|
|
667
|
+
fireTurnEnd(makeBranch(10));
|
|
668
|
+
}
|
|
669
|
+
await reviewSettledSignal.promise;
|
|
670
|
+
assert.strictEqual(execCalls.length, 2, "second review should trigger after counter reset");
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it("does not trigger when neither threshold is met", async () => {
|
|
674
|
+
const config = { ...defaultConfig, nudgeToolCalls: 15 };
|
|
675
|
+
const pi = createMockPi();
|
|
676
|
+
setup(pi, config);
|
|
677
|
+
|
|
678
|
+
fireMessageEnd("user");
|
|
679
|
+
fireMessageEnd("user");
|
|
680
|
+
fireMessageEnd("user");
|
|
681
|
+
|
|
682
|
+
// Only 2 tool calls (below 15 threshold) and 5 turns (below 10 threshold)
|
|
683
|
+
const branchWithFewToolCalls = [
|
|
684
|
+
...makeBranch(4),
|
|
685
|
+
{
|
|
686
|
+
type: "message",
|
|
687
|
+
message: {
|
|
688
|
+
role: "assistant",
|
|
689
|
+
content: [
|
|
690
|
+
{ type: "toolCall", id: "tc1", name: "read", arguments: {} },
|
|
691
|
+
{ type: "toolCall", id: "tc2", name: "bash", arguments: {} },
|
|
692
|
+
],
|
|
693
|
+
timestamp: 1,
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
];
|
|
697
|
+
|
|
698
|
+
for (let i = 0; i < 5; i++) {
|
|
699
|
+
fireTurnEnd(branchWithFewToolCalls);
|
|
700
|
+
}
|
|
701
|
+
await settle();
|
|
702
|
+
|
|
703
|
+
assert.strictEqual(execCalls.length, 0, "exec should NOT be called when neither threshold met");
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("ignores text blocks when counting tool calls", async () => {
|
|
707
|
+
const config = { ...defaultConfig, nudgeToolCalls: 3 };
|
|
708
|
+
const pi = createMockPi();
|
|
709
|
+
setup(pi, config);
|
|
710
|
+
|
|
711
|
+
fireMessageEnd("user");
|
|
712
|
+
fireMessageEnd("user");
|
|
713
|
+
fireMessageEnd("user");
|
|
714
|
+
|
|
715
|
+
// Branch with text-only messages (no toolCall blocks)
|
|
716
|
+
const branchWithTextOnly = [
|
|
717
|
+
...makeBranch(10),
|
|
718
|
+
];
|
|
719
|
+
|
|
720
|
+
// Fire enough turns but no tool calls
|
|
721
|
+
for (let i = 0; i < 5; i++) {
|
|
722
|
+
fireTurnEnd(branchWithTextOnly);
|
|
723
|
+
}
|
|
724
|
+
await settle();
|
|
725
|
+
|
|
726
|
+
assert.strictEqual(execCalls.length, 0, "exec should NOT be called — no toolCall blocks, turn threshold not met");
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("uses direct review by default and does not call subprocess", async () => {
|
|
730
|
+
const pi = createMockPi();
|
|
731
|
+
setupWithDirectDeps(pi, { ok: true, appliedCount: 1 }, {
|
|
732
|
+
...defaultConfig,
|
|
733
|
+
reviewTransport: "direct",
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
fireMessageEnd("user");
|
|
737
|
+
fireMessageEnd("user");
|
|
738
|
+
fireMessageEnd("user");
|
|
739
|
+
|
|
740
|
+
for (let i = 0; i < 10; i++) {
|
|
741
|
+
fireTurnEnd();
|
|
742
|
+
}
|
|
743
|
+
await reviewSettledSignal.promise;
|
|
744
|
+
|
|
745
|
+
assert.strictEqual(directCalls.length, 1, "direct review should run once");
|
|
746
|
+
assert.strictEqual(execCalls.length, 0, "subprocess should not run on successful direct review");
|
|
747
|
+
const directOptions = directCalls[0][3] as { systemPrompt: string };
|
|
748
|
+
assert.match(directOptions.systemPrompt, /target routing/i);
|
|
749
|
+
assert.match(directOptions.systemPrompt, /use target "memory"/i);
|
|
750
|
+
assert.match(directOptions.systemPrompt, /do not emit target "project"/i);
|
|
751
|
+
const reviewNotify = notifyCalls.find((n) => n.msg.includes("Memory auto-reviewed"));
|
|
752
|
+
assert.ok(reviewNotify, "should notify when direct review applies memory");
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it("falls back to subprocess when direct review cannot run", async () => {
|
|
756
|
+
const pi = createMockPi();
|
|
757
|
+
setupWithDirectDeps(pi, { ok: false, appliedCount: 0, fallbackReason: "no_model" }, {
|
|
758
|
+
...defaultConfig,
|
|
759
|
+
reviewTransport: "direct",
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
fireMessageEnd("user");
|
|
763
|
+
fireMessageEnd("user");
|
|
764
|
+
fireMessageEnd("user");
|
|
765
|
+
|
|
766
|
+
for (let i = 0; i < 10; i++) {
|
|
767
|
+
fireTurnEnd();
|
|
768
|
+
}
|
|
769
|
+
await reviewSettledSignal.promise;
|
|
770
|
+
|
|
771
|
+
assert.strictEqual(directCalls.length, 1, "direct review should be attempted first");
|
|
772
|
+
assert.strictEqual(execCalls.length, 1, "subprocess should run as fallback");
|
|
773
|
+
});
|
|
774
|
+
it("inherits the active session model and execution context for subprocess fallback", async () => {
|
|
775
|
+
const pi = createMockPi();
|
|
776
|
+
setupWithDirectDeps(pi, { ok: false, appliedCount: 0, fallbackReason: "no_auth" }, {
|
|
777
|
+
...defaultConfig,
|
|
778
|
+
reviewTransport: "direct",
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
fireMessageEnd("user");
|
|
782
|
+
fireMessageEnd("user");
|
|
783
|
+
fireMessageEnd("user");
|
|
784
|
+
const signal = new AbortController().signal;
|
|
785
|
+
for (let i = 0; i < 10; i++) {
|
|
786
|
+
fireTurnEnd(makeBranch(10), {
|
|
787
|
+
cwd: "/tmp/local-session",
|
|
788
|
+
model: { provider: "local-llama", id: "local-9b" },
|
|
789
|
+
signal,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
await reviewSettledSignal.promise;
|
|
793
|
+
|
|
794
|
+
assert.deepStrictEqual(logicalChildArgs(0).slice(0, 5), [
|
|
795
|
+
"-p", "--no-session", "--model", "local-llama/local-9b", "--no-extensions",
|
|
796
|
+
]);
|
|
797
|
+
assert.deepStrictEqual(execCalls[0][2], {
|
|
798
|
+
cwd: "/tmp/local-session",
|
|
799
|
+
timeout: 125000,
|
|
800
|
+
});
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it("surfaces one actionable diagnostic when direct and subprocess review both fail", async () => {
|
|
804
|
+
const pi = createMockPi({ code: 1, stdout: "", stderr: "No API key for local-llama/local-9b" });
|
|
805
|
+
setupWithDirectDeps(pi, {
|
|
806
|
+
ok: false,
|
|
807
|
+
appliedCount: 0,
|
|
808
|
+
fallbackReason: "no_auth",
|
|
809
|
+
error: "No API key for local-llama",
|
|
810
|
+
}, {
|
|
811
|
+
...defaultConfig,
|
|
812
|
+
reviewTransport: "direct",
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
fireMessageEnd("user");
|
|
816
|
+
fireMessageEnd("user");
|
|
817
|
+
fireMessageEnd("user");
|
|
818
|
+
for (let i = 0; i < 10; i++) {
|
|
819
|
+
fireTurnEnd(makeBranch(10), {
|
|
820
|
+
model: { provider: "local-llama", id: "local-9b" },
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
await reviewSettledSignal.promise;
|
|
824
|
+
|
|
825
|
+
const failures = notifyCalls.filter((n) => n.level === "warning");
|
|
826
|
+
assert.equal(failures.length, 1);
|
|
827
|
+
assert.match(failures[0].msg, /both transports/i);
|
|
828
|
+
assert.match(failures[0].msg, /no_auth/i);
|
|
829
|
+
assert.match(failures[0].msg, /No API key for local-llama\/local-9b/i);
|
|
830
|
+
assert.match(failures[0].msg, /llmModelOverride/i);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
it("falls back to subprocess when direct review throws", async () => {
|
|
835
|
+
const pi = createMockPi();
|
|
836
|
+
setup(pi, { ...defaultConfig, reviewTransport: "direct" } as MemoryConfig, {
|
|
837
|
+
runDirectReview: async (...args: any[]) => {
|
|
838
|
+
directCalls.push(args);
|
|
839
|
+
throw new Error("direct transport exploded");
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
fireMessageEnd("user");
|
|
844
|
+
fireMessageEnd("user");
|
|
845
|
+
fireMessageEnd("user");
|
|
846
|
+
|
|
847
|
+
for (let i = 0; i < 10; i++) {
|
|
848
|
+
fireTurnEnd();
|
|
849
|
+
}
|
|
850
|
+
await reviewSettledSignal.promise;
|
|
851
|
+
|
|
852
|
+
assert.strictEqual(directCalls.length, 1, "direct review should be attempted first");
|
|
853
|
+
assert.strictEqual(execCalls.length, 1, "subprocess should run as fallback when direct review throws");
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it("does not notify when direct review returns no operations", async () => {
|
|
857
|
+
const pi = createMockPi();
|
|
858
|
+
setupWithDirectDeps(pi, { ok: true, appliedCount: 0, fallbackReason: "empty" }, {
|
|
859
|
+
...defaultConfig,
|
|
860
|
+
reviewTransport: "direct",
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
fireMessageEnd("user");
|
|
864
|
+
fireMessageEnd("user");
|
|
865
|
+
fireMessageEnd("user");
|
|
866
|
+
|
|
867
|
+
for (let i = 0; i < 10; i++) {
|
|
868
|
+
fireTurnEnd();
|
|
869
|
+
}
|
|
870
|
+
await reviewSettledSignal.promise;
|
|
871
|
+
|
|
872
|
+
const reviewNotify = notifyCalls.find((n) => n.msg.includes("Memory auto-reviewed"));
|
|
873
|
+
assert.strictEqual(reviewNotify, undefined, "empty direct review should not notify");
|
|
874
|
+
assert.strictEqual(execCalls.length, 0, "empty direct review should not fall back");
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it("includes explicit target routing for an available project store", () => {
|
|
878
|
+
const prompt = buildSubprocessReviewPrompt({
|
|
879
|
+
parts: ["[USER] hello", "[ASSISTANT] hi"],
|
|
880
|
+
currentMemory: "global fact",
|
|
881
|
+
currentUser: "user preference",
|
|
882
|
+
currentProject: "project convention",
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
assert.match(prompt, /project-specific facts.*target "project"/i);
|
|
886
|
+
assert.match(prompt, /global or cross-project facts.*target "memory"/i);
|
|
887
|
+
assert.match(prompt, /failures, corrections.*target "failure"/i);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it("keeps project target unavailable when no project store is present", () => {
|
|
891
|
+
const prompt = buildSubprocessReviewPrompt({
|
|
892
|
+
parts: ["[USER] hello", "[ASSISTANT] hi"],
|
|
893
|
+
currentMemory: "global fact",
|
|
894
|
+
currentUser: "user preference",
|
|
895
|
+
currentProject: null,
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
assert.match(prompt, /do not emit target "project"/i);
|
|
899
|
+
assert.match(prompt, /use target "memory"/i);
|
|
900
|
+
assert.match(prompt, /target "failure"/i);
|
|
901
|
+
assert.doesNotMatch(prompt, /--- Current Project Memory ---/i);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it("builds separate prompts for direct and subprocess transports", () => {
|
|
905
|
+
const input = {
|
|
906
|
+
parts: ["[USER] hello", "[ASSISTANT] hi"],
|
|
907
|
+
currentMemory: "uses pnpm",
|
|
908
|
+
currentUser: "likes TypeScript",
|
|
909
|
+
currentProject: "monorepo layout",
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
const subprocessPrompt = buildSubprocessReviewPrompt(input);
|
|
913
|
+
const directPrompt = buildDirectReviewUserPrompt(input);
|
|
914
|
+
|
|
915
|
+
assert.match(subprocessPrompt, /save using memory_add/i);
|
|
916
|
+
assert.match(directPrompt, /Conversation to Review/);
|
|
917
|
+
assert.doesNotMatch(directPrompt, /save using memory_add/i);
|
|
918
|
+
assert.ok(subprocessPrompt.includes("uses pnpm"));
|
|
919
|
+
assert.ok(directPrompt.includes("monorepo layout"));
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
it("falls back gracefully if getBranch throws", async () => {
|
|
923
|
+
const config = { ...defaultConfig, nudgeToolCalls: 3 };
|
|
924
|
+
const pi = createMockPi();
|
|
925
|
+
setup(pi, config);
|
|
926
|
+
|
|
927
|
+
fireMessageEnd("user");
|
|
928
|
+
fireMessageEnd("user");
|
|
929
|
+
fireMessageEnd("user");
|
|
930
|
+
|
|
931
|
+
// getBranch throws — should not crash
|
|
932
|
+
const crashCtx = {
|
|
933
|
+
sessionManager: { getBranch: () => { throw new Error("session expired"); } },
|
|
934
|
+
signal: undefined as any,
|
|
935
|
+
ui: { notify: () => {} },
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
const h = handlers["turn_end"];
|
|
939
|
+
// Fire 10 turns with crashing getBranch
|
|
940
|
+
for (let i = 0; i < 10; i++) {
|
|
941
|
+
for (const fn of h) {
|
|
942
|
+
fn({}, crashCtx);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
await settle();
|
|
946
|
+
|
|
947
|
+
// Should not throw — we got here = test passed
|
|
948
|
+
assert.ok(true, "no crash when getBranch throws");
|
|
949
|
+
});
|
|
950
|
+
});
|