@dmsdc-ai/aigentry-telepty 0.1.97 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +326 -0
- package/CLAUDE.md +5 -1
- package/README.md +3 -0
- package/cli.js +109 -16
- package/daemon.js +431 -42
- package/docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md +447 -0
- package/docs/superpowers/specs/2026-04-26-prompt-symbol-render-gate.md +571 -0
- package/docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md +608 -0
- package/docs/superpowers/specs/2026-05-02-submit-force-and-retry.md +139 -0
- package/package.json +4 -4
- package/specs/codex-inject-spec.md +201 -0
- package/specs/enforce-report-spec.md +237 -0
- package/src/prompt-symbol-registry.js +97 -0
- package/src/report-enforcement.js +86 -0
- package/src/submit-gate.js +269 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# SPEC: `inject --submit` Enter reliability — render-gated submit
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-04-26
|
|
4
|
+
**Author:** aigentry-telepty-coder
|
|
5
|
+
**Status:** SPEC — Phase 1, awaiting orchestrator approval
|
|
6
|
+
**Track:** orchestrator UX recurring trap (parallel to #329 Track E27)
|
|
7
|
+
**Related prior specs:**
|
|
8
|
+
- `specs/codex-inject-spec.md` (Phase 1, established kitty/cmux priority chain — landed)
|
|
9
|
+
- `specs/enforce-report-spec.md` (in-flight, REPORT enforcement, orthogonal)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 0. Problem statement
|
|
14
|
+
|
|
15
|
+
`telepty inject --ref --submit --from <orch> <target> "<body>"` reports
|
|
16
|
+
`✅ Submitted via cmux (3 attempts).` but the body remains in the target
|
|
17
|
+
session's input prompt with Enter NOT applied. The target then idles
|
|
18
|
+
without ever processing the inject. Most reproducible against
|
|
19
|
+
freshly-spawned `claude` sessions where the REPL is still rendering its
|
|
20
|
+
welcome / model-select / trust-prompt UI when telepty fires Enter.
|
|
21
|
+
|
|
22
|
+
Workaround in production: orchestrator follows every inject with
|
|
23
|
+
`sleep N && telepty send-key <id> enter` (4–6s). This is the trap
|
|
24
|
+
this spec exists to remove.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Root cause analysis
|
|
29
|
+
|
|
30
|
+
### 1.1 Code path for `inject --submit`
|
|
31
|
+
|
|
32
|
+
CLI side (`cli.js:1540-1660`):
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// cli.js:1626-1635 — inject body with no_enter:true
|
|
36
|
+
const body = buildInjectRequestBody(injectPrompt, {
|
|
37
|
+
fromId, replyTo, replyExpected,
|
|
38
|
+
noEnter: useSubmit // <-- daemon won't send CR after text
|
|
39
|
+
});
|
|
40
|
+
const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
41
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
42
|
+
});
|
|
43
|
+
// cli.js:1641-1659 — terminal-level submit
|
|
44
|
+
if (useSubmit) {
|
|
45
|
+
await new Promise(resolve => setTimeout(resolve, 500)); // (A) 500ms gap CLI-side
|
|
46
|
+
try {
|
|
47
|
+
const submitRes = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/submit`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({ pre_delay_ms: 600, retries: 2, retry_delay_ms: 500 }) // (B)
|
|
51
|
+
});
|
|
52
|
+
const submitData = await submitRes.json();
|
|
53
|
+
if (submitRes.ok) {
|
|
54
|
+
console.log(`✅ Submitted via ${submitData.strategy}${submitData.attempts > 1 ? ` (${submitData.attempts} attempts)` : ''}.`);
|
|
55
|
+
}
|
|
56
|
+
...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Daemon side (`daemon.js:1474-1520`, full body verbatim):
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
// daemon.js:1474 — POST /api/sessions/:id/submit
|
|
63
|
+
app.post('/api/sessions/:id/submit', async (req, res) => {
|
|
64
|
+
const requestedId = req.params.id;
|
|
65
|
+
const resolvedId = resolveSessionAlias(requestedId);
|
|
66
|
+
if (!resolvedId) return res.status(404).json({ error: 'Session not found', requested: requestedId });
|
|
67
|
+
const session = sessions[resolvedId];
|
|
68
|
+
const id = resolvedId;
|
|
69
|
+
|
|
70
|
+
const retries = Math.min(Math.max(Number(req.body?.retries) || 0, 0), 3);
|
|
71
|
+
const retryDelayMs = Math.min(Math.max(Number(req.body?.retry_delay_ms) || 500, 100), 2000);
|
|
72
|
+
const preDelayMs = Math.min(Math.max(Number(req.body?.pre_delay_ms) || 0, 0), 1000);
|
|
73
|
+
|
|
74
|
+
console.log(`[SUBMIT] Session ${id} (${session.command})${retries > 0 ? `, retries: ${retries}, pre_delay: ${preDelayMs}ms` : ''}`);
|
|
75
|
+
|
|
76
|
+
// Pre-delay: wait for paste rendering to complete before sending CR
|
|
77
|
+
if (preDelayMs > 0) {
|
|
78
|
+
await new Promise(resolve => setTimeout(resolve, preDelayMs));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let strategy = terminalLevelSubmit(id, session);
|
|
82
|
+
let attempts = 1;
|
|
83
|
+
|
|
84
|
+
// Retry: resend CR if paste may have absorbed the first one
|
|
85
|
+
for (let i = 0; i < retries && strategy; i++) {
|
|
86
|
+
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
|
|
87
|
+
terminalLevelSubmit(id, session); // ← BUG: return value discarded
|
|
88
|
+
attempts++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (strategy) {
|
|
92
|
+
...
|
|
93
|
+
res.json({ success: true, strategy, attempts });
|
|
94
|
+
} else {
|
|
95
|
+
res.status(503).json({ error: 'Submit failed via all strategies (kitty/cmux/pty)', strategy: 'none', attempts });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`terminalLevelSubmit` (`daemon.js:635-643`):
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
function terminalLevelSubmit(id, session) {
|
|
104
|
+
if (session.type === 'wrapped' && sendViaKitty(id, '\r')) return 'kitty';
|
|
105
|
+
if (session.backend === 'cmux' && session.cmuxWorkspaceId && submitViaCmux(id)) return 'cmux';
|
|
106
|
+
if (submitViaPty(session)) return 'pty_cr';
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`submitViaCmux` (`daemon.js:1458-1472`):
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
function submitViaCmux(sessionId) {
|
|
115
|
+
const { execSync } = require('child_process');
|
|
116
|
+
const session = sessions[sessionId];
|
|
117
|
+
if (!session || !session.cmuxWorkspaceId) return false;
|
|
118
|
+
try {
|
|
119
|
+
execSync(`cmux send-key --workspace ${session.cmuxWorkspaceId} return`, {
|
|
120
|
+
timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
|
|
121
|
+
});
|
|
122
|
+
console.log(`[SUBMIT] cmux send-key return for ${sessionId} (workspace ${session.cmuxWorkspaceId})`);
|
|
123
|
+
return true; // ← "true" only confirms cmux process exit code 0,
|
|
124
|
+
// NOT that the REPL consumed the key
|
|
125
|
+
} catch (err) { ... return false; }
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 1.2 Timing assumptions (the actual root cause)
|
|
130
|
+
|
|
131
|
+
The CLI / daemon path does not check whether the target REPL is ready.
|
|
132
|
+
It blindly fires Enter with bounded delays:
|
|
133
|
+
|
|
134
|
+
| Step | Wall time after `/inject` returned |
|
|
135
|
+
|---|---|
|
|
136
|
+
| CLI sleeps before /submit (cli.js:1643) | +500ms |
|
|
137
|
+
| Daemon `pre_delay_ms` (cli sends 600) | +1100ms |
|
|
138
|
+
| Submit attempt 1 (`terminalLevelSubmit` returns) | +1100ms |
|
|
139
|
+
| `retry_delay_ms` (cli sends 500) → attempt 2 | +1600ms |
|
|
140
|
+
| `retry_delay_ms` → attempt 3 | +2100ms |
|
|
141
|
+
|
|
142
|
+
Total wall budget: **~2.1 s** to complete all 3 Enter dispatches.
|
|
143
|
+
|
|
144
|
+
A freshly-spawned `claude` REPL takes **3–6 s** before its input
|
|
145
|
+
loop is ready to accept Enter. Empirically observed phases on a fresh
|
|
146
|
+
spawn:
|
|
147
|
+
|
|
148
|
+
1. Process launch + Node bootstrap (~0.8–1.5 s, no terminal output yet)
|
|
149
|
+
2. Welcome banner / `Welcome to Claude Code!` render
|
|
150
|
+
3. Trust-this-folder dialog OR model-select dialog (consumes 1 Enter to dismiss)
|
|
151
|
+
4. Initial empty prompt rendered with cursor in input box
|
|
152
|
+
5. Inject text appears in input box (delivered via mailbox/PTY in step 4)
|
|
153
|
+
6. Ready for submit-Enter
|
|
154
|
+
|
|
155
|
+
If telepty's 3 Enter attempts land in phases 2–3, they are absorbed by
|
|
156
|
+
the welcome screen / dialog. By the time phase 6 starts (>2.1s later),
|
|
157
|
+
no further Enter is queued — the body sits in the input box, claude
|
|
158
|
+
goes idle waiting for keystroke.
|
|
159
|
+
|
|
160
|
+
### 1.3 Compounding bugs (in addition to the timing miss)
|
|
161
|
+
|
|
162
|
+
1. **Retry return value discarded** (`daemon.js:1500`). The retry loop
|
|
163
|
+
ignores the result of `terminalLevelSubmit`, so attempts 2 and 3 are
|
|
164
|
+
not actually verified. The reported `attempts` count includes silent
|
|
165
|
+
no-ops if cmux reports failure or kitty socket disappears mid-loop.
|
|
166
|
+
2. **`true` does not mean "consumed"**. `submitViaCmux` returns `true`
|
|
167
|
+
on `cmux send-key return` exit-0 — that confirms the cmux process
|
|
168
|
+
accepted the request, **not** that the target REPL processed the key.
|
|
169
|
+
Same for `sendViaKitty` (kitty `send-text` exit-0) and `submitViaPty`
|
|
170
|
+
(PTY write syscall succeeded).
|
|
171
|
+
3. **No render-completion check**. There is no observation of the
|
|
172
|
+
target's screen state, no use of the `sessionStateManager` `idle`
|
|
173
|
+
transition, no OSC 133 watch. The submit decision is purely
|
|
174
|
+
open-loop.
|
|
175
|
+
|
|
176
|
+
### 1.4 Why this is most reproducible on fresh sessions
|
|
177
|
+
|
|
178
|
+
- Long-running claude sessions are already at phase 6 → first Enter
|
|
179
|
+
attempt at +1.1 s is consumed correctly. Submit "feels" reliable.
|
|
180
|
+
- Fresh sessions are mid phase 2–3 at +1.1 s → all 3 attempts wasted.
|
|
181
|
+
- The orchestrator's "spawn-then-immediately-inject" pattern (used
|
|
182
|
+
for parallel session fan-out) maximally exposes the trap.
|
|
183
|
+
|
|
184
|
+
### 1.5 Existing telepty primitives we can use (no new deps)
|
|
185
|
+
|
|
186
|
+
- `sessionStateManager.getState(id)` → `{ state, since, confidence, ... }`
|
|
187
|
+
with states `starting | idle | working | thinking | waiting | error | restarting | dead`.
|
|
188
|
+
`idle` with confidence ≥ 0.9 is a high-quality "ready for Enter" signal
|
|
189
|
+
(OSC 133;A/B mark or matched prompt pattern).
|
|
190
|
+
(`session-state.js:225-238`, `daemon.js:54,767,1123`)
|
|
191
|
+
- `sessionStateManager.onTransition(cb)` → callback on state changes
|
|
192
|
+
(`session-state.js:570`, `daemon.js:54-117` already wires this).
|
|
193
|
+
- `GET /api/sessions/:id/screen?lines=N` → returns ANSI-stripped recent
|
|
194
|
+
screen text from `outputRing` (`daemon.js:1748-1802`). Usable for
|
|
195
|
+
detecting the body text in the input box if state machine is
|
|
196
|
+
inconclusive.
|
|
197
|
+
- `outputRing` is already maintained per-session, capped at 200KB
|
|
198
|
+
(`daemon.js:729-738`).
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 2. Decision matrix
|
|
203
|
+
|
|
204
|
+
Every option below preserves the `/submit` HTTP contract, the
|
|
205
|
+
`✅ Submitted via <strategy>` CLI output, and the kitty / cmux / PTY
|
|
206
|
+
priority chain. Only the *internal* "when do we fire Enter, and how do
|
|
207
|
+
we know it landed" logic changes.
|
|
208
|
+
|
|
209
|
+
| Approach | What it does | Reliability | Latency (typical) | Latency (worst) | Code changes | Cross-OS | Verdict |
|
|
210
|
+
|---|---|---|---|---|---|---|---|
|
|
211
|
+
| **A. Bigger blind delays** | Raise `pre_delay_ms` default to e.g. 4000ms; raise `retry_delay_ms`. | Better but still open-loop. Will miss slower spawns; will pay 4 s on already-ready sessions. | +4 s | +5+ s | trivial (constants) | OK | ❌ Brittle. Pure regression on warm sessions. |
|
|
212
|
+
| **B. Render-completion gate via `sessionStateManager`** | Before firing Enter, await `getState(id).state === 'idle'` (or `working` returning to `idle`) with bounded timeout. | High when state machine sees prompt or OSC 133. Lower (silence-fallback @ 0.6 conf) for CLIs that don't emit OSC 133 and have unusual prompts (claude UI is one). | +1.1 s typical | +5 s + idle_timeout (bounded) | ~30 LOC daemon + helper | ✅ pure JS, no shell-out | ✅ Best primary. Already-existing primitive. |
|
|
213
|
+
| **C. Cursor-position / input-box check via `read-screen`** | Poll `outputRing` after inject. Look for the body text in the last screen. Once visible, fire Enter. After Enter, wait for body to disappear from screen → success. | High but fragile. Body text may render across line wraps; ANSI box-drawing complicates matching; long bodies truncated. | +1–2 s | +5 s | ~80 LOC matcher + edge cases | ✅ pure JS | ⚠️ Reasonable as fallback when state machine is inconclusive. Heavy alone. |
|
|
214
|
+
| **D. Hybrid (B + C-as-fallback + bounded retries)** | Primary: wait for `idle` state with conf ≥ 0.85 (timeout 5 s). If timeout, fallback: confirm body present in screen (timeout 2 s). Then fire Enter via existing kitty→cmux→PTY chain. Then verify via screen poll: body left input → success. Else one bounded retry, then surface a structured failure. | Highest. Closes the loop on both render and consumption. | +1.2 s typical | +8 s | ~70 LOC daemon + 1 helper module | ✅ pure JS | ✅ **Recommended.** |
|
|
215
|
+
|
|
216
|
+
### 2.1 Recommendation: **Approach D (hybrid)**
|
|
217
|
+
|
|
218
|
+
**Rationale tied to constitutional rules:**
|
|
219
|
+
|
|
220
|
+
- **Rule 1 (경량)**: Reuses `sessionStateManager` and `outputRing` —
|
|
221
|
+
primitives already shipped and exercised. New code is one
|
|
222
|
+
~70-LOC daemon helper + targeted edits in two functions. No new
|
|
223
|
+
module imports, no new long-running goroutines/timers.
|
|
224
|
+
- **Rule 17 (무의존)**: Zero new external dependencies. Uses only
|
|
225
|
+
Node built-ins, existing `child_process` paths, and existing
|
|
226
|
+
internal modules.
|
|
227
|
+
- **Rule 26 (cross-OS)**: All logic is pure JS evaluating the same
|
|
228
|
+
in-memory `outputRing` and state machine that already runs on every
|
|
229
|
+
supported OS. The OS-specific shell-outs (`kitty`, `cmux`,
|
|
230
|
+
`osascript`) are unchanged — only the *gate* before invoking them
|
|
231
|
+
is added.
|
|
232
|
+
- **Reliability vs latency**: Warm sessions (state already `idle`)
|
|
233
|
+
pay ~0 ms extra gate cost — they short-circuit the wait immediately.
|
|
234
|
+
Cold sessions pay an honest 1–5 s wait that matches actual REPL
|
|
235
|
+
readiness, instead of guessing 600 ms × 3.
|
|
236
|
+
|
|
237
|
+
### 2.2 Approaches explicitly rejected
|
|
238
|
+
|
|
239
|
+
- **A** alone — raises latency floor for the common case without
|
|
240
|
+
closing the open-loop hole.
|
|
241
|
+
- **C** alone — fragile against ANSI box-drawing / line-wrap; high
|
|
242
|
+
maintenance burden as CLIs evolve their input UI.
|
|
243
|
+
- "Send Enter twice" — pure brute force, see §5.
|
|
244
|
+
- "Hardcode 10 s sleep" — see §5.
|
|
245
|
+
- "Force tmux fallback" — see §5.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 3. Cross-OS abstraction (Rule 26)
|
|
250
|
+
|
|
251
|
+
The change does **not** introduce per-OS code paths. The new gate is
|
|
252
|
+
pure JS reading in-memory state. The existing `kitty` / `cmux` /
|
|
253
|
+
`osascript` shell-outs already encode OS behavior and are unchanged.
|
|
254
|
+
|
|
255
|
+
`telepty` does not have a `lib/platform.sh` style abstraction layer —
|
|
256
|
+
backend selection currently happens via:
|
|
257
|
+
- `terminal-backend.js:detectTerminal` (env vars + file probes)
|
|
258
|
+
- `daemon.js:terminalLevelSubmit` (priority chain)
|
|
259
|
+
- `daemon.js:getSubmitStrategy` (CLI binary → strategy table)
|
|
260
|
+
|
|
261
|
+
This spec adds **no new OS conditionals**. If a future spec wants to
|
|
262
|
+
unify these three sites into a single platform module, that is
|
|
263
|
+
out-of-scope here and should be raised as a follow-up.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## 4. Test plan
|
|
268
|
+
|
|
269
|
+
All new tests use `node:test` (existing harness). No new dev deps.
|
|
270
|
+
|
|
271
|
+
### 4.1 Unit-style (test/submit-gate.test.js — new file)
|
|
272
|
+
|
|
273
|
+
1. `awaitReplReady(id, sm, opts)` resolves immediately when state is
|
|
274
|
+
already `idle` with conf ≥ 0.85.
|
|
275
|
+
2. Resolves on transition to `idle` from `starting`/`working` within
|
|
276
|
+
timeout.
|
|
277
|
+
3. Times out after `opts.timeout_ms` and resolves with
|
|
278
|
+
`{ ready: false, reason: 'timeout', last_state }`.
|
|
279
|
+
4. Honors `waiting` state — does not gate (waiting *is* a prompt, just
|
|
280
|
+
for y/n) — resolves immediately so a body-Enter still fires.
|
|
281
|
+
5. `verifyBodyConsumed(session, bodyText, opts)` returns `{ consumed: true }`
|
|
282
|
+
when last `outputRing` screen no longer contains `bodyText`.
|
|
283
|
+
6. Returns `{ consumed: false, reason: 'still_visible' }` when body
|
|
284
|
+
still in screen after `opts.timeout_ms`.
|
|
285
|
+
7. Tolerates ANSI in the screen (uses existing `stripAnsi` from
|
|
286
|
+
daemon.js:1766) and tolerates whitespace collapsing.
|
|
287
|
+
|
|
288
|
+
### 4.2 Daemon endpoint integration (test/daemon.test.js additions)
|
|
289
|
+
|
|
290
|
+
8. `POST /submit` with the new gate path, simulated state machine
|
|
291
|
+
already `idle` → exactly 1 Enter dispatch, response
|
|
292
|
+
`{ success: true, strategy: ..., attempts: 1, gated: true, gate_wait_ms: <small> }`.
|
|
293
|
+
9. `POST /submit` with simulated never-idle session → response
|
|
294
|
+
`{ success: false, error: 'gate_timeout', attempts: 0, gated: true, gate_wait_ms: <opts.timeout_ms> }`,
|
|
295
|
+
HTTP 504 (new), CLI prints structured warning.
|
|
296
|
+
10. `POST /submit` retry: simulated body-still-visible after first
|
|
297
|
+
Enter → attempts=2, eventual success.
|
|
298
|
+
11. **Regression: response shape compatibility.** When `gated: true`
|
|
299
|
+
is omitted (legacy path / opt-out env var), response remains
|
|
300
|
+
exactly the current `{ success, strategy, attempts }` shape.
|
|
301
|
+
|
|
302
|
+
### 4.3 End-to-end reliability harness (test/e2e-submit.manual.js — new opt-in)
|
|
303
|
+
|
|
304
|
+
Not run in `npm test` (kept gated behind `TELEPTY_E2E=1`):
|
|
305
|
+
|
|
306
|
+
12. **100× spawn-and-inject loop on fresh `claude`** (the failure mode
|
|
307
|
+
cited in the bug report). For each iteration:
|
|
308
|
+
- `telepty allow --id e2e-claude-NN claude` (background)
|
|
309
|
+
- `telepty inject --submit --from e2e e2e-claude-NN "ECHO: <nonce>"`
|
|
310
|
+
- Within 30 s, expect `<nonce>` to appear in claude's response.
|
|
311
|
+
- Pass criterion: ≥ 99/100 iterations succeed without manual
|
|
312
|
+
`send-key` follow-up. Current baseline: ~0/100.
|
|
313
|
+
13. Same harness against `codex` and `gemini` sessions — ≥ 99/100.
|
|
314
|
+
|
|
315
|
+
### 4.4 Regression coverage
|
|
316
|
+
|
|
317
|
+
14. All 170 existing tests pass unchanged.
|
|
318
|
+
15. `inject --ref` (without `--submit`) — unchanged daemon path
|
|
319
|
+
(`deliverInjectionToSession` mailbox+CR), unchanged tests.
|
|
320
|
+
16. `send-key <id> enter` — same `/submit` endpoint, same gate,
|
|
321
|
+
confirms latency budget acceptable for solo-Enter use case.
|
|
322
|
+
17. Non-cmux strategies (kitty-only, PTY-only) — gate still applies,
|
|
323
|
+
chain unchanged.
|
|
324
|
+
18. Aterm sessions — `terminalLevelSubmit` already short-circuits via
|
|
325
|
+
`session.type === 'aterm'` guards in `deliverInjectionToSession`;
|
|
326
|
+
aterm path is skipped. Verified by existing test
|
|
327
|
+
`test/daemon.test.js:135 'inject endpoint accepts an empty prompt
|
|
328
|
+
and still submits enter'`.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
## 5. Failed approaches (must NOT propose)
|
|
333
|
+
|
|
334
|
+
| Anti-approach | Why rejected |
|
|
335
|
+
|---|---|
|
|
336
|
+
| "Just send Enter twice" | Fresh-session welcome screens consume one Enter and may not preserve the input body across the resulting UI transition. Brute-forcing risks data loss, not just latency. |
|
|
337
|
+
| Hardcode `setTimeout(10000)` in `/submit` before firing | Imposes 10 s latency on every warm-session inject (regression; orchestrator fan-out runs at high frequency). Violates Rule 1 (경량). |
|
|
338
|
+
| Drop cmux strategy, force tmux fallback | Cross-environment regression. The orchestrator and most aigentry sessions run inside cmux; this would break their primary submit path. |
|
|
339
|
+
| Add a new external dependency (e.g. `xdotool`, `node-keypress`, `terminus-screen`) | Violates Rule 17 (무의존). Existing primitives suffice. |
|
|
340
|
+
| Spam Enter in a tight loop until the body disappears | Risks submitting partial state if claude's input echoes mid-Enter. Pollutes input on UI transitions. Indistinguishable from genuine user keyboard mashing. |
|
|
341
|
+
| Move the gate to the CLI (`cli.js`) | Couples CLI to in-process daemon state. Breaks remote injects (`crossMachine.remoteInject` — `cli.js:1606`). Daemon is the only component that owns `sessionStateManager` + `outputRing`, so the gate must live there. |
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 6. Constitution check
|
|
346
|
+
|
|
347
|
+
| Rule | Compliance |
|
|
348
|
+
|---|---|
|
|
349
|
+
| **Rule 1 — 경량** | ✅ Reuses existing `sessionStateManager`, `outputRing`, kitty/cmux/PTY chain. ~70 LOC net add. |
|
|
350
|
+
| **Rule 5 — 최선** | ✅ Closes the open-loop instead of widening the blind delay. No "차선책 workaround" of `sleep N && send-key`. |
|
|
351
|
+
| **Rule 13 — 비판적+건설적+객관적** | ✅ Anti-approaches and risks listed verbatim with reasons; no rhetoric. |
|
|
352
|
+
| **Rule 17 — 무의존** | ✅ Zero new external dependencies. |
|
|
353
|
+
| **Rule 26 — cross-OS** | ✅ No new per-OS branches. New code is OS-neutral pure JS. |
|
|
354
|
+
| **Constitution Rule 1 (AI gap)** | ✅ Removes a recurring orchestrator UX trap that wastes parallel-fanout latency budget. |
|
|
355
|
+
| **Constitution Rule 5 (best-first)** | ✅ Identifies and fixes the actual cause; refuses brittle workarounds. |
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## 7. Invariants preserved
|
|
360
|
+
|
|
361
|
+
- ✅ `telepty list`, `telepty allow` semantics unchanged.
|
|
362
|
+
- ✅ `inject --ref` without `--submit` behavior unchanged (mailbox-text +
|
|
363
|
+
deferred `\r` path remains the default for non-`--submit` injects).
|
|
364
|
+
- ✅ `send-key <id> enter` still works; uses the same gated `/submit`
|
|
365
|
+
endpoint. Fresh-session use case for `send-key` *also* benefits.
|
|
366
|
+
- ✅ Non-cmux strategies (kitty, daemon PTY \r, osascript fallback)
|
|
367
|
+
unchanged in dispatch order.
|
|
368
|
+
- ✅ Output contract: `✅ Submitted via <strategy>` line preserved on
|
|
369
|
+
success. New optional fields (`gated`, `gate_wait_ms`, `attempts`) are
|
|
370
|
+
additive in the daemon JSON response; CLI may surface them only when
|
|
371
|
+
`--verbose` is set (out of scope; default CLI output unchanged).
|
|
372
|
+
- ✅ HTTP status codes: `200` on success, `503` on dispatch-failure
|
|
373
|
+
preserved. New `504 gate_timeout` distinguishes "we never even tried
|
|
374
|
+
to fire Enter because the REPL never readied" from "we tried and
|
|
375
|
+
failed".
|
|
376
|
+
- ✅ Aterm sessions unaffected (gate is bypassed for `session.type === 'aterm'`).
|
|
377
|
+
- ✅ Bus event `submit` still emitted on success with the same shape;
|
|
378
|
+
optional `gated`/`gate_wait_ms` fields added.
|
|
379
|
+
- ✅ Existing 170-test suite passes unchanged.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## 8. Implementation outline (for Phase 2 — orchestrator approval required first)
|
|
384
|
+
|
|
385
|
+
**Files to modify:**
|
|
386
|
+
|
|
387
|
+
| File | Change |
|
|
388
|
+
|---|---|
|
|
389
|
+
| `daemon.js` (new helper, ~40 LOC near line 643) | `awaitReplReady(sessionId, opts)` — promise that resolves when `sessionStateManager.getState(id)` reports `idle` (conf ≥ 0.85), `waiting`, or already had OSC 133 within last `idle_timeout_ms`. Bounded by `opts.timeout_ms` (default 5000). Listens via `sessionStateManager.onTransition`; immediate-resolve fast path when already ready. |
|
|
390
|
+
| `daemon.js` (new helper, ~25 LOC) | `verifyBodyConsumed(session, bodyText, opts)` — reads last N lines of `session.outputRing` via the existing `stripAnsi` (extract or import from line 1766). Returns truthy when normalized body text no longer present in screen tail. Bounded poll (default 1500 ms, 200 ms interval). |
|
|
391
|
+
| `daemon.js:1474-1520` (POST `/submit`) | Replace blind retry loop with: (a) `await awaitReplReady`, (b) single `terminalLevelSubmit`, (c) if injected body provided in POST (new optional field), `await verifyBodyConsumed` → if not consumed, one bounded retry; (d) return augmented JSON. Preserve existing shape on legacy callers (no `injected_body` field). |
|
|
392
|
+
| `cli.js:1641-1659` (inject --submit) | Pass `{ injected_body: injectPrompt }` in the `/submit` POST body so the daemon can verify consumption. Remove the CLI-side 500 ms `setTimeout` (gate handles timing). Keep `pre_delay_ms`/`retries`/`retry_delay_ms` accepted for back-compat but treat them as upper bounds, not floors. |
|
|
393
|
+
| `cli.js:1706-1730` (send-key) | Optional cosmetic: surface `gated`/`gate_wait_ms` on `--verbose`. Default output unchanged. |
|
|
394
|
+
| `daemon.js` env-var opt-out | `TELEPTY_SUBMIT_GATE=off` reverts to the legacy 3-attempt blind path (escape hatch for parity testing). Default: `on`. |
|
|
395
|
+
| `test/submit-gate.test.js` (new) | Unit tests §4.1, daemon integration §4.2. |
|
|
396
|
+
| `test/e2e-submit.manual.js` (new, opt-in) | §4.3 reliability harness. |
|
|
397
|
+
| `CHANGELOG.md` | Entry under upcoming patch (`0.2.1` or `0.3.0` depending on whether the new HTTP 504 path is judged breaking — see §10). |
|
|
398
|
+
|
|
399
|
+
**LOC estimate:** ~70 net add (helpers) + ~30 modified (endpoint refactor) + ~120 new test LOC. Total ≤ 250 LOC.
|
|
400
|
+
|
|
401
|
+
**Risk surface:** confined to one HTTP endpoint and the CLI submit branch. No bus-event schema changes, no persistence-layer changes, no state-machine changes — only *reading* state.
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## 9. Reliability target
|
|
406
|
+
|
|
407
|
+
Current baseline (estimated from 2026-04-26 orchestrator evidence
|
|
408
|
+
across ≥ 5 incidents in one session-day): **~0%** of fresh-session
|
|
409
|
+
`inject --submit` calls land Enter without manual `send-key`
|
|
410
|
+
follow-up.
|
|
411
|
+
|
|
412
|
+
Target: **≥ 99%** in the 100× spawn-and-inject E2E harness (§4.3 #12).
|
|
413
|
+
Failure cases above 1% must surface a structured `gate_timeout` /
|
|
414
|
+
`body_not_consumed` reason for orchestrator visibility.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## 10. Open questions (Phase 2 input requested)
|
|
419
|
+
|
|
420
|
+
1. **HTTP 504 introduction**: Adding a new `504 gate_timeout` status to
|
|
421
|
+
`/submit` is technically an additive change but consumers may treat
|
|
422
|
+
any non-2xx as fatal. Is this a patch (0.2.1) or minor (0.3.0)?
|
|
423
|
+
Recommendation: minor (0.3.0) since it is observable new behavior.
|
|
424
|
+
2. **CLI default verbosity**: Should the gate timing (`gate_wait_ms`)
|
|
425
|
+
be surfaced in the default `✅ Submitted via cmux ...` line, or
|
|
426
|
+
only on `--verbose`? Default-on increases noise during fan-out;
|
|
427
|
+
default-off hides the new diagnostic value. Recommendation: hide
|
|
428
|
+
by default, expose via `--verbose`.
|
|
429
|
+
3. **Per-CLI tuning**: claude takes 3–6 s to ready; codex and gemini
|
|
430
|
+
are usually faster. Should `awaitReplReady` timeout be CLI-aware
|
|
431
|
+
(5 s for claude, 3 s for others)? Recommendation: single 5 s
|
|
432
|
+
default for now; revisit only if E2E harness §4.3 shows latency
|
|
433
|
+
regressions on warm sessions.
|
|
434
|
+
4. **Interaction with REPORT enforcement (`specs/enforce-report-spec.md`)**:
|
|
435
|
+
None — that spec governs what happens *after* a session goes idle
|
|
436
|
+
post-inject. This spec governs whether the inject ever submitted
|
|
437
|
+
in the first place. Orthogonal.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## 11. Phase 2 entry criteria
|
|
442
|
+
|
|
443
|
+
- Orchestrator approves Approach D.
|
|
444
|
+
- Open question §10.1 (semver) decided.
|
|
445
|
+
- Phase 2 implementation budget ≤ 250 LOC, ≤ 4 h wall.
|
|
446
|
+
- Phase 2 success: §4.3 harness ≥ 99/100, full suite green, no
|
|
447
|
+
regression on `inject --ref` (no-submit) or aterm paths.
|