@sema-agent/server 7.99.0 → 7.101.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 (174) hide show
  1. package/MIGRATION.md +35 -1
  2. package/README.md +12 -1
  3. package/README.zh-CN.md +9 -1
  4. package/USAGE.md +150 -56
  5. package/dist/approval-ask-machine.d.ts +10 -0
  6. package/dist/approval-ask-machine.js +3 -0
  7. package/dist/approval-card.d.ts +86 -1
  8. package/dist/approval-card.js +42 -8
  9. package/dist/approval-policy-settlement.d.ts +31 -0
  10. package/dist/approval-policy-settlement.js +16 -0
  11. package/dist/approval-reconciler.d.ts +8 -1
  12. package/dist/approval-reconciler.js +5 -4
  13. package/dist/approval.d.ts +44 -20
  14. package/dist/approval.js +14 -6
  15. package/dist/boot/config-center.d.ts +16 -8
  16. package/dist/boot/config-center.js +114 -90
  17. package/dist/boot/execution-env.d.ts +1 -2
  18. package/dist/boot/execution-env.js +21 -3
  19. package/dist/boot/leader.js +12 -9
  20. package/dist/boot/memory-consolidation.d.ts +38 -0
  21. package/dist/boot/memory-consolidation.js +35 -0
  22. package/dist/boot/parked-revive-gate.js +1 -0
  23. package/dist/boot/reapers.js +1 -0
  24. package/dist/boot/resolve-spec.d.ts +0 -1
  25. package/dist/boot/resolve-spec.js +28 -10
  26. package/dist/boot/runner-deps.d.ts +2 -2
  27. package/dist/boot/runner-deps.js +9 -3
  28. package/dist/boot/stage-02-registry-pull.js +9 -0
  29. package/dist/boot/stage-05-execution-env.d.ts +1 -1
  30. package/dist/boot/stage-06-runners.d.ts +2 -2
  31. package/dist/boot/stage-06-runners.js +3 -3
  32. package/dist/boot/stage-07-capability-layer.d.ts +2 -3
  33. package/dist/boot/stage-07-capability-layer.js +6 -6
  34. package/dist/boot/stage-08-reapers.d.ts +3 -4
  35. package/dist/boot/stage-08-reapers.js +2 -2
  36. package/dist/boot/stage-09-leader.d.ts +2 -3
  37. package/dist/boot/stage-10-http-server.d.ts +2 -3
  38. package/dist/boot/stage-10-http-server.js +2 -2
  39. package/dist/boot/stores.js +4 -0
  40. package/dist/capabilities/center-plugins.d.ts +14 -2
  41. package/dist/capabilities/center-plugins.js +13 -5
  42. package/dist/capabilities/hands-lane.js +2 -1
  43. package/dist/capabilities/scenarios.js +18 -5
  44. package/dist/capabilities/tool-defer.d.ts +2 -2
  45. package/dist/config-catalog.d.ts +2 -2
  46. package/dist/config-catalog.js +16 -15
  47. package/dist/config-center/apply-effective.d.ts +59 -39
  48. package/dist/config-center/apply-effective.js +231 -120
  49. package/dist/config-center/apply-ledger.d.ts +45 -16
  50. package/dist/config-center/apply-ledger.js +28 -18
  51. package/dist/config-center/facade.d.ts +12 -6
  52. package/dist/config-center/facade.js +2 -2
  53. package/dist/config-center/hot-keys-registry.d.ts +11 -4
  54. package/dist/config-center/hot-keys-registry.js +11 -7
  55. package/dist/config-center/http-client.d.ts +4 -1
  56. package/dist/config-center/http-client.js +15 -10
  57. package/dist/config-center/read-warnings.d.ts +115 -6
  58. package/dist/config-center/read-warnings.js +115 -5
  59. package/dist/config-center/restart-signal.d.ts +21 -14
  60. package/dist/config-center/restart-signal.js +16 -22
  61. package/dist/config-center/types.d.ts +9 -3
  62. package/dist/config-invariants.d.ts +2 -2
  63. package/dist/config-invariants.js +15 -0
  64. package/dist/config-provider.d.ts +4 -0
  65. package/dist/config-provider.js +64 -9
  66. package/dist/config-types.d.ts +57 -13
  67. package/dist/config.js +87 -22
  68. package/dist/cross-session-settings.js +1 -1
  69. package/dist/deployment-governance.d.ts +45 -12
  70. package/dist/deployment-governance.js +37 -9
  71. package/dist/env-name-allowlist-knobs.d.ts +37 -0
  72. package/dist/env-name-allowlist-knobs.js +24 -0
  73. package/dist/fleet-client.d.ts +2 -1
  74. package/dist/hooks/hook-llm.js +13 -4
  75. package/dist/host-lsp-manager.d.ts +41 -0
  76. package/dist/host-lsp-manager.js +15 -0
  77. package/dist/http/active-run-conflict.d.ts +2 -12
  78. package/dist/http/active-run-conflict.js +1 -6
  79. package/dist/http/admission.js +30 -7
  80. package/dist/http/memory-store-refusal-reply.d.ts +24 -0
  81. package/dist/http/memory-store-refusal-reply.js +10 -0
  82. package/dist/http/resume-legs.d.ts +1 -1
  83. package/dist/http/resume-legs.js +14 -10
  84. package/dist/http/route-ctx.d.ts +2 -2
  85. package/dist/http/routes/approvals-assistant.d.ts +1 -1
  86. package/dist/http/routes/approvals-assistant.js +3 -2
  87. package/dist/http/routes/capabilities.js +3 -3
  88. package/dist/http/routes/memory-bundle.js +3 -0
  89. package/dist/http/routes/memory-compliance.js +3 -0
  90. package/dist/http/routes/memory-origin.js +3 -0
  91. package/dist/http/routes/sessions.js +7 -0
  92. package/dist/http/routes/side-query.js +10 -4
  93. package/dist/http/routes/tasks.js +7 -6
  94. package/dist/http/server.d.ts +6 -5
  95. package/dist/http/wire-types.d.ts +6 -3
  96. package/dist/instruction-source-notice.d.ts +97 -0
  97. package/dist/instruction-source-notice.js +78 -0
  98. package/dist/leader/wire.d.ts +20 -12
  99. package/dist/leader/wire.js +7 -6
  100. package/dist/memory-layer-legacy-lock.d.ts +14 -0
  101. package/dist/memory-layer-legacy-lock.js +74 -0
  102. package/dist/memory-store-refusal.d.ts +37 -0
  103. package/dist/memory-store-refusal.js +14 -0
  104. package/dist/model-entry-refusal.d.ts +130 -0
  105. package/dist/model-entry-refusal.js +157 -0
  106. package/dist/model-provider.d.ts +9 -3
  107. package/dist/model-route-endpoint.d.ts +1 -1
  108. package/dist/model-select.d.ts +49 -4
  109. package/dist/model-select.js +48 -17
  110. package/dist/observability/corrupt-read-seat.d.ts +21 -4
  111. package/dist/observability/corrupt-read-seat.js +31 -0
  112. package/dist/observability/fail-open.d.ts +16 -4
  113. package/dist/observability/fail-open.js +16 -4
  114. package/dist/observability/metrics.js +1 -0
  115. package/dist/observability/run-terminal.js +1 -0
  116. package/dist/observability/secret-env-scrub.d.ts +27 -3
  117. package/dist/observability/secret-env-scrub.js +22 -6
  118. package/dist/orchestration/workflow-completion-inbox.d.ts +31 -23
  119. package/dist/orchestration/workflow-completion-inbox.js +8 -3
  120. package/dist/plugins/approval-ask-store-file.d.ts +20 -2
  121. package/dist/plugins/approval-ask-store-file.js +21 -3
  122. package/dist/plugins/approval-ask-store-memory.d.ts +2 -0
  123. package/dist/plugins/approval-ask-store-memory.js +39 -13
  124. package/dist/plugins/approval-ask-store-sql.d.ts +24 -5
  125. package/dist/plugins/approval-ask-store-sql.js +27 -3
  126. package/dist/plugins/caching-session-store.d.ts +6 -0
  127. package/dist/plugins/caching-session-store.js +6 -0
  128. package/dist/plugins/checkpoint-store-sql.d.ts +9 -3
  129. package/dist/plugins/checkpoint-store-sql.js +19 -2
  130. package/dist/plugins/local-checkpoint-store.js +2 -0
  131. package/dist/plugins/local-session-store.d.ts +20 -25
  132. package/dist/plugins/local-session-store.js +111 -64
  133. package/dist/plugins/mailbox-store-sql.d.ts +7 -5
  134. package/dist/plugins/mailbox-store-sql.js +6 -4
  135. package/dist/plugins/permission-rule-store-file.js +3 -2
  136. package/dist/plugins/pg-session-storage.d.ts +7 -0
  137. package/dist/plugins/pg-session-storage.js +19 -0
  138. package/dist/plugins/remote-env-host.d.ts +3 -1
  139. package/dist/plugins/remote-env-host.js +3 -6
  140. package/dist/plugins/sql-json-column.d.ts +16 -3
  141. package/dist/plugins/sql-json-column.js +15 -4
  142. package/dist/plugins/tidb-pool.js +5 -0
  143. package/dist/plugins/tidb-session-store.d.ts +13 -0
  144. package/dist/plugins/tidb-session-store.js +14 -0
  145. package/dist/plugins/web-search.d.ts +44 -23
  146. package/dist/plugins/web-search.js +161 -50
  147. package/dist/project-memory.d.ts +10 -0
  148. package/dist/project-memory.js +28 -14
  149. package/dist/run-local.d.ts +17 -15
  150. package/dist/run-local.js +119 -18
  151. package/dist/runs.js +6 -5
  152. package/dist/runtime-governance.d.ts +50 -3
  153. package/dist/runtime-governance.js +5 -0
  154. package/dist/sealed-key.d.ts +5 -2
  155. package/dist/server-secret-env.d.ts +26 -2
  156. package/dist/server-secret-env.js +9 -0
  157. package/dist/task-settings.d.ts +1 -1
  158. package/dist/tool-approval.d.ts +150 -28
  159. package/dist/tool-approval.js +150 -54
  160. package/dist/trace/core-keyset-guard.d.ts +30 -8
  161. package/dist/trace/engine-notice-wire.d.ts +27 -1
  162. package/dist/trace/engine-notice-wire.js +8 -0
  163. package/dist/trace/ledger-sink.js +2 -2
  164. package/dist/trace/project.d.ts +13 -0
  165. package/dist/trace/project.js +22 -5
  166. package/dist/trace/projection-drop.d.ts +2 -0
  167. package/dist/trace/projection-drop.js +6 -0
  168. package/dist/trace/sema-provenance.d.ts +4 -1
  169. package/dist/trace/sema-provenance.js +1 -1
  170. package/dist/trace/task-notification-facets.d.ts +65 -0
  171. package/dist/trace/task-notification-facets.js +31 -0
  172. package/dist/trace/wire-projection-faces.d.ts +3 -3
  173. package/dist/trace/wire-projection-faces.js +3 -3
  174. package/package.json +3 -3
