@sema-agent/server 1.221.0 → 1.223.0

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/main.js CHANGED
@@ -70,6 +70,8 @@ import { ToolApprovalCoordinator } from "./tool-approval.js";
70
70
  import { applyEffective, mutateInPlace, logEffectiveDiff, applyCenterSkills, resolveMcpServers, mcpForScenario, restartReasons } from "./sema-registry.js";
71
71
  import { ensureSealedKeyStore, reportExecutionPublicKey } from "./sealed-key.js";
72
72
  import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "./config-provider.js";
73
+ import { validateCenterPrompts, centerPromptProvider } from "./capabilities/center-prompts.js";
74
+ import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg } from "./config-lkg.js";
73
75
  import { createPrincipalCapsClient, gateExecutionLane, scopedTokenNeedsWorker, applyObserverEnvOptIn } from "./runtime-caps-resolver.js";
74
76
  import { applyRuntimeGovernance, stripDelegationTools } from "./runtime-governance.js";
75
77
  import { parseTaskSettings, applyTaskSettings, coercePermissionMode, withPermissionMode, effectiveThinking } from "./task-settings.js";
@@ -154,24 +156,100 @@ async function main() {
154
156
  logger.info("model_roster_landed", { note: "ready gate open — billable submissions accepted" });
155
157
  }
156
158
  };
159
+ let centerPrompts;
160
+ const adoptCenterPrompts = (eff, phase) => {
161
+ const raw = eff?.prompts;
162
+ if (raw === undefined) {
163
+ if (centerPrompts)
164
+ logger.info("center_prompts_cleared", { phase, note: "effective carries no prompts key (publish gate off) — built-in providers resume" });
165
+ centerPrompts = undefined;
166
+ return;
167
+ }
168
+ const v = validateCenterPrompts(raw);
169
+ if (!v.ok) {
170
+ logger.warn("center_prompts_invalid", { phase, error: v.error, kept: centerPrompts?.packId ?? "(builtin)", note: "malformed prompts face rejected (core assemble would THROW per task) — keeping the previous pack/built-ins; fix the center payload" });
171
+ return;
172
+ }
173
+ if (centerPrompts?.contentDigest !== v.value.contentDigest) {
174
+ logger.info("center_prompts_adopted", { phase, packId: v.value.packId, contentDigest: v.value.contentDigest, sections: v.value.sections.length, scenarioOverrides: Object.keys(v.value.scenarioOverrides ?? {}).length, note: "new tasks assemble the new pack (epoch re-pins at the next task boundary); in-flight sessions stay pinned" });
175
+ }
176
+ centerPrompts = v.value;
177
+ };
157
178
  let bootConfigPending;
