@sema-agent/server 7.55.0 → 7.56.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 (46) hide show
  1. package/MIGRATION.md +3 -3
  2. package/README.md +12 -6
  3. package/README.zh-CN.md +9 -5
  4. package/USAGE.md +108 -52
  5. package/deploy/sema-up/chart/values.yaml +1 -1
  6. package/dist/approval-content-kind.d.ts +22 -0
  7. package/dist/approval-content-kind.js +5 -0
  8. package/dist/boot/leader.d.ts +21 -0
  9. package/dist/boot/leader.js +6 -0
  10. package/dist/boot/runtime-caps.d.ts +2 -1
  11. package/dist/boot/runtime-caps.js +3 -1
  12. package/dist/config-center/types.d.ts +2 -1
  13. package/dist/config-provider.js +1 -0
  14. package/dist/http/route-ctx.d.ts +15 -3
  15. package/dist/http/routes/approvals-assistant.js +2 -2
  16. package/dist/http/routes/leader.js +2 -2
  17. package/dist/http/routes/workflows.js +1 -1
  18. package/dist/http/server.js +17 -3
  19. package/dist/leader/endpoint.js +5 -4
  20. package/dist/leader/wire.d.ts +50 -0
  21. package/dist/leader/wire.js +19 -7
  22. package/dist/main.js +1 -1
  23. package/dist/plugins/checkpoint-store-sql.d.ts +12 -0
  24. package/dist/plugins/checkpoint-store-sql.js +4 -1
  25. package/dist/plugins/file-run-store.js +2 -0
  26. package/dist/plugins/local-checkpoint-store.js +2 -0
  27. package/dist/plugins/local-session-store.d.ts +9 -9
  28. package/dist/plugins/local-session-store.js +5 -3
  29. package/dist/plugins/memory-run-store.js +2 -0
  30. package/dist/plugins/permission-rule-store-file.d.ts +22 -4
  31. package/dist/plugins/permission-rule-store-file.js +17 -8
  32. package/dist/plugins/permission-rule-store-sql.d.ts +60 -30
  33. package/dist/plugins/permission-rule-store-sql.js +23 -13
  34. package/dist/plugins/pg-session-storage.js +17 -13
  35. package/dist/plugins/run-store-sql.js +6 -4
  36. package/dist/plugins/store-contracts.d.ts +4 -0
  37. package/dist/plugins/tidb-session-store.js +18 -14
  38. package/dist/rules-consent.d.ts +5 -4
  39. package/dist/rules-consent.js +5 -33
  40. package/dist/runtime-caps-resolver.js +3 -1
  41. package/dist/security.d.ts +7 -0
  42. package/dist/tool-approval.d.ts +12 -60
  43. package/dist/tool-approval.js +6 -61
  44. package/package.json +2 -2
  45. package/skills/find-skills.md +1 -1
  46. package/skills/loop.md +1 -1
@@ -51,6 +51,12 @@ export function createLeaderFace(ctx) {
51
51
  retentionPolicy: governanceSeams.retentionPolicy,
52
52
  },
53
53
  deploymentPosture: buildDeploymentPostureSeats(config),
54
+ ...(ctx.tracer ? { tracer: ctx.tracer } : {}),
55
+ ...(config.usageWindows && ctx.usageWindowStore
56
+ ? { usageWindows: config.usageWindows, usageWindowStore: ctx.usageWindowStore }
57
+ : {}),
58
+ ...(ctx.promptSource ? { promptSource: ctx.promptSource } : {}),
59
+ ...(ctx.onError ? { onError: ctx.onError } : {}),
54
60
  fanoutEnabled: config.leaderFanoutEnabled,
