@dmsdc-ai/aigentry-telepty 0.1.16 → 0.1.18
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/README.md +11 -0
- package/cli.js +506 -4
- package/daemon.js +267 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,17 @@ The installer now stops older local telepty daemons before starting the new one,
|
|
|
49
49
|
telepty inject my-session "echo 'Hello from nowhere!'"
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
+
4. **Universal CLI submit (split_cr):**
|
|
53
|
+
|
|
54
|
+
All AI CLIs (Claude, Codex, Gemini) submit reliably via the `split_cr` strategy — text is injected first, then `\r` is sent separately after a 300ms delay. This works universally across all CLIs without any per-CLI workarounds.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# The inject API handles split_cr automatically
|
|
58
|
+
curl -X POST http://127.0.0.1:3848/api/sessions/my-session/inject \
|
|
59
|
+
-H "Content-Type: application/json" \
|
|
60
|
+
-d '{"prompt": "your command here"}'
|
|
61
|
+
```
|
|
62
|
+
|
|
52
63
|
CLI commands such as `list`, `attach`, `inject`, `rename`, `multicast`, and `broadcast` now auto-discover sessions across your Tailnet by default. If the same session ID exists on multiple hosts, disambiguate with `session_id@host`.
|
|
53
64
|
|
|
54
65
|
## Testing
|
package/cli.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
|
+
const fs = require('fs');
|
|
5
6
|
const WebSocket = require('ws');
|
|
6
7
|
const { execSync, spawn } = require('child_process');
|
|
7
8
|
const readline = require('readline');
|
|
@@ -556,6 +557,11 @@ async function main() {
|
|
|
556
557
|
return manageInteractive();
|
|
557
558
|
}
|
|
558
559
|
|
|
560
|
+
if (cmd === '--version' || cmd === '-v' || cmd === 'version') {
|
|
561
|
+
console.log(pkg.version);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
559
565
|
if (cmd === 'update') {
|
|
560
566
|
console.log('\x1b[36m🔄 Updating telepty to the latest version...\x1b[0m');
|
|
561
567
|
try {
|
|
@@ -870,9 +876,27 @@ async function main() {
|
|
|
870
876
|
const noEnterIndex = args.indexOf('--no-enter');
|
|
871
877
|
const noEnter = noEnterIndex !== -1;
|
|
872
878
|
if (noEnter) args.splice(noEnterIndex, 1);
|
|
873
|
-
|
|
879
|
+
|
|
880
|
+
// Extract --from flag
|
|
881
|
+
let fromId;
|
|
882
|
+
const fromIndex = args.indexOf('--from');
|
|
883
|
+
if (fromIndex !== -1 && args[fromIndex + 1]) {
|
|
884
|
+
fromId = args[fromIndex + 1];
|
|
885
|
+
args.splice(fromIndex, 2);
|
|
886
|
+
} else {
|
|
887
|
+
fromId = process.env.TELEPTY_SESSION_ID || undefined;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Extract --reply-to flag
|
|
891
|
+
let replyTo;
|
|
892
|
+
const replyToIndex = args.indexOf('--reply-to');
|
|
893
|
+
if (replyToIndex !== -1 && args[replyToIndex + 1]) {
|
|
894
|
+
replyTo = args[replyToIndex + 1];
|
|
895
|
+
args.splice(replyToIndex, 2);
|
|
896
|
+
}
|
|
897
|
+
|
|
874
898
|
const sessionId = args[1]; const prompt = args.slice(2).join(' ');
|
|
875
|
-
if (!sessionId || !prompt) { console.error('❌ Usage: telepty inject [--no-enter] <session_id> "<prompt text>"'); process.exit(1); }
|
|
899
|
+
if (!sessionId || !prompt) { console.error('❌ Usage: telepty inject [--no-enter] [--from <id>] [--reply-to <id>] <session_id> "<prompt text>"'); process.exit(1); }
|
|
876
900
|
try {
|
|
877
901
|
const target = await resolveSessionTarget(sessionId);
|
|
878
902
|
if (!target) {
|
|
@@ -880,8 +904,12 @@ async function main() {
|
|
|
880
904
|
process.exit(1);
|
|
881
905
|
}
|
|
882
906
|
|
|
907
|
+
const body = { prompt, no_enter: noEnter };
|
|
908
|
+
if (fromId) body.from = fromId;
|
|
909
|
+
if (replyTo) body.reply_to = replyTo;
|
|
910
|
+
|
|
883
911
|
const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
884
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(
|
|
912
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
885
913
|
});
|
|
886
914
|
const data = await res.json();
|
|
887
915
|
if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
|
|
@@ -891,6 +919,31 @@ async function main() {
|
|
|
891
919
|
return;
|
|
892
920
|
}
|
|
893
921
|
|
|
922
|
+
if (cmd === 'reply') {
|
|
923
|
+
const mySessionId = process.env.TELEPTY_SESSION_ID;
|
|
924
|
+
if (!mySessionId) { console.error('❌ TELEPTY_SESSION_ID env var is required for reply command'); process.exit(1); }
|
|
925
|
+
const replyText = args.slice(1).join(' ');
|
|
926
|
+
if (!replyText) { console.error('❌ Usage: telepty reply "<text>"'); process.exit(1); }
|
|
927
|
+
try {
|
|
928
|
+
const metaRes = await fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(mySessionId)}`);
|
|
929
|
+
if (!metaRes.ok) { console.error(`❌ Could not fetch session metadata for '${mySessionId}'`); process.exit(1); }
|
|
930
|
+
const meta = await metaRes.json();
|
|
931
|
+
const replyTo = meta.lastInjectReplyTo;
|
|
932
|
+
if (!replyTo) { console.error(`❌ No pending reply-to found for session '${mySessionId}'`); process.exit(1); }
|
|
933
|
+
const target = await resolveSessionTarget(replyTo);
|
|
934
|
+
if (!target) { console.error(`❌ Session '${replyTo}' was not found on any discovered host.`); process.exit(1); }
|
|
935
|
+
const fullPrompt = `[from: ${mySessionId}] [reply-to: ${mySessionId}] ${replyText}`;
|
|
936
|
+
const body = { prompt: fullPrompt, from: mySessionId, reply_to: mySessionId };
|
|
937
|
+
const res = await fetchWithAuth(`http://${target.host}:${PORT}/api/sessions/${encodeURIComponent(target.id)}/inject`, {
|
|
938
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
|
|
939
|
+
});
|
|
940
|
+
const data = await res.json();
|
|
941
|
+
if (!res.ok) { console.error(`❌ Error: ${data.error}`); return; }
|
|
942
|
+
console.log(`✅ Reply sent to '\x1b[36m${replyTo}\x1b[0m'.`);
|
|
943
|
+
} catch (e) { console.error(`❌ ${e.message || 'Failed to connect to the target daemon.'}`); }
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
|
|
894
947
|
if (cmd === 'multicast') {
|
|
895
948
|
const sessionIdsRaw = args[1]; const prompt = args.slice(2).join(' ');
|
|
896
949
|
if (!sessionIdsRaw || !prompt) { console.error('❌ Usage: telepty multicast <id1,id2,...> "<prompt text>"'); process.exit(1); }
|
|
@@ -983,6 +1036,441 @@ async function main() {
|
|
|
983
1036
|
return;
|
|
984
1037
|
}
|
|
985
1038
|
|
|
1039
|
+
if (cmd === 'deliberate') {
|
|
1040
|
+
await ensureDaemonRunning();
|
|
1041
|
+
const subCmd = args[1];
|
|
1042
|
+
|
|
1043
|
+
if (subCmd === 'status') {
|
|
1044
|
+
// telepty deliberate status [thread_id]
|
|
1045
|
+
const threadId = args[2];
|
|
1046
|
+
try {
|
|
1047
|
+
if (threadId) {
|
|
1048
|
+
const resp = await fetchWithAuth(`${DAEMON_URL}/api/threads/${threadId}`);
|
|
1049
|
+
const thread = await resp.json();
|
|
1050
|
+
if (!resp.ok) { console.error('Error:', thread.error); process.exit(1); }
|
|
1051
|
+
console.log(`\n Thread: ${thread.id}`);
|
|
1052
|
+
console.log(` Topic: ${thread.topic}`);
|
|
1053
|
+
console.log(` Status: ${thread.status}`);
|
|
1054
|
+
console.log(` Orchestrator: ${thread.orchestrator_session_id || '(none)'}`);
|
|
1055
|
+
console.log(` Participants: ${thread.participant_session_ids.join(', ') || '(none)'}`);
|
|
1056
|
+
console.log(` Messages: ${thread.message_count}`);
|
|
1057
|
+
console.log(` Created: ${thread.created_at}`);
|
|
1058
|
+
if (thread.closed_at) console.log(` Closed: ${thread.closed_at}`);
|
|
1059
|
+
console.log();
|
|
1060
|
+
} else {
|
|
1061
|
+
const resp = await fetchWithAuth(`${DAEMON_URL}/api/threads`);
|
|
1062
|
+
const list = await resp.json();
|
|
1063
|
+
if (list.length === 0) {
|
|
1064
|
+
console.log('No deliberation threads found.');
|
|
1065
|
+
} else {
|
|
1066
|
+
console.log(`\n Deliberation Threads (${list.length}):\n`);
|
|
1067
|
+
for (const t of list) {
|
|
1068
|
+
const icon = t.status === 'active' ? '🟢' : '⏹️';
|
|
1069
|
+
console.log(` ${icon} ${t.id.slice(0, 8)} ${t.status.padEnd(8)} msgs:${t.message_count} participants:${t.participant_count} "${t.topic}"`);
|
|
1070
|
+
}
|
|
1071
|
+
console.log();
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
console.error('Failed:', err.message);
|
|
1076
|
+
process.exit(1);
|
|
1077
|
+
}
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (subCmd === 'end') {
|
|
1082
|
+
// telepty deliberate end <thread_id>
|
|
1083
|
+
const threadId = args[2];
|
|
1084
|
+
if (!threadId) { console.error('Usage: telepty deliberate end <thread_id>'); process.exit(1); }
|
|
1085
|
+
try {
|
|
1086
|
+
const resp = await fetchWithAuth(`${DAEMON_URL}/api/threads/${threadId}`, {
|
|
1087
|
+
method: 'PATCH',
|
|
1088
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1089
|
+
body: JSON.stringify({ status: 'closed' })
|
|
1090
|
+
});
|
|
1091
|
+
const result = await resp.json();
|
|
1092
|
+
if (!resp.ok) { console.error('Error:', result.error); process.exit(1); }
|
|
1093
|
+
console.log(`Deliberation thread ${threadId} closed.`);
|
|
1094
|
+
} catch (err) {
|
|
1095
|
+
console.error('Failed:', err.message);
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// telepty deliberate --topic "..." [--sessions id1,id2,...] [--context path]
|
|
1102
|
+
// Extract flags
|
|
1103
|
+
const topicIdx = args.indexOf('--topic');
|
|
1104
|
+
const sessionsIdx = args.indexOf('--sessions');
|
|
1105
|
+
const contextIdx = args.indexOf('--context');
|
|
1106
|
+
|
|
1107
|
+
const topic = topicIdx !== -1 && args[topicIdx + 1] ? args[topicIdx + 1] : null;
|
|
1108
|
+
const sessionsArg = sessionsIdx !== -1 && args[sessionsIdx + 1] ? args[sessionsIdx + 1] : null;
|
|
1109
|
+
const contextPath = contextIdx !== -1 && args[contextIdx + 1] ? args[contextIdx + 1] : null;
|
|
1110
|
+
|
|
1111
|
+
if (!topic) {
|
|
1112
|
+
console.error('Usage: telepty deliberate --topic "topic description" [--sessions id1,id2,...] [--context file]');
|
|
1113
|
+
console.error(' telepty deliberate status [thread_id]');
|
|
1114
|
+
console.error(' telepty deliberate end <thread_id>');
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const orchestratorId = process.env.TELEPTY_SESSION_ID || null;
|
|
1119
|
+
|
|
1120
|
+
// Read context file if provided
|
|
1121
|
+
let contextContent = null;
|
|
1122
|
+
if (contextPath) {
|
|
1123
|
+
try {
|
|
1124
|
+
contextContent = fs.readFileSync(contextPath, 'utf-8');
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
console.error(`Failed to read context file: ${err.message}`);
|
|
1127
|
+
process.exit(1);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Discover target sessions
|
|
1132
|
+
let targetSessions;
|
|
1133
|
+
try {
|
|
1134
|
+
const discovered = await discoverSessions({ silent: true });
|
|
1135
|
+
if (sessionsArg) {
|
|
1136
|
+
const requestedIds = sessionsArg.split(',').map(s => s.trim());
|
|
1137
|
+
targetSessions = discovered.filter(s => requestedIds.includes(s.id));
|
|
1138
|
+
const foundIds = targetSessions.map(s => s.id);
|
|
1139
|
+
const missing = requestedIds.filter(id => !foundIds.includes(id));
|
|
1140
|
+
if (missing.length > 0) {
|
|
1141
|
+
console.error(`Warning: Sessions not found: ${missing.join(', ')}`);
|
|
1142
|
+
}
|
|
1143
|
+
} else {
|
|
1144
|
+
// All sessions except orchestrator
|
|
1145
|
+
targetSessions = discovered.filter(s => s.id !== orchestratorId);
|
|
1146
|
+
}
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
console.error('Failed to discover sessions:', err.message);
|
|
1149
|
+
process.exit(1);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (targetSessions.length === 0) {
|
|
1153
|
+
console.error('No target sessions found.');
|
|
1154
|
+
process.exit(1);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const participantIds = targetSessions.map(s => s.id);
|
|
1158
|
+
|
|
1159
|
+
// Create thread on daemon
|
|
1160
|
+
let threadId;
|
|
1161
|
+
try {
|
|
1162
|
+
const resp = await fetchWithAuth(`${DAEMON_URL}/api/threads`, {
|
|
1163
|
+
method: 'POST',
|
|
1164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1165
|
+
body: JSON.stringify({
|
|
1166
|
+
topic,
|
|
1167
|
+
orchestrator_session_id: orchestratorId,
|
|
1168
|
+
participant_session_ids: participantIds,
|
|
1169
|
+
context: contextContent
|
|
1170
|
+
})
|
|
1171
|
+
});
|
|
1172
|
+
const result = await resp.json();
|
|
1173
|
+
if (!resp.ok) { console.error('Error:', result.error); process.exit(1); }
|
|
1174
|
+
threadId = result.thread_id;
|
|
1175
|
+
} catch (err) {
|
|
1176
|
+
console.error('Failed to create thread:', err.message);
|
|
1177
|
+
process.exit(1);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Build session directory
|
|
1181
|
+
const sessionDirectory = targetSessions.map(s => {
|
|
1182
|
+
const proj = s.cwd ? s.cwd.split('/').pop() : '(unknown)';
|
|
1183
|
+
return ` - ${s.id} (${s.command || 'unknown'}, project: ${proj})`;
|
|
1184
|
+
}).join('\n');
|
|
1185
|
+
|
|
1186
|
+
// Build protocol template
|
|
1187
|
+
const protocolTemplate = `[from: ${orchestratorId || 'orchestrator'}] [reply-to: ${orchestratorId || 'orchestrator'}]
|
|
1188
|
+
|
|
1189
|
+
## Bidirectional Multi-Session Deliberation
|
|
1190
|
+
|
|
1191
|
+
**Thread ID:** ${threadId}
|
|
1192
|
+
**Topic:** ${topic}
|
|
1193
|
+
**Orchestrator:** ${orchestratorId || '(not set)'}
|
|
1194
|
+
|
|
1195
|
+
### Session Directory
|
|
1196
|
+
${sessionDirectory}
|
|
1197
|
+
|
|
1198
|
+
${contextContent ? `### Context\n${contextContent}\n` : ''}
|
|
1199
|
+
### Protocol Rules (MANDATORY)
|
|
1200
|
+
|
|
1201
|
+
1. **Always include sender identity**: Every message you send to another session MUST include \`[from: YOUR_SESSION_ID] [reply-to: YOUR_SESSION_ID]\` at the beginning.
|
|
1202
|
+
|
|
1203
|
+
2. **Use telepty for cross-session communication**: To send a message to another session:
|
|
1204
|
+
\`\`\`
|
|
1205
|
+
telepty inject --from YOUR_SESSION_ID --reply-to YOUR_SESSION_ID <target_session_id> "your message"
|
|
1206
|
+
\`\`\`
|
|
1207
|
+
Or use: \`telepty reply "your message"\` to reply to the last sender.
|
|
1208
|
+
|
|
1209
|
+
3. **Do NOT self-resolve cross-cutting concerns**: If a question involves another project's domain, ASK that session directly via telepty inject. Do not guess or assume.
|
|
1210
|
+
|
|
1211
|
+
4. **Sub-deliberation allowed**: You may initiate side conversations with specific sessions for detailed technical discussions.
|
|
1212
|
+
|
|
1213
|
+
5. **Thread tracking**: Include \`thread_id: ${threadId}\` in bus events for this deliberation.
|
|
1214
|
+
|
|
1215
|
+
6. **Completion**: When you believe the discussion on your part is complete, send a summary to the orchestrator (${orchestratorId || 'orchestrator'}).
|
|
1216
|
+
|
|
1217
|
+
### Your Task
|
|
1218
|
+
Discuss the following topic from your project's perspective. Engage with other sessions to align on interfaces and implementation details.
|
|
1219
|
+
|
|
1220
|
+
**Topic:** ${topic}
|
|
1221
|
+
`;
|
|
1222
|
+
|
|
1223
|
+
// Inject protocol to all target sessions
|
|
1224
|
+
console.log(`\nStarting deliberation thread ${threadId.slice(0, 8)}...`);
|
|
1225
|
+
console.log(`Topic: ${topic}`);
|
|
1226
|
+
console.log(`Participants: ${participantIds.length}\n`);
|
|
1227
|
+
|
|
1228
|
+
let successCount = 0;
|
|
1229
|
+
let failCount = 0;
|
|
1230
|
+
|
|
1231
|
+
for (const session of targetSessions) {
|
|
1232
|
+
try {
|
|
1233
|
+
const host = session._host || '127.0.0.1';
|
|
1234
|
+
const body = {
|
|
1235
|
+
prompt: protocolTemplate,
|
|
1236
|
+
no_enter: true,
|
|
1237
|
+
from: orchestratorId,
|
|
1238
|
+
reply_to: orchestratorId,
|
|
1239
|
+
thread_id: threadId
|
|
1240
|
+
};
|
|
1241
|
+
const resp = await fetchWithAuth(`http://${host}:${PORT}/api/sessions/${encodeURIComponent(session.id)}/inject`, {
|
|
1242
|
+
method: 'POST',
|
|
1243
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1244
|
+
body: JSON.stringify(body)
|
|
1245
|
+
});
|
|
1246
|
+
if (resp.ok) {
|
|
1247
|
+
// Submit after text injection (300ms delay handled by daemon)
|
|
1248
|
+
setTimeout(async () => {
|
|
1249
|
+
try {
|
|
1250
|
+
await fetchWithAuth(`http://${host}:${PORT}/api/sessions/${encodeURIComponent(session.id)}/submit`, { method: 'POST' });
|
|
1251
|
+
} catch {}
|
|
1252
|
+
}, 500);
|
|
1253
|
+
console.log(` ✅ Injected to ${session.id}`);
|
|
1254
|
+
successCount++;
|
|
1255
|
+
} else {
|
|
1256
|
+
const err = await resp.json();
|
|
1257
|
+
console.log(` ❌ Failed ${session.id}: ${err.error}`);
|
|
1258
|
+
failCount++;
|
|
1259
|
+
}
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
console.log(` ❌ Failed ${session.id}: ${err.message}`);
|
|
1262
|
+
failCount++;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
console.log(`\nDeliberation started: ${successCount} injected, ${failCount} failed`);
|
|
1267
|
+
console.log(`Thread ID: ${threadId}`);
|
|
1268
|
+
console.log(`Monitor: telepty deliberate status ${threadId}`);
|
|
1269
|
+
console.log(`End: telepty deliberate end ${threadId}`);
|
|
1270
|
+
|
|
1271
|
+
// Wait for submit timeouts to complete
|
|
1272
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (cmd === 'handoff') {
|
|
1277
|
+
const handoffCmd = args[1];
|
|
1278
|
+
|
|
1279
|
+
if (!handoffCmd || handoffCmd === 'list') {
|
|
1280
|
+
// telepty handoff list [--status=pending]
|
|
1281
|
+
const statusFilter = args.find(a => a.startsWith('--status='));
|
|
1282
|
+
const qs = statusFilter ? `?status=${statusFilter.split('=')[1]}` : '';
|
|
1283
|
+
try {
|
|
1284
|
+
const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff${qs}`);
|
|
1285
|
+
const list = await resp.json();
|
|
1286
|
+
if (list.length === 0) {
|
|
1287
|
+
console.log('No handoffs found.');
|
|
1288
|
+
} else {
|
|
1289
|
+
console.log(`\n Handoffs (${list.length}):\n`);
|
|
1290
|
+
for (const h of list) {
|
|
1291
|
+
const statusIcon = { pending: '⏳', claimed: '🔄', executing: '⚙️', completed: '✅', failed: '❌' }[h.status] || '?';
|
|
1292
|
+
console.log(` ${statusIcon} ${h.id.slice(0, 8)} ${h.status.padEnd(10)} tasks:${h.task_count} ${h.deliberation_id || '(no delib)'} ${h.created_at}`);
|
|
1293
|
+
}
|
|
1294
|
+
console.log();
|
|
1295
|
+
}
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
console.error('Failed to list handoffs:', err.message);
|
|
1298
|
+
process.exit(1);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
} else if (handoffCmd === 'drop') {
|
|
1302
|
+
// telepty handoff drop [--delib=ID] [--source=SESSION] [--auto-execute] < synthesis.json
|
|
1303
|
+
// Or: telepty handoff drop --summary="..." --tasks='[{"task":"do X","files":["a.js"]}]'
|
|
1304
|
+
const delibFlag = args.find(a => a.startsWith('--delib='));
|
|
1305
|
+
const sourceFlag = args.find(a => a.startsWith('--source='));
|
|
1306
|
+
const autoExec = args.includes('--auto-execute');
|
|
1307
|
+
const summaryFlag = args.find(a => a.startsWith('--summary='));
|
|
1308
|
+
const tasksFlag = args.find(a => a.startsWith('--tasks='));
|
|
1309
|
+
|
|
1310
|
+
let synthesis;
|
|
1311
|
+
if (summaryFlag && tasksFlag) {
|
|
1312
|
+
synthesis = {
|
|
1313
|
+
summary: summaryFlag.split('=').slice(1).join('='),
|
|
1314
|
+
tasks: JSON.parse(tasksFlag.split('=').slice(1).join('='))
|
|
1315
|
+
};
|
|
1316
|
+
} else if (!process.stdin.isTTY) {
|
|
1317
|
+
// Read from stdin
|
|
1318
|
+
const chunks = [];
|
|
1319
|
+
for await (const chunk of process.stdin) {
|
|
1320
|
+
chunks.push(chunk);
|
|
1321
|
+
}
|
|
1322
|
+
synthesis = JSON.parse(Buffer.concat(chunks).toString());
|
|
1323
|
+
} else {
|
|
1324
|
+
console.error('Usage: telepty handoff drop --summary="..." --tasks=\'[...]\'');
|
|
1325
|
+
console.error(' Or pipe JSON: echo \'{"summary":"...","tasks":[...]}\' | telepty handoff drop');
|
|
1326
|
+
process.exit(1);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
try {
|
|
1330
|
+
const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff`, {
|
|
1331
|
+
method: 'POST',
|
|
1332
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1333
|
+
body: JSON.stringify({
|
|
1334
|
+
deliberation_id: delibFlag ? delibFlag.split('=').slice(1).join('=') : null,
|
|
1335
|
+
source_session_id: sourceFlag ? sourceFlag.split('=').slice(1).join('=') : (process.env.TELEPTY_SESSION_ID || null),
|
|
1336
|
+
synthesis,
|
|
1337
|
+
auto_execute: autoExec
|
|
1338
|
+
})
|
|
1339
|
+
});
|
|
1340
|
+
const result = await resp.json();
|
|
1341
|
+
if (resp.ok) {
|
|
1342
|
+
console.log(`Handoff created: ${result.handoff_id}`);
|
|
1343
|
+
} else {
|
|
1344
|
+
console.error('Failed:', result.error);
|
|
1345
|
+
process.exit(1);
|
|
1346
|
+
}
|
|
1347
|
+
} catch (err) {
|
|
1348
|
+
console.error('Failed to create handoff:', err.message);
|
|
1349
|
+
process.exit(1);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
} else if (handoffCmd === 'claim') {
|
|
1353
|
+
// telepty handoff claim <handoff_id> [--agent=SESSION_ID]
|
|
1354
|
+
const handoffId = args[2];
|
|
1355
|
+
if (!handoffId) {
|
|
1356
|
+
console.error('Usage: telepty handoff claim <handoff_id> [--agent=SESSION_ID]');
|
|
1357
|
+
process.exit(1);
|
|
1358
|
+
}
|
|
1359
|
+
const agentFlag = args.find(a => a.startsWith('--agent='));
|
|
1360
|
+
const agentId = agentFlag ? agentFlag.split('=').slice(1).join('=') : process.env.TELEPTY_SESSION_ID;
|
|
1361
|
+
if (!agentId) {
|
|
1362
|
+
console.error('Error: --agent=SESSION_ID or TELEPTY_SESSION_ID env required');
|
|
1363
|
+
process.exit(1);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
try {
|
|
1367
|
+
const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff/${handoffId}/claim`, {
|
|
1368
|
+
method: 'POST',
|
|
1369
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1370
|
+
body: JSON.stringify({ agent_session_id: agentId })
|
|
1371
|
+
});
|
|
1372
|
+
const result = await resp.json();
|
|
1373
|
+
if (resp.ok) {
|
|
1374
|
+
console.log(`Claimed handoff ${handoffId}`);
|
|
1375
|
+
} else {
|
|
1376
|
+
console.error('Failed:', result.error);
|
|
1377
|
+
process.exit(1);
|
|
1378
|
+
}
|
|
1379
|
+
} catch (err) {
|
|
1380
|
+
console.error('Failed to claim handoff:', err.message);
|
|
1381
|
+
process.exit(1);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
} else if (handoffCmd === 'status') {
|
|
1385
|
+
// telepty handoff status <handoff_id> [executing|completed|failed] [--message="..."]
|
|
1386
|
+
const handoffId = args[2];
|
|
1387
|
+
if (!handoffId) {
|
|
1388
|
+
console.error('Usage: telepty handoff status <handoff_id> [new_status] [--message="..."]');
|
|
1389
|
+
process.exit(1);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
const newStatus = args[3] && !args[3].startsWith('--') ? args[3] : null;
|
|
1393
|
+
const msgFlag = args.find(a => a.startsWith('--message='));
|
|
1394
|
+
|
|
1395
|
+
if (!newStatus) {
|
|
1396
|
+
// GET status
|
|
1397
|
+
try {
|
|
1398
|
+
const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff/${handoffId}`);
|
|
1399
|
+
const handoff = await resp.json();
|
|
1400
|
+
if (!resp.ok) {
|
|
1401
|
+
console.error('Error:', handoff.error);
|
|
1402
|
+
process.exit(1);
|
|
1403
|
+
}
|
|
1404
|
+
console.log(`\n Handoff: ${handoff.id}`);
|
|
1405
|
+
console.log(` Status: ${handoff.status}`);
|
|
1406
|
+
console.log(` Deliberation: ${handoff.deliberation_id || '(none)'}`);
|
|
1407
|
+
console.log(` Claimed by: ${handoff.claimed_by || '(unclaimed)'}`);
|
|
1408
|
+
console.log(` Tasks: ${Array.isArray(handoff.synthesis.tasks) ? handoff.synthesis.tasks.length : 0}`);
|
|
1409
|
+
if (handoff.synthesis.summary) console.log(` Summary: ${handoff.synthesis.summary}`);
|
|
1410
|
+
if (handoff.progress.length > 0) {
|
|
1411
|
+
console.log(` Progress:`);
|
|
1412
|
+
for (const p of handoff.progress) {
|
|
1413
|
+
console.log(` - ${p.timestamp}: ${p.message}`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
console.log();
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
console.error('Failed:', err.message);
|
|
1419
|
+
process.exit(1);
|
|
1420
|
+
}
|
|
1421
|
+
} else {
|
|
1422
|
+
// PATCH status
|
|
1423
|
+
try {
|
|
1424
|
+
const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff/${handoffId}`, {
|
|
1425
|
+
method: 'PATCH',
|
|
1426
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1427
|
+
body: JSON.stringify({
|
|
1428
|
+
status: newStatus,
|
|
1429
|
+
message: msgFlag ? msgFlag.split('=').slice(1).join('=') : null
|
|
1430
|
+
})
|
|
1431
|
+
});
|
|
1432
|
+
const result = await resp.json();
|
|
1433
|
+
if (resp.ok) {
|
|
1434
|
+
console.log(`Handoff ${handoffId} -> ${newStatus}`);
|
|
1435
|
+
} else {
|
|
1436
|
+
console.error('Failed:', result.error);
|
|
1437
|
+
process.exit(1);
|
|
1438
|
+
}
|
|
1439
|
+
} catch (err) {
|
|
1440
|
+
console.error('Failed:', err.message);
|
|
1441
|
+
process.exit(1);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
} else if (handoffCmd === 'get') {
|
|
1446
|
+
// telepty handoff get <handoff_id> — dump full synthesis JSON
|
|
1447
|
+
const handoffId = args[2];
|
|
1448
|
+
if (!handoffId) {
|
|
1449
|
+
console.error('Usage: telepty handoff get <handoff_id>');
|
|
1450
|
+
process.exit(1);
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
const resp = await fetchWithAuth(`${DAEMON_URL}/api/handoff/${handoffId}`);
|
|
1454
|
+
const handoff = await resp.json();
|
|
1455
|
+
if (!resp.ok) {
|
|
1456
|
+
console.error('Error:', handoff.error);
|
|
1457
|
+
process.exit(1);
|
|
1458
|
+
}
|
|
1459
|
+
// Output raw JSON for piping to other tools
|
|
1460
|
+
console.log(JSON.stringify(handoff.synthesis, null, 2));
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
console.error('Failed:', err.message);
|
|
1463
|
+
process.exit(1);
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
} else {
|
|
1467
|
+
console.error(`Unknown handoff command: ${handoffCmd}`);
|
|
1468
|
+
console.error('Available: list, drop, claim, status, get');
|
|
1469
|
+
process.exit(1);
|
|
1470
|
+
}
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
986
1474
|
if (cmd === 'listen' || cmd === 'monitor') {
|
|
987
1475
|
await ensureDaemonRunning();
|
|
988
1476
|
|
|
@@ -1063,13 +1551,27 @@ Usage:
|
|
|
1063
1551
|
telepty allow [--id <id>] <command> [args...] Allow inject on a CLI
|
|
1064
1552
|
telepty list List all active sessions across discovered hosts
|
|
1065
1553
|
telepty attach [id[@host]] Attach to a session (Interactive picker if no ID)
|
|
1066
|
-
telepty inject [--no-enter] <id[@host]> "<prompt>" Inject text into a single session
|
|
1554
|
+
telepty inject [--no-enter] [--from <id>] [--reply-to <id>] <id[@host]> "<prompt>" Inject text into a single session
|
|
1555
|
+
telepty reply "<text>" Reply to the session that last injected into $TELEPTY_SESSION_ID
|
|
1067
1556
|
telepty multicast <id1[@host],id2[@host]> "<prompt>" Inject text into multiple specific sessions
|
|
1068
1557
|
telepty broadcast "<prompt>" Inject text into ALL active sessions
|
|
1069
1558
|
telepty rename <old_id[@host]> <new_id> Rename a session (updates terminal title too)
|
|
1070
1559
|
telepty listen Listen to the event bus and print JSON to stdout
|
|
1071
1560
|
telepty monitor Human-readable real-time billboard of bus events
|
|
1072
1561
|
telepty update Update telepty to the latest version
|
|
1562
|
+
|
|
1563
|
+
Handoff Commands:
|
|
1564
|
+
handoff list [--status=S] List handoffs (filter: pending/claimed/executing/completed)
|
|
1565
|
+
handoff drop [options] Create handoff from synthesis (pipe JSON or use --summary/--tasks)
|
|
1566
|
+
handoff claim <id> [--agent=S] Claim a pending handoff
|
|
1567
|
+
handoff status <id> [status] Get or update handoff status
|
|
1568
|
+
handoff get <id> Get full synthesis JSON (for piping)
|
|
1569
|
+
|
|
1570
|
+
Deliberation Commands:
|
|
1571
|
+
deliberate --topic "..." [--sessions s1,s2] [--context file]
|
|
1572
|
+
Start multi-session deliberation
|
|
1573
|
+
deliberate status [thread_id] List threads or show thread details
|
|
1574
|
+
deliberate end <thread_id> Close a deliberation thread
|
|
1073
1575
|
`);
|
|
1074
1576
|
}
|
|
1075
1577
|
|
package/daemon.js
CHANGED
|
@@ -46,6 +46,8 @@ if (!daemonClaim.claimed) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
const sessions = {};
|
|
49
|
+
const handoffs = {};
|
|
50
|
+
const threads = {};
|
|
49
51
|
const STRIPPED_SESSION_ENV_KEYS = [
|
|
50
52
|
'CLAUDECODE',
|
|
51
53
|
'CODEX_CI',
|
|
@@ -208,6 +210,22 @@ app.get('/api/sessions', (req, res) => {
|
|
|
208
210
|
res.json(list);
|
|
209
211
|
});
|
|
210
212
|
|
|
213
|
+
app.get('/api/sessions/:id', (req, res) => {
|
|
214
|
+
const { id } = req.params;
|
|
215
|
+
const session = sessions[id];
|
|
216
|
+
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
217
|
+
res.json({
|
|
218
|
+
id,
|
|
219
|
+
type: session.type || 'spawned',
|
|
220
|
+
command: session.command,
|
|
221
|
+
cwd: session.cwd,
|
|
222
|
+
createdAt: session.createdAt,
|
|
223
|
+
active_clients: session.clients ? session.clients.size : 0,
|
|
224
|
+
lastInjectFrom: session.lastInjectFrom || null,
|
|
225
|
+
lastInjectReplyTo: session.lastInjectReplyTo || null
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
211
229
|
app.get('/api/meta', (req, res) => {
|
|
212
230
|
res.json({
|
|
213
231
|
name: pkg.name,
|
|
@@ -215,7 +233,7 @@ app.get('/api/meta', (req, res) => {
|
|
|
215
233
|
pid: process.pid,
|
|
216
234
|
host: HOST,
|
|
217
235
|
port: Number(PORT),
|
|
218
|
-
capabilities: ['sessions', 'wrapped-sessions', 'skill-installer', 'singleton-daemon']
|
|
236
|
+
capabilities: ['sessions', 'wrapped-sessions', 'skill-installer', 'singleton-daemon', 'handoff-inbox', 'deliberation-threads']
|
|
219
237
|
});
|
|
220
238
|
});
|
|
221
239
|
|
|
@@ -458,10 +476,12 @@ app.post('/api/sessions/submit-all', (req, res) => {
|
|
|
458
476
|
|
|
459
477
|
app.post('/api/sessions/:id/inject', (req, res) => {
|
|
460
478
|
const { id } = req.params;
|
|
461
|
-
const { prompt, no_enter, auto_submit } = req.body;
|
|
479
|
+
const { prompt, no_enter, auto_submit, from, reply_to } = req.body;
|
|
462
480
|
const session = sessions[id];
|
|
463
481
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
464
482
|
if (!prompt) return res.status(400).json({ error: 'prompt is required' });
|
|
483
|
+
if (from) session.lastInjectFrom = from;
|
|
484
|
+
if (reply_to) session.lastInjectReplyTo = reply_to;
|
|
465
485
|
const inject_id = crypto.randomUUID();
|
|
466
486
|
try {
|
|
467
487
|
// Always inject text WITHOUT \r first, then send \r separately after delay
|
|
@@ -501,12 +521,31 @@ app.post('/api/sessions/:id/inject', (req, res) => {
|
|
|
501
521
|
sender: 'daemon',
|
|
502
522
|
target_agent: id,
|
|
503
523
|
content: prompt,
|
|
524
|
+
from: from || null,
|
|
525
|
+
reply_to: reply_to || null,
|
|
504
526
|
timestamp: new Date().toISOString()
|
|
505
527
|
});
|
|
506
528
|
busClients.forEach(client => {
|
|
507
529
|
if (client.readyState === 1) client.send(busMsg);
|
|
508
530
|
});
|
|
509
531
|
|
|
532
|
+
if (from && reply_to) {
|
|
533
|
+
const routedMsg = JSON.stringify({
|
|
534
|
+
type: 'message_routed',
|
|
535
|
+
message_id: inject_id,
|
|
536
|
+
from,
|
|
537
|
+
to: id,
|
|
538
|
+
reply_to,
|
|
539
|
+
inject_id,
|
|
540
|
+
deliberation_session_id: req.body.deliberation_session_id || null,
|
|
541
|
+
thread_id: req.body.thread_id || null,
|
|
542
|
+
timestamp: new Date().toISOString()
|
|
543
|
+
});
|
|
544
|
+
busClients.forEach(client => {
|
|
545
|
+
if (client.readyState === 1) client.send(routedMsg);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
510
549
|
res.json({ success: true, inject_id, submit: submitResult });
|
|
511
550
|
} catch (err) {
|
|
512
551
|
const busFailMsg = JSON.stringify({
|
|
@@ -593,6 +632,232 @@ app.post('/api/bus/publish', (req, res) => {
|
|
|
593
632
|
res.json({ success: true, delivered: deliveredCount });
|
|
594
633
|
});
|
|
595
634
|
|
|
635
|
+
app.post('/api/handoff', (req, res) => {
|
|
636
|
+
const { source_session_id, deliberation_id, synthesis, auto_execute } = req.body;
|
|
637
|
+
if (!synthesis) return res.status(400).json({ error: 'synthesis is required' });
|
|
638
|
+
|
|
639
|
+
const handoff_id = crypto.randomUUID();
|
|
640
|
+
const handoff = {
|
|
641
|
+
id: handoff_id,
|
|
642
|
+
source_session_id: source_session_id || null,
|
|
643
|
+
deliberation_id: deliberation_id || null,
|
|
644
|
+
synthesis,
|
|
645
|
+
status: 'pending',
|
|
646
|
+
auto_execute: !!auto_execute,
|
|
647
|
+
claimed_by: null,
|
|
648
|
+
created_at: new Date().toISOString(),
|
|
649
|
+
updated_at: new Date().toISOString(),
|
|
650
|
+
progress: [],
|
|
651
|
+
result: null
|
|
652
|
+
};
|
|
653
|
+
handoffs[handoff_id] = handoff;
|
|
654
|
+
|
|
655
|
+
const busMsg = JSON.stringify({
|
|
656
|
+
type: 'handoff.created',
|
|
657
|
+
handoff_id,
|
|
658
|
+
source_session_id: handoff.source_session_id,
|
|
659
|
+
deliberation_id: handoff.deliberation_id,
|
|
660
|
+
auto_execute: handoff.auto_execute,
|
|
661
|
+
task_count: Array.isArray(synthesis.tasks) ? synthesis.tasks.length : 0,
|
|
662
|
+
timestamp: handoff.created_at
|
|
663
|
+
});
|
|
664
|
+
busClients.forEach(client => {
|
|
665
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
console.log(`[HANDOFF] Created ${handoff_id} (${Array.isArray(synthesis.tasks) ? synthesis.tasks.length : 0} tasks)`);
|
|
669
|
+
res.status(201).json({ handoff_id, status: 'pending' });
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
app.get('/api/handoff', (req, res) => {
|
|
673
|
+
const status = req.query.status;
|
|
674
|
+
const list = Object.values(handoffs)
|
|
675
|
+
.filter(h => !status || h.status === status)
|
|
676
|
+
.map(h => ({
|
|
677
|
+
id: h.id,
|
|
678
|
+
status: h.status,
|
|
679
|
+
deliberation_id: h.deliberation_id,
|
|
680
|
+
source_session_id: h.source_session_id,
|
|
681
|
+
auto_execute: h.auto_execute,
|
|
682
|
+
claimed_by: h.claimed_by,
|
|
683
|
+
task_count: Array.isArray(h.synthesis.tasks) ? h.synthesis.tasks.length : 0,
|
|
684
|
+
created_at: h.created_at,
|
|
685
|
+
updated_at: h.updated_at
|
|
686
|
+
}));
|
|
687
|
+
res.json(list);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
app.get('/api/handoff/:id', (req, res) => {
|
|
691
|
+
const handoff = handoffs[req.params.id];
|
|
692
|
+
if (!handoff) return res.status(404).json({ error: 'Handoff not found' });
|
|
693
|
+
res.json(handoff);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
app.post('/api/handoff/:id/claim', (req, res) => {
|
|
697
|
+
const handoff = handoffs[req.params.id];
|
|
698
|
+
if (!handoff) return res.status(404).json({ error: 'Handoff not found' });
|
|
699
|
+
if (handoff.status !== 'pending') {
|
|
700
|
+
return res.status(409).json({ error: `Handoff already ${handoff.status}`, claimed_by: handoff.claimed_by });
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const { agent_session_id } = req.body;
|
|
704
|
+
if (!agent_session_id) return res.status(400).json({ error: 'agent_session_id is required' });
|
|
705
|
+
|
|
706
|
+
handoff.status = 'claimed';
|
|
707
|
+
handoff.claimed_by = agent_session_id;
|
|
708
|
+
handoff.updated_at = new Date().toISOString();
|
|
709
|
+
|
|
710
|
+
const busMsg = JSON.stringify({
|
|
711
|
+
type: 'handoff.claimed',
|
|
712
|
+
handoff_id: handoff.id,
|
|
713
|
+
agent_session_id,
|
|
714
|
+
timestamp: handoff.updated_at
|
|
715
|
+
});
|
|
716
|
+
busClients.forEach(client => {
|
|
717
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
console.log(`[HANDOFF] ${handoff.id} claimed by ${agent_session_id}`);
|
|
721
|
+
res.json({ success: true, handoff_id: handoff.id, status: 'claimed' });
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
app.patch('/api/handoff/:id', (req, res) => {
|
|
725
|
+
const handoff = handoffs[req.params.id];
|
|
726
|
+
if (!handoff) return res.status(404).json({ error: 'Handoff not found' });
|
|
727
|
+
|
|
728
|
+
const { status, message, result } = req.body;
|
|
729
|
+
const validTransitions = {
|
|
730
|
+
pending: ['claimed'],
|
|
731
|
+
claimed: ['executing', 'failed'],
|
|
732
|
+
executing: ['completed', 'failed'],
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
if (status) {
|
|
736
|
+
const allowed = validTransitions[handoff.status] || [];
|
|
737
|
+
if (!allowed.includes(status)) {
|
|
738
|
+
return res.status(400).json({ error: `Invalid transition: ${handoff.status} -> ${status}` });
|
|
739
|
+
}
|
|
740
|
+
handoff.status = status;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (message) {
|
|
744
|
+
handoff.progress.push({ message, timestamp: new Date().toISOString() });
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (result) {
|
|
748
|
+
handoff.result = result;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
handoff.updated_at = new Date().toISOString();
|
|
752
|
+
|
|
753
|
+
const busMsg = JSON.stringify({
|
|
754
|
+
type: `handoff.${handoff.status}`,
|
|
755
|
+
handoff_id: handoff.id,
|
|
756
|
+
claimed_by: handoff.claimed_by,
|
|
757
|
+
message: message || null,
|
|
758
|
+
timestamp: handoff.updated_at
|
|
759
|
+
});
|
|
760
|
+
busClients.forEach(client => {
|
|
761
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
console.log(`[HANDOFF] ${handoff.id} -> ${handoff.status}${message ? ': ' + message : ''}`);
|
|
765
|
+
res.json({ success: true, handoff_id: handoff.id, status: handoff.status });
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
// --- Deliberation Thread Tracking ---
|
|
769
|
+
|
|
770
|
+
app.post('/api/threads', (req, res) => {
|
|
771
|
+
const { topic, orchestrator_session_id, participant_session_ids, context } = req.body;
|
|
772
|
+
if (!topic) return res.status(400).json({ error: 'topic is required' });
|
|
773
|
+
|
|
774
|
+
const thread_id = crypto.randomUUID();
|
|
775
|
+
const thread = {
|
|
776
|
+
id: thread_id,
|
|
777
|
+
topic,
|
|
778
|
+
orchestrator_session_id: orchestrator_session_id || null,
|
|
779
|
+
participant_session_ids: participant_session_ids || [],
|
|
780
|
+
context: context || null,
|
|
781
|
+
status: 'active',
|
|
782
|
+
message_count: 0,
|
|
783
|
+
created_at: new Date().toISOString(),
|
|
784
|
+
updated_at: new Date().toISOString(),
|
|
785
|
+
closed_at: null
|
|
786
|
+
};
|
|
787
|
+
threads[thread_id] = thread;
|
|
788
|
+
|
|
789
|
+
const busMsg = JSON.stringify({
|
|
790
|
+
type: 'thread.opened',
|
|
791
|
+
thread_id,
|
|
792
|
+
topic,
|
|
793
|
+
orchestrator_session_id: thread.orchestrator_session_id,
|
|
794
|
+
participant_session_ids: thread.participant_session_ids,
|
|
795
|
+
timestamp: thread.created_at
|
|
796
|
+
});
|
|
797
|
+
busClients.forEach(client => {
|
|
798
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
console.log(`[THREAD] Opened ${thread_id}: "${topic}" (${thread.participant_session_ids.length} participants)`);
|
|
802
|
+
res.status(201).json({ thread_id, status: 'active' });
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
app.get('/api/threads', (req, res) => {
|
|
806
|
+
const status = req.query.status;
|
|
807
|
+
const list = Object.values(threads)
|
|
808
|
+
.filter(t => !status || t.status === status)
|
|
809
|
+
.map(t => ({
|
|
810
|
+
id: t.id,
|
|
811
|
+
topic: t.topic,
|
|
812
|
+
status: t.status,
|
|
813
|
+
orchestrator_session_id: t.orchestrator_session_id,
|
|
814
|
+
participant_count: t.participant_session_ids.length,
|
|
815
|
+
message_count: t.message_count,
|
|
816
|
+
created_at: t.created_at,
|
|
817
|
+
updated_at: t.updated_at
|
|
818
|
+
}));
|
|
819
|
+
res.json(list);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
app.get('/api/threads/:id', (req, res) => {
|
|
823
|
+
const thread = threads[req.params.id];
|
|
824
|
+
if (!thread) return res.status(404).json({ error: 'Thread not found' });
|
|
825
|
+
res.json(thread);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
app.patch('/api/threads/:id', (req, res) => {
|
|
829
|
+
const thread = threads[req.params.id];
|
|
830
|
+
if (!thread) return res.status(404).json({ error: 'Thread not found' });
|
|
831
|
+
|
|
832
|
+
const { status, message_count } = req.body;
|
|
833
|
+
|
|
834
|
+
if (status === 'closed' && thread.status === 'active') {
|
|
835
|
+
thread.status = 'closed';
|
|
836
|
+
thread.closed_at = new Date().toISOString();
|
|
837
|
+
thread.updated_at = thread.closed_at;
|
|
838
|
+
|
|
839
|
+
const busMsg = JSON.stringify({
|
|
840
|
+
type: 'thread.closed',
|
|
841
|
+
thread_id: thread.id,
|
|
842
|
+
topic: thread.topic,
|
|
843
|
+
message_count: thread.message_count,
|
|
844
|
+
timestamp: thread.closed_at
|
|
845
|
+
});
|
|
846
|
+
busClients.forEach(client => {
|
|
847
|
+
if (client.readyState === 1) client.send(busMsg);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
console.log(`[THREAD] Closed ${thread.id}: "${thread.topic}" (${thread.message_count} messages)`);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (typeof message_count === 'number') {
|
|
854
|
+
thread.message_count = message_count;
|
|
855
|
+
thread.updated_at = new Date().toISOString();
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
res.json({ success: true, thread_id: thread.id, status: thread.status });
|
|
859
|
+
});
|
|
860
|
+
|
|
596
861
|
const server = app.listen(PORT, HOST, () => {
|
|
597
862
|
console.log(`🚀 aigentry-telepty daemon listening on http://${HOST}:${PORT}`);
|
|
598
863
|
});
|