@sema-agent/server 7.92.1 → 7.92.2
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/dist/approval-ask-machine.d.ts +1 -1
- package/dist/bench/s1/arms.js +5 -6
- package/dist/bench/s1/live-deps.js +2 -2
- package/dist/boot/stage-06-runners.js +3 -2
- package/dist/boot/stores.js +9 -0
- package/dist/http/resume-legs.js +10 -9
- package/dist/http/routes/rules.js +7 -1
- package/dist/http/routes/session-sync.js +24 -12
- package/dist/http/routes/sessions.js +12 -8
- package/dist/http/routes/tasks.js +12 -12
- package/dist/http/send.d.ts +11 -0
- package/dist/http/send.js +9 -0
- package/dist/http/server.js +3 -2
- package/dist/leader/fanout.js +3 -3
- package/dist/leader/leader.js +4 -4
- package/dist/leader/repair-wire.js +2 -2
- package/dist/leader/wire.js +2 -2
- package/dist/plugins/file-run-store.js +3 -3
- package/dist/plugins/memory-run-store.js +3 -3
- package/dist/plugins/run-store-sql.js +3 -3
- package/dist/plugins/schema-consistency.d.ts +88 -0
- package/dist/plugins/schema-consistency.js +202 -0
- package/dist/plugins/schema-ensure-legs.d.ts +55 -0
- package/dist/plugins/schema-ensure-legs.js +87 -0
- package/dist/plugins/session-policy-store-sql.js +23 -5
- package/dist/rules-consent.d.ts +23 -2
- package/dist/rules-consent.js +6 -4
- package/dist/run-local.js +2 -2
- package/dist/runs.js +14 -14
- package/dist/terminal.d.ts +58 -4
- package/dist/terminal.js +28 -6
- package/dist/trace/ledger-sink.js +3 -2
- package/package.json +3 -3
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **启动期「库 ≟ 代码」列集对账探针**(S-547)—— 存量库缺列/多列一律**响亮拒启**。
|
|
3
|
+
*
|
|
4
|
+
* ── 病(test T-47 三库一致的真读数)───────────────────────────────────────────────────────────────
|
|
5
|
+
* 7.91.2 建的 `session_policy`(那时还没有 `gen` 列)+ 一行数据,7.92.0 的二进制接上去:`CREATE TABLE
|
|
6
|
+
* IF NOT EXISTS` 对存量表是**空操作** ⇒ 启动全绿、日志零提 `gen`、健康检查 healthy,直到**用户路径上**
|
|
7
|
+
* 第一次读写策略行才炸 —— `PUT/GET /v1/sessions/:id/policy` 500,`POST /v1/tasks` 的 run 终局对外
|
|
8
|
+
* `errorCode` 直接是裸 `42703` / `ER_BAD_FIELD_ERROR`。
|
|
9
|
+
*
|
|
10
|
+
* ── 与 SCHEMA POLICY(`tidb-pool.ts` 顶注,clay 裁 2026-07-26)不冲突 ─────────────────────────────
|
|
11
|
+
* 那条口径禁的是 **ALTER seam**:「代码是 schema 的唯一真源、存量库删库重建、`ensure*` 里不许再出现
|
|
12
|
+
* `ALTER TABLE`」。本探针**一条 DDL 都不发**,它是只读的 information_schema 对账 —— 恰恰是那条口径的
|
|
13
|
+
* **执行面**:口径说「库必须等于代码」,在此之前没有任何东西检查过这句话是不是真的。两者一个是禁令
|
|
14
|
+
* (不许代码偷改库)、一个是对账(库偏离了就别起),同向不同面。
|
|
15
|
+
*
|
|
16
|
+
* ── 为什么两个方向都拒(缺列 **和** 多列)──────────────────────────────────────────────────────
|
|
17
|
+
* 缺列 = 库比代码旧(升级没重建库):读写会在用户路径上炸。多列 = 库比代码**新** —— 滚动升级窗里一个
|
|
18
|
+
* **旧副本**连上了已被新副本重建过的库;它按旧列集读写,新列上的不变量(本例:`gen` 血统)在它眼里
|
|
19
|
+
* 根本不存在,于是它会悄悄写出违反新契约的行。后者比前者更隐蔽,所以不做「幽灵列只警告」。
|
|
20
|
+
* ⇒ 运维顺序因此是**先升库再升副本**(见 `docs/DEPLOY-PREREQS.md` 滚动升级窗那一节)。
|
|
21
|
+
*
|
|
22
|
+
* ── 期望列集从哪来:**机器派生,不许手抄**([ref])────────────────────────────────────────────
|
|
23
|
+
* 期望值 = `schema-ensure-legs.ts` 的录制驱动跑一遍**真的** `ensure*Schema`,把录到的 `CREATE TABLE`
|
|
24
|
+
* 语句文本解析出列集。于是「代码里的 schema」与「探针拿来对账的 schema」是**同一份字节**,不存在第二
|
|
25
|
+
* 处清单会腐掉。(更好的形是把语句数组本身改成结构化再生成 SQL —— 那要改 70 张表的 DDL 写法,爆炸
|
|
26
|
+
* 半径远大于本批;先解析文本,理由写在这里。)拒启文案里的那句 `ALTER TABLE … ADD COLUMN …` 也由
|
|
27
|
+
* **同一份解析结果**生成,不是手写的。
|
|
28
|
+
*
|
|
29
|
+
* ── 失败方向:读不到就拒,不跳过 ─────────────────────────────────────────────────────────────
|
|
30
|
+
* information_schema 查询抛错、或整库一行都读不出来 ⇒ **拒启**。探针的语义是「我证明了库等于代码」,
|
|
31
|
+
* 证不出来时放行等于把一条保护性断言静默降级成祝福([ref]:保护型缺席必须 fail-closed)。
|
|
32
|
+
* `recordFailOpen` 在这条腿上**不适用** —— 它是 F 类(装饰/缓存)的登记口,不是 P 类保护臂的逃生门。
|
|
33
|
+
*/
|
|
34
|
+
import { type SchemaDialect } from "./schema-ensure-legs.js";
|
|
35
|
+
/** 一列:名字 + 它在 `CREATE TABLE` 里的**原样定义文本**(生成 ADD COLUMN 指路用,不另手写)。 */
|
|
36
|
+
export interface ParsedColumn {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly ddl: string;
|
|
39
|
+
}
|
|
40
|
+
/** 一张表的列形。 */
|
|
41
|
+
export interface ParsedTable {
|
|
42
|
+
readonly table: string;
|
|
43
|
+
readonly columns: readonly ParsedColumn[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 解析一条 `CREATE TABLE`。不是建表语句(CREATE INDEX / INSERT / SELECT / CREATE EXTENSION)⇒ `undefined`。
|
|
47
|
+
*
|
|
48
|
+
* 认识面窄得刻意:只取**表名 + 顶层列名 + 该列的定义文本**。类型、默认值、可空性一律**不比对** ——
|
|
49
|
+
* 本探针守的是「列在不在」这一类(T-47 的真病),把类型也拉进来会让方言差异(`DATETIME(3)` ⇄
|
|
50
|
+
* `TIMESTAMPTZ(3)`、`vector(d)` ⇄ `jsonb` 的运行期参数化)变成一堆假红,而那是另一条判据另一台车。
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseCreateTable(stmt: string): ParsedTable | undefined;
|
|
53
|
+
/** 启动时真会建出来的**全部**表形(录制 → 解析)。同名表二次出现以**先到**为准(`IF NOT EXISTS` 的真语义)。 */
|
|
54
|
+
export declare function expectedTableShapes(dialect: SchemaDialect): Promise<Map<string, ParsedTable>>;
|
|
55
|
+
/** 拒启码(机读)。附录 A / 拒启码表的登记名。 */
|
|
56
|
+
export declare const SCHEMA_MISMATCH_ERROR_CODE = "config.schema_mismatch";
|
|
57
|
+
/** 一张表的对账结论。 */
|
|
58
|
+
export interface TableMismatch {
|
|
59
|
+
readonly table: string;
|
|
60
|
+
/** 代码有、库没有(库比代码**旧**)。 */
|
|
61
|
+
readonly missing: readonly ParsedColumn[];
|
|
62
|
+
/** 库有、代码没有(库比代码**新** —— 滚动窗里的旧副本)。 */
|
|
63
|
+
readonly ghost: readonly string[];
|
|
64
|
+
}
|
|
65
|
+
/** 探针读口:一条 SQL → 行数组(两方言由调用方各自绑)。 */
|
|
66
|
+
export type SchemaProbeRead = (sql: string) => Promise<Array<Record<string, unknown>>>;
|
|
67
|
+
/**
|
|
68
|
+
* 库里实际的列集(表名 → 列名集合)。
|
|
69
|
+
*
|
|
70
|
+
* 🔴 **PG 侧读的是「查询真正会解析到的那个关系」,不是 `current_schema()`**(codex 对抗复审本轮 [medium]
|
|
71
|
+
* 采纳):店里的 SQL 全是**不限定 schema** 的裸表名,它按 `search_path` **逐个** schema 解析,命中第一个;
|
|
72
|
+
* 而 `current_schema()` 只是路径的第一段。两者不等的部署是真的存在(角色的 `search_path` 配了多段),那时
|
|
73
|
+
* 只读第一段会让探针对「表其实住在路径后段、而且缺列」这一形**完全沉默**。⇒ 按 `current_schemas(false)`
|
|
74
|
+
* 的**顺序**取每张表的第一次命中 —— 探针于是与执行器看同一张表。MySQL 协议没有这个概念(库 = 连接上的
|
|
75
|
+
* `DATABASE()`),那条臂一个字不变。
|
|
76
|
+
*/
|
|
77
|
+
export declare function readLiveColumns(read: SchemaProbeRead, dialect: SchemaDialect): Promise<Map<string, Set<string>>>;
|
|
78
|
+
/** 对账(纯函数:期望 × 实况 → 不符表清单)。 */
|
|
79
|
+
export declare function diffSchema(expected: Map<string, ParsedTable>, live: Map<string, Set<string>>): TableMismatch[];
|
|
80
|
+
/** 拒启文案(表名 + 缺/多列 + 两条路;ADD COLUMN 句由解析结果**生成**)。 */
|
|
81
|
+
export declare function describeSchemaMismatch(dialect: SchemaDialect, mismatches: readonly TableMismatch[]): string;
|
|
82
|
+
/**
|
|
83
|
+
* 启动期对账:**建表之后**跑,不符即抛(= 拒启)。两方言 + InnoDB 同腿(MySQL 协议共用 `mysql` 臂)。
|
|
84
|
+
*
|
|
85
|
+
* 失败方向见文件头注:读抛 / 整库零行 ⇒ 拒。
|
|
86
|
+
*/
|
|
87
|
+
export declare function assertSchemaMatchesCode(read: SchemaProbeRead, dialect: SchemaDialect): Promise<void>;
|
|
88
|
+
//# sourceMappingURL=schema-consistency.d.ts.map
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { recordEnsureStatements } from "./schema-ensure-legs.js";
|
|
2
|
+
const QUOTES = new Set(["'", '"', "`"]);
|
|
3
|
+
function stripLineComments(sql) {
|
|
4
|
+
let out = "";
|
|
5
|
+
for (let i = 0; i < sql.length; i++) {
|
|
6
|
+
const c = sql[i];
|
|
7
|
+
if (QUOTES.has(c)) {
|
|
8
|
+
const q = c;
|
|
9
|
+
out += c;
|
|
10
|
+
i++;
|
|
11
|
+
while (i < sql.length) {
|
|
12
|
+
out += sql[i];
|
|
13
|
+
if (sql[i] === q)
|
|
14
|
+
break;
|
|
15
|
+
i++;
|
|
16
|
+
}
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (c === "-" && sql[i + 1] === "-") {
|
|
20
|
+
while (i < sql.length && sql[i] !== "\n")
|
|
21
|
+
i++;
|
|
22
|
+
out += "\n";
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
out += c;
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
function splitTopLevel(body) {
|
|
30
|
+
const parts = [];
|
|
31
|
+
let depth = 0;
|
|
32
|
+
let start = 0;
|
|
33
|
+
for (let i = 0; i < body.length; i++) {
|
|
34
|
+
const c = body[i];
|
|
35
|
+
if (QUOTES.has(c)) {
|
|
36
|
+
const q = c;
|
|
37
|
+
i++;
|
|
38
|
+
while (i < body.length && body[i] !== q)
|
|
39
|
+
i++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (c === "(")
|
|
43
|
+
depth++;
|
|
44
|
+
else if (c === ")")
|
|
45
|
+
depth--;
|
|
46
|
+
else if (c === "," && depth === 0) {
|
|
47
|
+
parts.push(body.slice(start, i));
|
|
48
|
+
start = i + 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
parts.push(body.slice(start));
|
|
52
|
+
return parts;
|
|
53
|
+
}
|
|
54
|
+
const CONSTRAINT_HEADS = new Set(["PRIMARY", "UNIQUE", "KEY", "INDEX", "CONSTRAINT", "FOREIGN", "CHECK", "FULLTEXT", "SPATIAL", "EXCLUDE"]);
|
|
55
|
+
const unquote = (s) => s.replace(/^[`"](.*)[`"]$/s, "$1");
|
|
56
|
+
export function parseCreateTable(stmt) {
|
|
57
|
+
const sql = stripLineComments(stmt).trim();
|
|
58
|
+
const head = /^CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"]?[\w$.]+[`"]?)\s*\(/i.exec(sql);
|
|
59
|
+
if (!head)
|
|
60
|
+
return undefined;
|
|
61
|
+
const open = sql.indexOf("(", head[0].length - 1);
|
|
62
|
+
let depth = 0;
|
|
63
|
+
let close = -1;
|
|
64
|
+
for (let i = open; i < sql.length; i++) {
|
|
65
|
+
const c = sql[i];
|
|
66
|
+
if (QUOTES.has(c)) {
|
|
67
|
+
const q = c;
|
|
68
|
+
i++;
|
|
69
|
+
while (i < sql.length && sql[i] !== q)
|
|
70
|
+
i++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (c === "(")
|
|
74
|
+
depth++;
|
|
75
|
+
else if (c === ")") {
|
|
76
|
+
depth--;
|
|
77
|
+
if (depth === 0) {
|
|
78
|
+
close = i;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (close < 0)
|
|
84
|
+
return undefined;
|
|
85
|
+
const columns = [];
|
|
86
|
+
for (const raw of splitTopLevel(sql.slice(open + 1, close))) {
|
|
87
|
+
const part = raw.trim().replace(/\s+/g, " ");
|
|
88
|
+
if (part === "")
|
|
89
|
+
continue;
|
|
90
|
+
const first = part.split(/[\s(]/, 1)[0];
|
|
91
|
+
if (CONSTRAINT_HEADS.has(first.toUpperCase()))
|
|
92
|
+
continue;
|
|
93
|
+
columns.push({ name: unquote(first).toLowerCase(), ddl: part });
|
|
94
|
+
}
|
|
95
|
+
return { table: unquote(head[1]).toLowerCase(), columns };
|
|
96
|
+
}
|
|
97
|
+
export async function expectedTableShapes(dialect) {
|
|
98
|
+
const shapes = new Map();
|
|
99
|
+
for (const stmt of await recordEnsureStatements(dialect)) {
|
|
100
|
+
const parsed = parseCreateTable(stmt);
|
|
101
|
+
if (parsed && !shapes.has(parsed.table))
|
|
102
|
+
shapes.set(parsed.table, parsed);
|
|
103
|
+
}
|
|
104
|
+
return shapes;
|
|
105
|
+
}
|
|
106
|
+
export const SCHEMA_MISMATCH_ERROR_CODE = "config.schema_mismatch";
|
|
107
|
+
export async function readLiveColumns(read, dialect) {
|
|
108
|
+
const live = new Map();
|
|
109
|
+
if (dialect !== "pg") {
|
|
110
|
+
for (const row of await read("SELECT table_name AS t, column_name AS c FROM information_schema.columns WHERE table_schema = DATABASE()")) {
|
|
111
|
+
addColumn(live, row["t"], row["c"], "fold");
|
|
112
|
+
}
|
|
113
|
+
return live;
|
|
114
|
+
}
|
|
115
|
+
const order = new Map();
|
|
116
|
+
for (const row of await read("SELECT s AS s, ord AS ord FROM unnest(current_schemas(false)) WITH ORDINALITY AS t(s, ord)")) {
|
|
117
|
+
const s = String(row["s"] ?? "");
|
|
118
|
+
if (s !== "")
|
|
119
|
+
order.set(s, Number(row["ord"]));
|
|
120
|
+
}
|
|
121
|
+
const winner = new Map();
|
|
122
|
+
for (const row of await read("SELECT table_schema AS s, table_name AS t, column_name AS c FROM information_schema.columns WHERE table_schema = ANY (current_schemas(false))")) {
|
|
123
|
+
const t = String(row["t"] ?? "");
|
|
124
|
+
const rank = order.get(String(row["s"] ?? "")) ?? Number.MAX_SAFE_INTEGER;
|
|
125
|
+
const held = winner.get(t);
|
|
126
|
+
if (held !== undefined && rank > held)
|
|
127
|
+
continue;
|
|
128
|
+
if (held === undefined || rank < held) {
|
|
129
|
+
winner.set(t, rank);
|
|
130
|
+
live.delete(t);
|
|
131
|
+
}
|
|
132
|
+
addColumn(live, t, row["c"], "verbatim");
|
|
133
|
+
}
|
|
134
|
+
return live;
|
|
135
|
+
}
|
|
136
|
+
function addColumn(live, table, column, mode) {
|
|
137
|
+
const t = mode === "fold" ? String(table ?? "").toLowerCase() : String(table ?? "");
|
|
138
|
+
const c = mode === "fold" ? String(column ?? "").toLowerCase() : String(column ?? "");
|
|
139
|
+
if (t === "" || c === "")
|
|
140
|
+
return;
|
|
141
|
+
let set = live.get(t);
|
|
142
|
+
if (set === undefined) {
|
|
143
|
+
set = new Set();
|
|
144
|
+
live.set(t, set);
|
|
145
|
+
}
|
|
146
|
+
set.add(c);
|
|
147
|
+
}
|
|
148
|
+
export function diffSchema(expected, live) {
|
|
149
|
+
const out = [];
|
|
150
|
+
for (const [table, shape] of expected) {
|
|
151
|
+
const cols = live.get(table);
|
|
152
|
+
if (cols === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
const missing = shape.columns.filter((c) => !cols.has(c.name));
|
|
155
|
+
const known = new Set(shape.columns.map((c) => c.name));
|
|
156
|
+
const ghost = [...cols].filter((c) => !known.has(c)).sort();
|
|
157
|
+
if (missing.length > 0 || ghost.length > 0)
|
|
158
|
+
out.push({ table, missing, ghost });
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
export function describeSchemaMismatch(dialect, mismatches) {
|
|
163
|
+
const lines = [
|
|
164
|
+
`${SCHEMA_MISMATCH_ERROR_CODE}: the ${dialect === "pg" ? "PostgreSQL" : "MySQL-protocol"} database does not match this build's schema — refusing to start.`,
|
|
165
|
+
"This repo ships no ALTER seam (schema policy: the code is the single source of truth), so a drifted database is",
|
|
166
|
+
"a deployment-order problem, not something the process may quietly work around.",
|
|
167
|
+
"",
|
|
168
|
+
];
|
|
169
|
+
for (const m of mismatches) {
|
|
170
|
+
if (m.missing.length > 0) {
|
|
171
|
+
lines.push(` table ${m.table}: MISSING column(s) ${m.missing.map((c) => c.name).join(", ")} — the database is OLDER than this build.`);
|
|
172
|
+
for (const c of m.missing)
|
|
173
|
+
lines.push(` ALTER TABLE ${m.table} ADD COLUMN ${c.ddl};`);
|
|
174
|
+
}
|
|
175
|
+
if (m.ghost.length > 0) {
|
|
176
|
+
lines.push(` table ${m.table}: UNKNOWN column(s) ${m.ghost.join(", ")} — the database is NEWER than this build (a rolling window where an old replica reached an upgraded database).`);
|
|
177
|
+
lines.push(` No ALTER is suggested for this direction: deploy the build that owns those columns (upgrade the database FIRST, then the replicas).`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
lines.push("", " Two ways out: (1) deploy the build whose code matches this database; (2) rebuild the database from this build", " (DROP DATABASE <name>; CREATE DATABASE <name>; restart — every table is recreated by ensure*Schema at boot).");
|
|
181
|
+
return lines.join("\n");
|
|
182
|
+
}
|
|
183
|
+
export async function assertSchemaMatchesCode(read, dialect) {
|
|
184
|
+
const expected = await expectedTableShapes(dialect);
|
|
185
|
+
let live;
|
|
186
|
+
try {
|
|
187
|
+
live = await readLiveColumns(read, dialect);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
throw new Error(`${SCHEMA_MISMATCH_ERROR_CODE}: could not read information_schema.columns to verify the ${dialect} schema — refusing to start ` +
|
|
191
|
+
`(an unverifiable schema is treated as a mismatch, not as a pass): ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
192
|
+
}
|
|
193
|
+
if (live.size === 0) {
|
|
194
|
+
throw new Error(`${SCHEMA_MISMATCH_ERROR_CODE}: information_schema.columns returned no rows anywhere on this ${dialect} connection's schema path — refusing to start. ` +
|
|
195
|
+
`Either the connection points at an empty/foreign schema, or the account cannot see its own catalog; both make the ` +
|
|
196
|
+
`"database matches the code" check unprovable, and an unprovable protective check never passes.`);
|
|
197
|
+
}
|
|
198
|
+
const mismatches = diffSchema(expected, live);
|
|
199
|
+
if (mismatches.length > 0)
|
|
200
|
+
throw new Error(describeSchemaMismatch(dialect, mismatches));
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=schema-consistency.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { PgQueryFn } from "./pg-query.js";
|
|
2
|
+
/** 录制的方言。`mysql` = MySQL 协议(TiDB / InnoDB 同腿),`pg` = PostgreSQL。 */
|
|
3
|
+
export type SchemaDialect = "mysql" | "pg";
|
|
4
|
+
/** advisory lock 语句 —— 与 schema 无关,录制时跳过(但要答成功,否则 ensure* 走降级告警路径)。 */
|
|
5
|
+
export declare const LOCK_RE: RegExp;
|
|
6
|
+
/**
|
|
7
|
+
* **「一个新库对 ensure 探针怎么答」的唯一属主**(合并复审 车HB 后收口:此前生成器录制器、`test/ensure-schema.test.ts`
|
|
8
|
+
* 的假池、`test/schema-baseline-generated.test.ts` ③c 的假驱动各抄一份,S-287 加了索引探针 + PG 锁改 `got === true`
|
|
9
|
+
* 之后三份各漂一处 —— 假池答零行诱发 68 条 ALTER、③c 答 `got: 1` 让 PG 锁永远拿不到)。
|
|
10
|
+
* 返回 `undefined` = 不是探针,由调用方录制 / 执行;三条例外的理由见上面三条正则的注。
|
|
11
|
+
*/
|
|
12
|
+
export declare function answerEnsureProbe(text: string): {
|
|
13
|
+
rows: Record<string, unknown>[];
|
|
14
|
+
} | undefined;
|
|
15
|
+
/** 基线内容:建表/建索引/建扩展 + 种子行。其余一律视为**增量 seam**(违反 SCHEMA POLICY)。 */
|
|
16
|
+
/**
|
|
17
|
+
* 一条 `ensure*` 腿真正需要的三个句柄面。**录制**驱动(基线生成,下面的 {@link recorder})与**真库**驱动
|
|
18
|
+
* (`scripts/db-lane-warm.ts` 的按车分库暖库)实现同一组签名 ⇒ {@link MYSQL_LEGS} / {@link PG_LEGS} 是这
|
|
19
|
+
* 两条用途的**单源**:加一条 boot ensure 腿只改那两个数组一处,基线生成与车道库暖库同时跟上。
|
|
20
|
+
*
|
|
21
|
+
* 为什么必须单源(S-285/S-256/S-343 真案):暖库此前手抄了「中央两条」(`ensureSchema`/`ensurePgSchema`),
|
|
22
|
+
* 其余 per-store 腿(现值 mysql 10 / pg 11)在**新建的**车道库上从没被建过 ⇒ 新车道首跑几十格红,而发车机那只老库因为历史上
|
|
23
|
+
* 被跑过一次就有表,于是这个缺口在老库上**恒绿**。两张手抄清单迟早分叉,正是本仓反复吃的亏。
|
|
24
|
+
*/
|
|
25
|
+
export interface EnsureDriver {
|
|
26
|
+
/** mysql2 Pool 形(`getConnection()` + `query`)。 */
|
|
27
|
+
readonly mysqlPool: unknown;
|
|
28
|
+
/** node-pg Pool 形(`connect()` + `query`)。 */
|
|
29
|
+
readonly pgPool: unknown;
|
|
30
|
+
/** 裸 {@link PgQueryFn} 形(per-store 的 PG ensure 直接收这个)。 */
|
|
31
|
+
readonly pgQuery: PgQueryFn;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 一条 `ensure*` 腿。`name` 不是装饰:`test/schema-baseline-generated.test.ts` 判据 ③b 拿它与
|
|
35
|
+
* `src/boot/stores.ts` 里真被 await 的 ensure 名对账,抓"boot 会建、录制没录"的漏族(usage_window
|
|
36
|
+
* 就是这么漏掉一整张表的 —— 那种漏在两条方言上**同时**发生,判据 ④ 的方言对拍完美对称 ⇒ 永不报警)。
|
|
37
|
+
* ⇒ 遍历与自报清单必须同一份数组,否则清单会自己腐掉,对账就成了自说自话。
|
|
38
|
+
*
|
|
39
|
+
* 第三个消费方(S-285/S-256/S-343):`scripts/db-lane-warm.ts` 拿**同一份数组**对真库跑一遍 ⇒ 按车分库的
|
|
40
|
+
* 新库与生产 boot 建出同一套表。判据 ③c 钉住「warm 跑过的腿名 == 本清单」,任何人把它抄成第二份就当场红。
|
|
41
|
+
*/
|
|
42
|
+
export interface EnsureLeg {
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly run: (d: EnsureDriver) => Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export declare const MYSQL_LEGS: readonly EnsureLeg[];
|
|
47
|
+
export declare const PG_LEGS: readonly EnsureLeg[];
|
|
48
|
+
/** 生成器自报的驱动清单(判据 ③b 的对账一侧)。与上面两个数组同源 ⇒ 加腿不改这里就不会漏报。 */
|
|
49
|
+
export declare const DRIVEN_ENSURES: {
|
|
50
|
+
readonly mysql: readonly string[];
|
|
51
|
+
readonly pg: readonly string[];
|
|
52
|
+
};
|
|
53
|
+
/** 跑一遍一条方言的真 `ensure*` 全腿,返回**录制到的语句流**(顺序即执行顺序)。纯内存,不碰数据库。 */
|
|
54
|
+
export declare function recordEnsureStatements(dialect: SchemaDialect): Promise<string[]>;
|
|
55
|
+
//# sourceMappingURL=schema-ensure-legs.d.ts.map
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ensureSchema } from "./tidb-pool.js";
|
|
2
|
+
import { ensurePgSchema } from "./pg-pool.js";
|
|
3
|
+
import { ensureTiDBMemoryEngineSchema } from "./memory-engine-tidb.js";
|
|
4
|
+
import { ensurePgMemoryEngineSchema } from "./memory-engine-pg.js";
|
|
5
|
+
import { ensurePgMemoryEmbedderMetaSchema } from "./memory-embedder-fingerprint.js";
|
|
6
|
+
import { ensureTiDBMemorySyncSchema, ensureTiDBMemoryHistorySchema } from "./memory-sync-store-tidb.js";
|
|
7
|
+
import { ensurePgMemorySyncSchema, ensurePgMemoryHistorySchema } from "./memory-sync-store-pg.js";
|
|
8
|
+
import { ensureTiDBRosterSchema, ensurePgRosterSchema } from "./roster-store-sql.js";
|
|
9
|
+
import { ensureTiDBBackgroundAgentSchema, ensurePgBackgroundAgentSchema } from "./background-agent-store-sql.js";
|
|
10
|
+
import { ensureTiDBMailboxSchema, ensurePgMailboxSchema } from "./mailbox-store-sql.js";
|
|
11
|
+
import { ensureTiDBTaskAttachmentSchema, ensurePgTaskAttachmentSchema } from "./task-attachment-store.js";
|
|
12
|
+
import { ensureTiDBUsageWindowSchema, ensurePgUsageWindowSchema } from "./usage-window-store-sql.js";
|
|
13
|
+
import { ensureTiDBTaskListSchema, ensurePgTaskListSchema } from "./task-list-store-sql.js";
|
|
14
|
+
import { ensureTiDBDeviceSchema, ensurePgDeviceSchema } from "./device-store-sql.js";
|
|
15
|
+
export const LOCK_RE = /GET_LOCK|RELEASE_LOCK|pg_(try_)?advisory/i;
|
|
16
|
+
const INDEX_PROBE_RE = /FROM information_schema\.statistics\b/i;
|
|
17
|
+
const PG_INDEX_VALID_RE = /FROM pg_index i WHERE i\.indexrelid = to_regclass/i;
|
|
18
|
+
export function answerEnsureProbe(text) {
|
|
19
|
+
if (LOCK_RE.test(text))
|
|
20
|
+
return { rows: [{ got: true }] };
|
|
21
|
+
if (INDEX_PROBE_RE.test(text))
|
|
22
|
+
return { rows: [{ present: 1 }] };
|
|
23
|
+
if (PG_INDEX_VALID_RE.test(text))
|
|
24
|
+
return { rows: [{ indisvalid: true }] };
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
function recorder() {
|
|
28
|
+
const sql = [];
|
|
29
|
+
const answer = (text) => (answerEnsureProbe(text) ?? { rows: [] });
|
|
30
|
+
const record = (text) => {
|
|
31
|
+
sql.push(text);
|
|
32
|
+
return answer(text);
|
|
33
|
+
};
|
|
34
|
+
const mysqlConn = {
|
|
35
|
+
query: async (text) => {
|
|
36
|
+
const r = record(text);
|
|
37
|
+
return [r.rows, []];
|
|
38
|
+
},
|
|
39
|
+
release: () => undefined,
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
sql,
|
|
43
|
+
mysqlPool: { getConnection: async () => mysqlConn, query: async (t) => [record(t).rows, []] },
|
|
44
|
+
pgPool: { connect: async () => ({ query: async (t) => record(t), release: () => undefined }) },
|
|
45
|
+
pgQuery: async (t) => record(t),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const asMysqlPool = (p) => p;
|
|
49
|
+
const asPgPool = (p) => p;
|
|
50
|
+
export const MYSQL_LEGS = [
|
|
51
|
+
{ name: "ensureSchema", run: (r) => ensureSchema(asMysqlPool(r.mysqlPool)) },
|
|
52
|
+
{ name: "ensureTiDBMemoryEngineSchema", run: (r) => ensureTiDBMemoryEngineSchema(asMysqlPool(r.mysqlPool)) },
|
|
53
|
+
{ name: "ensureTiDBMemorySyncSchema", run: (r) => ensureTiDBMemorySyncSchema(asMysqlPool(r.mysqlPool)) },
|
|
54
|
+
{ name: "ensureTiDBMemoryHistorySchema", run: (r) => ensureTiDBMemoryHistorySchema(asMysqlPool(r.mysqlPool)) },
|
|
55
|
+
{ name: "ensureTiDBRosterSchema", run: (r) => ensureTiDBRosterSchema(asMysqlPool(r.mysqlPool)) },
|
|
56
|
+
{ name: "ensureTiDBBackgroundAgentSchema", run: (r) => ensureTiDBBackgroundAgentSchema(asMysqlPool(r.mysqlPool)) },
|
|
57
|
+
{ name: "ensureTiDBMailboxSchema", run: (r) => ensureTiDBMailboxSchema(asMysqlPool(r.mysqlPool)) },
|
|
58
|
+
{ name: "ensureTiDBTaskAttachmentSchema", run: (r) => ensureTiDBTaskAttachmentSchema(asMysqlPool(r.mysqlPool)) },
|
|
59
|
+
{ name: "ensureTiDBUsageWindowSchema", run: (r) => ensureTiDBUsageWindowSchema(asMysqlPool(r.mysqlPool)) },
|
|
60
|
+
{ name: "ensureTiDBTaskListSchema", run: (r) => ensureTiDBTaskListSchema(asMysqlPool(r.mysqlPool)) },
|
|
61
|
+
{ name: "ensureTiDBDeviceSchema", run: (r) => ensureTiDBDeviceSchema(asMysqlPool(r.mysqlPool)) },
|
|
62
|
+
];
|
|
63
|
+
export const PG_LEGS = [
|
|
64
|
+
{ name: "ensurePgSchema", run: (r) => ensurePgSchema(asPgPool(r.pgPool)) },
|
|
65
|
+
{ name: "ensurePgMemoryEngineSchema", run: (r) => ensurePgMemoryEngineSchema(r.pgQuery) },
|
|
66
|
+
{ name: "ensurePgMemoryEmbedderMetaSchema", run: (r) => ensurePgMemoryEmbedderMetaSchema(r.pgQuery) },
|
|
67
|
+
{ name: "ensurePgMemorySyncSchema", run: (r) => ensurePgMemorySyncSchema(r.pgQuery) },
|
|
68
|
+
{ name: "ensurePgMemoryHistorySchema", run: (r) => ensurePgMemoryHistorySchema(r.pgQuery) },
|
|
69
|
+
{ name: "ensurePgRosterSchema", run: (r) => ensurePgRosterSchema(r.pgQuery) },
|
|
70
|
+
{ name: "ensurePgBackgroundAgentSchema", run: (r) => ensurePgBackgroundAgentSchema(r.pgQuery) },
|
|
71
|
+
{ name: "ensurePgMailboxSchema", run: (r) => ensurePgMailboxSchema(r.pgQuery) },
|
|
72
|
+
{ name: "ensurePgTaskAttachmentSchema", run: (r) => ensurePgTaskAttachmentSchema(r.pgQuery) },
|
|
73
|
+
{ name: "ensurePgUsageWindowSchema", run: (r) => ensurePgUsageWindowSchema(r.pgQuery) },
|
|
74
|
+
{ name: "ensurePgTaskListSchema", run: (r) => ensurePgTaskListSchema(r.pgQuery) },
|
|
75
|
+
{ name: "ensurePgDeviceSchema", run: (r) => ensurePgDeviceSchema(r.pgQuery) },
|
|
76
|
+
];
|
|
77
|
+
export const DRIVEN_ENSURES = {
|
|
78
|
+
mysql: MYSQL_LEGS.map((l) => l.name),
|
|
79
|
+
pg: PG_LEGS.map((l) => l.name),
|
|
80
|
+
};
|
|
81
|
+
export async function recordEnsureStatements(dialect) {
|
|
82
|
+
const r = recorder();
|
|
83
|
+
for (const leg of dialect === "mysql" ? MYSQL_LEGS : PG_LEGS)
|
|
84
|
+
await leg.run(r);
|
|
85
|
+
return r.sql;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=schema-ensure-legs.js.map
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { loosenReasons, SessionPolicyError, normalizeRules, stripRev } from "@sema-agent/core";
|
|
3
|
+
import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../session-policy-wire.js";
|
|
3
4
|
import { pgHasUnstorable } from "./pg-safe-json.js";
|
|
4
5
|
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
5
6
|
import { isDeadlockError, isDupKeyError } from "./sql-errors.js";
|
|
6
7
|
export const policyKeyFor = (sessionId, principal) => createHash("sha256").update(JSON.stringify([sessionId, principal ?? null])).digest("hex");
|
|
7
8
|
const policyKey = policyKeyFor;
|
|
8
9
|
const RETRY_LIMIT = 8;
|
|
9
|
-
|
|
10
|
+
function readRulesBody(text, where) {
|
|
11
|
+
let body;
|
|
12
|
+
try {
|
|
13
|
+
body = JSON.parse(text);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new SessionPolicyError("corrupt", `session rules row ${where} carries a rules body that is not JSON (a torn or hand-edited write) — refusing rather than serve a row nobody can read as rules`);
|
|
17
|
+
}
|
|
18
|
+
const read = readSessionPermissionRules(body);
|
|
19
|
+
if (read.problem !== undefined) {
|
|
20
|
+
throw new SessionPolicyError("corrupt", `session rules row ${where} carries a malformed rules body (${describeSessionPermissionRulesProblem(read.problem)}) — refusing rather than serve it as an EMPTY rule set, which would silently drop every tightening this session carries`);
|
|
21
|
+
}
|
|
22
|
+
return read.rules;
|
|
23
|
+
}
|
|
10
24
|
function rowToStored(r, where) {
|
|
11
25
|
const rev = Number(r.rev);
|
|
12
26
|
if (!Number.isFinite(rev) || !Number.isInteger(rev) || rev < 1) {
|
|
@@ -16,7 +30,11 @@ function rowToStored(r, where) {
|
|
|
16
30
|
if (!Number.isFinite(gen) || !Number.isInteger(gen) || gen < 0) {
|
|
17
31
|
throw new SessionPolicyError("corrupt", `session rules row ${where} carries lineage gen ${String(r.gen)} — a generation is a whole number ≥ 0; refusing rather than let a reader judge lineage by a value nothing minted`);
|
|
18
32
|
}
|
|
19
|
-
|
|
33
|
+
const raw = r.rules;
|
|
34
|
+
if (typeof raw !== "string") {
|
|
35
|
+
throw new Error(`session rules row ${where}: the read did not project the rules column as TEXT (got ${typeof raw}) — the SELECT and rowToStored have drifted apart`);
|
|
36
|
+
}
|
|
37
|
+
return { ...readRulesBody(raw, where), rev, gen };
|
|
20
38
|
}
|
|
21
39
|
export class SqlSessionPolicyStore {
|
|
22
40
|
db;
|
|
@@ -30,13 +48,13 @@ export class SqlSessionPolicyStore {
|
|
|
30
48
|
return isDupKeyError(this.db.dialect, e);
|
|
31
49
|
}
|
|
32
50
|
async getRules(sessionId, principal) {
|
|
33
|
-
const { rows } = await this.db.query(this.q("SELECT rules, rev, gen FROM session_policy WHERE policy_key = ?", "SELECT rules, rev, gen FROM session_policy WHERE policy_key = $1"), [policyKey(sessionId, principal)]);
|
|
51
|
+
const { rows } = await this.db.query(this.q("SELECT CAST(rules AS CHAR) AS rules, rev, gen FROM session_policy WHERE policy_key = ?", "SELECT rules::text AS rules, rev, gen FROM session_policy WHERE policy_key = $1"), [policyKey(sessionId, principal)]);
|
|
34
52
|
if (!rows[0])
|
|
35
53
|
return null;
|
|
36
54
|
return rowToStored(rows[0], `for session ${JSON.stringify(sessionId)}`);
|
|
37
55
|
}
|
|
38
56
|
async listBySession(sessionId) {
|
|
39
|
-
const { rows } = await this.db.query(this.q("SELECT principal, rules, rev, gen FROM session_policy WHERE session_id = ?", "SELECT principal, rules, rev, gen FROM session_policy WHERE session_id = $1"), [sessionId]);
|
|
57
|
+
const { rows } = await this.db.query(this.q("SELECT principal, CAST(rules AS CHAR) AS rules, rev, gen FROM session_policy WHERE session_id = ?", "SELECT principal, rules::text AS rules, rev, gen FROM session_policy WHERE session_id = $1"), [sessionId]);
|
|
40
58
|
return rows.map((r) => ({
|
|
41
59
|
principal: r.principal == null ? undefined : String(r.principal),
|
|
42
60
|
rules: rowToStored(r, `for session ${JSON.stringify(sessionId)} (enumeration)`),
|
|
@@ -45,7 +63,7 @@ export class SqlSessionPolicyStore {
|
|
|
45
63
|
async putRules(sessionId, principal, rules, opts) {
|
|
46
64
|
const key = policyKey(sessionId, principal);
|
|
47
65
|
return this.retrying(() => this.tx(async (conn) => {
|
|
48
|
-
const { rows } = await conn.query(this.q("SELECT rules, rev, gen FROM session_policy WHERE policy_key = ? FOR UPDATE", "SELECT rules, rev, gen FROM session_policy WHERE policy_key = $1 FOR UPDATE"), [key]);
|
|
66
|
+
const { rows } = await conn.query(this.q("SELECT CAST(rules AS CHAR) AS rules, rev, gen FROM session_policy WHERE policy_key = ? FOR UPDATE", "SELECT rules::text AS rules, rev, gen FROM session_policy WHERE policy_key = $1 FOR UPDATE"), [key]);
|
|
49
67
|
const prior = rows[0] ? rowToStored(rows[0], `for session ${JSON.stringify(sessionId)} (pre-write snapshot)`) : undefined;
|
|
50
68
|
const priorRev = prior?.rev ?? 0;
|
|
51
69
|
if (opts?.expectedRev !== undefined && priorRev !== opts.expectedRev) {
|
package/dist/rules-consent.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DirectWritableRuleBehavior } from "./permission-rule-vocab.js";
|
|
1
|
+
import { type DirectWritableRuleBehavior, type RemovalLiveness } from "./permission-rule-vocab.js";
|
|
2
2
|
import { type RuleOwner, type ImportPreview, type RedeemedBatchMember, type ImportedSettingsLayer, type PersistedRule, type RuleBehavior, type RuleCandidate, type EffectivePermissionRule, type RemoveResult, type EditedRuleTextPrecheck, type RuleScope, type RuleOffer } from "@sema-agent/core";
|
|
3
3
|
import type { DurableRulePartitionProvider, PermissionRuleStoreProvider, RuleApprovalRecordStore } from "@sema-agent/core";
|
|
4
4
|
import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./plugins/permission-rule-store-sql.js";
|
|
@@ -599,15 +599,36 @@ export type RuleWritten =
|
|
|
599
599
|
{
|
|
600
600
|
status: "persisted" | "no-op";
|
|
601
601
|
rev: number;
|
|
602
|
+
stillLive: Extract<RemovalLiveness, "yes">;
|
|
602
603
|
row: PersistedRuleWireRow;
|
|
603
604
|
}
|
|
605
|
+
/** 同上两个词,但**写后读回时它已经不在了** —— 一次合法并发(自己的 / operator 的撤销挤在了「引擎落了
|
|
606
|
+
* dot」与「读回那一行」之间)。
|
|
607
|
+
*
|
|
608
|
+
* 🔴 B3(core fable 复审 2026-09-20):这一形**不是** `indeterminate`。core 已经答了 `added` 并把 `rev`
|
|
609
|
+
* 交到手里 —— 「写没写」这一问有答案;没答案的是「它现在还站着吗」,而那是**另一个问题**,撤销面早就有
|
|
610
|
+
* 一只键在回答它({@link RemovalLiveness} 三词)。折成 `indeterminate` 的代价有名字:那条腿的处置是
|
|
611
|
+
* 「retry」,而照着重试会把**刚刚被人撤掉的收紧重新立起来**(core `AddResult.failed` 头注逐字警告过
|
|
612
|
+
* 这条路)。
|
|
613
|
+
*
|
|
614
|
+
* 🔴 `row` 在这一支**缺席**,而缺席由 `stillLive` 判别(不是「未知」):没有活行可端的时候编一行出来
|
|
615
|
+
* 才是谎 —— 亲读 core 的 `AddResult`,`added` 臂只有 `{status, rev, dot}`,**刻意**不带行(头注:之后
|
|
616
|
+
* 落地的撤销是「`list()` 答的普通后续事件」),而本仓的行形带着 `status: "live"` 这一格,给一行已经
|
|
617
|
+
* 不在的规则铸它就是第二个谎。 */
|
|
618
|
+
| {
|
|
619
|
+
status: "persisted" | "no-op";
|
|
620
|
+
rev: number;
|
|
621
|
+
stillLive: Extract<RemovalLiveness, "no">;
|
|
622
|
+
row?: undefined;
|
|
623
|
+
}
|
|
604
624
|
/** 裁定性不收(内容判据 / 方向判据)。`detail` 是给人看的那一半,`reason` 是给客户端分流的那一半。 */
|
|
605
625
|
| {
|
|
606
626
|
status: "rejected";
|
|
607
627
|
reason: LocalImportRefusalCode;
|
|
608
628
|
detail: string;
|
|
609
629
|
}
|
|
610
|
-
/**
|
|
630
|
+
/** **不知道落没落**:店在这一条上出了状况(core 的 `failed` 两种强度)。如实编,禁猜(端点侧 503 +
|
|
631
|
+
* 指路 `GET /v1/rules` 对账)。**「写落了随后被撤」不在本臂** —— 那一形有答案,见上面 `stillLive:"no"`。 */
|
|
611
632
|
| {
|
|
612
633
|
status: "indeterminate";
|
|
613
634
|
detail: string;
|
package/dist/rules-consent.js
CHANGED
|
@@ -739,7 +739,7 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
739
739
|
const before = await lane.listRules(input.principal);
|
|
740
740
|
const standing = rowOf(before.rows);
|
|
741
741
|
if (standing !== undefined)
|
|
742
|
-
return { status: "no-op", rev: before.rev, row: standing };
|
|
742
|
+
return { status: "no-op", rev: before.rev, stillLive: "yes", row: standing };
|
|
743
743
|
const screened = screenLocalImportRow(row);
|
|
744
744
|
if ("reason" in screened)
|
|
745
745
|
return { status: "rejected", reason: screened.reason, detail: screened.detail };
|
|
@@ -758,15 +758,17 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
758
758
|
}
|
|
759
759
|
const readBack = await lane.listRules(input.principal);
|
|
760
760
|
const landed = rowOf(readBack.rows);
|
|
761
|
+
const status = added.status === "no-op" ? "no-op" : "persisted";
|
|
761
762
|
if (landed === undefined) {
|
|
762
763
|
localImportLogger.warn("direct_rule_write_vanished_before_read_back", {
|
|
763
764
|
behavior: input.behavior,
|
|
764
765
|
scope: row.scope,
|
|
765
|
-
note: "the store reported this rule as
|
|
766
|
+
note: "the store reported this rule as in effect, and it was already gone when the row was read back — a revocation landed in between",
|
|
767
|
+
wrote: status === "persisted",
|
|
766
768
|
});
|
|
767
|
-
return { status:
|
|
769
|
+
return { status, rev: added.rev, stillLive: "no" };
|
|
768
770
|
}
|
|
769
|
-
return { status: added.
|
|
771
|
+
return { status, rev: added.rev, stillLive: "yes", row: landed };
|
|
770
772
|
},
|
|
771
773
|
};
|
|
772
774
|
return lane;
|
package/dist/run-local.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { DEFAULT_MODEL_REF } from "./model-select.js";
|
|
3
|
-
import {
|
|
3
|
+
import { outwardPlane } from "./terminal.js";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { realpathSync } from "node:fs";
|
|
@@ -535,7 +535,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
535
535
|
print(result.result);
|
|
536
536
|
}
|
|
537
537
|
else {
|
|
538
|
-
const plane =
|
|
538
|
+
const plane = outwardPlane(result.terminal);
|
|
539
539
|
const salvage = result.result || result.salvagedOutput || plane.blockedReason || "";
|
|
540
540
|
if (salvage)
|
|
541
541
|
print(salvage);
|