@sellable/install 0.1.792 → 0.1.794

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.
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
3
3
 
4
4
  USER root
5
5
 
6
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.792
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.794
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.957
8
8
  ARG INSTALLER_INTEGRITY
9
9
  ARG MCP_INTEGRITY=sha512-tIZuO5IEanT/afamDIWdorUNMdqZA6xnjL9jGAmGqYIlIqkXdHB9slWiwfLfmM44iEp7OT98IBHJlvbpMThk6A==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.792" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.794" \
12
12
  && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.957" \
13
13
  && test -n "${INSTALLER_INTEGRITY}" \
14
14
  && test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.792" \
23
23
  && mkdir -p /opt/sellable-agent/install-root /opt/sellable-agent/mcp-root \
24
24
  && npm install --prefix /opt/sellable-agent/install-root --omit=dev --ignore-scripts --no-audit --no-fund "${INSTALLER_PACKAGE}" \
25
25
  && npm install --prefix /opt/sellable-agent/mcp-root --omit=dev --ignore-scripts --no-audit --no-fund "${MCP_PACKAGE}" \
26
- && node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.792' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.957') throw new Error('package identity rejected');" \
26
+ && node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.794' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.957') throw new Error('package identity rejected');" \
27
27
  && node --input-type=module -e "import { buildExternalRuntimeClosure } from '/opt/sellable-agent/install-root/node_modules/@sellable/install/lib/sellable-agent/external-runtime-builder.mjs'; const built = buildExternalRuntimeClosure({ mcpPackageRoot: '/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp', mcpNodeModulesRoot: '/opt/sellable-agent/mcp-root/node_modules', hermesSourceRoot: '/opt/hermes', ownerUid: 0, ownerGid: 0 }); if (!built.closureDigest) throw new Error('runtime closure rejected');" \
28
- && node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.792', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.957', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
28
+ && node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.794', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.957', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
29
29
  && npm cache clean --force
30
30
 
31
31
  COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
@@ -3,7 +3,7 @@
3
3
  This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
4
4
 
5
5
  Build from this directory after
6
- `@sellable/install@0.1.792` is
6
+ `@sellable/install@0.1.794` is
7
7
  published:
8
8
 