@@ -1,5 +1,6 @@
1
1
  import { isThinkingLevel, expandTiers } from "@sema-agent/core";
2
- import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
2
+ import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
3
+ import { modelUnavailableMessage } from "../model-entry-refusal.js";
3
4
  import { HttpError } from "../security.js";
4
5
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
5
6
  import { taskBodyKeyIssue } from "../request-key-closure.js";
@@ -7,6 +8,7 @@ import { singleMemberOffFieldIssue } from "../request-value-closure.js";
7
8
  import { selfOrchestrationDenial } from "../task-workflow.js";
8
9
  import { centerEntitlementSourceWired } from "../runtime-caps-resolver.js";
9
10
  import { parseHooksConfig } from "../hooks/hook-runner.js";
11
+ import { judgeRequestWebSearch } from "../plugins/web-search.js";
10
12
  import { normalizeApproachNotice, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
11
13
  import { isValidCwd, MAX_ADDITIONAL_DIRS, MAX_CWD_CHARS } from "../task-cwd.js";
12
14
  import { verifyRoundsFromBody } from "./verify-rounds.js";
@@ -38,7 +40,15 @@ const MAX_IMAGES_PER_REQUEST = 20;
38
40
  const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
39
41
  function mentionableNames(config, catalog) {
40
42
  const allowlist = config.atModelAllowlist ?? [];
41
- return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist));
43
+ return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist, config.modelRefusals));
44
+ }
45
+ function refuseUnavailableModel(res, config, field, ref) {
46
+ const hit = refusedCatalogEntry(ref, config.modelRefusals);
47
+ if (hit === undefined)
48
+ return false;
49
+ const message = modelUnavailableMessage(field, ref.slice(0, 120), hit.name, hit.refusal);
50
+ sendError(res, 400, "request.model_unavailable", message);
51
+ return true;
42
52
  }
43
53
  export function createAdmission(ctx) {
44
54
  const { deps, readJson, runnerFor } = ctx;
@@ -193,6 +203,13 @@ export function createAdmission(ctx) {
193
203
  return null;
194
204
  }
195
205
  }
206
+ {
207
+ const webSearchVerdict = judgeRequestWebSearch(st.webSearch);
208
+ if (webSearchVerdict.kind === "malformed") {
209
+ sendError(res, 400, "request.field_invalid", webSearchVerdict.issue);
210
+ return null;
211
+ }
212
+ }
196
213
  }
197
214
  const skillsErr = validateUserSkills(body.skills);
