@sema-agent/server 7.109.0 → 7.111.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 +25 -0
- package/README.md +8 -0
- package/README.zh-CN.md +5 -0
- package/USAGE.md +2 -2
- package/dist/approval-ask-audit-store.d.ts +38 -25
- package/dist/approval-ask-audit-store.js +26 -59
- package/dist/approval-card.d.ts +6 -5
- package/dist/approval-card.js +8 -11
- package/dist/bench/s1/run-firm.d.ts +11 -0
- package/dist/bench/s1/run-firm.js +47 -32
- package/dist/boot/coordinators.js +1 -1
- package/dist/boot/reapers.d.ts +3 -0
- package/dist/boot/reapers.js +3 -2
- package/dist/boot/resolve-spec.js +3 -4
- package/dist/boot/session-faces.d.ts +4 -0
- package/dist/boot/session-faces.js +3 -1
- package/dist/boot/stage-07-capability-layer.d.ts +2 -0
- package/dist/boot/stage-07-capability-layer.js +18 -3
- package/dist/boot/stage-08-reapers.d.ts +2 -0
- package/dist/boot/stage-08-reapers.js +2 -2
- package/dist/boot/stage-09-leader.d.ts +2 -0
- package/dist/boot/stage-09-leader.js +2 -1
- package/dist/boot/stage-10-http-server.d.ts +2 -0
- package/dist/boot/stage-10-http-server.js +2 -1
- package/dist/boot/workflow-orchestration.js +1 -1
- package/dist/config-catalog.js +1 -1
- package/dist/config-types.d.ts +3 -2
- package/dist/config.js +19 -7
- package/dist/deployment-governance.d.ts +8 -2
- package/dist/deployment-governance.js +9 -8
- package/dist/hooks/hook-runner.d.ts +2 -2
- package/dist/hooks/hook-runner.js +40 -26
- package/dist/http/durable-run-leg.js +1 -1
- package/dist/http/resume-legs.js +12 -4
- package/dist/http/routes/runs.d.ts +0 -1
- package/dist/http/routes/runs.js +36 -1
- package/dist/http/routes/sessions.js +12 -2
- package/dist/http/routes/tasks.js +13 -7
- package/dist/http/server.d.ts +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/instruction-selection.d.ts +34 -0
- package/dist/instruction-selection.js +23 -0
- package/dist/instruction-source-notice.d.ts +3 -3
- package/dist/memory-layer-legacy-lock.d.ts +3 -3
- package/dist/model-entry-headers.d.ts +5 -2
- package/dist/model-entry-headers.js +4 -4
- package/dist/model-unreachable-notice.d.ts +2 -2
- package/dist/observability/corrupt-read-seat.d.ts +24 -0
- package/dist/observability/corrupt-read-seat.js +24 -0
- package/dist/observability/fail-open.d.ts +25 -1
- package/dist/observability/fail-open.js +25 -1
- package/dist/observability/metrics.js +2 -1
- package/dist/observability/run-terminal.js +10 -0
- package/dist/operator-ask.d.ts +9 -8
- package/dist/operator-ask.js +9 -1
- package/dist/orchestration/workflow-agent-session-index.d.ts +12 -23
- package/dist/orchestration/workflow-agent-session-index.js +11 -50
- package/dist/orchestration/workflow-notify-journal.d.ts +11 -8
- package/dist/orchestration/workflow-notify-journal.js +20 -36
- package/dist/plugins/file-outcome-sink.d.ts +4 -1
- package/dist/plugins/file-outcome-sink.js +9 -3
- package/dist/plugins/file-run-store.js +4 -4
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/pg-pool.js +15 -0
- package/dist/plugins/retention-store-sql.d.ts +1 -1
- package/dist/plugins/retention-store-sql.js +2 -0
- package/dist/plugins/run-store-sql.d.ts +1 -1
- package/dist/plugins/run-store-sql.js +3 -2
- package/dist/plugins/schema-indexes.js +2 -0
- package/dist/plugins/store-backend.d.ts +4 -0
- package/dist/plugins/store-backend.js +9 -0
- package/dist/plugins/store-contracts.d.ts +24 -0
- package/dist/plugins/store-contracts.js +10 -0
- package/dist/plugins/tidb-pool.js +17 -0
- package/dist/plugins/user-input-ledger-file.d.ts +35 -0
- package/dist/plugins/user-input-ledger-file.js +108 -0
- package/dist/plugins/user-input-ledger-sql.d.ts +54 -0
- package/dist/plugins/user-input-ledger-sql.js +129 -0
- package/dist/project-memory.d.ts +10 -2
- package/dist/project-memory.js +5 -2
- package/dist/run-local.d.ts +18 -5
- package/dist/run-local.js +24 -9
- package/dist/runs.d.ts +8 -1
- package/dist/runs.js +11 -5
- package/dist/runtime-governance.d.ts +20 -8
- package/dist/runtime-governance.js +7 -12
- package/dist/session-records-skipped-notice.d.ts +4 -3
- package/dist/task-settings.d.ts +0 -6
- package/dist/task-settings.js +0 -1
- package/dist/tool-approval.d.ts +8 -1
- package/dist/tool-approval.js +4 -1
- package/dist/trace/core-keyset-guard.d.ts +16 -1
- package/dist/trace/engine-notice-wire.d.ts +30 -35
- package/dist/trace/engine-notice-wire.js +23 -23
- package/dist/trace/ledger-sink.d.ts +4 -0
- package/dist/trace/ledger-sink.js +1 -1
- package/dist/trace/project.d.ts +11 -3
- package/dist/trace/project.js +6 -5
- package/dist/trace/self-minted-wire.d.ts +40 -0
- package/dist/trace/self-minted-wire.js +15 -0
- package/dist/trace/wire-projection-faces.d.ts +3 -3
- package/dist/trace/wire-projection-faces.js +4 -2
- package/dist/user-input-ledger.d.ts +349 -0
- package/dist/user-input-ledger.js +354 -0
- package/package.json +3 -3
|
@@ -6,6 +6,7 @@ import { pgSanitizeText } from "./pg-safe-json.js";
|
|
|
6
6
|
import { toIso as iso } from "./sql-row-helpers.js";
|
|
7
7
|
import { recordFailOpen } from "../observability/fail-open.js";
|
|
8
8
|
import { createObjectColumnReader, decodeJsonColumn, decodeNullableJsonColumn, foldJsonColumn, isJsonColumnBodyProblem, jsonColumnText, readJsonObjectColumn, readNonNullJsonValueColumn } from "./sql-json-column.js";
|
|
9
|
+
import { RUN_STALLED_ERROR } from "./store-contracts.js";
|
|
9
10
|
import { notifyRunTerminal, runTerminalLogLevel } from "../observability/run-terminal.js";
|
|
10
11
|
import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
|
|
11
12
|
import { isDupKeyError } from "./sql-errors.js";
|
|
@@ -423,8 +424,8 @@ export class SqlRunStore {
|
|
|
423
424
|
const cutoff = new Date(Date.now() - olderThanMs);
|
|
424
425
|
let reaped = 0;
|
|
425
426
|
await this.tx(async (conn) => {
|
|
426
|
-
const { affected } = await conn.query(this.q(
|
|
427
|
-
"WHERE status = 'running' AND updated_at < ?",
|
|
427
|
+
const { affected } = await conn.query(this.q(`UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', updated_at = ? ` +
|
|
428
|
+
"WHERE status = 'running' AND updated_at < ?", `UPDATE task_run SET status = 'failed', result = NULL, error = '${RUN_STALLED_ERROR}', updated_at = $1 ` +
|
|
428
429
|
"WHERE status = 'running' AND updated_at < $2"), [new Date(), cutoff]);
|
|
429
430
|
reaped = affected;
|
|
430
431
|
await conn.query(this.q("DELETE ta FROM task_active ta JOIN task_run tr ON ta.task_id = tr.task_id WHERE tr.status NOT IN ('running','suspended','needs_review')", "DELETE FROM task_active ta USING task_run tr WHERE ta.task_id = tr.task_id AND tr.status NOT IN ('running','suspended','needs_review')"));
|
|
@@ -8,6 +8,8 @@ export const CENTRAL_SCHEMA_INDEXES = [
|
|
|
8
8
|
{ table: "checkpoint", name: "idx_checkpoint_session_status", columns: ["session_id", "status"] },
|
|
9
9
|
{ table: "resume_anchor", name: "idx_resume_anchor_session", columns: ["session_id"] },
|
|
10
10
|
{ table: "approval_nonce", name: "idx_approval_nonce_expires", columns: ["expires_at_ms"] },
|
|
11
|
+
{ table: "user_input_acceptance", name: "idx_user_input_acceptance_session", columns: ["session_id", "state", "accepted_at_ms"] },
|
|
12
|
+
{ table: "user_input_acceptance", name: "idx_user_input_acceptance_state", columns: ["state", "accepted_at_ms", "accept_id"] },
|
|
11
13
|
{ table: "sendfile_link", name: "idx_sendfile_link_scope", columns: ["scope", "id"] },
|
|
12
14
|
{ table: "session_policy", name: "idx_session_policy_session", columns: ["session_id"] },
|
|
13
15
|
{ table: "session_policy_rev_mark", name: "idx_session_policy_rev_mark_session", columns: ["session_id"] },
|
|
@@ -12,6 +12,7 @@ import { type ApprovalNonceStore } from "./approval-nonce-store.js";
|
|
|
12
12
|
import { type ApprovalAskStore as ApprovalAskStoreType } from "./approval-ask-store-sql.js";
|
|
13
13
|
import { type MemoryOptOutGrantStore } from "./memory-optout-grant-store-sql.js";
|
|
14
14
|
import { type SendFileLedger } from "./send-file-ledger.js";
|
|
15
|
+
import type { UserInputLedgerStore } from "../user-input-ledger.js";
|
|
15
16
|
import { TiDBResumeAnchorStore, PgResumeAnchorStore } from "./resume-anchor-store-sql.js";
|
|
16
17
|
import { type SessionPolicyStoreLogger } from "./session-policy-store-sql.js";
|
|
17
18
|
import type { ServiceConfig } from "../config-types.js";
|
|
@@ -195,6 +196,9 @@ export interface StoreBackend {
|
|
|
195
196
|
/** SendUserFile scope↔object ledger (multi-tenant list/revoke handle; the hashed key segment hides the
|
|
196
197
|
* mapping from URLs). REQUIRED on all backends (works local — one JSONL, like approvalExemption). */
|
|
197
198
|
sendFileLedger(): SendFileLedger;
|
|
199
|
+
/** S-928 用户输入受理账本的存储孪生(「200 applied」受理的影子;语义属主 `src/user-input-ledger.ts`)。REQUIRED on all backends ——
|
|
200
|
+
* local = File(数据根下 `user-input-ledger/ledger.jsonl`,singleton:追加 fd 与内存折叠都在实例里),SQL = 表 `user_input_acceptance`。 */
|
|
201
|
+
userInputLedger(): UserInputLedgerStore;
|
|
198
202
|
/** E6 durable SessionPolicyStore (operator-tightened per-session tool rules). REQUIRED on all backends — works local
|
|
199
203
|
* (local = core's FileSessionPolicyStore), like resumeAnchor. Backs core's RunnerDeps.sessionPolicyStore.
|
|
200
204
|
* `logger`:SQL 孪生抹除封键那一臂(S-891)的点名口(缺席 ⇒ 店退回 `console.warn`);local(core 的 File 店)不收。 */
|
|
@@ -23,6 +23,8 @@ import { FileApprovalAskStore } from "./approval-ask-store-file.js";
|
|
|
23
23
|
import { TiDBMemoryOptOutGrantStore, PgMemoryOptOutGrantStore } from "./memory-optout-grant-store-sql.js";
|
|
24
24
|
import { TiDBSessionCaptureRecordStore, PgSessionCaptureRecordStore } from "./session-capture-record-store-sql.js";
|
|
25
25
|
import { TiDBSendFileLedger, PgSendFileLedger, FileSendFileLedger } from "./send-file-ledger.js";
|
|
26
|
+
import { FileUserInputLedgerStore } from "./user-input-ledger-file.js";
|
|
27
|
+
import { TiDBUserInputLedgerStore, PgUserInputLedgerStore } from "./user-input-ledger-sql.js";
|
|
26
28
|
import { TiDBResumeAnchorStore, PgResumeAnchorStore } from "./resume-anchor-store-sql.js";
|
|
27
29
|
import { TiDBSessionPolicyStore, PgSessionPolicyStore } from "./session-policy-store-sql.js";
|
|
28
30
|
import { TiDBWorkflowJournalStore, PgWorkflowJournalStore } from "./workflow-journal-store-sql.js";
|
|
@@ -123,6 +125,7 @@ class TiDBBackend {
|
|
|
123
125
|
approvalAsk() { return new TiDBApprovalAskStore(this.pool); }
|
|
124
126
|
permissionRule() { return createSqlPermissionRuleStores(mysqlDriver(this.pool)); }
|
|
125
127
|
sendFileLedger() { return new TiDBSendFileLedger(this.pool); }
|
|
128
|
+
userInputLedger() { return new TiDBUserInputLedgerStore(this.pool); }
|
|
126
129
|
sessionPolicy(logger) { return new TiDBSessionPolicyStore(this.pool, logger); }
|
|
127
130
|
fileHistory() { return new TiDBFileHistoryStore(this.pool, historyBlobBackend(this.config, "tidb", this.pool), this.config.fileHistory); }
|
|
128
131
|
memoryOptOutGrant() { return new TiDBMemoryOptOutGrantStore(this.pool); }
|
|
@@ -171,6 +174,7 @@ class PgBackend {
|
|
|
171
174
|
approvalAsk() { return new PgApprovalAskStore(this.pool); }
|
|
172
175
|
permissionRule() { return createSqlPermissionRuleStores(pgDriver(this.pool)); }
|
|
173
176
|
sendFileLedger() { return new PgSendFileLedger(this.pool); }
|
|
177
|
+
userInputLedger() { return new PgUserInputLedgerStore(this.pool); }
|
|
174
178
|
sessionPolicy(logger) { return new PgSessionPolicyStore(this.pool, logger); }
|
|
175
179
|
fileHistory() { return new PgFileHistoryStore(this.pool, historyBlobBackend(this.config, "pg", this.pool), this.config.fileHistory); }
|
|
176
180
|
memoryOptOutGrant() { return new PgMemoryOptOutGrantStore(this.pool); }
|
|
@@ -212,6 +216,7 @@ class LocalBackend {
|
|
|
212
216
|
approvalNonceStore;
|
|
213
217
|
approvalAskStore;
|
|
214
218
|
sendFileLedgerStore;
|
|
219
|
+
userInputLedgerStore;
|
|
215
220
|
workflowJournalStore;
|
|
216
221
|
outcomeSinkInst;
|
|
217
222
|
onWarn;
|
|
@@ -249,6 +254,7 @@ class LocalBackend {
|
|
|
249
254
|
this.approvalNonceStore = new FileApprovalNonceStore(this.fileBackend.root, { warn: corruptReadWarn });
|
|
250
255
|
this.approvalAskStore = new FileApprovalAskStore(this.fileBackend.root, { warn: corruptReadWarn });
|
|
251
256
|
this.sendFileLedgerStore = new FileSendFileLedger(this.fileBackend.root, { warn: corruptReadWarn });
|
|
257
|
+
this.userInputLedgerStore = new FileUserInputLedgerStore(this.fileBackend.root, { warn: corruptReadWarn });
|
|
252
258
|
this.workflowJournalStore = new FileWorkflowJournalStore(this.fileBackend.root, undefined, {
|
|
253
259
|
onCorruptRead: createCorruptReadSeat((msg, fields) => this.discloseWarn(msg, fields), "workflow_journal_corrupt_read"),
|
|
254
260
|
});
|
|
@@ -270,6 +276,8 @@ class LocalBackend {
|
|
|
270
276
|
this.approvalNonceStore.dispose();
|
|
271
277
|
this.approvalAskStore.dispose();
|
|
272
278
|
this.sendFileLedgerStore.dispose();
|
|
279
|
+
this.userInputLedgerStore.dispose();
|
|
280
|
+
this.outcomeSinkInst.dispose();
|
|
273
281
|
this.workflowJournalStore.dispose();
|
|
274
282
|
this.checkpointStoreInst?.close();
|
|
275
283
|
this.permissionRuleStoresInst?.dispose();
|
|
@@ -291,6 +299,7 @@ class LocalBackend {
|
|
|
291
299
|
}
|
|
292
300
|
permissionRuleStoresInst;
|
|
293
301
|
sendFileLedger() { return this.sendFileLedgerStore; }
|
|
302
|
+
userInputLedger() { return this.userInputLedgerStore; }
|
|
294
303
|
checkpoint() {
|
|
295
304
|
if (!this.checkpointStoreInst) {
|
|
296
305
|
const cs = new LocalCheckpointStore(this.fileBackend.root, {
|
|
@@ -104,6 +104,30 @@ export interface RunCancelContext {
|
|
|
104
104
|
export type PersistedTaskResult = TaskResult & {
|
|
105
105
|
cancelContext?: RunCancelContext;
|
|
106
106
|
};
|
|
107
|
+
/**
|
|
108
|
+
* 🔴 run 店「持有它的实例丢了」那一判的**行上落款**(S-928 codex r1-F1 提为常量):三只 run 店的 `reapStale` 把心跳过期的 running 行翻成
|
|
109
|
+
* `failed` 并写下这一句。它是 run 店死亡判决留在行上的证据之一(另一只 = {@link RUN_ORPHANED_ERROR})—— 用户输入受理账本的收敛判词
|
|
110
|
+
* (S-969 L8 起两车道同一只)据 {@link isHolderDeathVerdict} 在 reap **之后**仍能判出「写者死了」。改这一句 = 改那条判据;所以三个写者与那一个读者都只引用本常量。读时折叠成同一句话的 wire 展示站点
|
|
111
|
+
* (`routes/runs.ts` 的 stale 折叠、`routes/trace-usage.ts` 的 WORKER_DOWN 帧)刻意留字面量:它们由文案冻结门逐字钉着,且不参与任何判据。
|
|
112
|
+
*/
|
|
113
|
+
export declare const RUN_STALLED_ERROR = "run stalled (instance lost?)";
|
|
114
|
+
/**
|
|
115
|
+
* 🔴 File 车道「持有它的进程随重启没了」那一判的行上落款(S-969 L8 提为常量):boot 孤儿回收(`FileRunStore.reclaimOrphanedAtBoot`,
|
|
116
|
+
* 数据根 LOCK 作证前世进程已死 ⇒ 仍 running 的行无条件判孤儿)与运行期 claim 接管的 running 臂(`judgeClaim`,外来 + owner 已死 + 无可续)
|
|
117
|
+
* 都写这一句;接管臂在括号里附上前任持有者的进程指纹(取证用,形 = {@link buildRunOrphanedError})。与 {@link RUN_STALLED_ERROR} 同一种身份:
|
|
118
|
+
* run 店写在行上的**死亡判决**,读者 = {@link isHolderDeathVerdict}。
|
|
119
|
+
*/
|
|
120
|
+
export declare const RUN_ORPHANED_ERROR = "run orphaned by engine restart";
|
|
121
|
+
/** File 车道两个孤儿落款写口共用的那一句(`heldBy` = 接管臂的前任持有者描述;boot 回收臂不带)。 */
|
|
122
|
+
export declare function buildRunOrphanedError(heldBy?: string): string;
|
|
123
|
+
/**
|
|
124
|
+
* run 行上是不是一份**持有者死亡判决**:`failed`,且 `error` 是 run 店的两只死亡落款之一 —— {@link RUN_STALLED_ERROR}(三只 run 店的
|
|
125
|
+
* `reapStale`:心跳过期 ⇒ 实例丢了)或 {@link RUN_ORPHANED_ERROR}(File 车道:前世进程随重启没了;接管臂带括注)。判决只说「持有它的那个进程
|
|
126
|
+
* 死在这条腿收场之前」,不说是谁 —— 持有者是谁读行上的 `instanceId`(两只落款都不改它)。唯一读者 = 用户输入受理账本的收敛判词
|
|
127
|
+
* (`src/user-input-ledger.ts` `judgeOpen`);改任一句落款 = 改那条判据,所以写者与读者都只引用本模块的常量。
|
|
128
|
+
* 正常失败(别的 error)/ park 过期收割(`approval expired before decision`:腿 park 过)都**不是**死亡判决。
|
|
129
|
+
*/
|
|
130
|
+
export declare function isHolderDeathVerdict(run: Pick<RunRecord, "status" | "error">): boolean;
|
|
107
131
|
export interface RunRecord {
|
|
108
132
|
taskId: string;
|
|
109
133
|
sessionId: string;
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
export class StoreBootRefusalError extends Error {
|
|
2
2
|
}
|
|
3
|
+
export const RUN_STALLED_ERROR = "run stalled (instance lost?)";
|
|
4
|
+
export const RUN_ORPHANED_ERROR = "run orphaned by engine restart";
|
|
5
|
+
export function buildRunOrphanedError(heldBy) {
|
|
6
|
+
return heldBy === undefined ? RUN_ORPHANED_ERROR : `${RUN_ORPHANED_ERROR} (claim held by ${heldBy}, which is gone)`;
|
|
7
|
+
}
|
|
8
|
+
export function isHolderDeathVerdict(run) {
|
|
9
|
+
if (run.status !== "failed" || run.error === null)
|
|
10
|
+
return false;
|
|
11
|
+
return run.error === RUN_STALLED_ERROR || run.error === RUN_ORPHANED_ERROR || run.error.startsWith(`${RUN_ORPHANED_ERROR} (`);
|
|
12
|
+
}
|
|
3
13
|
export const RUN_STATUS_CLASS = {
|
|
4
14
|
running: "live",
|
|
5
15
|
completed: "succeeded",
|
|
@@ -349,6 +349,23 @@ export const SCHEMA_STATEMENTS = [
|
|
|
349
349
|
expires_at_ms BIGINT NOT NULL,
|
|
350
350
|
PRIMARY KEY (session_id, nonce),
|
|
351
351
|
KEY idx_approval_nonce_expires (expires_at_ms)
|
|
352
|
+
) COLLATE utf8mb4_bin`,
|
|
353
|
+
`CREATE TABLE IF NOT EXISTS user_input_acceptance (
|
|
354
|
+
accept_id VARCHAR(64) NOT NULL,
|
|
355
|
+
input_kind VARCHAR(16) NOT NULL,
|
|
356
|
+
message_id VARCHAR(128) NOT NULL,
|
|
357
|
+
session_id VARCHAR(64) NOT NULL,
|
|
358
|
+
task_id VARCHAR(64) NOT NULL,
|
|
359
|
+
writer VARCHAR(64) NOT NULL,
|
|
360
|
+
accepted_at_ms BIGINT NOT NULL,
|
|
361
|
+
state VARCHAR(16) NOT NULL,
|
|
362
|
+
state_at_ms BIGINT NULL,
|
|
363
|
+
settle_how VARCHAR(16) NULL,
|
|
364
|
+
announced_at_ms BIGINT NULL,
|
|
365
|
+
announced_task_id VARCHAR(64) NULL,
|
|
366
|
+
PRIMARY KEY (accept_id),
|
|
367
|
+
KEY idx_user_input_acceptance_session (session_id, state, accepted_at_ms),
|
|
368
|
+
KEY idx_user_input_acceptance_state (state, accepted_at_ms, accept_id)
|
|
352
369
|
) COLLATE utf8mb4_bin`,
|
|
353
370
|
`CREATE TABLE IF NOT EXISTS sendfile_link (
|
|
354
371
|
id VARCHAR(64) NOT NULL,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Logger } from "../observability/logger.js";
|
|
2
|
+
import { UserInputLedgerFold, type NewUserInputAcceptance, type OpenScanCursor, type OpenScanPage, type SettleHow, type UserInputAcceptance, type UserInputLedgerRecord, type UserInputLedgerStore } from "../user-input-ledger.js";
|
|
3
|
+
/** 账本文件名(取证按文件字节对账 —— 崩溃语义的真源是文件)。 */
|
|
4
|
+
export declare const USER_INPUT_LEDGER_FILE = "ledger.jsonl";
|
|
5
|
+
/** 纯内存孪生(测试 / 夹具)。 */
|
|
6
|
+
export declare class MemoryUserInputLedgerStore implements UserInputLedgerStore {
|
|
7
|
+
protected readonly fold: UserInputLedgerFold;
|
|
8
|
+
/** 落一行:守卫不过 ⇒ false、零写;过了 ⇒ 先交给 {@link persist}(File 孪生在这里 fsync),再翻折叠。 */
|
|
9
|
+
protected write(rec: UserInputLedgerRecord): Promise<boolean>;
|
|
10
|
+
/** 持久面挂点(Memory = 无)。抛 ⇒ 这一行不存在,折叠不动。 */
|
|
11
|
+
protected persist(_rec: UserInputLedgerRecord): void;
|
|
12
|
+
accept(a: NewUserInputAcceptance): Promise<void>;
|
|
13
|
+
settle(acceptId: string, how: SettleHow, atMs: number): Promise<boolean>;
|
|
14
|
+
converge(acceptId: string, atMs: number): Promise<boolean>;
|
|
15
|
+
announce(acceptId: string, taskId: string, atMs: number): Promise<boolean>;
|
|
16
|
+
listOpen(after?: OpenScanCursor, limit?: number): Promise<OpenScanPage>;
|
|
17
|
+
listCrashed(sessionId: string): Promise<UserInputAcceptance[]>;
|
|
18
|
+
listUnannounced(sessionId: string, limit: number): Promise<UserInputAcceptance[]>;
|
|
19
|
+
/** S-965:会话树删除 = 一行会话级墓碑(守卫:该会话此刻有行 ⇒ 落;否则零写、回 0)。写抛 ⇒ 原样抛(协调器据此不删会话本体)。 */
|
|
20
|
+
deleteBySession(sessionId: string): Promise<number>;
|
|
21
|
+
}
|
|
22
|
+
/** local 车道(File)孪生。 */
|
|
23
|
+
export declare class FileUserInputLedgerStore extends MemoryUserInputLedgerStore {
|
|
24
|
+
/** 账本绝对路径。 */
|
|
25
|
+
readonly path: string;
|
|
26
|
+
private log;
|
|
27
|
+
/** @param opts.warn 腐读披露座(缺席 ⇒ 重放属主退回 `console.warn`)。 */
|
|
28
|
+
constructor(root: string, opts?: {
|
|
29
|
+
warn?: Logger["warn"];
|
|
30
|
+
});
|
|
31
|
+
protected persist(rec: UserInputLedgerRecord): void;
|
|
32
|
+
/** 收追加口(LocalBackend.close → 优雅重启重开)。core `AppendLog.close()` 自己吞 `closeSync` 的错,不抛。 */
|
|
33
|
+
dispose(): void;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=user-input-ledger-file.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { AppendLog, ensureDir } from "@sema-agent/core";
|
|
3
|
+
import { replayJsonlSkipping } from "../observability/corrupt-read-seat.js";
|
|
4
|
+
import { CRASHED_CAUSE, SETTLE_HOW_WORDS, USER_INPUT_KIND_WORDS, USER_INPUT_LEDGER_DIR, USER_INPUT_OPEN_SCAN_MAX, USER_INPUT_SESSION_READ_MAX, UserInputLedgerFold, isServingLayerUndrainedCause, } from "../user-input-ledger.js";
|
|
5
|
+
export const USER_INPUT_LEDGER_FILE = "ledger.jsonl";
|
|
6
|
+
function duplicateAcceptance(acceptId) {
|
|
7
|
+
return new Error(`user-input ledger: acceptId ${acceptId} is already recorded — an acceptance id is one acceptance`);
|
|
8
|
+
}
|
|
9
|
+
export class MemoryUserInputLedgerStore {
|
|
10
|
+
fold = new UserInputLedgerFold();
|
|
11
|
+
async write(rec) {
|
|
12
|
+
if (!this.fold.admits(rec))
|
|
13
|
+
return false;
|
|
14
|
+
this.persist(rec);
|
|
15
|
+
this.fold.apply(rec);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
persist(_rec) { }
|
|
19
|
+
async accept(a) {
|
|
20
|
+
const rec = { kind: "accepted", acceptId: a.acceptId, input: a.input, messageId: a.messageId, sessionId: a.sessionId, taskId: a.taskId, writer: a.writer, at: a.acceptedAtMs };
|
|
21
|
+
if (!(await this.write(rec)))
|
|
22
|
+
throw duplicateAcceptance(a.acceptId);
|
|
23
|
+
}
|
|
24
|
+
settle(acceptId, how, atMs) {
|
|
25
|
+
return this.write({ kind: "settled", acceptId, how, at: atMs });
|
|
26
|
+
}
|
|
27
|
+
converge(acceptId, atMs) {
|
|
28
|
+
return this.write({ kind: "converged", acceptId, cause: CRASHED_CAUSE, at: atMs });
|
|
29
|
+
}
|
|
30
|
+
announce(acceptId, taskId, atMs) {
|
|
31
|
+
return this.write({ kind: "announced", acceptId, taskId, at: atMs });
|
|
32
|
+
}
|
|
33
|
+
async listOpen(after, limit = USER_INPUT_OPEN_SCAN_MAX) {
|
|
34
|
+
return this.fold.listOpen(after, limit);
|
|
35
|
+
}
|
|
36
|
+
async listCrashed(sessionId) {
|
|
37
|
+
return this.fold.listCrashed(sessionId, USER_INPUT_SESSION_READ_MAX);
|
|
38
|
+
}
|
|
39
|
+
async listUnannounced(sessionId, limit) {
|
|
40
|
+
return this.fold.listUnannounced(sessionId, limit);
|
|
41
|
+
}
|
|
42
|
+
async deleteBySession(sessionId) {
|
|
43
|
+
const n = this.fold.sessionSize(sessionId);
|
|
44
|
+
return (await this.write({ kind: "erased", sessionId, at: Date.now() })) ? n : 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const RECORD_SHAPE = {
|
|
48
|
+
what: "user-input ledger record of an unknown kind or with a missing / out-of-vocabulary member",
|
|
49
|
+
read(record) {
|
|
50
|
+
const o = record;
|
|
51
|
+
const str = (k) => (typeof o[k] === "string" && o[k].length > 0 ? o[k] : undefined);
|
|
52
|
+
const at = typeof o.at === "number" && Number.isFinite(o.at) ? o.at : undefined;
|
|
53
|
+
if (at === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (o.kind === "erased") {
|
|
56
|
+
const sessionId = str("sessionId");
|
|
57
|
+
return sessionId === undefined ? undefined : { kind: "erased", sessionId, at };
|
|
58
|
+
}
|
|
59
|
+
const acceptId = str("acceptId");
|
|
60
|
+
if (acceptId === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
switch (o.kind) {
|
|
63
|
+
case "accepted": {
|
|
64
|
+
const input = typeof o.input === "string" && Object.hasOwn(USER_INPUT_KIND_WORDS, o.input) ? o.input : undefined;
|
|
65
|
+
const messageId = str("messageId");
|
|
66
|
+
const sessionId = str("sessionId");
|
|
67
|
+
const taskId = str("taskId");
|
|
68
|
+
const writer = str("writer");
|
|
69
|
+
if (input === undefined || messageId === undefined || sessionId === undefined || taskId === undefined || writer === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
return { kind: "accepted", acceptId, input, messageId, sessionId, taskId, writer, at };
|
|
72
|
+
}
|
|
73
|
+
case "settled": {
|
|
74
|
+
const how = typeof o.how === "string" && Object.hasOwn(SETTLE_HOW_WORDS, o.how) ? o.how : undefined;
|
|
75
|
+
return how === undefined ? undefined : { kind: "settled", acceptId, how, at };
|
|
76
|
+
}
|
|
77
|
+
case "converged":
|
|
78
|
+
return isServingLayerUndrainedCause(o.cause) ? { kind: "converged", acceptId, cause: o.cause, at } : undefined;
|
|
79
|
+
case "announced": {
|
|
80
|
+
const taskId = str("taskId");
|
|
81
|
+
return taskId === undefined ? undefined : { kind: "announced", acceptId, taskId, at };
|
|
82
|
+
}
|
|
83
|
+
default:
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
export class FileUserInputLedgerStore extends MemoryUserInputLedgerStore {
|
|
89
|
+
path;
|
|
90
|
+
log;
|
|
91
|
+
constructor(root, opts) {
|
|
92
|
+
super();
|
|
93
|
+
const dir = join(root, USER_INPUT_LEDGER_DIR);
|
|
94
|
+
ensureDir(dir);
|
|
95
|
+
this.path = join(dir, USER_INPUT_LEDGER_FILE);
|
|
96
|
+
for (const rec of replayJsonlSkipping(this.path, "user-input-ledger", RECORD_SHAPE, opts?.warn).records)
|
|
97
|
+
this.fold.apply(rec);
|
|
98
|
+
}
|
|
99
|
+
persist(rec) {
|
|
100
|
+
this.log ??= new AppendLog(this.path);
|
|
101
|
+
this.log.append(rec, true);
|
|
102
|
+
}
|
|
103
|
+
dispose() {
|
|
104
|
+
this.log?.close();
|
|
105
|
+
this.log = undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=user-input-ledger-file.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S-928 用户输入受理账本的 **SQL** 孪生(单文件双方言,[ref] A12 收拢形;两条语句并排写死,方言差异显式)。
|
|
3
|
+
* 语义属主 = `src/user-input-ledger.ts`;File / Memory 两只在 `user-input-ledger-file.ts`。四只孪生吃同一组试剂盒断言
|
|
4
|
+
* (`test/helpers/user-input-ledger-kit.ts`;本孪生的真双库腿在 `test/store-contract-kit-db-integration.test.ts`)。
|
|
5
|
+
*
|
|
6
|
+
* ## 一张表,一行一受理(四形行落在列上)
|
|
7
|
+
* File 孪生把四形行逐行追加;这里把同一组转移落在**一行**的列上:`accepted` = INSERT(`state='open'`),`settled` / `converged` =
|
|
8
|
+
* `state` 列的 CAS(`WHERE state='open'`,互斥、先到者赢),`announced` = `announced_*` 两列的 CAS(`WHERE state='converged' AND
|
|
9
|
+
* announced_at_ms IS NULL`,至多一次)。File 的「行序 = 语义序」在这里由 CAS 谓词结构性给出 —— 两者同义的证据是同一组试剂盒格。
|
|
10
|
+
* 多副本并发:两台副本同时收敛同一行 ⇒ 单赢者;settle 与 converge 互斥(同一行只会从 open 走出去一次)。
|
|
11
|
+
*
|
|
12
|
+
* ## 身份列
|
|
13
|
+
* 无 owner / principal / scope 列(行随它挂靠的会话走 —— 收编形Ⅲ);`writer` 是受理进程的 `instanceId`(随进程消亡,不是租户轴)。
|
|
14
|
+
* 行里不存输入正文(只有身份与时刻)。
|
|
15
|
+
*
|
|
16
|
+
* ## 会话树删除(S-965)
|
|
17
|
+
* `deleteBySession` = `DELETE … WHERE session_id = ?`(真删行;File 孪生是一行会话级墓碑,同义)。两条删会话树的级联各有一臂:在线删除协调器
|
|
18
|
+
* 经本动词;托管留存的事务化孪生(`retention-store-sql.ts` `purgeSessionTrees` ④)在**自己的事务**里对本表发同一只谓词(`session_id` 单键,
|
|
19
|
+
* 多值集合按方言)—— 它不调店,理由见该文件顶注的布局裁定(一个事务 ⇒ 一条连接 ⇒ 一个属主)。谓词走 `(session_id, state, accepted_at_ms)` 索引的前缀。
|
|
20
|
+
*
|
|
21
|
+
* ## 读不出的行(S-969 L6):逐行跳过,游标按原始行推进
|
|
22
|
+
* 三只读口都按 `state` 列取行(`= 'open'` / `= 'converged'`),所以 `state` 词表外的行任何读口都取不到;被取到的行上读不出的列 —— `input_kind`
|
|
23
|
+
* 词表外(新版本写的输入族:滚动发布 / 回滚的混版窗)、收敛行缺 `state_at_ms` —— 由 {@link readRows} 逐行跳过 + 计 F 类 tag
|
|
24
|
+
* `server.user-input-ledger.corrupt-record-skipped`(File 孪生重放坏行的同一只登记座),其余行照交。修前一行读不出 ⇒ 整页抛:收敛断点不前进
|
|
25
|
+
* (其后的 open 行永不收敛)、同会话的通告与读面一并失效。`listOpen` 的续翻游标取**原始**末行的键(`accept_id` / `accepted_at_ms` 是 NOT NULL
|
|
26
|
+
* 键列,坏行也读得出),跳过的行照样推进;页里少了几行不等于扫到底(那由原始行数判)。SQL 文本 / DDL 不变。
|
|
27
|
+
*/
|
|
28
|
+
import type { Pool as MysqlPool } from "mysql2/promise";
|
|
29
|
+
import type { Pool as PgPool } from "pg";
|
|
30
|
+
import { type SqlDriver } from "./sql-driver.js";
|
|
31
|
+
import { type NewUserInputAcceptance, type OpenScanCursor, type OpenScanPage, type SettleHow, type UserInputAcceptance, type UserInputLedgerStore } from "../user-input-ledger.js";
|
|
32
|
+
export declare const USER_INPUT_ACCEPTANCE_TABLE = "user_input_acceptance";
|
|
33
|
+
export declare class SqlUserInputLedgerStore implements UserInputLedgerStore {
|
|
34
|
+
private readonly db;
|
|
35
|
+
constructor(db: SqlDriver);
|
|
36
|
+
private q;
|
|
37
|
+
accept(a: NewUserInputAcceptance): Promise<void>;
|
|
38
|
+
settle(acceptId: string, how: SettleHow, atMs: number): Promise<boolean>;
|
|
39
|
+
converge(acceptId: string, atMs: number): Promise<boolean>;
|
|
40
|
+
announce(acceptId: string, taskId: string, atMs: number): Promise<boolean>;
|
|
41
|
+
listOpen(after?: OpenScanCursor, limit?: number): Promise<OpenScanPage>;
|
|
42
|
+
listCrashed(sessionId: string): Promise<UserInputAcceptance[]>;
|
|
43
|
+
listUnannounced(sessionId: string, limit: number): Promise<UserInputAcceptance[]>;
|
|
44
|
+
deleteBySession(sessionId: string): Promise<number>;
|
|
45
|
+
}
|
|
46
|
+
/** MySQL 协议腿的构造绑定。 */
|
|
47
|
+
export declare class TiDBUserInputLedgerStore extends SqlUserInputLedgerStore {
|
|
48
|
+
constructor(pool: MysqlPool);
|
|
49
|
+
}
|
|
50
|
+
/** PostgreSQL 腿的构造绑定。 */
|
|
51
|
+
export declare class PgUserInputLedgerStore extends SqlUserInputLedgerStore {
|
|
52
|
+
constructor(pool: PgPool);
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=user-input-ledger-sql.d.ts.map
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { recordFailOpen } from "../observability/fail-open.js";
|
|
2
|
+
import { isDupKeyError } from "./sql-errors.js";
|
|
3
|
+
import { mysqlDriver, pgDriver } from "./sql-driver.js";
|
|
4
|
+
import { assertPageLimit, CRASHED_CAUSE, SETTLE_HOW_WORDS, USER_INPUT_KIND_WORDS, USER_INPUT_OPEN_SCAN_MAX, USER_INPUT_SESSION_READ_MAX, } from "../user-input-ledger.js";
|
|
5
|
+
export const USER_INPUT_ACCEPTANCE_TABLE = "user_input_acceptance";
|
|
6
|
+
const COLUMNS = "accept_id, input_kind, message_id, session_id, task_id, writer, accepted_at_ms, state, state_at_ms, settle_how, announced_at_ms, announced_task_id";
|
|
7
|
+
function rowState(r) {
|
|
8
|
+
const state = String(r["state"]);
|
|
9
|
+
const at = r["state_at_ms"] == null ? Number.NaN : Number(r["state_at_ms"]);
|
|
10
|
+
switch (state) {
|
|
11
|
+
case "open":
|
|
12
|
+
return { kind: "open" };
|
|
13
|
+
case "settled": {
|
|
14
|
+
const how = String(r["settle_how"]);
|
|
15
|
+
if (!Object.hasOwn(SETTLE_HOW_WORDS, how) || !Number.isFinite(at))
|
|
16
|
+
return undefined;
|
|
17
|
+
return { kind: "settled", how: how, atMs: at };
|
|
18
|
+
}
|
|
19
|
+
case "converged":
|
|
20
|
+
if (!Number.isFinite(at))
|
|
21
|
+
return undefined;
|
|
22
|
+
return { kind: "converged", cause: CRASHED_CAUSE, atMs: at };
|
|
23
|
+
default:
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function readRow(r) {
|
|
28
|
+
const input = String(r["input_kind"]);
|
|
29
|
+
if (!Object.hasOwn(USER_INPUT_KIND_WORDS, input))
|
|
30
|
+
return { ok: false, reason: `input_kind ${input} is not a ledger word` };
|
|
31
|
+
const state = rowState(r);
|
|
32
|
+
if (state === undefined)
|
|
33
|
+
return { ok: false, reason: `unreadable state (${String(r["state"])} / ${String(r["settle_how"])} / ${String(r["state_at_ms"])})` };
|
|
34
|
+
const announcedAt = r["announced_at_ms"] == null ? undefined : Number(r["announced_at_ms"]);
|
|
35
|
+
return {
|
|
36
|
+
ok: true,
|
|
37
|
+
acceptance: {
|
|
38
|
+
acceptId: String(r["accept_id"]),
|
|
39
|
+
input: input,
|
|
40
|
+
messageId: String(r["message_id"]),
|
|
41
|
+
sessionId: String(r["session_id"]),
|
|
42
|
+
taskId: String(r["task_id"]),
|
|
43
|
+
writer: String(r["writer"]),
|
|
44
|
+
acceptedAtMs: Number(r["accepted_at_ms"]),
|
|
45
|
+
state,
|
|
46
|
+
...(announcedAt !== undefined ? { announced: { atMs: announcedAt, taskId: String(r["announced_task_id"]) } } : {}),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function readRows(rows, op) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const r of rows) {
|
|
53
|
+
const read = readRow(r);
|
|
54
|
+
if (read.ok)
|
|
55
|
+
out.push(read.acceptance);
|
|
56
|
+
else
|
|
57
|
+
recordFailOpen("server.user-input-ledger.corrupt-record-skipped", `table=${USER_INPUT_ACCEPTANCE_TABLE} op=${op} accept_id=${String(r["accept_id"])} ${read.reason}`);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function rawCursor(r) {
|
|
62
|
+
const acceptedAtMs = Number(r["accepted_at_ms"]);
|
|
63
|
+
if (!Number.isFinite(acceptedAtMs))
|
|
64
|
+
throw new Error(`${USER_INPUT_ACCEPTANCE_TABLE} row ${String(r["accept_id"])}: accepted_at_ms ${String(r["accepted_at_ms"])} is not a number — cannot page past it`);
|
|
65
|
+
return { acceptedAtMs, acceptId: String(r["accept_id"]) };
|
|
66
|
+
}
|
|
67
|
+
export class SqlUserInputLedgerStore {
|
|
68
|
+
db;
|
|
69
|
+
constructor(db) {
|
|
70
|
+
this.db = db;
|
|
71
|
+
}
|
|
72
|
+
q(tidb, pg) {
|
|
73
|
+
return this.db.dialect === "tidb" ? tidb : pg;
|
|
74
|
+
}
|
|
75
|
+
async accept(a) {
|
|
76
|
+
try {
|
|
77
|
+
await this.db.query(this.q(`INSERT INTO ${USER_INPUT_ACCEPTANCE_TABLE} (accept_id, input_kind, message_id, session_id, task_id, writer, accepted_at_ms, state) VALUES (?,?,?,?,?,?,?,'open')`, `INSERT INTO ${USER_INPUT_ACCEPTANCE_TABLE} (accept_id, input_kind, message_id, session_id, task_id, writer, accepted_at_ms, state) VALUES ($1,$2,$3,$4,$5,$6,$7,'open')`), [a.acceptId, a.input, a.messageId, a.sessionId, a.taskId, a.writer, a.acceptedAtMs]);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (isDupKeyError(this.db.dialect, err))
|
|
81
|
+
throw new Error(`user-input ledger: acceptId ${a.acceptId} is already recorded — an acceptance id is one acceptance`);
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async settle(acceptId, how, atMs) {
|
|
86
|
+
const res = await this.db.query(this.q(`UPDATE ${USER_INPUT_ACCEPTANCE_TABLE} SET state = 'settled', state_at_ms = ?, settle_how = ? WHERE accept_id = ? AND state = 'open'`, `UPDATE ${USER_INPUT_ACCEPTANCE_TABLE} SET state = 'settled', state_at_ms = $1, settle_how = $2 WHERE accept_id = $3 AND state = 'open'`), [atMs, how, acceptId]);
|
|
87
|
+
return res.affected > 0;
|
|
88
|
+
}
|
|
89
|
+
async converge(acceptId, atMs) {
|
|
90
|
+
const res = await this.db.query(this.q(`UPDATE ${USER_INPUT_ACCEPTANCE_TABLE} SET state = 'converged', state_at_ms = ? WHERE accept_id = ? AND state = 'open'`, `UPDATE ${USER_INPUT_ACCEPTANCE_TABLE} SET state = 'converged', state_at_ms = $1 WHERE accept_id = $2 AND state = 'open'`), [atMs, acceptId]);
|
|
91
|
+
return res.affected > 0;
|
|
92
|
+
}
|
|
93
|
+
async announce(acceptId, taskId, atMs) {
|
|
94
|
+
const res = await this.db.query(this.q(`UPDATE ${USER_INPUT_ACCEPTANCE_TABLE} SET announced_at_ms = ?, announced_task_id = ? WHERE accept_id = ? AND state = 'converged' AND announced_at_ms IS NULL`, `UPDATE ${USER_INPUT_ACCEPTANCE_TABLE} SET announced_at_ms = $1, announced_task_id = $2 WHERE accept_id = $3 AND state = 'converged' AND announced_at_ms IS NULL`), [atMs, taskId, acceptId]);
|
|
95
|
+
return res.affected > 0;
|
|
96
|
+
}
|
|
97
|
+
async listOpen(after, limit = USER_INPUT_OPEN_SCAN_MAX) {
|
|
98
|
+
assertPageLimit("listOpen", limit);
|
|
99
|
+
const { rows } = after === undefined
|
|
100
|
+
? await this.db.query(this.q(`SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE state = 'open' ORDER BY accepted_at_ms, accept_id LIMIT ${limit}`, `SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE state = 'open' ORDER BY accepted_at_ms, accept_id LIMIT ${limit}`), [])
|
|
101
|
+
: await this.db.query(this.q(`SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE state = 'open' AND (accepted_at_ms > ? OR (accepted_at_ms = ? AND accept_id > ?)) ORDER BY accepted_at_ms, accept_id LIMIT ${limit}`, `SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE state = 'open' AND (accepted_at_ms > $1 OR (accepted_at_ms = $2 AND accept_id > $3)) ORDER BY accepted_at_ms, accept_id LIMIT ${limit}`), [after.acceptedAtMs, after.acceptedAtMs, after.acceptId]);
|
|
102
|
+
const open = readRows(rows, "listOpen");
|
|
103
|
+
return rows.length === limit ? { open, next: rawCursor(rows[rows.length - 1]) } : { open };
|
|
104
|
+
}
|
|
105
|
+
async listCrashed(sessionId) {
|
|
106
|
+
const { rows } = await this.db.query(this.q(`SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = ? AND state = 'converged' ORDER BY accepted_at_ms DESC, accept_id DESC LIMIT ${USER_INPUT_SESSION_READ_MAX}`, `SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = $1 AND state = 'converged' ORDER BY accepted_at_ms DESC, accept_id DESC LIMIT ${USER_INPUT_SESSION_READ_MAX}`), [sessionId]);
|
|
107
|
+
return readRows(rows, "listCrashed").reverse();
|
|
108
|
+
}
|
|
109
|
+
async listUnannounced(sessionId, limit) {
|
|
110
|
+
const n = assertPageLimit("listUnannounced", limit);
|
|
111
|
+
const { rows } = await this.db.query(this.q(`SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = ? AND state = 'converged' AND announced_at_ms IS NULL ORDER BY accepted_at_ms, accept_id LIMIT ${n}`, `SELECT ${COLUMNS} FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = $1 AND state = 'converged' AND announced_at_ms IS NULL ORDER BY accepted_at_ms, accept_id LIMIT ${n}`), [sessionId]);
|
|
112
|
+
return readRows(rows, "listUnannounced");
|
|
113
|
+
}
|
|
114
|
+
async deleteBySession(sessionId) {
|
|
115
|
+
const res = await this.db.query(this.q(`DELETE FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = ?`, `DELETE FROM ${USER_INPUT_ACCEPTANCE_TABLE} WHERE session_id = $1`), [sessionId]);
|
|
116
|
+
return res.affected;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export class TiDBUserInputLedgerStore extends SqlUserInputLedgerStore {
|
|
120
|
+
constructor(pool) {
|
|
121
|
+
super(mysqlDriver(pool));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export class PgUserInputLedgerStore extends SqlUserInputLedgerStore {
|
|
125
|
+
constructor(pool) {
|
|
126
|
+
super(pgDriver(pool));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=user-input-ledger-sql.js.map
|
package/dist/project-memory.d.ts
CHANGED
|
@@ -25,7 +25,8 @@ interface ProjectMemoryCtx {
|
|
|
25
25
|
* every CLAUDE file it finds, per directory level up the ancestor chain, whereas this loader reads one root and
|
|
26
26
|
* takes the single first hit. */
|
|
27
27
|
declare const INSTRUCTION_FILE_CHAIN: readonly ["CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md", ".claude/AGENTS.md"];
|
|
28
|
-
|
|
28
|
+
/** 一个链成员(相对项目根的拼写)。导出给 S-961 的 wire 段与它的值闭集登记(`trace/self-minted-wire.ts`)—— 链长一员,两处 tsc 跟着红。 */
|
|
29
|
+
export type InstructionChainMember = (typeof INSTRUCTION_FILE_CHAIN)[number];
|
|
29
30
|
/** Recover (root, rel) for a path the LOADER declared — every declared path is `root + one chain member`.
|
|
30
31
|
* The probe needs this to re-run the loader's parent-directory symlink gate each round (self-review: the probe
|
|
31
32
|
* checked only the final component, so swapping `.claude` for a symlink out of the repo MID-RUN made the probe
|
|
@@ -33,7 +34,7 @@ type ChainMember = (typeof INSTRUCTION_FILE_CHAIN)[number];
|
|
|
33
34
|
* calls it with the host `node:path`. */
|
|
34
35
|
export declare function instructionChainCoordinates(absPath: string, p?: PlatformPath): {
|
|
35
36
|
root: string;
|
|
36
|
-
rel:
|
|
37
|
+
rel: InstructionChainMember;
|
|
37
38
|
} | null;
|
|
38
39
|
/**
|
|
39
40
|
* Build a `loadProjectMemory` for the host lane. Returns null (skip) unless hands are enabled AND cwd is a local
|
|
@@ -54,6 +55,13 @@ export declare function instructionChainCoordinates(absPath: string, p?: Platfor
|
|
|
54
55
|
* an absolute local directory) is "not applicable", not "none" — it never reaches the mint (O6, see `LoadOutcome`). The member is the one the snapshot carries — a cache hit answers the cached pick, so
|
|
55
56
|
* the notice follows what the model actually reads, not the disk (a same-process edit changes nothing until the
|
|
56
57
|
* snapshot is re-read: restart, post-compact, or a new session).
|
|
58
|
+
*
|
|
59
|
+
* S-961 — the same statement (same predicate, same member) also settles the calling LEG's `wiring_manifest.instructions`
|
|
60
|
+
* segment through {@link settleLegInstructionStatement} (`instruction-selection.ts`): the leg scope is the run identity
|
|
61
|
+
* the ctx does not carry, and the first call inside a leg's scope whose `ctx.sessionId` equals the leg's session is that
|
|
62
|
+
* leg's own prepare (a session-less adhoc leg falls back to plain first-call; [ref] X1). The segment projects
|
|
63
|
+
* `instructionFile`, not `instructionSources`: an empty selected file with no git narrative returns no answer and so no
|
|
64
|
+
* `instructionSources`, yet it IS the pick (it shadows every member below it) — and the notice reads the same value.
|
|
57
65
|
*/
|
|
58
66
|
export declare function makeLoadProjectMemory(opts: {
|
|
59
67
|
logger?: {
|
package/dist/project-memory.js
CHANGED
|
@@ -3,6 +3,7 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { createHmac, randomBytes } from "node:crypto";
|
|
4
4
|
import hostPath, { isAbsolute, join } from "node:path";
|
|
5
5
|
import { announceInstructionSource } from "./instruction-source-notice.js";
|
|
6
|
+
import { settleLegInstructionStatement } from "./instruction-selection.js";
|
|
6
7
|
import { gitSubprocessEnv } from "./server-secret-env.js";
|
|
7
8
|
import { redactSecrets } from "./trace/redact.js";
|
|
8
9
|
const INSTRUCTION_FILE_CHAIN = ["CLAUDE.md", ".claude/CLAUDE.md", "AGENTS.md", ".claude/AGENTS.md"];
|
|
@@ -92,8 +93,10 @@ export function makeLoadProjectMemory(opts) {
|
|
|
92
93
|
const seat = opts.instructionSource;
|
|
93
94
|
return async (ctx) => {
|
|
94
95
|
const outcome = await loadOutcome(ctx);
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
const statement = outcome.walked && !ctx.isSubagent ? { stated: true, file: outcome.instructionFile } : { stated: false };
|
|
97
|
+
settleLegInstructionStatement(statement, ctx.sessionId);
|
|
98
|
+
if (seat && ctx.sessionId && statement.stated)
|
|
99
|
+
await announceInstructionSource(seat, ctx.sessionId, statement.file);
|
|
97
100
|
return outcome.answer;
|
|
98
101
|
};
|
|
99
102
|
async function loadOutcome(ctx) {
|
package/dist/run-local.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { type AskRequest, type Brain, type TaskSpec } from "@sema-agent/core";
|
|
3
|
+
import { type UnattendedPolicyRefusal } from "./tool-approval.js";
|
|
3
4
|
import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView } from "./deployment-governance.js";
|
|
4
5
|
import { type ServiceConfig } from "./config.js";
|
|
5
6
|
import { type Logger } from "./observability/logger.js";
|
|
@@ -105,6 +106,9 @@ export interface LocalApproverIo {
|
|
|
105
106
|
readonly askYesNo: (prompt: string, signal?: AbortSignal) => Promise<boolean>;
|
|
106
107
|
/** 非 TTY 臂的告知面(stderr)。 */
|
|
107
108
|
readonly printErr: (line: string) => void;
|
|
109
|
+
/** S-956:本席**没有人被问**就拒了一只 ask(无 TTY / 参数展示不出)的事实出口 —— 退出码的判据之一(见 {@link unfinishedStopOf}):
|
|
110
|
+
* 这一拒交给 core 的是部署政策拒(不停批、不记人拒),「活没干完」于是不再经 core 的人拒标记传出来,由本席自己说。 */
|
|
111
|
+
readonly noteRefusedWithoutPerson: () => void;
|
|
108
112
|
}
|
|
109
113
|
/**
|
|
110
114
|
* [ref] 件三:**onAsk 座**(§0 潜雷 / §5 锁 6)。
|
|
@@ -130,12 +134,21 @@ export interface LocalApproverIo {
|
|
|
130
134
|
* ask(core 把 `matchedAskRule` 刻意留在闸内、不上请求),所以无 TTY 拒句**列出**可能产门的全部旋钮,
|
|
131
135
|
* `APPROVAL_DENY` 也点名(它当场拒、不经本席 —— 运维排查「为什么这只工具不跑」时要在同一句里看到它)。
|
|
132
136
|
*
|
|
133
|
-
* 🔴 非 TTY
|
|
137
|
+
* 🔴 非 TTY 臂答**拒**(真 deny),**绝不答 `"unavailable"`**:后者被 core 读成「席位够不到」并**改道
|
|
134
138
|
* `suspendAsk`**(dist `core/hooks.js` 的 `approverUnavailable` 重投)。今天本腿无店 ⇒ 那条改道无处
|
|
135
139
|
* 可去(fail-closed 拒),但这一条仍是本席的契约:答案是「不批」,不是「找不到人」。
|
|
140
|
+
*
|
|
141
|
+
* 🔴 S-956:**谁拒的就记谁**。本席的拒分两种,交给 core 的形不同:
|
|
142
|
+
* · **人答 N**(TTY 臂)⇒ 裸 `false` —— core 契约里这就是「一个人拒了」(`human_refused` / `who.party:"person"`,模型读「用户不想继续,
|
|
143
|
+
* 停下来等他」,run 以 `haltedOnUserRejection` 停批);
|
|
144
|
+
* · **没有人被问**(无 TTY 臂 / 参数展示不出臂)⇒ 部署政策拒对象 {@link UnattendedPolicyRefusal}(经与协调器 deny 政策臂**同一只**构造
|
|
145
|
+
* `buildUnattendedPolicyRefusal`;core 7.37.1 `readApproverOutcomeObject` ⇒ `policy_refused` / `who.party:"none"`,模型读 core 的
|
|
146
|
+
* 策略句 + 本席的拒因,不停批)。修前两臂交裸 `false` ⇒ 机器的拒被记成人拒、模型被告知去等一个不存在的人。
|
|
147
|
+
* 两臂的拒都经 `io.noteRefusedWithoutPerson` 报一次 —— 「活没干完」(退 1)此前搭的是 core 人拒标记的便车,S-956 之后由本席的事实给。
|
|
136
148
|
*/
|
|
137
|
-
/** 本席的形:core `OnAsk` 的函数臂(`"allow"` / `"deny"` 两个字面臂本席从不用)
|
|
138
|
-
|
|
149
|
+
/** 本席的形:core `OnAsk` 的函数臂(`"allow"` / `"deny"` 两个字面臂本席从不用)—— 人的答是布尔,没有人被问的拒是部署政策拒对象;
|
|
150
|
+
* 结构上可赋给 `OnAsk`。 */
|
|
151
|
+
export type LocalApprover = (req: AskRequest, signal?: AbortSignal) => Promise<boolean | UnattendedPolicyRefusal>;
|
|
139
152
|
export declare function createLocalApprover(io: LocalApproverIo): LocalApprover;
|
|
140
153
|
export declare function promptYesNoOnTty(prompt: string, signal?: AbortSignal, streams?: {
|
|
141
154
|
input: NodeJS.ReadableStream;
|
|
@@ -143,8 +156,8 @@ export declare function promptYesNoOnTty(prompt: string, signal?: AbortSignal, s
|
|
|
143
156
|
}): Promise<boolean>;
|
|
144
157
|
/**
|
|
145
158
|
* Boot the engine in LOCAL mode, run ONE task to completion, print the result. Returns the process exit
|
|
146
|
-
* code (0 = completed its work, 1 = the task did not complete — including a `completed` run that a person stopped
|
|
147
|
-
* see {@link
|
|
159
|
+
* code (0 = completed its work, 1 = the task did not complete — including a `completed` run that a person stopped
|
|
160
|
+
* or whose approval no person could answer, see {@link unfinishedStopOf} — / a fatal error, 2 = bad usage / unmet env). Pure of
|
|
148
161
|
* `process.exit` so a test can assert the code; `main()` applies it.
|
|
149
162
|
*/
|
|
150
163
|
export declare function runLocal(argv: string[], deps?: RunLocalDeps): Promise<number>;
|