9
9
  ```sh
@@ -62,6 +62,7 @@ import {
62
62
  sellableSlackToolsetsForMaterialization,
63
63
  } from "../fly-capabilities-bridge.mjs";
64
64
  import { createCustomerEndpointRelay } from "../fly-customer-image/customer-runtime.mjs";
65
+ import { createHermesSessionRetention } from "../fly-customer-image/hermes-runs-client.mjs";
65
66
  import {
66
67
  claimWithSoulObservation,
67
68
  createFlyCustomerControlClient,
@@ -88,12 +89,12 @@ import {
88
89
  HERMES_SNAPSHOT_HOOK_ALLOWLIST_BYTES,
89
90
  applyHermesSellableHostedPolicy,
90
91
  ensureHermesClientRelationshipsCrons,
91
- ensureHermesRefillSendsCron,
92
92
  ensureHermesCustomerProgramCrons,
93
93
  ensureHermesOpportunitySequencesCron,
94
+ ensureHermesRefillSendsCron,
94
95
  ensureHermesSnapshotCron,
95
- markHermesSnapshotDirty,
96
96
  linkHermesState,
97
+ markHermesSnapshotDirty,
97
98
  migrateLegacyHermesUserState,
98
99
  quarantineLegacyLightfieldVenv,
99
100
  reconcileHermesSnapshot,
@@ -142,6 +143,7 @@ const PROFILES_ROOT = join(DATA_ROOT, "profiles");
142
143
  const RUNTIME_ROOT = join(DATA_ROOT, ".sellable-agent", "admin-runtime");
143
144
  const DELIVERY_ROOT = join(RUNTIME_ROOT, "delivery");
144
145
  const SECRET_ROOT = "/run/sellable-agent/admin-runtime";
146
+ const sessionRetention = createHermesSessionRetention();
145
147
  // Turn descriptor + consumed roots for the Product MCP context proxy. They are
146
148
  // normally empty and receive one signed descriptor only while the fenced
147
149
  // endpoint relay owns a turn; the proxy fails closed if either root is absent.
@@ -3972,6 +3974,14 @@ async function runAdminProduction() {
3972
3974
  bytes
3973
3975
  ),
3974
3976
  });
3977
+ void sessionRetention.tick({
3978
+ apiKeyFile: join(SECRET_ROOT, "api-server-key"),
3979
+ statePath: join(
3980
+ activeLaunch.profile.profileHome,
3981
+ ".sellable-agent",
3982
+ "session-retention.json"
3983
+ ),
3984
+ });
3975
3985
  }
3976
3986
  await sleep(1_000);
3977
3987
  continue;
@@ -8,7 +8,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
8
8
 
9
9
  ARG MCP_PACKAGE=@sellable/mcp@0.1.957
10
10
  ARG MCP_INTEGRITY=sha512-tIZuO5IEanT/afamDIWdorUNMdqZA6xnjL9jGAmGqYIlIqkXdHB9slWiwfLfmM44iEp7OT98IBHJlvbpMThk6A==
11
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.792
11
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.794
12
12
  ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.202
13
13
  ARG ADMIN_MCP_INTEGRITY=sha512-ym3aye/0vQ8tfULJlKz9JFOxvIQsl1G+n3np+TTgK/tecwNblBo/j3AkTnRqH9iomKzPbZzP+n4tEQsvsU8ByA==
14
14
  ARG TUS_JS_CLIENT_VERSION=4.3.1
@@ -94,6 +94,7 @@ import {
94
94
  } from "./endpoint-uat-completion-gate.mjs";
95
95
  import {
96
96
  createHermesRunsClient,
97
+ createHermesSessionRetention,
97
98
  HERMES_RUNS_FIXTURE_IDENTITY,
98
99
  validateEndpointModelSelection,
99
100
  validateOpenerOutputContract,
@@ -106,6 +107,7 @@ import {
106
107
  const DATA_ROOT = "/opt/data";
107
108
  const PROFILES_ROOT = join(DATA_ROOT, "profiles");
108
109
  const SECRET_ROOT = "/run/sellable-agent";
110
+ const sessionRetention = createHermesSessionRetention();
109
111
  const TRUST_ROOT = "/usr/local/share/sellable-agent/mcp-turn-trust-root.pem";
110
112
  const HERMES = "/opt/hermes/.venv/bin/hermes";
111
113
  const HERMES_PYTHON = "/opt/hermes/.venv/bin/python3";
@@ -124,7 +126,7 @@ const RUNTIME_GENERATION = Number(
124
126
  const BOOT_SESSION_ID = randomUUID();
125
127
  const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.21.5";
126
128
  const INSTALLER_PACKAGE =
127
- process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.792";
129
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.794";
128
130
  const MCP_PACKAGE =
129
131
  process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.957";
130
132
  export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
@@ -1394,6 +1396,25 @@ export function endpointProductProjection(productExecution, output) {
1394
1396
 
1395
1397
  export function endpointOpenerOutputContract(claim) {
1396
1398
  if (!usesOpenerDraftStrategy(claim)) return undefined;
1399
+ return openerOutputContractFor(claim);
1400
+ }
1401
+
1402
+ // A single-run opener on a caller-named model (Opus on OpenRouter) is held to
1403
+ // the same strict schema, which the bridge sends as the provider's json_schema
1404
+ // so the model cannot return malformed JSON. The machine's default model has
1405
+ // no strict transport, so it gets no contract.
1406
+ export function endpointStrictOutputContract(claim, modelSelection) {
1407
+ if (
1408
+ !modelSelection ||
1409
+ usesOpenerDraftStrategy(claim) ||
1410
+ claim?.run?.productExecution?.workKind !== "OPENER" ||
1411
+ !claim.run.productExecution.providerOutputSchema
1412
+ )
1413
+ return undefined;
1414
+ return openerOutputContractFor(claim);
1415
+ }
1416
+
1417
+ function openerOutputContractFor(claim) {
1397
1418
  const execution = claim.run.productExecution;
1398
1419
  if (
1399
1420
  execution.nativeProposalSchemaVersion !==
@@ -2153,6 +2174,12 @@ export function createCustomerEndpointRelay(options = {}) {
2153
2174
  correlation: `${claim.attempt.conversationId}:${claim.attempt.runId}:${claim.attempt.id}`,
2154
2175
  ...(modelSelection ? { modelSelection } : {}),
2155
2176
  };
2177
+ const strictOutputContract = endpointStrictOutputContract(
2178
+ claim,
2179
+ modelSelection
2180
+ );
2181
+ if (strictOutputContract)
2182
+ submitRequest.outputContract = strictOutputContract;
2156
2183
  while (!submitted) {
2157
2184
  try {
2158
2185
  submitted = await runs.submit(submitRequest, {
@@ -4236,6 +4263,18 @@ async function main() {
4236
4263
  bytes
4237
4264
  ),
4238
4265
  });
4266
+ void sessionRetention.tick({
4267
+ apiKeyFile: join(
4268
+ SECRET_ROOT,
4269
+ activeRuntime.desired.profileId,
4270
+ "api-server-key"
4271
+ ),
4272
+ statePath: join(
4273
+ activeRuntime.profileRoot,
4274
+ ".sellable-agent",
4275
+ "session-retention.json"
4276
+ ),
4277
+ });
4239
4278
  }
4240
4279
  if (
4241
4280
  activeRuntime?.action !== "INSTANTIATE" &&
@@ -1,6 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
- import { lstatSync, readFileSync } from "node:fs";
3
- import { isAbsolute } from "node:path";
2
+ import {
3
+ lstatSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ renameSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { dirname, isAbsolute } from "node:path";
4
10
 
5
11
  const LOOPBACK_ORIGIN = "http://127.0.0.1:8642";
6
12
  const RUN_ID = /^run_[a-f0-9]{32}$/;
@@ -265,7 +271,8 @@ function compileCreateBody(input, openerStage = false) {
265
271
  "input",
266
272
  "instructions",
267
273
  "correlation",
268
- ...(openerStage ? ["outputContract"] : ["modelSelection"]),
274
+ "outputContract",
275
+ ...(openerStage ? [] : ["modelSelection"]),
269
276
  ];
270
277
  if (Object.keys(input).some((key) => !allowed.includes(key))) {
271
278
  throw new Error("hermes_runs_submit_rejected");
@@ -291,6 +298,24 @@ function compileCreateBody(input, openerStage = false) {
291
298
  input.modelSelection === undefined
292
299
  ? null
293
300
  : validateEndpointModelSelection(input.modelSelection);
301
+ // A per-run model's opener is held to the strict output contract by its
302
+ // provider (the bridge sends it as OpenRouter json_schema). Outside opener
303
+ // stages the contract only travels with an allowlisted model selection.
304
+ if (!openerStage && input.outputContract !== undefined && !modelSelection) {
305
+ throw new Error("hermes_runs_submit_rejected");
306
+ }
307
+ const outputContract =
308
+ input.outputContract === undefined
309
+ ? null
310
+ : validateOpenerOutputContract(input.outputContract);
311
+ const modelOptions = {
312
+ ...(modelSelection === null
313
+ ? {}
314
+ : { reasoning_effort: modelSelection.reasoningEffort }),
315
+ ...(outputContract === null
316
+ ? {}
317
+ : { sellable_opener_output_contract: outputContract }),
318
+ };
294
319
  return Object.freeze({
295
320
  input: compiledInput,
296
321
  instructions,
@@ -300,20 +325,10 @@ function compileCreateBody(input, openerStage = false) {
300
325
  // resolves the provider's credential from the machine's own pool.
301
326
  ...(modelSelection === null
302
327
  ? {}
303
- : {
304
- provider: modelSelection.provider,
305
- model: modelSelection.model,
306
- model_options: { reasoning_effort: modelSelection.reasoningEffort },
307
- }),
308
- ...(input.outputContract === undefined
328
+ : { provider: modelSelection.provider, model: modelSelection.model }),
329
+ ...(Object.keys(modelOptions).length === 0
309
330
  ? {}
310
- : {
311
- model_options: {
312
- sellable_opener_output_contract: validateOpenerOutputContract(
313
- input.outputContract
314
- ),
315
- },
316
- }),
331
+ : { model_options: modelOptions }),
317
332
  });
318
333
  }
319
334
 
@@ -887,3 +902,200 @@ export function createHermesRunsClient(options = {}) {
887
902
  },
888
903
  });
889
904
  }
905
+
906
+ // Endpoint runs (`api_server`) and cron jobs each leave a Hermes session that
907
+ // nothing reads again: an endpoint attempt gets a fresh session and its result
908
+ // lives in Sellable. Hermes only closes these after 90 idle days and deletes
909
+ // them after another 90, so state.db grows with every run, and so do the
910
+ // backup and the volume. Retention deletes them after a week. It goes through
911
+ // the running gateway's session API because Hermes refuses bulk deletes from a
912
+ // second writer, and it never touches conversations (Slack, Desktop, TUI).
913
+ export const HERMES_AUTOMATION_SESSION_SOURCES = Object.freeze([
914
+ "api_server",
915
+ "cron",
916
+ ]);
917
+ export const HERMES_AUTOMATION_SESSION_RETENTION_DAYS = 7;
918
+ const SESSION_PAGE_SIZE = 200;
919
+ const SESSION_RETENTION_MAX_DELETES = 1_000;
920
+ const SESSION_RETENTION_INTERVAL_MS = 24 * 60 * 60 * 1000;
921
+ const SESSION_RETENTION_RETRY_MS = 60 * 60 * 1000;
922
+ // A runtime that just started is still bringing its gateway up; the first pass
923
+ // waits so it does not fail against an API that is not serving yet.
924
+ const SESSION_RETENTION_START_DELAY_MS = 10 * 60 * 1000;
925
+
926
+ // A bounded reason for the operator log; never a raw error message.
927
+ function sessionRetentionFailureReason(error) {
928
+ const message = String(error?.message ?? "");
929
+ const http =
930
+ /^hermes_session_retention_(list|delete)_http_([1-5][0-9]{2})$/.exec(
931
+ message
932
+ );
933
+ if (http) return `${http[1]}_http_${http[2]}`;
934
+ if (
935
+ message === "hermes_runs_api_key_file_rejected" ||
936
+ error?.code === "ENOENT"
937
+ ) {
938
+ return "key_rejected";
939
+ }
940
+ if (error?.name === "TimeoutError" || error?.name === "AbortError") {
941
+ return "timeout";
942
+ }
943
+ return "request_failed";
944
+ }
945
+
946
+ function sessionLastActiveSeconds(session) {
947
+ return Math.max(
948
+ ...[session.last_active, session.ended_at, session.started_at].filter(
949
+ (value) => typeof value === "number" && Number.isFinite(value)
950
+ ),
951
+ 0
952
+ );
953
+ }
954
+
955
+ /** @param {any} options */
956
+ export async function pruneHermesAutomationSessions({
957
+ apiKeyFile,
958
+ fetchImpl = fetch,
959
+ now = Date.now(),
960
+ retentionDays = HERMES_AUTOMATION_SESSION_RETENTION_DAYS,
961
+ maxDeletes = SESSION_RETENTION_MAX_DELETES,
962
+ }) {
963
+ const apiKey = readApiKey(apiKeyFile);
964
+ const call = (method, path) =>
965
+ fetchImpl(`${LOOPBACK_ORIGIN}${path}`, {
966
+ method,
967
+ headers: { authorization: `Bearer ${apiKey}` },
968
+ signal: AbortSignal.timeout(20_000),
969
+ });
970
+ const cutoff = now / 1000 - retentionDays * 86_400;
971
+ const stale = new Set();
972
+ for (const source of HERMES_AUTOMATION_SESSION_SOURCES) {
973
+ // Newest activity first, so every page is read; listing stops once one
974
+ // pass worth of stale sessions is known.
975
+ for (
976
+ let offset = 0;
977
+ stale.size <= maxDeletes;
978
+ offset += SESSION_PAGE_SIZE
979
+ ) {
980
+ const response = await call(
981
+ "GET",
982
+ `/api/sessions?source=${source}&limit=${SESSION_PAGE_SIZE}&offset=${offset}`
983
+ );
984
+ if (!response.ok) {
985
+ throw new Error(
986
+ `hermes_session_retention_list_http_${response.status}`
987
+ );
988
+ }
989
+ const page = await response.json();
990
+ const rows = Array.isArray(page?.data) ? page.data : [];
991
+ for (const session of rows) {
992
+ if (
993
+ session?.source === source &&
994
+ typeof session.id === "string" &&
995
+ session.pinned !== true &&
996
+ sessionLastActiveSeconds(session) < cutoff
997
+ ) {
998
+ stale.add(session.id);
999
+ }
1000
+ }
1001
+ if (page?.has_more !== true || rows.length === 0) break;
1002
+ }
1003
+ }
1004
+ let deleted = 0;
1005
+ for (const id of [...stale].slice(0, maxDeletes)) {
1006
+ const response = await call(
1007
+ "DELETE",
1008
+ `/api/sessions/${encodeURIComponent(id)}`
1009
+ );
1010
+ if (response.ok) deleted += 1;
1011
+ else if (response.status !== 404) {
1012
+ throw new Error(
1013
+ `hermes_session_retention_delete_http_${response.status}`
1014
+ );
1015
+ }
1016
+ }
1017
+ return {
1018
+ ok: true,
1019
+ deleted,
1020
+ remaining: Math.max(0, stale.size - maxDeletes),
1021
+ };
1022
+ }
1023
+
1024
+ function readRetentionNextRunAt(statePath) {
1025
+ try {
1026
+ const value = JSON.parse(readFileSync(statePath, "utf8"))?.nextRunAt;
1027
+ return Number.isFinite(value) ? value : 0;
1028
+ } catch {
1029
+ return 0;
1030
+ }
1031
+ }
1032
+
1033
+ function writeRetentionNextRunAt(statePath, nextRunAt) {
1034
+ mkdirSync(dirname(statePath), { recursive: true, mode: 0o700 });
1035
+ const temporary = `${statePath}.${process.pid}.tmp`;
1036
+ writeFileSync(temporary, `${JSON.stringify({ nextRunAt })}\n`, {
1037
+ mode: 0o600,
1038
+ });
1039
+ renameSync(temporary, statePath);
1040
+ }
1041
+
1042
+ /**
1043
+ * Daily, single-flight session retention for a runtime's idle loop. `tick`
1044
+ * returns the running pass (or null when not due) and never rejects, so the
1045
+ * loop can fire it without waiting.
1046
+ * @param {any} options
1047
+ */
1048
+ export function createHermesSessionRetention({
1049
+ fetchImpl = fetch,
1050
+ now = () => Date.now(),
1051
+ maxDeletes = SESSION_RETENTION_MAX_DELETES,
1052
+ startDelayMs = SESSION_RETENTION_START_DELAY_MS,
1053
+ log = (entry) => process.stderr.write(`${JSON.stringify(entry)}\n`),
1054
+ } = {}) {
1055
+ let running = null;
1056
+ const startedAt = now();
1057
+ return {
1058
+ tick({ apiKeyFile, statePath }) {
1059
+ if (running) return running;
1060
+ if (now() < startedAt + startDelayMs) return null;
1061
+ if (now() < readRetentionNextRunAt(statePath)) return null;
1062
+ running = (async () => {
1063
+ let nextRunAt;
1064
+ try {
1065
+ const result = await pruneHermesAutomationSessions({
1066
+ apiKeyFile,
1067
+ fetchImpl,
1068
+ now: now(),
1069
+ maxDeletes,
1070
+ });
1071
+ log({
1072
+ ok: true,
1073
+ code: "hermes_session_retention_pruned",
1074
+ deleted: result.deleted,
1075
+ remaining: result.remaining,
1076
+ });
1077
+ nextRunAt =
1078
+ now() +
1079
+ (result.remaining > 0
1080
+ ? SESSION_RETENTION_RETRY_MS
1081
+ : SESSION_RETENTION_INTERVAL_MS);
1082
+ } catch (error) {
1083
+ log({
1084
+ ok: false,
1085
+ code: "hermes_session_retention_failed",
1086
+ reason: sessionRetentionFailureReason(error),
1087
+ });
1088
+ nextRunAt = now() + SESSION_RETENTION_RETRY_MS;
1089
+ }
1090
+ try {
1091
+ writeRetentionNextRunAt(statePath, nextRunAt);
1092
+ } catch {
1093
+ // The next tick simply runs again.
1094
+ }
1095
+ })().finally(() => {
1096
+ running = null;
1097
+ });
1098
+ return running;
1099
+ },
1100
+ };
1101
+ }
@@ -1244,13 +1244,16 @@ def _sellable_opener_output_format(model_options):
1244
1244
  reject()
1245
1245
  return {"type": "json_schema", "name": "sellable_opener_proposal", "strict": True, "schema": schema}
1246
1246
 
1247
+ def _sellable_opener_json_schema(output_format):
1248
+ # Chat-completions spelling of the same strict format (OpenRouter response_format).
1249
+ return {"type": "json_schema", "json_schema": {"name": output_format["name"], "strict": True, "schema": output_format["schema"]}}
1250
+
1247
1251
  def _sellable_opener_stage_kwargs(session_id, agent_kwargs, model_options=None):
1248
1252
  import os
1249
1253
  import re
1250
- stage = (
1251
- os.getenv("SELLABLE_AGENT_RUNTIME", "") == "1"
1252
- and re.fullmatch(r"sellable_opener_stage_[a-f0-9]{32}", str(session_id or "")) is not None
1253
- )
1254
+ runtime = os.getenv("SELLABLE_AGENT_RUNTIME", "") == "1"
1255
+ stage = runtime and re.fullmatch(r"sellable_opener_stage_[a-f0-9]{32}", str(session_id or "")) is not None
1256
+ endpoint = runtime and re.fullmatch(r"sellable_endpoint_[a-f0-9]{32}", str(session_id or "")) is not None
1254
1257
  if stage:
1255
1258
  agent_kwargs.update(
1256
1259
  enabled_toolsets=[], skip_memory=True, skip_background_review=True,
@@ -1265,6 +1268,17 @@ def _sellable_opener_stage_kwargs(session_id, agent_kwargs, model_options=None):
1265
1268
  extra_body["text"] = {"format": output_format}
1266
1269
  overrides["extra_body"] = extra_body
1267
1270
  agent_kwargs.update(request_overrides=overrides, fallback_model=None)
1271
+ elif endpoint and _sellable_opener_output_format(model_options) is not None:
1272
+ # A per-run model's endpoint session keeps its tools; only its output
1273
+ # is held to the strict schema, routed to providers that enforce it.
1274
+ output_format = _sellable_opener_output_format(model_options)
1275
+ overrides = dict(agent_kwargs.get("request_overrides") or {})
1276
+ extra_body = dict(overrides.get("extra_body") or {})
1277
+ if "text" in overrides or "text" in extra_body or "response_format" in overrides or "response_format" in extra_body:
1278
+ raise RuntimeError("sellable_opener_output_format_conflict")
1279
+ extra_body["response_format"] = _sellable_opener_json_schema(output_format)
1280
+ overrides["extra_body"] = extra_body
1281
+ agent_kwargs.update(request_overrides=overrides, fallback_model=None, provider_require_parameters=True)
1268
1282
  elif isinstance(model_options, dict) and "sellable_opener_output_contract" in model_options:
1269
1283
  raise RuntimeError("sellable_opener_output_contract_session_rejected")
1270
1284
  return stage
@@ -1272,20 +1286,27 @@ def _sellable_opener_stage_kwargs(session_id, agent_kwargs, model_options=None):
1272
1286
  def _sellable_assert_opener_stage_tools(stage, agent, model_options=None):
1273
1287
  if stage and (agent.tools or agent.valid_tool_names):
1274
1288
  raise RuntimeError("sellable_opener_stage_tools_not_empty")
1275
- output_format = _sellable_opener_output_format(model_options) if stage else None
1289
+ output_format = _sellable_opener_output_format(model_options)
1276
1290
  if output_format is None:
1277
1291
  return
1278
- if agent.api_mode != "codex_responses" or agent.provider != "openai-codex" or agent._fallback_chain:
1279
- raise RuntimeError("sellable_opener_output_format_unsupported")
1280
1292
  import copy
1281
- expected_format = copy.deepcopy(output_format)
1293
+ if stage:
1294
+ if agent.api_mode != "codex_responses" or agent.provider != "openai-codex" or agent._fallback_chain:
1295
+ raise RuntimeError("sellable_opener_output_format_unsupported")
1296
+ expected_key, expected_value = "text", {"format": copy.deepcopy(output_format)}
1297
+ else:
1298
+ if agent.api_mode != "chat_completions" or agent.provider != "openrouter" or agent._fallback_chain:
1299
+ raise RuntimeError("sellable_opener_output_format_unsupported")
1300
+ expected_key, expected_value = "response_format", _sellable_opener_json_schema(copy.deepcopy(output_format))
1282
1301
  selected = (agent.model, agent.provider, agent.api_mode)
1283
1302
  build_api_kwargs = agent._build_api_kwargs
1284
1303
  def strict_build_api_kwargs(*args, **kwargs):
1285
1304
  built = build_api_kwargs(*args, **kwargs)
1305
+ extra_body = built.get("extra_body") or {}
1286
1306
  if (
1287
1307
  (agent.model, agent.provider, agent.api_mode) != selected or agent._fallback_chain
1288
- or (built.get("extra_body") or {}).get("text") != {"format": expected_format}
1308
+ or extra_body.get(expected_key) != expected_value
1309
+ or (not stage and (extra_body.get("provider") or {}).get("require_parameters") is not True)
1289
1310
  ):
1290
1311
  raise RuntimeError("sellable_opener_output_format_not_enforced")
1291
1312
  return built
@@ -2033,11 +2054,11 @@ def getenv(name, default=""):
2033
2054
  " prerun_script = None",
2034
2055
  ' script_path = job.get("script")',
2035
2056
  " if (",
2036
- " str(job.get(\"name\") or \"\") in _SELLABLE_MANAGED_CUSTOMER_PROGRAM_JOBS",
2057
+ ' str(job.get("name") or "") in _SELLABLE_MANAGED_CUSTOMER_PROGRAM_JOBS',
2037
2058
  " and not script_path",
2038
2059
  " ):",
2039
2060
  " return _sellable_agent_managed_gate_block(",
2040
- " job_id, job_name, \"managed wake gate script is missing\"",
2061
+ ' job_id, job_name, "managed wake gate script is missing"',
2041
2062
  " )",
2042
2063
  " if script_path:",
2043
2064
  ].join("\n"),
@@ -2065,9 +2086,9 @@ def getenv(name, default=""):
2065
2086
  " cancel_event=cancel_event,",
2066
2087
  " )",
2067
2088
  " except Exception as _managed_launch_exc:",
2068
- " if str(job.get(\"name\") or \"\") in _SELLABLE_MANAGED_CUSTOMER_PROGRAM_JOBS:",
2089
+ ' if str(job.get("name") or "") in _SELLABLE_MANAGED_CUSTOMER_PROGRAM_JOBS:',
2069
2090
  " return _sellable_agent_managed_gate_block(",
2070
- " job_id, job_name, f\"managed wake gate could not start: {_managed_launch_exc}\"",
2091
+ ' job_id, job_name, f"managed wake gate could not start: {_managed_launch_exc}"',
2071
2092
  " )",
2072
2093
  " raise",
2073
2094
  ].join("\n"),
@@ -2082,9 +2103,9 @@ def getenv(name, default=""):
2082
2103
  " try:",
2083
2104
  " prerun_script = _run_job_script_with_claim_heartbeat(job, script_path, cancel_event=cancel_event)",
2084
2105
  " except Exception as _managed_launch_exc:",
2085
- " if str(job.get(\"name\") or \"\") in _SELLABLE_MANAGED_CUSTOMER_PROGRAM_JOBS:",
2106
+ ' if str(job.get("name") or "") in _SELLABLE_MANAGED_CUSTOMER_PROGRAM_JOBS:',
2086
2107
  " return _sellable_agent_managed_gate_block(",
2087
- " job_id, job_name, f\"managed wake gate could not start: {_managed_launch_exc}\"",
2108
+ ' job_id, job_name, f"managed wake gate could not start: {_managed_launch_exc}"',
2088
2109
  " )",
2089
2110
  " raise",
2090
2111
  ].join("\n"),
@@ -2542,231 +2563,298 @@ export const HERMES_AGENT_BRIDGE_DEDICATED_V020_CONTRACT = normalizeContract({
2542
2563
  });
2543
2564
 
2544
2565
  export const HERMES_AGENT_BRIDGE_DEDICATED_V021_CONTRACT = normalizeContract({
2545
- "bridgeId": "sellable-agent-hermes-bridge/v1",
2546
- "hermesVersion": "0.21.1",
2547
- "versionFile": {
2548
- "path": "hermes_cli/__init__.py",
2549
- "sha256": "efae023ac37622277aac298ac15c646b855e870132c38970feeda9552f1ecc27"
2566
+ bridgeId: "sellable-agent-hermes-bridge/v1",
2567
+ hermesVersion: "0.21.1",
2568
+ versionFile: {
2569
+ path: "hermes_cli/__init__.py",
2570
+ sha256: "efae023ac37622277aac298ac15c646b855e870132c38970feeda9552f1ecc27",
2550
2571
  },
2551
- "files": [
2572
+ files: [
2552
2573
  {
2553
- "path": "gateway/platforms/api_server.py",
2554
- "patchKind": "api-opener-stage-v021",
2555
- "sourceSha256": "221589ae8c45349736bb291b4679b86e9e12cb22f5a33220ae3d22929fea8297",
2556
- "patchedSha256": "7f5d1093b1f384d98d61d59a6c217c363f9f81f7c1c594f8537b1566c3365596"
2574
+ path: "gateway/platforms/api_server.py",
2575
+ patchKind: "api-opener-stage-v021",
2576
+ sourceSha256:
2577
+ "221589ae8c45349736bb291b4679b86e9e12cb22f5a33220ae3d22929fea8297",
2578
+ patchedSha256:
2579
+ "b029a26050aa777a520ab136c203f3755b178e04af11ca6e9f810046a9eae6d4",
2557
2580
  },
2558
2581
  {
2559
- "path": "gateway/config_env.py",
2560
- "patchKind": "gateway-config-dedicated-v021",
2561
- "sourceSha256": "984ae7b68befecd52fc17b6d38e763f40c7040a2ba69ed4a2c39f1a88ae6734a",
2562
- "patchedSha256": "0180fbcb1df6e5a7651f172c492a8e317f7455c8ed0e955c7a4f0488fa23b6c2"
2582
+ path: "gateway/config_env.py",
2583
+ patchKind: "gateway-config-dedicated-v021",
2584
+ sourceSha256:
2585
+ "984ae7b68befecd52fc17b6d38e763f40c7040a2ba69ed4a2c39f1a88ae6734a",
2586
+ patchedSha256:
2587
+ "0180fbcb1df6e5a7651f172c492a8e317f7455c8ed0e955c7a4f0488fa23b6c2",
2563
2588
  },
2564
2589
  {
2565
- "path": "plugins/platforms/slack/adapter.py",
2566
- "patchKind": "slack-adapter-dedicated-v021",
2567
- "sourceSha256": "23048a107304993686250af044256199be2f73171bb5b4cca6a900ea272a6064",
2568
- "patchedSha256": "bc05fd5f227e42f733ee78c80ffab6c7c53840975fc5bc9755aa2ef8f2c8ff47"
2590
+ path: "plugins/platforms/slack/adapter.py",
2591
+ patchKind: "slack-adapter-dedicated-v021",
2592
+ sourceSha256:
2593
+ "23048a107304993686250af044256199be2f73171bb5b4cca6a900ea272a6064",
2594
+ patchedSha256:
2595
+ "bc05fd5f227e42f733ee78c80ffab6c7c53840975fc5bc9755aa2ef8f2c8ff47",
2569
2596
  },
2570
2597
  {
2571
- "path": "tools/mcp_tool_handlers.py",
2572
- "patchKind": "mcp-tool-endpoint-v021",
2573
- "sourceSha256": "e6af250f0fbfeb12a936c340a46f40f8d40332ed8bbc4eb63fded66e37aaa7bf",
2574
- "patchedSha256": "6c4765aad3074b6f8ad6951299edafa36cf536c9a9f0b57a48bdd541e3209790"
2598
+ path: "tools/mcp_tool_handlers.py",
2599
+ patchKind: "mcp-tool-endpoint-v021",
2600
+ sourceSha256:
2601
+ "e6af250f0fbfeb12a936c340a46f40f8d40332ed8bbc4eb63fded66e37aaa7bf",
2602
+ patchedSha256:
2603
+ "6c4765aad3074b6f8ad6951299edafa36cf536c9a9f0b57a48bdd541e3209790",
2575
2604
  },
2576
2605
  {
2577
- "path": "gateway/run.py",
2578
- "patchKind": "gateway-session-v021",
2579
- "sourceSha256": "3b1f670243d4932ac1f2a2a68bee61a8c7f9bcdac7bd48d43289db90f17bab67",
2580
- "patchedSha256": "2e25d50add6c49748b2cdd4c2bd115039e72c0b4eeb9c882eb1cd07a2a25c304"
2606
+ path: "gateway/run.py",
2607
+ patchKind: "gateway-session-v021",
2608
+ sourceSha256:
2609
+ "3b1f670243d4932ac1f2a2a68bee61a8c7f9bcdac7bd48d43289db90f17bab67",
2610
+ patchedSha256:
2611
+ "2e25d50add6c49748b2cdd4c2bd115039e72c0b4eeb9c882eb1cd07a2a25c304",
2581
2612
  },
2582
2613
  {
2583
- "path": "gateway/run_turn_runner.py",
2584
- "patchKind": "gateway-provider-preflight-v021",
2585
- "sourceSha256": "8ebd74318db2259ac52c35dbf9568b98dbb19c1d2e30289f4efc76a12ac8ceeb",
2586
- "patchedSha256": "26a13f167835036c37ff92da3c1d036267544cf8a6529f5d40c3ee0ae58fa1cb"
2614
+ path: "gateway/run_turn_runner.py",
2615
+ patchKind: "gateway-provider-preflight-v021",
2616
+ sourceSha256:
2617
+ "8ebd74318db2259ac52c35dbf9568b98dbb19c1d2e30289f4efc76a12ac8ceeb",
2618
+ patchedSha256:
2619
+ "26a13f167835036c37ff92da3c1d036267544cf8a6529f5d40c3ee0ae58fa1cb",
2587
2620
  },
2588
2621
  {
2589
- "path": "agent/turn_context.py",
2590
- "patchKind": "turn-context-v021",
2591
- "sourceSha256": "d1a84a24c5b50a7a866268fde453694c6b98790e31d7d4310d8756027149863a",
2592
- "patchedSha256": "13517e034866224c072a860dbd9fbb7eb25c9d3cb86a1f59ae3a2e5ec1cef7ad"
2622
+ path: "agent/turn_context.py",
2623
+ patchKind: "turn-context-v021",
2624
+ sourceSha256:
2625
+ "d1a84a24c5b50a7a866268fde453694c6b98790e31d7d4310d8756027149863a",
2626
+ patchedSha256:
2627
+ "13517e034866224c072a860dbd9fbb7eb25c9d3cb86a1f59ae3a2e5ec1cef7ad",
2593
2628
  },
2594
2629
  {
2595
- "path": "cron/scheduler.py",
2596
- "patchKind": "cron-scheduler-v020",
2597
- "sourceSha256": "f1891e94697ccf5f7b66ae7788345368b452af447fbc43356ae5376a3c753518",
2598
- "patchedSha256": "03d0848e584b7cbc5b126e0fae989dbd5d3d83ccee3ad989c08ffc8f8ef423b9"
2630
+ path: "cron/scheduler.py",
2631
+ patchKind: "cron-scheduler-v020",
2632
+ sourceSha256:
2633
+ "f1891e94697ccf5f7b66ae7788345368b452af447fbc43356ae5376a3c753518",
2634
+ patchedSha256:
2635
+ "03d0848e584b7cbc5b126e0fae989dbd5d3d83ccee3ad989c08ffc8f8ef423b9",
2599
2636
  },
2600
2637
  {
2601
- "path": "tools/skills_tool.py",
2602
- "patchKind": "skills-cache-freshness-v021",
2603
- "sourceSha256": "0f205b8c2d9266d272d8c6cae3d189b86a5aebce98f03b61605d266b65f9497c",
2604
- "patchedSha256": "8696b37ef58c0cc570c437abac730ac0e670e44365ddeae452ad40a501f13a1b"
2638
+ path: "tools/skills_tool.py",
2639
+ patchKind: "skills-cache-freshness-v021",
2640
+ sourceSha256:
2641
+ "0f205b8c2d9266d272d8c6cae3d189b86a5aebce98f03b61605d266b65f9497c",
2642
+ patchedSha256:
2643
+ "8696b37ef58c0cc570c437abac730ac0e670e44365ddeae452ad40a501f13a1b",
2605
2644
  },
2606
2645
  {
2607
- "path": "agent/prompt_builder.py",
2608
- "patchKind": "skills-prompt-freshness-v021",
2609
- "sourceSha256": "83160fab72772012bc308bc730a0085e7acb126f1bea49d62f990505f00ce677",
2610
- "patchedSha256": "6ab2aa30e06b124333d28ed73fc7b56394fbc71f040f3387f6fbc835d799f266"
2611
- }
2646
+ path: "agent/prompt_builder.py",
2647
+ patchKind: "skills-prompt-freshness-v021",
2648
+ sourceSha256:
2649
+ "83160fab72772012bc308bc730a0085e7acb126f1bea49d62f990505f00ce677",
2650
+ patchedSha256:
2651
+ "6ab2aa30e06b124333d28ed73fc7b56394fbc71f040f3387f6fbc835d799f266",
2652
+ },
2612
2653
  ],
2613
- "contractDigest": "a8520b6decd64b43de1074005c8eda2d9be4a451f0f9a43b1777659acd5a86b6"
2654
+ contractDigest:
2655
+ "a8520b6decd64b43de1074005c8eda2d9be4a451f0f9a43b1777659acd5a86b6",
2614
2656
  });
2615
2657
 
2616
2658
  export const HERMES_AGENT_BRIDGE_DEDICATED_V0213_CONTRACT = normalizeContract({
2617
- "bridgeId": "sellable-agent-hermes-bridge/v1",
2618
- "hermesVersion": "0.21.3",
2619
- "versionFile": {
2620
- "path": "hermes_cli/__init__.py",
2621
- "sha256": "0d78a58a9f27f32adfdac959e89767cecde93a424bcfbd39d64fb95f6cf13e6c"
2659
+ bridgeId: "sellable-agent-hermes-bridge/v1",
2660
+ hermesVersion: "0.21.3",
2661
+ versionFile: {
2662
+ path: "hermes_cli/__init__.py",
2663
+ sha256: "0d78a58a9f27f32adfdac959e89767cecde93a424bcfbd39d64fb95f6cf13e6c",
2622
2664
  },
2623
- "files": [
2665
+ files: [
2624
2666
  {
2625
- "path": "gateway/config_env.py",
2626
- "patchKind": "gateway-config-dedicated-v021",
2627
- "sourceSha256": "40bb1029e482ebe423a5a6ddc9175ef96f821f89e9e040b5cf62cdd7ec903e4c",
2628
- "patchedSha256": "c189faaa2ab7c0ea603b44eb3e6b37dcf847a83b12f603ca185f37ba0ca582fc"
2667
+ path: "gateway/config_env.py",
2668
+ patchKind: "gateway-config-dedicated-v021",
2669
+ sourceSha256:
2670
+ "40bb1029e482ebe423a5a6ddc9175ef96f821f89e9e040b5cf62cdd7ec903e4c",
2671
+ patchedSha256:
2672
+ "c189faaa2ab7c0ea603b44eb3e6b37dcf847a83b12f603ca185f37ba0ca582fc",
2629
2673
  },
2630
2674
  {
2631
- "path": "plugins/platforms/slack/adapter.py",
2632
- "patchKind": "slack-adapter-dedicated-v0213",
2633
- "sourceSha256": "12bb80dffde14eca299f6f0fa57907fbe1636519402c08dc07d96d1c144e5f83",
2634
- "patchedSha256": "da13c90b8a81c32b57909e5d82f9e3500d14b290cf9a38ffd6dd53e77785e038"
2675
+ path: "plugins/platforms/slack/adapter.py",
2676
+ patchKind: "slack-adapter-dedicated-v0213",
2677
+ sourceSha256:
2678
+ "12bb80dffde14eca299f6f0fa57907fbe1636519402c08dc07d96d1c144e5f83",
2679
+ patchedSha256:
2680
+ "da13c90b8a81c32b57909e5d82f9e3500d14b290cf9a38ffd6dd53e77785e038",
2635
2681
  },
2636
2682
  {
2637
- "path": "tools/mcp_tool_handlers.py",
2638
- "patchKind": "mcp-tool-endpoint-v021",
2639
- "sourceSha256": "c4713aa289c7ca17b53c0ae53ceb6a276321f6d36e259084974e8b5738b6a4dd",
2640
- "patchedSha256": "1bd801ebf1c6e17a0d81899b2ee0401f038ae4d1a493552f021a1b66fccf0e98"
2683
+ path: "tools/mcp_tool_handlers.py",
2684
+ patchKind: "mcp-tool-endpoint-v021",
2685
+ sourceSha256:
2686
+ "c4713aa289c7ca17b53c0ae53ceb6a276321f6d36e259084974e8b5738b6a4dd",
2687
+ patchedSha256:
2688
+ "1bd801ebf1c6e17a0d81899b2ee0401f038ae4d1a493552f021a1b66fccf0e98",
2641
2689
  },
2642
2690
  {
2643
- "path": "gateway/run.py",
2644
- "patchKind": "gateway-session-v021",
2645
- "sourceSha256": "ab619aa0dc193c6d8741f3cabc9a028c2fb7459c22262c69597b0e872091bde5",
2646
- "patchedSha256": "50fc00c3fa236c201d4f4e0831ec728d87ee7ad16b0865c69242304a5de65a23"
2691
+ path: "gateway/run.py",
2692
+ patchKind: "gateway-session-v021",
2693
+ sourceSha256:
2694
+ "ab619aa0dc193c6d8741f3cabc9a028c2fb7459c22262c69597b0e872091bde5",
2695
+ patchedSha256:
2696
+ "50fc00c3fa236c201d4f4e0831ec728d87ee7ad16b0865c69242304a5de65a23",
2647
2697
  },
2648
2698
  {
2649
- "path": "gateway/run_turn.py",
2650
- "patchKind": "gateway-first-contact-v0213",
2651
- "sourceSha256": "56e6fb62b60e954ef0d258c7c44ed3fca99c4aa6d2c502d8f80a1c516569f537",
2652
- "patchedSha256": "d66e2aecd00ebb1c6f30a1ecc522b1f408d4a8ff1413b63516752e17ea0e55a1"
2699
+ path: "gateway/run_turn.py",
2700
+ patchKind: "gateway-first-contact-v0213",
2701
+ sourceSha256:
2702
+ "56e6fb62b60e954ef0d258c7c44ed3fca99c4aa6d2c502d8f80a1c516569f537",
2703
+ patchedSha256:
2704
+ "d66e2aecd00ebb1c6f30a1ecc522b1f408d4a8ff1413b63516752e17ea0e55a1",
2653
2705
  },
2654
2706
  {
2655
- "path": "gateway/run_turn_runner.py",
2656
- "patchKind": "gateway-provider-preflight-v021",
2657
- "sourceSha256": "b87d6072f2cc13634398d380eb0857d550f75502a8ace137138cf16e00d9a763",
2658
- "patchedSha256": "0113b3df868e73c7f06bacf0c5cec916077e21973e3c1b03b6778c3b91e92b8b"
2707
+ path: "gateway/run_turn_runner.py",
2708
+ patchKind: "gateway-provider-preflight-v021",
2709
+ sourceSha256:
2710
+ "b87d6072f2cc13634398d380eb0857d550f75502a8ace137138cf16e00d9a763",
2711
+ patchedSha256:
2712
+ "0113b3df868e73c7f06bacf0c5cec916077e21973e3c1b03b6778c3b91e92b8b",
2659
2713
  },
2660
2714
  {
2661
- "path": "agent/turn_context.py",
2662
- "patchKind": "turn-context-v021",
2663
- "sourceSha256": "46ed030ee35a13f75994bf8e28cde3633db14969d1fb5c859fc0ff6964cbb0bd",
2664
- "patchedSha256": "facc6ff45a479766b0c0fe719d96fd6867ee25c40e638d5e82499cc97bd72e04"
2715
+ path: "agent/turn_context.py",
2716
+ patchKind: "turn-context-v021",
2717
+ sourceSha256:
2718
+ "46ed030ee35a13f75994bf8e28cde3633db14969d1fb5c859fc0ff6964cbb0bd",
2719
+ patchedSha256:
2720
+ "facc6ff45a479766b0c0fe719d96fd6867ee25c40e638d5e82499cc97bd72e04",
2665
2721
  },
2666
2722
  {
2667
- "path": "cron/scheduler.py",
2668
- "patchKind": "cron-scheduler-v020",
2669
- "sourceSha256": "df7b82c50485cece408264f3cd44dc910b6b2a7d319b6d138bd42e2cf7ba2e86",
2670
- "patchedSha256": "ea8b4df9d12dbd82c59044d401de5e99ed466657ac873f018ac5d8d741893b2c"
2723
+ path: "cron/scheduler.py",
2724
+ patchKind: "cron-scheduler-v020",
2725
+ sourceSha256:
2726
+ "df7b82c50485cece408264f3cd44dc910b6b2a7d319b6d138bd42e2cf7ba2e86",
2727
+ patchedSha256:
2728
+ "ea8b4df9d12dbd82c59044d401de5e99ed466657ac873f018ac5d8d741893b2c",
2671
2729
  },
2672
2730
  {
2673
- "path": "tools/skills_tool.py",
2674
- "patchKind": "skills-cache-freshness-v021",
2675
- "sourceSha256": "1bec6a244ac61603b1adeb5599149a4097b68263458f63eafe55836415e5f3a0",
2676
- "patchedSha256": "168bad88d45af80328c38296be3f1c9b3509f680a7939735836feac94328843e"
2731
+ path: "tools/skills_tool.py",
2732
+ patchKind: "skills-cache-freshness-v021",
2733
+ sourceSha256:
2734
+ "1bec6a244ac61603b1adeb5599149a4097b68263458f63eafe55836415e5f3a0",
2735
+ patchedSha256:
2736
+ "168bad88d45af80328c38296be3f1c9b3509f680a7939735836feac94328843e",
2677
2737
  },
2678
2738
  {
2679
- "path": "agent/prompt_builder.py",
2680
- "patchKind": "skills-prompt-freshness-v021",
2681
- "sourceSha256": "586ea363fa1e70bb0fdd5426af40758976a16c54f07efeb7a1b4f3fe0ad99309",
2682
- "patchedSha256": "769a0662a396bcb0ffb82e14cacd03b2b864526e68c1cde601f68ccea61e2836"
2739
+ path: "agent/prompt_builder.py",
2740
+ patchKind: "skills-prompt-freshness-v021",
2741
+ sourceSha256:
2742
+ "586ea363fa1e70bb0fdd5426af40758976a16c54f07efeb7a1b4f3fe0ad99309",
2743
+ patchedSha256:
2744
+ "769a0662a396bcb0ffb82e14cacd03b2b864526e68c1cde601f68ccea61e2836",
2683
2745
  },
2684
2746
  {
2685
- "path": "gateway/platforms/api_server.py",
2686
- "patchKind": "api-opener-stage-v021",
2687
- "sourceSha256": "7ec405dc2a59789582ad9f61a189a41079e621f14642d3c9dc754e312616a868",
2688
- "patchedSha256": "a92e9fd87a69fe7761f8473acd7233a27c1613a163abc7fbfac288a8002f47cb"
2689
- }
2747
+ path: "gateway/platforms/api_server.py",
2748
+ patchKind: "api-opener-stage-v021",
2749
+ sourceSha256:
2750
+ "7ec405dc2a59789582ad9f61a189a41079e621f14642d3c9dc754e312616a868",
2751
+ patchedSha256:
2752
+ "1423ea98ec142e9b0dc5ba423dadfe9cf192c39c0565cf58cafc374805725052",
2753
+ },
2690
2754
  ],
2691
- "contractDigest": "75b5d16081dd2fdc5795d3b86d341f2ff341bf3c599e4c57de5ec14b3dd63ce0"
2755
+ contractDigest:
2756
+ "75b5d16081dd2fdc5795d3b86d341f2ff341bf3c599e4c57de5ec14b3dd63ce0",
2692
2757
  });
2693
2758
 
2694
2759
  export const HERMES_AGENT_BRIDGE_DEDICATED_V0215_CONTRACT = normalizeContract({
2695
- "bridgeId": "sellable-agent-hermes-bridge/v1",
2696
- "hermesVersion": "0.21.5",
2697
- "versionFile": {
2698
- "path": "hermes_cli/__init__.py",
2699
- "sha256": "d05c98cd44e22781497a20cd968af0d9b8962406b909f1889168c624545b229e"
2760
+ bridgeId: "sellable-agent-hermes-bridge/v1",
2761
+ hermesVersion: "0.21.5",
2762
+ versionFile: {
2763
+ path: "hermes_cli/__init__.py",
2764
+ sha256: "d05c98cd44e22781497a20cd968af0d9b8962406b909f1889168c624545b229e",
2700
2765
  },
2701
- "files": [
2766
+ files: [
2702
2767
  {
2703
- "path": "gateway/config_env.py",
2704
- "patchKind": "gateway-config-dedicated-v021",
2705
- "sourceSha256": "5661d9cfb7d798d6c995b03727bdd4018669ae12c96d1e7a8f3c99a3ea85acc7",
2706
- "patchedSha256": "cd8efa414e0313bca77d0d8093acc505d9f345b8fb48d734821e66027545bf57"
2768
+ path: "gateway/config_env.py",
2769
+ patchKind: "gateway-config-dedicated-v021",
2770
+ sourceSha256:
2771
+ "5661d9cfb7d798d6c995b03727bdd4018669ae12c96d1e7a8f3c99a3ea85acc7",
2772
+ patchedSha256:
2773
+ "cd8efa414e0313bca77d0d8093acc505d9f345b8fb48d734821e66027545bf57",
2707
2774
  },
2708
2775
  {
2709
- "path": "plugins/platforms/slack/adapter.py",
2710
- "patchKind": "slack-adapter-dedicated-v0213",
2711
- "sourceSha256": "3f2ec0cef97964bf4cb1e9791c0917a03cd1e73bb0bc3293768680cd6302f613",
2712
- "patchedSha256": "26cedfad352e8dadae82bebcedf8813ef948622a350b392ce99097a8ede2f056"
2776
+ path: "plugins/platforms/slack/adapter.py",
2777
+ patchKind: "slack-adapter-dedicated-v0213",
2778
+ sourceSha256:
2779
+ "3f2ec0cef97964bf4cb1e9791c0917a03cd1e73bb0bc3293768680cd6302f613",
2780
+ patchedSha256:
2781
+ "26cedfad352e8dadae82bebcedf8813ef948622a350b392ce99097a8ede2f056",
2713
2782
  },
2714
2783
  {
2715
- "path": "tools/mcp_tool_handlers.py",
2716
- "patchKind": "mcp-tool-endpoint-v021",
2717
- "sourceSha256": "d1b4463309a2de351aaf6c7cd8f7a2a1e83f5e26dbe058371c9eeb9d0c567903",
2718
- "patchedSha256": "61ceab8c24aef50e88d6c1dad1bbf80daed66f8950b05fb642525a62a0bb789c"
2784
+ path: "tools/mcp_tool_handlers.py",
2785
+ patchKind: "mcp-tool-endpoint-v021",
2786
+ sourceSha256:
2787
+ "d1b4463309a2de351aaf6c7cd8f7a2a1e83f5e26dbe058371c9eeb9d0c567903",
2788
+ patchedSha256:
2789
+ "61ceab8c24aef50e88d6c1dad1bbf80daed66f8950b05fb642525a62a0bb789c",
2719
2790
  },
2720
2791
  {
2721
- "path": "gateway/run.py",
2722
- "patchKind": "gateway-session-v021",
2723
- "sourceSha256": "324f7095223816b865dd68a75dc83bfd1c3d101ac8c1c38dba69026dab2b6af1",
2724
- "patchedSha256": "4b398b8c033e5aaba90b6ba312baf01e7d88d46a91843e64949ade0b65f2ade8"
2792
+ path: "gateway/run.py",
2793
+ patchKind: "gateway-session-v021",
2794
+ sourceSha256:
2795
+ "324f7095223816b865dd68a75dc83bfd1c3d101ac8c1c38dba69026dab2b6af1",
2796
+ patchedSha256:
2797
+ "4b398b8c033e5aaba90b6ba312baf01e7d88d46a91843e64949ade0b65f2ade8",
2725
2798
  },
2726
2799
  {
2727
- "path": "gateway/run_turn.py",
2728
- "patchKind": "gateway-first-contact-v0213",
2729
- "sourceSha256": "5bdee5c82c00e02f664516f9da0af99674e23493ec2a30eff06cb03afc6718a0",
2730
- "patchedSha256": "0729145f27559df970c16d5be11e01ebce657874e4be1d9e151562042f5208c0"
2800
+ path: "gateway/run_turn.py",
2801
+ patchKind: "gateway-first-contact-v0213",
2802
+ sourceSha256:
2803
+ "5bdee5c82c00e02f664516f9da0af99674e23493ec2a30eff06cb03afc6718a0",
2804
+ patchedSha256:
2805
+ "0729145f27559df970c16d5be11e01ebce657874e4be1d9e151562042f5208c0",
2731
2806
  },
2732
2807
  {
2733
- "path": "gateway/run_turn_runner.py",
2734
- "patchKind": "gateway-provider-preflight-v021",
2735
- "sourceSha256": "216bdca083b7d2bfae481d07bd0791a76834674b3614fd4ee0aafac5c58f30db",
2736
- "patchedSha256": "7b6beabeed7b78781d142dcdb0eff4c0110263ee173f7696737c4c965e1c72ab"
2808
+ path: "gateway/run_turn_runner.py",
2809
+ patchKind: "gateway-provider-preflight-v021",
2810
+ sourceSha256:
2811
+ "216bdca083b7d2bfae481d07bd0791a76834674b3614fd4ee0aafac5c58f30db",
2812
+ patchedSha256:
2813
+ "7b6beabeed7b78781d142dcdb0eff4c0110263ee173f7696737c4c965e1c72ab",
2737
2814
  },
2738
2815
  {
2739
- "path": "agent/turn_context.py",
2740
- "patchKind": "turn-context-v021",
2741
- "sourceSha256": "85f857dc3c317366d918516bc8b37a022f1020b34da0c3966a31bd289b2942e1",
2742
- "patchedSha256": "a6487c43bc2d21f40e3a86fe31a9a0c2e4e603fff813f7c8429bbf5c22591648"
2816
+ path: "agent/turn_context.py",
2817
+ patchKind: "turn-context-v021",
2818
+ sourceSha256:
2819
+ "85f857dc3c317366d918516bc8b37a022f1020b34da0c3966a31bd289b2942e1",
2820
+ patchedSha256:
2821
+ "a6487c43bc2d21f40e3a86fe31a9a0c2e4e603fff813f7c8429bbf5c22591648",
2743
2822
  },
2744
2823
  {
2745
- "path": "cron/scheduler.py",
2746
- "patchKind": "cron-scheduler-v020",
2747
- "sourceSha256": "dcf0e6794a220e478584a21f786b8451b915fc7397d38eda357af1363249fa2a",
2748
- "patchedSha256": "e98d197ce3d0b414184e3c746fe3814c213407da68ce0a46a8b4dd63a9054a11"
2824
+ path: "cron/scheduler.py",
2825
+ patchKind: "cron-scheduler-v020",
2826
+ sourceSha256:
2827
+ "dcf0e6794a220e478584a21f786b8451b915fc7397d38eda357af1363249fa2a",
2828
+ patchedSha256:
2829
+ "e98d197ce3d0b414184e3c746fe3814c213407da68ce0a46a8b4dd63a9054a11",
2749
2830
  },
2750
2831
  {
2751
- "path": "tools/skills_tool.py",
2752
- "patchKind": "skills-cache-freshness-v021",
2753
- "sourceSha256": "0d47bda41ed9d1ec84856df3d336a42bb94161e89bd9a8b857e76ec76be9f7b7",
2754
- "patchedSha256": "1bdc17603f3bb9fb34c5d04099dee2ca7d631eedc58b523bcd74d6e946dc9eb7"
2832
+ path: "tools/skills_tool.py",
2833
+ patchKind: "skills-cache-freshness-v021",
2834
+ sourceSha256:
2835
+ "0d47bda41ed9d1ec84856df3d336a42bb94161e89bd9a8b857e76ec76be9f7b7",
2836
+ patchedSha256:
2837
+ "1bdc17603f3bb9fb34c5d04099dee2ca7d631eedc58b523bcd74d6e946dc9eb7",
2755
2838
  },
2756
2839
  {
2757
- "path": "agent/prompt_builder.py",
2758
- "patchKind": "skills-prompt-freshness-v0215",
2759
- "sourceSha256": "64bc77a26641754b8c5ca76fc876687d3c9e81aad7816c2e76ce251a41121526",
2760
- "patchedSha256": "56edefd92c50f38a3fed8d56b6fc02352f8e5892b43553ab277e3113606d25fb"
2840
+ path: "agent/prompt_builder.py",
2841
+ patchKind: "skills-prompt-freshness-v0215",
2842
+ sourceSha256:
2843
+ "64bc77a26641754b8c5ca76fc876687d3c9e81aad7816c2e76ce251a41121526",
2844
+ patchedSha256:
2845
+ "56edefd92c50f38a3fed8d56b6fc02352f8e5892b43553ab277e3113606d25fb",
2761
2846
  },
2762
2847
  {
2763
- "path": "gateway/platforms/api_server.py",
2764
- "patchKind": "api-opener-stage-v021",
2765
- "sourceSha256": "fa83a20bd4f9f3a90a3ac010db0a68f548259587946ea80b81b2f3253bafd5ec",
2766
- "patchedSha256": "5289cc21e1804d0aaa49b117f32ea1a506375fa76459cdac8f5013f0566d05e1"
2767
- }
2848
+ path: "gateway/platforms/api_server.py",
2849
+ patchKind: "api-opener-stage-v021",
2850
+ sourceSha256:
2851
+ "fa83a20bd4f9f3a90a3ac010db0a68f548259587946ea80b81b2f3253bafd5ec",
2852
+ patchedSha256:
2853
+ "dbe6e400c7c7a9d3b377b7f9f08e05f2e9f6b622f9019f873e4fdb0cd828b19f",
2854
+ },
2768
2855
  ],
2769
- "contractDigest": "d8b2ee47591435091a81a3e9913ba5acace2f9c1418cf75c0e23b4d0632e4024"
2856
+ contractDigest:
2857
+ "d8b2ee47591435091a81a3e9913ba5acace2f9c1418cf75c0e23b4d0632e4024",
2770
2858
  });
2771
2859
 
2772
2860
  function assertRelativeContractPath(path) {
@@ -16,6 +16,7 @@ import {
16
16
  readdirSync,
17
17
  renameSync,
18
18
  rmSync,
19
+ statfsSync,
19
20
  writeFileSync,
20
21
  } from "node:fs";
21
22
  import { tmpdir } from "node:os";
@@ -1006,12 +1007,14 @@ import time
1006
1007
 
1007
1008
  source_path, target_path = sys.argv[1], sys.argv[2]
1008
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.
1009
1012
  source = sqlite3.connect("file:" + source_path + "?mode=ro", uri=True)
1010
- destination = sqlite3.connect(target_path)
1011
1013
  try:
1012
- source.backup(destination)
1014
+ source.execute("VACUUM INTO ?", (target_path,))
1013
1015
  finally:
1014
1016
  source.close()
1017
+ destination = sqlite3.connect(target_path)
1015
1018
 
1016
1019
  try:
1017
1020
  if destination.execute("PRAGMA quick_check").fetchone()[0] != "ok":
@@ -1038,8 +1041,11 @@ try:
1038
1041
  )
1039
1042
  destination.execute(
1040
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.
1041
1046
  " SELECT id FROM sessions"
1042
- " WHERE source = 'cron' AND ended_at IS NOT NULL"
1047
+ " WHERE ((source = 'cron' AND ended_at IS NOT NULL)"
1048
+ " OR source = 'api_server')"
1043
1049
  " AND COALESCE(last_activity_at, ended_at, started_at) < ? "
1044
1050
  + pinned_clause
1045
1051
  + " UNION"
@@ -1077,13 +1083,14 @@ try:
1077
1083
  ")"
1078
1084
  )
1079
1085
  destination.commit()
1080
- for table in ("messages_fts", "messages_fts_trigram", "messages_fts_cjk"):
1081
- if table in table_names:
1082
- destination.execute(
1083
- 'INSERT INTO "' + table + '" ("' + table + '") VALUES (\'optimize\')'
1084
- )
1085
- destination.commit()
1086
- destination.execute("VACUUM")
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")
1087
1094
  if destination.execute("PRAGMA quick_check").fetchone()[0] != "ok":
1088
1095
  raise RuntimeError("compacted backup failed quick_check")
1089
1096
  if destination.execute("PRAGMA foreign_key_check").fetchone() is not None:
@@ -1101,12 +1108,38 @@ print(json.dumps({
1101
1108
  }, separators=(",", ":")))
1102
1109
  `;
