@sellable/install 0.1.800 → 0.1.802
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/container/Dockerfile +4 -4
- package/container/README.md +1 -1
- package/lib/sellable-agent/fly-customer-image/Dockerfile +1 -1
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +267 -129
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.d.mts +49 -2
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.mjs +150 -14
- package/lib/sellable-agent/fly-customer-image/runs-api-health-check.mjs +19 -8
- package/lib/sellable-agent/hermes-memory-snapshot.mjs +37 -323
- package/lib/sellable-agent/host-bootstrap.mjs +1 -1
- package/lib/sellable-agent/host-worker.mjs +1 -1
- package/lib/sellable-agent/profile-materializer.mjs +1 -1
- package/lib/sellable-agent/provisioning-adapter.mjs +1 -1
- package/package.json +1 -1
|
@@ -16,10 +16,8 @@ import {
|
|
|
16
16
|
readdirSync,
|
|
17
17
|
renameSync,
|
|
18
18
|
rmSync,
|
|
19
|
-
statfsSync,
|
|
20
19
|
writeFileSync,
|
|
21
20
|
} from "node:fs";
|
|
22
|
-
import { tmpdir } from "node:os";
|
|
23
21
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
24
22
|
import { promisify } from "node:util";
|
|
25
23
|
import { parseDocument } from "yaml";
|
|
@@ -213,18 +211,11 @@ export const HERMES_SNAPSHOT_MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
|
|
|
213
211
|
const MAX_PLUGIN_DATA_BYTES = 10 * 1024 * 1024;
|
|
214
212
|
const MAX_PORTABLE_EXTERNAL_BYTES = 32 * 1024 * 1024;
|
|
215
213
|
const MAX_PORTABLE_CONFIG_BYTES = 64 * 1024;
|
|
216
|
-
// Portable snapshots contain
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
// persistent retry scheduler back off instead of contending with the live
|
|
221
|
-
// Slack gateway for a full ten minutes.
|
|
214
|
+
// Portable snapshots contain only state that compounds (see
|
|
215
|
+
// HERMES_SNAPSHOT_USER_STATE_ENTRIES), a few megabytes. If native Hermes cannot
|
|
216
|
+
// package that within two minutes, abort the helper and let the persistent
|
|
217
|
+
// retry scheduler back off instead of contending with the live Slack gateway.
|
|
222
218
|
const NATIVE_COMMAND_TIMEOUT_MS = 120_000;
|
|
223
|
-
// A healthy established Admin state DB can exceed 500 MiB. Keep compaction
|
|
224
|
-
// bounded inside the 15-minute Upgrade backup stage without killing SQLite's
|
|
225
|
-
// final VACUUM on shared Fly CPU.
|
|
226
|
-
const STATE_DB_COMPACTION_TIMEOUT_MS = 600_000;
|
|
227
|
-
export const HERMES_SNAPSHOT_CRON_SESSION_RETENTION_HOURS = 48;
|
|
228
219
|
const TRANSIENT_PROFILE_ROOTS = new Set([
|
|
229
220
|
".artifact-venv",
|
|
230
221
|
".cache",
|
|
@@ -259,11 +250,11 @@ export function isHermesTransientProfilePath(relativePath) {
|
|
|
259
250
|
TRANSIENT_PROFILE_PATHS.has(relativePath)
|
|
260
251
|
);
|
|
261
252
|
}
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
253
|
+
// Durable profile entries the pre-profile layout kept directly under
|
|
254
|
+
// HERMES_HOME. The one-time legacy move carries all of them, run history
|
|
255
|
+
// included, because it moves live state on the volume rather than packing a
|
|
256
|
+
// recovery archive.
|
|
257
|
+
const HERMES_LEGACY_USER_STATE_ENTRIES = Object.freeze([
|
|
267
258
|
"AGENTS.md",
|
|
268
259
|
"SOUL.md",
|
|
269
260
|
"artifacts",
|
|
@@ -286,6 +277,26 @@ export const HERMES_SNAPSHOT_USER_STATE_ENTRIES = Object.freeze([
|
|
|
286
277
|
"state.db",
|
|
287
278
|
"workspace",
|
|
288
279
|
]);
|
|
280
|
+
// The encrypted Fly volume remains the authoritative full profile and keeps
|
|
281
|
+
// everything across an in-place Upgrade. The portable archive, used only to
|
|
282
|
+
// rebuild an Agent on a replacement volume, carries just the state that
|
|
283
|
+
// compounds: who the Agent is (SOUL, AGENTS), what it remembers, the skills it
|
|
284
|
+
// learned, its scheduled work (cron and the scripts cron runs), hooks and
|
|
285
|
+
// plugins. Run history (state.db, sessions), task boards and scratch areas
|
|
286
|
+
// never travel: a rebuilt Agent starts fresh conversations with all of the
|
|
287
|
+
// above. Caches, dependency trees, secrets and live logs remain excluded.
|
|
288
|
+
export const HERMES_SNAPSHOT_USER_STATE_ENTRIES = Object.freeze([
|
|
289
|
+
"AGENTS.md",
|
|
290
|
+
"SOUL.md",
|
|
291
|
+
"cron",
|
|
292
|
+
"hooks",
|
|
293
|
+
"memories",
|
|
294
|
+
"memory",
|
|
295
|
+
"plugin-data",
|
|
296
|
+
"plugins",
|
|
297
|
+
"scripts",
|
|
298
|
+
"skills",
|
|
299
|
+
]);
|
|
289
300
|
export const HERMES_SNAPSHOT_RETRY_BACKOFF_MS = Object.freeze([
|
|
290
301
|
15 * 60_000,
|
|
291
302
|
60 * 60_000,
|
|
@@ -998,272 +1009,6 @@ const childRunner = (runChild) => runChild ?? runBoundedHermesChild;
|
|
|
998
1009
|
const childFailed = (result) =>
|
|
999
1010
|
result?.exitCode !== 0 || result?.timedOut || result?.outputLimitExceeded;
|
|
1000
1011
|
|
|
1001
|
-
const STATE_DB_COMPACTION_SCRIPT = String.raw`
|
|
1002
|
-
import json
|
|
1003
|
-
import os
|
|
1004
|
-
import sqlite3
|
|
1005
|
-
import sys
|
|
1006
|
-
import time
|
|
1007
|
-
|
|
1008
|
-
source_path, target_path = sys.argv[1], sys.argv[2]
|
|
1009
|
-
retention_hours = int(sys.argv[3])
|
|
1010
|
-
# VACUUM INTO writes one consistent, compact copy of the live pages only, so
|
|
1011
|
-
# space the live database has freed never travels with the backup.
|
|
1012
|
-
source = sqlite3.connect("file:" + source_path + "?mode=ro", uri=True)
|
|
1013
|
-
try:
|
|
1014
|
-
source.execute("VACUUM INTO ?", (target_path,))
|
|
1015
|
-
finally:
|
|
1016
|
-
source.close()
|
|
1017
|
-
destination = sqlite3.connect(target_path)
|
|
1018
|
-
|
|
1019
|
-
try:
|
|
1020
|
-
if destination.execute("PRAGMA quick_check").fetchone()[0] != "ok":
|
|
1021
|
-
raise RuntimeError("source backup failed quick_check")
|
|
1022
|
-
destination.execute("PRAGMA foreign_keys=ON")
|
|
1023
|
-
table_names = {
|
|
1024
|
-
row[0]
|
|
1025
|
-
for row in destination.execute(
|
|
1026
|
-
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
1027
|
-
)
|
|
1028
|
-
}
|
|
1029
|
-
pruned_sessions = 0
|
|
1030
|
-
pruned_messages = 0
|
|
1031
|
-
if {"sessions", "messages"}.issubset(table_names):
|
|
1032
|
-
cutoff = time.time() - retention_hours * 60 * 60
|
|
1033
|
-
session_columns = {
|
|
1034
|
-
row[1] for row in destination.execute("PRAGMA table_info(sessions)")
|
|
1035
|
-
}
|
|
1036
|
-
pinned_clause = (
|
|
1037
|
-
"AND COALESCE(pinned, 0) = 0" if "pinned" in session_columns else ""
|
|
1038
|
-
)
|
|
1039
|
-
destination.execute(
|
|
1040
|
-
"CREATE TEMP TABLE snapshot_pruned_sessions(id TEXT PRIMARY KEY)"
|
|
1041
|
-
)
|
|
1042
|
-
destination.execute(
|
|
1043
|
-
"WITH RECURSIVE doomed(id) AS ("
|
|
1044
|
-
# Hermes never ends an endpoint run's (api_server) session; past
|
|
1045
|
-
# the retention it is as finished as an ended cron session.
|
|
1046
|
-
" SELECT id FROM sessions"
|
|
1047
|
-
" WHERE ((source = 'cron' AND ended_at IS NOT NULL)"
|
|
1048
|
-
" OR source = 'api_server')"
|
|
1049
|
-
" AND COALESCE(last_activity_at, ended_at, started_at) < ? "
|
|
1050
|
-
+ pinned_clause
|
|
1051
|
-
+ " UNION"
|
|
1052
|
-
" SELECT child.id FROM sessions child"
|
|
1053
|
-
" JOIN doomed parent ON child.parent_session_id = parent.id"
|
|
1054
|
-
") INSERT OR IGNORE INTO snapshot_pruned_sessions(id)"
|
|
1055
|
-
" SELECT id FROM doomed",
|
|
1056
|
-
(cutoff,),
|
|
1057
|
-
)
|
|
1058
|
-
pruned_sessions = destination.execute(
|
|
1059
|
-
"SELECT COUNT(*) FROM snapshot_pruned_sessions"
|
|
1060
|
-
).fetchone()[0]
|
|
1061
|
-
pruned_messages = destination.execute(
|
|
1062
|
-
"SELECT COUNT(*) FROM messages WHERE session_id IN "
|
|
1063
|
-
"(SELECT id FROM snapshot_pruned_sessions)"
|
|
1064
|
-
).fetchone()[0]
|
|
1065
|
-
destination.execute(
|
|
1066
|
-
"DELETE FROM messages WHERE session_id IN "
|
|
1067
|
-
"(SELECT id FROM snapshot_pruned_sessions)"
|
|
1068
|
-
)
|
|
1069
|
-
if "session_model_usage" in table_names:
|
|
1070
|
-
destination.execute(
|
|
1071
|
-
"DELETE FROM session_model_usage WHERE session_id IN "
|
|
1072
|
-
"(SELECT id FROM snapshot_pruned_sessions)"
|
|
1073
|
-
)
|
|
1074
|
-
destination.execute(
|
|
1075
|
-
"DELETE FROM sessions WHERE id IN "
|
|
1076
|
-
"(SELECT id FROM snapshot_pruned_sessions)"
|
|
1077
|
-
)
|
|
1078
|
-
if "system_prompts" in table_names:
|
|
1079
|
-
destination.execute(
|
|
1080
|
-
"DELETE FROM system_prompts WHERE NOT EXISTS ("
|
|
1081
|
-
" SELECT 1 FROM sessions"
|
|
1082
|
-
" WHERE sessions.system_prompt_hash = system_prompts.hash"
|
|
1083
|
-
")"
|
|
1084
|
-
)
|
|
1085
|
-
destination.commit()
|
|
1086
|
-
if pruned_sessions:
|
|
1087
|
-
for table in ("messages_fts", "messages_fts_trigram", "messages_fts_cjk"):
|
|
1088
|
-
if table in table_names:
|
|
1089
|
-
destination.execute(
|
|
1090
|
-
'INSERT INTO "' + table + '" ("' + table + '") VALUES (\'optimize\')'
|
|
1091
|
-
)
|
|
1092
|
-
destination.commit()
|
|
1093
|
-
destination.execute("VACUUM")
|
|
1094
|
-
if destination.execute("PRAGMA quick_check").fetchone()[0] != "ok":
|
|
1095
|
-
raise RuntimeError("compacted backup failed quick_check")
|
|
1096
|
-
if destination.execute("PRAGMA foreign_key_check").fetchone() is not None:
|
|
1097
|
-
raise RuntimeError("compacted backup failed foreign_key_check")
|
|
1098
|
-
finally:
|
|
1099
|
-
destination.close()
|
|
1100
|
-
|
|
1101
|
-
os.chmod(target_path, 0o600)
|
|
1102
|
-
print(json.dumps({
|
|
1103
|
-
"ok": True,
|
|
1104
|
-
"prunedSessions": pruned_sessions,
|
|
1105
|
-
"prunedMessages": pruned_messages,
|
|
1106
|
-
"retentionHours": retention_hours,
|
|
1107
|
-
"archiveStateBytes": os.path.getsize(target_path),
|
|
1108
|
-
}, separators=(",", ":")))
|
|
1109
|
-
`;
|
|
1110
|
-
|
|
1111
|
-
// Room the volume keeps after the compact copy lands, so a backup never
|
|
1112
|
-
// leaves the live gateway without space to write.
|
|
1113
|
-
const STATE_DB_VOLUME_HEADROOM_BYTES = 64 * 1024 * 1024;
|
|
1114
|
-
|
|
1115
|
-
function freeBytesAt(directory) {
|
|
1116
|
-
const stats = statfsSync(directory);
|
|
1117
|
-
return stats.bavail * stats.bsize;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
const compactionFailure = (reason) =>
|
|
1121
|
-
new Error("hermes_snapshot_state_db_compaction_failed", { cause: reason });
|
|
1122
|
-
|
|
1123
|
-
// A bounded reason for the operator log; never the child's raw output.
|
|
1124
|
-
function compactionChildFailureReason(result) {
|
|
1125
|
-
if (result?.timedOut) return "timeout";
|
|
1126
|
-
if (result?.outputLimitExceeded) return "output_limit";
|
|
1127
|
-
const stderr = String(result?.stderr ?? "");
|
|
1128
|
-
if (/database or disk is full|No space left on device/i.test(stderr)) {
|
|
1129
|
-
return "disk_full";
|
|
1130
|
-
}
|
|
1131
|
-
if (/quick_check|foreign_key_check/.test(stderr)) return "integrity";
|
|
1132
|
-
if (/sqlite3\.\w*Error/.test(stderr)) return "sqlite_error";
|
|
1133
|
-
return "child_failed";
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
export async function compactHermesSnapshotStateDb({
|
|
1137
|
-
sourcePath,
|
|
1138
|
-
targetPath,
|
|
1139
|
-
retentionHours = HERMES_SNAPSHOT_CRON_SESSION_RETENTION_HOURS,
|
|
1140
|
-
hermesPython = HERMES_PYTHON,
|
|
1141
|
-
scratchDirectory = tmpdir(),
|
|
1142
|
-
volumeFreeBytes = freeBytesAt,
|
|
1143
|
-
runChild = /** @type {typeof runBoundedHermesChild | undefined} */ (
|
|
1144
|
-
undefined
|
|
1145
|
-
),
|
|
1146
|
-
}) {
|
|
1147
|
-
if (
|
|
1148
|
-
!isAbsolute(sourcePath) ||
|
|
1149
|
-
!isAbsolute(targetPath) ||
|
|
1150
|
-
!isAbsolute(scratchDirectory) ||
|
|
1151
|
-
resolve(sourcePath) === resolve(targetPath) ||
|
|
1152
|
-
!existsSync(sourcePath) ||
|
|
1153
|
-
existsSync(targetPath) ||
|
|
1154
|
-
!existsSync(scratchDirectory) ||
|
|
1155
|
-
!Number.isSafeInteger(retentionHours) ||
|
|
1156
|
-
retentionHours < 1 ||
|
|
1157
|
-
retentionHours > 30 * 24
|
|
1158
|
-
) {
|
|
1159
|
-
throw new Error("hermes_snapshot_state_db_compaction_rejected");
|
|
1160
|
-
}
|
|
1161
|
-
regularFile(sourcePath);
|
|
1162
|
-
const scratchStat = lstatSync(scratchDirectory);
|
|
1163
|
-
if (scratchStat.isSymbolicLink() || !scratchStat.isDirectory()) {
|
|
1164
|
-
throw new Error("hermes_snapshot_state_db_compaction_rejected");
|
|
1165
|
-
}
|
|
1166
|
-
mkdirSync(dirname(targetPath), { recursive: true, mode: 0o700 });
|
|
1167
|
-
let scratchRoot;
|
|
1168
|
-
let finalTemporaryPath;
|
|
1169
|
-
let targetCreated = false;
|
|
1170
|
-
try {
|
|
1171
|
-
// Keep SQLite's destination plus VACUUM journal on the Machine's
|
|
1172
|
-
// ephemeral root filesystem. Building both beside the protected staging
|
|
1173
|
-
// tree can briefly require multiple full state.db copies on the small
|
|
1174
|
-
// persistent Fly volume and strand an otherwise healthy Upgrade at
|
|
1175
|
-
// BACKING_UP with ENOSPC. Only the verified compact database crosses back
|
|
1176
|
-
// onto the volume.
|
|
1177
|
-
scratchRoot = mkdtempSync(
|
|
1178
|
-
join(scratchDirectory, ".sellable-hermes-state-db-")
|
|
1179
|
-
);
|
|
1180
|
-
chmodSync(scratchRoot, 0o700);
|
|
1181
|
-
const scratchTargetPath = join(scratchRoot, "state.db");
|
|
1182
|
-
const result = await childRunner(runChild)({
|
|
1183
|
-
command: hermesPython,
|
|
1184
|
-
args: [
|
|
1185
|
-
"-c",
|
|
1186
|
-
STATE_DB_COMPACTION_SCRIPT,
|
|
1187
|
-
sourcePath,
|
|
1188
|
-
scratchTargetPath,
|
|
1189
|
-
String(retentionHours),
|
|
1190
|
-
],
|
|
1191
|
-
env: { ...process.env, NO_COLOR: "1" },
|
|
1192
|
-
shell: false,
|
|
1193
|
-
timeoutMs: STATE_DB_COMPACTION_TIMEOUT_MS,
|
|
1194
|
-
maxOutputBytes: 64 * 1024,
|
|
1195
|
-
});
|
|
1196
|
-
if (childFailed(result) || !existsSync(scratchTargetPath)) {
|
|
1197
|
-
throw compactionFailure(compactionChildFailureReason(result));
|
|
1198
|
-
}
|
|
1199
|
-
const scratchTarget = regularFile(scratchTargetPath);
|
|
1200
|
-
const summary = JSON.parse(String(result.stdout ?? ""));
|
|
1201
|
-
if (
|
|
1202
|
-
summary?.ok !== true ||
|
|
1203
|
-
summary.retentionHours !== retentionHours ||
|
|
1204
|
-
!Number.isSafeInteger(summary.prunedSessions) ||
|
|
1205
|
-
summary.prunedSessions < 0 ||
|
|
1206
|
-
!Number.isSafeInteger(summary.prunedMessages) ||
|
|
1207
|
-
summary.prunedMessages < 0 ||
|
|
1208
|
-
summary.archiveStateBytes !== scratchTarget.size
|
|
1209
|
-
) {
|
|
1210
|
-
throw compactionFailure("invalid_result");
|
|
1211
|
-
}
|
|
1212
|
-
// Only this compact copy lands on the persistent volume (the rest of the
|
|
1213
|
-
// staged profile is hard links). Refuse up front when it cannot fit,
|
|
1214
|
-
// rather than half-writing it and failing as a generic error.
|
|
1215
|
-
if (
|
|
1216
|
-
volumeFreeBytes(dirname(targetPath)) <
|
|
1217
|
-
scratchTarget.size + STATE_DB_VOLUME_HEADROOM_BYTES
|
|
1218
|
-
) {
|
|
1219
|
-
throw compactionFailure("disk_full");
|
|
1220
|
-
}
|
|
1221
|
-
finalTemporaryPath = join(
|
|
1222
|
-
dirname(targetPath),
|
|
1223
|
-
`.${basename(targetPath)}.${randomUUID()}`
|
|
1224
|
-
);
|
|
1225
|
-
copyFileSync(
|
|
1226
|
-
scratchTargetPath,
|
|
1227
|
-
finalTemporaryPath,
|
|
1228
|
-
constants.COPYFILE_EXCL
|
|
1229
|
-
);
|
|
1230
|
-
chmodSync(finalTemporaryPath, 0o600);
|
|
1231
|
-
const targetFd = openSync(finalTemporaryPath, constants.O_RDONLY);
|
|
1232
|
-
try {
|
|
1233
|
-
fsyncSync(targetFd);
|
|
1234
|
-
} finally {
|
|
1235
|
-
closeSync(targetFd);
|
|
1236
|
-
}
|
|
1237
|
-
// link(2) gives this staged publication no-replace semantics. If another
|
|
1238
|
-
// writer created targetPath after admission, fail without deleting or
|
|
1239
|
-
// overwriting the other writer's file.
|
|
1240
|
-
linkSync(finalTemporaryPath, targetPath);
|
|
1241
|
-
targetCreated = true;
|
|
1242
|
-
rmSync(finalTemporaryPath, { force: true });
|
|
1243
|
-
finalTemporaryPath = undefined;
|
|
1244
|
-
fsyncDirectory(dirname(targetPath));
|
|
1245
|
-
if (regularFile(targetPath).size !== scratchTarget.size) {
|
|
1246
|
-
throw new Error("hermes_snapshot_state_db_compaction_failed");
|
|
1247
|
-
}
|
|
1248
|
-
return summary;
|
|
1249
|
-
} catch (error) {
|
|
1250
|
-
if (targetCreated) rmSync(targetPath, { force: true });
|
|
1251
|
-
const reason =
|
|
1252
|
-
error?.cause ?? (error?.code === "ENOSPC" ? "disk_full" : "copy_failed");
|
|
1253
|
-
process.stderr.write(
|
|
1254
|
-
`${JSON.stringify({
|
|
1255
|
-
ok: false,
|
|
1256
|
-
code: "hermes_snapshot_state_db_compaction_failed",
|
|
1257
|
-
reason,
|
|
1258
|
-
})}\n`
|
|
1259
|
-
);
|
|
1260
|
-
throw new Error("hermes_snapshot_state_db_compaction_failed");
|
|
1261
|
-
} finally {
|
|
1262
|
-
if (finalTemporaryPath) rmSync(finalTemporaryPath, { force: true });
|
|
1263
|
-
if (scratchRoot) rmSync(scratchRoot, { recursive: true, force: true });
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
1012
|
function nativeSummaryAccepted(result, complete, incomplete) {
|
|
1268
1013
|
const output = `${result?.stdout ?? ""}\n${result?.stderr ?? ""}`;
|
|
1269
1014
|
return (
|
|
@@ -1387,8 +1132,8 @@ function stageHermesUserState({ profileId, profileRoot, hermesRoot }) {
|
|
|
1387
1132
|
}
|
|
1388
1133
|
const target = join(stagedProfileRoot, name);
|
|
1389
1134
|
// Hermes rejects the live SOUL when its link count is not exactly one.
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1135
|
+
// A capture can run for a while, so a staged hard link would make
|
|
1136
|
+
// otherwise healthy Agent turns fail for the whole capture window.
|
|
1392
1137
|
copyFileSync(source, target, constants.COPYFILE_EXCL);
|
|
1393
1138
|
chmodSync(target, 0o600);
|
|
1394
1139
|
if (regularFile(target).nlink !== 1) {
|
|
@@ -1396,10 +1141,6 @@ function stageHermesUserState({ profileId, profileRoot, hermesRoot }) {
|
|
|
1396
1141
|
}
|
|
1397
1142
|
continue;
|
|
1398
1143
|
}
|
|
1399
|
-
// state.db is copied online and compacted after the rest of the staged
|
|
1400
|
-
// tree is assembled. Never hard-link this one file: pruning the staged
|
|
1401
|
-
// recovery copy must be physically incapable of touching the live DB.
|
|
1402
|
-
if (name === "state.db") continue;
|
|
1403
1144
|
stageUserStateEntry(
|
|
1404
1145
|
source,
|
|
1405
1146
|
join(stagedProfileRoot, name),
|
|
@@ -1411,20 +1152,6 @@ function stageHermesUserState({ profileId, profileRoot, hermesRoot }) {
|
|
|
1411
1152
|
}
|
|
1412
1153
|
: null
|
|
1413
1154
|
);
|
|
1414
|
-
// Keep live SQLite sidecars beside their database hard link so Hermes'
|
|
1415
|
-
// sqlite3.backup() observes committed WAL data. Hermes excludes the
|
|
1416
|
-
// sidecars themselves from the resulting ZIP.
|
|
1417
|
-
if (name.endsWith(".db")) {
|
|
1418
|
-
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
|
1419
|
-
const sidecar = `${source}${suffix}`;
|
|
1420
|
-
if (existsSync(sidecar)) {
|
|
1421
|
-
stageUserStateEntry(
|
|
1422
|
-
sidecar,
|
|
1423
|
-
join(stagedProfileRoot, `${name}${suffix}`)
|
|
1424
|
-
);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
1155
|
}
|
|
1429
1156
|
}
|
|
1430
1157
|
for (const external of HERMES_SNAPSHOT_EXTERNAL_USER_STATE) {
|
|
@@ -1485,7 +1212,7 @@ function readLegacyUserStateMigration(path, profileId) {
|
|
|
1485
1212
|
value.entries.some(
|
|
1486
1213
|
(name) =>
|
|
1487
1214
|
typeof name !== "string" ||
|
|
1488
|
-
!
|
|
1215
|
+
!HERMES_LEGACY_USER_STATE_ENTRIES.includes(name)
|
|
1489
1216
|
) ||
|
|
1490
1217
|
new Set(value.entries).size !== value.entries.length
|
|
1491
1218
|
) {
|
|
@@ -1501,9 +1228,9 @@ function readLegacyUserStateMigration(path, profileId) {
|
|
|
1501
1228
|
* (for example /opt/data/skills and /opt/data/cron). Profile isolation moved
|
|
1502
1229
|
* those paths under profiles/<id>. Snapshot restore cannot discover that
|
|
1503
1230
|
* unscoped state because it intentionally restores profile archives only.
|
|
1504
|
-
* Stage only the
|
|
1505
|
-
*
|
|
1506
|
-
*
|
|
1231
|
+
* Stage only the bounded legacy user-state allowlist, merge without
|
|
1232
|
+
* overwriting newer profile bytes, then mark the resulting profile dirty so the
|
|
1233
|
+
* next portable snapshot becomes self-contained.
|
|
1507
1234
|
*/
|
|
1508
1235
|
export function migrateLegacyHermesUserState({
|
|
1509
1236
|
profileId,
|
|
@@ -1524,7 +1251,7 @@ export function migrateLegacyHermesUserState({
|
|
|
1524
1251
|
entries: receipt.entries,
|
|
1525
1252
|
};
|
|
1526
1253
|
}
|
|
1527
|
-
const entries =
|
|
1254
|
+
const entries = HERMES_LEGACY_USER_STATE_ENTRIES.filter((name) =>
|
|
1528
1255
|
existsSync(join(hermesRoot, name))
|
|
1529
1256
|
);
|
|
1530
1257
|
if (entries.length === 0) {
|
|
@@ -2565,10 +2292,7 @@ export async function captureHermesBackup({
|
|
|
2565
2292
|
hermesVersion,
|
|
2566
2293
|
archiveDirectory = "/tmp",
|
|
2567
2294
|
hermesCli = HERMES,
|
|
2568
|
-
hermesPython = HERMES_PYTHON,
|
|
2569
2295
|
maxArchiveBytes = HERMES_SNAPSHOT_MAX_ARCHIVE_BYTES,
|
|
2570
|
-
cronSessionRetentionHours = HERMES_SNAPSHOT_CRON_SESSION_RETENTION_HOURS,
|
|
2571
|
-
stateDbCompactor = compactHermesSnapshotStateDb,
|
|
2572
2296
|
runChild,
|
|
2573
2297
|
operationIdentity = /** @type {any} */ (undefined),
|
|
2574
2298
|
}) {
|
|
@@ -2607,16 +2331,6 @@ export async function captureHermesBackup({
|
|
|
2607
2331
|
try {
|
|
2608
2332
|
stagingRoot = stageHermesUserState({ profileId, profileRoot, hermesRoot });
|
|
2609
2333
|
const stagedProfileRoot = join(stagingRoot, "profiles", profileId);
|
|
2610
|
-
const sourceStateDb = join(profileRoot, "state.db");
|
|
2611
|
-
if (existsSync(sourceStateDb)) {
|
|
2612
|
-
await stateDbCompactor({
|
|
2613
|
-
sourcePath: sourceStateDb,
|
|
2614
|
-
targetPath: join(stagedProfileRoot, "state.db"),
|
|
2615
|
-
retentionHours: cronSessionRetentionHours,
|
|
2616
|
-
hermesPython,
|
|
2617
|
-
runChild,
|
|
2618
|
-
});
|
|
2619
|
-
}
|
|
2620
2334
|
assertStagedCronDependencies(stagedProfileRoot);
|
|
2621
2335
|
const result = await childRunner(runChild)(
|
|
2622
2336
|
invocation(["backup", "-o", archivePath], stagingRoot, hermesCli)
|
|
@@ -2652,7 +2366,7 @@ export async function captureHermesBackup({
|
|
|
2652
2366
|
} catch (error) {
|
|
2653
2367
|
const code = error instanceof Error ? error.message : "";
|
|
2654
2368
|
return failed(
|
|
2655
|
-
/^hermes_snapshot_(?:external_state|plugin_data|portable_config|dependency|
|
|
2369
|
+
/^hermes_snapshot_(?:external_state|plugin_data|portable_config|dependency|soul_metadata)/.test(
|
|
2656
2370
|
code
|
|
2657
2371
|
)
|
|
2658
2372
|
? code
|
|
@@ -38,7 +38,7 @@ import { installAgentHostServices } from "./service-installer.mjs";
|
|
|
38
38
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
39
39
|
const VERSION = "sellable-agent-host-bootstrap/v1";
|
|
40
40
|
const RECEIPT_VERSION = "sellable-agent-host-registration/v1";
|
|
41
|
-
const INSTALLER_PACKAGE = "@sellable/install@0.1.
|
|
41
|
+
const INSTALLER_PACKAGE = "@sellable/install@0.1.802";
|
|
42
42
|
const MCP_PACKAGE = "@sellable/mcp@0.1.957";
|
|
43
43
|
const HERMES_VERSION = "0.18.0";
|
|
44
44
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
@@ -1546,7 +1546,7 @@ export function compileClaimToProfileDesired(activeClaim, config) {
|
|
|
1546
1546
|
]) ||
|
|
1547
1547
|
pinned.hermesCli !== "hermes" ||
|
|
1548
1548
|
pinned.hermesVersion !== "0.18.0" ||
|
|
1549
|
-
pinned.installerPackage !== "@sellable/install@0.1.
|
|
1549
|
+
pinned.installerPackage !== "@sellable/install@0.1.802" ||
|
|
1550
1550
|
pinned.mcpPackage !== "@sellable/mcp@0.1.957" ||
|
|
1551
1551
|
!Array.isArray(policy.toolInclude) ||
|
|
1552
1552
|
policy.toolInclude.length === 0 ||
|
|
@@ -312,7 +312,7 @@ function validateDesired(desired) {
|
|
|
312
312
|
desired.serviceCredentialGeneration < 1 ||
|
|
313
313
|
desired.hermesCli !== "hermes" ||
|
|
314
314
|
desired.hermesVersion !== "0.18.0" ||
|
|
315
|
-
desired.installerPackage !== "@sellable/install@0.1.
|
|
315
|
+
desired.installerPackage !== "@sellable/install@0.1.802" ||
|
|
316
316
|
desired.mcpPackage !== "@sellable/mcp@0.1.957" ||
|
|
317
317
|
!Array.isArray(desired.toolInclude) ||
|
|
318
318
|
desired.toolInclude.length === 0 ||
|
|
@@ -25,7 +25,7 @@ import { fileURLToPath } from "node:url";
|
|
|
25
25
|
import { deriveContainedProfileId } from "./profile-materializer.mjs";
|
|
26
26
|
|
|
27
27
|
export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
|
|
28
|
-
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.
|
|
28
|
+
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.802";
|
|
29
29
|
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.957";
|
|
30
30
|
const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
|
|
31
31
|
PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
|