@sema-agent/server 7.101.1 → 7.102.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 (82) hide show
  1. package/MIGRATION.md +23 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +58 -4
  5. package/dist/approval-card.d.ts +35 -2
  6. package/dist/approval-card.js +19 -1
  7. package/dist/background-session-reap.d.ts +55 -0
  8. package/dist/background-session-reap.js +90 -0
  9. package/dist/boot/config-center.d.ts +16 -4
  10. package/dist/boot/config-center.js +44 -15
  11. package/dist/boot/exit-boundary.js +17 -0
  12. package/dist/boot/runner-deps.js +1 -0
  13. package/dist/boot/runtime-caps.js +3 -2
  14. package/dist/boot/session-faces.js +3 -4
  15. package/dist/boot/shutdown.d.ts +2 -1
  16. package/dist/boot/shutdown.js +33 -12
  17. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  18. package/dist/boot/stage-07-capability-layer.js +6 -4
  19. package/dist/boot/stage-08-reapers.d.ts +1 -1
  20. package/dist/boot/stage-09-leader.d.ts +1 -1
  21. package/dist/boot/stage-10-http-server.d.ts +1 -1
  22. package/dist/boot/stage-10-http-server.js +7 -3
  23. package/dist/capabilities/builtin-tools.d.ts +14 -1
  24. package/dist/capabilities/builtin-tools.js +3 -0
  25. package/dist/capabilities/scenarios.d.ts +1 -1
  26. package/dist/capabilities/scenarios.js +3 -3
  27. package/dist/center-credential-renewer.d.ts +76 -0
  28. package/dist/center-credential-renewer.js +359 -0
  29. package/dist/center-credential.d.ts +76 -22
  30. package/dist/center-credential.js +119 -43
  31. package/dist/config-catalog.js +9 -4
  32. package/dist/config-center/apply-effective.js +5 -12
  33. package/dist/config-center/center-request.d.ts +13 -0
  34. package/dist/config-center/center-request.js +23 -3
  35. package/dist/config-center/facade.d.ts +1 -1
  36. package/dist/config-center/http-client.js +2 -5
  37. package/dist/config-center/read-warnings.d.ts +19 -48
  38. package/dist/config-center/read-warnings.js +7 -49
  39. package/dist/config-center/restart-signal.d.ts +8 -11
  40. package/dist/config-center/types.d.ts +4 -2
  41. package/dist/config-provider.js +1 -48
  42. package/dist/config-types.d.ts +77 -5
  43. package/dist/config-types.js +5 -0
  44. package/dist/config.js +49 -13
  45. package/dist/device-store.d.ts +1 -1
  46. package/dist/fleet-client.d.ts +21 -7
  47. package/dist/fleet-client.js +41 -12
  48. package/dist/fleet-lease.d.ts +1 -1
  49. package/dist/http/approval-sweeps.js +5 -20
  50. package/dist/http/dispatch.js +20 -14
  51. package/dist/http/readiness.d.ts +87 -0
  52. package/dist/http/readiness.js +54 -0
  53. package/dist/http/resume-legs.js +5 -4
  54. package/dist/http/route-table.d.ts +7 -0
  55. package/dist/http/route-table.js +6 -0
  56. package/dist/http/routes/a2a-serve.js +6 -14
  57. package/dist/http/routes/approvals-assistant.js +1 -0
  58. package/dist/http/routes/background.d.ts +80 -0
  59. package/dist/http/routes/background.js +94 -0
  60. package/dist/http/routes/capabilities.js +2 -0
  61. package/dist/http/server.d.ts +21 -8
  62. package/dist/http/server.js +2 -2
  63. package/dist/model-entry-refusal.d.ts +11 -10
  64. package/dist/model-entry-refusal.js +13 -10
  65. package/dist/observability/fail-open.d.ts +2 -2
  66. package/dist/observability/fail-open.js +2 -2
  67. package/dist/parked-decide.d.ts +11 -5
  68. package/dist/parked-decide.js +4 -1
  69. package/dist/parked-revive-surface.d.ts +47 -0
  70. package/dist/parked-revive-surface.js +20 -0
  71. package/dist/plugins/background-shell-support.d.ts +20 -1
  72. package/dist/plugins/background-shell-support.js +26 -0
  73. package/dist/plugins/host-platform.d.ts +11 -1
  74. package/dist/plugins/host-platform.js +11 -0
  75. package/dist/plugins/remote-env-host.d.ts +14 -1
  76. package/dist/plugins/remote-env-host.js +18 -5
  77. package/dist/tool-approval.d.ts +30 -1
  78. package/dist/tool-approval.js +31 -8
  79. package/dist/trace/core-keyset-guard.d.ts +3 -3
  80. package/dist/trace/engine-notice-wire.d.ts +1 -1
  81. package/dist/trace/engine-notice-wire.js +2 -0
  82. package/package.json +3 -3
@@ -3,7 +3,8 @@ import type { IncomingMessage } from "node:http";
3
3
  import { type Runner, type TaskSpec, type TaskResult, type WorkflowRunStore, type MemoryEntry, type MemoryExportBundle, type MemoryImportReport, type EntryProvenanceAccount, type MemoryErasureAttestation, type MemoryEntryOrigin, type OriginClearanceRow, type SessionMemoryStatus, type ConsolidationRunReceipt, type ConsolidationDriverRunRow, type SideQuerySpec, type SideQueryResult, type Brain, type EngineNotice } from "@sema-agent/core";
4
4
  import type { TaskRequestBody } from "./wire-types.js";
5
5
  import type { ServiceConfig } from "../config-types.js";
6
- import { type RestartSignal, type SessionMirrorRuling } from "../config-center/facade.js";
6
+ import { type SessionMirrorRuling } from "../config-center/facade.js";
7
+ import type { RestartSignal } from "@sema-agent/settings-schema/fleet";
7
8
  import { type OwnerAwareSessionStore } from "../security.js";
8
9
  import type { RunStore, ImageIndex, ImageBake, CheckpointStoreFull, ResumeAnchorStore, ApprovalExemptionStore, ApprovalNonceStore, ServiceSessionPolicyStore, ServiceFileHistoryStore, ToolResultStoreFull, StoreBackend } from "../plugins/store-backend.js";
9
10
  import type { RuleConsentLane } from "../rules-consent.js";
@@ -549,7 +550,9 @@ export interface ServiceSeamDeps {
549
550
  };
550
551
  };
551
552
  }) => void;
