svamp-cli 0.1.68 → 0.1.70
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 +185 -172
- package/dist/{commands-D4_QgXrS.mjs → commands-BodoXVL9.mjs} +1 -1
- package/dist/{commands-BwT-PFl4.mjs → commands-CIgrbviM.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{package-CHfluhvW.mjs → package-B0d2rUXI.mjs} +1 -1
- package/dist/{run-DWFTrXVF.mjs → run-C7CEDmD4.mjs} +479 -354
- package/dist/{run-BYtrsPjt.mjs → run-CgSj6KtU.mjs} +1 -1
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -907,353 +907,351 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
907
907
|
});
|
|
908
908
|
return msg;
|
|
909
909
|
};
|
|
910
|
-
const
|
|
911
|
-
{
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
}
|
|
910
|
+
const serviceDefinition = {
|
|
911
|
+
id: `svamp-session-${sessionId}`,
|
|
912
|
+
name: `Svamp Session ${sessionId.slice(0, 8)}`,
|
|
913
|
+
type: "svamp-session",
|
|
914
|
+
config: { visibility: "unlisted", require_context: true },
|
|
915
|
+
// ── Messages ──
|
|
916
|
+
getMessages: async (afterSeq, limit, context) => {
|
|
917
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
918
|
+
const after = afterSeq ?? 0;
|
|
919
|
+
const lim = Math.min(limit ?? 100, 500);
|
|
920
|
+
const filtered = messages.filter((m) => m.seq > after);
|
|
921
|
+
const page = filtered.slice(0, lim);
|
|
922
|
+
return {
|
|
923
|
+
messages: page,
|
|
924
|
+
hasMore: filtered.length > lim
|
|
925
|
+
};
|
|
926
|
+
},
|
|
927
|
+
sendMessage: async (content, localId, meta, context) => {
|
|
928
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
929
|
+
if (localId) {
|
|
930
|
+
const existing = messages.find((m) => m.localId === localId);
|
|
931
|
+
if (existing) {
|
|
932
|
+
return { id: existing.id, seq: existing.seq, localId: existing.localId };
|
|
949
933
|
}
|
|
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);
|
|
934
|
+
}
|
|
935
|
+
let parsed = content;
|
|
936
|
+
if (typeof parsed === "string") {
|
|
937
|
+
try {
|
|
938
|
+
parsed = JSON.parse(parsed);
|
|
939
|
+
} catch {
|
|
963
940
|
}
|
|
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
|
-
};
|
|
941
|
+
}
|
|
942
|
+
if (parsed && typeof parsed.content === "string" && !parsed.role) {
|
|
943
|
+
try {
|
|
944
|
+
const inner = JSON.parse(parsed.content);
|
|
945
|
+
if (inner && typeof inner === "object") parsed = inner;
|
|
946
|
+
} catch {
|
|
988
947
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
948
|
+
}
|
|
949
|
+
const wrappedContent = parsed && parsed.role === "user" ? { role: "user", content: parsed.content } : { role: "user", content: { type: "text", text: typeof parsed === "string" ? parsed : JSON.stringify(parsed) } };
|
|
950
|
+
const msg = {
|
|
951
|
+
id: randomUUID(),
|
|
952
|
+
seq: nextSeq++,
|
|
953
|
+
content: wrappedContent,
|
|
954
|
+
localId: localId || randomUUID(),
|
|
955
|
+
createdAt: Date.now(),
|
|
956
|
+
updatedAt: Date.now()
|
|
957
|
+
};
|
|
958
|
+
messages.push(msg);
|
|
959
|
+
if (messages.length > 1e3) messages.splice(0, messages.length - 1e3);
|
|
960
|
+
if (options?.messagesDir) {
|
|
961
|
+
appendMessage(options.messagesDir, sessionId, msg);
|
|
962
|
+
}
|
|
963
|
+
notifyListeners({
|
|
964
|
+
type: "new-message",
|
|
965
|
+
sessionId,
|
|
966
|
+
message: msg
|
|
967
|
+
});
|
|
968
|
+
callbacks.onUserMessage(content, meta);
|
|
969
|
+
return { id: msg.id, seq: msg.seq, localId: msg.localId };
|
|
970
|
+
},
|
|
971
|
+
// ── Metadata ──
|
|
972
|
+
getMetadata: async (context) => {
|
|
973
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
974
|
+
return {
|
|
975
|
+
metadata,
|
|
976
|
+
version: metadataVersion
|
|
977
|
+
};
|
|
978
|
+
},
|
|
979
|
+
updateMetadata: async (newMetadata, expectedVersion, context) => {
|
|
980
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
981
|
+
if (expectedVersion !== void 0 && expectedVersion !== metadataVersion) {
|
|
997
982
|
return {
|
|
998
|
-
result: "
|
|
983
|
+
result: "version-mismatch",
|
|
999
984
|
version: metadataVersion,
|
|
1000
985
|
metadata
|
|
1001
986
|
};
|
|
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
|
-
|
|
987
|
+
}
|
|
988
|
+
metadata = newMetadata;
|
|
989
|
+
metadataVersion++;
|
|
990
|
+
notifyListeners({
|
|
991
|
+
type: "update-session",
|
|
992
|
+
sessionId,
|
|
993
|
+
metadata: { value: metadata, version: metadataVersion }
|
|
994
|
+
});
|
|
995
|
+
callbacks.onMetadataUpdate?.(metadata);
|
|
996
|
+
return {
|
|
997
|
+
result: "success",
|
|
998
|
+
version: metadataVersion,
|
|
999
|
+
metadata
|
|
1000
|
+
};
|
|
1001
|
+
},
|
|
1002
|
+
/**
|
|
1003
|
+
* Patch the session config file (.svamp/{sessionId}/config.json).
|
|
1004
|
+
* Used by the frontend to set title, session_link, ralph_loop, etc.
|
|
1005
|
+
* Null values remove keys from the config.
|
|
1006
|
+
*/
|
|
1007
|
+
updateConfig: async (patch, context) => {
|
|
1008
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1009
|
+
callbacks.onUpdateConfig?.(patch);
|
|
1010
|
+
return { success: true };
|
|
1011
|
+
},
|
|
1012
|
+
// ── Agent State ──
|
|
1013
|
+
getAgentState: async (context) => {
|
|
1014
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1015
|
+
return {
|
|
1016
|
+
agentState,
|
|
1017
|
+
version: agentStateVersion
|
|
1018
|
+
};
|
|
1019
|
+
},
|
|
1020
|
+
updateAgentState: async (newState, expectedVersion, context) => {
|
|
1021
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1022
|
+
if (expectedVersion !== void 0 && expectedVersion !== agentStateVersion) {
|
|
1037
1023
|
return {
|
|
1038
|
-
result: "
|
|
1024
|
+
result: "version-mismatch",
|
|
1039
1025
|
version: agentStateVersion,
|
|
1040
1026
|
agentState
|
|
1041
1027
|
};
|
|
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
|
-
|
|
1028
|
+
}
|
|
1029
|
+
agentState = newState;
|
|
1030
|
+
agentStateVersion++;
|
|
1031
|
+
notifyListeners({
|
|
1032
|
+
type: "update-session",
|
|
1033
|
+
sessionId,
|
|
1034
|
+
agentState: { value: agentState, version: agentStateVersion }
|
|
1035
|
+
});
|
|
1036
|
+
return {
|
|
1037
|
+
result: "success",
|
|
1038
|
+
version: agentStateVersion,
|
|
1039
|
+
agentState
|
|
1040
|
+
};
|
|
1041
|
+
},
|
|
1042
|
+
// ── Session Control RPCs ──
|
|
1043
|
+
abort: async (context) => {
|
|
1044
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1045
|
+
callbacks.onAbort();
|
|
1046
|
+
return { success: true };
|
|
1047
|
+
},
|
|
1048
|
+
permissionResponse: async (params, context) => {
|
|
1049
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1050
|
+
callbacks.onPermissionResponse(params);
|
|
1051
|
+
return { success: true };
|
|
1052
|
+
},
|
|
1053
|
+
switchMode: async (mode, context) => {
|
|
1054
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1055
|
+
callbacks.onSwitchMode(mode);
|
|
1056
|
+
return { success: true };
|
|
1057
|
+
},
|
|
1058
|
+
restartClaude: async (context) => {
|
|
1059
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1060
|
+
return await callbacks.onRestartClaude();
|
|
1061
|
+
},
|
|
1062
|
+
killSession: async (context) => {
|
|
1063
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1064
|
+
callbacks.onKillSession();
|
|
1065
|
+
return { success: true };
|
|
1066
|
+
},
|
|
1067
|
+
// ── Activity ──
|
|
1068
|
+
keepAlive: async (thinking, mode, context) => {
|
|
1069
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1070
|
+
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
1071
|
+
notifyListeners({
|
|
1072
|
+
type: "activity",
|
|
1073
|
+
sessionId,
|
|
1074
|
+
...lastActivity
|
|
1075
|
+
});
|
|
1076
|
+
},
|
|
1077
|
+
sessionEnd: async (context) => {
|
|
1078
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
1079
|
+
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
1080
|
+
notifyListeners({
|
|
1081
|
+
type: "activity",
|
|
1082
|
+
sessionId,
|
|
1083
|
+
...lastActivity
|
|
1084
|
+
});
|
|
1085
|
+
},
|
|
1086
|
+
// ── Activity State Query ──
|
|
1087
|
+
getActivityState: async (context) => {
|
|
1088
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1089
|
+
const pendingPermissions = agentState?.requests ? Object.entries(agentState.requests).filter(([, req]) => req.status === "pending" || !req.status).map(([id, req]) => ({
|
|
1090
|
+
id,
|
|
1091
|
+
tool: req.tool,
|
|
1092
|
+
arguments: req.arguments,
|
|
1093
|
+
createdAt: req.createdAt
|
|
1094
|
+
})) : [];
|
|
1095
|
+
return { ...lastActivity, sessionId, pendingPermissions };
|
|
1096
|
+
},
|
|
1097
|
+
// ── File Operations (optional, admin-only) ──
|
|
1098
|
+
readFile: async (path, context) => {
|
|
1099
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1100
|
+
if (!callbacks.onReadFile) throw new Error("readFile not supported");
|
|
1101
|
+
return await callbacks.onReadFile(path);
|
|
1102
|
+
},
|
|
1103
|
+
writeFile: async (path, content, context) => {
|
|
1104
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1105
|
+
if (!callbacks.onWriteFile) throw new Error("writeFile not supported");
|
|
1106
|
+
await callbacks.onWriteFile(path, content);
|
|
1107
|
+
return { success: true };
|
|
1108
|
+
},
|
|
1109
|
+
listDirectory: async (path, context) => {
|
|
1110
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1111
|
+
if (!callbacks.onListDirectory) throw new Error("listDirectory not supported");
|
|
1112
|
+
return await callbacks.onListDirectory(path);
|
|
1113
|
+
},
|
|
1114
|
+
bash: async (command, cwd, context) => {
|
|
1115
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1116
|
+
if (!callbacks.onBash) throw new Error("bash not supported");
|
|
1117
|
+
return await callbacks.onBash(command, cwd);
|
|
1118
|
+
},
|
|
1119
|
+
ripgrep: async (args, cwd, context) => {
|
|
1120
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1121
|
+
if (!callbacks.onRipgrep) throw new Error("ripgrep not supported");
|
|
1122
|
+
try {
|
|
1123
|
+
const stdout = await callbacks.onRipgrep(args, cwd);
|
|
1124
|
+
return { success: true, stdout, stderr: "", exitCode: 0 };
|
|
1125
|
+
} catch (err) {
|
|
1126
|
+
return { success: false, stdout: "", stderr: err.message || "", exitCode: 1, error: err.message };
|
|
1127
|
+
}
|
|
1128
|
+
},
|
|
1129
|
+
getDirectoryTree: async (path, maxDepth, context) => {
|
|
1130
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1131
|
+
if (!callbacks.onGetDirectoryTree) throw new Error("getDirectoryTree not supported");
|
|
1132
|
+
return await callbacks.onGetDirectoryTree(path, maxDepth ?? 3);
|
|
1133
|
+
},
|
|
1134
|
+
// ── Sharing Management ──
|
|
1135
|
+
getSharing: async (context) => {
|
|
1136
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1137
|
+
return { sharing: metadata.sharing || null };
|
|
1138
|
+
},
|
|
1139
|
+
/** Returns the caller's effective role (null if no access). Does not throw. */
|
|
1140
|
+
getEffectiveRole: async (context) => {
|
|
1141
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1142
|
+
const role = getEffectiveRole(context, metadata.sharing);
|
|
1143
|
+
return { role };
|
|
1144
|
+
},
|
|
1145
|
+
updateSharing: async (newSharing, context) => {
|
|
1146
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1147
|
+
if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
|
|
1148
|
+
throw new Error("Only the session owner can update sharing settings");
|
|
1149
|
+
}
|
|
1150
|
+
if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
|
|
1151
|
+
newSharing = { ...newSharing, owner: context.user.email };
|
|
1152
|
+
}
|
|
1153
|
+
metadata = { ...metadata, sharing: newSharing };
|
|
1154
|
+
metadataVersion++;
|
|
1155
|
+
notifyListeners({
|
|
1156
|
+
type: "update-session",
|
|
1157
|
+
sessionId,
|
|
1158
|
+
metadata: { value: metadata, version: metadataVersion }
|
|
1159
|
+
});
|
|
1160
|
+
callbacks.onSharingUpdate?.(newSharing);
|
|
1161
|
+
return { success: true, sharing: newSharing };
|
|
1162
|
+
},
|
|
1163
|
+
/** Update security context and restart the agent process with new rules */
|
|
1164
|
+
updateSecurityContext: async (newSecurityContext, context) => {
|
|
1165
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1166
|
+
if (metadata.sharing && context?.user?.email && metadata.sharing.owner && context.user.email.toLowerCase() !== metadata.sharing.owner.toLowerCase()) {
|
|
1167
|
+
throw new Error("Only the session owner can update security context");
|
|
1168
|
+
}
|
|
1169
|
+
if (!callbacks.onUpdateSecurityContext) {
|
|
1170
|
+
throw new Error("Security context updates are not supported for this session");
|
|
1171
|
+
}
|
|
1172
|
+
metadata = { ...metadata, securityContext: newSecurityContext };
|
|
1173
|
+
metadataVersion++;
|
|
1174
|
+
notifyListeners({
|
|
1175
|
+
type: "update-session",
|
|
1176
|
+
sessionId,
|
|
1177
|
+
metadata: { value: metadata, version: metadataVersion }
|
|
1178
|
+
});
|
|
1179
|
+
return await callbacks.onUpdateSecurityContext(newSecurityContext);
|
|
1180
|
+
},
|
|
1181
|
+
/** Apply a new system prompt and restart the agent process */
|
|
1182
|
+
applySystemPrompt: async (prompt, context) => {
|
|
1183
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
1184
|
+
if (!callbacks.onApplySystemPrompt) {
|
|
1185
|
+
throw new Error("System prompt updates are not supported for this session");
|
|
1186
|
+
}
|
|
1187
|
+
return await callbacks.onApplySystemPrompt(prompt);
|
|
1188
|
+
},
|
|
1189
|
+
// ── Listener Registration ──
|
|
1190
|
+
registerListener: async (callback, context) => {
|
|
1191
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
1192
|
+
listeners.push(callback);
|
|
1193
|
+
const replayMessages = messages.slice(-50);
|
|
1194
|
+
const REPLAY_MESSAGE_TIMEOUT_MS = 1e4;
|
|
1195
|
+
for (const msg of replayMessages) {
|
|
1196
|
+
if (listeners.indexOf(callback) < 0) break;
|
|
1123
1197
|
try {
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1198
|
+
const result = callback.onUpdate({
|
|
1199
|
+
type: "new-message",
|
|
1200
|
+
sessionId,
|
|
1201
|
+
message: msg
|
|
1202
|
+
});
|
|
1203
|
+
if (result && typeof result.catch === "function") {
|
|
1204
|
+
try {
|
|
1205
|
+
await Promise.race([
|
|
1206
|
+
result,
|
|
1207
|
+
new Promise(
|
|
1208
|
+
(_, reject) => setTimeout(() => reject(new Error("Replay message timeout")), REPLAY_MESSAGE_TIMEOUT_MS)
|
|
1209
|
+
)
|
|
1210
|
+
]);
|
|
1211
|
+
} catch (err) {
|
|
1212
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
1213
|
+
removeListener(callback, "replay error");
|
|
1214
|
+
return { success: false, error: "Listener removed during replay" };
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1126
1217
|
} catch (err) {
|
|
1127
|
-
|
|
1218
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
1219
|
+
removeListener(callback, "replay error");
|
|
1220
|
+
return { success: false, error: "Listener removed during replay" };
|
|
1128
1221
|
}
|
|
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({
|
|
1222
|
+
}
|
|
1223
|
+
if (listeners.indexOf(callback) < 0) {
|
|
1224
|
+
return { success: false, error: "Listener was removed during replay" };
|
|
1225
|
+
}
|
|
1226
|
+
try {
|
|
1227
|
+
const result = callback.onUpdate({
|
|
1157
1228
|
type: "update-session",
|
|
1158
1229
|
sessionId,
|
|
1159
|
-
metadata: { value: metadata, version: metadataVersion }
|
|
1230
|
+
metadata: { value: metadata, version: metadataVersion },
|
|
1231
|
+
agentState: { value: agentState, version: agentStateVersion }
|
|
1160
1232
|
});
|
|
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");
|
|
1233
|
+
if (result && typeof result.catch === "function") {
|
|
1234
|
+
result.catch(() => {
|
|
1235
|
+
});
|
|
1172
1236
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1237
|
+
} catch {
|
|
1238
|
+
}
|
|
1239
|
+
try {
|
|
1240
|
+
const result = callback.onUpdate({
|
|
1241
|
+
type: "activity",
|
|
1177
1242
|
sessionId,
|
|
1178
|
-
|
|
1243
|
+
...lastActivity
|
|
1179
1244
|
});
|
|
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 }
|
|
1245
|
+
if (result && typeof result.catch === "function") {
|
|
1246
|
+
result.catch(() => {
|
|
1233
1247
|
});
|
|
1234
|
-
if (result && typeof result.catch === "function") {
|
|
1235
|
-
result.catch(() => {
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
} catch {
|
|
1239
1248
|
}
|
|
1240
|
-
|
|
1241
|
-
const result = callback.onUpdate({
|
|
1242
|
-
type: "activity",
|
|
1243
|
-
sessionId,
|
|
1244
|
-
...lastActivity
|
|
1245
|
-
});
|
|
1246
|
-
if (result && typeof result.catch === "function") {
|
|
1247
|
-
result.catch(() => {
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
} catch {
|
|
1251
|
-
}
|
|
1252
|
-
return { success: true, listenerId: listeners.length - 1 };
|
|
1249
|
+
} catch {
|
|
1253
1250
|
}
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1251
|
+
return { success: true, listenerId: listeners.length - 1 };
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
const serviceInfo = await server.registerService(serviceDefinition, { overwrite: true });
|
|
1257
1255
|
console.log(`[HYPHA SESSION] Session service registered: ${serviceInfo.id}`);
|
|
1258
1256
|
return {
|
|
1259
1257
|
serviceInfo,
|
|
@@ -1316,6 +1314,13 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
1316
1314
|
removeListener(listener, "disconnect");
|
|
1317
1315
|
}
|
|
1318
1316
|
await server.unregisterService(serviceInfo.id);
|
|
1317
|
+
},
|
|
1318
|
+
reregister: async () => {
|
|
1319
|
+
try {
|
|
1320
|
+
await server.registerService(serviceDefinition, { overwrite: true });
|
|
1321
|
+
} catch (e) {
|
|
1322
|
+
if (!String(e?.message).includes("already exists")) throw e;
|
|
1323
|
+
}
|
|
1319
1324
|
}
|
|
1320
1325
|
};
|
|
1321
1326
|
}
|
|
@@ -1796,6 +1801,10 @@ function wrapWithNono(command, args, config) {
|
|
|
1796
1801
|
if (existsSync(realLocalDir)) {
|
|
1797
1802
|
nonoArgs.push("--read", realLocalDir);
|
|
1798
1803
|
}
|
|
1804
|
+
const realKeychainDir = join$1(homedir(), "Library", "Keychains");
|
|
1805
|
+
if (existsSync(realKeychainDir)) {
|
|
1806
|
+
nonoArgs.push("--read", realKeychainDir);
|
|
1807
|
+
}
|
|
1799
1808
|
}
|
|
1800
1809
|
if (config.nonoConfig?.allowDirs) {
|
|
1801
1810
|
for (const dir of config.nonoConfig.allowDirs) {
|
|
@@ -3886,9 +3895,8 @@ async function stageCredentialsForSharing(sessionId) {
|
|
|
3886
3895
|
const realHome = homedir();
|
|
3887
3896
|
const realClaudeDir = join$1(realHome, ".claude");
|
|
3888
3897
|
await mkdir(STAGED_HOMES_DIR, { recursive: true });
|
|
3889
|
-
const tmpHome =
|
|
3890
|
-
|
|
3891
|
-
);
|
|
3898
|
+
const tmpHome = join$1(STAGED_HOMES_DIR, sessionId);
|
|
3899
|
+
await mkdir(tmpHome, { recursive: true });
|
|
3892
3900
|
const stagedClaudeDir = join$1(tmpHome, ".claude");
|
|
3893
3901
|
await mkdir(stagedClaudeDir, { recursive: true });
|
|
3894
3902
|
const credentialFiles = ["credentials.json", ".credentials.json"];
|
|
@@ -3909,10 +3917,12 @@ async function stageCredentialsForSharing(sessionId) {
|
|
|
3909
3917
|
);
|
|
3910
3918
|
} catch {
|
|
3911
3919
|
}
|
|
3912
|
-
const
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3920
|
+
const claudeJsonPath = join$1(tmpHome, ".claude.json");
|
|
3921
|
+
if (!existsSync(claudeJsonPath)) {
|
|
3922
|
+
try {
|
|
3923
|
+
await writeFile(claudeJsonPath, "{}");
|
|
3924
|
+
} catch {
|
|
3925
|
+
}
|
|
3916
3926
|
}
|
|
3917
3927
|
return {
|
|
3918
3928
|
homePath: tmpHome,
|
|
@@ -4154,7 +4164,7 @@ class ProcessSupervisor {
|
|
|
4154
4164
|
async persistSpec(spec) {
|
|
4155
4165
|
const filePath = path.join(this.persistDir, `${spec.id}.json`);
|
|
4156
4166
|
const tmpPath = filePath + ".tmp";
|
|
4157
|
-
await writeFile(tmpPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
4167
|
+
await writeFile$1(tmpPath, JSON.stringify(spec, null, 2), "utf-8");
|
|
4158
4168
|
await rename(tmpPath, filePath);
|
|
4159
4169
|
}
|
|
4160
4170
|
async deleteSpec(id) {
|
|
@@ -4467,6 +4477,53 @@ async function installSkillFromMarketplace(name) {
|
|
|
4467
4477
|
writeFileSync(localPath, content, "utf-8");
|
|
4468
4478
|
}
|
|
4469
4479
|
}
|
|
4480
|
+
function preventMachineSleep(logger) {
|
|
4481
|
+
if (process.platform === "darwin") {
|
|
4482
|
+
const caff = spawn$1("caffeinate", ["-s", "-i", "-m"], { stdio: "ignore", detached: false });
|
|
4483
|
+
caff.on("error", (err) => {
|
|
4484
|
+
logger.log(`Warning: could not start caffeinate to prevent sleep: ${err.message}`);
|
|
4485
|
+
});
|
|
4486
|
+
caff.on("exit", (code) => {
|
|
4487
|
+
if (code !== null && code !== 0) {
|
|
4488
|
+
logger.log(`Warning: caffeinate exited unexpectedly (code ${code})`);
|
|
4489
|
+
}
|
|
4490
|
+
});
|
|
4491
|
+
process.on("exit", () => {
|
|
4492
|
+
try {
|
|
4493
|
+
caff.kill();
|
|
4494
|
+
} catch {
|
|
4495
|
+
}
|
|
4496
|
+
});
|
|
4497
|
+
logger.log("caffeinate started \u2014 machine will not sleep while daemon is running");
|
|
4498
|
+
return;
|
|
4499
|
+
}
|
|
4500
|
+
if (process.platform === "linux") {
|
|
4501
|
+
const inh = spawn$1(
|
|
4502
|
+
"systemd-inhibit",
|
|
4503
|
+
["--what=idle:sleep:handle-lid-switch", "--who=svamp", "--why=Svamp daemon running", "--mode=block", "sleep", "infinity"],
|
|
4504
|
+
{ stdio: "ignore", detached: false }
|
|
4505
|
+
);
|
|
4506
|
+
inh.on("error", () => {
|
|
4507
|
+
});
|
|
4508
|
+
inh.on("exit", (code) => {
|
|
4509
|
+
if (code !== null && code !== 0 && code !== 130) {
|
|
4510
|
+
logger.log(`Warning: systemd-inhibit exited with code ${code} \u2014 sleep prevention may be inactive`);
|
|
4511
|
+
}
|
|
4512
|
+
});
|
|
4513
|
+
process.on("exit", () => {
|
|
4514
|
+
try {
|
|
4515
|
+
inh.kill();
|
|
4516
|
+
} catch {
|
|
4517
|
+
}
|
|
4518
|
+
});
|
|
4519
|
+
setImmediate(() => {
|
|
4520
|
+
if (!inh.killed && inh.exitCode === null) {
|
|
4521
|
+
logger.log("systemd-inhibit started \u2014 machine will not idle-sleep while daemon is running");
|
|
4522
|
+
}
|
|
4523
|
+
});
|
|
4524
|
+
return;
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4470
4527
|
async function ensureAutoInstalledSkills(logger) {
|
|
4471
4528
|
const tasks = [
|
|
4472
4529
|
{
|
|
@@ -5159,8 +5216,12 @@ async function startDaemon(options) {
|
|
|
5159
5216
|
// hypha-rpc connection closed message
|
|
5160
5217
|
"Client disconnected",
|
|
5161
5218
|
// Hypha client disconnect events
|
|
5162
|
-
"fetch failed"
|
|
5219
|
+
"fetch failed",
|
|
5163
5220
|
// Fetch API errors during reconnect
|
|
5221
|
+
"already exists in the cache store",
|
|
5222
|
+
// hypha-rpc: stale in-flight RPC calls after reconnect try to resolve with duplicate message keys
|
|
5223
|
+
"Failed to send the request when calling method"
|
|
5224
|
+
// hypha-rpc: RPC call failed after reconnect (often wraps the cache store error)
|
|
5164
5225
|
];
|
|
5165
5226
|
let unhandledRejectionCount = 0;
|
|
5166
5227
|
let unhandledRejectionResetTimer = null;
|
|
@@ -5255,6 +5316,7 @@ async function startDaemon(options) {
|
|
|
5255
5316
|
await supervisor.init();
|
|
5256
5317
|
ensureAutoInstalledSkills(logger).catch(() => {
|
|
5257
5318
|
});
|
|
5319
|
+
preventMachineSleep(logger);
|
|
5258
5320
|
try {
|
|
5259
5321
|
logger.log("Connecting to Hypha server...");
|
|
5260
5322
|
server = await connectToHypha({
|
|
@@ -5268,14 +5330,22 @@ async function startDaemon(options) {
|
|
|
5268
5330
|
logger.log(`Hypha connection permanently lost: ${reason}`);
|
|
5269
5331
|
requestShutdown("hypha-disconnected", String(reason));
|
|
5270
5332
|
});
|
|
5333
|
+
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
5271
5334
|
server.on("services_registered", () => {
|
|
5272
5335
|
if (consecutiveHeartbeatFailures > 0) {
|
|
5273
5336
|
logger.log(`Hypha reconnection successful \u2014 services re-registered (resetting ${consecutiveHeartbeatFailures} failures)`);
|
|
5274
5337
|
consecutiveHeartbeatFailures = 0;
|
|
5275
5338
|
lastReconnectAt = Date.now();
|
|
5276
5339
|
}
|
|
5340
|
+
const activeSessions = Array.from(pidToTrackedSession.values()).filter((s) => !s.stopped && s.hyphaService);
|
|
5341
|
+
if (activeSessions.length > 0) {
|
|
5342
|
+
logger.log(`Re-registering ${activeSessions.length} session services after reconnect`);
|
|
5343
|
+
Promise.allSettled(activeSessions.map((s) => s.hyphaService.reregister())).then((results) => {
|
|
5344
|
+
const failed = results.filter((r) => r.status === "rejected").length;
|
|
5345
|
+
if (failed > 0) logger.log(`Warning: ${failed} session service re-registrations failed`);
|
|
5346
|
+
});
|
|
5347
|
+
}
|
|
5277
5348
|
});
|
|
5278
|
-
const pidToTrackedSession = /* @__PURE__ */ new Map();
|
|
5279
5349
|
const getCurrentChildren = () => {
|
|
5280
5350
|
return Array.from(pidToTrackedSession.values()).map((s) => ({
|
|
5281
5351
|
sessionId: s.svampSessionId || `PID-${s.pid}`,
|
|
@@ -5412,6 +5482,10 @@ async function startDaemon(options) {
|
|
|
5412
5482
|
securityContext: options2.securityContext,
|
|
5413
5483
|
tags: options2.tags,
|
|
5414
5484
|
parentSessionId: options2.parentSessionId,
|
|
5485
|
+
...options2.parentSessionId && (() => {
|
|
5486
|
+
const parentTracked = Array.from(pidToTrackedSession.values()).find((t) => t.svampSessionId === options2.parentSessionId);
|
|
5487
|
+
return parentTracked?.directory ? { parentSessionPath: parentTracked.directory } : {};
|
|
5488
|
+
})(),
|
|
5415
5489
|
...options2.injectPlatformGuidance !== void 0 && { injectPlatformGuidance: options2.injectPlatformGuidance }
|
|
5416
5490
|
};
|
|
5417
5491
|
let claudeProcess = null;
|
|
@@ -5423,6 +5497,9 @@ async function startDaemon(options) {
|
|
|
5423
5497
|
let sessionWasProcessing = !!options2.wasProcessing;
|
|
5424
5498
|
let lastAssistantText = "";
|
|
5425
5499
|
let spawnHasReceivedInit = false;
|
|
5500
|
+
let startupFailureRetryPending = false;
|
|
5501
|
+
let startupRetryMessage;
|
|
5502
|
+
let startupNonJsonLines = [];
|
|
5426
5503
|
const signalProcessing = (processing) => {
|
|
5427
5504
|
sessionService.sendKeepAlive(processing);
|
|
5428
5505
|
const newState = processing ? "running" : "idle";
|
|
@@ -5473,6 +5550,8 @@ async function startDaemon(options) {
|
|
|
5473
5550
|
let isolationCleanupFiles = [];
|
|
5474
5551
|
const spawnClaude = (initialMessage, meta) => {
|
|
5475
5552
|
const effectiveMeta = { ...lastSpawnMeta, ...meta };
|
|
5553
|
+
startupNonJsonLines = [];
|
|
5554
|
+
startupRetryMessage = initialMessage;
|
|
5476
5555
|
let rawPermissionMode = effectiveMeta.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
|
|
5477
5556
|
if (options2.forceIsolation || sessionMetadata.sharing?.enabled) {
|
|
5478
5557
|
rawPermissionMode = rawPermissionMode === "default" ? "auto-approve-all" : rawPermissionMode;
|
|
@@ -5668,23 +5747,34 @@ async function startDaemon(options) {
|
|
|
5668
5747
|
if (msg.is_error) {
|
|
5669
5748
|
const resultText = msg.result || "";
|
|
5670
5749
|
logger.error(`[Session ${sessionId}] Claude error (is_error=true, api_ms=${msg.duration_api_ms}): "${resultText}"`);
|
|
5671
|
-
const
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
hint = "\n\nRun `claude login` in your terminal on the machine running the daemon to re-authenticate.";
|
|
5677
|
-
} else if (isResumeIssue) {
|
|
5678
|
-
hint = "\n\nThe conversation history may be corrupted. Try starting a fresh session.";
|
|
5750
|
+
const isStartupFailure = msg.duration_api_ms === 0 && msg.num_turns === 0;
|
|
5751
|
+
if (isStartupFailure && !startupFailureRetryPending) {
|
|
5752
|
+
logger.log(`[Session ${sessionId}] Startup failure detected \u2014 scheduling silent retry without --resume`);
|
|
5753
|
+
startupFailureRetryPending = true;
|
|
5754
|
+
lastErrorMessagePushed = true;
|
|
5679
5755
|
} else {
|
|
5680
|
-
|
|
5756
|
+
const lower = resultText.toLowerCase();
|
|
5757
|
+
const isLoginIssue = lower.includes("login") || lower.includes("logged in") || lower.includes("auth") || lower.includes("api key") || lower.includes("unauthorized");
|
|
5758
|
+
const isResumeIssue = lower.includes("tool_use.name") || lower.includes("invalid_request") || lower.includes("messages.");
|
|
5759
|
+
let hint = "";
|
|
5760
|
+
if (isLoginIssue) {
|
|
5761
|
+
hint = "\n\nRun `claude login` in your terminal on the machine running the daemon to re-authenticate.";
|
|
5762
|
+
} else if (isResumeIssue) {
|
|
5763
|
+
hint = "\n\nThe conversation history may be corrupted. Try starting a fresh session.";
|
|
5764
|
+
} else {
|
|
5765
|
+
hint = "\n\nCheck that the Claude Code CLI is properly installed and configured.";
|
|
5766
|
+
}
|
|
5767
|
+
const displayMsg = resultText || "Claude Code exited with an error.";
|
|
5768
|
+
let contextInfo = "";
|
|
5769
|
+
if (startupNonJsonLines.length > 0) {
|
|
5770
|
+
contextInfo = "\n\n**Startup output:**\n```\n" + startupNonJsonLines.slice(-10).join("\n") + "\n```";
|
|
5771
|
+
}
|
|
5772
|
+
sessionService.pushMessage({
|
|
5773
|
+
type: "assistant",
|
|
5774
|
+
content: [{ type: "text", text: `**Error:** ${displayMsg}${hint}${contextInfo}` }]
|
|
5775
|
+
}, "agent");
|
|
5776
|
+
lastErrorMessagePushed = true;
|
|
5681
5777
|
}
|
|
5682
|
-
const displayMsg = resultText || "Claude Code exited with an error.";
|
|
5683
|
-
sessionService.pushMessage({
|
|
5684
|
-
type: "assistant",
|
|
5685
|
-
content: [{ type: "text", text: `**Error:** ${displayMsg}${hint}` }]
|
|
5686
|
-
}, "agent");
|
|
5687
|
-
lastErrorMessagePushed = true;
|
|
5688
5778
|
}
|
|
5689
5779
|
}
|
|
5690
5780
|
if (msg.type === "result") {
|
|
@@ -5899,6 +5989,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5899
5989
|
const isResumeFailure = !spawnHasReceivedInit && claudeResumeId && msg.session_id !== claudeResumeId;
|
|
5900
5990
|
const isConversationClear = spawnHasReceivedInit && claudeResumeId && msg.session_id !== claudeResumeId;
|
|
5901
5991
|
spawnHasReceivedInit = true;
|
|
5992
|
+
startupFailureRetryPending = false;
|
|
5902
5993
|
claudeResumeId = msg.session_id;
|
|
5903
5994
|
sessionMetadata = { ...sessionMetadata, claudeSessionId: msg.session_id };
|
|
5904
5995
|
sessionService.updateMetadata(sessionMetadata);
|
|
@@ -5940,6 +6031,9 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5940
6031
|
}
|
|
5941
6032
|
} catch {
|
|
5942
6033
|
logger.log(`[Session ${sessionId}] Claude stdout (non-JSON): ${line}`);
|
|
6034
|
+
if (!spawnHasReceivedInit) {
|
|
6035
|
+
startupNonJsonLines.push(line.slice(0, 500));
|
|
6036
|
+
}
|
|
5943
6037
|
}
|
|
5944
6038
|
}
|
|
5945
6039
|
});
|
|
@@ -5984,6 +6078,19 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
5984
6078
|
sessionMetadata = { ...sessionMetadata, lifecycleState: claudeResumeId ? "idle" : "stopped" };
|
|
5985
6079
|
sessionService.updateMetadata(sessionMetadata);
|
|
5986
6080
|
sessionWasProcessing = false;
|
|
6081
|
+
if (startupFailureRetryPending && !trackedSession.stopped) {
|
|
6082
|
+
startupFailureRetryPending = false;
|
|
6083
|
+
const prevResumeId = claudeResumeId;
|
|
6084
|
+
claudeResumeId = void 0;
|
|
6085
|
+
logger.log(`[Session ${sessionId}] Startup failure \u2014 cleared stale resume ID (was: ${prevResumeId})`);
|
|
6086
|
+
if (startupRetryMessage !== void 0) {
|
|
6087
|
+
logger.log(`[Session ${sessionId}] Retrying startup without --resume`);
|
|
6088
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "running" };
|
|
6089
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6090
|
+
spawnClaude(startupRetryMessage);
|
|
6091
|
+
return;
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
5987
6094
|
const queueLen = sessionMetadata.messageQueue?.length ?? 0;
|
|
5988
6095
|
if (queueLen > 0 && claudeResumeId && !trackedSession.stopped) {
|
|
5989
6096
|
signalProcessing(false);
|
|
@@ -6242,6 +6349,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6242
6349
|
stopSession(sessionId);
|
|
6243
6350
|
},
|
|
6244
6351
|
onMetadataUpdate: (newMeta) => {
|
|
6352
|
+
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
6245
6353
|
sessionMetadata = {
|
|
6246
6354
|
...newMeta,
|
|
6247
6355
|
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
@@ -6249,8 +6357,16 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6249
6357
|
// Preserve claudeSessionId set by 'system init' (frontend may not have it)
|
|
6250
6358
|
...sessionMetadata.claudeSessionId ? { claudeSessionId: sessionMetadata.claudeSessionId } : {},
|
|
6251
6359
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
6252
|
-
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {}
|
|
6360
|
+
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
|
|
6361
|
+
// Preserve parentSessionId — set at spawn time, frontend may not track it
|
|
6362
|
+
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {},
|
|
6363
|
+
// Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
|
|
6364
|
+
// which would wipe the active loop state and cause the bar to disappear mid-run.
|
|
6365
|
+
...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
|
|
6253
6366
|
};
|
|
6367
|
+
if (prevRalphLoop && !newMeta.ralphLoop) {
|
|
6368
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6369
|
+
}
|
|
6254
6370
|
const queue = newMeta.messageQueue;
|
|
6255
6371
|
if (queue && queue.length > 0 && !sessionWasProcessing && !trackedSession.stopped) {
|
|
6256
6372
|
setTimeout(() => {
|
|
@@ -6635,13 +6751,22 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
6635
6751
|
stopSession(sessionId);
|
|
6636
6752
|
},
|
|
6637
6753
|
onMetadataUpdate: (newMeta) => {
|
|
6754
|
+
const prevRalphLoop = sessionMetadata.ralphLoop;
|
|
6638
6755
|
sessionMetadata = {
|
|
6639
6756
|
...newMeta,
|
|
6640
6757
|
// Daemon drives lifecycleState — don't let frontend overwrite with stale value
|
|
6641
6758
|
lifecycleState: sessionMetadata.lifecycleState,
|
|
6642
6759
|
...sessionMetadata.summary && !newMeta.summary ? { summary: sessionMetadata.summary } : {},
|
|
6643
|
-
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {}
|
|
6760
|
+
...sessionMetadata.sessionLink && !newMeta.sessionLink ? { sessionLink: sessionMetadata.sessionLink } : {},
|
|
6761
|
+
// Preserve parentSessionId — set at spawn time, frontend may not track it
|
|
6762
|
+
...sessionMetadata.parentSessionId ? { parentSessionId: sessionMetadata.parentSessionId } : {},
|
|
6763
|
+
// Preserve daemon-owned ralphLoop — frontend may send stale snapshot without it,
|
|
6764
|
+
// which would wipe the active loop state and cause the bar to disappear mid-run.
|
|
6765
|
+
...prevRalphLoop ? { ralphLoop: prevRalphLoop } : {}
|
|
6644
6766
|
};
|
|
6767
|
+
if (prevRalphLoop && !newMeta.ralphLoop) {
|
|
6768
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
6769
|
+
}
|
|
6645
6770
|
if (acpStopped) return;
|
|
6646
6771
|
const queue = newMeta.messageQueue;
|
|
6647
6772
|
if (queue && queue.length > 0 && sessionMetadata.lifecycleState === "idle") {
|
|
@@ -7291,7 +7416,7 @@ The automated loop has finished. Review the progress above and let me know if yo
|
|
|
7291
7416
|
console.log(` Service: svamp-machine-${machineId}`);
|
|
7292
7417
|
console.log(` Log file: ${logger.logFilePath}`);
|
|
7293
7418
|
const HEARTBEAT_INTERVAL_MS = 1e4;
|
|
7294
|
-
const PING_TIMEOUT_MS =
|
|
7419
|
+
const PING_TIMEOUT_MS = 6e4;
|
|
7295
7420
|
const MAX_FAILURES = 60;
|
|
7296
7421
|
const POST_RECONNECT_GRACE_MS = 2e4;
|
|
7297
7422
|
let heartbeatRunning = false;
|