@sema-agent/server 7.55.0 → 7.57.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.
Files changed (129) hide show
  1. package/MIGRATION.md +3 -3
  2. package/README.md +14 -7
  3. package/README.zh-CN.md +10 -6
  4. package/USAGE.md +207 -59
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-ask-audit-store.d.ts +100 -1
  7. package/dist/approval-ask-audit-store.js +103 -2
  8. package/dist/approval-card.d.ts +148 -13
  9. package/dist/approval-card.js +82 -13
  10. package/dist/approval-content-kind.d.ts +22 -0
  11. package/dist/approval-content-kind.js +5 -0
  12. package/dist/approval.d.ts +31 -0
  13. package/dist/approval.js +18 -0
  14. package/dist/auto-mode-face.d.ts +111 -0
  15. package/dist/auto-mode-face.js +99 -0
  16. package/dist/bench/s1/arms.js +5 -5
  17. package/dist/bench/s1/live-deps.js +11 -11
  18. package/dist/bench/s1/run-firm.js +3 -0
  19. package/dist/bench/s1/runner-ctx.d.ts +4 -0
  20. package/dist/bench/s1/runner-ctx.js +7 -0
  21. package/dist/boot/config-center.js +23 -1
  22. package/dist/boot/coordinators.js +13 -1
  23. package/dist/boot/leader.d.ts +21 -0
  24. package/dist/boot/leader.js +6 -0
  25. package/dist/boot/parked-revive-gate.d.ts +7 -2
  26. package/dist/boot/parked-revive-gate.js +12 -1
  27. package/dist/boot/resolve-spec.d.ts +3 -0
  28. package/dist/boot/resolve-spec.js +3 -1
  29. package/dist/boot/runner-deps.d.ts +12 -0
  30. package/dist/boot/runner-deps.js +28 -3
  31. package/dist/boot/runtime-caps.d.ts +19 -8
  32. package/dist/boot/runtime-caps.js +50 -20
  33. package/dist/boot/session-shell-gate-registry.d.ts +39 -0
  34. package/dist/boot/session-shell-gate-registry.js +21 -0
  35. package/dist/config-catalog.js +14 -5
  36. package/dist/config-center/types.d.ts +2 -1
  37. package/dist/config-provider.js +1 -0
  38. package/dist/config-types.d.ts +41 -10
  39. package/dist/config.d.ts +21 -0
  40. package/dist/config.js +40 -5
  41. package/dist/http/route-ctx.d.ts +90 -3
  42. package/dist/http/routes/a2a-serve.js +5 -3
  43. package/dist/http/routes/approvals-assistant.js +21 -8
  44. package/dist/http/routes/capabilities.js +21 -4
  45. package/dist/http/routes/leader.js +2 -2
  46. package/dist/http/routes/memory-origin.d.ts +2 -2
  47. package/dist/http/routes/rules.js +3 -2
  48. package/dist/http/routes/runs.d.ts +0 -14
  49. package/dist/http/routes/runs.js +20 -8
  50. package/dist/http/routes/tasks.js +34 -8
  51. package/dist/http/routes/workflows.js +20 -5
  52. package/dist/http/server.d.ts +11 -1
  53. package/dist/http/server.js +220 -34
  54. package/dist/http/wire-types.d.ts +9 -4
  55. package/dist/leader/diffout.js +2 -1
  56. package/dist/leader/endpoint.js +49 -15
  57. package/dist/leader/fanout.js +6 -5
  58. package/dist/leader/leader.js +17 -14
  59. package/dist/leader/merge.js +17 -12
  60. package/dist/leader/planner.js +3 -2
  61. package/dist/leader/repair-oracle.js +6 -4
  62. package/dist/leader/repair-wire.js +5 -3
  63. package/dist/leader/wire.d.ts +80 -1
  64. package/dist/leader/wire.js +55 -26
  65. package/dist/main.js +44 -8
  66. package/dist/observability/err-text.d.ts +6 -0
  67. package/dist/observability/err-text.js +10 -0
  68. package/dist/observability/fail-open.d.ts +40 -0
  69. package/dist/observability/fail-open.js +19 -0
  70. package/dist/observability/metrics.js +1 -1
  71. package/dist/observability/run-terminal-log.d.ts +120 -0
  72. package/dist/observability/run-terminal-log.js +360 -0
  73. package/dist/orchestration/workflow-completion-inbox.d.ts +9 -0
  74. package/dist/orchestration/workflow-completion-inbox.js +8 -0
  75. package/dist/permission-rule-vocab.d.ts +40 -0
  76. package/dist/permission-rule-vocab.js +17 -0
  77. package/dist/plugins/checkpoint-store-sql.d.ts +26 -0
  78. package/dist/plugins/checkpoint-store-sql.js +23 -4
  79. package/dist/plugins/file-run-store.d.ts +3 -34
  80. package/dist/plugins/file-run-store.js +21 -0
  81. package/dist/plugins/local-checkpoint-store.d.ts +10 -0
  82. package/dist/plugins/local-checkpoint-store.js +5 -0
  83. package/dist/plugins/local-session-store.d.ts +9 -9
  84. package/dist/plugins/local-session-store.js +5 -3
  85. package/dist/plugins/memory-run-store.d.ts +3 -15
  86. package/dist/plugins/memory-run-store.js +21 -0
  87. package/dist/plugins/permission-rule-store-file.d.ts +22 -4
  88. package/dist/plugins/permission-rule-store-file.js +25 -11
  89. package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
  90. package/dist/plugins/permission-rule-store-sql.js +39 -23
  91. package/dist/plugins/pg-session-storage.js +17 -13
  92. package/dist/plugins/remote-scratchpad.js +3 -2
  93. package/dist/plugins/run-store-sql.d.ts +3 -42
  94. package/dist/plugins/run-store-sql.js +36 -5
  95. package/dist/plugins/store-backend.d.ts +1 -1
  96. package/dist/plugins/store-contracts.d.ts +70 -1
  97. package/dist/plugins/tidb-session-store.js +18 -14
  98. package/dist/plugins/workflow-run-store-sql.d.ts +5 -0
  99. package/dist/plugins/workflow-run-store-sql.js +10 -2
  100. package/dist/rules-consent.d.ts +5 -4
  101. package/dist/rules-consent.js +25 -46
  102. package/dist/run-cancel-context.d.ts +13 -0
  103. package/dist/run-cancel-context.js +15 -0
  104. package/dist/run-local.js +2 -2
  105. package/dist/runs.d.ts +4 -1
  106. package/dist/runs.js +41 -10
  107. package/dist/runtime-caps-resolver.d.ts +133 -17
  108. package/dist/runtime-caps-resolver.js +44 -4
  109. package/dist/security.d.ts +7 -0
  110. package/dist/task-settings.d.ts +57 -3
  111. package/dist/task-settings.js +78 -5
  112. package/dist/task-workflow.d.ts +32 -2
  113. package/dist/task-workflow.js +8 -3
  114. package/dist/tool-approval.d.ts +38 -63
  115. package/dist/tool-approval.js +102 -74
  116. package/dist/trace/core-keyset-guard.d.ts +2 -2
  117. package/dist/trace/ledger-sink.d.ts +13 -4
  118. package/dist/trace/ledger-sink.js +14 -6
  119. package/dist/trace/project.d.ts +13 -0
  120. package/dist/trace/project.js +10 -1
  121. package/dist/trace/redact.d.ts +22 -11
  122. package/dist/trace/redact.js +655 -20
  123. package/dist/trace/sema-provenance.d.ts +26 -0
  124. package/dist/trace/sema-provenance.js +11 -0
  125. package/dist/turn-activity.d.ts +32 -2
  126. package/dist/turn-activity.js +29 -4
  127. package/package.json +3 -3
  128. package/skills/find-skills.md +1 -1
  129. package/skills/loop.md +1 -1
