@sema-agent/server 7.101.1 → 7.103.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 (113) hide show
  1. package/MIGRATION.md +84 -0
  2. package/README.md +4 -4
  3. package/README.zh-CN.md +4 -4
  4. package/USAGE.md +156 -9
  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 +58 -0
  8. package/dist/background-session-reap.js +90 -0
  9. package/dist/bake-runner/main.d.ts +21 -3
  10. package/dist/bake-runner/main.js +18 -11
  11. package/dist/boot/config-center.d.ts +21 -4
  12. package/dist/boot/config-center.js +62 -19
  13. package/dist/boot/exit-boundary.js +17 -0
  14. package/dist/boot/runner-deps.js +1 -0
  15. package/dist/boot/runtime-caps.js +3 -2
  16. package/dist/boot/session-faces.js +3 -4
  17. package/dist/boot/shutdown.d.ts +2 -1
  18. package/dist/boot/shutdown.js +33 -12
  19. package/dist/boot/stage-01-config.js +2 -0
  20. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  21. package/dist/boot/stage-07-capability-layer.js +6 -4
  22. package/dist/boot/stage-08-reapers.d.ts +1 -1
  23. package/dist/boot/stage-09-leader.d.ts +1 -1
  24. package/dist/boot/stage-10-http-server.d.ts +1 -1
  25. package/dist/boot/stage-10-http-server.js +13 -4
  26. package/dist/brain.d.ts +28 -8
  27. package/dist/brain.js +20 -4
  28. package/dist/capabilities/builtin-tools.d.ts +14 -1
  29. package/dist/capabilities/builtin-tools.js +3 -0
  30. package/dist/capabilities/center-plugins.d.ts +9 -3
  31. package/dist/capabilities/center-plugins.js +5 -10
  32. package/dist/capabilities/scenarios.d.ts +1 -1
  33. package/dist/capabilities/scenarios.js +3 -3
  34. package/dist/center-credential-renewer.d.ts +76 -0
  35. package/dist/center-credential-renewer.js +359 -0
  36. package/dist/center-credential.d.ts +76 -22
  37. package/dist/center-credential.js +119 -43
  38. package/dist/config-catalog.js +17 -8
  39. package/dist/config-center/apply-effective.d.ts +16 -0
  40. package/dist/config-center/apply-effective.js +26 -21
  41. package/dist/config-center/center-request.d.ts +13 -0
  42. package/dist/config-center/center-request.js +23 -3
  43. package/dist/config-center/facade.d.ts +1 -1
  44. package/dist/config-center/http-client.js +2 -5
  45. package/dist/config-center/mcp-revocation.d.ts +2 -2
  46. package/dist/config-center/mcp-revocation.js +1 -1
  47. package/dist/config-center/read-warnings.d.ts +19 -48
  48. package/dist/config-center/read-warnings.js +7 -49
  49. package/dist/config-center/restart-signal.d.ts +8 -21
  50. package/dist/config-center/restart-signal.js +2 -12
  51. package/dist/config-center/types.d.ts +4 -2
  52. package/dist/config-provider.js +1 -48
  53. package/dist/config-types.d.ts +107 -17
  54. package/dist/config-types.js +7 -2
  55. package/dist/config.d.ts +7 -0
  56. package/dist/config.js +91 -29
  57. package/dist/device-store.d.ts +1 -1
  58. package/dist/env-name-allowlist-knobs.d.ts +21 -7
  59. package/dist/env-name-allowlist-knobs.js +7 -2
  60. package/dist/fleet-client.d.ts +27 -7
  61. package/dist/fleet-client.js +43 -12
  62. package/dist/fleet-lease.d.ts +1 -1
  63. package/dist/host-lsp-manager.d.ts +4 -3
  64. package/dist/host-lsp-manager.js +2 -6
  65. package/dist/http/approval-sweeps.js +5 -20
  66. package/dist/http/dispatch.js +24 -14
  67. package/dist/http/readiness.d.ts +87 -0
  68. package/dist/http/readiness.js +54 -0
  69. package/dist/http/resume-legs.js +5 -4
  70. package/dist/http/route-table.d.ts +7 -0
  71. package/dist/http/route-table.js +6 -0
  72. package/dist/http/routes/a2a-serve.js +6 -14
  73. package/dist/http/routes/approvals-assistant.js +1 -0
  74. package/dist/http/routes/background.d.ts +80 -0
  75. package/dist/http/routes/background.js +94 -0
  76. package/dist/http/routes/capabilities.js +2 -0
  77. package/dist/http/server.d.ts +25 -8
  78. package/dist/http/server.js +2 -2
  79. package/dist/leader/wire.d.ts +8 -1
  80. package/dist/leader/wire.js +4 -3
  81. package/dist/model-entry-refusal.d.ts +42 -19
  82. package/dist/model-entry-refusal.js +34 -19
  83. package/dist/model-route-endpoint.d.ts +10 -0
  84. package/dist/model-route-endpoint.js +6 -2
  85. package/dist/observability/fail-open.d.ts +2 -2
  86. package/dist/observability/fail-open.js +2 -2
  87. package/dist/observability/secret-env-scrub.d.ts +12 -0
  88. package/dist/observability/secret-env-scrub.js +12 -0
  89. package/dist/outbound-dispatcher.d.ts +125 -0
  90. package/dist/outbound-dispatcher.js +211 -0
  91. package/dist/parked-decide.d.ts +11 -5
  92. package/dist/parked-decide.js +4 -1
  93. package/dist/parked-revive-surface.d.ts +47 -0
  94. package/dist/parked-revive-surface.js +20 -0
  95. package/dist/plugins/background-shell-support.d.ts +20 -1
  96. package/dist/plugins/background-shell-support.js +26 -0
  97. package/dist/plugins/host-platform.d.ts +16 -1
  98. package/dist/plugins/host-platform.js +11 -0
  99. package/dist/plugins/remote-env-host.d.ts +14 -1
  100. package/dist/plugins/remote-env-host.js +15 -10
  101. package/dist/project-memory.js +3 -6
  102. package/dist/run-local.js +2 -0
  103. package/dist/server-secret-env.d.ts +97 -24
  104. package/dist/server-secret-env.js +37 -1
  105. package/dist/tool-approval.d.ts +30 -1
  106. package/dist/tool-approval.js +31 -8
  107. package/dist/trace/core-keyset-guard.d.ts +9 -4
  108. package/dist/trace/engine-notice-wire.d.ts +1 -1
  109. package/dist/trace/engine-notice-wire.js +2 -0
  110. package/dist/trace/project.js +13 -0
  111. package/dist/trace/redact.d.ts +11 -0
  112. package/dist/trace/redact.js +1 -0
  113. package/package.json +4 -3
@@ -4,7 +4,8 @@ import { PlanCacheProbe } from "../plan-cache-probe.js";
4
4
  import { purgeScratchpadDir } from "../env-facts.js";
5
5
  import { setLeafAdvanceListener } from "../session-leaf-bus.js";
6
6
  import { SessionWatchRegistry, posIntEnv } from "../session-watch.js";
