@sema-agent/server 7.119.0 → 7.121.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 (127) hide show
  1. package/MIGRATION.md +129 -0
  2. package/USAGE.md +72 -23
  3. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -2
  4. package/deploy/sema-up/chart/templates/server.yaml +2 -2
  5. package/deploy/sema-up/stack/sema-stack-cluster.yaml +1 -1
  6. package/dist/boot/config-center.d.ts +5 -7
  7. package/dist/boot/config-center.js +37 -13
  8. package/dist/boot/engine-lease.d.ts +1 -1
  9. package/dist/boot/execution-env.js +3 -2
  10. package/dist/boot/fatal-exit.d.ts +7 -1
  11. package/dist/boot/fatal-exit.js +2 -1
  12. package/dist/boot/resolve-spec.js +1 -0
  13. package/dist/boot/runner-deps.d.ts +4 -3
  14. package/dist/boot/runner-deps.js +7 -3
  15. package/dist/boot/shutdown.d.ts +9 -10
  16. package/dist/boot/shutdown.js +54 -16
  17. package/dist/boot/stage-01-config.js +2 -0
  18. package/dist/boot/stage-09-leader.d.ts +2 -9
  19. package/dist/boot/stage-10-http-server.d.ts +1 -9
  20. package/dist/boot/stage-10-http-server.js +31 -8
  21. package/dist/capabilities/builtin-tools.d.ts +31 -8
  22. package/dist/capabilities/builtin-tools.js +22 -1
  23. package/dist/capabilities/composite-members.d.ts +1 -1
  24. package/dist/capabilities/composite-members.js +1 -1
  25. package/dist/capabilities/scenarios.d.ts +8 -6
  26. package/dist/capabilities/scenarios.js +3 -3
  27. package/dist/capabilities/send-user-file-tool.d.ts +6 -3
  28. package/dist/capabilities/send-user-file-tool.js +1 -1
  29. package/dist/config-catalog.js +13 -7
  30. package/dist/config-center/center-request.d.ts +16 -1
  31. package/dist/config-center/center-request.js +36 -5
  32. package/dist/config-lkg.d.ts +33 -5
  33. package/dist/config-lkg.js +55 -11
  34. package/dist/config-types.d.ts +46 -3
  35. package/dist/config-types.js +6 -0
  36. package/dist/config.d.ts +33 -4
  37. package/dist/config.js +107 -15
  38. package/dist/deployment-governance.d.ts +3 -1
  39. package/dist/deployment-governance.js +3 -1
  40. package/dist/device-store.d.ts +7 -0
  41. package/dist/device-store.js +4 -0
  42. package/dist/device-ws-hub.js +17 -9
  43. package/dist/device-ws-protocol.d.ts +15 -0
  44. package/dist/fleet/drain-entry.d.ts +87 -0
  45. package/dist/fleet/drain-entry.js +35 -0
  46. package/dist/fleet/engine-signal.d.ts +144 -0
  47. package/dist/fleet/engine-signal.js +60 -0
  48. package/dist/fleet/serving-lease.d.ts +47 -0
  49. package/dist/fleet/serving-lease.js +235 -0
  50. package/dist/fleet-client.d.ts +35 -19
  51. package/dist/fleet-client.js +49 -25
  52. package/dist/http/approval-sweeps.js +3 -5
  53. package/dist/http/dispatch.js +18 -13
  54. package/dist/http/health-self-report.d.ts +60 -0
  55. package/dist/http/health-self-report.js +53 -0
  56. package/dist/http/principal-gate.d.ts +8 -1
  57. package/dist/http/principal-gate.js +1 -0
  58. package/dist/http/readiness.d.ts +25 -5
  59. package/dist/http/readiness.js +31 -6
  60. package/dist/http/resume-legs.js +5 -1
  61. package/dist/http/route-ctx.d.ts +6 -4
  62. package/dist/http/route-table.d.ts +3 -0
  63. package/dist/http/route-table.js +4 -1
  64. package/dist/http/routes/a2a-serve.d.ts +1 -1
  65. package/dist/http/routes/a2a-serve.js +5 -5
  66. package/dist/http/routes/admin-drain.d.ts +12 -31
  67. package/dist/http/routes/admin-drain.js +15 -9
  68. package/dist/http/routes/approvals-assistant.d.ts +3 -3
  69. package/dist/http/routes/approvals-assistant.js +3 -3
  70. package/dist/http/routes/capabilities.js +3 -1
  71. package/dist/http/routes/devices.js +6 -5
  72. package/dist/http/routes/leader.d.ts +1 -1
  73. package/dist/http/routes/leader.js +1 -1
  74. package/dist/http/routes/memory-consolidation.d.ts +1 -1
  75. package/dist/http/routes/memory-consolidation.js +1 -1
  76. package/dist/http/routes/notify-wake.d.ts +1 -1
  77. package/dist/http/routes/notify-wake.js +1 -1
  78. package/dist/http/routes/rules.js +14 -6
  79. package/dist/http/routes/runs.d.ts +2 -2
  80. package/dist/http/routes/runs.js +2 -2
  81. package/dist/http/routes/side-query.d.ts +1 -1
  82. package/dist/http/routes/side-query.js +1 -1
  83. package/dist/http/routes/tasks.d.ts +2 -2
  84. package/dist/http/routes/tasks.js +3 -3
  85. package/dist/http/server.d.ts +7 -21
  86. package/dist/http/server.js +20 -1
  87. package/dist/http/wire-types.d.ts +7 -1
  88. package/dist/list-entry-display.d.ts +14 -0
  89. package/dist/list-entry-display.js +14 -0
  90. package/dist/model-reach-scope.d.ts +15 -3
  91. package/dist/model-reach-scope.js +3 -2
  92. package/dist/model-unreachable-notice.d.ts +2 -1
  93. package/dist/model-unreachable-notice.js +1 -0
  94. package/dist/namespaced-rule-name.js +2 -6
  95. package/dist/observability/fail-open.d.ts +20 -0
  96. package/dist/observability/fail-open.js +20 -0
  97. package/dist/operator-ask.d.ts +1 -1
  98. package/dist/plugins/one-time-migrations.d.ts +8 -0
  99. package/dist/plugins/one-time-migrations.js +72 -3
  100. package/dist/plugins/permission-rule-store-file.js +2 -6
  101. package/dist/plugins/permission-rule-store-sql.d.ts +65 -6
  102. package/dist/plugins/permission-rule-store-sql.js +166 -59
  103. package/dist/plugins/remote-env-device.d.ts +12 -1
  104. package/dist/plugins/remote-env-device.js +13 -6
  105. package/dist/plugins/scheduler-support.d.ts +8 -4
  106. package/dist/plugins/scheduler-support.js +5 -5
  107. package/dist/request-key-closure.js +1 -1
  108. package/dist/rule-entry-list.d.ts +0 -1
  109. package/dist/rule-entry-list.js +4 -9
  110. package/dist/rules-consent.d.ts +16 -3
  111. package/dist/rules-consent.js +8 -4
  112. package/dist/run-local.js +2 -0
  113. package/dist/runs.js +1 -1
  114. package/dist/sema-config-dir.d.ts +19 -0
  115. package/dist/sema-config-dir.js +33 -0
  116. package/dist/session-policy-wire.js +5 -4
  117. package/dist/spec-fields.d.ts +5 -4
  118. package/dist/task-settings.d.ts +3 -3
  119. package/dist/tool-approval.d.ts +7 -0
  120. package/dist/tool-approval.js +4 -1
  121. package/dist/trace/core-keyset-guard.d.ts +2 -2
  122. package/dist/trace/engine-notice-wire.d.ts +6 -4
  123. package/dist/trace/engine-notice-wire.js +16 -9
  124. package/dist/trace/self-minted-wire.d.ts +78 -0
  125. package/dist/trace/self-minted-wire.js +32 -0
  126. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
  127. package/package.json +3 -3