55
61
  ...((process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID)
56
62
  ? { routerModel: (process.env.MODEL_ROUTER_ID || process.env.MODEL_CHEAP_ID) }
@@ -30,11 +30,12 @@ export interface RuntimeCapsCtx {
30
30
  * 都不问 verdict)。
31
31
  *
32
32
  * ⚠️ center 键 `EntitlementRuntimeCaps.allowMemoryOptOut` 是 settings-schema 的独立小件([ref] §四 开放问题①),
33
- * 到货前**不算源**;到货后本断言加一条「center 在场」析取臂即可,判据不变。
33
+ * 已到货(1.4.0,[ref])——析取臂已加(centerEntitlementPresent),判据不变;dry-run 不算源(不 APPLY 即不 ENFORCE 同律)。
34
34
  */
35
35
  export declare function assertMemoryCapturePolicyWirable(input: {
36
36
  policy: ServiceConfig["memoryCapturePolicy"];
37
37
  grantSourceWired: boolean;
38
+ centerEntitlementPresent?: boolean;
38
39
  }): void;
39
40
  export declare function createRuntimeCaps(ctx: RuntimeCapsCtx): {
40
41
  principalCaps: import("../runtime-caps-resolver.js").PrincipalEntitlementsClient | undefined;
@@ -4,6 +4,8 @@ export function assertMemoryCapturePolicyWirable(input) {
4
4
  return;
5
5
  if (input.grantSourceWired)
6
6
  return;
7
+ if (input.centerEntitlementPresent === true)
8
+ return;
7
9
  throw new Error(`MEMORY_CAPTURE_POLICY=governed requires a per-principal memory opt-out verdict source, and this deployment wires NONE ` +
8
10
  `— under "governed" core REFUSES every session memory-capture opt-out whose verdict is absent (fail-closed, the posture's ` +
9
11
  `own contract), so a governed worker without a source would refuse every \`memory.capture:"off"\` declaration and the ` +
@@ -15,7 +17,7 @@ export function assertMemoryCapturePolicyWirable(input) {
15
17
  }
16
18
  export function createRuntimeCaps(ctx) {
17
19
  const { config, logger } = ctx;
18
- assertMemoryCapturePolicyWirable({ policy: config.memoryCapturePolicy, grantSourceWired: ctx.memoryOptOutGrant !== undefined });
20
+ assertMemoryCapturePolicyWirable({ policy: config.memoryCapturePolicy, grantSourceWired: ctx.memoryOptOutGrant !== undefined, centerEntitlementPresent: Boolean(config.configCenter && !config.configCenter.dryRun) });
19
21
  if (config.configCenter && !config.configCenter.dryRun && scopedTokenNeedsWorker(config.configCenter.token, config.configCenter.worker)) {
20
22
  logger.warn("runtime_caps_scoped_token_no_worker", {
21
23
  reason: "SEMA_REGISTRY_TOKEN is a worker-scoped wpt_ token but SEMA_REGISTRY_WORKER is unset → center 403s per-principal caps → ALL workflow self-orchestration will be fail-closed denied. Set SEMA_REGISTRY_WORKER (the orchestrator normally injects it) or use the full SERVICE_PULL_TOKEN.",
@@ -103,7 +103,8 @@ export interface CenterMcpServer {
103
103
  * 所以它的家在 center 下发这条腿;请求腿那半场的门与结构性兑现见 `task-mcp.ts`
104
104
  * 的 `assertRequestMcpContentOrigin` 顶注。
105
105
  *
106
- * ⚠️ **两条腿今天都到不了这里**([ref] / [ref]③ / [ref],2026-08-31 双绊线实测更正 —— 本注旧版那句
106
+ * **两条腿已通**([ref],settings-schema 1.4.0 补键 + config-provider 映射行,2026-09-02;双绊线翻正销账)。
107
+ * 病史([ref]③ / [ref]③ / [ref],2026-08-31 双绊线实测更正 —— 本注更旧版那句
107
108
  * 「远端 /effective.mcp 腿是直读 JSON,今天就通」**不成立**,doc-rot):settings-schema(原 registry-core)
108
109
  * 的 `McpServerSpec` 是默认 strip 的 zod object 且没有 `contentOrigin` 可选键,本地腿在 FileConfigStore 的
109
110
  * `parseDomainLoud(DOMAIN_SCHEMAS.mcp)`、远端腿在消费端边界 `readEffectiveWire`([ref] 裁B)**各被剥一次**。
@@ -145,6 +145,7 @@ export function mapToServiceEffective(eff, version) {
145
145
  enabled: s.enabled,
146
146
  ...(s.allowTools !== undefined ? { allowTools: s.allowTools } : {}),
147
147
  ...(s.elicitation !== undefined ? { elicitation: s.elicitation } : {}),
148
+ ...(s.contentOrigin !== undefined ? { contentOrigin: s.contentOrigin } : {}),
148
149
  };
149
150
  if (s.transport.kind === "stdio") {
150
151
  return {
@@ -198,10 +198,16 @@ export interface RouteLegs {
198
198
  status: number;
199
199
  body: object;
200
200
  }>;
201
- /** assistant 抢占腿的复位(gate-kind 守卫 + 驱动)。 */
201
+ /** assistant 抢占腿的复位(gate-kind 守卫 + 行级属主复核 + 驱动)。 */
202
202
  resumePreempted(sessionId: string, req: IncomingMessage | undefined,
203
203
  /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/resume` 腿传 `true`。 */
204
- acceptEarly?: boolean): Promise<{
204
+ acceptEarly?: boolean,
205
+ /** [ref]②([ref]):调用方已验身份的裁定 —— 腿内在被真正载入的 checkpoint 行上重跑属主判
206
+ * ([ref] R3 同形;缺席 = 匿名 dev 部署,不咬)。 */
207
+ decider?: {
208
+ principal?: string;
209
+ explicitOperator: boolean;
210
+ }): Promise<{
205
211
  status: number;
206
212
  body: object;
207
213
  }>;
@@ -221,7 +227,13 @@ export interface RouteLegs {
221
227
  principalPresent?: boolean;
222
228
  },
223
229
  /** [ref] 件 S-1:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/plan_review` 腿传 `true`。 */
224
- acceptEarly?: boolean): Promise<{
230
+ acceptEarly?: boolean,
231
+ /** [ref]②([ref]):调用方已验身份的裁定(直连门 = HMAC 验出的 principal)—— 腿内行级属主复核
232
+ * ([ref] R3 同形;缺席 = 匿名 dev 部署,不咬)。 */
233
+ decider?: {
234
+ principal?: string;
235
+ explicitOperator: boolean;
236
+ }): Promise<{
225
237
  status: number;
226
238
  body: object;
227
239
  }>;
@@ -299,7 +299,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
299
299
  sendError(res, 404, "not_found.run", "task not found");
300
300
  return;
301
301
  }
302
- const out = await resumePreempted(run.sessionId, req, true);
302
+ const out = await resumePreempted(run.sessionId, req, true, { principal, explicitOperator });
303
303
  sendResumeOutcome(res, out, deps.logger);
304
304
  return;
305
305
  }
@@ -365,7 +365,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
365
365
  sendError(res, 404, "not_found.run", "task not found");
366
366
  return;
367
367
  }
368
- const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, { taskId, principalPresent: principal !== undefined }, true);
368
+ const out = await resumePlanReview(run.sessionId, decision, decision === "edit" ? body.editedPlan : undefined, typeof body.reason === "string" ? body.reason : undefined, req, { taskId, principalPresent: principal !== undefined }, true, { principal: deciderPrincipal, explicitOperator });
369
369
  sendResumeOutcome(res, out, deps.logger);
370
370
  return;
371
371
  }
@@ -7,7 +7,7 @@ export async function handleLeader(req, res, url, ctx) {
7
7
  }
8
8
  async function handleLeaderBody(req, res, url, ctx, miss) {
9
9
  const { deps } = ctx;
10
- const { readJson, rateLimited, quotaExceeded, leaseDenied } = ctx.helpers;
10
+ const { readJson, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied } = ctx.helpers;
11
11
  if (deps.leaderEndpoint) {
12
12
  const isLeaderPost = req.method === "POST" && url === "/v1/leader";
13
13
  const isLeaderGet = req.method === "GET" && /^\/v1\/leader\/[^/]+$/.test(url);
@@ -18,7 +18,7 @@ async function handleLeaderBody(req, res, url, ctx, miss) {
18
18
  }
19
19
  const requester = gatedPrincipal(req, deps.config) ?? null;
20
20
  if (isLeaderPost) {
21
- if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)))
21
+ if (rateLimited(req, res) || quotaExceeded(req, res) || (await leaseDenied(req, res)) || (await usageWindowDenied(req, res)))
22
22
  return;
23
23
  let body;
24
24
  try {
@@ -196,7 +196,7 @@ async function handleWorkflowAgentSteerBody(req, res, url, ctx, miss) {
196
196
  sendNotRunningWf(!wfRun
197
197
  ? "workflow agent is not running on this replica (no live handle)"
198
198
  : wfRun.status === "running"
199
- ? "workflow agent is active on another replica cross-replica live-steer is not yet supported"
199
+ ? "no live handle for this workflow agent on this replica — the run may be live on another replica (cross-replica live-steer is not yet supported), or this replica holds no steerable stream for it; read the workflow status faces for progress"
200
200
  : `workflow is ${wfRun.status} — agent is not running`);
201
201
  return;
202
202
  }
@@ -1809,7 +1809,7 @@ export function createHttpServer(rawDeps) {
1809
1809
  },
1810
1810
  };
1811
1811
  }
1812
- async function resumePreempted(sessionId, req, acceptEarly) {
1812
+ async function resumePreempted(sessionId, req, acceptEarly, decider) {
1813
1813
  const cs = deps.checkpointStore;
1814
1814
  const RESUMABLE_GATE_KIND = "resource_limit";
1815
1815
  const token = (await cs.findPendingTokenBySession(sessionId, undefined, { gateKinds: [RESUMABLE_GATE_KIND] })) ?? (await cs.findPendingTokenBySession(sessionId));
@@ -1824,6 +1824,13 @@ export function createHttpServer(rawDeps) {
1824
1824
  }
1825
1825
  if (!cp)
1826
1826
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1827
+ if (decider !== undefined && !decider.explicitOperator) {
1828
+ const cpOwner = decodeCheckpointScope(cp.scope);
1829
+ if (cpOwner !== undefined && cpOwner !== decider.principal) {
1830
+ deps.logger?.warn?.("assistant_resume_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
1831
+ return { status: 404, body: { error: "no resumable suspension for this task (already running, resumed, or expired)", errorCode: "not_found.suspension" } };
1832
+ }
1833
+ }
1827
1834
  const gateKind = cp.gate?.kind;
1828
1835
  if (gateKind !== RESUMABLE_GATE_KIND) {
1829
1836
  return { status: 409, body: { error: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a resumable resource/preempt suspension — an approval gate must be decided via POST /v1/approvals/:id/decide`, errorCode: "gate_not_resumable" } };
@@ -1899,7 +1906,7 @@ export function createHttpServer(rawDeps) {
1899
1906
  ...(acceptEarly === true ? { acceptEarly: true } : {}),
1900
1907
  });
1901
1908
  }
1902
- async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly) {
1909
+ async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log, acceptEarly, decider) {
1903
1910
  const cs = deps.checkpointStore;
1904
1911
  const log404 = (r) => deps.logger?.info?.("decide_404", {
1905
1912
  reason: r,
@@ -1923,6 +1930,13 @@ export function createHttpServer(rawDeps) {
1923
1930
  log404("checkpoint-missing");
1924
1931
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1925
1932
  }
1933
+ if (decider !== undefined && !decider.explicitOperator) {
1934
+ const cpOwner = decodeCheckpointScope(cp.scope);
1935
+ if (cpOwner !== undefined && cpOwner !== decider.principal) {
1936
+ deps.logger?.warn?.("plan_review_row_scope_mismatch", { sessionId, note: "loaded pending row is foreign to the verified caller — TOCTOU fold" });
1937
+ return { status: 404, body: { error: "no pending plan_review for this session (already decided or expired)", errorCode: "not_found.plan_review" } };
1938
+ }
1939
+ }
1926
1940
  const gateKind = cp.gate?.kind;
1927
1941
  if (gateKind !== PLAN_REVIEW_GATE_KIND) {
1928
1942
  return {
@@ -2106,7 +2120,7 @@ export function createHttpServer(rawDeps) {
2106
2120
  if (chunks.length === 0)
2107
2121
  return {};
2108
2122
  try {
2109
- return JSON.parse(Buffer.concat(chunks).toString("utf8"));
2123
+ return JSON.parse(Buffer.concat(chunks).toString("utf8")) ?? {};
2110
2124
  }
2111
2125
  catch {
2112
2126
  throw new HttpError(400, "invalid JSON body");
@@ -1,4 +1,5 @@
1
1
  import { uuidv7 } from "@sema-agent/core";
2
+ import { withPrincipal } from "../observability/principal-context.js";
2
3
  import { leaderIdemKey } from "../plugins/leader-run-store-sql.js";
3
4
  function statusForLeaderResult(result) {
4
5
  switch (result.repairTerminal) {
@@ -37,11 +38,11 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
37
38
  }
38
39
  }
39
40
  }
40
- function drive(id, body) {
41
+ function drive(id, body, owner) {
41
42
  void (async () => {
42
43
  let terminal;
43
44
  try {
44
- const result = await runLeader(body);
45
+ const result = await withPrincipal(owner ?? undefined, () => runLeader(body));
45
46
  const status = statusForLeaderResult(result);
46
47
  terminal = { status: status, result };
47
48
  opts.logger?.info?.("leader_run_done", { id, ok: result.ok, status, ...(result.repairTerminal ? { repairTerminal: result.repairTerminal } : {}), merged: result.merge && result.merge.ok ? result.merge.merged : undefined });
@@ -120,7 +121,7 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
120
121
  return { status: 202, body: { leaderRunId: run.id, status: run.status } };
121
122
  }
122
123
  runs.set(run.id, { id: run.id, status: "running", startedAt: run.startedAtMs, owner });
123
- drive(run.id, b);
124
+ drive(run.id, b, owner);
124
125
  return { status: 202, body: { leaderRunId: run.id, status: "running" } };
125
126
  }
126
127
  if (scopedIdem) {
@@ -133,7 +134,7 @@ export function createLeaderEndpoint(runLeader, opts = {}) {
133
134
  runs.set(id, { id, status: "running", startedAt: Date.now(), owner });
134
135
  if (scopedIdem)
135
136
  memIdem.set(scopedIdem, id);
136
- drive(id, b);
137
+ drive(id, b, owner);
137
138
  return { status: 202, body: { leaderRunId: id, status: "running" } };
138
139
  }
139
140
  const m = method === "GET" ? LEADER_ID_RE.exec(url) : null;
@@ -48,6 +48,56 @@ export interface LeaderWireConfig {
48
48
  * leader 编排的 planner / worker / repair / conflict 四类任务**静默不生效**(core 内建 deny 表仍在)。
49
49
  */
50
50
  deploymentPosture?: DeploymentPostureSeats;
51
+ /**
52
+ * [ref]([ref])—— **计费/遥测追踪席**,原样递给本车道的每一只 Runner。唯一属主 =
53
+ * `boot/budget-tracing.ts` 的 `createTracer(...)`(主 runner / subRunner / hook runner 经共享基座吃的
54
+ * 就是它),`boot/leader.ts` 搬的是**同一只**。
55
+ *
56
+ * 🔴 为什么这一席是承重的:`src/budget.ts` 的 `createTracer` 是 `brain.call` **唯一**的记账点 ——
57
+ * `costQuota.add` / `fleetUsage.record` / `fleetLease.recordSpend` / `model_cost_micro_usd_total` 四个写口
58
+ * 全在那里(`createBrain` 自己不记账)。而 `POST /v1/leader` 的准入处**读**的正是 costQuota / fleetLease
59
+ * 这两个桶。缺席 ⇒ 门查的桶与花钱的手不是同一只:`MAX_PRINCIPAL_COST_USD` 与 fleet 租约对这条腿结构性
60
+ * 失效,而一条 leader run 扇出 ≤6 个带手 worker + planner + repair/conflict 环,全是实钱。
61
+ * 归因的另一半在 `leader/endpoint.ts` 的 `drive()`(ALS `withPrincipal`)—— 记账臂按 `currentPrincipal()`
62
+ * 分租户,只补席不包裹 = 归因恒 undefined = 假绿。缺席 ⇒ 展开空对象(与接线前逐字相同)。
63
+ */
64
+ tracer?: RunnerDeps["tracer"];
65
+ /**
66
+ * [ref]([ref],[ref]-T1)—— **部署级 token 治理窗**两键,**成对**递给本车道的每一只 Runner。
67
+ * 属主 = `config.usageWindows`(声明)+ `boot/stores.ts` 造的账本店(两者同真同假,与
68
+ * `boot/runner-deps.ts` 的共享基座同规);leader 不自己读 config。
69
+ *
70
+ * 成对的理由是 core 的消费形:`prepare-task.js` 的 `buildUsageGovernance(usageWindows, deps, …)` 要求
71
+ * 声明与 `deps.usageWindowStore` **同时**在场才铸 `usageGovernance`,缺一即 undefined(既不 check 也不
72
+ * commit)⇒ 半席只是死键。缺席 ⇒ 展开空对象。
73
+ * 提交面那一半(`POST /v1/leader` 的 `usageWindowDenied` 门)在 `src/http/routes/leader.ts`。
74
+ */
75
+ usageWindows?: RunnerDeps["usageWindows"];
76
+ usageWindowStore?: RunnerDeps["usageWindowStore"];
77
+ /**
78
+ * [ref]([ref])—— **center 发布的 prompt catalog** 席,原样递给本车道的每一只 Runner。唯一属主 =
79
+ * `boot/config-center.ts` 的 `configCenter.promptSource`(主 runner / subRunner 同一只;run-local 有自己
80
+ * 的等价腿)。缺席 ⇒ leader 的 planner/worker/repair/conflict 四类**顶层**任务跑引擎内置提示词,而同一
81
+ * 部署的 `/v1/tasks`、`/v1/runs` 跑运维发布的那一份 —— 且 core 顶层新任务走的是**静默**臂(pinned /
82
+ * parentCenterArtifactDigest 两支才 `prompt.snapshot_unavailable` fail-loud),运维面零信号。
83
+ * 缺席 ⇒ 展开空对象(与接线前逐字相同)。
84
+ */
85
+ promptSource?: RunnerDeps["promptSource"];
86
+ /**
87
+ * [ref] 的**披露半件**([ref],codex 对抗复审 r2-[high],亲读装树 core 后修)—— 部署级错误汇。
88
+ *
89
+ * 🔴 为什么它与治理窗席**同生共死**:core 对「治理窗最终扣账失败」的唯一出口就是这一席 ——
90
+ * `dist/core/runner/runtask.js` 收尾段 `catch (flushErr) { this.deps.onError?.(flushErr, { phase: "config", … }) }`
91
+ * (以及 10s 慢盘披露的同一只回调),抛错之后任务**按原结果收尾**。挂了窗席却不挂本席 = 一次窗账
92
+ * 永久丢失时这条腿上零日志零计数,后续提交继续读偏低的窗被放行 —— CLAUDE.md [ref]「治理/门轴禁静默
93
+ * fail-open」正是说这个。它顺带也把 leader 车道其余 phase(degraded / rewind / memory / mcp / a2a /
94
+ * prompt-cache …)的既有观测面补齐,与 `onNotice` 席当年([ref]②)同一个理由。
95
+ *
96
+ * 属主唯一 = `boot/runner-deps.ts` `createRunnerDeps` 铸的那一只;`boot/leader.ts` 直引
97
+ * `runnerDeps.onError`(**同一实例**,[ref]§三族A 的最强同源形,与 main.ts subRunner 的先例逐字同姿势),
98
+ * leader 绝不另写一份臂表(两份必漂)。缺席 ⇒ 展开空对象(与接线前逐字相同)。
99
+ */
100
+ onError?: RunnerDeps["onError"];
51
101
  /** Worker model brain + catalog (the same the service runs tasks on). */
52
102
  brain: Brain;
53
103
  models: Record<string, Model>;
@@ -13,6 +13,7 @@ import { runLeaderTask } from "./leader.js";
13
13
  import { attachRepairLoopDeps } from "./repair-wire.js";
14
14
  import { routePlanWithFallback, validateSubtasks } from "./planner.js";
15
15
  import { createEngineNoticeSeat } from "../boot/runner-deps.js";
16
+ import { currentPrincipal } from "../observability/principal-context.js";
16
17
  const REPAIR_LOOP_WIRED = true;
17
18
  const sh = (env) => async (cmd) => {
18
19
  const r = await env.exec(`bash -lc ${JSON.stringify(cmd)}`);
@@ -130,6 +131,14 @@ export function createLeaderRunner(cfg) {
130
131
  const onNoticeSeat = createEngineNoticeSeat(cfg.logger);
131
132
  const governanceSeat = cfg.governance ?? {};
132
133
  const deploymentPostureSeat = cfg.deploymentPosture ?? {};
134
+ const tracerSeat = cfg.tracer ? { tracer: cfg.tracer } : {};
135
+ const usageWindowSeat = cfg.usageWindows && cfg.usageWindowStore ? { usageWindows: cfg.usageWindows, usageWindowStore: cfg.usageWindowStore } : {};
136
+ const promptSourceSeat = cfg.promptSource ? { promptSource: cfg.promptSource } : {};
137
+ const onErrorSeat = cfg.onError ? { onError: cfg.onError } : {};
138
+ const principalSpec = () => {
139
+ const p = currentPrincipal();
140
+ return p ? { principal: p } : {};
141
+ };
133
142
  const mkRepair = (rawEnv, repoDir, oracleFiles) => {
134
143
  if (repairRounds <= 0)
135
144
  return undefined;
@@ -143,7 +152,7 @@ export function createLeaderRunner(cfg) {
143
152
  throw new Error(`oracle still present after remove (${f.path}) — refusing repair (measurement integrity)`);
144
153
  }
145
154
  try {
146
- const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, executionEnv: rawEnv, rootPath: repoDir });
155
+ const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, executionEnv: rawEnv, rootPath: repoDir });
147
156
  const objective = [
148
157
  `The integrated project at ${repoDir} fails its build/test. Make the MINIMAL change to the working tree so this command exits 0 (cd into the repo and run it yourself to confirm):`,
149
158
  ` ${testCmd}`,
@@ -155,6 +164,7 @@ export function createLeaderRunner(cfg) {
155
164
  const res = await runner
156
165
  .runTaskStream({
157
166
  objective,
167
+ ...principalSpec(),
158
168
  sessionId: `leader-repair-${Date.now()}-${round}`,
159
169
  limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
160
170
  })
@@ -186,7 +196,7 @@ export function createLeaderRunner(cfg) {
186
196
  throw new Error(`oracle still present after remove (${f.path}) — refusing conflict-resolve (measurement integrity)`);
187
197
  }
188
198
  try {
189
- const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, executionEnv: rawEnv, rootPath: repoDir });
199
+ const runner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, executionEnv: rawEnv, rootPath: repoDir });
190
200
  const objective = [
191
201
  `A parallel worker '${workerId}' ported a module on its own branch, but applying its patch onto the already-integrated tree at ${repoDir} produced a MERGE CONFLICT (git apply --3way). The OTHER workers' patches already applied cleanly — integrate THIS worker's changes too, resolving the overlap.`,
192
202
  `Resolve EVERY conflict in the working tree: open each file containing conflict markers (<<<<<<< / ======= / >>>>>>>) and merge BOTH sides' real intent (keep both workers' behaviour — never drop one side just to make it apply). Then apply any rejected hunks recorded in *.rej files by hand, and DELETE every *.rej and *.orig file.`,
@@ -199,6 +209,7 @@ export function createLeaderRunner(cfg) {
199
209
  const res = await runner
200
210
  .runTaskStream({
201
211
  objective,
212
+ ...principalSpec(),
202
213
  sessionId: `leader-conflict-${Date.now()}-${round}`,
203
214
  limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
204
215
  })
@@ -249,10 +260,11 @@ export function createLeaderRunner(cfg) {
249
260
  if (Array.isArray(body.subtasks) && body.subtasks.length > 0) {
250
261
  return validateSubtasks(body.subtasks);
251
262
  }
252
- const planRunner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat });
263
+ const planRunner = new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat });
253
264
  const runRoute = async (prompt) => {
254
265
  const res = await planRunner.runTaskStream({
255
266
  objective: prompt,
267
+ ...principalSpec(),
256
268
  sessionId: `leader-route-${Date.now()}`,
257
269
  ...(cfg.routerModel ? { model: cfg.routerModel } : {}),
258
270
  }).result();
@@ -339,7 +351,7 @@ export function createLeaderRunner(cfg) {
339
351
  workerId: sub.workerId, sessionId, branch: sub.branch,
340
352
  baseSha,
341
353
  runner: keepCtxWarm(new Runner({
342
- brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat,
354
+ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat,
343
355
  ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}),
344
356
  ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}),
345
357
  executionEnvFactory: (ctx) => withStaging(envFactory(ctx), stage, async (e) => {
@@ -347,7 +359,7 @@ export function createLeaderRunner(cfg) {
347
359
  }),
348
360
  })),
349
361
  fetchDiff: () => fetchUploadedDiff(getUrl),
350
- spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, objective: `${sub.spec.objective}${uploadStepSuffix(W)}` },
362
+ spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` },
351
363
  };
352
364
  }
353
365
  if (!cfg.e2bApiKey)
@@ -359,10 +371,10 @@ export function createLeaderRunner(cfg) {
359
371
  const baseSha = await sh(env)(`cd ${repo} && git rev-parse HEAD`);
360
372
  return {
361
373
  workerId: sub.workerId, sessionId, branch: sub.branch, baseSha,
362
- runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
374
+ runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat, ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
363
375
  diffEnv: env,
364
376
  destroy: () => env.destroy().then(() => { }),
365
- spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec },
377
+ spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() },
366
378
  };
367
379
  };
368
380
  const provisionIntegrationSandbox = async () => {
package/dist/main.js CHANGED
@@ -536,7 +536,7 @@ async function main() {
536
536
  resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileHistoryStore,
537
537
  workflowCompletionInbox, taskListLane,
538
538
  });
539
- const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend });
539
+ const leaderEndpoint = createLeaderFace({ config, logger, brain, pricing, executionEnvFactory, toolResultStore, sessionStore, checkpointStore, governanceSeams, backend, tracer, usageWindowStore, promptSource: configCenter.promptSource, onError: runnerDeps.onError });
540
540
  const drainState = { draining: false };
541
541
  const authBridgeIssuer = process.env.AUTH_BRIDGE_ISSUER || config.configCenter?.baseUrl;
542
542
  const registryJwtVerifier = authBridgeIssuer
@@ -120,6 +120,18 @@ export interface PendingCheckpoint {
120
120
  * 展示/分诊用,永不参与 resume / gate / CAS(core 同款 ECHO-ONLY 定性)。
121
121
  */
122
122
  hasBidiControls?: true;
123
+ /**
124
+ * [ref]([ref]② core 点名 / [ref] 认领):**内容问句分型键** —— `"content_ask"` 当这条 tool_approval
125
+ * park 门住的是保留工具 AskUserQuestion(问用户的问题,不是副作用工具),消费端(cli/壳审批面)据它
126
+ * 渲问句 UI 而不用嗅探 `toolName`。词与判据 = core `summarizeCheckpoint` 的同一行规则(行上 core 铸的
127
+ * `toolName` 的**全函数**派生;词属主 `approval-content-kind.ts`,parity 钉看漂移)——**不是**
128
+ * `hasBidiControls` 那类「server 不重算」的扫描位:派生 ≠ 重扫。
129
+ * 🔴 `"content_ask"` 或**缺席**([ref]② OMIT 纪律,恒不铸 `null`/`false`):缺席 = 非内容问句门
130
+ * (普通工具门 / 非工具 park / toolName 缺席的存量行)。展示/分诊用,永不参与 resume / gate / CAS。
131
+ * 两条 durable 读面(`GET /v1/approvals` 行 + `/stream` `pending` 帧)经同一投影同时携带;live 腿的
132
+ * 同名键在 `LivePendingRow.contentKind`(同一词属主)。
133
+ */
134
+ contentKind?: "content_ask";
123
135
  }
124
136
  /**
125
137
  * [ref]([ref] / DEBTS [ref]):session 键读口(`findPendingTokenBySession` / `peekPendingScope`)的**行绑定**。
@@ -1,3 +1,4 @@
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";
@@ -437,6 +438,7 @@ export class SqlCheckpointStore {
437
438
  ? await this.db.query(`${base}${this.q(" AND c.scope=?", " AND c.scope=$1")} ORDER BY c.created_at_ms ASC`, [scope])
438
439
  : await this.db.query(`${base} ORDER BY c.created_at_ms ASC`);
439
440
  const out = rows.map((r) => {
441
+ const toolName = r.tool_name ?? null;
440
442
  const toolCallId = r.tool_call_id ?? null;
441
443
  const boundInputHash = r.bound_input_hash ?? null;
442
444
  const gateKind = r.gate_kind ?? null;
@@ -444,7 +446,7 @@ export class SqlCheckpointStore {
444
446
  return {
445
447
  sessionId: String(r.session_id),
446
448
  scope: String(r.scope),
447
- toolName: r.tool_name ?? null,
449
+ toolName,
448
450
  toolCallId,
449
451
  ...(toolCallId !== null ? { boundCallId: toolCallId } : {}),
450
452
  ...(boundInputHash !== null ? { boundInputHash } : {}),
@@ -456,6 +458,7 @@ export class SqlCheckpointStore {
456
458
  riskDescriptor: readRiskDescriptorCell(r.risk_descriptor),
457
459
  ...(ruleOffers !== undefined ? { ruleOffers } : {}),
458
460
  ...(Number(r.has_bidi_controls) === 1 ? { hasBidiControls: true } : {}),
461
+ ...(contentKindOf(toolName) !== undefined ? { contentKind: "content_ask" } : {}),
459
462
  };
460
463
  });
461
464
  return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
@@ -453,6 +453,7 @@ export class FileRunStore {
453
453
  continue;
454
454
  const sorted = [...arr].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || (a.taskId < b.taskId ? 1 : a.taskId > b.taskId ? -1 : 0));
455
455
  const latest = sorted[0];
456
+ const latestRunId = latest.result?.runId;
456
457
  const times = arr.map((r) => r.createdAt.getTime());
457
458
  const last = Math.max(...times);
458
459
  const first = Math.min(...times);
@@ -465,6 +466,7 @@ export class FileRunStore {
465
466
  objectivePreview: latest.objectivePreview,
466
467
  lastStatus: latest.status,
467
468
  lastRunId: latest.taskId,
469
+ ...(typeof latestRunId === "string" && latestRunId !== "" ? { lastTaskRunId: latestRunId } : {}),
468
470
  title: null,
469
471
  });
470
472
  }
@@ -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);
@@ -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 {
@@ -186,6 +186,7 @@ export class MemoryRunStore {
186
186
  continue;
187
187
  const sorted = [...arr].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime() || (a.taskId < b.taskId ? 1 : a.taskId > b.taskId ? -1 : 0));
188
188
  const latest = sorted[0];
189
+ const latestRunId = latest.result?.runId;
189
190
  const times = arr.map((r) => r.createdAt.getTime());
190
191
  const last = Math.max(...times);
191
192
  const first = Math.min(...times);
@@ -198,6 +199,7 @@ export class MemoryRunStore {
198
199
  objectivePreview: latest.objectivePreview,
199
200
  lastStatus: latest.status,
200
201
  lastRunId: latest.taskId,
202
+ ...(typeof latestRunId === "string" && latestRunId !== "" ? { lastTaskRunId: latestRunId } : {}),
201
203
  title: null,
202
204
  });
203
205
  }