svamp-cli 0.1.69 → 0.1.71
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/dist/cli.mjs +19 -19
- package/dist/{commands-CF32XIau.mjs → commands-BLjcT1Vl.mjs} +2 -2
- package/dist/{commands-CNWZF0B9.mjs → commands-CheZ8rae.mjs} +2 -2
- package/dist/{commands-CgWkG45c.mjs → commands-DDqf8dQ_.mjs} +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{package-D2QVDgm_.mjs → package-CxTZatXO.mjs} +2 -2
- package/dist/{run-DqPEyYsf.mjs → run-6vD6-456.mjs} +2 -2
- package/dist/{run-DxmtlxnM.mjs → run-DGKMT1Af.mjs} +550 -363
- package/dist/{tunnel-C3UsqTxi.mjs → tunnel-C2kqST5d.mjs} +65 -31
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(import.meta.url);import os__default from 'os';
|
|
2
|
-
import fs, { mkdir as mkdir$1, readdir, readFile, writeFile, rename, unlink } from 'fs/promises';
|
|
2
|
+
import fs, { mkdir as mkdir$1, readdir, readFile, writeFile as writeFile$1, rename, unlink } from 'fs/promises';
|
|
3
3
|
import { readFileSync as readFileSync$1, mkdirSync, writeFileSync, renameSync, existsSync as existsSync$1, copyFileSync, unlinkSync, watch, rmdirSync } from 'fs';
|
|
4
4
|
import path, { join, dirname, resolve, basename } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
@@ -15,7 +15,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
|
15
15
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
16
16
|
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
17
17
|
import { z } from 'zod';
|
|
18
|
-
import { mkdir, rm, chmod, access, mkdtemp, copyFile } from 'node:fs/promises';
|
|
18
|
+
import { mkdir, rm, chmod, access, mkdtemp, copyFile, writeFile } from 'node:fs/promises';
|
|
19
19
|
import { promisify } from 'node:util';
|
|
20
20
|
|
|
21
21
|
let connectToServerFn = null;
|
|
@@ -45,6 +45,11 @@ async function connectToHypha(config) {
|
|
|
45
45
|
"Timeout connecting to Hypha server (30s). A previous daemon may still be connected. Retrying..."
|
|
46
46
|
)), 3e4))
|
|
47
47
|
]);
|
|
48
|
+
if (!server.on && server.rpc) {
|
|
49
|
+
server.on = server.rpc.on.bind(server.rpc);
|
|
50
|
+
server.off = server.rpc.off.bind(server.rpc);
|
|
51
|
+
server.emit = server.rpc.emit.bind(server.rpc);
|
|
52
|
+
}
|
|
48
53
|
return server;
|
|
49
54
|
}
|
|
50
55
|
function parseWorkspaceFromToken(token) {
|
|
@@ -378,6 +383,72 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
378
383
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
379
384
|
return handlers.getTrackedSessions();
|
|
380
385
|
},
|
|
386
|
+
/**
|
|
387
|
+
* Get summary info for all sessions (metadata, agent state, activity).
|
|
388
|
+
* Replaces the need to discover and query N individual session services.
|
|
389
|
+
*/
|
|
390
|
+
getSessions: async (context) => {
|
|
391
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
392
|
+
const sessionIds = handlers.getSessionIds?.() || [];
|
|
393
|
+
const sessions = [];
|
|
394
|
+
for (const sid of sessionIds) {
|
|
395
|
+
const rpc = handlers.getSessionRPCHandlers?.(sid);
|
|
396
|
+
if (!rpc) continue;
|
|
397
|
+
try {
|
|
398
|
+
const [metaResult, stateResult, activity] = await Promise.all([
|
|
399
|
+
rpc.getMetadata(context),
|
|
400
|
+
rpc.getAgentState(context),
|
|
401
|
+
rpc.getActivityState(context).catch(() => ({
|
|
402
|
+
active: false,
|
|
403
|
+
thinking: false,
|
|
404
|
+
time: Date.now()
|
|
405
|
+
}))
|
|
406
|
+
]);
|
|
407
|
+
sessions.push({
|
|
408
|
+
id: sid,
|
|
409
|
+
metadata: metaResult.metadata,
|
|
410
|
+
metadataVersion: metaResult.version,
|
|
411
|
+
agentState: stateResult.agentState,
|
|
412
|
+
agentStateVersion: stateResult.version,
|
|
413
|
+
active: activity.active ?? false,
|
|
414
|
+
thinking: activity.thinking ?? false,
|
|
415
|
+
activeAt: activity.time || Date.now()
|
|
416
|
+
});
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return sessions;
|
|
421
|
+
},
|
|
422
|
+
/**
|
|
423
|
+
* Dispatch an RPC call to a specific session's handler.
|
|
424
|
+
* This consolidates all session RPCs through the machine service,
|
|
425
|
+
* eliminating the need for per-session Hypha service registration.
|
|
426
|
+
*/
|
|
427
|
+
sessionRPC: async (sessionId, method, args, context) => {
|
|
428
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
429
|
+
const rpc = handlers.getSessionRPCHandlers?.(sessionId);
|
|
430
|
+
if (!rpc) {
|
|
431
|
+
throw new Error(`Session ${sessionId} not found on this machine`);
|
|
432
|
+
}
|
|
433
|
+
const handler = rpc[method];
|
|
434
|
+
if (typeof handler !== "function") {
|
|
435
|
+
throw new Error(`Unknown session method: ${method}`);
|
|
436
|
+
}
|
|
437
|
+
const argArray = Array.isArray(args) ? args : args !== void 0 ? [args] : [];
|
|
438
|
+
return await handler(...argArray, context);
|
|
439
|
+
},
|
|
440
|
+
/**
|
|
441
|
+
* Register a listener for a specific session's real-time updates.
|
|
442
|
+
* Delegates to the session store's registerListener.
|
|
443
|
+
*/
|
|
444
|
+
registerSessionListener: async (sessionId, callback, context) => {
|
|
445
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
446
|
+
const rpc = handlers.getSessionRPCHandlers?.(sessionId);
|
|
447
|
+
if (!rpc) {
|
|
448
|
+
throw new Error(`Session ${sessionId} not found on this machine`);
|
|
449
|
+
}
|
|
450
|
+
return await rpc.registerListener(callback, context);
|
|
451
|
+
},
|
|
381
452
|
// Spawn a new session
|
|
382
453
|
spawnSession: async (options, context) => {
|
|
383
454
|
authorizeRequest(context, currentMetadata.sharing, "interact");
|
|
@@ -771,6 +842,7 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
771
842
|
console.log(`[HYPHA MACHINE] Machine service registered: ${serviceInfo.id}`);
|
|
772
843
|
return {
|
|
773
844
|
serviceInfo,
|
|
845
|
+
notifySessionEvent: notifyListeners,
|
|
774
846
|
updateMetadata: (newMetadata) => {
|
|
775
847
|
currentMetadata = newMetadata;
|
|
776
848
|
metadataVersion++;
|
|
@@ -827,7 +899,7 @@ function appendMessage(messagesDir, sessionId, msg) {
|
|
|
827
899
|
console.error(`[HYPHA SESSION ${sessionId}] Failed to persist message: ${err?.message ?? err}`);
|
|
828
900
|
}
|
|
829
901
|
}
|
|
830
|
-
|
|
902
|
+
function createSessionStore(server, sessionId, initialMetadata, initialAgentState, callbacks, options) {
|
|
831
903
|
const messages = options?.messagesDir ? loadMessages(options.messagesDir) : [];
|
|
832
904
|
let nextSeq = messages.length > 0 ? messages[messages.length - 1].seq + 1 : 1;
|
|
833
905
|
let metadata = { ...initialMetadata };
|
|
@@ -854,6 +926,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
854
926
|
}
|
|
855
927
|
};
|
|
856
928
|
const notifyListeners = (update) => {
|
|
929
|
+
options?.onSessionEvent?.(update);
|
|
857
930
|
const snapshot = [...listeners];
|
|
858
931
|
for (let i = snapshot.length - 1; i >= 0; i--) {
|
|
859
932
|
const listener = snapshot[i];
|
|
@@ -880,7 +953,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
880
953
|
}
|
|
881
954
|
wrappedContent = { role: "agent", content: { type: "output", data } };
|
|
882
955
|
} else if (role === "event") {
|
|
883
|
-
wrappedContent = { role: "agent", content: { type: "event", data: content } };
|
|
956
|
+
wrappedContent = { role: "agent", content: { type: "event", id: randomUUID(), data: content } };
|
|
884
957
|
} else if (role === "session") {
|
|
885
958
|
wrappedContent = { role: "session", content: { type: "session", data: content } };
|
|
886
959
|
} else {
|
|
@@ -907,356 +980,348 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
907
980
|
});
|
|
908
981
|
return msg;
|
|
909
982
|
};
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
authorizeRequest(context, metadata.sharing, "interact");
|
|
930
|
-
if (localId) {
|
|
931
|
-
const existing = messages.find((m) => m.localId === localId);
|
|
932
|
-
if (existing) {
|
|
933
|
-
return { id: existing.id, seq: existing.seq, localId: existing.localId };
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
let parsed = content;
|
|
937
|
-
if (typeof parsed === "string") {
|
|
938
|
-
try {
|
|
939
|
-
parsed = JSON.parse(parsed);
|
|
940
|
-
} catch {
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
if (parsed && typeof parsed.content === "string" && !parsed.role) {
|
|
944
|
-
try {
|
|
945
|
-
const inner = JSON.parse(parsed.content);
|
|
946
|
-
if (inner && typeof inner === "object") parsed = inner;
|
|
947
|
-
} catch {
|
|
948
|
-
}
|
|
983
|
+
const rpcHandlers = {
|
|
984
|
+
// ── Messages ──
|
|
985
|
+
getMessages: async (afterSeq, limit, context) => {
|
|
986
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
987
|
+
const after = afterSeq ?? 0;
|
|
988
|
+
const lim = Math.min(limit ?? 100, 500);
|
|
989
|
+
const filtered = messages.filter((m) => m.seq > after);
|
|
990
|
+
const page = filtered.slice(0, lim);
|
|
991
|
+
return {
|
|
992
|
+
messages: page,
|
|
993
|
+
hasMore: filtered.length > lim
|
|
994
|
+
};
|
|
995
|
+
},
|
|
996
|
+
sendMessage: async (content, localId, meta, context) => {
|
|
997
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
998
|
+
if (localId) {
|
|
999
|
+
const existing = messages.find((m) => m.localId === localId);
|
|
1000
|
+
if (existing) {
|
|
1001
|
+
return { id: existing.id, seq: existing.seq, localId: existing.localId };
|
|
949
1002
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
createdAt: Date.now(),
|
|
957
|
-
updatedAt: Date.now()
|
|
958
|
-
};
|
|
959
|
-
messages.push(msg);
|
|
960
|
-
if (messages.length > 1e3) messages.splice(0, messages.length - 1e3);
|
|
961
|
-
if (options?.messagesDir) {
|
|
962
|
-
appendMessage(options.messagesDir, sessionId, msg);
|
|
1003
|
+
}
|
|
1004
|
+
let parsed = content;
|
|
1005
|
+
if (typeof parsed === "string") {
|
|
1006
|
+
try {
|
|
1007
|
+
parsed = JSON.parse(parsed);
|
|
1008
|
+
} catch {
|
|
963
1009
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
return { id: msg.id, seq: msg.seq, localId: msg.localId };
|
|
971
|
-
},
|
|
972
|
-
// ── Metadata ──
|
|
973
|
-
getMetadata: async (context) => {
|
|
974
|
-
authorizeRequest(context, metadata.sharing, "view");
|
|
975
|
-
return {
|
|
976
|
-
metadata,
|
|
977
|
-
version: metadataVersion
|
|
978
|
-
};
|
|
979
|
-
},
|
|
980
|
-
updateMetadata: async (newMetadata, expectedVersion, context) => {
|
|
981
|
-
authorizeRequest(context, metadata.sharing, "admin");
|
|
982
|
-
if (expectedVersion !== void 0 && expectedVersion !== metadataVersion) {
|
|
983
|
-
return {
|
|
984
|
-
result: "version-mismatch",
|
|
985
|
-
version: metadataVersion,
|
|
986
|
-
metadata
|
|
987
|
-
};
|
|
1010
|
+
}
|
|
1011
|
+
if (parsed && typeof parsed.content === "string" && !parsed.role) {
|
|
1012
|
+
try {
|
|
1013
|
+
const inner = JSON.parse(parsed.content);
|
|
1014
|
+
if (inner && typeof inner === "object") parsed = inner;
|
|
1015
|
+
} catch {
|
|
988
1016
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1017
|
+
}
|
|
1018
|
+
const wrappedContent = parsed && parsed.role === "user" ? { role: "user", content: parsed.content } : { role: "user", content: { type: "text", text: typeof parsed === "string" ? parsed : JSON.stringify(parsed) } };
|
|
1019
|
+
const msg = {
|
|
1020
|
+
id: randomUUID(),
|
|
1021
|
+
seq: nextSeq++,
|
|
1022
|
+
content: wrappedContent,
|
|
1023
|
+
localId: localId || randomUUID(),
|
|
1024
|
+
createdAt: Date.now(),
|
|
1025
|
+
updatedAt: Date.now()
|
|
1026
|
+
};
|
|
1027
|
+
messages.push(msg);
|
|
1028
|
+
if (messages.length > 1e3) messages.splice(0, messages.length - 1e3);
|
|
1029
|
+
if (options?.messagesDir) {
|
|
1030
|
+
appendMessage(options.messagesDir, sessionId, msg);
|
|
1031
|
+
}
|
|
1032
|
+
notifyListeners({
|
|
1033
|
+
type: "new-message",
|
|
1034
|
+
sessionId,
|
|
1035
|
+
message: msg
|
|
1036
|
+
});
|
|
1037
|
+
callbacks.onUserMessage(content, meta);
|
|
1038
|
+
return { id: msg.id, seq: msg.seq, localId: msg.localId };
|
|
1039
|
+
},
|
|
1040
|
+
// ── Metadata ──
|
|
1041
|
+
getMetadata: async (context) => {
|
|
1042
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1043
|
+
return {
|
|
1044
|
+
metadata,
|
|
1045
|
+
version: metadataVersion
|
|
1046
|
+
};
|
|
1047
|
+
},
|
|
1048
|
+
updateMetadata: async (newMetadata, expectedVersion, context) => {
|
|
1049
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1050
|
+
if (expectedVersion !== void 0 && expectedVersion !== metadataVersion) {
|
|
997
1051
|
return {
|
|
998
|
-
result: "
|
|
1052
|
+
result: "version-mismatch",
|
|
999
1053
|
version: metadataVersion,
|
|
1000
1054
|
metadata
|
|
1001
1055
|
};
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1056
|
+
}
|
|
1057
|
+
metadata = newMetadata;
|
|
1058
|
+
metadataVersion++;
|
|
1059
|
+
notifyListeners({
|
|
1060
|
+
type: "update-session",
|
|
1061
|
+
sessionId,
|
|
1062
|
+
metadata: { value: metadata, version: metadataVersion }
|
|
1063
|
+
});
|
|
1064
|
+
callbacks.onMetadataUpdate?.(metadata);
|
|
1065
|
+
return {
|
|
1066
|
+
result: "success",
|
|
1067
|
+
version: metadataVersion,
|
|
1068
|
+
metadata
|
|
1069
|
+
};
|
|
1070
|
+
},
|
|
1071
|
+
/**
|
|
1072
|
+
* Patch the session config file (.svamp/{sessionId}/config.json).
|
|
1073
|
+
* Used by the frontend to set title, session_link, ralph_loop, etc.
|
|
1074
|
+
* Null values remove keys from the config.
|
|
1075
|
+
*/
|
|
1076
|
+
updateConfig: async (patch, context) => {
|
|
1077
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1078
|
+
callbacks.onUpdateConfig?.(patch);
|
|
1079
|
+
return { success: true };
|
|
1080
|
+
},
|
|
1081
|
+
// ── Agent State ──
|
|
1082
|
+
getAgentState: async (context) => {
|
|
1083
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1084
|
+
return {
|
|
1085
|
+
agentState,
|
|
1086
|
+
version: agentStateVersion
|
|
1087
|
+
};
|
|
1088
|
+
},
|
|
1089
|
+
updateAgentState: async (newState, expectedVersion, context) => {
|
|
1090
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1091
|
+
if (expectedVersion !== void 0 && expectedVersion !== agentStateVersion) {
|
|
1037
1092
|
return {
|
|
1038
|
-
result: "
|
|
1093
|
+
result: "version-mismatch",
|
|
1039
1094
|
version: agentStateVersion,
|
|
1040
1095
|
agentState
|
|
1041
1096
|
};
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1097
|
+
}
|
|
1098
|
+
agentState = newState;
|
|
1099
|
+
agentStateVersion++;
|
|
1100
|
+
notifyListeners({
|
|
1101
|
+
type: "update-session",
|
|
1102
|
+
sessionId,
|
|
1103
|
+
agentState: { value: agentState, version: agentStateVersion }
|
|
1104
|
+
});
|
|
1105
|
+
return {
|
|
1106
|
+
result: "success",
|
|
1107
|
+
version: agentStateVersion,
|
|
1108
|
+
agentState
|
|
1109
|
+
};
|
|
1110
|
+
},
|
|
1111
|
+
// ── Session Control RPCs ──
|
|
1112
|
+
abort: async (context) => {
|
|
1113
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1114
|
+
callbacks.onAbort();
|
|
1115
|
+
return { success: true };
|
|
1116
|
+
},
|
|
1117
|
+
permissionResponse: async (params, context) => {
|
|
1118
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1119
|
+
callbacks.onPermissionResponse(params);
|
|
1120
|
+
return { success: true };
|
|
1121
|
+
},
|
|
1122
|
+
switchMode: async (mode, context) => {
|
|
1123
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1124
|
+
callbacks.onSwitchMode(mode);
|
|
1125
|
+
return { success: true };
|
|
1126
|
+
},
|
|
1127
|
+
restartClaude: async (context) => {
|
|
1128
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1129
|
+
return await callbacks.onRestartClaude();
|
|
1130
|
+
},
|
|
1131
|
+
killSession: async (context) => {
|
|
1132
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1133
|
+
callbacks.onKillSession();
|
|
1134
|
+
return { success: true };
|
|
1135
|
+
},
|
|
1136
|
+
// ── Activity ──
|
|
1137
|
+
keepAlive: async (thinking, mode, context) => {
|
|
1138
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1139
|
+
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
1140
|
+
notifyListeners({
|
|
1141
|
+
type: "activity",
|
|
1142
|
+
sessionId,
|
|
1143
|
+
...lastActivity
|
|
1144
|
+
});
|
|
1145
|
+
},
|
|
1146
|
+
sessionEnd: async (context) => {
|
|
1147
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1148
|
+
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
1149
|
+
notifyListeners({
|
|
1150
|
+
type: "activity",
|
|
1151
|
+
sessionId,
|
|
1152
|
+
...lastActivity
|
|
1153
|
+
});
|
|
1154
|
+
},
|
|
1155
|
+
// ── Activity State Query ──
|
|
1156
|
+
getActivityState: async (context) => {
|
|
1157
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1158
|
+
const pendingPermissions = agentState?.requests ? Object.entries(agentState.requests).filter(([, req]) => req.status === "pending" || !req.status).map(([id, req]) => ({
|
|
1159
|
+
id,
|
|
1160
|
+
tool: req.tool,
|
|
1161
|
+
arguments: req.arguments,
|
|
1162
|
+
createdAt: req.createdAt
|
|
1163
|
+
})) : [];
|
|
1164
|
+
return { ...lastActivity, sessionId, pendingPermissions };
|
|
1165
|
+
},
|
|
1166
|
+
// ── File Operations (optional, admin-only) ──
|
|
1167
|
+
readFile: async (path, context) => {
|
|
1168
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1169
|
+
if (!callbacks.onReadFile) throw new Error("readFile not supported");
|
|
1170
|
+
return await callbacks.onReadFile(path);
|
|
1171
|
+
},
|
|
1172
|
+
writeFile: async (path, content, context) => {
|
|
1173
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1174
|
+
if (!callbacks.onWriteFile) throw new Error("writeFile not supported");
|
|
1175
|
+
await callbacks.onWriteFile(path, content);
|
|
1176
|
+
return { success: true };
|
|
1177
|
+
},
|
|
1178
|
+
listDirectory: async (path, context) => {
|
|
1179
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1180
|
+
if (!callbacks.onListDirectory) throw new Error("listDirectory not supported");
|
|
1181
|
+
return await callbacks.onListDirectory(path);
|
|
1182
|
+
},
|
|
1183
|
+
bash: async (command, cwd, context) => {
|
|
1184
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1185
|
+
if (!callbacks.onBash) throw new Error("bash not supported");
|
|
1186
|
+
return await callbacks.onBash(command, cwd);
|
|
1187
|
+
},
|
|
1188
|
+
ripgrep: async (args, cwd, context) => {
|
|
1189
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1190
|
+
if (!callbacks.onRipgrep) throw new Error("ripgrep not supported");
|
|
1191
|
+
try {
|
|
1192
|
+
const stdout = await callbacks.onRipgrep(args, cwd);
|
|
1193
|
+
return { success: true, stdout, stderr: "", exitCode: 0 };
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
return { success: false, stdout: "", stderr: err.message || "", exitCode: 1, error: err.message };
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
getDirectoryTree: async (path, maxDepth, context) => {
|
|
1199
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1200
|
+
if (!callbacks.onGetDirectoryTree) throw new Error("getDirectoryTree not supported");
|
|
1201
|
+
return await callbacks.onGetDirectoryTree(path, maxDepth ?? 3);
|
|
1202
|
+
},
|
|
1203
|
+
// ── Sharing Management ──
|
|
1204
|
+
getSharing: async (context) => {
|
|
1205
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1206
|
+
return { sharing: metadata.sharing || null };
|
|
1207
|
+
},
|
|
1208
|
+
/** Returns the caller's effective role (null if no access). Does not throw. */
|
|
1209
|
+
getEffectiveRole: async (context) => {
|
|
1210
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1211
|
+
const role = getEffectiveRole(context, metadata.sharing);
|
|
1212
|
+
return { role };
|
|
1213
|
+
},
|
|
1214
|
+
updateSharing: async (newSharing, context) => {
|
|
1215
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1216
|
+
if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
|
|
1217
|
+
throw new Error("Only the session owner can update sharing settings");
|
|
1218
|
+
}
|
|
1219
|
+
if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
|
|
1220
|
+
newSharing = { ...newSharing, owner: context.user.email };
|
|
1221
|
+
}
|
|
1222
|
+
metadata = { ...metadata, sharing: newSharing };
|
|
1223
|
+
metadataVersion++;
|
|
1224
|
+
notifyListeners({
|
|
1225
|
+
type: "update-session",
|
|
1226
|
+
sessionId,
|
|
1227
|
+
metadata: { value: metadata, version: metadataVersion }
|
|
1228
|
+
});
|
|
1229
|
+
callbacks.onSharingUpdate?.(newSharing);
|
|
1230
|
+
return { success: true, sharing: newSharing };
|
|
1231
|
+
},
|
|
1232
|
+
/** Update security context and restart the agent process with new rules */
|
|
1233
|
+
updateSecurityContext: async (newSecurityContext, context) => {
|
|
1234
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1235
|
+
if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
|
|
1236
|
+
throw new Error("Only the session owner can update security context");
|
|
1237
|
+
}
|
|
1238
|
+
if (!callbacks.onUpdateSecurityContext) {
|
|
1239
|
+
throw new Error("Security context updates are not supported for this session");
|
|
1240
|
+
}
|
|
1241
|
+
metadata = { ...metadata, securityContext: newSecurityContext };
|
|
1242
|
+
metadataVersion++;
|
|
1243
|
+
notifyListeners({
|
|
1244
|
+
type: "update-session",
|
|
1245
|
+
sessionId,
|
|
1246
|
+
metadata: { value: metadata, version: metadataVersion }
|
|
1247
|
+
});
|
|
1248
|
+
return await callbacks.onUpdateSecurityContext(newSecurityContext);
|
|
1249
|
+
},
|
|
1250
|
+
/** Apply a new system prompt and restart the agent process */
|
|
1251
|
+
applySystemPrompt: async (prompt, context) => {
|
|
1252
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1253
|
+
if (!callbacks.onApplySystemPrompt) {
|
|
1254
|
+
throw new Error("System prompt updates are not supported for this session");
|
|
1255
|
+
}
|
|
1256
|
+
return await callbacks.onApplySystemPrompt(prompt);
|
|
1257
|
+
},
|
|
1258
|
+
// ── Listener Registration ──
|
|
1259
|
+
registerListener: async (callback, context) => {
|
|
1260
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1261
|
+
listeners.push(callback);
|
|
1262
|
+
const replayMessages = messages.slice(-50);
|
|
1263
|
+
const REPLAY_MESSAGE_TIMEOUT_MS = 1e4;
|
|
1264
|
+
for (const msg of replayMessages) {
|
|
1265
|
+
if (listeners.indexOf(callback) < 0) break;
|
|
1123
1266
|
try {
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1267
|
+
const result = callback.onUpdate({
|
|
1268
|
+
type: "new-message",
|
|
1269
|
+
sessionId,
|
|
1270
|
+
message: msg
|
|
1271
|
+
});
|
|
1272
|
+
if (result && typeof result.catch === "function") {
|
|
1273
|
+
try {
|
|
1274
|
+
await Promise.race([
|
|
1275
|
+
result,
|
|
1276
|
+
new Promise(
|
|
1277
|
+
(_, reject) => setTimeout(() => reject(new Error("Replay message timeout")), REPLAY_MESSAGE_TIMEOUT_MS)
|
|
1278
|
+
)
|
|
1279
|
+
]);
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
1282
|
+
removeListener(callback, "replay error");
|
|
1283
|
+
return { success: false, error: "Listener removed during replay" };
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1126
1286
|
} catch (err) {
|
|
1127
|
-
|
|
1287
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
1288
|
+
removeListener(callback, "replay error");
|
|
1289
|
+
return { success: false, error: "Listener removed during replay" };
|
|
1128
1290
|
}
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
// ── Sharing Management ──
|
|
1136
|
-
getSharing: async (context) => {
|
|
1137
|
-
authorizeRequest(context, metadata.sharing, "view");
|
|
1138
|
-
return { sharing: metadata.sharing || null };
|
|
1139
|
-
},
|
|
1140
|
-
/** Returns the caller's effective role (null if no access). Does not throw. */
|
|
1141
|
-
getEffectiveRole: async (context) => {
|
|
1142
|
-
authorizeRequest(context, metadata.sharing, "view");
|
|
1143
|
-
const role = getEffectiveRole(context, metadata.sharing);
|
|
1144
|
-
return { role };
|
|
1145
|
-
},
|
|
1146
|
-
updateSharing: async (newSharing, context) => {
|
|
1147
|
-
authorizeRequest(context, metadata.sharing, "admin");
|
|
1148
|
-
if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
|
|
1149
|
-
throw new Error("Only the session owner can update sharing settings");
|
|
1150
|
-
}
|
|
1151
|
-
if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
|
|
1152
|
-
newSharing = { ...newSharing, owner: context.user.email };
|
|
1153
|
-
}
|
|
1154
|
-
metadata = { ...metadata, sharing: newSharing };
|
|
1155
|
-
metadataVersion++;
|
|
1156
|
-
notifyListeners({
|
|
1291
|
+
}
|
|
1292
|
+
if (listeners.indexOf(callback) < 0) {
|
|
1293
|
+
return { success: false, error: "Listener was removed during replay" };
|
|
1294
|
+
}
|
|
1295
|
+
try {
|
|
1296
|
+
const result = callback.onUpdate({
|
|
1157
1297
|
type: "update-session",
|
|
1158
1298
|
sessionId,
|
|
1159
|
-
metadata: { value: metadata, version: metadataVersion }
|
|
1299
|
+
metadata: { value: metadata, version: metadataVersion },
|
|
1300
|
+
agentState: { value: agentState, version: agentStateVersion }
|
|
1160
1301
|
});
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
/** Update security context and restart the agent process with new rules */
|
|
1165
|
-
updateSecurityContext: async (newSecurityContext, context) => {
|
|
1166
|
-
authorizeRequest(context, metadata.sharing, "admin");
|
|
1167
|
-
if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
|
|
1168
|
-
throw new Error("Only the session owner can update security context");
|
|
1169
|
-
}
|
|
1170
|
-
if (!callbacks.onUpdateSecurityContext) {
|
|
1171
|
-
throw new Error("Security context updates are not supported for this session");
|
|
1302
|
+
if (result && typeof result.catch === "function") {
|
|
1303
|
+
result.catch(() => {
|
|
1304
|
+
});
|
|
1172
1305
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1306
|
+
} catch {
|
|
1307
|
+
}
|
|
1308
|
+
try {
|
|
1309
|
+
const result = callback.onUpdate({
|
|
1310
|
+
type: "activity",
|
|
1177
1311
|
sessionId,
|
|
1178
|
-
|
|
1312
|
+
...lastActivity
|
|
1179
1313
|
});
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
/** Apply a new system prompt and restart the agent process */
|
|
1183
|
-
applySystemPrompt: async (prompt, context) => {
|
|
1184
|
-
authorizeRequest(context, metadata.sharing, "admin");
|
|
1185
|
-
if (!callbacks.onApplySystemPrompt) {
|
|
1186
|
-
throw new Error("System prompt updates are not supported for this session");
|
|
1187
|
-
}
|
|
1188
|
-
return await callbacks.onApplySystemPrompt(prompt);
|
|
1189
|
-
},
|
|
1190
|
-
// ── Listener Registration ──
|
|
1191
|
-
registerListener: async (callback, context) => {
|
|
1192
|
-
authorizeRequest(context, metadata.sharing, "view");
|
|
1193
|
-
listeners.push(callback);
|
|
1194
|
-
const replayMessages = messages.slice(-50);
|
|
1195
|
-
const REPLAY_MESSAGE_TIMEOUT_MS = 1e4;
|
|
1196
|
-
for (const msg of replayMessages) {
|
|
1197
|
-
if (listeners.indexOf(callback) < 0) break;
|
|
1198
|
-
try {
|
|
1199
|
-
const result = callback.onUpdate({
|
|
1200
|
-
type: "new-message",
|
|
1201
|
-
sessionId,
|
|
1202
|
-
message: msg
|
|
1203
|
-
});
|
|
1204
|
-
if (result && typeof result.catch === "function") {
|
|
1205
|
-
try {
|
|
1206
|
-
await Promise.race([
|
|
1207
|
-
result,
|
|
1208
|
-
new Promise(
|
|
1209
|
-
(_, reject) => setTimeout(() => reject(new Error("Replay message timeout")), REPLAY_MESSAGE_TIMEOUT_MS)
|
|
1210
|
-
)
|
|
1211
|
-
]);
|
|
1212
|
-
} catch (err) {
|
|
1213
|
-
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
1214
|
-
removeListener(callback, "replay error");
|
|
1215
|
-
return { success: false, error: "Listener removed during replay" };
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
} catch (err) {
|
|
1219
|
-
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
1220
|
-
removeListener(callback, "replay error");
|
|
1221
|
-
return { success: false, error: "Listener removed during replay" };
|
|
1222
|
-
}
|
|
1223
|
-
}
|
|
1224
|
-
if (listeners.indexOf(callback) < 0) {
|
|
1225
|
-
return { success: false, error: "Listener was removed during replay" };
|
|
1226
|
-
}
|
|
1227
|
-
try {
|
|
1228
|
-
const result = callback.onUpdate({
|
|
1229
|
-
type: "update-session",
|
|
1230
|
-
sessionId,
|
|
1231
|
-
metadata: { value: metadata, version: metadataVersion },
|
|
1232
|
-
agentState: { value: agentState, version: agentStateVersion }
|
|
1233
|
-
});
|
|
1234
|
-
if (result && typeof result.catch === "function") {
|
|
1235
|
-
result.catch(() => {
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
} catch {
|
|
1239
|
-
}
|
|
1240
|
-
try {
|
|
1241
|
-
const result = callback.onUpdate({
|
|
1242
|
-
type: "activity",
|
|
1243
|
-
sessionId,
|
|
1244
|
-
...lastActivity
|
|
1314
|
+
if (result && typeof result.catch === "function") {
|
|
1315
|
+
result.catch(() => {
|
|
1245
1316
|
});
|
|
1246
|
-
if (result && typeof result.catch === "function") {
|
|
1247
|
-
result.catch(() => {
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
} catch {
|
|
1251
1317
|
}
|
|
1252
|
-
|
|
1318
|
+
} catch {
|
|
1253
1319
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
serviceInfo,
|
|
1320
|
+
return { success: true, listenerId: listeners.length - 1 };
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
const store = {
|
|
1324
|
+
serviceInfo: { id: `svamp-session-${sessionId}` },
|
|
1260
1325
|
pushMessage,
|
|
1261
1326
|
get _agentState() {
|
|
1262
1327
|
return agentState;
|
|
@@ -1315,9 +1380,44 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1315
1380
|
for (const listener of toRemove) {
|
|
1316
1381
|
removeListener(listener, "disconnect");
|
|
1317
1382
|
}
|
|
1318
|
-
|
|
1383
|
+
},
|
|
1384
|
+
reregister: async () => {
|
|
1319
1385
|
}
|
|
1320
1386
|
};
|
|
1387
|
+
return { store, rpcHandlers };
|
|
1388
|
+
}
|
|
1389
|
+
async function registerSessionService(server, sessionId, initialMetadata, initialAgentState, callbacks, options) {
|
|
1390
|
+
const { store, rpcHandlers } = createSessionStore(
|
|
1391
|
+
server,
|
|
1392
|
+
sessionId,
|
|
1393
|
+
initialMetadata,
|
|
1394
|
+
initialAgentState,
|
|
1395
|
+
callbacks,
|
|
1396
|
+
options
|
|
1397
|
+
);
|
|
1398
|
+
const serviceDefinition = {
|
|
1399
|
+
id: `svamp-session-${sessionId}`,
|
|
1400
|
+
name: `Svamp Session ${sessionId.slice(0, 8)}`,
|
|
1401
|
+
type: "svamp-session",
|
|
1402
|
+
config: { visibility: "unlisted", require_context: true },
|
|
1403
|
+
...rpcHandlers
|
|
1404
|
+
};
|
|
1405
|
+
const serviceInfo = await server.registerService(serviceDefinition, { overwrite: true });
|
|
1406
|
+
console.log(`[HYPHA SESSION] Session service registered: ${serviceInfo.id}`);
|
|
1407
|
+
const originalDisconnect = store.disconnect;
|
|
1408
|
+
store.disconnect = async () => {
|
|
1409
|
+
await originalDisconnect();
|
|
1410
|
+
await server.unregisterService(serviceInfo.id);
|
|
1411
|
+
};
|
|
1412
|
+
store.reregister = async () => {
|
|
1413
|
+
try {
|
|
1414
|
+
await server.registerService(serviceDefinition, { overwrite: true });
|
|
1415
|
+
} catch (e) {
|
|
1416
|
+
if (!String(e?.message).includes("already exists")) throw e;
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
store.serviceInfo = serviceInfo;
|
|
1420
|
+
return store;
|
|
1321
1421
|
}
|
|
1322
1422
|
|
|
1323
1423
|
async function registerDebugService(server, machineId, deps) {
|
|
@@ -1796,6 +1896,10 @@ function wrapWithNono(command, args, config) {
|
|
|
1796
1896
|
if (existsSync(realLocalDir)) {
|
|
1797
1897
|
nonoArgs.push("--read", realLocalDir);
|
|
1798
1898
|
}
|
|
1899
|
+
const realKeychainDir = join$1(homedir(), "Library", "Keychains");
|
|
1900
|
+
if (existsSync(realKeychainDir)) {
|
|
1901
|
+
nonoArgs.push("--read", realKeychainDir);
|
|
1902
|
+
}
|
|
1799
1903
|
}
|
|
1800
1904
|
if (config.nonoConfig?.allowDirs) {
|
|
1801
1905
|
for (const dir of config.nonoConfig.allowDirs) {
|
|
@@ -3886,9 +3990,8 @@ async function stageCredentialsForSharing(sessionId) {
|
|
|
3886
3990
|
const realHome = homedir();
|
|
3887
3991
|
const realClaudeDir = join$1(realHome, ".claude");
|
|
3888
3992
|
await mkdir(STAGED_HOMES_DIR, { recursive: true });
|
|
3889
|
-
const tmpHome =
|
|
3890
|
-
|
|
3891
|
-
);
|
|
3993
|
+
const tmpHome = join$1(STAGED_HOMES_DIR, sessionId);
|
|
3994
|
+
await mkdir(tmpHome, { recursive: true });
|
|
3892
3995
|
const stagedClaudeDir = join$1(tmpHome, ".claude");
|
|
3893
3996
|
await mkdir(stagedClaudeDir, { recursive: true });
|
|
3894
3997
|
const credentialFiles = ["credentials.json", ".credentials.json"];
|
|
@@ -3909,10 +4012,12 @@ async function stageCredentialsForSharing(sessionId) {
|
|
|
3909
4012
|
);
|
|
3910
4013
|
} catch {
|
|
3911
4014
|
}
|
|
3912
|
-
const
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
4015
|
+
const claudeJsonPath = join$1(tmpHome, ".claude.json");
|
|
4016
|
+
if (!existsSync(claudeJsonPath)) {
|
|
4017
|
+
try {
|
|
4018
|
+
await writeFile(claudeJsonPath, "{}");
|
|
4019
|
+
} catch {
|
|
4020
|
+
}
|
|
3916
4021
|
}
|
|
3917
4022
|
return {
|
|
3918
4023
|
homePath: tmpHome,
|
|
@@ -4154,7 +4259,7 @@ class ProcessSupervisor {
|
|
|
4154
4259
|
async persistSpec(spec) {
|
|
4155
4260
|
const filePath = path.join(this.persistDir, `${spec.id}.json`);
|
|
4156
4261
|
const tmpPath = filePath + ".tmp";
|
|
4157
|
-
await writeFile(tmpPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
4262
|
+
await writeFile$1(tmpPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
4158
4263
|
await rename(tmpPath, filePath);
|
|
4159
4264
|
}
|
|
4160
4265
|
async deleteSpec(id) {
|
|
@@ -5313,6 +5418,7 @@ async function startDaemon(options) {
|
|
|
5313
5418
|
serverUrl: hyphaServerUrl,
|
|
5314
5419
|
token: hyphaToken,
|
|
5315
5420
|
name: `svamp-machine-${machineId}`,
|
|
5421
|
+
transport: "http",
|
|
5316
5422
|
...hyphaClientId ? { clientId: hyphaClientId } : {}
|
|
5317
5423
|
});
|
|
5318
5424
|
logger.log(`Connected to Hypha (workspace: ${server.config.workspace})`);
|
|
@@ -5320,6 +5426,7 @@ async function startDaemon(options) {
|
|
|
5320
5426
|
logger.log(`Hypha connection permanently lost: ${reason}`);
|
|
5321
5427
|
requestShutdown("hypha-disconnected", String(reason));
|
|
5322
5428
|
});
|
|
5429
|
+
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
5323
5430
|
server.on("services_registered", () => {
|
|
5324
5431
|
if (consecutiveHeartbeatFailures > 0) {
|
|
5325
5432
|
logger.log(`Hypha reconnection successful \u2014 services re-registered (resetting ${consecutiveHeartbeatFailures} failures)`);
|
|
@@ -5327,7 +5434,6 @@ async function startDaemon(options) {
|
|
|
5327
5434
|
lastReconnectAt = Date.now();
|
|
5328
5435
|
}
|
|
5329
5436
|
});
|
|
5330
|
-
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
5331
5437
|
const getCurrentChildren = () => {
|
|
5332
5438
|
return Array.from(pidToTrackedSession.values()).map((s) => ({
|
|
5333
5439
|
sessionId: s.svampSessionId || `PID-${s.pid}`,
|
|
@@ -5337,6 +5443,7 @@ async function startDaemon(options) {
|
|
|
5337
5443
|
active: !s.stopped && s.hyphaService != null
|
|
5338
5444
|
}));
|
|
5339
5445
|
};
|
|
5446
|
+
let machineServiceRef = null;
|
|
5340
5447
|
const spawnSession = async (options2) => {
|
|
5341
5448
|
logger.log("Spawning session:", JSON.stringify(options2));
|
|
5342
5449
|
const { directory, approvedNewDirectoryCreation = true, resumeSessionId } = options2;
|
|
@@ -5479,6 +5586,9 @@ async function startDaemon(options) {
|
|
|
5479
5586
|
let sessionWasProcessing = !!options2.wasProcessing;
|
|
5480
5587
|
let lastAssistantText = "";
|
|
5481
5588
|
let spawnHasReceivedInit = false;
|
|
5589
|
+
let startupFailureRetryPending = false;
|
|
5590
|
+
let startupRetryMessage;
|
|
5591
|
+
let startupNonJsonLines = [];
|
|
5482
5592
|
const signalProcessing = (processing) => {
|
|
5483
5593
|
sessionService.sendKeepAlive(processing);
|
|
5484
5594
|
const newState = processing ? "running" : "idle";
|
|
@@ -5529,6 +5639,8 @@ async function startDaemon(options) {
|
|
|
5529
5639
|
let isolationCleanupFiles = [];
|
|
5530
5640
|
const spawnClaude = (initialMessage, meta) => {
|
|
5531
5641
|
const effectiveMeta = { ...lastSpawnMeta, ...meta };
|
|
5642
|
+
startupNonJsonLines = [];
|
|
5643
|
+
startupRetryMessage = initialMessage;
|
|
5532
5644
|
let rawPermissionMode = effectiveMeta.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
|
|
5533
5645
|
if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
|
|
5534
5646
|
rawPermissionMode = rawPermissionMode === "default" ? "auto-approve-all" : rawPermissionMode;
|
|
@@ -5724,23 +5836,36 @@ async function startDaemon(options) {
|
|
|
5724
5836
|
if (msg.is_error) {
|
|
5725
5837
|
const resultText = msg.result || "";
|
|
5726
5838
|
logger.error(`[Session ${sessionId}] Claude error (is_error=true, api_ms=${msg.duration_api_ms}): "${resultText}"`);
|
|
5727
|
-
const
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
hint = "\n\nRun `claude login` in your terminal on the machine running the daemon to re-authenticate.";
|
|
5733
|
-
} else if (isResumeIssue) {
|
|
5734
|
-
hint = "\n\nThe conversation history may be corrupted. Try starting a fresh session.";
|
|
5839
|
+
const isStartupFailure = msg.duration_api_ms === 0 && msg.num_turns === 0;
|
|
5840
|
+
if (isStartupFailure && !startupFailureRetryPending) {
|
|
5841
|
+
logger.log(`[Session ${sessionId}] Startup failure detected \u2014 scheduling silent retry without --resume`);
|
|
5842
|
+
startupFailureRetryPending = true;
|
|
5843
|
+
lastErrorMessagePushed = true;
|
|
5735
5844
|
} else {
|
|
5736
|
-
|
|
5845
|
+
const lower = resultText.toLowerCase();
|
|
5846
|
+
const isLoginIssue = lower.includes("login") || lower.includes("logged in") || lower.includes("auth") || lower.includes("api key") || lower.includes("unauthorized");
|
|
5847
|
+
const isResumeIssue = lower.includes("tool_use.name") || lower.includes("invalid_request") || lower.includes("messages.");
|
|
5848
|
+
const isBillingIssue = lower.includes("credit") || lower.includes("balance") || lower.includes("billing") || lower.includes("quota") || lower.includes("subscription") || lower.includes("payment");
|
|
5849
|
+
let hint = "";
|
|
5850
|
+
if (isBillingIssue) {
|
|
5851
|
+
hint = "\n\nCheck your Claude account credits or subscription at https://console.anthropic.com.";
|
|
5852
|
+
} else if (isLoginIssue) {
|
|
5853
|
+
hint = "\n\nRun `claude login` in your terminal on the machine running the daemon to re-authenticate.";
|
|
5854
|
+
} else if (isResumeIssue) {
|
|
5855
|
+
hint = "\n\nThe conversation history may be corrupted. Try starting a fresh session.";
|
|
5856
|
+
} else {
|
|
5857
|
+
hint = "\n\nCheck that the Claude Code CLI is properly installed and configured.";
|
|
5858
|
+
}
|
|
5859
|
+
const displayMsg = resultText || "Claude Code exited with an error.";
|
|
5860
|
+
let contextInfo = "";
|
|
5861
|
+
if (startupNonJsonLines.length > 0) {
|
|
5862
|
+
contextInfo = "\n\n**Startup output:**\n```\n" + startupNonJsonLines.slice(-10).join("\n") + "\n```";
|
|
5863
|
+
}
|
|
5864
|
+
const errorText = `${displayMsg}${hint}${contextInfo}`;
|
|
5865
|
+
logger.log(`[Session ${sessionId}] Pushing error to UI: "${displayMsg}"`);
|
|
5866
|
+
sessionService.pushMessage({ type: "message", message: errorText, level: "error" }, "event");
|
|
5867
|
+
lastErrorMessagePushed = true;
|
|
5737
5868
|
}
|
|
5738
|
-
const displayMsg = resultText || "Claude Code exited with an error.";
|
|
5739
|
-
sessionService.pushMessage({
|
|
5740
|
-
type: "assistant",
|
|
5741
|
-
content: [{ type: "text", text: `**Error:** ${displayMsg}${hint}` }]
|
|
5742
|
-
}, "agent");
|
|
5743
|
-
lastErrorMessagePushed = true;
|
|
5744
5869
|
}
|
|
5745
5870
|
}
|
|
5746
5871
|
if (msg.type === "result") {
|
|
@@ -5955,6 +6080,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5955
6080
|
const isResumeFailure = !spawnHasReceivedInit && claudeResumeId && msg.session_id !== claudeResumeId;
|
|
5956
6081
|
const isConversationClear = spawnHasReceivedInit && claudeResumeId && msg.session_id !== claudeResumeId;
|
|
5957
6082
|
spawnHasReceivedInit = true;
|
|
6083
|
+
startupFailureRetryPending = false;
|
|
5958
6084
|
claudeResumeId = msg.session_id;
|
|
5959
6085
|
sessionMetadata = { ...sessionMetadata, claudeSessionId: msg.session_id };
|
|
5960
6086
|
sessionService.updateMetadata(sessionMetadata);
|
|
@@ -5996,6 +6122,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5996
6122
|
}
|
|
5997
6123
|
} catch {
|
|
5998
6124
|
logger.log(`[Session ${sessionId}] Claude stdout (non-JSON): ${line}`);
|
|
6125
|
+
if (!spawnHasReceivedInit) {
|
|
6126
|
+
startupNonJsonLines.push(line.slice(0, 500));
|
|
6127
|
+
}
|
|
5999
6128
|
}
|
|
6000
6129
|
}
|
|
6001
6130
|
});
|
|
@@ -6040,6 +6169,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6040
6169
|
sessionMetadata = { ...sessionMetadata, lifecycleState: claudeResumeId ? "idle" : "stopped" };
|
|
6041
6170
|
sessionService.updateMetadata(sessionMetadata);
|
|
6042
6171
|
sessionWasProcessing = false;
|
|
6172
|
+
if (startupFailureRetryPending && !trackedSession.stopped) {
|
|
6173
|
+
startupFailureRetryPending = false;
|
|
6174
|
+
const prevResumeId = claudeResumeId;
|
|
6175
|
+
claudeResumeId = void 0;
|
|
6176
|
+
logger.log(`[Session ${sessionId}] Startup failure \u2014 cleared stale resume ID (was: ${prevResumeId})`);
|
|
6177
|
+
if (startupRetryMessage !== void 0) {
|
|
6178
|
+
logger.log(`[Session ${sessionId}] Retrying startup without --resume`);
|
|
6179
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
6180
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6181
|
+
spawnClaude(startupRetryMessage);
|
|
6182
|
+
return;
|
|
6183
|
+
}
|
|
6184
|
+
}
|
|
6043
6185
|
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
6044
6186
|
if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
|
|
6045
6187
|
signalProcessing(false);
|
|
@@ -6115,7 +6257,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6115
6257
|
}
|
|
6116
6258
|
}
|
|
6117
6259
|
let processMessageQueueRef;
|
|
6118
|
-
const sessionService =
|
|
6260
|
+
const { store: sessionService, rpcHandlers: sessionRPCHandlers } = createSessionStore(
|
|
6119
6261
|
server,
|
|
6120
6262
|
sessionId,
|
|
6121
6263
|
sessionMetadata,
|
|
@@ -6298,6 +6440,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6298
6440
|
stopSession(sessionId);
|
|
6299
6441
|
},
|
|
6300
6442
|
onMetadataUpdate: (newMeta) => {
|
|
6443
|
+
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
6301
6444
|
sessionMetadata = {
|
|
6302
6445
|
...newMeta,
|
|
6303
6446
|
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
@@ -6305,8 +6448,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6305
6448
|
// Preserve claudeSessionId set by 'system init' (frontend may not have it)
|
|
6306
6449
|
...sessionMetadata.claudeSessionId ? { claudeSessionId: sessionMetadata.claudeSessionId } : {},
|
|
6307
6450
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
6308
|
-
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {}
|
|
6451
|
+
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
|
|
6452
|
+
// Preserve parentSessionId — set at spawn time, frontend may not track it
|
|
6453
|
+
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {},
|
|
6454
|
+
// Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
|
|
6455
|
+
// which would wipe the active loop state and cause the bar to disappear mid-run.
|
|
6456
|
+
...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
|
|
6309
6457
|
};
|
|
6458
|
+
if (prevRalphLoop && !newMeta.ralphLoop) {
|
|
6459
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6460
|
+
}
|
|
6310
6461
|
const queue = newMeta.messageQueue;
|
|
6311
6462
|
if (queue && queue.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
|
|
6312
6463
|
setTimeout(() => {
|
|
@@ -6407,7 +6558,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6407
6558
|
return { success: !!tree, tree };
|
|
6408
6559
|
}
|
|
6409
6560
|
},
|
|
6410
|
-
{ messagesDir: getSessionDir(directory, sessionId) }
|
|
6561
|
+
{ messagesDir: getSessionDir(directory, sessionId), onSessionEvent: (update) => machineServiceRef?.notifySessionEvent(update) }
|
|
6411
6562
|
);
|
|
6412
6563
|
const svampConfig = createSvampConfigChecker(
|
|
6413
6564
|
directory,
|
|
@@ -6494,6 +6645,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6494
6645
|
pid: process.pid,
|
|
6495
6646
|
svampSessionId: sessionId,
|
|
6496
6647
|
hyphaService: sessionService,
|
|
6648
|
+
sessionRPCHandlers,
|
|
6497
6649
|
checkSvampConfig,
|
|
6498
6650
|
cleanupSvampConfig,
|
|
6499
6651
|
directory,
|
|
@@ -6579,7 +6731,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6579
6731
|
const allowedBashLiterals = /* @__PURE__ */ new Set();
|
|
6580
6732
|
const allowedBashPrefixes = /* @__PURE__ */ new Set();
|
|
6581
6733
|
const EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "Write", "NotebookEdit"]);
|
|
6582
|
-
const sessionService =
|
|
6734
|
+
const { store: sessionService, rpcHandlers: sessionRPCHandlersAcp } = createSessionStore(
|
|
6583
6735
|
server,
|
|
6584
6736
|
sessionId,
|
|
6585
6737
|
sessionMetadata,
|
|
@@ -6691,13 +6843,22 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6691
6843
|
stopSession(sessionId);
|
|
6692
6844
|
},
|
|
6693
6845
|
onMetadataUpdate: (newMeta) => {
|
|
6846
|
+
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
6694
6847
|
sessionMetadata = {
|
|
6695
6848
|
...newMeta,
|
|
6696
6849
|
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
6697
6850
|
lifecycleState: sessionMetadata.lifecycleState,
|
|
6698
6851
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
6699
|
-
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {}
|
|
6852
|
+
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
|
|
6853
|
+
// Preserve parentSessionId — set at spawn time, frontend may not track it
|
|
6854
|
+
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {},
|
|
6855
|
+
// Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
|
|
6856
|
+
// which would wipe the active loop state and cause the bar to disappear mid-run.
|
|
6857
|
+
...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
|
|
6700
6858
|
};
|
|
6859
|
+
if (prevRalphLoop && !newMeta.ralphLoop) {
|
|
6860
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6861
|
+
}
|
|
6701
6862
|
if (acpStopped) return;
|
|
6702
6863
|
const queue = newMeta.messageQueue;
|
|
6703
6864
|
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
@@ -6809,7 +6970,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6809
6970
|
return { success: !!tree, tree };
|
|
6810
6971
|
}
|
|
6811
6972
|
},
|
|
6812
|
-
{ messagesDir: getSessionDir(directory, sessionId) }
|
|
6973
|
+
{ messagesDir: getSessionDir(directory, sessionId), onSessionEvent: (update) => machineServiceRef?.notifySessionEvent(update) }
|
|
6813
6974
|
);
|
|
6814
6975
|
let insideOnTurnEnd = false;
|
|
6815
6976
|
const svampConfigChecker = createSvampConfigChecker(
|
|
@@ -7031,6 +7192,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7031
7192
|
pid: process.pid,
|
|
7032
7193
|
svampSessionId: sessionId,
|
|
7033
7194
|
hyphaService: sessionService,
|
|
7195
|
+
sessionRPCHandlers: sessionRPCHandlersAcp,
|
|
7034
7196
|
checkSvampConfig,
|
|
7035
7197
|
cleanupSvampConfig: svampConfigChecker.cleanup,
|
|
7036
7198
|
directory,
|
|
@@ -7156,10 +7318,28 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7156
7318
|
restartSession,
|
|
7157
7319
|
requestShutdown: () => requestShutdown("hypha-app"),
|
|
7158
7320
|
getTrackedSessions: getCurrentChildren,
|
|
7321
|
+
getSessionRPCHandlers: (sessionId) => {
|
|
7322
|
+
for (const [, session] of pidToTrackedSession) {
|
|
7323
|
+
if (session.svampSessionId === sessionId && session.sessionRPCHandlers) {
|
|
7324
|
+
return session.sessionRPCHandlers;
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
return void 0;
|
|
7328
|
+
},
|
|
7329
|
+
getSessionIds: () => {
|
|
7330
|
+
const ids = [];
|
|
7331
|
+
for (const [, session] of pidToTrackedSession) {
|
|
7332
|
+
if (session.svampSessionId && !session.stopped && session.sessionRPCHandlers) {
|
|
7333
|
+
ids.push(session.svampSessionId);
|
|
7334
|
+
}
|
|
7335
|
+
}
|
|
7336
|
+
return ids;
|
|
7337
|
+
},
|
|
7159
7338
|
supervisor
|
|
7160
7339
|
}
|
|
7161
7340
|
);
|
|
7162
7341
|
logger.log(`Machine service registered: svamp-machine-${machineId}`);
|
|
7342
|
+
machineServiceRef = machineService;
|
|
7163
7343
|
const artifactSync = new SessionArtifactSync(server, logger.log);
|
|
7164
7344
|
const debugService = await registerDebugService(server, machineId, {
|
|
7165
7345
|
machineId,
|
|
@@ -7347,7 +7527,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7347
7527
|
console.log(` Service: svamp-machine-${machineId}`);
|
|
7348
7528
|
console.log(` Log file: ${logger.logFilePath}`);
|
|
7349
7529
|
const HEARTBEAT_INTERVAL_MS = 1e4;
|
|
7350
|
-
const PING_TIMEOUT_MS =
|
|
7530
|
+
const PING_TIMEOUT_MS = 6e4;
|
|
7351
7531
|
const MAX_FAILURES = 60;
|
|
7352
7532
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
7353
7533
|
let heartbeatRunning = false;
|
|
@@ -7431,6 +7611,13 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7431
7611
|
} catch {
|
|
7432
7612
|
}
|
|
7433
7613
|
}
|
|
7614
|
+
if (conn?._reader) {
|
|
7615
|
+
logger.log("Aborting stale HTTP stream to trigger reconnection");
|
|
7616
|
+
try {
|
|
7617
|
+
conn._reader.cancel?.("Stale connection");
|
|
7618
|
+
} catch {
|
|
7619
|
+
}
|
|
7620
|
+
}
|
|
7434
7621
|
}
|
|
7435
7622
|
if (consecutiveHeartbeatFailures >= MAX_FAILURES) {
|
|
7436
7623
|
logger.log(`Heartbeat failed ${MAX_FAILURES} times. Shutting down.`);
|