@@ -1,4 +1,10 @@
1
- /** `err` 的 fatal 日志文本:`String(err)` 过形状律一次。纯函数,零副作用。 */
1
+ /**
2
+ * `err` 的 fatal 日志文本:标题过形状律一次。纯函数,零副作用。
3
+ *
4
+ * 标题 = `String(err)`;异常**自带机器码**(`err.code` 是串 —— 引擎的构造期拒因都带,如 `config.agent_config_dir_invalid`)⇒ 按 Node 给自家
5
+ * 带码异常的写法 `<name> [<code>]: <message>` 把码写进标题(`String(err)` 不带 `code`,修前拒启行里看不到引擎的码)。一条规则,不分谁铸的:
6
+ * 多数 Node 自己的 `ERR_*` 异常本来就印成这一形(逐字不变),系统调用异常(`EEXIST` 等)标题里多一次码。
7
+ */
2
8
  export declare function buildFatalErrorText(err: unknown): string;
3
9
  /**
4
10
  * 进程级 fatal 出口的**唯一一只腿**:同一只 {@link buildFatalErrorText} 产物写两个面,然后按崩溃码退。
@@ -1,7 +1,8 @@
1
1
  import { redactSecrets } from "../trace/redact.js";
2
2
  import { createLogger } from "../observability/logger.js";
3
3
  export function buildFatalErrorText(err) {
4
- return redactSecrets(String(err));
4
+ const code = err instanceof Error ? err.code : undefined;
5
+ return redactSecrets(typeof code === "string" && err instanceof Error ? `${err.name} [${code}]: ${err.message}` : String(err));
5
6
  }
6
7
  export function fatalExit(err) {
7
8
  const text = buildFatalErrorText(err);
@@ -331,6 +331,7 @@ export function createResolveSpec(ctx) {
331
331
  objective: attachmentNotice ? `${picked.cleanedObjective}\n\n${attachmentNotice}` : picked.cleanedObjective,
332
332
  systemPrompt: typeof body.systemPrompt === "string" ? body.systemPrompt : undefined,
333
333
  appendSystemPrompt: acceptedAppend,
334
+ ...(body.outputStyle !== undefined ? { outputStyle: body.outputStyle } : {}),
334
335
  sessionId: auth?.sessionId,
335
336
  principal: auth?.principal,
336
337
  images: body.images,
@@ -311,9 +311,10 @@ export declare function createEngineNoticeSeat(logger: {
311
311
  * 「键为 undefined」与「键缺席」对引擎逐字等价)。**不复制引擎缺省**(`roots`/`static-face`/`carry`/`open`/
312
312
  * CC parity 20/200 一个都不抄):上游改缺省之日起本仓不会成为第二份真源。
313
313
  */
314
- export type DeploymentPostureSeats = Pick<RunnerDeps, "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "delegationEntryCaps" | "crossSessionInbound" | "crossSessionDialogExpiry" | "writeProtectedPaths" | "allowedTools">;
315
- /** 本族读的配置切面:席名与 config 键同名的那几席 + 运维 allow 表(席 `allowedTools` ← config `approvalGrant`,env 名 `APPROVAL_GRANT` 不改)。 */
316
- export type DeploymentPostureConfig = Pick<ServiceConfig, Exclude<keyof DeploymentPostureSeats, "allowedTools"> | "approvalGrant">;
314
+ export type DeploymentPostureSeats = Pick<RunnerDeps, "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude" | "memoryDelegationEvidence" | "memoryProvenance" | "memoryCapturePolicy" | "delegationEntryCaps" | "crossSessionInbound" | "crossSessionDialogExpiry" | "writeProtectedPaths" | "allowedTools" | "agentConfigDir">;
315
+ /** 本族读的配置切面:席名与 config 键同名的那几席 + 运维 allow 表(席 `allowedTools` ← config `approvalGrant`,env 名 `APPROVAL_GRANT` 不改)
316
+ * + LKG 覆写路径(`readDenyPatterns` 席并入它派生的读拒行,`lkgReadDenyRows`)。 */
317
+ export type DeploymentPostureConfig = Pick<ServiceConfig, Exclude<keyof DeploymentPostureSeats, "allowedTools"> | "approvalGrant" | "configLkgPath">;
317
318
  export declare function buildDeploymentPostureSeats(config: DeploymentPostureConfig): DeploymentPostureSeats;
318
319
  export declare function createSharedRunnerDeps(ctx: SharedRunnerDepsCtx): SharedRunnerDeps;
319
320
  export declare function createRunnerDeps(ctx: RunnerDepsCtx): RunnerDeps;
@@ -1,6 +1,7 @@
1
1
  import { createFileWorkflowScriptStore } from "@sema-agent/core";
2
2
  import { join } from "node:path";
3
3
  import { listCollabWorkflows, resolveCollabWorkflow } from "../capabilities/collab-workflows.js";
4
+ import { lkgReadDenyRows } from "../config-lkg.js";
4
5
  import { fleetBackgroundChildPublisher, FleetEventBus } from "../fleet/fleet-bus.js";
5
6
  import { createMemoryEngineIncidentSink } from "../memory-incident-sink.js";
6
7
  import { createMemorySessionIncarnationSeat, sessionFaceReaderOf } from "./session-incarnation-seat.js";
@@ -9,7 +10,7 @@ import { createHardenedVmRunner } from "../orchestration/hardened-vm-runner.js";
9
10
  import { createWorkerHardenedVmRunner } from "../orchestration/hardened-vm-worker-runner.js";
10
11
  import { WorkflowAgentRegistry } from "../orchestration/workflow-agent-steer.js";
11
12
  import { resolveServedSession } from "../orchestration/workflow-completion-inbox.js";
