@selesai/code 0.13.15 → 0.13.18
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 +37 -0
- package/README.md +2 -14
- package/dist/bun/cli.d.ts +3 -1
- package/dist/bun/cli.js +3 -9
- package/dist/bun/{register-bedrock.js → runtime-setup.js} +5 -0
- package/dist/bun/sandbox-env-setup.d.ts +1 -0
- package/dist/bun/sandbox-env-setup.js +3 -0
- package/dist/cli/auth-command.d.ts +23 -0
- package/dist/cli/auth-command.js +102 -0
- package/dist/cli/config-selector.js +2 -1
- package/dist/cli/setup.d.ts +1 -0
- package/dist/cli/setup.js +11 -0
- package/dist/cli.js +2 -14
- package/dist/config.d.ts +3 -2
- package/dist/config.js +9 -4
- package/dist/core/agent-session-runtime.js +15 -7
- package/dist/core/agent-session.d.ts +2 -6
- package/dist/core/agent-session.js +15 -4
- package/dist/core/compaction/branch-summarization.d.ts +1 -1
- package/dist/core/compaction/branch-summarization.js +2 -1
- package/dist/core/extensions/loader.js +3 -0
- package/dist/core/extensions/types.d.ts +16 -2
- package/dist/core/http-dispatcher.js +2 -0
- package/dist/core/keybindings.d.ts +5 -0
- package/dist/core/keybindings.js +4 -0
- package/dist/core/messages.d.ts +1 -1
- package/dist/core/model-config.d.ts +15 -0
- package/dist/core/model-config.js +3 -0
- package/dist/core/model-runtime.d.ts +1 -0
- package/dist/core/model-runtime.js +5 -2
- package/dist/core/provider-composer.js +8 -2
- package/dist/core/session-export.d.ts +3 -0
- package/dist/core/session-export.js +31 -0
- package/dist/core/session-manager.d.ts +3 -2
- package/dist/core/session-manager.js +40 -13
- package/dist/core/skills.d.ts +1 -1
- package/dist/core/skills.js +4 -2
- package/dist/core/system-prompt.js +10 -10
- package/dist/core/tools/bash.js +3 -117
- package/dist/core/tools/edit.d.ts +1 -12
- package/dist/core/tools/edit.js +5 -163
- package/dist/core/tools/find.js +4 -55
- package/dist/core/tools/grep.js +4 -60
- package/dist/core/tools/ls.js +4 -49
- package/dist/core/tools/read.d.ts +4 -0
- package/dist/core/tools/read.js +10 -122
- package/dist/core/tools/renderers/bash.d.ts +11 -0
- package/dist/core/tools/renderers/bash.js +129 -0
- package/dist/core/tools/renderers/edit.d.ts +22 -0
- package/dist/core/tools/renderers/edit.js +166 -0
- package/dist/core/tools/renderers/find.d.ts +9 -0
- package/dist/core/tools/renderers/find.js +63 -0
- package/dist/core/tools/renderers/grep.d.ts +9 -0
- package/dist/core/tools/renderers/grep.js +68 -0
- package/dist/core/tools/renderers/index.d.ts +27 -0
- package/dist/core/tools/renderers/index.js +46 -0
- package/dist/core/tools/renderers/ls.d.ts +9 -0
- package/dist/core/tools/renderers/ls.js +57 -0
- package/dist/core/tools/renderers/read.d.ts +10 -0
- package/dist/core/tools/renderers/read.js +128 -0
- package/dist/core/tools/renderers/write.d.ts +9 -0
- package/dist/core/tools/renderers/write.js +152 -0
- package/dist/core/tools/write.js +5 -146
- package/dist/extensions/capability-gateway/index.ts +12 -1
- package/dist/extensions/capability-gateway/integration.test.ts +23 -0
- package/dist/extensions/pi-hermes-memory/CHANGELOG.md +18 -0
- package/dist/extensions/pi-hermes-memory/README.md +16 -12
- package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +1 -1
- package/dist/extensions/pi-hermes-memory/package-lock.json +587 -62
- package/dist/extensions/pi-hermes-memory/package.json +4 -4
- package/dist/extensions/pi-hermes-memory/src/config.ts +32 -0
- package/dist/extensions/pi-hermes-memory/src/constants.ts +12 -2
- package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +109 -51
- package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +9 -3
- package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +257 -98
- package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +9 -2
- package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +5 -0
- package/dist/extensions/pi-hermes-memory/src/index.ts +22 -2
- package/dist/extensions/pi-hermes-memory/src/paths.ts +1 -1
- package/dist/extensions/pi-hermes-memory/src/store/db.ts +6 -0
- package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +58 -1
- package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +60 -8
- package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +169 -7
- package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +7 -21
- package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +134 -15
- package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +4 -2
- package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +24 -9
- package/dist/extensions/pi-hermes-memory/src/types.ts +11 -0
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +42 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +159 -3
- package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +452 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +39 -0
- package/dist/extensions/pi-hermes-memory/tests/integration/corruption-recovery-integration.test.ts +91 -0
- package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +29 -0
- package/dist/extensions/pi-hermes-memory/tests/store/fts-query.test.ts +116 -0
- package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +163 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +233 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +12 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +175 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +2 -2
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +22 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +37 -1
- package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
- package/dist/extensions/pi-intercom/README.md +16 -2
- package/dist/extensions/pi-intercom/broker/spawn.test.ts +19 -2
- package/dist/extensions/pi-intercom/broker/spawn.ts +4 -4
- package/dist/extensions/pi-intercom/index.ts +1 -1
- package/dist/extensions/pi-intercom/intercom.integration.test.ts +121 -2
- package/dist/extensions/pi-intercom/package.json +1 -1
- package/dist/extensions/pi-intercom/project-agent.test.ts +1 -1
- package/dist/extensions/pi-intercom/project-agent.ts +1 -6
- package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +7 -6
- package/dist/extensions/pi-rewind-hook/index.test.ts +7 -0
- package/dist/extensions/pi-subagents/.github/workflows/test.yml +36 -4
- package/dist/extensions/pi-subagents/CHANGELOG.md +120 -0
- package/dist/extensions/pi-subagents/README.md +3 -3
- package/dist/extensions/pi-subagents/agents/researcher.md +23 -13
- package/dist/extensions/pi-subagents/agents/reviewer.md +1 -1
- package/dist/extensions/pi-subagents/agents/scout.md +1 -1
- package/dist/extensions/pi-subagents/docs/agents.md +32 -16
- package/dist/extensions/pi-subagents/docs/configuration.md +41 -15
- package/dist/extensions/pi-subagents/docs/extension-api.md +109 -6
- package/dist/extensions/pi-subagents/docs/missions.md +2 -0
- package/dist/extensions/pi-subagents/docs/models.md +58 -1
- package/dist/extensions/pi-subagents/docs/observability.md +50 -12
- package/dist/extensions/pi-subagents/docs/tool-reference.md +17 -10
- package/dist/extensions/pi-subagents/docs/watchdog.md +1 -1
- package/dist/extensions/pi-subagents/docs/workflows.md +26 -9
- package/dist/extensions/pi-subagents/package-lock.json +747 -177
- package/dist/extensions/pi-subagents/package.json +6 -4
- package/dist/extensions/pi-subagents/runner-server-preload.mjs +13 -0
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +21 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- package/dist/extensions/pi-subagents/src/agents/advertised-agent-prompt.ts +63 -0
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +61 -16
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +8 -0
- package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +0 -1
- package/dist/extensions/pi-subagents/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/dist/extensions/pi-subagents/src/api/preflight.ts +7 -4
- package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -2
- package/dist/extensions/pi-subagents/src/api/workflow-resources.ts +6 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +4 -2
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +2 -10
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +9 -11
- package/dist/extensions/pi-subagents/src/extension/index.ts +99 -10
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +13 -0
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +7 -23
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +11 -8
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +25 -7
- package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +73 -0
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +228 -136
- package/dist/extensions/pi-subagents/src/intercom/supervisor-ui.ts +244 -0
- package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +37 -16
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +18 -18
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +51 -25
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +46 -3
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -3
- package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +10 -12
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -15
- package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +40 -29
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +8 -0
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +79 -247
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +88 -12
- package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +6 -6
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +24 -24
- package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +642 -0
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +75 -6
- package/dist/extensions/pi-subagents/src/runs/background/runner-aliases.ts +163 -0
- package/dist/extensions/pi-subagents/src/runs/background/runner-child-launch.ts +86 -0
- package/dist/extensions/pi-subagents/src/runs/background/runner-child-sessions.ts +31 -0
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +18 -4
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +597 -1092
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +3 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +20 -17
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +296 -394
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +630 -283
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +78 -98
- package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +16 -3
- package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +47 -47
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -2
- package/dist/extensions/pi-subagents/src/runs/shared/child-hooks.ts +174 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +14 -3
- package/dist/extensions/pi-subagents/src/runs/shared/child-launch.ts +319 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-lifecycle.ts +25 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-runtime-config.ts +126 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-session.ts +373 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +530 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +11 -11
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +30 -20
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +84 -15
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +88 -12
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +37 -53
- package/dist/extensions/pi-subagents/src/runs/shared/nested-path.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +18 -7
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +57 -12
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +3 -4
- package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +0 -11
- package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/dist/extensions/pi-subagents/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/dist/extensions/pi-subagents/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +8 -21
- package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +3 -30
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +17 -4
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +99 -386
- package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +18 -62
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +0 -14
- package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +31 -9
- package/dist/extensions/pi-subagents/src/runs/shared/worktree-setup-command.ts +190 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +506 -226
- package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +1 -1
- package/dist/extensions/pi-subagents/src/shared/jsonl-writer.ts +11 -0
- package/dist/extensions/pi-subagents/src/shared/model-response-aliases.ts +13 -0
- package/dist/extensions/pi-subagents/src/shared/thinking-ceiling.ts +0 -6
- package/dist/extensions/pi-subagents/src/shared/types.ts +145 -89
- package/dist/extensions/pi-subagents/src/shared/utils.ts +13 -6
- package/dist/extensions/pi-subagents/src/shared/watch-strategy.ts +2 -0
- package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +18 -13
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +0 -6
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +1 -1
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +11 -6
- package/dist/extensions/pi-subagents/src/tui/render.ts +277 -46
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +0 -1
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +12 -12
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -3
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +116 -15
- package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +15 -18
- package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +57 -8
- package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +19 -19
- package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +3 -3
- package/dist/extensions/pi-subagents/src/workflows/workflow-resources.ts +97 -22
- package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +3 -0
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +336 -21
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-1.test.ts +1181 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-2.test.ts +2240 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +2268 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.part-4.test.ts +1512 -0
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +231 -16
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +4 -18
- package/dist/extensions/pi-subagents/test/integration/foreign-workflow-steering.test.ts +95 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +9 -15
- package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +374 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -10
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +18 -6
- package/dist/extensions/pi-subagents/test/integration/recovered-assistant-error.test.ts +151 -0
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +42 -1
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +150 -25
- package/dist/extensions/pi-subagents/test/integration/result-publication.test.ts +354 -0
- package/dist/extensions/pi-subagents/test/integration/shared-cwd-terminal-evidence.test.ts +117 -0
- package/dist/extensions/pi-subagents/test/integration/single-execution.part-1.test.ts +3678 -0
- package/dist/extensions/pi-subagents/test/integration/{single-execution.test.ts → single-execution.part-2.test.ts} +500 -4207
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -2
- package/dist/extensions/pi-subagents/test/integration/workflow-result-publication.test.ts +127 -0
- package/dist/extensions/pi-subagents/test/integration/workflow-steer-inbox.test.ts +365 -0
- package/dist/extensions/pi-subagents/test/smoke/pi085-child.ts +31 -0
- package/dist/extensions/pi-subagents/test/smoke/pi085-clean-install.mjs +74 -0
- package/dist/extensions/pi-subagents/test/smoke/pi085-extension.ts +16 -0
- package/dist/extensions/pi-subagents/test/support/async-execution-fixture.ts +699 -0
- package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +418 -0
- package/dist/extensions/pi-subagents/test/support/foreign-workflow-steer-host.mjs +13 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +30 -76
- package/dist/extensions/pi-subagents/test/support/notify-diagnostics-fixture.ts +84 -0
- package/dist/extensions/pi-subagents/test/support/result-publication-capacity-preload.mjs +70 -0
- package/dist/extensions/pi-subagents/test/support/runner-child-session-factory.ts +17 -0
- package/dist/extensions/pi-subagents/test/support/single-execution-fixture.ts +390 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance-compaction.test.ts +120 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +24 -3
- package/dist/extensions/pi-subagents/test/unit/advertised-agent-prompt.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/unit/advertised-agent-refresh.test.ts +163 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -31
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +78 -3
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +2 -8
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +73 -66
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +51 -1
- package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +103 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +9 -8
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +10 -7
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +48 -6
- package/dist/extensions/pi-subagents/test/unit/child-runtime-config.test.ts +90 -0
- package/dist/extensions/pi-subagents/test/unit/{pi-args-permission-system.test.ts → child-tool-plan-permission-system.test.ts} +48 -86
- package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +18 -18
- package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +9 -9
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +8 -7
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +225 -65
- package/dist/extensions/pi-subagents/test/unit/extension-bindings.test.ts +17 -22
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +11 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +47 -3
- package/dist/extensions/pi-subagents/test/unit/foreign-workflow-steering.test.ts +74 -0
- package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +23 -29
- package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +96 -4
- package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +4 -4
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +116 -55
- package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +58 -1
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +104 -3
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +10 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +31 -1
- package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +102 -51
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +196 -6
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +181 -38
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +62 -70
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +58 -53
- package/dist/extensions/pi-subagents/test/unit/notify-diagnostics.test.ts +42 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +13 -5
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +21 -5
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +5 -4
- package/dist/extensions/pi-subagents/test/unit/permissions.test.ts +2 -6
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +28 -2
- package/dist/extensions/pi-subagents/test/unit/pi-web-session-liveness.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +38 -1
- package/dist/extensions/pi-subagents/test/unit/prompt-audit-optional-peers.test.ts +43 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +24 -2
- package/dist/extensions/pi-subagents/test/unit/readonly-drain-observation.test.ts +174 -0
- package/dist/extensions/pi-subagents/test/unit/readonly-model-continuation.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/readonly-session-evidence.test.ts +1329 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +29 -80
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +11 -8
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/retained-nested-route-tracker.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +38 -12
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +2 -5
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +89 -1
- package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +2 -16
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +27 -3
- package/dist/extensions/pi-subagents/test/unit/scheduled-supervisor-demand.test.ts +215 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +15 -11
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +237 -1
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -5
- package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +14 -1
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +130 -739
- package/dist/extensions/pi-subagents/test/unit/supervisor-ask-registration.test.ts +682 -0
- package/dist/extensions/pi-subagents/test/unit/supervisor-ui.test.ts +137 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +1 -11
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +23 -3
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +8 -1
- package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +19 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +72 -4
- package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +17 -3
- package/dist/extensions/pi-subagents/test/unit/workflow-child-summary.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +12 -2
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +46 -6
- package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/workflow-resources.test.ts +133 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +53 -2
- package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +222 -14
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +526 -85
- package/dist/extensions/pi-web-agent/src/backends/config.ts +1 -0
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +5 -3
- package/dist/extensions/pi-web-agent/src/search/tokenin.ts +7 -3
- package/dist/extensions/pi-web-agent/src/types.ts +1 -1
- package/dist/extensions/pi-zentui/README.md +17 -0
- package/dist/extensions/pi-zentui/assets/screenshots/footer-status-hyperlink.png +0 -0
- package/dist/extensions/pi-zentui/docs/configuration.md +71 -2
- package/dist/extensions/pi-zentui/extensions/zentui/config.ts +11 -0
- package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +14 -0
- package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +29 -13
- package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +5 -4
- package/dist/extensions/pi-zentui/extensions/zentui/footer-text.ts +11 -0
- package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +11 -10
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +77 -3
- package/dist/extensions/pi-zentui/extensions/zentui/presets.ts +77 -0
- package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +103 -2
- package/dist/extensions/pi-zentui/extensions/zentui/working-line-extension-segments.ts +115 -0
- package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +87 -5
- package/dist/extensions/pi-zentui/package-lock.json +277 -517
- package/dist/extensions/pi-zentui/package.json +7 -4
- package/dist/extensions/pi-zentui/test/config-presets.test.ts +238 -0
- package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +24 -1
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +570 -2
- package/dist/extensions/pi-zentui/test/extension-status.test.ts +90 -0
- package/dist/extensions/pi-zentui/test/footer-hyperlink.test.ts +243 -0
- package/dist/extensions/pi-zentui/test/package-contents.mjs +1 -0
- package/dist/extensions/pi-zentui/test/presets.test.ts +121 -0
- package/dist/extensions/pi-zentui/test/settings-command.test.ts +205 -1
- package/dist/extensions/pi-zentui/test/working-line-extension-segments.test.ts +139 -0
- package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +237 -3
- package/dist/extensions/pi-zentui/test/working-line.test.ts +94 -2
- package/dist/index.d.ts +1 -1
- package/dist/main.js +4 -1
- package/dist/modes/interactive/chat-viewport.d.ts +19 -0
- package/dist/modes/interactive/chat-viewport.js +27 -0
- package/dist/modes/interactive/components/assistant-message.d.ts +6 -2
- package/dist/modes/interactive/components/assistant-message.js +30 -13
- package/dist/modes/interactive/components/custom-editor.d.ts +10 -1
- package/dist/modes/interactive/components/custom-editor.js +39 -1
- package/dist/modes/interactive/components/first-time-setup.js +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/markdown-transform.d.ts +2 -0
- package/dist/modes/interactive/components/markdown-transform.js +18 -0
- package/dist/modes/interactive/components/model-selector.js +19 -17
- package/dist/modes/interactive/components/scoped-models-selector.js +21 -23
- package/dist/modes/interactive/components/settings-selector.d.ts +7 -1
- package/dist/modes/interactive/components/settings-selector.js +128 -20
- package/dist/modes/interactive/components/settings-submenu.d.ts +71 -0
- package/dist/modes/interactive/components/settings-submenu.js +164 -0
- package/dist/modes/interactive/components/status-indicator.d.ts +3 -1
- package/dist/modes/interactive/components/status-indicator.js +10 -3
- package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
- package/dist/modes/interactive/components/thinking-selector.js +75 -18
- package/dist/modes/interactive/components/tool-execution.d.ts +21 -4
- package/dist/modes/interactive/components/tool-execution.js +35 -33
- package/dist/modes/interactive/components/trust-selector.js +2 -2
- package/dist/modes/interactive/interactive-mode.d.ts +13 -15
- package/dist/modes/interactive/interactive-mode.js +144 -80
- package/dist/modes/interactive/session-share.d.ts +15 -0
- package/dist/modes/interactive/session-share.js +183 -0
- package/dist/modes/interactive/theme/dark.json +2 -1
- package/dist/modes/interactive/theme/light.json +2 -1
- package/dist/modes/interactive/theme/theme-controller.d.ts +1 -0
- package/dist/modes/interactive/theme/theme-controller.js +5 -0
- package/dist/modes/interactive/theme/theme-json.d.ts +83 -0
- package/dist/modes/interactive/theme/theme-json.js +129 -0
- package/dist/modes/interactive/theme/theme-schema.json +9 -1
- package/dist/modes/interactive/theme/theme.d.ts +14 -5
- package/dist/modes/interactive/theme/theme.js +18 -119
- package/dist/modes/interactive/tui-renderer.d.ts +20 -0
- package/dist/modes/interactive/tui-renderer.js +65 -0
- package/dist/utils/exif-orientation.js +2 -3
- package/dist/utils/management-http.d.ts +24 -0
- package/dist/utils/management-http.js +53 -0
- package/dist/utils/text.d.ts +7 -0
- package/dist/utils/text.js +8 -0
- package/dist/utils/tools-manager.d.ts +1 -0
- package/dist/utils/tools-manager.js +43 -21
- package/docs/docs.json +0 -4
- package/package.json +4 -4
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +0 -415
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +0 -1059
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/dist/extensions/pi-subagents/src/shared/post-exit-stdio-guard.ts +0 -85
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +0 -224
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +0 -6986
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +0 -447
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +0 -185
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +0 -311
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +0 -84
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +0 -202
- package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +0 -101
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +0 -2347
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +0 -104
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +0 -49
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -1465
- package/dist/skills/imagegen-frontend-web/SKILL.md +0 -987
- package/dist/skills/implanger/SKILL.md +0 -70
- package/dist/skills/output/SKILL.md +0 -49
- package/dist/skills/planger/SKILL.md +0 -167
- package/dist/skills/workflow/SKILL.md +0 -95
- package/docs/workflows.md +0 -18
- /package/dist/bun/{register-bedrock.d.ts → runtime-setup.d.ts} +0 -0
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
} from "../../src/handlers/review-memory-ops.js";
|
|
15
15
|
import { DatabaseManager } from "../../src/store/db.js";
|
|
16
16
|
import { getMemories, reconcileMarkdownMemoryScope } from "../../src/store/sqlite-memory-store.js";
|
|
17
|
+
import { acquireMarkdownMutationLock } from "../../src/store/markdown-mutation-lock.js";
|
|
18
|
+
import { MEMORY_FILE } from "../../src/constants.js";
|
|
17
19
|
|
|
18
20
|
function mockModel(reasoning: boolean): Model<Api> {
|
|
19
21
|
return {
|
|
@@ -90,6 +92,19 @@ describe("provider auth resolution", () => {
|
|
|
90
92
|
return { usedKeys, complete };
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
function registryWithHeaderAuth(...headers: Array<Record<string, string | null>>) {
|
|
96
|
+
let authCalls = 0;
|
|
97
|
+
return {
|
|
98
|
+
getApiKeyAndHeaders: async () => ({
|
|
99
|
+
ok: true as const,
|
|
100
|
+
headers: headers[Math.min(authCalls++, headers.length - 1)],
|
|
101
|
+
}),
|
|
102
|
+
getAll: () => [mockModel(false)],
|
|
103
|
+
getAvailable: () => [mockModel(false)],
|
|
104
|
+
get authCalls() { return authCalls; },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
93
108
|
const emptyOperations = {
|
|
94
109
|
stopReason: "stop",
|
|
95
110
|
content: [{ type: "text", text: JSON.stringify({ operations: [] }) }],
|
|
@@ -99,6 +114,18 @@ describe("provider auth resolution", () => {
|
|
|
99
114
|
return { userPrompt: "u", systemPrompt: "s", config: {} };
|
|
100
115
|
}
|
|
101
116
|
|
|
117
|
+
async function runReview(modelRegistry: unknown, complete: unknown) {
|
|
118
|
+
return runDirectMemoryCompletion(
|
|
119
|
+
{ model: mockModel(false), modelRegistry } as never,
|
|
120
|
+
null as never,
|
|
121
|
+
null,
|
|
122
|
+
directOptions(),
|
|
123
|
+
null,
|
|
124
|
+
null,
|
|
125
|
+
{ completeSimple: complete as never },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
102
129
|
it("resolves credentials through the public registry API", async () => {
|
|
103
130
|
const registry = registryWithAuthResponses("current-key");
|
|
104
131
|
const { usedKeys, complete } = completionStub(() => emptyOperations);
|
|
@@ -118,6 +145,44 @@ describe("provider auth resolution", () => {
|
|
|
118
145
|
assert.deepStrictEqual(usedKeys, ["current-key"]);
|
|
119
146
|
});
|
|
120
147
|
|
|
148
|
+
it("runs direct review with header-only OAuth request auth", async () => {
|
|
149
|
+
const headers = {
|
|
150
|
+
Authorization: "Bearer kimi-oauth-token",
|
|
151
|
+
"User-Agent": "pi-coding-agent",
|
|
152
|
+
"X-Drop": null,
|
|
153
|
+
};
|
|
154
|
+
const usedHeaders: Array<Record<string, string | null> | undefined> = [];
|
|
155
|
+
const complete = async (_model: unknown, _request: unknown, options: { headers?: Record<string, string | null> }) => {
|
|
156
|
+
usedHeaders.push(options.headers);
|
|
157
|
+
return emptyOperations;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const result = await runReview(registryWithHeaderAuth(headers), complete);
|
|
161
|
+
|
|
162
|
+
assert.strictEqual(result.ok, true);
|
|
163
|
+
assert.deepStrictEqual(usedHeaders, [headers]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
for (const { name, headers } of [
|
|
167
|
+
{ name: "empty headers", headers: {} },
|
|
168
|
+
{ name: "User-Agent-only headers", headers: { "User-Agent": "pi-coding-agent" } },
|
|
169
|
+
{ name: "null credential headers", headers: { Authorization: null, "x-api-key": null } },
|
|
170
|
+
]) {
|
|
171
|
+
it(`rejects ${name} as missing request authentication`, async () => {
|
|
172
|
+
let completionCalls = 0;
|
|
173
|
+
const complete = async () => {
|
|
174
|
+
completionCalls++;
|
|
175
|
+
return emptyOperations;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const result = await runReview(registryWithHeaderAuth(headers), complete);
|
|
179
|
+
|
|
180
|
+
assert.strictEqual(result.ok, false);
|
|
181
|
+
assert.strictEqual(result.fallbackReason, "no_auth");
|
|
182
|
+
assert.strictEqual(completionCalls, 0);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
121
186
|
it("re-resolves credentials after a provider auth rejection", async () => {
|
|
122
187
|
const { modelRegistry } = registryWithAuthResponses("revoked-key", "rotated-key");
|
|
123
188
|
const { usedKeys, complete } = completionStub((_key, attempt) => {
|
|
@@ -139,6 +204,35 @@ describe("provider auth resolution", () => {
|
|
|
139
204
|
assert.deepStrictEqual(usedKeys, ["revoked-key", "rotated-key"]);
|
|
140
205
|
});
|
|
141
206
|
|
|
207
|
+
it("re-resolves rotated header-only OAuth credentials after rejection", async () => {
|
|
208
|
+
const usedHeaders: Array<Record<string, string> | undefined> = [];
|
|
209
|
+
const complete = async (_model: unknown, _request: unknown, options: { headers?: Record<string, string> }) => {
|
|
210
|
+
usedHeaders.push(options.headers);
|
|
211
|
+
if (usedHeaders.length === 1) throw new Error("HTTP 401 Unauthorized: token expired");
|
|
212
|
+
return emptyOperations;
|
|
213
|
+
};
|
|
214
|
+
const modelRegistry = registryWithHeaderAuth(
|
|
215
|
+
{ Authorization: "Bearer stale-token" },
|
|
216
|
+
{ Authorization: "Bearer fresh-token" },
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const result = await runDirectMemoryCompletion(
|
|
220
|
+
{ model: mockModel(false), modelRegistry } as never,
|
|
221
|
+
null as never,
|
|
222
|
+
null,
|
|
223
|
+
directOptions(),
|
|
224
|
+
null,
|
|
225
|
+
null,
|
|
226
|
+
{ completeSimple: complete as never },
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
assert.strictEqual(result.ok, true);
|
|
230
|
+
assert.deepStrictEqual(usedHeaders, [
|
|
231
|
+
{ Authorization: "Bearer stale-token" },
|
|
232
|
+
{ Authorization: "Bearer fresh-token" },
|
|
233
|
+
]);
|
|
234
|
+
});
|
|
235
|
+
|
|
142
236
|
it("does not retry when the refreshed key is the same one the provider rejected", async () => {
|
|
143
237
|
const { modelRegistry } = registryWithAuthResponses("only-key");
|
|
144
238
|
const { usedKeys, complete } = completionStub(() => new Error("HTTP 401 Unauthorized"));
|
|
@@ -158,6 +252,129 @@ describe("provider auth resolution", () => {
|
|
|
158
252
|
assert.strictEqual(usedKeys.length, 1, "an unchanged key means a real auth problem, not a rotation race");
|
|
159
253
|
});
|
|
160
254
|
|
|
255
|
+
it("does not retry an unchanged header-only OAuth credential", async () => {
|
|
256
|
+
let completionCalls = 0;
|
|
257
|
+
const complete = async () => {
|
|
258
|
+
completionCalls++;
|
|
259
|
+
throw new Error("HTTP 401 Unauthorized");
|
|
260
|
+
};
|
|
261
|
+
const modelRegistry = registryWithHeaderAuth({ Authorization: "Bearer unchanged-token" });
|
|
262
|
+
|
|
263
|
+
const result = await runDirectMemoryCompletion(
|
|
264
|
+
{ model: mockModel(false), modelRegistry } as never,
|
|
265
|
+
null as never,
|
|
266
|
+
null,
|
|
267
|
+
directOptions(),
|
|
268
|
+
null,
|
|
269
|
+
null,
|
|
270
|
+
{ completeSimple: complete as never },
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
assert.strictEqual(result.ok, false);
|
|
274
|
+
assert.strictEqual(result.fallbackReason, "provider_error");
|
|
275
|
+
assert.strictEqual(completionCalls, 1);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("rotates header-only auth after an error assistant 401 response", async () => {
|
|
279
|
+
const usedHeaders: Array<Record<string, string | null> | undefined> = [];
|
|
280
|
+
const complete = async (_model: unknown, _request: unknown, options: { headers?: Record<string, string | null> }) => {
|
|
281
|
+
usedHeaders.push(options.headers);
|
|
282
|
+
if (usedHeaders.length === 1) {
|
|
283
|
+
return { stopReason: "error", errorMessage: "HTTP 401 Unauthorized: token expired" };
|
|
284
|
+
}
|
|
285
|
+
return emptyOperations;
|
|
286
|
+
};
|
|
287
|
+
const modelRegistry = registryWithHeaderAuth(
|
|
288
|
+
{ Authorization: "Bearer stale-token" },
|
|
289
|
+
{ Authorization: "Bearer fresh-token" },
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const result = await runDirectMemoryCompletion(
|
|
293
|
+
{ model: mockModel(false), modelRegistry } as never,
|
|
294
|
+
null as never,
|
|
295
|
+
null,
|
|
296
|
+
directOptions(),
|
|
297
|
+
null,
|
|
298
|
+
null,
|
|
299
|
+
{ completeSimple: complete as never },
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
assert.strictEqual(result.ok, true);
|
|
303
|
+
assert.deepStrictEqual(usedHeaders, [
|
|
304
|
+
{ Authorization: "Bearer stale-token" },
|
|
305
|
+
{ Authorization: "Bearer fresh-token" },
|
|
306
|
+
]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("does not retry when only the credential header name casing changed", async () => {
|
|
310
|
+
let completionCalls = 0;
|
|
311
|
+
const complete = async () => {
|
|
312
|
+
completionCalls++;
|
|
313
|
+
throw new Error("HTTP 401 Unauthorized");
|
|
314
|
+
};
|
|
315
|
+
const modelRegistry = registryWithHeaderAuth(
|
|
316
|
+
{ Authorization: "Bearer same-token" },
|
|
317
|
+
{ authorization: "Bearer same-token" },
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const result = await runDirectMemoryCompletion(
|
|
321
|
+
{ model: mockModel(false), modelRegistry } as never,
|
|
322
|
+
null as never,
|
|
323
|
+
null,
|
|
324
|
+
directOptions(),
|
|
325
|
+
null,
|
|
326
|
+
null,
|
|
327
|
+
{ completeSimple: complete as never },
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
assert.strictEqual(result.ok, false);
|
|
331
|
+
assert.strictEqual(result.fallbackReason, "provider_error");
|
|
332
|
+
assert.strictEqual(completionCalls, 1);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("retries once when a duplicate-case credential header value changes", async () => {
|
|
336
|
+
const stale = { Authorization: "Bearer stale", authorization: "Bearer shared" };
|
|
337
|
+
const fresh = { Authorization: "Bearer fresh", authorization: "Bearer shared" };
|
|
338
|
+
const usedHeaders: Array<Record<string, string | null> | undefined> = [];
|
|
339
|
+
const complete = async (_model: unknown, _request: unknown, options: { headers?: Record<string, string | null> }) => {
|
|
340
|
+
usedHeaders.push(options.headers);
|
|
341
|
+
if (usedHeaders.length === 1) {
|
|
342
|
+
return { stopReason: "error", errorMessage: "HTTP 401 Unauthorized: token expired" };
|
|
343
|
+
}
|
|
344
|
+
return emptyOperations;
|
|
345
|
+
};
|
|
346
|
+
const modelRegistry = registryWithHeaderAuth(stale, fresh);
|
|
347
|
+
|
|
348
|
+
const result = await runReview(modelRegistry, complete);
|
|
349
|
+
|
|
350
|
+
assert.strictEqual(result.ok, true);
|
|
351
|
+
assert.strictEqual(modelRegistry.authCalls, 2);
|
|
352
|
+
assert.deepStrictEqual(usedHeaders, [stale, fresh]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("returns provider_error after a second auth failure without a third completion", async () => {
|
|
356
|
+
let completionCalls = 0;
|
|
357
|
+
const complete = async () => {
|
|
358
|
+
completionCalls++;
|
|
359
|
+
if (completionCalls <= 2) {
|
|
360
|
+
return { stopReason: "error", errorMessage: "HTTP 401 Unauthorized" };
|
|
361
|
+
}
|
|
362
|
+
return emptyOperations;
|
|
363
|
+
};
|
|
364
|
+
const modelRegistry = registryWithHeaderAuth(
|
|
365
|
+
{ Authorization: "Bearer stale-token" },
|
|
366
|
+
{ Authorization: "Bearer still-bad-token" },
|
|
367
|
+
{ Authorization: "Bearer should-not-be-used" },
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const result = await runReview(modelRegistry, complete);
|
|
371
|
+
|
|
372
|
+
assert.strictEqual(result.ok, false);
|
|
373
|
+
assert.strictEqual(result.fallbackReason, "provider_error");
|
|
374
|
+
assert.strictEqual(completionCalls, 2);
|
|
375
|
+
assert.strictEqual(modelRegistry.authCalls, 2);
|
|
376
|
+
});
|
|
377
|
+
|
|
161
378
|
it("classifies provider auth rejections without swallowing other failures", () => {
|
|
162
379
|
for (const message of [
|
|
163
380
|
"HTTP 401 Unauthorized",
|
|
@@ -182,6 +399,87 @@ describe("provider auth resolution", () => {
|
|
|
182
399
|
});
|
|
183
400
|
});
|
|
184
401
|
|
|
402
|
+
describe("fallback model chain", () => {
|
|
403
|
+
function twoModelRegistry() {
|
|
404
|
+
const m1 = { id: "m1", provider: "p1", api: "openai-completions", reasoning: false } as Model<Api>;
|
|
405
|
+
const m2 = { id: "m2", provider: "p2", api: "openai-completions", reasoning: false } as Model<Api>;
|
|
406
|
+
let authCalls = 0;
|
|
407
|
+
const registry = {
|
|
408
|
+
getApiKeyAndHeaders: async () => {
|
|
409
|
+
authCalls++;
|
|
410
|
+
return { ok: true as const, apiKey: "k" };
|
|
411
|
+
},
|
|
412
|
+
getAll: () => [m1, m2],
|
|
413
|
+
getAvailable: () => [m1, m2],
|
|
414
|
+
};
|
|
415
|
+
return { registry, get authCalls() { return authCalls; } };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function chainOptions(signal?: AbortSignal) {
|
|
419
|
+
return {
|
|
420
|
+
userPrompt: "u",
|
|
421
|
+
systemPrompt: "s",
|
|
422
|
+
config: { llmModelOverride: "p1/m1", llmFallbackModels: ["p2/m2"] },
|
|
423
|
+
...(signal ? { signal } : {}),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const emptyReview = {
|
|
428
|
+
stopReason: "stop",
|
|
429
|
+
content: [{ type: "text", text: JSON.stringify({ operations: [] }) }],
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
it("stops the chain when the caller aborts instead of trying the next model", async () => {
|
|
433
|
+
const caller = new AbortController();
|
|
434
|
+
const attempted: string[] = [];
|
|
435
|
+
const complete = async (model: Model<Api>) => {
|
|
436
|
+
attempted.push(model.id);
|
|
437
|
+
caller.abort();
|
|
438
|
+
return { stopReason: "aborted" };
|
|
439
|
+
};
|
|
440
|
+
const chain = twoModelRegistry();
|
|
441
|
+
|
|
442
|
+
const result = await runDirectMemoryCompletion(
|
|
443
|
+
{ model: undefined, modelRegistry: chain.registry } as never,
|
|
444
|
+
null as never,
|
|
445
|
+
null,
|
|
446
|
+
chainOptions(caller.signal),
|
|
447
|
+
null,
|
|
448
|
+
null,
|
|
449
|
+
{ completeSimple: complete as never },
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
assert.strictEqual(result.ok, false);
|
|
453
|
+
assert.strictEqual(result.fallbackReason, "aborted");
|
|
454
|
+
assert.deepStrictEqual(attempted, ["m1"]);
|
|
455
|
+
assert.strictEqual(chain.authCalls, 1);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("still tries the next model after a per-model timeout while the caller is alive", async () => {
|
|
459
|
+
const caller = new AbortController();
|
|
460
|
+
const attempted: string[] = [];
|
|
461
|
+
const complete = async (model: Model<Api>) => {
|
|
462
|
+
attempted.push(model.id);
|
|
463
|
+
if (attempted.length === 1) return { stopReason: "aborted" };
|
|
464
|
+
return emptyReview;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const chain = twoModelRegistry();
|
|
468
|
+
const result = await runDirectMemoryCompletion(
|
|
469
|
+
{ model: undefined, modelRegistry: chain.registry } as never,
|
|
470
|
+
null as never,
|
|
471
|
+
null,
|
|
472
|
+
chainOptions(caller.signal),
|
|
473
|
+
null,
|
|
474
|
+
null,
|
|
475
|
+
{ completeSimple: complete as never },
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
assert.deepStrictEqual(attempted, ["m1", "m2"]);
|
|
479
|
+
assert.strictEqual(result.ok, true);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
185
483
|
describe("parseReviewOperations", () => {
|
|
186
484
|
it("parses valid JSON operations", () => {
|
|
187
485
|
const parsed = parseReviewOperations(JSON.stringify({
|
|
@@ -575,6 +873,160 @@ describe("applyReviewOperations", () => {
|
|
|
575
873
|
assert.deepStrictEqual(store.getMemoryEntries(), beforeEntries);
|
|
576
874
|
});
|
|
577
875
|
|
|
876
|
+
it("skips auth, provider, and store work when the external signal is already aborted", async () => {
|
|
877
|
+
let authCalls = 0;
|
|
878
|
+
let completeCalls = 0;
|
|
879
|
+
let mutated = false;
|
|
880
|
+
const controller = new AbortController();
|
|
881
|
+
controller.abort();
|
|
882
|
+
const store = {
|
|
883
|
+
add: async () => {
|
|
884
|
+
mutated = true;
|
|
885
|
+
return { success: true };
|
|
886
|
+
},
|
|
887
|
+
};
|
|
888
|
+
const modelRegistry = {
|
|
889
|
+
getApiKeyAndHeaders: async () => {
|
|
890
|
+
authCalls++;
|
|
891
|
+
return { ok: true as const, apiKey: "test-key" };
|
|
892
|
+
},
|
|
893
|
+
getAll: () => [mockModel(false)],
|
|
894
|
+
getAvailable: () => [mockModel(false)],
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
const result = await runDirectMemoryCompletion(
|
|
898
|
+
{ model: mockModel(false), modelRegistry } as never,
|
|
899
|
+
store as never,
|
|
900
|
+
null,
|
|
901
|
+
{
|
|
902
|
+
userPrompt: "u",
|
|
903
|
+
systemPrompt: "s",
|
|
904
|
+
config: {},
|
|
905
|
+
signal: controller.signal,
|
|
906
|
+
},
|
|
907
|
+
null,
|
|
908
|
+
null,
|
|
909
|
+
{
|
|
910
|
+
completeSimple: (async () => {
|
|
911
|
+
completeCalls++;
|
|
912
|
+
return {
|
|
913
|
+
stopReason: "stop",
|
|
914
|
+
content: [{ type: "text", text: JSON.stringify({ operations: [{ action: "add", target: "memory", content: "late" }] }) }],
|
|
915
|
+
};
|
|
916
|
+
}) as never,
|
|
917
|
+
},
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
assert.deepStrictEqual(result, { ok: false, appliedCount: 0, fallbackReason: "aborted" });
|
|
921
|
+
assert.equal(authCalls, 0);
|
|
922
|
+
assert.equal(completeCalls, 0);
|
|
923
|
+
assert.equal(mutated, false);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it("does not apply operations when the provider ignores abort and returns success", async () => {
|
|
927
|
+
const store = new MemoryStore({
|
|
928
|
+
memoryDir: tmpDir,
|
|
929
|
+
memoryCharLimit: 5000,
|
|
930
|
+
userCharLimit: 5000,
|
|
931
|
+
autoConsolidate: true,
|
|
932
|
+
});
|
|
933
|
+
await store.loadFromDisk();
|
|
934
|
+
const controller = new AbortController();
|
|
935
|
+
const modelRegistry = {
|
|
936
|
+
getApiKeyAndHeaders: async () => ({ ok: true as const, apiKey: "test-key" }),
|
|
937
|
+
getAll: () => [mockModel(false)],
|
|
938
|
+
getAvailable: () => [mockModel(false)],
|
|
939
|
+
};
|
|
940
|
+
const complete = async () => {
|
|
941
|
+
controller.abort();
|
|
942
|
+
return {
|
|
943
|
+
stopReason: "stop",
|
|
944
|
+
content: [{
|
|
945
|
+
type: "text",
|
|
946
|
+
text: JSON.stringify({
|
|
947
|
+
operations: [{ action: "add", target: "memory", content: "should not persist after cancel" }],
|
|
948
|
+
}),
|
|
949
|
+
}],
|
|
950
|
+
};
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const result = await runDirectMemoryCompletion(
|
|
954
|
+
{ model: mockModel(false), modelRegistry } as never,
|
|
955
|
+
store,
|
|
956
|
+
null,
|
|
957
|
+
{
|
|
958
|
+
userPrompt: "u",
|
|
959
|
+
systemPrompt: "s",
|
|
960
|
+
config: {},
|
|
961
|
+
signal: controller.signal,
|
|
962
|
+
},
|
|
963
|
+
null,
|
|
964
|
+
null,
|
|
965
|
+
{ completeSimple: complete as never },
|
|
966
|
+
);
|
|
967
|
+
|
|
968
|
+
assert.deepStrictEqual(result, { ok: false, appliedCount: 0, fallbackReason: "aborted" });
|
|
969
|
+
assert.equal(store.getMemoryEntries().some((entry) => entry.includes("should not persist after cancel")), false);
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it("does not write after abort while waiting for the markdown mutation lock", async () => {
|
|
973
|
+
const store = new MemoryStore({
|
|
974
|
+
memoryDir: tmpDir,
|
|
975
|
+
memoryCharLimit: 5000,
|
|
976
|
+
userCharLimit: 5000,
|
|
977
|
+
autoConsolidate: true,
|
|
978
|
+
});
|
|
979
|
+
await store.loadFromDisk();
|
|
980
|
+
const controller = new AbortController();
|
|
981
|
+
const modelRegistry = {
|
|
982
|
+
getApiKeyAndHeaders: async () => ({ ok: true as const, apiKey: "test-key" }),
|
|
983
|
+
getAll: () => [mockModel(false)],
|
|
984
|
+
getAvailable: () => [mockModel(false)],
|
|
985
|
+
};
|
|
986
|
+
const completeEntered = Promise.withResolvers<void>();
|
|
987
|
+
const continueComplete = Promise.withResolvers<void>();
|
|
988
|
+
const complete = async () => {
|
|
989
|
+
completeEntered.resolve();
|
|
990
|
+
await continueComplete.promise;
|
|
991
|
+
return {
|
|
992
|
+
stopReason: "stop",
|
|
993
|
+
content: [{
|
|
994
|
+
type: "text",
|
|
995
|
+
text: JSON.stringify({
|
|
996
|
+
operations: [{ action: "add", target: "memory", content: "late-after-shutdown" }],
|
|
997
|
+
}),
|
|
998
|
+
}],
|
|
999
|
+
};
|
|
1000
|
+
};
|
|
1001
|
+
const lease = await acquireMarkdownMutationLock(path.join(tmpDir, MEMORY_FILE));
|
|
1002
|
+
try {
|
|
1003
|
+
const completion = runDirectMemoryCompletion(
|
|
1004
|
+
{ model: mockModel(false), modelRegistry } as never,
|
|
1005
|
+
store,
|
|
1006
|
+
null,
|
|
1007
|
+
{
|
|
1008
|
+
userPrompt: "u",
|
|
1009
|
+
systemPrompt: "s",
|
|
1010
|
+
config: {},
|
|
1011
|
+
signal: controller.signal,
|
|
1012
|
+
},
|
|
1013
|
+
null,
|
|
1014
|
+
null,
|
|
1015
|
+
{ completeSimple: complete as never },
|
|
1016
|
+
);
|
|
1017
|
+
await completeEntered.promise;
|
|
1018
|
+
continueComplete.resolve();
|
|
1019
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
1020
|
+
controller.abort();
|
|
1021
|
+
lease.release();
|
|
1022
|
+
const result = await completion;
|
|
1023
|
+
assert.deepStrictEqual(result, { ok: false, appliedCount: 0, fallbackReason: "aborted" });
|
|
1024
|
+
assert.equal(store.getMemoryEntries().some((entry) => entry.includes("late-after-shutdown")), false);
|
|
1025
|
+
} finally {
|
|
1026
|
+
lease.release();
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
|
|
578
1030
|
it("uses the in-lock mutation observer as the sole SQLite reconciliation path", async () => {
|
|
579
1031
|
const store = new MemoryStore({
|
|
580
1032
|
memoryDir: tmpDir,
|
|
@@ -189,4 +189,43 @@ describe('session backfill handler', () => {
|
|
|
189
189
|
|
|
190
190
|
assert.equal(completed, false);
|
|
191
191
|
});
|
|
192
|
+
|
|
193
|
+
it('propagates the retention cutoff to the backfill file set', async () => {
|
|
194
|
+
// Only an expired file exists (mtime older than the 30-day cutoff). Even
|
|
195
|
+
// with recent backfill timestamps, the backfill must not be scheduled
|
|
196
|
+
// because the file is outside the retained window.
|
|
197
|
+
writeJsonlSession(sessionsDir, 'project-a', 'old');
|
|
198
|
+
const oldFile = path.join(sessionsDir, 'project-a', 'old.jsonl');
|
|
199
|
+
const cutoffMs = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
200
|
+
const oldMtime = cutoffMs - 1000;
|
|
201
|
+
fs.utimesSync(oldFile, new Date(oldMtime), new Date(oldMtime));
|
|
202
|
+
|
|
203
|
+
touchBackfillTimestamp(dbManager);
|
|
204
|
+
|
|
205
|
+
let receivedCutoff: number | undefined;
|
|
206
|
+
let indexOptions: { retentionCutoffMs?: number } | undefined;
|
|
207
|
+
const state: SessionBackfillState = { inProgress: false, promise: null };
|
|
208
|
+
|
|
209
|
+
const scheduled = scheduleSessionBackfill(dbManager, sessionsDir, {
|
|
210
|
+
state,
|
|
211
|
+
retentionCutoffMs: cutoffMs,
|
|
212
|
+
setTimeoutFn: () => 0,
|
|
213
|
+
// Capture the cutoff that would be forwarded to the store so we prove
|
|
214
|
+
// the wiring reaches both the eligibility check and the indexer.
|
|
215
|
+
needsBackfillFn: (_db, _dir, _now, cutoff) => {
|
|
216
|
+
receivedCutoff = cutoff;
|
|
217
|
+
return false;
|
|
218
|
+
},
|
|
219
|
+
indexSessionsFn: (_db, _dir, options) => {
|
|
220
|
+
indexOptions = options;
|
|
221
|
+
return { sessionsProcessed: 0, sessionsIndexed: 0, sessionsSkipped: 0, messagesIndexed: 0, reachedLimit: undefined, errors: [] };
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// needsBackfill ran and received the cutoff; because it returned false for
|
|
226
|
+
// an expired file set, no backfill pass was scheduled.
|
|
227
|
+
assert.equal(scheduled, false);
|
|
228
|
+
assert.equal(receivedCutoff, cutoffMs);
|
|
229
|
+
assert.equal(indexOptions, undefined);
|
|
230
|
+
});
|
|
192
231
|
});
|
package/dist/extensions/pi-hermes-memory/tests/integration/corruption-recovery-integration.test.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { DatabaseManager } from '../../src/store/db.js';
|
|
7
|
+
import { isFts5QueryError } from '../../src/store/fts-query.js';
|
|
8
|
+
import { reconcileMarkdownMemoryScope } from '../../src/store/sqlite-memory-store.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Behavior change under test (PR #184): reconcileMarkdownMemoryScope now
|
|
12
|
+
* runs its persistence inside DatabaseManager.withCorruptionRecovery().
|
|
13
|
+
* Before this change, corruption errors ("database disk image is malformed",
|
|
14
|
+
* SQLITE_CORRUPT, SQLITE_NOTADB) thrown during markdown reconciliation
|
|
15
|
+
* surfaced as raw failures on every memory write — corruption recovery was
|
|
16
|
+
* only wired into the session-indexing paths, so a corrupt sessions.db kept
|
|
17
|
+
* failing markdown syncs until repaired by hand. Now the markdown reconcile
|
|
18
|
+
* path participates in the same recovery: the corrupt database is
|
|
19
|
+
* quarantined, rebuilt, and the reconcile is retried on a fresh handle, so
|
|
20
|
+
* the memory entry is persisted instead of lost.
|
|
21
|
+
*
|
|
22
|
+
* The FTS5 soft fallback stays narrow: only genuine FTS query/index errors
|
|
23
|
+
* (isFts5QueryError) degrade to a warned, repair-guided result. Corruption
|
|
24
|
+
* signals never match that classifier, so they can never be swallowed into a
|
|
25
|
+
* zero-count success here.
|
|
26
|
+
*/
|
|
27
|
+
describe('Corruption recovery during markdown sync', () => {
|
|
28
|
+
it('keeps corruption signals out of the FTS5 fallback classifier', () => {
|
|
29
|
+
// Corruption/recovery signals must propagate so DatabaseManager can
|
|
30
|
+
// quarantine + rebuild, not be swallowed by the search-index fallback.
|
|
31
|
+
assert.equal(isFts5QueryError(new Error('database disk image is malformed')), false);
|
|
32
|
+
assert.equal(isFts5QueryError(new Error('SQL logic error')), false);
|
|
33
|
+
assert.equal(isFts5QueryError(new Error('file is not a database')), false);
|
|
34
|
+
// Genuine FTS5 query/index errors remain recoverable search-path failures.
|
|
35
|
+
assert.equal(isFts5QueryError(new Error('fts5: syntax error near "AND"')), true);
|
|
36
|
+
assert.equal(isFts5QueryError(new Error('fts5: unterminated string')), true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('withCorruptionRecovery quarantines a corrupt DB, rebuilds, and persists the entry', async () => {
|
|
40
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-corruption-'));
|
|
41
|
+
const dbManager = new DatabaseManager(tmpDir);
|
|
42
|
+
const dbFile = path.join(tmpDir, 'sessions.db');
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// 1. Prime a healthy database with one memory.
|
|
46
|
+
const first = reconcileMarkdownMemoryScope(dbManager, ['first healthy entry'], 'memory', null);
|
|
47
|
+
assert.equal(first.inserted, 1);
|
|
48
|
+
await dbManager.waitForStartupIntegrityScan();
|
|
49
|
+
|
|
50
|
+
// 2. Close the handle and corrupt the DB file on disk.
|
|
51
|
+
dbManager.close();
|
|
52
|
+
fs.writeFileSync(dbFile, 'GARBAGE-NOT-A-SQLITE-DB'.repeat(32), 'utf-8');
|
|
53
|
+
assert.equal(isFts5QueryError(new Error('file is not a database')), false);
|
|
54
|
+
|
|
55
|
+
// 3. Reconcile a second entry. The corrupt file must be detected,
|
|
56
|
+
// quarantined, and a fresh database rebuilt — and the entry persisted
|
|
57
|
+
// on the healthy database instead of being swallowed as a zero-count
|
|
58
|
+
// "successful" reconcile.
|
|
59
|
+
const second = reconcileMarkdownMemoryScope(dbManager, ['second entry after corruption'], 'memory', null);
|
|
60
|
+
|
|
61
|
+
// 4. Recovery must have run (not a no-op reuse of the corrupt file).
|
|
62
|
+
const recovery = dbManager.getLastRecovery();
|
|
63
|
+
assert.ok(recovery, 'expected a corruption recovery to be recorded');
|
|
64
|
+
assert.notEqual(recovery.strategy, 'reused', 'the corrupt file must not have been reused');
|
|
65
|
+
assert.ok(recovery.backupPaths.length >= 1, 'the corrupt file must have been quarantined to a backup');
|
|
66
|
+
for (const backup of recovery.backupPaths) {
|
|
67
|
+
assert.ok(fs.existsSync(backup), `quarantined backup missing: ${backup}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 5. The entry must actually be persisted in the rebuilt database —
|
|
71
|
+
// a swallowed zero-count success would leave the memory in markdown only.
|
|
72
|
+
assert.ok(second.inserted >= 1, `expected the entry to persist after recovery, got ${JSON.stringify(second)}`);
|
|
73
|
+
const rows = dbManager.getDb()
|
|
74
|
+
.prepare('SELECT content FROM memories WHERE target = ? ORDER BY id ASC')
|
|
75
|
+
.all('memory') as Array<{ content: string }>;
|
|
76
|
+
assert.ok(
|
|
77
|
+
rows.some((row) => row.content.includes('second entry after corruption')),
|
|
78
|
+
'the second entry must be present in the rebuilt database',
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// 6. The original path must now hold a fresh, healthy database.
|
|
82
|
+
const header = fs.readFileSync(dbFile).subarray(0, 16).toString('utf-8');
|
|
83
|
+
assert.equal(header, 'SQLite format 3\x00', 'a fresh SQLite database must exist at the original path');
|
|
84
|
+
} finally {
|
|
85
|
+
try { dbManager.close(); } catch { /* best effort */ }
|
|
86
|
+
// Best-effort: on Windows the shared AtomicLockCoordinator keeps
|
|
87
|
+
// .pi-hermes-locks.sqlite open, so the directory may not be removable.
|
|
88
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* OS cleans temp eventually */ }
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -905,6 +905,35 @@ describe('DatabaseManager', () => {
|
|
|
905
905
|
assert.ok(fs.readdirSync(tmpDir).some((name) => name.startsWith('sessions.db.corrupt-')), 'corrupt DB should be quarantined');
|
|
906
906
|
});
|
|
907
907
|
|
|
908
|
+
it('skips the startup integrity scan when disabled while preserving normal reads and writes', async () => {
|
|
909
|
+
const db = dbManager.getDb();
|
|
910
|
+
db.prepare(`
|
|
911
|
+
INSERT INTO sessions (id, project, cwd, started_at)
|
|
912
|
+
VALUES (?, ?, ?, ?)
|
|
913
|
+
`).run('skip-scan-session', 'skip-scan-project', '/work/skip-scan', '2026-05-03T00:00:00Z');
|
|
914
|
+
db.prepare(`
|
|
915
|
+
INSERT INTO messages (id, session_id, role, content, timestamp)
|
|
916
|
+
VALUES (?, ?, ?, ?, ?)
|
|
917
|
+
`).run('skip-scan-message', 'skip-scan-session', 'user', 'readable message', '2026-05-03T00:00:00Z');
|
|
918
|
+
dbManager.close();
|
|
919
|
+
|
|
920
|
+
corruptRecoverableIndexPage(path.join(tmpDir, 'sessions.db'), 'idx_messages_timestamp');
|
|
921
|
+
|
|
922
|
+
dbManager = new DatabaseManager(tmpDir);
|
|
923
|
+
dbManager.setQuickCheckOnOpen(false);
|
|
924
|
+
const reopenedDb = dbManager.getDb();
|
|
925
|
+
await dbManager.waitForStartupIntegrityScan();
|
|
926
|
+
|
|
927
|
+
assert.strictEqual(dbManager.getLastRecovery(), null);
|
|
928
|
+
const sessionCount = reopenedDb.prepare('SELECT COUNT(*) AS count FROM sessions').get();
|
|
929
|
+
assert.strictEqual(sessionCount.count, 1);
|
|
930
|
+
reopenedDb.prepare(`
|
|
931
|
+
INSERT INTO sessions (id, project, cwd, started_at)
|
|
932
|
+
VALUES (?, ?, ?, ?)
|
|
933
|
+
`).run('skip-scan-session-2', 'skip-scan-project', '/work/skip-scan', '2026-05-03T00:01:00Z');
|
|
934
|
+
assert.strictEqual(reopenedDb.prepare('SELECT COUNT(*) AS count FROM sessions').get().count, 2);
|
|
935
|
+
});
|
|
936
|
+
|
|
908
937
|
it('quarantines unrecoverable files and recreates an empty database', () => {
|
|
909
938
|
dbManager.close();
|
|
910
939
|
const dbPath = path.join(tmpDir, 'sessions.db');
|