@sema-agent/server 7.106.0 → 7.108.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 +52 -0
- package/README.md +2 -2
- package/USAGE.md +40 -4
- package/dist/approval-ask-audit-store.d.ts +62 -4
- package/dist/approval-ask-audit-store.js +67 -11
- package/dist/approval.js +16 -13
- package/dist/boot/coordinators.d.ts +4 -0
- package/dist/boot/coordinators.js +3 -2
- package/dist/boot/shutdown.d.ts +8 -0
- package/dist/boot/shutdown.js +31 -8
- package/dist/boot/stage-06-runners.d.ts +1 -0
- package/dist/boot/stage-06-runners.js +6 -3
- package/dist/boot/stage-07-capability-layer.d.ts +1 -0
- package/dist/boot/stage-08-reapers.d.ts +1 -0
- package/dist/boot/stage-09-leader.d.ts +1 -0
- package/dist/boot/stage-10-http-server.d.ts +1 -0
- package/dist/boot/stage-10-http-server.js +3 -1
- package/dist/brain.js +2 -1
- package/dist/capabilities/composite-members.d.ts +1 -1
- package/dist/capabilities/composite-members.js +1 -1
- package/dist/config-catalog.d.ts +6 -1
- package/dist/config-catalog.js +34 -17
- package/dist/config-center/apply-effective.js +2 -0
- package/dist/config-center/types.d.ts +7 -0
- package/dist/deployment-governance.js +2 -1
- package/dist/fleet-client.d.ts +10 -2
- package/dist/fleet-client.js +14 -8
- package/dist/http/memory-compliance-failure-reply.d.ts +41 -0
- package/dist/http/memory-compliance-failure-reply.js +73 -0
- package/dist/http/memory-store-refusal-reply.d.ts +2 -2
- package/dist/http/resume-legs.js +3 -2
- package/dist/http/routes/capabilities.js +0 -2
- package/dist/http/routes/diagnostics.js +2 -0
- package/dist/http/routes/memory-compliance.js +2 -34
- package/dist/http/routes/memory-origin.d.ts +1 -1
- package/dist/http/routes/sessions.js +3 -11
- package/dist/http/routes/side-query.js +7 -2
- package/dist/http/routes/tasks.js +2 -1
- package/dist/memory-store-refusal.d.ts +2 -1
- package/dist/model-entry-headers.d.ts +41 -0
- package/dist/model-entry-headers.js +61 -0
- package/dist/model-entry-refusal.d.ts +6 -0
- package/dist/model-entry-refusal.js +4 -0
- package/dist/model-reach-scope.d.ts +55 -0
- package/dist/model-reach-scope.js +16 -0
- package/dist/model-unreachable-notice.d.ts +38 -0
- package/dist/model-unreachable-notice.js +72 -0
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/observability/run-terminal.d.ts +8 -5
- package/dist/outbound-dispatcher.d.ts +69 -0
- package/dist/outbound-dispatcher.js +123 -3
- package/dist/plugins/park-commit-watch.d.ts +48 -0
- package/dist/plugins/park-commit-watch.js +87 -0
- package/dist/router/route-orchestration.js +7 -2
- package/dist/runs.js +3 -2
- package/dist/runtime-governance.js +3 -2
- package/dist/task-settings.d.ts +30 -17
- package/dist/task-settings.js +15 -4
- package/dist/tool-approval.d.ts +62 -0
- package/dist/tool-approval.js +67 -3
- package/dist/tool-rule-names.d.ts +25 -0
- package/dist/tool-rule-names.js +5 -0
- package/dist/trace/core-keyset-guard.d.ts +13 -1
- package/dist/trace/engine-notice-wire.d.ts +3 -1
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/project.d.ts +6 -3
- package/dist/trace/self-minted-wire.d.ts +71 -0
- package/dist/trace/self-minted-wire.js +28 -0
- package/package.json +2 -2
|
@@ -122,12 +122,10 @@ export async function handleCapabilities(req, res, match, ctx) {
|
|
|
122
122
|
subagentSteer: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
|
|
123
123
|
subagentResume: Boolean(deps.subagentSteerRegistry) && Boolean(deps.runStore),
|
|
124
124
|
taskSettings: { permissions: true, permissionMode: true, model: true, outputStyle: true, env: cwdHonored(deps.config) || deviceCwdHonored(deps.config), hooks: deps.config.requirePrincipal !== true },
|
|
125
|
-
modeShellGateTranslation: true,
|
|
126
125
|
permissionModeAuto: {
|
|
127
126
|
accepted: true,
|
|
128
127
|
classifierSeat: deps.autoModeFace?.seatMounted === true,
|
|
129
128
|
entitlementSource,
|
|
130
|
-
intentArming: true,
|
|
131
129
|
...autoMode,
|
|
132
130
|
},
|
|
133
131
|
readFace: deps.config.readFace ?? null,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildPostureKnobReading } from "../../posture-source.js";
|
|
2
2
|
import { buildReadFaceWiring } from "../../read-face-posture.js";
|
|
3
3
|
import { projectExecutionLaneCapability } from "../../capabilities/execution-lane.js";
|
|
4
|
+
import { readOutboundWiring } from "../../outbound-dispatcher.js";
|
|
4
5
|
import { resolveStreamApprovalGate } from "../../tool-approval.js";
|
|
5
6
|
import { sendJson, sendError } from "../send.js";
|
|
6
7
|
import { gatedPrincipal, explicitOperatorOk, isLoopbackAddress, verifiedIdentitySource } from "../principal-gate.js";
|
|
@@ -106,6 +107,7 @@ export async function handleDiagnostics(req, res, match, ctx) {
|
|
|
106
107
|
writeProtection: deps.writeProtection ?? null,
|
|
107
108
|
readFace: buildReadFaceWiring(deps.config),
|
|
108
109
|
executionLane: projectExecutionLaneCapability(deps.config),
|
|
110
|
+
outbound: readOutboundWiring(),
|
|
109
111
|
configApply: deps.configApplyState?.() ?? null,
|
|
110
112
|
mcpRevocations: deps.mcpRevocationState?.() ?? null,
|
|
111
113
|
serverGates: buildServerWiringGates({
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { ControlPlaneCorruptError } from "@sema-agent/core";
|
|
3
2
|
import { erasureInputSmugglesPrototypeKey } from "../../memory-operator-faces.js";
|
|
4
|
-
import {
|
|
3
|
+
import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
|
|
5
4
|
import { sendJson, sendError } from "../send.js";
|
|
6
5
|
import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
|
|
7
6
|
import { assertNever } from "../route-ctx.js";
|
|
@@ -11,40 +10,9 @@ const EraseRequestSchema = z.object({
|
|
|
11
10
|
select: z.record(z.string(), z.unknown()),
|
|
12
11
|
allowUnevidenced: z.boolean().optional(),
|
|
13
12
|
});
|
|
14
|
-
function sentCoreCode(res, coreCode, message) {
|
|
15
|
-
switch (coreCode) {
|
|
16
|
-
case "config.memory_erasure_request":
|
|
17
|
-
sendError(res, 400, "config.memory_erasure_request", message);
|
|
18
|
-
return true;
|
|
19
|
-
case "memory.erasure_evidence_unavailable":
|
|
20
|
-
sendError(res, 409, "memory.erasure_evidence_unavailable", message);
|
|
21
|
-
return true;
|
|
22
|
-
case "memory.erasure_selector_mismatch":
|
|
23
|
-
sendError(res, 409, "memory.erasure_selector_mismatch", message);
|
|
24
|
-
return true;
|
|
25
|
-
case "memory.erasure_census_incomplete":
|
|
26
|
-
sendError(res, 409, "memory.erasure_census_incomplete", message);
|
|
27
|
-
return true;
|
|
28
|
-
default:
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function codeOf(err) {
|
|
33
|
-
if (!(err instanceof Error))
|
|
34
|
-
return undefined;
|
|
35
|
-
const { code } = err;
|
|
36
|
-
return typeof code === "string" ? code : undefined;
|
|
37
|
-
}
|
|
38
13
|
function sentClassifiedFailure(res, ctx, face, err) {
|
|
39
|
-
|
|
40
|
-
if (coreCode !== undefined && sentCoreCode(res, coreCode, err instanceof Error ? err.message : String(err)))
|
|
14
|
+
if (sentMemoryComplianceFailure(res, err, { kind: "operator" }))
|
|
41
15
|
return true;
|
|
42
|
-
if (sentMemoryStoreRefusal(res, err))
|
|
43
|
-
return true;
|
|
44
|
-
if (err instanceof ControlPlaneCorruptError) {
|
|
45
|
-
sendError(res, 500, "internal.memory_control_plane_corrupt", err.message);
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
16
|
ctx.deps.logger?.warn?.(`memory_compliance_${face}_failed`, { err: String(err) });
|
|
49
17
|
return false;
|
|
50
18
|
}
|
|
@@ -114,7 +114,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
|
|
|
114
114
|
baseRev: string;
|
|
115
115
|
origin: {
|
|
116
116
|
at: number;
|
|
117
|
-
cause?: "static" | "
|
|
117
|
+
cause?: "static" | "observed" | "unattributed" | "derived" | undefined;
|
|
118
118
|
taint: "external";
|
|
119
119
|
};
|
|
120
120
|
};
|
|
@@ -4,7 +4,7 @@ import { isUuidV7, isUuidShape, legacyUnownedSessionOk } from "../../security.js
|
|
|
4
4
|
import { windowMessages, truncateMessageBlobs } from "../../audit.js";
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
6
6
|
import { recordFailOpen } from "../../observability/fail-open.js";
|
|
7
|
-
import {
|
|
7
|
+
import { sentMemoryComplianceFailure } from "../memory-compliance-failure-reply.js";
|
|
8
8
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
|
|
9
9
|
import { readWritableSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
|
|
10
10
|
import { sendJson, sendError, sendSessionError, sendSessionPolicyError, sseHeaders } from "../send.js";
|
|
@@ -526,17 +526,9 @@ export async function handleSessions(req, res, match, ctx) {
|
|
|
526
526
|
sendJson(res, 200, await faces.erase({ requestId: raw.requestId, select: { sessionId }, ...(raw.allowUnevidenced !== undefined ? { allowUnevidenced: raw.allowUnevidenced } : {}) }));
|
|
527
527
|
}
|
|
528
528
|
catch (err) {
|
|
529
|
-
const
|
|
530
|
-
if (
|
|
531
|
-
sendError(res, 400, "config.memory_erasure_request", err instanceof Error ? err.message : "invalid memory erasure request");
|
|
529
|
+
const withheld = (coreMessage) => deps.logger?.warn?.("session_memory_erase_refused", { sessionId, err: coreMessage });
|
|
530
|
+
if (sentMemoryComplianceFailure(res, err, { kind: "session_owner", withheld }))
|
|
532
531
|
return;
|
|
533
|
-
}
|
|
534
|
-
const outside = targetOutsideStoreOf(err);
|
|
535
|
-
if (outside !== undefined) {
|
|
536
|
-
deps.logger?.warn?.("session_memory_erase_refused", { sessionId, err: outside.message });
|
|
537
|
-
sendError(res, 409, "memory.target_outside_store", "session memory erasure refused — a directory in this deployment's memory store leads outside it (a link), so this erasure did not complete (a refusal before its commit point changed nothing; one after it keeps the deletions already made and a journal that finishes the rest once the link is removed); ask the operator to remove the link, then resend with the same requestId");
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
532
|
deps.logger?.warn?.("session_memory_erase_failed", { sessionId, err: String(err) });
|
|
541
533
|
sendError(res, 500, "internal.error", "session memory erasure failed");
|
|
542
534
|
}
|
|
@@ -3,7 +3,7 @@ import { HttpError } from "../../security.js";
|
|
|
3
3
|
import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../../model-select.js";
|
|
4
4
|
import { modelUnavailableMessage } from "../../model-entry-refusal.js";
|
|
5
5
|
import { redactSecrets } from "../../trace/redact.js";
|
|
6
|
-
import { admitProviderEcho } from "../../observability/run-terminal.js";
|
|
6
|
+
import { admitProviderEcho, redactErrorMessage } from "../../observability/run-terminal.js";
|
|
7
7
|
import { sendJson, sendError, httpErrorCode, msg } from "../send.js";
|
|
8
8
|
import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
|
|
9
9
|
import { assertNever } from "../route-ctx.js";
|
|
@@ -35,6 +35,11 @@ function shapeSideQueryEcho(result) {
|
|
|
35
35
|
const { responseModel: _outOfShape, ...admitted } = result;
|
|
36
36
|
return admitted;
|
|
37
37
|
}
|
|
38
|
+
function projectSideQueryResult(result) {
|
|
39
|
+
const shaped = shapeSideQueryEcho(result);
|
|
40
|
+
const redacted = redactErrorMessage(shaped.errorMessage);
|
|
41
|
+
return redacted === undefined || redacted === shaped.errorMessage ? shaped : { ...shaped, errorMessage: redacted };
|
|
42
|
+
}
|
|
38
43
|
export async function handleSideQuery(req, res, match, ctx) {
|
|
39
44
|
const { deps } = ctx;
|
|
40
45
|
const { readJson, rateLimited, quotaExceeded, leaseDenied } = ctx.helpers;
|
|
@@ -107,7 +112,7 @@ export async function handleSideQuery(req, res, match, ctx) {
|
|
|
107
112
|
deps.sideQueryAccounting?.(principal, { model: modelId, usage: rr.usage });
|
|
108
113
|
deps.metrics?.inc?.("side_query_total", { result: "ok" });
|
|
109
114
|
if (!res.writableEnded)
|
|
110
|
-
sendJson(res, 200,
|
|
115
|
+
sendJson(res, 200, projectSideQueryResult(result));
|
|
111
116
|
}
|
|
112
117
|
catch (e) {
|
|
113
118
|
deps.metrics?.inc?.("side_query_total", { result: "error" });
|
|
@@ -27,6 +27,7 @@ import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate
|
|
|
27
27
|
import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
|
|
28
28
|
import { buildSyncSubmitBody } from "../sync-submit-body.js";
|
|
29
29
|
import { assertNever } from "../route-ctx.js";
|
|
30
|
+
import { withModelReachScope } from "../../model-reach-scope.js";
|
|
30
31
|
const DISCONNECT_PARK_GRACE_MS = 10_000;
|
|
31
32
|
function sseData(res, payload) {
|
|
32
33
|
const anchor = payload.eventId;
|
|
@@ -644,7 +645,7 @@ export async function handleTasks(req, res, match, ctx) {
|
|
|
644
645
|
});
|
|
645
646
|
}
|
|
646
647
|
const withApproval = () => approvalCtx && deps.toolApproval ? deps.toolApproval.runWithContext(approvalCtx, withQuestion) : withQuestion();
|
|
647
|
-
await (deps.sendUserFile
|
|
648
|
+
await withModelReachScope(prepared.spec.sessionId, () => deps.sendUserFile
|
|
648
649
|
? deps.sendUserFile.runWithContext({
|
|
649
650
|
taskId: askTaskId,
|
|
650
651
|
emit: async (f) => {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ## 两条到达车道,两个读者(本模块各给一只,别处不许再写第二份判据)
|
|
10
10
|
* ① **抛出面**(抹除 / 出处 / 外源列表 / 清标 / bundle 导出导入 —— 凡触 store 树的 backend 面):`.code` 就是码
|
|
11
|
-
* ⇒ {@link targetOutsideStoreOf}。HTTP 映射在 `http/memory-store-refusal-reply.ts`(409
|
|
11
|
+
* ⇒ {@link targetOutsideStoreOf}。HTTP 映射在 `http/memory-store-refusal-reply.ts`(409,operator 面一处字面量)与
|
|
12
|
+
* `http/memory-compliance-failure-reply.ts`(合规面两受众的那一只;会话所有者受众换固定句,S-930)。
|
|
12
13
|
* ② **报告面**(harvest `incident`):core 契约写明 report 车道上码**只在文本里** —— `sidecar_corrupt` 的 detail 恰两种形
|
|
13
14
|
* `memory commit refused: memory.target_outside_store: …` / `memory control plane refused: memory.target_outside_store: …`
|
|
14
15
|
* (core `docs/wire-consumption/7.31.1.md` 的 harvest 行逐字)⇒ {@link targetOutsideStoreInIncident} 只认**这两个 lead 紧跟码**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 白名单 = 名字 → 读它的线路(闭集;**加一个名字 = 加一行并写明哪条线路读它、为什么是部署的事**)。
|
|
3
|
+
* `anthropic-beta`:Anthropic Messages 的 beta 开关(逗号分隔的 beta 名;core anthropic 腿会把自己的 beta 并进同一个头)。
|
|
4
|
+
*/
|
|
5
|
+
export declare const CATALOG_ENTRY_HEADER_LANES: {
|
|
6
|
+
readonly "anthropic-beta": readonly ["anthropic-messages"];
|
|
7
|
+
};
|
|
8
|
+
export type CatalogEntryHeaderName = keyof typeof CATALOG_ENTRY_HEADER_LANES;
|
|
9
|
+
export declare const CATALOG_ENTRY_HEADER_NAMES: readonly CatalogEntryHeaderName[];
|
|
10
|
+
/** 判过的袋(只含白名单里的名字,值逐字)。 */
|
|
11
|
+
export type CatalogEntryHeaders = Readonly<Partial<Record<CatalogEntryHeaderName, string>>>;
|
|
12
|
+
/** 值的长度上界(字符)。`anthropic-beta` 的真实值是几枚逗号分隔的 beta 名,远在此下;上界只挡「把别的东西塞进来」。 */
|
|
13
|
+
export declare const MAX_HEADER_VALUE_CHARS = 1024;
|
|
14
|
+
/**
|
|
15
|
+
* 拒因 / warn 里名字的回显尺:token 形、≤64、且脱敏器不改它 ⇒ 照写;否则固定占位(名字位也可能被塞进秘密,值更是从不回显)。
|
|
16
|
+
*/
|
|
17
|
+
export declare function displayHeaderName(name: string): string;
|
|
18
|
+
/** 判决:`ok` ⇒ 判过的袋(`undefined` = 条目没声明任何头:缺席与 `{}` 同一个读法,都不铸键);否则 ⇒ 坏字段坐标 + 一句不带值的理由。 */
|
|
19
|
+
export type CatalogEntryHeadersVerdict = {
|
|
20
|
+
readonly ok: true;
|
|
21
|
+
readonly headers: CatalogEntryHeaders | undefined;
|
|
22
|
+
} | {
|
|
23
|
+
readonly ok: false;
|
|
24
|
+
readonly fields: readonly string[];
|
|
25
|
+
readonly issue: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* **唯一判据**:条目的 `headers` 原值(`unknown`:承载形,不信类型)+ 条目的线路词原值(`api`;两条读腿恒补齐它,
|
|
29
|
+
* 绕过读腿的输入缺它 ⇒ 按「没有线路读它」拒 —— 判据与铸点读同一个原值,不另补一份 schema 缺省)。
|
|
30
|
+
* 整只判:任一名字 / 值不合 ⇒ 整只不收(留下好的那几个 = 让运维以为整袋生效了)。坏字段逐个列出(`headers.<名>`)。
|
|
31
|
+
*/
|
|
32
|
+
export declare function readCatalogEntryHeaders(raw: unknown, api: unknown): CatalogEntryHeadersVerdict;
|
|
33
|
+
/**
|
|
34
|
+
* **唯一铸点**(`toModel` 调):判过的袋 → `Model.headers` 的键。缺席 ⇒ 不铸键(一个 `undefined` 值的键也不铸:
|
|
35
|
+
* 缺席只有一种拼法)。判不过 ⇒ **抛**:到这里的条目已经过 S-704 逐条判据(判不过的进 `modelRefusals`、到不了 `toModel`),
|
|
36
|
+
* 抛 = 两处判据分叉了,stage 整候选拒、响亮,不静默丢头。
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildModelHeadersKey(entryName: string, raw: unknown, api: unknown): {
|
|
39
|
+
headers?: Record<string, string>;
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=model-entry-headers.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { redactSecrets } from "./trace/redact.js";
|
|
2
|
+
export const CATALOG_ENTRY_HEADER_LANES = {
|
|
3
|
+
"anthropic-beta": ["anthropic-messages"],
|
|
4
|
+
};
|
|
5
|
+
export const CATALOG_ENTRY_HEADER_NAMES = Object.keys(CATALOG_ENTRY_HEADER_LANES);
|
|
6
|
+
export const MAX_HEADER_VALUE_CHARS = 1024;
|
|
7
|
+
const HEADER_VALUE_SHAPE = /^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?$/;
|
|
8
|
+
const HEADER_NAME_TOKEN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]{1,64}$/;
|
|
9
|
+
export function displayHeaderName(name) {
|
|
10
|
+
return HEADER_NAME_TOKEN.test(name) && redactSecrets(name) === name ? name : "(unprintable header name)";
|
|
11
|
+
}
|
|
12
|
+
function isCatalogEntryHeaderName(name) {
|
|
13
|
+
return Object.hasOwn(CATALOG_ENTRY_HEADER_LANES, name);
|
|
14
|
+
}
|
|
15
|
+
export function readCatalogEntryHeaders(raw, api) {
|
|
16
|
+
if (raw === undefined)
|
|
17
|
+
return { ok: true, headers: undefined };
|
|
18
|
+
const proto = typeof raw === "object" && raw !== null ? Object.getPrototypeOf(raw) : undefined;
|
|
19
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw) || (proto !== Object.prototype && proto !== null)) {
|
|
20
|
+
return { ok: false, fields: ["headers"], issue: `headers: expected a plain object of { <header name>: <value> } (allowed names: ${CATALOG_ENTRY_HEADER_NAMES.join(", ")})` };
|
|
21
|
+
}
|
|
22
|
+
const fields = [];
|
|
23
|
+
const issues = [];
|
|
24
|
+
const bag = {};
|
|
25
|
+
for (const name of Object.keys(raw)) {
|
|
26
|
+
const shown = displayHeaderName(name);
|
|
27
|
+
const field = `headers.${shown}`;
|
|
28
|
+
const value = Reflect.get(raw, name);
|
|
29
|
+
if (!isCatalogEntryHeaderName(name)) {
|
|
30
|
+
const canonical = CATALOG_ENTRY_HEADER_NAMES.find((n) => n === name.toLowerCase());
|
|
31
|
+
fields.push(field);
|
|
32
|
+
issues.push(canonical !== undefined
|
|
33
|
+
? `${field}: write the header name in its canonical spelling "${canonical}" (one spelling per header)`
|
|
34
|
+
: `${field}: not in the closed allowlist of catalog request headers (${CATALOG_ENTRY_HEADER_NAMES.join(", ")}); credentials ride sealedApiKey / apiKeyEnv, never a header`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const lanes = CATALOG_ENTRY_HEADER_LANES[name];
|
|
38
|
+
if (typeof api !== "string" || !lanes.includes(api)) {
|
|
39
|
+
fields.push(field);
|
|
40
|
+
issues.push(`${field}: only read on the ${lanes.join(" / ")} lane — this entry ${typeof api === "string" ? `is api "${api}"` : "declares no api"}, where the header would be sent and do nothing`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value !== "string" || value.length > MAX_HEADER_VALUE_CHARS || !HEADER_VALUE_SHAPE.test(value)) {
|
|
44
|
+
fields.push(field);
|
|
45
|
+
issues.push(`${field}: the value must be a non-empty string of visible ASCII (spaces only between words, at most ${MAX_HEADER_VALUE_CHARS} characters)`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
bag[name] = value;
|
|
49
|
+
}
|
|
50
|
+
if (fields.length > 0)
|
|
51
|
+
return { ok: false, fields: [...new Set(fields)], issue: issues.join("; ") };
|
|
52
|
+
return { ok: true, headers: Object.keys(bag).length > 0 ? bag : undefined };
|
|
53
|
+
}
|
|
54
|
+
export function buildModelHeadersKey(entryName, raw, api) {
|
|
55
|
+
const verdict = readCatalogEntryHeaders(raw, api);
|
|
56
|
+
if (!verdict.ok) {
|
|
57
|
+
throw new Error(`catalog entry "${entryName}" reached the Model mint with unjudged headers (${verdict.fields.join(", ")}) — judgeCatalogEntry must refuse it first; this is a server defect, not a config error`);
|
|
58
|
+
}
|
|
59
|
+
return verdict.headers === undefined ? {} : { headers: { ...verdict.headers } };
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=model-entry-headers.js.map
|
|
@@ -43,9 +43,15 @@ export interface CatalogEntryLike {
|
|
|
43
43
|
readonly name: string;
|
|
44
44
|
readonly id: string;
|
|
45
45
|
readonly provider: string;
|
|
46
|
+
/** S-931:线路词(`headers` 的线路判据读它;原值,不信这张型)。 */
|
|
47
|
+
readonly api?: unknown;
|
|
48
|
+
/** S-931:供应商附加头的承载形(`unknown`:判据在 `model-entry-headers.ts`,不信这张型)。 */
|
|
49
|
+
readonly headers?: unknown;
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
52
|
* **逐条判据**:`undefined` = 本部署能完整服务它。判序即语义:先形(不合形的条目谈不上走哪条腿),再腿。
|
|
53
|
+
* 形分两段、同一个码(`config.model_entry_invalid`):settings-schema 的 `ModelEntry`,再是 S-931 的 `headers`(白名单 + 线路 + 值形,
|
|
54
|
+
* 判据单属主 `model-entry-headers.ts`;settings-schema 6.3.0 的 `ModelEntry` 还没有这个键 —— 候裁加字段,届时词表不另抄)。
|
|
49
55
|
*
|
|
50
56
|
* 形判的属主是 settings-schema 的 `ModelEntry`(两条读腿用的就是它)—— 本仓不手抄任何一条字段规则;这里只是
|
|
51
57
|
* 在第二道边界(绕过读腿的输入)上**再问一次同一个属主**,并把答案变成一条带码的拒因。只取 `success` 与 issue
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CC_MODEL_TIER_ALIASES, DEFAULT_TIER_ORDER, expandTiers } from "@sema-agent/core";
|
|
2
2
|
import { ModelEntry, isReportableRefusalName } from "@sema-agent/settings-schema";
|
|
3
3
|
import { catalogBrainLane } from "./model-provider.js";
|
|
4
|
+
import { readCatalogEntryHeaders } from "./model-entry-headers.js";
|
|
4
5
|
import { describeSchemaIssueFields, describeSchemaIssues } from "./config-center/read-warnings.js";
|
|
5
6
|
import { DEFAULT_MODEL_REF } from "./model-select.js";
|
|
6
7
|
export const ENTRY_REFUSAL_REASON_OF = {
|
|
@@ -26,6 +27,9 @@ export function judgeCatalogEntry(m, ctx) {
|
|
|
26
27
|
const fields = describeSchemaIssueFields(shape.error);
|
|
27
28
|
return entryInvalidRefusal(typeof m.id === "string" ? m.id : "", typeof m.provider === "string" ? m.provider : "", fields, describeSchemaIssues(shape.error) ?? fields.join(", "));
|
|
28
29
|
}
|
|
30
|
+
const headers = readCatalogEntryHeaders(m.headers, m.api);
|
|
31
|
+
if (!headers.ok)
|
|
32
|
+
return entryInvalidRefusal(m.id, m.provider, headers.fields, headers.issue);
|
|
29
33
|
const lane = catalogBrainLane(m.provider);
|
|
30
34
|
const mount = laneMount(lane, ctx);
|
|
31
35
|
if (!mount.mounted) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-925(7.108.0)—— 「这次模型调用的每一次出网尝试落在哪」的**异步作用域**(叶:只引 node 与类型)。
|
|
3
|
+
*
|
|
4
|
+
* 两层,一只 `AsyncLocalStorage`:
|
|
5
|
+
* · **腿层**(`withModelReachScope`,三条带用户面投递口的 run 腿各包一次:流式 `POST /v1/tasks/stream`、后台 `POST /v1/runs`、续跑):
|
|
6
|
+
* 带着这条腿的 `sessionId`(通告归属键)与本腿已经说过的目标集合(去重单位 = 每腿每目标一次);
|
|
7
|
+
* · **调用层**(`createModelReachBrain` 每次模型调用包一次):一只可变的 {@link ModelCallReach},出网观察者
|
|
8
|
+
* (`outbound-dispatcher.ts` 的 undici 诊断通道订阅)把这次调用发出的每一条请求的**结局**记在上面 —— 最后一条是
|
|
9
|
+
* 「连上了、有了回答」还是「建连就失败了」。
|
|
10
|
+
*
|
|
11
|
+
* 为什么是 ALS 而不是参数:模型调用的出网发生在 core 的 brain 里(`fetch` → 全局 dispatcher → undici),本仓在那条链上没有任何
|
|
12
|
+
* 参数座;undici 的 `undici:request:create` 通道在创建请求的那一拍同步发布、就在发起 `fetch` 的异步上下文里 —— 读 ALS 就知道
|
|
13
|
+
* 「这条请求属于哪一次模型调用」,不靠目标主机 + 时间去猜(那是把相关写成因果)。
|
|
14
|
+
* 作用域外(side-query、记忆整理、工具自己的出网 …)⇒ 没有腿层 ⇒ 只有运维日志,没有用户通告。
|
|
15
|
+
* 腿里的**辅助**模型调用(WebFetch 摘要器、压实、提示建议、分类器 …)与主循环**同一条规则**:通告说的是「这个模型端点够不着」,
|
|
16
|
+
* 不是「这条 run 失败了」—— 不设豁免名单(codex r1 / r2:豁免名单结构上开放,core 自己发起的辅助调用本仓包不到,列不全)。
|
|
17
|
+
*/
|
|
18
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
19
|
+
import type { OutboundConnectFailure } from "./outbound-dispatcher.js";
|
|
20
|
+
/** 一条请求的结局(观察者按到达顺序覆盖写;模型调用终态时读**最后一条**)。 */
|
|
21
|
+
export type ModelCallOutcome = {
|
|
22
|
+
readonly kind: "reached";
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: "connect_failed";
|
|
25
|
+
readonly failure: OutboundConnectFailure;
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: "other";
|
|
28
|
+
};
|
|
29
|
+
/** 一次模型调用的出网观察(可变:观察者写、装饰器读)。 */
|
|
30
|
+
export interface ModelCallReach {
|
|
31
|
+
last?: ModelCallOutcome;
|
|
32
|
+
}
|
|
33
|
+
/** 一条 run 腿的作用域。 */
|
|
34
|
+
export interface ModelReachLegScope {
|
|
35
|
+
readonly sessionId: string;
|
|
36
|
+
/** 本腿已经通告过的目标(`targetHost`)—— 去重单位 = 每腿每目标一次。 */
|
|
37
|
+
readonly announced: Set<string>;
|
|
38
|
+
}
|
|
39
|
+
export interface ModelReachStore {
|
|
40
|
+
readonly leg?: ModelReachLegScope;
|
|
41
|
+
readonly call?: ModelCallReach;
|
|
42
|
+
}
|
|
43
|
+
export declare const modelReachContext: AsyncLocalStorage<ModelReachStore>;
|
|
44
|
+
/**
|
|
45
|
+
* 腿层入口:每条腿都开**自己的**作用域 —— `sessionId` 缺席(无会话腿)⇒ 空作用域(没有归属键就没有用户面可投;也不继承外层腿的
|
|
46
|
+
* 归属键 —— codex r1 旁注:修前缺席即原样执行、会沿用外层腿)。嵌套时内层覆盖外层 —— 归属跟着「这一轮是谁的腿」走。
|
|
47
|
+
*/
|
|
48
|
+
export declare function withModelReachScope<T>(sessionId: string | undefined, fn: () => T): T;
|
|
49
|
+
/** 调用层入口(`createModelReachBrain` 用):在当前腿作用域(可能缺席)之上开一只新的调用观察。 */
|
|
50
|
+
export declare function withModelCallReach<T>(call: ModelCallReach, fn: () => T): T;
|
|
51
|
+
/** 观察者读口:当前异步上下文里的模型调用观察(作用域外 ⇒ `undefined`)。 */
|
|
52
|
+
export declare function currentModelCallReach(): ModelCallReach | undefined;
|
|
53
|
+
/** 装饰器读口:当前腿(作用域外 ⇒ `undefined`)。 */
|
|
54
|
+
export declare function currentModelReachLeg(): ModelReachLegScope | undefined;
|
|
55
|
+
//# sourceMappingURL=model-reach-scope.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
export const modelReachContext = new AsyncLocalStorage();
|
|
3
|
+
export function withModelReachScope(sessionId, fn) {
|
|
4
|
+
return modelReachContext.run(sessionId === undefined || sessionId.length === 0 ? {} : { leg: { sessionId, announced: new Set() } }, fn);
|
|
5
|
+
}
|
|
6
|
+
export function withModelCallReach(call, fn) {
|
|
7
|
+
const outer = modelReachContext.getStore();
|
|
8
|
+
return modelReachContext.run({ ...(outer?.leg !== undefined ? { leg: outer.leg } : {}), call }, fn);
|
|
9
|
+
}
|
|
10
|
+
export function currentModelCallReach() {
|
|
11
|
+
return modelReachContext.getStore()?.call;
|
|
12
|
+
}
|
|
13
|
+
export function currentModelReachLeg() {
|
|
14
|
+
return modelReachContext.getStore()?.leg;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=model-reach-scope.js.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-925(7.108.0;test [ref] ② 用户报案分诊,验收 `srv7108-s925-connect-timeout-diagnostic`)—— **模型调用因建连失败终态**时
|
|
3
|
+
* 给这条腿的用户铸的通告 `server.model_unreachable` 的**唯一铸点**,与把它挂到每一次模型调用上的 brain 装饰器。
|
|
4
|
+
*
|
|
5
|
+
* ## 病
|
|
6
|
+
* 运营用户看不到日志 / 诊断面:VPN 中途切换之后模型网关建连超时,用户只拿到一句 undici 原句(`fetch failed (connect …)`),
|
|
7
|
+
* 分不清是网络断了还是起服时读进的代理已经失效,也没有「查 VPN / 代理、代理变过就重启」这条出路。只落运维日志不算闭环
|
|
8
|
+
* (test 立案原话)。core 没有现成的「模型不可达」用户通告码(`ENGINE_NOTICE_CODES` 亲查零命中)⇒ 本仓自铸,走既有的
|
|
9
|
+
* `SERVER_NOTICE_AUDIENCE` 族(S-690 形:白名单 / 归属 / 消毒 / 隔离四件判据一个不绕),不开第二条通道。
|
|
10
|
+
*
|
|
11
|
+
* ## 判据(一句):**这一次模型调用的终局是 `stopReason: "error"`,且它发出的最后一条请求在建连时就失败了**
|
|
12
|
+
* 「最后一条请求落在哪」由出网观察者记在调用作用域上(`model-reach-scope.ts`,undici 诊断通道,见 `outbound-dispatcher.ts`)——
|
|
13
|
+
* 不按目标主机 + 时间去猜。于是:
|
|
14
|
+
* · 服务商回了 5xx / 4xx(收到了响应头)⇒ 最后一条是 `reached` ⇒ 不铸(那是服务商的回答);
|
|
15
|
+
* · 建连失败、重试之后连上了并成功 ⇒ 终局不是 error ⇒ 不铸;
|
|
16
|
+
* · 中途断流 / TLS / 主动取消 / 首字节超时 ⇒ 最后一条是 `other` ⇒ 不铸(判不出「够不着」)。
|
|
17
|
+
*
|
|
18
|
+
* ## 投递 / 去重
|
|
19
|
+
* 归属键 = 这条 run 腿的 `sessionId`(腿层作用域,三条带用户面投递口的腿各包一次)。作用域外的模型调用(side-query、记忆整理 …)
|
|
20
|
+
* 没有归属键 ⇒ 不铸(运维日志 `outbound_connect_failed` 照有)。腿里的**辅助**调用(WebFetch 摘要器、压实、提示建议、分类器 …)与
|
|
21
|
+
* 主循环**同一条规则**:本帧说的是「这个模型端点够不着」,不是「这条 run 失败了」(措辞如此,不暗示终态);不设豁免名单 —— codex r1 / r2
|
|
22
|
+
* 两轮证明豁免名单结构上开放(core 自己发起的辅助调用本仓包不到)。按 **run 终态**归属是另一种设计,候裁(收车档 ⑤)。去重单位 = **每腿每目标一次**(同一条腿里
|
|
23
|
+
* 重试 / 子代对同一个网关再失败,不再说一遍)。受众 user;detail = `{ code, targetHost, proxyInUse?, remedy, sessionId }` ——
|
|
24
|
+
* `proxyInUse` 只在这一跳走了代理时在场,且只是**变量名**(代理地址是基础设施细节,不上租户 wire,与 `OutboundProxyError` 的
|
|
25
|
+
* F3 裁定同一条)。`targetHost`(模型端点的 `host:port`)对 user 受众是**新披露的部署事实**(F7 裁定:它是部署自己声明的模型端点,
|
|
26
|
+
* 不是基础设施秘密,可以上;代理地址仍不上)—— 不要读成「终局句里本就有」:代理形下 core 终局句展开到代理根因、不含模型主机,
|
|
27
|
+
* 辅助调用形下 run 可 completed、租户面本无主机句。
|
|
28
|
+
*/
|
|
29
|
+
import type { Brain } from "@sema-agent/core";
|
|
30
|
+
import { type EngineNoticeRouter } from "./trace/engine-notice-wire.js";
|
|
31
|
+
/**
|
|
32
|
+
* `create*`:包在 `createBrain` 装好的那只 brain **最外层**(路由 / 切备 / 断路器 / 降级全在里面 ⇒「这次调用」= 调用方看到的那一次,
|
|
33
|
+
* 重试与切备都在同一个调用作用域里)。每次调用开一只新的调用观察;腿层作用域在**发起调用那一拍**读(之后的回调在别的上下文里)。
|
|
34
|
+
* 不改流、不改消息、不改错误:只在终局 promise 上挂一个观察。
|
|
35
|
+
* 形 = 同型 `Proxy`:表里 `observed` 的成员(内层有它才有)换成观察器;其余成员逐字转给内层、方法绑内层(`this` 不漂)。
|
|
36
|
+
*/
|
|
37
|
+
export declare function createModelReachBrain(inner: Brain, router?: EngineNoticeRouter): Brain;
|
|
38
|
+
//# sourceMappingURL=model-unreachable-notice.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { currentModelReachLeg, withModelCallReach } from "./model-reach-scope.js";
|
|
2
|
+
import { defaultEngineNoticeRouter } from "./trace/engine-notice-wire.js";
|
|
3
|
+
const SERVER_MODEL_UNREACHABLE = "server.model_unreachable";
|
|
4
|
+
const MODEL_UNREACHABLE_REMEDY = "check VPN / proxy; restart if the proxy changed since boot";
|
|
5
|
+
function announceModelUnreachable(leg, failure, router = defaultEngineNoticeRouter) {
|
|
6
|
+
if (leg.announced.has(failure.targetHost))
|
|
7
|
+
return;
|
|
8
|
+
leg.announced.add(failure.targetHost);
|
|
9
|
+
const via = failure.proxyInUse !== null ? ` through the proxy from ${failure.proxyInUse}` : "";
|
|
10
|
+
router.route({
|
|
11
|
+
code: SERVER_MODEL_UNREACHABLE,
|
|
12
|
+
message: `The model endpoint could not be reached: the connection to ${failure.targetHost}${via} failed (${failure.code}). ` +
|
|
13
|
+
"Check the VPN / proxy; if the proxy changed since the server started, restart it (proxy settings are read once at startup).",
|
|
14
|
+
detail: {
|
|
15
|
+
code: failure.code,
|
|
16
|
+
targetHost: failure.targetHost,
|
|
17
|
+
...(failure.proxyInUse !== null ? { proxyInUse: failure.proxyInUse } : {}),
|
|
18
|
+
remedy: MODEL_UNREACHABLE_REMEDY,
|
|
19
|
+
sessionId: leg.sessionId,
|
|
20
|
+
},
|
|
21
|
+
sessionId: leg.sessionId,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const isThenable = (v) => typeof v.then === "function";
|
|
25
|
+
function settle(leg, call, message, router) {
|
|
26
|
+
if (leg === undefined || message.stopReason !== "error")
|
|
27
|
+
return;
|
|
28
|
+
const last = call.last;
|
|
29
|
+
if (last?.kind === "connect_failed")
|
|
30
|
+
announceModelUnreachable(leg, last.failure, router);
|
|
31
|
+
}
|
|
32
|
+
const BRAIN_MEMBER_TREATMENT = {
|
|
33
|
+
stream: "observed",
|
|
34
|
+
complete: "observed",
|
|
35
|
+
adjudicateRoute: "forwarded",
|
|
36
|
+
};
|
|
37
|
+
export function createModelReachBrain(inner, router = defaultEngineNoticeRouter) {
|
|
38
|
+
const observe = (leg, call, final) => {
|
|
39
|
+
void final.then((message) => settle(leg, call, message, router), () => undefined);
|
|
40
|
+
};
|
|
41
|
+
const MEMBER_OBSERVERS = {
|
|
42
|
+
stream: (innerStream) => (model, context, options) => {
|
|
43
|
+
const leg = currentModelReachLeg();
|
|
44
|
+
const call = {};
|
|
45
|
+
const s = withModelCallReach(call, () => innerStream.call(inner, model, context, options));
|
|
46
|
+
if (isThenable(s))
|
|
47
|
+
observe(leg, call, Promise.resolve(s).then((st) => st.result()));
|
|
48
|
+
else
|
|
49
|
+
observe(leg, call, s.result());
|
|
50
|
+
return s;
|
|
51
|
+
},
|
|
52
|
+
complete: (innerComplete) => (model, context, options) => {
|
|
53
|
+
const leg = currentModelReachLeg();
|
|
54
|
+
const call = {};
|
|
55
|
+
const p = withModelCallReach(call, () => innerComplete.call(inner, model, context, options));
|
|
56
|
+
observe(leg, call, p);
|
|
57
|
+
return p;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const observedStream = MEMBER_OBSERVERS.stream(inner.stream);
|
|
61
|
+
const observedComplete = inner.complete !== undefined ? MEMBER_OBSERVERS.complete(inner.complete) : undefined;
|
|
62
|
+
const observed = { stream: observedStream, complete: observedComplete };
|
|
63
|
+
return new Proxy(inner, {
|
|
64
|
+
get(target, prop) {
|
|
65
|
+
if (typeof prop === "string" && Object.hasOwn(observed, prop))
|
|
66
|
+
return Reflect.get(observed, prop);
|
|
67
|
+
const v = Reflect.get(target, prop, target);
|
|
68
|
+
return typeof v === "function" ? v.bind(target) : v;
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=model-unreachable-notice.js.map
|
|
@@ -321,6 +321,10 @@ export declare const FAIL_OPEN_TAGS: {
|
|
|
321
321
|
readonly cls: "F";
|
|
322
322
|
readonly note: "S-582(`plugins/send-file-ledger.ts`,local 车道的发文件账本):一条账行读不出形(`42` / 缺 id·scope·objectKey 三元组 / 中间半截 JSON)⇒ 跳过它继续。最坏后果 = 那条已签发的链接在它租户的列表里**看不见**(对象本身没动;账本本来就不存 URL)。治理面少一行是可见性损失,不是放行 ⇒ F;但它恰是治理面要看的那一类事实,所以必须留痕 —— 修前零披露。";
|
|
323
323
|
};
|
|
324
|
+
readonly "server.outbound.connect-observer-threw": {
|
|
325
|
+
readonly cls: "F";
|
|
326
|
+
readonly note: "S-925(`outbound-dispatcher.ts` 的 undici 诊断通道订阅者:`undici:request:create` / `:headers` / `:error`):一只订阅回调抛了。通道在发布方的调用栈里同步回调订阅者,抛出去在 Node 上是未捕获异常(整进程倒),所以每只回调整体 try、落本 tag。最坏后果 = 这一条请求的建连失败没被记下:少一行 `outbound_connect_failed`、诊断段 `outbound.lastConnectFailure` 不更新、那次模型调用不铸 `server.model_unreachable` 用户通告 —— 请求本身、它的错误、run 的终态一个字节不受影响(观察只读)。计数恒零是证据;非零 = undici 通道载荷的形变了,去看 detail 的 site。";
|
|
327
|
+
};
|
|
324
328
|
};
|
|
325
329
|
/** 词表键推导的闭集类型——未登记的 tag 传不进 {@link recordFailOpen}(编译期拒)。 */
|
|
326
330
|
export type FailOpenTag = keyof typeof FAIL_OPEN_TAGS;
|
|
@@ -294,6 +294,10 @@ export const FAIL_OPEN_TAGS = {
|
|
|
294
294
|
cls: "F",
|
|
295
295
|
note: "S-582(`plugins/send-file-ledger.ts`,local 车道的发文件账本):一条账行读不出形(`42` / 缺 id·scope·objectKey 三元组 / 中间半截 JSON)⇒ 跳过它继续。最坏后果 = 那条已签发的链接在它租户的列表里**看不见**(对象本身没动;账本本来就不存 URL)。治理面少一行是可见性损失,不是放行 ⇒ F;但它恰是治理面要看的那一类事实,所以必须留痕 —— 修前零披露。",
|
|
296
296
|
},
|
|
297
|
+
"server.outbound.connect-observer-threw": {
|
|
298
|
+
cls: "F",
|
|
299
|
+
note: "S-925(`outbound-dispatcher.ts` 的 undici 诊断通道订阅者:`undici:request:create` / `:headers` / `:error`):一只订阅回调抛了。通道在发布方的调用栈里同步回调订阅者,抛出去在 Node 上是未捕获异常(整进程倒),所以每只回调整体 try、落本 tag。最坏后果 = 这一条请求的建连失败没被记下:少一行 `outbound_connect_failed`、诊断段 `outbound.lastConnectFailure` 不更新、那次模型调用不铸 `server.model_unreachable` 用户通告 —— 请求本身、它的错误、run 的终态一个字节不受影响(观察只读)。计数恒零是证据;非零 = undici 通道载荷的形变了,去看 detail 的 site。",
|
|
300
|
+
},
|
|
297
301
|
};
|
|
298
302
|
export function failOpenTagForDroppedFrame(frameType) {
|
|
299
303
|
switch (frameType) {
|
|
@@ -130,14 +130,17 @@ export declare function redactModelText(raw: string): string;
|
|
|
130
130
|
*
|
|
131
131
|
* **回声字段是什么**:模型服务商(上游模型 API / 中间网关)在响应里给、引擎**逐字转交**的不透明标识 ——
|
|
132
132
|
* `apiFailure.requestId`(响应头 `request-id` / `x-request-id`,core `brain/errors.js` 的 `readProviderRequestId`)、
|
|
133
|
-
* `responseModel`(服务商自报的模型名,与账本 `turn_end.responseModel` 是孪生)
|
|
134
|
-
* `
|
|
135
|
-
* `
|
|
133
|
+
* `responseModel`(服务商自报的模型名,与账本 `turn_end.responseModel` 是孪生)。字节由**部署方接的那个端点**决定,本栈一个都没铸 ⇒ **不可信**。
|
|
134
|
+
* 工具调用 id(`strandedHumanAnswers[].toolCallId` / `stats.humanReview.gates[].toolCallId`)**出身**同样是服务商,但自 core 7.36.0(#1207)起
|
|
135
|
+
* 它进引擎之前已**准入一次**:三只 brain 在建调用的那一行(`brain/tool-call-id.js` 的 `admitToolCallId`)—— 缺席 ⇒ 车道兜底铸;
|
|
136
|
+
* 在形(可见 ASCII 1–128 且不含 `~`)⇒ 原样;越界 ⇒ 单射折叠成引擎 id(`<前 32 码元,越界字符记 `-`>~<sha256 前 32 hex>`),原字节只挂在
|
|
137
|
+
* 转录的 `ToolCall.wireId` 上、只回给 provider。⇒ 一方 brain 在 7.107.0 起新铸的调用,到达本仓任何一张脸的 `toolCallId` 恒在形。
|
|
138
|
+
* 残余两条(S-921,如实):① ≤7.106.0 写下的存量行(待批 park 行 `boundCallId` / 检查点 `pendingAction.toolCallId` / 转录)是服务商原串逐字 —— 新版不对存量行二次准入(e2e G4 钉);② 宿主自己的 `Brain`(不经三只一方 adapter)铸的 id 不准入(core KL-1207-a;本仓生产装配的 brain 恒是 `createBrain` 的一方 adapter 外套委托装饰,这一形只经嵌入 / 测试注入的 brain 到达)。两条残余里的 id 仍是跨面 join 键、每张脸同值,只是形状不保证(本闸照旧不动它们:动一张脸 = join 断)。
|
|
136
139
|
*
|
|
137
140
|
* 🔴 **本闸只施加在「这个值出现的每一张脸都被覆盖到」的回声上**(codex r1 F1,实测成立的回归后改判):工具调用 id 是
|
|
138
141
|
* **跨面 join 键** —— 同一枚 id 同时逐字出现在 `tool_start` / `tool_end` / 审批卡等逐帧面上。只在结果面动它 ⇒ 结果面与逐帧面
|
|
139
|
-
* join 不上。所以它的处置词是 `"provider-join-key"
|
|
140
|
-
* (
|
|
142
|
+
* join 不上。所以它的处置词是 `"provider-join-key"`:出身是 provider,形状在**全部面的共同入口一次施加** —— 那个入口就是上面那条
|
|
143
|
+
* core brain 准入(S-890 v1 / v2 小稿提给 core、#1207 落地;本仓零闸,不加第二道门),本面逐字 = 每张脸同一枚引擎 id,join 成立。`requestId` / `responseModel` 的每一张脸(结果面四处 + 账本孪生 + 子代通告 + `POST /v1/side-query` 200 体的 `responseModel`,
|
|
141
144
|
* 后者 7.105.0 合并复审 X1 补上 —— `http/routes/side-query.ts` 的 `shapeSideQueryEcho`)本闸都覆盖到了。
|
|
142
145
|
*
|
|
143
146
|
* **只做形状校验,不过脱敏器**(本车裁,与 S-270 / clay [ref] ② 的 `structuredOutput` 逐字回声面同一方向):回声值
|