@sema-agent/server 7.99.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 +35 -1
- package/README.md +12 -1
- package/README.zh-CN.md +9 -1
- package/USAGE.md +150 -56
- package/dist/approval-ask-machine.d.ts +10 -0
- package/dist/approval-ask-machine.js +3 -0
- package/dist/approval-card.d.ts +86 -1
- package/dist/approval-card.js +42 -8
- 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/approval.d.ts +44 -20
- package/dist/approval.js +14 -6
- package/dist/boot/config-center.d.ts +16 -8
- package/dist/boot/config-center.js +114 -90
- package/dist/boot/execution-env.d.ts +1 -2
- package/dist/boot/execution-env.js +21 -3
- package/dist/boot/leader.js +12 -9
- package/dist/boot/memory-consolidation.d.ts +38 -0
- package/dist/boot/memory-consolidation.js +35 -0
- package/dist/boot/parked-revive-gate.js +1 -0
- package/dist/boot/reapers.js +1 -0
- package/dist/boot/resolve-spec.d.ts +0 -1
- package/dist/boot/resolve-spec.js +28 -10
- package/dist/boot/runner-deps.d.ts +2 -2
- package/dist/boot/runner-deps.js +9 -3
- package/dist/boot/stage-02-registry-pull.js +9 -0
- package/dist/boot/stage-05-execution-env.d.ts +1 -1
- package/dist/boot/stage-06-runners.d.ts +2 -2
- package/dist/boot/stage-06-runners.js +3 -3
- package/dist/boot/stage-07-capability-layer.d.ts +2 -3
- package/dist/boot/stage-07-capability-layer.js +6 -6
- package/dist/boot/stage-08-reapers.d.ts +3 -4
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +2 -3
- package/dist/boot/stage-10-http-server.d.ts +2 -3
- package/dist/boot/stage-10-http-server.js +2 -2
- package/dist/boot/stores.js +4 -0
- package/dist/capabilities/center-plugins.d.ts +14 -2
- package/dist/capabilities/center-plugins.js +13 -5
- package/dist/capabilities/hands-lane.js +2 -1
- package/dist/capabilities/scenarios.js +18 -5
- package/dist/capabilities/tool-defer.d.ts +2 -2
- package/dist/config-catalog.d.ts +2 -2
- package/dist/config-catalog.js +16 -15
- package/dist/config-center/apply-effective.d.ts +59 -39
- package/dist/config-center/apply-effective.js +231 -120
- package/dist/config-center/apply-ledger.d.ts +45 -16
- package/dist/config-center/apply-ledger.js +28 -18
- package/dist/config-center/facade.d.ts +12 -6
- package/dist/config-center/facade.js +2 -2
- package/dist/config-center/hot-keys-registry.d.ts +11 -4
- package/dist/config-center/hot-keys-registry.js +11 -7
- 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/restart-signal.d.ts +21 -14
- package/dist/config-center/restart-signal.js +16 -22
- package/dist/config-center/types.d.ts +9 -3
- 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 +57 -13
- package/dist/config.js +87 -22
- package/dist/cross-session-settings.js +1 -1
- 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 +41 -0
- package/dist/host-lsp-manager.js +15 -0
- package/dist/http/active-run-conflict.d.ts +2 -12
- package/dist/http/active-run-conflict.js +1 -6
- package/dist/http/admission.js +30 -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/resume-legs.d.ts +1 -1
- package/dist/http/resume-legs.js +14 -10
- package/dist/http/route-ctx.d.ts +2 -2
- package/dist/http/routes/approvals-assistant.d.ts +1 -1
- package/dist/http/routes/approvals-assistant.js +3 -2
- 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 -0
- package/dist/http/routes/memory-origin.js +3 -0
- package/dist/http/routes/sessions.js +7 -0
- package/dist/http/routes/side-query.js +10 -4
- package/dist/http/routes/tasks.js +7 -6
- package/dist/http/server.d.ts +6 -5
- package/dist/http/wire-types.d.ts +6 -3
- package/dist/instruction-source-notice.d.ts +97 -0
- package/dist/instruction-source-notice.js +78 -0
- package/dist/leader/wire.d.ts +20 -12
- package/dist/leader/wire.js +7 -6
- package/dist/memory-layer-legacy-lock.d.ts +14 -0
- package/dist/memory-layer-legacy-lock.js +74 -0
- 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/corrupt-read-seat.d.ts +21 -4
- package/dist/observability/corrupt-read-seat.js +31 -0
- package/dist/observability/fail-open.d.ts +16 -4
- package/dist/observability/fail-open.js +16 -4
- package/dist/observability/metrics.js +1 -0
- package/dist/observability/run-terminal.js +1 -0
- package/dist/observability/secret-env-scrub.d.ts +27 -3
- package/dist/observability/secret-env-scrub.js +22 -6
- package/dist/orchestration/workflow-completion-inbox.d.ts +31 -23
- package/dist/orchestration/workflow-completion-inbox.js +8 -3
- package/dist/plugins/approval-ask-store-file.d.ts +20 -2
- package/dist/plugins/approval-ask-store-file.js +21 -3
- package/dist/plugins/approval-ask-store-memory.d.ts +2 -0
- package/dist/plugins/approval-ask-store-memory.js +39 -13
- package/dist/plugins/approval-ask-store-sql.d.ts +24 -5
- package/dist/plugins/approval-ask-store-sql.js +27 -3
- package/dist/plugins/caching-session-store.d.ts +6 -0
- package/dist/plugins/caching-session-store.js +6 -0
- package/dist/plugins/checkpoint-store-sql.d.ts +9 -3
- package/dist/plugins/checkpoint-store-sql.js +19 -2
- package/dist/plugins/local-checkpoint-store.js +2 -0
- package/dist/plugins/local-session-store.d.ts +20 -25
- package/dist/plugins/local-session-store.js +111 -64
- package/dist/plugins/mailbox-store-sql.d.ts +7 -5
- package/dist/plugins/mailbox-store-sql.js +6 -4
- package/dist/plugins/permission-rule-store-file.js +3 -2
- 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/sql-json-column.d.ts +16 -3
- package/dist/plugins/sql-json-column.js +15 -4
- 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 +44 -23
- package/dist/plugins/web-search.js +161 -50
- 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 +119 -18
- package/dist/runs.js +6 -5
- package/dist/runtime-governance.d.ts +50 -3
- package/dist/runtime-governance.js +5 -0
- package/dist/sealed-key.d.ts +5 -2
- package/dist/server-secret-env.d.ts +26 -2
- package/dist/server-secret-env.js +9 -0
- package/dist/task-settings.d.ts +1 -1
- package/dist/tool-approval.d.ts +150 -28
- package/dist/tool-approval.js +150 -54
- package/dist/trace/core-keyset-guard.d.ts +30 -8
- package/dist/trace/engine-notice-wire.d.ts +27 -1
- package/dist/trace/engine-notice-wire.js +8 -0
- package/dist/trace/ledger-sink.js +2 -2
- package/dist/trace/project.d.ts +13 -0
- package/dist/trace/project.js +22 -5
- package/dist/trace/projection-drop.d.ts +2 -0
- package/dist/trace/projection-drop.js +6 -0
- package/dist/trace/sema-provenance.d.ts +4 -1
- package/dist/trace/sema-provenance.js +1 -1
- package/dist/trace/task-notification-facets.d.ts +65 -0
- package/dist/trace/task-notification-facets.js +31 -0
- package/dist/trace/wire-projection-faces.d.ts +3 -3
- package/dist/trace/wire-projection-faces.js +3 -3
- package/package.json +3 -3
|
@@ -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
|
}
|
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
* 2026-07-25)起不再与 drop 同句**:reap 是收件人活着时的龄扫,**清箱保高水位**(删消息、清租约、盒行与
|
|
13
13
|
* next_seq 留着)——本文件 2026-09-03(车BF,codex r1 [high])才跟上;此前 reap 删盒行 ⇒ 下一条 append 从 1
|
|
14
14
|
* 重铸,陈旧 ack 可删掉一条没人见过的新消息。跨进程试剂盒明钉此条。
|
|
15
|
-
* 3. **ack
|
|
16
|
-
* 时顺带清 lease(全量 ack = 释放)
|
|
15
|
+
* 3. **ack 带 owner 围栏**(MB1,core 1.452 起;原「无守卫、信任 claim 赢者链」已被推翻):当前持租者才删
|
|
16
|
+
* `seq <= upToSeq` 的行;lease 的 `maxSeq <= upToSeq` 时顺带清 lease(全量 ack = 释放)。**围栏的判决报出来**
|
|
17
|
+
* (core 7.30.0 #1036,S-616):放行 ⇒ `{ acked: true }`(删 0 行也是 true —— 契约是「围栏放行」不是「删了几行」),
|
|
18
|
+
* 盒不在 / 无人持租 / 非持租者 ⇒ `{ acked: false }`;逐格与 core `InMemoryMailboxStore.ack` 同答。
|
|
17
19
|
* 4. **lease 过期判 = 严格 `expiresAt > now` 才挡**;`releaseLease` 只在 owner 匹配时清。
|
|
18
20
|
* 5. `peekCount` 含已 lease 消息(可见性 ≠ lease 态);空盒 claim = null(box 行在而消息 0 也 null)。
|
|
19
21
|
* 6. **reap = SQL 全量真扫**(core File 实现只扫已加载盒 = 进程内 backstop,[ref] 确认 SQL 是全量
|
|
@@ -44,7 +46,7 @@
|
|
|
44
46
|
*/
|
|
45
47
|
import type { Pool as MySqlPool } from "mysql2/promise";
|
|
46
48
|
import type { Pool as PgPool } from "pg";
|
|
47
|
-
import type { MailboxAppendMessage, MailboxLease, MailboxStore } from "@sema-agent/core";
|
|
49
|
+
import type { MailboxAckResult, MailboxAppendMessage, MailboxLease, MailboxStore } from "@sema-agent/core";
|
|
48
50
|
import { type IndexSpec } from "./ensure-index.js";
|
|
49
51
|
import type { MailboxRecipientLifecycle, MailboxRecipientTombstone } from "./mailbox-recipient-lifecycle.js";
|
|
50
52
|
export declare const MAILBOX_TABLE = "mailbox";
|
|
@@ -82,7 +84,7 @@ export declare class TiDBMailboxStore implements MailboxStore, MailboxRecipientL
|
|
|
82
84
|
private tx;
|
|
83
85
|
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
84
86
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
85
|
-
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<
|
|
87
|
+
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<MailboxAckResult>;
|
|
86
88
|
releaseLease(scope: string, handle: string, owner: string): Promise<void>;
|
|
87
89
|
peekCount(scope: string, handle: string): Promise<number>;
|
|
88
90
|
drop(scope: string, handle: string): Promise<void>;
|
|
@@ -108,7 +110,7 @@ export declare class PgMailboxStore implements MailboxStore, MailboxRecipientLif
|
|
|
108
110
|
private assertIdentity;
|
|
109
111
|
append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
|
|
110
112
|
claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
|
|
111
|
-
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<
|
|
113
|
+
ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<MailboxAckResult>;
|
|
112
114
|
releaseLease(scope: string, handle: string, owner: string): Promise<void>;
|
|
113
115
|
peekCount(scope: string, handle: string): Promise<number>;
|
|
114
116
|
drop(scope: string, handle: string): Promise<void>;
|
|
@@ -263,13 +263,14 @@ export class TiDBMailboxStore {
|
|
|
263
263
|
});
|
|
264
264
|
}
|
|
265
265
|
async ack(scope, handle, owner, upToSeq) {
|
|
266
|
-
|
|
266
|
+
return this.tx(async (c) => {
|
|
267
267
|
const [rows] = (await c.query(`SELECT lease_owner FROM ${MAILBOX_TABLE} WHERE scope_key = ? AND handle = ? FOR UPDATE`, [scope, handle]));
|
|
268
268
|
if (rows.length === 0 || rows[0].lease_owner == null || bufToStr(rows[0].lease_owner) !== owner)
|
|
269
|
-
return;
|
|
269
|
+
return { acked: false };
|
|
270
270
|
await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = ? AND handle = ? AND seq <= ?`, [scope, handle, upToSeq]);
|
|
271
271
|
await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL
|
|
272
272
|
WHERE scope_key = ? AND handle = ? AND lease_max_seq IS NOT NULL AND lease_max_seq <= ?`, [scope, handle, upToSeq]);
|
|
273
|
+
return { acked: true };
|
|
273
274
|
});
|
|
274
275
|
}
|
|
275
276
|
async releaseLease(scope, handle, owner) {
|
|
@@ -410,13 +411,14 @@ export class PgMailboxStore {
|
|
|
410
411
|
});
|
|
411
412
|
}
|
|
412
413
|
async ack(scope, handle, owner, upToSeq) {
|
|
413
|
-
|
|
414
|
+
return this.tx(async (c) => {
|
|
414
415
|
const r = (await c.query(`SELECT lease_owner FROM ${MAILBOX_TABLE} WHERE scope_key = $1 AND handle = $2 FOR UPDATE`, [scope, handle]));
|
|
415
416
|
if (r.rows.length === 0 || r.rows[0].lease_owner == null || bufToStr(r.rows[0].lease_owner) !== owner)
|
|
416
|
-
return;
|
|
417
|
+
return { acked: false };
|
|
417
418
|
await c.query(`DELETE FROM ${MAILBOX_MSG_TABLE} WHERE scope_key = $1 AND handle = $2 AND seq <= $3`, [scope, handle, upToSeq]);
|
|
418
419
|
await c.query(`UPDATE ${MAILBOX_TABLE} SET lease_owner = NULL, lease_expires_at_ms = NULL, lease_max_seq = NULL
|
|
419
420
|
WHERE scope_key = $1 AND handle = $2 AND lease_max_seq IS NOT NULL AND lease_max_seq <= $3`, [scope, handle, upToSeq]);
|
|
421
|
+
return { acked: true };
|
|
420
422
|
});
|
|
421
423
|
}
|
|
422
424
|
async releaseLease(scope, handle, owner) {
|
|
@@ -5,6 +5,7 @@ import { performance } from "node:perf_hooks";
|
|
|
5
5
|
import { FileDurableRulePartitionProvider, AppendLog, ensureDir, readJsonlRecords, } from "@sema-agent/core";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { buildRulePayloadHash } from "./permission-rule-store-sql.js";
|
|
8
|
+
import { createSkippedRecordSink } from "../observability/corrupt-read-seat.js";
|
|
8
9
|
import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
|
|
9
10
|
import { RULE_BEHAVIORS } from "@sema-agent/core";
|
|
10
11
|
const RuleScopeSchema = z.union([
|
|
@@ -140,7 +141,7 @@ export class FileRuleApprovalRecordStore {
|
|
|
140
141
|
ensureDir(dir);
|
|
141
142
|
const path = join(dir, "approvals.jsonl");
|
|
142
143
|
this.gate = new CorruptionGate("permission-rule approval log", path, onError);
|
|
143
|
-
for (const raw of readJsonlRecords(path, (info) => this.gate.trip(info.reason))) {
|
|
144
|
+
for (const raw of readJsonlRecords(path, createSkippedRecordSink((info) => this.gate.trip(info.reason)))) {
|
|
144
145
|
const parsed = ApprovalLineSchema.safeParse(raw);
|
|
145
146
|
if (!parsed.success) {
|
|
146
147
|
const stale = StaleApprovalLineSchema.safeParse(raw);
|
|
@@ -209,7 +210,7 @@ export class FileRuleImportTicketStore {
|
|
|
209
210
|
ensureDir(dir);
|
|
210
211
|
const path = join(dir, "import-tickets.jsonl");
|
|
211
212
|
this.gate = new CorruptionGate("permission-rule import-ticket log", path, onError);
|
|
212
|
-
for (const raw of readJsonlRecords(path, (info) => this.gate.trip(info.reason))) {
|
|
213
|
+
for (const raw of readJsonlRecords(path, createSkippedRecordSink((info) => this.gate.trip(info.reason)))) {
|
|
213
214
|
const parsed = TicketLineSchema.safeParse(raw);
|
|
214
215
|
if (!parsed.success) {
|
|
215
216
|
this.gate.trip(`a replayed record does not carry a readable shape: ${parsed.error.message}`);
|
|
@@ -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);
|
|
@@ -46,11 +46,24 @@ import type { SqlDialect } from "./sql-driver.js";
|
|
|
46
46
|
*
|
|
47
47
|
* 🔴 **别名是产物的一部分,不是调用点的自由**:MySQL 协议下 `CAST(rules AS CHAR)` 的回报字段名是
|
|
48
48
|
* `CAST(rules AS CHAR)` 而不是 `rules` —— 把别名留给调用点写,就等于给每一处读腿留一个「忘了写别名,
|
|
49
|
-
* 行上于是没有这个键、读回恒 `undefined
|
|
49
|
+
* 行上于是没有这个键、读回恒 `undefined`」的静默洞。别名在这里一次钉死,行的键恒等于列名(成员形
|
|
50
|
+
* {@link JsonColumnMember}:恒等于声明的小写别名)。
|
|
50
51
|
*
|
|
51
52
|
* 🔴 **`CAST` 不影响索引**:投影不在 `WHERE` 里;大体量列多一次文本拷贝,量级与现状同(驱动本来也解一次)。
|
|
52
53
|
*/
|
|
53
|
-
export declare function jsonColumnText(dialect: SqlDialect, column: string): string;
|
|
54
|
+
export declare function jsonColumnText(dialect: SqlDialect, column: string, member?: JsonColumnMember): string;
|
|
55
|
+
/**
|
|
56
|
+
* 一列 JSON 的**一个顶层成员**(NP-3,7.100.0)—— 读腿只要文档里的一棵子树时用它,整只文档不出引擎。
|
|
57
|
+
* · `key` = 成员名(裸标识符;进 MySQL 路径 `'$.<key>'` 与 PG 字面 `'<key>'`,所以只认裸标识符,不转义不猜);
|
|
58
|
+
* · `as` = 行上的键(**小写**裸标识符:PG 把不带引号的别名折成小写,别的拼法会让两方言的行键不同)。
|
|
59
|
+
* 产物仍是**文本**(同一条「按文本投影、恰好解一次」),成员缺席 / 文档不是对象 ⇒ SQL NULL(两引擎同答),
|
|
60
|
+
* 所以读腿用 {@link decodeNullableJsonColumn}:NULL = 「这份文档没有这棵子树」,JSON `null` 字面 = 一个值(交形读者判)。
|
|
61
|
+
* 首个读者:`checkpoint-store-sql.ts` 的 `listPending`(运维队列 / `/v1/approvals/stream` 热读)只取 `pendingAction`。
|
|
62
|
+
*/
|
|
63
|
+
export interface JsonColumnMember {
|
|
64
|
+
readonly key: string;
|
|
65
|
+
readonly as: string;
|
|
66
|
+
}
|
|
54
67
|
/**
|
|
55
68
|
* 同一只投影的**裸表达式**(不带别名)—— 给 `WHERE` / `UPDATE … WHERE` 这种**没有别名位**的地方用。
|
|
56
69
|
*
|
|
@@ -68,7 +81,7 @@ export declare function jsonColumnText(dialect: SqlDialect, column: string): str
|
|
|
68
81
|
* 子树,本函数的插值对它是一个不透明表达式。所以「误用进 SELECT 投影且没给别名」这一形没有机器门 ——
|
|
69
82
|
* 误用的落点是真库上当场的 `SqlJsonColumnError("not-text")` 响亮抛(行上没有那个键 ⇒ `undefined`),不是静默。
|
|
70
83
|
*/
|
|
71
|
-
export declare function jsonColumnTextExpr(dialect: SqlDialect, column: string): string;
|
|
84
|
+
export declare function jsonColumnTextExpr(dialect: SqlDialect, column: string, memberKey?: string): string;
|
|
72
85
|
/**
|
|
73
86
|
* 一次读回失败的**种别**(店层码,**闭集**;运行期词表在这里,类型从它推导 —— 一张词表只有一个写者):
|
|
74
87
|
* · `not-text` —— 拿到的不是串 = **读腿漏了投影**,那是**码的问题不是数据的问题** ⇒ 响亮的程序错,
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
const COLUMN_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3
|
-
export function jsonColumnText(dialect, column) {
|
|
4
|
-
|
|
3
|
+
export function jsonColumnText(dialect, column, member) {
|
|
4
|
+
if (member === undefined)
|
|
5
|
+
return `${jsonColumnTextExpr(dialect, column)} AS ${column}`;
|
|
6
|
+
if (!MEMBER_ALIAS_RE.test(member.as)) {
|
|
7
|
+
throw new Error(`jsonColumnText: member alias ${JSON.stringify(member.as)} must be a lower-case bare identifier — PostgreSQL folds unquoted aliases to lower case, so any other spelling would give the two dialects different row keys`);
|
|
8
|
+
}
|
|
9
|
+
return `${jsonColumnTextExpr(dialect, column, member.key)} AS ${member.as}`;
|
|
5
10
|
}
|
|
6
|
-
|
|
11
|
+
const MEMBER_ALIAS_RE = /^[a-z_][a-z0-9_]*$/;
|
|
12
|
+
export function jsonColumnTextExpr(dialect, column, memberKey) {
|
|
7
13
|
if (!COLUMN_NAME_RE.test(column)) {
|
|
8
14
|
throw new Error(`jsonColumnTextExpr: ${JSON.stringify(column)} is not a bare column identifier — refusing to splice it into SQL text`);
|
|
9
15
|
}
|
|
10
|
-
|
|
16
|
+
if (memberKey === undefined)
|
|
17
|
+
return dialect === "pg" ? `${column}::text` : `CAST(${column} AS CHAR)`;
|
|
18
|
+
if (!COLUMN_NAME_RE.test(memberKey)) {
|
|
19
|
+
throw new Error(`jsonColumnTextExpr: member ${JSON.stringify(memberKey)} is not a bare identifier — refusing to splice it into a JSON path`);
|
|
20
|
+
}
|
|
21
|
+
return dialect === "pg" ? `(${column}::jsonb -> '${memberKey}')::text` : `CAST(JSON_EXTRACT(${column}, '$.${memberKey}') AS CHAR)`;
|
|
11
22
|
}
|
|
12
23
|
export const SQL_JSON_COLUMN_PROBLEMS = ["not-text", "not-json", "malformed"];
|
|
13
24
|
export class SqlJsonColumnError extends Error {
|
|
@@ -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=真删除、
|