7
- import { defaultTaskRegistry, peerSessionBoxHandle } from "@sema-agent/core";
7
+ import { peerSessionBoxHandle } from "@sema-agent/core";
8
+ import { reapSessionBackgroundHard } from "../background-session-reap.js";
8
9
  export function createSessionFaces(ctx) {
9
10
  const { config, logger, metrics, localRoot, backend, sessionStore, runStore, checkpointStore, toolResultStore, resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileHistoryErasure, workflowCompletionInbox, taskListLane, deviceStore, dropSessionScoped, mailboxStore, mailboxRecipientLifecycle, } = ctx;
10
11
  const mysqlPool = backend?.mysqlPool();
@@ -87,9 +88,7 @@ export function createSessionFaces(ctx) {
87
88
  }
88
89
  await purgeScratchpadDir(config.localDataRoot ?? localRoot, sessionId);
89
90
  try {
90
- const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, owner ?? "default");
91
- if (reaped > 0)
92
- logger.info("session_background_reaped", { sessionId, reaped });
91
+ await reapSessionBackgroundHard(sessionId, owner ?? "default", { includeRetained: false, reason: "session_deleted" }, logger);
93
92
  }
94
93
  catch (err) {
95
94
  logger.warn("session_background_reap_failed", { sessionId, err: String(err) });
@@ -9,7 +9,8 @@
9
9
  * 时点即"从此刻起信号可被优雅处理";提前注册会让一个尚未 listen 的进程走 drain 分支。
10
10
  * 2. 三个 `process.on` 的**相对次序**保持 SIGTERM → SIGINT → SIGHUP(与原文件逐字一致);
11
11
  * 它们互不覆盖,但 handler 之间共享 `closing`/`draining` 两个 latch,次序变化会改变
12
- * "第二个信号"的语义读数。
12
+ * "第二个信号"的语义读数。S-752 起「排空期的第二发」只有**一条**判据({@link installShutdownHandlers}
13
+ * 内 `onStopSignalWhileDraining`:与排空起点相隔 ≥ `drainSecondSignalMinGapMs` 才硬停),SIGTERM / SIGINT 共用。
13
14
  * 3. `clearInterval(reaper)` 必须是 hardShutdown 的第一件事(在 `server.close()` 之前),否则
14
15
  * 收尾期还会有 reaper tick 打向正在关闭的池。
15
16
  * 4. **进程级监听一律成对**([ref] 二轮件4):本函数装的六只(三信号 + [ref]① 双钩 + S-557 退出边界钩
@@ -106,16 +106,13 @@ export function installShutdownHandlers(ctx) {
106
106
  forceClose.unref?.();
107
107
  };
108
108
  let draining = false;
109
+ let drainStartedAt = 0;
109
110
  let drainTick;
110
111
  const drainThenShutdown = () => {
111
- if (closing)
112
- return;
113
- if (draining) {
114
- logger.info("drain_second_signal_hard_stop", {});
115
- hardShutdown();
112
+ if (closing || draining)
116
113
  return;
117
- }
118
114
  draining = true;
115
+ drainStartedAt = performance.now();
119
116
  drainState.draining = true;
120
117
  deviceHub?.beginDrain({ graceMs: () => config.drainGraceMs });
121
118
  drainState.since = Date.now();
@@ -126,10 +123,9 @@ export function installShutdownHandlers(ctx) {
126
123
  hardShutdown();
127
124
  return;
128
125
  }
129
- const t0 = performance.now();
130
126
  const tick = setInterval(() => {
131
127
  const n = drainState.inflight?.() ?? 0;
132
- const elapsed = Math.round(performance.now() - t0);
128
+ const elapsed = Math.round(performance.now() - drainStartedAt);
133
129
  if (n === 0 || elapsed >= config.drainGraceMs) {
134
130
  clearInterval(tick);
135
131
  drainTick = undefined;
@@ -139,8 +135,33 @@ export function installShutdownHandlers(ctx) {
139
135
  }, 1_000);
140
136
  drainTick = tick;
141
137
  };
142
- process.on("SIGTERM", drainThenShutdown);
143
- process.on("SIGINT", hardShutdown);
138
+ const onStopSignalWhileDraining = (signal) => {
139
+ const sinceFirst = performance.now() - drainStartedAt;
140
+ const minGapMs = config.drainSecondSignalMinGapMs;
141
+ if (sinceFirst < minGapMs) {
142
+ logger.info("drain_duplicate_signal_ignored", { signal, sinceFirstMs: Math.floor(sinceFirst), minGapMs });
143
+ return;
144
+ }
145
+ logger.info("drain_second_signal_hard_stop", { signal, sinceFirstMs: Math.floor(sinceFirst) });
146
+ hardShutdown();
147
+ };
148
+ const firstStopSignal = {
149
+ SIGTERM: drainThenShutdown,
150
+ SIGINT: hardShutdown,
151
+ };
152
+ const onStopSignal = (signal) => {
153
+ if (closing)
154
+ return;
155
+ if (draining) {
156
+ onStopSignalWhileDraining(signal);
157
+ return;
158
+ }
159
+ firstStopSignal[signal]();
160
+ };
161
+ const onSigterm = () => onStopSignal("SIGTERM");
162
+ const onSigint = () => onStopSignal("SIGINT");
163
+ process.on("SIGTERM", onSigterm);
164
+ process.on("SIGINT", onSigint);
144
165
  const onSighup = createSighupIdleHandler({
145
166
  inflight: () => drainState.inflight?.() ?? 0,
146
167
  lastActivityAt: () => drainState.lastActivityAt?.() ?? 0,
@@ -194,8 +215,8 @@ export function installShutdownHandlers(ctx) {
194
215
  exitBoundary.dispose();
195
216
  process.removeListener("uncaughtException", onUncaughtException);
196
217
  process.removeListener("unhandledRejection", onUnhandledRejection);
197
- process.removeListener("SIGTERM", drainThenShutdown);
198
- process.removeListener("SIGINT", hardShutdown);
218
+ process.removeListener("SIGTERM", onSigterm);
219
+ process.removeListener("SIGINT", onSigint);
199
220
  process.removeListener("SIGHUP", onSighup);
200
221
  onSighup.stop();
201
222
  if (drainTick !== undefined) {
@@ -12,6 +12,7 @@ import { installFileHistoryFailureRecorder } from "../file-history-disclosure.js
12
12
  import { installSecretEnvScrubRecorder } from "../observability/secret-env-scrub.js";
13
13
  import { setRunTerminalLogObserver } from "../observability/run-terminal.js";
14
14
  import { setRedactionObserver } from "../trace/redact.js";
15
+ import { installOutboundDispatcher } from "../outbound-dispatcher.js";
15
16
  export function runStage01() {
16
17
  const config = loadConfig();
17
18
  const runStaleFailure = crossInvariant("run-stale-heartbeat").check({ live: config, next: config });
@@ -56,6 +57,7 @@ export function runStage01() {
56
57
  note: "LOCAL_DATA_ROOT moved the local backend's DATA root, but config.d / remote-exec.json are still read from the config root (LOCAL_DATA_ROOT is not consulted there). If you meant to move both, set CONFIG_LOCAL_DIR to the same directory.",
57
58
  });
58
59
  }
60
+ installOutboundDispatcher(logger);
59
61
  return { config, localRoot, logger, metrics };
60
62
  }
61
63
  //# sourceMappingURL=stage-01-config.js.map
@@ -10,7 +10,7 @@ export declare function runStage07(ctx: Awaited<ReturnType<typeof runStage06>>):
10
10
  imageBakes: import("../plugins/store-backend.js").ImageBake | undefined;
11
11
  imageIndex: import("../plugins/store-backend.js").ImageIndex | undefined;
12
12
  parkedReviveInheritedGate: ((row: import("@sema-agent/core").BackgroundAgentRecord, identity: import("./parked-revive-gate.js").ParkedReviveIdentity) => Promise<import("./parked-revive-gate.js").RebuiltInheritedGate>) | undefined;
13
- parkedReviveTool: import("@sema-agent/core").ToolSpec<import("@sema-agent/core").TSchema> | undefined;
13
+ parkedReviveSurface: import("../parked-revive-surface.js").ParkedReviveSurfaceFactory | undefined;
14
14
  rateLimiter: RateLimiter | import("../plugins/pg-rate-limiter.js").PgRateLimiter | import("../plugins/tidb-rate-limiter.js").TiDBRateLimiter;
15
15
  resumeAnchorStore: import("../plugins/store-backend.js").ResumeAnchorStore | undefined;
16
16
  runStore: import("../plugins/store-backend.js").RunStore | undefined;
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_MODEL_REF } from "../model-select.js";
2
- import { probeSearchBackend, DEFAULT_SUBAGENT_TOOL_NAME } from "@sema-agent/core";
2
+ import { probeSearchBackend } from "@sema-agent/core";
3
3
  import { selectEnvironmentTool } from "../capabilities/select-environment-tool.js";
4
4
  import { subagentSendUserFileExtraTools } from "../capabilities/send-user-file-tool.js";
5
5
  import { parseCapEnv, parseNumOrFailNonNegative } from "../config.js";
@@ -17,6 +17,7 @@ import { selfOrchestrationDenial } from "../task-workflow.js";
17
17
  import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
18
18
  import { reclaimOrphansAtBoot } from "../boot-reclaim.js";
19
19
  import { createParkedReviveInheritedGate } from "./parked-revive-gate.js";
20
+ import { createParkedReviveSurface } from "../parked-revive-surface.js";
20
21
  import { createWebFetchSummarizeLane } from "./webfetch-summarize-lane.js";
21
22
  import { createRunnerDepsOnAsk } from "./runner-deps.js";
22
23
  import { bindRateLimiter, bindRunStore } from "./late-binds.js";
@@ -141,8 +142,9 @@ export async function runStage07(ctx) {
141
142
  : undefined,
142
143
  };
143
144
  const scenarios = buildScenarios(scenarioDeps);
144
- const parkedReviveTool = checkpointStore && backgroundAgentStore ? scenarios.default?.({})?.tools.find((t) => t.name === DEFAULT_SUBAGENT_TOOL_NAME) : undefined;
145
- const parkedReviveInheritedGate = parkedReviveTool && config.durableApproval
145
+ const builtinDefaultScenario = scenarios.default;
146
+ const parkedReviveSurface = checkpointStore && backgroundAgentStore && builtinDefaultScenario ? createParkedReviveSurface(builtinDefaultScenario) : undefined;
147
+ const parkedReviveInheritedGate = parkedReviveSurface && config.durableApproval
146
148
  ? createParkedReviveInheritedGate({
147
149
  config, question, approvalExemptionStore, logger, localRoot,
148
150
  memoryEngineRoot: memoryEngine?.root,
@@ -155,6 +157,6 @@ export async function runStage07(ctx) {
155
157
  const builtinScenarioNames = Object.keys(scenarioDetails);
156
158
  configCenter.applyCenterScenarios({ scenarioDeps, scenarios, scenarioDetails, builtinScenarioNames });
157
159
  logger.info("capabilities", { skills: skills.length, scenarios: Object.keys(scenarios), repoTools: Boolean(repoClient) });
158
- return { ...ctx, approvalExemptionStore, approvalNonceStore, authorize, imageBakes, imageIndex, parkedReviveInheritedGate, parkedReviveTool, rateLimiter, resumeAnchorStore, runStore, scenarioDetails, scenarios, selectEnvTool, taskLimitCaps, taskTimeoutSec, webSearchProvider: webSearchCfg?.provider };
160
+ return { ...ctx, approvalExemptionStore, approvalNonceStore, authorize, imageBakes, imageIndex, parkedReviveInheritedGate, parkedReviveSurface, rateLimiter, resumeAnchorStore, runStore, scenarioDetails, scenarios, selectEnvTool, taskLimitCaps, taskTimeoutSec, webSearchProvider: webSearchCfg?.provider };
159
161
  }
160
162
  //# sourceMappingURL=stage-07-capability-layer.js.map
@@ -23,7 +23,7 @@ export declare function runStage08(ctx: Awaited<ReturnType<typeof runStage07>>):
23
23
  imageBakes: import("../plugins/store-backend.js").ImageBake | undefined;
24
24
  imageIndex: import("../plugins/store-backend.js").ImageIndex | undefined;
25
25
  parkedReviveInheritedGate: ((row: import("@sema-agent/core").BackgroundAgentRecord, identity: import("./parked-revive-gate.js").ParkedReviveIdentity) => Promise<import("./parked-revive-gate.js").RebuiltInheritedGate>) | undefined;
26
- parkedReviveTool: import("@sema-agent/core").ToolSpec<import("@sema-agent/core").TSchema> | undefined;
26
+ parkedReviveSurface: import("../parked-revive-surface.js").ParkedReviveSurfaceFactory | undefined;
27
27
  rateLimiter: import("../index.js").RateLimiter | import("../plugins/pg-rate-limiter.js").PgRateLimiter | import("../plugins/tidb-rate-limiter.js").TiDBRateLimiter;
28
28
  resumeAnchorStore: import("../plugins/store-backend.js").ResumeAnchorStore | undefined;
29
29
  runStore: import("../plugins/store-backend.js").RunStore | undefined;
@@ -57,7 +57,7 @@ export declare function runStage09(ctx: Awaited<ReturnType<typeof runStage08>>):
57
57
  imageBakes: import("../plugins/store-backend.js").ImageBake | undefined;
58
58
  imageIndex: import("../plugins/store-backend.js").ImageIndex | undefined;
59
59
  parkedReviveInheritedGate: ((row: import("@sema-agent/core").BackgroundAgentRecord, identity: import("./parked-revive-gate.js").ParkedReviveIdentity) => Promise<import("./parked-revive-gate.js").RebuiltInheritedGate>) | undefined;
60
- parkedReviveTool: import("@sema-agent/core").ToolSpec<import("@sema-agent/core").TSchema> | undefined;
60
+ parkedReviveSurface: import("../parked-revive-surface.js").ParkedReviveSurfaceFactory | undefined;
61
61
  rateLimiter: import("../index.js").RateLimiter | import("../plugins/pg-rate-limiter.js").PgRateLimiter | import("../plugins/tidb-rate-limiter.js").TiDBRateLimiter;
62
62
  resumeAnchorStore: import("../plugins/store-backend.js").ResumeAnchorStore | undefined;
63
63
  runStore: import("../plugins/store-backend.js").RunStore | undefined;
@@ -57,7 +57,7 @@ export declare function runStage10(ctx: Awaited<ReturnType<typeof runStage09>>):
57
57
  imageBakes: import("../plugins/store-backend.js").ImageBake | undefined;
58
58
  imageIndex: import("../plugins/store-backend.js").ImageIndex | undefined;
59
59
  parkedReviveInheritedGate: ((row: import("@sema-agent/core").BackgroundAgentRecord, identity: import("./parked-revive-gate.js").ParkedReviveIdentity) => Promise<import("./parked-revive-gate.js").RebuiltInheritedGate>) | undefined;
60
- parkedReviveTool: import("@sema-agent/core").ToolSpec<import("@sema-agent/core").TSchema> | undefined;
60
+ parkedReviveSurface: import("../parked-revive-surface.js").ParkedReviveSurfaceFactory | undefined;
61
61
  rateLimiter: import("../index.js").RateLimiter | import("../plugins/pg-rate-limiter.js").PgRateLimiter | import("../plugins/tidb-rate-limiter.js").TiDBRateLimiter;
62
62
  resumeAnchorStore: import("../plugins/store-backend.js").ResumeAnchorStore | undefined;
63
63
  runStore: import("../plugins/store-backend.js").RunStore | undefined;
@@ -1,6 +1,6 @@
1
1
  import { autoRunOnRecommendationEffective, defaultTaskRegistry } from "@sema-agent/core";
2
2
  import { posIntEnv } from "../session-watch.js";
3
- import { brainSummary } from "../brain.js";
3
+ import { brainSummary, buildFailoverFirstByteNotice } from "../brain.js";
4
4
  import { resolveBindHost } from "../config.js";
5
5
  import { buildWriteProtectionPosture } from "../write-protection.js";
6
6
  import { isPricingConfigured } from "../budget.js";
@@ -13,6 +13,7 @@ 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 { startFleetClientFromEnv } from "../fleet-client.js";
16
+ import { readinessOf } from "../http/readiness.js";
16
17
  import { createResolveSpec } from "./resolve-spec.js";
17
18
  import { startFleetReconciler } from "../fleet/fleet-reconciler.js";
18
19
  import { createSideQueryLane } from "./side-query-lane.js";
@@ -23,7 +24,7 @@ import { bindServerLateSeats } from "./late-binds.js";
23
24
  import { serviceVersion } from "../service-version.js";
24
25
  import { buildDisplaySafeUrl } from "../trace/redact.js";
25
26
  export async function runStage10(ctx) {
26
- const { approvalAskAudit, approvalExemptionStore, approvalNonceStore, authorize, backend, backgroundAgentStore, brain, breakerState, centerRuntimeCapsResolver, checkpointStore, config, configCenter, costQuota, deploymentHooks, deviceLane, drainState, durableEnabled, elicitation, fenceSessionOwner, fileHistoryStore, fleetBus, fleetLease, fleetUsage, governanceSeams, handslessRunner, hookAgent, hookLlm, hookWakeBus, imageBakes, imageIndex, instanceId, instrumentDegenerate, leaderEndpoint, localRoot, logger, lspManager, mcpRevocations, memoryBundleFaces, memoryComplianceFaces, memoryConsolidationFaces, memoryEngine, memoryExportBackend, memoryOptOutGrantStore, memoryOriginFaces, memoryPosture, memorySyncCursors, metrics, modelUsageTracker, orgMemoryAdmission, otelExporter, outcomeSink, ownerAware, parkedReviveInheritedGate, parkedReviveTool, perSessionCwd, perTaskImage, planCacheProbe, pricing, principalCaps, promptManifestTracker, purgeSession, question, rateLimiter, reaper, registryJwtVerifier, resumeAnchorStore, retentionLaneStarted, retentionLaneStore, ruleConsent, runStore, runner, runnerDeps, runtimeCapsPeekResolver, scenarioDetails, scenarios, selectEnvTool, sendFileLedger, sendUserFileEmitter, sendUserFileToolSpec, sessionAudit, sessionEnvSelection, sessionMemoryStatusFace, sessionPolicyStore, sessionShellGateRegistry, sessionTitler, sessionWatchRegistry, setSessionCwd, setSessionShellEnv, sharedMemoryStore, sideQueryAccounting, sqlEngineFacts, sqlWorkflowRunStore, staticWiring, storeBackendDegraded, subRunner, subagentSteerRegistry, taskAttachmentStore, taskLimitCaps, taskTimeoutSec, toolApproval, toolResultStore, usageWindowStore, webSearchProvider, workflowAgentRegistry, workflowAgentSessionIndex, workflowCompletionInbox, workflowJournalStore, workflowModelAllowlistSnapshot, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, workflowRunStore, workflowsCapable } = ctx;
27
+ const { approvalAskAudit, approvalExemptionStore, approvalNonceStore, authorize, backend, backgroundAgentStore, brain, breakerState, centerRuntimeCapsResolver, checkpointStore, config, configCenter, costQuota, deploymentHooks, deviceLane, drainState, durableEnabled, elicitation, fenceSessionOwner, fileHistoryStore, fleetBus, fleetLease, fleetUsage, governanceSeams, handslessRunner, hookAgent, hookLlm, hookWakeBus, imageBakes, imageIndex, instanceId, instrumentDegenerate, leaderEndpoint, localRoot, logger, lspManager, mcpRevocations, memoryBundleFaces, memoryComplianceFaces, memoryConsolidationFaces, memoryEngine, memoryExportBackend, memoryOptOutGrantStore, memoryOriginFaces, memoryPosture, memorySyncCursors, metrics, modelUsageTracker, orgMemoryAdmission, otelExporter, outcomeSink, ownerAware, parkedReviveInheritedGate, parkedReviveSurface, perSessionCwd, perTaskImage, planCacheProbe, pricing, principalCaps, promptManifestTracker, purgeSession, question, rateLimiter, reaper, registryJwtVerifier, resumeAnchorStore, retentionLaneStarted, retentionLaneStore, ruleConsent, runStore, runner, runnerDeps, runtimeCapsPeekResolver, scenarioDetails, scenarios, selectEnvTool, sendFileLedger, sendUserFileEmitter, sendUserFileToolSpec, sessionAudit, sessionEnvSelection, sessionMemoryStatusFace, sessionPolicyStore, sessionShellGateRegistry, sessionTitler, sessionWatchRegistry, setSessionCwd, setSessionShellEnv, sharedMemoryStore, sideQueryAccounting, sqlEngineFacts, sqlWorkflowRunStore, staticWiring, storeBackendDegraded, subRunner, subagentSteerRegistry, taskAttachmentStore, taskLimitCaps, taskTimeoutSec, toolApproval, toolResultStore, usageWindowStore, webSearchProvider, workflowAgentRegistry, workflowAgentSessionIndex, workflowCompletionInbox, workflowJournalStore, workflowModelAllowlistSnapshot, workflowNotifyGate, workflowNotifyJournal, workflowRecoverOpts, workflowRunStore, workflowsCapable } = ctx;
27
28
  const stores = {
28
29
  runStore,
29
30
  resumeAnchorStore: resumeAnchorStore ? resumeAnchorStore : undefined,
@@ -95,7 +96,7 @@ export async function runStage10(ctx) {
95
96
  : undefined,
96
97
  instrumentDegenerate,
97
98
  sideQueryAccounting,
98
- parkedReviveTool: parkedReviveTool ? parkedReviveTool : undefined,
99
+ parkedReviveSurface: parkedReviveSurface ? parkedReviveSurface : undefined,
99
100
  parkedReviveInheritedGate: parkedReviveInheritedGate ? parkedReviveInheritedGate : undefined,
100
101
  };
101
102
  const observability = {
@@ -149,6 +150,8 @@ export async function runStage10(ctx) {
149
150
  restartState: () => configCenter.restartState(),
150
151
  planeDeferredState: () => configCenter.planeDeferredState(),
151
152
  configApplyState: () => configCenter.configApplyState(),
153
+ centerCredentialState: () => configCenter.centerCredentialState(),
154
+ modelRefusalsState: () => configCenter.modelRefusalsState(),
152
155
  mcpRevocations,
153
156
  mcpRevocationState: () => configCenter.mcpRevocationState(),
154
157
  configRefreshNow: () => configCenter.refreshNow(),
@@ -237,12 +240,18 @@ export async function runStage10(ctx) {
237
240
  const fleetClient = startFleetClientFromEnv(config, {
238
241
  instanceId,
239
242
  version: serviceVersion(),
240
- isDraining: () => drainState.draining,
243
+ readiness: () => readinessOf(deployment),
244
+ restart: () => deployment.restartState?.(),
245
+ modelRefusals: () => deployment.modelRefusalsState?.(),
241
246
  ...(fleetUsage ? { usage: fleetUsage } : {}),
242
247
  logger: { info: (m, d) => logger.info(m, d ?? {}), warn: (m, d) => logger.warn(m, d ?? {}) },
243
248
  });
249
+ configCenter.onSelfReportChange(() => void fleetClient?.announceNow());
244
250
  if (fleetClient)
245
251
  logger.info("fleet_client_started", { worker: config.configCenter?.worker, address: buildDisplaySafeUrl(process.env.FLEET_ADVERTISE_ADDRESS ?? "") });
252
+ const failoverFirstByte = buildFailoverFirstByteNotice(config);
253
+ if (failoverFirstByte)
254
+ logger.warn("failover_first_byte_lane", failoverFirstByte);
246
255
  logger.info("listening", {
247
256
  port: config.port,
248
257
  bindHost: bindHost ?? "0.0.0.0/::(all interfaces)",
package/dist/brain.d.ts CHANGED
@@ -12,12 +12,14 @@ export declare const FAST_FAIL_MAX_RETRIES = 2;
12
12
  * Build the external "Brain" (LLM) from config.
13
13
  *
14
14
  * Composition (A1 + the 1.38 resilience stack, ordered timeout-innermost → breaker → failover-outermost):
15
- * - **Tiered timeouts** (1.38, `connectTimeoutMs`/`firstTokenTimeoutMs`) are passed into each
16
- * openai-compatible brain at creation — they bound "gateway never accepts the connection" and
17
- * "stream opened but hangs without a token" as retryable `[network]` failures. Default ON
18
- * (connect 30s per the fleet-audit recommendation; first-token 600s since S-202 — a self-hosted backend's long
19
- * prefill legitimately takes minutes to the first byte, and 120s turned that into a retry handing the
20
- * same prompt back to the same queue. env =0 disables; env =120000 restores the old posture).
15
+ * - **Tiered timeouts** (1.38; re-cut by core 7.33.0 along the segments of a call) are passed into each
16
+ * brain at creation: `firstByteTimeoutMs` (fetch() → response headers; S-753: **passed only when the
17
+ * operator set `MODEL_FIRST_BYTE_TIMEOUT_MS`** — absent = no key, core's own default, which carries an
18
+ * upload allowance and one widened retry of its own), `firstTokenTimeoutMs` (headers → first delta,
19
+ * 600s since S-202 — a self-hosted backend's long prefill legitimately takes minutes, and 120s turned that
20
+ * into a retry handing the same prompt back to the same queue) and `idleTimeoutMs`. The TCP + TLS connect
21
+ * is the transport's own clock (core 7.33.0), not a knob here. env =0 = no engine clock on that segment
22
+ * and the transport's header / body clocks off too (the connect keeps the transport's own clock).
21
23
  * - **Circuit breaker** (`createCircuitBreakerBrain`) wraps the *primary* gateway route(s) when
22
24
  * `MODEL_CIRCUIT_BREAKER=true`: after N transient failures it opens and fast-fails, so failover
23
25
  * switches to the bare backup immediately instead of waiting out each timeout. The last gateway
@@ -30,14 +32,32 @@ export declare const FAST_FAIL_MAX_RETRIES = 2;
30
32
  * ↔ vLLM is a *routing* choice, not failover.)
31
33
  *
32
34
  * When no fallback URL, no Anthropic key, and no resilience thresholds are configured, this returns the
33
- * bare openai brain (carrying the default-on watchdog timeouts — set the three env knobs to 0 for the
34
- * pre-[ref] fully-bare shape). The Model itself (id / contextWindow / maxTokens / provider) flows
35
+ * bare openai brain (carrying the watchdogs: core's own first-byte default unless `MODEL_FIRST_BYTE_TIMEOUT_MS`
36
+ * is set, plus the server's first-token / idle defaults; a knob at 0 = no engine clock on that segment and the
37
+ * transport's header / body clocks off too). The Model itself (id / contextWindow / maxTokens / provider) flows
35
38
  * per-request through `Runner({ models })`.
36
39
  */
37
40
  export declare function createBrain(config: ServiceConfig, deps?: {
38
41
  fetchImpl?: typeof fetch;
39
42
  breakerState?: BreakerState;
40
43
  }): Brain;
44
+ /**
45
+ * S-753 复审(opus F2 + codex C1)—— **failover 拓扑 × 首字节段没有部署窗**的启动期披露(纯数据,`build*`;告警由 boot 出)。
46
+ *
47
+ * 缺省下首字节段不铸键(core 自缺省当家,[ref])、`0` = 该段不设引擎钟;而切备只发生在一次调用**失败之后**。于是主网关接了连接
48
+ * 却不回响应头时:缺省 ⇒ 要等 core 的首字节 lane 走完(缺省首窗 + 一次加宽重试;本仓不铸 `firstByteRetries`,S-753 车PA)才切备,
49
+ * 时间预算短于那一段的调用在备网关被尝试之前就被取消 —— 以 core 7.33.x 缺省计 lane ≈ 15 min,code-review council 每个镜头的 180 s 与 team
50
+ * discuss 的 600 s 都在其内(车PA codex r1:600 s 在主网关的加宽重试窗里到点,不是「只剩余量」);
51
+ * `0` ⇒ 首字节 lane 永不切备。启动日志里同行的 `gatewayRetryCap:"fast-fail:…"` 只管共享重试梯子,读起来却像「会快切」—— 这条披露把它补齐。
52
+ * 显式设了正数窗 ⇒ `undefined`(部署已表态);单路由 ⇒ `undefined`(没有备可切)。**不替运维铸窗**:出路是旋钮,不是本仓的数。
53
+ */
54
+ export declare function buildFailoverFirstByteNotice(config: ServiceConfig): {
55
+ readonly knob: "MODEL_FIRST_BYTE_TIMEOUT_MS";
56
+ readonly fallbacks: number;
57
+ readonly firstByteTimeoutMs: number | null;
58
+ readonly effect: string;
59
+ readonly fix: string;
60
+ } | undefined;
41
61
  /** One-line summary of the active brain composition, for the startup log. */
42
62
  export declare function brainSummary(config: ServiceConfig): Record<string, unknown>;
43
63
  //# sourceMappingURL=brain.d.ts.map
package/dist/brain.js CHANGED
@@ -38,7 +38,7 @@ export function createBrain(config, deps = {}) {
38
38
  const { fetchImpl, breakerState } = deps;
39
39
  const r = config.resilience ?? DEFAULT_RESILIENCE;
40
40
  const timeouts = {
41
- connectTimeoutMs: r.connectTimeoutMs,
41
+ ...(r.firstByteTimeoutMs !== undefined ? { firstByteTimeoutMs: r.firstByteTimeoutMs } : {}),
42
42
  firstTokenTimeoutMs: r.firstTokenTimeoutMs,
43
43
  idleTimeoutMs: r.idleTimeoutMs,
44
44
  };
@@ -135,6 +135,22 @@ export function createBrain(config, deps = {}) {
135
135
  }
136
136
  return brain;
137
137
  }
138
+ export function buildFailoverFirstByteNotice(config) {
139
+ const r = config.resilience ?? DEFAULT_RESILIENCE;
140
+ const fallbacks = config.gatewayFallbackUrls.length;
141
+ if (fallbacks === 0 || (r.firstByteTimeoutMs !== undefined && r.firstByteTimeoutMs > 0))
142
+ return undefined;
143
+ const zero = r.firstByteTimeoutMs === 0;
144
+ return {
145
+ knob: "MODEL_FIRST_BYTE_TIMEOUT_MS",
146
+ fallbacks,
147
+ firstByteTimeoutMs: r.firstByteTimeoutMs ?? null,
148
+ effect: zero
149
+ ? "the first-byte lane never fails over a primary that accepts the connection but sends no response headers (0 = no engine clock on that segment); only a run-level limit ends that wait"
150
+ : "a primary that accepts the connection but sends no response headers is failed over only after core's default first-byte lane runs out (the first window, then one widened retry); a call with a time budget shorter than that — the code-review council's 180 s lenses and team discuss's 600 s, with core 7.33.x defaults — is cancelled before the backup is ever tried",
151
+ fix: "set MODEL_FIRST_BYTE_TIMEOUT_MS to the longest response-header wait a healthy primary needs (queue + prefill), so a hung primary is cut over after that window and its one widened retry",
152
+ };
153
+ }
138
154
  export function brainSummary(config) {
139
155
  const r = config.resilience ?? DEFAULT_RESILIENCE;
140
156
  const hasFailover = config.gatewayFallbackUrls.length > 0;
@@ -147,9 +163,9 @@ export function brainSummary(config) {
147
163
  failover: hasFailover,
148
164
  anthropicRoute: Boolean(config.anthropic),
149
165
  anthropicCacheBreakpoints: config.anthropic?.cacheBreakpoints ?? null,
150
- connectTimeoutMs: r.connectTimeoutMs || null,
151
- firstTokenTimeoutMs: r.firstTokenTimeoutMs || null,
152
- idleTimeoutMs: r.idleTimeoutMs || null,
166
+ firstByteTimeoutMs: r.firstByteTimeoutMs ?? null,
167
+ firstTokenTimeoutMs: r.firstTokenTimeoutMs,
168
+ idleTimeoutMs: r.idleTimeoutMs,
153
169
  circuitBreaker: breakerActive,
154
170
  circuitBreakerRequested: r.circuitBreaker,
155
171
  gatewayRetryCap: config.gatewayMaxRetries !== undefined
@@ -1,6 +1,19 @@
1
- import type { ToolSpec } from "@sema-agent/core";
1
+ import type { SubagentSpawnContext, 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
+ /**
7
+ * #1110(core 7.32.0)—— 委派子代的**逐次注入**工厂(`SubagentToolOptions.extraTools`):`Now` + 部署的 extra tools(今天 = SendUserFile)。
8
+ *
9
+ * 为什么 `Now` 走这条通道:core 7.32.0 起子代按**身份**继承场景层 bundle 的工具(`assembleCodeTools` 交给 Agent 的
10
+ * `inheritedTools` ∩ 父 run 真挂载),而 `Now` 是本仓在 bundle **旁边**组的宿主工具(scenarios.ts),宿主工具**从不**被继承
11
+ * (core:继承整张 `spec.tools` 会把宿主工具带过宿主自己的逐次门)—— 逐次注入是它到达子代的唯一通道。一个工厂、两个产地:
12
+ * 部署工厂的门(SendUserFile 的审批面预判 / 只读内建排除)原样在它自己那只函数里,这里只做合成。
13
+ * ⚠️ 工厂恒在场 ⇒ core 不再给 Agent 工具铸静态 `agentToolPool`(core:工厂可在 spawn 时注入身份未知的工具 ⇒ 池不可静态知 ⇒
14
+ * 记忆内容来源读者按「外部」读,fail-closed)。本仓 default bundle 的继承面本来就含 WebFetch(外部),这一翻面只在
15
+ * 「子代面恰好不含任何外部工具」的窄形上把委派结果多判一次外部,方向是收紧。
16
+ * 工厂**不抛**:`nowTool()` 是纯构造;部署工厂的抛由 core 收成「这一 spawn 什么都不继承 + 模型面披露句」(onExtraToolsError 记 warn)。
17
+ */
18
+ export declare function createSubagentExtraTools(deployment?: (ctx: SubagentSpawnContext) => ToolSpec[]): (ctx: SubagentSpawnContext) => ToolSpec[];
6
19
  //# sourceMappingURL=builtin-tools.d.ts.map
@@ -8,4 +8,7 @@ export function nowTool() {
8
8
  execute: () => new Date().toISOString(),
9
9
  };
10
10
  }
11
+ export function createSubagentExtraTools(deployment) {
12
+ return (ctx) => [nowTool(), ...(deployment?.(ctx) ?? [])];
13
+ }
11
14
  //# sourceMappingURL=builtin-tools.js.map
@@ -39,14 +39,19 @@ export type PluginValidation = {
39
39
  };
40
40
  /** 条款 1/2/4:https-only、host 全等白名单、sha 40hex。纯函数,worker 侧纵深复验(不信任 wire 已验过)。 */
41
41
  export declare function validatePluginDecl(decl: PluginDeclWire, allowHosts: readonly string[]): PluginValidation;
42
- /** 条款 3/5:clone 计划(argv 序列 + 隔离 env)。纯数据,让"姿势"可被锚定。 */
42
+ /**
43
+ * 条款 3:clone 计划(argv 序列)。纯数据,让"步骤"可被锚定。
44
+ *
45
+ * 7.103.0(S-684 残余):计划**只**产 argv。隔离 env(收紧③:GIT_CONFIG_GLOBAL/SYSTEM=/dev/null、GIT_TERMINAL_PROMPT=0)的唯一属主是
46
+ * `gitSubprocessEnv({ isolateConfig: true })` —— 修前这三键写在这里、由 `runGit` 叠在 `{ ...process.env }` 上,一件姿势两个写者,
47
+ * 而剥密恰好落在两者之外。锚点随之移到真 spawn 的 env 上(`test/secret-env-scrub-host-legs.test.ts` R2:假 git 读到的整份 env)。
48
+ */
43
49
  export declare function gitClonePlan(o: {
44
50
  url: string;
45
51
  dest: string;
46
52
  sha?: string;
47
53
  }): {
48
54
  steps: string[][];
49
- env: Record<string, string>;
50
55
  };
51
56
  /** manifest 本体尺寸上限(#97 redesign⑥ V2-⑦:**新的本地输入上限**,非既有格式事实——manifest 只被
52
57
  * 消费一个 `skillsPath` 字段,几行 JSON;超界视为畸形输入整条拒装)。 */
@@ -101,6 +106,7 @@ export declare function applyCenterPlugins(baseline: LoadedSkill[], eff: {
101
106
  /** 测试口:跳过 validate(materialize 只接受已验声明;真 git 腿用本地 bare repo,validate 层的
102
107
  * https-only 单独测——两层分开,谁也不掩护谁)。生产恒 false/缺席。 */
103
108
  skipValidate?: boolean;
104
- runGit?: (args: string[], env: Record<string, string>) => Promise<void>;
109
+ /** 测试口:替换真 git(生产恒缺席)。env 不在这一格上 —— 缺省腿自己经唯一构造取(替身腿本来就不起 git)。 */
110
+ runGit?: (args: string[]) => Promise<void>;
105
111
  }): Promise<ApplyPluginsResult>;
106
112
  //# sourceMappingURL=center-plugins.d.ts.map
@@ -4,6 +4,7 @@ import { promises as fsp, realpathSync, existsSync, readFileSync, lstatSync, sta
4
4
  import { join, resolve, isAbsolute, sep } from "node:path";
5
5
  import { assertConfined, loadSkills } from "./skills.js";
6
6
  import { buildDisplaySafeUrl, redactSecrets } from "../trace/redact.js";
7
+ import { gitSubprocessEnv } from "../server-secret-env.js";
7
8
  const execFileP = promisify(execFile);
8
9
  export const MAX_PLUGIN_SOURCE_URL_CHARS = 2048;
9
10
  export const DEFAULT_PLUGIN_ALLOW_HOSTS = ["github.com"];
@@ -53,13 +54,8 @@ export function validatePluginDecl(decl, allowHosts) {
53
54
  return { ok: true, name, url };
54
55
  }
55
56
  export function gitClonePlan(o) {
56
- const env = {
57
- GIT_CONFIG_GLOBAL: "/dev/null",
58
- GIT_CONFIG_SYSTEM: "/dev/null",
59
- GIT_TERMINAL_PROMPT: "0",
60
- };
61
57
  if (o.sha === undefined) {
62
- return { steps: [["clone", "--depth", "1", "--quiet", o.url, o.dest]], env };
58
+ return { steps: [["clone", "--depth", "1", "--quiet", o.url, o.dest]] };
63
59
  }
64
60
  return {
65
61
  steps: [
@@ -67,7 +63,6 @@ export function gitClonePlan(o) {
67
63
  ["-C", o.dest, "fetch", "--depth", "1", "--quiet", o.url, o.sha],
68
64
  ["-C", o.dest, "checkout", "--quiet", o.sha],
69
65
  ],
70
- env,
71
66
  };
72
67
  }
73
68
  export const PLUGIN_MANIFEST_MAX_BYTES = 64 * 1024;
@@ -130,8 +125,8 @@ export async function applyCenterPlugins(baseline, eff, opts) {
130
125
  const out = { skills: [...baseline], failures: [], letGo: [], lkgUsed: [] };
131
126
  if (declared.length === 0)
132
127
  return out;
133
- const runGit = opts.runGit ?? (async (args, env) => {
134
- await execFileP("git", args, { env: { ...process.env, ...env }, timeout: 120_000, maxBuffer: 4 * 1024 * 1024 });
128
+ const runGit = opts.runGit ?? (async (args) => {
129
+ await execFileP("git", args, { env: gitSubprocessEnv({ site: "server.center-plugins.git-clone", isolateConfig: true }), timeout: 120_000, maxBuffer: 4 * 1024 * 1024 });
135
130
  });
136
131
  const taken = new Set(baseline.map((s) => s.spec.name));
137
132
  for (const decl of declared) {
@@ -155,7 +150,7 @@ export async function applyCenterPlugins(baseline, eff, opts) {
155
150
  await fsp.mkdir(staging, { recursive: true });
156
151
  const plan = gitClonePlan({ url: v.url, dest: staging, sha: v.sha });
157
152
  for (const step of plan.steps)
158
- await runGit(step, plan.env);
153
+ await runGit(step);
159
154
  await fsp.rm(liveDir, { recursive: true, force: true });
160
155
  await fsp.mkdir(join(opts.cacheRoot, v.name), { recursive: true });
161
156
  await fsp.rename(staging, liveDir);
@@ -43,7 +43,7 @@ export interface ScenarioContext {
43
43
  * 等于让一个租户读另一个租户的清单(同 spec 组装处那条「sessionId comes from `auth`, NEVER the
44
44
  * body」的逐字纪律)。
45
45
  *
46
- * 缺席 = 这条调用形本来就没有会话(`main.ts` 的 parkedReviveTool 只取裸 Agent 工具、
46
+ * 缺席 = 这条调用形本来就没有会话(`parked-revive-surface.ts` 对深于一层的行只取 boot 期那只裸 Agent 工具、
47
47
  * `builtinScenarioDetails` 的工具名探针)。此时会话级设施退化为 per-call 实例 —— 不为它发明
48
48
  * 一个假 session。
49
49
  */
@@ -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 } from "../security.js";
3
- import { nowTool } from "./builtin-tools.js";
3
+ import { createSubagentExtraTools, 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,8 +70,8 @@ export function buildScenarios(deps) {
70
70
  ...(deps.checkpointStore ? { checkpointStore: deps.checkpointStore } : {}),
71
71
  ...(deps.ensureChildSessionDurable ? { ensureChildSessionDurable: deps.ensureChildSessionDurable } : {}),
72
72
  },
73
- ...(deps.subagentExtraTools ? { extraTools: deps.subagentExtraTools } : {}),
74
- ...(deps.subagentExtraTools && deps.logger
73
+ extraTools: createSubagentExtraTools(deps.subagentExtraTools),
74
+ ...(deps.logger
75
75
  ? { onExtraToolsError: (err, info) => deps.logger?.warn?.("subagent_extra_tools_error", { ...info, err: err instanceof Error ? err.message : String(err) }) }
76
76
  : {}),
77
77
  },
@@ -0,0 +1,76 @@
1
+ import type { CenterCredentialFailure, CenterCredentialHealth, CenterCredentialSource } from "./center-credential.js";
2
+ import type { Logger } from "./observability/logger.js";
3
+ export declare const CENTER_CREDENTIAL_RENEW_LEAD_KEY = "CENTER_CREDENTIAL_RENEW_LEAD_MS";
4
+ /** 缺省提前量:5 min(设计 §0)。 */
5
+ export declare const CENTER_CREDENTIAL_RENEW_LEAD_DEFAULT_MS: number;
6
+ /** 下界 = 一次续签请求的传输上界:更短的提前量连一次尝试都装不下,令牌注定先过期(配了 ≈ 没配)。 */
7
+ export declare const CENTER_CREDENTIAL_RENEW_LEAD_MIN_MS = 8000;
8
+ /** 上界 = 契约访问令牌寿命(`ACCESS_TOKEN_TTL_SECONDS`)之半:提前量 ≥ 寿命 ⇒ 每只刚铸出来的令牌都已「该续签」,续签环空转。 */
9
+ export declare const CENTER_CREDENTIAL_RENEW_LEAD_MAX_MS: number;
10
+ /**
11
+ * `CENTER_CREDENTIAL_RENEW_LEAD_MS` 的唯一解析口([ref] A 档:坏值拒启,不夹取、不回默认 —— 凭证轴上一根被静默改掉的旋钮 =
12
+ * 部署以为续签开着 / 关着)。收 **raw 值**(调用点写字面 `process.env.<键>`,目录门按字面对账)。
13
+ * · 缺席 / 空串 ⇒ 缺省 300000;`0` ⇒ 关续签(只读信封);
14
+ * · 其余必须是整数毫秒且落在 [{@link CENTER_CREDENTIAL_RENEW_LEAD_MIN_MS}, {@link CENTER_CREDENTIAL_RENEW_LEAD_MAX_MS}]。
15
+ */
16
+ export declare function parseCenterCredentialRenewLeadMs(raw: string | undefined): number;
17
+ /** 续签失败的成因闭集。两形会自己重试(`pending`):`center_unavailable`(中心不可用)与 `write_failed`(续签已成交、改写没落地 ——
18
+ * 新令牌留在内存里重写,绝不拿已消费的 refresh token 再续一次);其余四形是终态(`failed`),等文件内容变化(壳重新登录 / 手工修)。 */
19
+ export type CenterCredentialRenewalFailure = "invalid_grant" | "issuer_mismatch" | "rejected" | "malformed_grant" | "write_failed" | "center_unavailable";
20
+ type RetryingFailure = Extract<CenterCredentialRenewalFailure, "center_unavailable" | "write_failed">;
21
+ type TerminalFailure = Exclude<CenterCredentialRenewalFailure, RetryingFailure>;
22
+ /** 续签器的状态(`/health.centerCredential.renewal` 的来源;`idle` 不上 wire)。非 `idle` 的状态恒**关于**文件里某一只 refresh
23
+ * token —— 文件换了一只(壳轮换 / 重新登录 / 登出 / 换成裸行),下一次取值即清回 `idle`(codex F3)。 */
24
+ export type CenterCredentialRenewal = {
25
+ readonly state: "idle";
26
+ } | {
27
+ readonly state: "pending";
28
+ readonly reason: RetryingFailure;
29
+ readonly since: number;
30
+ readonly attempts: number;
31
+ readonly retryAt: number;
32
+ } | {
33
+ readonly state: "failed";
34
+ readonly reason: TerminalFailure;
35
+ readonly since: number;
36
+ };
37
+ export interface CenterCredentialRenewer {
38
+ renewal(): CenterCredentialRenewal;
39
+ /** 最近一次读到的信封的访问令牌到期时刻(epoch ms;最近一次取值不是信封 ⇒ undefined)。 */
40
+ sessionExpiresAt(): number | undefined;
41
+ /** 退订 + 停定时器(幂等;在飞的那一次照常落地 —— 它的结果对壳同样有用)。 */
42
+ stop(): void;
43
+ }
44
+ /** `/health.centerCredential` 的 wire 形(免鉴权面:只有闭集词、时刻与计数 —— 不带路径、不带 reason 原句、不带令牌)。 */
45
+ export interface CenterCredentialHealthView {
46
+ readonly state: "ok" | "unreadable";
47
+ readonly failure?: CenterCredentialFailure;
48
+ readonly since?: number;
49
+ readonly failures?: number;
50
+ readonly renewal?: {
51
+ readonly state: "pending" | "failed";
52
+ readonly reason: CenterCredentialRenewalFailure;
53
+ readonly since: number;
54
+ readonly expiresAt?: string;
55
+ };
56
+ }
57
+ /** 健康态 + 续签态 → `/health` 那一格;都健康 ⇒ `undefined`(键缺席,正常形状不变)。 */
58
+ export declare function buildCenterCredentialHealthView(health: CenterCredentialHealth, renewer: CenterCredentialRenewer | undefined): CenterCredentialHealthView | undefined;
59
+ export interface CenterCredentialRenewerDeps {
60
+ /** 文件形凭证来源(续签器只从它的取值被触发;文件路径取自它的 `origin`)。 */
61
+ readonly source: CenterCredentialSource;
62
+ /** `configCenter.url` —— 续签只向与它同源的 `issuer` 发。 */
63
+ readonly centerUrl: string;
64
+ /** `CENTER_CREDENTIAL_RENEW_LEAD_MS`(已过 {@link parseCenterCredentialRenewLeadMs});0 = 关续签。 */
65
+ readonly leadMs: number;
66
+ readonly logger: Logger;
67
+ readonly now?: () => number;
68
+ readonly fetchImpl?: typeof fetch;
69
+ }
70
+ /**
71
+ * 续签器工厂(活对象:订阅来源的取值、持一只定时器、持单飞与退避状态)。只对文件形来源有意义 —— env / absent 形调用即抛
72
+ * (装配点只在文件形时装它;装错 = 编程错误,响亮)。
73
+ */
74
+ export declare function createCenterCredentialRenewer(deps: CenterCredentialRenewerDeps): CenterCredentialRenewer;
75
+ export {};
76
+ //# sourceMappingURL=center-credential-renewer.d.ts.map