feat(queue): server-side PTY-inject + idle-drained follow-up queue (W2)
The walk-away primitive: queue follow-up prompts and let a session advance itself
while you're gone — "run the tests" drains, and when Claude next goes idle "open a
PR" drains. Injection reuses writeInput (byte-identical to a keystroke, broadcasts
to all mirrored devices); the byte-shuttle is untouched.
- POST/GET/DELETE /live-sessions/:id/queue — POST/DELETE behind requireAllowedOrigin
+ a per-IP rate limit (QUEUE_RATE_MAX=20/min) + SESSION_ID_RE; text non-empty,
byte-capped (QUEUE_ITEM_MAX_BYTES=4096, 16kb body), count-capped (QUEUE_MAX_ITEMS=10).
- Bounded per-session queue in manager (enqueueFollowup/drainOne/clearQueue);
new optional queueLength on LiveSessionInfo.
- Idle drain: the Stop/SessionEnd /hook branch scheduleDrain()s a debounced timer
(QUEUE_SETTLE_MS=1500). It drains exactly one entry only if the session still
exists, hasn't exited, produced no output during settle, and is idle — three
guards (debounced single timer + pop-one + settle re-check) → one entry per idle.
- New additive ServerMessage {type:'queue',length} broadcasts the count to mirrors
(⧗N badge); FE enqueue via the quick-reply editor + TabApp.enqueueToActive.
Injected bytes go verbatim to the PTY (never built into a shell command). Verified
independently: typecheck + build:web clean, 1737 tests pass (real-PTY integration
covers idle-drain / one-per-idle / settle-guard). Foundation for templated launches,
auto-continue, and issue-intake (docs/ROADMAP.md).
This commit is contained in:
@@ -63,6 +63,10 @@ const DEFAULT_DIFF_MAX_FILES = 300
|
||||
const DEFAULT_STATUSLINE_TTL_MS = 30_000
|
||||
// B3 worktrees
|
||||
const DEFAULT_WORKTREE_TIMEOUT_MS = 10_000
|
||||
// W2 inject queue
|
||||
const DEFAULT_QUEUE_MAX_ITEMS = 10
|
||||
const DEFAULT_QUEUE_ITEM_MAX_BYTES = 4096
|
||||
const DEFAULT_QUEUE_SETTLE_MS = 1500
|
||||
|
||||
/** Valid --permission-mode values (R0-confirmed). Whitelist for validation. */
|
||||
const PERMISSION_MODES: readonly PermissionMode[] = ['default', 'acceptEdits', 'plan', 'auto']
|
||||
@@ -384,6 +388,16 @@ export function loadConfig(env: EnvLike): Config {
|
||||
)
|
||||
const allowAutoMode = parseBool(env['ALLOW_AUTO_MODE'], false) // default off (SEC-M5)
|
||||
|
||||
// W2 server-side PTY-inject follow-up queue
|
||||
const queueEnabled = parseBool(env['QUEUE_ENABLED'], true)
|
||||
const queueMaxItems = parseNonNegativeInt(env['QUEUE_MAX_ITEMS'], 'QUEUE_MAX_ITEMS', DEFAULT_QUEUE_MAX_ITEMS)
|
||||
const queueItemMaxBytes = parseNonNegativeInt(
|
||||
env['QUEUE_ITEM_MAX_BYTES'],
|
||||
'QUEUE_ITEM_MAX_BYTES',
|
||||
DEFAULT_QUEUE_ITEM_MAX_BYTES,
|
||||
)
|
||||
const queueSettleMs = parseNonNegativeInt(env['QUEUE_SETTLE_MS'], 'QUEUE_SETTLE_MS', DEFAULT_QUEUE_SETTLE_MS)
|
||||
|
||||
// ── assemble + freeze ───────────────────────────────────────────────────────
|
||||
// `satisfies Config` on the base object verifies all existing Config fields
|
||||
// are present without triggering excess-property checks on the v0.7 additions.
|
||||
@@ -435,5 +449,10 @@ export function loadConfig(env: EnvLike): Config {
|
||||
worktreeTimeoutMs,
|
||||
defaultPermissionMode,
|
||||
allowAutoMode,
|
||||
// W2 inject queue
|
||||
queueEnabled,
|
||||
queueMaxItems,
|
||||
queueItemMaxBytes,
|
||||
queueSettleMs,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user