198
215
  if (skillsErr) {
@@ -411,8 +428,10 @@ export function createAdmission(ctx) {
411
428
  return null;
412
429
  }
413
430
  const agentsBare = deps.config.models ?? {};
414
- const agentsCatalog = expandTiers(agentsBare, deps.config.tiers ?? {}) ?? agentsBare;
431
+ const agentsCatalog = selectableCatalog(agentsBare, deps.config.tiers, deps.config.modelRefusals);
415
432
  const um = unknownTaskAgentModelRef(body.agents, agentsCatalog);
433
+ if (um !== undefined && refuseUnavailableModel(res, deps.config, `agents[${um.index}].model`, um.ref))
434
+ return null;
416
435
  if (um !== undefined) {
417
436
  sendError(res, 400, "request.unknown_reference", `unknown agents[${um.index}].model "${um.ref.slice(0, 120)}" — not in the configured catalog (a catalog name or tier word)`, { available: mentionableNames(deps.config, agentsCatalog) });
418
437
  return null;
@@ -486,8 +505,10 @@ export function createAdmission(ctx) {
486
505
  return null;
487
506
  }
488
507
  const bare = deps.config.models ?? {};
489
- const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
490
- if (matchCatalogModel(body.model, catalog) === undefined) {
508
+ const catalog = selectableCatalog(bare, deps.config.tiers, deps.config.modelRefusals);
509
+ if (matchCatalogModel(body.model, catalog, deps.config.modelRefusals) === undefined) {
510
+ if (refuseUnavailableModel(res, deps.config, "model", body.model))
511
+ return null;
491
512
  sendError(res, 400, "request.unknown_reference", `unknown model "${body.model.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: mentionableNames(deps.config, catalog) });
492
513
  return null;
493
514
  }
@@ -498,8 +519,10 @@ export function createAdmission(ctx) {
498
519
  return null;
499
520
  }
500
521
  const bare = deps.config.models ?? {};
501
- const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
502
- if (matchCatalogModel(body.compactionModel, catalog) === undefined) {
522
+ const catalog = selectableCatalog(bare, deps.config.tiers, deps.config.modelRefusals);
523
+ if (matchCatalogModel(body.compactionModel, catalog, deps.config.modelRefusals) === undefined) {
524
+ if (refuseUnavailableModel(res, deps.config, "compactionModel", body.compactionModel))
525
+ return null;
503
526
  sendError(res, 400, "request.unknown_reference", `unknown compactionModel "${body.compactionModel.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: mentionableNames(deps.config, catalog) });
504
527
  return null;
505
528
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * core 7.31.1 #1094 `memory.target_outside_store` 的 **operator 面 HTTP 映射** —— 唯一一处字面量(判据在
3
+ * `src/memory-store-refusal.ts`)。挂在每一个触 store 树的 operator 记忆口的已分类收尾上:
4
+ * `routes/memory-compliance.ts`(出处 / 抹除)· `routes/memory-origin.ts`(外源列表 / 清标;**不含**只读控制面的清标账列表 ——
5
+ * core 的 wire 表逐字:只碰控制面的面不判,给它挂一条永远收不到的码路径正是附录 A 活触发门要防的形)·
6
+ * `routes/memory-bundle.ts`(导出 / 导入)。会话所有者的自助抹除口**不走这里**(`routes/sessions.ts`:同码、固定文案,见那里的注)。
7
+ * `POST /v1/admin/memory/consolidation/run` 也不走这里:那口把 core 的**任何**有码拒一律收成 409 `memory.consolidation_refused`
8
+ * (码进文案,附录 A 该行),本码已在那条臂上,不再给它第二个码。
9
+ *
10
+ * ## 为什么 409
11
+ * core 的读法:operator 要处置的拒(去掉 store 里那只通到外面的目录链),不是可重试的 IO 故障,也不是对调用方的裁决。与
12
+ * `memory.erasure_census_incomplete` 同一类(**当前 store 状态**下这次做不了;状态修好后重发即改判)⇒ 409,不 500(服务器没坏)、
13
+ * 不 4xx 请求族(请求没写错)。抹除面:链先于请求在场 ⇒ core 在提交点之前拒,零删除、零 journal(真 core 格钉在
14
+ * `test/memory-compliance-http.test.ts`);链在提交点之后才换上 ⇒ 已删的几条算数、journal 留盘 —— core 原句会带
15
+ * `after the commit point; the journal is kept`,本层原样转发、不嗅它分类(codex 一审 C1)。
16
+ *
17
+ * ## 文案原样
18
+ * core 的诊断句点名动作、路径与解析到哪 —— 那是运维找到那只链的唯一材料,与本族其它 core 码同一条「码与文案都不改写」纪律。
19
+ * 只对 operator 面这么做:这几口都过了 operator 门,宿主路径对这个受众不是泄露。
20
+ */
21
+ import type { ServerResponse } from "node:http";
22
+ /** `err` 是 `memory.target_outside_store` ⇒ 409 带码 + core 原句,返回 true(已应答);否则 false,一个字节都不写。 */
23
+ export declare function sentMemoryStoreRefusal(res: ServerResponse, err: unknown): boolean;
24
+ //# sourceMappingURL=memory-store-refusal-reply.d.ts.map
@@ -0,0 +1,10 @@
1
+ import { targetOutsideStoreOf } from "../memory-store-refusal.js";
2
+ import { sendError } from "./send.js";
3
+ export function sentMemoryStoreRefusal(res, err) {
4
+ const refusal = targetOutsideStoreOf(err);
5
+ if (refusal === undefined)
6
+ return false;
7
+ sendError(res, 409, "memory.target_outside_store", refusal.message);
8
+ return true;
9
+ }
10
+ //# sourceMappingURL=memory-store-refusal-reply.js.map
@@ -29,7 +29,7 @@ export declare function createResumeLegs(ctx: {
29
29
  routeCtxBase: RouteCtxBase;
30
30
  resumeCheckpoint: (sessionId: string, decision: "approve" | "deny", reason: string | undefined, hostDecision: HostDecision, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding, onResumeCommitted?: (grant: {
31
31
  overrideSessionId?: string;
32
- validatedToolName: string | null;
32
+ rememberableToolName: string | null;
33
33
  }) => Promise<void>, acceptEarly?: boolean, decider?: {
34
34
  principal?: string;
35
35
  explicitOperator: boolean;
@@ -1,6 +1,6 @@
1
1
  import { redactErrorMessage, redactHead } from "../observability/run-terminal.js";
2
2
  import { createHash } from "node:crypto";
3
- import { uuidv7, resumeWithVerification, CheckpointError, defaultTaskRegistry, validatePendingSteer, GLOBAL_USAGE_KEY, PAUSE_REGISTRY } from "@sema-agent/core";
3
+ import { uuidv7, resumeWithVerification, CheckpointError, defaultTaskRegistry, taskNotificationDedupKey, validatePendingSteer, GLOBAL_USAGE_KEY, PAUSE_REGISTRY } from "@sema-agent/core";
4
4
  import { decideParkedAgent, denyWorkflowParksUnreadable, findParkOrigin, planWorkflowParkedDecide } from "../parked-decide.js";
5
5
  import { HttpError, decodeCheckpointScope, CHECKPOINT_PUBLIC_SCOPE } from "../security.js";
6
6
  import { evictIfConflict, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
@@ -9,10 +9,10 @@ import { mintCancelledResult } from "../run-cancel-context.js";
9
9
  import {} from "../host-decision.js";
10
10
  import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
11
11
  import { recordResumeResourceWindow } from "../resource-window.js";
12
- import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
12
+ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
13
13
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
14
14
  import { defaultSubagentTailBus } from "../fleet/subagent-tail-bus.js";
15
- import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, resolveApprovalLeg, resolveStreamApprovalGate } from "../tool-approval.js";
15
+ import { APPROVAL_GATE_KINDS, createApprovalCardEmitter, isApprovalGateKind, parkRowStandingAnswerFacts, resolveApprovalLeg, resolveStreamApprovalGate, standingAnswerMayRetire } from "../tool-approval.js";
16
16
  import { buildCheckpointAnchorHash } from "../approval-ask-audit-store.js";
17
17
  import { TERMINAL_PARK_GATE_KINDS } from "../plugins/checkpoint-store-sql.js";
18
18
  import { withPrincipal } from "../observability/principal-context.js";
@@ -198,11 +198,14 @@ export function createResumeLegs(ctx) {
198
198
  };
199
199
  }
200
200
  const paForGrant = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction : undefined;
201
- const loadedGrantTool = paForGrant !== undefined && paForGrant.toolName && paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME
201
+ const rememberableTool = paForGrant !== undefined &&
202
+ paForGrant.toolName &&
203
+ paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
204
+ standingAnswerMayRetire(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config))
202
205
  ? paForGrant.toolName
203
206
  : null;
204
207
  const boundGrant = onResumeCommitted
205
- ? (overrideSessionId) => onResumeCommitted({ ...(overrideSessionId !== undefined ? { overrideSessionId } : {}), validatedToolName: loadedGrantTool })
208
+ ? (overrideSessionId) => onResumeCommitted({ ...(overrideSessionId !== undefined ? { overrideSessionId } : {}), rememberableToolName: rememberableTool })
206
209
  : undefined;
207
210
  const lateSettleSink = deps.approvalLateSettle;
208
211
  const lateSettleAnchor = lateSettleSink !== undefined && req !== undefined && token
@@ -496,8 +499,9 @@ export function createResumeLegs(ctx) {
496
499
  const { type, ...rest } = frame;
497
500
  const f = frame;
498
501
  const write = rs0.appendEvent(taskId, drainSeq++, type, rest);
499
- if (f.type === "task_notification" && f.task_id)
500
- resumeNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), write.then(() => true, () => false));
502
+ const fKey = f.type === "task_notification" ? taskNotificationStreamKeyOf(f) : undefined;
503
+ if (fKey !== undefined)
504
+ resumeNotifiedKeys.set(fKey, write.then(() => true, () => false));
501
505
  return write;
502
506
  }, { route: "resume-open", connection: "durable-append", log: (m, x) => deps.logger?.info?.(m, x) });
503
507
  }
@@ -544,7 +548,7 @@ export function createResumeLegs(ctx) {
544
548
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume-verify", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
545
549
  }
546
550
  else if (resumeLegLive) {
547
- const key = taskNotificationStreamKey(n);
551
+ const key = taskNotificationDedupKey(n);
548
552
  if (resumeNotifiedKeys.get(key) === undefined) {
549
553
  resumeNotifiedKeys.set(key, vAppend("task_notification", taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) })).then(() => true, () => false));
550
554
  }