552
- parkedReviveTool?: import("@sema-agent/core").ToolSpec;
553
+ /** #1110(core 7.32.0):parked 赎回面工厂(按行解析驱动哪只 Agent 工具 + 同出一张列表的继承座;属主 `parked-revive-surface.ts`)。
554
+ * 缺席 = 这台部署没有 park 底座(checkpointStore / backgroundAgentStore 任一缺席),bg 赎回臂不接线。 */
555
+ parkedReviveSurface?: import("../parked-revive-surface.js").ParkedReviveSurfaceFactory;
553
556
  /** 可选第三件:revive args 的 `subagent_type` roster 成员集([ref] Q1 成员性规则)。当前生产装配
554
557
  * **恒缺席**(boot 内建 default 无自定义 agents roster,per-request 的 spec.agents 不进 boot 工具)
555
558
  * = 恒省略 subagent_type,typed 子代按 general-purpose 形复活(保真度 caveat 已报 core,[ref]
@@ -562,7 +565,7 @@ export interface ServiceSeamDeps {
562
565
  * `rebuildInheritedGate` 头注。 */
563
566
  parkedReviveInheritedGate?: (row: import("@sema-agent/core").BackgroundAgentRecord, identity: {
564
567
  principal: string | undefined;
565
- }) => Promise<unknown>;
568
+ }) => Promise<import("../boot/parked-revive-gate.js").RebuiltInheritedGate | undefined>;
566
569
  }
567
570
  /** **可观测面**:日志/指标/累加器。全体 fail-open——观测失败绝不改变业务面行为。 */