179
+ const lkgEnabled = configProvider?.kind === "remote" && process.env.CONFIG_LKG_DISABLED !== "true";
180
+ const lkgPath = process.env.CONFIG_LKG_PATH ?? defaultLkgPath(cc?.worker);
181
+ let lkgBooted = false;
182
+ let bootLkgCandidate;
183
+ const persistLkgDurable = async (effectiveObj, etag) => {
184
+ if (!lkgEnabled)
185
+ return;
186
+ try {
187
+ if (effectiveObj.skills && config.configCenter) {
188
+ await applyCenterSkills([], effectiveObj.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, defaultSkillCacheDir());
189
+ }
190
+ await saveLkg(lkgPath, cc?.worker, effectiveObj, etag);
191
+ }
192
+ catch (err) {
193
+ logger.warn("config_lkg_save_failed", { path: lkgPath, err: String(err), note: "durable persist failed — restart signal proceeds anyway (a dead disk + dead center double-fault can re-loop; fix the disk)" });
194
+ }
195
+ };
158
196
  if (configProvider) {
159
197
  const dryRun = cc?.dryRun ?? false;
198
+ const bootFromLkg = async (why) => {
199
+ if (!lkgEnabled || dryRun)
200
+ return null;
201
+ const read = await loadLkg(lkgPath, cc?.worker);
202
+ if (!read.lkg) {
203
+ if (read.reason !== "absent")
204
+ logger.warn("config_lkg_unusable", { path: lkgPath, reason: read.reason, note: "ignoring the on-disk LKG — env fallback serves; the cache rewrites on the next clean candidate" });
205
+ return null;
206
+ }
207
+ logger.info("config_loaded_lkg", { path: lkgPath, savedAt: read.lkg.savedAt, why, note: "booting from the persisted last-known-good (all restart-to-apply faces included) — the live pull continues in the background and reconciles on arrival" });
208
+ return { effective: read.lkg.effective, ...(read.lkg.etag !== undefined ? { etag: read.lkg.etag } : {}) };
209
+ };
210
+ const bootApplyLkgInline = (lkgR) => {
211
+ applyEffective(config, lkgR.effective, logger, { sealedKeys });
212
+ adoptCenterPrompts(lkgR.effective, "boot-lkg");
213
+ markRosterLanded(lkgR.effective);
214
+ effective = lkgR.effective;
215
+ latestEffective = lkgR.effective;
216
+ if (lkgR.etag !== undefined)
217
+ ccEtag = lkgR.etag;
218
+ const cfgModels = (lkgR.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
219
+ logger.info("config_loaded", { source: "lkg", version: lkgR.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels });
220
+ };
160
221
  try {
161
222
  const bootFetch = configProvider.fetchEffective(undefined);
162
223
  const raced = await raceBootFetch(bootFetch, config.configBootFetchBudgetMs);
224
+ let lkgFallback = null;
163
225
  if (raced === BOOT_FETCH_DEFERRED) {
164
226
  bootConfigPending = bootFetch;
165
227
  bootFetch.catch(() => { });
228
+ lkgFallback = await bootFromLkg("boot fetch deferred (budget exceeded)");
229
+ lkgBooted = lkgFallback !== null;
166
230
  logger.warn("config_boot_fetch_deferred", {
167
231
  source: configProvider.kind,
168
232
  budgetMs: config.configBootFetchBudgetMs,
169
- note: "center slow/unreachable — serving on the env fallback now; the pull continues in the background and hot-applies on arrival (roster gate opens then). Raise CONFIG_BOOT_FETCH_BUDGET_MS to block boot instead.",
233
+ lkg: lkgBooted,
234
+ note: lkgBooted
235
+ ? "center slow — booting from the persisted LKG; the pull continues in the background and reconciles via the refresh path on arrival"
236
+ : "center slow/unreachable and no usable LKG — serving on the env fallback; the pull continues in the background (late-boot adoption on arrival). Raise CONFIG_BOOT_FETCH_BUDGET_MS to block boot instead.",
170
237
  });
171
238
  }
239
+ if (raced === BOOT_FETCH_DEFERRED && lkgFallback) {
240
+ try {
241
+ bootApplyLkgInline(lkgFallback);
242
+ }
243
+ catch (applyErr) {
244
+ lkgBooted = false;
245
+ logger.warn("config_lkg_apply_failed", { err: String(applyErr), note: "persisted LKG failed to apply — env fallback serves; the cache rewrites on the next clean candidate" });
246
+ }
247
+ }
172
248
  const r = raced === BOOT_FETCH_DEFERRED ? null : raced;
173
249
  if (r) {
174
- const bootClean = (r.domainErrors ?? []).length === 0;
250
+ const bootPromptsRaw = r.effective.prompts;
251
+ const bootPromptsOk = bootPromptsRaw === undefined || validateCenterPrompts(bootPromptsRaw).ok;
252
+ const bootClean = (r.domainErrors ?? []).length === 0 && bootPromptsOk;
175
253
  if (bootClean)
176
254
  ccEtag = r.etag;
177
255
  for (const de of r.domainErrors ?? [])
@@ -181,10 +259,13 @@ async function main() {
181
259
  }
182
260
  else {
183
261
  applyEffective(config, r.effective, logger, { sealedKeys });
262
+ adoptCenterPrompts(r.effective, "boot");
184
263
  markRosterLanded(r.effective);
185
264
  effective = r.effective;
186
265
  if (bootClean)
187
266
  latestEffective = r.effective;
267
+ if (bootClean && !lkgBooted)
268
+ bootLkgCandidate = { effective: r.effective, etag: r.etag };
188
269
  const cfgModels = (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length;
189
270
  const cfgDir = configProvider.kind === "local" ? { dir: configProvider.root } : {};
190
271
  logger.info("config_loaded", { source: configProvider.kind, version: r.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels, ...cfgDir });
@@ -195,6 +276,17 @@ async function main() {
195
276
  }
196
277
  catch (err) {
197
278
  logger.warn("config_source_unreachable_using_env", { source: configProvider.kind, err: String(err) });
279
+ const lkgFallback = await bootFromLkg(`boot fetch failed: ${String(err).slice(0, 120)}`);
280
+ if (lkgFallback) {
281
+ lkgBooted = true;
282
+ try {
283
+ bootApplyLkgInline(lkgFallback);
284
+ }
285
+ catch (applyErr) {
286
+ lkgBooted = false;
287
+ logger.warn("config_lkg_apply_failed", { err: String(applyErr), note: "persisted LKG failed to apply — env fallback serves" });
288
+ }
289
+ }
198
290
  }
199
291
  }
200
292
  if (!modelReadyState.ready)
@@ -948,7 +1040,10 @@ async function main() {
948
1040
  : undefined;
949
1041
  let skills = loadSkills(config.skillsDir);
950
1042
  if (effective?.skills && config.configCenter) {
951
- skills = await applyCenterSkills(skills, effective.skills, config.configCenter.baseUrl, config.configCenter.token, logger);
1043
+ skills = await applyCenterSkills(skills, effective.skills, config.configCenter.baseUrl, config.configCenter.token, logger, undefined, process.env.CONFIG_LKG_DISABLED !== "true" ? defaultSkillCacheDir() : undefined);
1044
+ }
1045
+ if (bootLkgCandidate && lkgEnabled) {
1046
+ await saveLkg(lkgPath, cc?.worker, bootLkgCandidate.effective, bootLkgCandidate.etag).catch((err) => logger.warn("config_lkg_save_failed", { path: lkgPath, err: String(err), note: "boot LKG persist failed — next clean candidate rewrites" }));
952
1047
  }
953
1048
  if (effective?.mcp)
954
1049
  config.mcpServers = resolveMcpServers(effective.mcp, logger);
@@ -1054,21 +1149,30 @@ async function main() {
1054
1149
  if (configProvider) {
1055
1150
  const ccRef = config.configCenter;
1056
1151
  let refreshInFlight = false;
1057
- const refreshTick = async () => {
1152
+ const refreshTick = async (prefetched) => {
1058
1153
  if (refreshInFlight)
1059
1154
  return;
1060
1155
  refreshInFlight = true;
1061
1156
  try {
1062
- const r = await configProvider.fetchEffective(ccEtag);
1157
+ const r = prefetched !== undefined ? prefetched : await configProvider.fetchEffective(ccEtag);
1063
1158
  if (r) {
1064
- const badDomains = [...new Set((r.domainErrors ?? []).map((de) => de.domain))].sort().join(",");
1159
+ const promptsRaw = r.effective.prompts;
1160
+ const promptsGate = promptsRaw !== undefined ? validateCenterPrompts(promptsRaw) : { ok: true };
1161
+ const badDomains = [
1162
+ ...new Set([...(r.domainErrors ?? []).map((de) => de.domain), ...(promptsGate.ok ? [] : ["prompts"])]),
1163
+ ]
1164
+ .sort()
1165
+ .join(",");
1065
1166
  if (badDomains) {
1066
1167
  if (lastRejectedCandidate?.version !== r.effective.version || lastRejectedCandidate.domains !== badDomains) {
1067
1168
  lastRejectedCandidate = { version: r.effective.version, domains: badDomains, at: Date.now() };
1068
1169
  logger.warn("config_candidate_rejected", {
1069
1170
  version: r.effective.version,
1070
1171
  domains: badDomains,
1071
- errors: (r.domainErrors ?? []).map((de) => ({ domain: de.domain, error: de.error })),
1172
+ errors: [
1173
+ ...(r.domainErrors ?? []).map((de) => ({ domain: de.domain, error: de.error })),
1174
+ ...(promptsGate.ok ? [] : [{ domain: "prompts", error: promptsGate.error }]),
1175
+ ],
1072
1176
  lkgVersion: latestEffective?.version ?? "(env)",
1073
1177
  note: "candidate rejected whole (§9.5-5) — live config keeps the last-known-good; fix the named files to unblock",
1074
1178
  });
@@ -1085,6 +1189,7 @@ async function main() {
1085
1189
  markRosterLanded(r.effective);
1086
1190
  mutateInPlace(pricing, buildPricing(config.models));
1087
1191
  keyResolver = buildKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1192
+ await persistLkgDurable(r.effective, r.etag);
1088
1193
  const reasons = restartReasons(effective, r.effective);
1089
1194
  if (reasons.length === 0) {
1090
1195
  pendingRestart = undefined;
@@ -1114,8 +1219,51 @@ async function main() {
1114
1219
  };
1115
1220
  const ccTimer = setInterval(() => void refreshTick(), 60_000);
1116
1221
  ccTimer.unref?.();
1222
+ const deferredBootApply = async (r) => {
1223
+ if (!r)
1224
+ return;
1225
+ if (refreshInFlight)
1226
+ return;
1227
+ refreshInFlight = true;
1228
+ try {
1229
+ await deferredBootApplyInner(r);
1230
+ }
1231
+ finally {
1232
+ refreshInFlight = false;
1233
+ }
1234
+ };
1235
+ const deferredBootApplyInner = async (r) => {
1236
+ const promptsRaw = r.effective.prompts;
1237
+ const promptsGate = promptsRaw !== undefined ? validateCenterPrompts(promptsRaw) : { ok: true };
1238
+ if ((r.domainErrors ?? []).length > 0 || !promptsGate.ok) {
1239
+ logger.warn("config_boot_deferred_candidate_rejected", {
1240
+ version: r.effective.version,
1241
+ errors: [...(r.domainErrors ?? []).map((de) => ({ domain: de.domain, error: de.error })), ...(promptsGate.ok ? [] : [{ domain: "prompts", error: promptsGate.error }])],
1242
+ note: "late boot pull carried an invalid candidate — env fallback keeps serving; the refresh cadence re-judges the same tree (etag not advanced)",
1243
+ });
1244
+ return;
1245
+ }
1246
+ if (ccRef?.dryRun) {
1247
+ logEffectiveDiff(config, r.effective, logger);
1248
+ ccEtag = r.etag;
1249
+ return;
1250
+ }
1251
+ applyEffective(config, r.effective, logger, { teamsOnly: true, sealedKeys });
1252
+ adoptCenterPrompts(r.effective, "boot-deferred");
1253
+ markRosterLanded(r.effective);
1254
+ mutateInPlace(pricing, buildPricing(config.models));
1255
+ keyResolver = buildKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1256
+ effective = r.effective;
1257
+ latestEffective = r.effective;
1258
+ ccEtag = r.etag;
1259
+ await persistLkgDurable(r.effective, r.etag);
1260
+ const reasons = restartReasons(undefined, r.effective).filter((sl) => sl !== "prompts");
1261
+ if (reasons.length > 0)
1262
+ pendingRestart = { restartRequired: true, reasons, version: r.effective.version, since: Date.now() };
1263
+ logger.info("config_loaded_deferred", { source: configProvider.kind, version: r.effective.version, models: (r.effective.models?.models ?? []).filter((m) => m.enabled !== false).length, ...(reasons.length > 0 ? { restartRequired: true, restartReasons: reasons } : {}) });
1264
+ };
1117
1265
  if (bootConfigPending)
1118
- void bootConfigPending.then(() => refreshTick(), () => { });
1266
+ void bootConfigPending.then((r) => (lkgBooted ? refreshTick(r) : deferredBootApply(r)), () => { });
1119
1267
  }
1120
1268
  let keyResolver = buildKeyResolver(config.modelApiKeyEnv, process.env, config.modelApiKeys);
1121
1269
  const mysqlPool = backend?.mysqlPool();
@@ -1447,7 +1595,7 @@ async function main() {
1447
1595
  })(cap.tools),
1448
1596
  skills: mergeUserSkills(cap.skills, body.skills, logger),
1449
1597
  mcp: resolveRequestMcp(mcpForScenario(config.mcpServers, scenarioName), body.mcpServers, config, logger),
1450
- promptProvider: cap.promptProvider,
1598
+ promptProvider: centerPrompts ? centerPromptProvider(centerPrompts, scenarioName) : cap.promptProvider,
1451
1599
  toolPolicy: durableEnabled
1452
1600
  ? combinePolicies(createDurableQuestionPolicy(), createDurableAskPolicy({
1453
1601
  requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,