@dmsdc-ai/aigentry-telepty 0.1.51 → 0.1.52
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/BUS_EVENT_SCHEMA.md +171 -0
- package/daemon.js +39 -0
- package/package.json +1 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Telepty Bus Event Schema Standard
|
|
2
|
+
|
|
3
|
+
Version: 1.0 (2026-03-15)
|
|
4
|
+
Agreed by: telepty, deliberation, devkit, brain, orchestrator
|
|
5
|
+
|
|
6
|
+
## Transport
|
|
7
|
+
|
|
8
|
+
- **HTTP**: `POST /api/bus/publish` with JSON body
|
|
9
|
+
- **WebSocket**: `ws://HOST:3848/api/bus` send JSON message
|
|
10
|
+
- Both paths trigger bus auto-router for routable events
|
|
11
|
+
|
|
12
|
+
## Envelope Structure (All Events)
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"message_id": "string (UUID or prefixed ID)",
|
|
17
|
+
"kind": "string (event type)",
|
|
18
|
+
"source": "string (sender identifier)",
|
|
19
|
+
"target": "string | null (target session ID, optional @host suffix)",
|
|
20
|
+
"ts": "ISO 8601 timestamp"
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Canonical Field Names
|
|
25
|
+
|
|
26
|
+
| Field | Type | Description |
|
|
27
|
+
|-------|------|-------------|
|
|
28
|
+
| `kind` | string | Event type (NOT `type` — `kind` is canonical) |
|
|
29
|
+
| `target` | string | Target telepty session ID. May include `@host` suffix for remote |
|
|
30
|
+
| `source` | string | Sender identifier (format: `project:session_id`) |
|
|
31
|
+
| `message_id` | string | Unique message identifier |
|
|
32
|
+
| `ts` | string | ISO 8601 timestamp |
|
|
33
|
+
|
|
34
|
+
## Routable Events (Auto-Router)
|
|
35
|
+
|
|
36
|
+
### `turn_request`
|
|
37
|
+
|
|
38
|
+
Published by deliberation to request a turn from a session. Telepty daemon auto-routes to target session PTY.
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"message_id": "turn_request-<uuid>",
|
|
43
|
+
"session_id": "<deliberation_session_id>",
|
|
44
|
+
"project": "<project_name>",
|
|
45
|
+
"kind": "turn_request",
|
|
46
|
+
"source": "deliberation:<deliberation_session_id>",
|
|
47
|
+
"target": "<telepty_session_id>[@<host>]",
|
|
48
|
+
"reply_to": "<deliberation_session_id>",
|
|
49
|
+
"trace": ["project:<name>", "speaker:<id>", "turn:<turn_id>"],
|
|
50
|
+
"payload": {
|
|
51
|
+
"turn_id": "string",
|
|
52
|
+
"round": "number",
|
|
53
|
+
"max_rounds": "number",
|
|
54
|
+
"speaker": "string (target telepty session ID)",
|
|
55
|
+
"role": "string | null",
|
|
56
|
+
"prompt": "string (full prompt text — inject as-is to PTY)",
|
|
57
|
+
"prompt_sha1": "string (40-char SHA1)",
|
|
58
|
+
"history_entries": "number",
|
|
59
|
+
"transport_timeout_ms": "number",
|
|
60
|
+
"semantic_timeout_ms": "number"
|
|
61
|
+
},
|
|
62
|
+
"ts": "ISO 8601"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Important Notes:**
|
|
67
|
+
- `session_id` is the DELIBERATION session ID, NOT the target telepty session
|
|
68
|
+
- `target` is the telepty session ID to inject into
|
|
69
|
+
- `payload.prompt` is the full text to write to PTY (no further processing needed)
|
|
70
|
+
- `@host` suffix on target: strip before resolving, use for remote routing
|
|
71
|
+
|
|
72
|
+
**Auto-Router Behavior:**
|
|
73
|
+
1. Daemon receives turn_request via HTTP POST or WS
|
|
74
|
+
2. Extracts `target` field, strips `@host` suffix
|
|
75
|
+
3. Resolves session via `resolveSessionAlias()`
|
|
76
|
+
4. Delivers `payload.prompt` to session PTY (kitty primary, WS fallback)
|
|
77
|
+
5. Emits `inject_written` ack on bus
|
|
78
|
+
|
|
79
|
+
### `inject_written` (ACK)
|
|
80
|
+
|
|
81
|
+
Emitted by telepty after successful auto-route delivery.
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"type": "inject_written",
|
|
86
|
+
"inject_id": "UUID",
|
|
87
|
+
"sender": "daemon",
|
|
88
|
+
"target_agent": "<session_id>",
|
|
89
|
+
"source_type": "bus_auto_route",
|
|
90
|
+
"delivered": true,
|
|
91
|
+
"timestamp": "ISO 8601"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Session Lifecycle Events
|
|
96
|
+
|
|
97
|
+
### `session_register`
|
|
98
|
+
```json
|
|
99
|
+
{ "type": "session_register", "sender": "daemon", "session_id": "string", "command": "string", "cwd": "string", "timestamp": "ISO 8601" }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### `session.replaced`
|
|
103
|
+
```json
|
|
104
|
+
{ "type": "session.replaced", "sender": "daemon", "old_id": "string", "new_id": "string", "alias": "string", "timestamp": "ISO 8601" }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### `session.idle`
|
|
108
|
+
```json
|
|
109
|
+
{ "type": "session.idle", "session_id": "string", "idleSeconds": "number", "lastActivityAt": "ISO 8601", "timestamp": "ISO 8601" }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### `session_health` (periodic, every 10s)
|
|
113
|
+
```json
|
|
114
|
+
{ "type": "session_health", "session_id": "string", "payload": { "alive": true, "pid": "number|null", "type": "string", "clients": "number", "idleSeconds": "number|null" }, "timestamp": "ISO 8601" }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Inject Events
|
|
118
|
+
|
|
119
|
+
### `inject_written`
|
|
120
|
+
```json
|
|
121
|
+
{ "type": "inject_written", "inject_id": "UUID", "sender": "daemon", "target_agent": "string", "content": "string", "from": "string|null", "reply_to": "string|null", "thread_id": "string|null", "reply_expected": "boolean", "timestamp": "ISO 8601" }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `message_routed`
|
|
125
|
+
```json
|
|
126
|
+
{ "type": "message_routed", "message_id": "UUID", "from": "string", "to": "string", "reply_to": "string", "inject_id": "UUID", "deliberation_session_id": "string|null", "thread_id": "string|null", "timestamp": "ISO 8601" }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Handoff Events
|
|
130
|
+
|
|
131
|
+
### `handoff.created` / `handoff.claimed` / `handoff.executing` / `handoff.completed`
|
|
132
|
+
```json
|
|
133
|
+
{ "type": "handoff.<status>", "handoff_id": "UUID", "source_session_id": "string|null", "deliberation_id": "string|null", "auto_execute": "boolean", "task_count": "number", "timestamp": "ISO 8601" }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Thread Events
|
|
137
|
+
|
|
138
|
+
### `thread.opened`
|
|
139
|
+
```json
|
|
140
|
+
{ "type": "thread.opened", "thread_id": "UUID", "topic": "string", "orchestrator_session_id": "string|null", "participant_session_ids": ["string"], "timestamp": "ISO 8601" }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### `thread.closed`
|
|
144
|
+
```json
|
|
145
|
+
{ "type": "thread.closed", "thread_id": "UUID", "topic": "string", "message_count": "number", "timestamp": "ISO 8601" }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Termination Signal Detection
|
|
149
|
+
|
|
150
|
+
Messages containing these strings suppress auto-reply guide footer:
|
|
151
|
+
- `no further reply needed`
|
|
152
|
+
- `thread closed` / `closed on X side`
|
|
153
|
+
- `ack received` / `ack-only`
|
|
154
|
+
- `회신 불필요` / `스레드 종료`
|
|
155
|
+
|
|
156
|
+
## Inject API Reference
|
|
157
|
+
|
|
158
|
+
### `POST /api/sessions/:id/inject`
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"prompt": "string (REQUIRED — canonical body field)",
|
|
163
|
+
"from": "string (sender session ID)",
|
|
164
|
+
"reply_to": "string (defaults to from if omitted)",
|
|
165
|
+
"thread_id": "string (optional)",
|
|
166
|
+
"reply_expected": "boolean (optional)",
|
|
167
|
+
"no_enter": "boolean (skip Enter after inject)"
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Note:** The canonical body field is `prompt`, NOT `text`, `content`, or `message`.
|
package/daemon.js
CHANGED
|
@@ -10,6 +10,26 @@ const { claimDaemonState, clearDaemonState } = require('./daemon-control');
|
|
|
10
10
|
|
|
11
11
|
const config = getConfig();
|
|
12
12
|
const EXPECTED_TOKEN = config.authToken;
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const SESSION_PERSIST_PATH = require('path').join(os.homedir(), '.config', 'aigentry-telepty', 'sessions.json');
|
|
15
|
+
|
|
16
|
+
function persistSessions() {
|
|
17
|
+
try {
|
|
18
|
+
const data = {};
|
|
19
|
+
for (const [id, s] of Object.entries(sessions)) {
|
|
20
|
+
data[id] = { id, type: s.type, command: s.command, cwd: s.cwd, createdAt: s.createdAt, lastActivityAt: s.lastActivityAt || null };
|
|
21
|
+
}
|
|
22
|
+
fs.mkdirSync(require('path').dirname(SESSION_PERSIST_PATH), { recursive: true });
|
|
23
|
+
fs.writeFileSync(SESSION_PERSIST_PATH, JSON.stringify(data, null, 2));
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadPersistedSessions() {
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.existsSync(SESSION_PERSIST_PATH)) return {};
|
|
30
|
+
return JSON.parse(fs.readFileSync(SESSION_PERSIST_PATH, 'utf8'));
|
|
31
|
+
} catch { return {}; }
|
|
32
|
+
}
|
|
13
33
|
|
|
14
34
|
const app = express();
|
|
15
35
|
app.use(cors());
|
|
@@ -48,6 +68,21 @@ if (!daemonClaim.claimed) {
|
|
|
48
68
|
const sessions = {};
|
|
49
69
|
const handoffs = {};
|
|
50
70
|
const threads = {};
|
|
71
|
+
|
|
72
|
+
// Restore persisted session metadata (wrapped sessions await reconnect)
|
|
73
|
+
const _persisted = loadPersistedSessions();
|
|
74
|
+
for (const [id, meta] of Object.entries(_persisted)) {
|
|
75
|
+
if (meta.type === 'wrapped') {
|
|
76
|
+
sessions[id] = {
|
|
77
|
+
id, type: 'wrapped', ptyProcess: null, ownerWs: null,
|
|
78
|
+
command: meta.command || 'wrapped', cwd: meta.cwd || process.cwd(),
|
|
79
|
+
createdAt: meta.createdAt || new Date().toISOString(),
|
|
80
|
+
lastActivityAt: meta.lastActivityAt || new Date().toISOString(),
|
|
81
|
+
clients: new Set(), isClosing: false
|
|
82
|
+
};
|
|
83
|
+
console.log(`[PERSIST] Restored session ${id} (awaiting reconnect)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
51
86
|
const STRIPPED_SESSION_ENV_KEYS = [
|
|
52
87
|
'CLAUDECODE',
|
|
53
88
|
'CODEX_CI',
|
|
@@ -185,6 +220,7 @@ app.post('/api/sessions/spawn', (req, res) => {
|
|
|
185
220
|
});
|
|
186
221
|
|
|
187
222
|
console.log(`[SPAWN] Created session ${session_id} (${command})`);
|
|
223
|
+
persistSessions();
|
|
188
224
|
res.status(201).json({ session_id, command, cwd });
|
|
189
225
|
} catch (err) {
|
|
190
226
|
res.status(500).json({ error: err.message });
|
|
@@ -250,6 +286,7 @@ app.post('/api/sessions/register', (req, res) => {
|
|
|
250
286
|
});
|
|
251
287
|
|
|
252
288
|
console.log(`[REGISTER] Registered wrapped session ${session_id}`);
|
|
289
|
+
persistSessions();
|
|
253
290
|
res.status(201).json({ session_id, type: 'wrapped', command: sessionRecord.command, cwd });
|
|
254
291
|
});
|
|
255
292
|
|
|
@@ -814,9 +851,11 @@ app.delete('/api/sessions/:id', (req, res) => {
|
|
|
814
851
|
session.clients.forEach(ws => ws.close(1000, 'Session destroyed'));
|
|
815
852
|
delete sessions[id];
|
|
816
853
|
console.log(`[KILL] Wrapped session ${id} removed`);
|
|
854
|
+
persistSessions();
|
|
817
855
|
} else {
|
|
818
856
|
session.ptyProcess.kill();
|
|
819
857
|
console.log(`[KILL] Session ${id} forcefully closed`);
|
|
858
|
+
persistSessions();
|
|
820
859
|
}
|
|
821
860
|
res.json({ success: true, status: 'closing' });
|
|
822
861
|
} catch (err) {
|