@sema-agent/server 7.66.0 → 7.68.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/USAGE.md +6 -3
- package/dist/approval-card.d.ts +8 -0
- package/dist/approval.d.ts +3 -0
- package/dist/approval.js +2 -1
- package/dist/boot/resolve-spec.js +13 -6
- package/dist/config-catalog.js +2 -2
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config-types.d.ts +15 -2
- package/dist/config.d.ts +25 -3
- package/dist/config.js +58 -30
- package/dist/deployment-governance.d.ts +8 -0
- package/dist/http/route-ctx.d.ts +5 -1
- package/dist/http/routes/diagnostics.d.ts +27 -2
- package/dist/http/routes/diagnostics.js +21 -1
- package/dist/http/routes/rules.js +15 -10
- package/dist/http/routes/runs.js +1 -1
- package/dist/http/routes/tasks.js +4 -8
- package/dist/http/server.d.ts +6 -1
- package/dist/http/server.js +7 -21
- package/dist/mcp-tool-faces.js +11 -2
- package/dist/observability/fail-open.d.ts +4 -0
- package/dist/observability/fail-open.js +4 -0
- package/dist/observability/metrics.js +1 -1
- package/dist/permission-rule-vocab.d.ts +13 -21
- package/dist/permission-rule-vocab.js +6 -5
- package/dist/plugins/permission-rule-store-file.d.ts +3 -5
- package/dist/plugins/permission-rule-store-file.js +2 -1
- package/dist/plugins/permission-rule-store-sql.d.ts +10 -10
- package/dist/plugins/permission-rule-store-sql.js +35 -20
- package/dist/posture-source.d.ts +57 -0
- package/dist/posture-source.js +27 -0
- package/dist/read-face-posture.d.ts +9 -4
- package/dist/read-face-posture.js +7 -11
- package/dist/rules-consent.d.ts +15 -10
- package/dist/rules-consent.js +41 -21
- package/dist/runs.d.ts +10 -1
- package/dist/runs.js +2 -1
- package/dist/session-titler.js +3 -12
- package/dist/task-mcp.d.ts +50 -6
- package/dist/task-mcp.js +71 -35
- package/dist/task-settings.d.ts +30 -0
- package/dist/task-settings.js +39 -18
- package/dist/tool-approval.d.ts +5 -4
- package/dist/tool-approval.js +2 -2
- package/dist/trace/engine-notice-wire.d.ts +17 -1
- package/dist/trace/engine-notice-wire.js +6 -1
- package/dist/trace/project.d.ts +4 -3
- package/package.json +3 -3
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta,
|
|
3
|
+
import { applyTombstones, assertDeleteDeltaCarriesNoAdd, assertRedemptionNotQuarantined, assertWriteDeltaScopeDurable, foldDelta, parseRuleText, readRowBehavior, screenRuleSyncState, PERMISSION_RULE_WRITER, RULE_BEHAVIORS, RULE_SYNC_DROP_CODES, EMPTY_DURABLE_RULE_PARTITION, } from "@sema-agent/core";
|
|
4
4
|
import { dialectProtocolJsonEncoder } from "./sql-driver.js";
|
|
5
5
|
import { isMissingColumnError } from "./sql-errors.js";
|
|
6
6
|
import { canonicalJson } from "../canonical-json.js";
|
|
7
|
-
import { EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES,
|
|
7
|
+
import { EDITED_RULE_BREADTH_WARNING_CODES, PERSISTED_RULE_MATCHES, isPersistedRuleTool, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
|
|
8
8
|
export function writerOfSqlRuleStore(store) {
|
|
9
9
|
const w = Reflect.get(store, PERMISSION_RULE_WRITER);
|
|
10
10
|
if (w === null || typeof w !== "object")
|
|
@@ -24,10 +24,11 @@ const RuleDotSchema = z.object({ actor: z.string().min(1), counter: z.number().i
|
|
|
24
24
|
const RuleAddSchema = z
|
|
25
25
|
.object({ dot: RuleDotSchema, origin: z.enum(["user", "imported-cc", "starter"]), createdAt: z.string() })
|
|
26
26
|
.strict();
|
|
27
|
-
const
|
|
27
|
+
const PersistedRuleRowSchema = z
|
|
28
28
|
.object({
|
|
29
|
+
behavior: z.string().optional(),
|
|
29
30
|
rule: z.string(),
|
|
30
|
-
tool: z.
|
|
31
|
+
tool: z.string().min(1).refine(isPersistedRuleTool, { message: "not a tool the persisted-rule grammar speaks for" }),
|
|
31
32
|
match: z.enum(PERSISTED_RULE_MATCHES),
|
|
32
33
|
command: z.string(),
|
|
33
34
|
scope: DurableRuleScopeSchema,
|
|
@@ -35,14 +36,15 @@ const PersistedAllowRuleSchema = z
|
|
|
35
36
|
})
|
|
36
37
|
.strict();
|
|
37
38
|
const RuleTombstoneSchema = z
|
|
38
|
-
.object({ rule: z.string(), scope: DurableRuleScopeSchema, removedDots: z.array(RuleDotSchema).min(1), deletedBy: RuleDotSchema })
|
|
39
|
+
.object({ behavior: z.string().optional(), rule: z.string(), scope: DurableRuleScopeSchema, removedDots: z.array(RuleDotSchema).min(1), deletedBy: RuleDotSchema })
|
|
39
40
|
.strict();
|
|
40
41
|
const QUARANTINE_REASONS = new Set(Object.entries(RULE_SYNC_DROP_CODES)
|
|
41
42
|
.filter(([, side]) => side.includes("local-quarantined"))
|
|
42
43
|
.map(([code]) => code));
|
|
43
44
|
const isQuarantineReason = (v) => typeof v === "string" && QUARANTINE_REASONS.has(v);
|
|
44
|
-
const
|
|
45
|
+
const QuarantinedRuleAddRowSchema = z
|
|
45
46
|
.object({
|
|
47
|
+
behavior: z.string().optional(),
|
|
46
48
|
rule: z.string(),
|
|
47
49
|
scope: DurableRuleScopeSchema,
|
|
48
50
|
add: RuleAddSchema,
|
|
@@ -50,8 +52,18 @@ const QuarantinedRuleAddSchema = z
|
|
|
50
52
|
atMs: z.number().int().nonnegative().safe(),
|
|
51
53
|
})
|
|
52
54
|
.strict();
|
|
55
|
+
function readBucketRowBehavior(raw, what, ownerKey) {
|
|
56
|
+
const behavior = readRowBehavior(raw);
|
|
57
|
+
if (behavior === undefined) {
|
|
58
|
+
throw new Error(`permission_rule bucket ${ownerKey} carries a ${what} whose behavior word is outside the closed set (${JSON.stringify(raw)}) — ` +
|
|
59
|
+
`refusing the whole bucket rather than reading a standing deny as "no rule"`);
|
|
60
|
+
}
|
|
61
|
+
return behavior;
|
|
62
|
+
}
|
|
63
|
+
const WritablePersistedRuleSchema = PersistedRuleRowSchema.extend({ behavior: z.enum(RULE_BEHAVIORS) });
|
|
64
|
+
const WritableRuleTombstoneSchema = RuleTombstoneSchema.extend({ behavior: z.enum(RULE_BEHAVIORS) });
|
|
53
65
|
const FrontierSchema = z.record(z.string(), z.number().int().nonnegative().safe());
|
|
54
|
-
const RuleCandidateSchema = z.object({ rule: z.string(), scope: DurableRuleScopeSchema }).strict();
|
|
66
|
+
const RuleCandidateSchema = z.object({ behavior: z.enum(RULE_BEHAVIORS), rule: z.string(), scope: DurableRuleScopeSchema }).strict();
|
|
55
67
|
const RedeemedDotsSchema = z.record(z.string(), RuleDotSchema);
|
|
56
68
|
const RuleEditedSchema = z
|
|
57
69
|
.object({ index: z.number().int().nonnegative().safe(), text: z.string(), at: z.string(), warnings: z.array(z.enum(EDITED_RULE_BREADTH_WARNING_CODES)).optional() })
|
|
@@ -108,9 +120,9 @@ export const TIDB_PERMISSION_RULE_STATEMENTS = [
|
|
|
108
120
|
state VARCHAR(32) NOT NULL,
|
|
109
121
|
rev BIGINT NOT NULL DEFAULT 0,
|
|
110
122
|
candidates_json LONGTEXT NOT NULL,
|
|
111
|
-
-- version:**数据格式版本**(= core \`RuleApprovalRecord.schema\`,本形恒
|
|
123
|
+
-- version:**数据格式版本**(= core \`RuleApprovalRecord.schema\`,本形恒 4 —— core 7.9.0 #625 由 3 升,
|
|
112
124
|
-- 代码里的单点是 RULE_APPROVAL_RECORD_SCHEMA;OCC 守卫另在 rev,两轴禁混,schema-naming 门③ 白名单
|
|
113
|
-
-- 登记)。读侧对非
|
|
125
|
+
-- 登记)。读侧对非 4 的行(含上一形的 3)交还 design/375 §4.5 的
|
|
114
126
|
-- StaleRuleApprovalRecord 信封(identity 键 only)—— 本仓 SCHEMA POLICY 是删表重建,重建后的表
|
|
115
127
|
-- 不该有这种行,这一臂是「未来再换形」的防御位,绝不静默 widen 成一条本形担保不了的记录。
|
|
116
128
|
version INT NOT NULL,
|
|
@@ -263,7 +275,7 @@ export async function ensurePgPermissionRuleSchema(q) {
|
|
|
263
275
|
rev BIGINT NOT NULL DEFAULT 0,
|
|
264
276
|
candidates_json TEXT COLLATE "C" NOT NULL,
|
|
265
277
|
-- version / offers_json / selected_offer:design/375 换形三列(MySQL 孪生的行内注写了理由:
|
|
266
|
-
-- version=数据格式版本恒
|
|
278
|
+
-- version=数据格式版本恒 4(core 7.9.0 #625 由 3 升;单点 RULE_APPROVAL_RECORD_SCHEMA),非 4 的行(含 3)
|
|
267
279
|
-- 读成 Stale 信封;offers_json=按 index 引用的 OPTION 结构,
|
|
268
280
|
-- 禁存文本副本;selected_offer=确认的 OFFER index,与 ticket/redeemed_dots 的 CANDIDATE index
|
|
269
281
|
-- 两空间分离)。
|
|
@@ -356,10 +368,18 @@ class SqlDurableRulePartition {
|
|
|
356
368
|
const row = rows[0];
|
|
357
369
|
if (row === undefined)
|
|
358
370
|
return { ...EMPTY_BUCKET, rules: [], tombstones: [], quarantined: [] };
|
|
359
|
-
const
|
|
360
|
-
const
|
|
361
|
-
const
|
|
371
|
+
const rulesRaw = parseColumn(z.array(PersistedRuleRowSchema), row.rules_json, "rules_json", this.ownerKey);
|
|
372
|
+
const tombstonesRaw = parseColumn(z.array(RuleTombstoneSchema), row.tombstones_json, "tombstones_json", this.ownerKey);
|
|
373
|
+
const quarantinedRaw = parseColumn(z.array(QuarantinedRuleAddRowSchema), row.quarantined_json, "quarantined_json", this.ownerKey);
|
|
362
374
|
const observedVector = row.observed_vector_json === null || row.observed_vector_json === undefined ? undefined : parseColumn(FrontierSchema, row.observed_vector_json, "observed_vector_json", this.ownerKey);
|
|
375
|
+
const expected = buildRuleBucketChecksum({ rules: rulesRaw, tombstones: tombstonesRaw, quarantined: quarantinedRaw, ...(observedVector !== undefined ? { observedVector } : {}) });
|
|
376
|
+
if (String(row.checksum ?? "") !== expected) {
|
|
377
|
+
throw new Error(`permission_rule bucket ${this.ownerKey} failed its integrity check (stored ${String(row.checksum)}, computed ${expected}) — ` +
|
|
378
|
+
`refusing the whole bucket rather than reporting a partial rule set`);
|
|
379
|
+
}
|
|
380
|
+
const rules = rulesRaw.map((r) => ({ ...r, behavior: readBucketRowBehavior(r.behavior, "rule row", this.ownerKey) }));
|
|
381
|
+
const tombstones = tombstonesRaw.map((t) => ({ ...t, behavior: readBucketRowBehavior(t.behavior, "tombstone", this.ownerKey) }));
|
|
382
|
+
const quarantined = quarantinedRaw.map((q) => ({ ...q, behavior: readBucketRowBehavior(q.behavior, "quarantined row", this.ownerKey) }));
|
|
363
383
|
const state = {
|
|
364
384
|
actor: String(row.actor ?? ""),
|
|
365
385
|
counter: Number(row.counter ?? 0),
|
|
@@ -369,13 +389,8 @@ class SqlDurableRulePartition {
|
|
|
369
389
|
quarantined,
|
|
370
390
|
...(observedVector !== undefined ? { observedVector } : {}),
|
|
371
391
|
};
|
|
372
|
-
const expected = buildRuleBucketChecksum({ rules, tombstones, quarantined, ...(observedVector !== undefined ? { observedVector } : {}) });
|
|
373
|
-
if (String(row.checksum ?? "") !== expected) {
|
|
374
|
-
throw new Error(`permission_rule bucket ${this.ownerKey} failed its integrity check (stored ${String(row.checksum)}, computed ${expected}) — ` +
|
|
375
|
-
`refusing the whole bucket rather than reporting a partial rule set`);
|
|
376
|
-
}
|
|
377
392
|
const badTombstone = tombstones.find((t) => {
|
|
378
|
-
const parsed =
|
|
393
|
+
const parsed = parseRuleText(t.rule, t.behavior);
|
|
379
394
|
return "reject" in parsed || parsed.rule.rule !== t.rule;
|
|
380
395
|
});
|
|
381
396
|
if (badTombstone !== undefined) {
|
|
@@ -467,7 +482,7 @@ class SqlDurableRulePartition {
|
|
|
467
482
|
assertDeleteDeltaCarriesNoAdd(delta);
|
|
468
483
|
nextTombstones = [...cur.tombstones, delta.tombstone];
|
|
469
484
|
}
|
|
470
|
-
const writable = z.object({ rules: z.array(
|
|
485
|
+
const writable = z.object({ rules: z.array(WritablePersistedRuleSchema), tombstones: z.array(WritableRuleTombstoneSchema) }).safeParse({ rules: nextRules, tombstones: nextTombstones });
|
|
471
486
|
if (!writable.success) {
|
|
472
487
|
throw new Error(`refusing to write a permission_rule bucket this backend could not read back: ${writable.error.message}`);
|
|
473
488
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-167 → S-178 —— **一个部署级旋钮的值是谁定的**,四词闭集 + 一句运维指路。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么是一个共享件而不是每键一份
|
|
5
|
+
*
|
|
6
|
+
* S-167 给 `READ_FACE` 造了这套东西(值 + 来源 + 指路句),理由写在 `read-face-posture.ts` 的顶注里:
|
|
7
|
+
* 「缺席」这一态里塞着两件不同的事(单机 turnkey 没表态 vs 多租户就是要引擎默认),而把它们分开之后,
|
|
8
|
+
* **env 恒赢的判据**与**运维面「为什么这台机器是这个值」**才有真答案。S-178 把同一条律推到另外三个键
|
|
9
|
+
* (`STREAM_ASK_WINDOW_MS` / `DURABLE_APPROVAL` / `SESSION_AUTO_TITLE`)时,唯一诚实的做法是**抽这一份**
|
|
10
|
+
* ——四词表抄第二遍的那天,两份表会在加词时分家,而「这个值是谁定的」在运维面上是个是非题,不能有两个答案。
|
|
11
|
+
*
|
|
12
|
+
* ## 优先序(与 `config.ts` 的 `postureOn` 同形)
|
|
13
|
+
* `env 显式` > `center 发布` > `posture 派生` > `引擎/服务端默认`。
|
|
14
|
+
*
|
|
15
|
+
* ## 定界
|
|
16
|
+
* 本模块只说**来源**。值本身、以及「这台机器凭什么派生出这个值」那句话,由每个键自己给
|
|
17
|
+
* ({@link PostureKnobFacts.postureMeaning})——那句话是键专属的事实,共享它只会得到一句谁都不认的空话。
|
|
18
|
+
*/
|
|
19
|
+
/** 旋钮取值来源的闭集词表(顺序 = 优先序)。 */
|
|
20
|
+
export declare const POSTURE_SOURCE_WORDS: readonly ["env", "center", "posture", "engine-default"];
|
|
21
|
+
/** 一个旋钮的取值来源(闭集四词)。加词 ⇒ {@link postureSourceNote} 的穷举 switch 编译红。 */
|
|
22
|
+
export type PostureSource = (typeof POSTURE_SOURCE_WORDS)[number];
|
|
23
|
+
/** 一个旋钮报出「我是谁定的」所需要的三件事。 */
|
|
24
|
+
export interface PostureKnobFacts {
|
|
25
|
+
/** 这台机器上占住这个键的 env 变量名(指路句要逐字点它,运维才知道去改哪个)。 */
|
|
26
|
+
readonly knob: string;
|
|
27
|
+
readonly source: PostureSource;
|
|
28
|
+
/**
|
|
29
|
+
* `posture` 那一臂**专属**的一句话:这台机器为什么派生出了这个值,以及怎么把它钉回去。
|
|
30
|
+
* 只在 `source === "posture"` 时被读到 —— 别的臂的句子是通用的(见 {@link postureSourceNote})。
|
|
31
|
+
*/
|
|
32
|
+
readonly postureMeaning: string;
|
|
33
|
+
}
|
|
34
|
+
/** 运维指路句 —— **穷举 switch,无缺省臂**:四词闭集加一个成员这里就编译红。 */
|
|
35
|
+
export declare function postureSourceNote(facts: PostureKnobFacts): string;
|
|
36
|
+
/**
|
|
37
|
+
* 这个值是**运维显式表态**的吗?
|
|
38
|
+
*
|
|
39
|
+
* 🔴 S-178 施工时真踩(本批全量抓出):`hasOperatorGateIntent`(`src/approval.ts`)判「运维表达过门意图
|
|
40
|
+
* 没有」时读的是 `durableApproval` 这个**值** —— 那在本键进 posture 家族之前是对的(它只有显式设过才为
|
|
41
|
+
* 真),之后当场失真:单机 turnkey 上 posture 派生的 `true` 会被读成「运维要门」,于是 run-local 那条
|
|
42
|
+
* **结构上没有 durable 门**的腿在每次 boot 打一条补偿 warn,而它其实正处在「零门意图的既定姿势」里。
|
|
43
|
+
* 这与 S-167 头注①记的是**同一形**病(用一个会被 posture 铸出来的量当「有人表态过」的代理)。
|
|
44
|
+
*
|
|
45
|
+
* ⇒ 判据落到**来源**上,穷举 switch(四词闭集加员即编译红):`env`(这台机器的运维写下的)与 `center`
|
|
46
|
+
* (组织发布的)是表态;`posture`(姿态派生)与 `engine-default`(内建缺省)都**不是**「有人要求过」。
|
|
47
|
+
*/
|
|
48
|
+
export declare function isExplicitPosture(source: PostureSource): boolean;
|
|
49
|
+
/** operator 诊断面的一行读数:值 + 来源 + 指路句。三个 S-178 旋钮与 S-167 的 readFace 同形。 */
|
|
50
|
+
export interface PostureKnobReading<V> {
|
|
51
|
+
value: V;
|
|
52
|
+
source: PostureSource;
|
|
53
|
+
note: string;
|
|
54
|
+
}
|
|
55
|
+
/** 一行读数的铸点(纯数据 ⇒ `build*`)。 */
|
|
56
|
+
export declare function buildPostureKnobReading<V>(value: V, facts: PostureKnobFacts): PostureKnobReading<V>;
|
|
57
|
+
//# sourceMappingURL=posture-source.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const POSTURE_SOURCE_WORDS = ["env", "center", "posture", "engine-default"];
|
|
2
|
+
export function postureSourceNote(facts) {
|
|
3
|
+
switch (facts.source) {
|
|
4
|
+
case "env":
|
|
5
|
+
return `pinned by this machine's ${facts.knob} env var (deployment sovereignty — it wins over any published value for this key)`;
|
|
6
|
+
case "center":
|
|
7
|
+
return `applied from the config-center (restart-to-apply); set ${facts.knob} on this machine to take the key back`;
|
|
8
|
+
case "posture":
|
|
9
|
+
return facts.postureMeaning;
|
|
10
|
+
case "engine-default":
|
|
11
|
+
return `not pinned by this deployment — the built-in default for ${facts.knob} is in force`;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isExplicitPosture(source) {
|
|
15
|
+
switch (source) {
|
|
16
|
+
case "env":
|
|
17
|
+
case "center":
|
|
18
|
+
return true;
|
|
19
|
+
case "posture":
|
|
20
|
+
case "engine-default":
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function buildPostureKnobReading(value, facts) {
|
|
25
|
+
return { value, source: facts.source, note: postureSourceNote(facts) };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=posture-source.js.map
|
|
@@ -27,10 +27,14 @@
|
|
|
27
27
|
* 两者在 core 里是并列机制(deny 表在 open 面上照样拦)。core 的 `config.read_face_deployment_clamped`
|
|
28
28
|
* (只读挂载钳制)同样不受本件影响。
|
|
29
29
|
*/
|
|
30
|
-
|
|
30
|
+
import { type PostureSource } from "./posture-source.js";
|
|
31
|
+
/**
|
|
32
|
+
* 档位来源的闭集词表 —— **S-178 起转 `posture-source.ts` 的共享件**(本模块只是给这一档取个本地名字,
|
|
33
|
+
* 不再持第二份词表:同一张四词表抄两遍的那天,两份会在加词时分家)。
|
|
34
|
+
*/
|
|
31
35
|
export declare const READ_FACE_SOURCE_WORDS: readonly ["env", "center", "posture", "engine-default"];
|
|
32
|
-
/** 档位的来源(闭集四词)。加词 ⇒ {@link
|
|
33
|
-
export type ReadFaceSource =
|
|
36
|
+
/** 档位的来源(闭集四词)。加词 ⇒ {@link postureSourceNote} 的穷举 switch 编译红。 */
|
|
37
|
+
export type ReadFaceSource = PostureSource;
|
|
34
38
|
/** 一台 worker 的 READ 面档位事实(值 + 来源)。`ServiceConfigFlat` 结构上满足本形。 */
|
|
35
39
|
export interface ReadFacePostureFacts {
|
|
36
40
|
readonly readFace?: "open" | "roots" | undefined;
|
|
@@ -41,7 +45,8 @@ export interface ReadFacePostureFacts {
|
|
|
41
45
|
* 单用户 turnkey ⇒ `open`;多租户 ⇒ 不铸(引擎默认 `roots`,server 不复制上游默认)。
|
|
42
46
|
*/
|
|
43
47
|
export declare function readFaceForPosture(singleUserTurnkey: boolean): ReadFacePostureFacts;
|
|
44
|
-
/**
|
|
48
|
+
/** 本档的运维指路句 —— 骨架由共享件 {@link postureSourceNote} 给(四词穷举在那里,加词即编译红);
|
|
49
|
+
* 本函数只提供 `posture` 那一臂的**键专属**事实句。 */
|
|
45
50
|
export declare function readFaceSourceNote(source: ReadFaceSource): string;
|
|
46
51
|
/** operator 诊断面的投影(`GET /v1/diagnostics/wiring`)。`face:null` = 本部署没钉档,引擎默认接管。 */
|
|
47
52
|
export declare function buildReadFaceWiring(facts: ReadFacePostureFacts): {
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { POSTURE_SOURCE_WORDS, postureSourceNote } from "./posture-source.js";
|
|
2
|
+
export const READ_FACE_SOURCE_WORDS = POSTURE_SOURCE_WORDS;
|
|
2
3
|
export function readFaceForPosture(singleUserTurnkey) {
|
|
3
4
|
return singleUserTurnkey ? { readFace: "open", readFaceSource: "posture" } : { readFaceSource: "engine-default" };
|
|
4
5
|
}
|
|
5
6
|
export function readFaceSourceNote(source) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
case "posture":
|
|
12
|
-
return "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set): reads outside the workspace roots are allowed by default — set READ_FACE=roots to pin the containing posture back";
|
|
13
|
-
case "engine-default":
|
|
14
|
-
return "not pinned by this deployment — the engine's own default (roots) is in force";
|
|
15
|
-
}
|
|
7
|
+
return postureSourceNote({
|
|
8
|
+
knob: "READ_FACE",
|
|
9
|
+
source,
|
|
10
|
+
postureMeaning: "derived from the single-user turnkey posture (REQUIRE_PRINCIPAL is not set): reads outside the workspace roots are allowed by default — set READ_FACE=roots to pin the containing posture back",
|
|
11
|
+
});
|
|
16
12
|
}
|
|
17
13
|
export function buildReadFaceWiring(facts) {
|
|
18
14
|
return { face: facts.readFace ?? null, source: facts.readFaceSource, note: readFaceSourceNote(facts.readFaceSource) };
|
package/dist/rules-consent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type
|
|
1
|
+
import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedRule, type RuleBehavior, type RuleCandidate, type EffectivePermissionRule, type RemoveResult, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
|
|
2
2
|
import type { DurableRulePartitionProvider, PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
|
|
3
3
|
import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./plugins/permission-rule-store-sql.js";
|
|
4
4
|
/**
|
|
@@ -20,10 +20,7 @@ export interface RuleConsentRecordStores {
|
|
|
20
20
|
principal: string;
|
|
21
21
|
approvalId: string;
|
|
22
22
|
purpose: RuleTicketPurpose;
|
|
23
|
-
candidates: ReadonlyArray<
|
|
24
|
-
rule: string;
|
|
25
|
-
scope: RuleScope;
|
|
26
|
-
}>;
|
|
23
|
+
candidates: ReadonlyArray<RuleCandidate>;
|
|
27
24
|
ttlMs: number;
|
|
28
25
|
}): Promise<RuleImportTicket>;
|
|
29
26
|
/** 一次**认领**(不是「已完成」)。语义与「认领之后怎么办」见 {@link RuleConsentStores.tickets.release}。
|
|
@@ -144,9 +141,12 @@ export declare const MAX_LOCAL_IMPORT_BYTES: number;
|
|
|
144
141
|
* 救回来",而不是等下一次 prepare。运维旋钮暂不开(没有部署形要求它可调)。
|
|
145
142
|
*/
|
|
146
143
|
export declare const LOCAL_IMPORT_CLAIM_LEASE_MS = 60000;
|
|
147
|
-
/** 一条规则的 wire 身份键(规则文本 + {@link serializeRuleScope} 的判别式串)。两个面
|
|
148
|
-
* 用**同一把键**,客户端拿它把两张表 join 起来。
|
|
144
|
+
/** 一条规则的 wire 身份键(**态** + 规则文本 + {@link serializeRuleScope} 的判别式串)。两个面
|
|
145
|
+
* (预览 / 兑付回执)用**同一把键**,客户端拿它把两张表 join 起来。
|
|
146
|
+
* 🔴 `behavior` 自 7.67.0 起是这把键的一格(core 7.9.0 [ref]:规则的身份是三元组)—— 少了它,同文本
|
|
147
|
+
* 同作用域的 deny 与 allow 两行会被本口判成 `duplicate_row`,而它们是两条不同的规则。 */
|
|
149
148
|
export interface LocalImportRuleKey {
|
|
149
|
+
behavior: RuleBehavior;
|
|
150
150
|
rule: string;
|
|
151
151
|
scope: string;
|
|
152
152
|
}
|
|
@@ -502,14 +502,18 @@ export declare function parseRuleScope(text: string): DurableRuleScope | undefin
|
|
|
502
502
|
* `adds` **如实上 wire**、不折叠成单值:同一条规则可以被批准过多次(不同 dot、不同来源、不同时刻),
|
|
503
503
|
* 折成「一个 addedAt」会让「这条规则是我导入的还是我点过卡的」在治理面上不可分。 */
|
|
504
504
|
export interface PersistedRuleWireRow {
|
|
505
|
+
/** core 7.9.0([ref])—— 这一行是哪一态的规则(`deny` / `ask` / `allow`)。**身份的一格**:
|
|
506
|
+
* 规则的身份是 (behavior, text, scope) 三元组,同文本不同态是两条不同的行(core `sameRuleIdentity`),
|
|
507
|
+
* 所以它既在列举面上,也在**导入/撤销的输入面**上必填 —— 少了它,一次撤销会瞄准同文本的另一态。 */
|
|
508
|
+
behavior: PersistedRule["behavior"];
|
|
505
509
|
rule: string;
|
|
506
510
|
/** {@link serializeRuleScope} 的判别式串。 */
|
|
507
511
|
scope: string;
|
|
508
|
-
tool:
|
|
509
|
-
match:
|
|
512
|
+
tool: PersistedRule["tool"];
|
|
513
|
+
match: PersistedRule["match"];
|
|
510
514
|
/** 去规范化的命令(core 的匹配器信的就是它 + `match`,不是重新解析 `rule`)。 */
|
|
511
515
|
command: string;
|
|
512
|
-
adds:
|
|
516
|
+
adds: PersistedRule["adds"];
|
|
513
517
|
/** [ref](core 7.5.0):这一行来自哪个源。**由 `scope` 派生**(`global`⟺`user`、`project`⟺
|
|
514
518
|
* `project`、`session`⟺`session`),不是行上另存的第二个字节 —— core 的 `ruleSourceOf` 是整条映射。
|
|
515
519
|
* 今天本部署只接 durable 一格分区 ⇒ 值域实为 `user | project`。
|
|
@@ -600,6 +604,7 @@ export interface RuleConsentLane {
|
|
|
600
604
|
*/
|
|
601
605
|
removeRule(input: {
|
|
602
606
|
principal: RuleBucketRef;
|
|
607
|
+
behavior: RuleBehavior;
|
|
603
608
|
rule: string;
|
|
604
609
|
scope: RuleScope;
|
|
605
610
|
}): Promise<RemoveResult>;
|
package/dist/rules-consent.js
CHANGED
|
@@ -4,7 +4,7 @@ import { recordFailOpen } from "./observability/fail-open.js";
|
|
|
4
4
|
import { createLogger } from "./observability/logger.js";
|
|
5
5
|
import { MAX_CWD_CHARS } from "./task-cwd.js";
|
|
6
6
|
import { CONTROL_AND_BIDI_CHARS, hasBidiControls } from "./text-bidi.js";
|
|
7
|
-
import { confirmRuleApproval,
|
|
7
|
+
import { confirmRuleApproval, RULE_BEHAVIORS, parseRuleText, ruleReachesProgramRun, ruleToolGrammarOf, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, removePersistedRule, effectiveOrThrow, readRowBehavior, } from "@sema-agent/core";
|
|
8
8
|
import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
|
|
9
9
|
export const RULE_IMPORT_TICKET_TTL_MS = 10 * 60_000;
|
|
10
10
|
export const RULE_IMPORT_RETRY_AFTER_SEC = 2;
|
|
@@ -18,7 +18,7 @@ const LocalImportOutcomeSchema = z
|
|
|
18
18
|
.object({
|
|
19
19
|
members: z.array(z
|
|
20
20
|
.object({
|
|
21
|
-
key: z.object({ rule: z.string().min(1), scope: z.string().min(1) }).strict(),
|
|
21
|
+
key: z.object({ behavior: z.preprocess((v) => readRowBehavior(v) ?? v, z.enum(RULE_BEHAVIORS)), rule: z.string().min(1), scope: z.string().min(1) }).strict(),
|
|
22
22
|
outcome: z.enum(["persisted", "no-op", "refused", "indeterminate"]),
|
|
23
23
|
reason: z.enum(["store_refused", "walk_failed"]).optional(),
|
|
24
24
|
detail: z.string().optional(),
|
|
@@ -83,6 +83,7 @@ function isSupportedRuleMatch(match) {
|
|
|
83
83
|
case "wildcard":
|
|
84
84
|
return true;
|
|
85
85
|
case "subpath":
|
|
86
|
+
case "path":
|
|
86
87
|
return false;
|
|
87
88
|
default: {
|
|
88
89
|
const unknown = match;
|
|
@@ -177,13 +178,25 @@ function screenLocalImportRow(row) {
|
|
|
177
178
|
`${derived.tool}/${derived.match}/${JSON.stringify(derived.command)} but the row says ${String(row.tool)}/${String(row.match)}/${JSON.stringify(row.command)}`,
|
|
178
179
|
};
|
|
179
180
|
}
|
|
180
|
-
|
|
181
|
+
if (derived.behavior !== row.behavior) {
|
|
182
|
+
return { reason: "row_inconsistent", detail: `this row says behavior ${JSON.stringify(String(row.behavior))} but the parser resolved ${JSON.stringify(derived.behavior)}` };
|
|
183
|
+
}
|
|
184
|
+
return { behavior: derived.behavior, rule: derived.rule, scope };
|
|
181
185
|
};
|
|
182
|
-
const written =
|
|
186
|
+
const written = parseRuleText(row.rule, row.behavior);
|
|
183
187
|
if ("reject" in written)
|
|
184
188
|
return { reason: "rule_rejected", detail: `${written.reject.code}: ${written.reject.message}` };
|
|
185
|
-
if (written.rule.tool
|
|
189
|
+
if (ruleToolGrammarOf(written.rule.tool) !== "command")
|
|
190
|
+
return consistent(written.rule);
|
|
191
|
+
if (written.rule.behavior !== "allow") {
|
|
192
|
+
if (!ruleReachesProgramRun(written.rule, row.command)) {
|
|
193
|
+
return {
|
|
194
|
+
reason: "row_inconsistent",
|
|
195
|
+
detail: `this tightening rule does not speak about the command it names: ${JSON.stringify(written.rule.rule)} vs ${JSON.stringify(row.command)}`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
186
198
|
return consistent(written.rule);
|
|
199
|
+
}
|
|
187
200
|
const pre = precheckCardRuleText(row.rule, row.command);
|
|
188
201
|
if (pre === undefined) {
|
|
189
202
|
return { reason: "rule_uncheckable", detail: "the rule text gate could not be asked about this row (its command is absent or not a string) — the row itself is malformed" };
|
|
@@ -199,22 +212,29 @@ function buildLocalImportLayers(candidates) {
|
|
|
199
212
|
const byRoot = new Map();
|
|
200
213
|
for (const c of candidates) {
|
|
201
214
|
const key = c.scope.kind === "global" ? "" : c.scope.root;
|
|
202
|
-
|
|
203
|
-
if (bucket === undefined)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
let bucket = byRoot.get(key);
|
|
216
|
+
if (bucket === undefined) {
|
|
217
|
+
bucket = { deny: [], ask: [], allow: [] };
|
|
218
|
+
byRoot.set(key, bucket);
|
|
219
|
+
}
|
|
220
|
+
bucket[c.behavior].push(c.rule);
|
|
207
221
|
}
|
|
208
|
-
return [...byRoot].map(([root,
|
|
222
|
+
return [...byRoot].map(([root, lists]) => ({
|
|
209
223
|
layer: root === "" ? "userSettings" : "projectSettings",
|
|
210
224
|
path: root === "" ? LOCAL_IMPORT_LAYER_PATH : `${LOCAL_IMPORT_LAYER_PATH}:${root}`,
|
|
211
225
|
root: root === "" ? "/" : root,
|
|
212
|
-
readFile: async () => JSON.stringify({
|
|
226
|
+
readFile: async () => JSON.stringify({
|
|
227
|
+
permissions: {
|
|
228
|
+
...(lists.deny.length > 0 ? { deny: lists.deny } : {}),
|
|
229
|
+
...(lists.ask.length > 0 ? { ask: lists.ask } : {}),
|
|
230
|
+
...(lists.allow.length > 0 ? { allow: lists.allow } : {}),
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
213
233
|
}));
|
|
214
234
|
}
|
|
215
235
|
function buildLocalImportMembers(members) {
|
|
216
236
|
return members.map((m) => {
|
|
217
|
-
const key = { rule: m.rule, scope: serializeRuleScope(m.scope) };
|
|
237
|
+
const key = { behavior: m.behavior, rule: m.rule, scope: serializeRuleScope(m.scope) };
|
|
218
238
|
switch (m.status) {
|
|
219
239
|
case "persisted":
|
|
220
240
|
return { key, outcome: "persisted" };
|
|
@@ -312,7 +332,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
312
332
|
if (minted === undefined) {
|
|
313
333
|
return { ok: false, reason: "confirm-refused", detail: "the edited confirmation landed without a minted candidate — nothing to redeem" };
|
|
314
334
|
}
|
|
315
|
-
const canonical =
|
|
335
|
+
const canonical = parseRuleText(minted.rule, "allow");
|
|
316
336
|
if ("reject" in canonical) {
|
|
317
337
|
return { ok: false, reason: "edit-rejected", detail: `the canonical spelling the engine minted does not re-parse: ${canonical.reject.message}` };
|
|
318
338
|
}
|
|
@@ -444,11 +464,11 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
444
464
|
for (const row of rows) {
|
|
445
465
|
const screened = screenLocalImportRow(row);
|
|
446
466
|
if ("reason" in screened) {
|
|
447
|
-
members.push({ key: { rule: row.rule, scope: row.scope }, admitted: false, reason: screened.reason, detail: screened.detail });
|
|
467
|
+
members.push({ key: { behavior: row.behavior, rule: row.rule, scope: row.scope }, admitted: false, reason: screened.reason, detail: screened.detail });
|
|
448
468
|
continue;
|
|
449
469
|
}
|
|
450
|
-
const key = { rule: screened.rule, scope: serializeRuleScope(screened.scope) };
|
|
451
|
-
const dedupKey = `${key.scope}${LOCAL_IMPORT_KEY_SEP}${key.rule}`;
|
|
470
|
+
const key = { behavior: screened.behavior, rule: screened.rule, scope: serializeRuleScope(screened.scope) };
|
|
471
|
+
const dedupKey = `${key.behavior}${LOCAL_IMPORT_KEY_SEP}${key.scope}${LOCAL_IMPORT_KEY_SEP}${key.rule}`;
|
|
452
472
|
if (seen.has(dedupKey)) {
|
|
453
473
|
members.push({ key, admitted: false, reason: "duplicate_row", detail: "this (rule, scope) appears earlier in the same batch — the engine would fold the two silently, so it is reported here instead" });
|
|
454
474
|
continue;
|
|
@@ -460,12 +480,12 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
460
480
|
if (admitted.length === 0)
|
|
461
481
|
return { ok: true, preview: { members } };
|
|
462
482
|
const { preview, approvalId } = await prepareCcImport({ layers: buildLocalImportLayers(admitted), principal, deps });
|
|
463
|
-
const engineKeys = new Set(preview.candidates.map((c) => `${serializeRuleScope(c.scope)}${LOCAL_IMPORT_KEY_SEP}${c.rule}`));
|
|
483
|
+
const engineKeys = new Set(preview.candidates.map((c) => `${c.behavior}${LOCAL_IMPORT_KEY_SEP}${serializeRuleScope(c.scope)}${LOCAL_IMPORT_KEY_SEP}${c.rule}`));
|
|
464
484
|
for (let i = 0; i < members.length; i++) {
|
|
465
485
|
const m = members[i];
|
|
466
486
|
if (m === undefined || !m.admitted)
|
|
467
487
|
continue;
|
|
468
|
-
if (engineKeys.has(`${m.key.scope}${LOCAL_IMPORT_KEY_SEP}${m.key.rule}`))
|
|
488
|
+
if (engineKeys.has(`${m.key.behavior}${LOCAL_IMPORT_KEY_SEP}${m.key.scope}${LOCAL_IMPORT_KEY_SEP}${m.key.rule}`))
|
|
469
489
|
continue;
|
|
470
490
|
const why = preview.skipped.find((s) => s.rule === m.key.rule)?.reason ?? "the engine's import gate did not take it, and said nothing about it";
|
|
471
491
|
localImportLogger.warn("local_import_row_admitted_but_not_importable", {
|
|
@@ -683,12 +703,12 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
683
703
|
},
|
|
684
704
|
async listRules(bucket) {
|
|
685
705
|
const view = await effectiveOrThrow(storeForBucket(stores.provider, bucket));
|
|
686
|
-
const rows = view.rules.map((r) => ({ rule: r.rule, scope: serializeRuleScope(r.scope), tool: r.tool, match: r.match, command: r.command, adds: r.adds, source: r.source, status: r.status }));
|
|
706
|
+
const rows = view.rules.map((r) => ({ behavior: r.behavior, rule: r.rule, scope: serializeRuleScope(r.scope), tool: r.tool, match: r.match, command: r.command, adds: r.adds, source: r.source, status: r.status }));
|
|
687
707
|
rows.sort((a, b) => (a.scope < b.scope ? -1 : a.scope > b.scope ? 1 : a.rule < b.rule ? -1 : a.rule > b.rule ? 1 : 0));
|
|
688
708
|
return { rows, rev: view.rev };
|
|
689
709
|
},
|
|
690
710
|
async removeRule(input) {
|
|
691
|
-
return await removePersistedRule({ rule: input.rule, scope: input.scope, principal: input.principal, provider: stores.provider });
|
|
711
|
+
return await removePersistedRule({ behavior: input.behavior, rule: input.rule, scope: input.scope, principal: input.principal, provider: stores.provider });
|
|
692
712
|
},
|
|
693
713
|
};
|
|
694
714
|
}
|
package/dist/runs.d.ts
CHANGED
|
@@ -324,7 +324,16 @@ parkCheckpointReader?: {
|
|
|
324
324
|
toolCallId?: unknown;
|
|
325
325
|
};
|
|
326
326
|
} | null>;
|
|
327
|
-
}
|
|
327
|
+
},
|
|
328
|
+
/** L-167:装配期(`prepareSpec` → `resolveSpec`)铸下的用户面通告 —— 那一刻本腿的通告口还没开,
|
|
329
|
+
* 所以由本腿在 `registerEngineNoticeLeg` 那一拍连同 `pending` 一起投(投递单点仍是 `route()`)。
|
|
330
|
+
* 缺席 ⇒ 一个字节不投(既有调用方逐字不变)。 */
|
|
331
|
+
pendingEngineNotices?: readonly {
|
|
332
|
+
code: string;
|
|
333
|
+
message: string;
|
|
334
|
+
detail?: Record<string, unknown>;
|
|
335
|
+
sessionId?: unknown;
|
|
336
|
+
}[]): Promise<void>;
|
|
328
337
|
/** How often a running instance refreshes its run's updated_at (liveness, independent of events).
|
|
329
338
|
* Must stay strictly below `runStaleSec` (asserted at startup AND on every hot config candidate) or the
|
|
330
339
|
* reaper would race live runs. [ref]:数值的单源是 `config-invariants.ts`(那条不变量的属主),两个消费面
|
package/dist/runs.js
CHANGED
|
@@ -153,7 +153,7 @@ export function evictIfConflict(runner, sessionId, result) {
|
|
|
153
153
|
runner.sessions.evict?.(sessionId);
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader) {
|
|
156
|
+
export async function runInBackground(runner, spec, runStore, taskId, metrics, principal, verify, cascade, instrumentDegenerate, planCacheProbe, persistThinking = false, inflight, preemptable, steerable, modelUsage, elicitation, elicitOwner, captureTurnAnchor, fleetPublisher, captureUserMessageAnchor, question, workflowCompletionInbox, subagentSteer, completionDiagLog, sendUserFile, promptManifests, approval, parkCheckpointReader, pendingEngineNotices) {
|
|
157
157
|
const startedAt = Date.now();
|
|
158
158
|
metrics?.addGauge("runs_active", 1);
|
|
159
159
|
fleetPublisher?.onStart();
|
|
@@ -248,6 +248,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
248
248
|
};
|
|
249
249
|
const unregisterEngineNotice = registerEngineNoticeLeg({
|
|
250
250
|
sessionId: spec.sessionId,
|
|
251
|
+
...(pendingEngineNotices !== undefined ? { pending: pendingEngineNotices } : {}),
|
|
251
252
|
durable: (row) => void append("engine_notice", row).catch(() => recordFailOpen("server.engine-notice.durable-append-failed", `leg=bg code=${row.code} task=${taskId}`)),
|
|
252
253
|
});
|
|
253
254
|
try {
|
package/dist/session-titler.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
|
|
2
|
+
import { recordFailOpen } from "./observability/fail-open.js";
|
|
2
3
|
export const TITLE_MAX_CHARS = 80;
|
|
3
4
|
const OBJECTIVE_SNIPPET_CHARS = 600;
|
|
4
5
|
const LLM_TIMEOUT_MS = 15_000;
|
|
5
6
|
const NO_ROW_RETRY_MS = 20_000;
|
|
6
7
|
const FAIL_CAUSE_KEY_CHARS = 80;
|
|
7
|
-
const FAIL_CAUSE_CAP = 100;
|
|
8
8
|
const SEEN_CAP = 10_000;
|
|
9
9
|
export function sanitizeTitle(raw) {
|
|
10
10
|
const oneLine = raw.replace(/[\r\n]+/g, " ").replace(CONTROL_AND_BIDI_STRIP_RE, "").trim();
|
|
@@ -22,18 +22,9 @@ export function buildTitlePrompt(objective) {
|
|
|
22
22
|
}
|
|
23
23
|
export function createSessionTitler(deps) {
|
|
24
24
|
const seen = new Set();
|
|
25
|
-
const warnedCauses = new Set();
|
|
26
25
|
const logFailure = (sessionId, error) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (warnedCauses.has(cause)) {
|
|
30
|
-
deps.logger?.debug?.("session_title_failed", meta);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
if (warnedCauses.size >= FAIL_CAUSE_CAP)
|
|
34
|
-
warnedCauses.clear();
|
|
35
|
-
warnedCauses.add(cause);
|
|
36
|
-
deps.logger?.warn?.("session_title_failed", { ...meta, note: "同 cause 后续降为 debug 级" });
|
|
26
|
+
deps.logger?.debug?.("session_title_failed", { sessionId, error: error.slice(0, 200) });
|
|
27
|
+
recordFailOpen("server.session-titler.gateway-unreachable", error.slice(0, FAIL_CAUSE_KEY_CHARS));
|
|
37
28
|
};
|
|
38
29
|
return {
|
|
39
30
|
maybeTitle(sessionId, objective, model) {
|