@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 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 — CLI needs time to process text input
1304
- execSync('sleep 0.5', { timeout: 2000 });
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
- const strategy = 'pty_cr';
1396
- console.log(`[SUBMIT] Session ${id} (${session.command}) strategy: ${strategy}${retries > 0 ? `, retries: ${retries}, pre_delay: ${preDelayMs}ms` : ''}`);
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
- function executeSubmit() {
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 && success; i++) {
1440
+ for (let i = 0; i < retries && strategy; i++) {
1412
1441
  await new Promise(resolve => setTimeout(resolve, retryDelayMs));
1413
- executeSubmit();
1442
+ terminalLevelSubmit(id, session);
1414
1443
  attempts++;
1415
1444
  }
1416
1445
 
1417
- if (success) {
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: `Submit failed via ${strategy}`, strategy, attempts });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.97",
3
+ "version": "0.1.98",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
@@ -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.