@cluesmith/codev 3.2.3 → 3.3.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/dashboard-dist/assets/index-BZRTEMtH.css +32 -0
- package/dashboard-dist/assets/{index-BZ1x7x1X.js → index-JCbkpAcV.js} +33 -33
- package/dashboard-dist/assets/index-JCbkpAcV.js.map +1 -0
- package/dashboard-dist/index.html +2 -2
- package/dist/agent-farm/cli.d.ts.map +1 -1
- package/dist/agent-farm/cli.js +173 -0
- package/dist/agent-farm/cli.js.map +1 -1
- package/dist/agent-farm/commands/attach.js +2 -2
- package/dist/agent-farm/commands/attach.js.map +1 -1
- package/dist/agent-farm/commands/cleanup.d.ts.map +1 -1
- package/dist/agent-farm/commands/cleanup.js +16 -0
- package/dist/agent-farm/commands/cleanup.js.map +1 -1
- package/dist/agent-farm/commands/inbox.d.ts +42 -0
- package/dist/agent-farm/commands/inbox.d.ts.map +1 -0
- package/dist/agent-farm/commands/inbox.js +129 -0
- package/dist/agent-farm/commands/inbox.js.map +1 -0
- package/dist/agent-farm/commands/interrupt.d.ts +20 -0
- package/dist/agent-farm/commands/interrupt.d.ts.map +1 -0
- package/dist/agent-farm/commands/interrupt.js +65 -0
- package/dist/agent-farm/commands/interrupt.js.map +1 -0
- package/dist/agent-farm/commands/reset/constants.d.ts +52 -0
- package/dist/agent-farm/commands/reset/constants.d.ts.map +1 -0
- package/dist/agent-farm/commands/reset/constants.js +52 -0
- package/dist/agent-farm/commands/reset/constants.js.map +1 -0
- package/dist/agent-farm/commands/reset/context.d.ts +230 -0
- package/dist/agent-farm/commands/reset/context.d.ts.map +1 -0
- package/dist/agent-farm/commands/reset/context.js +428 -0
- package/dist/agent-farm/commands/reset/context.js.map +1 -0
- package/dist/agent-farm/commands/reset/index.d.ts +192 -0
- package/dist/agent-farm/commands/reset/index.d.ts.map +1 -0
- package/dist/agent-farm/commands/reset/index.js +488 -0
- package/dist/agent-farm/commands/reset/index.js.map +1 -0
- package/dist/agent-farm/commands/reset/receipt.d.ts +97 -0
- package/dist/agent-farm/commands/reset/receipt.d.ts.map +1 -0
- package/dist/agent-farm/commands/reset/receipt.js +149 -0
- package/dist/agent-farm/commands/reset/receipt.js.map +1 -0
- package/dist/agent-farm/commands/reset/reorient.d.ts +117 -0
- package/dist/agent-farm/commands/reset/reorient.d.ts.map +1 -0
- package/dist/agent-farm/commands/reset/reorient.js +313 -0
- package/dist/agent-farm/commands/reset/reorient.js.map +1 -0
- package/dist/agent-farm/commands/reset.d.ts +19 -0
- package/dist/agent-farm/commands/reset.d.ts.map +1 -0
- package/dist/agent-farm/commands/reset.js +292 -0
- package/dist/agent-farm/commands/reset.js.map +1 -0
- package/dist/agent-farm/commands/send.d.ts +2 -2
- package/dist/agent-farm/commands/send.d.ts.map +1 -1
- package/dist/agent-farm/commands/send.js +52 -11
- package/dist/agent-farm/commands/send.js.map +1 -1
- package/dist/agent-farm/commands/setup.d.ts +4 -1
- package/dist/agent-farm/commands/setup.d.ts.map +1 -1
- package/dist/agent-farm/commands/setup.js +8 -4
- package/dist/agent-farm/commands/setup.js.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.d.ts +116 -6
- package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn-worktree.js +345 -74
- package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
- package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
- package/dist/agent-farm/commands/spawn.js +13 -1
- package/dist/agent-farm/commands/spawn.js.map +1 -1
- package/dist/agent-farm/commands/status.d.ts.map +1 -1
- package/dist/agent-farm/commands/status.js +105 -9
- package/dist/agent-farm/commands/status.js.map +1 -1
- package/dist/agent-farm/commands/tower-sweep-husks.d.ts +6 -0
- package/dist/agent-farm/commands/tower-sweep-husks.d.ts.map +1 -0
- package/dist/agent-farm/commands/tower-sweep-husks.js +77 -0
- package/dist/agent-farm/commands/tower-sweep-husks.js.map +1 -0
- package/dist/agent-farm/commands/tower.d.ts.map +1 -1
- package/dist/agent-farm/commands/tower.js +34 -0
- package/dist/agent-farm/commands/tower.js.map +1 -1
- package/dist/agent-farm/commands/whoami.d.ts +65 -0
- package/dist/agent-farm/commands/whoami.d.ts.map +1 -0
- package/dist/agent-farm/commands/whoami.js +196 -0
- package/dist/agent-farm/commands/whoami.js.map +1 -0
- package/dist/agent-farm/db/index.d.ts +1 -1
- package/dist/agent-farm/db/index.d.ts.map +1 -1
- package/dist/agent-farm/db/index.js +80 -1
- package/dist/agent-farm/db/index.js.map +1 -1
- package/dist/agent-farm/db/mailbox.d.ts +222 -0
- package/dist/agent-farm/db/mailbox.d.ts.map +1 -0
- package/dist/agent-farm/db/mailbox.js +304 -0
- package/dist/agent-farm/db/mailbox.js.map +1 -0
- package/dist/agent-farm/db/schema.d.ts +1 -1
- package/dist/agent-farm/db/schema.d.ts.map +1 -1
- package/dist/agent-farm/db/schema.js +35 -0
- package/dist/agent-farm/db/schema.js.map +1 -1
- package/dist/agent-farm/db/types.d.ts +45 -0
- package/dist/agent-farm/db/types.d.ts.map +1 -1
- package/dist/agent-farm/db/types.js.map +1 -1
- package/dist/agent-farm/lib/builder-lookup.d.ts +1 -1
- package/dist/agent-farm/lib/builder-lookup.js +2 -2
- package/dist/agent-farm/lib/builder-lookup.js.map +1 -1
- package/dist/agent-farm/lib/reconnect-backoff.d.ts +54 -0
- package/dist/agent-farm/lib/reconnect-backoff.d.ts.map +1 -0
- package/dist/agent-farm/lib/reconnect-backoff.js +47 -0
- package/dist/agent-farm/lib/reconnect-backoff.js.map +1 -0
- package/dist/agent-farm/lib/tower-client.d.ts +16 -6
- package/dist/agent-farm/lib/tower-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tower-client.js +37 -6
- package/dist/agent-farm/lib/tower-client.js.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.d.ts +69 -1
- package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
- package/dist/agent-farm/lib/tunnel-client.js +218 -27
- package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
- package/dist/agent-farm/servers/architect-session-holder.d.ts +133 -0
- package/dist/agent-farm/servers/architect-session-holder.d.ts.map +1 -0
- package/dist/agent-farm/servers/architect-session-holder.js +268 -0
- package/dist/agent-farm/servers/architect-session-holder.js.map +1 -0
- package/dist/agent-farm/servers/cron-delivery.d.ts +72 -0
- package/dist/agent-farm/servers/cron-delivery.d.ts.map +1 -0
- package/dist/agent-farm/servers/cron-delivery.js +80 -0
- package/dist/agent-farm/servers/cron-delivery.js.map +1 -0
- package/dist/agent-farm/servers/delayed-send.d.ts +85 -0
- package/dist/agent-farm/servers/delayed-send.d.ts.map +1 -0
- package/dist/agent-farm/servers/delayed-send.js +165 -0
- package/dist/agent-farm/servers/delayed-send.js.map +1 -0
- package/dist/agent-farm/servers/gate-profiles.d.ts +67 -0
- package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -0
- package/dist/agent-farm/servers/gate-profiles.js +113 -0
- package/dist/agent-farm/servers/gate-profiles.js.map +1 -0
- package/dist/agent-farm/servers/mailbox-delivery.d.ts +370 -0
- package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -0
- package/dist/agent-farm/servers/mailbox-delivery.js +617 -0
- package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -0
- package/dist/agent-farm/servers/mailbox-wiring.d.ts +125 -0
- package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -0
- package/dist/agent-farm/servers/mailbox-wiring.js +425 -0
- package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -0
- package/dist/agent-farm/servers/message-write.d.ts +53 -1
- package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
- package/dist/agent-farm/servers/message-write.js +63 -0
- package/dist/agent-farm/servers/message-write.js.map +1 -1
- package/dist/agent-farm/servers/overview.d.ts.map +1 -1
- package/dist/agent-farm/servers/overview.js +23 -4
- package/dist/agent-farm/servers/overview.js.map +1 -1
- package/dist/agent-farm/servers/process-census.d.ts +41 -0
- package/dist/agent-farm/servers/process-census.d.ts.map +1 -0
- package/dist/agent-farm/servers/process-census.js +62 -0
- package/dist/agent-farm/servers/process-census.js.map +1 -0
- package/dist/agent-farm/servers/render-gate.d.ts +141 -0
- package/dist/agent-farm/servers/render-gate.d.ts.map +1 -0
- package/dist/agent-farm/servers/render-gate.js +251 -0
- package/dist/agent-farm/servers/render-gate.js.map +1 -0
- package/dist/agent-farm/servers/session-log-sweep.d.ts +93 -0
- package/dist/agent-farm/servers/session-log-sweep.d.ts.map +1 -0
- package/dist/agent-farm/servers/session-log-sweep.js +165 -0
- package/dist/agent-farm/servers/session-log-sweep.js.map +1 -0
- package/dist/agent-farm/servers/session-submit.d.ts +90 -0
- package/dist/agent-farm/servers/session-submit.d.ts.map +1 -0
- package/dist/agent-farm/servers/session-submit.js +127 -0
- package/dist/agent-farm/servers/session-submit.js.map +1 -0
- package/dist/agent-farm/servers/shellper-husk-sweep.d.ts +87 -0
- package/dist/agent-farm/servers/shellper-husk-sweep.d.ts.map +1 -0
- package/dist/agent-farm/servers/shellper-husk-sweep.js +129 -0
- package/dist/agent-farm/servers/shellper-husk-sweep.js.map +1 -0
- package/dist/agent-farm/servers/tower-cron.d.ts +10 -7
- package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-cron.js +58 -53
- package/dist/agent-farm/servers/tower-cron.js.map +1 -1
- package/dist/agent-farm/servers/tower-instances.d.ts +14 -2
- package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-instances.js +128 -19
- package/dist/agent-farm/servers/tower-instances.js.map +1 -1
- package/dist/agent-farm/servers/tower-messages.d.ts +38 -1
- package/dist/agent-farm/servers/tower-messages.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-messages.js +106 -1
- package/dist/agent-farm/servers/tower-messages.js.map +1 -1
- package/dist/agent-farm/servers/tower-routes.d.ts +16 -4
- package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-routes.js +807 -99
- package/dist/agent-farm/servers/tower-routes.js.map +1 -1
- package/dist/agent-farm/servers/tower-server.js +241 -15
- package/dist/agent-farm/servers/tower-server.js.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.d.ts +17 -2
- package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-terminals.js +139 -124
- package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-tunnel.js +63 -4
- package/dist/agent-farm/servers/tower-tunnel.js.map +1 -1
- package/dist/agent-farm/servers/tower-types.d.ts +2 -1
- package/dist/agent-farm/servers/tower-types.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.d.ts +99 -1
- package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-utils.js +242 -6
- package/dist/agent-farm/servers/tower-utils.js.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
- package/dist/agent-farm/servers/tower-websocket.js +6 -21
- package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
- package/dist/agent-farm/servers/write-queue.d.ts +35 -0
- package/dist/agent-farm/servers/write-queue.d.ts.map +1 -0
- package/dist/agent-farm/servers/write-queue.js +56 -0
- package/dist/agent-farm/servers/write-queue.js.map +1 -0
- package/dist/agent-farm/state.d.ts +6 -1
- package/dist/agent-farm/state.d.ts.map +1 -1
- package/dist/agent-farm/state.js +5 -2
- package/dist/agent-farm/state.js.map +1 -1
- package/dist/agent-farm/types.d.ts +39 -0
- package/dist/agent-farm/types.d.ts.map +1 -1
- package/dist/agent-farm/utils/agent-names.d.ts +2 -2
- package/dist/agent-farm/utils/agent-names.d.ts.map +1 -1
- package/dist/agent-farm/utils/agent-names.js +3 -3
- package/dist/agent-farm/utils/agent-names.js.map +1 -1
- package/dist/agent-farm/utils/architect-name.d.ts +2 -2
- package/dist/agent-farm/utils/architect-name.d.ts.map +1 -1
- package/dist/agent-farm/utils/architect-name.js +3 -3
- package/dist/agent-farm/utils/architect-name.js.map +1 -1
- package/dist/agent-farm/utils/config.d.ts +13 -0
- package/dist/agent-farm/utils/config.d.ts.map +1 -1
- package/dist/agent-farm/utils/config.js +33 -1
- package/dist/agent-farm/utils/config.js.map +1 -1
- package/dist/agent-farm/utils/harness.d.ts +103 -7
- package/dist/agent-farm/utils/harness.d.ts.map +1 -1
- package/dist/agent-farm/utils/harness.js +126 -19
- package/dist/agent-farm/utils/harness.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -32
- package/dist/cli.js.map +1 -1
- package/dist/commands/adopt.d.ts +3 -2
- package/dist/commands/adopt.d.ts.map +1 -1
- package/dist/commands/adopt.js +7 -6
- package/dist/commands/adopt.js.map +1 -1
- package/dist/commands/consult/agy-auth-cache.d.ts +97 -0
- package/dist/commands/consult/agy-auth-cache.d.ts.map +1 -0
- package/dist/commands/consult/agy-auth-cache.js +320 -0
- package/dist/commands/consult/agy-auth-cache.js.map +1 -0
- package/dist/commands/consult/cli-options.d.ts +34 -0
- package/dist/commands/consult/cli-options.d.ts.map +1 -0
- package/dist/commands/consult/cli-options.js +67 -0
- package/dist/commands/consult/cli-options.js.map +1 -0
- package/dist/commands/consult/index.d.ts +56 -2
- package/dist/commands/consult/index.d.ts.map +1 -1
- package/dist/commands/consult/index.js +330 -27
- package/dist/commands/consult/index.js.map +1 -1
- package/dist/commands/consult/metrics.d.ts +42 -0
- package/dist/commands/consult/metrics.d.ts.map +1 -1
- package/dist/commands/consult/metrics.js +129 -9
- package/dist/commands/consult/metrics.js.map +1 -1
- package/dist/commands/doctor.d.ts +28 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +258 -22
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +3 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +7 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/porch/checks.d.ts +23 -0
- package/dist/commands/porch/checks.d.ts.map +1 -1
- package/dist/commands/porch/checks.js +60 -0
- package/dist/commands/porch/checks.js.map +1 -1
- package/dist/commands/porch/config.d.ts +20 -0
- package/dist/commands/porch/config.d.ts.map +1 -1
- package/dist/commands/porch/config.js +20 -0
- package/dist/commands/porch/config.js.map +1 -1
- package/dist/commands/porch/index.d.ts.map +1 -1
- package/dist/commands/porch/index.js +32 -36
- package/dist/commands/porch/index.js.map +1 -1
- package/dist/commands/porch/next.d.ts.map +1 -1
- package/dist/commands/porch/next.js +2 -41
- package/dist/commands/porch/next.js.map +1 -1
- package/dist/commands/update.d.ts +4 -4
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +9 -11
- package/dist/commands/update.js.map +1 -1
- package/dist/lib/config.d.ts +70 -3
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +67 -3
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/consult-lanes.d.ts +149 -0
- package/dist/lib/consult-lanes.d.ts.map +1 -0
- package/dist/lib/consult-lanes.js +360 -0
- package/dist/lib/consult-lanes.js.map +1 -0
- package/dist/lib/default-branch.d.ts +1 -1
- package/dist/lib/default-branch.js +1 -1
- package/dist/lib/github.js +1 -1
- package/dist/lib/github.js.map +1 -1
- package/dist/lib/gitignore.d.ts +25 -2
- package/dist/lib/gitignore.d.ts.map +1 -1
- package/dist/lib/gitignore.js +70 -0
- package/dist/lib/gitignore.js.map +1 -1
- package/dist/lib/migration-backup-audit.d.ts +64 -0
- package/dist/lib/migration-backup-audit.d.ts.map +1 -0
- package/dist/lib/migration-backup-audit.js +145 -0
- package/dist/lib/migration-backup-audit.js.map +1 -0
- package/dist/lib/prompt-behavior-metrics.d.ts +67 -0
- package/dist/lib/prompt-behavior-metrics.d.ts.map +1 -0
- package/dist/lib/prompt-behavior-metrics.js +290 -0
- package/dist/lib/prompt-behavior-metrics.js.map +1 -0
- package/dist/lib/protocol-drift-audit.d.ts +118 -0
- package/dist/lib/protocol-drift-audit.d.ts.map +1 -0
- package/dist/lib/protocol-drift-audit.js +217 -0
- package/dist/lib/protocol-drift-audit.js.map +1 -0
- package/dist/lib/scaffold.d.ts +10 -3
- package/dist/lib/scaffold.d.ts.map +1 -1
- package/dist/lib/scaffold.js +29 -26
- package/dist/lib/scaffold.js.map +1 -1
- package/dist/lib/skeleton.d.ts +22 -1
- package/dist/lib/skeleton.d.ts.map +1 -1
- package/dist/lib/skeleton.js +120 -0
- package/dist/lib/skeleton.js.map +1 -1
- package/dist/lib/test-env.d.ts +74 -0
- package/dist/lib/test-env.d.ts.map +1 -0
- package/dist/lib/test-env.js +92 -0
- package/dist/lib/test-env.js.map +1 -0
- package/dist/terminal/pty-manager.d.ts +9 -0
- package/dist/terminal/pty-manager.d.ts.map +1 -1
- package/dist/terminal/pty-manager.js +32 -6
- package/dist/terminal/pty-manager.js.map +1 -1
- package/dist/terminal/pty-session.d.ts +156 -4
- package/dist/terminal/pty-session.d.ts.map +1 -1
- package/dist/terminal/pty-session.js +307 -42
- package/dist/terminal/pty-session.js.map +1 -1
- package/dist/terminal/ring-buffer.d.ts +39 -6
- package/dist/terminal/ring-buffer.d.ts.map +1 -1
- package/dist/terminal/ring-buffer.js +74 -6
- package/dist/terminal/ring-buffer.js.map +1 -1
- package/dist/terminal/session-manager.d.ts +129 -0
- package/dist/terminal/session-manager.d.ts.map +1 -1
- package/dist/terminal/session-manager.js +337 -44
- package/dist/terminal/session-manager.js.map +1 -1
- package/dist/terminal/session-screen.d.ts +91 -0
- package/dist/terminal/session-screen.d.ts.map +1 -0
- package/dist/terminal/session-screen.js +135 -0
- package/dist/terminal/session-screen.js.map +1 -0
- package/dist/terminal/shellper-client.d.ts +19 -4
- package/dist/terminal/shellper-client.d.ts.map +1 -1
- package/dist/terminal/shellper-client.js +70 -8
- package/dist/terminal/shellper-client.js.map +1 -1
- package/dist/terminal/shellper-main.js +2 -1
- package/dist/terminal/shellper-main.js.map +1 -1
- package/dist/terminal/shellper-process.d.ts +1 -1
- package/dist/terminal/shellper-process.d.ts.map +1 -1
- package/dist/terminal/shellper-process.js +33 -9
- package/dist/terminal/shellper-process.js.map +1 -1
- package/dist/terminal/shellper-protocol.d.ts +40 -2
- package/dist/terminal/shellper-protocol.d.ts.map +1 -1
- package/dist/terminal/shellper-protocol.js +72 -5
- package/dist/terminal/shellper-protocol.js.map +1 -1
- package/dist/terminal/shellper-replay-buffer.d.ts +69 -7
- package/dist/terminal/shellper-replay-buffer.d.ts.map +1 -1
- package/dist/terminal/shellper-replay-buffer.js +167 -31
- package/dist/terminal/shellper-replay-buffer.js.map +1 -1
- package/package.json +11 -9
- package/skeleton/.claude/skills/afx/SKILL.md +27 -1
- package/skeleton/.claude/skills/arch-init/SKILL.md +135 -0
- package/skeleton/.claude/skills/arch-save/SKILL.md +183 -0
- package/skeleton/.claude/skills/codev/SKILL.md +33 -2
- package/skeleton/.claude/skills/runnable-worktrees/SKILL.md +119 -0
- package/skeleton/.codex/skills/afx/SKILL.md +170 -0
- package/skeleton/.codex/skills/arch-init/SKILL.md +135 -0
- package/skeleton/.codex/skills/arch-save/SKILL.md +183 -0
- package/skeleton/.codex/skills/codev/SKILL.md +90 -0
- package/skeleton/.codex/skills/consult/SKILL.md +96 -0
- package/skeleton/.codex/skills/generate-image/SKILL.md +45 -0
- package/skeleton/.codex/skills/porch/SKILL.md +60 -0
- package/skeleton/.codex/skills/runnable-worktrees/SKILL.md +119 -0
- package/skeleton/.codex/skills/update-arch-docs/SKILL.md +132 -0
- package/skeleton/protocols/air/builder-prompt.md +28 -47
- package/skeleton/protocols/air/consult-types/impl-review.md +17 -34
- package/skeleton/protocols/air/consult-types/pr-review.md +14 -37
- package/skeleton/protocols/air/prompts/implement.md +20 -73
- package/skeleton/protocols/air/prompts/pr.md +22 -54
- package/skeleton/protocols/air/protocol.md +40 -71
- package/skeleton/protocols/aspir/builder-prompt.md +29 -66
- package/skeleton/protocols/aspir/consult-types/impl-review.md +17 -44
- package/skeleton/protocols/aspir/consult-types/phase-review.md +17 -44
- package/skeleton/protocols/aspir/consult-types/plan-review.md +14 -38
- package/skeleton/protocols/aspir/consult-types/pr-review.md +13 -39
- package/skeleton/protocols/aspir/consult-types/spec-review.md +13 -33
- package/skeleton/protocols/aspir/prompts/implement.md +27 -196
- package/skeleton/protocols/aspir/prompts/plan.md +21 -85
- package/skeleton/protocols/aspir/prompts/review.md +29 -228
- package/skeleton/protocols/aspir/prompts/specify.md +25 -111
- package/skeleton/protocols/aspir/protocol.json +10 -0
- package/skeleton/protocols/aspir/protocol.md +36 -82
- package/skeleton/protocols/bugfix/builder-prompt.md +23 -45
- package/skeleton/protocols/bugfix/consult-types/impl-review.md +21 -45
- package/skeleton/protocols/bugfix/consult-types/pr-review.md +18 -52
- package/skeleton/protocols/bugfix/prompts/fix.md +16 -61
- package/skeleton/protocols/bugfix/prompts/investigate.md +18 -62
- package/skeleton/protocols/bugfix/prompts/pr.md +22 -57
- package/skeleton/protocols/bugfix/protocol.md +44 -50
- package/skeleton/protocols/experiment/builder-prompt.md +25 -58
- package/skeleton/protocols/experiment/protocol.md +22 -188
- package/skeleton/protocols/experiment/templates/notes.md +11 -71
- package/skeleton/protocols/maintain/builder-prompt.md +24 -45
- package/skeleton/protocols/maintain/consult-types/impl-review.md +17 -44
- package/skeleton/protocols/maintain/consult-types/pr-review.md +13 -39
- package/skeleton/protocols/maintain/prompts/maintain.md +1 -1
- package/skeleton/protocols/maintain/protocol.md +32 -266
- package/skeleton/protocols/maintain/templates/maintenance-run.md +11 -0
- package/skeleton/protocols/pir/builder-prompt.md +30 -64
- package/skeleton/protocols/pir/consult-types/impl-review.md +17 -50
- package/skeleton/protocols/pir/consult-types/pr-review.md +11 -40
- package/skeleton/protocols/pir/prompts/implement.md +1 -2
- package/skeleton/protocols/pir/prompts/plan.md +0 -3
- package/skeleton/protocols/pir/prompts/review.md +1 -1
- package/skeleton/protocols/pir/protocol.md +54 -177
- package/skeleton/protocols/research/builder-prompt.md +27 -58
- package/skeleton/protocols/research/protocol.md +28 -153
- package/skeleton/protocols/spike/builder-prompt.md +30 -48
- package/skeleton/protocols/spike/protocol.md +24 -107
- package/skeleton/protocols/spike/templates/findings.md +9 -39
- package/skeleton/protocols/spir/builder-prompt.md +33 -62
- package/skeleton/protocols/spir/consult-types/impl-review.md +17 -44
- package/skeleton/protocols/spir/consult-types/phase-review.md +17 -44
- package/skeleton/protocols/spir/consult-types/plan-review.md +14 -38
- package/skeleton/protocols/spir/consult-types/pr-review.md +13 -39
- package/skeleton/protocols/spir/consult-types/spec-review.md +13 -33
- package/skeleton/protocols/spir/prompts/implement.md +26 -195
- package/skeleton/protocols/spir/prompts/plan.md +20 -84
- package/skeleton/protocols/spir/prompts/review.md +28 -227
- package/skeleton/protocols/spir/prompts/specify.md +24 -110
- package/skeleton/protocols/spir/protocol.json +10 -0
- package/skeleton/protocols/spir/protocol.md +84 -633
- package/skeleton/protocols/spir/templates/plan.md +28 -147
- package/skeleton/protocols/spir/templates/review.md +21 -80
- package/skeleton/protocols/spir/templates/spec.md +24 -118
- package/skeleton/resources/commands/agent-farm.md +348 -0
- package/skeleton/resources/commands/codev.md +5 -0
- package/skeleton/resources/commands/consult.md +262 -8
- package/skeleton/resources/commands/overview.md +1 -0
- package/skeleton/roles/architect.md +67 -306
- package/skeleton/roles/builder.md +77 -191
- package/skeleton/templates/AGENTS.md +2 -0
- package/skeleton/templates/CLAUDE.md +2 -0
- package/templates/tower.html +39 -2
- package/dashboard-dist/assets/index-BZ1x7x1X.js.map +0 -1
- package/dashboard-dist/assets/index-DB1KBSNE.css +0 -32
- package/dist/agent-farm/servers/send-buffer.d.ts +0 -59
- package/dist/agent-farm/servers/send-buffer.d.ts.map +0 -1
- package/dist/agent-farm/servers/send-buffer.js +0 -103
- package/dist/agent-farm/servers/send-buffer.js.map +0 -1
- package/skeleton/porch/prompts/defend.md +0 -108
- package/skeleton/porch/prompts/diagnose.md +0 -70
- package/skeleton/porch/prompts/evaluate.md +0 -132
- package/skeleton/porch/prompts/fix.md +0 -59
- package/skeleton/porch/prompts/implement.md +0 -88
- package/skeleton/porch/prompts/plan.md +0 -74
- package/skeleton/porch/prompts/pr.md +0 -84
- package/skeleton/porch/prompts/review.md +0 -249
- package/skeleton/porch/prompts/specify.md +0 -53
- package/skeleton/porch/prompts/test.md +0 -63
- package/skeleton/protocols/aspir/templates/plan.md +0 -184
- package/skeleton/protocols/aspir/templates/review.md +0 -120
- package/skeleton/protocols/aspir/templates/spec.md +0 -154
|
@@ -31,41 +31,42 @@ import { getWorktreeConfig, getActivityHooks } from '../utils/config.js';
|
|
|
31
31
|
import { ensureCodevConfigWatcher } from './codev-config-watcher.js';
|
|
32
32
|
import { hasTeam, loadTeamMembers, loadMessages } from '../../lib/team.js';
|
|
33
33
|
import { fetchTeamGitHubData } from '../../lib/team-github.js';
|
|
34
|
-
import { resolveTarget, broadcastMessage, isResolveError } from './tower-messages.js';
|
|
34
|
+
import { resolveTarget, resolveAgentInRegistry, broadcastMessage, isResolveError } from './tower-messages.js';
|
|
35
35
|
import { handleCommandRoute, COMMAND_ROUTE } from './command-relay.js';
|
|
36
36
|
import { formatArchitectMessage, formatBuilderMessage } from '../utils/message-format.js';
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
37
|
+
import { writeMessageToSession, writeEscapeToSession } from './message-write.js';
|
|
38
|
+
import { makeDeliveryPorts, getMailboxDrainer } from './mailbox-wiring.js';
|
|
39
|
+
import { deliverAgentMailSerialized } from './mailbox-delivery.js';
|
|
40
|
+
import { deliverCronMail, CRON_SENDER } from './cron-delivery.js';
|
|
41
|
+
import { enqueue as enqueueMailbox, getById as getMailboxById, markDelivered as markMailboxDelivered, listHeld as listHeldMailbox, dismiss as dismissMailbox, } from '../db/mailbox.js';
|
|
42
|
+
// Spec 1273 per-terminal submission lock — preserved across the Spec 1313 merge for
|
|
43
|
+
// the two explicit human-bypass paths (escape + interrupt), which do NOT route
|
|
44
|
+
// through the mailbox's per-agent serializer and so need their own anti-fusion lock.
|
|
45
|
+
import { submitToSession } from './session-submit.js';
|
|
46
|
+
// Spec 1307 `--delay` — Tower-side deferred delivery, re-homed onto the Spec 1313
|
|
47
|
+
// mailbox (the merge that carried this feature was flattened by a later rebase, so it
|
|
48
|
+
// is grafted here explicitly): the due-time callback enqueues to the mailbox and
|
|
49
|
+
// triggers a gated drain (see handleSend), so a delayed message delivers onto a
|
|
50
|
+
// render-verified empty prompt like any normal send — never force-injected.
|
|
51
|
+
import { scheduleDelayedSend, validateDelaySeconds } from './delayed-send.js';
|
|
52
|
+
import { getKnownWorkspacePaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, instancesReady, stopInstance, addArchitect, removeArchitect, } from './tower-instances.js';
|
|
40
53
|
import { OverviewCache } from './overview.js';
|
|
41
54
|
import { fetchIssue, searchIssues, fetchPRList, fetchCurrentUser, parseLinkedIssue, parseArea, } from '../../lib/github.js';
|
|
42
55
|
import { computeAnalytics } from './analytics.js';
|
|
43
56
|
import { getAllTasks, executeTask, getTaskId } from './tower-cron.js';
|
|
44
57
|
import { getGlobalDb } from '../db/index.js';
|
|
58
|
+
import { listProcessCensus } from './process-census.js';
|
|
59
|
+
import { SHELLPER_MARKER, computeRegisteredShellperPids, findHuskShellpers, sweepShellperHusks, resolveHuskGraceMs, } from './shellper-husk-sweep.js';
|
|
60
|
+
import { getProcessStartTime } from '../../terminal/session-manager.js';
|
|
45
61
|
import { getWorkspaceTerminals, getTerminalManager, getWorkspaceTerminalsEntry, getNextShellId, saveTerminalSession, isSessionPersistent, deleteTerminalSession, removeTerminalFromRegistry, deleteWorkspaceTerminalSessions, deleteFileTabsForWorkspace, saveFileTab, deleteFileTab, getRehydratedTerminalsEntry, isStartupReconcileSettled, getTerminalSessionById, getActiveShellLabels, updateTerminalLabel, } from './tower-terminals.js';
|
|
46
62
|
const __filename = fileURLToPath(import.meta.url);
|
|
47
63
|
const __dirname = path.dirname(__filename);
|
|
48
64
|
// Singleton cache for overview endpoint (Spec 0126 Phase 4)
|
|
49
65
|
const overviewCache = new OverviewCache();
|
|
50
|
-
// Singleton send buffer for typing-aware message delivery (Spec 403)
|
|
51
|
-
const sendBuffer = new SendBuffer();
|
|
52
|
-
/** Deliver a buffered message to a session (write + broadcast + log).
|
|
53
|
-
* Returns the ms timestamp when all writes complete (for serialization). */
|
|
54
|
-
function deliverBufferedMessage(session, msg, delayOffset = 0) {
|
|
55
|
-
const endTime = writeMessageToSession(session, msg.formattedMessage, msg.noEnter, delayOffset);
|
|
56
|
-
broadcastMessage(msg.broadcastPayload);
|
|
57
|
-
return endTime;
|
|
58
|
-
}
|
|
59
|
-
/** Start the send buffer flush timer (called from tower-server during init). */
|
|
60
|
-
export function startSendBuffer(log) {
|
|
61
|
-
sendBuffer.start((id) => getTerminalManager().getSession(id), deliverBufferedMessage, log);
|
|
62
|
-
}
|
|
63
|
-
/** Stop the send buffer and deliver remaining messages (called from tower-server during shutdown). */
|
|
64
|
-
export function stopSendBuffer() {
|
|
65
|
-
sendBuffer.stop();
|
|
66
|
-
}
|
|
67
66
|
const ROUTES = {
|
|
68
|
-
'GET /health': (_req, res) => handleHealthCheck(res),
|
|
67
|
+
'GET /health': (_req, res, _url, ctx) => handleHealthCheck(res, ctx),
|
|
68
|
+
'GET /api/shellpers/husks': (_req, res, _url, ctx) => handleHuskPreview(res, ctx),
|
|
69
|
+
'POST /api/shellpers/husks/sweep': (_req, res, _url, ctx) => handleHuskSweep(res, ctx),
|
|
69
70
|
'GET /api/workspaces': (_req, res) => handleListWorkspaces(res),
|
|
70
71
|
'POST /api/terminals': (req, res, _url, ctx) => handleTerminalCreate(req, res, ctx),
|
|
71
72
|
'GET /api/terminals': (_req, res) => handleTerminalList(res),
|
|
@@ -85,10 +86,29 @@ const ROUTES = {
|
|
|
85
86
|
'POST /api/launch': (req, res) => handleLaunchInstance(req, res),
|
|
86
87
|
'POST /api/stop': (req, res) => handleStopInstance(req, res),
|
|
87
88
|
'POST /api/send': (req, res, _url, ctx) => handleSend(req, res, ctx),
|
|
89
|
+
'GET /api/inbox': (_req, res, url) => handleInboxList(res, url),
|
|
88
90
|
'GET /api/cron/tasks': (_req, res, url) => handleCronList(res, url),
|
|
89
91
|
'GET /': (_req, res, _url, ctx) => handleDashboard(res, ctx),
|
|
90
92
|
'GET /index.html': (_req, res, _url, ctx) => handleDashboard(res, ctx),
|
|
91
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* Issue #1261: tell "Tower isn't wired up yet" apart from "no such thing".
|
|
96
|
+
* A route that needs the instances module must not report a missing terminal
|
|
97
|
+
* (404) or a successful kill (204) when the truth is that it could not act at
|
|
98
|
+
* all. 503 + Retry-After says so, and matches what `stopInstance()` already
|
|
99
|
+
* returns in the same situation.
|
|
100
|
+
*
|
|
101
|
+
* Tower's readiness gate holds requests until boot completes, so these guards
|
|
102
|
+
* should be unreachable in practice — they exist so the failure mode, if the
|
|
103
|
+
* gate is ever bypassed, is honest rather than misleading.
|
|
104
|
+
*/
|
|
105
|
+
function respondStartingUp(res) {
|
|
106
|
+
res.writeHead(503, { 'Content-Type': 'application/json', 'Retry-After': '1' });
|
|
107
|
+
res.end(JSON.stringify({
|
|
108
|
+
error: 'STARTING_UP',
|
|
109
|
+
message: 'Tower is still starting up. Try again shortly.',
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
92
112
|
// ============================================================================
|
|
93
113
|
// Main request handler
|
|
94
114
|
// ============================================================================
|
|
@@ -160,6 +180,18 @@ export async function handleRequest(req, res, ctx) {
|
|
|
160
180
|
if (cronTaskMatch) {
|
|
161
181
|
return await handleCronTaskAction(req, res, url, cronTaskMatch);
|
|
162
182
|
}
|
|
183
|
+
// Inbox dismiss: POST /api/inbox/:id/dismiss (Spec 1313, Phase 7)
|
|
184
|
+
const inboxDismissMatch = url.pathname.match(/^\/api\/inbox\/([^/]+)\/dismiss$/);
|
|
185
|
+
if (inboxDismissMatch) {
|
|
186
|
+
return handleInboxDismiss(req, res, ctx, inboxDismissMatch);
|
|
187
|
+
}
|
|
188
|
+
// Inbox show: GET /api/inbox/:id — a single row INCLUDING its body (Spec 1313 §178).
|
|
189
|
+
// Checked AFTER the dismiss match, so /:id/dismiss never falls through here (its
|
|
190
|
+
// trailing segment can't match this single-segment pattern anyway).
|
|
191
|
+
const inboxShowMatch = url.pathname.match(/^\/api\/inbox\/([^/]+)$/);
|
|
192
|
+
if (inboxShowMatch) {
|
|
193
|
+
return handleInboxShow(req, res, inboxShowMatch);
|
|
194
|
+
}
|
|
163
195
|
// Workspace routes: /workspace/:base64urlPath/* (Spec 0090 Phase 4)
|
|
164
196
|
if (url.pathname.startsWith('/workspace/')) {
|
|
165
197
|
return await handleWorkspaceRoutes(req, res, ctx, url);
|
|
@@ -177,9 +209,56 @@ export async function handleRequest(req, res, ctx) {
|
|
|
177
209
|
// ============================================================================
|
|
178
210
|
// Global route handlers
|
|
179
211
|
// ============================================================================
|
|
180
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Issue #1227: fleet RSS + unregistered-shellper count, scoped to this Tower
|
|
214
|
+
* instance's socketDir. `fleetRssKb` sums every in-scope shellper AND its
|
|
215
|
+
* direct children (the full process-group tree Tower manages) regardless of
|
|
216
|
+
* DB-registration state, so a husk not yet swept still counts toward the
|
|
217
|
+
* visible cost. `unregisteredShellperCount` is the lighter, ungated signal
|
|
218
|
+
* ("N shellpers Tower doesn't currently track") — distinct from the stricter
|
|
219
|
+
* childless+aged reap predicate in shellper-husk-sweep.ts.
|
|
220
|
+
*
|
|
221
|
+
* Best-effort: returns undefined fields (never throws) if `ps` or the DB read
|
|
222
|
+
* fails, so a fleet-accounting hiccup never takes down /health itself.
|
|
223
|
+
*/
|
|
224
|
+
async function computeFleetHealthFields(ctx) {
|
|
225
|
+
const shellperManager = ctx.getShellperManager();
|
|
226
|
+
if (!shellperManager)
|
|
227
|
+
return {};
|
|
228
|
+
try {
|
|
229
|
+
const census = await listProcessCensus();
|
|
230
|
+
const scopeMarker = shellperManager.socketDir.endsWith('/')
|
|
231
|
+
? shellperManager.socketDir
|
|
232
|
+
: `${shellperManager.socketDir}/`;
|
|
233
|
+
const inScopePids = new Set();
|
|
234
|
+
let fleetRssKb = 0;
|
|
235
|
+
for (const entry of census) {
|
|
236
|
+
if (entry.cmdline.includes(SHELLPER_MARKER) && entry.cmdline.includes(scopeMarker)) {
|
|
237
|
+
inScopePids.add(entry.pid);
|
|
238
|
+
fleetRssKb += entry.rssKb;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const entry of census) {
|
|
242
|
+
if (inScopePids.has(entry.ppid)) {
|
|
243
|
+
fleetRssKb += entry.rssKb;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const registered = await computeRegisteredShellperPids(getGlobalDb());
|
|
247
|
+
let unregisteredShellperCount = 0;
|
|
248
|
+
for (const pid of inScopePids) {
|
|
249
|
+
if (!registered.has(pid))
|
|
250
|
+
unregisteredShellperCount++;
|
|
251
|
+
}
|
|
252
|
+
return { fleetRssKb, unregisteredShellperCount };
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return {};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function handleHealthCheck(res, ctx) {
|
|
181
259
|
const instances = await getInstances();
|
|
182
260
|
const activeCount = instances.filter((i) => i.running).length;
|
|
261
|
+
const fleetFields = await computeFleetHealthFields(ctx);
|
|
183
262
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
184
263
|
res.end(JSON.stringify({
|
|
185
264
|
status: 'healthy',
|
|
@@ -191,9 +270,82 @@ async function handleHealthCheck(res) {
|
|
|
191
270
|
activeWorkspaces: activeCount,
|
|
192
271
|
totalWorkspaces: instances.length,
|
|
193
272
|
memoryUsage: process.memoryUsage().heapUsed,
|
|
273
|
+
...fleetFields,
|
|
194
274
|
timestamp: new Date().toISOString(),
|
|
195
275
|
}));
|
|
196
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Issue #1227: GET /api/shellpers/husks — preview, read-only. Returns the
|
|
279
|
+
* husk candidates the next sweep (periodic or `--apply`) would reap, without
|
|
280
|
+
* touching anything.
|
|
281
|
+
*/
|
|
282
|
+
async function handleHuskPreview(res, ctx) {
|
|
283
|
+
const shellperManager = ctx.getShellperManager();
|
|
284
|
+
if (!shellperManager) {
|
|
285
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
286
|
+
res.end(JSON.stringify({ error: 'Shellper manager not initialized' }));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const graceMs = resolveHuskGraceMs();
|
|
291
|
+
// One census snapshot, shared between the candidate decision and the
|
|
292
|
+
// displayed RSS — passing it via the `census` seam avoids a second `ps`
|
|
293
|
+
// scan and guarantees the RSS shown is for the exact snapshot that decided
|
|
294
|
+
// candidacy, not a later, possibly-different one (codex #1227 PR review).
|
|
295
|
+
const census = await listProcessCensus();
|
|
296
|
+
const rssByPid = new Map(census.map((entry) => [entry.pid, entry.rssKb]));
|
|
297
|
+
const registered = await computeRegisteredShellperPids(getGlobalDb());
|
|
298
|
+
const pids = await findHuskShellpers({
|
|
299
|
+
socketDir: shellperManager.socketDir,
|
|
300
|
+
registeredShellperPids: registered,
|
|
301
|
+
graceMs,
|
|
302
|
+
census: () => census,
|
|
303
|
+
});
|
|
304
|
+
const now = Date.now();
|
|
305
|
+
const candidates = await Promise.all(pids.map(async (pid) => {
|
|
306
|
+
const startTime = await getProcessStartTime(pid);
|
|
307
|
+
return {
|
|
308
|
+
pid,
|
|
309
|
+
rssKb: rssByPid.get(pid) ?? 0,
|
|
310
|
+
ageMs: startTime !== null ? now - startTime : null,
|
|
311
|
+
};
|
|
312
|
+
}));
|
|
313
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
314
|
+
res.end(JSON.stringify({ candidates, graceMs }));
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
318
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Issue #1227: POST /api/shellpers/husks/sweep — apply. Actually reaps the
|
|
323
|
+
* current husk candidates. Destructive; the `afx tower sweep-husks` CLI is
|
|
324
|
+
* the only caller expected in practice, and it gates this behind `--apply`
|
|
325
|
+
* plus a confirmation prompt (unless `--yes`).
|
|
326
|
+
*/
|
|
327
|
+
async function handleHuskSweep(res, ctx) {
|
|
328
|
+
const shellperManager = ctx.getShellperManager();
|
|
329
|
+
if (!shellperManager) {
|
|
330
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
331
|
+
res.end(JSON.stringify({ error: 'Shellper manager not initialized' }));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const result = await sweepShellperHusks({
|
|
336
|
+
socketDir: shellperManager.socketDir,
|
|
337
|
+
db: getGlobalDb(),
|
|
338
|
+
graceMs: resolveHuskGraceMs(),
|
|
339
|
+
log: (msg) => ctx.log('INFO', msg),
|
|
340
|
+
});
|
|
341
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
342
|
+
res.end(JSON.stringify(result));
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
346
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
197
349
|
/**
|
|
198
350
|
* GET /api/version (#983) — report the version of the *running* Tower process.
|
|
199
351
|
*
|
|
@@ -448,11 +600,19 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
448
600
|
...defaultSessionOptions(),
|
|
449
601
|
cols: cols || DEFAULT_COLS,
|
|
450
602
|
});
|
|
451
|
-
|
|
603
|
+
// Read session info BEFORE awaiting replay: an instantly-exiting
|
|
604
|
+
// child's EXIT frame can remove the session from the manager during
|
|
605
|
+
// the await, and this lookup must not miss (#1198).
|
|
452
606
|
const shellperInfo = shellperManager.getSessionInfo(sessionId);
|
|
607
|
+
const replayData = await client.waitForReplay(); // #1198: fresh shellpers always send REPLAY (possibly empty); awaiting avoids racing early child output
|
|
453
608
|
const session = manager.createSessionRaw({
|
|
454
609
|
label: label || `terminal-${sessionId.slice(0, 8)}`,
|
|
455
610
|
cwd,
|
|
611
|
+
// Spec 1313: thread the launch command so the render-gate can resolve
|
|
612
|
+
// this session's profile (builders keep the `.builder-start.sh` backstop
|
|
613
|
+
// too; this makes identity direct and restart-safe via the persisted row).
|
|
614
|
+
command,
|
|
615
|
+
args,
|
|
456
616
|
});
|
|
457
617
|
const ptySession = manager.getSession(session.id);
|
|
458
618
|
if (ptySession) {
|
|
@@ -469,7 +629,7 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
469
629
|
else {
|
|
470
630
|
entry.shells.set(roleId, session.id);
|
|
471
631
|
}
|
|
472
|
-
saveTerminalSession(session.id, workspacePath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime, label ?? null, cwd ?? null);
|
|
632
|
+
saveTerminalSession(session.id, workspacePath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime, label ?? null, cwd ?? null, command ?? null);
|
|
473
633
|
ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for workspace ${workspacePath}`);
|
|
474
634
|
}
|
|
475
635
|
}
|
|
@@ -491,7 +651,7 @@ async function handleTerminalCreate(req, res, ctx) {
|
|
|
491
651
|
else {
|
|
492
652
|
entry.shells.set(roleId, info.id);
|
|
493
653
|
}
|
|
494
|
-
saveTerminalSession(info.id, workspacePath, termType, roleId, info.pid, null, null, null, null, cwd ?? null);
|
|
654
|
+
saveTerminalSession(info.id, workspacePath, termType, roleId, info.pid, null, null, null, null, cwd ?? null, command ?? null);
|
|
495
655
|
ctx.log('WARN', `Terminal ${info.id} for ${workspacePath} is non-persistent (shellper unavailable)`);
|
|
496
656
|
}
|
|
497
657
|
}
|
|
@@ -528,6 +688,12 @@ async function handleTerminalRoutes(req, res, url, match) {
|
|
|
528
688
|
}
|
|
529
689
|
// DELETE /api/terminals/:id - Kill terminal (disable shellper auto-restart if applicable)
|
|
530
690
|
if (req.method === 'DELETE' && (!subpath || subpath === '')) {
|
|
691
|
+
// Issue #1261: without this, a not-yet-wired Tower answers 404 for a
|
|
692
|
+
// terminal that exists — the exact symptom the issue reports.
|
|
693
|
+
if (!instancesReady()) {
|
|
694
|
+
respondStartingUp(res);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
531
697
|
if (!(await killTerminalWithShellper(manager, terminalId))) {
|
|
532
698
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
533
699
|
res.end(JSON.stringify({ error: 'NOT_FOUND', message: `Session ${terminalId} not found` }));
|
|
@@ -724,7 +890,7 @@ async function handleOverview(res, url, workspaceOverride, ctx) {
|
|
|
724
890
|
// every collection field is required ('never undefined' for `architects`,
|
|
725
891
|
// Issue 1104), so emit them all empty rather than a partial payload.
|
|
726
892
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
727
|
-
res.end(JSON.stringify({ builders: [], pendingPRs: [], backlog: [], recentlyClosed: [], architects: [] }));
|
|
893
|
+
res.end(JSON.stringify({ builders: [], pendingPRs: [], backlog: [], recentlyClosed: [], architects: [], heldCount: 0, mailboxEscalated: false }));
|
|
728
894
|
return;
|
|
729
895
|
}
|
|
730
896
|
// Build set of active builder role_ids (lowercased) from live terminal sessions.
|
|
@@ -1033,6 +1199,249 @@ async function handleNotify(req, res, ctx) {
|
|
|
1033
1199
|
// ============================================================================
|
|
1034
1200
|
// POST /api/send — send a message to a resolved agent terminal
|
|
1035
1201
|
// ============================================================================
|
|
1202
|
+
/** Minimal JSON responder for the send route. */
|
|
1203
|
+
function sendJson(res, status, payload) {
|
|
1204
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
1205
|
+
res.end(JSON.stringify(payload));
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* The specific architect NAME whose live terminal is `terminalId`, or null when
|
|
1209
|
+
* `terminalId` is a builder/shell terminal. Reverse-maps via the routing registry
|
|
1210
|
+
* (Spec 1313): storing the canonical architect name on a mailbox row is what lets
|
|
1211
|
+
* held mail redeliver to the right terminal after a respawn, and it also tells an
|
|
1212
|
+
* architect target from a builder target for message formatting.
|
|
1213
|
+
*/
|
|
1214
|
+
function architectNameForTerminal(workspacePath, terminalId) {
|
|
1215
|
+
const entry = getWorkspaceTerminals().get(workspacePath);
|
|
1216
|
+
if (!entry)
|
|
1217
|
+
return null;
|
|
1218
|
+
for (const [name, tid] of entry.architects) {
|
|
1219
|
+
if (tid === terminalId)
|
|
1220
|
+
return name;
|
|
1221
|
+
}
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* The canonical mailbox identity of a live-resolved target: architect targets are
|
|
1226
|
+
* stored under their SPECIFIC architect name (reverse-mapped from the terminal),
|
|
1227
|
+
* everything else under its resolved agent id.
|
|
1228
|
+
*/
|
|
1229
|
+
function liveTargetIdentity(result) {
|
|
1230
|
+
const archName = architectNameForTerminal(result.workspacePath, result.terminalId);
|
|
1231
|
+
return { toAgent: archName ?? result.agent, isArchitectTarget: archName !== null };
|
|
1232
|
+
}
|
|
1233
|
+
/** Format a message per sender/target — preserves the pre-1313 formatting rules. */
|
|
1234
|
+
function formatMessageForTarget(isArchitectTarget, from, message, raw) {
|
|
1235
|
+
if (isArchitectTarget && from)
|
|
1236
|
+
return formatBuilderMessage(from, message, undefined, raw); // builder → architect
|
|
1237
|
+
if (!isArchitectTarget)
|
|
1238
|
+
return formatArchitectMessage(message, undefined, raw); // any → builder
|
|
1239
|
+
return raw ? message : formatArchitectMessage(message, undefined, false); // unknown → architect
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Route a cron notification through the Spec 1313 mailbox + gate — the Tower-wired
|
|
1243
|
+
* front half of {@link deliverCronMail} (Phase 6). Resolves the task's target to a
|
|
1244
|
+
* canonical recipient agent: a live terminal via {@link resolveTarget} plus the
|
|
1245
|
+
* architect reverse-map ({@link liveTargetIdentity}, because a bare `architect`
|
|
1246
|
+
* target resolves to the generic id, which the mailbox can't address), or — when the
|
|
1247
|
+
* agent is known but has no live PTY — via {@link resolveAgentInRegistry}, so the
|
|
1248
|
+
* message HOLDS as `no-live-pty` instead of vanishing (spec decision 9). Then it
|
|
1249
|
+
* hands off to the registry-free core. Cron is a non-builder sender, so no
|
|
1250
|
+
* sender-affinity or spoofing check applies. Wired into the cron scheduler as its
|
|
1251
|
+
* `deliver` port (see `initCron`), keeping the scheduler ignorant of mailbox
|
|
1252
|
+
* internals and giving cron exactly one gated path shared with `handleSend`.
|
|
1253
|
+
*/
|
|
1254
|
+
export async function deliverCronMessage(task, message, log) {
|
|
1255
|
+
const db = getGlobalDb();
|
|
1256
|
+
const ports = makeDeliveryPorts(log);
|
|
1257
|
+
// Preserve the pre-1313 cron framing: a message FROM the `af-cron` pseudo-builder,
|
|
1258
|
+
// regardless of whether the target is an architect or a builder.
|
|
1259
|
+
const base = {
|
|
1260
|
+
body: message,
|
|
1261
|
+
formattedMessage: formatBuilderMessage(CRON_SENDER, message),
|
|
1262
|
+
supersedeKey: task.name,
|
|
1263
|
+
};
|
|
1264
|
+
const live = resolveTarget(task.target, task.workspacePath);
|
|
1265
|
+
if (!isResolveError(live)) {
|
|
1266
|
+
const { toAgent } = liveTargetIdentity(live);
|
|
1267
|
+
return deliverCronMail(ports, db, {
|
|
1268
|
+
...base,
|
|
1269
|
+
workspacePath: live.workspacePath,
|
|
1270
|
+
toAgent,
|
|
1271
|
+
terminalId: live.terminalId,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
// Live resolution failed. A NOT_FOUND target may still be a known agent with no
|
|
1275
|
+
// live PTY (Tower restarting, builder between respawns) — hold its mail so a
|
|
1276
|
+
// respawn drains it, instead of the old blind drop-with-WARN (decision 9).
|
|
1277
|
+
if (live.code === 'NOT_FOUND') {
|
|
1278
|
+
const reg = resolveAgentInRegistry(task.target, task.workspacePath);
|
|
1279
|
+
if (!isResolveError(reg)) {
|
|
1280
|
+
return deliverCronMail(ports, db, {
|
|
1281
|
+
...base,
|
|
1282
|
+
workspacePath: reg.workspacePath,
|
|
1283
|
+
toAgent: reg.agent,
|
|
1284
|
+
terminalId: null,
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
log('WARN', `Cron '${task.name}': target '${task.target}' not found — message not delivered`);
|
|
1289
|
+
return { outcome: 'unresolved', reason: null, mailboxId: null };
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Persist a `held` mailbox row and write the Spec 1313 `held` send response. Used
|
|
1293
|
+
* for both dead-session cases (no live PTY, held `no-live-pty`). The row exists
|
|
1294
|
+
* before the response returns, so the backstop drainer will redeliver it once the
|
|
1295
|
+
* agent has a clean prompt — nothing is dropped.
|
|
1296
|
+
*/
|
|
1297
|
+
function holdAndRespond(res, ctx, input, reason) {
|
|
1298
|
+
const row = enqueueMailbox(getGlobalDb(), { ...input, reason });
|
|
1299
|
+
ctx.log('INFO', `Message held (${reason}) → ${input.toAgent} @ ${path.basename(input.workspacePath)} (mailbox ${row.id.slice(0, 8)}...)`);
|
|
1300
|
+
// A new held row appeared → refresh the held-count indicator (Spec 1313, Phase 7).
|
|
1301
|
+
ctx.broadcastNotification({ type: 'overview-changed', title: 'Held mail changed', body: `held ${reason}` });
|
|
1302
|
+
sendJson(res, 200, {
|
|
1303
|
+
ok: true,
|
|
1304
|
+
terminalId: input.terminalId ?? null,
|
|
1305
|
+
resolvedTo: input.toAgent,
|
|
1306
|
+
deferred: true, // back-compat: a held message is "deferred" to old binaries
|
|
1307
|
+
delivered: false,
|
|
1308
|
+
held: true,
|
|
1309
|
+
reason,
|
|
1310
|
+
mailboxId: row.id,
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Handle a delayed (`--delay`) send (Spec 1307, re-homed onto the Spec 1313 mailbox; round 3
|
|
1315
|
+
* durable rework). The row is RESOLVED, authorized (the builder-spoofing check inside
|
|
1316
|
+
* `resolveTarget`), formatted, and PERSISTED here at REQUEST time with `not_before = now +
|
|
1317
|
+
* delay*1000` — the security property the immediate path documents (a delayed send must not
|
|
1318
|
+
* defer an authorization check past the conditions that would fail it) is preserved, and only
|
|
1319
|
+
* DELIVERY is deferred. Persisting at request time is what makes `--delay` DURABLE across a
|
|
1320
|
+
* Tower restart (the conscious, architect+maintainer-approved reversal of Spec 1307's
|
|
1321
|
+
* drop-on-restart semantics): the render gate still guarantees a post-restart delivery only
|
|
1322
|
+
* ever lands on a verified-empty prompt, and a pre-due row is visible/cancellable in
|
|
1323
|
+
* `afx inbox`.
|
|
1324
|
+
*
|
|
1325
|
+
* Resolution is live-first then registry (a known agent with no live PTY) so a
|
|
1326
|
+
* dead/unwritable/registry-only target schedules UNIFORMLY — a delayed send does not require a
|
|
1327
|
+
* live session now. reason is left null: the row is SCHEDULED, not held-for-a-reason; the
|
|
1328
|
+
* drainer re-evaluates liveness at due time.
|
|
1329
|
+
*
|
|
1330
|
+
* The body is NEVER written from here. A normal delayed send is delivered by the gated
|
|
1331
|
+
* backstop drainer once `not_before` passes (≤ one 1.5 s tick after due — the delay is a lower
|
|
1332
|
+
* bound). A delayed `--interrupt` (change 2 reshape) additionally keeps a small in-memory timer
|
|
1333
|
+
* that fires ONLY the Ctrl+C at due time (guarded by `isStillLive` + a re-fetched writable
|
|
1334
|
+
* session, inside the submission lock); the ^C ends the turn and the body then delivers through
|
|
1335
|
+
* the SAME gate every send uses — nothing is marked delivered here, so a #1198 dropped write or
|
|
1336
|
+
* a shutdown during the wait can never falsely report delivery or double-deliver. A restart
|
|
1337
|
+
* during the wait loses only the ^C nudge, never the message.
|
|
1338
|
+
*/
|
|
1339
|
+
function handleDelayedSend(res, ctx, db, params) {
|
|
1340
|
+
const { to, workspace, from, message, raw, noEnter, interrupt, deliverAfter, senderWorkspace } = params;
|
|
1341
|
+
const now = Date.now();
|
|
1342
|
+
const notBefore = now + deliverAfter * 1000;
|
|
1343
|
+
// Resolve to a canonical AGENT — live first, then registry (known agent, no live PTY yet).
|
|
1344
|
+
let workspacePath;
|
|
1345
|
+
let toAgent;
|
|
1346
|
+
let isArchitectTarget;
|
|
1347
|
+
let terminalId;
|
|
1348
|
+
const live = resolveTarget(to, workspace, from);
|
|
1349
|
+
if (!isResolveError(live)) {
|
|
1350
|
+
const identity = liveTargetIdentity(live);
|
|
1351
|
+
workspacePath = live.workspacePath;
|
|
1352
|
+
toAgent = identity.toAgent;
|
|
1353
|
+
isArchitectTarget = identity.isArchitectTarget;
|
|
1354
|
+
terminalId = live.terminalId;
|
|
1355
|
+
}
|
|
1356
|
+
else if (live.code === 'NOT_FOUND') {
|
|
1357
|
+
const reg = resolveAgentInRegistry(to, workspace, from);
|
|
1358
|
+
if (isResolveError(reg)) {
|
|
1359
|
+
const statusCode = reg.code === 'AMBIGUOUS' ? 409 : reg.code === 'NO_CONTEXT' ? 400 : 404;
|
|
1360
|
+
const errorCode = reg.code === 'NO_CONTEXT' ? 'INVALID_PARAMS' : reg.code;
|
|
1361
|
+
sendJson(res, statusCode, { error: errorCode, message: reg.message });
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
workspacePath = reg.workspacePath;
|
|
1365
|
+
toAgent = reg.agent;
|
|
1366
|
+
isArchitectTarget = reg.kind === 'architect';
|
|
1367
|
+
terminalId = null;
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
const statusCode = live.code === 'AMBIGUOUS' ? 409 : live.code === 'NO_CONTEXT' ? 400 : 404;
|
|
1371
|
+
const errorCode = live.code === 'NO_CONTEXT' ? 'INVALID_PARAMS' : live.code;
|
|
1372
|
+
sendJson(res, statusCode, { error: errorCode, message: live.message });
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const formattedMessage = formatMessageForTarget(isArchitectTarget, from, message, raw);
|
|
1376
|
+
// Persist NOW, at request time, with the due time. reason=null → SCHEDULED, not stuck.
|
|
1377
|
+
const row = enqueueMailbox(db, {
|
|
1378
|
+
workspacePath,
|
|
1379
|
+
toAgent,
|
|
1380
|
+
body: message,
|
|
1381
|
+
formattedMessage,
|
|
1382
|
+
fromAgent: from ?? null,
|
|
1383
|
+
fromWorkspace: senderWorkspace,
|
|
1384
|
+
noEnter,
|
|
1385
|
+
terminalId,
|
|
1386
|
+
notBefore,
|
|
1387
|
+
});
|
|
1388
|
+
// A new held (scheduled) row appeared → refresh the held-count indicator (Phase 7).
|
|
1389
|
+
ctx.broadcastNotification({ type: 'overview-changed', title: 'Held mail changed', body: 'scheduled' });
|
|
1390
|
+
if (interrupt) {
|
|
1391
|
+
// Change 2 reshape: an in-memory timer that fires ONLY the Ctrl+C at due time. It writes
|
|
1392
|
+
// no message body and marks nothing delivered — the body delivers through the gated
|
|
1393
|
+
// drainer after the ^C ends the turn. Everything the ^C depends on — `isStillLive`
|
|
1394
|
+
// (delayed-send.ts's generation guard), the session's existence, and its writability — is
|
|
1395
|
+
// re-checked INSIDE the submission lock, right before the write, so a shutdown OR a session
|
|
1396
|
+
// teardown/respawn during the lock-wait writes nothing (invariant 2; the directive's
|
|
1397
|
+
// preferred shape). Diagnostics arg falls back to toAgent when the target has no terminal
|
|
1398
|
+
// id yet.
|
|
1399
|
+
scheduleDelayedSend(deliverAfter, terminalId ?? toAgent, (isStillLive) => {
|
|
1400
|
+
if (!isStillLive())
|
|
1401
|
+
return; // shutdown before/at due → drop the ^C nudge (body survives)
|
|
1402
|
+
if (terminalId) {
|
|
1403
|
+
let fired = false;
|
|
1404
|
+
void submitToSession(terminalId, () => {
|
|
1405
|
+
// Re-check EVERYTHING inside the lock: the queued submission can acquire the lock only
|
|
1406
|
+
// AFTER a shutdown or a session teardown/respawn that landed while it waited behind an
|
|
1407
|
+
// in-flight write. Re-fetch the session and re-check live + writable here; bail with no
|
|
1408
|
+
// ^C otherwise (the body still delivers via the gate).
|
|
1409
|
+
if (!isStillLive())
|
|
1410
|
+
return 0;
|
|
1411
|
+
const live = getTerminalManager().getSession(terminalId);
|
|
1412
|
+
if (!live || !live.writable)
|
|
1413
|
+
return 0;
|
|
1414
|
+
live.write('\x03'); // Ctrl+C only — end the turn; body follows via the gate
|
|
1415
|
+
fired = true;
|
|
1416
|
+
return 0; // no body, no Enter on this path
|
|
1417
|
+
})
|
|
1418
|
+
.then(() => ctx.log('INFO', fired
|
|
1419
|
+
? `Delayed interrupt ^C fired → ${toAgent} (terminal ${terminalId.slice(0, 8)}...); body delivers via the gate`
|
|
1420
|
+
: `Delayed interrupt for ${toAgent}: session not live/writable at write time — body delivers via the gate on the next clean pass`))
|
|
1421
|
+
.catch((err) => ctx.log('ERROR', `Delayed interrupt ^C failed for ${toAgent}: ${err.message}`));
|
|
1422
|
+
}
|
|
1423
|
+
else {
|
|
1424
|
+
ctx.log('INFO', `Delayed interrupt for ${toAgent}: no live terminal at due time — body delivers via the gate on the next clean pass`);
|
|
1425
|
+
}
|
|
1426
|
+
// Nudge the gated drainer so the now-due body delivers promptly (gated — a spurious
|
|
1427
|
+
// nudge onto a busy/mid-turn screen simply re-holds; the backstop is the ultimate net).
|
|
1428
|
+
void getMailboxDrainer().scheduleDrain(workspacePath, toAgent);
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
// A normal delayed send keeps NO timer: the persisted `not_before` row is delivered by the
|
|
1432
|
+
// gated backstop drainer once due — durable across restart by construction.
|
|
1433
|
+
ctx.log('INFO', `Message scheduled (+${deliverAfter}s): ${from ?? 'unknown'} → ${toAgent} (mailbox ${row.id.slice(0, 8)}...)`);
|
|
1434
|
+
sendJson(res, 200, {
|
|
1435
|
+
ok: true,
|
|
1436
|
+
terminalId,
|
|
1437
|
+
resolvedTo: toAgent,
|
|
1438
|
+
deferred: false,
|
|
1439
|
+
scheduled: true,
|
|
1440
|
+
deliverAfter,
|
|
1441
|
+
mailboxId: row.id,
|
|
1442
|
+
notBefore,
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1036
1445
|
async function handleSend(req, res, ctx) {
|
|
1037
1446
|
const body = await parseJsonBody(req);
|
|
1038
1447
|
// Validate required fields
|
|
@@ -1058,99 +1467,381 @@ async function handleSend(req, res, ctx) {
|
|
|
1058
1467
|
const raw = options.raw === true;
|
|
1059
1468
|
const noEnter = options.noEnter === true;
|
|
1060
1469
|
const interrupt = options.interrupt === true;
|
|
1061
|
-
|
|
1470
|
+
const escape = options.escape === true;
|
|
1471
|
+
// Spec 1307 `--delay` (re-homed onto the mailbox): optional deferred delivery.
|
|
1472
|
+
// Validated here as well as at the CLI boundary — /api/send is a public route, so an
|
|
1473
|
+
// unvalidated value would become a setTimeout that fires instantly (NaN) or never
|
|
1474
|
+
// (Infinity). `escape` cannot be combined with a delay: an ESC interrupts the CURRENT
|
|
1475
|
+
// turn by design, so deferring it is contradictory — refuse rather than silently drop
|
|
1476
|
+
// one of the two (a quietly-dropped delay would look like it worked).
|
|
1477
|
+
let deliverAfter;
|
|
1478
|
+
if (options.deliverAfter !== undefined && options.deliverAfter !== null) {
|
|
1479
|
+
const delayError = validateDelaySeconds(options.deliverAfter);
|
|
1480
|
+
if (delayError) {
|
|
1481
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1482
|
+
res.end(JSON.stringify({ error: 'INVALID_PARAMS', message: delayError }));
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (escape) {
|
|
1486
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1487
|
+
res.end(JSON.stringify({
|
|
1488
|
+
error: 'INVALID_PARAMS',
|
|
1489
|
+
message: 'escape cannot be combined with a delay: an ESC keystroke bypasses buffering by design so that it interrupts the CURRENT turn. Send the ESC now, or send a delayed message without escape.',
|
|
1490
|
+
}));
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
deliverAfter = options.deliverAfter;
|
|
1494
|
+
}
|
|
1495
|
+
const db = getGlobalDb();
|
|
1496
|
+
const senderWorkspace = fromWorkspace ?? workspace ?? 'unknown';
|
|
1497
|
+
// Spec 1313 round 3: a delayed send (`--delay`) is RESOLVED, authorized, formatted, and
|
|
1498
|
+
// PERSISTED here at REQUEST time, then deferred — it does NOT require a live session now,
|
|
1499
|
+
// so it is handled BEFORE the immediate live/dead/unwritable/escape/interrupt branches
|
|
1500
|
+
// below (those would 404/503 a delayed send to a target that has no live PTY yet). Only
|
|
1501
|
+
// DELIVERY is deferred, through the same render gate every send uses; the row's `not_before`
|
|
1502
|
+
// makes the delay durable across a Tower restart.
|
|
1503
|
+
if (deliverAfter !== undefined) {
|
|
1504
|
+
handleDelayedSend(res, ctx, db, {
|
|
1505
|
+
to, workspace, from, message, raw, noEnter, interrupt, deliverAfter, senderWorkspace,
|
|
1506
|
+
});
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
// Resolve the target address against LIVE terminals.
|
|
1062
1510
|
// Spec 755: pass `from` so architect resolution is sender-affinity-aware
|
|
1063
1511
|
// when the sender is a builder. Non-builder senders see unchanged behavior.
|
|
1064
1512
|
const result = resolveTarget(to, workspace, from);
|
|
1513
|
+
// --- Resolution failed against live terminals ---
|
|
1065
1514
|
if (isResolveError(result)) {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1515
|
+
// Spec 1313 dead-session seam: a NOT_FOUND target may still be a KNOWN agent
|
|
1516
|
+
// with no live PTY (e.g. registered in global.db while Tower restarts). Hold
|
|
1517
|
+
// its mail instead of 404ing. escape/interrupt act on a live session only, so
|
|
1518
|
+
// an unresolved target keeps the original error for them.
|
|
1519
|
+
if (result.code === 'NOT_FOUND' && !escape && !interrupt) {
|
|
1520
|
+
const reg = resolveAgentInRegistry(to, workspace, from);
|
|
1521
|
+
if (!isResolveError(reg)) {
|
|
1522
|
+
holdAndRespond(res, ctx, {
|
|
1523
|
+
workspacePath: reg.workspacePath,
|
|
1524
|
+
toAgent: reg.agent,
|
|
1525
|
+
body: message,
|
|
1526
|
+
formattedMessage: formatMessageForTarget(reg.kind === 'architect', from, message, raw),
|
|
1527
|
+
fromAgent: from ?? null,
|
|
1528
|
+
fromWorkspace: senderWorkspace,
|
|
1529
|
+
noEnter,
|
|
1530
|
+
terminalId: null,
|
|
1531
|
+
}, 'no-live-pty');
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
if (reg.code === 'AMBIGUOUS') {
|
|
1535
|
+
sendJson(res, 409, { error: 'AMBIGUOUS', message: reg.message });
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
// else: fall through to the original live-resolution error below.
|
|
1539
|
+
}
|
|
1540
|
+
const statusCode = result.code === 'AMBIGUOUS' ? 409 : result.code === 'NO_CONTEXT' ? 400 : 404;
|
|
1069
1541
|
// Map NO_CONTEXT to INVALID_PARAMS per plan's error contract
|
|
1070
1542
|
const errorCode = result.code === 'NO_CONTEXT' ? 'INVALID_PARAMS' : result.code;
|
|
1071
|
-
res
|
|
1072
|
-
res.end(JSON.stringify({ error: errorCode, message: result.message }));
|
|
1543
|
+
sendJson(res, statusCode, { error: errorCode, message: result.message });
|
|
1073
1544
|
return;
|
|
1074
1545
|
}
|
|
1075
|
-
//
|
|
1546
|
+
// --- Live target resolved; locate its session ---
|
|
1076
1547
|
const manager = getTerminalManager();
|
|
1077
1548
|
const session = manager.getSession(result.terminalId);
|
|
1549
|
+
const { toAgent, isArchitectTarget } = liveTargetIdentity(result);
|
|
1550
|
+
// Dead session: the routing entry resolved but the PTY is gone (exited > 30s).
|
|
1551
|
+
// Hold a normal message; escape/interrupt need a live session → original 404.
|
|
1078
1552
|
if (!session) {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1553
|
+
if (escape || interrupt) {
|
|
1554
|
+
sendJson(res, 404, {
|
|
1555
|
+
error: 'NOT_FOUND',
|
|
1556
|
+
message: `Terminal session ${result.terminalId} not found (agent '${result.agent}' resolved but terminal is gone).`,
|
|
1557
|
+
});
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
holdAndRespond(res, ctx, {
|
|
1561
|
+
workspacePath: result.workspacePath,
|
|
1562
|
+
toAgent,
|
|
1563
|
+
body: message,
|
|
1564
|
+
formattedMessage: formatMessageForTarget(isArchitectTarget, from, message, raw),
|
|
1565
|
+
fromAgent: from ?? null,
|
|
1566
|
+
fromWorkspace: senderWorkspace,
|
|
1567
|
+
noEnter,
|
|
1568
|
+
terminalId: result.terminalId,
|
|
1569
|
+
}, 'no-live-pty');
|
|
1084
1570
|
return;
|
|
1085
1571
|
}
|
|
1086
|
-
//
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1572
|
+
// #1198: a session whose shellper connection died still reports status
|
|
1573
|
+
// 'running', but every write is dropped. Hold a normal message (it delivers
|
|
1574
|
+
// when the connection recovers); keep the loud 503 for escape/interrupt, which
|
|
1575
|
+
// are operator actions that require the live PTY here and now.
|
|
1576
|
+
if (!session.writable) {
|
|
1577
|
+
if (escape || interrupt) {
|
|
1578
|
+
ctx.log('ERROR', `Interrupt/ESC not deliverable: ${from ?? 'unknown'} → ${result.agent} (terminal not writable, shellper connection down)`);
|
|
1579
|
+
sendJson(res, 503, {
|
|
1580
|
+
error: 'TERMINAL_NOT_WRITABLE',
|
|
1581
|
+
message: `Terminal for '${result.agent}' is not accepting input (its process connection is down). Retry shortly; if this persists, check Tower logs.`,
|
|
1582
|
+
});
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
holdAndRespond(res, ctx, {
|
|
1586
|
+
workspacePath: result.workspacePath,
|
|
1587
|
+
toAgent,
|
|
1588
|
+
body: message,
|
|
1589
|
+
formattedMessage: formatMessageForTarget(isArchitectTarget, from, message, raw),
|
|
1590
|
+
fromAgent: from ?? null,
|
|
1591
|
+
fromWorkspace: senderWorkspace,
|
|
1592
|
+
noEnter,
|
|
1593
|
+
terminalId: result.terminalId,
|
|
1594
|
+
}, 'no-live-pty');
|
|
1595
|
+
return;
|
|
1096
1596
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1597
|
+
// --- Live, writable session ---
|
|
1598
|
+
// Spec 1273: `escape` delivers a bare ESC keystroke and returns. Explicit human
|
|
1599
|
+
// bypass — no gate, no mailbox row. ESC ends the running turn so already-queued
|
|
1600
|
+
// messages process; the trailing Enter (default) is what lets them through
|
|
1601
|
+
// (matching the verified recovery `afx send <b> --raw "$(printf '\x1b')"`).
|
|
1602
|
+
if (escape) {
|
|
1603
|
+
// Awaited: the response must not claim delivery before the ESC and its
|
|
1604
|
+
// Enter have actually been written (Spec 1273 verify).
|
|
1605
|
+
await submitToSession(result.terminalId, () => writeEscapeToSession(session, noEnter));
|
|
1606
|
+
broadcastMessage({
|
|
1607
|
+
type: 'message',
|
|
1608
|
+
from: { project: path.basename(senderWorkspace), agent: from ?? 'unknown' },
|
|
1609
|
+
to: { project: path.basename(result.workspacePath), agent: toAgent },
|
|
1610
|
+
content: '<ESC>',
|
|
1611
|
+
metadata: { raw: true, source: 'api', escape: true },
|
|
1612
|
+
timestamp: new Date().toISOString(),
|
|
1613
|
+
});
|
|
1614
|
+
ctx.log('INFO', `Interrupt (ESC) sent: ${from ?? 'unknown'} → ${toAgent} (terminal ${result.terminalId.slice(0, 8)}...)`);
|
|
1615
|
+
sendJson(res, 200, { ok: true, terminalId: result.terminalId, resolvedTo: toAgent, deferred: false });
|
|
1616
|
+
return;
|
|
1100
1617
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
},
|
|
1109
|
-
to: {
|
|
1110
|
-
project: path.basename(result.workspacePath),
|
|
1111
|
-
agent: result.agent,
|
|
1112
|
-
},
|
|
1113
|
-
content: message,
|
|
1114
|
-
metadata: { raw, source: 'api' },
|
|
1115
|
-
timestamp: new Date().toISOString(),
|
|
1116
|
-
};
|
|
1117
|
-
const logMessage = `Message sent: ${from ?? 'unknown'} → ${result.agent} (terminal ${result.terminalId.slice(0, 8)}...)`;
|
|
1118
|
-
// Optionally interrupt first — bypass buffering entirely
|
|
1618
|
+
const formattedMessage = formatMessageForTarget(isArchitectTarget, from, message, raw);
|
|
1619
|
+
// NB: `--delay` (deliverAfter) is handled earlier by handleDelayedSend, before this
|
|
1620
|
+
// immediate-path block — a delayed send is resolved + persisted at request time and does
|
|
1621
|
+
// not fall through here.
|
|
1622
|
+
// Spec 1313: `interrupt` is the explicit human bypass. Ctrl+C, then deliver
|
|
1623
|
+
// WITHOUT the render-gate (the operator is looking at this terminal). A row is
|
|
1624
|
+
// still persisted and marked delivered for audit parity — every send is a row.
|
|
1119
1625
|
if (interrupt) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
// Defer only when user has typed recently (within idle threshold).
|
|
1125
|
-
// Bugfix #492: removed session.composing check — composing gets stuck true
|
|
1126
|
-
// after non-Enter keystrokes (Ctrl+C, arrows, Tab), causing 60s delays.
|
|
1127
|
-
const shouldDefer = !interrupt && !session.isUserIdle(sendBuffer.idleThresholdMs);
|
|
1128
|
-
if (shouldDefer) {
|
|
1129
|
-
// User is actively typing — buffer for deferred delivery
|
|
1130
|
-
sendBuffer.enqueue({
|
|
1131
|
-
sessionId: result.terminalId,
|
|
1626
|
+
const row = enqueueMailbox(db, {
|
|
1627
|
+
workspacePath: result.workspacePath,
|
|
1628
|
+
toAgent,
|
|
1629
|
+
body: message,
|
|
1132
1630
|
formattedMessage,
|
|
1631
|
+
fromAgent: from ?? null,
|
|
1632
|
+
fromWorkspace: senderWorkspace,
|
|
1133
1633
|
noEnter,
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1634
|
+
terminalId: result.terminalId,
|
|
1635
|
+
});
|
|
1636
|
+
// Claim the row as delivered SYNCHRONOUSLY, before any await (CMAP round 2 — Codex): the
|
|
1637
|
+
// interrupt writes the message itself (a gate bypass), so the row must never be visible to
|
|
1638
|
+
// the mailbox drainer as `held`, or a concurrent backstop/scheduleDrain pass could gate-deliver
|
|
1639
|
+
// the SAME row and put the bytes on the wire twice. enqueue→markDelivered are both synchronous
|
|
1640
|
+
// (no await between), so there is no window for the drainer to pick it up before we own it.
|
|
1641
|
+
// Tradeoff (CMAP round 3 — Codex/Claude): claiming BEFORE the write below means that if
|
|
1642
|
+
// submitToSession throws/crashes, the row reads `delivered` for audit though no bytes reached
|
|
1643
|
+
// the PTY — the message is lost, not retried. This is the deliberate choice: the write is not
|
|
1644
|
+
// transactional, so a partial write may already have put bytes on the wire, and re-holding (the
|
|
1645
|
+
// alternative) would let the backstop gate-deliver a SECOND copy. Losing a crashed interrupt is
|
|
1646
|
+
// preferred over double-delivering it; `--interrupt` is the explicit human gate-bypass anyway.
|
|
1647
|
+
markMailboxDelivered(db, row.id);
|
|
1648
|
+
// Deliver the interrupt as ONE atomic critical section under the Spec 1273 per-terminal
|
|
1649
|
+
// submission lock (CMAP round 1 — Gemini/Codex/Claude): the Ctrl+C, its 100 ms settle, and
|
|
1650
|
+
// the message write all occur inside a single lock acquisition. Previously the \x03 + the
|
|
1651
|
+
// settle sat OUTSIDE the lock, so a concurrent submission to the same terminal could land
|
|
1652
|
+
// its Ctrl+C inside another submission's text→Enter window (killing that composer) or run
|
|
1653
|
+
// during the 100 ms gap. `writeMessageToSession(..., 100)` schedules the text 100 ms after
|
|
1654
|
+
// the ^C (the settle) and returns the completion offset, so the lock is held until the
|
|
1655
|
+
// whole interrupt is on the wire; uncontended, it runs at once.
|
|
1656
|
+
// Scope of the guarantee: this serializes interrupt against interrupt/escape — the only
|
|
1657
|
+
// /api/send writers that take this per-terminal lock. It does NOT serialize against a
|
|
1658
|
+
// concurrent mailbox/backstop delivery (which writes through the per-AGENT serializer, a
|
|
1659
|
+
// disjoint lock); interrupt is the explicit gate-bypassing human action, and closing that
|
|
1660
|
+
// cross-path race would require the mailbox write edge to take this lock too (a separate,
|
|
1661
|
+
// larger change — flagged, not done here).
|
|
1662
|
+
await submitToSession(result.terminalId, () => {
|
|
1663
|
+
session.write('\x03'); // Ctrl+C
|
|
1664
|
+
return writeMessageToSession(session, formattedMessage, noEnter, 100);
|
|
1665
|
+
});
|
|
1666
|
+
broadcastMessage({
|
|
1667
|
+
type: 'message',
|
|
1668
|
+
from: { project: path.basename(senderWorkspace), agent: from ?? 'unknown' },
|
|
1669
|
+
to: { project: path.basename(result.workspacePath), agent: toAgent },
|
|
1670
|
+
content: message,
|
|
1671
|
+
metadata: { raw, source: 'api' },
|
|
1672
|
+
timestamp: new Date().toISOString(),
|
|
1137
1673
|
});
|
|
1138
|
-
ctx.log('INFO', `Message
|
|
1674
|
+
ctx.log('INFO', `Message delivered (interrupt): ${from ?? 'unknown'} → ${toAgent} (terminal ${result.terminalId.slice(0, 8)}...)`);
|
|
1675
|
+
sendJson(res, 200, {
|
|
1676
|
+
ok: true,
|
|
1677
|
+
terminalId: result.terminalId,
|
|
1678
|
+
resolvedTo: toAgent,
|
|
1679
|
+
deferred: false,
|
|
1680
|
+
delivered: true,
|
|
1681
|
+
held: false,
|
|
1682
|
+
mailboxId: row.id,
|
|
1683
|
+
reason: null,
|
|
1684
|
+
});
|
|
1685
|
+
return;
|
|
1139
1686
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1687
|
+
// Spec 1313 normal path: PERSIST first (survives a crash), then attempt gated
|
|
1688
|
+
// delivery through the single serialized path. The response reports the row's
|
|
1689
|
+
// real first outcome — a clean, render-verified empty prompt delivers now;
|
|
1690
|
+
// anything else (busy/menu/wrapper/no-profile) stays held for the backstop.
|
|
1691
|
+
const row = enqueueMailbox(db, {
|
|
1692
|
+
workspacePath: result.workspacePath,
|
|
1693
|
+
toAgent,
|
|
1694
|
+
body: message,
|
|
1695
|
+
formattedMessage,
|
|
1696
|
+
fromAgent: from ?? null,
|
|
1697
|
+
fromWorkspace: senderWorkspace,
|
|
1698
|
+
noEnter,
|
|
1699
|
+
terminalId: result.terminalId,
|
|
1700
|
+
});
|
|
1701
|
+
// Deliver to the session THIS request already resolved rather than re-resolving
|
|
1702
|
+
// by agent: the base resolver would repeat the routing-map lookup (redundant) and
|
|
1703
|
+
// could target a different terminal if the map changed mid-request. The backstop
|
|
1704
|
+
// drainer, which only has the agent, still uses the base resolver.
|
|
1705
|
+
const basePorts = makeDeliveryPorts(ctx.log);
|
|
1706
|
+
const ports = {
|
|
1707
|
+
...basePorts,
|
|
1708
|
+
getSessionForAgent: (ws, agent) => ws === result.workspacePath && agent === toAgent ? session : basePorts.getSessionForAgent(ws, agent),
|
|
1709
|
+
};
|
|
1710
|
+
try {
|
|
1711
|
+
await deliverAgentMailSerialized(ports, db, result.workspacePath, toAgent);
|
|
1146
1712
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1713
|
+
catch (err) {
|
|
1714
|
+
// A gate/write error leaves the row HELD (markDelivered only runs on a
|
|
1715
|
+
// completed write); the backstop drainer will retry. Report held, not a 500.
|
|
1716
|
+
ctx.log('ERROR', `Delivery attempt errored for ${toAgent} (row ${row.id.slice(0, 8)}... stays held): ${err.message}`);
|
|
1717
|
+
}
|
|
1718
|
+
const stored = getMailboxById(db, row.id);
|
|
1719
|
+
if (stored?.status === 'delivered') {
|
|
1720
|
+
ctx.log('INFO', `Message delivered: ${from ?? 'unknown'} → ${toAgent} (terminal ${result.terminalId.slice(0, 8)}...)`);
|
|
1721
|
+
sendJson(res, 200, {
|
|
1722
|
+
ok: true,
|
|
1723
|
+
terminalId: result.terminalId,
|
|
1724
|
+
resolvedTo: toAgent,
|
|
1725
|
+
deferred: false,
|
|
1726
|
+
delivered: true,
|
|
1727
|
+
held: false,
|
|
1728
|
+
mailboxId: row.id,
|
|
1729
|
+
reason: null,
|
|
1730
|
+
});
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const reason = stored?.reason ?? 'busy';
|
|
1734
|
+
ctx.log('INFO', `Message held (${reason}): ${from ?? 'unknown'} → ${toAgent} (mailbox ${row.id.slice(0, 8)}...)`);
|
|
1735
|
+
// The message stayed held → a new held row is in the set; refresh the indicator
|
|
1736
|
+
// count (Spec 1313, Phase 7). The delivered branch above needs no fire — the
|
|
1737
|
+
// delivery path's onHeldStateChange already broadcast when the row left the set.
|
|
1738
|
+
ctx.broadcastNotification({ type: 'overview-changed', title: 'Held mail changed', body: `held ${reason}` });
|
|
1739
|
+
sendJson(res, 200, {
|
|
1149
1740
|
ok: true,
|
|
1150
1741
|
terminalId: result.terminalId,
|
|
1151
|
-
resolvedTo:
|
|
1152
|
-
deferred:
|
|
1742
|
+
resolvedTo: toAgent,
|
|
1743
|
+
deferred: true,
|
|
1744
|
+
delivered: false,
|
|
1745
|
+
held: true,
|
|
1746
|
+
reason,
|
|
1747
|
+
mailboxId: row.id,
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* GET /api/inbox — list held (undelivered) mailbox rows for a workspace. Backs the
|
|
1752
|
+
* workspace-scoped `afx inbox` (Spec 1313 decision 8): `?workspace=<path>` selects the
|
|
1753
|
+
* workspace (the CLI passes the current one by default); the path is normalized to the
|
|
1754
|
+
* same realpath form the enqueue path stores, so a raw workspace root still matches its
|
|
1755
|
+
* held rows. Omitting `?workspace=` lists every workspace — an API-level convenience the
|
|
1756
|
+
* CLI never triggers, kept for direct callers. Metadata-only projection (Spec 1313
|
|
1757
|
+
* redaction rule): id, addresses, why-held reason, escalation flag, and enqueue time —
|
|
1758
|
+
* the message BODY is deliberately never surfaced here (it travels only over the live
|
|
1759
|
+
* terminal stream on delivery). `escalated` is normalized from SQLite's 0/1 to a bool.
|
|
1760
|
+
*/
|
|
1761
|
+
function handleInboxList(res, url) {
|
|
1762
|
+
const rawWorkspace = url.searchParams.get('workspace');
|
|
1763
|
+
// Normalize to the stored realpath key (mailbox workspace_path is normalized at
|
|
1764
|
+
// enqueue — tower-routes handleSend / holdAndRespond — matching overview.ts). Without
|
|
1765
|
+
// this a symlinked workspace root would miss its own held rows.
|
|
1766
|
+
const workspace = rawWorkspace ? normalizeWorkspacePath(rawWorkspace) : undefined;
|
|
1767
|
+
const rows = listHeldMailbox(getGlobalDb(), workspace);
|
|
1768
|
+
const projected = rows.map((r) => ({
|
|
1769
|
+
id: r.id,
|
|
1770
|
+
workspacePath: r.workspace_path,
|
|
1771
|
+
toAgent: r.to_agent,
|
|
1772
|
+
fromAgent: r.from_agent,
|
|
1773
|
+
reason: r.reason,
|
|
1774
|
+
escalated: r.escalated === 1,
|
|
1775
|
+
createdAt: r.created_at,
|
|
1776
|
+
// Spec 1313 round 3: due time of a pre-due delayed (`--delay`) row; null = deliver-ASAP.
|
|
1777
|
+
// The CLI renders "in Ns" for a row whose notBefore is still in the future.
|
|
1778
|
+
notBefore: r.not_before,
|
|
1153
1779
|
}));
|
|
1780
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1781
|
+
res.end(JSON.stringify(projected));
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* POST /api/inbox/:id/dismiss — mark a held row `dismissed` (operator-cleared via
|
|
1785
|
+
* `afx inbox dismiss`). Soft transition: the row is marked, not deleted, and NEVER
|
|
1786
|
+
* delivered. The dispatch matches this path for ANY method, so the method is guarded
|
|
1787
|
+
* here: a non-POST request (e.g. GET) must not mutate state → 405. 404 when the id names
|
|
1788
|
+
* no currently-held row (already terminal or unknown), so the CLI reports a clean error.
|
|
1789
|
+
* On success, fires `overview-changed` so the held-count indicator drops immediately.
|
|
1790
|
+
* Authorized at the workspace-human trust level — any local operator may dismiss any held
|
|
1791
|
+
* row (Spec 1313 decision 8); no ownership check.
|
|
1792
|
+
*/
|
|
1793
|
+
function handleInboxDismiss(req, res, ctx, match) {
|
|
1794
|
+
// Dismissal mutates state — only POST may reach it. The path match in the dispatch is
|
|
1795
|
+
// method-agnostic, so without this guard a GET (or any method) to this URL would dismiss
|
|
1796
|
+
// mail. Matches the method-guard convention used by the cron action routes.
|
|
1797
|
+
if (req.method !== 'POST') {
|
|
1798
|
+
sendJson(res, 405, { error: 'Method not allowed' });
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
const id = decodeURIComponent(match[1]);
|
|
1802
|
+
if (!dismissMailbox(getGlobalDb(), id)) {
|
|
1803
|
+
sendJson(res, 404, { error: 'NOT_FOUND', message: `No held message with id '${id}'` });
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
ctx.broadcastNotification({ type: 'overview-changed', title: 'Held mail changed', body: 'dismissed' });
|
|
1807
|
+
ctx.log('INFO', `Inbox: dismissed held message ${id.slice(0, 8)}...`);
|
|
1808
|
+
sendJson(res, 200, { ok: true });
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* GET /api/inbox/:id — return a single mailbox row INCLUDING its body. Backs
|
|
1812
|
+
* `afx inbox show <id>` (Spec 1313 §178: `afx inbox` is a UI surface that legitimately
|
|
1813
|
+
* displays message bodies over the local Tower connection — the redaction rule applies to
|
|
1814
|
+
* logs/diagnostics/telemetry only, never this view). Mirrors dismiss's addressing model:
|
|
1815
|
+
* by unique id at the workspace-human trust level, no per-recipient/workspace ownership
|
|
1816
|
+
* check (decision 8). GET-only (the path match in the dispatch is method-agnostic, so a
|
|
1817
|
+
* non-GET is rejected here); 404 when the id names no row. The body is returned to the
|
|
1818
|
+
* caller but never logged.
|
|
1819
|
+
*/
|
|
1820
|
+
function handleInboxShow(req, res, match) {
|
|
1821
|
+
if (req.method !== 'GET') {
|
|
1822
|
+
sendJson(res, 405, { error: 'Method not allowed' });
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
const id = decodeURIComponent(match[1]);
|
|
1826
|
+
const row = getMailboxById(getGlobalDb(), id);
|
|
1827
|
+
if (!row) {
|
|
1828
|
+
sendJson(res, 404, { error: 'NOT_FOUND', message: `No message with id '${id}'` });
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
sendJson(res, 200, {
|
|
1832
|
+
id: row.id,
|
|
1833
|
+
workspacePath: row.workspace_path,
|
|
1834
|
+
toAgent: row.to_agent,
|
|
1835
|
+
fromAgent: row.from_agent,
|
|
1836
|
+
fromWorkspace: row.from_workspace,
|
|
1837
|
+
status: row.status,
|
|
1838
|
+
reason: row.reason,
|
|
1839
|
+
escalated: row.escalated === 1,
|
|
1840
|
+
body: row.body,
|
|
1841
|
+
createdAt: row.created_at,
|
|
1842
|
+
notBefore: row.not_before,
|
|
1843
|
+
resolvedAt: row.resolved_at,
|
|
1844
|
+
});
|
|
1154
1845
|
}
|
|
1155
1846
|
async function handleBrowse(res, url) {
|
|
1156
1847
|
const inputPath = url.searchParams.get('path') || '';
|
|
@@ -1706,11 +2397,21 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
|
|
|
1706
2397
|
env: shellEnv,
|
|
1707
2398
|
...defaultSessionOptions(),
|
|
1708
2399
|
});
|
|
1709
|
-
|
|
2400
|
+
// Read session info BEFORE awaiting replay: an instantly-exiting
|
|
2401
|
+
// child's EXIT frame can remove the session from the manager during
|
|
2402
|
+
// the await, and this lookup must not miss (#1198).
|
|
1710
2403
|
const shellperInfo = shellperManager.getSessionInfo(sessionId);
|
|
2404
|
+
const replayData = await client.waitForReplay(); // #1198: fresh shellpers always send REPLAY (possibly empty); awaiting avoids racing early child output
|
|
1711
2405
|
const session = manager.createSessionRaw({
|
|
1712
2406
|
label: `Shell ${shellId.replace('shell-', '')}`,
|
|
1713
2407
|
cwd: workspacePath,
|
|
2408
|
+
// Spec 1313: thread/persist for reconstruction symmetry with the other
|
|
2409
|
+
// createSessionRaw sites. A workspace-root shell resolves to no-profile
|
|
2410
|
+
// (its command is a shell, not an agent, and the cwd has no launch script),
|
|
2411
|
+
// so `afx send` correctly holds. (A shell whose cwd happened to be a builder
|
|
2412
|
+
// worktree would resolve that worktree's harness via the launch-script fallback.)
|
|
2413
|
+
command: shellCmd,
|
|
2414
|
+
args: shellArgs,
|
|
1714
2415
|
});
|
|
1715
2416
|
const ptySession = manager.getSession(session.id);
|
|
1716
2417
|
if (ptySession) {
|
|
@@ -1718,7 +2419,7 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
|
|
|
1718
2419
|
}
|
|
1719
2420
|
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1720
2421
|
entry.shells.set(shellId, session.id);
|
|
1721
|
-
saveTerminalSession(session.id, workspacePath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime, session.label, workspacePath);
|
|
2422
|
+
saveTerminalSession(session.id, workspacePath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime, session.label, workspacePath, shellCmd);
|
|
1722
2423
|
shellCreated = true;
|
|
1723
2424
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1724
2425
|
res.end(JSON.stringify({
|
|
@@ -1747,7 +2448,7 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
|
|
|
1747
2448
|
});
|
|
1748
2449
|
const entry = getWorkspaceTerminalsEntry(workspacePath);
|
|
1749
2450
|
entry.shells.set(shellId, session.id);
|
|
1750
|
-
saveTerminalSession(session.id, workspacePath, 'shell', shellId, session.pid, null, null, null, session.label, workspacePath);
|
|
2451
|
+
saveTerminalSession(session.id, workspacePath, 'shell', shellId, session.pid, null, null, null, session.label, workspacePath, shellCmd);
|
|
1751
2452
|
ctx.log('WARN', `Shell ${shellId} for ${workspacePath} is non-persistent (shellper unavailable)`);
|
|
1752
2453
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1753
2454
|
res.end(JSON.stringify({
|
|
@@ -2000,6 +2701,13 @@ async function handleWorkspaceTabDelete(res, ctx, workspacePath, tabId) {
|
|
|
2000
2701
|
return;
|
|
2001
2702
|
}
|
|
2002
2703
|
if (terminalId) {
|
|
2704
|
+
// Issue #1261: this path discards the kill result and answers 204 either
|
|
2705
|
+
// way, so a not-yet-wired Tower would report a successful close for a
|
|
2706
|
+
// terminal it never touched. Refuse instead.
|
|
2707
|
+
if (!instancesReady()) {
|
|
2708
|
+
respondStartingUp(res);
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2003
2711
|
// Disable shellper auto-restart if applicable, then kill the PtySession
|
|
2004
2712
|
await killTerminalWithShellper(manager, terminalId);
|
|
2005
2713
|
// TICK-001: Delete from SQLite
|