@co0ontty/wand 4.106.0 → 4.106.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/ai-text-runner.js +24 -14
- package/dist/build-info.d.ts +2 -0
- package/dist/build-info.js +18 -0
- package/dist/build-info.json +3 -3
- package/dist/cli.js +8 -2
- package/dist/daemon-maintenance-state.js +1 -1
- package/dist/daemon-maintenance-targets.d.ts +1 -0
- package/dist/daemon-maintenance-targets.js +25 -9
- package/dist/daemon-maintenance.d.ts +3 -0
- package/dist/daemon-maintenance.js +60 -13
- package/dist/employee-im-types.d.ts +1 -1
- package/dist/employee-input-scheduling.d.ts +4 -0
- package/dist/employee-knowledge-cli.js +5 -5
- package/dist/employee-knowledge.d.ts +3 -1
- package/dist/employee-knowledge.js +24 -8
- package/dist/git-quick-commit.d.ts +2 -0
- package/dist/git-quick-commit.js +15 -2
- package/dist/git-worktree-batch.d.ts +26 -0
- package/dist/git-worktree-batch.js +139 -0
- package/dist/global-knowledge.d.ts +12 -0
- package/dist/global-knowledge.js +124 -0
- package/dist/iteration-log.d.ts +13 -5
- package/dist/iteration-log.js +120 -59
- package/dist/knowledge-types.d.ts +12 -1
- package/dist/knowledge-types.js +4 -0
- package/dist/middleware/api-scope.js +1 -0
- package/dist/model-groups.js +6 -3
- package/dist/models/parsers/opencode.js +31 -1
- package/dist/models.js +15 -14
- package/dist/npm-update-utils.d.ts +5 -2
- package/dist/npm-update-utils.js +34 -4
- package/dist/opencode-cli.d.ts +6 -0
- package/dist/opencode-cli.js +51 -0
- package/dist/opencode-system-fallback.d.ts +1 -0
- package/dist/opencode-system-fallback.js +16 -4
- package/dist/pi-native-event-bridge.js +55 -3
- package/dist/pi-native-input.d.ts +22 -0
- package/dist/pi-native-input.js +77 -0
- package/dist/pi-tmux-host.d.ts +14 -5
- package/dist/pi-tmux-host.js +68 -17
- package/dist/process-manager.js +17 -3
- package/dist/pty-tmux-backend.d.ts +27 -2
- package/dist/pty-tmux-backend.js +471 -70
- package/dist/pty-tmux-log.d.ts +1 -1
- package/dist/pty-tmux-log.js +23 -2
- package/dist/retention-protection.d.ts +1 -1
- package/dist/retention-protection.js +3 -2
- package/dist/retention.d.ts +5 -0
- package/dist/retention.js +11 -0
- package/dist/server-knowledge-routes.js +2 -2
- package/dist/server-session-input-routes.js +38 -19
- package/dist/server-session-lifecycle-routes.js +14 -1
- package/dist/server-session-worktree-routes.js +105 -12
- package/dist/server-task-routes.js +41 -12
- package/dist/server-workspace-routes.js +77 -9
- package/dist/server.js +67 -35
- package/dist/session-registry.d.ts +17 -3
- package/dist/session-registry.js +66 -13
- package/dist/session-service.js +1 -1
- package/dist/storage/internal.d.ts +4 -3
- package/dist/storage/internal.js +3 -3
- package/dist/storage/knowledge-repository.d.ts +5 -0
- package/dist/storage/knowledge-repository.js +75 -16
- package/dist/storage/mappers.d.ts +1 -0
- package/dist/storage/mappers.js +6 -1
- package/dist/storage/migrations.js +60 -0
- package/dist/storage/session-input-repository.d.ts +19 -0
- package/dist/storage/session-input-repository.js +35 -0
- package/dist/storage/task-workflow-repository.d.ts +29 -0
- package/dist/storage/task-workflow-repository.js +110 -0
- package/dist/storage/tasks-repository.d.ts +6 -1
- package/dist/storage/tasks-repository.js +94 -34
- package/dist/storage/workspaces-repository.d.ts +7 -0
- package/dist/storage/workspaces-repository.js +28 -2
- package/dist/storage-cleanup.d.ts +7 -2
- package/dist/storage-cleanup.js +16 -0
- package/dist/storage-schema.js +2 -0
- package/dist/storage.d.ts +5 -1
- package/dist/storage.js +12 -0
- package/dist/structured-exec-host.d.ts +4 -0
- package/dist/structured-exec-pump.d.ts +1 -0
- package/dist/structured-exec-pump.js +18 -11
- package/dist/structured-opencode-adapter.d.ts +2 -1
- package/dist/structured-opencode-adapter.js +32 -8
- package/dist/structured-pi-adapter.js +13 -2
- package/dist/structured-runner.d.ts +6 -0
- package/dist/structured-session-manager-replay.d.ts +2 -0
- package/dist/structured-session-manager-replay.js +27 -0
- package/dist/structured-session-manager.d.ts +32 -21
- package/dist/structured-session-manager.js +395 -118
- package/dist/structured-tmux-host.d.ts +23 -2
- package/dist/structured-tmux-host.js +143 -20
- package/dist/task-actions.js +5 -3
- package/dist/task-archive-policy.js +2 -1
- package/dist/task-comment-dispatch.d.ts +2 -1
- package/dist/task-comment-dispatch.js +19 -6
- package/dist/task-execution-queue.d.ts +2 -0
- package/dist/task-execution-queue.js +10 -10
- package/dist/task-hierarchy.js +1 -1
- package/dist/task-read-model.d.ts +1 -1
- package/dist/task-read-model.js +2 -1
- package/dist/task-types.d.ts +34 -3
- package/dist/task-types.js +21 -5
- package/dist/task-workflow-cli.d.ts +2 -0
- package/dist/task-workflow-cli.js +29 -0
- package/dist/task-workflow-runner.d.ts +4 -0
- package/dist/task-workflow-runner.js +36 -0
- package/dist/task-workflow.d.ts +13 -0
- package/dist/task-workflow.js +48 -0
- package/dist/terminal-daemon-server.js +12 -4
- package/dist/tmux-policy.d.ts +9 -0
- package/dist/tmux-policy.js +46 -0
- package/dist/tmux-terminal-host.js +5 -2
- package/dist/tui/commands.js +2 -1
- package/dist/types.d.ts +5 -0
- package/dist/wand-task-sync.js +5 -0
- package/dist/web-ui/content/ai-teams.js +9 -9
- package/dist/web-ui/content/module-manifest.json +1 -1
- package/dist/web-ui/content/modules/{52574891ef08e29b → 014fa5c6f973badf}/host.js +3 -3
- package/dist/web-ui/content/modules/0818b1d31a11d006/employee-profile.js +1 -0
- package/dist/web-ui/content/modules/{31157ceb6d23b809 → 0a58d9ec273078e4}/chunk.js +3 -3
- package/dist/web-ui/content/modules/{bf3ee58e0da44822 → 0a979dcd165cd0cc}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{5f298a8f43dd0589 → 0c143928e8f96abc}/chunk.js +1 -1
- package/dist/web-ui/content/modules/0e9e42a319f6b8fc/workspace-window.js +1 -0
- package/dist/web-ui/content/modules/{b713cad51ca4c512 → 0f0d1769b732c0bd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{4970319a83432ec7 → 1447886e6a1e74a4}/chunk.js +1 -1
- package/dist/web-ui/content/modules/151c734cbca31da5/host.js +1 -0
- package/dist/web-ui/content/modules/{e5174201f9d918fb → 16e4ab9c5263fcfd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/17e752fb53952753/host.js +1 -0
- package/dist/web-ui/content/modules/{913187c92a98a1ac → 20ed1454fc09fa6f}/chunk.js +1 -1
- package/dist/web-ui/content/modules/255a3baf59d2b734/home.js +6 -0
- package/dist/web-ui/content/modules/{2f801e53dda62770 → 296dd81886c42b45}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{8891ca8165435ca1 → 2a01f4e525c0725e}/chunk.js +1 -1
- package/dist/web-ui/content/modules/2ab4409c82b44fba/markdown.js +1 -0
- package/dist/web-ui/content/modules/2c955eced11610c2/chunk.js +1 -0
- package/dist/web-ui/content/modules/2d7ee4f9e2c0aca4/panel.js +1 -0
- package/dist/web-ui/content/modules/{3f8830f8ad28decc → 33ae550e4a295c85}/chunk.js +1 -1
- package/dist/web-ui/content/modules/35f4aa4443696bd0/chunk.js +1 -0
- package/dist/web-ui/content/modules/388b37ffcce9d4ff/host.js +1 -0
- package/dist/web-ui/content/modules/39279604dcdac7c7/runtime.js +338 -0
- package/dist/web-ui/content/modules/{57c357d321db5df9 → 3a3cdff2a662b7d3}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{d49f106c4d106057 → 3ab646be55514243}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{16dab1297efd71a7 → 3ae88a5c5e1949d2}/chunk.js +1 -1
- package/dist/web-ui/content/modules/3e3b0e71d6ce80d8/chunk.js +1 -0
- package/dist/web-ui/content/modules/{ec33e17c00c1eb24 → 434aaaaf9604dc34}/chunk.js +1 -1
- package/dist/web-ui/content/modules/4385cd48771e1d4f/host.js +1 -0
- package/dist/web-ui/content/modules/{face3dbf117d63e3 → 4638129d60f549a0}/chunk.js +1 -1
- package/dist/web-ui/content/modules/47407209931b7011/host.js +13 -0
- package/dist/web-ui/content/modules/4829dd712d8b366b/chunk.js +1 -0
- package/dist/web-ui/content/modules/488d5b747bf374c8/chunk.js +1 -0
- package/dist/web-ui/content/modules/{ed7405f064944215 → 4ab085c195f7d76a}/sha256.js +1 -1
- package/dist/web-ui/content/modules/{f27a5d3c6c02a71c → 4c485bd44740fec7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{771439a41e737ee2 → 4ca0494308ab2bb3}/chunk.js +1 -1
- package/dist/web-ui/content/modules/4ec571218779ba3e/composer-surface-adapter.js +1 -0
- package/dist/web-ui/content/modules/{5933ce8965aa4004 → 5023bf0294c2e7cb}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{e63582a3bce48f7d → 5456e3417600bf15}/chunk.js +1 -1
- package/dist/web-ui/content/modules/55c588b0f5b41a52/host-bridge.js +1 -0
- package/dist/web-ui/content/modules/{9d941d6ff39861b8 → 5c3c8237ba7d8460}/chunk.js +1 -1
- package/dist/web-ui/content/modules/624b858da9834757/host.js +1 -0
- package/dist/web-ui/content/modules/6290be936320d4b1/host.js +1 -0
- package/dist/web-ui/content/modules/66be408793048cc3/progress.js +1 -0
- package/dist/web-ui/content/modules/67a11176a563317b/chunk.js +1 -0
- package/dist/web-ui/content/modules/{e060508ecd50fa7e → 68ef6ec7d26d78cd}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{41445c6589dba325 → 6ec3e41df31927d9}/host.js +2 -2
- package/dist/web-ui/content/modules/{97d7f267959fd385 → 6f10f369445bbcbe}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{d47d75a78ae598e3 → 7295d9ff92d5e1e7}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{f383d892fef73483 → 72cd613de1a5f69b}/chunk.js +1 -1
- package/dist/web-ui/content/modules/73aae1daafeea7e7/workspaces-panel.js +4 -0
- package/dist/web-ui/content/modules/{c2f09c8a472f5d83 → 73d2689f13b9a3e9}/dialog-modal.js +1 -1
- package/dist/web-ui/content/modules/{2f97ddba3842adaa → 75cf0c048e9b92ab}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{4ba1e8c74bed7ef2 → 75dabda3b141296e}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{f3cff7792ba39df8 → 76c10b58d5f92d02}/login-visual.js +1 -1
- package/dist/web-ui/content/modules/{b63e075f222a1266 → 783583a12f5010f8}/library-bridge.js +1 -1
- package/dist/web-ui/content/modules/7e13a8a4a981f2f1/group-editor.js +1 -0
- package/dist/web-ui/content/modules/{0d76e582a7a71b2f → 7e3034fea34c6dae}/chunk.js +1 -1
- package/dist/web-ui/content/modules/80bf3291f2577a68/host.js +1 -0
- package/dist/web-ui/content/modules/{cba6f13c0b2aec1c → 82c3e08f34a6b828}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{dedd4a19bb70e327 → 83d1f4cf82c65f19}/page.js +3 -3
- package/dist/web-ui/content/modules/844f97958f25240c/shell-file-panel-drawer.js +1 -0
- package/dist/web-ui/content/modules/{62e0023451f428cf → 899cdc467044d418}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{5083a5dbb5b155b7 → 8c7ca1fe72741c07}/chunk.js +1 -1
- package/dist/web-ui/content/modules/8dae4ca2261525e4/task-board-host.js +27 -0
- package/dist/web-ui/content/modules/92195c14c74929da/list-filter-panel.js +1 -0
- package/dist/web-ui/content/modules/{a61393bfa976d8d5 → 93793696af76bcbf}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{db521a246e14e5e4 → 97cd6af3da5622b8}/chunk.js +4 -4
- package/dist/web-ui/content/modules/{8de8368570ab089d → 9d3179258249ec77}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{579d486763d47c0d → a234c484078c0568}/chunk.js +1 -1
- package/dist/web-ui/content/modules/a4d215448a089a7d/host.js +1 -0
- package/dist/web-ui/content/modules/{c0a446100d969f3e → ae003f89901c5687}/chunk.js +1 -1
- package/dist/web-ui/content/modules/ae712bdd45b9917e/employee-card.js +1 -0
- package/dist/web-ui/content/modules/{a2caed0139df4b21 → b2a15293f7965d09}/chunk.js +1 -1
- package/dist/web-ui/content/modules/b4ee65e9998f0088/host.js +61 -0
- package/dist/web-ui/content/modules/{bd3daf04a8bf4422 → b666137d17f83be9}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{831b6c7b43e800be → b66beb6c0185256b}/host.js +1 -1
- package/dist/web-ui/content/modules/{816eb9b3a000fb2c → bb03b17e877657de}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{1eab7410d5586038 → bc343afc991db1a1}/chunk.js +1 -1
- package/dist/web-ui/content/modules/bca45976ff8fe2ac/host.js +69 -0
- package/dist/web-ui/content/modules/{d434edbb2f4672e5 → c2715dbfc9eaf510}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{d6fffe2c2a3a738d → c30c220281f35d74}/toast-region.js +1 -1
- package/dist/web-ui/content/modules/c4cc46fa9b4adddc/dropdown-surface.js +1 -0
- package/dist/web-ui/content/modules/c6dd19ee3d3ffd59/brand-identity.js +1 -0
- package/dist/web-ui/content/modules/c7ea6a114e6ff8ae/host.js +1 -0
- package/dist/web-ui/content/modules/{a59734be368daa7e → c916d82c8acef9c8}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{182929e188c1ab3a → cceaedda619a737a}/host.js +1 -1
- package/dist/web-ui/content/modules/cf67f52b076b9d2d/panel-host.js +1 -0
- package/dist/web-ui/content/modules/{9e163d54ff8fd1aa → d26c86558d422313}/chunk.js +1 -1
- package/dist/web-ui/content/modules/d2b8c6c6f3706032/queue-view-adapter.js +1 -0
- package/dist/web-ui/content/modules/d52ebe2ef12a9fa8/chunk.js +3 -0
- package/dist/web-ui/content/modules/{3fe11a75284ad8ae → d552b3bff2151eb6}/host.js +2 -2
- package/dist/web-ui/content/modules/d5d6b4511ac76a6e/chunk.js +3 -0
- package/dist/web-ui/content/modules/{bd7dc4bb46731ee7 → df42e151d625463b}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{2a791cd2e14aa1e0 → e0a04f5ae56c68aa}/notice-view.js +1 -1
- package/dist/web-ui/content/modules/{438807b8bf112cd0 → e28e607c95d6f63a}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{50da446c1377c220 → e3880df97ea62d0c}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{87c7f50d9455d55e → e66196f1ece3e3ad}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{9b12c2301f3a18f3 → eb76307444ac9d71}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{c33935155289c22d → ebff7ca22cee1468}/chunk.js +1 -1
- package/dist/web-ui/content/modules/{4861eca9a8a87ad0 → ed1c712c569960d6}/chunk.js +1 -1
- package/dist/web-ui/content/modules/ee8b9c2d4bec63d4/chunk.js +1 -0
- package/dist/web-ui/content/modules/f458a194ce4208e2/chunk.js +1 -0
- package/dist/web-ui/content/modules/{11af5d14b30e57b6 → f938e826420cfe1b}/chunk.js +1 -1
- package/dist/web-ui/content/modules/fa7b9bf6cc84bfc1/chat-render.js +81 -0
- package/dist/web-ui/content/modules/{ea8fe4b9999fd2aa → fd1fb1834f5f06df}/chunk.js +1 -1
- package/dist/web-ui/content/modules/fd3084e562a3dbd6/host.js +1 -0
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +416 -413
- package/dist/web-ui/embedded-assets.js +5 -5
- package/dist/web-ui/page-route.js +2 -2
- package/dist/web-ui/react/workspaces/types.d.ts +3 -0
- package/dist/web-ui/thinking-efforts.d.ts +2 -0
- package/dist/web-ui/thinking-efforts.js +8 -0
- package/package.json +1 -2
- package/scripts/stage-render-binaries.js +0 -1
- package/skills/wand-knowledge/SKILL.md +5 -3
- package/dist/structured-bounded-keys.d.ts +0 -19
- package/dist/structured-bounded-keys.js +0 -47
- package/dist/web-ui/content/modules/00b168a19664439e/dropdown-surface.js +0 -1
- package/dist/web-ui/content/modules/059c5fb7f128d340/task-board-host.js +0 -27
- package/dist/web-ui/content/modules/07494f5a623e4471/chat-render.js +0 -76
- package/dist/web-ui/content/modules/0a743ad3b4db6874/chunk.js +0 -3
- package/dist/web-ui/content/modules/1858670c0f84fec5/employee-profile.js +0 -1
- package/dist/web-ui/content/modules/20c47dc0442f5a04/host.js +0 -1
- package/dist/web-ui/content/modules/226241b999d12008/host.js +0 -1
- package/dist/web-ui/content/modules/23c4b8d223ec64c5/host.js +0 -61
- package/dist/web-ui/content/modules/2882c97388ff4f7b/employee-card.js +0 -1
- package/dist/web-ui/content/modules/2c90c3be260e6cce/progress.js +0 -1
- package/dist/web-ui/content/modules/2d7be9935394f397/workspace-window.js +0 -1
- package/dist/web-ui/content/modules/389a5112332ec23a/host.js +0 -1
- package/dist/web-ui/content/modules/3ea13e8b3c2deaa4/panel-host.js +0 -1
- package/dist/web-ui/content/modules/42e5c34002fcffb7/chunk.js +0 -1
- package/dist/web-ui/content/modules/4a5d669c90f545cb/host.js +0 -1
- package/dist/web-ui/content/modules/5705d25468274534/host.js +0 -1
- package/dist/web-ui/content/modules/5961ab2947c86ca1/shell-file-panel-drawer.js +0 -1
- package/dist/web-ui/content/modules/5b6e1369df7dea69/chunk.js +0 -1
- package/dist/web-ui/content/modules/6787bdcf56f743a5/host.js +0 -1
- package/dist/web-ui/content/modules/67c39b0398186a9f/host.js +0 -1
- package/dist/web-ui/content/modules/6b16f35c0627bafb/chunk.js +0 -1
- package/dist/web-ui/content/modules/6dd4f3e2f619f7a0/chunk.js +0 -1
- package/dist/web-ui/content/modules/70a2e13e0ccc8549/panel.js +0 -1
- package/dist/web-ui/content/modules/710db590e9707f73/runtime.js +0 -338
- package/dist/web-ui/content/modules/7249950f8771dbeb/workspaces-panel.js +0 -4
- package/dist/web-ui/content/modules/87dcd516ffd9f688/composer-surface-adapter.js +0 -1
- package/dist/web-ui/content/modules/8b7cd7bffdfd69e7/brand-identity.js +0 -1
- package/dist/web-ui/content/modules/8bf9917f6c3429f6/host.js +0 -1
- package/dist/web-ui/content/modules/8e612d4881fa4118/home.js +0 -6
- package/dist/web-ui/content/modules/8e6178262ea63dec/markdown.js +0 -1
- package/dist/web-ui/content/modules/96f2ae970f8c576b/host.js +0 -1
- package/dist/web-ui/content/modules/ba89cd97627e6682/queue-view-adapter.js +0 -1
- package/dist/web-ui/content/modules/c351eb11e0bdec2c/sidebar-peek.js +0 -1
- package/dist/web-ui/content/modules/ca7ca5902a35d26e/chunk.js +0 -1
- package/dist/web-ui/content/modules/d5d28ec375826ef2/chunk.js +0 -3
- package/dist/web-ui/content/modules/d6b16dedb9011c33/chunk.js +0 -1
- package/dist/web-ui/content/modules/d8a7011760f61719/group-editor.js +0 -1
- package/dist/web-ui/content/modules/d9e2b65e9679ad39/host.js +0 -75
- package/dist/web-ui/content/modules/eac72f69716dba05/chunk.js +0 -1
- package/dist/web-ui/content/modules/ebc53dedc3945514/host.js +0 -1
- package/dist/web-ui/content/modules/f43da7050badf54c/host.js +0 -1
- package/dist/web-ui/content/modules/fb2356eced24c4cc/host-bridge.js +0 -1
|
@@ -3,10 +3,9 @@ import { resolveAutoAssign } from "./model-auto-assign.js";
|
|
|
3
3
|
import { withModelGroups } from "./model-group-runner.js";
|
|
4
4
|
import { classifyStructuredFailure, hasStructuredExecutionProgress } from "./structured-failure.js";
|
|
5
5
|
import { withAutomaticPiResources } from "./pi-auto-resources.js";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { waitForSessionProcessExit } from "./session-artifact-cleanup.js";
|
|
8
8
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
9
|
-
import { BoundedKeySet } from "./structured-bounded-keys.js";
|
|
10
9
|
import { buildChildEnv } from "./env-utils.js";
|
|
11
10
|
import { getErrorMessage } from "./error-utils.js";
|
|
12
11
|
import { fallbackProviderModel } from "./session-model.js";
|
|
@@ -36,6 +35,7 @@ import { QoderRunner } from "./structured-qoder-adapter.js";
|
|
|
36
35
|
import { PiRunner, isMissingPiSession } from "./structured-pi-adapter.js";
|
|
37
36
|
import { PiOutputFile } from "./pi-output-file.js";
|
|
38
37
|
import { PiProtocolReader } from "./pi-protocol-reader.js";
|
|
38
|
+
import { upsertPiAssistantSegment } from "./structured-session-manager-replay.js";
|
|
39
39
|
import { structuredRunId } from "./structured-exec-host.js";
|
|
40
40
|
import { defaultStructuredRunner, defaultStructuredState, isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner } from "./structured-provider-common.js";
|
|
41
41
|
import { TurnQuietHeartbeat } from "./turn-heartbeat.js";
|
|
@@ -79,19 +79,18 @@ const EMPLOYEE_CANDIDATE_STARTUP_TIMEOUT_MS = 180_000;
|
|
|
79
79
|
// second on the event loop.
|
|
80
80
|
const STREAM_SAVE_THROTTLE_MS = 1_000;
|
|
81
81
|
const DETACHED_RECOVERY_RETRY_MS = 500;
|
|
82
|
-
/** Upper bound on remembered (session, idempotency key) pairs; oldest half is dropped past it. */
|
|
83
|
-
const IDEMPOTENCY_KEY_LIMIT = 1024;
|
|
84
82
|
/** The turn recorded for one user prompt; an open AskUserQuestion is answered by it as a tool_result. */
|
|
85
|
-
function buildUserTurn(prompt, pendingAsk, createdAt, requestId) {
|
|
83
|
+
function buildUserTurn(prompt, pendingAsk, createdAt, requestId, messageId) {
|
|
84
|
+
const identity = { ...(requestId ? { requestId } : {}), ...(messageId ? { messageId } : {}) };
|
|
86
85
|
if (pendingAsk) {
|
|
87
86
|
return {
|
|
88
87
|
role: "user",
|
|
89
|
-
...
|
|
88
|
+
...identity,
|
|
90
89
|
createdAt,
|
|
91
90
|
content: [{ type: "tool_result", tool_use_id: pendingAsk.id, content: prompt, is_error: false }],
|
|
92
91
|
};
|
|
93
92
|
}
|
|
94
|
-
return { role: "user", ...
|
|
93
|
+
return { role: "user", ...identity, createdAt, content: [{ type: "text", text: prompt }] };
|
|
95
94
|
}
|
|
96
95
|
export class StructuredSessionManager {
|
|
97
96
|
storage;
|
|
@@ -122,19 +121,12 @@ export class StructuredSessionManager {
|
|
|
122
121
|
interruptedImmediateStart = new Set();
|
|
123
122
|
employeeInputAdmission = null;
|
|
124
123
|
inputSequence = 0;
|
|
124
|
+
dispatchingAcceptedInputs = new Set();
|
|
125
125
|
/** Last wall-clock time (ms) a streaming checkpoint reached SQLite. */
|
|
126
126
|
lastStreamSaveAt = new Map();
|
|
127
127
|
lastPiCheckpointAt = new Map();
|
|
128
128
|
streamCheckpointTimers = new Map();
|
|
129
129
|
streamCheckpointDirty = new Map();
|
|
130
|
-
/**
|
|
131
|
-
* Idempotency keys we've already accepted, mapped to their wall-clock timestamp.
|
|
132
|
-
* Android WebView 在进程恢复时偶尔会重发上一个未收到响应的 POST(HTTP/2 stream
|
|
133
|
-
* reset 等场景),客户端 JS 没有重试逻辑也拦不住。这里用 (sessionId, key) 永
|
|
134
|
-
* 久去重,重复就抛错让前端弹 toast 提示,**不**做任何处理。timestamp 仅用于
|
|
135
|
-
* map 大小溢出时按时间裁剪。
|
|
136
|
-
*/
|
|
137
|
-
seenIdempotencyKeys = new BoundedKeySet(IDEMPOTENCY_KEY_LIMIT);
|
|
138
130
|
relayHandlers = new Map();
|
|
139
131
|
systemCallDrainReleases = new Set();
|
|
140
132
|
emitEvent = null;
|
|
@@ -163,6 +155,7 @@ export class StructuredSessionManager {
|
|
|
163
155
|
unsupportedStructuredProviders = new Set();
|
|
164
156
|
/** Structured CLI runs that were mid-flight when the previous web process died. */
|
|
165
157
|
pendingRecoveryIds = [];
|
|
158
|
+
detachedInventoryReconciled = false;
|
|
166
159
|
detachedRecoveryPromise = null;
|
|
167
160
|
detachedRecoveryRetryTimer = null;
|
|
168
161
|
detachedRecoveryFailureLogged = false;
|
|
@@ -309,8 +302,9 @@ export class StructuredSessionManager {
|
|
|
309
302
|
&& !(this.coreRestartDrains > 0 && this.usesCoreEngine(session)))
|
|
310
303
|
.flatMap(session => {
|
|
311
304
|
const metadata = queuedInputScheduling(session);
|
|
312
|
-
const
|
|
313
|
-
const
|
|
305
|
+
const available = (item) => !item.acceptanceId || !this.dispatchingAcceptedInputs.has(item.acceptanceId);
|
|
306
|
+
const userIndex = metadata.findIndex(item => available(item) && item.source === "user");
|
|
307
|
+
const index = userIndex >= 0 ? userIndex : metadata.findIndex(available);
|
|
314
308
|
// 同一会话只暴露最高来源档的队首,保留用户显式 reorder 的意义。
|
|
315
309
|
return metadata[index] ? [{ session, index, scheduling: metadata[index] }] : [];
|
|
316
310
|
});
|
|
@@ -449,7 +443,7 @@ export class StructuredSessionManager {
|
|
|
449
443
|
// ---------------------------------------------------------------------------
|
|
450
444
|
/** Called once after startup wiring; transient daemon failures retry automatically. */
|
|
451
445
|
recoverDetachedRuns() {
|
|
452
|
-
if (this.disposed || this.execHost?.persistent !== true || this.pendingRecoveryIds.length === 0) {
|
|
446
|
+
if (this.disposed || this.execHost?.persistent !== true || (this.detachedInventoryReconciled && this.pendingRecoveryIds.length === 0)) {
|
|
453
447
|
return Promise.resolve();
|
|
454
448
|
}
|
|
455
449
|
if (this.detachedRecoveryPromise)
|
|
@@ -485,6 +479,23 @@ export class StructuredSessionManager {
|
|
|
485
479
|
}
|
|
486
480
|
if (this.disposed)
|
|
487
481
|
return;
|
|
482
|
+
// The durable UI status may have failed before the actual process exited.
|
|
483
|
+
// Inventory is authoritative: recover those live owners too, never spawn a
|
|
484
|
+
// replacement merely because a previous observer wrote idle/failed.
|
|
485
|
+
for (const run of runs) {
|
|
486
|
+
if (!run.runId.startsWith("structured:"))
|
|
487
|
+
continue;
|
|
488
|
+
const id = run.runId.slice("structured:".length);
|
|
489
|
+
const session = this.sessions.get(id);
|
|
490
|
+
if (!session || this.usesCoreEngine(session) || this.unsupportedStructuredProviders.has(id)
|
|
491
|
+
|| this.pendingRecoveryIds.includes(id) || this.pendingRunnerExecutions.has(id))
|
|
492
|
+
continue;
|
|
493
|
+
if (run.status === "running")
|
|
494
|
+
this.pendingRecoveryIds.push(id);
|
|
495
|
+
else
|
|
496
|
+
this.execHost.forgetRun(run.runId);
|
|
497
|
+
}
|
|
498
|
+
this.detachedInventoryReconciled = true;
|
|
488
499
|
const ids = [...this.pendingRecoveryIds];
|
|
489
500
|
const byRunId = new Map(runs.map((run) => [run.runId, run]));
|
|
490
501
|
for (const sessionId of ids) {
|
|
@@ -495,10 +506,13 @@ export class StructuredSessionManager {
|
|
|
495
506
|
}
|
|
496
507
|
const state = byRunId.get(structuredRunId(sessionId));
|
|
497
508
|
if (!state) {
|
|
509
|
+
if (session.structuredState?.activeRequestId)
|
|
510
|
+
this.settleNativeInputs(sessionId, session.structuredState.activeRequestId, { blocks: [], result: "", sessionId: session.claudeSessionId }, true);
|
|
511
|
+
const reconciled = this.requireSession(sessionId);
|
|
498
512
|
// Inventory was read successfully, so this is a genuinely lost run,
|
|
499
513
|
// not a temporary control-plane outage. Commit the interrupted view.
|
|
500
514
|
const interrupted = {
|
|
501
|
-
...
|
|
515
|
+
...reconciled,
|
|
502
516
|
status: "idle",
|
|
503
517
|
exitCode: null,
|
|
504
518
|
endedAt: null,
|
|
@@ -539,7 +553,7 @@ export class StructuredSessionManager {
|
|
|
539
553
|
this.scheduleDetachedRecovery();
|
|
540
554
|
}
|
|
541
555
|
scheduleDetachedRecovery() {
|
|
542
|
-
if (this.disposed || this.pendingRecoveryIds.length === 0 || this.detachedRecoveryRetryTimer)
|
|
556
|
+
if (this.disposed || (this.detachedInventoryReconciled && this.pendingRecoveryIds.length === 0) || this.detachedRecoveryRetryTimer)
|
|
543
557
|
return;
|
|
544
558
|
this.detachedRecoveryRetryTimer = setTimeout(() => {
|
|
545
559
|
this.detachedRecoveryRetryTimer = null;
|
|
@@ -654,7 +668,9 @@ export class StructuredSessionManager {
|
|
|
654
668
|
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
655
669
|
usage: turnState.usage,
|
|
656
670
|
};
|
|
657
|
-
const messages =
|
|
671
|
+
const messages = turnState.inputMessageId
|
|
672
|
+
? upsertPiAssistantSegment(current.messages, turn, turnState.inputMessageId)
|
|
673
|
+
: upsertAssistantMessage(current.messages, turn, false, !replayingHistory);
|
|
658
674
|
const patched = {
|
|
659
675
|
...current,
|
|
660
676
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
@@ -690,6 +706,10 @@ export class StructuredSessionManager {
|
|
|
690
706
|
let lastStdoutSeq = initialState.stdoutSeq;
|
|
691
707
|
let lastStderrSeq = initialState.stderrSeq;
|
|
692
708
|
const feedLine = (line) => {
|
|
709
|
+
if (resumed.provider === "pi" && line.trim().startsWith("{")) {
|
|
710
|
+
const event = JSON.parse(line);
|
|
711
|
+
this.handleNativeInputEvent(sessionId, requestId, event, processor.state);
|
|
712
|
+
}
|
|
693
713
|
onApplied(processor.feed(line, replayingHistory ? undefined : new Date().toISOString()));
|
|
694
714
|
};
|
|
695
715
|
const piReader = resumed.provider === "pi" && resumed.structuredState?.engine !== "core"
|
|
@@ -715,10 +735,11 @@ export class StructuredSessionManager {
|
|
|
715
735
|
return;
|
|
716
736
|
fileFailed = true;
|
|
717
737
|
stdoutFile?.dispose();
|
|
718
|
-
processor.primaryError = `
|
|
738
|
+
processor.primaryError = `CLI 输出观察失败:${error.message};送达结果未知,不自动重试。`;
|
|
719
739
|
this.finalizeRecoveredRun(sessionId, requestId, processor, replayView, {
|
|
720
740
|
exitCode: 1, signal: null, stderr: processor.stderr, stdoutTruncated: true,
|
|
721
741
|
});
|
|
742
|
+
runningHandle?.interrupt();
|
|
722
743
|
flushEmit();
|
|
723
744
|
};
|
|
724
745
|
const captureStdout = (text) => {
|
|
@@ -880,6 +901,10 @@ export class StructuredSessionManager {
|
|
|
880
901
|
processor.stderr += event.data;
|
|
881
902
|
});
|
|
882
903
|
runningHandle.onExit((event) => {
|
|
904
|
+
if (event.processExited === false) {
|
|
905
|
+
fileFailure(new Error("CLI 输出连接中断,正在等待原进程停止;新消息会排队。"));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
883
908
|
if (fileFailed)
|
|
884
909
|
return;
|
|
885
910
|
try {
|
|
@@ -911,6 +936,7 @@ export class StructuredSessionManager {
|
|
|
911
936
|
this.releasePendingRunnerExecution(sessionId, execution);
|
|
912
937
|
else
|
|
913
938
|
this.execHost?.forgetRun(structuredRunId(sessionId));
|
|
939
|
+
this.settleNativeInputs(sessionId, requestId, processor.state);
|
|
914
940
|
const current = this.sessions.get(sessionId);
|
|
915
941
|
if (!current)
|
|
916
942
|
return;
|
|
@@ -1095,8 +1121,6 @@ export class StructuredSessionManager {
|
|
|
1095
1121
|
return;
|
|
1096
1122
|
this.unacceptedTeamStarts.delete(sessionId);
|
|
1097
1123
|
this.recoveredLogicalRequests.delete(sessionId);
|
|
1098
|
-
const keyPrefix = `${sessionId}:`;
|
|
1099
|
-
this.seenIdempotencyKeys.deleteMatching((key) => key.startsWith(keyPrefix));
|
|
1100
1124
|
}
|
|
1101
1125
|
/**
|
|
1102
1126
|
* Arms the per-session "no protocol output yet" watchdog. It is unref'd so it can
|
|
@@ -1136,8 +1160,10 @@ export class StructuredSessionManager {
|
|
|
1136
1160
|
this.clearStreamingCheckpoint(snapshot.id);
|
|
1137
1161
|
}
|
|
1138
1162
|
checkpointSessionMessages(snapshot, includeOutput = false) {
|
|
1139
|
-
this.storage.
|
|
1140
|
-
|
|
1163
|
+
this.storage.transaction(() => {
|
|
1164
|
+
this.storage.updateSessionRuntimeMetadata(snapshot);
|
|
1165
|
+
this.storage.checkpointSessionMessages(snapshot.id, snapshot.messages ?? [], snapshot.structuredState, includeOutput ? snapshot.output : undefined);
|
|
1166
|
+
});
|
|
1141
1167
|
}
|
|
1142
1168
|
list() {
|
|
1143
1169
|
return Array.from(this.sessions.values())
|
|
@@ -1504,6 +1530,138 @@ export class StructuredSessionManager {
|
|
|
1504
1530
|
if (this.disposed)
|
|
1505
1531
|
throw new Error("StructuredSessionManager has been disposed.");
|
|
1506
1532
|
let session = this.requireSession(id);
|
|
1533
|
+
// Scheduler/interrupt continuation consumes an already accepted item, never
|
|
1534
|
+
// creates a second acceptance or replays an uncertain external execution.
|
|
1535
|
+
if (opts?.scheduling?.acceptanceId)
|
|
1536
|
+
return this.sendAcceptedMessage(id, input, opts);
|
|
1537
|
+
const key = opts?.idempotencyKey;
|
|
1538
|
+
if (key !== undefined && (!key || Buffer.byteLength(key) > 256)) {
|
|
1539
|
+
throw Object.assign(new Error("幂等键必须为 1 至 256 字节。"), { code: "invalid_idempotency_key" });
|
|
1540
|
+
}
|
|
1541
|
+
const signature = createHash("sha256").update(JSON.stringify(opts?.promotion
|
|
1542
|
+
? { v: 1, action: "promote", index: opts.promotion.index, expectedText: opts.promotion.expectedText ?? null }
|
|
1543
|
+
: { v: 1, action: "message", input: input.trim(), interrupt: !!opts?.interrupt,
|
|
1544
|
+
preserveQueue: !!opts?.interrupt && !!opts?.preserveQueue, source: opts?.source ?? "task" })).digest("hex");
|
|
1545
|
+
const previous = key ? this.storage.getStructuredInputReceipt(id, key, signature) : null;
|
|
1546
|
+
if (previous) {
|
|
1547
|
+
opts?.onAccepted?.(previous);
|
|
1548
|
+
return session;
|
|
1549
|
+
}
|
|
1550
|
+
let prompt = input.trim();
|
|
1551
|
+
if (opts?.promotion) {
|
|
1552
|
+
this.requireRecoveryControl(id);
|
|
1553
|
+
this.assertNativeInputMutable(session);
|
|
1554
|
+
if (this.coreRestartDrains > 0 && this.usesCoreEngine(session)) {
|
|
1555
|
+
throw new Error("服务正在排空 Wand Agent,排队消息已保留,请稍后重试立即发送。");
|
|
1556
|
+
}
|
|
1557
|
+
const { index, expectedText } = opts.promotion;
|
|
1558
|
+
const queue = session.queuedMessages ?? [];
|
|
1559
|
+
if (!Number.isInteger(index) || index < 0 || index >= queue.length)
|
|
1560
|
+
throw new Error("队列中没有该条消息(可能已被处理)。");
|
|
1561
|
+
if (expectedText !== undefined && queue[index] !== expectedText)
|
|
1562
|
+
throw new Error("排队消息已变化,请按最新顺序重试。");
|
|
1563
|
+
prompt = queue[index];
|
|
1564
|
+
session = { ...session, queuedMessages: queue.filter((_, at) => at !== index),
|
|
1565
|
+
queuedInputScheduling: queuedInputScheduling(session).filter((_, at) => at !== index) };
|
|
1566
|
+
}
|
|
1567
|
+
if (!prompt) {
|
|
1568
|
+
opts?.onAccepted?.();
|
|
1569
|
+
return session;
|
|
1570
|
+
}
|
|
1571
|
+
if (this.unsupportedStructuredProviders.has(id))
|
|
1572
|
+
throw new Error("会话使用的 provider 已不受支持,不能继续发送。");
|
|
1573
|
+
const acceptanceId = randomUUID();
|
|
1574
|
+
const acceptedAt = isoNow();
|
|
1575
|
+
const receipt = key
|
|
1576
|
+
? { accepted: true, acceptanceId, sessionId: id, idempotencyKey: key, acceptedAt } : undefined;
|
|
1577
|
+
const scheduling = { ...(opts?.scheduling
|
|
1578
|
+
?? { source: opts?.source ?? "task", enqueuedAt: acceptedAt, sequence: ++this.inputSequence,
|
|
1579
|
+
teamRequestStarted: opts?.teamRequestStarted }), acceptanceId };
|
|
1580
|
+
const relay = this.relayHandlerFor(session);
|
|
1581
|
+
const childActive = this.pendingRunnerExecutions.has(id) || this.pendingRecoveryIds.includes(id) || this.nativeProcessExits.has(id);
|
|
1582
|
+
const interrupt = !!opts?.interrupt && !!session.structuredState?.inFlight && childActive;
|
|
1583
|
+
if (interrupt) {
|
|
1584
|
+
this.requireRecoveryControl(id);
|
|
1585
|
+
this.assertNativeInputMutable(session);
|
|
1586
|
+
}
|
|
1587
|
+
const mustQueue = !relay && ((this.coreRestartDrains > 0 && this.usesCoreEngine(session))
|
|
1588
|
+
|| this.autoAssignPending.has(id) || (childActive && !interrupt)
|
|
1589
|
+
|| (!!session.employeeId && !!this.employeeInputAdmission
|
|
1590
|
+
&& !this.employeeInputAdmission.canStart(session, scheduling, opts?.immediateStart)));
|
|
1591
|
+
if (mustQueue && !opts?.promotion) {
|
|
1592
|
+
if (isDuplicateStructuredQueueInput(session, prompt)) {
|
|
1593
|
+
throw Object.assign(new Error("与上一条消息相同,已忽略,不会加入排队。"), { code: "duplicate_queued_message" });
|
|
1594
|
+
}
|
|
1595
|
+
if ((session.queuedMessages?.length ?? 0) >= 10)
|
|
1596
|
+
throw new Error("排队消息已满(最多 10 条),请等待当前消息处理完成。");
|
|
1597
|
+
}
|
|
1598
|
+
let queue = session.queuedMessages ?? [];
|
|
1599
|
+
let metadata = queuedInputScheduling(session);
|
|
1600
|
+
if (interrupt && !opts?.preserveQueue) {
|
|
1601
|
+
queue = [];
|
|
1602
|
+
metadata = [];
|
|
1603
|
+
}
|
|
1604
|
+
if (interrupt && opts?.preserveQueue && !opts.queueAlreadyRemoved && !opts.promotion) {
|
|
1605
|
+
const at = queue.indexOf(prompt);
|
|
1606
|
+
if (at >= 0) {
|
|
1607
|
+
queue = queue.filter((_, index) => index !== at);
|
|
1608
|
+
metadata = metadata.filter((_, index) => index !== at);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const accepted = relay
|
|
1612
|
+
? { ...session, messages: [...(session.messages ?? []), buildUserTurn(prompt, undefined, acceptedAt)] }
|
|
1613
|
+
: { ...session, queuedMessages: interrupt ? [prompt, ...queue] : [...queue, prompt],
|
|
1614
|
+
queuedInputScheduling: interrupt ? [scheduling, ...metadata] : [...metadata, scheduling] };
|
|
1615
|
+
// Persist before publishing state, generating a topic, interrupting or starting a runner.
|
|
1616
|
+
const inserted = receipt ? this.storage.acceptStructuredInput(accepted, signature, receipt, !!relay)
|
|
1617
|
+
: this.storage.transaction(() => {
|
|
1618
|
+
if (relay)
|
|
1619
|
+
this.checkpointSessionMessages(accepted);
|
|
1620
|
+
else
|
|
1621
|
+
this.storage.updateSessionRuntimeMetadata(accepted);
|
|
1622
|
+
return true;
|
|
1623
|
+
});
|
|
1624
|
+
if (!inserted) {
|
|
1625
|
+
opts?.onAccepted?.(this.storage.getStructuredInputReceipt(id, key, signature));
|
|
1626
|
+
return this.requireSession(id);
|
|
1627
|
+
}
|
|
1628
|
+
this.sessions.set(id, accepted);
|
|
1629
|
+
opts?.onAccepted?.(receipt);
|
|
1630
|
+
if (relay)
|
|
1631
|
+
return this.relayUserMessage(id, prompt, relay, true);
|
|
1632
|
+
if (mustQueue && !interrupt) {
|
|
1633
|
+
this.emitStructuredSnapshot(accepted);
|
|
1634
|
+
if (accepted.employeeId)
|
|
1635
|
+
this.employeeInputAdmission?.schedule(accepted.employeeId);
|
|
1636
|
+
return accepted;
|
|
1637
|
+
}
|
|
1638
|
+
return this.sendAcceptedMessage(id, prompt, { ...opts, scheduling, queueAlreadyRemoved: true,
|
|
1639
|
+
// Accepted replacement is now itself a durable queue item; the original
|
|
1640
|
+
// clear/preserve intent was applied in the acceptance transaction above.
|
|
1641
|
+
preserveQueue: interrupt ? true : opts?.preserveQueue });
|
|
1642
|
+
}
|
|
1643
|
+
withoutAcceptedQueueItem(session, scheduling) {
|
|
1644
|
+
if (!scheduling?.acceptanceId)
|
|
1645
|
+
return session;
|
|
1646
|
+
const at = queuedInputScheduling(session).findIndex(item => item.acceptanceId === scheduling.acceptanceId);
|
|
1647
|
+
if (at < 0)
|
|
1648
|
+
return session;
|
|
1649
|
+
return { ...session, queuedMessages: session.queuedMessages?.filter((_, index) => index !== at),
|
|
1650
|
+
queuedInputScheduling: queuedInputScheduling(session).filter((_, index) => index !== at) };
|
|
1651
|
+
}
|
|
1652
|
+
sendAcceptedMessage(id, input, opts) {
|
|
1653
|
+
const acceptanceId = opts?.scheduling?.acceptanceId;
|
|
1654
|
+
if (acceptanceId)
|
|
1655
|
+
this.dispatchingAcceptedInputs.add(acceptanceId);
|
|
1656
|
+
return this.runAcceptedMessage(id, input, opts).finally(() => {
|
|
1657
|
+
if (acceptanceId)
|
|
1658
|
+
this.dispatchingAcceptedInputs.delete(acceptanceId);
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
async runAcceptedMessage(id, input, opts) {
|
|
1662
|
+
if (this.disposed)
|
|
1663
|
+
throw new Error("StructuredSessionManager has been disposed.");
|
|
1664
|
+
let session = this.withoutAcceptedQueueItem(this.requireSession(id), opts?.scheduling);
|
|
1507
1665
|
const prompt = input.trim();
|
|
1508
1666
|
if (!prompt)
|
|
1509
1667
|
return session;
|
|
@@ -1515,8 +1673,6 @@ export class StructuredSessionManager {
|
|
|
1515
1673
|
const relay = this.relayHandlerFor(session);
|
|
1516
1674
|
if (relay)
|
|
1517
1675
|
return await this.relayUserMessage(id, prompt, relay);
|
|
1518
|
-
if (opts?.idempotencyKey)
|
|
1519
|
-
this.claimIdempotencyKey(id, opts.idempotencyKey);
|
|
1520
1676
|
// The drain owns Core admission, not CLI admission. Check before any
|
|
1521
1677
|
// asynchronous selection/topic work; accepted Core inputs remain durable.
|
|
1522
1678
|
if (this.coreRestartDrains > 0 && this.usesCoreEngine(session))
|
|
@@ -1525,6 +1681,8 @@ export class StructuredSessionManager {
|
|
|
1525
1681
|
// 「智能分配」结算窗口:还没有 runner 归属,新输入先排队,不能另起一轮。
|
|
1526
1682
|
if (this.autoAssignPending.has(id))
|
|
1527
1683
|
return this.enqueueInput(session, prompt, false, scheduling);
|
|
1684
|
+
if (!session.structuredState?.inFlight)
|
|
1685
|
+
this.assertNativeInputMutable(session);
|
|
1528
1686
|
const inFlight = this.resolveInFlightSend(id, session, prompt, opts);
|
|
1529
1687
|
if ("result" in inFlight)
|
|
1530
1688
|
return inFlight.result;
|
|
@@ -1533,10 +1691,10 @@ export class StructuredSessionManager {
|
|
|
1533
1691
|
return this.enqueueInput(session, prompt, false, scheduling);
|
|
1534
1692
|
}
|
|
1535
1693
|
if (isAutoAssignSelector(session.selectedModel)) {
|
|
1536
|
-
const assigned = await this.settleAutoAssignForSend(id, session, prompt);
|
|
1694
|
+
const assigned = await this.settleAutoAssignForSend(id, session, prompt, scheduling.acceptanceId);
|
|
1537
1695
|
if ("result" in assigned)
|
|
1538
1696
|
return assigned.result;
|
|
1539
|
-
session = this.requireSession(id);
|
|
1697
|
+
session = this.withoutAcceptedQueueItem(this.requireSession(id), scheduling);
|
|
1540
1698
|
// Selection yielded: a drain or another input may now own admission.
|
|
1541
1699
|
// This request preceded inputs accepted during its selection window.
|
|
1542
1700
|
if (this.coreRestartDrains > 0 && this.usesCoreEngine(session))
|
|
@@ -1563,7 +1721,7 @@ export class StructuredSessionManager {
|
|
|
1563
1721
|
// a durable transcript identity. Team startup correlation IDs are private
|
|
1564
1722
|
// lifecycle tokens and must never be copied into persisted/public messages.
|
|
1565
1723
|
const transcriptRequestId = session.automationId?.startsWith("wand-task-comment:") ? requestId : undefined;
|
|
1566
|
-
const userTurn = buildUserTurn(prompt, pendingAsk, userTurnAt, transcriptRequestId);
|
|
1724
|
+
const userTurn = buildUserTurn(prompt, pendingAsk, userTurnAt, transcriptRequestId, session.provider === "pi" && session.structuredState?.engine !== "core" ? randomUUID() : undefined);
|
|
1567
1725
|
this.unacceptedTeamStarts.delete(id);
|
|
1568
1726
|
if (this.unsupportedStructuredProviders.has(id)) {
|
|
1569
1727
|
throw new Error(`会话使用的 provider 已不受支持,不能继续发送。`);
|
|
@@ -1584,10 +1742,10 @@ export class StructuredSessionManager {
|
|
|
1584
1742
|
lastError: null,
|
|
1585
1743
|
}),
|
|
1586
1744
|
};
|
|
1745
|
+
this.checkpointSessionMessages(updated);
|
|
1587
1746
|
this.sessions.set(id, updated);
|
|
1588
1747
|
if (updated.automationId?.startsWith("ai-team:"))
|
|
1589
1748
|
(opts?.teamRequestStarted ?? scheduling.teamRequestStarted)?.(id, requestId);
|
|
1590
|
-
this.checkpointSessionMessages(updated);
|
|
1591
1749
|
this.emitStructuredSnapshot(updated);
|
|
1592
1750
|
if (transcriptRequestId)
|
|
1593
1751
|
this.storage.bindTaskCommentResultStart(id, transcriptRequestId, updated.messages.length - 1);
|
|
@@ -1616,8 +1774,11 @@ export class StructuredSessionManager {
|
|
|
1616
1774
|
}
|
|
1617
1775
|
}
|
|
1618
1776
|
/** 群聊不起 CLI:先落下用户这句话,再交给转发方;转发失败以提示行告诉用户。 */
|
|
1619
|
-
async relayUserMessage(id, prompt, relay) {
|
|
1620
|
-
const posted = this.
|
|
1777
|
+
async relayUserMessage(id, prompt, relay, accepted = false) {
|
|
1778
|
+
const posted = accepted ? this.requireSession(id)
|
|
1779
|
+
: this.appendRelayTurns(id, [{ role: "user", content: [{ type: "text", text: prompt }] }]);
|
|
1780
|
+
if (accepted)
|
|
1781
|
+
this.emitStructuredSnapshot(posted);
|
|
1621
1782
|
try {
|
|
1622
1783
|
await relay(id, prompt);
|
|
1623
1784
|
}
|
|
@@ -1628,29 +1789,18 @@ export class StructuredSessionManager {
|
|
|
1628
1789
|
}
|
|
1629
1790
|
return this.sessions.get(id) ?? posted;
|
|
1630
1791
|
}
|
|
1631
|
-
/** Rejects a repeated (session, key) pair; otherwise remembers it (bounded). */
|
|
1632
|
-
claimIdempotencyKey(id, idempotencyKey) {
|
|
1633
|
-
const mapKey = `${id}:${idempotencyKey}`;
|
|
1634
|
-
if (this.seenIdempotencyKeys.has(mapKey)) {
|
|
1635
|
-
const err = new Error("检测到重复发送,已拦截。");
|
|
1636
|
-
err.code = "duplicate_idempotency_key";
|
|
1637
|
-
throw err;
|
|
1638
|
-
}
|
|
1639
|
-
// Bounded: BoundedKeySet drops the oldest half past IDEMPOTENCY_KEY_LIMIT.
|
|
1640
|
-
this.seenIdempotencyKeys.add(mapKey);
|
|
1641
|
-
}
|
|
1642
1792
|
/**
|
|
1643
1793
|
* While a turn is in flight, decide what this send does: recover a turn whose
|
|
1644
1794
|
* runner is gone, interrupt the current turn, or queue the input. Returns the
|
|
1645
1795
|
* session to continue with, or the value sendMessage should return now.
|
|
1646
1796
|
*/
|
|
1647
1797
|
resolveInFlightSend(id, session, prompt, opts) {
|
|
1648
|
-
if (!session.structuredState?.inFlight)
|
|
1798
|
+
if (!session.structuredState?.inFlight && !this.nativeProcessExits.has(id))
|
|
1649
1799
|
return { session };
|
|
1650
1800
|
const runnerExecution = this.pendingRunnerExecutions.get(id);
|
|
1651
1801
|
// interrupt() only requests cancellation; completion can settle later.
|
|
1652
1802
|
// Treat runner-map ownership as the authoritative in-flight state.
|
|
1653
|
-
const childActive = Boolean(runnerExecution) || this.pendingRecoveryIds.includes(id);
|
|
1803
|
+
const childActive = Boolean(runnerExecution) || this.pendingRecoveryIds.includes(id) || this.nativeProcessExits.has(id);
|
|
1654
1804
|
if (!childActive) {
|
|
1655
1805
|
const recovered = {
|
|
1656
1806
|
...session,
|
|
@@ -1662,8 +1812,8 @@ export class StructuredSessionManager {
|
|
|
1662
1812
|
activeRequestId: null,
|
|
1663
1813
|
},
|
|
1664
1814
|
};
|
|
1665
|
-
this
|
|
1666
|
-
|
|
1815
|
+
// The next start/queue checkpoint persists this transition together with
|
|
1816
|
+
// the accepted item. Do not remove its durable queue entry in a prior write.
|
|
1667
1817
|
return { session: recovered };
|
|
1668
1818
|
}
|
|
1669
1819
|
if (!opts?.interrupt)
|
|
@@ -1705,10 +1855,10 @@ export class StructuredSessionManager {
|
|
|
1705
1855
|
* 「智能分配」:只有真拿到这一轮提示词才能结算成本次使用的分组。结算后钉在会话上,
|
|
1706
1856
|
* 后续轮次用同一个分组(不在会话中途换模型);客户端选择器同步显示实际分组。
|
|
1707
1857
|
*/
|
|
1708
|
-
async settleAutoAssignForSend(id, session, prompt) {
|
|
1858
|
+
async settleAutoAssignForSend(id, session, prompt, acceptanceId) {
|
|
1709
1859
|
if (!isAutoAssignSelector(session.selectedModel))
|
|
1710
1860
|
return { session };
|
|
1711
|
-
const token = { cancelled: false };
|
|
1861
|
+
const token = { cancelled: false, acceptanceId };
|
|
1712
1862
|
this.autoAssignPending.set(id, token);
|
|
1713
1863
|
let assigned;
|
|
1714
1864
|
try {
|
|
@@ -1854,6 +2004,9 @@ export class StructuredSessionManager {
|
|
|
1854
2004
|
// stop() or a newer turn may have invalidated this execution while its
|
|
1855
2005
|
// runner was unwinding. A stale rejection must never fail the new turn.
|
|
1856
2006
|
if (!this.isCurrentRequest(id, requestId)) {
|
|
2007
|
+
if (!this.pendingRunnerExecutions.has(id) && !this.nativeProcessExits.has(id)
|
|
2008
|
+
&& !current.structuredState?.inFlight)
|
|
2009
|
+
setImmediate(() => { void this.flushNextQueuedMessage(id); });
|
|
1857
2010
|
return current;
|
|
1858
2011
|
}
|
|
1859
2012
|
const retry = this.retryEmployeeCandidate(id, session, prompt, error, requestId);
|
|
@@ -1904,6 +2057,7 @@ export class StructuredSessionManager {
|
|
|
1904
2057
|
*/
|
|
1905
2058
|
reorderQueuedMessages(sessionId, order, expectedQueue) {
|
|
1906
2059
|
const session = this.requireSession(sessionId);
|
|
2060
|
+
this.assertNativeInputMutable(session);
|
|
1907
2061
|
const queue = session.queuedMessages ?? [];
|
|
1908
2062
|
this.assertExpectedQueue(queue, expectedQueue);
|
|
1909
2063
|
if (!Array.isArray(order) || order.length !== queue.length) {
|
|
@@ -1930,6 +2084,7 @@ export class StructuredSessionManager {
|
|
|
1930
2084
|
/** Edit only the pending item still identified by both index and original text. */
|
|
1931
2085
|
editQueuedMessage(sessionId, index, expectedText, text) {
|
|
1932
2086
|
const session = this.requireSession(sessionId);
|
|
2087
|
+
this.assertNativeInputMutable(session);
|
|
1933
2088
|
const queue = session.queuedMessages ?? [];
|
|
1934
2089
|
if (!Number.isInteger(index) || index < 0 || index >= queue.length || queue[index] !== expectedText) {
|
|
1935
2090
|
throw new Error("排队消息已变化,请刷新后重试。");
|
|
@@ -1948,6 +2103,7 @@ export class StructuredSessionManager {
|
|
|
1948
2103
|
/** Remove a single queued message only while index and text still identify the same item. */
|
|
1949
2104
|
deleteQueuedMessage(sessionId, index, expectedText) {
|
|
1950
2105
|
const session = this.requireSession(sessionId);
|
|
2106
|
+
this.assertNativeInputMutable(session);
|
|
1951
2107
|
const queue = session.queuedMessages ?? [];
|
|
1952
2108
|
if (!Number.isInteger(index) || index < 0 || index >= queue.length) {
|
|
1953
2109
|
throw new Error("队列中没有该条消息(可能已被处理)。");
|
|
@@ -1971,48 +2127,12 @@ export class StructuredSessionManager {
|
|
|
1971
2127
|
* on the server prevents clients from re-sending the text while the original
|
|
1972
2128
|
* queue entry remains available for the automatic flush path.
|
|
1973
2129
|
*/
|
|
1974
|
-
async promoteQueuedMessage(sessionId, index, expectedText, idempotencyKey) {
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
}
|
|
1980
|
-
if (idempotencyKey && this.seenIdempotencyKeys.has(`${sessionId}:${idempotencyKey}`)) {
|
|
1981
|
-
return session;
|
|
1982
|
-
}
|
|
1983
|
-
const queue = session.queuedMessages ?? [];
|
|
1984
|
-
if (!Number.isInteger(index) || index < 0 || index >= queue.length) {
|
|
1985
|
-
throw new Error("队列中没有该条消息(可能已被处理)。");
|
|
1986
|
-
}
|
|
1987
|
-
if (expectedText !== undefined && queue[index] !== expectedText) {
|
|
1988
|
-
throw new Error("排队消息已变化,请按最新顺序重试。");
|
|
1989
|
-
}
|
|
1990
|
-
const prompt = queue[index];
|
|
1991
|
-
const remaining = queue.slice(0, index).concat(queue.slice(index + 1));
|
|
1992
|
-
const inFlight = session.status === "running" && session.structuredState?.inFlight === true;
|
|
1993
|
-
const updated = {
|
|
1994
|
-
...session,
|
|
1995
|
-
queuedMessages: remaining,
|
|
1996
|
-
queuedInputScheduling: queuedInputScheduling(session).filter((_, at) => at !== index),
|
|
1997
|
-
};
|
|
1998
|
-
this.sessions.set(sessionId, updated);
|
|
1999
|
-
this.storage.updateSessionRuntimeMetadata(updated);
|
|
2000
|
-
this.emitStructuredSnapshot(updated);
|
|
2001
|
-
try {
|
|
2002
|
-
return await this.sendMessage(sessionId, prompt, {
|
|
2003
|
-
interrupt: inFlight,
|
|
2004
|
-
preserveQueue: inFlight,
|
|
2005
|
-
queueAlreadyRemoved: true,
|
|
2006
|
-
immediateStart: true,
|
|
2007
|
-
idempotencyKey,
|
|
2008
|
-
source: "user",
|
|
2009
|
-
});
|
|
2010
|
-
}
|
|
2011
|
-
catch {
|
|
2012
|
-
// Once the item has been promoted it must not return to the queue: the
|
|
2013
|
-
// send path may have already persisted its user turn before a runner error.
|
|
2014
|
-
return this.requireSession(sessionId);
|
|
2015
|
-
}
|
|
2130
|
+
async promoteQueuedMessage(sessionId, index, expectedText, idempotencyKey, onAccepted) {
|
|
2131
|
+
return this.sendMessage(sessionId, "", {
|
|
2132
|
+
onAccepted,
|
|
2133
|
+
interrupt: true, preserveQueue: true, queueAlreadyRemoved: true,
|
|
2134
|
+
immediateStart: true, idempotencyKey, source: "user", promotion: { index, expectedText },
|
|
2135
|
+
});
|
|
2016
2136
|
}
|
|
2017
2137
|
assertExpectedQueue(queue, expected) {
|
|
2018
2138
|
if (expected !== undefined && (!Array.isArray(expected) || expected.length !== queue.length
|
|
@@ -2023,6 +2143,7 @@ export class StructuredSessionManager {
|
|
|
2023
2143
|
/** Clear all queued messages. No-op when queue is already empty. */
|
|
2024
2144
|
clearQueuedMessages(sessionId, expectedQueue) {
|
|
2025
2145
|
const session = this.requireSession(sessionId);
|
|
2146
|
+
this.assertNativeInputMutable(session);
|
|
2026
2147
|
this.assertExpectedQueue(session.queuedMessages ?? [], expectedQueue);
|
|
2027
2148
|
if (!session.queuedMessages || session.queuedMessages.length === 0) {
|
|
2028
2149
|
return session;
|
|
@@ -2236,10 +2357,12 @@ export class StructuredSessionManager {
|
|
|
2236
2357
|
const updated = { ...session, piSettings: updatedSettings };
|
|
2237
2358
|
// Do not publish a setting that did not reach durable storage.
|
|
2238
2359
|
this.storage.updateSessionRuntimeMetadata(updated);
|
|
2239
|
-
//
|
|
2360
|
+
// 只记住交互会话的选择,自动化/启动会话的临时工具限制不能污染用户默认。
|
|
2361
|
+
// automationId 也用于用户单聊关联,不能用它判断会话来源。
|
|
2362
|
+
// 新建 Pi CLI 会话以默认起步。只合并本次真正改动的字段——
|
|
2240
2363
|
// 从旧默认起步的会话不能用自己的 stale 值(例如 autoResources=false)覆盖后来在别处开启的默认。
|
|
2241
2364
|
// SDK 会话表达不出资源/CodeMode,不写回默认。
|
|
2242
|
-
if (resolution.engine === "cli") {
|
|
2365
|
+
if (resolution.engine === "cli" && (session.sessionSource ?? "interactive") === "interactive") {
|
|
2243
2366
|
const defaults = this.storage.getPiSessionDefaults() ?? defaultPiCliSessionSettings(this.config.harness?.compaction?.enabled ?? true);
|
|
2244
2367
|
this.storage.setPiSessionDefaults(mergePiSessionDefaults(defaults, patch));
|
|
2245
2368
|
}
|
|
@@ -2397,8 +2520,13 @@ export class StructuredSessionManager {
|
|
|
2397
2520
|
}
|
|
2398
2521
|
stop(id) {
|
|
2399
2522
|
this.unacceptedTeamStarts.delete(id);
|
|
2400
|
-
|
|
2523
|
+
let session = this.requireSession(id);
|
|
2401
2524
|
this.requireRecoveryControl(id);
|
|
2525
|
+
if (session.structuredState?.activeRequestId) {
|
|
2526
|
+
this.settleNativeInputs(id, session.structuredState.activeRequestId, { blocks: [], result: "", sessionId: session.claudeSessionId }, true);
|
|
2527
|
+
session = this.requireSession(id);
|
|
2528
|
+
}
|
|
2529
|
+
session = this.withoutAcceptedQueueItem(session, this.autoAssignPending.get(id));
|
|
2402
2530
|
// 结算中的智能分配还没有 runner:置位取消,结算回来后不得再启动。
|
|
2403
2531
|
this.disposeSession(id, "run");
|
|
2404
2532
|
// Clearing activeRequestId is the generation barrier: late data/close callbacks
|
|
@@ -2506,7 +2634,26 @@ export class StructuredSessionManager {
|
|
|
2506
2634
|
releasePendingRunnerExecution(sessionId, execution, forgetRun = true) {
|
|
2507
2635
|
if (this.pendingRunnerExecutions.get(sessionId) !== execution)
|
|
2508
2636
|
return false;
|
|
2637
|
+
const exits = this.nativeProcessExits.get(sessionId);
|
|
2638
|
+
if (forgetRun && exits?.size) {
|
|
2639
|
+
// A protocol/observation result is not an OS exit. Keep the handle usable
|
|
2640
|
+
// for stop and prevent a new input from colliding with the live tmux owner.
|
|
2641
|
+
void Promise.all([...exits]).then(() => {
|
|
2642
|
+
if (!this.releasePendingRunnerExecution(sessionId, execution, forgetRun))
|
|
2643
|
+
return;
|
|
2644
|
+
const current = this.sessions.get(sessionId);
|
|
2645
|
+
if (!this.disposed && (this.execHost?.persistent || current?.status === "idle")
|
|
2646
|
+
&& current?.status !== "running" && !current?.structuredState?.inFlight) {
|
|
2647
|
+
void this.flushNextQueuedMessage(sessionId);
|
|
2648
|
+
}
|
|
2649
|
+
}, () => { });
|
|
2650
|
+
return false;
|
|
2651
|
+
}
|
|
2509
2652
|
this.pendingRunnerExecutions.delete(sessionId);
|
|
2653
|
+
const current = this.sessions.get(sessionId);
|
|
2654
|
+
if (forgetRun && !this.disposed && current?.status === "idle" && !current.structuredState?.inFlight) {
|
|
2655
|
+
setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
|
|
2656
|
+
}
|
|
2510
2657
|
// A failed recovery attach must retain the daemon record for the next retry.
|
|
2511
2658
|
if (forgetRun) {
|
|
2512
2659
|
const exits = this.nativeProcessExits.get(sessionId);
|
|
@@ -2532,7 +2679,7 @@ export class StructuredSessionManager {
|
|
|
2532
2679
|
beginTurn(sessionId, state) {
|
|
2533
2680
|
const at = isoNow();
|
|
2534
2681
|
this.turnHeartbeat.observe(sessionId);
|
|
2535
|
-
return { ...state, turnStartedAt: at, lastActivityAt: at };
|
|
2682
|
+
return { ...state, inputMode: state.engine === "core" ? "steer" : "turn-end", turnStartedAt: at, lastActivityAt: at };
|
|
2536
2683
|
}
|
|
2537
2684
|
/**
|
|
2538
2685
|
* 领养 daemon 里仍在飞的回合:起点是重启前那一轮的真实时刻,不能改写成重连时刻;
|
|
@@ -2607,14 +2754,126 @@ export class StructuredSessionManager {
|
|
|
2607
2754
|
data,
|
|
2608
2755
|
});
|
|
2609
2756
|
}
|
|
2757
|
+
/** Native Pi claims one durable queue item; the reservation survives until its transcript commit. */
|
|
2758
|
+
reserveNativeInput(sessionId, requestId, identity) {
|
|
2759
|
+
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
2760
|
+
if (!current || this.disposed || this.interruptedWith.has(sessionId))
|
|
2761
|
+
return null;
|
|
2762
|
+
const scheduling = queuedInputScheduling(current);
|
|
2763
|
+
const index = scheduling.findIndex(item => item.source === "user");
|
|
2764
|
+
const text = index >= 0 ? current.queuedMessages?.[index] : undefined;
|
|
2765
|
+
if (text === undefined || scheduling.some(item => item.handoff))
|
|
2766
|
+
return null;
|
|
2767
|
+
const reserved = scheduling.map((item, at) => at === index ? { ...item, handoff: identity } : item);
|
|
2768
|
+
const updated = { ...current, queuedInputScheduling: reserved };
|
|
2769
|
+
this.storage.updateSessionRuntimeMetadata(updated);
|
|
2770
|
+
this.sessions.set(sessionId, updated);
|
|
2771
|
+
this.emitStructuredSnapshot(updated);
|
|
2772
|
+
return text;
|
|
2773
|
+
}
|
|
2774
|
+
/** A claim is the native transcript commit: persist the boundary and remove only that reservation. */
|
|
2775
|
+
commitNativeInput(sessionId, requestId, offer, state) {
|
|
2776
|
+
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
2777
|
+
if (!current)
|
|
2778
|
+
return;
|
|
2779
|
+
const scheduling = queuedInputScheduling(current);
|
|
2780
|
+
const index = scheduling.findIndex(item => item.handoff?.id === offer.id);
|
|
2781
|
+
const alreadyCommitted = current.messages?.some(turn => turn.role === "user" && turn.messageId === offer.id);
|
|
2782
|
+
if (state.inputMessageId === offer.id || (index < 0 && !alreadyCommitted)
|
|
2783
|
+
|| (index >= 0 && current.queuedMessages?.[index] !== offer.text))
|
|
2784
|
+
return;
|
|
2785
|
+
const preceding = state.blocks.length || state.result
|
|
2786
|
+
? this.buildCompletedAssistantMessages(current, state) : [...(current.messages ?? [])];
|
|
2787
|
+
const committed = {
|
|
2788
|
+
...current,
|
|
2789
|
+
queuedMessages: current.queuedMessages?.filter((_, at) => at !== index),
|
|
2790
|
+
queuedInputScheduling: scheduling.filter((_, at) => at !== index),
|
|
2791
|
+
messages: preceding.some(turn => turn.messageId === offer.id) ? preceding
|
|
2792
|
+
: [...preceding, buildUserTurn(offer.text, undefined, isoNow(), undefined, offer.id)],
|
|
2793
|
+
};
|
|
2794
|
+
this.saveAuthoritativeSession(committed);
|
|
2795
|
+
state.blocks = [];
|
|
2796
|
+
state.result = "";
|
|
2797
|
+
state.usage = undefined;
|
|
2798
|
+
state.inputMessageId = offer.id;
|
|
2799
|
+
this.sessions.set(sessionId, committed);
|
|
2800
|
+
this.emitStructuredSnapshot(committed);
|
|
2801
|
+
}
|
|
2802
|
+
handleNativeInputEvent(sessionId, requestId, event, state) {
|
|
2803
|
+
if (event.type === "wand_input_boundary" && typeof event.id === "string") {
|
|
2804
|
+
this.execHost?.offerPiInput?.(structuredRunId(sessionId), event.id, identity => this.reserveNativeInput(sessionId, requestId, identity));
|
|
2805
|
+
}
|
|
2806
|
+
if (event.type === "wand_input_ready" && typeof event.id === "string")
|
|
2807
|
+
state.inputMessageId = event.id;
|
|
2808
|
+
if (event.type === "wand_input_boundary") {
|
|
2809
|
+
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
2810
|
+
if (current && current.structuredState?.inputMode !== "steer") {
|
|
2811
|
+
const updated = { ...current, structuredState: { ...current.structuredState, inputMode: "steer" } };
|
|
2812
|
+
this.sessions.set(sessionId, updated);
|
|
2813
|
+
this.storage.updateSessionRuntimeMetadata(updated);
|
|
2814
|
+
this.emitStructuredSnapshot(updated);
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
if (event.type !== "wand_input_committed")
|
|
2818
|
+
return;
|
|
2819
|
+
const offer = event.message;
|
|
2820
|
+
if (!offer || typeof offer.id !== "string" || typeof offer.text !== "string")
|
|
2821
|
+
return;
|
|
2822
|
+
this.commitNativeInput(sessionId, requestId, offer, state);
|
|
2823
|
+
}
|
|
2824
|
+
/** Reconcile before any end-of-run flush. Claimed-but-unconfirmed input is never replayed. */
|
|
2825
|
+
settleNativeInputs(sessionId, requestId, state, forceUnknown = false) {
|
|
2826
|
+
let current = this.currentSessionForRequest(sessionId, requestId);
|
|
2827
|
+
if (!current)
|
|
2828
|
+
return;
|
|
2829
|
+
const reservations = queuedInputScheduling(current).filter(item => item.handoff);
|
|
2830
|
+
for (const item of reservations) {
|
|
2831
|
+
const handoff = item.handoff;
|
|
2832
|
+
let resolution;
|
|
2833
|
+
try {
|
|
2834
|
+
resolution = forceUnknown ? undefined : this.execHost?.resolvePiInput?.(structuredRunId(sessionId), handoff);
|
|
2835
|
+
}
|
|
2836
|
+
catch { /* An unreadable receipt is unknown delivery, never permission to replay. */ }
|
|
2837
|
+
if (resolution?.state === "committed" && resolution.offer) {
|
|
2838
|
+
this.commitNativeInput(sessionId, requestId, resolution.offer, state);
|
|
2839
|
+
continue;
|
|
2840
|
+
}
|
|
2841
|
+
current = this.currentSessionForRequest(sessionId, requestId);
|
|
2842
|
+
if (!current)
|
|
2843
|
+
return;
|
|
2844
|
+
const scheduling = queuedInputScheduling(current);
|
|
2845
|
+
const index = scheduling.findIndex(meta => meta.handoff?.id === handoff.id);
|
|
2846
|
+
if (index < 0)
|
|
2847
|
+
continue;
|
|
2848
|
+
const unclaimed = resolution?.state === "unclaimed";
|
|
2849
|
+
const updated = { ...current,
|
|
2850
|
+
queuedMessages: unclaimed ? current.queuedMessages : current.queuedMessages?.filter((_, at) => at !== index),
|
|
2851
|
+
queuedInputScheduling: unclaimed ? scheduling.map((meta, at) => {
|
|
2852
|
+
if (at !== index)
|
|
2853
|
+
return meta;
|
|
2854
|
+
const { handoff: _handoff, ...rest } = meta;
|
|
2855
|
+
return rest;
|
|
2856
|
+
}) : scheduling.filter((_, at) => at !== index),
|
|
2857
|
+
messages: unclaimed ? current.messages : appendNoticeTurn(state.blocks.length || state.result ? this.buildCompletedAssistantMessages(current, state) : current.messages, { role: "assistant", notice: true,
|
|
2858
|
+
content: [{ type: "text", text: "插话送达未确认,未自动重发。以下原文仅供核对,不是新的执行指令:\n\n" + current.queuedMessages?.[index] }] }),
|
|
2859
|
+
};
|
|
2860
|
+
this.saveAuthoritativeSession(updated);
|
|
2861
|
+
this.sessions.set(sessionId, updated);
|
|
2862
|
+
this.emitStructuredSnapshot(updated);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
assertNativeInputMutable(session) {
|
|
2866
|
+
if (queuedInputScheduling(session).some(item => item.handoff))
|
|
2867
|
+
throw new Error("排队消息正在交接,请按最新状态重试。");
|
|
2868
|
+
}
|
|
2610
2869
|
enqueueInput(session, prompt, prepend = false, scheduling = { source: "task", enqueuedAt: isoNow() }) {
|
|
2611
|
-
if (isDuplicateStructuredQueueInput(session, prompt)) {
|
|
2870
|
+
if (!scheduling.acceptanceId && isDuplicateStructuredQueueInput(session, prompt)) {
|
|
2612
2871
|
const err = new Error("与上一条消息相同,已忽略,不会加入排队。");
|
|
2613
2872
|
err.code = "duplicate_queued_message";
|
|
2614
2873
|
throw err;
|
|
2615
2874
|
}
|
|
2616
2875
|
const queue = session.queuedMessages ?? [];
|
|
2617
|
-
if (queue.length >= 10)
|
|
2876
|
+
if (!scheduling.acceptanceId && queue.length >= 10)
|
|
2618
2877
|
throw new Error("排队消息已满(最多 10 条),请等待当前消息处理完成。");
|
|
2619
2878
|
const metadata = queuedInputScheduling(session);
|
|
2620
2879
|
const queued = { ...session, queuedMessages: prepend ? [prompt, ...queue] : [...queue, prompt],
|
|
@@ -2630,10 +2889,11 @@ export class StructuredSessionManager {
|
|
|
2630
2889
|
if (this.disposed)
|
|
2631
2890
|
return;
|
|
2632
2891
|
const current = this.sessions.get(sessionId);
|
|
2633
|
-
if (!current || (current.queuedMessages?.length ?? 0) === 0) {
|
|
2892
|
+
if (!current || queuedInputScheduling(current).some(item => item.handoff) || (current.queuedMessages?.length ?? 0) === 0) {
|
|
2634
2893
|
return;
|
|
2635
2894
|
}
|
|
2636
|
-
if (current.structuredState?.inFlight ||
|
|
2895
|
+
if (current.structuredState?.inFlight || this.nativeProcessExits.has(sessionId) || this.pendingRunnerExecutions.has(sessionId)
|
|
2896
|
+
|| (this.coreRestartDrains > 0 && this.usesCoreEngine(current))) {
|
|
2637
2897
|
return;
|
|
2638
2898
|
}
|
|
2639
2899
|
if (current.employeeId && this.employeeInputAdmission && !fromEmployeeScheduler) {
|
|
@@ -2641,28 +2901,25 @@ export class StructuredSessionManager {
|
|
|
2641
2901
|
return;
|
|
2642
2902
|
}
|
|
2643
2903
|
const nextInput = current.queuedMessages?.[index];
|
|
2644
|
-
const scheduling = queuedInputScheduling(current)[index]
|
|
2645
|
-
|
|
2646
|
-
if (!nextInput)
|
|
2904
|
+
const scheduling = { ...queuedInputScheduling(current)[index],
|
|
2905
|
+
acceptanceId: queuedInputScheduling(current)[index]?.acceptanceId ?? randomUUID() };
|
|
2906
|
+
if (!nextInput)
|
|
2647
2907
|
return;
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
};
|
|
2654
|
-
this.sessions.set(sessionId, nextSession);
|
|
2655
|
-
this.storage.updateSessionRuntimeMetadata(nextSession);
|
|
2656
|
-
this.emitStructuredSnapshot(nextSession);
|
|
2908
|
+
// Legacy queues acquire an identity before dispatch too. The queue item stays
|
|
2909
|
+
// durable until its user turn and removal commit together.
|
|
2910
|
+
const identified = { ...current, queuedInputScheduling: queuedInputScheduling(current).map((item, at) => at === index ? scheduling : item) };
|
|
2911
|
+
this.storage.updateSessionRuntimeMetadata(identified);
|
|
2912
|
+
this.sessions.set(sessionId, identified);
|
|
2657
2913
|
try {
|
|
2658
|
-
await this.
|
|
2914
|
+
await this.sendAcceptedMessage(sessionId, nextInput, { scheduling });
|
|
2659
2915
|
}
|
|
2660
2916
|
catch (error) {
|
|
2661
2917
|
console.error("[WAND] flushNextQueuedMessage failed:", error);
|
|
2662
2918
|
// Only an explicit pre-execution rejection permits replay. A persisted
|
|
2663
2919
|
// runner failure or arbitrary exception can mean delivery is unknown.
|
|
2664
2920
|
const afterFail = this.sessions.get(sessionId);
|
|
2665
|
-
|
|
2921
|
+
const stillQueued = afterFail && queuedInputScheduling(afterFail).some(item => item.acceptanceId === scheduling.acceptanceId);
|
|
2922
|
+
if (afterFail && error instanceof UnacceptedStructuredInputError && !stillQueued) {
|
|
2666
2923
|
const rescued = {
|
|
2667
2924
|
...afterFail,
|
|
2668
2925
|
queuedMessages: [nextInput, ...(afterFail.queuedMessages ?? [])],
|
|
@@ -2672,7 +2929,7 @@ export class StructuredSessionManager {
|
|
|
2672
2929
|
this.storage.updateSessionRuntimeMetadata(rescued);
|
|
2673
2930
|
this.emitStructuredSnapshot(rescued);
|
|
2674
2931
|
}
|
|
2675
|
-
else if (afterFail && (afterFail.messages?.length ?? 0) <= (current.messages?.length ?? 0)) {
|
|
2932
|
+
else if (afterFail && !stillQueued && (afterFail.messages?.length ?? 0) <= (current.messages?.length ?? 0)) {
|
|
2676
2933
|
// If dispatch failed before a user turn was checkpointed, retain the
|
|
2677
2934
|
// original visibly for reconciliation, never as another sendable item.
|
|
2678
2935
|
const retained = { ...afterFail, messages: appendNoticeTurn(afterFail.messages, {
|
|
@@ -2721,7 +2978,9 @@ export class StructuredSessionManager {
|
|
|
2721
2978
|
usage: turnState.usage,
|
|
2722
2979
|
...(turnState.resourceSelection ? { resourceSelection: turnState.resourceSelection } : {}),
|
|
2723
2980
|
};
|
|
2724
|
-
messages =
|
|
2981
|
+
messages = turnState.inputMessageId
|
|
2982
|
+
? upsertPiAssistantSegment(current.messages, turn, turnState.inputMessageId)
|
|
2983
|
+
: upsertAssistantMessage(current.messages, turn);
|
|
2725
2984
|
}
|
|
2726
2985
|
const patched = {
|
|
2727
2986
|
...current,
|
|
@@ -2767,6 +3026,9 @@ export class StructuredSessionManager {
|
|
|
2767
3026
|
session,
|
|
2768
3027
|
prompt,
|
|
2769
3028
|
env: buildChildEnv(this.config.inheritEnv !== false),
|
|
3029
|
+
...(provider === "pi" && session.structuredState?.engine !== "core" ? {
|
|
3030
|
+
onPiInputEvent: (event, state) => this.handleNativeInputEvent(sessionId, requestId, event, state),
|
|
3031
|
+
} : {}),
|
|
2770
3032
|
...(provider === "pi" && session.structuredState?.engine === "core" ? {
|
|
2771
3033
|
takeSteering: (turnState) => {
|
|
2772
3034
|
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
@@ -2834,6 +3096,10 @@ export class StructuredSessionManager {
|
|
|
2834
3096
|
let result;
|
|
2835
3097
|
try {
|
|
2836
3098
|
result = await execution.completion;
|
|
3099
|
+
// Wrappers may resolve their exit promise a microtask after completion.
|
|
3100
|
+
// Only an explicit observation-only result keeps ownership after return.
|
|
3101
|
+
if (result.processExited !== false && execution.exited)
|
|
3102
|
+
await execution.exited;
|
|
2837
3103
|
}
|
|
2838
3104
|
finally {
|
|
2839
3105
|
turnCompleted = true;
|
|
@@ -2871,6 +3137,8 @@ export class StructuredSessionManager {
|
|
|
2871
3137
|
primaryError: result.primaryError,
|
|
2872
3138
|
...(provider === "codex" ? { codexErrors: result.errors, codexTurnFailed: result.primaryError } : {}),
|
|
2873
3139
|
});
|
|
3140
|
+
if (provider === "pi")
|
|
3141
|
+
this.settleNativeInputs(sessionId, requestId, result.state);
|
|
2874
3142
|
const current = this.currentSessionForRequest(sessionId, requestId);
|
|
2875
3143
|
if (!current)
|
|
2876
3144
|
return;
|
|
@@ -3018,7 +3286,16 @@ export class StructuredSessionManager {
|
|
|
3018
3286
|
usage: turnState.usage,
|
|
3019
3287
|
...(turnState.resourceSelection ? { resourceSelection: turnState.resourceSelection } : {}),
|
|
3020
3288
|
};
|
|
3021
|
-
|
|
3289
|
+
if (turnState.inputMessageId && !turnState.blocks.length && !turnState.result && current.messages?.at(-1)?.role === "user")
|
|
3290
|
+
return [...current.messages];
|
|
3291
|
+
if (turnState.inputMessageId)
|
|
3292
|
+
return upsertPiAssistantSegment(current.messages, assistantTurn, turnState.inputMessageId, true);
|
|
3293
|
+
// A delivery notice is independent of the preceding assistant segment.
|
|
3294
|
+
const messages = [...(current.messages ?? [])];
|
|
3295
|
+
const notices = [];
|
|
3296
|
+
while (messages.at(-1)?.notice)
|
|
3297
|
+
notices.unshift(messages.pop());
|
|
3298
|
+
return [...upsertAssistantMessage(messages, assistantTurn, true, stampNewTools), ...notices];
|
|
3022
3299
|
}
|
|
3023
3300
|
/**
|
|
3024
3301
|
* Rebuild the trailing assistant turn when the replayed log was truncated.
|