@bastani/atomic 0.9.8 → 0.9.9-alpha.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 +28 -0
- package/dist/builtin/cursor/CHANGELOG.md +6 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +6 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/runs/background/async-execution-chain.ts +2 -0
- package/dist/builtin/subagents/src/runs/background/async-execution-common.ts +7 -1
- package/dist/builtin/subagents/src/runs/background/async-execution-single.ts +2 -0
- package/dist/builtin/subagents/src/runs/background/async-execution-types.ts +8 -2
- package/dist/builtin/subagents/src/runs/foreground/chain-execution-parallel-runner.ts +2 -0
- package/dist/builtin/subagents/src/runs/foreground/chain-execution-sequential-step.ts +2 -0
- package/dist/builtin/subagents/src/runs/foreground/execution-attempt.ts +2 -1
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-async.ts +2 -0
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +2 -0
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-resume.ts +2 -0
- package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +2 -0
- package/dist/builtin/subagents/src/runs/shared/pi-args.ts +6 -0
- package/dist/builtin/subagents/src/shared/types-config.ts +4 -0
- package/dist/builtin/subagents/src/shared/types-depth.ts +29 -2
- package/dist/builtin/web-access/CHANGELOG.md +6 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +22 -0
- package/dist/builtin/workflows/README.md +17 -8
- package/dist/builtin/workflows/builtin/goal-prompts.ts +8 -2
- package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +26 -9
- package/dist/builtin/workflows/builtin/ralph-forked-prompts.ts +1 -1
- package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +8 -3
- package/dist/builtin/workflows/builtin/ralph-runner.ts +2 -0
- package/dist/builtin/workflows/builtin/shared-prompts.ts +24 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/durable/backend.ts +53 -15
- package/dist/builtin/workflows/src/durable/completed-catalog.ts +245 -0
- package/dist/builtin/workflows/src/durable/completed-inspection.ts +213 -0
- package/dist/builtin/workflows/src/durable/dbos-backend.ts +120 -170
- package/dist/builtin/workflows/src/durable/dbos-envelope.ts +84 -8
- package/dist/builtin/workflows/src/durable/dbos-metadata.ts +98 -0
- package/dist/builtin/workflows/src/durable/dbos-tombstone.ts +27 -0
- package/dist/builtin/workflows/src/durable/factory.ts +1 -1
- package/dist/builtin/workflows/src/durable/file-backend.ts +247 -241
- package/dist/builtin/workflows/src/durable/file-lock.ts +153 -0
- package/dist/builtin/workflows/src/durable/file-state.ts +104 -0
- package/dist/builtin/workflows/src/durable/format-version.ts +12 -0
- package/dist/builtin/workflows/src/durable/index.ts +12 -0
- package/dist/builtin/workflows/src/durable/resume-catalog.ts +16 -27
- package/dist/builtin/workflows/src/durable/resume-eligibility.ts +20 -0
- package/dist/builtin/workflows/src/durable/resume-runtime.ts +29 -29
- package/dist/builtin/workflows/src/durable/scoped-backend.ts +20 -3
- package/dist/builtin/workflows/src/durable/stage-primitive.ts +29 -8
- package/dist/builtin/workflows/src/durable/types.ts +3 -0
- package/dist/builtin/workflows/src/engine/primitives/task.ts +1 -0
- package/dist/builtin/workflows/src/engine/run.ts +14 -14
- package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +39 -9
- package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +10 -0
- package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +133 -0
- package/dist/builtin/workflows/src/extension/runtime.ts +15 -40
- package/dist/builtin/workflows/src/extension/wiring.ts +9 -6
- package/dist/builtin/workflows/src/extension/workflow-durable-resume-command.ts +226 -0
- package/dist/builtin/workflows/src/extension/workflow-prompts.ts +1 -0
- package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +66 -112
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +5 -5
- package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +16 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +50 -43
- package/dist/builtin/workflows/src/runs/foreground/executor-direct-output.ts +91 -0
- package/dist/builtin/workflows/src/runs/foreground/executor-direct.ts +95 -46
- package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +3 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-task-prompts.ts +1 -1
- package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +3 -0
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +1 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner-options.ts +36 -7
- package/dist/builtin/workflows/src/runs/shared/worktree-cache-lifecycle.ts +25 -0
- package/dist/builtin/workflows/src/runs/shared/worktree-cwd.ts +103 -0
- package/dist/builtin/workflows/src/runs/shared/worktree-generation.ts +57 -0
- package/dist/builtin/workflows/src/runs/shared/worktree-git.ts +134 -8
- package/dist/builtin/workflows/src/runs/shared/worktree.ts +2 -0
- package/dist/builtin/workflows/src/shared/resumable-workflow-notices.ts +43 -0
- package/dist/builtin/workflows/src/shared/timing.ts +4 -0
- package/dist/builtin/workflows/src/shared/types.ts +5 -3
- package/dist/builtin/workflows/src/tui/graph-view-constants.ts +1 -0
- package/dist/builtin/workflows/src/tui/graph-view-input.ts +24 -14
- package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +43 -13
- package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-auto-compaction.js +5 -7
- package/dist/core/agent-session-auto-compaction.js.map +1 -1
- package/dist/core/agent-session-compaction.d.ts +6 -34
- package/dist/core/agent-session-compaction.d.ts.map +1 -1
- package/dist/core/agent-session-compaction.js +95 -233
- package/dist/core/agent-session-compaction.js.map +1 -1
- package/dist/core/agent-session-message-queue.d.ts +0 -4
- package/dist/core/agent-session-message-queue.d.ts.map +1 -1
- package/dist/core/agent-session-message-queue.js +1 -5
- package/dist/core/agent-session-message-queue.js.map +1 -1
- package/dist/core/agent-session-methods.d.ts +5 -6
- package/dist/core/agent-session-methods.d.ts.map +1 -1
- package/dist/core/agent-session-methods.js.map +1 -1
- package/dist/core/agent-session-types.d.ts +2 -12
- package/dist/core/agent-session-types.d.ts.map +1 -1
- package/dist/core/agent-session-types.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +1 -2
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction-boundary.d.ts +9 -0
- package/dist/core/compaction/compaction-boundary.d.ts.map +1 -0
- package/dist/core/compaction/compaction-boundary.js +115 -0
- package/dist/core/compaction/compaction-boundary.js.map +1 -0
- package/dist/core/compaction/compaction-parameters.d.ts +5 -0
- package/dist/core/compaction/compaction-parameters.d.ts.map +1 -0
- package/dist/core/compaction/compaction-parameters.js +27 -0
- package/dist/core/compaction/compaction-parameters.js.map +1 -0
- package/dist/core/compaction/compaction-runner.d.ts +14 -0
- package/dist/core/compaction/compaction-runner.d.ts.map +1 -0
- package/dist/core/compaction/compaction-runner.js +29 -0
- package/dist/core/compaction/compaction-runner.js.map +1 -0
- package/dist/core/compaction/compaction-types.d.ts +79 -0
- package/dist/core/compaction/compaction-types.d.ts.map +1 -0
- package/dist/core/compaction/compaction-types.js +6 -0
- package/dist/core/compaction/compaction-types.js.map +1 -0
- package/dist/core/compaction/deleted-ranges.d.ts +6 -0
- package/dist/core/compaction/deleted-ranges.d.ts.map +1 -0
- package/dist/core/compaction/deleted-ranges.js +134 -0
- package/dist/core/compaction/deleted-ranges.js.map +1 -0
- package/dist/core/compaction/index.d.ts +7 -1
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +7 -1
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/range-planner.d.ts +21 -0
- package/dist/core/compaction/range-planner.d.ts.map +1 -0
- package/dist/core/compaction/range-planner.js +145 -0
- package/dist/core/compaction/range-planner.js.map +1 -0
- package/dist/core/compaction/transcript-serialization.d.ts +11 -0
- package/dist/core/compaction/transcript-serialization.d.ts.map +1 -0
- package/dist/core/compaction/transcript-serialization.js +104 -0
- package/dist/core/compaction/transcript-serialization.js.map +1 -0
- package/dist/core/extensions/context-types.d.ts +2 -2
- package/dist/core/extensions/context-types.d.ts.map +1 -1
- package/dist/core/extensions/context-types.js.map +1 -1
- package/dist/core/extensions/event-results.d.ts +2 -2
- package/dist/core/extensions/event-results.d.ts.map +1 -1
- package/dist/core/extensions/event-results.js.map +1 -1
- package/dist/core/extensions/session-events.d.ts +7 -7
- package/dist/core/extensions/session-events.d.ts.map +1 -1
- package/dist/core/extensions/session-events.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/messages.d.ts +4 -0
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +11 -0
- package/dist/core/messages.js.map +1 -1
- package/dist/core/provider-context-usage.d.ts.map +1 -1
- package/dist/core/provider-context-usage.js +1 -2
- package/dist/core/provider-context-usage.js.map +1 -1
- package/dist/core/session-manager-archive.d.ts +2 -1
- package/dist/core/session-manager-archive.d.ts.map +1 -1
- package/dist/core/session-manager-archive.js +2 -2
- package/dist/core/session-manager-archive.js.map +1 -1
- package/dist/core/session-manager-classification.d.ts +9 -0
- package/dist/core/session-manager-classification.d.ts.map +1 -0
- package/dist/core/session-manager-classification.js +37 -0
- package/dist/core/session-manager-classification.js.map +1 -0
- package/dist/core/session-manager-core.d.ts +4 -4
- package/dist/core/session-manager-core.d.ts.map +1 -1
- package/dist/core/session-manager-core.js +13 -8
- package/dist/core/session-manager-core.js.map +1 -1
- package/dist/core/session-manager-entries.d.ts +4 -3
- package/dist/core/session-manager-entries.d.ts.map +1 -1
- package/dist/core/session-manager-entries.js +12 -10
- package/dist/core/session-manager-entries.js.map +1 -1
- package/dist/core/session-manager-history.d.ts +6 -23
- package/dist/core/session-manager-history.d.ts.map +1 -1
- package/dist/core/session-manager-history.js +23 -256
- package/dist/core/session-manager-history.js.map +1 -1
- package/dist/core/session-manager-list.d.ts.map +1 -1
- package/dist/core/session-manager-list.js +3 -3
- package/dist/core/session-manager-list.js.map +1 -1
- package/dist/core/session-manager-storage.d.ts +1 -1
- package/dist/core/session-manager-storage.d.ts.map +1 -1
- package/dist/core/session-manager-storage.js +3 -2
- package/dist/core/session-manager-storage.js.map +1 -1
- package/dist/core/session-manager-types.d.ts +14 -15
- package/dist/core/session-manager-types.d.ts.map +1 -1
- package/dist/core/session-manager-types.js.map +1 -1
- package/dist/core/session-manager.d.ts +3 -2
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +1 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/main-session.d.ts +1 -0
- package/dist/main-session.d.ts.map +1 -1
- package/dist/main-session.js +7 -0
- package/dist/main-session.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts +4 -1
- package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-message-renderer.js +12 -22
- package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-events.js +5 -11
- package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-rendering.js +3 -0
- package/dist/modes/interactive/components/chat-session-host-rendering.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.d.ts +1 -0
- package/dist/modes/interactive/components/chat-session-host-state.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-state.js +1 -0
- package/dist/modes/interactive/components/chat-session-host-state.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-utils.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host-utils.js +1 -1
- package/dist/modes/interactive/components/chat-session-host-utils.js.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
- package/dist/modes/interactive/components/chat-session-host.js +1 -0
- package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
- package/dist/modes/interactive/components/compaction-boundary-message.d.ts +20 -0
- package/dist/modes/interactive/components/compaction-boundary-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/compaction-boundary-message.js +48 -0
- package/dist/modes/interactive/components/compaction-boundary-message.js.map +1 -0
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/session-selector-list.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector-list.js +3 -0
- package/dist/modes/interactive/components/session-selector-list.js.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-agent-events.js +3 -47
- package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-deps.d.ts +2 -2
- package/dist/modes/interactive/interactive-mode-deps.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-deps.js +1 -1
- package/dist/modes/interactive/interactive-mode-deps.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +5 -3
- package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.js +21 -12
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +3 -5
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +1 -6
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.js +0 -4
- package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +2 -11
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/compaction.md +97 -772
- package/docs/extensions.md +25 -29
- package/docs/json.md +2 -2
- package/docs/rpc.md +32 -24
- package/docs/sdk.md +3 -3
- package/docs/session-format.md +49 -30
- package/docs/sessions.md +5 -3
- package/docs/settings.md +14 -4
- package/docs/usage.md +1 -1
- package/docs/workflows.md +30 -15
- package/examples/extensions/custom-compaction.ts +14 -58
- package/examples/extensions/handoff.ts +2 -3
- package/npm-shrinkwrap.json +23 -23
- package/package.json +2 -2
- package/dist/core/compaction/context-assistant-turns.d.ts +0 -42
- package/dist/core/compaction/context-assistant-turns.d.ts.map +0 -1
- package/dist/core/compaction/context-assistant-turns.js +0 -87
- package/dist/core/compaction/context-assistant-turns.js.map +0 -1
- package/dist/core/compaction/context-compaction-critical.d.ts +0 -15
- package/dist/core/compaction/context-compaction-critical.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-critical.js +0 -57
- package/dist/core/compaction/context-compaction-critical.js.map +0 -1
- package/dist/core/compaction/context-compaction-eviction-alternates.d.ts +0 -18
- package/dist/core/compaction/context-compaction-eviction-alternates.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-eviction-alternates.js +0 -186
- package/dist/core/compaction/context-compaction-eviction-alternates.js.map +0 -1
- package/dist/core/compaction/context-compaction-eviction.d.ts +0 -12
- package/dist/core/compaction/context-compaction-eviction.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-eviction.js +0 -222
- package/dist/core/compaction/context-compaction-eviction.js.map +0 -1
- package/dist/core/compaction/context-compaction-metrics.d.ts +0 -28
- package/dist/core/compaction/context-compaction-metrics.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-metrics.js +0 -107
- package/dist/core/compaction/context-compaction-metrics.js.map +0 -1
- package/dist/core/compaction/context-compaction-prompt.d.ts +0 -9
- package/dist/core/compaction/context-compaction-prompt.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-prompt.js +0 -163
- package/dist/core/compaction/context-compaction-prompt.js.map +0 -1
- package/dist/core/compaction/context-compaction-runner.d.ts +0 -11
- package/dist/core/compaction/context-compaction-runner.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-runner.js +0 -273
- package/dist/core/compaction/context-compaction-runner.js.map +0 -1
- package/dist/core/compaction/context-compaction-strategy.d.ts +0 -5
- package/dist/core/compaction/context-compaction-strategy.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-strategy.js +0 -27
- package/dist/core/compaction/context-compaction-strategy.js.map +0 -1
- package/dist/core/compaction/context-compaction-types.d.ts +0 -75
- package/dist/core/compaction/context-compaction-types.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction-types.js +0 -6
- package/dist/core/compaction/context-compaction-types.js.map +0 -1
- package/dist/core/compaction/context-compaction.d.ts +0 -10
- package/dist/core/compaction/context-compaction.d.ts.map +0 -1
- package/dist/core/compaction/context-compaction.js +0 -8
- package/dist/core/compaction/context-compaction.js.map +0 -1
- package/dist/core/compaction/context-deletion-application.d.ts +0 -14
- package/dist/core/compaction/context-deletion-application.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-application.js +0 -261
- package/dist/core/compaction/context-deletion-application.js.map +0 -1
- package/dist/core/compaction/context-deletion-store.d.ts +0 -78
- package/dist/core/compaction/context-deletion-store.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-store.js +0 -162
- package/dist/core/compaction/context-deletion-store.js.map +0 -1
- package/dist/core/compaction/context-deletion-targets.d.ts +0 -43
- package/dist/core/compaction/context-deletion-targets.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-targets.js +0 -292
- package/dist/core/compaction/context-deletion-targets.js.map +0 -1
- package/dist/core/compaction/context-deletion-tool-definitions.d.ts +0 -193
- package/dist/core/compaction/context-deletion-tool-definitions.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-tool-definitions.js +0 -98
- package/dist/core/compaction/context-deletion-tool-definitions.js.map +0 -1
- package/dist/core/compaction/context-deletion-tool-helpers.d.ts +0 -19
- package/dist/core/compaction/context-deletion-tool-helpers.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-tool-helpers.js +0 -210
- package/dist/core/compaction/context-deletion-tool-helpers.js.map +0 -1
- package/dist/core/compaction/context-deletion-tools.d.ts +0 -4
- package/dist/core/compaction/context-deletion-tools.d.ts.map +0 -1
- package/dist/core/compaction/context-deletion-tools.js +0 -403
- package/dist/core/compaction/context-deletion-tools.js.map +0 -1
- package/dist/core/compaction/context-transcript-analysis.d.ts +0 -11
- package/dist/core/compaction/context-transcript-analysis.d.ts.map +0 -1
- package/dist/core/compaction/context-transcript-analysis.js +0 -228
- package/dist/core/compaction/context-transcript-analysis.js.map +0 -1
- package/dist/core/session-manager-tool-dependencies.d.ts +0 -10
- package/dist/core/session-manager-tool-dependencies.d.ts.map +0 -1
- package/dist/core/session-manager-tool-dependencies.js +0 -133
- package/dist/core/session-manager-tool-dependencies.js.map +0 -1
- package/dist/modes/interactive/components/context-compaction-summary-message.d.ts +0 -17
- package/dist/modes/interactive/components/context-compaction-summary-message.d.ts.map +0 -1
- package/dist/modes/interactive/components/context-compaction-summary-message.js +0 -83
- package/dist/modes/interactive/components/context-compaction-summary-message.js.map +0 -1
|
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { createGitEnvironment } from "@bastani/atomic";
|
|
5
5
|
import type { GitResult, GitWorktreeSetupOptions, GitWorktreeSetupResult } from "./worktree-types.js";
|
|
6
|
+
import { openGitWorktreeGenerationAnchor, type GitWorktreeGenerationAnchor } from "./worktree-generation.js";
|
|
6
7
|
|
|
7
8
|
const DISABLED_GIT_HOOKS_PATH = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
8
9
|
const GIT_COMMAND_TIMEOUT_MS = 60_000;
|
|
@@ -133,26 +134,53 @@ function runGitReadOnlyProbe(cwd: string, args: readonly string[]): GitResult {
|
|
|
133
134
|
|
|
134
135
|
export interface GitWorktreeSetupCache {
|
|
135
136
|
get(options: GitWorktreeSetupOptions): GitWorktreeSetupResult;
|
|
137
|
+
dispose(): void;
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
function gitWorktreeSetupCacheKey(options: GitWorktreeSetupOptions): string {
|
|
141
|
+
const repoRoot = repositoryRootForGitWorktree(options.cwd);
|
|
142
|
+
const { logicalRepoRoot } = cwdWithinGitRepository(options.cwd, repoRoot);
|
|
143
|
+
const worktreeRoot = resolveGitWorktreePath(options.gitWorktreeDir, logicalRepoRoot);
|
|
139
144
|
return JSON.stringify([
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
options.baseBranch?.trim() ?? "",
|
|
145
|
+
comparableRealPath(gitCommonDirForWorktree(repoRoot)),
|
|
146
|
+
comparablePathThroughExistingAncestor(worktreeRoot),
|
|
143
147
|
]);
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
interface CachedGitWorktreeSetup {
|
|
151
|
+
readonly result: GitWorktreeSetupResult;
|
|
152
|
+
readonly identity: GitWorktreeIdentity;
|
|
153
|
+
}
|
|
154
|
+
|
|
146
155
|
export function createGitWorktreeSetupCache(): GitWorktreeSetupCache {
|
|
147
|
-
const results = new Map<string,
|
|
156
|
+
const results = new Map<string, CachedGitWorktreeSetup>();
|
|
157
|
+
let disposed = false;
|
|
148
158
|
return {
|
|
149
159
|
get(options) {
|
|
160
|
+
if (disposed) throw new Error("Git worktree setup cache is already disposed");
|
|
150
161
|
const key = gitWorktreeSetupCacheKey(options);
|
|
151
162
|
const cached = results.get(key);
|
|
152
|
-
if (cached !== undefined)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
163
|
+
if (cached !== undefined) {
|
|
164
|
+
assertCachedGitWorktreeIdentity(cached, options);
|
|
165
|
+
return cached.result;
|
|
166
|
+
}
|
|
167
|
+
const result = setupGitWorktree(options);
|
|
168
|
+
results.set(key, { result, identity: captureGitWorktreeIdentity(result) });
|
|
169
|
+
return result;
|
|
170
|
+
},
|
|
171
|
+
dispose() {
|
|
172
|
+
if (disposed) return;
|
|
173
|
+
disposed = true;
|
|
174
|
+
let firstError: Error | undefined;
|
|
175
|
+
for (const cached of results.values()) {
|
|
176
|
+
try {
|
|
177
|
+
cached.identity.generation.dispose();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
firstError ??= error instanceof Error ? error : new Error(String(error));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
results.clear();
|
|
183
|
+
if (firstError !== undefined) throw firstError;
|
|
156
184
|
},
|
|
157
185
|
};
|
|
158
186
|
}
|
|
@@ -223,6 +251,31 @@ function comparableRealPath(value: string): string {
|
|
|
223
251
|
return process.platform === "win32" ? realpath.toLowerCase() : realpath;
|
|
224
252
|
}
|
|
225
253
|
|
|
254
|
+
function comparablePathThroughExistingAncestor(value: string): string {
|
|
255
|
+
let existing = path.resolve(value);
|
|
256
|
+
const missing: string[] = [];
|
|
257
|
+
while (!pathExistsSync(existing)) {
|
|
258
|
+
const parent = path.dirname(existing);
|
|
259
|
+
if (parent === existing) break;
|
|
260
|
+
missing.unshift(path.basename(existing));
|
|
261
|
+
existing = parent;
|
|
262
|
+
}
|
|
263
|
+
const resolved = path.resolve(comparableRealPath(existing), ...missing).replace(/\\/g, "/");
|
|
264
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function validateWorktreeOutsideInvokingCheckout(worktreeRoot: string, repoRoot: string): void {
|
|
268
|
+
const repository = comparableRealPath(repoRoot);
|
|
269
|
+
const candidate = comparablePathThroughExistingAncestor(worktreeRoot);
|
|
270
|
+
const relativePath = path.relative(repository, candidate);
|
|
271
|
+
if (relativePath === "") {
|
|
272
|
+
throw new Error("gitWorktreeDir must not resolve to the invoking checkout; provide a separate same-repository worktree path.");
|
|
273
|
+
}
|
|
274
|
+
if (!relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath)) {
|
|
275
|
+
throw new Error("gitWorktreeDir must be outside the invoking checkout; provide a separate same-repository worktree path.");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
226
279
|
function gitPathFromOutput(value: string, cwd: string): string | undefined {
|
|
227
280
|
const trimmed = value.trim();
|
|
228
281
|
if (!trimmed) return undefined;
|
|
@@ -318,10 +371,74 @@ function validateExistingGitWorktreeRoot(worktreeRoot: string, repoRoot: string)
|
|
|
318
371
|
}
|
|
319
372
|
}
|
|
320
373
|
|
|
374
|
+
interface GitWorktreeIdentitySnapshot {
|
|
375
|
+
readonly worktreeRealPath: string;
|
|
376
|
+
readonly repositoryRealPath: string;
|
|
377
|
+
readonly gitDirRealPath: string;
|
|
378
|
+
readonly commonDirRealPath: string;
|
|
379
|
+
readonly device: number;
|
|
380
|
+
readonly inode: number;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
interface GitWorktreeIdentity extends GitWorktreeIdentitySnapshot {
|
|
384
|
+
readonly generation: GitWorktreeGenerationAnchor;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function gitDirectoryForWorktree(cwd: string): string {
|
|
388
|
+
const result = runGitReadOnlyProbe(cwd, ["rev-parse", "--absolute-git-dir"]);
|
|
389
|
+
if (result.status !== 0) {
|
|
390
|
+
throw new Error(`Failed to validate Git directory for gitWorktreeDir ${cwd}. Git reported: ${gitFailureMessage(result)}`);
|
|
391
|
+
}
|
|
392
|
+
const gitDir = gitPathFromOutput(result.stdout, cwd);
|
|
393
|
+
if (gitDir === undefined) throw new Error("git rev-parse --absolute-git-dir returned an empty path");
|
|
394
|
+
return gitDir;
|
|
395
|
+
}
|
|
396
|
+
function captureGitWorktreeIdentitySnapshot(setup: GitWorktreeSetupResult): GitWorktreeIdentitySnapshot {
|
|
397
|
+
validateWorktreeOutsideInvokingCheckout(setup.worktreeRoot, setup.repositoryRoot);
|
|
398
|
+
validateExistingGitWorktreeRoot(setup.worktreeRoot, setup.repositoryRoot);
|
|
399
|
+
const stats = fs.statSync(setup.worktreeRoot);
|
|
400
|
+
return {
|
|
401
|
+
worktreeRealPath: comparableRealPath(setup.worktreeRoot),
|
|
402
|
+
repositoryRealPath: comparableRealPath(setup.repositoryRoot),
|
|
403
|
+
gitDirRealPath: comparableRealPath(gitDirectoryForWorktree(setup.worktreeRoot)),
|
|
404
|
+
commonDirRealPath: comparableRealPath(gitCommonDirForWorktree(setup.worktreeRoot)),
|
|
405
|
+
device: stats.dev,
|
|
406
|
+
inode: stats.ino,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function captureGitWorktreeIdentity(setup: GitWorktreeSetupResult): GitWorktreeIdentity {
|
|
411
|
+
const snapshot = captureGitWorktreeIdentitySnapshot(setup);
|
|
412
|
+
return { ...snapshot, generation: openGitWorktreeGenerationAnchor(setup.worktreeRoot) };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function assertCachedGitWorktreeIdentity(cached: CachedGitWorktreeSetup, options: GitWorktreeSetupOptions): void {
|
|
416
|
+
let current: GitWorktreeIdentitySnapshot;
|
|
417
|
+
try {
|
|
418
|
+
current = captureGitWorktreeIdentitySnapshot(cached.result);
|
|
419
|
+
cached.identity.generation.assertCurrent();
|
|
420
|
+
} catch (error) {
|
|
421
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
422
|
+
throw new Error(`Cached gitWorktreeDir changed before reuse: ${options.gitWorktreeDir}. Reason: ${message}`);
|
|
423
|
+
}
|
|
424
|
+
const expected = cached.identity;
|
|
425
|
+
if (
|
|
426
|
+
current.worktreeRealPath !== expected.worktreeRealPath ||
|
|
427
|
+
current.repositoryRealPath !== expected.repositoryRealPath ||
|
|
428
|
+
current.gitDirRealPath !== expected.gitDirRealPath ||
|
|
429
|
+
current.commonDirRealPath !== expected.commonDirRealPath ||
|
|
430
|
+
current.device !== expected.device ||
|
|
431
|
+
current.inode !== expected.inode
|
|
432
|
+
) {
|
|
433
|
+
throw new Error(`Cached gitWorktreeDir changed before reuse: ${options.gitWorktreeDir}. Start a new run with the intended same-repository worktree.`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
321
437
|
export function setupGitWorktree(options: GitWorktreeSetupOptions): GitWorktreeSetupResult {
|
|
322
438
|
const repoRoot = repositoryRootForGitWorktree(options.cwd);
|
|
323
439
|
const { relativeCwd, logicalRepoRoot } = cwdWithinGitRepository(options.cwd, repoRoot);
|
|
324
440
|
const worktreeRoot = resolveGitWorktreePath(options.gitWorktreeDir, logicalRepoRoot);
|
|
441
|
+
validateWorktreeOutsideInvokingCheckout(worktreeRoot, repoRoot);
|
|
325
442
|
if (pathExistsSync(worktreeRoot)) {
|
|
326
443
|
validateExistingGitWorktreeRoot(worktreeRoot, repoRoot);
|
|
327
444
|
return {
|
|
@@ -346,6 +463,15 @@ export function setupGitWorktree(options: GitWorktreeSetupOptions): GitWorktreeS
|
|
|
346
463
|
`If another process just created this same-repository worktree, rerun the workflow to resume it. If this is an orphaned worktree from an interrupted run, recover or remove it with: ${worktreeRecoveryCommand(repoRoot, worktreeRoot)}`,
|
|
347
464
|
].join("\n"));
|
|
348
465
|
}
|
|
466
|
+
try {
|
|
467
|
+
validateWorktreeOutsideInvokingCheckout(worktreeRoot, repoRoot);
|
|
468
|
+
validateExistingGitWorktreeRoot(worktreeRoot, repoRoot);
|
|
469
|
+
} catch (error) {
|
|
470
|
+
// Best-effort rollback of a target that changed while Git was creating it.
|
|
471
|
+
runGit(repoRoot, ["worktree", "remove", "--force", worktreeRoot]);
|
|
472
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
473
|
+
throw new Error(`Created gitWorktreeDir failed post-creation validation: ${message}`);
|
|
474
|
+
}
|
|
349
475
|
return {
|
|
350
476
|
worktreeRoot,
|
|
351
477
|
cwd: workspaceCwdForGitWorktreeRoot(worktreeRoot, relativeCwd),
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { createGitWorktreeSetupCache, setupGitWorktree, setupGitWorktreeCached } from "./worktree-git.js";
|
|
2
|
+
export { createGitWorktreeSetupCacheOwner } from "./worktree-cache-lifecycle.js";
|
|
3
|
+
export type { GitWorktreeSetupCacheOwner } from "./worktree-cache-lifecycle.js";
|
|
2
4
|
export {
|
|
3
5
|
cleanupWorktrees,
|
|
4
6
|
createWorktrees,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { WorkflowSerializableValue } from "./types.js";
|
|
2
|
+
import { isDurableWorkflowResumable } from "../durable/resume-eligibility.js";
|
|
3
|
+
import type { ResumableWorkflowEntry } from "../durable/types.js";
|
|
4
|
+
|
|
5
|
+
export interface ResumableWorkflowNotice {
|
|
6
|
+
readonly workflowId: string;
|
|
7
|
+
readonly name: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function recordOrUndefined(value: unknown): Record<string, WorkflowSerializableValue> | undefined {
|
|
11
|
+
return typeof value === "object" && value !== null
|
|
12
|
+
? value as Record<string, WorkflowSerializableValue>
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Return authoritative resumable workflow references cached in a session. */
|
|
17
|
+
export function findResumableWorkflowNotices(
|
|
18
|
+
entries: readonly unknown[],
|
|
19
|
+
authoritativeCatalog: readonly ResumableWorkflowEntry[],
|
|
20
|
+
): ResumableWorkflowNotice[] {
|
|
21
|
+
const authoritativeById = new Map(authoritativeCatalog.map((entry) => [entry.workflowId, entry]));
|
|
22
|
+
const sessionWorkflowIds = new Set<string>();
|
|
23
|
+
for (const value of entries) {
|
|
24
|
+
const entry = recordOrUndefined(value);
|
|
25
|
+
if (entry === undefined) continue;
|
|
26
|
+
const payload = entry["type"] === "custom" && entry["customType"] === "workflow.durable.checkpoint"
|
|
27
|
+
? recordOrUndefined(entry["data"])
|
|
28
|
+
: entry["type"] === "workflow.durable.checkpoint"
|
|
29
|
+
? recordOrUndefined(entry["payload"]) ?? entry
|
|
30
|
+
: undefined;
|
|
31
|
+
const workflowId = payload?.["workflowId"];
|
|
32
|
+
if (typeof workflowId === "string") sessionWorkflowIds.add(workflowId);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const notices: ResumableWorkflowNotice[] = [];
|
|
36
|
+
for (const workflowId of sessionWorkflowIds) {
|
|
37
|
+
const authoritative = authoritativeById.get(workflowId);
|
|
38
|
+
if (authoritative !== undefined && isDurableWorkflowResumable(authoritative)) {
|
|
39
|
+
notices.push({ workflowId, name: authoritative.name });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return notices;
|
|
43
|
+
}
|
|
@@ -18,6 +18,10 @@ function nonNegative(ms: number): number {
|
|
|
18
18
|
return Math.max(0, ms);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function rebasedStageStartedAt(accumulatedDurationMs: number | undefined, resumedAt: number): number {
|
|
22
|
+
return resumedAt - nonNegative(accumulatedDurationMs ?? 0);
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
function elapsedFromStart(
|
|
22
26
|
startedAt: number,
|
|
23
27
|
now: number,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type {
|
|
7
7
|
AgentSession,
|
|
8
8
|
AgentSessionEvent,
|
|
9
|
-
|
|
9
|
+
VerbatimCompactionResult,
|
|
10
10
|
CreateAgentSessionOptions,
|
|
11
11
|
ModelCycleResult,
|
|
12
12
|
PromptOptions,
|
|
@@ -19,7 +19,7 @@ import type * as AuthoringContract from "./authoring-contract.js";
|
|
|
19
19
|
|
|
20
20
|
export type { TSchema };
|
|
21
21
|
|
|
22
|
-
export type { AgentSessionEvent,
|
|
22
|
+
export type { AgentSessionEvent, VerbatimCompactionResult, ModelCycleResult, PromptOptions };
|
|
23
23
|
|
|
24
24
|
export type StageUserMessageContent = Parameters<AgentSession["sendUserMessage"]>[0];
|
|
25
25
|
|
|
@@ -191,6 +191,8 @@ export interface StageOptions<TSchemaDef extends TSchema | undefined = TSchema |
|
|
|
191
191
|
resumeFromSessionFile?: string;
|
|
192
192
|
/** Internal durable replay key used to map a live LM session to durable resume state. */
|
|
193
193
|
durableReplayKey?: string;
|
|
194
|
+
/** Internal durable timing baseline accumulated before a process-boundary resume. */
|
|
195
|
+
durableAccumulatedDurationMs?: number;
|
|
194
196
|
}
|
|
195
197
|
|
|
196
198
|
// ---------------------------------------------------------------------------
|
|
@@ -352,7 +354,7 @@ export interface StageContext<TSchemaDef extends TSchema | undefined = undefined
|
|
|
352
354
|
): Promise<{ editorText?: string; cancelled: boolean }>;
|
|
353
355
|
|
|
354
356
|
/** Compaction. */
|
|
355
|
-
compact(): Promise<
|
|
357
|
+
compact(): Promise<VerbatimCompactionResult>;
|
|
356
358
|
abortCompaction(): void;
|
|
357
359
|
|
|
358
360
|
/** Abort current operation. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { expandedStageTarget } from "../shared/expanded-workflow-graph.js";
|
|
2
2
|
import {
|
|
3
|
+
GRAPH_SCROLL_STEP_COLS,
|
|
3
4
|
GRAPH_SCROLL_STEP_ROWS,
|
|
4
5
|
} from "./graph-view-constants.js";
|
|
5
6
|
import { GraphViewRenderer } from "./graph-view-render.js";
|
|
@@ -18,6 +19,11 @@ interface SgrMouseEvent {
|
|
|
18
19
|
final: "M" | "m";
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
interface MouseWheelDelta {
|
|
23
|
+
cols: number;
|
|
24
|
+
rows: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
/** Keyboard, mouse, switcher, prompt, and focus navigation handling. */
|
|
22
28
|
export abstract class GraphViewInputController extends GraphViewRenderer {
|
|
23
29
|
/** Returns true if consumed. */
|
|
@@ -46,10 +52,7 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
private _isNonTextGraphControlBeforePrompt(data: string): boolean {
|
|
49
|
-
return (
|
|
50
|
-
this._mouseWheelDeltaRows(data) !== 0 ||
|
|
51
|
-
matchesKey(data, Key.ctrl("d"))
|
|
52
|
-
);
|
|
55
|
+
return this._mouseWheelDelta(data) !== null || matchesKey(data, Key.ctrl("d"));
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
private _handlePromptInput(data: string): boolean {
|
|
@@ -81,9 +84,10 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
|
|
|
81
84
|
|
|
82
85
|
private _handleGraphInput(data: string): boolean {
|
|
83
86
|
const stageCount = this.cachedLayout.length;
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
this._scrollGraphBy(
|
|
87
|
+
const wheelDelta = this._mouseWheelDelta(data);
|
|
88
|
+
if (wheelDelta) {
|
|
89
|
+
if (wheelDelta.rows !== 0) this._scrollGraphBy(wheelDelta.rows);
|
|
90
|
+
if (wheelDelta.cols !== 0) this._scrollGraphHorizontallyBy(wheelDelta.cols);
|
|
87
91
|
return true;
|
|
88
92
|
}
|
|
89
93
|
|
|
@@ -308,6 +312,11 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
|
|
|
308
312
|
this.graphScrollOffset = Math.max(0, this.graphScrollOffset + deltaRows);
|
|
309
313
|
}
|
|
310
314
|
|
|
315
|
+
private _scrollGraphHorizontallyBy(deltaCols: number): void {
|
|
316
|
+
this.pendingEnsureFocusedVisible = false;
|
|
317
|
+
this.graphScrollColOffset = Math.max(0, this.graphScrollColOffset + deltaCols);
|
|
318
|
+
}
|
|
319
|
+
|
|
311
320
|
private _graphNodeIndexForClick(data: string): number | null | undefined {
|
|
312
321
|
const click = this._sgrLeftMousePress(data);
|
|
313
322
|
if (!click) return undefined;
|
|
@@ -353,7 +362,7 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
|
|
|
353
362
|
return { col: sgr.col, row: sgr.row };
|
|
354
363
|
}
|
|
355
364
|
|
|
356
|
-
private
|
|
365
|
+
private _mouseWheelDelta(data: string): MouseWheelDelta | null {
|
|
357
366
|
const sgr = this._parseSgrMouse(data);
|
|
358
367
|
if (sgr && sgr.final === "M") {
|
|
359
368
|
return this._wheelDeltaForButtonCode(sgr.buttonCode);
|
|
@@ -361,15 +370,16 @@ export abstract class GraphViewInputController extends GraphViewRenderer {
|
|
|
361
370
|
if (data.startsWith("\x1b[M") && data.length >= 6) {
|
|
362
371
|
return this._wheelDeltaForButtonCode(data.charCodeAt(3) - 32);
|
|
363
372
|
}
|
|
364
|
-
return
|
|
373
|
+
return null;
|
|
365
374
|
}
|
|
366
375
|
|
|
367
|
-
private _wheelDeltaForButtonCode(code: number):
|
|
368
|
-
if ((code & 64) === 0) return
|
|
376
|
+
private _wheelDeltaForButtonCode(code: number): MouseWheelDelta | null {
|
|
377
|
+
if ((code & 64) === 0) return null;
|
|
369
378
|
const direction = code & 3;
|
|
370
|
-
if (direction === 0) return -GRAPH_SCROLL_STEP_ROWS;
|
|
371
|
-
if (direction === 1) return GRAPH_SCROLL_STEP_ROWS;
|
|
372
|
-
return 0;
|
|
379
|
+
if (direction === 0) return { cols: 0, rows: -GRAPH_SCROLL_STEP_ROWS };
|
|
380
|
+
if (direction === 1) return { cols: 0, rows: GRAPH_SCROLL_STEP_ROWS };
|
|
381
|
+
if (direction === 2) return { cols: -GRAPH_SCROLL_STEP_COLS, rows: 0 };
|
|
382
|
+
return { cols: GRAPH_SCROLL_STEP_COLS, rows: 0 };
|
|
373
383
|
}
|
|
374
384
|
|
|
375
385
|
// ---- test seams ----
|
|
@@ -10,6 +10,7 @@ import type { RunSnapshot, StageSnapshot } from "../shared/store-types.js";
|
|
|
10
10
|
export type WorkflowResumeSelectorResult =
|
|
11
11
|
| { kind: "live"; runId: string }
|
|
12
12
|
| { kind: "durable"; workflowId: string }
|
|
13
|
+
| { kind: "completed"; workflowId: string }
|
|
13
14
|
| { kind: "close" };
|
|
14
15
|
|
|
15
16
|
export interface WorkflowResumeSelectorUiSurface {
|
|
@@ -54,47 +55,67 @@ function liveRunSession(run: RunSnapshot): WorkflowResumeSelectorItem {
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
function durableWorkflowSession(
|
|
58
|
+
function durableWorkflowSession(
|
|
59
|
+
entry: ResumableWorkflowEntry,
|
|
60
|
+
kind: "durable" | "completed",
|
|
61
|
+
): WorkflowResumeSelectorItem {
|
|
58
62
|
const checkpointText = `${entry.completedCheckpoints} checkpoints`;
|
|
59
63
|
const promptText = `${entry.pendingPrompts} prompts`;
|
|
60
|
-
const
|
|
64
|
+
const statusText = kind === "completed" ? "✓ completed" : entry.status;
|
|
61
65
|
return {
|
|
62
|
-
result: { kind
|
|
66
|
+
result: { kind, workflowId: entry.workflowId },
|
|
63
67
|
session: {
|
|
64
|
-
path: `workflow
|
|
68
|
+
path: `workflow-${kind}:${entry.workflowId}`,
|
|
65
69
|
id: entry.workflowId,
|
|
66
|
-
cwd: "Durable workflow runs",
|
|
70
|
+
cwd: kind === "completed" ? "Completed workflow runs" : "Durable workflow runs",
|
|
67
71
|
created: new Date(entry.createdAt),
|
|
68
72
|
modified: new Date(entry.updatedAt),
|
|
69
73
|
messageCount: entry.completedCheckpoints,
|
|
70
|
-
firstMessage
|
|
71
|
-
allMessagesText: `${entry.workflowId} ${entry.name} ${
|
|
74
|
+
firstMessage: `${entry.name} ${statusText} ${checkpointText} ${promptText}`,
|
|
75
|
+
allMessagesText: `${entry.workflowId} ${entry.name} ${statusText} ${checkpointText} ${promptText}`,
|
|
76
|
+
...(kind === "completed" ? { messageColor: "success" as const } : {}),
|
|
72
77
|
},
|
|
73
78
|
};
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
function compareResumeItemsByRecency(
|
|
82
|
+
left: WorkflowResumeSelectorItem,
|
|
83
|
+
right: WorkflowResumeSelectorItem,
|
|
84
|
+
): number {
|
|
85
|
+
const recencyDifference = right.session.modified.getTime() - left.session.modified.getTime();
|
|
86
|
+
if (recencyDifference !== 0) return recencyDifference;
|
|
87
|
+
const idDifference = left.session.id.localeCompare(right.session.id);
|
|
88
|
+
return idDifference !== 0 ? idDifference : left.session.path.localeCompare(right.session.path);
|
|
89
|
+
}
|
|
90
|
+
|
|
76
91
|
export function workflowResumeSelectorItems(
|
|
77
92
|
liveRuns: readonly RunSnapshot[],
|
|
78
93
|
durableEntries: readonly ResumableWorkflowEntry[],
|
|
94
|
+
completedEntries: readonly ResumableWorkflowEntry[] = [],
|
|
79
95
|
): WorkflowResumeSelectorItem[] {
|
|
80
96
|
const liveIds = new Set(liveRuns.map((run) => run.id));
|
|
97
|
+
const durableIds = new Set(durableEntries.map((entry) => entry.workflowId));
|
|
81
98
|
return [
|
|
82
99
|
...liveRuns.map(liveRunSession),
|
|
83
100
|
...durableEntries
|
|
84
101
|
.filter((entry) => !liveIds.has(entry.workflowId))
|
|
85
|
-
.map(durableWorkflowSession),
|
|
86
|
-
|
|
102
|
+
.map((entry) => durableWorkflowSession(entry, "durable")),
|
|
103
|
+
...completedEntries
|
|
104
|
+
.filter((entry) => !liveIds.has(entry.workflowId) && !durableIds.has(entry.workflowId))
|
|
105
|
+
.map((entry) => durableWorkflowSession(entry, "completed")),
|
|
106
|
+
].sort(compareResumeItemsByRecency);
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
export function openWorkflowResumeSelector(
|
|
90
110
|
ui: WorkflowResumeSelectorUiSurface,
|
|
91
111
|
liveRuns: readonly RunSnapshot[],
|
|
92
112
|
durableEntries: readonly ResumableWorkflowEntry[],
|
|
113
|
+
completedEntries: readonly ResumableWorkflowEntry[] = [],
|
|
93
114
|
): Promise<WorkflowResumeSelectorResult> {
|
|
94
115
|
const custom = ui.custom;
|
|
95
116
|
if (typeof custom !== "function") return Promise.resolve({ kind: "close" });
|
|
96
117
|
|
|
97
|
-
const items = workflowResumeSelectorItems(liveRuns, durableEntries);
|
|
118
|
+
const items = workflowResumeSelectorItems(liveRuns, durableEntries, completedEntries);
|
|
98
119
|
|
|
99
120
|
const resultByPath = new Map(items.map((item) => [item.session.path, item.result]));
|
|
100
121
|
const sessions = items.map((item) => item.session);
|
|
@@ -108,8 +129,11 @@ export function openWorkflowResumeSelector(
|
|
|
108
129
|
const settle = (result: WorkflowResumeSelectorResult, done?: (result: undefined) => void): void => {
|
|
109
130
|
if (settled) return;
|
|
110
131
|
settled = true;
|
|
111
|
-
|
|
112
|
-
|
|
132
|
+
try {
|
|
133
|
+
done?.(undefined);
|
|
134
|
+
} finally {
|
|
135
|
+
resolve(result);
|
|
136
|
+
}
|
|
113
137
|
};
|
|
114
138
|
|
|
115
139
|
const factory = (
|
|
@@ -146,6 +170,12 @@ export function openWorkflowResumeSelector(
|
|
|
146
170
|
};
|
|
147
171
|
};
|
|
148
172
|
|
|
149
|
-
|
|
173
|
+
try {
|
|
174
|
+
void Promise.resolve(custom(factory, { overlay: false })).catch(() => {
|
|
175
|
+
settle({ kind: "close" });
|
|
176
|
+
});
|
|
177
|
+
} catch {
|
|
178
|
+
settle({ kind: "close" });
|
|
179
|
+
}
|
|
150
180
|
});
|
|
151
181
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session-auto-compaction.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-auto-compaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAOrE,OAAO,KAAK,EAAE,2BAA2B,IAAI,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE9F;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAElD,eAAO,MAAM,6CAA6C,IAAI,CAAC;AAc/D,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,gBAAgB,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA8HrI;AAGD,wBAAgB,6CAA6C,CAAC,IAAI,EAAE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,GAAG,OAAO,CAQ7H;AAED,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,OAAO,CAEnF;AAmDD,wBAAgB,8BAA8B,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,OAAO,CAQ1F;AAED,wBAAgB,mCAAmC,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,OAAO,CAE/F;AAED;;GAEG;AAEH,wBAAgB,kDAAkD,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAQ3F;AAED;;GAEG;AAEH,wBAAgB,4CAA4C,CAAC,IAAI,EAAE,YAAY,EAC9E,OAAO,EAAE,UAAU,GAAG,WAAW,EACjC,SAAS,EAAE,OAAO,GAChB,IAAI,CAoCN;AAED,wBAAsB,uCAAuC,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAI/F;AAED;;GAEG;AAEH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAWlF;AAED;;;;;;GAMG;AAEH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAQrE;AAOD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,GAAG,WAAW,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAyEhI;AAED;;GAEG;AAEH,eAAO,MAAM,iCAAiC;;;;;;;;;CAS7C,CAAC","sourcesContent":["import type { AssistantMessage } from \"@earendil-works/pi-ai/compat\";\nimport { isContextOverflow } from \"@earendil-works/pi-ai/compat\";\nimport { getEffectiveInputBudget } from \"./context-window.ts\";\nimport { parseCopilotPromptLimitError } from \"./copilot-errors.ts\";\nimport { calculateContextTokens, estimateContextTokens, shouldCompact } from \"./compaction/index.ts\";\nimport { getLatestCompactionBoundaryEntry } from \"./session-manager.ts\";\nimport { MIN_RESPONSES_MAX_OUTPUT_TOKENS } from \"./openai-responses-payload-sanitizer.ts\";\nimport type { AgentSessionInternalSurface as AgentSession } from \"./agent-session-methods.ts\";\n\n/**\n * Upper bound on consecutive automatic continuations of a response that was\n * truncated at the output-token cap (\"length\") while the context is still\n * below the compaction budget. Each continuation regenerates the cut-off turn,\n * so a model that insists on emitting more than its per-turn output cap can\n * still terminate instead of looping forever.\n */\nexport const MAX_LENGTH_CONTINUATION_ATTEMPTS = 3;\n\nexport const MAX_OUTPUT_BUDGET_ERROR_CONTINUATION_ATTEMPTS = 1;\n\ntype JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };\ntype ProviderErrorDetails = {\n\tmessage?: string;\n\tcode?: string;\n\tparam?: string;\n};\n\nconst OUTPUT_BUDGET_PARAMETER_PATTERN = /\\bmax_output_tokens\\b/;\nconst OUTPUT_BUDGET_UNDERFLOW_PATTERN = new RegExp(\n\t`(?:integer\\\\s+below\\\\s+minimum\\\\s+value|expected\\\\s+(?:a\\\\s+)?value\\\\s*>=\\\\s*${MIN_RESPONSES_MAX_OUTPUT_TOKENS}|got\\\\s+1\\\\s+instead)`,\n);\n\nexport async function _checkCompaction(this: AgentSession, assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<void> {\n\t// The agent_end path passes skipAbortedCheck=true; the pre-prompt path passes\n\t// false. Only the live turn-completion path may auto-continue a truncated\n\t// response — before a fresh user prompt we must not resume the old turn.\n\tconst isLiveTurnCompletion = skipAbortedCheck;\n\tconst settings = this.settingsManager.getCompactionSettings();\n\tif (!settings.enabled) return;\n\n\t// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false\n\tif (skipAbortedCheck && assistantMessage.stopReason === \"aborted\") return;\n\n\tconst contextWindow = this.model?.contextWindow ?? 0;\n\n\t// Skip overflow check if the message came from a different model.\n\t// This handles the case where user switched from a smaller-context model (e.g. opus)\n\t// to a larger-context model (e.g. codex) - the overflow error from the old model\n\t// shouldn't trigger compaction for the new model.\n\tconst sameModel =\n\t\tthis.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;\n\n\t// Skip compaction checks if this assistant message is older than the latest\n\t// compaction boundary. This prevents a stale pre-compaction usage/error\n\t// from retriggering compaction on the first prompt after compaction.\n\tconst compactionBoundaryEntry = getLatestCompactionBoundaryEntry(this.sessionManager.getBranch());\n\tconst assistantIsFromBeforeCompactionBoundary =\n\t\tcompactionBoundaryEntry !== null &&\n\t\tassistantMessage.timestamp <= new Date(compactionBoundaryEntry.timestamp).getTime();\n\tif (assistantIsFromBeforeCompactionBoundary) {\n\t\treturn;\n\t}\n\n\t// Case 1: Overflow - LLM returned context overflow error\n\t// When Copilot rejects a 1m client-budget prompt at a lower server cap (for example\n\t// because long-context/usage-based billing entitlement is missing), leave the friendly\n\t// error visible instead of auto-compacting down to a smaller server tier silently.\n\tif (sameModel && this._isCopilotServerCapBelowSelectedContextWindow(assistantMessage)) {\n\t\treturn;\n\t}\n\tif (sameModel && isContextOverflow(assistantMessage, contextWindow)) {\n\t\tconst willRetry = assistantMessage.stopReason !== \"stop\";\n\t\tif (!willRetry) {\n\t\t\tawait this._runAutoCompaction(\"overflow\", false);\n\t\t\treturn;\n\t\t}\n\n\t\tif (this._overflowRecoveryAttempted) {\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"overflow\",\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t\tunresolvedOverflow: true,\n\t\t\t\terrorMessage:\n\t\t\t\t\t\"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.\",\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tthis._overflowRecoveryAttempted = true;\n\t\t// Remove the error message from agent state (it IS saved to session for history,\n\t\t// but we don't want it in context for the retry)\n\t\tconst messages = this.agent.state.messages;\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t}\n\t\tawait this._runAutoCompaction(\"overflow\", willRetry);\n\t\treturn;\n\t}\n\n\t// Case 2: Threshold - context is getting large\n\t// For error messages (no usage data), estimate from last successful response.\n\t// This ensures sessions that hit persistent API errors (e.g. 529) can still compact.\n\tlet contextTokens: number;\n\tif (assistantMessage.stopReason === \"error\") {\n\t\tconst messages = this.agent.state.messages;\n\t\tconst estimate = estimateContextTokens(messages);\n\t\tif (estimate.lastUsageIndex === null) return; // No usage data at all\n\t\t// Verify the usage source is post-compaction. Kept pre-compaction messages\n\t\t// have stale usage reflecting the old (larger) context and would falsely\n\t\t// trigger compaction right after one just finished.\n\t\tconst usageMsg = messages[estimate.lastUsageIndex];\n\t\tif (\n\t\t\tcompactionBoundaryEntry &&\n\t\t\tusageMsg.role === \"assistant\" &&\n\t\t\t(usageMsg as AssistantMessage).timestamp <= new Date(compactionBoundaryEntry.timestamp).getTime()\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tcontextTokens = estimate.tokens;\n\t} else {\n\t\tcontextTokens = calculateContextTokens(assistantMessage.usage);\n\t}\n\t// Compact against the effective input budget (the hard prompt cap for providers like Copilot\n\t// that advertise a larger total window) so we compact before overrunning the server-side limit\n\t// rather than relying on reactive overflow recovery near the cap.\n\tconst compactionBudget = this.model ? getEffectiveInputBudget(this.model) : contextWindow;\n\tif (shouldCompact(contextTokens, compactionBudget, settings)) {\n\t\tconst willRetry = shouldRetryAfterThresholdCompaction(assistantMessage);\n\t\tif (willRetry && isRetryWorthyOutputBudgetError(assistantMessage)) {\n\t\t\tif (this._outputBudgetErrorContinuationAttempts >= MAX_OUTPUT_BUDGET_ERROR_CONTINUATION_ATTEMPTS) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason: \"threshold\",\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t\terrorMessage:\n\t\t\t\t\t\t\"Output-budget recovery stopped after a compact-and-retry attempt. Try reducing context or switching to a larger-context model.\",\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis._outputBudgetErrorContinuationAttempts += 1;\n\t\t}\n\t\tawait this._runAutoCompaction(\"threshold\", willRetry);\n\t\treturn;\n\t}\n\n\t// A response truncated at the output-token cap (\"length\") with the context\n\t// still below the compaction budget is genuine work cut off mid-flight, not a\n\t// context overflow. Compaction would not free any room, so continue the\n\t// generation directly instead of dead-ending on the truncation and leaving\n\t// the task half-finished.\n\tif (isLiveTurnCompletion && isRetryWorthyLengthStop(assistantMessage)) {\n\t\tthis._resumeAfterLengthTruncation();\n\t}\n}\n\n\nexport function _isCopilotServerCapBelowSelectedContextWindow(this: AgentSession, assistantMessage: AssistantMessage): boolean {\n\tif (!this.model || this.model.provider !== \"github-copilot\" || !assistantMessage.errorMessage) return false;\n\tconst promptLimitError = parseCopilotPromptLimitError(assistantMessage.errorMessage);\n\t// Compare against the effective input budget (the model's real prompt cap), not the displayed\n\t// total window. A rejection at the prompt cap is a normal overflow we should compact-and-retry;\n\t// only a rejection *below* the cap (e.g. a missing long-context entitlement dropping the account\n\t// to a lower server tier) keeps the friendly error visible instead of silently compacting down.\n\treturn promptLimitError !== undefined && getEffectiveInputBudget(this.model) > promptLimitError.limitTokens;\n}\n\nexport function isRetryWorthyLengthStop(assistantMessage: AssistantMessage): boolean {\n\treturn assistantMessage.stopReason === \"length\" && assistantMessage.usage.output > 0;\n}\n\nfunction isJsonRecord(value: JsonValue): value is { [key: string]: JsonValue } {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringField(record: { [key: string]: JsonValue }, key: string): string | undefined {\n\tconst value = record[key];\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction parseProviderErrorDetails(errorMessage: string): ProviderErrorDetails | undefined {\n\tconst start = errorMessage.indexOf(\"{\");\n\tconst end = errorMessage.lastIndexOf(\"}\");\n\tif (start === -1 || end <= start) return undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(errorMessage.slice(start, end + 1)) as JsonValue;\n\t\tif (!isJsonRecord(parsed)) return undefined;\n\n\t\tconst nestedError = parsed.error;\n\t\tif (nestedError !== undefined && isJsonRecord(nestedError)) {\n\t\t\treturn {\n\t\t\t\tmessage: stringField(nestedError, \"message\"),\n\t\t\t\tcode: stringField(nestedError, \"code\") ?? stringField(parsed, \"code\"),\n\t\t\t\tparam: stringField(nestedError, \"param\") ?? stringField(parsed, \"param\"),\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tmessage: stringField(parsed, \"message\"),\n\t\t\tcode: stringField(parsed, \"code\"),\n\t\t\tparam: stringField(parsed, \"param\"),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction isOutputBudgetUnderflowText(text: string): boolean {\n\tconst message = text.toLowerCase();\n\treturn OUTPUT_BUDGET_PARAMETER_PATTERN.test(message) && OUTPUT_BUDGET_UNDERFLOW_PATTERN.test(message);\n}\n\nfunction isStructuredOutputBudgetUnderflow(details: ProviderErrorDetails): boolean {\n\tconst message = details.message?.toLowerCase() ?? \"\";\n\tconst param = details.param?.toLowerCase();\n\tif (!OUTPUT_BUDGET_UNDERFLOW_PATTERN.test(message)) return false;\n\treturn param !== undefined ? OUTPUT_BUDGET_PARAMETER_PATTERN.test(param) : OUTPUT_BUDGET_PARAMETER_PATTERN.test(message);\n}\n\nexport function isRetryWorthyOutputBudgetError(assistantMessage: AssistantMessage): boolean {\n\tif (assistantMessage.stopReason !== \"error\" || !assistantMessage.errorMessage) return false;\n\tif (assistantMessage.api !== \"openai-responses\") return false;\n\n\tconst structuredDetails = parseProviderErrorDetails(assistantMessage.errorMessage);\n\tif (structuredDetails && isStructuredOutputBudgetUnderflow(structuredDetails)) return true;\n\n\treturn isOutputBudgetUnderflowText(assistantMessage.errorMessage);\n}\n\nexport function shouldRetryAfterThresholdCompaction(assistantMessage: AssistantMessage): boolean {\n\treturn isRetryWorthyLengthStop(assistantMessage) || isRetryWorthyOutputBudgetError(assistantMessage);\n}\n\n/**\n * Internal: remove an incomplete assistant from retry context before auto-continuing after compaction.\n */\n\nexport function _dropTrailingAutoCompactionRetryAssistantIfPresent(this: AgentSession): void {\n\tconst messages = this.agent.state.messages;\n\tconst lastMsg = messages[messages.length - 1];\n\tif (lastMsg?.role !== \"assistant\") return;\n\tconst stopReason = (lastMsg as AssistantMessage).stopReason;\n\tif (stopReason === \"error\" || stopReason === \"length\") {\n\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t}\n}\n\n/**\n * Internal: schedule a live post-event continuation probe after compaction_end listeners can flush queues.\n */\n\nexport function _schedulePostAutoCompactionContinuationProbe(this: AgentSession,\n\t_reason: \"overflow\" | \"threshold\",\n\twillRetry: boolean,\n): void {\n\tif (willRetry) {\n\t\tconst token = this._postCompactionContinuationToken + 1;\n\t\tthis._postCompactionContinuationToken = token;\n\t\tlet pending: Promise<void>;\n\t\tpending = new Promise<void>((resolve) => {\n\t\t\tsetTimeout(() => {\n\t\t\t\tvoid (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (this._postCompactionContinuationToken !== token) return;\n\t\t\t\t\t\tif (this.isCompacting || this.isStreaming) return;\n\t\t\t\t\t\tawait this._resumeAfterAutoCompaction();\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (this._pendingPostCompactionContinuation === pending) {\n\t\t\t\t\t\t\tthis._pendingPostCompactionContinuation = undefined;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t}, 100);\n\t\t});\n\t\tthis._pendingPostCompactionContinuation = pending;\n\t\treturn;\n\t}\n\n\tsetTimeout(() => {\n\t\tif (this.isCompacting || this.isStreaming) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.agent.hasQueuedMessages()) {\n\t\t\treturn;\n\t\t}\n\n\t\tvoid this._resumeAfterAutoCompaction();\n\t}, 100);\n}\n\nexport async function _awaitPendingPostCompactionContinuation(this: AgentSession): Promise<void> {\n\tconst pending = this._pendingPostCompactionContinuation;\n\tif (pending === undefined) return;\n\tawait pending;\n}\n\n/**\n * Internal: resume generation after successful auto-compaction only when active work remains.\n */\n\nexport async function _resumeAfterAutoCompaction(this: AgentSession): Promise<void> {\n\ttry {\n\t\tawait this._runAgentContinue();\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tthis._emit({\n\t\t\ttype: \"agent_continue_error\",\n\t\t\tsource: \"post_compaction\",\n\t\t\terrorMessage: `Post-compaction continuation failed: ${message}`,\n\t\t});\n\t}\n}\n\n/**\n * Internal: resume a response that was truncated at the output-token cap\n * (\"length\") when the context does not warrant compaction. The generation is\n * continued directly so the model finishes the work it was cut off from, rather\n * than dead-ending on the truncation. Bounded by MAX_LENGTH_CONTINUATION_ATTEMPTS\n * so a turn that keeps exceeding the per-turn output cap can still terminate.\n */\n\nexport function _resumeAfterLengthTruncation(this: AgentSession): void {\n\tif (this._lengthContinuationAttempts >= MAX_LENGTH_CONTINUATION_ATTEMPTS) return;\n\tthis._lengthContinuationAttempts += 1;\n\t// agent.continue() rejects an assistant tail; drop the incomplete\n\t// length-stopped message so the preceding user/tool-result anchors the\n\t// continuation. It remains persisted in session history.\n\tthis._dropTrailingAutoCompactionRetryAssistantIfPresent();\n\tthis._schedulePostAutoCompactionContinuationProbe(\"threshold\", true);\n}\n\n\nfunction overflowUnresolved(reason: \"overflow\" | \"threshold\", aborted = false): boolean | undefined {\n\treturn reason === \"overflow\" && !aborted ? true : undefined;\n}\n\nexport async function _runAutoCompaction(this: AgentSession, reason: \"overflow\" | \"threshold\", willRetry: boolean): Promise<void> {\n\tthis._emit({ type: \"compaction_start\", reason });\n\tthis._autoCompactionAbortController = new AbortController();\n\n\ttry {\n\t\tif (!this.model) {\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason,\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t\tunresolvedOverflow: overflowUnresolved(reason),\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\t// Auth is resolved lazily: only called when the planner fallback is needed.\n\t\t// This allows extension-provided deletion requests to run before auth is checked,\n\t\t// enabling local extension compaction even when API credentials are unavailable.\n\t\t// Auto-mode resolver returns undefined (rather than throwing) when auth is missing;\n\t\t// overflow recovery then falls back to deterministic no-auth eviction, while\n\t\t// threshold compaction keeps the previous no-op behavior.\n\t\tconst model = this.model;\n\t\tconst result = await this._applyContextVerbatimCompaction({\n\t\t\tresolvePlannerAuth: async () => {\n\t\t\t\tconst authResult = await this._modelRegistry.getApiKeyAndHeaders(model);\n\t\t\t\tif (!authResult.ok || !authResult.apiKey) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\treturn { apiKey: authResult.apiKey, headers: authResult.headers };\n\t\t\t},\n\t\t\tabortController: this._autoCompactionAbortController,\n\t\t\tbackupLabel: reason === \"overflow\" ? \"overflow-auto-compact\" : \"auto-compact\",\n\t\t\treason,\n\t\t});\n\t\tif (!result) {\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason,\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t\tunresolvedOverflow: overflowUnresolved(reason),\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tif (willRetry) {\n\t\t\tthis._dropTrailingAutoCompactionRetryAssistantIfPresent();\n\t\t}\n\n\t\tthis._emit({ type: \"compaction_end\", reason, result, aborted: false, willRetry });\n\t\tthis._schedulePostAutoCompactionContinuationProbe(reason, willRetry);\n\t} catch (error) {\n\t\tconst errorMessage = error instanceof Error ? error.message : \"compaction failed\";\n\t\tconst aborted = errorMessage === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\tthis._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason,\n\t\t\tresult: undefined,\n\t\t\taborted,\n\t\t\twillRetry: false,\n\t\t\tunresolvedOverflow: overflowUnresolved(reason, aborted),\n\t\t\terrorMessage: aborted\n\t\t\t\t? undefined\n\t\t\t\t: reason === \"overflow\"\n\t\t\t\t\t? `Context overflow recovery failed: ${errorMessage}`\n\t\t\t\t\t: `Auto-compaction failed: ${errorMessage}`,\n\t\t});\n\t} finally {\n\t\tthis._autoCompactionAbortController = undefined;\n\t}\n}\n\n/**\n * Toggle auto-compaction setting.\n */\n\nexport const agentSessionAutoCompactionMethods = {\n\t_awaitPendingPostCompactionContinuation,\n\t_checkCompaction,\n\t_isCopilotServerCapBelowSelectedContextWindow,\n\t_dropTrailingAutoCompactionRetryAssistantIfPresent,\n\t_schedulePostAutoCompactionContinuationProbe,\n\t_resumeAfterAutoCompaction,\n\t_resumeAfterLengthTruncation,\n\t_runAutoCompaction,\n};\n"]}
|
|
1
|
+
{"version":3,"file":"agent-session-auto-compaction.d.ts","sourceRoot":"","sources":["../../src/core/agent-session-auto-compaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAOrE,OAAO,KAAK,EAAE,2BAA2B,IAAI,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE9F;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,IAAI,CAAC;AAElD,eAAO,MAAM,6CAA6C,IAAI,CAAC;AAc/D,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,gBAAgB,UAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA8HrI;AAGD,wBAAgB,6CAA6C,CAAC,IAAI,EAAE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,GAAG,OAAO,CAQ7H;AAED,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,OAAO,CAEnF;AAmDD,wBAAgB,8BAA8B,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,OAAO,CAQ1F;AAED,wBAAgB,mCAAmC,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,OAAO,CAE/F;AAED;;GAEG;AAEH,wBAAgB,kDAAkD,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAQ3F;AAED;;GAEG;AAEH,wBAAgB,4CAA4C,CAAC,IAAI,EAAE,YAAY,EAC9E,OAAO,EAAE,UAAU,GAAG,WAAW,EACjC,SAAS,EAAE,OAAO,GAChB,IAAI,CAoCN;AAED,wBAAsB,uCAAuC,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAI/F;AAED;;GAEG;AAEH,wBAAsB,0BAA0B,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAWlF;AAED;;;;;;GAMG;AAEH,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAQrE;AAOD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,UAAU,GAAG,WAAW,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAuEhI;AAED;;GAEG;AAEH,eAAO,MAAM,iCAAiC;;;;;;;;;CAS7C,CAAC","sourcesContent":["import type { AssistantMessage } from \"@earendil-works/pi-ai/compat\";\nimport { isContextOverflow } from \"@earendil-works/pi-ai/compat\";\nimport { getEffectiveInputBudget } from \"./context-window.ts\";\nimport { parseCopilotPromptLimitError } from \"./copilot-errors.ts\";\nimport { calculateContextTokens, estimateContextTokens, shouldCompact } from \"./compaction/index.ts\";\nimport { getLatestCompactionBoundaryEntry } from \"./session-manager.ts\";\nimport { MIN_RESPONSES_MAX_OUTPUT_TOKENS } from \"./openai-responses-payload-sanitizer.ts\";\nimport type { AgentSessionInternalSurface as AgentSession } from \"./agent-session-methods.ts\";\n\n/**\n * Upper bound on consecutive automatic continuations of a response that was\n * truncated at the output-token cap (\"length\") while the context is still\n * below the compaction budget. Each continuation regenerates the cut-off turn,\n * so a model that insists on emitting more than its per-turn output cap can\n * still terminate instead of looping forever.\n */\nexport const MAX_LENGTH_CONTINUATION_ATTEMPTS = 3;\n\nexport const MAX_OUTPUT_BUDGET_ERROR_CONTINUATION_ATTEMPTS = 1;\n\ntype JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };\ntype ProviderErrorDetails = {\n\tmessage?: string;\n\tcode?: string;\n\tparam?: string;\n};\n\nconst OUTPUT_BUDGET_PARAMETER_PATTERN = /\\bmax_output_tokens\\b/;\nconst OUTPUT_BUDGET_UNDERFLOW_PATTERN = new RegExp(\n\t`(?:integer\\\\s+below\\\\s+minimum\\\\s+value|expected\\\\s+(?:a\\\\s+)?value\\\\s*>=\\\\s*${MIN_RESPONSES_MAX_OUTPUT_TOKENS}|got\\\\s+1\\\\s+instead)`,\n);\n\nexport async function _checkCompaction(this: AgentSession, assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<void> {\n\t// The agent_end path passes skipAbortedCheck=true; the pre-prompt path passes\n\t// false. Only the live turn-completion path may auto-continue a truncated\n\t// response — before a fresh user prompt we must not resume the old turn.\n\tconst isLiveTurnCompletion = skipAbortedCheck;\n\tconst settings = this.settingsManager.getCompactionSettings();\n\tif (!settings.enabled) return;\n\n\t// Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false\n\tif (skipAbortedCheck && assistantMessage.stopReason === \"aborted\") return;\n\n\tconst contextWindow = this.model?.contextWindow ?? 0;\n\n\t// Skip overflow check if the message came from a different model.\n\t// This handles the case where user switched from a smaller-context model (e.g. opus)\n\t// to a larger-context model (e.g. codex) - the overflow error from the old model\n\t// shouldn't trigger compaction for the new model.\n\tconst sameModel =\n\t\tthis.model && assistantMessage.provider === this.model.provider && assistantMessage.model === this.model.id;\n\n\t// Skip compaction checks if this assistant message is older than the latest\n\t// compaction boundary. This prevents a stale pre-compaction usage/error\n\t// from retriggering compaction on the first prompt after compaction.\n\tconst compactionBoundaryEntry = getLatestCompactionBoundaryEntry(this.sessionManager.getBranch());\n\tconst assistantIsFromBeforeCompactionBoundary =\n\t\tcompactionBoundaryEntry !== null &&\n\t\tassistantMessage.timestamp <= new Date(compactionBoundaryEntry.timestamp).getTime();\n\tif (assistantIsFromBeforeCompactionBoundary) {\n\t\treturn;\n\t}\n\n\t// Case 1: Overflow - LLM returned context overflow error\n\t// When Copilot rejects a 1m client-budget prompt at a lower server cap (for example\n\t// because long-context/usage-based billing entitlement is missing), leave the friendly\n\t// error visible instead of auto-compacting down to a smaller server tier silently.\n\tif (sameModel && this._isCopilotServerCapBelowSelectedContextWindow(assistantMessage)) {\n\t\treturn;\n\t}\n\tif (sameModel && isContextOverflow(assistantMessage, contextWindow)) {\n\t\tconst willRetry = assistantMessage.stopReason !== \"stop\";\n\t\tif (!willRetry) {\n\t\t\tawait this._runAutoCompaction(\"overflow\", false);\n\t\t\treturn;\n\t\t}\n\n\t\tif (this._overflowRecoveryAttempted) {\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason: \"overflow\",\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t\tunresolvedOverflow: true,\n\t\t\t\terrorMessage:\n\t\t\t\t\t\"Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.\",\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tthis._overflowRecoveryAttempted = true;\n\t\t// Remove the error message from agent state (it IS saved to session for history,\n\t\t// but we don't want it in context for the retry)\n\t\tconst messages = this.agent.state.messages;\n\t\tif (messages.length > 0 && messages[messages.length - 1].role === \"assistant\") {\n\t\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t\t}\n\t\tawait this._runAutoCompaction(\"overflow\", willRetry);\n\t\treturn;\n\t}\n\n\t// Case 2: Threshold - context is getting large\n\t// For error messages (no usage data), estimate from last successful response.\n\t// This ensures sessions that hit persistent API errors (e.g. 529) can still compact.\n\tlet contextTokens: number;\n\tif (assistantMessage.stopReason === \"error\") {\n\t\tconst messages = this.agent.state.messages;\n\t\tconst estimate = estimateContextTokens(messages);\n\t\tif (estimate.lastUsageIndex === null) return; // No usage data at all\n\t\t// Verify the usage source is post-compaction. Kept pre-compaction messages\n\t\t// have stale usage reflecting the old (larger) context and would falsely\n\t\t// trigger compaction right after one just finished.\n\t\tconst usageMsg = messages[estimate.lastUsageIndex];\n\t\tif (\n\t\t\tcompactionBoundaryEntry &&\n\t\t\tusageMsg.role === \"assistant\" &&\n\t\t\t(usageMsg as AssistantMessage).timestamp <= new Date(compactionBoundaryEntry.timestamp).getTime()\n\t\t) {\n\t\t\treturn;\n\t\t}\n\t\tcontextTokens = estimate.tokens;\n\t} else {\n\t\tcontextTokens = calculateContextTokens(assistantMessage.usage);\n\t}\n\t// Compact against the effective input budget (the hard prompt cap for providers like Copilot\n\t// that advertise a larger total window) so we compact before overrunning the server-side limit\n\t// rather than relying on reactive overflow recovery near the cap.\n\tconst compactionBudget = this.model ? getEffectiveInputBudget(this.model) : contextWindow;\n\tif (shouldCompact(contextTokens, compactionBudget, settings)) {\n\t\tconst willRetry = shouldRetryAfterThresholdCompaction(assistantMessage);\n\t\tif (willRetry && isRetryWorthyOutputBudgetError(assistantMessage)) {\n\t\t\tif (this._outputBudgetErrorContinuationAttempts >= MAX_OUTPUT_BUDGET_ERROR_CONTINUATION_ATTEMPTS) {\n\t\t\t\tthis._emit({\n\t\t\t\t\ttype: \"compaction_end\",\n\t\t\t\t\treason: \"threshold\",\n\t\t\t\t\tresult: undefined,\n\t\t\t\t\taborted: false,\n\t\t\t\t\twillRetry: false,\n\t\t\t\t\terrorMessage:\n\t\t\t\t\t\t\"Output-budget recovery stopped after a compact-and-retry attempt. Try reducing context or switching to a larger-context model.\",\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis._outputBudgetErrorContinuationAttempts += 1;\n\t\t}\n\t\tawait this._runAutoCompaction(\"threshold\", willRetry);\n\t\treturn;\n\t}\n\n\t// A response truncated at the output-token cap (\"length\") with the context\n\t// still below the compaction budget is genuine work cut off mid-flight, not a\n\t// context overflow. Compaction would not free any room, so continue the\n\t// generation directly instead of dead-ending on the truncation and leaving\n\t// the task half-finished.\n\tif (isLiveTurnCompletion && isRetryWorthyLengthStop(assistantMessage)) {\n\t\tthis._resumeAfterLengthTruncation();\n\t}\n}\n\n\nexport function _isCopilotServerCapBelowSelectedContextWindow(this: AgentSession, assistantMessage: AssistantMessage): boolean {\n\tif (!this.model || this.model.provider !== \"github-copilot\" || !assistantMessage.errorMessage) return false;\n\tconst promptLimitError = parseCopilotPromptLimitError(assistantMessage.errorMessage);\n\t// Compare against the effective input budget (the model's real prompt cap), not the displayed\n\t// total window. A rejection at the prompt cap is a normal overflow we should compact-and-retry;\n\t// only a rejection *below* the cap (e.g. a missing long-context entitlement dropping the account\n\t// to a lower server tier) keeps the friendly error visible instead of silently compacting down.\n\treturn promptLimitError !== undefined && getEffectiveInputBudget(this.model) > promptLimitError.limitTokens;\n}\n\nexport function isRetryWorthyLengthStop(assistantMessage: AssistantMessage): boolean {\n\treturn assistantMessage.stopReason === \"length\" && assistantMessage.usage.output > 0;\n}\n\nfunction isJsonRecord(value: JsonValue): value is { [key: string]: JsonValue } {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction stringField(record: { [key: string]: JsonValue }, key: string): string | undefined {\n\tconst value = record[key];\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction parseProviderErrorDetails(errorMessage: string): ProviderErrorDetails | undefined {\n\tconst start = errorMessage.indexOf(\"{\");\n\tconst end = errorMessage.lastIndexOf(\"}\");\n\tif (start === -1 || end <= start) return undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(errorMessage.slice(start, end + 1)) as JsonValue;\n\t\tif (!isJsonRecord(parsed)) return undefined;\n\n\t\tconst nestedError = parsed.error;\n\t\tif (nestedError !== undefined && isJsonRecord(nestedError)) {\n\t\t\treturn {\n\t\t\t\tmessage: stringField(nestedError, \"message\"),\n\t\t\t\tcode: stringField(nestedError, \"code\") ?? stringField(parsed, \"code\"),\n\t\t\t\tparam: stringField(nestedError, \"param\") ?? stringField(parsed, \"param\"),\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tmessage: stringField(parsed, \"message\"),\n\t\t\tcode: stringField(parsed, \"code\"),\n\t\t\tparam: stringField(parsed, \"param\"),\n\t\t};\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction isOutputBudgetUnderflowText(text: string): boolean {\n\tconst message = text.toLowerCase();\n\treturn OUTPUT_BUDGET_PARAMETER_PATTERN.test(message) && OUTPUT_BUDGET_UNDERFLOW_PATTERN.test(message);\n}\n\nfunction isStructuredOutputBudgetUnderflow(details: ProviderErrorDetails): boolean {\n\tconst message = details.message?.toLowerCase() ?? \"\";\n\tconst param = details.param?.toLowerCase();\n\tif (!OUTPUT_BUDGET_UNDERFLOW_PATTERN.test(message)) return false;\n\treturn param !== undefined ? OUTPUT_BUDGET_PARAMETER_PATTERN.test(param) : OUTPUT_BUDGET_PARAMETER_PATTERN.test(message);\n}\n\nexport function isRetryWorthyOutputBudgetError(assistantMessage: AssistantMessage): boolean {\n\tif (assistantMessage.stopReason !== \"error\" || !assistantMessage.errorMessage) return false;\n\tif (assistantMessage.api !== \"openai-responses\") return false;\n\n\tconst structuredDetails = parseProviderErrorDetails(assistantMessage.errorMessage);\n\tif (structuredDetails && isStructuredOutputBudgetUnderflow(structuredDetails)) return true;\n\n\treturn isOutputBudgetUnderflowText(assistantMessage.errorMessage);\n}\n\nexport function shouldRetryAfterThresholdCompaction(assistantMessage: AssistantMessage): boolean {\n\treturn isRetryWorthyLengthStop(assistantMessage) || isRetryWorthyOutputBudgetError(assistantMessage);\n}\n\n/**\n * Internal: remove an incomplete assistant from retry context before auto-continuing after compaction.\n */\n\nexport function _dropTrailingAutoCompactionRetryAssistantIfPresent(this: AgentSession): void {\n\tconst messages = this.agent.state.messages;\n\tconst lastMsg = messages[messages.length - 1];\n\tif (lastMsg?.role !== \"assistant\") return;\n\tconst stopReason = (lastMsg as AssistantMessage).stopReason;\n\tif (stopReason === \"error\" || stopReason === \"length\") {\n\t\tthis.agent.state.messages = messages.slice(0, -1);\n\t}\n}\n\n/**\n * Internal: schedule a live post-event continuation probe after compaction_end listeners can flush queues.\n */\n\nexport function _schedulePostAutoCompactionContinuationProbe(this: AgentSession,\n\t_reason: \"overflow\" | \"threshold\",\n\twillRetry: boolean,\n): void {\n\tif (willRetry) {\n\t\tconst token = this._postCompactionContinuationToken + 1;\n\t\tthis._postCompactionContinuationToken = token;\n\t\tlet pending: Promise<void>;\n\t\tpending = new Promise<void>((resolve) => {\n\t\t\tsetTimeout(() => {\n\t\t\t\tvoid (async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (this._postCompactionContinuationToken !== token) return;\n\t\t\t\t\t\tif (this.isCompacting || this.isStreaming) return;\n\t\t\t\t\t\tawait this._resumeAfterAutoCompaction();\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif (this._pendingPostCompactionContinuation === pending) {\n\t\t\t\t\t\t\tthis._pendingPostCompactionContinuation = undefined;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t}, 100);\n\t\t});\n\t\tthis._pendingPostCompactionContinuation = pending;\n\t\treturn;\n\t}\n\n\tsetTimeout(() => {\n\t\tif (this.isCompacting || this.isStreaming) {\n\t\t\treturn;\n\t\t}\n\n\t\tif (!this.agent.hasQueuedMessages()) {\n\t\t\treturn;\n\t\t}\n\n\t\tvoid this._resumeAfterAutoCompaction();\n\t}, 100);\n}\n\nexport async function _awaitPendingPostCompactionContinuation(this: AgentSession): Promise<void> {\n\tconst pending = this._pendingPostCompactionContinuation;\n\tif (pending === undefined) return;\n\tawait pending;\n}\n\n/**\n * Internal: resume generation after successful auto-compaction only when active work remains.\n */\n\nexport async function _resumeAfterAutoCompaction(this: AgentSession): Promise<void> {\n\ttry {\n\t\tawait this._runAgentContinue();\n\t} catch (error) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tthis._emit({\n\t\t\ttype: \"agent_continue_error\",\n\t\t\tsource: \"post_compaction\",\n\t\t\terrorMessage: `Post-compaction continuation failed: ${message}`,\n\t\t});\n\t}\n}\n\n/**\n * Internal: resume a response that was truncated at the output-token cap\n * (\"length\") when the context does not warrant compaction. The generation is\n * continued directly so the model finishes the work it was cut off from, rather\n * than dead-ending on the truncation. Bounded by MAX_LENGTH_CONTINUATION_ATTEMPTS\n * so a turn that keeps exceeding the per-turn output cap can still terminate.\n */\n\nexport function _resumeAfterLengthTruncation(this: AgentSession): void {\n\tif (this._lengthContinuationAttempts >= MAX_LENGTH_CONTINUATION_ATTEMPTS) return;\n\tthis._lengthContinuationAttempts += 1;\n\t// agent.continue() rejects an assistant tail; drop the incomplete\n\t// length-stopped message so the preceding user/tool-result anchors the\n\t// continuation. It remains persisted in session history.\n\tthis._dropTrailingAutoCompactionRetryAssistantIfPresent();\n\tthis._schedulePostAutoCompactionContinuationProbe(\"threshold\", true);\n}\n\n\nfunction overflowUnresolved(reason: \"overflow\" | \"threshold\", aborted = false): boolean | undefined {\n\treturn reason === \"overflow\" && !aborted ? true : undefined;\n}\n\nexport async function _runAutoCompaction(this: AgentSession, reason: \"overflow\" | \"threshold\", willRetry: boolean): Promise<void> {\n\tthis._emit({ type: \"compaction_start\", reason });\n\tthis._autoCompactionAbortController = new AbortController();\n\n\ttry {\n\t\tif (!this.model) {\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason,\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t\tunresolvedOverflow: overflowUnresolved(reason),\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\t// Resolve auth only after extension hooks have had an opportunity to cancel\n\t\t// compaction or provide compacted text, so local extension compaction does not\n\t\t// require provider credentials. Missing auth then fails model-driven compaction\n\t\t// before persistence or continuation, matching other provider-call failures.\n\t\tconst model = this.model;\n\t\tconst result = await this._applyVerbatimCompaction({\n\t\t\tresolvePlannerAuth: async () => {\n\t\t\t\tconst authResult = await this._modelRegistry.getApiKeyAndHeaders(model);\n\t\t\t\tif (!authResult.ok || !authResult.apiKey) {\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t\treturn { apiKey: authResult.apiKey, headers: authResult.headers };\n\t\t\t},\n\t\t\tabortController: this._autoCompactionAbortController,\n\t\t\tbackupLabel: reason === \"overflow\" ? \"overflow-auto-compact\" : \"auto-compact\",\n\t\t\treason,\n\t\t});\n\t\tif (!result) {\n\t\t\tthis._emit({\n\t\t\t\ttype: \"compaction_end\",\n\t\t\t\treason,\n\t\t\t\tresult: undefined,\n\t\t\t\taborted: false,\n\t\t\t\twillRetry: false,\n\t\t\t\tunresolvedOverflow: overflowUnresolved(reason),\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\n\t\tif (willRetry) {\n\t\t\tthis._dropTrailingAutoCompactionRetryAssistantIfPresent();\n\t\t}\n\n\t\tthis._emit({ type: \"compaction_end\", reason, result, aborted: false, willRetry });\n\t\tthis._schedulePostAutoCompactionContinuationProbe(reason, willRetry);\n\t} catch (error) {\n\t\tconst errorMessage = error instanceof Error ? error.message : \"compaction failed\";\n\t\tconst aborted = errorMessage === \"Compaction cancelled\" || (error instanceof Error && error.name === \"AbortError\");\n\t\tthis._emit({\n\t\t\ttype: \"compaction_end\",\n\t\t\treason,\n\t\t\tresult: undefined,\n\t\t\taborted,\n\t\t\twillRetry: false,\n\t\t\tunresolvedOverflow: overflowUnresolved(reason, aborted),\n\t\t\terrorMessage: aborted\n\t\t\t\t? undefined\n\t\t\t\t: reason === \"overflow\"\n\t\t\t\t\t? `Context overflow recovery failed: ${errorMessage}`\n\t\t\t\t\t: `Auto-compaction failed: ${errorMessage}`,\n\t\t});\n\t} finally {\n\t\tthis._autoCompactionAbortController = undefined;\n\t}\n}\n\n/**\n * Toggle auto-compaction setting.\n */\n\nexport const agentSessionAutoCompactionMethods = {\n\t_awaitPendingPostCompactionContinuation,\n\t_checkCompaction,\n\t_isCopilotServerCapBelowSelectedContextWindow,\n\t_dropTrailingAutoCompactionRetryAssistantIfPresent,\n\t_schedulePostAutoCompactionContinuationProbe,\n\t_resumeAfterAutoCompaction,\n\t_resumeAfterLengthTruncation,\n\t_runAutoCompaction,\n};\n"]}
|
|
@@ -312,14 +312,12 @@ export async function _runAutoCompaction(reason, willRetry) {
|
|
|
312
312
|
});
|
|
313
313
|
return;
|
|
314
314
|
}
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
// overflow recovery then falls back to deterministic no-auth eviction, while
|
|
320
|
-
// threshold compaction keeps the previous no-op behavior.
|
|
315
|
+
// Resolve auth only after extension hooks have had an opportunity to cancel
|
|
316
|
+
// compaction or provide compacted text, so local extension compaction does not
|
|
317
|
+
// require provider credentials. Missing auth then fails model-driven compaction
|
|
318
|
+
// before persistence or continuation, matching other provider-call failures.
|
|
321
319
|
const model = this.model;
|
|
322
|
-
const result = await this.
|
|
320
|
+
const result = await this._applyVerbatimCompaction({
|
|
323
321
|
resolvePlannerAuth: async () => {
|
|
324
322
|
const authResult = await this._modelRegistry.getApiKeyAndHeaders(model);
|
|
325
323
|
if (!authResult.ok || !authResult.apiKey) {
|