1103
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
+
1104
1136
  export async function compactHermesSnapshotStateDb({
1105
1137
  sourcePath,
1106
1138
  targetPath,
1107
1139
  retentionHours = HERMES_SNAPSHOT_CRON_SESSION_RETENTION_HOURS,
1108
1140
  hermesPython = HERMES_PYTHON,
1109
1141
  scratchDirectory = tmpdir(),
1142
+ volumeFreeBytes = freeBytesAt,
1110
1143
  runChild = /** @type {typeof runBoundedHermesChild | undefined} */ (
1111
1144
  undefined
1112
1145
  ),
@@ -1161,7 +1194,7 @@ export async function compactHermesSnapshotStateDb({
1161
1194
  maxOutputBytes: 64 * 1024,
1162
1195
  });
1163
1196
  if (childFailed(result) || !existsSync(scratchTargetPath)) {
1164
- throw new Error("hermes_snapshot_state_db_compaction_failed");
1197
+ throw compactionFailure(compactionChildFailureReason(result));
1165
1198
  }
1166
1199
  const scratchTarget = regularFile(scratchTargetPath);
1167
1200
  const summary = JSON.parse(String(result.stdout ?? ""));
@@ -1174,7 +1207,16 @@ export async function compactHermesSnapshotStateDb({
1174
1207
  summary.prunedMessages < 0 ||
1175
1208
  summary.archiveStateBytes !== scratchTarget.size
1176
1209
  ) {
1177
- throw new Error("hermes_snapshot_state_db_compaction_failed");
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");
1178
1220
  }
1179
1221
  finalTemporaryPath = join(
1180
1222
  dirname(targetPath),
@@ -1204,8 +1246,17 @@ export async function compactHermesSnapshotStateDb({
1204
1246
  throw new Error("hermes_snapshot_state_db_compaction_failed");
1205
1247
  }
1206
1248
  return summary;
1207
- } catch {
1249
+ } catch (error) {
1208
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
+ );
1209
1260
  throw new Error("hermes_snapshot_state_db_compaction_failed");
1210
1261
  } finally {
1211
1262
  if (finalTemporaryPath) rmSync(finalTemporaryPath, { force: true });
@@ -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.792";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.794";
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.792" ||
1549
+ pinned.installerPackage !== "@sellable/install@0.1.794" ||
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.792" ||
315
+ desired.installerPackage !== "@sellable/install@0.1.794" ||
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.792";
28
+ export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.794";
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.792",
3
+ "version": "0.1.794",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {