@dmsdc-ai/aigentry-telepty 0.3.5 → 0.4.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/CHANGELOG.md +72 -0
- package/cli.js +36 -15
- package/daemon.js +355 -5
- package/package.json +25 -1
- package/session-state.js +23 -0
- package/src/prompt-symbol-registry.js +43 -1
- package/.claude/commands/telepty-allow.md +0 -58
- package/.claude/commands/telepty-attach.md +0 -22
- package/.claude/commands/telepty-inject.md +0 -72
- package/.claude/commands/telepty-list.md +0 -22
- package/.claude/commands/telepty-manual-test.md +0 -73
- package/.claude/commands/telepty-start.md +0 -25
- package/.claude/commands/telepty-test.md +0 -25
- package/.claude/commands/telepty.md +0 -82
- package/AGENTS.md +0 -97
- package/BOUNDARY.md +0 -31
- package/BUS_EVENT_SCHEMA.md +0 -206
- package/CLAUDE.md +0 -100
- package/GEMINI.md +0 -10
- package/URGENT_ISSUES.resolved.md +0 -1
- package/docs/reports/2026-05-05-issue-8-claude-review.md +0 -194
- package/docs/specs/2026-05-05-issue-8-telepty-init.md +0 -477
- package/docs/superpowers/specs/2026-04-26-inject-submit-enter-reliability.md +0 -447
- package/docs/superpowers/specs/2026-04-26-prompt-symbol-render-gate.md +0 -571
- package/docs/superpowers/specs/2026-04-26-submit-gate-fixes-v2.md +0 -608
- package/docs/superpowers/specs/2026-05-02-submit-force-and-retry.md +0 -139
- package/protocol/mailbox.md +0 -244
- package/scripts/regen-snippet-fixtures.js +0 -42
- package/specs/codex-inject-spec.md +0 -201
- package/specs/enforce-report-spec.md +0 -237
- package/templates/AGENTS.md +0 -71
- package/tests/snippet-protocol/v1/golden-agents.json +0 -1
- package/tests/snippet-protocol/v1/golden-agents.md +0 -17
- package/tests/snippet-protocol/v1/golden-all.json +0 -3
- package/tests/snippet-protocol/v1/golden-all.md +0 -53
- package/tests/snippet-protocol/v1/golden-claude.json +0 -1
- package/tests/snippet-protocol/v1/golden-claude.md +0 -17
- package/tests/snippet-protocol/v1/golden-gemini.json +0 -1
- package/tests/snippet-protocol/v1/golden-gemini.md +0 -17
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
# 2026-05-02 — `inject --submit-force` + idempotent client retry
|
|
2
|
-
|
|
3
|
-
Closes task #347 (telepty 0.3.2 `--submit` prompt-symbol gate reliability —
|
|
4
|
-
context-ref inject arrived at orchestrator but Enter was skipped when the
|
|
5
|
-
input area had a transient render mismatch: autocomplete dropdown open,
|
|
6
|
-
cursor moved, mid-render race).
|
|
7
|
-
|
|
8
|
-
## Problem
|
|
9
|
-
|
|
10
|
-
`telepty inject --submit` runs three layers of gating before pressing
|
|
11
|
-
Enter:
|
|
12
|
-
|
|
13
|
-
| Layer | File | Trigger | Skip behavior |
|
|
14
|
-
|---|---|---|---|
|
|
15
|
-
| 3. Prompt-symbol (0.3.2) | `src/submit-gate.js` `awaitPromptSymbol` | `cmux read-screen` does not show the per-CLI prompt symbol stably for ≥200 ms within 8 s | Falls through to Layer 1 (`no_prompt_symbol_seen`) |
|
|
16
|
-
| 1. State-gated (0.3.1) | `src/submit-gate.js` `awaitReplReady` | `sessionStateManager` is not in `idle`/`waiting` with conf ≥ 0.5 within 10 s | Best-effort dispatch on `timeout`; hard-fail short-circuits to 504 on `session_dead`/`error`/`restarting`/`no_state` |
|
|
17
|
-
| Verify | `src/submit-gate.js` `verifyBodyConsumed` | Injected body still visible in `outputRing` after dispatch | One bounded retry; if still visible, 504 with `reason: 'gated_dispatch_unconsumed'` |
|
|
18
|
-
|
|
19
|
-
In production this still produces a residual failure rate when the
|
|
20
|
-
orchestrator session has a transient render mismatch (autocomplete drop-down,
|
|
21
|
-
cursor outside input area, mid-paste). The body is injected, the gate times
|
|
22
|
-
out, the dispatch fires Enter into a "wrong" focus, and `verifyBodyConsumed`
|
|
23
|
-
correctly sees the body still in the input box → 504. Sub-sessions then
|
|
24
|
-
print `⚠️ Submit gated-timeout` and the human user has to press Enter
|
|
25
|
-
manually for the orchestrator to consume the inject.
|
|
26
|
-
|
|
27
|
-
## Constraints
|
|
28
|
-
|
|
29
|
-
- **Article 1 (경량)**: minimum-touch fix. No new modules, no new daemon
|
|
30
|
-
endpoint, no new helper module.
|
|
31
|
-
- **Article 17 (무의존)**: no new runtime dependency.
|
|
32
|
-
- **Article 9 (독립)**: telepty must keep working standalone (no cmux/kitty
|
|
33
|
-
required for the new flags).
|
|
34
|
-
- **Backward compat**: existing `--submit` semantics unchanged. Default
|
|
35
|
-
`--submit-retry` value MUST be 0-effect on the happy path (which is the
|
|
36
|
-
vast majority of calls, currently shipping reliably).
|
|
37
|
-
- **Idempotency**: a retry must never double-press Enter.
|
|
38
|
-
|
|
39
|
-
## Approach
|
|
40
|
-
|
|
41
|
-
Two opt-in CLI knobs on `telepty inject`, both implemented client-side
|
|
42
|
-
in `cli.js`. Daemon `/submit` endpoint is untouched — `force: true` is
|
|
43
|
-
already supported (introduced in 0.3.1 for `telepty send-key`); we just
|
|
44
|
-
plumb it through from the inject path.
|
|
45
|
-
|
|
46
|
-
### `--submit-force`
|
|
47
|
-
|
|
48
|
-
Adds `force: true` to the `/submit` POST body. Daemon-side this skips
|
|
49
|
-
both Layer 3 (prompt-symbol) and Layer 1 (state-gate) and dispatches Enter
|
|
50
|
-
once via the existing `terminalLevelSubmit` chain (kitty → cmux → PTY).
|
|
51
|
-
|
|
52
|
-
Use case: caller is confident the target REPL is ready (e.g., orchestrator
|
|
53
|
-
visibly idle, or Phase-6 cascade where sub-session has just verified the
|
|
54
|
-
orchestrator's last bus event). Mirrors the existing `telepty send-key`
|
|
55
|
-
escape hatch but at the inject level so a single command does both.
|
|
56
|
-
|
|
57
|
-
### `--submit-retry N` (default 1, clamp [0, 3])
|
|
58
|
-
|
|
59
|
-
After a 504 from `/submit` with a **retry-safe** reason, wait 300 ms and
|
|
60
|
-
re-issue the same `/submit` request up to N times. Retry-safe reasons:
|
|
61
|
-
|
|
62
|
-
| Reason | Source | Why retry is idempotent |
|
|
63
|
-
|---|---|---|
|
|
64
|
-
| `gated_dispatch_unconsumed` | `daemon.js:1680` | The verify path saw the body STILL in the input box after best-effort dispatch. Re-firing Enter when the body is visibly un-consumed cannot double-submit. |
|
|
65
|
-
| `gate_timeout` | `awaitReplReady` returning `timeout` (no longer reaches 504 directly in 0.3.1, but kept for forward-compat) | Same: body has not been consumed if we're still on the gated path. |
|
|
66
|
-
| `no_prompt_symbol_seen` | `awaitPromptSymbol` Layer 3 timeout (also not currently a 504 source, but kept for forward-compat) | Layer 3 alone never emits 504 today. Listed for completeness. |
|
|
67
|
-
|
|
68
|
-
Retry is **explicitly NOT** safe for hard-fail reasons — `session_dead`,
|
|
69
|
-
`session_error`, `session_restarting`, `no_state`, `no_state_manager`. Those
|
|
70
|
-
short-circuit the loop immediately because re-firing won't recover. Same
|
|
71
|
-
for any non-504 status (4xx) — no point retrying a malformed request.
|
|
72
|
-
|
|
73
|
-
The retry preserves the original flag set (`force` stays `force`, etc.).
|
|
74
|
-
The `attemptsMade` counter is rendered into the success line as
|
|
75
|
-
`[retry K/N]` so operators can see when the retry path actually fired.
|
|
76
|
-
|
|
77
|
-
### Why client-side (not daemon-side)?
|
|
78
|
-
|
|
79
|
-
- Server-side already retries once internally inside `verifyBodyConsumed`
|
|
80
|
-
(`daemon.js:1663-1672`). Adding a second loop server-side conflates two
|
|
81
|
-
feedback signals (the inner verify retry vs. the outer client retry) in
|
|
82
|
-
one response shape.
|
|
83
|
-
- Per-call client control is more flexible — sub-sessions that have
|
|
84
|
-
cheap evidence of orchestrator readiness can pass `--submit-retry 0`
|
|
85
|
-
to avoid the extra round-trip; ones that don't can pass `--submit-retry 2`.
|
|
86
|
-
- Keeps the daemon stable. 0.3.0 cluster (memory:
|
|
87
|
-
`feedback_telepty_send_key_regression.md`) was a daemon-side change that
|
|
88
|
-
rippled into manual-override breakage. Client-side change has a strictly
|
|
89
|
-
smaller blast radius.
|
|
90
|
-
|
|
91
|
-
## File map
|
|
92
|
-
|
|
93
|
-
| File | Change | LoC delta |
|
|
94
|
-
|---|---|---|
|
|
95
|
-
| `cli.js` (inject command) | Parse `--submit-force` + `--submit-retry`. Wrap existing `useSubmit` block in idempotent retry loop on 504-with-safe-reason. | +~55, -~25 |
|
|
96
|
-
| `test/cli.test.js` | Three new tests: --submit-force passes force=true; --submit-retry retries on safe-reason 504; --submit-retry does NOT retry on hard-fail 504. | +~120 |
|
|
97
|
-
| `CHANGELOG.md` | 0.3.3 entry. | +~30 |
|
|
98
|
-
| `package.json` | 0.3.2 → 0.3.3. | +1, -1 |
|
|
99
|
-
| `test/enforce-report.test.js:280` | Update stale version assertion 0.2.0 → 0.3.3. | +1, -1 |
|
|
100
|
-
| `README.md` | Mention new flags in inject summary. | +~6 |
|
|
101
|
-
|
|
102
|
-
No new files outside `test/` and `docs/`. No daemon changes. No new
|
|
103
|
-
dependencies. Total surface ≪ 200 LoC including tests.
|
|
104
|
-
|
|
105
|
-
## Tests
|
|
106
|
-
|
|
107
|
-
### Unit / integration (`test/cli.test.js`)
|
|
108
|
-
|
|
109
|
-
1. **`--submit-force` passes `force: true` to /submit**
|
|
110
|
-
Spawn a session, intercept `/submit` (use existing harness method or
|
|
111
|
-
inspect bus event), invoke `telepty inject --submit --submit-force <id>
|
|
112
|
-
"x"`, assert daemon received `{ force: true }` in the request body.
|
|
113
|
-
|
|
114
|
-
2. **`--submit-retry N` retries on safe-reason 504**
|
|
115
|
-
Mock the daemon to return 504 `{reason: 'gated_dispatch_unconsumed'}`
|
|
116
|
-
on the first call and 200 on the second. Assert the CLI made exactly
|
|
117
|
-
2 POST /submit calls and exited 0. Assert `[retry 1/N]` is present
|
|
118
|
-
in stdout.
|
|
119
|
-
|
|
120
|
-
3. **`--submit-retry N` does NOT retry on hard-fail 504**
|
|
121
|
-
Mock the daemon to return 504 `{reason: 'session_dead'}`. Assert the
|
|
122
|
-
CLI made exactly 1 POST /submit call (no retry).
|
|
123
|
-
|
|
124
|
-
### Regression — full suite
|
|
125
|
-
|
|
126
|
-
`npm test` — 229 tests, all should pass after updating the stale
|
|
127
|
-
`enforce-report.test.js:280` version assertion.
|
|
128
|
-
|
|
129
|
-
## Future-proofing notes
|
|
130
|
-
|
|
131
|
-
- If the daemon adds new 504 reasons, they are by default **NOT** retry-
|
|
132
|
-
safe (the safe set is an explicit allowlist). Adding a new safe reason
|
|
133
|
-
is a one-line `RETRY_SAFE_REASONS.add(...)` change in `cli.js`.
|
|
134
|
-
- The flag pair composes: `--submit-force --submit-retry 0` (force-once),
|
|
135
|
-
`--submit-force --submit-retry 2` (force, with idempotent retry on the
|
|
136
|
-
rare 503 — though force never returns 504 today).
|
|
137
|
-
- The 300 ms retry delay is a constant, not a flag, to keep the surface
|
|
138
|
-
small. Empirically chosen at the upper end of the architect's
|
|
139
|
-
100–300 ms window for the autocomplete-dropdown-close case.
|
package/protocol/mailbox.md
DELETED
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
# Aigentry Mailbox Protocol — SSOT
|
|
2
|
-
|
|
3
|
-
**Version**: 1.0-draft
|
|
4
|
-
**Status**: Design
|
|
5
|
-
**Scope**: Transport-agnostic protocol spec (message format, ACK semantics, state machine)
|
|
6
|
-
**Date**: 2026-04-07
|
|
7
|
-
|
|
8
|
-
This document is the single source of truth for the aigentry mailbox protocol.
|
|
9
|
-
Implementation details (storage backend, locking, transport) are in `aigentry-mailbox` crate spec.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## 1. Purpose
|
|
14
|
-
|
|
15
|
-
The mailbox protocol defines guaranteed, ordered, ACK-able message delivery between aigentry sessions (aterm workspaces, telepty sessions, orchestrators). It is transport-agnostic: the same protocol runs over files, Unix sockets, HTTP, or WebSocket.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## 2. Message Format
|
|
20
|
-
|
|
21
|
-
All messages are JSON objects. Field names use snake_case (Rust) / camelCase (TypeScript/Node.js) — both representations are valid; implementations must accept both via alias.
|
|
22
|
-
|
|
23
|
-
```json
|
|
24
|
-
{
|
|
25
|
-
"msg_id": "orchestrator:1743999600123456789",
|
|
26
|
-
"from": "aigentry-orchestrator-claude",
|
|
27
|
-
"to": "aigentry-analyst-claude",
|
|
28
|
-
"payload": "analyze the auth module",
|
|
29
|
-
"created_at": 1743999600,
|
|
30
|
-
"attempt": 0
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
| Field | Type | Required | Description |
|
|
35
|
-
|-------|------|----------|-------------|
|
|
36
|
-
| `msg_id` | string | yes | Globally unique message ID. Format: `{from}:{nanoseconds}` or UUID. Used for idempotency and ACK. |
|
|
37
|
-
| `from` | string | yes | Sender session ID or alias. |
|
|
38
|
-
| `to` | string | yes | Target session ID or alias. |
|
|
39
|
-
| `payload` | string | yes | Message body. Arbitrary UTF-8. Typically a CLI command string. |
|
|
40
|
-
| `created_at` | uint64 | yes | Unix timestamp (seconds) when message was first created by sender. Immutable across retries. |
|
|
41
|
-
| `attempt` | uint32 | yes | Delivery attempt count. 0 = first attempt. Incremented by mailbox on NACK+retry. |
|
|
42
|
-
|
|
43
|
-
### msg_id Requirements
|
|
44
|
-
- MUST be unique per logical message (not per delivery attempt)
|
|
45
|
-
- MUST remain the same across retries
|
|
46
|
-
- MUST be stable: same logical send operation always produces the same msg_id
|
|
47
|
-
- Recommended format: `"{from}:{created_at_nanos}"` — deterministic, no UUID library needed
|
|
48
|
-
|
|
49
|
-
---
|
|
50
|
-
|
|
51
|
-
## 3. Message State Machine
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
┌─────────────────────────────────────────────┐
|
|
55
|
-
│ enqueue() │
|
|
56
|
-
▼ │
|
|
57
|
-
PENDING ──────── dequeue() ──────► IN_FLIGHT │ (idempotent: duplicate
|
|
58
|
-
▲ │ │ enqueue → PENDING skip)
|
|
59
|
-
│ ack() ───┤
|
|
60
|
-
│ ▼
|
|
61
|
-
│ ACKED (terminal)
|
|
62
|
-
│
|
|
63
|
-
│ nack(reason) ────► NACKED
|
|
64
|
-
│ │
|
|
65
|
-
│ attempt < max ────────┘──── re-enqueue ──► PENDING
|
|
66
|
-
│ attempt ≥ max ──────────────────────────► DEAD_LETTER (terminal)
|
|
67
|
-
│
|
|
68
|
-
└──── TTL exceeded ───────────────────────────────────► EXPIRED (terminal)
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
### States
|
|
72
|
-
|
|
73
|
-
| State | Terminal | Description |
|
|
74
|
-
|-------|----------|-------------|
|
|
75
|
-
| `pending` | no | Awaiting dequeue by receiver |
|
|
76
|
-
| `in_flight` | no | Dequeued, receiver processing, awaiting ACK |
|
|
77
|
-
| `acked` | yes | Delivery confirmed by receiver |
|
|
78
|
-
| `nacked` | no | Delivery failed, scheduled for retry |
|
|
79
|
-
| `dead_letter` | yes | Exhausted retry count |
|
|
80
|
-
| `expired` | yes | TTL exceeded before delivery |
|
|
81
|
-
|
|
82
|
-
### State Transition Rules
|
|
83
|
-
1. Only `pending` → `in_flight` (via `dequeue`)
|
|
84
|
-
2. Only `in_flight` → `acked` or `nacked` (via `ack`/`nack`)
|
|
85
|
-
3. `nacked` with `attempt < max_retries` → new `pending` entry (same msg_id, attempt+1)
|
|
86
|
-
4. `nacked` with `attempt >= max_retries` → `dead_letter`
|
|
87
|
-
5. `pending` or `in_flight` past TTL → `expired` (by DeliveryEngine sweep)
|
|
88
|
-
6. Terminal states are immutable
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## 4. ACK Semantics
|
|
93
|
-
|
|
94
|
-
### enqueue → EnqueueAck
|
|
95
|
-
```
|
|
96
|
-
enqueue(msg) → { msg_id, queued: bool, pending: usize }
|
|
97
|
-
```
|
|
98
|
-
- `queued: true` — message newly added
|
|
99
|
-
- `queued: false` — msg_id already seen (idempotent, no-op). Safe to call multiple times.
|
|
100
|
-
|
|
101
|
-
### dequeue
|
|
102
|
-
```
|
|
103
|
-
dequeue(session_id) → Option<Message>
|
|
104
|
-
```
|
|
105
|
-
- Returns oldest `pending` message
|
|
106
|
-
- Transitions it to `in_flight`
|
|
107
|
-
- Returns `None` if no pending messages
|
|
108
|
-
|
|
109
|
-
### ack
|
|
110
|
-
```
|
|
111
|
-
ack(session_id, msg_id) → Result<()>
|
|
112
|
-
```
|
|
113
|
-
- Transitions `in_flight` → `acked`
|
|
114
|
-
- MUST be called after successful delivery to PTY or host
|
|
115
|
-
|
|
116
|
-
### nack
|
|
117
|
-
```
|
|
118
|
-
nack(session_id, msg_id, reason: string) → Result<()>
|
|
119
|
-
```
|
|
120
|
-
- Transitions `in_flight` → `nacked`
|
|
121
|
-
- Mailbox schedules retry with backoff: `retry_delay = base_backoff_secs × 2^attempt`
|
|
122
|
-
- After `max_retries` NACKs: transitions to `dead_letter`
|
|
123
|
-
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
## 5. Retry Policy
|
|
127
|
-
|
|
128
|
-
| Parameter | Default | Description |
|
|
129
|
-
|-----------|---------|-------------|
|
|
130
|
-
| `max_retries` | 3 | NACK count before dead-lettering |
|
|
131
|
-
| `base_backoff_secs` | 5 | First retry delay (seconds) |
|
|
132
|
-
| Backoff strategy | Exponential | `delay = base × 2^attempt`: 5s, 10s, 20s |
|
|
133
|
-
| `inflight_timeout_secs` | 30 | Auto-nack if ACK not received within this window |
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
|
|
137
|
-
## 6. Dead Letter Queue
|
|
138
|
-
|
|
139
|
-
Messages in `dead_letter` state are preserved indefinitely (no TTL). Dead letter entries contain:
|
|
140
|
-
|
|
141
|
-
```json
|
|
142
|
-
{
|
|
143
|
-
"msg_id": "orchestrator:...",
|
|
144
|
-
"from": "...",
|
|
145
|
-
"to": "...",
|
|
146
|
-
"payload": "...",
|
|
147
|
-
"reason": "max_retries exhausted",
|
|
148
|
-
"failed_at": 1744086000,
|
|
149
|
-
"attempts": 3
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
Dead letter queue is inspectable (`peek_dead_letter(session_id)`) and purgeable (`purge_dead_letter(session_id)`).
|
|
154
|
-
|
|
155
|
-
---
|
|
156
|
-
|
|
157
|
-
## 7. Ordering Guarantee
|
|
158
|
-
|
|
159
|
-
- Messages are delivered FIFO per `(from, to)` pair
|
|
160
|
-
- Messages from different senders to the same receiver are interleaved by `created_at`
|
|
161
|
-
- No total ordering across all receivers
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
## 8. Idempotency
|
|
166
|
-
|
|
167
|
-
- `enqueue` with duplicate `msg_id` is a no-op (returns `queued: false`)
|
|
168
|
-
- `ack` with already-acked `msg_id` is a no-op (returns `Ok(())`)
|
|
169
|
-
- `nack` with already-dead-lettered `msg_id` is a no-op
|
|
170
|
-
- Implementations MUST enforce idempotency at the storage layer, not the caller
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
## 9. Protocol Versioning
|
|
175
|
-
|
|
176
|
-
Messages MAY include `"protocol_version": "1.0"` field. Receivers MUST ignore unknown fields (forward compatibility). Senders SHOULD include version for diagnostics.
|
|
177
|
-
|
|
178
|
-
---
|
|
179
|
-
|
|
180
|
-
## 10. TypeScript Interface (Node.js / telepty)
|
|
181
|
-
|
|
182
|
-
```typescript
|
|
183
|
-
// message.ts
|
|
184
|
-
|
|
185
|
-
export interface Message {
|
|
186
|
-
msg_id: string; // or msgId (both accepted)
|
|
187
|
-
from: string;
|
|
188
|
-
to: string;
|
|
189
|
-
payload: string;
|
|
190
|
-
created_at: number; // or createdAt (Unix seconds)
|
|
191
|
-
attempt: number;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
export interface MessageSummary {
|
|
195
|
-
msg_id: string;
|
|
196
|
-
from: string;
|
|
197
|
-
created_at: number;
|
|
198
|
-
attempt: number;
|
|
199
|
-
state: MessageState;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export type MessageState =
|
|
203
|
-
| 'pending'
|
|
204
|
-
| 'in_flight'
|
|
205
|
-
| 'acked'
|
|
206
|
-
| 'nacked'
|
|
207
|
-
| 'dead_letter'
|
|
208
|
-
| 'expired';
|
|
209
|
-
|
|
210
|
-
export interface EnqueueAck {
|
|
211
|
-
msg_id: string;
|
|
212
|
-
queued: boolean;
|
|
213
|
-
pending: number;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
export interface DeadLetterEntry extends Message {
|
|
217
|
-
reason: string;
|
|
218
|
-
failed_at: number; // Unix seconds
|
|
219
|
-
attempts: number;
|
|
220
|
-
}
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
// mailbox.ts — transport-agnostic interface
|
|
225
|
-
|
|
226
|
-
export interface MailboxProtocol {
|
|
227
|
-
enqueue(msg: Message): Promise<EnqueueAck>;
|
|
228
|
-
dequeue(sessionId: string): Promise<Message | null>;
|
|
229
|
-
ack(sessionId: string, msgId: string): Promise<void>;
|
|
230
|
-
nack(sessionId: string, msgId: string, reason: string): Promise<void>;
|
|
231
|
-
peek(sessionId: string): Promise<MessageSummary[]>;
|
|
232
|
-
purge(sessionId: string): Promise<void>;
|
|
233
|
-
peekDeadLetter(sessionId: string): Promise<DeadLetterEntry[]>;
|
|
234
|
-
purgeDeadLetter(sessionId: string): Promise<void>;
|
|
235
|
-
}
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## 11. Changelog
|
|
241
|
-
|
|
242
|
-
| Version | Date | Change |
|
|
243
|
-
|---------|------|--------|
|
|
244
|
-
| 1.0-draft | 2026-04-07 | Initial protocol definition |
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const path = require('node:path');
|
|
5
|
-
const { buildOutput } = require('../src/init/print-snippet');
|
|
6
|
-
|
|
7
|
-
const projectRoot = path.resolve(__dirname, '..');
|
|
8
|
-
const fixtureDir = path.join(projectRoot, 'tests', 'snippet-protocol', 'v1');
|
|
9
|
-
const targets = ['claude', 'agents', 'gemini', 'all'];
|
|
10
|
-
const formats = [
|
|
11
|
-
{ name: 'markdown', ext: 'md' },
|
|
12
|
-
{ name: 'json', ext: 'json' }
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
function createCaptureStream() {
|
|
16
|
-
return {
|
|
17
|
-
value: '',
|
|
18
|
-
write(chunk) {
|
|
19
|
-
this.value += chunk;
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
fs.mkdirSync(fixtureDir, { recursive: true });
|
|
25
|
-
|
|
26
|
-
for (const target of targets) {
|
|
27
|
-
for (const format of formats) {
|
|
28
|
-
const stdout = createCaptureStream();
|
|
29
|
-
const stderr = createCaptureStream();
|
|
30
|
-
const code = buildOutput(['--print-snippet', '--target', target, '--format', format.name], {
|
|
31
|
-
stdout,
|
|
32
|
-
stderr
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
if (code !== 0) {
|
|
36
|
-
throw new Error(`failed to generate ${target} ${format.name}: ${stderr.value}`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const fixturePath = path.join(fixtureDir, `golden-${target}.${format.ext}`);
|
|
40
|
-
fs.writeFileSync(fixturePath, stdout.value, 'utf8');
|
|
41
|
-
}
|
|
42
|
-
}
|
|
@@ -1,201 +0,0 @@
|
|
|
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.
|