568
571
  export interface ServiceObservabilityDeps {
@@ -640,7 +643,8 @@ export interface ServiceDeploymentDeps {
640
643
  * respond/subagent) stay open so the runs we are waiting on can still finish/interact.
641
644
  * • `inflight` is ASSIGNED BY createServer (a live union of this replica's in-flight legs: durable bg/resume
642
645
  * `inflightRuns` + live sync/resume streams `steerableRuns`) — main.ts polls it to know when drain is done.
643
- * • /health carries `draining:true` (k8s readiness 摘流信号) + `version` (build self-description).
646
+ * • /health carries `draining:true` + `version` (build self-description); draining is a readiness reason
647
+ * (`http/readiness.ts`) ⇒ `/readyz` 503 + `/health` `ready:false` (the k8s readiness 摘流信号).
644
648
  * • [ref]:`attachedStreams` 同样 ASSIGNED BY createServer —— 本副本当前打开着的 SSE 流条数
645
649
  * (= 引擎附着租约集的大小)。消费方是 `boot/engine-lease.ts` 的自退判据;与 `inflight` 同族,
646
650
  * 同一个共享盒子,不另开第二条 main↔server 的通道。
@@ -654,21 +658,28 @@ export interface ServiceDeploymentDeps {
654
658
  attachedStreams?: () => number;
655
659
  lastAttachEndedAt?: () => number;
656
660
  };
657
- /** boot ready 门(b):false = registry 部署无显式 env 模型且首次 effective pull 尚未落 roster
658
- * (worker 只有占位模型)。计费提交 503 + /health 加性 `ready:false`。absent = 恒 ready(env 模型在/非
659
- * registry 部署),现有部署零影响。main.ts 在 applyEffective 落模型后翻 true(one-way latch)。 */
661
+ /** boot ready 门(b):false = 布防(`CONFIG_REQUIRE_ROSTER=true`)且首次 effective pull 尚未落 roster
662
+ * (worker 只有占位模型)。就绪原因 `model_roster_pending`(`http/readiness.ts`):计费提交 503 + `/readyz` 503 +
663
+ * /health 加性 `ready:false`。absent = 恒 ready(未布防 / 非 registry 部署),现有部署零影响。config-center 的
664
+ * markRosterLanded 在落模型后翻 true(one-way latch)。 */
660
665
  modelReady?: () => boolean;
661
666
  /** Sema-registry restart signal: the refresh loop sets this when a pull carries restart-to-apply
662
667
  * changes (skills/mcp/a2a/scenarios/… baked at boot — the `RestartSlice` closed set) that DIFFER from this process's boot config.
663
668
  * Surfaced at GET /health as `restartRequired` + `restart{reasons,version,since}` so the orchestrator can
664
669
  * auto rolling-restart the worker. Change-detected (not presence) — absent when nothing baked-at-boot
665
- * changed, so polling it never causes a restart loop. */
670
+ * changed, so polling it never causes a restart loop. S-741:the fleet heartbeat reads THIS getter too
671
+ * (`boot/stage-10-http-server.ts` → `restart: () => deployment.restartState?.()`), so `/health.restart` and the
672
+ * announcement's `restart` are the same object — one mint point, two exits. */
666
673
  restartState?: () => RestartSignal | undefined;
667
674
  /** [ref] 世代账([ref] §2):本副本的配置收敛状态 —— `/health` 取三键
668
675
  * (configTargetVersion / configAppliedVersion / configApplyStaleMs),`GET /v1/diagnostics/wiring` 取整份
669
676
  * (按组的 appliedVersion / deferredKeys+铸因 / lastRejected 消毒形)。缺席 = 这个进程没有配置管道
670
677
  * (纯 env 部署 / 测试夹具)⇒ 两个面的形状逐字不变(additive)。 */
671
678
  configApplyState?: () => import("../config-center/apply-ledger.js").ConfigApplyLedgerView | undefined;
679
+ /** S-698:中心凭证健康位(`/health.centerCredential`)—— 凭证来源的健康态 + 续签器状态(`center-credential-renewer.ts`
680
+ * `buildCenterCredentialHealthView` 铸形,只有闭集词 / 时刻 / 计数,不带路径与令牌)。缺席 / 返回 undefined = 健康或没有中心
681
+ * ⇒ `/health` 形状逐字不变(additive)。 */
682
+ centerCredentialState?: () => import("../center-credential-renewer.js").CenterCredentialHealthView | undefined;
672
683
  /** [ref] 追加件:`POST /v1/admin/config/refresh` 的实现面 —— 触发**一次既有**的 refresh 拍(在飞则 join)。
673
684
  * 缺席 / 回 `"not-wired"` = 本部署没有配置管道 ⇒ 该口诚实 4xx(绝不吞成 200)。 */
674
685
  configRefreshNow?: () => Promise<"triggered" | "not-wired" | "failed">;
@@ -798,6 +809,8 @@ export declare function flattenServiceDeps(deps: ServiceDeps): FlatServiceDeps;
798
809
  /**
799
810
  * Managed-Agents-style HTTP/SSE service surface.
800
811
  * GET /health
812
+ * GET /readyz → readiness probe: 200 {ready:true} / 503 {ready:false, reasons} (S-739; same predicate
813
+ * as /health.ready, the billable-submit 503s and the fleet heartbeat — http/readiness.ts)
801
814
  * POST /v1/tasks → run to completion, TaskResult JSON (synchronous)
802
815
  * POST /v1/tasks/stream → SSE of TaskEvent, live tokens (synchronous, same instance)
803
816
  * POST /v1/runs → async: 202 {taskId, sessionId, status}, runs in background (S1)
@@ -24,7 +24,7 @@ import { createRequestGuards } from "./request-guards.js";
24
24
  import { createAdmission, MAX_SKILL_CONTENT_CHARS, MAX_SYSTEM_PROMPT_CHARS, MAX_OUTPUT_SCHEMA_CHARS, validateUserSkills } from "./admission.js";
25
25
  import { createResumeLegs } from "./resume-legs.js";
26
26
  import { createDispatch, hasServiceAuth } from "./dispatch.js";
27
- import { routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath } from "./route-table.js";
27
+ import { routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, isProbePath } from "./route-table.js";
28
28
  import { createApprovalSweeps } from "./approval-sweeps.js";
29
29
  export { routeLabel, isBillableSubmitPath, isCredentialGatedRewrite, isMethodDispatchedSubmitPath, hasServiceAuth };
30
30
  export { MAX_SKILL_CONTENT_CHARS, MAX_SYSTEM_PROMPT_CHARS, MAX_OUTPUT_SCHEMA_CHARS, validateUserSkills };
@@ -49,7 +49,7 @@ export function createHttpServer(rawDeps) {
49
49
  deps.drainState.inflight = () => new Set([...inflightRuns.keys(), ...steerableRuns.keys()]).size + counters.uncountedBillableInflight + counters.admittedInflight + (deps.leaderEndpoint?.inflight?.() ?? 0);
50
50
  let lastBillableActivityAt = performance.now();
51
51
  const noteActivity = (url) => {
52
- if (url === "/health" || url.startsWith("/metrics"))
52
+ if (isProbePath(url))
53
53
  return;
54
54
  lastBillableActivityAt = performance.now();
55
55
  };
@@ -8,7 +8,7 @@ export declare const MODEL_ENTRY_REFUSAL_CODES: {
8
8
  export type ModelEntryRefusalCode = (typeof MODEL_ENTRY_REFUSAL_CODES)[ModelEntryRefusalReason];
9
9
  /** 「这张表没有缺省模型」的机读码(S-705;boot 拒启与热路径模型面组拒同一个码)。 */
10
10
  export declare const DEFAULT_MODEL_MISSING_CODE = "config.default_model_missing";
11
- /** 🔴 S-712「被拒的名字被档位 / 别名增广铸到了别的条目上(或读不出被丢条目的名字,无从证明没有)」的机读码
11
+ /** 🔴 S-712「被拒的名字被档位 / 别名增广铸到了别的条目上(或被丢身份载体形不对、读不出名字,无从证明没有)」的机读码
12
12
  * (不变量 `refused-names-not-minted`;热路径 / boot 首次 apply / dry-run 同一个码)。 */
13
13
  export declare const REFUSED_NAME_MINTED_CODE = "config.model_refused_name_minted";
14
14
  /**
@@ -47,20 +47,21 @@ export interface CatalogEntryLike {
47
47
  * 坐标,不取它的输出(输出会补缺省、剥扩展键,那是读腿的事)。
48
48
  */
49
49
  export declare function judgeCatalogEntry(m: CatalogEntryLike, ctx: CatalogEntryVerdictCtx): ModelEntryRefusal | undefined;
50
- /** 一批已启用条目的判决:可用的(保序)、不可用的(按 catalog name)、以及身份证明不了的被丢条目(坐标)。 */
50
+ /** 一批已启用条目的判决:可用的(保序)、不可用的(按 catalog name)、以及形不对的被丢身份载体行(坐标)。 */
51
51
  export interface CatalogEntriesVerdict<M extends CatalogEntryLike> {
52
52
  readonly usable: readonly M[];
53
53
  readonly refused: Readonly<Record<string, ModelEntryRefusal>>;
54
- /** S-712:读腿逐条丢掉、而身份证明不了的条目坐标(`models.3`;载体上不带 `name` 的那些 —— 读腿只在原文档对不上本次解析时
55
- * 这样交出,见 `read-warnings.ts` `DroppedModelIdentity`)。非空 ⇒ 不变量 `refused-names-not-minted` 无从成立 ⇒ 模型面组拒。 */
56
- readonly unreadable: readonly string[];
54
+ /** S-712:被丢身份**载体形不对**的行坐标(`models.3`;载体不是列表时是一个固定标记)。读腿只为有名的被丢条目铸载体行
55
+ * (`read-warnings.ts` `DroppedModelIdentity`,`name` 必在),所以这一形只来自绕过读腿的输入被改过(盘上 LKG 被手改 / 手写
56
+ * effective)。非空 ⇒ 读不出的名字无从证明它不被增广铸出 ⇒ 不变量 `refused-names-not-minted` 不成立 ⇒ 模型面组拒(fail-closed)。 */
57
+ readonly malformed: readonly string[];
57
58
  }
58
59
  /**
59
60
  * 批量形(模型面 stage 与 dry-run 共用这一只 —— 报数与真 apply 永不撕裂)。
60
61
  * S-712:`dropped` = 读腿交出的被丢条目身份(`EffectiveConfig.models.droppedIdentities`)—— 它们与逐条判据判出的 `entry_invalid`
61
- * 是**同一件事**(条目不合 `ModelEntry` 形),只是在第一道边界就被丢了;带名字的铸墓碑,不带名字的(读腿证明不了身份)进
62
- * `unreadable`。没有字符串名的坏行读腿已按停用处置、根本不进载体(7.101.0 验修轮 V-2:没有名字就没有可保留的名字)。
63
- * 入参按 `unknown` 判形:载体可能来自盘上 LKG(本仓写的,但文件可被改),形不对的一律按证明不了处置(fail-closed)。
62
+ * 是**同一件事**(条目不合 `ModelEntry` 形),只是在第一道边界就被丢了;每一行铸一块墓碑(拒因 provider 词空:告警身份不带它)。
63
+ * 入参按 `unknown` 判形:载体可能来自盘上 LKG(本仓写的,但文件可被改)。**规则一条**:载体要么缺席,要么是一列带字符串 `name`
64
+ * 的行;其余一律「载体形不对」进 `malformed`(fail-closed)—— 读腿自己从不铸这种形。
64
65
  * 与一条可用 / 已判不可用条目同名时不覆盖(可用条目仍可用;同名撞车由不变量 `refused-names-not-minted` 响亮拒)。
65
66
  */
66
67
  export declare function judgeCatalogEntries<M extends CatalogEntryLike>(enabled: readonly M[], ctx: CatalogEntryVerdictCtx, dropped?: unknown): CatalogEntriesVerdict<M>;
@@ -125,6 +126,6 @@ export interface RefusedNameView {
125
126
  export declare function mintedRefusedNames(view: RefusedNameView): MintedRefusedName[];
126
127
  /** 不变量 `refused-names-not-minted` 的拒因句(`undefined` = 成立)。warn 与世代账 / 拒因同一句;逐对明细另走 {@link mintedRefusedNames}。 */
127
128
  export declare function refusedNameMintedRefusal(view: RefusedNameView): string | undefined;
128
- /** 身份证明不了的被丢条目的拒因句(同一不变量同一个码:证明不了的名字无从证明它不被增广铸出)。 */
129
- export declare function unreadableDroppedRefusal(entries: readonly string[]): string;
129
+ /** 被丢身份载体形不对的拒因句(同一不变量同一个码:读不出的名字无从证明它不被增广铸出)。 */
130
+ export declare function malformedDroppedCarrierRefusal(entries: readonly string[]): string;
130
131
  //# sourceMappingURL=model-entry-refusal.d.ts.map
@@ -50,6 +50,7 @@ function entryInvalidRefusal(id, provider, fields, issues) {
50
50
  "deployment's catalog on its own (the other entries keep serving) instead of being served with a value nothing validated; fix the entry to bring it back",
51
51
  };
52
52
  }
53
+ const MALFORMED_CARRIER_NOT_A_LIST = "(droppedIdentities is not a list)";
53
54
  export function judgeCatalogEntries(enabled, ctx, dropped) {
54
55
  const usable = [];
55
56
  const refused = {};
@@ -60,20 +61,22 @@ export function judgeCatalogEntries(enabled, ctx, dropped) {
60
61
  else
61
62
  refused[String(m.name)] = r;
62
63
  }
63
- const unreadable = [];
64
- for (const d of Array.isArray(dropped) ? dropped : []) {
64
+ const rows = dropped === undefined ? [] : Array.isArray(dropped) ? dropped : undefined;
65
+ if (rows === undefined)
66
+ return { usable, refused, malformed: [MALFORMED_CARRIER_NOT_A_LIST] };
67
+ const malformed = [];
68
+ for (const d of rows) {
65
69
  const e = (typeof d === "object" && d !== null ? d : {});
66
- const where = typeof e.entry === "string" ? e.entry : "(unknown coordinate)";
67
70
  if (typeof e.name !== "string") {
68
- unreadable.push(where);
71
+ malformed.push(typeof e.entry === "string" ? e.entry : "(unknown coordinate)");
69
72
  continue;
70
73
  }
71
74
  if (Object.hasOwn(refused, e.name))
72
75
  continue;
73
76
  const fields = Array.isArray(e.fields) ? e.fields.filter((f) => typeof f === "string") : [];
74
- refused[e.name] = entryInvalidRefusal(typeof e.id === "string" ? e.id : "", typeof e.provider === "string" ? e.provider : "", fields, typeof e.issues === "string" ? e.issues : fields.join(", "));
77
+ refused[e.name] = entryInvalidRefusal(typeof e.id === "string" ? e.id : "", "", fields, typeof e.issues === "string" ? e.issues : fields.join(", "));
75
78
  }
76
- return { usable, refused, unreadable };
79
+ return { usable, refused, malformed };
77
80
  }
78
81
  export function refusalEventOf(reason) {
79
82
  switch (reason) {
@@ -149,9 +152,9 @@ export function refusedNameMintedRefusal(view) {
149
152
  "an explicit pick of that name (or a role bound to it) would silently run on the other entry's endpoint and account; " +
150
153
  "rename the unavailable entry, fix it (see its own warn), or rebind / drop the tier binding that mints the name.");
151
154
  }
152
- export function unreadableDroppedRefusal(entries) {
153
- return (`catalog entries dropped by the settings-schema read could not be identified [${REFUSED_NAME_MINTED_CODE}]: ${entries.join(", ")} — ` +
154
- "the raw document could not be matched to the snapshot that was parsed (e.g. the local file changed between two reads), so the candidate cannot prove the tier/alias expansion does not re-mint their names; " +
155
- "the next read re-judges (fix the named entries if this persists).");
155
+ export function malformedDroppedCarrierRefusal(entries) {
156
+ return (`the dropped-entry identity carrier of this candidate is malformed [${REFUSED_NAME_MINTED_CODE}]: ${entries.join(", ")} — ` +
157
+ "every row the settings-schema read writes there carries a string name, so this carrier was edited outside the read legs (e.g. a hand-edited last-known-good file); " +
158
+ "the candidate cannot prove the tier/alias expansion does not re-mint the unnamed entries, so its model plane is refused — restore or delete that carrier (a fresh read of the source re-mints it).");
156
159
  }
157
160
  //# sourceMappingURL=model-entry-refusal.js.map
@@ -123,7 +123,7 @@ export declare const FAIL_OPEN_TAGS: {
123
123
  };
124
124
  readonly "server.approvals.park-row-blob-unreadable": {
125
125
  readonly cls: "F";
126
- readonly note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。";
126
+ readonly note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)/ `mandate`(S-721)/ `origin`(S-755)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。";
127
127
  };
128
128
  readonly "server.sql-json.checkpoint.pending-steer-queue-lossy": {
129
129
  readonly cls: "F";
@@ -287,7 +287,7 @@ export declare const FAIL_OPEN_TAGS: {
287
287
  };
288
288
  readonly "server.approval-card.closed-word-out-of-set": {
289
289
  readonly cls: "F";
290
- readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天的唯一成员是 `readRuleOffersAbsence`):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。";
290
+ readonly note: "S-249(`approval-card.ts` 的边界窄读族;今天两个成员:`readRuleOffersAbsence`,与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。";
291
291
  };
292
292
  readonly "server.pg-pool.idle-client-error": {
293
293
  readonly cls: "F";
@@ -114,7 +114,7 @@ export const FAIL_OPEN_TAGS = {
114
114
  },
115
115
  "server.approvals.park-row-blob-unreadable": {
116
116
  cls: "F",
117
- note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。",
117
+ note: "S-700(`plugins/checkpoint-store-sql.ts` 的 `readParkRowFactsCell`):运维待批队列(`GET /v1/approvals` / `/stream`)的某一行,`checkpoint` 列里寄存行的 **`pendingAction` 子树**(NP-3 / 7.100.0 起引擎内只取这一棵、整只快照不出库;只为 park 行事实 `requiresRealApproval` / `ruleOffersAbsence` / `mandated`(S-663 二段,core #1079)/ `mandate`(S-721)/ `origin`(S-755)而读 —— 事实位的真源是 `approval-card.ts` 的 `ParkRowFacts`)**在场却不是一只对象**(JSON `null` 字面 / 串 / 数 —— 手改 / 迁移脚本改过)。子树**缺席**(SQL NULL:成员不在,或整只文档不是对象 —— 后者 `get()` 照旧拒;MariaDB 文本承载上的非 JSON 字节同样答 SQL NULL)⇒ 事实缺席、不计数。放行的最坏后果 = **这一行**的 park 行事实(`ParkRowFacts` 各位)缺席(展示/分诊材料;core 定性 display metadata,resume 的带子读 gate 自己的位),队列其余部分照常 —— 而这一行本来就 resume 不了(`get()` 对同一格拒)。不放行的代价是整只 `rows.map` 抛出 ⇒ 一行坏 blob 打掉整个租户的队列(列表 500、SSE 心跳照常而队列永远空),与同一 `rows.map` 里 `rule_suggestions` / `risk_descriptor` 两格同族同判。故 F 类;tag 与那两格**分开计**(哪一格在坏要读得出来)。计数非零 = 这一行的 decide 同样读不动(`get()` 对同一格拒),去按 session 查那一行。",
118
118
  },
119
119
  "server.sql-json.checkpoint.pending-steer-queue-lossy": {
120
120
  cls: "F",
@@ -260,7 +260,7 @@ export const FAIL_OPEN_TAGS = {
260
260
  "server.redact.ppk-eat-to-end": { cls: "F", note: "S-107 第三十二轮重扫:PuTTY .ppk 容器扫描器(redact.ts redactPpkContainers)在「无合格终止 / 收口后同容器范围再见私钥字段 / 收口后到下一版本头之间夹非空白」三形下从头**吃到输入末尾**(fail-closed 取证丢弃)——终态 errorMessage 座上=整条错误文案变成 «redacted:private-key»,与 redactions_applied_total{private-key} 的诚实替换同形不可分,故要有自己的计数/探针;影子扫不计;计数=多少次整段吃尾。" },
261
261
  "server.approval-card.closed-word-out-of-set": {
262
262
  cls: "F",
263
- note: "S-249(`approval-card.ts` 的边界窄读族;今天的唯一成员是 `readRuleOffersAbsence`):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。",
263
+ note: "S-249(`approval-card.ts` 的边界窄读族;今天两个成员:`readRuleOffersAbsence`,与 S-721 起的 `readClosedWordSeat` —— 一座一只 core 成员筛,座是参数:`mandate`(core 7.32.0 #1092,活卡帧 / 活挂起行 / 运维队列行同一只,闭集筛 = core `isPersistedRuleMandate`)与 S-755 起的 `origin`(运维队列行读 park 行孪生座,闭集筛 = core `isAskOrigin`,与 inbox 行 core 投影处同一只筛);detail 前缀 `ruleOffersAbsence=` / `mandate=` / `origin=` 判别是哪一座):活卡帧 / `card_json` / (S-700 起)运维队列行(`parkRowFacts` 读寄存行上的 park 孪生座;队列是热读面 ⇒ 同一行每读一次计一次)的一个**闭集词**座上,键在场而词不在本仓认得的那张表里(世代差,或第三方 producer)⇒ 该座**按缺席**处置。放行的最坏后果=卡上少一行「为什么这次没有规则候选」的解释(壳退回不显示该行),它**不改变任何裁决**——门在引擎里早已判完,本座是展示/观测面;反方向(把一个消费端词表外的值原样上卡)更坏:下游按闭集写的分支会落进 default 臂或渲染出生词。本臂存在的全部理由是**判别**:修前「上游发了个生词」与「上游根本没发这个键」在遥测上同形,而前者意味着本仓的词表该跟着 core 加词了(型面另有两向编译围栏,但那只在**重新编译**时说话,跑在线上的那一版不会自己红)。detail 带座名与那个词(经 redactSecrets 截 40);数 / 布尔等原始值原样,对象 / 数组 / 函数 / null 只报形名(`<object>` / `<array>` / `<function>` / `<null>`,S-700 codex r1:格式化从不调用值自己的转换方法 —— 持久层里一格 `{\"toString\":null}` 曾让诊断本身抛出、打掉整只队列)。",
264
264
  },
265
265
  "server.pg-pool.idle-client-error": {
266
266
  cls: "F",
@@ -9,7 +9,9 @@
9
9
  * 行永 parked、生命周期分叉)。
10
10
  */
11
11
  import type { HostDecision } from "./host-decision.js";
12
- import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, QuestionAnswer, ResumeOutcome, TaskRegistry, ToolSpec, WorkflowRun, WorkflowRunStore } from "@sema-agent/core";
12
+ import type { BackgroundAgentRecord, BackgroundAgentStore, CheckpointStore, QuestionAnswer, ResumeOutcome, TaskRegistry, WorkflowRun, WorkflowRunStore } from "@sema-agent/core";
13
+ import type { ParkedReviveSurfaceFactory } from "./parked-revive-surface.js";
14
+ import type { RebuiltInheritedGate } from "./boot/parked-revive-gate.js";
13
15
  import { type WorkflowRunParkKey, type WorkflowAgentSessionIndex } from "./orchestration/workflow-agent-session-index.js";
14
16
  export interface ParkedAgentMatch {
15
17
  handle: string;
@@ -112,9 +114,13 @@ export interface ParkedDecideDeps {
112
114
  checkpointStore: Pick<CheckpointStore, "get">;
113
115
  /** 与 reaper/scenarios 同一个 registry(claim/rollback 的 CAS 与 in-memory handle 收敛在同处)。 */
114
116
  registry: Pick<TaskRegistry, "claimParkedAgent" | "rollbackParkedClaim">;
115
- /** boot 期从生产装配(buildScenarios)取的裸 Agent ToolSpec——与 spawn 用同一装配路径,
116
- * subRunner/bg opts/checkpointStore 同实例天然成立([ref] 设计稿 §4)。 */
117
- reviveTool: ToolSpec;
117
+ /** #1110(core 7.32.0):按**行**解析赎回面 —— 驱动哪一只 Agent 工具(生产装配,与 spawn 同源:subRunner / bg opts /
118
+ * checkpointStore 同实例,[ref] 设计稿 §4),以及与它同出一张列表的继承座 `mountsCallerTool`(一层行才有;属主与全部判据
119
+ * 在 `parked-revive-surface.ts`)。修前是 boot 期的一只裸 Agent 工具、从不交座 ⇒ 7.32.0 起停在继承工具上的 allow 恒 422。 */
120
+ reviveSurface: ParkedReviveSurfaceFactory;
121
+ /** #1110 codex r1 [high]:会话属主面(`sessionStorage.ownerOf`)—— 赎回腿在 claim 前读**根会话现在**的属主交给赎回面(属主换人 / 会话
122
+ * 已不在 ⇒ 不交座)。缺席 = 本部署无属主面 ⇒ 读数缺席 ⇒ 不交座(fail-closed)。读抛 ⇒ 原样上抛(claim 之前,没有东西要回滚)。 */
123
+ sessionOwnerOf?: (sessionId: string) => Promise<string | null | undefined>;
118
124
  /** revive args 的 `subagent_type` 成员性判定集([ref] Q1 修正:非 roster 成员必须省略,否则
119
125
  * unknown subagent_type 拒;字面比对 general-purpose 在 roster 撞名时会错省)。缺省空集=恒省略。 */
120
126
  knownAgentTypes?: ReadonlySet<string>;
@@ -130,7 +136,7 @@ export interface ParkedDecideDeps {
130
136
  * 「席位必须同步」是那个文件此前自设的约束、不是结构事实。 */
131
137
  rebuildInheritedGate?: (row: BackgroundAgentRecord, identity: {
132
138
  principal: string | undefined;
133
- }) => Promise<unknown>;
139
+ }) => Promise<RebuiltInheritedGate | undefined>;
134
140
  warn?: (event: string, fields: Record<string, unknown>) => void;
135
141
  }
136
142
  export interface ParkedDecideRequest {
@@ -149,6 +149,8 @@ export async function decideParkedAgent(deps, req, match) {
149
149
  return { status: 409, body: { error: "parked row carries no agent name — the revive path requires a named agent (row damaged?)", errorCode: "decide.row_unrevivable", taskId: match.handle } };
150
150
  }
151
151
  const inheritedGate = deps.rebuildInheritedGate ? await deps.rebuildInheritedGate(row, { principal: decodeCheckpointScope(req.scope) }) : undefined;
152
+ const rootOwner = row.rootSessionId !== undefined && deps.sessionOwnerOf ? await deps.sessionOwnerOf(row.rootSessionId) : undefined;
153
+ const surface = deps.reviveSurface(row, { principal: decodeCheckpointScope(req.scope), ...(rootOwner !== undefined ? { rootOwner } : {}) });
152
154
  const stores = { agentStore: deps.agentStore, checkpointStore: deps.checkpointStore };
153
155
  const claim = await deps.registry.claimParkedAgent(stores, match.handle, { scope: row.scope, owner: row.owner });
154
156
  if (!claim.ok) {
@@ -180,6 +182,7 @@ export async function decideParkedAgent(deps, req, match) {
180
182
  const outcome = buildParkedResumeOutcome(req);
181
183
  const ctx = {
182
184
  toolCallId: `drv-${ticket.claimId}`,
185
+ ...(surface.mountsCallerTool !== undefined ? { mountsCallerTool: surface.mountsCallerTool } : {}),
183
186
  reviveClaim: {
184
187
  row: { ...ticket.row, parkClaimId: ticket.claimId },
185
188
  rev: ticket.reservedRev,
@@ -211,7 +214,7 @@ export async function decideParkedAgent(deps, req, match) {
211
214
  defaultSubagentTailBus.forgetHandle(match.handle);
212
215
  let receipt;
213
216
  try {
214
- receipt = await deps.reviveTool.execute(mintReviveArgs(row, deps.knownAgentTypes ?? new Set()), ctx);
217
+ receipt = await surface.reviveTool.execute(mintReviveArgs(row, deps.knownAgentTypes ?? new Set()), ctx);
215
218
  }
216
219
  catch (e) {
217
220
  const disp = await rollback("execute_threw");
@@ -0,0 +1,47 @@
1
+ /**
2
+ * #1110(core 7.32.0)—— `/decide` parked 赎回腿的**赎回面**:驱动哪一只 Agent 工具,以及交不交继承座
3
+ * `ToolExecuteContext.mountsCallerTool`。
4
+ *
5
+ * core 7.32.0 起 Agent 子代按**身份**继承场景层工具(bundle 交给 Agent 的 `inheritedTools` ∩ 父 run 的 `spec.tools`),由宿主
6
+ * **自己**驱动 `execute` 的一方(本腿)必须交座,否则 fail-closed:复活的子代什么都不继承,而停在一只**继承工具**上的 `allow` 在消费
7
+ * 审批之前被拒(`parked_resume.startup_failed` ⇒ `parked-decide.ts` 的 422 臂,审批留 pending;`deny` 照常结)。
8
+ *
9
+ * 规则(core [ref] / unreleased-1110 原话,一条):**座建在那条行的根会话解析出来的同一张列表上,赎回工具取自同一张列表**。
10
+ * · 列表 = 内建 default 场景工厂按行的根会话重解一次(`defaultScenario({}, principal, { sessionId: root })`)—— 与生产 resolve-spec
11
+ * 给那个会话的 run 组 `spec.tools` 的同一只工厂、同一个会话分区(任务清单店 `taskListStoreFor(root)` 与领队那一张是同一张;
12
+ * 多租户的 WebFetch 过滤同样生效 ⇒ 子代同样拿不到)。座 = `(t) => tools.includes(t)`(身份判:同名副本不算)。
13
+ * · **禁**用 boot 期单例填座:那只 bundle 没有会话,它的任务清单是一张私有表 —— 填了座,每个会话 / 租户的复活队友都会共用它。
14
+ * · **根会话现在还属于这张卡的租户**才填座(codex r1 [high]):任务清单店只按会话 id 分区,而会话 id 由调用方自选、删后可被别人重领 ——
15
+ * 根会话删了(或换了主人)之后赎回一张旧卡,按 id 重解出来的是**新主人**的清单。属主读数由赎回腿在 claim 前读(`identity.rootOwner`,
16
+ * 读自会话属主面;缺席 = 会话已不在 / 读不出),与卡的权威 principal 逐字相等才填;否则不交座(fail-closed:子代不继承,继承工具上的
17
+ * allow 回 422、审批待决,deny 照结)。根删除时把子孙的 parked 卡一并围栏 / 过期 = 另车(会话化身轴,[ref] 同族)。
18
+ * · 只对**一层**行填座(`rootSessionId` 在场且 = `parentSessionId`:派它的 run 就是根会话的 run,列表正是那个 run 的挂载)。更深的行
19
+ * (孙代:派它的是一个子代,它的挂载是继承裁剪后的子集,行上读不出来)不填 —— 用 boot 期那只裸 Agent、无座 = core 的 fail-closed 臂;
20
+ * 行上带继承组成是 core #1112(7.32.x)的半场,到货再改读行。
21
+ * 近似如实记:重解的列表是**部署缺省**那一份,读不回那一 run 的逐请求差异(`settings.webSearch` 后端、center overlay 场景)——
22
+ * 座只判 bundle 实例的成员性,继承面是 `[...web, ...scaffolds]`,那两项差异不改变谁在面内。
23
+ */
24
+ import { type BackgroundAgentRecord, type ToolSpec } from "@sema-agent/core";
25
+ import type { Scenario } from "./capabilities/scenarios.js";
26
+ /** 一次赎回用的面:驱动的 Agent 工具 + (一层行时)与它同出一张列表的继承座。 */
27
+ export interface ParkedReviveSurface {
28
+ readonly reviveTool: ToolSpec;
29
+ /** 缺席 = 不交座(core fail-closed:复活的子代不继承任何场景层工具)。 */
30
+ readonly mountsCallerTool?: (tool: ToolSpec) => boolean;
31
+ }
32
+ /**
33
+ * 按行解析赎回面的入参身份:`principal` = 权威 principal(取 checkpoint 的 scope 列,不取行上有损的 `row.scope`);`rootOwner` = 行的根会话
34
+ * **现在**的属主(赎回腿在 claim 前读会话属主面;`null` = 匿名会话,缺席 = 会话已不在 / 读不出 / 本部署无属主面)。
35
+ */
36
+ export interface ParkedReviveSurfaceIdentity {
37
+ readonly principal: string | undefined;
38
+ readonly rootOwner?: string | null;
39
+ }
40
+ /** 按行解析赎回面。 */
41
+ export type ParkedReviveSurfaceFactory = (row: BackgroundAgentRecord, identity: ParkedReviveSurfaceIdentity) => ParkedReviveSurface;
42
+ /**
43
+ * 赎回面工厂。入参 = **内建** default 场景工厂(stage-07 在 center overlay 盖名之前取的那一只;赎回要的是与 spawn 同源的生产装配)。
44
+ * 内建 bundle 里没有 Agent 工具 ⇒ `undefined`(park 本就不可达,赎回腿不接线)。
45
+ */
46
+ export declare function createParkedReviveSurface(defaultScenario: Scenario): ParkedReviveSurfaceFactory | undefined;
47
+ //# sourceMappingURL=parked-revive-surface.d.ts.map
@@ -0,0 +1,20 @@
1
+ import { DEFAULT_SUBAGENT_TOOL_NAME } from "@sema-agent/core";
2
+ const agentOf = (tools) => tools.find((t) => t.name === DEFAULT_SUBAGENT_TOOL_NAME);
3
+ export function createParkedReviveSurface(defaultScenario) {
4
+ const bootTool = agentOf(defaultScenario({}).tools);
5
+ if (bootTool === undefined)
6
+ return undefined;
7
+ return (row, identity) => {
8
+ const root = row.rootSessionId;
9
+ if (root === undefined || row.parentSessionId !== root)
10
+ return { reviveTool: bootTool };
11
+ if (identity.rootOwner === undefined || (identity.rootOwner ?? undefined) !== identity.principal)
12
+ return { reviveTool: bootTool };
13
+ const tools = defaultScenario({}, identity.principal, { sessionId: root }).tools;
14
+ const reviveTool = agentOf(tools);
15
+ if (reviveTool === undefined)
16
+ return { reviveTool: bootTool };
17
+ return { reviveTool, mountsCallerTool: (t) => tools.includes(t) };
18
+ };
19
+ }
20
+ //# sourceMappingURL=parked-revive-surface.js.map
@@ -21,7 +21,7 @@
21
21
  * re-read by byte cursor on each poll (path ①). Both are expressed through {@link BackgroundShellDriver}.
22
22
  */
23
23
  import { BackgroundShellError } from "@sema-agent/core";
24
- import type { BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions, Result } from "@sema-agent/core";
24
+ import type { BackgroundHardKillOutcome, BackgroundShellId, BackgroundPoll, BackgroundSpawnOptions, Result } from "@sema-agent/core";
25
25
  import { StringDecoder } from "node:string_decoder";
26
26
  /** Per-端 background capability bounds, surfaced as `backgroundCapabilities` on the env ([ref] §3.1/§3.6).
27
27
  *
@@ -105,6 +105,13 @@ export interface BackgroundShellDriver<S> {
105
105
  kill(state: S): Promise<void>;
106
106
  /** Best-effort cleanup of ONE shell's transport resources (kill + remove temp dirs/handles). Should not throw. */
107
107
  disposeOne(state: S): Promise<void>;
108
+ /**
109
+ * S-745(core 7.32.0 #1107)—— **可选**的同步硬收一枪(`BackgroundShellCapability.killBackgroundNow` 的 driver 半场)。
110
+ * 只有**本机有进程组可发**的 driver 实现它(今天只有 host 车道);远端 / 沙箱车道留空 ⇒ 座位根本不挂出来,core 对那些行
111
+ * 答 `no_pgid` 并派普通 kill。契约(core `docs/background-process-lifetime.md`):同步(无 TERM、无定时器、无 await)、
112
+ * 幂等、永不抛、只对**证明得了还是自己的** pid 发信号(观察到退出 ⇒ `already_gone`,一枪都不发)。
113
+ */
114
+ killNow?(state: S): BackgroundHardKillOutcome;
108
115
  }
109
116
  export declare class BackgroundShellManager<S> {
110
117
  private readonly driver;
@@ -154,6 +161,18 @@ export declare class BackgroundShellManager<S> {
154
161
  private register;
155
162
  poll(shellId: BackgroundShellId): Promise<Result<BackgroundPoll, BackgroundShellError>>;
156
163
  kill(shellId: BackgroundShellId): Promise<Result<void, BackgroundShellError>>;
164
+ /**
165
+ * S-745 —— 座位 `killBackgroundNow(shellId)` 的 manager 半场:**同步**、永不抛。
166
+ * · 不是本 env 的 shell(未知 / 伪造 / 已被驱逐的终态行)⇒ `already_gone`(core 契约「or not its shell」);
167
+ * · driver 不实现 {@link BackgroundShellDriver.killNow} ⇒ `no_pgid`(没有同步硬收可落;实现座位的 env 才会走到这里);
168
+ * · driver 答出三词之外 ⇒ `no_pgid`(没有落地,可重试)。driver 的座位按契约**永不抛**(host 的那只由构造保证:只读子进程句柄
169
+ * + `hardKillGroupNow` 自己吞 `kill` 的拒因);万一抛了,本函数**不吞** —— 异常原样上抛给 core 的注册表,由它读成 `no_pgid` 并记
170
+ * `SEMA_DEBUG_SHUTDOWN` 取证行(core 契约:「A seat that throws … reads no_pgid」),不在这里再写一只静默兜底;
171
+ * · 落地 `killed` 且条目还在 `running` ⇒ 条目结算 `killed` + 清 BG 超时墙(与 {@link kill} 的记账同一形:这一枪是**我们**
172
+ * 打的,之后迟到的 `close` 不再把它改判成 `exited`)。条目状态**不参与**判「该不该发」:软梯子会先把条目翻成 `killed`
173
+ * 再等宽限(屏蔽 TERM 的组此刻还活着),所以「该不该发」只由 driver 读子进程句柄判。
174
+ */
175
+ killNow(shellId: BackgroundShellId): BackgroundHardKillOutcome;
157
176
  /** Kill + clean up EVERY shell. Best-effort, MUST NOT throw, idempotent ([ref] §3.7).
158
177
  *
159
178
  * core `except` 契约(background-shell.d.ts:core runtask 尾调 `{except: keepAlive}`):
@@ -216,6 +216,32 @@ export class BackgroundShellManager {
216
216
  }
217
217
  return ok(undefined);
218
218
  }
219
+ killNow(shellId) {
220
+ const entry = this.shells.get(shellId);
221
+ if (!entry)
222
+ return "already_gone";
223
+ const seat = this.driver.killNow;
224
+ if (seat === undefined)
225
+ return "no_pgid";
226
+ const outcome = seat.call(this.driver, entry.state);
227
+ switch (outcome) {
228
+ case "killed":
229
+ if (entry.status === "running") {
230
+ entry.status = "killed";
231
+ if (entry.timer)
232
+ clearTimeout(entry.timer);
233
+ }
234
+ return "killed";
235
+ case "already_gone":
236
+ case "no_pgid":
237
+ return outcome;
238
+ default: {
239
+ const unreachable = outcome;
240
+ void unreachable;
241
+ return "no_pgid";
242
+ }
243
+ }
244
+ }
219
245
  async dispose(opts) {
220
246
  const keep = opts?.except?.length ? new Set(opts.except) : undefined;
221
247
  const doomed = [];
@@ -23,7 +23,7 @@
23
23
  * structural tests only on mac/Linux; behavior-level bite happens on the first Windows-runner green.
24
24
  */
25
25
  import type { ChildProcess } from "node:child_process";
26
- import { killProcessTree } from "@sema-agent/core";
26
+ import { killProcessTree, type BackgroundHardKillOutcome } from "@sema-agent/core";
27
27
  export declare const IS_WIN32: boolean;
28
28
  export interface HostShell {
29
29
  shell: string;
@@ -61,6 +61,16 @@ export declare function spawnGroupOptions(): {
61
61
  * win32 = core `signalProcessTree(pid,"SIGKILL")` → `taskkill /F /T` (never throws — fire-and-forget spawn).
62
62
  */
63
63
  export declare function killTreeHard(pid: number): void;
64
+ /**
65
+ * S-745(core 7.32.0 #1107)—— **同步组硬收的一枪**:后台 driver 的 `killBackgroundNow` 座位用它(梯子的「组 SIGKILL」那一级,
66
+ * 不经 TERM、不挂定时器)。读数是 core 座位闭集里的三词,判据只看 `kill` 系统调用自己的结局:
67
+ * · POSIX:`kill(-pid, SIGKILL)` 落地 ⇒ `killed`;`ESRCH`(这个组已经没有进程)⇒ `already_gone`;其余拒因(`EPERM` 一族)⇒
68
+ * `no_pgid`(没有落地,调用方可重试;core 随即派普通 kill);
69
+ * · win32 ⇒ 恒 `no_pgid`(core 契约:win32 无同步硬收 —— 没有进程组,树杀要一个延后的发现步,同步 `taskkill` 会杀掉启动器而
70
+ * 留下 Git Bash 的作业组、却自称 `killed`)。
71
+ * 「这个 pid 还是不是我的」由**调用方**先判(观察到退出 ⇒ 不许再发,pid 可能已经是别人的):本函数只管打出去的那一枪。永不抛。
72
+ */
73
+ export declare function hardKillGroupNow(pid: number): BackgroundHardKillOutcome;
64
74
  /**
65
75
  * SOFT kill (design D2 — the bg KillShell/timeout-wall first rung; the caller owns the grace→hard escalation
66
76
  * timer, unchanged). POSIX = `process.kill(-pid, "SIGTERM")` byte-identical (throws on dead group — the caller
@@ -46,6 +46,17 @@ export function killTreeHard(pid) {
46
46
  }
47
47
  process.kill(-pid, "SIGKILL");
48
48
  }
49
+ export function hardKillGroupNow(pid) {
50
+ if (IS_WIN32)
51
+ return "no_pgid";
52
+ try {
53
+ process.kill(-pid, "SIGKILL");
54
+ return "killed";
55
+ }
56
+ catch (err) {
57
+ return err.code === "ESRCH" ? "already_gone" : "no_pgid";
58
+ }
59
+ }
49
60
  export function killTreeSoft(pid) {
50
61
  if (IS_WIN32) {
51
62
  signalProcessTree(pid, "SIGTERM");
@@ -1,6 +1,10 @@
1
- import { FileError, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
1
+ import { FileError, ExecutionError, RemoteExecutionError, SchedulerError, type SchedulerCapability, type ScheduledIntent, type SchedulerContext, type ScheduledTaskId, type ScheduledTaskSummary, type ExecutionEnv, type RemoteExecutionEnv, type WorkspaceHandle, type FileInfo, type Result, type SymlinkChain, type OutputChunk, type ExecStreamOptions, type RemoteConnectConfig, type SnapshotId, type SessionToken, type VmLifecycleOptions, type ExecutionEnvFactory, BackgroundShellError, type BackgroundShellCapability, type BackgroundShellId, type BackgroundHardKillOutcome, type BackgroundPoll, type BackgroundSpawnOptions } from "@sema-agent/core";
2
2
  /** Options accepted by `ExecutionEnv.exec` (core does not re-export the type; derive it from the seam). */
3
3
  type ExecOpts = Parameters<ExecutionEnv["exec"]>[1];
4
+ /** SIGTERM → grace → SIGKILL (no host isolation). 🔴 宽限的**唯一属主**:本 driver 的 `kill` 梯子用它排升级定时器;会话级收敛
5
+ * (`background-session-reap.ts#reapSessionBackgroundHard`,DELETE 与停止口)在软面之后、硬面之前**等同一只**(车OL O1 —— 硬面不许把
6
+ * 这把梯子的用户面宽限取消),不另抄数。启动期读一次;`0` 被 `numEnvOr` 读作缺席(回落 1000),要几乎不等写 `1`。 */
7
+ export declare const HOST_BG_KILL_GRACE_MS: number;
4
8
  /** Construction config for {@link RemoteHostExecutionEnv}. */
5
9
  export interface HostEnvConfig {
6
10
  /**
@@ -242,6 +246,15 @@ export declare class RemoteHostExecutionEnv implements RemoteExecutionEnv, Sched
242
246
  private adoptRunningExec;
243
247
  pollBackground(shellId: BackgroundShellId): Promise<Result<BackgroundPoll, BackgroundShellError>>;
244
248
  killBackground(shellId: BackgroundShellId): Promise<Result<void, BackgroundShellError>>;
249
+ /**
250
+ * S-745(core 7.32.0 #1107)—— `BackgroundShellCapability.killBackgroundNow` 座位:退出边界 / 会话收敛的**同步硬收**面经它对
251
+ * 本 env 的一行发**一枪组 SIGKILL**(pid 不出 env,调用方只握不透明的 shellId)。同步、永不抛、幂等;答 `killed` /
252
+ * `already_gone` / `no_pgid`(win32 恒 `no_pgid`)。从没起过 shell 的 env(`_bgManager` 未建)⇒ 这个 id 不是本 env 的 ⇒
253
+ * `already_gone`;**不**顺手新建 manager(拆除路径上不造东西,与 `disposeBackgroundShells` 同一条)。
254
+ * 本仓只有 host 车道实现它:e2b / k8s·kata / local-docker / ssh / device / adb 没有本机进程组可发,留空 ⇒ core 每行 `no_pgid`
255
+ * 并派普通 kill(车道矩阵钉:`test/background-hard-kill-s745.test.ts`)。
256
+ */
257
+ killBackgroundNow(shellId: BackgroundShellId): BackgroundHardKillOutcome;
245
258
  disposeBackgroundShells(opts?: {
246
259
  except?: readonly BackgroundShellId[];
247
260
  }): Promise<void>;