@sema-agent/server 7.100.0 → 7.101.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 +22 -1
- package/README.md +12 -1
- package/README.zh-CN.md +9 -1
- package/USAGE.md +80 -9
- package/dist/approval-card.d.ts +24 -12
- package/dist/approval-card.js +4 -3
- package/dist/approval-policy-settlement.d.ts +31 -0
- package/dist/approval-policy-settlement.js +16 -0
- package/dist/approval-reconciler.d.ts +8 -1
- package/dist/approval-reconciler.js +5 -4
- package/dist/boot/config-center.js +86 -20
- package/dist/boot/execution-env.js +20 -2
- package/dist/boot/parked-revive-gate.js +1 -0
- package/dist/boot/reapers.js +1 -0
- package/dist/boot/resolve-spec.js +25 -8
- package/dist/boot/runner-deps.js +9 -3
- package/dist/boot/stage-02-registry-pull.js +9 -0
- package/dist/boot/stores.js +4 -0
- package/dist/config-catalog.js +5 -3
- package/dist/config-center/apply-effective.d.ts +25 -0
- package/dist/config-center/apply-effective.js +144 -69
- package/dist/config-center/apply-ledger.d.ts +12 -2
- package/dist/config-center/apply-ledger.js +4 -1
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/http-client.d.ts +4 -1
- package/dist/config-center/http-client.js +15 -10
- package/dist/config-center/read-warnings.d.ts +115 -6
- package/dist/config-center/read-warnings.js +115 -5
- package/dist/config-center/types.d.ts +6 -0
- package/dist/config-invariants.d.ts +2 -2
- package/dist/config-invariants.js +15 -0
- package/dist/config-provider.d.ts +4 -0
- package/dist/config-provider.js +64 -9
- package/dist/config-types.d.ts +37 -8
- package/dist/config.js +87 -22
- package/dist/deployment-governance.d.ts +45 -12
- package/dist/deployment-governance.js +37 -9
- package/dist/env-name-allowlist-knobs.d.ts +37 -0
- package/dist/env-name-allowlist-knobs.js +24 -0
- package/dist/fleet-client.d.ts +2 -1
- package/dist/hooks/hook-llm.js +13 -4
- package/dist/host-lsp-manager.d.ts +14 -2
- package/dist/host-lsp-manager.js +7 -6
- package/dist/http/admission.js +22 -7
- package/dist/http/memory-store-refusal-reply.d.ts +24 -0
- package/dist/http/memory-store-refusal-reply.js +10 -0
- package/dist/http/routes/approvals-assistant.js +1 -0
- package/dist/http/routes/capabilities.js +3 -3
- package/dist/http/routes/memory-bundle.js +3 -0
- package/dist/http/routes/memory-compliance.js +3 -5
- package/dist/http/routes/memory-origin.js +3 -5
- package/dist/http/routes/sessions.js +7 -6
- package/dist/http/routes/side-query.js +10 -4
- package/dist/instruction-source-notice.d.ts +97 -0
- package/dist/instruction-source-notice.js +78 -0
- package/dist/memory-layer-legacy-lock.d.ts +14 -0
- package/dist/memory-layer-legacy-lock.js +74 -0
- package/dist/memory-operator-faces.js +1 -17
- package/dist/memory-store-refusal.d.ts +37 -0
- package/dist/memory-store-refusal.js +14 -0
- package/dist/model-entry-refusal.d.ts +130 -0
- package/dist/model-entry-refusal.js +157 -0
- package/dist/model-provider.d.ts +9 -3
- package/dist/model-route-endpoint.d.ts +1 -1
- package/dist/model-select.d.ts +49 -4
- package/dist/model-select.js +48 -17
- package/dist/observability/fail-open.d.ts +9 -1
- package/dist/observability/fail-open.js +9 -1
- package/dist/observability/metrics.js +1 -0
- package/dist/observability/secret-env-scrub.d.ts +27 -4
- package/dist/observability/secret-env-scrub.js +22 -7
- package/dist/plugins/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/local-session-store.d.ts +20 -25
- package/dist/plugins/local-session-store.js +111 -64
- package/dist/plugins/pg-session-storage.d.ts +7 -0
- package/dist/plugins/pg-session-storage.js +19 -0
- package/dist/plugins/remote-env-host.d.ts +3 -1
- package/dist/plugins/remote-env-host.js +3 -6
- package/dist/plugins/tidb-pool.js +5 -0
- package/dist/plugins/tidb-session-store.d.ts +13 -0
- package/dist/plugins/tidb-session-store.js +14 -0
- package/dist/plugins/web-search.d.ts +2 -1
- package/dist/plugins/web-search.js +43 -24
- package/dist/project-memory.d.ts +10 -0
- package/dist/project-memory.js +28 -14
- package/dist/run-local.d.ts +17 -15
- package/dist/run-local.js +115 -15
- package/dist/sealed-key.d.ts +5 -2
- package/dist/server-secret-env.d.ts +23 -1
- package/dist/server-secret-env.js +9 -0
- package/dist/tool-approval.d.ts +65 -31
- package/dist/tool-approval.js +96 -49
- package/dist/trace/core-keyset-guard.d.ts +27 -5
- package/dist/trace/engine-notice-wire.d.ts +27 -1
- package/dist/trace/engine-notice-wire.js +8 -0
- package/dist/trace/wire-projection-faces.d.ts +2 -2
- package/dist/trace/wire-projection-faces.js +2 -2
- package/package.json +2 -2
- package/dist/memory-layer-preflight.d.ts +0 -46
- package/dist/memory-layer-preflight.js +0 -121
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
import type { ServerSecretEnvFinding } from "../server-secret-env.js";
|
|
2
2
|
import type { Logger } from "./logger.js";
|
|
3
3
|
import type { Metrics } from "./metrics.js";
|
|
4
|
+
/**
|
|
5
|
+
* 一个剥除现场的登记:`note` = 这条腿上泄漏意味着什么;`remedy` = 部署方能不能自己把某个被剥的键放回来。
|
|
6
|
+
*
|
|
7
|
+
* 🔴 S-699:`remedy` 在场与否决定披露行的**级别**,规则一句 —— **有出路的现场用 warn,并把出路句与当前放行名单写进同一行**;
|
|
8
|
+
* 没有出路的现场是纯保护留痕,照旧 info。理由:没有出路的剥除(模型可驱动的 shell、git 钩子)是本服务替部署方做的决定,
|
|
9
|
+
* 读者只需要知道它生效了;有出路的剥除(语言服务器)可能让一件功能静默变差(补全 / 诊断缺了私有 registry 的依赖),
|
|
10
|
+
* 那是部署方要自己决定的事 —— 看不见 = 无从归因。出路句不带任何值。
|
|
11
|
+
*/
|
|
12
|
+
export interface SecretEnvScrubSiteSpec {
|
|
13
|
+
readonly note: string;
|
|
14
|
+
readonly remedy: string | null;
|
|
15
|
+
}
|
|
4
16
|
/** 登记过的剥除现场。闭集:未登记的 site 传不进 {@link recordSecretEnvScrub}(编译期拒,同 FAIL_OPEN_TAGS 纪律)。 */
|
|
5
17
|
export declare const SECRET_ENV_SCRUB_SITES: {
|
|
6
|
-
readonly "server.remote-env-host.inherit-scrub":
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
readonly "server.remote-env-host.inherit-scrub": {
|
|
19
|
+
readonly note: "模型可驱动的 host shell 继承 env(`inheritEnv:\"scrub\"` 默认腿)——这条腿上的一次泄漏等于把编排器的脑钥/仓库令牌交给模型选的命令";
|
|
20
|
+
readonly remedy: null;
|
|
21
|
+
};
|
|
22
|
+
readonly "server.project-memory.git-subprocess": {
|
|
23
|
+
readonly note: "project-memory 读仓叙事时的 git 子进程 env —— 仓内的钩子/凭证助手会以本进程身份运行";
|
|
24
|
+
readonly remedy: null;
|
|
25
|
+
};
|
|
26
|
+
readonly "server.lsp.spawn-env": {
|
|
27
|
+
readonly note: "宿主车道本地语言服务器子进程 env(core 7.30.1 #1073 的 `NodeLspManagerOptions.env`)—— 语言服务器会执行仓里的代码(构建脚本/插件/工作区配置),一次泄漏等于把本进程的凭据交给仓";
|
|
28
|
+
readonly remedy: `LSP_ENV_ALLOW=NAME,NAME \u70B9\u540D\u653E\u884C(\u539F\u6837\u4EA4\u7ED9\u8BED\u8A00\u670D\u52A1\u5668,\u90E8\u7F72\u65B9\u663E\u5F0F\u627F\u62C5;${string});\u503C\u4E0D\u8FDB\u65E5\u5FD7`;
|
|
29
|
+
};
|
|
9
30
|
};
|
|
10
31
|
/** 词表键推导的闭集类型。 */
|
|
11
32
|
export type SecretEnvScrubSite = keyof typeof SECRET_ENV_SCRUB_SITES;
|
|
@@ -16,8 +37,10 @@ interface Sink {
|
|
|
16
37
|
/**
|
|
17
38
|
* 记一次剥除。**总不抛** —— 观测本身绝不能变成故障(同 `recordFailOpen` / reapers 的判据)。
|
|
18
39
|
* `findings` 为空(这一次没丢任何 key)时什么也不做:那不是事件,是常态。
|
|
40
|
+
* `findings` 只许装**最终没进子进程**的键 —— 点名放回的键不是剥除(不计数、不披露)。
|
|
41
|
+
* `allowed` = 这一次生效的点名放行名单(S-699),只在有出路的现场(`remedy` 在场)写进披露行;没有放行面的腿传 `[]`。
|
|
19
42
|
*/
|
|
20
|
-
export declare function recordSecretEnvScrub(site: SecretEnvScrubSite, findings: readonly ServerSecretEnvFinding[]): void;
|
|
43
|
+
export declare function recordSecretEnvScrub(site: SecretEnvScrubSite, findings: readonly ServerSecretEnvFinding[], allowed: readonly string[]): void;
|
|
21
44
|
/** 装配层把 logger/metrics 接上(main.ts,与 `installFailOpenRecorder` 同一处)。装配前累计的次数在此补进 metrics。 */
|
|
22
45
|
export declare function installSecretEnvScrubRecorder(deps: Sink): void;
|
|
23
46
|
/** 已留驻的「现场×键」条数(测试/诊断读面)。恒 ≤ {@link DISCLOSED_CAP}。 */
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { summarizeRedactions } from "@sema-agent/core";
|
|
2
|
+
import { describeAllowlistPostures } from "../env-name-allowlist-knobs.js";
|
|
2
3
|
export const SECRET_ENV_SCRUB_SITES = {
|
|
3
|
-
"server.remote-env-host.inherit-scrub":
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
"server.remote-env-host.inherit-scrub": {
|
|
5
|
+
note: "模型可驱动的 host shell 继承 env(`inheritEnv:\"scrub\"` 默认腿)——这条腿上的一次泄漏等于把编排器的脑钥/仓库令牌交给模型选的命令",
|
|
6
|
+
remedy: null,
|
|
7
|
+
},
|
|
8
|
+
"server.project-memory.git-subprocess": {
|
|
9
|
+
note: "project-memory 读仓叙事时的 git 子进程 env —— 仓内的钩子/凭证助手会以本进程身份运行",
|
|
10
|
+
remedy: null,
|
|
11
|
+
},
|
|
12
|
+
"server.lsp.spawn-env": {
|
|
13
|
+
note: "宿主车道本地语言服务器子进程 env(core 7.30.1 #1073 的 `NodeLspManagerOptions.env`)—— 语言服务器会执行仓里的代码(构建脚本/插件/工作区配置),一次泄漏等于把本进程的凭据交给仓",
|
|
14
|
+
remedy: `LSP_ENV_ALLOW=NAME,NAME 点名放行(原样交给语言服务器,部署方显式承担;${describeAllowlistPostures()});值不进日志`,
|
|
15
|
+
},
|
|
6
16
|
};
|
|
7
17
|
const SCRUB_COUNTER = "secret_env_scrubbed_total";
|
|
8
18
|
const DISCLOSED_CAP = 512;
|
|
@@ -31,7 +41,7 @@ function drainToMetrics() {
|
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
}
|
|
34
|
-
export function recordSecretEnvScrub(site, findings) {
|
|
44
|
+
export function recordSecretEnvScrub(site, findings, allowed) {
|
|
35
45
|
if (findings.length === 0)
|
|
36
46
|
return;
|
|
37
47
|
try {
|
|
@@ -49,12 +59,17 @@ export function recordSecretEnvScrub(site, findings) {
|
|
|
49
59
|
const fresh = findings.filter((f) => !disclosed.has(`${site}${SEP}${f.key}`)).slice(0, room);
|
|
50
60
|
if (fresh.length === 0)
|
|
51
61
|
return;
|
|
52
|
-
|
|
62
|
+
const spec = SECRET_ENV_SCRUB_SITES[site];
|
|
63
|
+
const fields = {
|
|
53
64
|
site,
|
|
54
|
-
note:
|
|
65
|
+
note: spec.note,
|
|
55
66
|
keys: fresh.map((f) => f.key),
|
|
56
67
|
summary: summarizeRedactions(fresh),
|
|
57
|
-
}
|
|
68
|
+
};
|
|
69
|
+
if (spec.remedy === null)
|
|
70
|
+
logger.info("secret_env_scrubbed", fields);
|
|
71
|
+
else
|
|
72
|
+
logger.warn("secret_env_scrubbed", { ...fields, allowed: [...allowed], hint: spec.remedy });
|
|
58
73
|
for (const f of fresh)
|
|
59
74
|
disclosed.add(`${site}${SEP}${f.key}`);
|
|
60
75
|
}
|
|
@@ -2,6 +2,7 @@ import type { AcquiredSession, PlacedSessionRow, RetentionDeclaration, SessionPl
|
|
|
2
2
|
import type { SessionListItem } from "../security.js";
|
|
3
3
|
import type { StagingHandle } from "../session-sync-kernel.js";
|
|
4
4
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
5
|
+
import type { InstructionSourceStore } from "../instruction-source-notice.js";
|
|
5
6
|
export interface CachingOptions {
|
|
6
7
|
/** Idle seconds a woken session is kept warm. 0 disables caching (pure passthrough). */
|
|
7
8
|
ttlSec: number;
|
|
@@ -64,6 +65,11 @@ export declare class CachingSessionStore implements SessionStore {
|
|
|
64
65
|
/** Write-once auto-title (SQL twins + local sidecar carry it; pure pass-through). */
|
|
65
66
|
readonly setTitleIfNull?: (sessionId: string, title: string) => Promise<boolean>;
|
|
66
67
|
readonly probeTitle?: (sessionId: string) => Promise<"none" | "untitled" | "titled">;
|
|
68
|
+
/** S-690 —— 换源通告的基线面(`session_meta.instruction_source` / local sidecar),纯转发、不经暖缓存。
|
|
69
|
+
* 🔴 必须转发(与 retention / placements 同一条教训):默认 `SESSION_CACHE_TTL_SEC=300` 把真店包进本类,
|
|
70
|
+
* 而装配点按「店上两个方法都在」判席 —— 不转发 = 默认部署上换源通告**静默不发**,且没有任何一处报错。 */
|
|
71
|
+
readonly readInstructionSource?: InstructionSourceStore["readInstructionSource"];
|
|
72
|
+
readonly recordInstructionSource?: InstructionSourceStore["recordInstructionSource"];
|
|
67
73
|
/** E18 — delegate the leaf read straight to inner (a cache-bypassing single SELECT; the resume-at turn-capture
|
|
68
74
|
* reads the DURABLE leaf, not a possibly-stale cached view — same discipline as the §0.5 repo ops above). */
|
|
69
75
|
readonly getLeafId?: (sessionId: string) => Promise<string | null>;
|
|
@@ -13,6 +13,8 @@ export class CachingSessionStore {
|
|
|
13
13
|
noteTaskRun;
|
|
14
14
|
setTitleIfNull;
|
|
15
15
|
probeTitle;
|
|
16
|
+
readInstructionSource;
|
|
17
|
+
recordInstructionSource;
|
|
16
18
|
getLeafId;
|
|
17
19
|
getHead;
|
|
18
20
|
exportEntries;
|
|
@@ -51,6 +53,10 @@ export class CachingSessionStore {
|
|
|
51
53
|
this.setTitleIfNull = ownerAware.setTitleIfNull.bind(inner);
|
|
52
54
|
if (ownerAware.probeTitle)
|
|
53
55
|
this.probeTitle = ownerAware.probeTitle.bind(inner);
|
|
56
|
+
if (ownerAware.readInstructionSource)
|
|
57
|
+
this.readInstructionSource = ownerAware.readInstructionSource.bind(inner);
|
|
58
|
+
if (ownerAware.recordInstructionSource)
|
|
59
|
+
this.recordInstructionSource = ownerAware.recordInstructionSource.bind(inner);
|
|
54
60
|
if (ownerAware.fork)
|
|
55
61
|
this.fork = ownerAware.fork.bind(inner);
|
|
56
62
|
if (ownerAware.noteTaskRun)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AcquiredSession, type SessionStore, type SessionRepo, type SessionTreeEntry } from "@sema-agent/core";
|
|
2
2
|
import type { SessionSummary } from "./store-contracts.js";
|
|
3
|
+
import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
3
4
|
import { type StagingHandle } from "../session-sync-kernel.js";
|
|
4
5
|
export declare class LocalSessionStore implements SessionStore {
|
|
5
6
|
private readonly repo;
|
|
@@ -50,36 +51,19 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
50
51
|
private lastWarnAt;
|
|
51
52
|
/** Absolute path to the lastActivity JSON sidecar, or undefined when not persisting (in-memory-only deployment). */
|
|
52
53
|
private readonly sidecarPath?;
|
|
53
|
-
/** Auto-title map
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
* (`test/file-stores-cross-instance-concurrency.test.ts`) showed **BOTH winning** the write-once IS-NULL gate
|
|
62
|
-
* (`wins=2`): each checked its own map, both saw absent, both returned `true`, and the second whole-map
|
|
63
|
-
* snapshot silently clobbered the first title. That is core's A-class (CAS double-win) and B-class
|
|
64
|
-
* (lost update) stacked. Fix per core's prescription: state + lock live at MODULE level, keyed by the resolved
|
|
65
|
-
* sidecar path, so any number of instances over one dataDir collapse to a single authority.
|
|
66
|
-
* Sharing key = the resolved path; NO sidecar (in-memory deployment) ⇒ no shared backend exists ⇒ that case
|
|
67
|
-
* keeps genuinely per-instance state (see {@link titleState}). */
|
|
68
|
-
private readonly titleSidecarPath?;
|
|
69
|
-
/** Per-instance fallback used ONLY when there is no sidecar path (nothing is shared, so nothing to collapse). */
|
|
70
|
-
private readonly ownTitleState;
|
|
54
|
+
/** Auto-title map (`session-titles.json`). Separate file from the lastActivity sidecar ON PURPOSE (that format is a
|
|
55
|
+
* #10 clobber area); titles are write-once + ultra-low-rate (one per session, ever). Cross-instance sharing: see
|
|
56
|
+
* {@link SidecarStringMap}. */
|
|
57
|
+
private readonly titles;
|
|
58
|
+
/** S-690 — instruction-source baselines (`session-instruction-sources.json`): the chain member the session's previous
|
|
59
|
+
* run selected, encoded like the SQL twins' `session_meta.instruction_source` column ('' = none; missing key = never
|
|
60
|
+
* recorded). Its own file for the same reason titles have theirs: one small map per concern, no format coupling. */
|
|
61
|
+
private readonly instructionSources;
|
|
71
62
|
/** @param repo the core SessionRepo this store projects into the §0.5 SessionStore shape.
|
|
72
63
|
* @param dataDir the SAME data dir the file-backed repo persists under (LocalBackend passes `fileBackend.root`).
|
|
73
64
|
* When given, lastActivity is persisted to `<dataDir>/session-last-activity.json` so it survives a
|
|
74
65
|
* restart (the file-backed deployment); omit it for the in-memory deployment (nothing survives). */
|
|
75
66
|
constructor(repo: SessionRepo, dataDir?: string);
|
|
76
|
-
/** 该实例应当使用的 title 状态:有 sidecar ⇒ 按路径共享的那份;无 sidecar ⇒ 自己那份(无共享后端)。 */
|
|
77
|
-
private get titleState();
|
|
78
|
-
/** Lazy one-shot hydrate of the persisted titles (mirrors ensureLoaded's posture: missing file silent, torn warn). */
|
|
79
|
-
private ensureTitlesLoaded;
|
|
80
|
-
/** Chained snapshot write of the titles sidecar (the chain guarantees the LAST rename carries the
|
|
81
|
-
* NEWEST map state: each link re-snapshots the SHARED map at write time, so an overtaken writer is harmless). */
|
|
82
|
-
private persistTitles;
|
|
83
67
|
/** Cheap pre-LLM probe (local twin). The map write needs no session_meta row, so there is
|
|
84
68
|
* no "none" (register-lag) dimension here — two states only. */
|
|
85
69
|
probeTitle(sessionId: string): Promise<"none" | "untitled" | "titled">;
|
|
@@ -87,6 +71,15 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
87
71
|
* (small) map via a SERIALIZED tmp+rename snapshot; a write failure keeps the in-memory title (degrades to
|
|
88
72
|
* re-title after restart). */
|
|
89
73
|
setTitleIfNull(sessionId: string, title: string): Promise<boolean>;
|
|
74
|
+
/** S-690 —— 换源通告的基线读(local 孪生;三态读法与 SQL 两孪生的 `session_meta.instruction_source` 同一只编解码)。 */
|
|
75
|
+
readInstructionSource(sessionId: string): Promise<InstructionSourceBaseline>;
|
|
76
|
+
/**
|
|
77
|
+
* S-690 —— CAS 覆写基线(契约见 `InstructionSourceStore`:期望值不符 ⇒ 不写;**没落盘 ⇒ 回滚内存再抛**,与 SQL 孪生
|
|
78
|
+
* 「要么提交要么抛」同形 —— 修前写失败只 warn、内存照样前进,同来源的后续 run 在相等比较上短路、再也不重写,重启后
|
|
79
|
+
* 比的是一份过期的盘,codex che-NM r1 [medium])。CAS 判定、落盘、失败回滚**整段在写链的一节里**(`commit`):下一节
|
|
80
|
+
* 开始之前内存已回到盘上那份,判定永远不会建立在一个没落过盘的值上(codex che-NM r2 [medium])。
|
|
81
|
+
*/
|
|
82
|
+
recordInstructionSource(sessionId: string, expected: InstructionSourceBaseline, source: string | null): Promise<void>;
|
|
90
83
|
/** Best-effort, one-shot hydrate of the persisted lastActivity map (no-op when not persisting / no sidecar yet). A
|
|
91
84
|
* read/parse error degrades to an empty map (createdAt ordering) rather than failing the list — the picker still
|
|
92
85
|
* works, just without restart-persisted recency. Memoized so concurrent listSessions share one read. */
|
|
@@ -121,6 +114,8 @@ export declare class LocalSessionStore implements SessionStore {
|
|
|
121
114
|
* ENOENT(首次启动没有这份档)在调用点就被排除,压根到不了这里。
|
|
122
115
|
*/
|
|
123
116
|
private refuseSidecar;
|
|
117
|
+
/** `which` 点名是哪一只边档(S-690 起有三只 —— 此前这句恒写 lastActivity,title 边档的失败也被报成「recency 降级」)。
|
|
118
|
+
* 节流仍是本实例一只。 */
|
|
124
119
|
private warnSidecar;
|
|
125
120
|
acquire(sessionId?: string, opts?: {
|
|
126
121
|
requireExisting?: boolean;
|
|
@@ -5,6 +5,7 @@ import { contentForkRelation, fastForwardSharedContentDiverged, identicalIdsAlso
|
|
|
5
5
|
import { quarantineUnreadableSidecar } from "./local-sidecar-quarantine.js";
|
|
6
6
|
import { decodeJsonColumn, readStringRecordColumn } from "./sql-json-column.js";
|
|
7
7
|
import { UNMANAGED_RETENTION } from "./retention-store-sql.js";
|
|
8
|
+
import { decodeInstructionSource, encodeInstructionSource, sameInstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
8
9
|
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, } from "../session-sync-kernel.js";
|
|
9
10
|
const ownerEq = (a, b) => (a ?? null) === (b ?? null);
|
|
10
11
|
const isNotFound = (e) => e instanceof SessionError && e.code === "not_found";
|
|
@@ -27,87 +28,131 @@ function userText(entry) {
|
|
|
27
28
|
}
|
|
28
29
|
return null;
|
|
29
30
|
}
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
get titleState() {
|
|
57
|
-
return this.titleSidecarPath ? TITLE_STATE_BY_PATH.get(this.titleSidecarPath) : this.ownTitleState;
|
|
58
|
-
}
|
|
59
|
-
async ensureTitlesLoaded() {
|
|
60
|
-
if (!this.titleSidecarPath)
|
|
61
|
-
return;
|
|
62
|
-
const st = this.titleState;
|
|
31
|
+
const SIDECAR_DEGRADES = { lastActivity: "recency", title: "auto-title", "instruction-source": "instruction-source baseline" };
|
|
32
|
+
const SIDECAR_MAP_STATE_BY_PATH = new Map();
|
|
33
|
+
class SidecarStringMap {
|
|
34
|
+
path;
|
|
35
|
+
label;
|
|
36
|
+
onUnreadable;
|
|
37
|
+
onWriteFailed;
|
|
38
|
+
own = { entries: new Map(), writeChain: Promise.resolve() };
|
|
39
|
+
constructor(path, label, onUnreadable, onWriteFailed) {
|
|
40
|
+
this.path = path;
|
|
41
|
+
this.label = label;
|
|
42
|
+
this.onUnreadable = onUnreadable;
|
|
43
|
+
this.onWriteFailed = onWriteFailed;
|
|
44
|
+
if (path && !SIDECAR_MAP_STATE_BY_PATH.has(path))
|
|
45
|
+
SIDECAR_MAP_STATE_BY_PATH.set(path, { entries: new Map(), writeChain: Promise.resolve() });
|
|
46
|
+
}
|
|
47
|
+
get state() {
|
|
48
|
+
return this.path ? SIDECAR_MAP_STATE_BY_PATH.get(this.path) : this.own;
|
|
49
|
+
}
|
|
50
|
+
ensureLoaded() {
|
|
51
|
+
const path = this.path;
|
|
52
|
+
if (!path)
|
|
53
|
+
return Promise.resolve();
|
|
54
|
+
const st = this.state;
|
|
63
55
|
if (!st.loaded) {
|
|
64
|
-
const path = this.titleSidecarPath;
|
|
65
56
|
st.loaded = (async () => {
|
|
66
57
|
try {
|
|
67
|
-
const parsed = decodeJsonColumn(await readFile(path, "utf8"), readStringRecordColumn,
|
|
68
|
-
for (const [id,
|
|
69
|
-
if (!st.
|
|
70
|
-
st.
|
|
58
|
+
const parsed = decodeJsonColumn(await readFile(path, "utf8"), readStringRecordColumn, this.label);
|
|
59
|
+
for (const [id, v] of Object.entries(parsed)) {
|
|
60
|
+
if (!st.entries.has(id))
|
|
61
|
+
st.entries.set(id, v);
|
|
71
62
|
}
|
|
72
63
|
}
|
|
73
64
|
catch (e) {
|
|
74
65
|
if (e?.code !== "ENOENT")
|
|
75
|
-
this.
|
|
66
|
+
this.onUnreadable(path, e);
|
|
76
67
|
}
|
|
77
68
|
})();
|
|
78
69
|
}
|
|
79
70
|
return st.loaded;
|
|
80
71
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
st
|
|
72
|
+
persist() {
|
|
73
|
+
return this.commit(() => () => undefined);
|
|
74
|
+
}
|
|
75
|
+
commit(change) {
|
|
76
|
+
const path = this.path;
|
|
77
|
+
const st = this.state;
|
|
78
|
+
const link = st.writeChain.then(async () => {
|
|
79
|
+
const undo = change(st.entries);
|
|
80
|
+
if (undo === undefined || !path)
|
|
81
|
+
return true;
|
|
87
82
|
try {
|
|
88
83
|
const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
|
|
89
|
-
await writeFile(tmp, JSON.stringify(Object.fromEntries(st.
|
|
84
|
+
await writeFile(tmp, JSON.stringify(Object.fromEntries(st.entries)), "utf8");
|
|
90
85
|
await rename(tmp, path);
|
|
86
|
+
return true;
|
|
91
87
|
}
|
|
92
88
|
catch (e) {
|
|
93
|
-
|
|
89
|
+
undo();
|
|
90
|
+
this.onWriteFailed(e);
|
|
91
|
+
return false;
|
|
94
92
|
}
|
|
95
93
|
});
|
|
96
|
-
|
|
94
|
+
st.writeChain = link.then(() => undefined);
|
|
95
|
+
return link;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export class LocalSessionStore {
|
|
99
|
+
repo;
|
|
100
|
+
retention = UNMANAGED_RETENTION;
|
|
101
|
+
pending = new Map();
|
|
102
|
+
owners = new Map();
|
|
103
|
+
lastActivity = new Map();
|
|
104
|
+
lastTaskRun = new Map();
|
|
105
|
+
stagings = new Map();
|
|
106
|
+
loaded;
|
|
107
|
+
flushTimer;
|
|
108
|
+
writeChain = Promise.resolve();
|
|
109
|
+
dirty = false;
|
|
110
|
+
writeSeq = 0;
|
|
111
|
+
lastWarnAt = 0;
|
|
112
|
+
sidecarPath;
|
|
113
|
+
titles;
|
|
114
|
+
instructionSources;
|
|
115
|
+
constructor(repo, dataDir) {
|
|
116
|
+
this.repo = repo;
|
|
117
|
+
this.sidecarPath = dataDir ? join(dataDir, "session-last-activity.json") : undefined;
|
|
118
|
+
const sidecar = (file, which) => new SidecarStringMap(dataDir ? resolve(join(dataDir, file)) : undefined, `${file} (the local ${which} sidecar)`, (path, e) => this.refuseSidecar(path, `LocalSessionStore ${which} sidecar`, e, which), (e) => this.warnSidecar("write", e, which));
|
|
119
|
+
this.titles = sidecar("session-titles.json", "title");
|
|
120
|
+
this.instructionSources = sidecar("session-instruction-sources.json", "instruction-source");
|
|
97
121
|
}
|
|
98
122
|
async probeTitle(sessionId) {
|
|
99
|
-
await this.
|
|
100
|
-
return this.
|
|
123
|
+
await this.titles.ensureLoaded();
|
|
124
|
+
return this.titles.state.entries.has(sessionId) ? "titled" : "untitled";
|
|
101
125
|
}
|
|
102
126
|
async setTitleIfNull(sessionId, title) {
|
|
103
|
-
await this.
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
127
|
+
await this.titles.ensureLoaded();
|
|
128
|
+
const entries = this.titles.state.entries;
|
|
129
|
+
if (entries.has(sessionId))
|
|
106
130
|
return false;
|
|
107
|
-
|
|
108
|
-
await this.
|
|
131
|
+
entries.set(sessionId, title);
|
|
132
|
+
await this.titles.persist();
|
|
109
133
|
return true;
|
|
110
134
|
}
|
|
135
|
+
async readInstructionSource(sessionId) {
|
|
136
|
+
await this.instructionSources.ensureLoaded();
|
|
137
|
+
return decodeInstructionSource(this.instructionSources.state.entries.get(sessionId));
|
|
138
|
+
}
|
|
139
|
+
async recordInstructionSource(sessionId, expected, source) {
|
|
140
|
+
await this.instructionSources.ensureLoaded();
|
|
141
|
+
const committed = await this.instructionSources.commit((entries) => {
|
|
142
|
+
const before = entries.get(sessionId);
|
|
143
|
+
if (!sameInstructionSourceBaseline(decodeInstructionSource(before), expected))
|
|
144
|
+
return undefined;
|
|
145
|
+
entries.set(sessionId, encodeInstructionSource(source));
|
|
146
|
+
return () => {
|
|
147
|
+
if (before === undefined)
|
|
148
|
+
entries.delete(sessionId);
|
|
149
|
+
else
|
|
150
|
+
entries.set(sessionId, before);
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
if (!committed)
|
|
154
|
+
throw new Error("LocalSessionStore: instruction-source sidecar write failed — baseline not recorded");
|
|
155
|
+
}
|
|
111
156
|
async ensureLoaded() {
|
|
112
157
|
if (!this.sidecarPath)
|
|
113
158
|
return;
|
|
@@ -165,16 +210,16 @@ export class LocalSessionStore {
|
|
|
165
210
|
if (this.dirty)
|
|
166
211
|
await this.doFlush();
|
|
167
212
|
}
|
|
168
|
-
refuseSidecar(path, what, e) {
|
|
213
|
+
refuseSidecar(path, what, e, which = "lastActivity") {
|
|
169
214
|
quarantineUnreadableSidecar(path, what, e instanceof Error ? e.message : String(e));
|
|
170
|
-
this.warnSidecar("read", e);
|
|
215
|
+
this.warnSidecar("read", e, which);
|
|
171
216
|
}
|
|
172
|
-
warnSidecar(op, e) {
|
|
217
|
+
warnSidecar(op, e, which = "lastActivity") {
|
|
173
218
|
const now = Date.now();
|
|
174
219
|
if (now - this.lastWarnAt < 60_000)
|
|
175
220
|
return;
|
|
176
221
|
this.lastWarnAt = now;
|
|
177
|
-
console.warn(`LocalSessionStore:
|
|
222
|
+
console.warn(`LocalSessionStore: ${which} sidecar ${op} failed (${SIDECAR_DEGRADES[which]} persistence degraded): ${e instanceof Error ? e.message : String(e)}`);
|
|
178
223
|
}
|
|
179
224
|
async acquire(sessionId, opts) {
|
|
180
225
|
if (!sessionId) {
|
|
@@ -271,7 +316,7 @@ export class LocalSessionStore {
|
|
|
271
316
|
}
|
|
272
317
|
async listSessions(opts) {
|
|
273
318
|
await this.ensureLoaded();
|
|
274
|
-
await this.
|
|
319
|
+
await this.titles.ensureLoaded();
|
|
275
320
|
const metas = await this.repo.list();
|
|
276
321
|
const all = [];
|
|
277
322
|
for (const meta of metas) {
|
|
@@ -291,7 +336,7 @@ export class LocalSessionStore {
|
|
|
291
336
|
lastStatus: "",
|
|
292
337
|
lastRunId: lastRun?.taskId ?? null,
|
|
293
338
|
...(lastRun?.runId !== undefined ? { lastTaskRunId: lastRun.runId } : {}),
|
|
294
|
-
title: this.
|
|
339
|
+
title: this.titles.state.entries.get(meta.id) ?? null,
|
|
295
340
|
});
|
|
296
341
|
}
|
|
297
342
|
const qlc = opts.q?.toLowerCase();
|
|
@@ -404,9 +449,11 @@ export class LocalSessionStore {
|
|
|
404
449
|
this.owners.delete(sessionId);
|
|
405
450
|
this.lastActivity.delete(sessionId);
|
|
406
451
|
this.lastTaskRun.delete(sessionId);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
452
|
+
for (const map of [this.titles, this.instructionSources]) {
|
|
453
|
+
await map.ensureLoaded();
|
|
454
|
+
if (map.state.entries.delete(sessionId))
|
|
455
|
+
await map.persist();
|
|
456
|
+
}
|
|
410
457
|
this.scheduleFlush();
|
|
411
458
|
return true;
|
|
412
459
|
}
|
|
@@ -3,6 +3,7 @@ import type { Pool, PoolClient } from "pg";
|
|
|
3
3
|
import type { SessionSummary } from "./store-contracts.js";
|
|
4
4
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
5
5
|
import { type StagingHandle } from "../session-sync-kernel.js";
|
|
6
|
+
import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
6
7
|
import { type IndexSpec } from "./ensure-index.js";
|
|
7
8
|
/**
|
|
8
9
|
* PG-dialect DDL for the L1 session log, translated from tidb-pool.ts SCHEMA_STATEMENTS:
|
|
@@ -113,6 +114,12 @@ export declare class PgSessionStore implements SessionStore {
|
|
|
113
114
|
probeTitle(sessionId: string): Promise<"none" | "untitled" | "titled">;
|
|
114
115
|
/** @see TiDBSessionStore.setTitleIfNull — the PG twin (write-once auto-title). */
|
|
115
116
|
setTitleIfNull(sessionId: string, title: string): Promise<boolean>;
|
|
117
|
+
/** @see TiDBSessionStore.readInstructionSource — the PG twin. */
|
|
118
|
+
readInstructionSource(sessionId: string): Promise<InstructionSourceBaseline>;
|
|
119
|
+
/** @see TiDBSessionStore.recordInstructionSource — the PG twin (same CAS predicate, same per-query timeout; node-pg's
|
|
120
|
+
* `query_timeout` rejects AND pg-pool destroys that client, so a lock-stuck baseline write does not keep a pooled
|
|
121
|
+
* connection). */
|
|
122
|
+
recordInstructionSource(sessionId: string, expected: InstructionSourceBaseline, source: string | null): Promise<void>;
|
|
116
123
|
/** @see TiDBSessionStore.fork — copy the WHOLE durable history (full session_event log, compaction floor bypassed,
|
|
117
124
|
* NOT wake's bounded tail) to a NEW uuidv7 session, re-seq dense from 0, ids/parents verbatim, owner=caller. */
|
|
118
125
|
fork(sourceId: string, owner: string | null): Promise<string | null>;
|
|
@@ -9,9 +9,13 @@ import { assertPlacementAdmission, LIST_PLACED_BATCH, PLACEMENT_SELECT_COLUMNS,
|
|
|
9
9
|
import { contentForkRelation, fastForwardSharedContentDiverged } from "../session-sync-content.js";
|
|
10
10
|
import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, STAGING_ID_MARKER, STAGING_GC_GRACE_MS, } from "../session-sync-kernel.js";
|
|
11
11
|
import { isPgUniqueViolation } from "./sql-errors.js";
|
|
12
|
+
import { BASELINE_IO_DEADLINE_MS, decodeInstructionSource, encodeInstructionSource } from "../instruction-source-notice.js";
|
|
12
13
|
import { MANAGED_RETENTION } from "./retention-store-sql.js";
|
|
13
14
|
import { ensureIndexes } from "./ensure-index.js";
|
|
14
15
|
const isDupKey = isPgUniqueViolation;
|
|
16
|
+
function baselineQuery(text, values) {
|
|
17
|
+
return { text, values, query_timeout: BASELINE_IO_DEADLINE_MS };
|
|
18
|
+
}
|
|
15
19
|
export const PG_SESSION_SCHEMA = [
|
|
16
20
|
`CREATE TABLE IF NOT EXISTS session_meta (
|
|
17
21
|
session_id VARCHAR(64) COLLATE "C" NOT NULL,
|
|
@@ -23,6 +27,8 @@ export const PG_SESSION_SCHEMA = [
|
|
|
23
27
|
-- Write-once auto-title (setTitleIfNull / probeTitle): NULL = "untitled", i.e. still claimable; the
|
|
24
28
|
-- three-state probe distinguishes it from "no such session". PG twin of the TiDB COLUMN_MIGRATIONS leg.
|
|
25
29
|
title VARCHAR(120) COLLATE "C",
|
|
30
|
+
-- instruction_source(S-690):换源通告的基线 —— TiDB 孪生同名同宽,三态读法(NULL / '' / 链成员相对路径)见那里的列注。
|
|
31
|
+
instruction_source VARCHAR(64) COLLATE "C",
|
|
26
32
|
-- #292 C18(core 5.48.0 design/323)subagent 转录持久化 —— placement 元组(TiDB 孪生同名同宽,
|
|
27
33
|
-- 语义与六条义务见 src/plugins/session-placement.ts 顶注):创建时一次落行、first-write 不可变;
|
|
28
34
|
-- placement_kind 在场 ⇒ 住在 placement 分区(不上宿主枚举 / claim 形 acquire 恒拒 / release=真删除 /
|
|
@@ -348,6 +354,19 @@ export class PgSessionStore {
|
|
|
348
354
|
const res = await this.pool.query("UPDATE session_meta SET title = $1 WHERE session_id = $2 AND title IS NULL", [title, sessionId]);
|
|
349
355
|
return (res.rowCount ?? 0) > 0;
|
|
350
356
|
}
|
|
357
|
+
async readInstructionSource(sessionId) {
|
|
358
|
+
const res = await this.pool.query(baselineQuery("SELECT instruction_source FROM session_meta WHERE session_id = $1", [sessionId]));
|
|
359
|
+
return decodeInstructionSource(res.rows[0]?.instruction_source);
|
|
360
|
+
}
|
|
361
|
+
async recordInstructionSource(sessionId, expected, source) {
|
|
362
|
+
const next = encodeInstructionSource(source);
|
|
363
|
+
if (expected.recorded) {
|
|
364
|
+
await this.pool.query(baselineQuery("UPDATE session_meta SET instruction_source = $1 WHERE session_id = $2 AND instruction_source = $3", [next, sessionId, encodeInstructionSource(expected.source)]));
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
await this.pool.query(baselineQuery("UPDATE session_meta SET instruction_source = $1 WHERE session_id = $2 AND instruction_source IS NULL", [next, sessionId]));
|
|
368
|
+
}
|
|
369
|
+
}
|
|
351
370
|
async fork(sourceId, owner) {
|
|
352
371
|
const metaRes = await this.pool.query("SELECT leaf_id FROM session_meta WHERE session_id = $1", [sourceId]);
|
|
353
372
|
if (!metaRes.rows[0])
|
|
@@ -37,7 +37,9 @@ export interface HostEnvConfig {
|
|
|
37
37
|
* `SERVICE_AUTH_TOKENS` / `APPROVAL_HMAC_KEYS` slip the shape rule)
|
|
38
38
|
* so a model-driven shell can't `printenv DEEPSEEK_API_KEY` and exfiltrate orchestrator secrets — keeps PATH/HOME/
|
|
39
39
|
* locale/build (low breakage). `"all"` = inherit everything (explicit opt-out, trusted local dev). A `string[]` =
|
|
40
|
-
* strict allowlist
|
|
40
|
+
* strict (hermetic) allowlist via `allowlistedEnv` — the repo's one "named keys pass verbatim" predicate, shared with
|
|
41
|
+
* the local language server's `LSP_ENV_ALLOW` (S-699; that leg puts named keys back ON TOP of the scrub instead).
|
|
42
|
+
* `env`/per-command `options.env` ALWAYS pass through verbatim. */
|
|
41
43
|
inheritEnv?: "scrub" | "all" | string[];
|
|
42
44
|
/** @deprecated use {@link inheritEnv}. `true`→`"all"`, `false`→`[]` (hermetic). Ignored if `inheritEnv` is set. */
|
|
43
45
|
inheritProcessEnv?: boolean;
|
|
@@ -11,7 +11,7 @@ import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeS
|
|
|
11
11
|
import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
|
|
12
12
|
import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
|
|
13
13
|
import { recordSecretEnvScrub } from "../observability/secret-env-scrub.js";
|
|
14
|
-
import { scrubServerSecrets } from "../server-secret-env.js";
|
|
14
|
+
import { allowlistedEnv, scrubServerSecrets } from "../server-secret-env.js";
|
|
15
15
|
import { trackEphemeralHostWorkspace, untrackEphemeralHostWorkspace } from "./host-workspace-registry.js";
|
|
16
16
|
const PROVIDER = "host";
|
|
17
17
|
const DEFAULT_COMMAND_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -1306,15 +1306,12 @@ export class RemoteHostExecutionEnv {
|
|
|
1306
1306
|
if (mode === "all")
|
|
1307
1307
|
base = { ...process.env };
|
|
1308
1308
|
else if (Array.isArray(mode)) {
|
|
1309
|
-
base =
|
|
1310
|
-
for (const k of mode)
|
|
1311
|
-
if (process.env[k] !== undefined)
|
|
1312
|
-
base[k] = process.env[k];
|
|
1309
|
+
base = allowlistedEnv(mode, process.env);
|
|
1313
1310
|
}
|
|
1314
1311
|
else {
|
|
1315
1312
|
const findings = [];
|
|
1316
1313
|
base = scrubServerSecrets(process.env, findings);
|
|
1317
|
-
recordSecretEnvScrub("server.remote-env-host.inherit-scrub", findings);
|
|
1314
|
+
recordSecretEnvScrub("server.remote-env-host.inherit-scrub", findings, []);
|
|
1318
1315
|
}
|
|
1319
1316
|
if (this.cfg.env)
|
|
1320
1317
|
Object.assign(base, this.cfg.env);
|
|
@@ -58,6 +58,11 @@ export const SCHEMA_STATEMENTS = [
|
|
|
58
58
|
-- title: auto-generated session title (cheap-model one-liner at first submit; write-once via
|
|
59
59
|
-- setTitleIfNull — the user-facing RENAME layer lives BFF-side as a label overlay, not here).
|
|
60
60
|
title VARCHAR(120) NULL,
|
|
61
|
+
-- instruction_source(S-690):本会话**上一次 run** 选中的项目指令文件 = 换源通告(instructions.source_changed)
|
|
62
|
+
-- 的基线。NULL = 还没有任何 run 记过(首 run 只写不发);'' = 记过、那一次没有项目指令文件;其余 = 链成员相对路径
|
|
63
|
+
-- (CLAUDE.md / .claude/CLAUDE.md / AGENTS.md / .claude/AGENTS.md)。服务端元数据:不进模型上下文、不进 wire
|
|
64
|
+
-- 会话面;住在会话行上 ⇒ 随会话删除 / 留存清扫一起消失,fork 出的新行为 NULL(新会话首 run 同样只写不发)。
|
|
65
|
+
instruction_source VARCHAR(64) NULL,
|
|
61
66
|
-- #292 C18(core 5.48.0 design/323)subagent 转录持久化 —— placement 元组,**创建时一次落行、
|
|
62
67
|
-- first-write 不可变**(register 的 ON DUPLICATE 臂永不重写这六列)。判别位 = placement_kind:
|
|
63
68
|
-- 在场 ⇒ 该会话住在 placement 分区里(不上宿主枚举、claim 形 acquire 恒拒、release=真删除、
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { type AcquiredSession, type PlacedSessionRow, type SessionPlacement, type SessionPlacementRecord, type SessionStore, type SessionTreeEntry } from "@sema-agent/core";
|
|
14
14
|
import type { Pool } from "mysql2/promise";
|
|
15
|
+
import { type InstructionSourceBaseline } from "../instruction-source-notice.js";
|
|
15
16
|
import type { SessionSummary } from "./store-contracts.js";
|
|
16
17
|
import { type PlacementAcquireOpts } from "./session-placement.js";
|
|
17
18
|
import { type StagingHandle } from "../session-sync-kernel.js";
|
|
@@ -115,6 +116,18 @@ export declare class TiDBSessionStore implements SessionStore {
|
|
|
115
116
|
/** Write-once auto-title: lands ONLY when title IS NULL (a concurrent/prior title wins;
|
|
116
117
|
* the user-facing rename layer is BFF-side). Returns whether this call set it. */
|
|
117
118
|
setTitleIfNull(sessionId: string, title: string): Promise<boolean>;
|
|
119
|
+
/**
|
|
120
|
+
* S-690 —— 换源通告的基线读(一行 PK SELECT;列的三态读法见 `tidb-pool.ts` 的列注)。行不在 ⇒ 读作「没记过」
|
|
121
|
+
* (loader 跑在 acquire 之后,行恒在;不在 = 被带外删了,没有下一 run 可比)。
|
|
122
|
+
*/
|
|
123
|
+
readInstructionSource(sessionId: string): Promise<InstructionSourceBaseline>;
|
|
124
|
+
/**
|
|
125
|
+
* S-690 —— CAS 覆写基线(期望值 = 调用方读到的那一份;不符 ⇒ 0 行、不抛 —— 契约见 `InstructionSourceStore`)。
|
|
126
|
+
* **不**碰 `updated_at`(那是会话活动时刻,留存腿按它判龄;记基线不是一次会话活动)。
|
|
127
|
+
* 两条基线查询都带每查询超时(`BASELINE_IO_DEADLINE_MS`,mysql2 的 `{sql, timeout}` 形):被行锁卡住的写由查询自己
|
|
128
|
+
* 在期限内拒。⚠️ mysql2 的超时是客户端侧的 —— 拒之后那条连接仍被挂着的命令占到服务端结束(S-9 同形,如实披露)。
|
|
129
|
+
*/
|
|
130
|
+
recordInstructionSource(sessionId: string, expected: InstructionSourceBaseline, source: string | null): Promise<void>;
|
|
118
131
|
/**
|
|
119
132
|
* E17 — fork a session: copy its WHOLE durable history to a NEW uuidv7 session owned by `owner`, mirroring
|
|
120
133
|
* core's `PgSessionRepo.fork`. Entry ids / parent links are per-session and copy VERBATIM; only `seq` is
|