@dmsdc-ai/aigentry-telepty 0.1.97 → 0.1.98
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/daemon.js +43 -14
- package/package.json +1 -1
- package/specs/codex-inject-spec.md +201 -0
package/daemon.js
CHANGED
|
@@ -33,13 +33,27 @@ const sessionStateManager = new SessionStateManager({
|
|
|
33
33
|
thinking_timeout_ms: Number(process.env.TELEPTY_STATE_THINKING_TIMEOUT_MS || 300000),
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
// Broadcast state transitions to the bus
|
|
36
|
+
// Broadcast state transitions to the bus + fire auto-report on idle
|
|
37
37
|
sessionStateManager.onTransition((sessionId, from, to, detail) => {
|
|
38
38
|
const session = sessions[sessionId];
|
|
39
39
|
if (!session) return;
|
|
40
40
|
broadcastSessionEvent('session_auto_state', sessionId, session, {
|
|
41
41
|
extra: { auto_state: to, auto_state_from: from, auto_detail: detail }
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
// Auto-report: fire when session transitions to idle after inject
|
|
45
|
+
if (to === 'idle' && pendingReports[sessionId]) {
|
|
46
|
+
const pendingReport = pendingReports[sessionId];
|
|
47
|
+
delete pendingReports[sessionId];
|
|
48
|
+
const elapsed = ((Date.now() - new Date(pendingReport.injectedAt).getTime()) / 1000).toFixed(1);
|
|
49
|
+
const reportMsg = `TASK_COMPLETE: ${sessionId} is now idle after processing inject (${elapsed}s)`;
|
|
50
|
+
const srcId = resolveSessionAlias(pendingReport.source) || pendingReport.source;
|
|
51
|
+
const srcSession = sessions[srcId];
|
|
52
|
+
if (srcSession) {
|
|
53
|
+
deliverInjectionToSession(srcId, srcSession, reportMsg, { noEnter: false, source: 'auto_report' });
|
|
54
|
+
console.log(`[AUTO-REPORT] ${sessionId} → ${srcId}: idle after ${elapsed}s (via state machine)`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
43
57
|
});
|
|
44
58
|
|
|
45
59
|
function persistSessions() {
|
|
@@ -554,6 +568,22 @@ async function writeDataToSession(id, session, data) {
|
|
|
554
568
|
return { success: true };
|
|
555
569
|
}
|
|
556
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Submit Enter to a session using terminal-level methods.
|
|
573
|
+
* Used by POST /submit endpoint for explicit terminal-level submit.
|
|
574
|
+
* Priority: kitty send-text → cmux send-key → PTY \r fallback.
|
|
575
|
+
* Returns the strategy name or null on failure.
|
|
576
|
+
*/
|
|
577
|
+
function terminalLevelSubmit(id, session) {
|
|
578
|
+
// Priority 1: kitty send-text (terminal-level, bypasses PTY raw mode quirks)
|
|
579
|
+
if (session.type === 'wrapped' && sendViaKitty(id, '\r')) return 'kitty';
|
|
580
|
+
// Priority 2: cmux send-key
|
|
581
|
+
if (session.backend === 'cmux' && session.cmuxWorkspaceId && submitViaCmux(id)) return 'cmux';
|
|
582
|
+
// Priority 3: PTY \r
|
|
583
|
+
if (submitViaPty(session)) return 'pty_cr';
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
557
587
|
async function deliverInjectionToSession(id, session, prompt, options = {}) {
|
|
558
588
|
const now = Date.now();
|
|
559
589
|
const injectFailure = getInjectFailure(session, { nowMs: now });
|
|
@@ -1300,8 +1330,11 @@ function sendViaKitty(sessionId, text) {
|
|
|
1300
1330
|
});
|
|
1301
1331
|
}
|
|
1302
1332
|
if (hasCr) {
|
|
1303
|
-
// Delay before sending Return —
|
|
1304
|
-
|
|
1333
|
+
// Delay before sending Return — only when text was sent in the same call
|
|
1334
|
+
// (when CR-only, text was already delivered via a different path)
|
|
1335
|
+
if (textOnly.length > 0) {
|
|
1336
|
+
execSync('sleep 0.5', { timeout: 2000 });
|
|
1337
|
+
}
|
|
1305
1338
|
execSync(`kitty @ --to unix:${socket} send-text --match id:${windowId} $'\\r'`, {
|
|
1306
1339
|
timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
|
|
1307
1340
|
});
|
|
@@ -1392,29 +1425,25 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
|
|
|
1392
1425
|
const retryDelayMs = Math.min(Math.max(Number(req.body?.retry_delay_ms) || 500, 100), 2000);
|
|
1393
1426
|
const preDelayMs = Math.min(Math.max(Number(req.body?.pre_delay_ms) || 0, 0), 1000);
|
|
1394
1427
|
|
|
1395
|
-
|
|
1396
|
-
console.log(`[SUBMIT] Session ${id} (${session.command})
|
|
1428
|
+
// Terminal-level submit: kitty → cmux → PTY fallback
|
|
1429
|
+
console.log(`[SUBMIT] Session ${id} (${session.command})${retries > 0 ? `, retries: ${retries}, pre_delay: ${preDelayMs}ms` : ''}`);
|
|
1397
1430
|
|
|
1398
1431
|
// Pre-delay: wait for paste rendering to complete before sending CR
|
|
1399
1432
|
if (preDelayMs > 0) {
|
|
1400
1433
|
await new Promise(resolve => setTimeout(resolve, preDelayMs));
|
|
1401
1434
|
}
|
|
1402
1435
|
|
|
1403
|
-
|
|
1404
|
-
return submitViaPty(session);
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
let success = executeSubmit();
|
|
1436
|
+
let strategy = terminalLevelSubmit(id, session);
|
|
1408
1437
|
let attempts = 1;
|
|
1409
1438
|
|
|
1410
1439
|
// Retry: resend CR if paste may have absorbed the first one
|
|
1411
|
-
for (let i = 0; i < retries &&
|
|
1440
|
+
for (let i = 0; i < retries && strategy; i++) {
|
|
1412
1441
|
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
|
|
1413
|
-
|
|
1442
|
+
terminalLevelSubmit(id, session);
|
|
1414
1443
|
attempts++;
|
|
1415
1444
|
}
|
|
1416
1445
|
|
|
1417
|
-
if (
|
|
1446
|
+
if (strategy) {
|
|
1418
1447
|
const busMsg = JSON.stringify({
|
|
1419
1448
|
type: 'submit',
|
|
1420
1449
|
sender: 'daemon',
|
|
@@ -1428,7 +1457,7 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
|
|
|
1428
1457
|
});
|
|
1429
1458
|
res.json({ success: true, strategy, attempts });
|
|
1430
1459
|
} else {
|
|
1431
|
-
res.status(503).json({ error:
|
|
1460
|
+
res.status(503).json({ error: 'Submit failed via all strategies (kitty/cmux/pty)', strategy: 'none', attempts });
|
|
1432
1461
|
}
|
|
1433
1462
|
});
|
|
1434
1463
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# SPEC: Codex inject reliability — 4 issues
|
|
2
|
+
|
|
3
|
+
**Bug source:** orchestrator inject e9f41301...
|
|
4
|
+
**Session:** aigentry-telepty
|
|
5
|
+
**Status:** SPEC — awaiting orchestrator approval
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Goal
|
|
10
|
+
|
|
11
|
+
Make `telepty inject` work reliably with codex sessions. Currently 4 failure
|
|
12
|
+
modes: Enter not pressed, active work overwrite, REPORT not sent, multi-task
|
|
13
|
+
partial processing.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Root Cause Analysis
|
|
18
|
+
|
|
19
|
+
### Issue 1: inject succeeds but Enter NOT pressed
|
|
20
|
+
|
|
21
|
+
**Flow:** daemon `deliverInjectionToSession()` → mailbox → `tick()` →
|
|
22
|
+
`writeDataToSession()` sends text via WS → allow-bridge → `child.write(text)`.
|
|
23
|
+
Then 500ms later, `writeDataToSession(id, session, '\r')` → WS → allow-bridge →
|
|
24
|
+
`child.write('\r')`.
|
|
25
|
+
|
|
26
|
+
**Root cause:** codex CLI puts terminal in raw mode with custom input handling.
|
|
27
|
+
PTY-level `\r` via `child.write('\r')` is NOT equivalent to pressing Enter in
|
|
28
|
+
codex's input model. codex reads PTY input character by character in raw mode
|
|
29
|
+
and interprets `\r` differently than a keyboard Enter event.
|
|
30
|
+
|
|
31
|
+
**Evidence:** Project memory: "PTY `\r` 직접 의존 금지" — don't depend on PTY
|
|
32
|
+
`\r` directly. "inject submit은 항상 osascript/kitty terminal-level submit 우선".
|
|
33
|
+
|
|
34
|
+
The `--submit` flag exists in CLI but POST /submit also uses `submitViaPty()` →
|
|
35
|
+
same `\r` via WS. It does NOT use terminal-level submit (kitty/cmux).
|
|
36
|
+
|
|
37
|
+
### Issue 2: New inject overwrites active work
|
|
38
|
+
|
|
39
|
+
**Flow:** `deliverInjectionToSession()` enqueues to mailbox and calls
|
|
40
|
+
`mailboxDelivery.tick()` immediately. Text goes via WS → allow-bridge.
|
|
41
|
+
|
|
42
|
+
Allow-bridge has queuing: if `isIdle()` is false, text goes to
|
|
43
|
+
`enqueueBridgeMessage()`. The safety timer flushes after 5s regardless. But the
|
|
44
|
+
daemon doesn't check session state — it pushes immediately.
|
|
45
|
+
|
|
46
|
+
**Root cause:** Two layers of the problem:
|
|
47
|
+
1. Daemon sends inject regardless of session state (working/thinking/idle)
|
|
48
|
+
2. Allow-bridge 5s safety flush writes queued text to PTY even if session is
|
|
49
|
+
still working, which interrupts codex's current task
|
|
50
|
+
|
|
51
|
+
### Issue 3: REPORT not sent after completion
|
|
52
|
+
|
|
53
|
+
**Flow:** Auto-report mechanism (`pendingReports`) triggers when allow-bridge
|
|
54
|
+
sends `{ type: 'ready' }` WS message. The `ready` signal fires when
|
|
55
|
+
`promptPattern.test(data)` matches in the PTY output.
|
|
56
|
+
|
|
57
|
+
**Root cause:** codex prompt pattern `codex: /[❯>]\s*$/` doesn't reliably match
|
|
58
|
+
codex's actual prompt output. If prompt is never detected → `ready` never sent →
|
|
59
|
+
`pendingReports` never cleared → auto-report never fires.
|
|
60
|
+
|
|
61
|
+
The new session state machine (#185) detects `idle` via OSC 133 + silence
|
|
62
|
+
timeout, but auto-report still uses the legacy `ready` WS signal (daemon.js
|
|
63
|
+
line 2290-2315), not the `session_auto_state` transitions.
|
|
64
|
+
|
|
65
|
+
### Issue 4: Multiple tasks in one inject — partial processing
|
|
66
|
+
|
|
67
|
+
**Root cause:** AI behavior, not telepty bug. When a --ref file contains Task A
|
|
68
|
+
+ Task B, codex processes Task A and returns to prompt. This is standard LLM
|
|
69
|
+
behavior — no telepty fix needed.
|
|
70
|
+
|
|
71
|
+
**Mitigation:** Orchestrator should split multi-task injects into separate
|
|
72
|
+
sequential calls with idle-gating between them (orchestrator-side logic).
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Scope
|
|
77
|
+
|
|
78
|
+
**Phase 1 (this spec):** Fix Issues 1 and 3 (guaranteed Enter + guaranteed
|
|
79
|
+
REPORT). These are telepty-side fixes.
|
|
80
|
+
|
|
81
|
+
**Phase 2 (separate task):** Fix Issue 2 (inject queuing during active work).
|
|
82
|
+
Requires daemon-side session state awareness.
|
|
83
|
+
|
|
84
|
+
**Out of scope:** Issue 4 (orchestrator-level task splitting).
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Files to Modify
|
|
89
|
+
|
|
90
|
+
| File | Change |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `daemon.js` | Fix 1: `deliverInjectionToSession()` — use `sendViaKitty()` for CR instead of PTY `\r`. Fix 3: Wire auto-report to session state `idle` transition instead of legacy `ready` signal. |
|
|
93
|
+
| `daemon.js` | Fix 1: POST `/submit` endpoint — use kitty send-text with cmux fallback instead of `submitViaPty()`. |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Approach
|
|
98
|
+
|
|
99
|
+
### Fix 1: Terminal-level submit for wrapped sessions
|
|
100
|
+
|
|
101
|
+
Replace PTY `\r` with `sendViaKitty()` in `deliverInjectionToSession()`:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// BEFORE (daemon.js ~line 590):
|
|
105
|
+
if (!options.noEnter && session.type !== 'aterm') {
|
|
106
|
+
const submitDelay = session.type === 'wrapped' ? 500 : 300;
|
|
107
|
+
setTimeout(async () => {
|
|
108
|
+
const submitResult = await writeDataToSession(id, session, '\r');
|
|
109
|
+
// ...
|
|
110
|
+
}, submitDelay);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// AFTER:
|
|
114
|
+
if (!options.noEnter && session.type !== 'aterm') {
|
|
115
|
+
const submitDelay = session.type === 'wrapped' ? 500 : 300;
|
|
116
|
+
setTimeout(async () => {
|
|
117
|
+
let submitted = false;
|
|
118
|
+
// Priority 1: kitty send-text (terminal-level, bypasses PTY quirks)
|
|
119
|
+
if (session.type === 'wrapped') {
|
|
120
|
+
submitted = sendViaKitty(id, '\r');
|
|
121
|
+
}
|
|
122
|
+
// Priority 2: cmux send-key (for cmux-managed sessions)
|
|
123
|
+
if (!submitted && session.backend === 'cmux' && session.cmuxWorkspaceId) {
|
|
124
|
+
submitted = submitViaCmux(id);
|
|
125
|
+
}
|
|
126
|
+
// Priority 3: PTY fallback (spawned sessions without kitty)
|
|
127
|
+
if (!submitted) {
|
|
128
|
+
const submitResult = await writeDataToSession(id, session, '\r');
|
|
129
|
+
if (!submitResult.success) {
|
|
130
|
+
emitInjectFailureEvent(id, submitResult.code, submitResult.error, {
|
|
131
|
+
phase: 'submit', source: options.source || 'inject'
|
|
132
|
+
}, session);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}, submitDelay);
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Also update POST `/submit` endpoint to use same priority chain instead of
|
|
140
|
+
always calling `submitViaPty()`.
|
|
141
|
+
|
|
142
|
+
### Fix 3: Auto-report via session state machine
|
|
143
|
+
|
|
144
|
+
Wire auto-report to the `session_auto_state` transition event (already emitted
|
|
145
|
+
by `sessionStateManager.onTransition()`). When a session transitions to `idle`
|
|
146
|
+
and has a pending report, fire the auto-report.
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
// In the existing sessionStateManager.onTransition callback (daemon.js ~line 37):
|
|
150
|
+
sessionStateManager.onTransition((sessionId, from, to, detail) => {
|
|
151
|
+
const session = sessions[sessionId];
|
|
152
|
+
if (!session) return;
|
|
153
|
+
broadcastSessionEvent('session_auto_state', sessionId, session, {
|
|
154
|
+
extra: { auto_state: to, auto_state_from: from, auto_detail: detail }
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Auto-report: fire when session transitions to idle after inject
|
|
158
|
+
if (to === 'idle' && pendingReports[sessionId]) {
|
|
159
|
+
const pendingReport = pendingReports[sessionId];
|
|
160
|
+
delete pendingReports[sessionId];
|
|
161
|
+
const elapsed = ((Date.now() - new Date(pendingReport.injectedAt).getTime()) / 1000).toFixed(1);
|
|
162
|
+
const reportMsg = `TASK_COMPLETE: ${sessionId} is now idle after processing inject (${elapsed}s)`;
|
|
163
|
+
const srcId = resolveSessionAlias(pendingReport.source) || pendingReport.source;
|
|
164
|
+
const srcSession = sessions[srcId];
|
|
165
|
+
if (srcSession) {
|
|
166
|
+
deliverInjectionToSession(srcId, srcSession, reportMsg, { noEnter: false, source: 'auto_report' });
|
|
167
|
+
console.log(`[AUTO-REPORT] ${sessionId} → ${srcId}: idle after ${elapsed}s`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Keep the legacy `ready`-based auto-report as fallback (don't remove it).
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Verification
|
|
178
|
+
|
|
179
|
+
1. **Test:** `telepty inject xtem-rtm "echo hello"` → codex processes it
|
|
180
|
+
(Enter pressed via kitty send-text)
|
|
181
|
+
2. **Test:** `telepty inject --ref --from orchestrator xtem-rtm 'task'` → after
|
|
182
|
+
codex completes → auto-report fires via idle state transition
|
|
183
|
+
3. **Test:** Sessions without kitty (spawned) → PTY `\r` fallback still works
|
|
184
|
+
4. **Test:** Existing 131 tests still pass
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Risks
|
|
189
|
+
|
|
190
|
+
1. **kitty not available.** Mitigated: 3-tier fallback (kitty → cmux → PTY).
|
|
191
|
+
PTY path preserved as last resort.
|
|
192
|
+
2. **`sendViaKitty()` needs kitty socket + window ID match.** Already
|
|
193
|
+
implemented and working for other features. If kitty window not found,
|
|
194
|
+
falls through to PTY.
|
|
195
|
+
3. **Auto-report via state machine may fire too early.** The idle detection
|
|
196
|
+
uses 5s silence timeout. If codex pauses >5s mid-task, it may fire
|
|
197
|
+
prematurely. Mitigated: auto-report has `AUTO_REPORT_IDLE_SECONDS` (10s)
|
|
198
|
+
threshold. Can add a minimum elapsed time guard.
|
|
199
|
+
4. **Dual auto-report paths (state machine + legacy ready).** Could fire
|
|
200
|
+
twice. Mitigated: `delete pendingReports[sessionId]` in both paths —
|
|
201
|
+
whichever fires first consumes the pending report.
|