@dmsdc-ai/aigentry-telepty 0.1.50 → 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.
@@ -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) {
@@ -824,15 +863,88 @@ app.delete('/api/sessions/:id', (req, res) => {
824
863
  }
825
864
  });
826
865
 
866
+ // Shared auto-router: handles turn_request events from any source (WS or HTTP)
867
+ function busAutoRoute(msg) {
868
+ const eventType = msg.type || msg.kind;
869
+ const isRoutable = (eventType === 'turn_request' || eventType === 'deliberation_route_turn') && (msg.target || msg.target_session_id);
870
+ if (!isRoutable) return;
871
+
872
+ const rawTarget = (msg.target || msg.target_session_id).split('@')[0];
873
+ console.log(`[BUS-ROUTE] Received ${eventType}: target=${rawTarget}`);
874
+ const targetId = resolveSessionAlias(rawTarget);
875
+ const targetSession = targetId ? sessions[targetId] : null;
876
+ if (!targetSession) {
877
+ console.log(`[BUS-ROUTE] Target ${rawTarget} not found`);
878
+ return;
879
+ }
880
+
881
+ const prompt = (msg.payload && msg.payload.prompt) || msg.content || msg.prompt || JSON.stringify(msg);
882
+ const inject_id = crypto.randomUUID();
883
+
884
+ // Write to session (kitty primary, WS fallback)
885
+ const sock = findKittySocket();
886
+ if (!targetSession.kittyWindowId && sock) targetSession.kittyWindowId = findKittyWindowId(sock, targetId);
887
+ const wid = targetSession.kittyWindowId;
888
+ let delivered = false;
889
+
890
+ if (wid && sock && targetSession.type === 'wrapped') {
891
+ try {
892
+ const escaped = prompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
893
+ require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} '${escaped}'`, {
894
+ timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
895
+ });
896
+ setTimeout(() => {
897
+ try {
898
+ require('child_process').execSync(`kitty @ --to unix:${sock} send-key --match id:${wid} Return`, {
899
+ timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
900
+ });
901
+ } catch {}
902
+ }, 500);
903
+ delivered = true;
904
+ } catch {}
905
+ }
906
+ if (!delivered) {
907
+ if (targetSession.type === 'wrapped' && targetSession.ownerWs && targetSession.ownerWs.readyState === 1) {
908
+ targetSession.ownerWs.send(JSON.stringify({ type: 'inject', data: prompt }));
909
+ setTimeout(() => {
910
+ if (targetSession.ownerWs && targetSession.ownerWs.readyState === 1) {
911
+ targetSession.ownerWs.send(JSON.stringify({ type: 'inject', data: '\r' }));
912
+ }
913
+ }, 300);
914
+ delivered = true;
915
+ } else if (targetSession.ptyProcess) {
916
+ targetSession.ptyProcess.write(prompt);
917
+ setTimeout(() => targetSession.ptyProcess.write('\r'), 300);
918
+ delivered = true;
919
+ }
920
+ }
921
+
922
+ // Emit inject_written ack
923
+ const ackMsg = JSON.stringify({
924
+ type: 'inject_written',
925
+ inject_id,
926
+ sender: 'daemon',
927
+ target_agent: targetId,
928
+ source_type: 'bus_auto_route',
929
+ delivered,
930
+ timestamp: new Date().toISOString()
931
+ });
932
+ busClients.forEach(client => {
933
+ if (client.readyState === 1) client.send(ackMsg);
934
+ });
935
+ targetSession.lastActivityAt = new Date().toISOString();
936
+ console.log(`[BUS-ROUTE] ${eventType} → ${targetId}: ${delivered ? 'delivered' : 'failed'}`);
937
+ }
938
+
827
939
  app.post('/api/bus/publish', (req, res) => {
828
940
  const payload = req.body;
829
-
941
+
830
942
  if (!payload || typeof payload !== 'object') {
831
943
  return res.status(400).json({ error: 'Payload must be a JSON object' });
832
944
  }
833
945
 
834
946
  let deliveredCount = 0;
835
-
947
+
836
948
  busClients.forEach(client => {
837
949
  if (client.readyState === 1) { // WebSocket.OPEN
838
950
  client.send(JSON.stringify(payload));
@@ -840,6 +952,9 @@ app.post('/api/bus/publish', (req, res) => {
840
952
  }
841
953
  });
842
954
 
955
+ // Auto-route if this is a turn_request
956
+ busAutoRoute(payload);
957
+
843
958
  res.json({ success: true, delivered: deliveredCount });
844
959
  });
845
960
 
@@ -1247,76 +1362,8 @@ busWss.on('connection', (ws, req) => {
1247
1362
  }
1248
1363
  });
1249
1364
 
1250
- // Auto-router: turn_request inject to target session PTY
1251
- // Matches both msg.type and msg.kind (deliberation uses 'kind')
1252
- const eventType = msg.type || msg.kind;
1253
- const isRoutable = (eventType === 'turn_request' || eventType === 'deliberation_route_turn') && (msg.target || msg.target_session_id);
1254
- if (isRoutable) {
1255
- // Parse target: may include @host suffix (e.g. "session-001@100.x.y.z")
1256
- const rawTarget = (msg.target || msg.target_session_id).split('@')[0];
1257
- console.log(`[BUS-ROUTE] Received ${eventType}: target=${rawTarget}`);
1258
- const targetId = resolveSessionAlias(rawTarget);
1259
- const targetSession = targetId ? sessions[targetId] : null;
1260
- if (targetSession) {
1261
- // Extract prompt from payload.prompt (deliberation schema) or fallbacks
1262
- const prompt = (msg.payload && msg.payload.prompt) || msg.content || msg.prompt || JSON.stringify(msg);
1263
- const inject_id = crypto.randomUUID();
1264
-
1265
- // Write to session (kitty primary, WS fallback)
1266
- const sock = findKittySocket();
1267
- if (!targetSession.kittyWindowId && sock) targetSession.kittyWindowId = findKittyWindowId(sock, targetId);
1268
- const wid = targetSession.kittyWindowId;
1269
- let delivered = false;
1270
-
1271
- if (wid && sock && targetSession.type === 'wrapped') {
1272
- try {
1273
- const escaped = prompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
1274
- require('child_process').execSync(`kitty @ --to unix:${sock} send-text --match id:${wid} '${escaped}'`, {
1275
- timeout: 5000, stdio: ['pipe', 'pipe', 'pipe']
1276
- });
1277
- setTimeout(() => {
1278
- try {
1279
- require('child_process').execSync(`kitty @ --to unix:${sock} send-key --match id:${wid} Return`, {
1280
- timeout: 3000, stdio: ['pipe', 'pipe', 'pipe']
1281
- });
1282
- } catch {}
1283
- }, 500);
1284
- delivered = true;
1285
- } catch {}
1286
- }
1287
- if (!delivered) {
1288
- // WS fallback
1289
- if (targetSession.type === 'wrapped' && targetSession.ownerWs && targetSession.ownerWs.readyState === 1) {
1290
- targetSession.ownerWs.send(JSON.stringify({ type: 'inject', data: prompt }));
1291
- setTimeout(() => {
1292
- if (targetSession.ownerWs && targetSession.ownerWs.readyState === 1) {
1293
- targetSession.ownerWs.send(JSON.stringify({ type: 'inject', data: '\r' }));
1294
- }
1295
- }, 300);
1296
- delivered = true;
1297
- } else if (targetSession.ptyProcess) {
1298
- targetSession.ptyProcess.write(prompt);
1299
- setTimeout(() => targetSession.ptyProcess.write('\r'), 300);
1300
- delivered = true;
1301
- }
1302
- }
1303
-
1304
- // Emit inject_written ack
1305
- const ackMsg = JSON.stringify({
1306
- type: 'inject_written',
1307
- inject_id,
1308
- sender: 'daemon',
1309
- target_agent: targetId,
1310
- source_type: 'bus_auto_route',
1311
- delivered,
1312
- timestamp: new Date().toISOString()
1313
- });
1314
- busClients.forEach(client => {
1315
- if (client.readyState === 1) client.send(ackMsg);
1316
- });
1317
- console.log(`[BUS-ROUTE] turn_request → ${targetId}: ${delivered ? 'delivered' : 'failed'}`);
1318
- }
1319
- }
1365
+ // Auto-route turn_request events (shared logic with HTTP publish)
1366
+ busAutoRoute(msg);
1320
1367
  } catch (e) {
1321
1368
  console.error('[BUS] Invalid message format', e);
1322
1369
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",