@sema-agent/server 7.57.0 → 7.59.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 +2 -0
- package/dist/approval-card.d.ts +47 -0
- package/dist/approval-card.js +18 -0
- package/dist/boot/parked-revive-gate.d.ts +3 -0
- package/dist/boot/parked-revive-gate.js +2 -2
- package/dist/boot/resolve-spec.js +1 -1
- package/dist/boot/runner-deps.d.ts +2 -2
- package/dist/boot/runner-deps.js +4 -0
- package/dist/boot/stores.js +15 -8
- package/dist/config-catalog.d.ts +0 -29
- package/dist/config-catalog.js +3 -0
- package/dist/config-center/http-client.js +7 -11
- package/dist/config-center/read-warnings.d.ts +20 -0
- package/dist/config-center/read-warnings.js +36 -0
- package/dist/config-provider.d.ts +3 -0
- package/dist/config-provider.js +4 -8
- package/dist/config-types.d.ts +12 -2
- package/dist/config.js +13 -2
- package/dist/cross-session-settings.d.ts +70 -0
- package/dist/cross-session-settings.js +37 -0
- package/dist/deployment-governance.d.ts +30 -1
- package/dist/deployment-governance.js +11 -5
- package/dist/hooks/hook-runner.js +8 -8
- package/dist/main.js +1 -0
- package/dist/memory-scope.d.ts +20 -0
- package/dist/memory-scope.js +9 -1
- package/dist/observability/metrics.js +1 -1
- package/dist/observability/run-terminal-log.d.ts +4 -0
- package/dist/observability/run-terminal-log.js +15 -1
- package/dist/plugins/device-store-sql.d.ts +30 -6
- package/dist/plugins/device-store-sql.js +23 -4
- package/dist/plugins/file-history-store-sql.d.ts +5 -0
- package/dist/plugins/file-history-store-sql.js +7 -7
- package/dist/plugins/image-bake-store-sql.js +1 -1
- package/dist/plugins/mailbox-store-sql.d.ts +33 -11
- package/dist/plugins/mailbox-store-sql.js +74 -12
- package/dist/plugins/retention-lane-store-sql.d.ts +8 -4
- package/dist/plugins/retention-lane-store-sql.js +1 -1
- package/dist/plugins/retention-store-sql.d.ts +7 -3
- package/dist/plugins/retention-store-sql.js +2 -2
- package/dist/plugins/task-list-store-sql.d.ts +2 -2
- package/dist/plugins/task-list-store-sql.js +1 -1
- package/dist/plugins/usage-window-store-sql.d.ts +5 -3
- package/dist/plugins/usage-window-store-sql.js +1 -5
- package/dist/run-local.d.ts +2 -1
- package/dist/run-local.js +3 -3
- package/dist/runs.d.ts +13 -0
- package/dist/runs.js +2 -0
- package/dist/task-settings.d.ts +13 -3
- package/dist/task-settings.js +1 -1
- package/dist/tool-approval.d.ts +23 -1
- package/dist/tool-approval.js +6 -1
- package/dist/trace/core-keyset-guard.d.ts +7 -8
- package/dist/trace/engine-notice-wire.d.ts +1 -1
- package/dist/trace/engine-notice-wire.js +5 -0
- package/dist/trace/project.d.ts +45 -3
- package/dist/trace/project.js +23 -0
- package/dist/trace/redact.js +7 -3
- package/dist/trace/sema-provenance.d.ts +15 -0
- package/dist/trace/sema-provenance.js +3 -0
- package/package.json +3 -3
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const CROSS_SESSION_INBOUND_LAYER_NAMES = ["managed", "user", "repo"];
|
|
2
|
+
export const CROSS_SESSION_INBOUND_LAYER_SOURCES = {
|
|
3
|
+
managed: {
|
|
4
|
+
source: "CROSS_SESSION_INBOUND —— 部署/组织层旋钮(词表 accept|hold|refuse,未设=本层不设)",
|
|
5
|
+
wired: true,
|
|
6
|
+
},
|
|
7
|
+
user: {
|
|
8
|
+
source: "body.settings.crossSessionInbound —— 提交面明拒(unsupportedKeys 400,宽容层不读),因引擎座是部署级、无会话上下文," +
|
|
9
|
+
"把某一次提交的值塞进部署座会让一个会话的设置治理另一个会话;候上游每任务座或本仓 per-run 上下文全腿覆盖",
|
|
10
|
+
wired: false,
|
|
11
|
+
},
|
|
12
|
+
repo: {
|
|
13
|
+
source: "本服务没有项目层 settings 源(无 .claude/settings.json 之类的仓库层读点);不造一个来凑数",
|
|
14
|
+
wired: false,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export function buildCrossSessionInboundLayers(cfg) {
|
|
18
|
+
const layers = {};
|
|
19
|
+
for (const name of CROSS_SESSION_INBOUND_LAYER_NAMES) {
|
|
20
|
+
if (!CROSS_SESSION_INBOUND_LAYER_SOURCES[name].wired)
|
|
21
|
+
continue;
|
|
22
|
+
switch (name) {
|
|
23
|
+
case "managed":
|
|
24
|
+
if (cfg.crossSessionInbound !== undefined)
|
|
25
|
+
layers.managed = cfg.crossSessionInbound;
|
|
26
|
+
break;
|
|
27
|
+
case "user":
|
|
28
|
+
case "repo":
|
|
29
|
+
throw new Error(`cross-session inbound layer "${name}" is marked wired in CROSS_SESSION_INBOUND_LAYER_SOURCES but has no value arm here — wire the source or set wired:false (an unwired layer must be ABSENT, never a silently empty one)`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return layers;
|
|
33
|
+
}
|
|
34
|
+
export function crossSessionDialogExpiryOf(cfg) {
|
|
35
|
+
return cfg.crossSessionDialogExpiry;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=cross-session-settings.js.map
|
|
@@ -11,6 +11,35 @@ export interface DeploymentGovernanceConfigView {
|
|
|
11
11
|
readonly commandPolicy?: ServiceConfig["commandPolicy"];
|
|
12
12
|
readonly manualModeShellGate?: ServiceConfig["manualModeShellGate"];
|
|
13
13
|
readonly sensitiveWritePatterns: ServiceConfig["sensitiveWritePatterns"];
|
|
14
|
+
/**
|
|
15
|
+
* 本部署的**本地店根**(`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR → ~/.ai-agent`)—— 在本口只喂
|
|
16
|
+
* **誊本门**(`<localDataRoot>/sessions`)。S-133 起它**不再**当守卫集的 `dataRoot`(那是引擎记忆根,见
|
|
17
|
+
* {@link DeploymentGovernanceRoots});下面这段历史注保留,记的是 S-125 件⑦ 把两根混作一根的由来。
|
|
18
|
+
*
|
|
19
|
+
* S-125 件⑦(core 7.4.0 [ref] HRD-PRM-10)—— 当时把本键原样交给 `createSensitivePathPolicy` 的 `dataRoot`。
|
|
20
|
+
*
|
|
21
|
+
* 为什么必须**显式**传而不是吃 core 的缺省:core 的缺省是 `$AGENT_DATA_DIR ?? ~/.ai-agent`,而本仓的
|
|
22
|
+
* 数据根解析链多一节最高优先级的覆盖(`config.ts`:`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR
|
|
23
|
+
* → ~/.ai-agent`)。只设 `LOCAL_DATA_ROOT=/data/sema` 的部署,数据真的躺在 `/data/sema`,而 core 的缺省
|
|
24
|
+
* 会去猜 `~/.ai-agent` ⇒ 守卫把真数据根**当普通路径**判(它的祖先段一旦命中守卫集里的某一条,整棵子树
|
|
25
|
+
* 连同模型被**指示**要写的记忆库一起被硬拒),同时又把一个不存在的目录当成豁免区。两个方向都错,且都静默。
|
|
26
|
+
* ⇒ 本键 = 「这道门与 transcript 门说的是同一个目录」的单一属主,值与 `ServiceConfig.localDataRoot`
|
|
27
|
+
* **同一个**解析结果(不在本口重算,重算就是第二个写者)。
|
|
28
|
+
*
|
|
29
|
+
* 缺席(窄接口的老调用方 / 测试夹具)⇒ **不铸 `dataRoot` 键**,core 走它自己的缺省 —— 与本口既有的
|
|
30
|
+
* `rootPath` 条件展开同姿势:缺席是「本调用方没有这项知识」,不是「数据根是 undefined」。
|
|
31
|
+
*/
|
|
32
|
+
readonly localDataRoot?: ServiceConfig["localDataRoot"];
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* S-133 —— 守卫集 `dataRoot` 的**显式供给**(与 `DeploymentGovernanceConfigView.localDataRoot` 是两件事):
|
|
36
|
+
* `engineDataRoot` = 引擎写记忆的根 = boot 已铸的 `memoryEngine.root`(boot/stores.ts 是唯一写者;推导函数
|
|
37
|
+
* `memoryEngineRootFor` 只在那里与 `memoryEngineBackendFor` 里调)。**调用腿读那只活对象,不重推导**——重推导
|
|
38
|
+
* 就是第二个写者(7.59.0 重扫 codex 轮抓到 HTTP 腿的 fallback 与 stores 不同源,正是这一形)。参数非可选:
|
|
39
|
+
* 一条腿忘了 = 编译红。`undefined` = 引擎未接(或窄接口的测试夹具),那时不铸 `dataRoot` 键,core 走自己的缺省。
|
|
40
|
+
*/
|
|
41
|
+
export interface DeploymentGovernanceRoots {
|
|
42
|
+
readonly engineDataRoot: string | undefined;
|
|
14
43
|
}
|
|
15
44
|
/** 审批基线读的配置切面(同上,窄接口)。 */
|
|
16
45
|
export interface ApprovalBaselineConfigView {
|
|
@@ -147,7 +176,7 @@ export declare function buildOnlySensitiveBaselineWarning(config: Pick<Deploymen
|
|
|
147
176
|
* `manualModeShellGate`/`sensitivePathPolicy` 按在场性条件展开 —— `applyRuntimeGovernance` 的
|
|
148
177
|
* `!== undefined` 判据对两者等价,但 profile 是折叠面的可观测字节,搬家不许顺手改。
|
|
149
178
|
*/
|
|
150
|
-
export declare function createDeploymentGovernanceInputs(config: DeploymentGovernanceConfigView, pathAdjudication: PathAdjudication): DeploymentGovernanceInputs;
|
|
179
|
+
export declare function createDeploymentGovernanceInputs(config: DeploymentGovernanceConfigView, pathAdjudication: PathAdjudication, roots: DeploymentGovernanceRoots): DeploymentGovernanceInputs;
|
|
151
180
|
/**
|
|
152
181
|
* 审批基线 —— `applyRuntimeGovernance` 那个 base 的 `toolPolicy` 座([ref] 件一)。**恒非
|
|
153
182
|
* `undefined`**:治理层是 tighten-only 的叠加层,没有基线可叠时它自己也产不出「门在场」这件事。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { posix } from "node:path";
|
|
2
|
-
import { FileError, StubExecutionEnv, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, createSensitivePathPolicy, err, ok, } from "@sema-agent/core";
|
|
1
|
+
import { join, posix } from "node:path";
|
|
2
|
+
import { FileError, StubExecutionEnv, combinePolicies, createAllowDenyPolicy, createDurableQuestionPolicy, createSensitivePathPolicy, createTranscriptIntegrityPolicy, err, ok, } from "@sema-agent/core";
|
|
3
3
|
import { createDurableAskPolicy } from "./approval.js";
|
|
4
4
|
export function createDurableQuestionGate(live) {
|
|
5
5
|
if (live === undefined)
|
|
@@ -47,17 +47,23 @@ export function buildOnlySensitiveBaselineWarning(config, seat) {
|
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
-
export function createDeploymentGovernanceInputs(config, pathAdjudication) {
|
|
50
|
+
export function createDeploymentGovernanceInputs(config, pathAdjudication, roots) {
|
|
51
51
|
const sensitivePathPolicy = config.sensitiveWritePatterns.length > 0
|
|
52
52
|
? (() => {
|
|
53
|
+
const dataRootOpt = roots.engineDataRoot !== undefined ? { dataRoot: roots.engineDataRoot } : {};
|
|
53
54
|
const realTarget = createSensitivePathPolicy({
|
|
54
55
|
env: pathAdjudication.env,
|
|
55
56
|
patterns: config.sensitiveWritePatterns,
|
|
56
57
|
...(pathAdjudication.cwd !== undefined ? { rootPath: pathAdjudication.cwd } : {}),
|
|
58
|
+
...dataRootOpt,
|
|
57
59
|
});
|
|
60
|
+
const transcriptGuard = config.localDataRoot !== undefined
|
|
61
|
+
? createTranscriptIntegrityPolicy({ sessionsDirs: [join(config.localDataRoot, "sessions")] })
|
|
62
|
+
: undefined;
|
|
63
|
+
const withTranscripts = (p) => (transcriptGuard ? combinePolicies(p, transcriptGuard) : p);
|
|
58
64
|
if (pathAdjudication.cwd !== undefined)
|
|
59
|
-
return realTarget;
|
|
60
|
-
return combinePolicies(realTarget, createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns }));
|
|
65
|
+
return withTranscripts(realTarget);
|
|
66
|
+
return withTranscripts(combinePolicies(realTarget, createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt })));
|
|
61
67
|
})()
|
|
62
68
|
: undefined;
|
|
63
69
|
return {
|
|
@@ -673,7 +673,7 @@ async function runObserveOnlyEvent(event, groups, matchValue, payload, ctx, once
|
|
|
673
673
|
if (!out)
|
|
674
674
|
continue;
|
|
675
675
|
if (typeof out.systemMessage === "string")
|
|
676
|
-
ctx.logger.
|
|
676
|
+
ctx.logger.info("hook_system_message", { event, message: clip(out.systemMessage, 500) });
|
|
677
677
|
if (out.decision !== undefined || out.continue === false || out.hookSpecificOutput !== undefined) {
|
|
678
678
|
ctx.logger.warn("hook_result_unsupported", { event });
|
|
679
679
|
}
|
|
@@ -747,7 +747,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
747
747
|
if (!out)
|
|
748
748
|
continue;
|
|
749
749
|
if (typeof out.systemMessage === "string")
|
|
750
|
-
ctx.logger.
|
|
750
|
+
ctx.logger.info("hook_system_message", { event: "PreToolUse", message: clip(out.systemMessage, 500) });
|
|
751
751
|
if (out.continue === false) {
|
|
752
752
|
const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
|
|
753
753
|
return { action: "deny", message: reason, ...buildHookContextField(contexts) };
|
|
@@ -845,7 +845,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
845
845
|
if (!out)
|
|
846
846
|
continue;
|
|
847
847
|
if (typeof out.systemMessage === "string")
|
|
848
|
-
ctx.logger.
|
|
848
|
+
ctx.logger.info("hook_system_message", { event: "PostToolUse", message: clip(out.systemMessage, 500) });
|
|
849
849
|
if (out.continue === false) {
|
|
850
850
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUse" });
|
|
851
851
|
}
|
|
@@ -895,7 +895,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
895
895
|
if (!out)
|
|
896
896
|
continue;
|
|
897
897
|
if (typeof out.systemMessage === "string")
|
|
898
|
-
ctx.logger.
|
|
898
|
+
ctx.logger.info("hook_system_message", { event: "PostToolUseFailure", message: clip(out.systemMessage, 500) });
|
|
899
899
|
if (out.continue === false)
|
|
900
900
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolUseFailure" });
|
|
901
901
|
if (out.decision === "block" && typeof out.reason === "string")
|
|
@@ -942,7 +942,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
942
942
|
if (!out)
|
|
943
943
|
continue;
|
|
944
944
|
if (typeof out.systemMessage === "string")
|
|
945
|
-
ctx.logger.
|
|
945
|
+
ctx.logger.info("hook_system_message", { event: "PostToolBatch", message: clip(out.systemMessage, 500) });
|
|
946
946
|
if (out.continue === false)
|
|
947
947
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PostToolBatch" });
|
|
948
948
|
if (out.decision === "block" && typeof out.reason === "string")
|
|
@@ -974,7 +974,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
974
974
|
if (!out)
|
|
975
975
|
continue;
|
|
976
976
|
if (typeof out.systemMessage === "string")
|
|
977
|
-
ctx.logger.
|
|
977
|
+
ctx.logger.info("hook_system_message", { event: "UserPromptSubmit", message: clip(out.systemMessage, 500) });
|
|
978
978
|
if (out.continue === false) {
|
|
979
979
|
const reason = clip(typeof out.stopReason === "string" ? out.stopReason : "hook requested stop", MAX_HOOK_FEEDBACK_CHARS);
|
|
980
980
|
return { block: reason, ...buildHookContextField(contexts) };
|
|
@@ -1063,7 +1063,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
1063
1063
|
continue;
|
|
1064
1064
|
}
|
|
1065
1065
|
if (typeof out.systemMessage === "string")
|
|
1066
|
-
ctx.logger.
|
|
1066
|
+
ctx.logger.info("hook_system_message", { event: "Stop", message: clip(out.systemMessage, 500) });
|
|
1067
1067
|
if (out.continue === false)
|
|
1068
1068
|
sawContinueFalse = true;
|
|
1069
1069
|
if (out.decision === "block" && block === undefined) {
|
|
@@ -1122,7 +1122,7 @@ export function createTaskHooks(config, ctx) {
|
|
|
1122
1122
|
if (!out)
|
|
1123
1123
|
continue;
|
|
1124
1124
|
if (typeof out.systemMessage === "string")
|
|
1125
|
-
ctx.logger.
|
|
1125
|
+
ctx.logger.info("hook_system_message", { event: "PreCompact", message: clip(out.systemMessage, 500) });
|
|
1126
1126
|
if (out.continue === false) {
|
|
1127
1127
|
ctx.logger.warn("hook_continue_false_unsupported", { event: "PreCompact" });
|
|
1128
1128
|
}
|
package/dist/main.js
CHANGED
|
@@ -475,6 +475,7 @@ async function main() {
|
|
|
475
475
|
const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
|
|
476
476
|
? createParkedReviveInheritedGate({
|
|
477
477
|
config, question, approvalExemptionStore, logger, localRoot,
|
|
478
|
+
memoryEngineRoot: memoryEngine?.root,
|
|
478
479
|
approverSeat: createRunnerDepsOnAsk(toolApproval),
|
|
479
480
|
...(runnerDeps.runtimeCapsResolver ? { resolveRuntimeCaps: runnerDeps.runtimeCapsResolver } : {}),
|
|
480
481
|
autoModeSeatMounted: runnerDeps.autoMode !== undefined,
|
package/dist/memory-scope.d.ts
CHANGED
|
@@ -84,6 +84,26 @@ engineWired: boolean): {
|
|
|
84
84
|
* Returns the backend + the resolved config root (the root also rides on `RunnerDeps.memoryEngineDir` so
|
|
85
85
|
* core derives the B3 control plane beside it).
|
|
86
86
|
*/
|
|
87
|
+
/**
|
|
88
|
+
* S-133(7.59.0 合并重扫 wf_5ac18676 #1)—— **引擎数据根的唯一推导点**。模型是用 fs 工具往这里写记忆的
|
|
89
|
+
* ([ref] 记忆边界不变式,boot/memory-boundary.ts),所以三件事必须读**同一只**值:①boot 起哪只记忆后端的根
|
|
90
|
+
* (boot/stores.ts 三腿)、②`RunnerDeps.memoryEngineDir`、③守卫集 `createSensitivePathPolicy` 的 `dataRoot`
|
|
91
|
+
* (deployment-governance.ts:落在这个根**里面**的目标按相对根判,根自己的祖先段不算目标的账)。
|
|
92
|
+
* 此前 ③ 读的是 `config.localDataRoot`(本地**店**根:`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR`),
|
|
93
|
+
* 而 ① 的 file 腿读的是 `MEMORY_ENGINE_DIR ?? AGENT_DATA_DIR`——两条链在「店根另指他处 + 记忆根坐在守卫段下
|
|
94
|
+
* (壳布局 `~/.sema/engine-data`)」时分家 ⇒ 记忆库每一次 Write 被硬拒(7.58.0 放行 ⇒ 回归);反向
|
|
95
|
+
* (`CONFIG_LOCAL_DIR=~/.sema` 当 dataRoot)则让配置根自身免检。两根本就是两件事:店根归誊本门,引擎根归本函数。
|
|
96
|
+
*
|
|
97
|
+
* 形与 boot/stores.ts 逐字同律:DB 记忆腿(pg/tidb)的根 = `<localDataRoot ?? fallbackRoot>/memory-work`
|
|
98
|
+
* (per-worker 物化区);file 腿 = `resolveMemoryEngineRoot(memoryEngineDir ?? fallbackRoot)`。**不判引擎是否
|
|
99
|
+
* 启用**——纯路径推导,禁用态下守卫仍以「若开引擎会写到哪」为根,与 core 缺省(`$AGENT_DATA_DIR`)同向。
|
|
100
|
+
*/
|
|
101
|
+
export declare function memoryEngineRootFor(config: Pick<ServiceConfig, "memoryEngineBackend" | "memoryEngineDir" | "localDataRoot">, fallbackRoot?: string): string;
|
|
102
|
+
/** file 后端的根链(`MEMORY_ENGINE_DIR ?? fallback ?? $AGENT_DATA_DIR ?? ~/.ai-agent`)—— 与后端选择器**无关**:
|
|
103
|
+
* `memoryEngineBackendFor` 恒造 file 后端(run-local 从 env 继承 `MEMORY_ENGINE_BACKEND=pg|tidb` 时仍是 file),
|
|
104
|
+
* 所以它的根必须走这条链而不是 `memoryEngineRootFor` 的分派(codex 修复验证轮 [medium]:分派会把 run-local 的
|
|
105
|
+
* 记忆搬到 `<localDataRoot>/memory-work`,存量记忆与控制面升级后隐形)。 */
|
|
106
|
+
export declare function fileMemoryEngineRoot(config: Pick<ServiceConfig, "memoryEngineDir">, fallbackRoot?: string): string;
|
|
87
107
|
export declare function memoryEngineBackendFor(config: ServiceConfig, fallbackRoot?: string): {
|
|
88
108
|
backend: FileMemoryEngineBackend;
|
|
89
109
|
root: string;
|
package/dist/memory-scope.js
CHANGED
|
@@ -60,12 +60,20 @@ export function buildMemoryRemoteLaneWarn(config, engineWired) {
|
|
|
60
60
|
"WORKER fs; verify the lane really shares it.",
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
export function memoryEngineRootFor(config, fallbackRoot) {
|
|
64
|
+
if (config.memoryEngineBackend !== "file")
|
|
65
|
+
return join(config.localDataRoot ?? fallbackRoot ?? resolveMemoryEngineRoot(undefined), "memory-work");
|
|
66
|
+
return fileMemoryEngineRoot(config, fallbackRoot);
|
|
67
|
+
}
|
|
68
|
+
export function fileMemoryEngineRoot(config, fallbackRoot) {
|
|
69
|
+
return resolveMemoryEngineRoot(config.memoryEngineDir ?? fallbackRoot);
|
|
70
|
+
}
|
|
63
71
|
export function memoryEngineBackendFor(config, fallbackRoot) {
|
|
64
72
|
if (config.requirePrincipal === true || !config.memoryEngineEnabled)
|
|
65
73
|
return undefined;
|
|
66
74
|
if (memoryEngineRemoteLanePosture(config)?.posture === "dark")
|
|
67
75
|
return undefined;
|
|
68
|
-
const root =
|
|
76
|
+
const root = fileMemoryEngineRoot(config, fallbackRoot);
|
|
69
77
|
const dir = join(root, "memory");
|
|
70
78
|
return { backend: new FileMemoryEngineBackend(dir, { controlDir: deriveControlPlaneDir(root, dir) }), root };
|
|
71
79
|
}
|
|
@@ -235,7 +235,7 @@ export function createMetrics() {
|
|
|
235
235
|
m.counter("remote_env_resume_total", "E2B remote-env resumeVM outcomes (v1.5, provider/result)");
|
|
236
236
|
m.counter("remote_env_exec_stream_read_timeout_total", "E2B execStream idle read-timeouts (#1128, provider)");
|
|
237
237
|
m.counter("resume_anchor_capture_failed", "E18 resume-at per-turn anchor captures that failed (best-effort, run unaffected)");
|
|
238
|
-
m.counter("brain_retry_total", "Brain-layer status events (S4), by phase (rate_limited/retrying/reconnecting/circuit_open)");
|
|
238
|
+
m.counter("brain_retry_total", "Brain-layer status events (S4), by phase (rate_limited/retrying/reconnecting/circuit_open/waiting_first_token/recovered/gave_up)");
|
|
239
239
|
m.gauge("store_backend_degraded", "1 when the auto DB probe failed at boot and this replica degraded to in-memory (S5)");
|
|
240
240
|
m.counter("images_omitted_total", "Tasks whose images will be degraded to text placeholders (no-vision model), by model (S6)");
|
|
241
241
|
m.counter("runs_reaped_total", "Run rows flipped by the background reapers (S7), by kind (stale/suspended/expired_checkpoint)");
|
|
@@ -60,6 +60,10 @@ export declare function redactTerminalResult<T extends {
|
|
|
60
60
|
remoteEnvFailures?: readonly {
|
|
61
61
|
message?: string | undefined;
|
|
62
62
|
}[] | undefined;
|
|
63
|
+
/** 🔴 形上**故意**是 `unknown`:本函数也是**读面**(旧持久 blob / 非 core Runner 的形不可信),
|
|
64
|
+
* 声明成 `{path,edits}[]` 会逼每一个喂坏形的调用点铸一次宽松断言 —— 而「坏形从哪来」正是本座要
|
|
65
|
+
* 处理的事(同一份不信任在 `remoteEnvFailures` 的实现里也已成文)。运行期逐项判形,不抛。 */
|
|
66
|
+
editedFiles?: unknown;
|
|
63
67
|
} | null | undefined>(result: T): T;
|
|
64
68
|
/** S-107(第五轮合并重扫 wf_f00c00bc 两根 [high]):同一个 `TaskResult` 除 run 行外还被逐字追加进 run_events 的 `done` 行
|
|
65
69
|
* (bg/verify/cascade/stream/resume 五腿),`failed` 行带原文 `errorMessage`,`GET /v1/runs/:id/events` 原样回放。
|
|
@@ -74,6 +74,7 @@ export function redactErrorMessage(raw) {
|
|
|
74
74
|
return OVERSIZED_REDACTION_PLACEHOLDER;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
const MAX_EDITED_FILES = 1000;
|
|
77
78
|
export function redactTerminalResult(result) {
|
|
78
79
|
if (result === undefined || result === null)
|
|
79
80
|
return result;
|
|
@@ -94,6 +95,19 @@ export function redactTerminalResult(result) {
|
|
|
94
95
|
: r.remoteEnvFailures;
|
|
95
96
|
changed = true;
|
|
96
97
|
}
|
|
98
|
+
if (r.editedFiles !== undefined) {
|
|
99
|
+
out.editedFiles = Array.isArray(r.editedFiles)
|
|
100
|
+
? r.editedFiles
|
|
101
|
+
.slice(0, MAX_EDITED_FILES)
|
|
102
|
+
.map((f) => {
|
|
103
|
+
if (f === null || typeof f !== "object")
|
|
104
|
+
return f;
|
|
105
|
+
const path = f.path;
|
|
106
|
+
return typeof path === "string" ? { ...f, path: redactSecrets(path) } : f;
|
|
107
|
+
})
|
|
108
|
+
: r.editedFiles;
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
97
111
|
return changed ? out : result;
|
|
98
112
|
}
|
|
99
113
|
export function redactLedgerEventData(type, data) {
|
|
@@ -177,7 +191,7 @@ export const OVERSIZED_PATCH_PLACEHOLDER = "«redacted:oversized-patch-not-inlin
|
|
|
177
191
|
function diffScanRefusal(patch, direct) {
|
|
178
192
|
if (/^diff --(?:cc|combined) |^@{3,} /m.test(patch))
|
|
179
193
|
return "combined-diff";
|
|
180
|
-
const fence = /-{4,5} ?BEGIN[^\n-]*PRIVATE KEY(?: BLOCK)?[^\n-]*-{4,5}/;
|
|
194
|
+
const fence = /-{4,5} ?BEGIN(?:[^\n-]|-(?!-))*PRIVATE KEY(?: BLOCK)?(?:[^\n-]|-(?!-))*-{4,5}/;
|
|
181
195
|
if (fence.test(direct))
|
|
182
196
|
return "pem-fence-survived";
|
|
183
197
|
const ppkField = /Private-(?:MAC|Hash): [0-9a-fA-F]{32}/;
|
|
@@ -129,15 +129,39 @@ export declare class SqlDeviceStore implements DeviceStore {
|
|
|
129
129
|
heartbeatConnection(input: HeartbeatConnectionInput): Promise<boolean>;
|
|
130
130
|
releaseConnection(deviceId: string, generation: number): Promise<boolean>;
|
|
131
131
|
/**
|
|
132
|
-
* §4.3.2 首绑写协议 —— **单事务**:设备校验(active ∧ owner)
|
|
132
|
+
* §4.3.2 首绑写协议 —— **单事务**:设备校验(active ∧ owner)与首绑 CAS 插入在一个事务里。
|
|
133
133
|
*
|
|
134
|
-
* 🔴 `beginPessimistic` + `FOR UPDATE
|
|
135
|
-
*
|
|
136
|
-
* double-admit 形)
|
|
137
|
-
*
|
|
138
|
-
*
|
|
134
|
+
* 🔴 `beginPessimistic` + `FOR UPDATE`(设备行):设备行必须是**当前读**。快照读下,一次与吊销并发的
|
|
135
|
+
* 绑定会读到「还 active」的旧版本,于是把 session 绑到一台**刚被吊销**的设备上(sql-driver.ts 头注点名
|
|
136
|
+
* 的 double-admit 形)。这把锁在 `devices` 表上,与下面被插入的 `device_session` 行不是同一行。
|
|
137
|
+
*
|
|
138
|
+
* 🔴 形 B · cas-insert(S-127 / B-013)—— 本站点是全仓 `INSERT IGNORE` 的**正当用法**:
|
|
139
|
+
* `INSERT IGNORE` / `ON CONFLICT DO NOTHING` 的 affected(1/0)是引擎对「谁赢」的**权威**回答,而
|
|
140
|
+
* `ON DUPLICATE KEY UPDATE pk = pk` 的 affected 新行/既有行都是 1、分不出赢家 ⇒ 这里换不得动词。
|
|
141
|
+
* 代价必须付清:**同一事务此后不得对被插入的那一行做加锁读**。InnoDB 下撞重复键的 IGNORE 在既有行
|
|
142
|
+
* 取 **S 锁**,随后的 `SELECT … FOR UPDATE` 要升 X;两个并发首绑各持一把 S 互等 ⇒ ER_LOCK_DEADLOCK
|
|
143
|
+
* (TiDB 悲观事务无共享锁,这一形在它上面恒绿 —— 双库门看不见)。于是两条路径分家:
|
|
144
|
+
* · **赢家**(affected === 1):行是自己刚插的,回执用**入参 + 插入时刻**直接构造,零回读。插入时刻
|
|
145
|
+
* 仍由 **DB 时钟**给出(插入前一句 `SELECT NOW(3)` / `SELECT now()`,再把它当参数写进 INSERT)——
|
|
146
|
+
* 时间权威留在 DB,回执里的 `boundAtMs` 与落库值**逐字节同一个**,不是应用侧的近似。
|
|
147
|
+
* · **输家**(affected === 0):**结束当前事务**,在**新事务**里 `SELECT … FOR UPDATE` 观察。那一刻
|
|
148
|
+
* 赢家已提交,读到的是一把干净的 X 锁(无升级);判 同 deviceId = 幂等成功,异 deviceId =
|
|
149
|
+
* `conflict`(无未定义态)。设备状态门在输家路径上已经过完,不需要重来。
|
|
139
150
|
*/
|
|
140
151
|
bindSession(input: BindSessionInput): Promise<BindSessionResult>;
|
|
152
|
+
/**
|
|
153
|
+
* 首绑 CAS 的**输家观察**(形 B 的后半,S-127)—— 独立事务,与那次 `INSERT IGNORE` 无锁关系。
|
|
154
|
+
*
|
|
155
|
+
* 🔴 `FOR UPDATE`(**真库红修**,2026-08-28 双库跑当场抓到,mock 抓不到):TiDB 的悲观事务里**普通
|
|
156
|
+
* SELECT 仍是快照读**(读的是本事务 start_ts 的视图),只有加锁读才是当前读。放在新事务里读同样要
|
|
157
|
+
* 这把锁 —— 新事务的 start_ts 虽在赢家提交之后,但「读的是不是当前」这条判据不该靠时序巧合来满足。
|
|
158
|
+
* 与旧形的**唯一**差别:此刻我们手上没有那一行的 S 锁,于是这把 X 是**直接取**的,不是升级 ⇒ 两个
|
|
159
|
+
* 并发输家不会互等(B-013 的死锁形正是那次升级)。
|
|
160
|
+
*
|
|
161
|
+
* 行不在 = 赢家的绑定在这一瞬被 `unbindSession` 删了(真实可能,不是不变量破)。这仍然是**响亮**的:
|
|
162
|
+
* 首绑既没成、也说不出冲突对象,折成任何一种业务回执都是撒谎([ref]:未知失败不伪装成正常拒绝)。
|
|
163
|
+
*/
|
|
164
|
+
private observeLostBind;
|
|
141
165
|
/**
|
|
142
166
|
* O4 显式换绑(2026-08-30 v2 稿 §2)—— **单事务**:目标设备门(当前读)+ 幂等短路 + 前态 CAS +
|
|
143
167
|
* `session_rebound` 审计行一起提交。三形语义 oracle = `InMemoryDeviceStore.rebindSession`。
|
|
@@ -533,14 +533,33 @@ export class SqlDeviceStore {
|
|
|
533
533
|
await conn.rollback();
|
|
534
534
|
return { outcome: "device_revoked" };
|
|
535
535
|
}
|
|
536
|
-
const
|
|
536
|
+
const clock = await conn.query(this.q(`SELECT ${ms("tidb", NOW("tidb"), "bound_at_ms")}`, `SELECT ${ms("pg", NOW("pg"), "bound_at_ms")}`));
|
|
537
|
+
const boundAtMs = Number(clock.rows[0].bound_at_ms);
|
|
538
|
+
const ins = await conn.query(this.q(`INSERT IGNORE INTO ${DEVICE_SESSIONS_TABLE} (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) VALUES (?, ?, ?, ?, FROM_UNIXTIME(? / 1000), 0)`, `INSERT INTO ${DEVICE_SESSIONS_TABLE} (root_session_id, device_id, owner_tenant, owner_subject, bound_at, rev) VALUES ($1, $2, $3, $4, to_timestamp($5::double precision / 1000.0), 0) ON CONFLICT (root_session_id) DO NOTHING`), [input.rootSessionId, input.deviceId, input.owner.tenant, input.owner.subject, boundAtMs]);
|
|
539
|
+
if (ins.affected === 1) {
|
|
540
|
+
await conn.commit();
|
|
541
|
+
return { outcome: "bound", row: { rootSessionId: input.rootSessionId, deviceId: input.deviceId, owner: input.owner, boundAtMs, rev: 0 } };
|
|
542
|
+
}
|
|
543
|
+
await conn.rollback();
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
await conn.rollback().catch(() => undefined);
|
|
547
|
+
throw err;
|
|
548
|
+
}
|
|
549
|
+
finally {
|
|
550
|
+
conn.release();
|
|
551
|
+
}
|
|
552
|
+
return await this.observeLostBind(input);
|
|
553
|
+
}
|
|
554
|
+
async observeLostBind(input) {
|
|
555
|
+
const conn = await this.db.connect();
|
|
556
|
+
try {
|
|
557
|
+
await conn.beginPessimistic();
|
|
537
558
|
const { rows } = await conn.query(this.q(`SELECT ${sessionCols("tidb")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = ? FOR UPDATE`, `SELECT ${sessionCols("pg")} FROM ${DEVICE_SESSIONS_TABLE} WHERE root_session_id = $1 FOR UPDATE`), [input.rootSessionId]);
|
|
538
559
|
await conn.commit();
|
|
539
560
|
const row = rows[0] ? mapSessionRow(rows[0]) : null;
|
|
540
561
|
if (!row)
|
|
541
|
-
throw new Error("device-store: session binding vanished
|
|
542
|
-
if (ins.affected === 1)
|
|
543
|
-
return { outcome: "bound", row };
|
|
562
|
+
throw new Error("device-store: the winning session binding vanished before the losing binder could observe it");
|
|
544
563
|
const same = row.deviceId === input.deviceId && row.owner.tenant === input.owner.tenant && row.owner.subject === input.owner.subject;
|
|
545
564
|
return { outcome: same ? "idempotent" : "conflict", row };
|
|
546
565
|
}
|
|
@@ -61,6 +61,11 @@ export declare class SqlFileHistoryStore implements FileHistoryStore {
|
|
|
61
61
|
* row-locks the scope's sentinel row — one lock, both dialects, and disjoint-key writers can no
|
|
62
62
|
* longer interleave into a merged/torn graph. The ensure+lock loops because deleteBySession removes
|
|
63
63
|
* the sentinel: a waiter woken by that delete sees zero rows and must re-ensure.
|
|
64
|
+
*
|
|
65
|
+
* Shape A (S-127): the MySQL ensure verb is `ON DUPLICATE KEY UPDATE scope = scope`, NOT `INSERT IGNORE`.
|
|
66
|
+
* On InnoDB a duplicate-key IGNORE takes an **S** lock on the existing sentinel and the `FOR UPDATE` right
|
|
67
|
+
* below has to upgrade it to X — two concurrent scope writers each holding an S deadlock (B-013). ON
|
|
68
|
+
* DUPLICATE takes X on the duplicate key, the same mode the locking read wants, so they serialize instead.
|
|
64
69
|
*/
|
|
65
70
|
private lockScope;
|
|
66
71
|
/** Returns the UPDATE's affected count — 0 = the record is gone (annulled) or was already observed;
|
|
@@ -49,7 +49,7 @@ export class SqlFileHistoryStore {
|
|
|
49
49
|
for (const scope of [...new Set(scopes)].sort()) {
|
|
50
50
|
let locked = false;
|
|
51
51
|
for (let i = 0; i < 3 && !locked; i++) {
|
|
52
|
-
await conn.query(this.q("INSERT
|
|
52
|
+
await conn.query(this.q("INSERT INTO file_history_scope (scope, created_at_ms) VALUES (?,?) ON DUPLICATE KEY UPDATE scope = scope", "INSERT INTO file_history_scope (scope, created_at_ms) VALUES ($1,$2) ON CONFLICT (scope) DO NOTHING"), [scope, Date.now()]);
|
|
53
53
|
const r = await conn.query(this.q("SELECT scope FROM file_history_scope WHERE scope = ? FOR UPDATE", "SELECT scope FROM file_history_scope WHERE scope = $1 FOR UPDATE"), [scope]);
|
|
54
54
|
locked = Boolean(r.rows[0]);
|
|
55
55
|
}
|
|
@@ -358,15 +358,15 @@ export class SqlFileHistoryStore {
|
|
|
358
358
|
}
|
|
359
359
|
const seqRow = await conn.query(this.q("SELECT publish_seq FROM file_history_boundary WHERE scope = ? ORDER BY publish_seq DESC LIMIT 1 FOR UPDATE", "SELECT publish_seq FROM file_history_boundary WHERE scope = $1 ORDER BY publish_seq DESC LIMIT 1"), [scope]);
|
|
360
360
|
const publishSeq = (seqRow.rows[0] !== undefined ? Number(seqRow.rows[0].publish_seq) : 0) + 1;
|
|
361
|
-
const
|
|
362
|
-
if (
|
|
363
|
-
const won = await conn.query(this.q("SELECT 1 FROM file_history_boundary WHERE scope = ? AND entry_id = ? FOR UPDATE", "SELECT 1 FROM file_history_boundary WHERE scope = $1 AND entry_id = $2 FOR UPDATE"), [scope, entryId]);
|
|
364
|
-
if (!won.rows[0]) {
|
|
365
|
-
throw new Error(`boundary publish for entry "${entryId}" was suppressed without a same-entry winner (publish_seq allocation invariant breach) — refusing to report a boundary that was not stored`);
|
|
366
|
-
}
|
|
361
|
+
const priorWinner = await conn.query(this.q("SELECT 1 FROM file_history_boundary WHERE scope = ? AND entry_id = ? FOR UPDATE", "SELECT 1 FROM file_history_boundary WHERE scope = $1 AND entry_id = $2 FOR UPDATE"), [scope, entryId]);
|
|
362
|
+
if (priorWinner.rows[0]) {
|
|
367
363
|
await conn.rollback();
|
|
368
364
|
return { ok: true };
|
|
369
365
|
}
|
|
366
|
+
const ins = await conn.query(this.q("INSERT IGNORE INTO file_history_boundary (scope, entry_id, published_at_ms, publish_seq) VALUES (?,?,CAST(UNIX_TIMESTAMP(NOW(3))*1000 AS SIGNED),?)", "INSERT INTO file_history_boundary (scope, entry_id, published_at_ms, publish_seq) VALUES ($1,$2,(EXTRACT(EPOCH FROM clock_timestamp())*1000)::bigint,$3) ON CONFLICT (scope, entry_id) DO NOTHING"), [scope, entryId, publishSeq]);
|
|
367
|
+
if (ins.affected !== 1) {
|
|
368
|
+
throw new Error(`boundary publish for entry "${entryId}" was suppressed without a same-entry winner (publish_seq allocation invariant breach) — refusing to report a boundary that was not stored`);
|
|
369
|
+
}
|
|
370
370
|
for (const [keyHash, cap] of staged) {
|
|
371
371
|
let version;
|
|
372
372
|
if (cap.kind === "unchanged") {
|
|
@@ -140,7 +140,7 @@ export class SqlImageBake {
|
|
|
140
140
|
try {
|
|
141
141
|
if (this.db.dialect === "tidb") {
|
|
142
142
|
await conn.beginPessimistic();
|
|
143
|
-
await conn.query("INSERT
|
|
143
|
+
await conn.query("INSERT INTO image_bake_admit (pool) VALUES (?) ON DUPLICATE KEY UPDATE pool = pool", [this.poolName]);
|
|
144
144
|
await conn.query("SELECT pool FROM image_bake_admit WHERE pool = ? FOR UPDATE", [this.poolName]);
|
|
145
145
|
const busy = await conn.query("SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 LIMIT 1 FOR UPDATE");
|
|
146
146
|
if (busy.rows.length === 0) {
|
|
@@ -7,15 +7,19 @@
|
|
|
7
7
|
* 终版文本,conformance 无两可):
|
|
8
8
|
* 1. **同主重 claim = 续期 + 纳新**(crash-retry 正形):活 lease 只挡**别的** owner;同 owner 任意
|
|
9
9
|
* 时刻重 claim 重设 TTL 并拿到当前全部未 ack 消息(maxSeq 随最新行走)。
|
|
10
|
-
* 2. **seq = 盒生命周期内不复用**:box 行携 `next_seq`,分配与 append 同事务;`drop
|
|
11
|
-
*
|
|
10
|
+
* 2. **seq = 盒生命周期内不复用**:box 行携 `next_seq`,分配与 append 同事务;`drop` 删整盒(含 box 行)
|
|
11
|
+
* = 生命周期终结,重建从 1(core 定谳:行随盒亡,dedup 键携 handle)。⚠️ **`reap` 自 [ref](core
|
|
12
|
+
* 2026-07-25)起不再与 drop 同句**:reap 是收件人活着时的龄扫,**清箱保高水位**(删消息、清租约、盒行与
|
|
13
|
+
* next_seq 留着)——本文件 2026-09-03(车BF,codex r1 [high])才跟上;此前 reap 删盒行 ⇒ 下一条 append 从 1
|
|
14
|
+
* 重铸,陈旧 ack 可删掉一条没人见过的新消息。跨进程试剂盒明钉此条。
|
|
12
15
|
* 3. **ack 无 owner 守卫**(信任 claim 赢者链):`seq <= upToSeq` 删行;lease 的 `maxSeq <= upToSeq`
|
|
13
16
|
* 时顺带清 lease(全量 ack = 释放)。
|
|
14
17
|
* 4. **lease 过期判 = 严格 `expiresAt > now` 才挡**;`releaseLease` 只在 owner 匹配时清。
|
|
15
18
|
* 5. `peekCount` 含已 lease 消息(可见性 ≠ lease 态);空盒 claim = null(box 行在而消息 0 也 null)。
|
|
16
19
|
* 6. **reap = SQL 全量真扫**(core File 实现只扫已加载盒 = 进程内 backstop,[ref] 确认 SQL 是全量
|
|
17
|
-
* 真扫的正确层):`maxAgeMs` 缺省 → 0;按盒 newest `sentAt < now - maxAgeMs`
|
|
18
|
-
* box 行无消息)
|
|
20
|
+
* 真扫的正确层):`maxAgeMs` 缺省 → 0;按盒 newest `sentAt < now - maxAgeMs` **清箱**(删消息 + 清租约,
|
|
21
|
+
* 盒行/高水位保留,见定谳 2 的 [ref] 半句);空盒(有 box 行无消息)不清(InMemory 同形——`newest ===
|
|
22
|
+
* undefined` 不动)。
|
|
19
23
|
*
|
|
20
24
|
* 键列 = 字节等价(TiDB VARBINARY / PG COLLATE "C",roster F2 案):scope/handle/lease_owner 的
|
|
21
25
|
* `=` 必须纯字节——PAD SPACE 近撞不得穿隔离/抢别人 lease。PG unstorable bytes([ref] 三层定谳):
|
|
@@ -32,20 +36,36 @@
|
|
|
32
36
|
* - reap 候删 = 事务外粗筛 + 事务内**逐盒锁定重验**(当前读 newest):窗内新到的消息救活盒,
|
|
33
37
|
* 绝不连带删信;盒行已被并发 drop = 跳过不计。
|
|
34
38
|
* - append 的 ensure 与 FOR UPDATE 之间被并发 drop 提交 = 同事务内重建一次(语义 = drop 后重建,
|
|
35
|
-
* seq 从 1)
|
|
36
|
-
*
|
|
39
|
+
* seq 从 1)。建盒动词 = 形 A(S-127)`ON DUPLICATE KEY UPDATE <pk> = <pk>`:重复键取 X 锁,与随后的
|
|
40
|
+
* `FOR UPDATE` **同锁模式**(`INSERT IGNORE` 取 S 再升 X = InnoDB 上的并发死锁形,B-013)。
|
|
41
|
+
* - 键长 > 190 字节写入即拒(fail-loud):截断键与全长查询参数字节比对恒 miss(分配读空行崩/隔离键
|
|
42
|
+
* 错位)。守卫在**语句之前**,与建行动词无关 —— 换成 ON DUPLICATE 之后超长键由引擎直接报错(严格
|
|
43
|
+
* 模式下 IGNORE 会把它降级成静默截断),方向=更响亮,守卫依旧是第一道。
|
|
37
44
|
*/
|
|
38
45
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
39
46
|
import type { Pool as PgPool } from "pg";
|
|
40
|
-
import type { MailboxLease, MailboxStore } from "@sema-agent/core";
|
|
41
|
-
type MailboxAppendMessage = Parameters<MailboxStore["append"]>[2];
|
|
47
|
+
import type { MailboxAppendMessage, MailboxLease, MailboxStore } from "@sema-agent/core";
|
|
42
48
|
export declare const MAILBOX_TABLE = "mailbox";
|
|
43
49
|
export declare const MAILBOX_MSG_TABLE = "mailbox_message";
|
|
50
|
+
/** 店侧日志座(可选;boot 注入 `Logger`)。7.58.0 合并重扫 [medium]:坏 `peer_meta` 行的 fail-closed 抛必须**响亮**——core 唯一的
|
|
51
|
+
* claimLease 调用方(peer-session-drain)吞掉该错,店不记这一笔就成了「静默永久卡箱」。抛之前记 `mailbox_peer_meta_corrupt`
|
|
52
|
+
* (scope/handle/seq/code),运维按 seq 修行。 */
|
|
53
|
+
export interface MailboxStoreLogger {
|
|
54
|
+
warn(event: string, fields: Record<string, unknown>): void;
|
|
55
|
+
}
|
|
44
56
|
export declare function ensureTiDBMailboxSchema(pool: MySqlPool): Promise<void>;
|
|
45
57
|
export declare function ensurePgMailboxSchema(q: (text: string, params?: unknown[]) => Promise<unknown>): Promise<void>;
|
|
46
58
|
export declare class TiDBMailboxStore implements MailboxStore {
|
|
47
59
|
private readonly pool;
|
|
48
|
-
|
|
60
|
+
private readonly logger?;
|
|
61
|
+
/** [ref]([ref] §1.2⑥):跨进程安全**显式**声明 —— core 的 `mailboxCrossProcessMountVerdict` 只对字面 `true` 放行
|
|
62
|
+
* peer lane(缺席/false/畸形一律响亮拒挂 `config.peer_lane_unmounted`)。真值前提 = 本文件头注「并发正确性」段:seq
|
|
63
|
+
* 在事务内经盒行 FOR UPDATE 单铸、lease/ack/drop/reap 统一盒行锁序、reap 事务内逐盒当前读重验 —— 多副本(=多 OS
|
|
64
|
+
* 进程)共享一个盒本就是 SQL 双生的设计象限。**终验** = core 跨进程试剂盒 `mailboxCrossProcessContract` 跑在真第二个
|
|
65
|
+
* OS 进程上:`test/mailbox-cross-process-kit.test.ts`(file 腿恒跑验 harness;tidb/pg 腿 ENV-GATED,发车前双库门必跑,
|
|
66
|
+
* 红即摘本声明——core 契约逐字「declares true AFTER passing」,声明不许先于试剂盒长期悬空)。 */
|
|
67
|
+
readonly crossProcessSafe = true;
|
|
68
|
+
constructor(pool: MySqlPool, logger?: MailboxStoreLogger | undefined);
|
|
49
69
|
private tx;
|
|
50
70
|
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
51
71
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
@@ -59,7 +79,10 @@ export declare class TiDBMailboxStore implements MailboxStore {
|
|
|
59
79
|
}
|
|
60
80
|
export declare class PgMailboxStore implements MailboxStore {
|
|
61
81
|
private readonly pool;
|
|
62
|
-
|
|
82
|
+
private readonly logger?;
|
|
83
|
+
/** [ref] 跨进程安全显式声明(TiDB 孪生同注:事务 + 盒行 FOR UPDATE 锁序是真值前提;终验 = test/mailbox-cross-process-kit.test.ts pg 腿)。 */
|
|
84
|
+
readonly crossProcessSafe = true;
|
|
85
|
+
constructor(pool: PgPool, logger?: MailboxStoreLogger | undefined);
|
|
63
86
|
private tx;
|
|
64
87
|
/** 身份键拒绝式([ref] 协议纪律):scope/handle/owner/from 清洗形变 = 路由/attribution 错位。 */
|
|
65
88
|
private assertIdentity;
|
|
@@ -73,5 +96,4 @@ export declare class PgMailboxStore implements MailboxStore {
|
|
|
73
96
|
maxAgeMs?: number;
|
|
74
97
|
}): Promise<number>;
|
|
75
98
|
}
|
|
76
|
-
export {};
|
|
77
99
|
//# sourceMappingURL=mailbox-store-sql.d.ts.map
|