@@ -671,7 +675,7 @@ export function createResumeLegs(ctx) {
671
675
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
672
676
  }
673
677
  else if (resumeLegLive) {
674
- const key = taskNotificationStreamKey(n);
678
+ const key = taskNotificationDedupKey(n);
675
679
  if (resumeNotifiedKeys.get(key) === undefined) {
676
680
  resumeNotifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) })).then(() => true, () => false));
677
681
  }
@@ -780,7 +784,7 @@ export function createResumeLegs(ctx) {
780
784
  case "task_notification": {
781
785
  await flush();
782
786
  const tnr = ev.notification;
783
- const tnrKey = tnr?.task_id ? taskNotificationStreamKey({ task_type: tnr.task_type, task_id: tnr.task_id, status: String(tnr.status), seq: tnr.seq }) : undefined;
787
+ const tnrKey = taskNotificationStreamKeyOf(tnr);
784
788
  const tnrPrior = tnrKey !== undefined ? resumeNotifiedKeys.get(tnrKey) : undefined;
785
789
  if (tnrPrior === undefined || !(await tnrPrior)) {
786
790
  const write = append("task_notification", taskNotificationEventData(ev));
@@ -357,11 +357,11 @@ export interface RouteLegs {
357
357
  resumeCheckpoint(sessionId: string, decision: "approve" | "deny", reason: string | undefined, hostDecision: HostDecision, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding,
358
358
  /** [ref] codex R4(验真后修):remember 豁免的 grant 挂点。**工具名不再由路由自己读**(那是与本函数
359
359
  * 载入行分歧的第三次 scope-less 读,三读分歧下会给他人工具落豁免=提权)——由本函数把**已属主校验的
360
- * 载入 cp** 的工具名(`validatedToolName`,非 tool_approval / AskUserQuestion / legacy null ⇒ `null`
360
+ * 载入 cp** 的可记工具名(`rememberableToolName`,非 tool_approval / AskUserQuestion / legacy null / S-663 强制卡 ⇒ `null`
361
361
  * = 无可记)一并回传,路由只对这个逐字落店。`overrideSessionId` = parked 腿的 root/host 会话 grant 锚。 */
362
362
  onResumeCommitted?: (grant: {
363
363
  overrideSessionId?: string;
364
- validatedToolName: string | null;
364
+ rememberableToolName: string | null;
365
365
  }) => Promise<void>,
366
366
  /** [ref]:透传给 {@link DriveResumeArgs.acceptEarly}(200 受理语义)。HTTP `/decide` 腿传 `true`;
367
367
  * 内部 D-D SLA deny-sweep 不传 —— 它没有在等响应的人,提前受理对它零收益而少一条同步失败面。 */
@@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { QuestionAnswer } from "@sema-agent/core";
3
3
  import type { CheckpointStoreFull } from "../../plugins/store-backend.js";
4
4
  import { type ToolApprovalCoordinator } from "../../tool-approval.js";
5
- import { type GovernancePosture } from "../active-run-conflict.js";
5
+ import { type GovernancePosture } from "../../runtime-governance.js";
6
6
  import { type RouteCtx, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
7
7
  import { type KeyClosureIssue } from "../../request-key-closure.js";
8
8
  /**
@@ -16,7 +16,7 @@ import { sleep } from "../sse-log.js";
16
16
  import { bindSseLifecycle } from "../sse-lifecycle.js";
17
17
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
18
18
  import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
19
- import { governanceOriginOf } from "../active-run-conflict.js";
19
+ import { governanceOriginOf } from "../../runtime-governance.js";
20
20
  import { sendResumeOutcome } from "../route-ctx.js";
21
21
  import { assertNever } from "../route-ctx.js";
22
22
  import { readResumeCtx, resumeCtxCorruptAnswer } from "../resume-ctx-read.js";
@@ -288,6 +288,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
288
288
  ...(s.deadline !== undefined ? { deadline: s.deadline } : {}),
289
289
  ...(s.hasBidiControls === true ? { hasBidiControls: true } : {}),
290
290
  ...(s.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
291
+ ...(s.mandated === true ? { mandated: true } : {}),
291
292
  ...(s.denialLimitFallback !== undefined ? { denialLimitFallback: s.denialLimitFallback } : {}),
292
293
  ...(s.origin !== undefined ? { origin: s.origin } : {}),
293
294
  ...(s.ruleStoreUnreadable !== undefined ? { ruleStoreUnreadable: s.ruleStoreUnreadable } : {}),
@@ -689,7 +690,7 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
689
690
  let pendingGrant;
690
691
  const grantOnCommit = remember && deps.approvalExemptionStore
691
692
  ? async (grant) => {
692
- const canonical = grant.validatedToolName;
693
+ const canonical = grant.rememberableToolName;
693
694
  if (!canonical)
694
695
  return;
695
696
  pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
@@ -1,5 +1,5 @@
1
1
  import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
2
- import { isModelAllowlisted } from "../../model-select.js";
2
+ import { isModelAllowlisted, selectableCatalog } from "../../model-select.js";
3
3
  import { resolveModelApiKey } from "../../key-resolver.js";
4
4
  import { isSealedKeyPoison } from "../../sealed-key.js";
5
5
  import { cwdHonored, deviceCwdHonored } from "../../task-cwd.js";
@@ -228,7 +228,7 @@ export async function handleCapabilities(req, res, match, ctx) {
228
228
  return;
229
229
  }
230
230
  case "models": {
231
- const atCatalog = expandTiers(deps.config.models, deps.config.tiers ?? {}) ?? deps.config.models;
231
+ const atCatalog = selectableCatalog(deps.config.models, deps.config.tiers, deps.config.modelRefusals);
232
232
  const atAllowlist = deps.config.atModelAllowlist ?? [];
233
233
  const models = Object.entries(deps.config.models)
234
234
  .filter(([name]) => name !== "default")
@@ -242,7 +242,7 @@ export async function handleCapabilities(req, res, match, ctx) {
242
242
  ...(m.maxTokens ? { maxOutputTokens: m.maxTokens } : {}),
243
243
  ...((levels) => (levels !== undefined ? { supportedEffortLevels: [...levels] } : {}))(reasoningEffortLevelsOf(m)),
244
244
  ...(m.compat !== undefined ? { compat: m.compat } : {}),
245
- ...(atAllowlist.length > 0 ? { atMentionable: isModelAllowlisted(name, atCatalog, atAllowlist) } : {}),
245
+ ...(atAllowlist.length > 0 ? { atMentionable: isModelAllowlisted(name, atCatalog, atAllowlist, deps.config.modelRefusals) } : {}),
246
246
  routePairing: routePairingOf(deps, name, m),
247
247
  }));
248
248
  const nonAlias = Object.entries(deps.config.models).filter(([n]) => n !== "default");
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { sendJson, sendError } from "../send.js";
3
+ import { sentMemoryStoreRefusal } from "../memory-store-refusal-reply.js";
3
4
  import { buildOriginFencedBundle, readOriginAware } from "../../memory-origin-fence.js";
4
5
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
5
6
  import { assertNever } from "../route-ctx.js";
@@ -35,6 +36,8 @@ function sentClassifiedFailure(res, ctx, face, err) {
35
36
  const code = codeOf(err);
36
37
  if (code !== undefined && sentCoreCode(res, code, err instanceof Error ? err.message : String(err)))
37
38
  return true;
39
+ if (sentMemoryStoreRefusal(res, err))
40
+ return true;
38
41
  ctx.deps.logger?.warn?.(`memory_bundle_${face}_failed`, { err: String(err) });
39
42
  return false;
40
43
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { ControlPlaneCorruptError } from "@sema-agent/core";
3
3
  import { erasureInputSmugglesPrototypeKey } from "../../memory-operator-faces.js";
4
+ import { sentMemoryStoreRefusal } from "../memory-store-refusal-reply.js";
4
5
  import { sendJson, sendError } from "../send.js";
5
6
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
7
  import { assertNever } from "../route-ctx.js";
@@ -38,6 +39,8 @@ function sentClassifiedFailure(res, ctx, face, err) {
38
39
  const coreCode = codeOf(err);
39
40
  if (coreCode !== undefined && sentCoreCode(res, coreCode, err instanceof Error ? err.message : String(err)))
40
41
  return true;
42
+ if (sentMemoryStoreRefusal(res, err))
43
+ return true;
41
44
  if (err instanceof ControlPlaneCorruptError) {
42
45
  sendError(res, 500, "internal.memory_control_plane_corrupt", err.message);
43
46
  return true;
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { ControlPlaneCorruptError } from "@sema-agent/core";
3
3
  import { sendJson, sendError } from "../send.js";
4
+ import { sentMemoryStoreRefusal } from "../memory-store-refusal-reply.js";
4
5
  import { buildOriginFence, readOriginAware } from "../../memory-origin-fence.js";
5
6
  import { gatedPrincipal, explicitOperatorOk, sendPrincipalRefusal } from "../principal-gate.js";
6
7
  import { assertNever } from "../route-ctx.js";
@@ -52,6 +53,8 @@ function sentClassifiedFailure(res, ctx, face, err) {
52
53
  if (coreCode !== undefined && sentClearCode(res, coreCode, err instanceof Error ? err.message : String(err)))
53
54
  return true;
54
55
  }
56
+ if (face !== "clearances" && sentMemoryStoreRefusal(res, err))
57
+ return true;
55
58
  if (err instanceof ControlPlaneCorruptError) {
56
59
  sendError(res, 500, "internal.memory_control_plane_corrupt", err.message);
57
60
  return true;
@@ -4,6 +4,7 @@ import { isUuidV7, isUuidShape, legacyUnownedSessionOk } from "../../security.js
4
4
  import { windowMessages, truncateMessageBlobs } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
6
  import { recordFailOpen } from "../../observability/fail-open.js";
7
+ import { targetOutsideStoreOf } from "../../memory-store-refusal.js";
7
8
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
8
9
  import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
9
10
  import { sendJson, sendError, sendSessionPolicyError, sseHeaders } from "../send.js";
@@ -504,6 +505,12 @@ export async function handleSessions(req, res, match, ctx) {
504
505
  sendError(res, 400, "config.memory_erasure_request", err instanceof Error ? err.message : "invalid memory erasure request");
505
506
  return;
506
507
  }
508
+ const outside = targetOutsideStoreOf(err);
509
+ if (outside !== undefined) {
510
+ deps.logger?.warn?.("session_memory_erase_refused", { sessionId, err: outside.message });
511
+ sendError(res, 409, "memory.target_outside_store", "session memory erasure refused — a directory in this deployment's memory store leads outside it (a link), so this erasure did not complete (a refusal before its commit point changed nothing; one after it keeps the deletions already made and a journal that finishes the rest once the link is removed); ask the operator to remove the link, then resend with the same requestId");
512
+ return;
513
+ }
507
514
  deps.logger?.warn?.("session_memory_erase_failed", { sessionId, err: String(err) });
508
515
  sendError(res, 500, "internal.error", "session memory erasure failed");
509
516
  }
@@ -1,6 +1,7 @@
1
1
  import { expandTiers, isThinkingLevel } from "@sema-agent/core";
2
2
  import { HttpError } from "../../security.js";
3
- import { isModelAllowlisted } from "../../model-select.js";
3
+ import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../../model-select.js";
4
+ import { modelUnavailableMessage } from "../../model-entry-refusal.js";
4
5
  import { redactSecrets } from "../../trace/redact.js";
5
6
  import { sendJson, sendError, httpErrorCode, msg } from "../send.js";
6
7
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
@@ -68,9 +69,14 @@ export async function handleSideQuery(req, res, match, ctx) {
68
69
  return;
69
70
  }
70
71
  if (typeof body.model === "string" && body.model.length > 0) {
71
- const bare = deps.config.models ?? {};
72
- const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
73
- if (!isModelAllowlisted(body.model, catalog, deps.config.atModelAllowlist)) {
72
+ const catalog = selectableCatalog(deps.config.models ?? {}, deps.config.tiers, deps.config.modelRefusals);
73
+ const unavailable = matchCatalogModel(body.model, catalog, deps.config.modelRefusals) === undefined ? refusedCatalogEntry(body.model, deps.config.modelRefusals) : undefined;
74
+ if (unavailable !== undefined) {
75
+ const message = modelUnavailableMessage("model", body.model.slice(0, 120), unavailable.name, unavailable.refusal);
76
+ sendError(res, 400, "request.model_unavailable", message);
77
+ return;
78
+ }
79
+ if (!isModelAllowlisted(body.model, catalog, deps.config.atModelAllowlist, deps.config.modelRefusals)) {
74
80
  sendError(res, 400, "request.model_not_allowed", `model is not in this deployment's @-mention allowlist: "${body.model.slice(0, 120)}" — the allowlist is configured by the operator (config-center models.atModelAllowlist)`);
75
81
  return;
76
82
  }
@@ -1,12 +1,12 @@
1
1
  import { redactErrorMessage, redactHead, redactTerminalResult } from "../../observability/run-terminal.js";
2
- import { runWithVerification, runCascade, uuidv7 } from "@sema-agent/core";
2
+ import { runWithVerification, runCascade, taskNotificationDedupKey, uuidv7 } from "@sema-agent/core";
3
3
  import { graftParkToolCallId, markChildrenStoppedByUserOnAbort, resumeAtHttpStatus, HEARTBEAT_MS } from "../../runs.js";
4
4
  import { sealResultForExit, outwardPlane } from "../../terminal.js";
5
5
  import { withPrincipal } from "../../observability/principal-context.js";
6
6
  import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../../fleet/fleet-bus.js";
7
7
  import { defaultSubagentTailBus } from "../../fleet/subagent-tail-bus.js";
8
8
  import { createReasoningSegments, reasoningStreamOfTask } from "../../trace/model-segment.js";
9
- import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
9
+ import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKeyOf, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
10
10
  import { createLedgerSink } from "../../trace/ledger-sink.js";
11
11
  import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
12
12
  import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
@@ -393,7 +393,7 @@ export async function handleTasks(req, res, match, ctx) {
393
393
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(prepared.spec.sessionId, askOwner, n, Date.now(), durableTaskId, opts?.priority)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "sync-stream", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
394
394
  }
395
395
  else if (deliverable) {
396
- const key = taskNotificationStreamKey(n);
396
+ const key = taskNotificationDedupKey(n);
397
397
  if (syncNotifiedKeys.get(key) === undefined) {
398
398
  syncNotifiedKeys.set(key, Promise.resolve(true));
399
399
  sseData(res, { type: "task_notification", ...taskNotificationEventData({ notification: n, ...(opts?.priority !== undefined ? { priority: opts.priority } : {}) }) });
@@ -465,7 +465,7 @@ export async function handleTasks(req, res, match, ctx) {
465
465
  }
466
466
  else if (ev.type === "task_notification") {
467
467
  const tnv = ev.notification;
468
- const tnvKey = tnv?.task_id ? taskNotificationStreamKey({ task_type: tnv.task_type, task_id: tnv.task_id, status: String(tnv.status), seq: tnv.seq }) : undefined;
468
+ const tnvKey = taskNotificationStreamKeyOf(tnv);
469
469
  const tnvPrior = tnvKey !== undefined ? syncNotifiedKeys.get(tnvKey) : undefined;
470
470
  if (tnvPrior === undefined || !(await tnvPrior)) {
471
471
  if (tnvKey !== undefined)
@@ -553,8 +553,9 @@ export async function handleTasks(req, res, match, ctx) {
553
553
  if (closed || res.writableEnded || res.destroyed)
554
554
  throw new Error("stream ended before the completion frame was written");
555
555
  const f = frame;
556
- if (f.type === "task_notification" && f.task_id)
557
- syncNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), Promise.resolve(true));
556
+ const fKey = f.type === "task_notification" ? taskNotificationStreamKeyOf(f) : undefined;
557
+ if (fKey !== undefined)
558
+ syncNotifiedKeys.set(fKey, Promise.resolve(true));
558
559
  sseData(res, frame);
559
560
  }, { route: "sync-stream-open", connection: "live-sse", log: (m, x) => deps.logger?.info?.(m, x) });
560
561
  const withElicit = deps.elicitation
@@ -659,7 +659,7 @@ export interface ServiceDeploymentDeps {
659
659
  * registry 部署),现有部署零影响。main.ts 在 applyEffective 落模型后翻 true(one-way latch)。 */
660
660
  modelReady?: () => boolean;
661
661
  /** Sema-registry restart signal: the refresh loop sets this when a pull carries restart-to-apply
662
- * changes (skills/mcp/scenarios/runtime-gates baked at boot) that DIFFER from this process's boot config.
662
+ * changes (skills/mcp/a2a/scenarios/… baked at boot — the `RestartSlice` closed set) that DIFFER from this process's boot config.
663
663
  * Surfaced at GET /health as `restartRequired` + `restart{reasons,version,since}` so the orchestrator can
664
664
  * auto rolling-restart the worker. Change-detected (not presence) — absent when nothing baked-at-boot
665
665
  * changed, so polling it never causes a restart loop. */
@@ -686,10 +686,11 @@ export interface ServiceDeploymentDeps {
686
686
  mcpRevocationState?: () => {
687
687
  revoked: string[];
688
688
  } | undefined;
689
- /** codex R18 (models-tiers deferral): the persistent "deferred WITHOUT a next-boot handoff" state — set when a
690
- * changed model plane could not hot-apply (tier-frozen Runner / tiered candidate) AND no restart-surviving LKG
691
- * exists to justify forcing a restart. Surfaced on /health (`modelPlaneDeferred`) so the stuck-degraded state
692
- * outlives the one-shot warn (the etag advances → later ticks 304 and never re-enter the defer branch). */
689
+ /** codex R18: the persistent "retained WITHOUT a next-boot handoff" state — a candidate whose restart signal is
690
+ * withheld because no restart-surviving handoff exists yet (durable-declared LKG publication incomplete, or a
691
+ * boot skills debt not yet proven repairable). Surfaced on /health (`modelPlaneDeferred` — the key keeps its birth
692
+ * name: it was born for the model-plane deferral, which retired with the `models-tiers` slice in S-668 波 0) so
693
+ * the stuck state outlives the one-shot warn (the etag advances → later ticks 304). */
693
694
  planeDeferredState?: () => {
694
695
  version: number;
695
696
  since: number;
@@ -450,9 +450,12 @@ export interface TaskRequestBody {
450
450
  /** The caller's per-request settings stamp (the client-side `SemaSettings` shape). The KEY SET is deliberately
451
451
  * NOT mirrored here — a hand-written second shape on this interface would be the classic mirror-drift form.
452
452
  * Per-key ownership is split, not single: `parseTaskSettings` (src/task-settings.ts) defensively owns the
453
- * shared settings keys on every path, while `settings.webSearch` has its OWN validator on the single-user
454
- * scenario lane (capabilities/scenarios.ts `webSearchConfigFromSettings` — the multi-tenant lane ignores it
455
- * by design). The declared type is the INTENDED caller form (sibling convention: `oneShot` declares boolean,
453
+ * shared settings keys on every path, while `settings.webSearch` has its OWN judge (plugins/web-search.ts
454
+ * `judgeRequestWebSearch`, S-199): a malformed block — including a missing or unknown `provider` — 400s on submit
455
+ * on every lane, and only the single-user
456
+ * scenario lane honors a well-formed one (capabilities/scenarios.ts — the multi-tenant lane ignores it by design;
457
+ * a malformed block replayed on resume mounts no WebSearch for that call — never a substitute destination — with a
458
+ * fail-open count). The declared type is the INTENDED caller form (sibling convention: `oneShot` declares boolean,
456
459
  * runtime drops garbage): a non-array object bag, or omit the key. A `null` reads as absence on both the
457
460
  * fresh-submit wall (prepareSpec) and replay (parseTaskSettings), and malformed forms 400 on submit /
458
461
  * no-op on replay. ([ref] 件1 leftover key, declared 2026-08-31; ownership wording per codex adversarial round.)
@@ -0,0 +1,97 @@
1
+ /**
2
+ * S-690 —— 项目指令文件**换源**通告(`engine_notice` 码 `instructions.source_changed`)的唯一铸点与基线属主口。
3
+ *
4
+ * ## 这件事为什么归本仓
5
+ * 选哪一份指令文件是**部署的内容半场**(`project-memory.ts` 的 `INSTRUCTION_FILE_CHAIN`;core 的 `loadProjectMemory`
6
+ * 契约逐字「core … NEVER hardcodes a filename」),所以「这一 run 读的是哪一份、上一 run 读的是哪一份」也只有本仓
7
+ * 答得出。core 的 run 内变更车道(`steering_injected`,`source: "instructions_change"`)只跟**本 run 自己声明的**
8
+ * 来源比,进程重启(= 升级)后没有任何东西记得上一 run 注入的是哪一份 —— 于是两文件并存的仓升级后静默换源
9
+ * (车MN 收车档 §1.5,判据 G6)。
10
+ *
11
+ * ## 基线 = 会话的服务端元数据
12
+ * 基线是「同一 session 上一次 run 选中的链成员相对路径」(不是指纹:指纹密钥每进程随机,跨进程不可比)。它住在会话店的
13
+ * 服务端元数据面 —— SQL 两孪生是 `session_meta.instruction_source` 一列,local 文件店是一只 sidecar —— 所以重启 /
14
+ * 跨副本都读得到、随会话删除一起消失,且不进模型上下文、不进 wire 会话面。不接这张面的店(core 的内存 dev 店、
15
+ * `run-local` 的 core 文件店)⇒ 席缺席 ⇒ 一帧不发(诚实缺席:那两形的会话本来就活不过进程,或没有会话流可投)。
16
+ *
17
+ * ## 铸点(唯一)
18
+ * {@link announceInstructionSource} 是本码**唯一**的铸点(`@contract instructions.source_changed.single_mint`)。
19
+ * 调用点只有一处:`makeLoadProjectMemory` 在每次**走了指令链**的 prepare 载入完成之后(core 每个 run 恰调一次 `loadProjectMemory`)
20
+ * ⇒ 一 run 至多一帧。走链之前就判「这次没有项目可读」的 prepare(无手场景、cwd 不是本机绝对目录)是**不适用**,不是
21
+ * 「来源 = 无」—— 不进本铸点,基线保持上一次真读的那份(O6,7.101.0 合并复审:无手孪生 Runner 与主 Runner 共用同一只
22
+ * loader 与会话店,把不适用折成 `null` 会让「默认 → 无手 → 默认」同 session 两跳无端发两帧)。分工照 `task-mcp.ts` `mintInjectionDropNotices` 的先例:本模块铸通告,`emit` 由装配方给
23
+ * (部署的 `onNotice` 转发体 —— 先日志后分流,白名单 / 归属 / 消毒 / 隔离四件判据一个不绕)。
24
+ *
25
+ * ## 不做的事
26
+ * 不区分「升级换源」与「用户改了文件」(跨进程指纹不可比;载荷只说前后两个来源)。detail 没有 `runId`:铸点在 core 的
27
+ * `loadProjectMemory` 席内,该席的 ctx 不带 runId(core 7.30.1 键集 = cwd / handsEnabled / isSubagent / agentName /
28
+ * sessionId / phase)—— 与 `mcp.injection_dropped`「装配期铸、无 runId」同形,run 归属由投递腿承载(这条 run 的 SSE /
29
+ * 这条 run 的账本)。向 core 要 ctx 加 runId 已登记为未闭环项。
30
+ */
31
+ import type { EngineNotice } from "@sema-agent/core";
32
+ import type { ServerNoticeCode } from "./trace/engine-notice-wire.js";
33
+ export declare const INSTRUCTION_SOURCE_CHANGED: ServerNoticeCode;
34
+ /** 基线读数。`recorded: false` = 这个会话还没有任何 run 记过基线(首 run ⇒ 只写不发);`source: null` = 记过,且那一次
35
+ * 没有项目指令文件被选中。两态必须可判别:把「没记过」读成「记过没有」会让新会话首 run 发一帧 `null → CLAUDE.md`。 */
36
+ export type InstructionSourceBaseline = {
37
+ readonly recorded: false;
38
+ } | {
39
+ readonly recorded: true;
40
+ readonly source: string | null;
41
+ };
42
+ /** 基线属主口(会话店的服务端元数据面)。 */
43
+ export interface InstructionSourceStore {
44
+ readInstructionSource(sessionId: string): Promise<InstructionSourceBaseline>;
45
+ /**
46
+ * 覆写基线 —— **CAS**:只在店里此刻的基线仍等于 `expected`(调用方读到的那一份)时写,否则什么都不做(不抛)。
47
+ * 为什么是 CAS(codex che-NM r1 [high]):铸点对基线读写有期限,超期的写会**晚到**;不带期望值的覆写会让一个
48
+ * 基于过期读数的旧 run 盖掉更新的 run 写下的基线,下一 run 于是比错。会话行不存在 ⇒ 同样不写。
49
+ * 写**没有提交**必须抛(SQL 的写本来就如此;local sidecar 回滚内存再抛)—— 「内存已前进而盘没跟上」会让同来源的
50
+ * 后续 run 在相等比较上短路、再也不重写,重启后比的是一份过期的盘(codex che-NM r1 [medium])。
51
+ */
52
+ recordInstructionSource(sessionId: string, expected: InstructionSourceBaseline, source: string | null): Promise<void>;
53
+ }
54
+ /** 一只装配好的席:基线店 + 通告出口。 */
55
+ export interface InstructionSourceSeat {
56
+ readonly store: InstructionSourceStore;
57
+ readonly emit: (notice: EngineNotice) => void;
58
+ }
59
+ /**
60
+ * 基线的**存储编码**(SQL 列与 local sidecar 同一只):「没有项目指令文件」存成空串,链成员存它的相对路径;
61
+ * 「没记过」是 SQL `NULL` / sidecar 里缺键。空串不会与任何文件名撞(没有叫空名的文件),所以不需要保留词。
62
+ */
63
+ export declare function encodeInstructionSource(source: string | null): string;
64
+ /** 两份基线读数是否同一(CAS 的判等;三店与测试夹具同用这一只)。 */
65
+ export declare function sameInstructionSourceBaseline(a: InstructionSourceBaseline, b: InstructionSourceBaseline): boolean;
66
+ /** {@link encodeInstructionSource} 的逆。非串非空的值 = 这一格被本仓以外的东西写坏了 ⇒ 抛(调用方按基线读不出处置)。 */
67
+ export declare function decodeInstructionSource(raw: unknown): InstructionSourceBaseline;
68
+ /**
69
+ * 会话店是否提供基线面 —— **两个方法都在**才算(只有一半的店让读写不同源,比整张缺席更坏)。结构判而不是按类名判
70
+ * (包装店 `CachingSessionStore` 逐方法转发,判据看的是能力不是名字)。
71
+ */
72
+ export declare function instructionSourceStoreOf(sessionStore: unknown): InstructionSourceStore | undefined;
73
+ /** 装配点的唯一口(server boot 与 run-local 两处同走):店没有基线面或没有通告出口 ⇒ 席缺席(一帧不发)。 */
74
+ export declare function createInstructionSourceSeat(sessionStore: unknown, emit: ((notice: EngineNotice) => void) | undefined): InstructionSourceSeat | undefined;
75
+ /**
76
+ * 基线每一次读 / 写的期限(codex che-NM r1 [high])。铸点跑在 prepare 里、loader 等它交出答案才返回,而两支 SQL 孪生的
77
+ * 查询默认没有超时(MySQL 无、PG `statement_timeout` 缺省关)—— 一次卡住的行锁 / 连接就会把整个 prepare 挂住,而修前
78
+ * loader 根本不碰库。两层同一个数:① 这里放手(超期 ⇒ 按读 / 写失败处置,留痕放行);② 两支 SQL 孪生的基线查询带**每查询
79
+ * 超时**(codex che-NM r2 [high];S-9 `write-behind-counter.ts` 先例:mysql2 `{sql, timeout}` / pg `query_timeout`,pg-pool
80
+ * 超时即销毁那条连接)—— 只放手不截断,卡住的查询会一直占着池里的连接。超期后仍在飞的写由 CAS 挡住,不会盖掉更新的基线。
81
+ * 数值与 core 给 `probeInstructionSources` 的 2s 天花板同量级:一行 PK 读写在健康库上是毫秒级。
82
+ */
83
+ export declare const BASELINE_IO_DEADLINE_MS = 2000;
84
+ /**
85
+ * **唯一铸点**:本次 prepare 选中的来源 `current`(链成员相对路径;没有 = `null`)与基线比。
86
+ * · 没有基线 ⇒ 只写不发(新会话首 run);
87
+ * · 相同 ⇒ 什么都不做(不写);
88
+ * · 不同 ⇒ 铸一帧、再以读到的基线为期望值 CAS 覆写。
89
+ * 基线读 / 写各有 {@link BASELINE_IO_DEADLINE_MS} 的期限。
90
+ *
91
+ * 失败方向(F 类,`server.instruction-source.announce-degraded`,detail 带 `op=read|write|emit`):通告是**披露**面
92
+ * 不是执法面,所以基线读 / 写 / 出口失败一律留痕后放行,**绝不**抛回调用方 —— 抛回去会让 core 把这次 prepare 的项目记忆整块丢掉(loader 抛 ⇒ 不注入),
93
+ * 为一条通告赔上模型要读的指令本身。读失败 ⇒ 本 run 不比也不写;写失败 ⇒ 本 run 的帧照发(它对本 run 为真),代价是
94
+ * 下一 run 可能再说一遍同一次换源;出口抛(转发体今天结构上不抛,守的是「本函数不抛」这条契约)⇒ 本 run 缺这一帧。
95
+ */
96
+ export declare function announceInstructionSource(seat: InstructionSourceSeat, sessionId: string, current: string | null): Promise<void>;
97
+ //# sourceMappingURL=instruction-source-notice.d.ts.map