tulingcode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +134 -0
- package/Dockerfile +18 -0
- package/README.md +15 -0
- package/bin/tuling +179 -0
- package/bunfig.toml +7 -0
- package/drizzle.config.ts +10 -0
- package/git +0 -0
- package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
- package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
- package/migration/20260211171708_add_project_commands/migration.sql +1 -0
- package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
- package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
- package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
- package/migration/20260225215848_workspace/migration.sql +7 -0
- package/migration/20260225215848_workspace/snapshot.json +959 -0
- package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
- package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
- package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
- package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
- package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
- package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
- package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
- package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
- package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
- package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
- package/migration/20260323234822_events/migration.sql +13 -0
- package/migration/20260323234822_events/snapshot.json +1271 -0
- package/migration/20260410174513_workspace-name/migration.sql +16 -0
- package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
- package/migration/20260413175956_chief_energizer/migration.sql +13 -0
- package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
- package/migration/20260422160000_context_inheritance/migration.sql +3 -0
- package/migration/20260422170000_task_registry/migration.sql +18 -0
- package/migration/20260423145421_remove_session_entry/migration.sql +4 -0
- package/migration/20260515000000_actor_rename/migration.sql +7 -0
- package/migration/20260515010000_memory_fts/migration.sql +33 -0
- package/migration/20260515020000_user_task/migration.sql +29 -0
- package/migration/20260519000000_last_checkpoint_message_id/migration.sql +1 -0
- package/migration/20260521000000_message_agent_id/migration.sql +2 -0
- package/migration/20260521000100_actor_registry_v6/migration.sql +25 -0
- package/migration/20260521010000_memory_fts_v6/migration.sql +33 -0
- package/migration/20260521020000_memory_fts_triggers/migration.sql +17 -0
- package/migration/20260526000000_agent_id_main/migration.sql +14 -0
- package/migration/20260527000000_actor_lifecycle/migration.sql +8 -0
- package/migration/20260527000100_inbox/migration.sql +12 -0
- package/migration/20260529000000_task_todo_redesign/migration.sql +16 -0
- package/migration/20260603000000_task_in_progress_owner/migration.sql +1 -0
- package/migration/20260603000000_workflow_run/migration.sql +17 -0
- package/migration/20260604000000_workflow_script_sha/migration.sql +1 -0
- package/migration/20260608000000_claude_import/migration.sql +7 -0
- package/migration/20260608010000_claude_import_message_ids/migration.sql +1 -0
- package/migration/20260609000000_history_fts/migration.sql +29 -0
- package/migration/20260609230000_workflow_agent_timeout/migration.sql +1 -0
- package/package.json +196 -0
- package/parsers-config.ts +290 -0
- package/script/build.ts +267 -0
- package/script/check-migrations.ts +16 -0
- package/script/fix-node-pty.ts +28 -0
- package/script/generate.ts +23 -0
- package/script/postinstall.mjs +102 -0
- package/script/publish.ts +60 -0
- package/script/run-workspace-server +106 -0
- package/script/schema.ts +63 -0
- package/script/time.ts +6 -0
- package/script/trace-imports.ts +153 -0
- package/script/upgrade-opentui.ts +64 -0
- package/src/account/account.sql.ts +39 -0
- package/src/account/account.ts +456 -0
- package/src/account/repo.ts +166 -0
- package/src/account/schema.ts +99 -0
- package/src/account/url.ts +8 -0
- package/src/acp/README.md +174 -0
- package/src/acp/agent.ts +1783 -0
- package/src/acp/session.ts +116 -0
- package/src/acp/types.ts +24 -0
- package/src/actor/actor.sql.ts +38 -0
- package/src/actor/events.ts +67 -0
- package/src/actor/index.ts +2 -0
- package/src/actor/registry.ts +412 -0
- package/src/actor/return-header.ts +24 -0
- package/src/actor/schema.ts +47 -0
- package/src/actor/spawn-ref.ts +16 -0
- package/src/actor/spawn.ts +741 -0
- package/src/actor/turn.ts +49 -0
- package/src/actor/waiter.ts +166 -0
- package/src/agent/agent.ts +554 -0
- package/src/agent/config.ts +5 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/checkpoint-writer.txt +167 -0
- package/src/agent/prompt/compaction.txt +9 -0
- package/src/agent/prompt/distill.txt +199 -0
- package/src/agent/prompt/dream.txt +155 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +11 -0
- package/src/agent/prompt/title.txt +44 -0
- package/src/audio.d.ts +9 -0
- package/src/auth/index.ts +97 -0
- package/src/bus/bus-event.ts +33 -0
- package/src/bus/global.ts +12 -0
- package/src/bus/index.ts +193 -0
- package/src/cli/bootstrap.ts +33 -0
- package/src/cli/cmd/account.ts +258 -0
- package/src/cli/cmd/acp.ts +70 -0
- package/src/cli/cmd/agent.ts +248 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/db.ts +120 -0
- package/src/cli/cmd/debug/agent.ts +192 -0
- package/src/cli/cmd/debug/config.ts +17 -0
- package/src/cli/cmd/debug/file.ts +100 -0
- package/src/cli/cmd/debug/index.ts +48 -0
- package/src/cli/cmd/debug/lsp.ts +61 -0
- package/src/cli/cmd/debug/ripgrep.ts +105 -0
- package/src/cli/cmd/debug/scrap.ts +16 -0
- package/src/cli/cmd/debug/skill.ts +23 -0
- package/src/cli/cmd/debug/snapshot.ts +53 -0
- package/src/cli/cmd/export.ts +306 -0
- package/src/cli/cmd/generate.ts +50 -0
- package/src/cli/cmd/github.ts +1647 -0
- package/src/cli/cmd/import.ts +208 -0
- package/src/cli/cmd/mcp.ts +812 -0
- package/src/cli/cmd/models.ts +88 -0
- package/src/cli/cmd/plug.ts +233 -0
- package/src/cli/cmd/pr.ts +138 -0
- package/src/cli/cmd/providers.ts +705 -0
- package/src/cli/cmd/run-completion.ts +77 -0
- package/src/cli/cmd/run.ts +694 -0
- package/src/cli/cmd/serve.ts +21 -0
- package/src/cli/cmd/session.ts +181 -0
- package/src/cli/cmd/stats.ts +413 -0
- package/src/cli/cmd/tui/app.tsx +1130 -0
- package/src/cli/cmd/tui/asset/TEN_VAD_LICENSE +12 -0
- package/src/cli/cmd/tui/asset/charge.wav +0 -0
- package/src/cli/cmd/tui/asset/pulse-a.wav +0 -0
- package/src/cli/cmd/tui/asset/pulse-b.wav +0 -0
- package/src/cli/cmd/tui/asset/pulse-c.wav +0 -0
- package/src/cli/cmd/tui/asset/ten_vad.wasm +0 -0
- package/src/cli/cmd/tui/asset/ten_vad_loader.js +30 -0
- package/src/cli/cmd/tui/attach.ts +83 -0
- package/src/cli/cmd/tui/component/background-image.tsx +150 -0
- package/src/cli/cmd/tui/component/bg-pulse.tsx +130 -0
- package/src/cli/cmd/tui/component/border.tsx +21 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-command.tsx +208 -0
- package/src/cli/cmd/tui/component/dialog-console-org.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +157 -0
- package/src/cli/cmd/tui/component/dialog-image-list.tsx +111 -0
- package/src/cli/cmd/tui/component/dialog-logo-design.tsx +37 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-mimo-login.tsx +224 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +253 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +490 -0
- package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +101 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +269 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-skill.tsx +42 -0
- package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +170 -0
- package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/dialog-variant.tsx +39 -0
- package/src/cli/cmd/tui/component/dialog-workflows.tsx +62 -0
- package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +289 -0
- package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +81 -0
- package/src/cli/cmd/tui/component/dialog-worktree.tsx +90 -0
- package/src/cli/cmd/tui/component/error-component.tsx +92 -0
- package/src/cli/cmd/tui/component/logo.tsx +961 -0
- package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +684 -0
- package/src/cli/cmd/tui/component/prompt/cwd.ts +0 -0
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +1812 -0
- package/src/cli/cmd/tui/component/prompt/part.ts +16 -0
- package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
- package/src/cli/cmd/tui/component/spinner.tsx +24 -0
- package/src/cli/cmd/tui/component/starry-background.tsx +305 -0
- package/src/cli/cmd/tui/component/startup-loading.tsx +67 -0
- package/src/cli/cmd/tui/component/task-item.tsx +63 -0
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
- package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
- package/src/cli/cmd/tui/config/cwd.ts +5 -0
- package/src/cli/cmd/tui/config/tui-migrate.ts +151 -0
- package/src/cli/cmd/tui/config/tui-schema.ts +38 -0
- package/src/cli/cmd/tui/config/tui.ts +219 -0
- package/src/cli/cmd/tui/context/args.tsx +16 -0
- package/src/cli/cmd/tui/context/directory.ts +15 -0
- package/src/cli/cmd/tui/context/event.ts +45 -0
- package/src/cli/cmd/tui/context/exit.tsx +65 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +105 -0
- package/src/cli/cmd/tui/context/kv.tsx +76 -0
- package/src/cli/cmd/tui/context/language.tsx +91 -0
- package/src/cli/cmd/tui/context/local.tsx +455 -0
- package/src/cli/cmd/tui/context/plugin-keybinds.ts +41 -0
- package/src/cli/cmd/tui/context/project.tsx +109 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +61 -0
- package/src/cli/cmd/tui/context/sdk.tsx +150 -0
- package/src/cli/cmd/tui/context/sync.tsx +828 -0
- package/src/cli/cmd/tui/context/theme/aura.json +69 -0
- package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
- package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +225 -0
- package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
- package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
- package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
- package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
- package/src/cli/cmd/tui/context/theme/github.json +233 -0
- package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
- package/src/cli/cmd/tui/context/theme/lucent-orng.json +234 -0
- package/src/cli/cmd/tui/context/theme/material.json +235 -0
- package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
- package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
- package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
- package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
- package/src/cli/cmd/tui/context/theme/nord.json +223 -0
- package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
- package/src/cli/cmd/tui/context/theme/orng.json +249 -0
- package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
- package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
- package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
- package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
- package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
- package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
- package/src/cli/cmd/tui/context/theme/tulingcode.json +245 -0
- package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
- package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
- package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
- package/src/cli/cmd/tui/context/theme.tsx +1298 -0
- package/src/cli/cmd/tui/context/thinking.ts +48 -0
- package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
- package/src/cli/cmd/tui/event.ts +56 -0
- package/src/cli/cmd/tui/feature-plugins/home/footer.tsx +93 -0
- package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +193 -0
- package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +54 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +114 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/cwd.tsx +45 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +62 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +93 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/goal.tsx +84 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/instructions.tsx +54 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +66 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +98 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/task.tsx +95 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +51 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/tps.ts +31 -0
- package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +274 -0
- package/src/cli/cmd/tui/i18n/en.ts +397 -0
- package/src/cli/cmd/tui/i18n/es.ts +433 -0
- package/src/cli/cmd/tui/i18n/fr.ts +440 -0
- package/src/cli/cmd/tui/i18n/ja.ts +392 -0
- package/src/cli/cmd/tui/i18n/locales.ts +82 -0
- package/src/cli/cmd/tui/i18n/ru.ts +452 -0
- package/src/cli/cmd/tui/i18n/zh.ts +390 -0
- package/src/cli/cmd/tui/i18n/zht.ts +360 -0
- package/src/cli/cmd/tui/layer.ts +6 -0
- package/src/cli/cmd/tui/plugin/api.tsx +402 -0
- package/src/cli/cmd/tui/plugin/index.ts +3 -0
- package/src/cli/cmd/tui/plugin/internal.ts +35 -0
- package/src/cli/cmd/tui/plugin/runtime.ts +1030 -0
- package/src/cli/cmd/tui/plugin/slots.tsx +60 -0
- package/src/cli/cmd/tui/routes/home.tsx +165 -0
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +116 -0
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +2532 -0
- package/src/cli/cmd/tui/routes/session/permission.tsx +691 -0
- package/src/cli/cmd/tui/routes/session/question.tsx +488 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +97 -0
- package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +142 -0
- package/src/cli/cmd/tui/thread.ts +246 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +61 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +95 -0
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +223 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +42 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +123 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +452 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +207 -0
- package/src/cli/cmd/tui/ui/link.tsx +28 -0
- package/src/cli/cmd/tui/ui/spinner.ts +378 -0
- package/src/cli/cmd/tui/ui/toast.tsx +102 -0
- package/src/cli/cmd/tui/util/clipboard.ts +203 -0
- package/src/cli/cmd/tui/util/editor.ts +35 -0
- package/src/cli/cmd/tui/util/image-protocol.ts +35 -0
- package/src/cli/cmd/tui/util/index.ts +6 -0
- package/src/cli/cmd/tui/util/model.ts +23 -0
- package/src/cli/cmd/tui/util/provider-origin.ts +7 -0
- package/src/cli/cmd/tui/util/revert-diff.ts +18 -0
- package/src/cli/cmd/tui/util/scroll.ts +23 -0
- package/src/cli/cmd/tui/util/selection.ts +23 -0
- package/src/cli/cmd/tui/util/signal.ts +41 -0
- package/src/cli/cmd/tui/util/sound.ts +154 -0
- package/src/cli/cmd/tui/util/system-locale.ts +209 -0
- package/src/cli/cmd/tui/util/terminal.ts +110 -0
- package/src/cli/cmd/tui/util/transcript.ts +112 -0
- package/src/cli/cmd/tui/util/vad.ts +229 -0
- package/src/cli/cmd/tui/util/voice.ts +360 -0
- package/src/cli/cmd/tui/win32.ts +130 -0
- package/src/cli/cmd/tui/worker.ts +104 -0
- package/src/cli/cmd/uninstall.ts +351 -0
- package/src/cli/cmd/upgrade.ts +79 -0
- package/src/cli/cmd/web.ts +81 -0
- package/src/cli/effect/prompt.ts +25 -0
- package/src/cli/error.ts +82 -0
- package/src/cli/heap.ts +59 -0
- package/src/cli/i18n.ts +15 -0
- package/src/cli/logo.ts +53 -0
- package/src/cli/network.ts +62 -0
- package/src/cli/ui.ts +133 -0
- package/src/cli/upgrade.ts +41 -0
- package/src/command/index.ts +276 -0
- package/src/command/template/initialize.txt +66 -0
- package/src/command/template/review.txt +101 -0
- package/src/config/agent.ts +197 -0
- package/src/config/command.ts +69 -0
- package/src/config/config.ts +1024 -0
- package/src/config/console-state.ts +16 -0
- package/src/config/entry-name.ts +16 -0
- package/src/config/error.ts +21 -0
- package/src/config/formatter.ts +17 -0
- package/src/config/history.ts +21 -0
- package/src/config/index.ts +16 -0
- package/src/config/keybinds.ts +127 -0
- package/src/config/layout.ts +10 -0
- package/src/config/lsp.ts +45 -0
- package/src/config/managed.ts +70 -0
- package/src/config/markdown.ts +97 -0
- package/src/config/mcp.ts +172 -0
- package/src/config/model-id.ts +14 -0
- package/src/config/parse.ts +44 -0
- package/src/config/paths.ts +73 -0
- package/src/config/permission.ts +76 -0
- package/src/config/plugin.ts +88 -0
- package/src/config/provider.ts +118 -0
- package/src/config/server.ts +20 -0
- package/src/config/skills.ts +16 -0
- package/src/config/variable.ts +90 -0
- package/src/control-plane/adaptors/index.ts +52 -0
- package/src/control-plane/adaptors/worktree.ts +47 -0
- package/src/control-plane/dev/debug-workspace-plugin.ts +73 -0
- package/src/control-plane/schema.ts +19 -0
- package/src/control-plane/sse.ts +66 -0
- package/src/control-plane/types.ts +34 -0
- package/src/control-plane/util.ts +37 -0
- package/src/control-plane/workspace-context.ts +26 -0
- package/src/control-plane/workspace.sql.ts +17 -0
- package/src/control-plane/workspace.ts +615 -0
- package/src/effect/app-runtime.ts +146 -0
- package/src/effect/bootstrap-runtime.ts +33 -0
- package/src/effect/bridge.ts +48 -0
- package/src/effect/cross-spawn-spawner.ts +514 -0
- package/src/effect/index.ts +5 -0
- package/src/effect/instance-ref.ts +11 -0
- package/src/effect/instance-registry.ts +12 -0
- package/src/effect/instance-state.ts +81 -0
- package/src/effect/logger.ts +73 -0
- package/src/effect/memo-map.ts +3 -0
- package/src/effect/observability.ts +107 -0
- package/src/effect/run-service.ts +52 -0
- package/src/effect/runner.ts +210 -0
- package/src/effect/runtime.ts +19 -0
- package/src/env/index.ts +37 -0
- package/src/file/ignore.ts +81 -0
- package/src/file/index.ts +664 -0
- package/src/file/protected.ts +59 -0
- package/src/file/ripgrep.ts +485 -0
- package/src/file/watcher.ts +163 -0
- package/src/flag/flag.ts +164 -0
- package/src/format/formatter.ts +403 -0
- package/src/format/index.ts +203 -0
- package/src/git/index.ts +260 -0
- package/src/global/index.ts +54 -0
- package/src/history/backfill.ts +162 -0
- package/src/history/extract.ts +67 -0
- package/src/history/fts-query.ts +15 -0
- package/src/history/fts.sql.ts +20 -0
- package/src/history/index.ts +10 -0
- package/src/history/resolve.ts +65 -0
- package/src/history/service.ts +258 -0
- package/src/history/writer.ts +112 -0
- package/src/id/id.ts +87 -0
- package/src/ide/index.ts +73 -0
- package/src/inbox/inbox-ref.ts +38 -0
- package/src/inbox/inbox.sql.ts +26 -0
- package/src/inbox/inbox.ts +223 -0
- package/src/inbox/index.ts +3 -0
- package/src/inbox/render.ts +40 -0
- package/src/index.ts +260 -0
- package/src/installation/index.ts +351 -0
- package/src/installation/version.ts +8 -0
- package/src/lsp/client.ts +249 -0
- package/src/lsp/diagnostic.ts +29 -0
- package/src/lsp/index.ts +3 -0
- package/src/lsp/language.ts +120 -0
- package/src/lsp/launch.ts +21 -0
- package/src/lsp/lsp.ts +519 -0
- package/src/lsp/server.ts +1956 -0
- package/src/mcp/auth.ts +144 -0
- package/src/mcp/index.ts +944 -0
- package/src/mcp/oauth-callback.ts +232 -0
- package/src/mcp/oauth-provider.ts +214 -0
- package/src/memory/fts-query.ts +37 -0
- package/src/memory/fts.sql.ts +19 -0
- package/src/memory/index.ts +1 -0
- package/src/memory/paths.ts +116 -0
- package/src/memory/reconcile.ts +144 -0
- package/src/memory/service.ts +144 -0
- package/src/metrics/client.ts +40 -0
- package/src/metrics/event.ts +43 -0
- package/src/metrics/index.ts +5 -0
- package/src/metrics/installation.ts +18 -0
- package/src/metrics/subscriber.ts +58 -0
- package/src/metrics/util.ts +9 -0
- package/src/node.ts +6 -0
- package/src/npm/config.ts +0 -0
- package/src/npm/index.ts +293 -0
- package/src/npmcli-config.d.ts +43 -0
- package/src/patch/index.ts +680 -0
- package/src/permission/arity.ts +163 -0
- package/src/permission/evaluate.ts +15 -0
- package/src/permission/index.ts +379 -0
- package/src/permission/schema.ts +17 -0
- package/src/plugin/checkpoint-splitover.ts +60 -0
- package/src/plugin/cloud-ai.ts +329 -0
- package/src/plugin/cloudflare.ts +76 -0
- package/src/plugin/codex.ts +607 -0
- package/src/plugin/github-copilot/copilot.ts +368 -0
- package/src/plugin/github-copilot/models.ts +153 -0
- package/src/plugin/index.ts +493 -0
- package/src/plugin/install.ts +439 -0
- package/src/plugin/loader.ts +216 -0
- package/src/plugin/matcher.ts +33 -0
- package/src/plugin/meta.ts +188 -0
- package/src/plugin/mimo-free.ts +153 -0
- package/src/plugin/mimo.ts +124 -0
- package/src/plugin/shared.ts +323 -0
- package/src/plugin/subagent-progress-checker.ts +147 -0
- package/src/project/bootstrap.ts +59 -0
- package/src/project/index.ts +2 -0
- package/src/project/instance.ts +190 -0
- package/src/project/project-id.ts +48 -0
- package/src/project/project.sql.ts +16 -0
- package/src/project/project.ts +501 -0
- package/src/project/schema.ts +15 -0
- package/src/project/vcs.ts +227 -0
- package/src/provider/auth.ts +234 -0
- package/src/provider/error.ts +216 -0
- package/src/provider/index.ts +5 -0
- package/src/provider/models.ts +180 -0
- package/src/provider/provider.ts +1782 -0
- package/src/provider/schema.ts +36 -0
- package/src/provider/sdk/copilot/README.md +5 -0
- package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
- package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
- package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +815 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +83 -0
- package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
- package/src/provider/sdk/copilot/index.ts +2 -0
- package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
- package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +335 -0
- package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
- package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +214 -0
- package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1770 -0
- package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +173 -0
- package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
- package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +87 -0
- package/src/provider/sdk/copilot/responses/tool/file-search.ts +127 -0
- package/src/provider/sdk/copilot/responses/tool/image-generation.ts +114 -0
- package/src/provider/sdk/copilot/responses/tool/local-shell.ts +64 -0
- package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +103 -0
- package/src/provider/sdk/copilot/responses/tool/web-search.ts +102 -0
- package/src/provider/transform.ts +1322 -0
- package/src/pty/index.ts +364 -0
- package/src/pty/pty.bun.ts +26 -0
- package/src/pty/pty.node.ts +27 -0
- package/src/pty/pty.ts +25 -0
- package/src/pty/schema.ts +17 -0
- package/src/question/index.ts +252 -0
- package/src/question/schema.ts +17 -0
- package/src/server/adapter.bun.ts +40 -0
- package/src/server/adapter.node.ts +66 -0
- package/src/server/adapter.ts +21 -0
- package/src/server/error.ts +53 -0
- package/src/server/event.ts +7 -0
- package/src/server/fence.ts +81 -0
- package/src/server/mdns.ts +60 -0
- package/src/server/middleware.ts +92 -0
- package/src/server/projectors.ts +28 -0
- package/src/server/proxy.ts +171 -0
- package/src/server/routes/control/index.ts +160 -0
- package/src/server/routes/control/workspace.ts +203 -0
- package/src/server/routes/global.ts +287 -0
- package/src/server/routes/instance/bash-interactive.ts +82 -0
- package/src/server/routes/instance/config.ts +89 -0
- package/src/server/routes/instance/event.ts +88 -0
- package/src/server/routes/instance/experimental.ts +408 -0
- package/src/server/routes/instance/file.ts +190 -0
- package/src/server/routes/instance/httpapi/config.ts +51 -0
- package/src/server/routes/instance/httpapi/permission.ts +72 -0
- package/src/server/routes/instance/httpapi/project.ts +62 -0
- package/src/server/routes/instance/httpapi/provider.ts +142 -0
- package/src/server/routes/instance/httpapi/question.ts +121 -0
- package/src/server/routes/instance/httpapi/server.ts +136 -0
- package/src/server/routes/instance/index.ts +301 -0
- package/src/server/routes/instance/mcp.ts +260 -0
- package/src/server/routes/instance/middleware.ts +35 -0
- package/src/server/routes/instance/permission.ts +73 -0
- package/src/server/routes/instance/project.ts +122 -0
- package/src/server/routes/instance/provider.ts +158 -0
- package/src/server/routes/instance/pty.ts +247 -0
- package/src/server/routes/instance/question.ts +162 -0
- package/src/server/routes/instance/session.ts +1296 -0
- package/src/server/routes/instance/sync.ts +143 -0
- package/src/server/routes/instance/trace.ts +59 -0
- package/src/server/routes/instance/tui.ts +384 -0
- package/src/server/routes/instance/workflows.ts +72 -0
- package/src/server/routes/ui.ts +55 -0
- package/src/server/server.ts +136 -0
- package/src/server/workspace.ts +122 -0
- package/src/session/auto-dream.ts +123 -0
- package/src/session/boundary.ts +77 -0
- package/src/session/budgeted-read.ts +118 -0
- package/src/session/checkpoint-align.ts +29 -0
- package/src/session/checkpoint-context.ts +36 -0
- package/src/session/checkpoint-paths.ts +86 -0
- package/src/session/checkpoint-progress-reconcile.ts +111 -0
- package/src/session/checkpoint-retry.ts +192 -0
- package/src/session/checkpoint-templates.ts +114 -0
- package/src/session/checkpoint-validator.ts +259 -0
- package/src/session/checkpoint.ts +1478 -0
- package/src/session/classify.ts +92 -0
- package/src/session/claude-import.sql.ts +13 -0
- package/src/session/claude-import.ts +379 -0
- package/src/session/compaction.ts +543 -0
- package/src/session/goal.ts +232 -0
- package/src/session/index.ts +1 -0
- package/src/session/instruction.ts +276 -0
- package/src/session/last-message-info.ts +32 -0
- package/src/session/llm-request-prefix.ts +82 -0
- package/src/session/llm.ts +735 -0
- package/src/session/max-mode.ts +397 -0
- package/src/session/message-v2.ts +1136 -0
- package/src/session/message.ts +191 -0
- package/src/session/overflow.ts +53 -0
- package/src/session/prefix-capture-ref.ts +48 -0
- package/src/session/processor.ts +962 -0
- package/src/session/projectors.ts +137 -0
- package/src/session/prompt/anthropic.txt +154 -0
- package/src/session/prompt/beast.txt +155 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex.txt +79 -0
- package/src/session/prompt/compose.txt +115 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/default.txt +151 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/gpt.txt +107 -0
- package/src/session/prompt/kimi.txt +95 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/trinity.txt +97 -0
- package/src/session/prompt.ts +3355 -0
- package/src/session/prune.ts +481 -0
- package/src/session/retry.ts +166 -0
- package/src/session/revert.ts +161 -0
- package/src/session/run-state.ts +135 -0
- package/src/session/schema.ts +36 -0
- package/src/session/session.sql.ts +110 -0
- package/src/session/session.ts +908 -0
- package/src/session/status.ts +89 -0
- package/src/session/summary.ts +163 -0
- package/src/session/system.ts +86 -0
- package/src/session/todo.ts +77 -0
- package/src/share/index.ts +2 -0
- package/src/share/session.ts +57 -0
- package/src/share/share-next.ts +381 -0
- package/src/share/share.sql.ts +13 -0
- package/src/shell/shell.ts +110 -0
- package/src/skill/compose/.bundle/ask/SKILL.md +58 -0
- package/src/skill/compose/.bundle/brainstorm/SKILL.md +220 -0
- package/src/skill/compose/.bundle/brainstorm/scripts/frame-template.html +214 -0
- package/src/skill/compose/.bundle/brainstorm/scripts/helper.js +88 -0
- package/src/skill/compose/.bundle/brainstorm/scripts/server.cjs +354 -0
- package/src/skill/compose/.bundle/brainstorm/scripts/start-server.sh +148 -0
- package/src/skill/compose/.bundle/brainstorm/scripts/stop-server.sh +56 -0
- package/src/skill/compose/.bundle/brainstorm/spec-document-reviewer-prompt.md +50 -0
- package/src/skill/compose/.bundle/brainstorm/visual-companion.md +287 -0
- package/src/skill/compose/.bundle/debug/CREATION-LOG.md +119 -0
- package/src/skill/compose/.bundle/debug/SKILL.md +297 -0
- package/src/skill/compose/.bundle/debug/condition-based-waiting-example.ts +158 -0
- package/src/skill/compose/.bundle/debug/condition-based-waiting.md +115 -0
- package/src/skill/compose/.bundle/debug/defense-in-depth.md +122 -0
- package/src/skill/compose/.bundle/debug/find-polluter.sh +63 -0
- package/src/skill/compose/.bundle/debug/root-cause-tracing.md +169 -0
- package/src/skill/compose/.bundle/debug/test-academic.md +14 -0
- package/src/skill/compose/.bundle/debug/test-pressure-1.md +58 -0
- package/src/skill/compose/.bundle/debug/test-pressure-2.md +68 -0
- package/src/skill/compose/.bundle/debug/test-pressure-3.md +69 -0
- package/src/skill/compose/.bundle/execute/SKILL.md +71 -0
- package/src/skill/compose/.bundle/feedback/SKILL.md +214 -0
- package/src/skill/compose/.bundle/merge/SKILL.md +252 -0
- package/src/skill/compose/.bundle/new-skill/SKILL.md +656 -0
- package/src/skill/compose/.bundle/new-skill/anthropic-best-practices.md +1150 -0
- package/src/skill/compose/.bundle/new-skill/examples/CLAUDE_MD_TESTING.md +189 -0
- package/src/skill/compose/.bundle/new-skill/graphviz-conventions.dot +172 -0
- package/src/skill/compose/.bundle/new-skill/persuasion-principles.md +187 -0
- package/src/skill/compose/.bundle/new-skill/render-graphs.js +168 -0
- package/src/skill/compose/.bundle/new-skill/testing-skills-with-subagents.md +384 -0
- package/src/skill/compose/.bundle/parallel/SKILL.md +182 -0
- package/src/skill/compose/.bundle/plan/SKILL.md +161 -0
- package/src/skill/compose/.bundle/plan/plan-document-reviewer-prompt.md +50 -0
- package/src/skill/compose/.bundle/report/SKILL.md +180 -0
- package/src/skill/compose/.bundle/review/SKILL.md +104 -0
- package/src/skill/compose/.bundle/review/code-reviewer.md +171 -0
- package/src/skill/compose/.bundle/subagent/SKILL.md +344 -0
- package/src/skill/compose/.bundle/subagent/code-quality-reviewer-prompt.md +24 -0
- package/src/skill/compose/.bundle/subagent/implementer-prompt.md +126 -0
- package/src/skill/compose/.bundle/subagent/spec-reviewer-prompt.md +112 -0
- package/src/skill/compose/.bundle/tdd/SKILL.md +372 -0
- package/src/skill/compose/.bundle/tdd/testing-anti-patterns.md +299 -0
- package/src/skill/compose/.bundle/verify/SKILL.md +140 -0
- package/src/skill/compose/.bundle/worktree/SKILL.md +234 -0
- package/src/skill/compose/LICENSE-karpathy +28 -0
- package/src/skill/compose/LICENSE-superpowers +26 -0
- package/src/skill/compose/bundle.macro.ts +30 -0
- package/src/skill/compose/extract.ts +85 -0
- package/src/skill/discovery.ts +116 -0
- package/src/skill/index.ts +311 -0
- package/src/snapshot/index.ts +777 -0
- package/src/sql.d.ts +4 -0
- package/src/storage/db.bun.ts +8 -0
- package/src/storage/db.node.ts +8 -0
- package/src/storage/db.ts +172 -0
- package/src/storage/index.ts +26 -0
- package/src/storage/json-migration.ts +426 -0
- package/src/storage/schema.sql.ts +10 -0
- package/src/storage/schema.ts +7 -0
- package/src/storage/storage.ts +331 -0
- package/src/sync/README.md +179 -0
- package/src/sync/event.sql.ts +16 -0
- package/src/sync/index.ts +278 -0
- package/src/sync/schema.ts +14 -0
- package/src/task/events.ts +28 -0
- package/src/task/gate-state.ts +54 -0
- package/src/task/gate.ts +116 -0
- package/src/task/index.ts +1 -0
- package/src/task/registry.ts +387 -0
- package/src/task/schema.ts +43 -0
- package/src/task/task.sql.ts +50 -0
- package/src/team/events.ts +22 -0
- package/src/team/index.ts +113 -0
- package/src/team/schema.ts +31 -0
- package/src/temporary.ts +33 -0
- package/src/tool/actor.shell.txt +72 -0
- package/src/tool/actor.ts +803 -0
- package/src/tool/actor.txt +103 -0
- package/src/tool/apply_patch.ts +308 -0
- package/src/tool/apply_patch.txt +33 -0
- package/src/tool/bash-interactive.ts +183 -0
- package/src/tool/bash.ts +696 -0
- package/src/tool/bash.txt +123 -0
- package/src/tool/change-directory.ts +91 -0
- package/src/tool/codesearch.ts +63 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +685 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/external-directory.ts +132 -0
- package/src/tool/glob.ts +100 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +145 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/history.ts +146 -0
- package/src/tool/history.txt +17 -0
- package/src/tool/index.ts +4 -0
- package/src/tool/invalid.ts +20 -0
- package/src/tool/invocation-style.ts +17 -0
- package/src/tool/lsp.ts +91 -0
- package/src/tool/lsp.txt +19 -0
- package/src/tool/mcp-exa.ts +78 -0
- package/src/tool/memory-path-guard.ts +162 -0
- package/src/tool/memory.ts +81 -0
- package/src/tool/memory.txt +69 -0
- package/src/tool/multiedit.ts +61 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/plan-enter.txt +14 -0
- package/src/tool/plan-exit.txt +13 -0
- package/src/tool/plan.ts +90 -0
- package/src/tool/question.ts +67 -0
- package/src/tool/question.txt +10 -0
- package/src/tool/read.ts +327 -0
- package/src/tool/read.txt +14 -0
- package/src/tool/registry.ts +413 -0
- package/src/tool/schema.ts +17 -0
- package/src/tool/session-cwd.ts +35 -0
- package/src/tool/shell-tokenize.ts +346 -0
- package/src/tool/shell-wrap.ts +190 -0
- package/src/tool/skill.ts +76 -0
- package/src/tool/skill.txt +5 -0
- package/src/tool/task.shell.txt +57 -0
- package/src/tool/task.ts +456 -0
- package/src/tool/task.txt +56 -0
- package/src/tool/tool.ts +153 -0
- package/src/tool/truncate.ts +201 -0
- package/src/tool/truncation-dir.ts +4 -0
- package/src/tool/webfetch.ts +199 -0
- package/src/tool/webfetch.txt +13 -0
- package/src/tool/websearch/index.ts +104 -0
- package/src/tool/websearch/mimo.ts +118 -0
- package/src/tool/websearch/websearch.txt +14 -0
- package/src/tool/workflow.ts +164 -0
- package/src/tool/workflow.txt +25 -0
- package/src/tool/write.ts +88 -0
- package/src/tool/write.txt +9 -0
- package/src/util/abort.ts +35 -0
- package/src/util/archive.ts +15 -0
- package/src/util/color.ts +17 -0
- package/src/util/data-url.ts +9 -0
- package/src/util/defer.ts +10 -0
- package/src/util/effect-http-client.ts +11 -0
- package/src/util/effect-zod.ts +367 -0
- package/src/util/error.ts +78 -0
- package/src/util/filesystem.ts +243 -0
- package/src/util/fn.ts +21 -0
- package/src/util/format.ts +20 -0
- package/src/util/iife.ts +3 -0
- package/src/util/index.ts +12 -0
- package/src/util/keybind.ts +101 -0
- package/src/util/lazy.ts +18 -0
- package/src/util/local-context.ts +23 -0
- package/src/util/locale.ts +79 -0
- package/src/util/lock.ts +96 -0
- package/src/util/log.ts +197 -0
- package/src/util/media.ts +26 -0
- package/src/util/mimo-process.ts +24 -0
- package/src/util/network.ts +9 -0
- package/src/util/process.ts +174 -0
- package/src/util/queue.ts +32 -0
- package/src/util/record.ts +3 -0
- package/src/util/rpc.ts +64 -0
- package/src/util/schema.ts +53 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +5 -0
- package/src/util/update-schema.ts +13 -0
- package/src/util/which.ts +14 -0
- package/src/util/wildcard.ts +57 -0
- package/src/workflow/builtin/deep-research.js +391 -0
- package/src/workflow/builtin.ts +54 -0
- package/src/workflow/events.ts +72 -0
- package/src/workflow/meta.ts +335 -0
- package/src/workflow/persistence.ts +312 -0
- package/src/workflow/resolve.ts +45 -0
- package/src/workflow/runtime-ref.ts +18 -0
- package/src/workflow/runtime.ts +1234 -0
- package/src/workflow/sandbox.ts +280 -0
- package/src/workflow/workflow.sql.ts +31 -0
- package/src/workflow/workspace.ts +69 -0
- package/src/worktree/index.ts +614 -0
- package/sst-env.d.ts +10 -0
- package/test/AGENTS.md +133 -0
- package/test/account/repo.test.ts +352 -0
- package/test/account/service.test.ts +456 -0
- package/test/acp/agent-interface.test.ts +51 -0
- package/test/acp/event-subscription.test.ts +725 -0
- package/test/actor/cancel-cascade.test.ts +432 -0
- package/test/actor/no-completion-listener.test.ts +41 -0
- package/test/actor/poststop-progress-write-permission.repro.test.ts +414 -0
- package/test/actor/registry-render.test.ts +113 -0
- package/test/actor/registry-status.test.ts +111 -0
- package/test/actor/registry.test.ts +619 -0
- package/test/actor/return-header.test.ts +40 -0
- package/test/actor/spawn-lifecycle.test.ts +346 -0
- package/test/actor/spawn-no-deadlock.test.ts +340 -0
- package/test/actor/spawn-notification.test.ts +393 -0
- package/test/actor/spawn-task-autostart.test.ts +530 -0
- package/test/actor/spawn.test.ts +1072 -0
- package/test/actor/status-event-payload.test.ts +132 -0
- package/test/actor/terminology.test.ts +39 -0
- package/test/actor/turn.test.ts +125 -0
- package/test/actor/waiter.test.ts +246 -0
- package/test/agent/agent.test.ts +874 -0
- package/test/agent/allowlist.test.ts +45 -0
- package/test/auth/auth.test.ts +86 -0
- package/test/bus/bus-effect.test.ts +162 -0
- package/test/bus/bus-integration.test.ts +87 -0
- package/test/bus/bus.test.ts +219 -0
- package/test/cli/account.test.ts +26 -0
- package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
- package/test/cli/error.test.ts +18 -0
- package/test/cli/github-action.test.ts +198 -0
- package/test/cli/github-remote.test.ts +80 -0
- package/test/cli/import.test.ts +54 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/cli/run-completion.test.ts +131 -0
- package/test/cli/tui/keybind-plugin.test.ts +90 -0
- package/test/cli/tui/plugin-add.test.ts +111 -0
- package/test/cli/tui/plugin-install.test.ts +87 -0
- package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
- package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
- package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
- package/test/cli/tui/plugin-loader.test.ts +816 -0
- package/test/cli/tui/plugin-toggle.test.ts +157 -0
- package/test/cli/tui/revert-diff.test.ts +35 -0
- package/test/cli/tui/route-agent-id.test.ts +26 -0
- package/test/cli/tui/sidebar-tps.test.ts +63 -0
- package/test/cli/tui/slot-replace.test.tsx +47 -0
- package/test/cli/tui/sync-bucket.test.ts +29 -0
- package/test/cli/tui/theme-store.test.ts +51 -0
- package/test/cli/tui/thread.test.ts +121 -0
- package/test/cli/tui/transcript.test.ts +426 -0
- package/test/cli/tui/use-event.test.tsx +175 -0
- package/test/cli/tui/voice.test.ts +269 -0
- package/test/command/deep-research-command.test.ts +16 -0
- package/test/config/agent-color.test.ts +77 -0
- package/test/config/checkpoint-fork.test.ts +21 -0
- package/test/config/config.test.ts +2577 -0
- package/test/config/fixtures/empty-frontmatter.md +4 -0
- package/test/config/fixtures/frontmatter.md +28 -0
- package/test/config/fixtures/markdown-header.md +11 -0
- package/test/config/fixtures/no-frontmatter.md +1 -0
- package/test/config/fixtures/weird-model-id.md +13 -0
- package/test/config/lsp.test.ts +87 -0
- package/test/config/markdown.test.ts +228 -0
- package/test/config/plugin.test.ts +0 -0
- package/test/config/tui.test.ts +627 -0
- package/test/control-plane/adaptors.test.ts +71 -0
- package/test/control-plane/sse.test.ts +56 -0
- package/test/effect/app-runtime-logger.test.ts +92 -0
- package/test/effect/cross-spawn-spawner.test.ts +411 -0
- package/test/effect/instance-state.test.ts +482 -0
- package/test/effect/observability.test.ts +46 -0
- package/test/effect/run-service.test.ts +46 -0
- package/test/effect/runner-warn-log.test.ts +111 -0
- package/test/effect/runner.test.ts +494 -0
- package/test/fake/provider.ts +90 -0
- package/test/file/fsmonitor.test.ts +68 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/file/index.test.ts +956 -0
- package/test/file/path-traversal.test.ts +204 -0
- package/test/file/ripgrep.test.ts +214 -0
- package/test/file/watcher.test.ts +249 -0
- package/test/filesystem/filesystem.test.ts +319 -0
- package/test/fixture/db.ts +11 -0
- package/test/fixture/fixture.test.ts +58 -0
- package/test/fixture/fixture.ts +190 -0
- package/test/fixture/flock-worker.ts +72 -0
- package/test/fixture/lsp/fake-lsp-server.js +75 -0
- package/test/fixture/plug-worker.ts +93 -0
- package/test/fixture/plugin-meta-worker.ts +19 -0
- package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
- package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
- package/test/fixture/skills/cloudflare/SKILL.md +211 -0
- package/test/fixture/skills/index.json +6 -0
- package/test/fixture/tui-plugin.ts +328 -0
- package/test/fixture/tui-runtime.ts +31 -0
- package/test/format/format.test.ts +244 -0
- package/test/git/git.test.ts +128 -0
- package/test/global/fixture/global-paths-worker.ts +17 -0
- package/test/global/mimocode-home.test.ts +143 -0
- package/test/history/backfill.test.ts +149 -0
- package/test/history/extract.test.ts +106 -0
- package/test/history/fts-query.test.ts +30 -0
- package/test/history/resolve.test.ts +130 -0
- package/test/history/service.test.ts +210 -0
- package/test/history/writer.test.ts +163 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/inbox/drain-in-loop.test.ts +230 -0
- package/test/inbox/fork-agent-compat.test.ts +387 -0
- package/test/inbox/gc-on-init.test.ts +167 -0
- package/test/inbox/send-no-block.test.ts +120 -0
- package/test/inbox/sender-cancel-independence.test.ts +160 -0
- package/test/inbox/wake-matrix.test.ts +141 -0
- package/test/installation/installation.test.ts +226 -0
- package/test/keybind.test.ts +421 -0
- package/test/lib/effect.ts +53 -0
- package/test/lib/filesystem.ts +10 -0
- package/test/lib/llm-server.ts +770 -0
- package/test/lib/scripted-llm-server.ts +245 -0
- package/test/lsp/client.test.ts +98 -0
- package/test/lsp/index.test.ts +109 -0
- package/test/lsp/launch.test.ts +22 -0
- package/test/lsp/lifecycle.test.ts +184 -0
- package/test/mcp/headers.test.ts +178 -0
- package/test/mcp/lifecycle.test.ts +824 -0
- package/test/mcp/oauth-auto-connect.test.ts +281 -0
- package/test/mcp/oauth-browser.test.ts +268 -0
- package/test/mcp/oauth-callback.test.ts +34 -0
- package/test/memory/abort-leak-webfetch.ts +49 -0
- package/test/memory/abort-leak.test.ts +127 -0
- package/test/memory/cc-frontmatter.test.ts +85 -0
- package/test/memory/cc-paths.test.ts +60 -0
- package/test/memory/cc-reconcile.test.ts +239 -0
- package/test/memory/cc-search.test.ts +151 -0
- package/test/memory/fts-query.test.ts +48 -0
- package/test/memory/fts-rowid-stability.test.ts +271 -0
- package/test/memory/paths.test.ts +210 -0
- package/test/memory/reconcile.test.ts +115 -0
- package/test/memory/service.test.ts +169 -0
- package/test/npm.test.ts +18 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/permission/abort.test.ts +116 -0
- package/test/permission/arity.test.ts +33 -0
- package/test/permission/disabled.test.ts +51 -0
- package/test/permission/next.test.ts +1080 -0
- package/test/permission/non-interactive.test.ts +55 -0
- package/test/permission-task.test.ts +326 -0
- package/test/plugin/actor-hooks.test.ts +1471 -0
- package/test/plugin/auth-override.test.ts +79 -0
- package/test/plugin/checkpoint-splitover.test.ts +434 -0
- package/test/plugin/cloudflare.test.ts +68 -0
- package/test/plugin/codex.test.ts +123 -0
- package/test/plugin/github-copilot-models.test.ts +163 -0
- package/test/plugin/install-concurrency.test.ts +140 -0
- package/test/plugin/install.test.ts +570 -0
- package/test/plugin/loader-shared.test.ts +1169 -0
- package/test/plugin/matcher.test.ts +97 -0
- package/test/plugin/meta.test.ts +137 -0
- package/test/plugin/mimo.test.ts +257 -0
- package/test/plugin/shared.test.ts +88 -0
- package/test/plugin/subagent-progress-checker.test.ts +227 -0
- package/test/plugin/trigger.test.ts +116 -0
- package/test/plugin/workspace-adaptor.test.ts +109 -0
- package/test/preload.ts +102 -0
- package/test/project/migrate-global.test.ts +150 -0
- package/test/project/project-id.test.ts +64 -0
- package/test/project/project.test.ts +481 -0
- package/test/project/vcs.test.ts +286 -0
- package/test/project/worktree-remove.test.ts +126 -0
- package/test/project/worktree.test.ts +214 -0
- package/test/provider/amazon-bedrock.test.ts +462 -0
- package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
- package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
- package/test/provider/error.test.ts +160 -0
- package/test/provider/gitlab-duo.test.ts +413 -0
- package/test/provider/model-groups.test.ts +389 -0
- package/test/provider/provider-chunk-timeout.test.ts +23 -0
- package/test/provider/provider.test.ts +2648 -0
- package/test/provider/transform.test.ts +3379 -0
- package/test/pty/pty-output-isolation.test.ts +146 -0
- package/test/pty/pty-session.test.ts +102 -0
- package/test/pty/pty-shell.test.ts +69 -0
- package/test/question/question.test.ts +464 -0
- package/test/server/global-session-list.test.ts +105 -0
- package/test/server/project-init-git.test.ts +122 -0
- package/test/server/session-actions.test.ts +49 -0
- package/test/server/session-list.test.ts +110 -0
- package/test/server/session-messages.test.ts +220 -0
- package/test/server/session-prompt-busy.test.ts +146 -0
- package/test/server/session-select.test.ts +100 -0
- package/test/server/session-task-route.test.ts +165 -0
- package/test/server/summarize-route-main-slice.test.ts +99 -0
- package/test/server/trace-attributes.test.ts +76 -0
- package/test/server/workflows-route.test.ts +279 -0
- package/test/session/bootstrap-skip-system.test.ts +121 -0
- package/test/session/boundary.test.ts +33 -0
- package/test/session/budgeted-read.test.ts +74 -0
- package/test/session/checkpoint-align.test.ts +58 -0
- package/test/session/checkpoint-boundary.test.ts +186 -0
- package/test/session/checkpoint-child-session.test.ts +508 -0
- package/test/session/checkpoint-context.test.ts +141 -0
- package/test/session/checkpoint-drain.test.ts +188 -0
- package/test/session/checkpoint-extract-titles.test.ts +58 -0
- package/test/session/checkpoint-fork-mode.test.ts +576 -0
- package/test/session/checkpoint-main-slice.test.ts +259 -0
- package/test/session/checkpoint-paths.test.ts +78 -0
- package/test/session/checkpoint-permission.test.ts +136 -0
- package/test/session/checkpoint-progress-reconcile.test.ts +219 -0
- package/test/session/checkpoint-rebuild-unify.test.ts +143 -0
- package/test/session/checkpoint-rebuild-v3.test.ts +248 -0
- package/test/session/checkpoint-render-verify.test.ts +512 -0
- package/test/session/checkpoint-retry.test.ts +150 -0
- package/test/session/checkpoint-splitover-integration.test.ts +533 -0
- package/test/session/checkpoint-templates.test.ts +51 -0
- package/test/session/checkpoint-thresholds.test.ts +120 -0
- package/test/session/checkpoint-validator.test.ts +189 -0
- package/test/session/classify-integration.test.ts +476 -0
- package/test/session/classify.test.ts +335 -0
- package/test/session/compaction-agent-scope.test.ts +164 -0
- package/test/session/context-inheritance.test.ts +46 -0
- package/test/session/fork-prefix-invariant.test.ts +116 -0
- package/test/session/goal.test.ts +106 -0
- package/test/session/instruction.test.ts +387 -0
- package/test/session/invalid-output-continuation.test.ts +150 -0
- package/test/session/last-message-info.test.ts +47 -0
- package/test/session/length-tool-safety.test.ts +121 -0
- package/test/session/llm-request-prefix.test.ts +197 -0
- package/test/session/llm-retry.test.ts +59 -0
- package/test/session/llm-system-prompt.test.ts +479 -0
- package/test/session/llm.test.ts +1272 -0
- package/test/session/main-lifecycle.test.ts +51 -0
- package/test/session/main-runloop-history-invariant.test.ts +182 -0
- package/test/session/max-mode-econnreset.test.ts +229 -0
- package/test/session/max-mode.test.ts +54 -0
- package/test/session/message-v2-filter.test.ts +197 -0
- package/test/session/message-v2.test.ts +1119 -0
- package/test/session/messages-default-main.test.ts +105 -0
- package/test/session/messages-pagination.test.ts +888 -0
- package/test/session/overflow.test.ts +576 -0
- package/test/session/processor-effect.test.ts +853 -0
- package/test/session/prompt-effect.test.ts +1574 -0
- package/test/session/prompt-rebuild-loop.test.ts +108 -0
- package/test/session/prompt-rebuild-reset.test.ts +67 -0
- package/test/session/prompt-sweep.test.ts +145 -0
- package/test/session/prompt-task-gate.test.ts +127 -0
- package/test/session/prompt.test.ts +703 -0
- package/test/session/prune-main-slice.test.ts +272 -0
- package/test/session/prune-skip-system.test.ts +346 -0
- package/test/session/prune.test.ts +419 -0
- package/test/session/rebuild-microcompact.test.ts +318 -0
- package/test/session/recall-reminder.test.ts +37 -0
- package/test/session/retry.test.ts +410 -0
- package/test/session/revert-compact.test.ts +639 -0
- package/test/session/run-state-tuple-key.test.ts +152 -0
- package/test/session/session-create-registers-main.test.ts +70 -0
- package/test/session/session.test.ts +181 -0
- package/test/session/snapshot-tool-race.test.ts +301 -0
- package/test/session/structured-output-integration.test.ts +264 -0
- package/test/session/structured-output-retry.test.ts +127 -0
- package/test/session/structured-output.test.ts +397 -0
- package/test/session/summary-main-slice.test.ts +170 -0
- package/test/session/system.test.ts +72 -0
- package/test/share/share-next.test.ts +332 -0
- package/test/shell/shell.test.ts +73 -0
- package/test/skill/compose-review.test.ts +141 -0
- package/test/skill/discovery.test.ts +116 -0
- package/test/skill/skill.test.ts +465 -0
- package/test/snapshot/snapshot.test.ts +1531 -0
- package/test/storage/db.test.ts +16 -0
- package/test/storage/json-migration.test.ts +831 -0
- package/test/storage/storage.test.ts +293 -0
- package/test/sync/index.test.ts +237 -0
- package/test/task/gate-state.test.ts +66 -0
- package/test/task/gate.test.ts +167 -0
- package/test/task/registry.test.ts +152 -0
- package/test/task/state-machine.test.ts +292 -0
- package/test/team/migrate-to-inbox.test.ts +124 -0
- package/test/team/team.test.ts +75 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/actor-cancel.test.ts +206 -0
- package/test/tool/actor-recover.test.ts +50 -0
- package/test/tool/actor-send.test.ts +200 -0
- package/test/tool/actor-status.test.ts +296 -0
- package/test/tool/actor-wait.test.ts +193 -0
- package/test/tool/actor.shell.test.ts +250 -0
- package/test/tool/actor.test.ts +748 -0
- package/test/tool/apply_patch.test.ts +626 -0
- package/test/tool/bash.test.ts +1195 -0
- package/test/tool/describe-workflow.test.ts +12 -0
- package/test/tool/edit.test.ts +691 -0
- package/test/tool/external-directory.test.ts +207 -0
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +65179 -0
- package/test/tool/glob.test.ts +81 -0
- package/test/tool/grep.test.ts +114 -0
- package/test/tool/history.test.ts +144 -0
- package/test/tool/invocation-style.test.ts +30 -0
- package/test/tool/memory-edit-ask-skip.test.ts +62 -0
- package/test/tool/memory-path-guard.test.ts +594 -0
- package/test/tool/memory.test.ts +71 -0
- package/test/tool/question.test.ts +167 -0
- package/test/tool/read.test.ts +483 -0
- package/test/tool/registry-invocation-style.test.ts +121 -0
- package/test/tool/registry.test.ts +164 -0
- package/test/tool/shell-tokenize.test.ts +273 -0
- package/test/tool/shell-wrap-missing-script.test.ts +128 -0
- package/test/tool/shell-wrap.test.ts +257 -0
- package/test/tool/skill.test.ts +99 -0
- package/test/tool/task-recover.test.ts +36 -0
- package/test/tool/task.shell.test.ts +234 -0
- package/test/tool/task.test.ts +296 -0
- package/test/tool/tool-def-shell-shape.test.ts +23 -0
- package/test/tool/tool-define.test.ts +59 -0
- package/test/tool/truncation.test.ts +253 -0
- package/test/tool/webfetch.test.ts +103 -0
- package/test/tool/whitelist.test.ts +373 -0
- package/test/tool/write.test.ts +244 -0
- package/test/util/data-url.test.ts +14 -0
- package/test/util/effect-zod.test.ts +869 -0
- package/test/util/error.test.ts +38 -0
- package/test/util/filesystem.test.ts +656 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/glob.test.ts +164 -0
- package/test/util/iife.test.ts +36 -0
- package/test/util/lazy.test.ts +50 -0
- package/test/util/lock.test.ts +72 -0
- package/test/util/log.test.ts +44 -0
- package/test/util/module.test.ts +59 -0
- package/test/util/process.test.ts +128 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/which.test.ts +100 -0
- package/test/util/wildcard.test.ts +90 -0
- package/test/workflow/builtin.test.ts +22 -0
- package/test/workflow/deep-research-cluster.test.ts +47 -0
- package/test/workflow/lib.ts +243 -0
- package/test/workflow/meta.test.ts +142 -0
- package/test/workflow/model-routing.test.ts +68 -0
- package/test/workflow/persistence.test.ts +229 -0
- package/test/workflow/resolve.test.ts +37 -0
- package/test/workflow/runtime-nested.test.ts +419 -0
- package/test/workflow/runtime-worktree.test.ts +261 -0
- package/test/workflow/runtime.test.ts +1078 -0
- package/test/workflow/sandbox.test.ts +259 -0
- package/test/workflow/tool.test.ts +473 -0
- package/test/workflow/verify-wow.test.ts +144 -0
- package/test/workflow/workspace.test.ts +88 -0
- package/test/workspace/workspace-restore.test.ts +281 -0
- package/test/worktree/index.test.ts +30 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,1478 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import { Global } from "@/global"
|
|
4
|
+
import { Bus } from "@/bus"
|
|
5
|
+
import { Config } from "@/config"
|
|
6
|
+
import { Memory } from "@/memory"
|
|
7
|
+
import { MemoryFtsTable } from "@/memory/fts.sql"
|
|
8
|
+
import { TaskRegistry } from "@/task/registry"
|
|
9
|
+
import { ActorRegistry } from "@/actor/registry"
|
|
10
|
+
import type { AgentOutcome, ForkContext } from "@/actor/spawn"
|
|
11
|
+
import { spawnRef } from "@/actor/spawn-ref"
|
|
12
|
+
import { prefixCaptureRef } from "./prefix-capture-ref"
|
|
13
|
+
import { Database, and, eq, or } from "@/storage"
|
|
14
|
+
import { Instance } from "@/project/instance"
|
|
15
|
+
import { ProjectID } from "@/project/schema"
|
|
16
|
+
import { SessionTable } from "./session.sql"
|
|
17
|
+
import * as Session from "./session"
|
|
18
|
+
import { MessageV2 } from "./message-v2"
|
|
19
|
+
import { SessionID, MessageID, PartID } from "./schema"
|
|
20
|
+
import { Log, Token } from "../util"
|
|
21
|
+
import { Effect, Layer, Deferred, Context, Scope } from "effect"
|
|
22
|
+
import { makeRuntime } from "@/effect/run-service"
|
|
23
|
+
import type { ActorPromptOps } from "@/tool/actor"
|
|
24
|
+
import type { ProviderID, ModelID } from "../provider/schema"
|
|
25
|
+
import PROMPT_CHECKPOINT_WRITER from "@/agent/prompt/checkpoint-writer.txt"
|
|
26
|
+
import { WriterCachePerf } from "@/actor/events"
|
|
27
|
+
import {
|
|
28
|
+
metaDir,
|
|
29
|
+
checkpointPath,
|
|
30
|
+
memoryPath,
|
|
31
|
+
notesPath,
|
|
32
|
+
globalMemoryPath,
|
|
33
|
+
migrateProjectMemory,
|
|
34
|
+
} from "./checkpoint-paths"
|
|
35
|
+
import { readBudgeted, readBudgetedSectionAware } from "./budgeted-read"
|
|
36
|
+
import type { LastMessageInfo } from "./last-message-info"
|
|
37
|
+
import { CHECKPOINT_TEMPLATE, MEMORY_TEMPLATE, NOTES_TEMPLATE, CHECKPOINT_SECTION_BUDGETS } from "./checkpoint-templates"
|
|
38
|
+
import { adjustBoundaryForApiInvariants } from "./boundary"
|
|
39
|
+
import { alignToNonToolResultUser } from "./checkpoint-align"
|
|
40
|
+
import { loadPriorDiscoveredTitles } from "./checkpoint-retry"
|
|
41
|
+
import * as CheckpointContext from "./checkpoint-context"
|
|
42
|
+
import { buildProgressDiff } from "./checkpoint-progress-reconcile"
|
|
43
|
+
|
|
44
|
+
const log = Log.create({ service: "session.checkpoint" })
|
|
45
|
+
|
|
46
|
+
function truncate(s: string, max: number): string {
|
|
47
|
+
return s.length <= max ? s : s.slice(0, max - 60) + "\n... (truncated, full body at file)"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function autonomousLoopReminder(): string {
|
|
51
|
+
return [
|
|
52
|
+
"<system-reminder>",
|
|
53
|
+
"You are mid-loop in an autonomous task. Continue your work loop:",
|
|
54
|
+
"respond to the tool results below and proceed to the next iteration.",
|
|
55
|
+
"</system-reminder>",
|
|
56
|
+
].join("\n")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function stopReminder(focusTaskID: string | undefined): string {
|
|
60
|
+
const taskHint = focusTaskID
|
|
61
|
+
? `Consult this session's tasks/${focusTaskID}/progress.md head section.`
|
|
62
|
+
: "Consult the most recently active task's progress.md head section."
|
|
63
|
+
return [
|
|
64
|
+
"<system-reminder>",
|
|
65
|
+
"The previous assistant turn ended with a stop. Before stopping again,",
|
|
66
|
+
taskHint,
|
|
67
|
+
"Compare the Task spec to the latest Progress entries. If the task is",
|
|
68
|
+
"incomplete, proceed to the next concrete step. Only stop when the spec",
|
|
69
|
+
"is genuinely satisfied or you need user input you cannot infer.",
|
|
70
|
+
"</system-reminder>",
|
|
71
|
+
].join("\n")
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toolResultContinueReminder(): string {
|
|
75
|
+
return [
|
|
76
|
+
"<system-reminder>",
|
|
77
|
+
"Tool results above are real history from the autonomous loop. Process",
|
|
78
|
+
"them and continue to the next iteration. Do not pause to summarize.",
|
|
79
|
+
"</system-reminder>",
|
|
80
|
+
].join("\n")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function ensureCheckpointTemplate(checkpointFile: string): Promise<void> {
|
|
84
|
+
if (!(await Bun.file(checkpointFile).exists())) {
|
|
85
|
+
await fs.mkdir(path.dirname(checkpointFile), { recursive: true })
|
|
86
|
+
await Bun.write(checkpointFile, CHECKPOINT_TEMPLATE)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function ensureMemoryTemplate(memoryFile: string): Promise<void> {
|
|
91
|
+
if (!(await Bun.file(memoryFile).exists())) {
|
|
92
|
+
await fs.mkdir(path.dirname(memoryFile), { recursive: true })
|
|
93
|
+
await Bun.write(memoryFile, MEMORY_TEMPLATE)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function ensureNotesTemplate(notesFile: string): Promise<void> {
|
|
98
|
+
if (!(await Bun.file(notesFile).exists())) {
|
|
99
|
+
await fs.mkdir(path.dirname(notesFile), { recursive: true })
|
|
100
|
+
await Bun.write(notesFile, NOTES_TEMPLATE)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Tail preservation budget (token-budgeted boundary).
|
|
105
|
+
// Session-memory compact: minimum guarantees the LLM has enough
|
|
106
|
+
// recent-context anchor (avoids the agent-Read-loop failure mode from
|
|
107
|
+
// v4 → v5 spec rationale); maximum is a SOFT ceiling on backward
|
|
108
|
+
// expansion — i.e. when the natural tail is below the floor we expand
|
|
109
|
+
// backward UP TO maxTokens, but if the natural tail already exceeds
|
|
110
|
+
// maxTokens we leave it alone. Single-message-granularity cap would
|
|
111
|
+
// break tool_use/result pairing.
|
|
112
|
+
//
|
|
113
|
+
// 20K is the empirical sweet spot — observed compact output is ~20K,
|
|
114
|
+
// not the 40K nominal default. The 40K appears in source as fallback,
|
|
115
|
+
// but the upstream config likely tunes it lower in production.
|
|
116
|
+
const TAIL_MIN_TOKENS = 10_000
|
|
117
|
+
const TAIL_MAX_TOKENS = 20_000
|
|
118
|
+
const TAIL_MIN_TEXT_BLOCK_MESSAGES = 5
|
|
119
|
+
|
|
120
|
+
// Rebuild-time microcompact (see
|
|
121
|
+
// docs/superpowers/specs/2026-06-03-rebuild-tail-microcompact-design.md).
|
|
122
|
+
//
|
|
123
|
+
// After computing the boundary, msgs strictly newer than the boundary
|
|
124
|
+
// survive into the rebuild context. Their tool_use parts are kept (so the
|
|
125
|
+
// LLM still sees what action was taken), but for tools in this whitelist
|
|
126
|
+
// the tool_result content is replaced with a placeholder. Result is either
|
|
127
|
+
// large-and-regeneratable (read/bash/grep/glob/webfetch/websearch) or
|
|
128
|
+
// essentially a "done" confirmation (edit/write/multiedit). Tools NOT here
|
|
129
|
+
// carry state the LLM references later (actor/task/question/skill/memory).
|
|
130
|
+
const COMPACTABLE_TOOL_NAMES = new Set<string>([
|
|
131
|
+
"read",
|
|
132
|
+
"bash",
|
|
133
|
+
"grep",
|
|
134
|
+
"glob",
|
|
135
|
+
"webfetch",
|
|
136
|
+
"websearch",
|
|
137
|
+
"edit",
|
|
138
|
+
"write",
|
|
139
|
+
"multiedit",
|
|
140
|
+
"apply_patch",
|
|
141
|
+
"codesearch",
|
|
142
|
+
])
|
|
143
|
+
|
|
144
|
+
function estimateMessageTokens(m: { parts: Array<{ type: string; [k: string]: unknown }> }): number {
|
|
145
|
+
// Same estimator used elsewhere in checkpoint.ts (Token.estimate over JSON).
|
|
146
|
+
// Sum across all parts of the message.
|
|
147
|
+
let sum = 0
|
|
148
|
+
for (const p of m.parts) {
|
|
149
|
+
// JSON.stringify throws on circular structures. Parser-produced parts are
|
|
150
|
+
// plain POJOs, but a plugin-injected part could contain a cycle. Fall back
|
|
151
|
+
// to a conservative NON-ZERO estimate so a bad part is never counted as
|
|
152
|
+
// "free" — a 0 here would let the tail-boundary algorithm swallow the part
|
|
153
|
+
// for nothing and skew the budget. The constant overstates a typical part,
|
|
154
|
+
// which is the safe direction (boundary walks back, never forward).
|
|
155
|
+
try {
|
|
156
|
+
sum += Token.estimate(JSON.stringify(p))
|
|
157
|
+
} catch {
|
|
158
|
+
sum += 1000
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return sum
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hasTextBlocks(m: { parts: Array<{ type: string }> }): boolean {
|
|
165
|
+
return m.parts.some((p) => p.type === "text" || p.type === "reasoning")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Token-budgeted, role-aware boundary choice for the preserved tail.
|
|
170
|
+
*
|
|
171
|
+
* Returns the ID of the FIRST message to preserve (boundary message ID;
|
|
172
|
+
* everything strictly before this ID is summarized into checkpoint.md and
|
|
173
|
+
* discarded from the rebuild context).
|
|
174
|
+
*
|
|
175
|
+
* Algorithm (token-budgeted boundary):
|
|
176
|
+
*
|
|
177
|
+
* 1. Start at the last finished assistant index minus 1, take it+successors
|
|
178
|
+
* as the candidate tail (preserves spec 2 starting point so reasonable
|
|
179
|
+
* tails are unchanged).
|
|
180
|
+
* 2. If tail tokens already >= TAIL_MAX_TOKENS: leave boundary as-is and
|
|
181
|
+
* return. Do NOT pull boundary forward — message-granularity truncation
|
|
182
|
+
* would split tool_use/tool_result pairs (downstream
|
|
183
|
+
* adjustBoundaryForApiInvariants would just walk back, net no-op + risk
|
|
184
|
+
* of thinking-block breaks). The cap is a SOFT ceiling on backward
|
|
185
|
+
* expansion, not a hard upper bound on tail size. If a single
|
|
186
|
+
* assistant turn legitimately produces 60K of tool_result, the tail
|
|
187
|
+
* will be 60K and that's fine.
|
|
188
|
+
* 3. Else if tail tokens < TAIL_MIN_TOKENS or text-block messages < min:
|
|
189
|
+
* walk backward (earlier) one message at a time until both minimums
|
|
190
|
+
* met OR TAIL_MAX_TOKENS hit OR no more messages.
|
|
191
|
+
*
|
|
192
|
+
* The downstream adjustBoundaryForApiInvariants call (in
|
|
193
|
+
* tryStartCheckpointWriter) handles tool_use/tool_result pairing and
|
|
194
|
+
* thinking-block atomicity — this function does NOT need to.
|
|
195
|
+
*
|
|
196
|
+
* Edge cases:
|
|
197
|
+
* - msgs.length === 0: return "" (matches old behavior).
|
|
198
|
+
* - No finished assistant: return msgs[0].info.id (degenerate; caller should
|
|
199
|
+
* not be invoking trim here, but stay safe).
|
|
200
|
+
* - lastAsstIdx === 0: return msgs[0].info.id (degenerate tail).
|
|
201
|
+
*/
|
|
202
|
+
export function computeBoundary(
|
|
203
|
+
msgs: ReadonlyArray<{ info: { id: string; role: "user" | "assistant"; finish?: string }; parts: Array<{ type: string; [k: string]: unknown }> }>,
|
|
204
|
+
): string {
|
|
205
|
+
if (msgs.length === 0) return ""
|
|
206
|
+
const lastAsstIdx = msgs.findLastIndex(
|
|
207
|
+
(m) => m.info.role === "assistant" && m.info.finish !== undefined,
|
|
208
|
+
)
|
|
209
|
+
if (lastAsstIdx <= 0) return msgs[lastAsstIdx >= 0 ? lastAsstIdx : 0].info.id
|
|
210
|
+
|
|
211
|
+
// Token estimate per message (computed once).
|
|
212
|
+
const tokens = msgs.map((m) => estimateMessageTokens(m))
|
|
213
|
+
|
|
214
|
+
// Spec 2 starting point: lastAsstIdx - 1.
|
|
215
|
+
let startIdx = lastAsstIdx - 1
|
|
216
|
+
let tailSum = 0
|
|
217
|
+
let textBlockCount = 0
|
|
218
|
+
for (let i = startIdx; i < msgs.length; i++) {
|
|
219
|
+
tailSum += tokens[i]
|
|
220
|
+
if (hasTextBlocks(msgs[i])) textBlockCount += 1
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Natural tail already >= cap: leave it alone (soft ceiling; do NOT pull
|
|
224
|
+
// boundary forward — see jsdoc rationale).
|
|
225
|
+
if (tailSum >= TAIL_MAX_TOKENS) {
|
|
226
|
+
return msgs[startIdx].info.id
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Tail too small — pull boundary earlier (include more history)
|
|
230
|
+
// until both floors met, capped at TAIL_MAX_TOKENS.
|
|
231
|
+
while (
|
|
232
|
+
startIdx > 0 &&
|
|
233
|
+
tailSum < TAIL_MAX_TOKENS &&
|
|
234
|
+
(tailSum < TAIL_MIN_TOKENS || textBlockCount < TAIL_MIN_TEXT_BLOCK_MESSAGES)
|
|
235
|
+
) {
|
|
236
|
+
startIdx -= 1
|
|
237
|
+
tailSum += tokens[startIdx]
|
|
238
|
+
if (hasTextBlocks(msgs[startIdx])) textBlockCount += 1
|
|
239
|
+
}
|
|
240
|
+
return msgs[startIdx].info.id
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function renderSectionBudgets(budgets: Record<string, number>): string {
|
|
244
|
+
const entries = Object.entries(budgets)
|
|
245
|
+
if (entries.length === 0) {
|
|
246
|
+
throw new Error("CHECKPOINT_SECTION_BUDGETS is empty — F43 substitution would produce an empty prompt block")
|
|
247
|
+
}
|
|
248
|
+
const cols = 3
|
|
249
|
+
const lines: string[] = ["Section budgets (~tokens):"]
|
|
250
|
+
for (let i = 0; i < entries.length; i += cols) {
|
|
251
|
+
const row = entries
|
|
252
|
+
.slice(i, i + cols)
|
|
253
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
254
|
+
.join(" ")
|
|
255
|
+
lines.push(` ${row}`)
|
|
256
|
+
}
|
|
257
|
+
return lines.join("\n")
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Composes the full writer prompt for the checkpoint subagent.
|
|
262
|
+
*
|
|
263
|
+
* The body wraps PROMPT_CHECKPOINT_WRITER with an ABSOLUTE-PATHS preamble
|
|
264
|
+
* that pins CHECKPOINT_PATH/MEMORY_PATH/TASK_MEM_DIR to the current session's
|
|
265
|
+
* dirs — without this, the model frequently invents legacy `/data/checkpoints/`
|
|
266
|
+
* style paths from training-data lookalikes.
|
|
267
|
+
*/
|
|
268
|
+
function composeWriterPrompt(input: {
|
|
269
|
+
checkpointFile: string
|
|
270
|
+
memoryFile: string
|
|
271
|
+
taskMemDir: string
|
|
272
|
+
notesFile: string
|
|
273
|
+
rangeDesc: string
|
|
274
|
+
progressDiff: string // Spec ② Chain 2: empty string when nothing to reconcile
|
|
275
|
+
}): string {
|
|
276
|
+
return [
|
|
277
|
+
"<system-reminder>",
|
|
278
|
+
"You are now operating in checkpoint-writer mode. Ignore the general coding-assistant framing in the system prompt above. The read, write, edit, glob, grep, and task tools are available; do not invoke others.",
|
|
279
|
+
"",
|
|
280
|
+
"========================================================================",
|
|
281
|
+
"ABSOLUTE PATHS — USE THESE VERBATIM. NEVER COMPUTE, INFER, OR MODIFY.",
|
|
282
|
+
"========================================================================",
|
|
283
|
+
"",
|
|
284
|
+
`CHECKPOINT_PATH = ${input.checkpointFile}`,
|
|
285
|
+
`MEMORY_PATH = ${input.memoryFile}`,
|
|
286
|
+
`TASK_MEM_DIR = ${input.taskMemDir}`,
|
|
287
|
+
`NOTES_PATH = ${input.notesFile}`,
|
|
288
|
+
"",
|
|
289
|
+
"When using the Write tool, the first arg MUST be one of these literal",
|
|
290
|
+
"absolute paths (or for task narrative, TASK_MEM_DIR + '/' + task_id +",
|
|
291
|
+
"'/progress.md' or '/notes.md'). Do NOT abbreviate. Do NOT change",
|
|
292
|
+
"parent directories. Do NOT insert paths from memory of similar projects.",
|
|
293
|
+
"If you find yourself typing '/data/checkpoints/' as a parent, STOP — that",
|
|
294
|
+
"is the legacy v2 layout and is wrong. The current parent for the",
|
|
295
|
+
"checkpoint file is the directory portion of CHECKPOINT_PATH above.",
|
|
296
|
+
"========================================================================",
|
|
297
|
+
"",
|
|
298
|
+
input.progressDiff,
|
|
299
|
+
"",
|
|
300
|
+
PROMPT_CHECKPOINT_WRITER.replace("{{SECTION_BUDGETS}}", renderSectionBudgets(CHECKPOINT_SECTION_BUDGETS)),
|
|
301
|
+
"</system-reminder>",
|
|
302
|
+
"",
|
|
303
|
+
`Write the next checkpoint for this session.`,
|
|
304
|
+
"",
|
|
305
|
+
input.rangeDesc,
|
|
306
|
+
"",
|
|
307
|
+
"Use the `task` tool for ALL task state ops (create / start / progress / done / abandon / approve / rename / block / unblock / batch_create). Use the Write tool for the checkpoint, memory, and task narrative files at the CHECKPOINT_PATH / MEMORY_PATH / TASK_MEM_DIR locations declared above. After all writes and tool calls, stop immediately.",
|
|
308
|
+
].join("\n")
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function aggregateWriterCacheMetrics(
|
|
312
|
+
sessions: Session.Interface,
|
|
313
|
+
sessionID: SessionID,
|
|
314
|
+
actorID: string,
|
|
315
|
+
) {
|
|
316
|
+
return Effect.gen(function* () {
|
|
317
|
+
const msgs = yield* sessions.messages({ sessionID, agentID: "*" })
|
|
318
|
+
let totalInput = 0
|
|
319
|
+
let cacheRead = 0
|
|
320
|
+
let cacheWrite = 0
|
|
321
|
+
let assistantCount = 0
|
|
322
|
+
for (const m of msgs) {
|
|
323
|
+
if (m.info.role !== "assistant") continue
|
|
324
|
+
if (m.info.agentID !== actorID) continue
|
|
325
|
+
// Count every assistant LLM call in the slice, even ones without
|
|
326
|
+
// billing-token data (errors / mid-stream interrupts). Token sums
|
|
327
|
+
// skip the no-data rows; the call count includes them so downstream
|
|
328
|
+
// consumers can distinguish "low cache hit" from "few calls".
|
|
329
|
+
assistantCount += 1
|
|
330
|
+
const t = m.info.tokens
|
|
331
|
+
if (!t) continue
|
|
332
|
+
totalInput += (t.input ?? 0) + (t.cache?.read ?? 0) + (t.cache?.write ?? 0)
|
|
333
|
+
cacheRead += t.cache?.read ?? 0
|
|
334
|
+
cacheWrite += t.cache?.write ?? 0
|
|
335
|
+
}
|
|
336
|
+
const billable = totalInput - cacheRead - cacheWrite
|
|
337
|
+
const denom = cacheRead + Math.max(billable, 0)
|
|
338
|
+
const hitRate = denom > 0 ? cacheRead / denom : 0
|
|
339
|
+
return {
|
|
340
|
+
total_input_tokens: totalInput,
|
|
341
|
+
cache_read_tokens: cacheRead,
|
|
342
|
+
cache_write_tokens: cacheWrite,
|
|
343
|
+
cache_hit_rate: hitRate,
|
|
344
|
+
num_llm_calls: assistantCount,
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// Service interface
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
export type TryStartCheckpointWriterInput = {
|
|
354
|
+
sessionID: SessionID
|
|
355
|
+
model: { providerID: string; modelID: string }
|
|
356
|
+
promptOps: ActorPromptOps
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Outcome of a tryStartCheckpointWriter call:
|
|
361
|
+
* - "started": no writer was running for this session, a fresh one was forked.
|
|
362
|
+
* - "queued": a writer is already running. The new request is held in the
|
|
363
|
+
* 1-slot pending queue and will fire once the current writer
|
|
364
|
+
* settles. If a pending request already exists it's evicted —
|
|
365
|
+
* newest wins because its range is a strict superset of the
|
|
366
|
+
* older pending range, so the older one would just duplicate
|
|
367
|
+
* work. (F40)
|
|
368
|
+
* - "skipped": the request was rejected outright — empty session, system-
|
|
369
|
+
* spawned subagent, or Actor service unavailable. No writer
|
|
370
|
+
* will fire for this request now or later.
|
|
371
|
+
*/
|
|
372
|
+
export type TryStartCheckpointWriterResult = "started" | "queued" | "skipped"
|
|
373
|
+
|
|
374
|
+
export interface Interface {
|
|
375
|
+
readonly tryStartCheckpointWriter: (
|
|
376
|
+
input: TryStartCheckpointWriterInput,
|
|
377
|
+
) => Effect.Effect<TryStartCheckpointWriterResult>
|
|
378
|
+
|
|
379
|
+
readonly waitForWriter: (sessionID: SessionID) => Effect.Effect<WriterOutcome | "no-writer">
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Await all in-flight writers across sessions up to `timeoutMs`. Used by
|
|
383
|
+
* the CLI shutdown path so headless `mimo run` invocations don't exit
|
|
384
|
+
* while a forked checkpoint writer is still waiting on its LLM round-trip.
|
|
385
|
+
* Returns the count of writers that completed vs. still pending when the
|
|
386
|
+
* timeout fired.
|
|
387
|
+
*/
|
|
388
|
+
readonly drainWriters: (input?: { timeoutMs?: number }) => Effect.Effect<{
|
|
389
|
+
drained: number
|
|
390
|
+
timedOut: number
|
|
391
|
+
}>
|
|
392
|
+
|
|
393
|
+
readonly hasCheckpoint: (sessionID: SessionID) => Effect.Effect<boolean>
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Returns true when the session has any memory artifacts:
|
|
397
|
+
* either a populated `<data>/memory/sessions/<sid>/` directory, or
|
|
398
|
+
* any tasks recorded in the task registry. Used by the per-user-message
|
|
399
|
+
* recall reminder so it fires whenever there is anything to recall —
|
|
400
|
+
* not only when classic v2 checkpoints exist.
|
|
401
|
+
*/
|
|
402
|
+
readonly hasMemoryOrTasks: (sessionID: SessionID) => Effect.Effect<boolean>
|
|
403
|
+
|
|
404
|
+
/** Returns the content of the latest checkpoint file, or undefined if none exists. */
|
|
405
|
+
readonly loadLatest: (sessionID: SessionID) => Effect.Effect<string | undefined>
|
|
406
|
+
|
|
407
|
+
/** Returns the content of the last N checkpoint files, ordered oldest to newest. */
|
|
408
|
+
readonly loadCheckpoints: (sessionID: SessionID, count: number) => Effect.Effect<string[]>
|
|
409
|
+
|
|
410
|
+
/** Returns a human-readable index overview for injection into rebuild context. */
|
|
411
|
+
readonly renderIndex: (sessionID: SessionID) => Effect.Effect<string>
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Returns the rebuild-time context that should be injected after trim.
|
|
415
|
+
* Format:
|
|
416
|
+
* <system-reminder>Verify-before-act note...</system-reminder>
|
|
417
|
+
* ## Accumulated learnings (chronological)
|
|
418
|
+
* ### From checkpoint #1 (<topic>)
|
|
419
|
+
* <Learning body>
|
|
420
|
+
* ...
|
|
421
|
+
* ## Current snapshot (as of checkpoint #N)
|
|
422
|
+
* <Snapshot body>
|
|
423
|
+
*
|
|
424
|
+
* Stale Snapshots from older checkpoints are intentionally dropped. Returns
|
|
425
|
+
* an empty string if no checkpoints exist. When checkpoints exist but all
|
|
426
|
+
* Learning sections are empty, emits "(no prior learnings)" placeholder;
|
|
427
|
+
* when the latest checkpoint has no Snapshot section, emits
|
|
428
|
+
* "(latest checkpoint has no Snapshot section)" placeholder — the full
|
|
429
|
+
* structure is always produced so the verify-before-act reminder is
|
|
430
|
+
* consistently visible.
|
|
431
|
+
*/
|
|
432
|
+
readonly renderRebuildContext: (
|
|
433
|
+
sessionID: SessionID,
|
|
434
|
+
opts?: { lastMessageInfo?: LastMessageInfo; agentID?: string },
|
|
435
|
+
) => Effect.Effect<string>
|
|
436
|
+
|
|
437
|
+
readonly lastBoundary: (sessionID: SessionID) => Effect.Effect<MessageID | undefined>
|
|
438
|
+
|
|
439
|
+
readonly isWriterRunning: (sessionID: SessionID) => Effect.Effect<boolean>
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Insert a synthetic checkpoint-boundary user message (boundary marker +
|
|
443
|
+
* index overview + rebuild context + active-actors text) just after the
|
|
444
|
+
* given boundary. Inserts nothing and returns false when rebuild context is
|
|
445
|
+
* empty. Never deletes DB messages.
|
|
446
|
+
*/
|
|
447
|
+
readonly insertRebuildBoundary: (input: {
|
|
448
|
+
sessionID: SessionID
|
|
449
|
+
boundary: MessageID
|
|
450
|
+
lastMessageInfo?: LastMessageInfo
|
|
451
|
+
agentID?: string
|
|
452
|
+
agent: string
|
|
453
|
+
model: { providerID: string; modelID: string }
|
|
454
|
+
boundaryCreatedAt?: number
|
|
455
|
+
}) => Effect.Effect<boolean>
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export class Service extends Context.Service<Service, Interface>()("@tulingcode/SessionCheckpoint") {}
|
|
459
|
+
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// Writer state per session
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
export type WriterOutcome = "success" | "failure"
|
|
465
|
+
|
|
466
|
+
interface WriterState {
|
|
467
|
+
// Holds the AgentOutcome Deferred returned by Actor.spawn so callers can
|
|
468
|
+
// await writer settlement (waitForWriter / drainWriters). The public
|
|
469
|
+
// WriterOutcome translation happens in waitForWriter.
|
|
470
|
+
writing: Deferred.Deferred<AgentOutcome>
|
|
471
|
+
// F40: 1-slot pending queue. When set, holds the input for a writer that
|
|
472
|
+
// should fire as soon as `writing` settles. Newer requests evict older
|
|
473
|
+
// pending values — the newest range is always a strict superset of the
|
|
474
|
+
// older one, so older pending checkpoints would only duplicate work.
|
|
475
|
+
pending?: TryStartCheckpointWriterInput
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
// Layer implementation
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
|
|
482
|
+
export const layer: Layer.Layer<
|
|
483
|
+
Service,
|
|
484
|
+
never,
|
|
485
|
+
Session.Service | Bus.Service | Config.Service | Memory.Service | TaskRegistry.Service | ActorRegistry.Service
|
|
486
|
+
> = Layer.effect(
|
|
487
|
+
Service,
|
|
488
|
+
Effect.gen(function* () {
|
|
489
|
+
const session = yield* Session.Service
|
|
490
|
+
const config = yield* Config.Service
|
|
491
|
+
const memory = yield* Memory.Service
|
|
492
|
+
const taskRegistry = yield* TaskRegistry.Service
|
|
493
|
+
const actorRegistry = yield* ActorRegistry.Service
|
|
494
|
+
const bus = yield* Bus.Service
|
|
495
|
+
const scope = yield* Scope.Scope
|
|
496
|
+
|
|
497
|
+
// Plain Map in the layer closure — same approach as compaction.ts
|
|
498
|
+
const writers = new Map<SessionID, WriterState>()
|
|
499
|
+
|
|
500
|
+
const tryStartCheckpointWriter: (
|
|
501
|
+
input: TryStartCheckpointWriterInput,
|
|
502
|
+
) => Effect.Effect<TryStartCheckpointWriterResult> = Effect.fn("SessionCheckpoint.tryStartCheckpointWriter")(function* (
|
|
503
|
+
input: TryStartCheckpointWriterInput,
|
|
504
|
+
) {
|
|
505
|
+
// F40: writer1 still running. Evict any prior pending and queue this
|
|
506
|
+
// request — newest wins because its range is a strict superset of the
|
|
507
|
+
// older pending range, so older pending checkpoints would only
|
|
508
|
+
// duplicate the work.
|
|
509
|
+
const existing = writers.get(input.sessionID)
|
|
510
|
+
if (existing) {
|
|
511
|
+
if (existing.pending) {
|
|
512
|
+
log.info("writer pending evicted (newer range arrived)", { sessionID: input.sessionID })
|
|
513
|
+
} else {
|
|
514
|
+
log.info("writer already running, queueing", { sessionID: input.sessionID })
|
|
515
|
+
}
|
|
516
|
+
existing.pending = input
|
|
517
|
+
return "queued" as const
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Defensive: skip if called for a system-spawned session. With Task 27's
|
|
521
|
+
// writer-as-subagent migration this becomes mostly impossible, but the
|
|
522
|
+
// guard stays so future paths that fold a system-spawn actor into the
|
|
523
|
+
// main loop don't accidentally re-enter the writer.
|
|
524
|
+
if (yield* actorRegistry.isSystemSpawned(input.sessionID, "main")) {
|
|
525
|
+
log.info("tryStartCheckpointWriter skipping system-spawned session")
|
|
526
|
+
return "skipped" as const
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Mirror parent runLoop's view (prompt.ts:2036-2040) so the writer's
|
|
530
|
+
// ForkContext is byte-equal at the watermark moment. Reading the
|
|
531
|
+
// unfiltered session stream would let computeBoundary land on a
|
|
532
|
+
// subagent/prior-writer assistant turn and misalign the prefix cache.
|
|
533
|
+
const sessionInfo = yield* session.get(input.sessionID)
|
|
534
|
+
const msgs = yield* MessageV2.filterCompactedEffect(input.sessionID, {
|
|
535
|
+
contextFrom: sessionInfo.contextFrom,
|
|
536
|
+
contextWatermark: sessionInfo.contextWatermark,
|
|
537
|
+
agentID: "main",
|
|
538
|
+
})
|
|
539
|
+
if (msgs.length === 0) {
|
|
540
|
+
log.info("no messages, skipping checkpoint", { sessionID: input.sessionID })
|
|
541
|
+
return "skipped" as const
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Compute boundary for last_checkpoint_message_id bookkeeping. Layer 6
|
|
545
|
+
// (Task 16): role-aware adjustment to ensure tool_use/tool_result pairs
|
|
546
|
+
// and same-message.id thinking blocks aren't split. OpenCode's ToolPart
|
|
547
|
+
// carries both use (input) and result (output) on the SAME message, so
|
|
548
|
+
// we project each ToolPart to both a tool_use and a tool_result block —
|
|
549
|
+
// pairing is intrinsically satisfied today and the algorithm acts as a
|
|
550
|
+
// no-op. Wiring is in place so future tool_result extraction
|
|
551
|
+
// (separate user message) will walk the boundary correctly
|
|
552
|
+
// without further changes here.
|
|
553
|
+
const candidateID = computeBoundary(msgs)
|
|
554
|
+
const candidateIdx = msgs.findIndex((m) => m.info.id === candidateID)
|
|
555
|
+
const adjustedIdx = adjustBoundaryForApiInvariants(
|
|
556
|
+
msgs.map((m) => ({
|
|
557
|
+
role: m.info.role,
|
|
558
|
+
id: m.info.id,
|
|
559
|
+
content: m.parts.flatMap((p) =>
|
|
560
|
+
p.type === "tool"
|
|
561
|
+
? [
|
|
562
|
+
{ type: "tool_use", id: p.callID },
|
|
563
|
+
{ type: "tool_result", tool_use_id: p.callID },
|
|
564
|
+
]
|
|
565
|
+
: [],
|
|
566
|
+
),
|
|
567
|
+
})),
|
|
568
|
+
Math.max(candidateIdx, 0),
|
|
569
|
+
)
|
|
570
|
+
const endMessageID = msgs[adjustedIdx]?.info.id ?? candidateID
|
|
571
|
+
|
|
572
|
+
// v5 paths: single checkpoint.md per session, single memory.md per
|
|
573
|
+
// project (carries across sessions in the same repo), task narrative
|
|
574
|
+
// under <sid>/tasks/<id>/. Resolve projectID once HERE — Instance.current
|
|
575
|
+
// is ALS-bound and lost once the writer subagent fiber detaches.
|
|
576
|
+
const projectID =
|
|
577
|
+
(yield* Effect.try({
|
|
578
|
+
try: () => Instance.current?.project?.id as ProjectID | undefined,
|
|
579
|
+
catch: () => undefined,
|
|
580
|
+
}).pipe(Effect.orElseSucceed(() => undefined))) ?? ProjectID.global
|
|
581
|
+
const sessMemDir = metaDir(input.sessionID)
|
|
582
|
+
const projectMemDir = path.join(Global.Path.data, "memory", "projects", projectID)
|
|
583
|
+
const checkpointFile = checkpointPath(input.sessionID)
|
|
584
|
+
const memoryFile = memoryPath(projectID)
|
|
585
|
+
const taskMemDir = path.join(sessMemDir, "tasks")
|
|
586
|
+
const notesFile = notesPath(input.sessionID)
|
|
587
|
+
|
|
588
|
+
// Ensure dirs exist before writer fires
|
|
589
|
+
yield* Effect.promise(() => fs.mkdir(sessMemDir, { recursive: true }))
|
|
590
|
+
yield* Effect.promise(() => fs.mkdir(taskMemDir, { recursive: true }))
|
|
591
|
+
yield* Effect.promise(() => fs.mkdir(projectMemDir, { recursive: true }))
|
|
592
|
+
|
|
593
|
+
// Migrate legacy lowercase memory.md → MEMORY.md before templating/reading.
|
|
594
|
+
yield* Effect.promise(() => migrateProjectMemory(projectID))
|
|
595
|
+
|
|
596
|
+
// Bootstrap checkpoint.md, memory.md, and notes.md from templates if missing.
|
|
597
|
+
// Self-contained helpers also mkdir parent so they're safe in isolation.
|
|
598
|
+
yield* Effect.promise(() => ensureCheckpointTemplate(checkpointFile))
|
|
599
|
+
yield* Effect.promise(() => ensureMemoryTemplate(memoryFile))
|
|
600
|
+
yield* Effect.promise(() => ensureNotesTemplate(notesFile))
|
|
601
|
+
|
|
602
|
+
// v5: single-file checkpoint, check if prior content exists
|
|
603
|
+
const checkpointExists = yield* Effect.promise(() => Bun.file(checkpointFile).exists())
|
|
604
|
+
const memoryExists = yield* Effect.promise(() => Bun.file(memoryFile).exists())
|
|
605
|
+
const rangeDesc = checkpointExists
|
|
606
|
+
? [
|
|
607
|
+
`Previous checkpoint: ${checkpointFile}`,
|
|
608
|
+
memoryExists ? `Previous memory: ${memoryFile}` : "",
|
|
609
|
+
"Read BOTH the prior checkpoint (to dedupe Discovered/Dead-end titles AND to carry forward Live Resources, Execution-context frames, and Session-metadata fields that are still alive) AND the prior memory (project memory) before writing yours.",
|
|
610
|
+
]
|
|
611
|
+
.filter((s) => s.length > 0)
|
|
612
|
+
.join("\n")
|
|
613
|
+
: "This is the first checkpoint of this session. No prior checkpoint exists; MEMORY.md and the task narrative directory likely don't exist yet either."
|
|
614
|
+
|
|
615
|
+
const progressDiff = yield* Effect.promise(() => buildProgressDiff(input.sessionID))
|
|
616
|
+
const promptText = composeWriterPrompt({ checkpointFile, memoryFile, taskMemDir, notesFile, rangeDesc, progressDiff })
|
|
617
|
+
|
|
618
|
+
// v6: spawn writer as subagent — shared sessionID, automatic
|
|
619
|
+
// ActorRegistry registration, automatic tool whitelist enforcement
|
|
620
|
+
// via permission system. Replaces the legacy session.create + manual
|
|
621
|
+
// forkDetach + WriterState tracking that lived here pre-Task-27.
|
|
622
|
+
//
|
|
623
|
+
// Resolved via spawnRef rather than `yield* Actor.Service` to break the
|
|
624
|
+
// (Actor → SessionPrompt → SessionCheckpoint → Actor) layer cycle.
|
|
625
|
+
const actor = spawnRef.current
|
|
626
|
+
if (!actor) {
|
|
627
|
+
log.warn("tryStartCheckpointWriter skipping — Actor service unavailable", { sessionID: input.sessionID })
|
|
628
|
+
return "skipped" as const
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Axis B: branch forkContext shape on config.checkpoint.fork.
|
|
632
|
+
// - true → preserve existing prefix-cache parent-fork behavior
|
|
633
|
+
// (parent agent's system + tools, full slice up to watermark).
|
|
634
|
+
// - false → cold-start: writer's own system + tools, delta slice since
|
|
635
|
+
// last_checkpoint_message_id (aligned past tool_use/tool_result).
|
|
636
|
+
// See spec 2026-06-09-checkpoint-writer-child-session-and-no-fork-fallback-design.md §3.
|
|
637
|
+
//
|
|
638
|
+
// Default-behavior change at this PR: previously the writer always forked
|
|
639
|
+
// the parent's full prefix (effectively fork: true). The default is now
|
|
640
|
+
// false (no-fork delta-only). Users on cache-breakpoint providers
|
|
641
|
+
// (Anthropic) who want to retain the prefix-cache benefit must set
|
|
642
|
+
// `checkpoint.fork: true` in their config. See the spec at
|
|
643
|
+
// docs/superpowers/specs/2026-06-09-checkpoint-writer-child-session-and-no-fork-fallback-design.md §4.5.
|
|
644
|
+
const cfg = yield* config.get()
|
|
645
|
+
const forkMode = cfg.checkpoint?.fork ?? false
|
|
646
|
+
|
|
647
|
+
const parentRow = yield* Effect.sync(() =>
|
|
648
|
+
Database.use((d) =>
|
|
649
|
+
d.select({ last: SessionTable.last_checkpoint_message_id })
|
|
650
|
+
.from(SessionTable)
|
|
651
|
+
.where(eq(SessionTable.id, input.sessionID))
|
|
652
|
+
.get(),
|
|
653
|
+
),
|
|
654
|
+
).pipe(Effect.catch(() => Effect.succeed(undefined as { last: MessageID | null } | undefined)))
|
|
655
|
+
const lastCheckpointMessageID = parentRow?.last ?? undefined
|
|
656
|
+
|
|
657
|
+
// Hoisted watermark + delta computation: must run BEFORE session.create
|
|
658
|
+
// so an empty-delta fork:false call short-circuits to "skipped" without
|
|
659
|
+
// creating a child session or invoking actor.spawn. Pre-fix, the
|
|
660
|
+
// empty-delta path fell through to spawn → runLoop's
|
|
661
|
+
// `isForkAgent && !forkCtx → break` → settle watcher resolved success →
|
|
662
|
+
// parent's last_checkpoint_message_id advanced silently (stale checkpoint).
|
|
663
|
+
const watermarkIdx = msgs.findIndex((m) => m.info.id === endMessageID)
|
|
664
|
+
if (watermarkIdx < 0) {
|
|
665
|
+
log.warn("tryStartCheckpointWriter: watermark message not found, skipping", {
|
|
666
|
+
sessionID: input.sessionID,
|
|
667
|
+
endMessageID,
|
|
668
|
+
})
|
|
669
|
+
return "skipped" as const
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// For fork:false only: precompute the aligned delta and bail if empty.
|
|
673
|
+
// fork:true uses msgs.slice(0, watermarkIdx + 1) which is never empty
|
|
674
|
+
// given msgs.length > 0 and watermarkIdx >= 0.
|
|
675
|
+
const lastIdx = lastCheckpointMessageID
|
|
676
|
+
? msgs.findIndex((m) => m.info.id === lastCheckpointMessageID)
|
|
677
|
+
: -1
|
|
678
|
+
const rawDeltaStart = lastIdx >= 0 ? lastIdx + 1 : 0
|
|
679
|
+
const alignedStart = alignToNonToolResultUser(
|
|
680
|
+
msgs.map((m) => ({ info: { role: m.info.role }, parts: m.parts })),
|
|
681
|
+
rawDeltaStart,
|
|
682
|
+
)
|
|
683
|
+
const delta = forkMode ? [] : msgs.slice(alignedStart, watermarkIdx + 1)
|
|
684
|
+
if (!forkMode && delta.length === 0) {
|
|
685
|
+
// Empty delta under fork:false signals either (a) a degenerate
|
|
686
|
+
// session, or (b) a bug elsewhere advanced last_checkpoint_message_id
|
|
687
|
+
// past the watermark. Either way, spawning a writer would be a
|
|
688
|
+
// silent no-op that would still advance the watermark on settle —
|
|
689
|
+
// skip visibly so it's observable in logs.
|
|
690
|
+
log.warn("tryStartCheckpointWriter: empty delta under fork:false, skipping", {
|
|
691
|
+
sessionID: input.sessionID,
|
|
692
|
+
endMessageID,
|
|
693
|
+
lastCheckpointMessageID,
|
|
694
|
+
})
|
|
695
|
+
return "skipped" as const
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Capture parent's view at the watermark for prefix-cache alignment.
|
|
699
|
+
// See docs/superpowers/specs/2026-05-26-fork-agent-prefix-cache-design.md
|
|
700
|
+
//
|
|
701
|
+
// prefixCaptureRef is populated by SessionPrompt.layer to break the
|
|
702
|
+
// (ToolRegistry → SessionCheckpoint → ToolRegistry) layer cycle.
|
|
703
|
+
const buildPrefix = prefixCaptureRef.current
|
|
704
|
+
if (!buildPrefix) {
|
|
705
|
+
log.warn("tryStartCheckpointWriter: prefixCaptureRef not set, spawning without forkContext", {
|
|
706
|
+
sessionID: input.sessionID,
|
|
707
|
+
})
|
|
708
|
+
}
|
|
709
|
+
const forkCtx: ForkContext | undefined = yield* (buildPrefix
|
|
710
|
+
? Effect.gen(function* () {
|
|
711
|
+
const watermarkMsg = msgs[watermarkIdx]
|
|
712
|
+
const parentAgentName = (watermarkMsg.info as { agent?: string }).agent
|
|
713
|
+
// NOTE: parentAgentName guard is scoped to the forkMode:true branch only —
|
|
714
|
+
// fork:false is agent-name-independent (always passes "checkpoint-writer"
|
|
715
|
+
// to buildPrefix), so a missing parent agent field must not gate it.
|
|
716
|
+
|
|
717
|
+
if (forkMode) {
|
|
718
|
+
if (!parentAgentName) {
|
|
719
|
+
log.warn(
|
|
720
|
+
"tryStartCheckpointWriter: watermark has no agent, fork:true requires parent agent — falling back to no forkContext",
|
|
721
|
+
{ sessionID: input.sessionID, endMessageID },
|
|
722
|
+
)
|
|
723
|
+
return undefined as ForkContext | undefined
|
|
724
|
+
}
|
|
725
|
+
// fork:true — preserve existing prefix-cache parent-fork behavior.
|
|
726
|
+
// Build system + tools + inheritedMessages snapshot via capture ref
|
|
727
|
+
// using the parent agent's identity and the full slice up to watermark.
|
|
728
|
+
// The closure inside SessionPrompt.layer resolves Agent.Info and Provider.Model.
|
|
729
|
+
const msgsAtWatermark = msgs.slice(0, watermarkIdx + 1)
|
|
730
|
+
const prefix = yield* buildPrefix({
|
|
731
|
+
sessionID: input.sessionID,
|
|
732
|
+
agentName: parentAgentName,
|
|
733
|
+
providerID: input.model.providerID,
|
|
734
|
+
modelID: input.model.modelID,
|
|
735
|
+
msgs: msgsAtWatermark,
|
|
736
|
+
})
|
|
737
|
+
return {
|
|
738
|
+
system: prefix.system,
|
|
739
|
+
tools: prefix.tools,
|
|
740
|
+
inheritedMessages: prefix.inheritedMessages,
|
|
741
|
+
parentPermission: prefix.parentPermission,
|
|
742
|
+
watermarkMsgID: endMessageID as MessageID,
|
|
743
|
+
model: {
|
|
744
|
+
providerID: input.model.providerID as ProviderID,
|
|
745
|
+
modelID: input.model.modelID as ModelID,
|
|
746
|
+
},
|
|
747
|
+
} satisfies ForkContext
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// fork:false — cold-start: use the writer's own system + tools and
|
|
751
|
+
// a delta slice since the last checkpoint. capture() resolves the
|
|
752
|
+
// checkpoint-writer agent's own definition when agentName is
|
|
753
|
+
// "checkpoint-writer", so a single call returns:
|
|
754
|
+
// - system + tools = writer's (because agentName === "checkpoint-writer"),
|
|
755
|
+
// - inheritedMessages = the delta we pass in, converted to ModelMessage[],
|
|
756
|
+
// - parentPermission = writer's own permission (used for tool-availability filter).
|
|
757
|
+
// Earlier draft considered two buildPrefix calls (one with msgs:[] for
|
|
758
|
+
// system+tools, one with msgs:delta for messages); rejected because
|
|
759
|
+
// buildLLMRequestPrefix `Effect.die`s if msgs has no user message.
|
|
760
|
+
// Delta is precomputed (and the empty-delta case is short-circuited)
|
|
761
|
+
// above, before session.create.
|
|
762
|
+
const writerPrefix = yield* buildPrefix({
|
|
763
|
+
sessionID: input.sessionID,
|
|
764
|
+
agentName: "checkpoint-writer",
|
|
765
|
+
providerID: input.model.providerID,
|
|
766
|
+
modelID: input.model.modelID,
|
|
767
|
+
msgs: delta,
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
system: writerPrefix.system,
|
|
772
|
+
tools: writerPrefix.tools,
|
|
773
|
+
inheritedMessages: writerPrefix.inheritedMessages,
|
|
774
|
+
parentPermission: writerPrefix.parentPermission,
|
|
775
|
+
watermarkMsgID: endMessageID as MessageID,
|
|
776
|
+
model: {
|
|
777
|
+
providerID: input.model.providerID as ProviderID,
|
|
778
|
+
modelID: input.model.modelID as ModelID,
|
|
779
|
+
},
|
|
780
|
+
} satisfies ForkContext
|
|
781
|
+
})
|
|
782
|
+
: Effect.succeed(undefined as ForkContext | undefined))
|
|
783
|
+
|
|
784
|
+
// Axis A: writer always runs in a fresh child session. This isolates the
|
|
785
|
+
// writer's messages and actor registration from the parent so:
|
|
786
|
+
// - parent's message table sees zero new rows,
|
|
787
|
+
// - parent's `sync.data.actor[parent]` does not include the writer,
|
|
788
|
+
// - Ctrl+X subagent cycle / SubagentFooter / DialogSubagent / etc. are
|
|
789
|
+
// all naturally clean (they all key on sessionID).
|
|
790
|
+
// The writer's checkpoint.md / memory.md / progress paths are absolute and
|
|
791
|
+
// computed from input.sessionID (parent) above, so file writes still target
|
|
792
|
+
// the parent's artifacts. Settle watcher below also targets parent.
|
|
793
|
+
// See spec 2026-06-09-checkpoint-writer-child-session-and-no-fork-fallback-design.md §2.
|
|
794
|
+
const writerChildSession = yield* session.create({
|
|
795
|
+
parentID: input.sessionID,
|
|
796
|
+
title: `checkpoint-writer: ${rangeDesc}`,
|
|
797
|
+
})
|
|
798
|
+
|
|
799
|
+
// Estimate delta tokens for observability. forkCtx.inheritedMessages is
|
|
800
|
+
// ModelMessage[]; an exact count requires the tokenizer, but a rough
|
|
801
|
+
// length heuristic is sufficient for the log line.
|
|
802
|
+
const deltaApproxBytes = JSON.stringify(forkCtx?.inheritedMessages ?? []).length
|
|
803
|
+
log.info("tryStartCheckpointWriter spawning", {
|
|
804
|
+
sessionID: input.sessionID,
|
|
805
|
+
childSessionID: writerChildSession.id,
|
|
806
|
+
mode: forkMode ? "fork" : "no-fork",
|
|
807
|
+
deltaApproxBytes,
|
|
808
|
+
rangeDesc,
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
const result = yield* actor.spawn({
|
|
812
|
+
mode: "subagent",
|
|
813
|
+
sessionID: writerChildSession.id,
|
|
814
|
+
// Axis A: writer runs under child session, but its checkpoint.md /
|
|
815
|
+
// memory.md / progress paths AND CheckpointContext entries are keyed
|
|
816
|
+
// on the PARENT. The splitover plugin reads these via actor.preStop
|
|
817
|
+
// and must see parentSessionID to re-derive the right paths — without
|
|
818
|
+
// it, checkpointPath(child) returns an empty file and the plugin
|
|
819
|
+
// emits a false topic-missing reflection that loops the writer up to
|
|
820
|
+
// MAX_PRE_REACT.
|
|
821
|
+
parentSessionID: input.sessionID,
|
|
822
|
+
agentType: "checkpoint-writer",
|
|
823
|
+
description: `checkpoint writer for session ${input.sessionID} covering ${rangeDesc}`,
|
|
824
|
+
task: promptText,
|
|
825
|
+
context: "full",
|
|
826
|
+
tools: ["read", "write", "edit", "apply_patch", "glob", "grep", "task"],
|
|
827
|
+
model: {
|
|
828
|
+
providerID: input.model.providerID as ProviderID,
|
|
829
|
+
modelID: input.model.modelID as ModelID,
|
|
830
|
+
},
|
|
831
|
+
background: true,
|
|
832
|
+
forkContext: forkCtx,
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
const actorID = result.actorID
|
|
836
|
+
|
|
837
|
+
// Capture priorTitles (from checkpoint.md as it stood at the watermark)
|
|
838
|
+
// and register the per-actor context entry BEFORE the writer's first
|
|
839
|
+
// turn so the splitover plugin's preStop hook can read it. The set
|
|
840
|
+
// runs in microseconds; the writer's first LLM round-trip takes
|
|
841
|
+
// seconds — no race in practice. See spec §6.1.
|
|
842
|
+
const priorTitles = yield* Effect.promise(() => loadPriorDiscoveredTitles(input.sessionID))
|
|
843
|
+
CheckpointContext.set(input.sessionID, actorID, {
|
|
844
|
+
priorTitles,
|
|
845
|
+
expectedRevisions: [],
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
writers.set(input.sessionID, { writing: result.outcome })
|
|
849
|
+
|
|
850
|
+
// Bookkeeping: the parent's last_checkpoint_message_id advances when the
|
|
851
|
+
// writer settles. Fork into the layer's scope so the watcher survives
|
|
852
|
+
// tryStartCheckpointWriter returning (background: true semantics) but is still tied
|
|
853
|
+
// to the layer's lifetime — no orphan fiber on shutdown.
|
|
854
|
+
yield* Effect.gen(function* () {
|
|
855
|
+
const outcome = yield* Deferred.await(result.outcome)
|
|
856
|
+
yield* Effect.sync(() =>
|
|
857
|
+
Database.use((d) =>
|
|
858
|
+
d.update(SessionTable)
|
|
859
|
+
.set({ last_checkpoint_message_id: endMessageID as MessageID })
|
|
860
|
+
.where(eq(SessionTable.id, input.sessionID))
|
|
861
|
+
.run(),
|
|
862
|
+
),
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
// F40: capture pending before deleting the slot so a queued writer
|
|
866
|
+
// (held while writer1 was running) can fire as a fresh writer.
|
|
867
|
+
const pending = writers.get(input.sessionID)?.pending
|
|
868
|
+
writers.delete(input.sessionID)
|
|
869
|
+
|
|
870
|
+
// F44: aggregate writer slice tokens and emit cache-perf metric so
|
|
871
|
+
// prefix-cache reuse is empirically observable. Degrades to zeros if
|
|
872
|
+
// aggregation fails (e.g. session messages unavailable post-shutdown).
|
|
873
|
+
const stats = yield* aggregateWriterCacheMetrics(session, input.sessionID, result.actorID).pipe(
|
|
874
|
+
Effect.catch(() =>
|
|
875
|
+
Effect.succeed({
|
|
876
|
+
total_input_tokens: 0,
|
|
877
|
+
cache_read_tokens: 0,
|
|
878
|
+
cache_write_tokens: 0,
|
|
879
|
+
cache_hit_rate: 0,
|
|
880
|
+
num_llm_calls: 0,
|
|
881
|
+
}),
|
|
882
|
+
),
|
|
883
|
+
)
|
|
884
|
+
yield* bus
|
|
885
|
+
.publish(WriterCachePerf, {
|
|
886
|
+
sessionID: input.sessionID,
|
|
887
|
+
writerActorID: result.actorID,
|
|
888
|
+
status: outcome.status === "success" ? ("completed" as const) : ("failed" as const),
|
|
889
|
+
...stats,
|
|
890
|
+
})
|
|
891
|
+
.pipe(Effect.ignore)
|
|
892
|
+
|
|
893
|
+
// F40: drain pending. If a queued request exists, fire a fresh writer
|
|
894
|
+
// for it. Errors are swallowed — the queued writer's failure should
|
|
895
|
+
// not interrupt the original writer's settlement watcher.
|
|
896
|
+
if (pending) {
|
|
897
|
+
log.info("draining pending writer", { sessionID: input.sessionID })
|
|
898
|
+
yield* tryStartCheckpointWriter(pending).pipe(Effect.ignore)
|
|
899
|
+
}
|
|
900
|
+
}).pipe(
|
|
901
|
+
Effect.ensuring(
|
|
902
|
+
Effect.sync(() => CheckpointContext.remove(input.sessionID, actorID)),
|
|
903
|
+
),
|
|
904
|
+
Effect.forkIn(scope),
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
return "started" as const
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
const waitForWriter = Effect.fn("SessionCheckpoint.waitForWriter")(function* (sessionID: SessionID) {
|
|
911
|
+
const state = writers.get(sessionID)
|
|
912
|
+
if (!state) return "no-writer" as const
|
|
913
|
+
|
|
914
|
+
// v2 writers manage 3 file types and frequently take 60-180s; pad to
|
|
915
|
+
// 5min so a long-but-honest writer is not mistaken for a failure by
|
|
916
|
+
// the prune retry watcher. AgentOutcome → WriterOutcome translation:
|
|
917
|
+
// success → "success", failure / cancelled → "failure".
|
|
918
|
+
const outcome = yield* Deferred.await(state.writing).pipe(
|
|
919
|
+
Effect.timeout(300_000),
|
|
920
|
+
Effect.catch(() => Effect.succeed<AgentOutcome>({ status: "failure", error: "timeout" })),
|
|
921
|
+
)
|
|
922
|
+
return outcome.status === "success" ? ("success" as const) : ("failure" as const)
|
|
923
|
+
})
|
|
924
|
+
|
|
925
|
+
const drainWriters = Effect.fn("SessionCheckpoint.drainWriters")(function* (input?: { timeoutMs?: number }) {
|
|
926
|
+
const timeoutMs = input?.timeoutMs ?? 120_000
|
|
927
|
+
const pending = [...writers.values()]
|
|
928
|
+
if (pending.length === 0) return { drained: 0, timedOut: 0 }
|
|
929
|
+
log.info("draining checkpoint writers before shutdown", {
|
|
930
|
+
count: pending.length,
|
|
931
|
+
timeoutMs,
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
// Deferred.await ignores fiber interruption during shutdown because
|
|
935
|
+
// it resolves via Deferred.succeed in the detached writer. We only
|
|
936
|
+
// need a collective upper bound so a stuck writer doesn't block exit.
|
|
937
|
+
yield* Effect.all(
|
|
938
|
+
pending.map((state) => Deferred.await(state.writing)),
|
|
939
|
+
{ concurrency: "unbounded" },
|
|
940
|
+
).pipe(
|
|
941
|
+
Effect.timeout(timeoutMs),
|
|
942
|
+
Effect.catch(() => Effect.succeed(undefined)),
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
// Writers delete themselves from the map on success/failure, so anything
|
|
946
|
+
// still present after the timeout is a writer that didn't settle in time.
|
|
947
|
+
const timedOut = writers.size
|
|
948
|
+
const drained = pending.length - timedOut
|
|
949
|
+
if (timedOut > 0) log.warn("drain timed out, writers still pending", { drained, timedOut })
|
|
950
|
+
else log.info("drain complete", { drained })
|
|
951
|
+
return { drained, timedOut }
|
|
952
|
+
})
|
|
953
|
+
|
|
954
|
+
const hasCheckpoint = Effect.fn("SessionCheckpoint.hasCheckpoint")(function* (sessionID: SessionID) {
|
|
955
|
+
return yield* Effect.promise(() => Bun.file(checkpointPath(sessionID)).exists())
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
const hasMemoryOrTasks = Effect.fn("SessionCheckpoint.hasMemoryOrTasks")(function* (sessionID: SessionID) {
|
|
959
|
+
const memoryRoot = yield* memory.root()
|
|
960
|
+
const sessMemDir = path.join(memoryRoot, "sessions", sessionID)
|
|
961
|
+
const memEntries = yield* Effect.promise(() =>
|
|
962
|
+
fs.readdir(sessMemDir).catch(() => [] as string[]),
|
|
963
|
+
)
|
|
964
|
+
if (memEntries.length > 0) return true
|
|
965
|
+
const tasks = yield* taskRegistry.list({ session_id: sessionID, include_terminal: true })
|
|
966
|
+
return tasks.length > 0
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
const loadLatest = Effect.fn("SessionCheckpoint.loadLatest")(function* (sessionID: SessionID) {
|
|
970
|
+
const content = yield* Effect.promise(() =>
|
|
971
|
+
Bun.file(checkpointPath(sessionID)).text().catch(() => ""),
|
|
972
|
+
)
|
|
973
|
+
return content || undefined
|
|
974
|
+
})
|
|
975
|
+
|
|
976
|
+
const loadCheckpoints = Effect.fn("SessionCheckpoint.loadCheckpoints")(function* (
|
|
977
|
+
sessionID: SessionID,
|
|
978
|
+
_count: number,
|
|
979
|
+
) {
|
|
980
|
+
const content = yield* Effect.promise(() =>
|
|
981
|
+
Bun.file(checkpointPath(sessionID)).text().catch(() => ""),
|
|
982
|
+
)
|
|
983
|
+
return content ? [content] : []
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
const renderIndex = Effect.fn("SessionCheckpoint.renderIndex")(function* (sessionID: SessionID) {
|
|
987
|
+
const snapFile = checkpointPath(sessionID)
|
|
988
|
+
const exists = yield* Effect.promise(() => Bun.file(snapFile).exists())
|
|
989
|
+
if (!exists) return "No checkpoints yet for this session."
|
|
990
|
+
|
|
991
|
+
const content = yield* Effect.promise(() => Bun.file(snapFile).text().catch(() => ""))
|
|
992
|
+
const topicMatch = content.match(/^Topic:\s*(.+)$/m)
|
|
993
|
+
const topic = topicMatch ? topicMatch[1].trim() : "(unknown)"
|
|
994
|
+
|
|
995
|
+
const dir = metaDir(sessionID)
|
|
996
|
+
const lines: string[] = []
|
|
997
|
+
lines.push("## Checkpoint")
|
|
998
|
+
lines.push("")
|
|
999
|
+
lines.push(`Directory: ${dir}/`)
|
|
1000
|
+
lines.push("")
|
|
1001
|
+
lines.push(`Current checkpoint (${topic}): checkpoint.md [shown below]`)
|
|
1002
|
+
lines.push("")
|
|
1003
|
+
lines.push(`Use read("${snapFile}") to access the full checkpoint.`)
|
|
1004
|
+
|
|
1005
|
+
return lines.join("\n")
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
const renderRebuildContext = Effect.fn("SessionCheckpoint.renderRebuildContext")(function* (
|
|
1009
|
+
sessionID: SessionID,
|
|
1010
|
+
opts?: { lastMessageInfo?: LastMessageInfo; agentID?: string },
|
|
1011
|
+
) {
|
|
1012
|
+
// renderRebuildContext is for the user-facing main agent's context rebuild.
|
|
1013
|
+
// Subagent-mode actors (system-spawned writers, model-spawned subagents)
|
|
1014
|
+
// share the parent's session but don't have their own checkpoint state to
|
|
1015
|
+
// render — return empty so the rebuild path is a no-op for them.
|
|
1016
|
+
// Note: agentID === "main" must pass through. After F49+F50 the main
|
|
1017
|
+
// agent's lastUser.agentID is "main" (DB row→info reconstruction in
|
|
1018
|
+
// message-v2.ts populates info.agentID from agent_id column), and the
|
|
1019
|
+
// runLoop calls this with that value. Treating "main" as subagent here
|
|
1020
|
+
// would skip rebuild → fall through to F39 compaction → context loss.
|
|
1021
|
+
if (opts?.agentID && opts.agentID !== "main") return ""
|
|
1022
|
+
|
|
1023
|
+
const inFlight = writers.get(sessionID)
|
|
1024
|
+
if (inFlight) {
|
|
1025
|
+
log.info("rebuild waiting for in-flight writer", { sessionID })
|
|
1026
|
+
yield* Effect.race(
|
|
1027
|
+
Deferred.await(inFlight.writing).pipe(Effect.as("done" as const)),
|
|
1028
|
+
Effect.sleep("60 seconds").pipe(
|
|
1029
|
+
Effect.tap(() => Effect.sync(() => log.warn("writer wait timeout — using on-disk checkpoint", { sessionID }))),
|
|
1030
|
+
Effect.as("timeout" as const),
|
|
1031
|
+
),
|
|
1032
|
+
).pipe(Effect.catch(() => Effect.succeed("error" as const)))
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const cfg = yield* config.get()
|
|
1036
|
+
const caps = cfg.checkpoint?.push_caps ?? {}
|
|
1037
|
+
const memoryRoot = yield* memory.root()
|
|
1038
|
+
|
|
1039
|
+
const sessMemDir = path.join(memoryRoot, "sessions", sessionID)
|
|
1040
|
+
|
|
1041
|
+
// Resolve current project ID once. Used by Section 7 (project memory
|
|
1042
|
+
// read) and Section 8 (FTS scope filter). ALS-bound — must be resolved
|
|
1043
|
+
// before any deferred work.
|
|
1044
|
+
const currentProjectID = yield* Effect.try({
|
|
1045
|
+
try: () => Instance.current?.project?.id as ProjectID | undefined,
|
|
1046
|
+
catch: () => undefined as ProjectID | undefined,
|
|
1047
|
+
}).pipe(Effect.catch(() => Effect.succeed<ProjectID | undefined>(undefined)))
|
|
1048
|
+
const projectID = currentProjectID ?? ProjectID.global
|
|
1049
|
+
|
|
1050
|
+
// Section data: tasks (SQL), session checkpoint (file), project memory (file).
|
|
1051
|
+
const tasks = yield* taskRegistry.list({ session_id: sessionID, include_terminal: true })
|
|
1052
|
+
|
|
1053
|
+
const checkpointResult = yield* Effect.promise(() =>
|
|
1054
|
+
readBudgetedSectionAware(checkpointPath(sessionID), caps.checkpoint ?? 11_000),
|
|
1055
|
+
)
|
|
1056
|
+
const checkpointText = checkpointResult?.text ?? ""
|
|
1057
|
+
|
|
1058
|
+
yield* Effect.promise(() => migrateProjectMemory(projectID))
|
|
1059
|
+
const memoryResult = yield* Effect.promise(() =>
|
|
1060
|
+
readBudgetedSectionAware(memoryPath(projectID), caps.memory ?? 10_000),
|
|
1061
|
+
)
|
|
1062
|
+
const memoryText = memoryResult?.text ?? ""
|
|
1063
|
+
|
|
1064
|
+
const notesResult = yield* Effect.promise(() =>
|
|
1065
|
+
readBudgeted(notesPath(sessionID), caps.notes ?? 6000),
|
|
1066
|
+
)
|
|
1067
|
+
const notesText = notesResult?.text ?? ""
|
|
1068
|
+
|
|
1069
|
+
const globalResult = yield* Effect.promise(() =>
|
|
1070
|
+
readBudgetedSectionAware(globalMemoryPath(), caps.global ?? 6000),
|
|
1071
|
+
)
|
|
1072
|
+
const globalText = globalResult?.text ?? ""
|
|
1073
|
+
|
|
1074
|
+
const actors = yield* actorRegistry.listActive()
|
|
1075
|
+
|
|
1076
|
+
// Bail early if absolutely nothing to push: no tasks, no memory content, no live actors.
|
|
1077
|
+
if (
|
|
1078
|
+
tasks.length === 0 &&
|
|
1079
|
+
!checkpointText.trim() &&
|
|
1080
|
+
!memoryText.trim() &&
|
|
1081
|
+
!globalText.trim() &&
|
|
1082
|
+
actors.length === 0
|
|
1083
|
+
) {
|
|
1084
|
+
return ""
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const lines: string[] = []
|
|
1088
|
+
|
|
1089
|
+
// F17: Explicit "already loaded" header. Anchors the active recall
|
|
1090
|
+
// protocol's "look for this header" instruction in buildMemoryInstructions.
|
|
1091
|
+
lines.push(
|
|
1092
|
+
"The following blocks are auto-loaded from your session memory. They are already in your context — do not Read them as whole files. Use Grep for specific facts instead.",
|
|
1093
|
+
)
|
|
1094
|
+
lines.push("")
|
|
1095
|
+
|
|
1096
|
+
// Section 3: tasks ledger (hierarchical with subtasks).
|
|
1097
|
+
lines.push("## Tasks ledger")
|
|
1098
|
+
if (tasks.length === 0) {
|
|
1099
|
+
lines.push("(none)")
|
|
1100
|
+
} else {
|
|
1101
|
+
const topLevel = tasks.filter((t) => !t.parent_task_id)
|
|
1102
|
+
const byParent = new Map<string, typeof tasks>()
|
|
1103
|
+
for (const t of tasks) {
|
|
1104
|
+
if (!t.parent_task_id) continue
|
|
1105
|
+
const bucket = byParent.get(t.parent_task_id) ?? []
|
|
1106
|
+
bucket.push(t)
|
|
1107
|
+
byParent.set(t.parent_task_id, bucket)
|
|
1108
|
+
}
|
|
1109
|
+
const statusIcon = (s: string) =>
|
|
1110
|
+
({ open: "🔵", in_progress: "🔄", blocked: "🟡", done: "✅", abandoned: "❌" })[s] ?? s
|
|
1111
|
+
const ledgerLines: string[] = []
|
|
1112
|
+
for (const t of topLevel) {
|
|
1113
|
+
ledgerLines.push(`- ${t.id} ${t.status} — ${t.summary}`)
|
|
1114
|
+
const subs = byParent.get(t.id) ?? []
|
|
1115
|
+
if (subs.length === 0) continue
|
|
1116
|
+
const sublist = subs
|
|
1117
|
+
.map((s) => `${statusIcon(s.status)}${s.id}`)
|
|
1118
|
+
.join(" / ")
|
|
1119
|
+
ledgerLines.push(` Subtasks: ${sublist}`)
|
|
1120
|
+
}
|
|
1121
|
+
lines.push(truncate(ledgerLines.join("\n"), caps.tasks_ledger ?? 2000))
|
|
1122
|
+
}
|
|
1123
|
+
lines.push("")
|
|
1124
|
+
|
|
1125
|
+
// Section 5: session checkpoint (full body, capped).
|
|
1126
|
+
if (checkpointText.trim()) {
|
|
1127
|
+
lines.push("## Session checkpoint")
|
|
1128
|
+
lines.push(checkpointText.trim())
|
|
1129
|
+
lines.push("")
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Section 6: active actors ledger (one line per running actor).
|
|
1133
|
+
if (actors.length > 0) {
|
|
1134
|
+
lines.push("## Active actors")
|
|
1135
|
+
let actorBudget = caps.actor_ledger ?? 500
|
|
1136
|
+
for (const a of actors) {
|
|
1137
|
+
const line = `- ${a.actorID} — ${a.status}, "${a.description}" (agent=${a.agent})`
|
|
1138
|
+
const cost = Token.estimate(line)
|
|
1139
|
+
if (actorBudget - cost < 0) break
|
|
1140
|
+
lines.push(line)
|
|
1141
|
+
actorBudget -= cost
|
|
1142
|
+
}
|
|
1143
|
+
lines.push("")
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Section 7: project memory (full body, capped).
|
|
1147
|
+
if (memoryText.trim()) {
|
|
1148
|
+
lines.push("## Project memory")
|
|
1149
|
+
lines.push(memoryText.trim())
|
|
1150
|
+
lines.push("")
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Section 7.4: global memory (full body, capped). User-level cross-project
|
|
1154
|
+
// preferences. Placed after project memory (more actionable) and before
|
|
1155
|
+
// session notes (more volatile).
|
|
1156
|
+
if (globalText.trim()) {
|
|
1157
|
+
lines.push("## Global memory")
|
|
1158
|
+
lines.push(globalText.trim())
|
|
1159
|
+
lines.push("")
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// F14 Section 7.5: session notes (full body, capped). Skip if empty.
|
|
1163
|
+
if (notesText.trim()) {
|
|
1164
|
+
lines.push("## Session notes")
|
|
1165
|
+
lines.push(notesText.trim())
|
|
1166
|
+
lines.push("")
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Section 8: memory keys index (paths only, omit already-pushed).
|
|
1170
|
+
// SQL-scoped to the current session/project + global so other
|
|
1171
|
+
// sessions' files are not leaked. Falls back to skipping the projects
|
|
1172
|
+
// scope when the current project is the global/non-git fallback.
|
|
1173
|
+
// Reconcile first so files written off-tool (e.g. by the checkpoint
|
|
1174
|
+
// writer subagent) are visible in the FTS index here.
|
|
1175
|
+
yield* memory.reconcile().pipe(Effect.ignore)
|
|
1176
|
+
const pushedPaths = new Set(
|
|
1177
|
+
[
|
|
1178
|
+
memoryPath(projectID),
|
|
1179
|
+
checkpointPath(sessionID),
|
|
1180
|
+
globalMemoryPath(),
|
|
1181
|
+
].filter((p) => p.length > 0),
|
|
1182
|
+
)
|
|
1183
|
+
|
|
1184
|
+
const scopeFilter =
|
|
1185
|
+
currentProjectID && currentProjectID !== ProjectID.global
|
|
1186
|
+
? or(
|
|
1187
|
+
eq(MemoryFtsTable.scope, "global"),
|
|
1188
|
+
and(eq(MemoryFtsTable.scope, "sessions"), eq(MemoryFtsTable.scope_id, sessionID as string)),
|
|
1189
|
+
and(eq(MemoryFtsTable.scope, "projects"), eq(MemoryFtsTable.scope_id, currentProjectID)),
|
|
1190
|
+
)
|
|
1191
|
+
: or(
|
|
1192
|
+
eq(MemoryFtsTable.scope, "global"),
|
|
1193
|
+
and(eq(MemoryFtsTable.scope, "sessions"), eq(MemoryFtsTable.scope_id, sessionID as string)),
|
|
1194
|
+
)
|
|
1195
|
+
const scopedPaths = yield* Effect.sync(() =>
|
|
1196
|
+
Database.use((db) =>
|
|
1197
|
+
db.select({ path: MemoryFtsTable.path }).from(MemoryFtsTable).where(scopeFilter).all(),
|
|
1198
|
+
),
|
|
1199
|
+
)
|
|
1200
|
+
const keyEntries = scopedPaths
|
|
1201
|
+
.map((r) => r.path)
|
|
1202
|
+
.filter((p) => !pushedPaths.has(p) && !p.includes(`${path.sep}checkpoint${path.sep}learning-`))
|
|
1203
|
+
.map((p) => p.replace(memoryRoot + path.sep, ""))
|
|
1204
|
+
if (keyEntries.length > 0) {
|
|
1205
|
+
lines.push("## Memory keys index")
|
|
1206
|
+
let kBudget = caps.memory_titles ?? 500
|
|
1207
|
+
for (const entry of keyEntries) {
|
|
1208
|
+
const cost = Token.estimate(entry)
|
|
1209
|
+
if (kBudget - cost < 0) break
|
|
1210
|
+
lines.push(`- ${entry}`)
|
|
1211
|
+
kBudget -= cost
|
|
1212
|
+
}
|
|
1213
|
+
lines.push("")
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Section 10: explicit seam framing for LLM continuity post-rebuild.
|
|
1217
|
+
// Compaction-summary pattern: tells the model
|
|
1218
|
+
// that preserved messages below are real history, not pseudo-content,
|
|
1219
|
+
// so it resumes mid-loop instead of asking "what would you like me
|
|
1220
|
+
// to do".
|
|
1221
|
+
lines.push("")
|
|
1222
|
+
lines.push(
|
|
1223
|
+
"This session is being continued from a previous conversation that hit a checkpoint. The session checkpoint and project memory above cover the earlier portion of the conversation.",
|
|
1224
|
+
)
|
|
1225
|
+
lines.push("")
|
|
1226
|
+
lines.push(
|
|
1227
|
+
"Recent messages are preserved verbatim below — the assistant turn (and any tool results) you'll see is real history, not pseudo-content. Continue your task by responding to the most recent state.",
|
|
1228
|
+
)
|
|
1229
|
+
lines.push("")
|
|
1230
|
+
lines.push(
|
|
1231
|
+
"Resume directly. Do not acknowledge this memory dump, do not recap, do not preface with \"I'll continue\" or similar. Pick up the last task as if the break never happened.",
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
// Section 11: tail-aware system reminder. Picks the appropriate nudge
|
|
1235
|
+
// based on how the preserved tail ends: tool-calls → continue loop,
|
|
1236
|
+
// stop → check task spec before stopping again, tool → process results,
|
|
1237
|
+
// user → no addendum needed.
|
|
1238
|
+
const info = opts?.lastMessageInfo
|
|
1239
|
+
if (info) {
|
|
1240
|
+
const reminder = (() => {
|
|
1241
|
+
switch (info.role) {
|
|
1242
|
+
case "assistant":
|
|
1243
|
+
if (info.finish === "tool-calls") return autonomousLoopReminder()
|
|
1244
|
+
return stopReminder(undefined)
|
|
1245
|
+
case "tool":
|
|
1246
|
+
return toolResultContinueReminder()
|
|
1247
|
+
case "user":
|
|
1248
|
+
return ""
|
|
1249
|
+
}
|
|
1250
|
+
})()
|
|
1251
|
+
if (reminder) {
|
|
1252
|
+
lines.push("")
|
|
1253
|
+
lines.push(reminder)
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
return lines.join("\n")
|
|
1258
|
+
})
|
|
1259
|
+
|
|
1260
|
+
const lastBoundary = Effect.fn("SessionCheckpoint.lastBoundary")(function* (sessionID: SessionID) {
|
|
1261
|
+
const row = yield* Effect.sync(() =>
|
|
1262
|
+
Database.use((db) =>
|
|
1263
|
+
db.select({ last_checkpoint_message_id: SessionTable.last_checkpoint_message_id })
|
|
1264
|
+
.from(SessionTable)
|
|
1265
|
+
.where(eq(SessionTable.id, sessionID))
|
|
1266
|
+
.get(),
|
|
1267
|
+
),
|
|
1268
|
+
)
|
|
1269
|
+
return row?.last_checkpoint_message_id as MessageID | undefined
|
|
1270
|
+
})
|
|
1271
|
+
|
|
1272
|
+
const isWriterRunning = Effect.fn("SessionCheckpoint.isWriterRunning")(function* (sessionID: SessionID) {
|
|
1273
|
+
return writers.has(sessionID)
|
|
1274
|
+
})
|
|
1275
|
+
|
|
1276
|
+
const insertRebuildBoundary = Effect.fn("SessionCheckpoint.insertRebuildBoundary")(function* (input: {
|
|
1277
|
+
sessionID: SessionID
|
|
1278
|
+
boundary: MessageID
|
|
1279
|
+
lastMessageInfo?: LastMessageInfo
|
|
1280
|
+
agentID?: string
|
|
1281
|
+
agent: string
|
|
1282
|
+
model: { providerID: string; modelID: string }
|
|
1283
|
+
boundaryCreatedAt?: number
|
|
1284
|
+
}) {
|
|
1285
|
+
const rebuildContext = yield* renderRebuildContext(input.sessionID, {
|
|
1286
|
+
lastMessageInfo: input.lastMessageInfo,
|
|
1287
|
+
agentID: input.agentID,
|
|
1288
|
+
}).pipe(Effect.catch(() => Effect.succeed("")))
|
|
1289
|
+
if (!rebuildContext) return false
|
|
1290
|
+
|
|
1291
|
+
const indexText = yield* renderIndex(input.sessionID).pipe(Effect.catch(() => Effect.succeed("")))
|
|
1292
|
+
|
|
1293
|
+
const syntheticTime = (input.boundaryCreatedAt ?? Date.now()) + 1
|
|
1294
|
+
const msg = yield* session.updateMessage({
|
|
1295
|
+
id: MessageID.ascending(),
|
|
1296
|
+
role: "user" as const,
|
|
1297
|
+
model: { providerID: input.model.providerID as ProviderID, modelID: input.model.modelID as ModelID },
|
|
1298
|
+
sessionID: input.sessionID,
|
|
1299
|
+
agent: input.agent,
|
|
1300
|
+
time: { created: syntheticTime },
|
|
1301
|
+
})
|
|
1302
|
+
|
|
1303
|
+
yield* session.updatePart({
|
|
1304
|
+
id: PartID.ascending(),
|
|
1305
|
+
messageID: msg.id,
|
|
1306
|
+
sessionID: input.sessionID,
|
|
1307
|
+
type: "checkpoint",
|
|
1308
|
+
checkpointDir: "",
|
|
1309
|
+
checkpointNumber: 0,
|
|
1310
|
+
coveredUpTo: input.boundary,
|
|
1311
|
+
})
|
|
1312
|
+
|
|
1313
|
+
if (indexText) {
|
|
1314
|
+
yield* session.updatePart({
|
|
1315
|
+
id: PartID.ascending(),
|
|
1316
|
+
messageID: msg.id,
|
|
1317
|
+
sessionID: input.sessionID,
|
|
1318
|
+
type: "text",
|
|
1319
|
+
synthetic: true,
|
|
1320
|
+
text: indexText,
|
|
1321
|
+
})
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
yield* session.updatePart({
|
|
1325
|
+
id: PartID.ascending(),
|
|
1326
|
+
messageID: msg.id,
|
|
1327
|
+
sessionID: input.sessionID,
|
|
1328
|
+
type: "text",
|
|
1329
|
+
synthetic: true,
|
|
1330
|
+
text: rebuildContext,
|
|
1331
|
+
})
|
|
1332
|
+
|
|
1333
|
+
const actorsText = yield* actorRegistry
|
|
1334
|
+
.renderForAgent(input.sessionID)
|
|
1335
|
+
.pipe(Effect.catch(() => Effect.succeed("")))
|
|
1336
|
+
if (actorsText) {
|
|
1337
|
+
yield* session.updatePart({
|
|
1338
|
+
id: PartID.ascending(),
|
|
1339
|
+
messageID: msg.id,
|
|
1340
|
+
sessionID: input.sessionID,
|
|
1341
|
+
type: "text",
|
|
1342
|
+
synthetic: true,
|
|
1343
|
+
text: actorsText,
|
|
1344
|
+
})
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// Microcompact: messages strictly newer than the boundary will survive
|
|
1348
|
+
// into the rebuild context. Clear tool_result content for compactable
|
|
1349
|
+
// tools so the first uncached request after rebuild is smaller. tool_use
|
|
1350
|
+
// is preserved — LLM still sees what action was taken; result body
|
|
1351
|
+
// becomes "[Old tool result content cleared]" via the converter at
|
|
1352
|
+
// message-v2.ts (ToolStateCompleted → output).
|
|
1353
|
+
// See docs/superpowers/specs/2026-06-03-rebuild-tail-microcompact-design.md.
|
|
1354
|
+
//
|
|
1355
|
+
// boundaryTime resolution (fail-closed):
|
|
1356
|
+
// 1. Prefer explicit input.boundaryCreatedAt (production callers
|
|
1357
|
+
// compute it but may pass undefined if the boundary message is no
|
|
1358
|
+
// longer in their filterCompactedEffect slice).
|
|
1359
|
+
// 2. Else look up input.boundary in allMsgs (full DB, includes
|
|
1360
|
+
// pre-marker history).
|
|
1361
|
+
// 3. Else SKIP — the previous fallback of 0 would clear EVERY
|
|
1362
|
+
// completed compactable tool result in the entire session,
|
|
1363
|
+
// corrupting future checkpoint writer input. Log a warning.
|
|
1364
|
+
const allMsgs = yield* session.messages({ sessionID: input.sessionID, agentID: "*" })
|
|
1365
|
+
const boundaryTime =
|
|
1366
|
+
input.boundaryCreatedAt ??
|
|
1367
|
+
allMsgs.find((m) => m.info.id === input.boundary)?.info.time.created
|
|
1368
|
+
if (boundaryTime === undefined) {
|
|
1369
|
+
log.warn("microcompact skipped: no boundary timestamp available", {
|
|
1370
|
+
sessionID: input.sessionID,
|
|
1371
|
+
boundary: input.boundary,
|
|
1372
|
+
})
|
|
1373
|
+
return true
|
|
1374
|
+
}
|
|
1375
|
+
let cleared = 0
|
|
1376
|
+
for (const m of allMsgs) {
|
|
1377
|
+
if (m.info.id === msg.id) continue
|
|
1378
|
+
if (m.info.time.created <= boundaryTime) continue
|
|
1379
|
+
for (const part of m.parts) {
|
|
1380
|
+
if (part.type !== "tool") continue
|
|
1381
|
+
if (!COMPACTABLE_TOOL_NAMES.has(part.tool)) continue
|
|
1382
|
+
if (part.state.status !== "completed") continue
|
|
1383
|
+
if (part.state.time.compacted) continue
|
|
1384
|
+
part.state.time.compacted = Date.now()
|
|
1385
|
+
yield* session.updatePart(part)
|
|
1386
|
+
cleared += 1
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (cleared > 0) {
|
|
1390
|
+
log.info("rebuild microcompact", { sessionID: input.sessionID, cleared })
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
return true
|
|
1394
|
+
})
|
|
1395
|
+
|
|
1396
|
+
return Service.of({
|
|
1397
|
+
tryStartCheckpointWriter,
|
|
1398
|
+
waitForWriter,
|
|
1399
|
+
drainWriters,
|
|
1400
|
+
hasCheckpoint,
|
|
1401
|
+
hasMemoryOrTasks,
|
|
1402
|
+
loadLatest,
|
|
1403
|
+
loadCheckpoints,
|
|
1404
|
+
renderIndex,
|
|
1405
|
+
renderRebuildContext,
|
|
1406
|
+
lastBoundary,
|
|
1407
|
+
isWriterRunning,
|
|
1408
|
+
insertRebuildBoundary,
|
|
1409
|
+
})
|
|
1410
|
+
}),
|
|
1411
|
+
)
|
|
1412
|
+
|
|
1413
|
+
// ---------------------------------------------------------------------------
|
|
1414
|
+
// Default layer
|
|
1415
|
+
// ---------------------------------------------------------------------------
|
|
1416
|
+
|
|
1417
|
+
// `defaultLayer` no longer requires `Actor.Service`: SessionCheckpoint reaches
|
|
1418
|
+
// the Actor implementation through the late-bound `spawnRef` (see
|
|
1419
|
+
// `actor/spawn-ref.ts`). This deliberately breaks the otherwise-unresolvable
|
|
1420
|
+
// layer cycle Actor → SessionPrompt → SessionCheckpoint → Actor. The AppLayer
|
|
1421
|
+
// constructs `Actor.defaultLayer` separately; its initialiser populates
|
|
1422
|
+
// `spawnRef`, which `tryStartCheckpointWriter` reads at call time.
|
|
1423
|
+
export const defaultLayer = Layer.suspend(() =>
|
|
1424
|
+
layer.pipe(
|
|
1425
|
+
Layer.provide(Session.defaultLayer),
|
|
1426
|
+
Layer.provide(Bus.layer),
|
|
1427
|
+
Layer.provide(Config.defaultLayer),
|
|
1428
|
+
Layer.provide(Memory.defaultLayer),
|
|
1429
|
+
Layer.provide(TaskRegistry.defaultLayer),
|
|
1430
|
+
Layer.provide(ActorRegistry.defaultLayer),
|
|
1431
|
+
),
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
// ---------------------------------------------------------------------------
|
|
1435
|
+
// Convenience wrappers
|
|
1436
|
+
// ---------------------------------------------------------------------------
|
|
1437
|
+
|
|
1438
|
+
const { runPromise } = makeRuntime(Service, defaultLayer)
|
|
1439
|
+
|
|
1440
|
+
export async function hasCheckpoint(input: { sessionID: SessionID }) {
|
|
1441
|
+
return runPromise((svc) => svc.hasCheckpoint(input.sessionID))
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
export async function loadLatest(input: { sessionID: SessionID }) {
|
|
1445
|
+
return runPromise((svc) => svc.loadLatest(input.sessionID))
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
export async function loadCheckpoints(input: { sessionID: SessionID; count: number }) {
|
|
1449
|
+
return runPromise((svc) => svc.loadCheckpoints(input.sessionID, input.count))
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
export async function renderIndex(input: { sessionID: SessionID }) {
|
|
1453
|
+
return runPromise((svc) => svc.renderIndex(input.sessionID))
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
export async function renderRebuildContext(input: {
|
|
1457
|
+
sessionID: SessionID
|
|
1458
|
+
lastMessageInfo?: LastMessageInfo
|
|
1459
|
+
agentID?: string
|
|
1460
|
+
}) {
|
|
1461
|
+
return runPromise((svc) =>
|
|
1462
|
+
svc.renderRebuildContext(input.sessionID, { lastMessageInfo: input.lastMessageInfo, agentID: input.agentID }),
|
|
1463
|
+
)
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
export async function lastBoundary(input: { sessionID: SessionID }) {
|
|
1467
|
+
return runPromise((svc) => svc.lastBoundary(input.sessionID))
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
export async function isWriterRunning(input: { sessionID: SessionID }) {
|
|
1471
|
+
return runPromise((svc) => svc.isWriterRunning(input.sessionID))
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
export * as SessionCheckpoint from "./checkpoint"
|
|
1475
|
+
|
|
1476
|
+
// Test-only re-export so test code can call composeWriterPrompt without
|
|
1477
|
+
// triggering the full SessionCheckpoint Service stack.
|
|
1478
|
+
export { composeWriterPrompt as composeWriterPromptForTest }
|