12
- import { defaultEngineNoticeRouter, presentNoticeDetail } from "../trace/engine-notice-wire.js";
13
+ import { defaultEngineNoticeRouter, redactNoticeRuleText } from "../trace/engine-notice-wire.js";
13
14
  import { WorkflowNotifyGate } from "../orchestration/workflow-notify-journal.js";
14
15
  import { makeLoadProjectMemory, makeProbeInstructionSources } from "../project-memory.js";
15
16
  import { createInstructionSourceSeat } from "../instruction-source-notice.js";
@@ -41,7 +42,7 @@ const PROMPT_CACHE_BREAK_LEVELS = new Map(Object.entries(PROMPT_CACHE_BREAK_CAUS
41
42
  const CEILING_ORIGINS = new Set(["walltime", "turns"]);
42
43
  export function createEngineNoticeForwarder(logger, router = defaultEngineNoticeRouter, metrics) {
43
44
  return (notice) => {
44
- const detail = presentNoticeDetail(notice.code, notice.detail);
45
+ const detail = redactNoticeRuleText(notice.code, notice.detail);
45
46
  logger.warn("engine_notice", { code: notice.code, message: notice.message, ...(detail !== undefined ? { detail } : {}) });
46
47
  if (metrics !== undefined && notice.code === "task.interrupt_unconsumed") {
47
48
  const origin = notice.detail?.origin;
@@ -57,11 +58,14 @@ export function createEngineNoticeSeat(logger, router = defaultEngineNoticeRoute
57
58
  }
58
59
  export function buildDeploymentPostureSeats(config) {
59
60
  const crossSessionLayers = buildCrossSessionInboundLayers(config);
61
+ const lkgRows = lkgReadDenyRows(config.configLkgPath);
62
+ const readDenyAdditions = lkgRows.length === 0 ? config.readDenyPatterns : [...(config.readDenyPatterns ?? []), ...lkgRows];
60
63
  const seats = {
61
64
  readFace: config.readFace ? config.readFace : undefined,
62
- readDenyPatterns: config.readDenyPatterns ? config.readDenyPatterns : undefined,
65
+ readDenyPatterns: readDenyAdditions ? readDenyAdditions : undefined,
63
66
  readDenyBuiltinTiers: config.readDenyBuiltinTiers ?? undefined,
64
67
  readDenyBuiltinExclude: config.readDenyBuiltinExclude ?? undefined,
68
+ agentConfigDir: config.agentConfigDir ?? undefined,
65
69
  memoryDelegationEvidence: config.memoryDelegationEvidence ?? undefined,
66
70
  memoryProvenance: config.memoryProvenance ?? undefined,
67
71
  memoryCapturePolicy: config.memoryCapturePolicy ?? undefined,
@@ -26,11 +26,12 @@ import type { Logger } from "../observability/logger.js";
26
26
  import type { startOtlpExporter } from "../observability/otel-exporter.js";
27
27
  import type { CostQuota } from "../observability/cost-quota.js";
28
28
  import type { RateLimiter } from "../observability/rate-limit.js";
29
- import type { startFleetClientFromEnv } from "../fleet-client.js";
29
+ import type { startFleetClientFromConfig } from "../fleet-client.js";
30
30
  import type { BreakerStateStore, CostQuotaStore, RateLimiterStore, StoreBackend } from "../plugins/store-backend.js";
31
31
  import type { WorkflowNotifyJournalStore } from "../orchestration/workflow-notify-journal.js";
32
32
  import type { ToolApprovalCoordinator } from "../tool-approval.js";
33
33
  import type { ParkCommitWatch } from "../plugins/park-commit-watch.js";
34
+ import { type DrainState } from "../fleet/drain-entry.js";
34
35
  export interface ShutdownCtx {
35
36
  config: ServiceConfig;
36
37
  /** [ref]①:崩溃遗言的 version 键(与 /v1/capabilities.version 同源=main 的 serviceVersion())。 */
@@ -61,16 +62,10 @@ export interface ShutdownCtx {
61
62
  subRunner: Runner;
62
63
  lspManager: RunnerDeps["lspManager"];
63
64
  workflowNotifyJournal: WorkflowNotifyJournalStore | undefined;
64
- fleetClient: ReturnType<typeof startFleetClientFromEnv>;
65
+ fleetClient: ReturnType<typeof startFleetClientFromConfig>;
65
66
  backend: StoreBackend | undefined;
66
- drainState: {
67
- draining: boolean;
68
- since?: number;
69
- inflight?: () => number;
70
- lastActivityAt?: () => number;
71
- attachedStreams?: () => number;
72
- lastAttachEndedAt?: () => number;
73
- };
67
+ /** 编排器片 2 件 2:共享排空盒子(`fleet/drain-entry.ts`)—— 本函数往上装**唯一**入口 `enter`。 */
68
+ drainState: DrainState;
74
69
  /** [ref]-2:store 活体探针(main 建)——不停的话 hardShutdown 后仍对正在关闭的池发探针,刷假
75
70
  * store_probe_dead 告警(文件头契约 3 的同族漏网)。缺席 = 该部署形没建探针。 */
76
71
  storeLiveProbe: {
@@ -95,6 +90,10 @@ export interface ShutdownCtx {
95
90
  /** S-929 F1:checkpoint 店的 park 提交观察层(`plugins/park-commit-watch.ts`;在场 ⇔ 本部署有 checkpoint 店)。hardShutdown 在
96
91
  * `backend.close()` 之前等它 quiesce —— 在飞的 `put`(审批 park / 资源挂起…,与审批协调器在不在场无关)不许被抽掉连接池。 */
97
92
  parkCommits: ParkCommitWatch | undefined;
93
+ /** 编排器片 2 件 3:服务租约客户端(在场 ⇔ fleet 成员 ∧ 中心车道配齐)。hardShutdown 起点停它的续租定时器(文件头契约 3 同族)。 */
94
+ servingLease?: {
95
+ stop(): void;
96
+ } | undefined;
98
97
  }
99
98
  /**
100
99
  * 受控退的**后台宽限**还要等多久(ms)—— hardShutdown 软停后台 shell 之后、`process.exit` 之前,给软停过的行**一次**
@@ -8,14 +8,16 @@ import { installProcessExitBoundary } from "./exit-boundary.js";
8
8
  import { createEngineLeaseTracker } from "./engine-lease.js";
9
9
  import { writeExitLast, readExitLast, clearExitLast } from "./exit-last.js";
10
10
  import { isScriptRealmRejection, describeRejectionReason } from "../orchestration/hardened-vm-runner.js";
11
+ import { DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL, HARD_STOP_ACTION_ID } from "../fleet/drain-entry.js";
11
12
  const HARD_STOP_GRACE_MS = 10_000;
13
+ const FINAL_EXIT_MS = 15_000;
12
14
  export function backgroundGraceRemainingMs(a) {
13
15
  if (a.softStopped <= 0)
14
16
  return 0;
15
17
  return Math.max(0, Math.min(a.softStoppedAt + a.graceMs, a.deadlineMs) - a.now);
16
18
  }
17
19
  export function installShutdownHandlers(ctx) {
18
- const { config, logger, server, reaper, fleetReconcile, retentionLane, releaseRetentionLease, otelExporter, breakerState, costQuota, rateLimiter, runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState, storeLiveProbe, configCenter, version, deviceHub, toolApproval, parkCommits, } = ctx;
20
+ const { config, logger, server, reaper, fleetReconcile, retentionLane, releaseRetentionLease, otelExporter, breakerState, costQuota, rateLimiter, runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState, storeLiveProbe, configCenter, version, deviceHub, toolApproval, parkCommits, servingLease, } = ctx;
19
21
  const dataRoot = config.localDataRoot;
20
22
  if (dataRoot) {
21
23
  const prior = readCrashLast(dataRoot);
@@ -69,12 +71,14 @@ export function installShutdownHandlers(ctx) {
69
71
  };
70
72
  process.on("unhandledRejection", onUnhandledRejection);
71
73
  let closing = false;
74
+ let closingAt = 0;
72
75
  let parentWatch;
73
76
  let engineLease;
74
77
  const hardShutdown = () => {
75
78
  if (closing)
76
79
  return;
77
80
  closing = true;
81
+ closingAt = Date.now();
78
82
  if (dataRoot)
79
83
  clearCrashLast(dataRoot);
80
84
  const graceDeadlineMs = Date.now() + HARD_STOP_GRACE_MS;
@@ -101,6 +105,7 @@ export function installShutdownHandlers(ctx) {
101
105
  parentWatch?.stop();
102
106
  engineLease?.stop();
103
107
  storeLiveProbe?.stop();
108
+ servingLease?.stop();
104
109
  void deviceHub?.close().catch((err) => logger.warn("device_hub_close_failed", { err: String(err) }));
105
110
  configCenter?.stopRefreshLoop();
106
111
  otelExporter?.stop();
@@ -133,7 +138,7 @@ export function installShutdownHandlers(ctx) {
133
138
  .finally(() => backgroundGrace)
134
139
  .finally(() => process.exit(0));
135
140
  });
136
- const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: 15_000 }); process.exit(0); }, 15_000);
141
+ const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: FINAL_EXIT_MS }); process.exit(0); }, FINAL_EXIT_MS);
137
142
  finalExit.unref?.();
138
143
  server.closeIdleConnections?.();
139
144
  const forceClose = setTimeout(() => server.closeAllConnections?.(), HARD_STOP_GRACE_MS);
@@ -141,36 +146,67 @@ export function installShutdownHandlers(ctx) {
141
146
  };
142
147
  let draining = false;
143
148
  let drainStartedAt = 0;
149
+ let firstStopSignalAt;
144
150
  let drainTick;
145
- const drainThenShutdown = () => {
146
- if (closing || draining)
147
- return;
151
+ let current;
152
+ const enterDrain = (entry, bySignal = false, at = Date.now()) => {
153
+ if (current !== undefined)
154
+ return { ...current, state: "already_draining" };
155
+ if (closing)
156
+ return { actionId: HARD_STOP_ACTION_ID, since: closingAt, deadlineAt: closingAt + FINAL_EXIT_MS, state: "already_draining" };
148
157
  draining = true;
149
158
  drainStartedAt = performance.now();
159
+ firstStopSignalAt = bySignal ? drainStartedAt : undefined;
160
+ const since = at;
161
+ current = { actionId: entry.actionId, since, deadlineAt: entry.deadlineAt };
150
162
  drainState.draining = true;
151
- deviceHub?.beginDrain({ graceMs: () => config.drainGraceMs });
152
- drainState.since = Date.now();
163
+ drainState.since = since;
164
+ drainState.actionId = entry.actionId;
165
+ drainState.deadlineAt = entry.deadlineAt;
166
+ const windowMs = Math.max(0, entry.deadlineAt - since);
167
+ const deadlineMono = drainStartedAt + windowMs;
168
+ deviceHub?.beginDrain({ graceMs: () => windowMs });
169
+ drainState.yieldInflight?.();
170
+ toolApproval?.beginDrain();
153
171
  void fleetClient?.announceNow();
154
172
  const inflight = drainState.inflight?.() ?? 0;
155
- logger.info("draining_started", { inflight, graceMs: config.drainGraceMs });
173
+ logger.info("draining_started", { actionId: entry.actionId, inflight, deadlineAt: entry.deadlineAt, windowMs });
156
174
  if (inflight === 0) {
157
175
  hardShutdown();
158
- return;
176
+ return { ...current, state: "draining" };
159
177
  }
160
178
  const tick = setInterval(() => {
161
179
  const n = drainState.inflight?.() ?? 0;
162
- const elapsed = Math.round(performance.now() - drainStartedAt);
163
- if (n === 0 || elapsed >= config.drainGraceMs) {
180
+ const now = performance.now();
181
+ if (n === 0 || now >= deadlineMono) {
164
182
  clearInterval(tick);
165
183
  drainTick = undefined;
166
- logger.info("drain_complete", { inflight: n, elapsedMs: elapsed, timedOut: n > 0 });
184
+ logger.info("drain_complete", { actionId: entry.actionId, inflight: n, elapsedMs: Math.round(now - drainStartedAt), timedOut: n > 0 });
167
185
  hardShutdown();
168
186
  }
169
187
  }, 1_000);
170
188
  drainTick = tick;
189
+ return { ...current, state: "draining" };
190
+ };
191
+ const drainOnCause = (cause) => {
192
+ const at = Date.now();
193
+ enterDrain({ actionId: cause, deadlineAt: at + config.drainGraceMs }, DRAIN_CAUSE_COUNTS_AS_FIRST_STOP_SIGNAL[cause], at);
171
194
  };
195
+ const enterByRequest = (entry) => enterDrain(entry);
196
+ drainState.enter = enterByRequest;
172
197
  const onStopSignalWhileDraining = (signal) => {
173
- const sinceFirst = performance.now() - drainStartedAt;
198
+ const now = performance.now();
199
+ if (firstStopSignalAt === undefined) {
200
+ firstStopSignalAt = now;
201
+ logger.info("drain_signal_after_request_drain_ignored", {
202
+ signal,
203
+ actionId: current?.actionId,
204
+ deadlineAt: current?.deadlineAt,
205
+ note: "this drain was entered by a drain request / lease denial / self-exit and its deadline is fixed — the first stop signal does not shorten it; a second stop signal at least DRAIN_SECOND_SIGNAL_MIN_GAP_MS later hard-stops",
206
+ });
207
+ return;
208
+ }
209
+ const sinceFirst = now - firstStopSignalAt;
174
210
  const minGapMs = config.drainSecondSignalMinGapMs;
175
211
  if (sinceFirst < minGapMs) {
176
212
  logger.info("drain_duplicate_signal_ignored", { signal, sinceFirstMs: Math.floor(sinceFirst), minGapMs });
@@ -180,7 +216,7 @@ export function installShutdownHandlers(ctx) {
180
216
  hardShutdown();
181
217
  };
182
218
  const firstStopSignal = {
183
- SIGTERM: drainThenShutdown,
219
+ SIGTERM: () => drainOnCause("signal:SIGTERM"),
184
220
  SIGINT: hardShutdown,
185
221
  };
186
222
  const onStopSignal = (signal) => {
@@ -200,7 +236,7 @@ export function installShutdownHandlers(ctx) {
200
236
  inflight: () => drainState.inflight?.() ?? 0,
201
237
  lastActivityAt: () => drainState.lastActivityAt?.() ?? 0,
202
238
  isStopped: () => closing || draining,
203
- escalate: drainThenShutdown,
239
+ escalate: () => drainOnCause("signal:SIGHUP"),
204
240
  shutdown: hardShutdown,
205
241
  idleGraceMs: config.sighupIdleGraceMs,
206
242
  log: (event, fields) => logger.info(event, fields),
@@ -231,7 +267,7 @@ export function installShutdownHandlers(ctx) {
231
267
  selfExit: (report) => {
232
268
  if (dataRoot)
233
269
  writeExitLast(dataRoot, { version, ...report });
234
- drainThenShutdown();
270
+ drainOnCause("self-exit:attachment");
235
271
  },
236
272
  lingerMs: config.engineAutoExit ? config.engineLingerMs : Number.POSITIVE_INFINITY,
237
273
  ...(config.parentPid !== undefined
@@ -259,6 +295,8 @@ export function installShutdownHandlers(ctx) {
259
295
  }
260
296
  parentWatch?.stop();
261
297
  engineLease?.stop();
298
+ if (drainState.enter === enterByRequest)
299
+ drainState.enter = undefined;
262
300
  },
263
301
  };
264
302
  }
@@ -13,6 +13,7 @@ import { installSecretEnvScrubRecorder } from "../observability/secret-env-scrub
13
13
  import { setRunTerminalLogObserver } from "../observability/run-terminal.js";
14
14
  import { setRedactionObserver } from "../trace/redact.js";
15
15
  import { installOutboundDispatcher } from "../outbound-dispatcher.js";
16
+ import { provisionSemaConfigDir } from "../sema-config-dir.js";
16
17
  export function runStage01() {
17
18
  const config = loadConfig();
18
19
  const runStaleFailure = crossInvariant("run-stale-heartbeat").check({ live: config, next: config });
@@ -44,6 +45,7 @@ export function runStage01() {
44
45
  else
45
46
  logger.info("run_failed", fields);
46
47
  });
48
+ provisionSemaConfigDir(config, logger);
47
49
  const localRoot = configLocalRoot(config);
48
50
  const rootsSplit = splitLocalRoots({
49
51
  localDataRoot: config.localDataRoot,
@@ -1,14 +1,7 @@
1
1
  import type { runStage08 } from "./stage-08-reapers.js";
2
+ import type { DrainState } from "../fleet/drain-entry.js";
2
3
  export declare function runStage09(ctx: Awaited<ReturnType<typeof runStage08>>): {
3
- drainState: {
4
- draining: boolean;
5
- since?: number;
6
- reason?: string;
7
- inflight?: () => number;
8
- lastActivityAt?: () => number;
9
- attachedStreams?: () => number;
10
- lastAttachEndedAt?: () => number;
11
- };
4
+ drainState: DrainState;
12
5
  hookWakeBus: {
13
6
  deliver?: (sessionId: string, text: string) => Promise<boolean>;
14
7
  };
@@ -1,14 +1,6 @@
1
1
  import type { runStage09 } from "./stage-09-leader.js";
2
2
  export declare function runStage10(ctx: Awaited<ReturnType<typeof runStage09>>): Promise<{
3
- drainState: {
4
- draining: boolean;
5
- since?: number;
6
- reason?: string;
7
- inflight?: () => number;
8
- lastActivityAt?: () => number;
9
- attachedStreams?: () => number;
10
- lastAttachEndedAt?: () => number;
11
- };
3
+ drainState: import("../fleet/drain-entry.js").DrainState;
12
4
  hookWakeBus: {
13
5
  deliver?: (sessionId: string, text: string) => Promise<boolean>;
14
6
  };
@@ -13,9 +13,11 @@ import { SQL_BLOB_DEFAULT_MAX_BYTES } from "../plugins/blob-backend.js";
13
13
  import { createStoreLiveProbe } from "../store-live-probe.js";
14
14
  import { counterStoreLabel } from "../plugins/store-backend.js";
15
15
  import { sessionStoreLabelOf } from "../store-posture.js";
16
- import { startFleetClientFromEnv } from "../fleet-client.js";
16
+ import { startFleetClientFromConfig } from "../fleet-client.js";
17
17
  import { createCenterWiringReader } from "../capabilities/center-wiring.js";
18
- import { readinessOf } from "../http/readiness.js";
18
+ import { mintEngineSignal } from "../fleet/engine-signal.js";
19
+ import { createServingLeaseClient } from "../fleet/serving-lease.js";
20
+ import { fleetFenceLaneOf } from "../config.js";
19
21
  import { createResolveSpec } from "./resolve-spec.js";
20
22
  import { executionLaneOf } from "../execution-lane-caps.js";
21
23
  import { startFleetReconciler } from "../fleet/fleet-reconciler.js";
@@ -133,6 +135,24 @@ export async function runStage10(ctx) {
133
135
  const storeLiveProbe = backend && backend.kind !== "local" && backend.dbNowMs && config.storeProbeIntervalMs > 0
134
136
  ? createStoreLiveProbe({ probe: () => backend.dbNowMs(), intervalMs: config.storeProbeIntervalMs, logger })
135
137
  : undefined;
138
+ const fenceLane = fleetFenceLaneOf(config);
139
+ const servingLease = fenceLane !== undefined
140
+ ? createServingLeaseClient({
141
+ center: fenceLane.center,
142
+ generation: fenceLane.generation,
143
+ instanceId,
144
+ dataRoot: config.localDataRoot,
145
+ outageMaxMs: config.fleetLeaseOutageMaxMs,
146
+ drain: (entry) => {
147
+ const enter = drainState.enter;
148
+ if (enter === undefined)
149
+ throw new Error("serving lease denied before the drain entry was installed — refusing to keep serving without a drain");
150
+ enter(entry);
151
+ },
152
+ drainWindowMs: () => config.drainGraceMs,
153
+ logger: { info: (m, d) => logger.info(m, d ?? {}), warn: (m, d) => logger.warn(m, d ?? {}), error: (m, d) => logger.error(m, d ?? {}) },
154
+ })
155
+ : undefined;
136
156
  const deployment = {
137
157
  sessionStoreLabel: sessionStoreLabelOf(config.sessionBackend, backend?.kind),
138
158
  modelReady: () => configCenter.modelReady(),
@@ -154,15 +174,16 @@ export async function runStage10(ctx) {
154
174
  },
155
175
  },
156
176
  restartState: () => configCenter.restartState(),
157
- planeDeferredState: () => configCenter.planeDeferredState(),
158
177
  configApplyState: () => configCenter.configApplyState(),
159
178
  centerCredentialState: () => configCenter.centerCredentialState(),
160
179
  modelRefusalsState: () => configCenter.modelRefusalsState(),
180
+ configHandoffState: () => configCenter.configHandoffState(),
161
181
  mcpRevocations,
162
182
  mcpRevocationState: () => configCenter.mcpRevocationState(),
163
183
  configRefreshNow: () => configCenter.refreshNow(),
164
184
  configCenterManagedKeys: () => configCenter.centerManagedKeys(),
165
185
  drainState,
186
+ servingLease: servingLease ? () => servingLease.view() : undefined,
166
187
  storePosture,
167
188
  centerWiring: createCenterWiringReader({ config, pullState: () => configCenter.centerPullState(), fleetLease, usageReport: fleetUsage !== undefined }),
168
189
  storeLiveState: storeLiveProbe ? () => storeLiveProbe.state() : undefined,
@@ -244,19 +265,19 @@ export async function runStage10(ctx) {
244
265
  ...("code" in e ? { code: String(e.code) } : {}),
245
266
  });
246
267
  });
247
- const fleetClient = startFleetClientFromEnv(config, {
268
+ const fleetClient = startFleetClientFromConfig(config, {
248
269
  instanceId,
249
270
  version: serviceVersion(),
250
- readiness: () => readinessOf(deployment),
251
- restart: () => deployment.restartState?.(),
271
+ signal: () => mintEngineSignal({ ...deployment, config }),
252
272
  modelRefusals: () => deployment.modelRefusalsState?.(),
273
+ ...(servingLease ? { onCenterCapabilities: (caps) => servingLease.noteCenterCapabilities(caps) } : {}),
253
274
  ...(fleetUsage ? { usage: fleetUsage } : {}),
254
275
  logger: { info: (m, d) => logger.info(m, d ?? {}), warn: (m, d) => logger.warn(m, d ?? {}) },
255
276
  });
256
277
  configCenter.onSelfReportChange(() => void fleetClient?.announceNow());
257
278
  if (fleetClient) {
258
- const advertised = process.env.FLEET_ADVERTISE_ADDRESS ?? "";
259
- logger.info("fleet_client_started", { worker: config.configCenter?.worker, usageReport: fleetUsage !== undefined, announce: advertised !== "", ...(advertised !== "" ? { address: buildDisplaySafeUrl(advertised) } : {}) });
279
+ const advertised = config.fleetAdvertiseAddress;
280
+ logger.info("fleet_client_started", { worker: config.configCenter?.worker, usageReport: fleetUsage !== undefined, announce: advertised !== undefined, ...(advertised !== undefined ? { address: buildDisplaySafeUrl(advertised) } : {}) });
260
281
  }
261
282
  const failoverFirstByte = buildFailoverFirstByteNotice(config);
262
283
  if (failoverFirstByte)
@@ -310,7 +331,9 @@ export async function runStage10(ctx) {
310
331
  deviceHub: deviceLane?.hub,
311
332
  retentionLane: retentionLaneStarted,
312
333
  releaseRetentionLease: retentionLaneStore !== undefined ? () => retentionLaneStore.release(instanceId) : undefined,
334
+ servingLease,
313
335
  });
336
+ servingLease?.resumeStickyDenial();
314
337
  return { ...ctx };
315
338
  }
316
339
  //# sourceMappingURL=stage-10-http-server.js.map
@@ -1,19 +1,42 @@
1
- import type { SubagentSpawnContext, ToolSpec } from "@sema-agent/core";
1
+ import type { SubagentSpawnContext, SubagentToolOptions, ToolSpec } from "@sema-agent/core";
2
2
  /** Current time — the model has no clock; OA/most tasks need it. No side effects. CC-parity tool name `Now`
3
3
  * (PascalCase, core 1.176 first-party rename wrap-up — the SERVICE owns this one; `nowTool` is not a core tool).
4
4
  * effect:"read" so it never suspends → no durable-resume rename strand (no pending-tool checkpoint to canonicalize). */
5
5
  export declare function nowTool(): ToolSpec;
6
6
  /**
7
- * #1110(core 7.32.0)—— 委派子代的**逐次注入**工厂(`SubagentToolOptions.extraTools`):`Now` + 部署的 extra tools(今天 = SendUserFile)。
7
+ * 委派子代逐次注入的**一组**工具:`tools` 是这组**可能**注入的全部工具对象,`forSpawn` 只在这些对象里选「这一次 spawn 拿不拿」(缺席 = 每次都拿)。
8
+ * 名册与工厂读的是同一张 `tools` —— 工厂只能交出名册里那几只对象,结构上交不出名册外的东西(core 7.46.0 #1432 的「声明不说谎」在本仓由形状保证,
9
+ * 不靠两张表对拍)。
10
+ */
11
+ export interface SubagentExtraToolSet {
12
+ readonly tools: readonly ToolSpec[];
13
+ readonly forSpawn?: (ctx: SubagentSpawnContext) => boolean;
14
+ }
15
+ /** 委派工具上与逐次注入相关的两键(core `SubagentToolOptions` 的 `extraTools` / `extraToolNames`):工厂(本仓的恒同步)+ 它的名册。 */
16
+ export interface SubagentExtraToolOptions {
17
+ readonly extraTools: (ctx: SubagentSpawnContext) => ToolSpec[];
18
+ readonly extraToolNames: NonNullable<SubagentToolOptions["extraToolNames"]>;
19
+ }
20
+ /**
21
+ * #1110(core 7.32.0)—— 委派子代的**逐次注入**:`Now` + 部署的 extra tools(今天 = SendUserFile)。
8
22
  *
9
23
  * 为什么 `Now` 走这条通道:core 7.32.0 起子代按**身份**继承场景层 bundle 的工具(`assembleCodeTools` 交给 Agent 的
10
24
  * `inheritedTools` ∩ 父 run 真挂载),而 `Now` 是本仓在 bundle **旁边**组的宿主工具(scenarios.ts),宿主工具**从不**被继承
11
25
  * (core:继承整张 `spec.tools` 会把宿主工具带过宿主自己的逐次门)—— 逐次注入是它到达子代的唯一通道。一个工厂、两个产地:
12
- * 部署工厂的门(SendUserFile 的审批面预判 / 只读内建排除)原样在它自己那只函数里,这里只做合成。
13
- * ⚠️ 工厂恒在场 ⇒ core 不再给 Agent 工具铸静态 `agentToolPool`(core:工厂可在 spawn 时注入身份未知的工具 ⇒ 池不可静态知 ⇒
14
- * 记忆内容来源读者按「外部」读,fail-closed)。本仓 default bundle 的继承面本来就含 WebFetch(外部),这一翻面只在
15
- * 「子代面恰好不含任何外部工具」的窄形上把委派结果多判一次外部,方向是收紧。
16
- * 工厂**不抛**:`nowTool()` 是纯构造;部署工厂的抛由 core 收成「这一 spawn 什么都不继承 + 模型面披露句」(onExtraToolsError 记 warn)。
26
+ * 部署那一组的门(只读内建排除)在它自己的 `forSpawn` 里,这里只做合成。
27
+ *
28
+ * core 7.46.0 #1432 —— **工厂与名册一个来源**:返回委派工具的两键,`extraTools`(每次 spawn 交出 `forSpawn` 选中的那几只)与
29
+ * `extraToolNames`(全部可能交出的那几只的名字 / 别名 / 内容来源,从同一批 `ToolSpec` 读出)。名册在场 ⇒ 引擎重新能列出子代的工具池:
30
+ * 定义级 / 工具级 / `TaskSpec.agents` 清单里的退役拼法与大小写孪生回到开跑前拒(`config.agent.unknown_tool`),记忆内容来源读者按声明读这几只。
31
+ * 修前(core 7.32.0–7.45.x)工厂恒在场 ⇒ 池不可静态知 ⇒ 那一族拼法只告不拒、委派结果一律按「外部」读。
32
+ * 工厂**不抛**:各组的 `tools` 是装配期就建好的对象;部署 `forSpawn` 的抛由 core 收成「这一 spawn 什么都不继承 + 模型面披露句」(onExtraToolsError 记 warn)。
33
+ * 一种拼法出现在两只工具上(部署工具与 `Now` 撞名 / 别名撞)⇒ core 在建委派工具处抛 `config.extra_tool_names`(`shape`),不静默取其一。
34
+ */
35
+ export declare function createSubagentExtraTools(deployment?: SubagentExtraToolSet): SubagentExtraToolOptions;
36
+ /**
37
+ * `onExtraToolsError` 收到的错误 → 运维行 `subagent_extra_tools_error` 的字段:句子,加上错误**自带**的码与 `detail.reason`(在场才铸)。
38
+ * core 7.46.0 #1432 被拒的 spawn(产物不在名册里)带 `config.extra_tool_names` + `undeclared_name` / `undeclared_origin`;部署工厂自己抛的错多半两位都不带。
39
+ * 本仓不按码分支、不重铸 —— 只把错误说的话原样过境到运维面(修前只记句子,码与原因词丢在这一跳)。
17
40
  */
18
- export declare function createSubagentExtraTools(deployment?: (ctx: SubagentSpawnContext) => ToolSpec[]): (ctx: SubagentSpawnContext) => ToolSpec[];
41
+ export declare function extraToolsErrorFields(err: unknown): Record<string, unknown>;
19
42
  //# sourceMappingURL=builtin-tools.d.ts.map
@@ -9,6 +9,27 @@ export function nowTool() {
9
9
  };
10
10
  }
11
11
  export function createSubagentExtraTools(deployment) {
12
- return (ctx) => [nowTool(), ...(deployment?.(ctx) ?? [])];
12
+ const sets = [{ tools: [nowTool()] }, ...(deployment !== undefined ? [deployment] : [])];
13
+ return {
14
+ extraTools: (ctx) => sets.flatMap((s) => (s.forSpawn === undefined || s.forSpawn(ctx) ? [...s.tools] : [])),
15
+ extraToolNames: sets.flatMap((s) => s.tools).map((t) => ({
16
+ name: t.name,
17
+ ...(t.aliases !== undefined ? { aliases: [...t.aliases] } : {}),
18
+ ...(t.contentOrigin !== undefined ? { contentOrigin: t.contentOrigin } : {}),
19
+ })),
20
+ };
21
+ }
22
+ export function extraToolsErrorFields(err) {
23
+ const fields = { err: err instanceof Error ? err.message : String(err) };
24
+ if (err === null || typeof err !== "object")
25
+ return fields;
26
+ const code = Reflect.get(err, "code");
27
+ if (typeof code === "string")
28
+ fields.code = code;
29
+ const detail = Reflect.get(err, "detail");
30
+ const reason = detail !== null && typeof detail === "object" ? Reflect.get(detail, "reason") : undefined;
31
+ if (typeof reason === "string")
32
+ fields.reason = reason;
33
+ return fields;
13
34
  }
14
35
  //# sourceMappingURL=builtin-tools.js.map
@@ -39,7 +39,7 @@ import type { NamedKeysOf } from "../request-key-closure.js";
39
39
  */
40
40
  export declare const COMPOSITE_CAPABILITY_MEMBERS: {
41
41
  readonly deviceExecutor: readonly ["enabled", "protocolVersion", "maxInflightPerDevice", "wsPath", "management"];
42
- readonly fleet: readonly ["stream", "sessionScope", "observe", "resume", "bgNotifyFailClosed", "steerPriority", "hookFailureNotice"];
42
+ readonly fleet: readonly ["stream", "sessionScope", "observe", "resume", "bgNotifyFailClosed", "steerPriority", "hookFailureNotice", "leaseClient", "generationEcho"];
43
43
  readonly memoryEngine: readonly ["backend", "vectorMode"];
44
44
  readonly store: readonly ["declared", "backend", "durable", "degradedReason", "sql"];
45
45
  readonly centerWiring: readonly ["effectivePull", "effectiveRevision", "lease", "leaseHealth", "failMode", "usageReport", "issuer", "caller"];
@@ -1,6 +1,6 @@
1
1
  export const COMPOSITE_CAPABILITY_MEMBERS = {
2
2
  deviceExecutor: ["enabled", "protocolVersion", "maxInflightPerDevice", "wsPath", "management"],
3
- fleet: ["stream", "sessionScope", "observe", "resume", "bgNotifyFailClosed", "steerPriority", "hookFailureNotice"],
3
+ fleet: ["stream", "sessionScope", "observe", "resume", "bgNotifyFailClosed", "steerPriority", "hookFailureNotice", "leaseClient", "generationEcho"],
4
4
  memoryEngine: ["backend", "vectorMode"],
5
5
  store: ["declared", "backend", "durable", "degradedReason", "sql"],
6
6
  centerWiring: ["effectivePull", "effectiveRevision", "lease", "leaseHealth", "failMode", "usageReport", "issuer", "caller"],
@@ -1,6 +1,7 @@
1
- import { type WebFetchConfig, type BackgroundAgentStore, type CheckpointStore, type PromptProvider, type Runner, type SkillSpec, type SubagentSpawnContext, type TaskListStore, type ToolSpec, type WebSearchConfig } from "@sema-agent/core";
1
+ import { type WebFetchConfig, type BackgroundAgentStore, type CheckpointStore, type PromptProvider, type Runner, type SkillSpec, type TaskListStore, type ToolSpec, type WebSearchConfig } from "@sema-agent/core";
2
2
  import type { Metrics } from "../observability/metrics.js";
3
3
  import type { Logger } from "../observability/logger.js";
4
+ import { type SubagentExtraToolSet } from "./builtin-tools.js";
4
5
  import { type RepoReadClient } from "./repo-tools.js";
5
6
  import { type LoadedSkill } from "./skills.js";
6
7
  import { type ScenarioHands } from "./hands-lane.js";
@@ -90,12 +91,13 @@ export interface ScenarioDeps {
90
91
  * core 1.287 `SubagentToolOptions.extraTools` seam wire-through — per-SPAWN dynamic tool injection into the
91
92
  * delegated (Agent-tool) child roster. Deployment-injected per-task tools (today: SendUserFile — its instance is
92
93
  * execute-time late-bound via ctx.taskId → TaskEnvRegistry, so ONE spec serves every spawn) cannot ride the
93
- * ASSEMBLY-time static `subagent.tools` pool; this factory is evaluated by core at each spawn and its product is
94
- * merged into the pool, then goes through core's `resolveToolSubset` whitelist like any other tool (agent-def
95
- * allowTools/denyTools still govern — no extra gate needed on our side). Absent ⇒ no dynamic injection (children
96
- * see exactly the static full-body roster, the pre-1.287 behavior).
94
+ * ASSEMBLY-time static `subagent.tools` pool; core evaluates the factory at each spawn and merges its product into the
95
+ * pool, then it goes through core's `resolveToolSubset` whitelist like any other tool (agent-def allowTools/denyTools
96
+ * still govern — no extra gate needed on our side). A SET, not a bare factory (core 7.46.0 #1432): its `tools` are both
97
+ * what a spawn can be handed and the roster the delegation tool declares (`extraToolNames`), so the factory cannot hand
98
+ * out a tool the roster does not name. Absent ⇒ the delegation tool injects `Now` alone.
97
99
  */
98
- subagentExtraTools?: (ctx: SubagentSpawnContext) => ToolSpec[];
100
+ subagentExtraTools?: SubagentExtraToolSet;
99
101
  /**
100
102
  * core 1.364 durable background agents([ref] S1,[ref] 提货单②写半场):后台子代 `a*` 行的
101
103
  * durable 执行记录 store。**必须与 `RunnerDeps.backgroundAgentStore` 同实例**(读写双点配对,engine
@@ -1,6 +1,6 @@
1
1
  import { CODE_AGENT_PROMPT, CODE_SYSTEM_PROMPT, DEFAULT_SYSTEM_PROMPT, WEB_FETCH_TOOL_NAME, assembleCodeTools, defaultTaskRegistry } from "@sema-agent/core";
2
2
  import { HttpError, createCodedHttpError } from "../security.js";
3
- import { createSubagentExtraTools, nowTool } from "./builtin-tools.js";
3
+ import { createSubagentExtraTools, extraToolsErrorFields, nowTool } from "./builtin-tools.js";
4
4
  import { parseRepo, repoToolsFor } from "./repo-tools.js";
5
5
  import { createCouncilTool, clampRounds } from "./code-review-council.js";
6
6
  import { stablePrompt } from "./prompt.js";
@@ -70,9 +70,9 @@ export function buildScenarios(deps) {
70
70
  ...(deps.checkpointStore ? { checkpointStore: deps.checkpointStore } : {}),
71
71
  ...(deps.ensureChildSessionDurable ? { ensureChildSessionDurable: deps.ensureChildSessionDurable } : {}),
72
72
  },
73
- extraTools: createSubagentExtraTools(deps.subagentExtraTools),
73
+ ...createSubagentExtraTools(deps.subagentExtraTools),
74
74
  ...(deps.logger
75
- ? { onExtraToolsError: (err, info) => deps.logger?.warn?.("subagent_extra_tools_error", { ...info, err: err instanceof Error ? err.message : String(err) }) }
75
+ ? { onExtraToolsError: (err, info) => deps.logger?.warn?.("subagent_extra_tools_error", { ...info, ...extraToolsErrorFields(err) }) }
76
76
  : {}),
77
77
  },
78
78
  ...(webSearch ? { webSearch } : {}),
@@ -1,5 +1,6 @@
1
- import type { SubagentSpawnContext, ToolSpec } from "@sema-agent/core";
1
+ import type { ToolSpec } from "@sema-agent/core";
2
2
  import type { IssuedFileLink } from "../plugins/send-user-file.js";
3
+ import type { SubagentExtraToolSet } from "./builtin-tools.js";
3
4
  /** The frame the web/shell renders as a file card. Rides the durable events tail (replayable). */
4
5
  export interface FileLinkFrame {
5
6
  type: "file_link";
@@ -44,7 +45,9 @@ export declare function sendUserFileTool(deps: {
44
45
  }): ToolSpec;
45
46
  export declare const SEND_USER_FILE_TOOL_NAME = "SendUserFile";
46
47
  /**
47
- * Build the `SubagentToolOptions.extraTools` factory that injects SendUserFile into delegated-child rosters.
48
+ * The deployment's group for the delegated-child injection (`createSubagentExtraTools` in `builtin-tools.ts` composes it with
49
+ * `Now` into the delegation tool's `extraTools` factory AND its `extraToolNames` roster — one list, read twice): SendUserFile,
50
+ * handed to every spawn except a read-only builtin agent's.
48
51
  *
49
52
  * F2 (MED, codex; a product judgment of this server, kept): read-only builtin agents (Explore/Plan) must NOT get
50
53
  * SendUserFile — a read-only agent that can read sensitive files AND mint public links off them breaks the read-only
@@ -59,5 +62,5 @@ export declare const SEND_USER_FILE_TOOL_NAME = "SendUserFile";
59
62
  */
60
63
  export declare function subagentSendUserFileExtraTools(deps: {
61
64
  spec: ToolSpec;
62
- }): (ctx: SubagentSpawnContext) => ToolSpec[];
65
+ }): SubagentExtraToolSet;
63
66
  //# sourceMappingURL=send-user-file-tool.d.ts.map