@@ -1,8 +1,9 @@
1
+ import { contentKindOf } from "../approval-content-kind.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { APPROVAL_GATE_KINDS_SQL_IN } from "../tool-approval.js";
3
4
  import { CheckpointError, MAX_RULE_TEXT_CHARS, validatePendingSteer, appendPendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, } from "@sema-agent/core";
4
5
  import { redactDeep, redactSecrets } from "../trace/redact.js";
5
- import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, RuleOfferRawSchema } from "../approval-card.js";
6
+ import { MAX_RULE_OFFERS, MAX_RULE_OFFER_BATCH_MEMBERS, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat } from "../approval-card.js";
6
7
  import { parseJsonStrict as parseJson } from "./sql-row-helpers.js";
7
8
  import { recordFailOpen } from "../observability/fail-open.js";
8
9
  import { mysqlDriver, pgDriver, dialectProtocolJsonEncoder } from "./sql-driver.js";
@@ -68,7 +69,7 @@ export function boundedRuleOffers(raw, opts = {}) {
68
69
  for (const item of raw.slice(0, MAX_RULE_OFFER_SCAN)) {
69
70
  if (out.length >= MAX_RULE_OFFERS)
70
71
  break;
71
- const parsed = RuleOfferRawSchema.safeParse(preboundBatchMembers(item));
72
+ const parsed = RuleOfferRawSchema.safeParse(screenUncoveredDetailSeat(backfillLegacyBatchMemberKind(preboundBatchMembers(item))));
72
73
  if (!parsed.success)
73
74
  continue;
74
75
  if (parsed.data.kind === "single") {
@@ -77,8 +78,18 @@ export function boundedRuleOffers(raw, opts = {}) {
77
78
  else {
78
79
  out.push({
79
80
  kind: "batch",
80
- rules: parsed.data.rules.slice(0, MAX_RULE_OFFER_BATCH_MEMBERS).map((r) => ({ rule: scrub(r.rule), match: r.match, command: scrub(r.command), segment: scrub(r.segment) })),
81
+ rules: parsed.data.rules.slice(0, MAX_RULE_OFFER_BATCH_MEMBERS).map((r) => {
82
+ switch (r.kind) {
83
+ case "command":
84
+ return { kind: "command", rule: scrub(r.rule), match: r.match, command: scrub(r.command), segment: scrub(r.segment) };
85
+ case "directoryRead":
86
+ return { kind: "directoryRead", rule: scrub(r.rule), directory: scrub(r.directory), segment: scrub(r.segment) };
87
+ default:
88
+ throw new Error(`unknown rule-offer batch member kind: ${JSON.stringify(r)}`);
89
+ }
90
+ }),
81
91
  uncoveredSegments: parsed.data.uncoveredSegments,
92
+ ...(parsed.data.uncoveredDetail !== undefined ? { uncoveredDetail: parsed.data.uncoveredDetail.map((d) => ({ segment: scrub(d.segment), reason: d.reason })) } : {}),
82
93
  });
83
94
  }
84
95
  }
@@ -437,6 +448,7 @@ export class SqlCheckpointStore {
437
448
  ? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at_ms ASC`, [scope])
438
449
  : await this.db.query(`${base} ORDER BY c.created_at_ms ASC`);
439
450
  const out = rows.map((r) => {
451
+ const toolName = r.tool_name ?? null;
440
452
  const toolCallId = r.tool_call_id ?? null;
441
453
  const boundInputHash = r.bound_input_hash ?? null;
442
454
  const gateKind = r.gate_kind ?? null;
@@ -444,7 +456,7 @@ export class SqlCheckpointStore {
444
456
  return {
445
457
  sessionId: String(r.session_id),
446
458
  scope: String(r.scope),
447
- toolName: r.tool_name ?? null,
459
+ toolName,
448
460
  toolCallId,
449
461
  ...(toolCallId !== null ? { boundCallId: toolCallId } : {}),
450
462
  ...(boundInputHash !== null ? { boundInputHash } : {}),
@@ -456,6 +468,7 @@ export class SqlCheckpointStore {
456
468
  riskDescriptor: readRiskDescriptorCell(r.risk_descriptor),
457
469
  ...(ruleOffers !== undefined ? { ruleOffers } : {}),
458
470
  ...(Number(r.has_bidi_controls) === 1 ? { hasBidiControls: true } : {}),
471
+ ...(contentKindOf(toolName) !== undefined ? { contentKind: "content_ask" } : {}),
459
472
  };
460
473
  });
461
474
  return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
@@ -536,6 +549,12 @@ export class SqlCheckpointStore {
536
549
  };
537
550
  });
538
551
  }
552
+ async peekScopeByToken(token) {
553
+ const { rows } = await this.db.query(this.q("SELECT scope FROM checkpoint WHERE token = ?", "SELECT scope FROM checkpoint WHERE token = $1"), [token]);
554
+ if (!rows[0])
555
+ return undefined;
556
+ return rows[0].scope ?? null;
557
+ }
539
558
  async peekPendingScope(sessionId, binding) {
540
559
  const { sql, params } = this.pendingRowQuery("scope", sessionId, undefined, binding);
541
560
  const { rows } = await this.db.query(sql, params);
@@ -1,37 +1,6 @@
1
- /**
2
- * File-backed async run registry + replayable event log — the DURABLE local twin of {@link MemoryRunStore} (and the
3
- * file-backed sibling of {@link TiDBRunStore}/{@link PgRunStore}). P0.5 variant-1 (clay 2026-06-26): the file-backed
4
- * follow-on the Memory twin's header promised — a locally-run HTTP service now keeps its runs ledger + trace event log
5
- * across a process restart, not just in memory. Behaviour is byte-identical to MemoryRunStore (the shared parity oracle
6
- * — same single-active claim, the suspend/needs_review parks that KEEP the claim, the markResuming CAS that resets
7
- * cancel/preempt, the owner null-safe guard, the keyset list order, the (taskId,seq) event dedup, and the structuredClone
8
- * write/read isolation); the only difference is durability.
9
- *
10
- * THREE on-disk data planes (all under `<root>/runs/`), each using core's crash-safe fs primitives — never hand-rolled
11
- * fs (zero drift, no zero-length/torn corruption):
12
- * 1. run registry — `<root>/runs/<taskId>/run.json` = the RunRow, replaced WHOLE on every status change via
13
- * `atomicWriteFile` (tmp→fsync→rename→fsync-dir, the 5-step atomic replace).
14
- * 2. single-active claim — `<root>/runs/active/<sanitizePathComponent(sessionId)>` carrying the active taskId, created
15
- * with `writeThenLink` (atomic create; EEXIST ⇒ the session already has an active run → the
16
- * createRun loses the claim, returns `{ok:false, activeTaskId}`). The file version of task_active's
17
- * unique key.
18
- * 3. per-run event log — `<root>/runs/<taskId>/events.jsonl` = an `AppendLog` (one line per event, fsync:false — events
19
- * are high-frequency, the torn-tail-drop on replay covers a crash tail; only the run.json status
20
- * commit + the active-claim create use fsync). (taskId,seq) is deduped on replay.
21
- *
22
- * An in-memory INDEX (the same Maps MemoryRunStore holds) is hydrated on construction by scanning each runs/<taskId>/
23
- * run.json + the active claim files — so listRuns/listSessions/getActiveTaskId are O(in-mem) with byte-identical keyset sort to the SQL/Memory
24
- * twins. Every write touches BOTH the disk and the index on one path. Single-process (the FileStorageBackend boot lock
25
- * guarantees one writer per data dir), so there is no per-op lock — with ONE exception: [ref]'s claim acquisition
26
- * (`withClaimLock`) runs under a per-session in-process critical section, because judging a claim stale involves
27
- * awaits (the checkpoint probe) and the claim + run row must commit as one unit (the SQL twin's transaction). The
28
- * checkpoint-coupled suspended-run reapers run
29
- * off an injected {@link RunStoreCheckpointProbe} ([ref]; wired by the local backend where checkpoint + run store
30
- * share one root) and degrade to honest no-ops when the probe is absent — exactly like MemoryRunStore.
31
- */
32
1
  import { type UsageRow } from "../usage-analytics.js";
33
- import type { TaskResult, TaskStatus } from "@sema-agent/core";
34
- import type { RunRecord, SessionSummary, RunEvent } from "./store-contracts.js";
2
+ import type { TaskStatus } from "@sema-agent/core";
3
+ import type { RunRecord, SessionSummary, RunEvent, PersistedTaskResult } from "./store-contracts.js";
35
4
  import type { RunStoreCheckpointProbe } from "./memory-run-store.js";
36
5
  import type { LedgerEventType } from "../trace/ledger-events.js";
37
6
  /**
@@ -136,7 +105,7 @@ export declare class FileRunStore {
136
105
  getEvents(taskId: string, afterSeq: number): Promise<RunEvent[]>;
137
106
  /** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review,
138
107
  * [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
139
- setTerminal(taskId: string, status: TaskStatus, result: TaskResult | null, error: string | null): Promise<void>;
108
+ setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
140
109
  /** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard). */
141
110
  setSuspended(taskId: string): Promise<void>;
142
111
  /** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review. */
@@ -1,9 +1,11 @@
1
+ import { redactErrorMessage, redactLedgerEventData, redactTerminalResult } from "../observability/run-terminal-log.js";
1
2
  import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
2
3
  import { join } from "node:path";
3
4
  import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
4
5
  import { randomBytes } from "node:crypto";
5
6
  import { AppendLog, atomicWriteFile, ensureDir, readJsonlRecords, sanitizePathComponent, writeThenLink } from "@sema-agent/core";
6
7
  import { recordFailOpen } from "../observability/fail-open.js";
8
+ import { notifyRunTerminal } from "../observability/run-terminal-log.js";
7
9
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
8
10
  const PROCESS_FACTS = {
9
11
  pid: process.pid,
@@ -338,6 +340,7 @@ export class FileRunStore {
338
340
  }
339
341
  }
340
342
  async appendEvent(taskId, seq, type, data) {
343
+ data = redactLedgerEventData(type, data);
341
344
  const list = this.events.get(taskId) ?? [];
342
345
  if (list.some((e) => e.seq === seq))
343
346
  return;
@@ -363,6 +366,8 @@ export class FileRunStore {
363
366
  .map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
364
367
  }
365
368
  async setTerminal(taskId, status, result, error) {
369
+ result = redactTerminalResult(result);
370
+ error = error === null ? null : (redactErrorMessage(error) ?? null);
366
371
  const r = this.runs.get(taskId);
367
372
  if (!r) {
368
373
  for (const [sessionId, tid] of this.active)
@@ -372,6 +377,7 @@ export class FileRunStore {
372
377
  }
373
378
  return;
374
379
  }
380
+ let pending;
375
381
  if (r.status === "running" || r.status === "suspended" || r.status === "needs_review") {
376
382
  r.status = status;
377
383
  r.result = result;
@@ -379,9 +385,22 @@ export class FileRunStore {
379
385
  r.errorCode = result?.errorCode ?? null;
380
386
  r.updatedAt = new Date();
381
387
  this.persist(r);
388
+ pending = {
389
+ runId: taskId,
390
+ sessionId: r.sessionId,
391
+ owner: r.owner,
392
+ status,
393
+ resultTaskId: result?.taskId,
394
+ model: result?.model,
395
+ errorCode: r.errorCode ?? undefined,
396
+ errorMessage: result?.errorMessage ?? error ?? undefined,
397
+ elapsedMs: Math.max(0, Date.now() - r.createdAt.getTime()),
398
+ };
382
399
  }
383
400
  if (this.active.get(r.sessionId) === taskId)
384
401
  this.releaseClaim(r.sessionId);
402
+ if (pending)
403
+ notifyRunTerminal(pending);
385
404
  }
386
405
  async setSuspended(taskId) {
387
406
  const r = this.runs.get(taskId);
@@ -453,6 +472,7 @@ export class FileRunStore {
453
472
  continue;
454
473
  const sorted = [...arr].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || (a.taskId < b.taskId ? 1 : a.taskId > b.taskId ? -1 : 0));
455
474
  const latest = sorted[0];
475
+ const latestRunId = latest.result?.runId;
456
476
  const times = arr.map((r) => r.createdAt.getTime());
457
477
  const last = Math.max(...times);
458
478
  const first = Math.min(...times);
@@ -465,6 +485,7 @@ export class FileRunStore {
465
485
  objectivePreview: latest.objectivePreview,
466
486
  lastStatus: latest.status,
467
487
  lastRunId: latest.taskId,
488
+ ...(typeof latestRunId === "string" && latestRunId !== "" ? { lastTaskRunId: latestRunId } : {}),
468
489
  title: null,
469
490
  });
470
491
  }
@@ -112,6 +112,16 @@ export declare class LocalCheckpointStore {
112
112
  * 投了就是「SQL 车道有、local 车道没有」的键集分岔。
113
113
  */
114
114
  findDecidedApprovalsForBinding(sessionId: string, boundCallId: string): Promise<DecidedApprovalRecord[]>;
115
+ /**
116
+ * [ref]:按 token 的 scope 读 —— 与 SQL 孪生**同一条契约**(`undefined` 无行 / `null` 匿名 / 属主串),
117
+ * 语义与存在理由的唯一属主 = SQL 侧 `peekScopeByToken` 顶注。
118
+ *
119
+ * ⚠️ 形差如实记:SQL 孪生那口是真「轻读」(只 SELECT scope 一列),本形没有列可选 —— core 的 File 店
120
+ * 只给 `get(token)`,所以这里读的是**整行**。后果对本形无害:本形的 `get()` 没有行级版本闸(唯一的
121
+ * 读不动形是账本重放级 = 整店同时读不动,`findPendingTokenBySession` 先抛),所以「行体读得出而 scope
122
+ * 读不出」这一支在本形结构上不存在。哪天本形补了行级闸,这里要跟着改成不碰行体的读。
123
+ */
124
+ peekScopeByToken(token: CheckpointToken): Promise<string | null | undefined>;
115
125
  /** `undefined` = no pending row; else the owner scope (local scopes are always strings — never null).
116
126
  * [ref]:与 `findPendingTokenBySession` 同一个 {@link pickPendingRow}(同 binding ⇒ 同一行)。 */
117
127
  peekPendingScope(sessionId: string, binding?: PendingRowBinding): Promise<string | null | undefined>;
@@ -1,3 +1,4 @@
1
+ import { contentKindOf } from "../approval-content-kind.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { existsSync, readFileSync, readdirSync, renameSync, unlinkSync, mkdirSync } from "node:fs";
3
4
  import { join } from "node:path";
@@ -160,6 +161,7 @@ export class LocalCheckpointStore {
160
161
  return rs !== undefined ? { ruleOffers: rs } : {};
161
162
  })(),
162
163
  ...(pa?.hasBidiControls === true ? { hasBidiControls: true } : {}),
164
+ ...(contentKindOf(pa?.toolName ?? null) !== undefined ? { contentKind: "content_ask" } : {}),
163
165
  });
164
166
  }
165
167
  return rows.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
@@ -229,6 +231,9 @@ export class LocalCheckpointStore {
229
231
  }
230
232
  return out;
231
233
  }
234
+ async peekScopeByToken(token) {
235
+ return (await this.inner.get(token))?.scope;
236
+ }
232
237
  async peekPendingScope(sessionId, binding) {
233
238
  return this.pickPendingRow(await this.pendings(), sessionId, binding)?.cp.scope;
234
239
  }
@@ -27,6 +27,7 @@ export declare class LocalSessionStore implements SessionStore {
27
27
  * has only TERMINAL runs (no live tail to re-attach), so `lastRunId` falls back to null and the shell uses its
28
28
  * no-anchor path — same posture as the other local degrades here (runCount:0, lastStatus:""). Persisting it would
29
29
  * need a sidecar-format change (the #10 clobber area); deferred until a real local-file-backed need. */
30
+ /** K-5c/[ref]/[ref]:per-session 最近一次 run 的 {taskId, runId?}(noteTaskRun 席;runId omit ⇒ 清座)。 */
30
31
  private readonly lastTaskRun;
31
32
  /** 2c P1d-β — live staged-import handles keyed by stagingId, so a local staging's in-memory buffer is readable via
32
33
  * readStagedEntries (the durable twins re-read the staged session_event rows; local has no such rows). */
@@ -173,15 +174,14 @@ export declare class LocalSessionStore implements SessionStore {
173
174
  /** K-5c (core 1.155 SessionStore seam): core's Runner calls this at runTask START with the run's taskId, so the
174
175
  * latest run surfaces as `lastRunId` on listSessions even while it is still running/suspended (the local backend
175
176
  * has no runs ledger to derive it from, unlike the TiDB/PG twins). In-memory, last-write-wins per session.
176
- * [ref](core 5.65 增量批,**有意缓议**):core 的第三参 engine runId 在此**尚未消费**——它唯一的
177
- * 合法出口是 listSessions 行上的 `lastTaskRunId`,而 `SessionSummary` canonical spec 的**闭集**
178
- * (sema-sdk spec/openapi.yaml `additionalProperties:false`,producer-contract sessionsList 格咬着),
179
- * 加键是跨仓 spec 节拍的事。spec 开键那一批再落:map 值扩成 `{taskId, runId?}` + 两座同动清座语义
180
- * (对齐 core TtlSessionStore.noteTaskRun 的 omit⇒clear,session-store.js 亲读)+ 行投影。
181
- * codex 5.65 增量批 R1-[medium] 曾建议「存储先行、投影另批」——**驳**:本店 runId 的唯一读点就是
182
- * 行投影,先写不读=黑盒不可测的死状态(红先律无红可铺);清座语义单独存在也无观察面。存储与投影
183
- * 必须同批,债主=sessions 列表 wire 键的 spec 节拍批(本注即债的定位锚) */
184
- noteTaskRun(sessionId: string, taskId: string): void;
177
+ * [ref] → [ref]([ref] 兑现;上一版「有意缓议」注即本批债锚,兑现随批撤):第三参 engine runId
178
+ * 自本批起**消费** —— map 值 {taskId, runId?} + 行投影 `lastTaskRunId` + **两座同动清座**(omit
179
+ * runId 座,对齐 core TtlSessionStore.noteTaskRun 的 omit⇒clear 契约,core session.d.ts:204-212
180
+ * 亲读:旧 run runId 挂在新 task = 把上一条 run 的身份错标给新 run)。存储与行投影同批落
181
+ * (codex 5.65 R1-[medium]「存储先行、投影另批」之驳仍成立:本店 runId 唯一读点即行投影)
182
+ * wire 半场:HTTP 面零投影透传(sessions-list.ts,additive 键随行上 wire);sema-sdk spec
183
+ * `SessionSummary` 闭集候 SDK 开键([ref]/.82 班车)——SDK 落键即端到端闭合。 */
184
+ noteTaskRun(sessionId: string, taskId: string, runId?: string): void;
185
185
  /** Synthesize a session's objective preview = the most-recent USER text message (the objective the run last saw).
186
186
  * Reads the session's entries through the repo; any read error degrades to null (never fails the list). */
187
187
  private previewOf;
@@ -275,6 +275,7 @@ export class LocalSessionStore {
275
275
  continue;
276
276
  const firstActivityAt = meta.createdAt || new Date(0).toISOString();
277
277
  const lastActivityAt = this.lastActivity.get(meta.id) ?? firstActivityAt;
278
+ const lastRun = this.lastTaskRun.get(meta.id);
278
279
  all.push({
279
280
  sessionId: meta.id,
280
281
  owner,
@@ -283,7 +284,8 @@ export class LocalSessionStore {
283
284
  runCount: 0,
284
285
  objectivePreview: await this.previewOf(meta),
285
286
  lastStatus: "",
286
- lastRunId: this.lastTaskRun.get(meta.id) ?? null,
287
+ lastRunId: lastRun?.taskId ?? null,
288
+ ...(lastRun?.runId !== undefined ? { lastTaskRunId: lastRun.runId } : {}),
287
289
  title: this.titleState.titles.get(meta.id) ?? null,
288
290
  });
289
291
  }
@@ -298,8 +300,8 @@ export class LocalSessionStore {
298
300
  : filtered;
299
301
  return after.slice(0, opts.limit);
300
302
  }
301
- noteTaskRun(sessionId, taskId) {
302
- this.lastTaskRun.set(sessionId, taskId);
303
+ noteTaskRun(sessionId, taskId, runId) {
304
+ this.lastTaskRun.set(sessionId, { taskId, ...(runId !== undefined ? { runId } : {}) });
303
305
  }
304
306
  async previewOf(meta) {
305
307
  try {
@@ -1,18 +1,6 @@
1
- /**
2
- * In-memory async run registry + replayable event log — the LOCAL (DB-less) twin of {@link TiDBRunStore}/
3
- * {@link PgRunStore} (clay 2026-06-25: the seamless local↔cloud switch — a locally-run HTTP service must serve the
4
- * SAME contract a cloud worker does, incl. `/v1/runs` async runs + the trace event log). Behaviourally identical to
5
- * the SQL twins (the shared `storeBehaviorSuite` keeps them in lock-step), but process-local + ephemeral: a restart
6
- * loses run history (file-backed durability is the immediate follow-on; the conversation HISTORY + checkpoints DO
7
- * persist via core's FileStorageBackend). Single-process, so the cross-replica/durable reapers degrade — see below.
8
- *
9
- * Mirrors the SQL semantics exactly: the `task_active` single-active-run claim (a Map keyed by sessionId), the
10
- * NON-terminal suspend/needs_review parks that KEEP the claim, the markResuming CAS that resets cancel/preempt, the
11
- * owner null-safe guard on the flag/heartbeat paths (`<=>` → JS `(a??null)===(b??null)`), and the keyset list order.
12
- */
13
1
  import { type UsageRow } from "../usage-analytics.js";
14
- import type { TaskResult, TaskStatus } from "@sema-agent/core";
15
- import type { RunRecord, SessionSummary, RunEvent } from "./store-contracts.js";
2
+ import type { TaskStatus } from "@sema-agent/core";
3
+ import type { RunRecord, SessionSummary, RunEvent, PersistedTaskResult } from "./store-contracts.js";
16
4
  import type { LedgerEventType } from "../trace/ledger-events.js";
17
5
  /**
18
6
  * [ref] P0 — the local lane's stand-in for the SQL twins' checkpoint-table JOIN. The file/memory run stores
@@ -66,7 +54,7 @@ export declare class MemoryRunStore {
66
54
  getEvents(taskId: string, afterSeq: number): Promise<RunEvent[]>;
67
55
  /** [ref] FIRST terminal writer wins(SQL 孪生同款正向 CAS on running/suspended/needs_review,
68
56
  * [1.207 codex M1] 负向形漏 blocked/timeout);claim 释放保持无条件(幂等)。 */
69
- setTerminal(taskId: string, status: TaskStatus, result: TaskResult | null, error: string | null): Promise<void>;
57
+ setTerminal(taskId: string, status: TaskStatus, result: PersistedTaskResult | null, error: string | null): Promise<void>;
70
58
  /** Durable F4: park NON-terminal `suspended`, KEEP the task_active claim. CAS on running/suspended (reaper-revert guard). */
71
59
  setSuspended(taskId: string): Promise<void>;
72
60
  /** [ref] D-B: park NON-terminal `needs_review`, KEEP the claim. CAS on running/needs_review. */
@@ -1,4 +1,6 @@
1
+ import { redactErrorMessage, redactLedgerEventData, redactTerminalResult } from "../observability/run-terminal-log.js";
1
2
  import { projectUsageStats, USAGE_SCAN_LIMIT } from "../usage-analytics.js";
3
+ import { notifyRunTerminal } from "../observability/run-terminal-log.js";
2
4
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
3
5
  export class MemoryRunStore {
4
6
  runs = new Map();
@@ -78,6 +80,7 @@ export class MemoryRunStore {
78
80
  r.updatedAt = new Date();
79
81
  }
80
82
  async appendEvent(taskId, seq, type, data) {
83
+ data = redactLedgerEventData(type, data);
81
84
  const list = this.events.get(taskId) ?? [];
82
85
  if (list.some((e) => e.seq === seq))
83
86
  return;
@@ -100,6 +103,8 @@ export class MemoryRunStore {
100
103
  .map((e) => ({ ...e, data: e.data == null ? null : structuredClone(e.data) }));
101
104
  }
102
105
  async setTerminal(taskId, status, result, error) {
106
+ result = redactTerminalResult(result);
107
+ error = error === null ? null : (redactErrorMessage(error) ?? null);
103
108
  const r = this.runs.get(taskId);
104
109
  if (!r) {
105
110
  for (const [sessionId, tid] of this.active)
@@ -109,15 +114,29 @@ export class MemoryRunStore {
109
114
  }
110
115
  return;
111
116
  }
117
+ let pending;
112
118
  if (r.status === "running" || r.status === "suspended" || r.status === "needs_review") {
113
119
  r.status = status;
114
120
  r.result = result;
115
121
  r.error = error;
116
122
  r.errorCode = result?.errorCode ?? null;
117
123
  r.updatedAt = new Date();
124
+ pending = {
125
+ runId: taskId,
126
+ sessionId: r.sessionId,
127
+ owner: r.owner,
128
+ status,
129
+ resultTaskId: result?.taskId,
130
+ model: result?.model,
131
+ errorCode: r.errorCode ?? undefined,
132
+ errorMessage: result?.errorMessage ?? error ?? undefined,
133
+ elapsedMs: Math.max(0, Date.now() - r.createdAt.getTime()),
134
+ };
118
135
  }
119
136
  if (this.active.get(r.sessionId) === taskId)
120
137
  this.active.delete(r.sessionId);
138
+ if (pending)
139
+ notifyRunTerminal(pending);
121
140
  }
122
141
  async setSuspended(taskId) {
123
142
  const r = this.runs.get(taskId);
@@ -186,6 +205,7 @@ export class MemoryRunStore {
186
205
  continue;
187
206
  const sorted = [...arr].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || (a.taskId < b.taskId ? 1 : a.taskId > b.taskId ? -1 : 0));
188
207
  const latest = sorted[0];
208
+ const latestRunId = latest.result?.runId;
189
209
  const times = arr.map((r) => r.createdAt.getTime());
190
210
  const last = Math.max(...times);
191
211
  const first = Math.min(...times);
@@ -198,6 +218,7 @@ export class MemoryRunStore {
198
218
  objectivePreview: latest.objectivePreview,
199
219
  lastStatus: latest.status,
200
220
  lastRunId: latest.taskId,
221
+ ...(typeof latestRunId === "string" && latestRunId !== "" ? { lastTaskRunId: latestRunId } : {}),
201
222
  title: null,
202
223
  });
203
224
  }
@@ -1,5 +1,5 @@
1
1
  import { FilePermissionRuleStoreProvider, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleScope, type StaleRuleApprovalRecord } from "@sema-agent/core";
2
- import { type RuleImportTicket, type RuleTicketDecisionClock, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./permission-rule-store-sql.js";
2
+ import { type RuleImportTicket, type RuleTicketPurpose, type RuleTicketReclaimResult, type RuleTicketRedeemResult, type RuleTicketSnapshot } from "./permission-rule-store-sql.js";
3
3
  import type { PermissionRuleStoreBundle } from "../rules-consent.js";
4
4
  /**
5
5
  * `RuleApprovalRecordStore` 的 File 形。
@@ -29,17 +29,34 @@ export declare class FileRuleApprovalRecordStore implements RuleApprovalRecordSt
29
29
  /**
30
30
  * CC 导入票的 File 形。四条信任边界(principal 绑定 / TTL / 一次性原子消费 / 载荷绑定)与 SQL 形
31
31
  * **同语义**;唯一不同的是「原子」由谁保证 —— 那边是引擎行锁,这边是单进程 + 单写者(顶注)。
32
+ *
33
+ * 🔴 **每只店对自己那口钟负责**([ref]):SQL 形的 TTL 轴锚在数据库的钟上(多副本共享的唯一一口);
34
+ * local 形没有那样一口外部钟,它的钟就是本进程的 —— 而进程墙钟**不单调**,一次 NTP 回拨就能让一张
35
+ * 真实已经走满 TTL 的票复活(codex 对抗复审 r1 [high],验真后修)。所以本形自己带一枚**单调锚**:
36
+ * 本进程铸的票,判定钟 = `铸票墙钟 + 自铸票起单调钟走过的量`;重启后重放回来的行没有锚,如实退回墙钟。
32
37
  */
33
38
  export declare class FileRuleImportTicketStore {
34
39
  private readonly rows;
35
40
  private readonly log;
36
41
  private readonly gate;
37
- constructor(dir: string, now?: () => number, onError?: (message: string) => void);
42
+ /** 单调钟(判定用)。与 {@link now}(墙钟,记账用)是两只不同的钟,别混。 */
43
+ private readonly monotonic;
44
+ constructor(dir: string, now?: () => number, onError?: (message: string) => void, monotonic?: () => number);
38
45
  private readonly now;
39
46
  /** 取证/自证用:本面是否因内部损坏而 fail-closed。 */
40
47
  get corrupt(): boolean;
41
48
  /** 归还本面 eager 持有的日志描述符(理由与姊妹面 {@link FileRuleApprovalRecordStore.close} 逐字同源)。 */
42
49
  close(): void;
50
+ /**
51
+ * 这一刻的**判定钟**([ref] r1-F1)。
52
+ *
53
+ * 本进程铸的票 ⇒ `铸票墙钟 + 自铸票起单调钟走过的量`:墙钟被前拨不会误杀(真实只过了几秒的票还活着),
54
+ * 被回拨也不会续命(单调钟照走)。重启后重放回来的行没有锚 ⇒ 退回墙钟(单调钟的零点跨进程无意义)。
55
+ *
56
+ * **给不了票额外寿命**:投影严格按单调钟前进,真实走满 TTL 必然越过 `expiresAtMs`(负控钉在
57
+ * `permission-rule-store-file.test.ts` 的 [ref] r1-F1 族)。
58
+ */
59
+ private decisionNowMs;
43
60
  mint(input: {
44
61
  ticketId: string;
45
62
  principal: string;
@@ -53,11 +70,11 @@ export declare class FileRuleImportTicketStore {
53
70
  }): Promise<RuleImportTicket>;
54
71
  /** 一次性**认领**。五条否定项的判序与 SQL 形逐字相同(unknown → wrong-principal → wrong-purpose →
55
72
  * consumed → expired),于是两形的服务端日志归因可比;wire 面把五类折成同一个 404(零存在性 oracle)。 */
56
- consume(ticketId: string, principal: string, purpose: RuleTicketPurpose, opts?: RuleTicketDecisionClock): Promise<RuleTicketRedeemResult>;
73
+ consume(ticketId: string, principal: string, purpose: RuleTicketPurpose): Promise<RuleTicketRedeemResult>;
57
74
  /** 把认领**放回去**。`mustRemainValidMs` = 放回之后至少还要能用多久 —— 撑不过就不算放回成功
58
75
  * (理由逐字见 SQL 侧 `release` 的头注:承诺一次必然兑现不了的重试比不承诺更坏)。
59
76
  * `claimId` = A7 的认领围栏(R1-F1):被顶替的旧持有者动不了现持有者的认领。 */
60
- release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number, opts?: RuleTicketDecisionClock): Promise<boolean>;
77
+ release(ticketId: string, principal: string, claimId: string, mustRemainValidMs?: number): Promise<boolean>;
61
78
  /** [ref] A7 CLAIMED → **SETTLED**。判据与 SQL 孪生逐字同源(持有**这一代**认领 ∧ 未落定;过期
62
79
  * **刻意不设门** —— 理由逐字见 SQL 侧 `settle` 的头注:拒绝记录一件已经发生的事只会把真相丢掉)。 */
63
80
  settle(ticketId: string, principal: string, claimId: string, outcome: unknown): Promise<boolean>;
@@ -89,5 +106,6 @@ export interface FilePermissionRuleStores extends PermissionRuleStoreBundle {
89
106
  export declare function createFilePermissionRuleStores(root: string, opts?: {
90
107
  onError?: (message: string) => void;
91
108
  now?: () => number;
109
+ monotonic?: () => number;
92
110
  }): FilePermissionRuleStores;
93
111
  //# sourceMappingURL=permission-rule-store-file.d.ts.map
@@ -1,9 +1,11 @@
1
1
  import { join } from "node:path";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { readdirSync } from "node:fs";
4
+ import { performance } from "node:perf_hooks";
4
5
  import { FilePermissionRuleStoreProvider, AppendLog, ensureDir, readJsonlRecords, } from "@sema-agent/core";
5
6
  import { z } from "zod";
6
7
  import { buildRulePayloadHash } from "./permission-rule-store-sql.js";
8
+ import { EDITED_RULE_BREADTH_WARNING_CODES, RULE_APPROVAL_RECORD_SCHEMA, UNCOVERED_SEGMENT_REASONS } from "../permission-rule-vocab.js";
7
9
  const RuleScopeSchema = z.union([
8
10
  z.object({ kind: z.literal("global") }).strict(),
9
11
  z.object({ kind: z.literal("project"), root: z.string().min(1) }).strict(),
@@ -18,6 +20,7 @@ const RuleOffer2Schema = z.union([
18
20
  candidates: z.array(z.number().int().nonnegative().safe()),
19
21
  segments: z.array(z.string()).optional(),
20
22
  uncoveredSegments: z.number().int().nonnegative().safe().optional(),
23
+ uncoveredDetail: z.array(z.object({ segment: z.string(), reason: z.enum(UNCOVERED_SEGMENT_REASONS) }).strict()).optional(),
21
24
  })
22
25
  .strict(),
23
26
  ]);
@@ -26,7 +29,7 @@ const ApprovalRecordSchema = z
26
29
  id: z.string().min(1),
27
30
  principal: z.string().optional(),
28
31
  owner: z.union([z.object({ kind: z.literal("principal"), principal: z.string() }).strict(), z.object({ kind: z.literal("local-owner") }).strict()]).optional(),
29
- schema: z.literal(2),
32
+ schema: z.literal(RULE_APPROVAL_RECORD_SCHEMA),
30
33
  kind: z.enum(["card", "import", "starter"]),
31
34
  state: z.enum(["pending", "approved", "redeemed"]),
32
35
  candidates: z.array(RuleCandidateSchema),
@@ -35,7 +38,10 @@ const ApprovalRecordSchema = z
35
38
  toolCallId: z.string().optional(),
36
39
  boundInputHash: z.string().optional(),
37
40
  command: z.string().optional(),
38
- edited: z.object({ index: z.number().int().nonnegative().safe(), text: z.string(), at: z.string() }).strict().optional(),
41
+ edited: z
42
+ .object({ index: z.number().int().nonnegative().safe(), text: z.string(), at: z.string(), warnings: z.array(z.enum(EDITED_RULE_BREADTH_WARNING_CODES)).optional() })
43
+ .strict()
44
+ .optional(),
39
45
  rev: z.number().int().nonnegative().safe(),
40
46
  selectedOffer: z.number().int().nonnegative().safe().optional(),
41
47
  redeemedDots: z.record(z.string(), RuleDotSchema).optional(),
@@ -137,7 +143,7 @@ export class FileRuleApprovalRecordStore {
137
143
  const parsed = ApprovalLineSchema.safeParse(raw);
138
144
  if (!parsed.success) {
139
145
  const stale = StaleApprovalLineSchema.safeParse(raw);
140
- if (stale.success && stale.data.record.schema !== 2) {
146
+ if (stale.success && stale.data.record.schema !== RULE_APPROVAL_RECORD_SCHEMA) {
141
147
  this.rows.set(stale.data.record.id, toStaleEnvelope(stale.data.record));
142
148
  continue;
143
149
  }
@@ -195,8 +201,10 @@ export class FileRuleImportTicketStore {
195
201
  rows = new Map();
196
202
  log;
197
203
  gate;
198
- constructor(dir, now, onError) {
204
+ monotonic;
205
+ constructor(dir, now, onError, monotonic) {
199
206
  this.now = now ?? Date.now;
207
+ this.monotonic = monotonic ?? (() => performance.now());
200
208
  ensureDir(dir);
201
209
  const path = join(dir, "import-tickets.jsonl");
202
210
  this.gate = new CorruptionGate("permission-rule import-ticket log", path, onError);
@@ -247,15 +255,21 @@ export class FileRuleImportTicketStore {
247
255
  close() {
248
256
  this.log.close();
249
257
  }
258
+ decisionNowMs(row) {
259
+ const a = row.mintAnchor;
260
+ return a === undefined ? this.now() : a.wallMs + (this.monotonic() - a.monoMs);
261
+ }
250
262
  async mint(input) {
251
263
  this.gate.assertUsable();
252
- const expiresAtMs = this.now() + input.ttlMs;
264
+ const mintedAtWall = this.now();
265
+ const mintedAtMono = this.monotonic();
266
+ const expiresAtMs = mintedAtWall + input.ttlMs;
253
267
  const payloadHash = buildRulePayloadHash(input.candidates);
254
268
  this.log.append({ op: "mint", ticketId: input.ticketId, ownerKey: input.principal, approvalId: input.approvalId, payloadHash, purpose: input.purpose, expiresAtMs }, true);
255
- this.rows.set(input.ticketId, { ownerKey: input.principal, approvalId: input.approvalId, payloadHash, purpose: input.purpose, expiresAtMs });
269
+ this.rows.set(input.ticketId, { ownerKey: input.principal, approvalId: input.approvalId, payloadHash, purpose: input.purpose, expiresAtMs, mintAnchor: { wallMs: mintedAtWall, monoMs: mintedAtMono } });
256
270
  return { ticketId: input.ticketId, approvalId: input.approvalId, payloadHash, expiresAtMs };
257
271
  }
258
- async consume(ticketId, principal, purpose, opts) {
272
+ async consume(ticketId, principal, purpose) {
259
273
  this.gate.assertUsable();
260
274
  const row = this.rows.get(ticketId);
261
275
  if (row === undefined)
@@ -267,7 +281,7 @@ export class FileRuleImportTicketStore {
267
281
  if (row.consumedAtMs !== undefined)
268
282
  return { ok: false, reason: "consumed" };
269
283
  const stampMs = this.now();
270
- if (row.expiresAtMs <= (opts?.decisionNowMs ?? stampMs))
284
+ if (row.expiresAtMs <= this.decisionNowMs(row))
271
285
  return { ok: false, reason: "expired" };
272
286
  const claimId = randomUUID();
273
287
  this.log.append({ op: "consume", ticketId, atMs: stampMs, claimId }, true);
@@ -275,12 +289,12 @@ export class FileRuleImportTicketStore {
275
289
  row.claimId = claimId;
276
290
  return { ok: true, approvalId: row.approvalId, payloadHash: row.payloadHash, claimId };
277
291
  }
278
- async release(ticketId, principal, claimId, mustRemainValidMs = 0, opts) {
292
+ async release(ticketId, principal, claimId, mustRemainValidMs = 0) {
279
293
  this.gate.assertUsable();
280
294
  const row = this.rows.get(ticketId);
281
295
  if (row === undefined || row.ownerKey !== principal || row.consumedAtMs === undefined || row.claimId !== claimId || row.outcome !== undefined)
282
296
  return false;
283
- if (row.expiresAtMs <= (opts?.decisionNowMs ?? this.now()) + mustRemainValidMs)
297
+ if (row.expiresAtMs <= this.decisionNowMs(row) + mustRemainValidMs)
284
298
  return false;
285
299
  this.log.append({ op: "release", ticketId }, true);
286
300
  delete row.consumedAtMs;
@@ -329,7 +343,7 @@ export function createFilePermissionRuleStores(root, opts) {
329
343
  ensureDir(dir);
330
344
  const provider = opts?.onError ? new FilePermissionRuleStoreProvider(dir, opts.onError) : new FilePermissionRuleStoreProvider(dir);
331
345
  const approvals = new FileRuleApprovalRecordStore(dir, opts?.onError);
332
- const tickets = new FileRuleImportTicketStore(dir, opts?.now, opts?.onError);
346
+ const tickets = new FileRuleImportTicketStore(dir, opts?.now, opts?.onError, opts?.monotonic);
333
347
  return {
334
348
  provider,
335
349
  approvals,