@sema-agent/server 7.97.0 → 7.99.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/MIGRATION.md +11 -0
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/USAGE.md +18 -9
- package/deploy/sema-up/cluster-up.sh +8 -4
- package/deploy/sema-up/kube-up.sh +40 -15
- package/deploy/sema-up/lib/envf.sh +241 -0
- package/deploy/sema-up/sema-up.sh +162 -101
- package/deploy/sema-up/smoke.sh +8 -1
- package/dist/boot/config-center.js +169 -24
- package/dist/boot/org-memory.d.ts +2 -1
- package/dist/boot/org-memory.js +9 -5
- package/dist/boot/resolve-spec.js +9 -4
- package/dist/boot/runtime-caps.js +4 -4
- package/dist/brain.js +28 -18
- package/dist/capabilities/center-prompts.d.ts +3 -0
- package/dist/capabilities/center-prompts.js +1 -1
- package/dist/center-credential.d.ts +112 -0
- package/dist/center-credential.js +131 -0
- package/dist/config-catalog.js +7 -6
- package/dist/config-center/apply-effective.js +7 -1
- package/dist/config-center/center-request.d.ts +60 -0
- package/dist/config-center/center-request.js +71 -0
- package/dist/config-center/facade.d.ts +2 -1
- package/dist/config-center/http-client.d.ts +17 -9
- package/dist/config-center/http-client.js +27 -36
- package/dist/config-center/skills-mcp.d.ts +4 -1
- package/dist/config-center/skills-mcp.js +2 -2
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +9 -9
- package/dist/config-lkg.d.ts +47 -8
- package/dist/config-lkg.js +107 -20
- package/dist/config-provider.d.ts +8 -7
- package/dist/config-provider.js +5 -5
- package/dist/config-types.d.ts +20 -6
- package/dist/config.d.ts +9 -0
- package/dist/config.js +53 -18
- package/dist/fleet-client.d.ts +8 -8
- package/dist/fleet-client.js +7 -7
- package/dist/fleet-lease.d.ts +11 -11
- package/dist/fleet-lease.js +17 -13
- package/dist/hooks/hook-llm.d.ts +1 -1
- package/dist/hooks/hook-llm.js +3 -1
- package/dist/hosted-posture.d.ts +49 -93
- package/dist/hosted-posture.js +1 -25
- package/dist/memory-scope.d.ts +62 -11
- package/dist/memory-scope.js +32 -3
- package/dist/model-provider.d.ts +13 -0
- package/dist/model-provider.js +3 -0
- package/dist/model-route-endpoint.d.ts +82 -0
- package/dist/model-route-endpoint.js +55 -0
- package/dist/observability/secret-env-scrub.d.ts +2 -26
- package/dist/observability/secret-env-scrub.js +2 -2
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +3 -2
- package/dist/project-identity.d.ts +29 -0
- package/dist/project-identity.js +120 -0
- package/dist/project-memory.d.ts +28 -1
- package/dist/project-memory.js +22 -22
- package/dist/run-local.d.ts +14 -4
- package/dist/run-local.js +8 -7
- package/dist/runtime-caps-resolver.d.ts +8 -8
- package/dist/runtime-caps-resolver.js +16 -14
- package/dist/sealed-key.js +3 -7
- package/dist/server-secret-env.d.ts +41 -0
- package/dist/server-secret-env.js +18 -0
- package/dist/tool-approval.d.ts +48 -1
- package/dist/tool-approval.js +10 -3
- package/dist/trace/redact.d.ts +1 -1
- package/package.json +4 -3
|
@@ -10,8 +10,9 @@ import { applyCenterPlugins } from "../capabilities/center-plugins.js";
|
|
|
10
10
|
import { applyCatalogToSource, withPromptArtifactBackfill } from "../capabilities/center-prompts.js";
|
|
11
11
|
import { validatePromptsDomain, CORE_ENGINE_VERSION } from "../prompts-domain-validate.js";
|
|
12
12
|
import { centerScenarios, centerScenarioDetails } from "../capabilities/scenarios.js";
|
|
13
|
-
import { configLkgEnabled } from "../config.js";
|
|
14
|
-
import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg } from "../config-lkg.js";
|
|
13
|
+
import { configLkgEnabled, isRosterGateArmed } from "../config.js";
|
|
14
|
+
import { defaultLkgPath, defaultSkillCacheDir, saveLkg, loadLkg, legacyV1LkgPath, migrateV1Lkg } from "../config-lkg.js";
|
|
15
|
+
import { answerStillCurrent, centerStateBin, credentialIdentityNow } from "../center-credential.js";
|
|
15
16
|
import { centerManagedConfigKeys } from "../config-center/apply-effective.js";
|
|
16
17
|
import { createConfigProvider, raceBootFetch, BOOT_FETCH_DEFERRED } from "../config-provider.js";
|
|
17
18
|
import { executionLaneCaps, executionLaneOf } from "../execution-lane-caps.js";
|
|
@@ -53,6 +54,43 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
53
54
|
let bootSkillStale = false;
|
|
54
55
|
let pendingRestart;
|
|
55
56
|
const cc = config.configCenter;
|
|
57
|
+
const bootCredential = cc?.credential.read();
|
|
58
|
+
if (cc !== undefined && bootCredential !== undefined && !bootCredential.ok) {
|
|
59
|
+
logger.error("center_credential_unreadable", {
|
|
60
|
+
key: "SEMA_REGISTRY_TOKEN_FILE",
|
|
61
|
+
reason: bootCredential.reason,
|
|
62
|
+
effect: "every center request fails its own failure arm until the file is readable (config pull keeps env/LKG-less boot, caps fail-closed, lease takes the loud auth-error arm); no on-disk LKG / prompt state is read back (credential identity unknown)",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const currentIdentity = () => (cc === undefined ? undefined : credentialIdentityNow(cc.credential));
|
|
66
|
+
const candidateIdentity = new WeakMap();
|
|
67
|
+
const candidateIdentityOf = (eff) => candidateIdentity.get(eff);
|
|
68
|
+
const validatorIdentity = new Map();
|
|
69
|
+
const bindValidator = (etag, identity) => {
|
|
70
|
+
if (etag === undefined)
|
|
71
|
+
return;
|
|
72
|
+
validatorIdentity.delete(etag);
|
|
73
|
+
validatorIdentity.set(etag, identity);
|
|
74
|
+
const oldest = validatorIdentity.keys().next();
|
|
75
|
+
if (validatorIdentity.size > 16 && oldest.done !== true)
|
|
76
|
+
validatorIdentity.delete(oldest.value);
|
|
77
|
+
};
|
|
78
|
+
const pullEffective = async (provider, etag) => {
|
|
79
|
+
if (provider.kind === "local")
|
|
80
|
+
return provider.fetchEffective(etag);
|
|
81
|
+
const before = currentIdentity();
|
|
82
|
+
const conditional = etag !== undefined && before !== undefined && validatorIdentity.get(etag) === before ? etag : undefined;
|
|
83
|
+
const r = await provider.fetchEffective(conditional);
|
|
84
|
+
const used = r === null ? before : r.credentialIdentity;
|
|
85
|
+
if (cc === undefined || !answerStillCurrent(cc.credential, used)) {
|
|
86
|
+
throw new Error("center credential identity changed while the effective pull was in flight — response discarded; the next pull runs under the current credential");
|
|
87
|
+
}
|
|
88
|
+
if (r !== null) {
|
|
89
|
+
candidateIdentity.set(r.effective, used);
|
|
90
|
+
bindValidator(r.etag, used);
|
|
91
|
+
}
|
|
92
|
+
return r;
|
|
93
|
+
};
|
|
56
94
|
let sealedKeys;
|
|
57
95
|
try {
|
|
58
96
|
sealedKeys = await ensureSealedKeyStore(join(config.localDataRoot ?? localRoot, "sealed-key"), logger);
|
|
@@ -70,11 +108,11 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
70
108
|
const configProvider = cc || config.configProvider === "local"
|
|
71
109
|
? createConfigProvider({
|
|
72
110
|
...(config.configProvider ? { provider: config.configProvider } : {}),
|
|
73
|
-
...(cc ? { configCenter: { baseUrl: cc.baseUrl,
|
|
111
|
+
...(cc ? { configCenter: { baseUrl: cc.baseUrl, credential: cc.credential, worker: cc.worker, dryRun: cc.dryRun } } : {}),
|
|
74
112
|
...(config.configProvider === "local" ? { localDir: localRoot } : {}),
|
|
75
113
|
})
|
|
76
114
|
: undefined;
|
|
77
|
-
const rosterGateArmed = cc
|
|
115
|
+
const rosterGateArmed = isRosterGateArmed(cc);
|
|
78
116
|
const modelReadyState = { ready: !rosterGateArmed };
|
|
79
117
|
const markRosterLanded = (eff) => {
|
|
80
118
|
if (!modelReadyState.ready && (eff.models?.models ?? []).some((m) => m.enabled !== false)) {
|
|
@@ -88,41 +126,106 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
88
126
|
if (!pendingCatalogFaces)
|
|
89
127
|
return;
|
|
90
128
|
const { target } = pendingCatalogFaces;
|
|
91
|
-
if (await applyCatalogToSource(
|
|
129
|
+
if (await applyCatalogToSource(catalogSource, target, centerPrompts, logger, phase)) {
|
|
92
130
|
centerPrompts = target;
|
|
93
131
|
pendingCatalogFaces = undefined;
|
|
94
132
|
}
|
|
95
133
|
};
|
|
96
134
|
const promptEpochDir = join(config.localDataRoot ?? localRoot, "prompt-epoch");
|
|
97
|
-
const
|
|
98
|
-
|
|
135
|
+
const promptStateRoot = join(promptEpochDir, "state");
|
|
136
|
+
let promptStateIdentity = currentIdentity();
|
|
137
|
+
const promptStateBin = () => (cc === undefined ? "global" : promptStateIdentity === undefined ? undefined : centerStateBin(cc.worker, promptStateIdentity));
|
|
138
|
+
const storePerBin = (make) => {
|
|
139
|
+
const stores = new Map();
|
|
140
|
+
return (bin) => {
|
|
141
|
+
const hit = stores.get(bin);
|
|
142
|
+
if (hit !== undefined)
|
|
143
|
+
return hit;
|
|
144
|
+
const made = make(bin);
|
|
145
|
+
stores.set(bin, made);
|
|
146
|
+
return made;
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
const fileStateStoreFor = storePerBin((bin) => new FilePromptSourceStateStore(join(promptStateRoot, bin)));
|
|
150
|
+
const binnedPromptStateStore = (storeFor) => ({
|
|
151
|
+
load: async () => {
|
|
152
|
+
const bin = promptStateBin();
|
|
153
|
+
return bin === undefined ? undefined : storeFor(bin).load();
|
|
154
|
+
},
|
|
155
|
+
save: async (state) => {
|
|
156
|
+
const bin = promptStateBin();
|
|
157
|
+
if (bin === undefined)
|
|
158
|
+
throw new Error("center credential identity unknown (credential unreadable at adoption) — prompt source state not persisted");
|
|
159
|
+
await storeFor(bin).save(state);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
let promptPersisted;
|
|
163
|
+
const recordingStateStore = (inner) => ({
|
|
164
|
+
load: async () => {
|
|
165
|
+
promptPersisted = await inner.load();
|
|
166
|
+
return promptPersisted;
|
|
167
|
+
},
|
|
168
|
+
save: async (state) => {
|
|
169
|
+
await inner.save(state);
|
|
170
|
+
promptPersisted = state;
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const withByDigestBackfill = (inner) => cc?.baseUrl ? withPromptArtifactBackfill(inner, (d) => fetchPromptArtifact(cc, d), logger) : inner;
|
|
99
174
|
const promptSource = (() => {
|
|
100
175
|
try {
|
|
176
|
+
const bootBin = promptStateBin();
|
|
177
|
+
if (bootBin !== undefined)
|
|
178
|
+
fileStateStoreFor(bootBin);
|
|
101
179
|
return new CenterPromptSource({
|
|
102
180
|
store: withByDigestBackfill(new FilePromptArtifactStore(promptEpochDir)),
|
|
103
|
-
stateStore:
|
|
181
|
+
stateStore: recordingStateStore(binnedPromptStateStore(fileStateStoreFor)),
|
|
104
182
|
engineVersion: CORE_ENGINE_VERSION,
|
|
105
183
|
});
|
|
106
184
|
}
|
|
107
185
|
catch (e) {
|
|
108
186
|
logger.warn("center_prompts_store_unwritable", { dir: promptEpochDir, err: String(e), note: "file-backed prompt-epoch store unavailable — memory fallback (catalog candidate not restart-durable); built-ins/declaration axis unaffected" });
|
|
109
|
-
return new CenterPromptSource({
|
|
187
|
+
return new CenterPromptSource({
|
|
188
|
+
store: withByDigestBackfill(new MemoryPromptArtifactStore()),
|
|
189
|
+
stateStore: recordingStateStore(binnedPromptStateStore(storePerBin(() => new MemoryPromptSourceStateStore()))),
|
|
190
|
+
engineVersion: CORE_ENGINE_VERSION,
|
|
191
|
+
});
|
|
110
192
|
}
|
|
111
193
|
})();
|
|
112
|
-
|
|
194
|
+
const catalogSource = {
|
|
195
|
+
applySnapshot: (raw) => promptSource.applySnapshot(raw),
|
|
196
|
+
applyDisabled: (sourceRevision) => promptSource.applyDisabled(sourceRevision),
|
|
197
|
+
current: () => promptSource.current(),
|
|
198
|
+
persistedActive: () => promptPersisted?.state === "active",
|
|
199
|
+
};
|
|
200
|
+
const rebasePromptState = async (identity, phase) => {
|
|
201
|
+
promptStateIdentity = identity;
|
|
202
|
+
centerPrompts = undefined;
|
|
203
|
+
pendingCatalogFaces = undefined;
|
|
204
|
+
promptPersisted = undefined;
|
|
113
205
|
try {
|
|
114
206
|
await promptSource.restore();
|
|
115
207
|
}
|
|
116
208
|
catch (e) {
|
|
117
|
-
logger.warn("center_prompts_catalog_restore_failed", { err: String(e), note: "
|
|
209
|
+
logger.warn("center_prompts_catalog_restore_failed", { phase, err: String(e), note: "the persisted catalog state for this credential identity could not be loaded — catalog candidate absent until the next adopt; declaration axis and built-ins unaffected" });
|
|
118
210
|
}
|
|
119
|
-
|
|
211
|
+
if (cc !== undefined && identity !== undefined && currentIdentity() !== identity) {
|
|
212
|
+
promptStateIdentity = undefined;
|
|
213
|
+
await promptSource.restore();
|
|
214
|
+
logger.warn("center_prompts_catalog_identity_changed", { phase, note: "the center credential changed while the persisted catalog state was being read — that state is not used; the current identity's next candidate re-establishes it" });
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
if (configProvider && !cc?.dryRun)
|
|
218
|
+
await rebasePromptState(promptStateIdentity, "boot");
|
|
120
219
|
const adoptCenterPrompts = async (eff, phase) => {
|
|
220
|
+
const adoptedIdentity = candidateIdentityOf(eff);
|
|
221
|
+
if (adoptedIdentity !== undefined && adoptedIdentity !== promptStateIdentity) {
|
|
222
|
+
await rebasePromptState(adoptedIdentity, phase);
|
|
223
|
+
}
|
|
121
224
|
const raw = eff?.prompts;
|
|
122
225
|
if (raw === undefined) {
|
|
123
226
|
if (centerPrompts)
|
|
124
227
|
logger.info("center_prompts_cleared", { phase, note: "effective carries no prompts key (publish gate off) — built-in providers resume" });
|
|
125
|
-
const cleared = await applyCatalogToSource(
|
|
228
|
+
const cleared = await applyCatalogToSource(catalogSource, undefined, centerPrompts, logger, phase);
|
|
126
229
|
if (cleared) {
|
|
127
230
|
centerPrompts = undefined;
|
|
128
231
|
pendingCatalogFaces = undefined;
|
|
@@ -148,7 +251,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
148
251
|
note: "new tasks assemble the new pack (epoch re-pins at the next task boundary); in-flight sessions stay pinned",
|
|
149
252
|
});
|
|
150
253
|
}
|
|
151
|
-
const transitioned = await applyCatalogToSource(
|
|
254
|
+
const transitioned = await applyCatalogToSource(catalogSource, v.value, centerPrompts, logger, phase);
|
|
152
255
|
if (transitioned) {
|
|
153
256
|
centerPrompts = v.value;
|
|
154
257
|
pendingCatalogFaces = undefined;
|
|
@@ -159,7 +262,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
159
262
|
};
|
|
160
263
|
let bootConfigPending;
|
|
161
264
|
const lkgEnabled = configProvider?.kind === "remote" && configLkgEnabled();
|
|
162
|
-
const
|
|
265
|
+
const lkgPathFor = (identity) => process.env.CONFIG_LKG_PATH ?? defaultLkgPath(cc?.worker, identity);
|
|
163
266
|
const lkgSurvivesRestart = lkgEnabled && process.env.CONFIG_LKG_DURABLE === "true";
|
|
164
267
|
let lkgBooted = false;
|
|
165
268
|
let bootLkgCandidate;
|
|
@@ -170,9 +273,15 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
170
273
|
return true;
|
|
171
274
|
if (!lkgEnabled)
|
|
172
275
|
return false;
|
|
276
|
+
const identity = candidateIdentityOf(effectiveObj);
|
|
277
|
+
if (identity === undefined) {
|
|
278
|
+
logger.warn("config_lkg_save_failed", { err: "candidate carries no credential identity", note: "LKG NOT written (the bin is keyed by the identity that fetched the candidate) — handoff NOT confirmed" });
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
const lkgPath = lkgPathFor(identity);
|
|
173
282
|
try {
|
|
174
283
|
if (effectiveObj.skills && config.configCenter) {
|
|
175
|
-
await applyCenterSkills([], effectiveObj.skills, config.configCenter
|
|
284
|
+
await applyCenterSkills([], effectiveObj.skills, config.configCenter, logger, undefined, defaultSkillCacheDir());
|
|
176
285
|
}
|
|
177
286
|
const enabledSkills = (effectiveObj.skills?.skills ?? []).filter((m) => m.enabled !== false);
|
|
178
287
|
const cacheDir = defaultSkillCacheDir();
|
|
@@ -193,7 +302,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
193
302
|
}
|
|
194
303
|
}
|
|
195
304
|
lastPrewarmBodiesVerified = true;
|
|
196
|
-
await saveLkg(lkgPath, cc?.worker, effectiveObj, etag);
|
|
305
|
+
await saveLkg(lkgPath, cc?.worker, identity, effectiveObj, etag);
|
|
197
306
|
return lkgSurvivesRestart;
|
|
198
307
|
}
|
|
199
308
|
catch (err) {
|
|
@@ -206,14 +315,40 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
206
315
|
const bootFromLkg = async (why) => {
|
|
207
316
|
if (!lkgEnabled || dryRun)
|
|
208
317
|
return null;
|
|
209
|
-
const
|
|
318
|
+
const identity = currentIdentity();
|
|
319
|
+
if (identity === undefined) {
|
|
320
|
+
logger.warn("config_lkg_unusable", { reason: "center credential unreadable — credential identity unknown, no LKG bin is read back", note: "env fallback serves; the live pull retries on the refresh cadence once the credential is readable" });
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const lkgPath = lkgPathFor(identity);
|
|
324
|
+
const legacy = await migrateV1Lkg({ legacyPath: process.env.CONFIG_LKG_PATH ?? legacyV1LkgPath(cc?.worker), binPath: lkgPath, worker: cc?.worker, identity });
|
|
325
|
+
switch (legacy.outcome) {
|
|
326
|
+
case "none":
|
|
327
|
+
break;
|
|
328
|
+
case "migrated":
|
|
329
|
+
logger.info("config_lkg_migrated", { from: legacy.from, retiredTo: legacy.retiredTo, to: lkgPath, note: "formatVersion 1 LKG (pull-token identity = this worker) migrated once into this credential identity's bin; the old file was renamed, not deleted" });
|
|
330
|
+
if (legacy.retireError !== undefined)
|
|
331
|
+
logger.warn("config_lkg_legacy_not_retired", { path: legacy.from, reason: legacy.retireError, note: "the bin was written and is used; only the old file's rename / copy failed" });
|
|
332
|
+
break;
|
|
333
|
+
case "not-migrated":
|
|
334
|
+
logger.warn("config_lkg_unusable", { path: legacy.path, reason: legacy.reason, note: "env fallback serves; the cache rewrites on the next clean candidate" });
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
const read = await loadLkg(lkgPath, cc?.worker, identity);
|
|
210
338
|
if (!read.lkg) {
|
|
211
339
|
if (read.reason !== "absent")
|
|
212
340
|
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" });
|
|
213
341
|
return null;
|
|
214
342
|
}
|
|
343
|
+
if (currentIdentity() !== identity) {
|
|
344
|
+
logger.warn("config_lkg_unusable", { path: lkgPath, reason: "center credential identity changed while the LKG was being read — not used", note: "env fallback serves; the current identity's next pull re-establishes its own LKG" });
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
215
347
|
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" });
|
|
216
|
-
|
|
348
|
+
const lkgEffective = read.lkg.effective;
|
|
349
|
+
candidateIdentity.set(lkgEffective, identity);
|
|
350
|
+
bindValidator(read.lkg.etag, identity);
|
|
351
|
+
return { effective: lkgEffective, ...(read.lkg.etag !== undefined ? { etag: read.lkg.etag } : {}) };
|
|
217
352
|
};
|
|
218
353
|
const bootApplyLkgInline = async (lkgR) => {
|
|
219
354
|
const lkgGen = generationOf(lkgR.effective.version);
|
|
@@ -231,7 +366,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
231
366
|
logger.info("config_loaded", { source: "lkg", version: lkgR.effective.version, worker: cc?.worker ?? "(global)", models: cfgModels });
|
|
232
367
|
};
|
|
233
368
|
try {
|
|
234
|
-
const bootFetch = configProvider
|
|
369
|
+
const bootFetch = pullEffective(configProvider, undefined);
|
|
235
370
|
const raced = await raceBootFetch(bootFetch, config.configBootFetchBudgetMs);
|
|
236
371
|
let lkgFallback = null;
|
|
237
372
|
if (raced === BOOT_FETCH_DEFERRED) {
|
|
@@ -327,6 +462,8 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
327
462
|
}
|
|
328
463
|
}
|
|
329
464
|
}
|
|
465
|
+
if (!dryRun && cc !== undefined && currentIdentity() !== promptStateIdentity)
|
|
466
|
+
await rebasePromptState(currentIdentity(), "boot-end");
|
|
330
467
|
}
|
|
331
468
|
const readFaceBaseline = { ...(config.readFace !== undefined ? { readFace: config.readFace } : {}), readFaceSource: config.readFaceSource };
|
|
332
469
|
const readFaceEnvHeld = applyCenterReadFace(config, effective ?? { version: 0, updatedAt: "", models: { models: [], roles: {} } }, effective ? logger : undefined).envHeld;
|
|
@@ -438,7 +575,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
438
575
|
},
|
|
439
576
|
async applyCenterCapabilities(skills) {
|
|
440
577
|
if (effective?.skills && config.configCenter) {
|
|
441
|
-
skills = await applyCenterSkills(skills, effective.skills, config.configCenter
|
|
578
|
+
skills = await applyCenterSkills(skills, effective.skills, config.configCenter, logger, undefined, configLkgEnabled() ? defaultSkillCacheDir() : undefined);
|
|
442
579
|
}
|
|
443
580
|
if (effective?.plugins) {
|
|
444
581
|
const pluginOut = await applyCenterPlugins(skills, effective, {
|
|
@@ -458,7 +595,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
458
595
|
}
|
|
459
596
|
if (bootLkgCandidate && lkgEnabled) {
|
|
460
597
|
const bootPublished = await persistLkgDurable(bootLkgCandidate.effective, bootLkgCandidate.etag).catch((err) => {
|
|
461
|
-
logger.warn("config_lkg_save_failed", {
|
|
598
|
+
logger.warn("config_lkg_save_failed", { err: String(err), note: "boot LKG persist failed — retained for the per-tick retry" });
|
|
462
599
|
return false;
|
|
463
600
|
});
|
|
464
601
|
{
|
|
@@ -521,7 +658,7 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
521
658
|
metrics.inc("models_tiers_plane_deferred_stuck_total");
|
|
522
659
|
logger.warn("models_tiers_deferred_no_handoff", { version: planeDeferredNoHandoff.version, since: planeDeferredNoHandoff.since, note: "candidate still retained without a next-boot handoff — mount restart-surviving LKG storage + CONFIG_LKG_DURABLE=true, or restart manually after ensuring the candidate is re-pulled at boot" });
|
|
523
660
|
}
|
|
524
|
-
const r = arrival !== undefined ? arrival.result : await configProvider
|
|
661
|
+
const r = arrival !== undefined ? arrival.result : await pullEffective(configProvider, ccEtag);
|
|
525
662
|
if (r === null && planeDeferredNoHandoff) {
|
|
526
663
|
const skillsDebt = (planeDeferredNoHandoff.blocked ?? []).includes("skills");
|
|
527
664
|
const promoted = lkgSurvivesRestart || skillsDebt ? await persistLkgDurable(planeDeferredNoHandoff.candidate, planeDeferredNoHandoff.candidateEtag) : false;
|
|
@@ -793,7 +930,15 @@ export async function createConfigCenterRuntime(ctx) {
|
|
|
793
930
|
};
|
|
794
931
|
if (bootConfigPending) {
|
|
795
932
|
const arrivalGen = reserveArrivalGeneration();
|
|
796
|
-
|
|
933
|
+
const continueArrival = (r) => void (lkgBooted ? runTick({ result: r, ordinal: arrivalGen }) : deferredBootApply(r, arrivalGen));
|
|
934
|
+
void bootConfigPending.then((r) => {
|
|
935
|
+
if (r !== null && configProvider.kind === "remote" && candidateIdentityOf(r.effective) !== currentIdentity()) {
|
|
936
|
+
logger.warn("config_boot_arrival_identity_changed", { note: "the late boot pull landed under a center credential that is no longer current — not adopted; re-pulling once under the current credential" });
|
|
937
|
+
void pullEffective(configProvider, undefined).then(continueArrival, (err) => logger.warn("config_boot_arrival_repull_failed", { err: String(err), note: "the refresh cadence retries under the current credential" }));
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
continueArrival(r);
|
|
941
|
+
}, () => { });
|
|
797
942
|
}
|
|
798
943
|
}
|
|
799
944
|
},
|
|
@@ -54,7 +54,8 @@ export interface OrgMemoryAdmissionWiring {
|
|
|
54
54
|
/** §7 第二消费者收编:同一个目录实例也喂 memory-policy 面的 `org:` 属主门(`ServiceDeps.orgMemoryDirectory`)。
|
|
55
55
|
* **同实例**是这条收编的全部意义——「谁属于 org:acme」在一个进程里必须只有一个答案,两面共享同一份
|
|
56
56
|
* TTL 缓存/退避窗/gen 高水位;各建各的目录会让准入门与策略面在吊销窗内公开分歧。缺席=无目录源
|
|
57
|
-
* (单用户/无 center 且无 env 表)⇒ 策略面逐字保持收编前的 operator-only 行为。
|
|
57
|
+
* (单用户/无 center 且无 env 表)⇒ 策略面逐字保持收编前的 operator-only 行为。
|
|
58
|
+
* 远程腿的这一个实例内部按中心凭证身份分箱([ref] E-15):两面问到的是同一身份的同一箱。 */
|
|
58
59
|
directory: OrgMemoryDirectory | undefined;
|
|
59
60
|
}
|
|
60
61
|
/** 依赖收窄到真实消费面(warn/inc 各一手)——接口隔离让测试用真形字面量,零铸形([ref] §1)。 */
|
package/dist/boot/org-memory.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fetchPrincipalOrgMemory } from "../config-center/http-client.js";
|
|
2
|
+
import { createIdentityBins } from "../center-credential.js";
|
|
2
3
|
import { createMemoryScopeAdmission, createOrgMemoryDirectory, parseOrgDirectoryStatic } from "../org-memory-admission.js";
|
|
3
4
|
const ORG_PREFIX = "org:";
|
|
4
5
|
function collectDeploymentOrgScopes(config) {
|
|
@@ -25,11 +26,14 @@ export function createOrgMemoryAdmissionWiring(opts) {
|
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
28
|
const directory = centerLeg !== undefined
|
|
28
|
-
?
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
? (() => {
|
|
30
|
+
const directoryNow = createIdentityBins(centerLeg.credential, (credential) => createOrgMemoryDirectory({
|
|
31
|
+
fetchSection: (principal) => fetchPrincipalOrgMemory({ baseUrl: centerLeg.baseUrl, credential }, principal, fetch, centerLeg.worker),
|
|
32
|
+
grantTtlMs: config.memoryOrgGrantTtlMs,
|
|
33
|
+
unavailableBackoffMs: config.memoryOrgUnavailableBackoffMs,
|
|
34
|
+
}));
|
|
35
|
+
return { lookup: (principal) => directoryNow().lookup(principal) };
|
|
36
|
+
})()
|
|
33
37
|
: staticJson !== undefined
|
|
34
38
|
? createOrgMemoryDirectory({
|
|
35
39
|
staticTable: parseOrgDirectoryStatic(staticJson),
|
|
@@ -25,7 +25,7 @@ import { isIsolatedExecEnv, routeServiceTask, supPostureOverrides } from "../rou
|
|
|
25
25
|
import { gateExecutionLane } from "../runtime-caps-resolver.js";
|
|
26
26
|
import { applyRuntimeGovernance, stripDelegationTools } from "../runtime-governance.js";
|
|
27
27
|
import { HttpError, encodeCheckpointScope } from "../security.js";
|
|
28
|
-
import { memorySpecForRequest } from "../memory-scope.js";
|
|
28
|
+
import { memoryProjectKeyFor, memorySpecForRequest } from "../memory-scope.js";
|
|
29
29
|
import { singleMemberOffFieldIssue } from "../request-value-closure.js";
|
|
30
30
|
import { a2aForScenario, mcpForScenario } from "../config-center/facade.js";
|
|
31
31
|
import { normalizeAttachments, normalizeResilience, normalizeResumeAtMode, normalizeSuggestNextPrompts, promptProfileFromBody, resolveTaskLimits, retainBackgroundProcessesFromBody, taskAgentsSpecFragment, toolMaterializeStrategyFromBody, toolNameListFromBody } from "../spec-fields.js";
|
|
@@ -43,6 +43,12 @@ export function createResolveSpec(ctx) {
|
|
|
43
43
|
const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, fenceSessionOwner, hookLlm, hookAgent, fleetBus, hookWakeBus, noteSessionShellGate, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, approverSeat, lockedKeys, } = ctx;
|
|
44
44
|
assertGuardPatternsUsable(config);
|
|
45
45
|
const onlySensitiveBaseline = buildOnlySensitiveBaselineWarning(config, { durableEnabled, singleUserAutoAcceptBaseline });
|
|
46
|
+
const realTaskRootOf = (auth) => {
|
|
47
|
+
if (isSandboxPathAdjudicationLane(config.remoteExec?.provider))
|
|
48
|
+
return undefined;
|
|
49
|
+
const sessionCwd = auth?.sessionId ? effectiveHostWorkspace(perSessionCwd.get(auth.sessionId), {}) : undefined;
|
|
50
|
+
return sessionCwd ?? (config.remoteExec === undefined ? process.cwd() : undefined);
|
|
51
|
+
};
|
|
46
52
|
if (onlySensitiveBaseline)
|
|
47
53
|
logger.warn(onlySensitiveBaseline.event, onlySensitiveBaseline.fields);
|
|
48
54
|
const resolveSpec = async (body, req, auth, opts) => {
|
|
@@ -391,7 +397,7 @@ export function createResolveSpec(ctx) {
|
|
|
391
397
|
})(),
|
|
392
398
|
...(config.requirePrincipal !== true ? { backgroundScope: "session" } : {}),
|
|
393
399
|
memory: memoryEngine
|
|
394
|
-
? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes, { multiTenant: config.requirePrincipal === true }, { memory: body.memory, memoryCapture: body.memoryCapture })
|
|
400
|
+
? memorySpecForRequest(auth?.memoryScope, memoryProjectKeyFor(config, auth?.memoryScope, realTaskRootOf(auth)), body.memoryWrite, s4DefaultScopes, { multiTenant: config.requirePrincipal === true }, { memory: body.memory, memoryCapture: body.memoryCapture })
|
|
395
401
|
: undefined,
|
|
396
402
|
...((v) => (v !== undefined ? { memoryPersistenceCapable: v } : {}))(effectiveMemoryPersistenceCapable(config)),
|
|
397
403
|
tools: ((base) => {
|
|
@@ -450,8 +456,7 @@ export function createResolveSpec(ctx) {
|
|
|
450
456
|
};
|
|
451
457
|
const pathAdjudication = hostSemanticsLane
|
|
452
458
|
? (() => {
|
|
453
|
-
const
|
|
454
|
-
const realRoot = sessionCwd ?? (config.remoteExec === undefined ? process.cwd() : undefined);
|
|
459
|
+
const realRoot = realTaskRootOf(auth);
|
|
455
460
|
const gateCwd = realRoot ?? join(config.localDataRoot ?? localRoot, "fs-write-gate-unrooted");
|
|
456
461
|
return {
|
|
457
462
|
env: new NodeExecutionEnv({ cwd: gateCwd }),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { applyGovernedVerdictAbsenceWarning, applyLocalAutoModeDeny, applyMemoryOptOutGrant, applyObserverEnvOptIn, centerEntitlementSourceWired, createPrincipalEntitlementsClient,
|
|
1
|
+
import { applyGovernedVerdictAbsenceWarning, applyLocalAutoModeDeny, applyMemoryOptOutGrant, applyObserverEnvOptIn, centerEntitlementSourceWired, createPrincipalEntitlementsClient, } from "../runtime-caps-resolver.js";
|
|
2
|
+
import { scopedTokenNeedsWorker } from "../center-credential.js";
|
|
2
3
|
export function assertMemoryCapturePolicyWirable(input) {
|
|
3
4
|
if (input.policy !== "governed")
|
|
4
5
|
return;
|
|
@@ -36,15 +37,14 @@ export function createRuntimeCaps(ctx) {
|
|
|
36
37
|
reason: "MEMORY_CAPTURE_POLICY=governed with NO local memory_optout_grant table: the only verdict source is the config center, and `allowMemoryOptOut` is an OPTIONAL entitlement key — for any principal whose caps omit it the verdict is ABSENT and core refuses that session's memory.capture:\"off\" (memory.capture_optout_denied, fail-closed). ANONYMOUS/system runs are worse off still: the center source is per-principal and is never consulted without an identity, so on this shape a principal-less run can never obtain a verdict at all. Verify the center emits allowMemoryOptOut for every principal, or wire a SQL backend (DB_BACKEND=mysql|pg) so the grant table answers (its deployment-default row also covers anonymous runs).",
|
|
37
38
|
});
|
|
38
39
|
}
|
|
39
|
-
if (centerEntitlementSourceWired(config) && config.configCenter && scopedTokenNeedsWorker(config.configCenter.
|
|
40
|
+
if (centerEntitlementSourceWired(config) && config.configCenter && scopedTokenNeedsWorker(config.configCenter.credential.read(), config.configCenter.worker)) {
|
|
40
41
|
logger.warn("runtime_caps_scoped_token_no_worker", {
|
|
41
42
|
reason: "SEMA_REGISTRY_TOKEN is a worker-scoped wpt_ token but SEMA_REGISTRY_WORKER is unset → center 403s per-principal caps → ALL workflow self-orchestration will be fail-closed denied. Set SEMA_REGISTRY_WORKER (the orchestrator normally injects it) or use the full SERVICE_PULL_TOKEN.",
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
const principalCaps = centerEntitlementSourceWired(config) && config.configCenter
|
|
45
46
|
? createPrincipalEntitlementsClient({
|
|
46
|
-
|
|
47
|
-
token: config.configCenter.token,
|
|
47
|
+
center: config.configCenter,
|
|
48
48
|
...(config.configCenter.worker ? { worker: config.configCenter.worker } : {}),
|
|
49
49
|
onError: (err, principal) => logger.warn("runtime_caps_resolve_failed", { principal, err: String(err) }),
|
|
50
50
|
})
|
package/dist/brain.js
CHANGED
|
@@ -2,7 +2,8 @@ import { adjudicateModelRoute, createAnthropicBrain, createCircuitBreakerBrain,
|
|
|
2
2
|
import { routeKeyOf } from "@sema-agent/core";
|
|
3
3
|
import { buildDefaultResilience } from "./config-types.js";
|
|
4
4
|
import { resolveModelApiKey } from "./key-resolver.js";
|
|
5
|
-
import {
|
|
5
|
+
import { dispatchBrainLane } from "./model-provider.js";
|
|
6
|
+
import { openaiRouteBaseUrl } from "./model-route-endpoint.js";
|
|
6
7
|
import { buildDisplaySafeRefusal, buildDisplaySafeUrl } from "./trace/redact.js";
|
|
7
8
|
function isOffDeploymentRoot(baseUrl, config) {
|
|
8
9
|
if (!baseUrl)
|
|
@@ -10,11 +11,17 @@ function isOffDeploymentRoot(baseUrl, config) {
|
|
|
10
11
|
const entry = routeKeyOf(baseUrl);
|
|
11
12
|
if (entry === "")
|
|
12
13
|
return false;
|
|
13
|
-
return !
|
|
14
|
+
return !deploymentGatewayRoots(config).some((u) => routeKeyOf(u) === entry);
|
|
15
|
+
}
|
|
16
|
+
function deploymentGatewayRoots(config) {
|
|
17
|
+
return config.gatewayBaseUrl !== undefined ? [config.gatewayBaseUrl, ...config.gatewayFallbackUrls] : [];
|
|
18
|
+
}
|
|
19
|
+
function deploymentGatewayKey(config) {
|
|
20
|
+
return config.gatewayBaseUrl !== undefined ? config.gatewayApiKey : undefined;
|
|
14
21
|
}
|
|
15
22
|
function degradeDeclaredRoot(fallbackBaseUrl, config) {
|
|
16
|
-
const armed = Boolean(config
|
|
17
|
-
return armed ? config.gatewayBaseUrl : (fallbackBaseUrl
|
|
23
|
+
const armed = Boolean(deploymentGatewayKey(config)) && isOffDeploymentRoot(fallbackBaseUrl, config);
|
|
24
|
+
return armed ? config.gatewayBaseUrl : openaiRouteBaseUrl(fallbackBaseUrl, config.gatewayBaseUrl);
|
|
18
25
|
}
|
|
19
26
|
export const FAST_FAIL_MAX_RETRIES = 2;
|
|
20
27
|
function constructRouteBrain(label, sourceUrls, construct) {
|
|
@@ -41,12 +48,12 @@ export function createBrain(config, deps = {}) {
|
|
|
41
48
|
: fastFailTopology
|
|
42
49
|
? { maxRetries: FAST_FAIL_MAX_RETRIES }
|
|
43
50
|
: {};
|
|
44
|
-
const routes =
|
|
51
|
+
const routes = deploymentGatewayRoots(config);
|
|
45
52
|
const openaiBrains = routes.map((baseUrl, i) => {
|
|
46
53
|
const brain = constructRouteBrain(i === 0 ? "gateway route" : `gateway fallback route[${i - 1}]`, [baseUrl], () => createOpenAIBrain({
|
|
47
54
|
baseUrl,
|
|
48
55
|
get apiKey() {
|
|
49
|
-
return config
|
|
56
|
+
return deploymentGatewayKey(config);
|
|
50
57
|
},
|
|
51
58
|
...retryCap,
|
|
52
59
|
fetchImpl,
|
|
@@ -62,7 +69,11 @@ export function createBrain(config, deps = {}) {
|
|
|
62
69
|
})
|
|
63
70
|
: brain;
|
|
64
71
|
});
|
|
65
|
-
const gatewayBrain = openaiBrains.length
|
|
72
|
+
const gatewayBrain = openaiBrains.length === 0
|
|
73
|
+
? constructRouteBrain("openai route (no gateway declared)", [], () => createOpenAIBrain({ ...retryCap, fetchImpl, ...timeouts }))
|
|
74
|
+
: openaiBrains.length > 1
|
|
75
|
+
? createFailoverBrain(openaiBrains)
|
|
76
|
+
: openaiBrains[0];
|
|
66
77
|
const anthropicCfg = config.anthropic;
|
|
67
78
|
const anthropicBrain = anthropicCfg
|
|
68
79
|
? constructRouteBrain("anthropic route", [anthropicCfg.baseUrl], () => createAnthropicBrain({
|
|
@@ -86,9 +97,8 @@ export function createBrain(config, deps = {}) {
|
|
|
86
97
|
if (fallbackModel) {
|
|
87
98
|
const fbOwnApiKey = () => resolveModelApiKey(fallbackModel.name, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {}) ??
|
|
88
99
|
resolveModelApiKey(degradeTo, config.modelApiKeyEnv ?? {}, process.env, config.modelApiKeys ?? {});
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const fallback = fallbackLane === "anthropic" && anthropicBrain
|
|
100
|
+
const anthropicLaneHop = dispatchBrainLane(fallbackModel.provider, anthropicBrain !== undefined) === "anthropic";
|
|
101
|
+
const fallback = anthropicLaneHop && anthropicBrain
|
|
92
102
|
?
|
|
93
103
|
anthropicBrain
|
|
94
104
|
: constructRouteBrain(`degrade target "${degradeTo}"`, [fallbackModel.baseUrl, config.gatewayBaseUrl], () => createOpenAIBrain({
|
|
@@ -96,7 +106,7 @@ export function createBrain(config, deps = {}) {
|
|
|
96
106
|
return degradeDeclaredRoot(fallbackModel.baseUrl, config);
|
|
97
107
|
},
|
|
98
108
|
get apiKey() {
|
|
99
|
-
return config
|
|
109
|
+
return deploymentGatewayKey(config) || undefined;
|
|
100
110
|
},
|
|
101
111
|
...(config.gatewayMaxRetries !== undefined ? { maxRetries: config.gatewayMaxRetries } : {}),
|
|
102
112
|
fetchImpl,
|
|
@@ -129,8 +139,10 @@ export function brainSummary(config) {
|
|
|
129
139
|
const r = config.resilience ?? DEFAULT_RESILIENCE;
|
|
130
140
|
const hasFailover = config.gatewayFallbackUrls.length > 0;
|
|
131
141
|
const breakerActive = r.circuitBreaker && hasFailover;
|
|
142
|
+
const degradeTarget = config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
|
|
143
|
+
const degradeEndpoint = degradeTarget ? openaiRouteBaseUrl(degradeTarget.baseUrl, config.gatewayBaseUrl) : undefined;
|
|
132
144
|
return {
|
|
133
|
-
gateway: buildDisplaySafeUrl(config.gatewayBaseUrl),
|
|
145
|
+
gateway: buildDisplaySafeUrl(config.gatewayBaseUrl ?? "") || null,
|
|
134
146
|
gatewayFallbacks: config.gatewayFallbackUrls.length,
|
|
135
147
|
failover: hasFailover,
|
|
136
148
|
anthropicRoute: Boolean(config.anthropic),
|
|
@@ -147,14 +159,12 @@ export function brainSummary(config) {
|
|
|
147
159
|
: "engine-default",
|
|
148
160
|
circuitBreakerNoop: r.circuitBreaker && !hasFailover ? "needs ≥2 gateway routes" : null,
|
|
149
161
|
reactiveDegradeTo: config.degrade?.reactive && config.models?.[config.degrade.to] ? config.degrade.to : null,
|
|
150
|
-
reactiveDegradeBaseUrl:
|
|
151
|
-
? buildDisplaySafeUrl(config.models[config.degrade.to].baseUrl || config.gatewayBaseUrl)
|
|
152
|
-
: null,
|
|
162
|
+
reactiveDegradeBaseUrl: buildDisplaySafeUrl(degradeEndpoint ?? "") || null,
|
|
153
163
|
...((() => {
|
|
154
164
|
const target = config.degrade?.reactive ? config.models?.[config.degrade.to] : undefined;
|
|
155
165
|
if (!target)
|
|
156
166
|
return { reactiveDegradeRoutePairing: null, reactiveDegradeRouteRefusal: null };
|
|
157
|
-
if (
|
|
167
|
+
if (dispatchBrainLane(target.provider, config.anthropic !== undefined) === "anthropic") {
|
|
158
168
|
return { reactiveDegradeRoutePairing: "anthropic-lane", reactiveDegradeRouteRefusal: null };
|
|
159
169
|
}
|
|
160
170
|
const declaresOwnCredential = config.modelApiKeyEnv?.[target.name] !== undefined ||
|
|
@@ -165,12 +175,12 @@ export function brainSummary(config) {
|
|
|
165
175
|
if (declaresOwnCredential) {
|
|
166
176
|
return { reactiveDegradeRoutePairing: "per-model-declared", reactiveDegradeRouteRefusal: null };
|
|
167
177
|
}
|
|
168
|
-
const verdict = adjudicateModelRoute({ id: target.id, baseUrl: target.baseUrl }, resolveRouteCredential(
|
|
178
|
+
const verdict = adjudicateModelRoute({ id: target.id, baseUrl: target.baseUrl }, resolveRouteCredential(((key) => (key ? { configApiKey: key } : {}))(deploymentGatewayKey(config))), { baseUrl: degradeDeclaredRoot(target.baseUrl, config) });
|
|
169
179
|
return verdict.ok
|
|
170
180
|
? { reactiveDegradeRoutePairing: `ok:${verdict.posture}`, reactiveDegradeRouteRefusal: null }
|
|
171
181
|
: {
|
|
172
182
|
reactiveDegradeRoutePairing: `broken:${verdict.code}`,
|
|
173
|
-
reactiveDegradeRouteRefusal: buildDisplaySafeRefusal(routeRefusalText(verdict), [target.baseUrl ?? "", degradeDeclaredRoot(target.baseUrl, config)]),
|
|
183
|
+
reactiveDegradeRouteRefusal: buildDisplaySafeRefusal(routeRefusalText(verdict), [target.baseUrl ?? "", degradeDeclaredRoot(target.baseUrl, config) ?? ""]),
|
|
174
184
|
};
|
|
175
185
|
})()),
|
|
176
186
|
};
|
|
@@ -55,6 +55,9 @@ export declare function applyCatalogToSource(source: {
|
|
|
55
55
|
/** codex-2 F2(restart-owed disable):撤下写盘失败后重启,内存 prev 丢了但持久态还 active——
|
|
56
56
|
* 以 source 自身的 live candidate 为准判「欠 disable」,不是只看内存 prev。 */
|
|
57
57
|
current?(): unknown;
|
|
58
|
+
/** S-651(codex 修复验证轮 V1):本箱**持久记录**是 active —— 即使那份 artifact 此刻加载不出(活候选为空)。
|
|
59
|
+
* 「欠撤下」的真源是持久记录;活候选只是它能加载时的影子。缺席 = 调用方不掌握持久记录(沿用前两条判据)。 */
|
|
60
|
+
persistedActive?(): boolean;
|
|
58
61
|
}, next: PromptsDomainFaces | undefined, prev: PromptsDomainFaces | undefined, log: {
|
|
59
62
|
info: (msg: string, meta?: Record<string, unknown>) => void;
|
|
60
63
|
warn: (msg: string, meta?: Record<string, unknown>) => void;
|
|
@@ -55,7 +55,7 @@ export async function applyCatalogToSource(source, next, prev, log, phase) {
|
|
|
55
55
|
log.warn("center_prompts_catalog_apply_failed", { phase, code: r.code, reason: r.reason, note: "validate passed but the source apply failed (disk/state) — declaration axis and built-ins unaffected; candidate unchanged; the transition retries on the next refresh tick" });
|
|
56
56
|
return false;
|
|
57
57
|
}
|
|
58
|
-
if (prev?.catalog || source.current?.() !== undefined) {
|
|
58
|
+
if (prev?.catalog || source.current?.() !== undefined || source.persistedActive?.() === true) {
|
|
59
59
|
await source.applyDisabled(`${phase}:catalog-absent`);
|
|
60
60
|
log.info("center_prompts_catalog_disabled", { phase, note: "catalog axis withdrawn — core candidate cleared (does not revive offline)" });
|
|
61
61
|
}
|