@sema-agent/server 7.116.1 → 7.118.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 (131) hide show
  1. package/MIGRATION.md +195 -0
  2. package/USAGE.md +65 -41
  3. package/dist/approval-card.d.ts +82 -18
  4. package/dist/approval-card.js +46 -4
  5. package/dist/approval.d.ts +4 -12
  6. package/dist/approval.js +4 -26
  7. package/dist/boot/budget-tracing.js +2 -3
  8. package/dist/boot/config-center.d.ts +2 -2
  9. package/dist/boot/config-center.js +5 -7
  10. package/dist/boot/parked-revive-gate.d.ts +1 -7
  11. package/dist/boot/parked-revive-gate.js +2 -12
  12. package/dist/boot/resolve-spec.js +18 -16
  13. package/dist/boot/runner-deps.d.ts +5 -3
  14. package/dist/boot/runner-deps.js +6 -4
  15. package/dist/boot/stage-07-capability-layer.js +2 -2
  16. package/dist/brain.d.ts +17 -8
  17. package/dist/brain.js +10 -7
  18. package/dist/capabilities/hands-lane.d.ts +25 -0
  19. package/dist/capabilities/hands-lane.js +1 -1
  20. package/dist/capabilities/mcp-probe.d.ts +2 -2
  21. package/dist/config-catalog.js +3 -2
  22. package/dist/config-center/apply-effective.d.ts +6 -16
  23. package/dist/config-center/apply-effective.js +0 -6
  24. package/dist/config-center/facade.d.ts +1 -1
  25. package/dist/config-center/facade.js +1 -1
  26. package/dist/config-center/restart-signal.d.ts +1 -1
  27. package/dist/config-types.d.ts +12 -16
  28. package/dist/config.d.ts +32 -29
  29. package/dist/config.js +20 -27
  30. package/dist/deployment-governance.d.ts +13 -19
  31. package/dist/deployment-governance.js +7 -14
  32. package/dist/governance-overrides.d.ts +20 -0
  33. package/dist/governance-overrides.js +8 -0
  34. package/dist/host-decision.d.ts +83 -25
  35. package/dist/host-decision.js +31 -1
  36. package/dist/http/admission.js +13 -7
  37. package/dist/http/approval-sweeps.js +16 -13
  38. package/dist/http/checkpoint-ctx.d.ts +83 -24
  39. package/dist/http/checkpoint-ctx.js +68 -8
  40. package/dist/http/mcp-dial-deadline.d.ts +35 -0
  41. package/dist/http/mcp-dial-deadline.js +21 -0
  42. package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
  43. package/dist/http/memory-compliance-failure-reply.js +17 -8
  44. package/dist/http/memory-store-refusal-reply.d.ts +14 -11
  45. package/dist/http/memory-store-refusal-reply.js +16 -5
  46. package/dist/http/resume-legs.js +18 -18
  47. package/dist/http/retired-skill-manifest.d.ts +1 -1
  48. package/dist/http/route-ctx.d.ts +3 -3
  49. package/dist/http/routes/approvals-assistant.js +21 -24
  50. package/dist/http/routes/capabilities.js +5 -3
  51. package/dist/http/routes/mcp-reconnect.d.ts +17 -0
  52. package/dist/http/routes/mcp-reconnect.js +6 -1
  53. package/dist/http/routes/memory-bundle.js +1 -1
  54. package/dist/http/routes/memory-compliance.js +1 -1
  55. package/dist/http/routes/memory-origin.js +1 -1
  56. package/dist/http/routes/runs.js +53 -33
  57. package/dist/http/routes/sessions.d.ts +1 -1
  58. package/dist/http/routes/sessions.js +3 -6
  59. package/dist/http/routes/workflows.js +2 -0
  60. package/dist/http/wire-types.d.ts +6 -1
  61. package/dist/memory-store-refusal.d.ts +42 -17
  62. package/dist/memory-store-refusal.js +25 -6
  63. package/dist/model-select.d.ts +16 -5
  64. package/dist/model-select.js +7 -3
  65. package/dist/model-unreachable-notice.d.ts +1 -1
  66. package/dist/namespaced-rule-name.d.ts +45 -39
  67. package/dist/namespaced-rule-name.js +36 -22
  68. package/dist/observability/corrupt-read-seat.d.ts +4 -0
  69. package/dist/observability/corrupt-read-seat.js +1 -0
  70. package/dist/observability/fail-open.d.ts +15 -3
  71. package/dist/observability/fail-open.js +15 -3
  72. package/dist/observability/permission-rule-events.d.ts +3 -0
  73. package/dist/observability/permission-rule-events.js +4 -0
  74. package/dist/observability/run-terminal.js +21 -1
  75. package/dist/operator-ask.d.ts +4 -1
  76. package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
  77. package/dist/orchestration/workflow-agent-session-index.js +80 -14
  78. package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
  79. package/dist/orchestration/workflow-completion-inbox.js +39 -17
  80. package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
  81. package/dist/orchestration/workflow-notify-journal.js +7 -2
  82. package/dist/outbound-dispatcher.d.ts +47 -20
  83. package/dist/outbound-dispatcher.js +198 -23
  84. package/dist/parked-decide.d.ts +43 -48
  85. package/dist/parked-decide.js +54 -16
  86. package/dist/permission-rule-list.d.ts +20 -31
  87. package/dist/permission-rule-vocab.d.ts +4 -3
  88. package/dist/permission-rule-vocab.js +1 -1
  89. package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
  90. package/dist/plugins/approval-ask-store-sql.js +0 -3
  91. package/dist/plugins/one-time-migrations.d.ts +25 -0
  92. package/dist/plugins/one-time-migrations.js +192 -6
  93. package/dist/plugins/pg-pool.js +1 -1
  94. package/dist/plugins/tidb-pool.js +1 -1
  95. package/dist/plugins/web-search.d.ts +20 -1
  96. package/dist/plugins/web-search.js +39 -10
  97. package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
  98. package/dist/plugins/workflow-run-store-sql.js +5 -7
  99. package/dist/request-key-closure.js +1 -1
  100. package/dist/request-value-closure.d.ts +1 -1
  101. package/dist/router/route-orchestration.d.ts +2 -23
  102. package/dist/rule-entry-list.d.ts +88 -0
  103. package/dist/rule-entry-list.js +102 -0
  104. package/dist/rules-consent.js +3 -0
  105. package/dist/run-local.d.ts +4 -2
  106. package/dist/run-local.js +4 -3
  107. package/dist/runs.d.ts +1 -1
  108. package/dist/runs.js +1 -0
  109. package/dist/runtime-governance.d.ts +5 -21
  110. package/dist/runtime-governance.js +4 -8
  111. package/dist/session-policy-wire.d.ts +13 -12
  112. package/dist/session-policy-wire.js +11 -6
  113. package/dist/spec-fields.d.ts +13 -24
  114. package/dist/spec-fields.js +13 -17
  115. package/dist/standing-answer-grant.d.ts +14 -2
  116. package/dist/standing-answer-grant.js +8 -1
  117. package/dist/task-settings.d.ts +18 -18
  118. package/dist/task-settings.js +63 -32
  119. package/dist/tool-approval.d.ts +77 -145
  120. package/dist/tool-approval.js +80 -62
  121. package/dist/trace/core-keyset-guard.d.ts +6 -6
  122. package/dist/trace/engine-notice-wire.d.ts +4 -6
  123. package/dist/trace/engine-notice-wire.js +14 -1
  124. package/dist/trace/self-minted-wire.d.ts +16 -2
  125. package/dist/trace/self-minted-wire.js +7 -0
  126. package/dist/user-input-ledger.d.ts +24 -12
  127. package/dist/user-input-ledger.js +33 -14
  128. package/package.json +2 -2
  129. package/scripts/drain-check-local.mjs +103 -7
  130. package/scripts/drain-check.mysql.sql +59 -8
  131. package/scripts/drain-check.pg.sql +66 -10
@@ -1,4 +1,4 @@
1
- import { isThinkingLevel, expandTiers } from "@sema-agent/core";
1
+ import { APPROVAL_REACH, MCP_IMAGE_MAX_BASE64, isApprovalReach, isThinkingLevel, expandTiers } from "@sema-agent/core";
2
2
  import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
3
3
  import { modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { HttpError } from "../security.js";
@@ -8,12 +8,13 @@ import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEn
8
8
  import * as facts from "../capability-facts.js";
9
9
  import { buildDeadToolMatcherRefusal, deadToolMatcherIssue, parseHooksConfig } from "../hooks/hook-runner.js";
10
10
  import { judgeRequestWebSearch } from "../plugins/web-search.js";
11
- import { describeUnreachableSkillToolListEntry, normalizeApproachNotice, skillToolListOk, unknownTaskAgentModelRef, unreachableSkillToolListEntryOf, validateTaskAgents } from "../spec-fields.js";
11
+ import { normalizeApproachNotice, skillToolListOk, skillToolListRefusal, unknownTaskAgentModelRef, validateTaskAgents } from "../spec-fields.js";
12
12
  import { isValidCwd, MAX_ADDITIONAL_DIRS, MAX_CWD_CHARS } from "../task-cwd.js";
13
13
  import { verifyRoundsFromBody } from "./verify-rounds.js";
14
14
  import { skillCarriesRetiredManifest } from "./retired-skill-manifest.js";
15
15
  import { resolveDevicePlacementIntent } from "./device-placement.js";
16
16
  import { sendError, httpErrorCode } from "./send.js";
17
+ const APPROVAL_REACH_WORDS = APPROVAL_REACH.map((word) => JSON.stringify(word)).join(" / ");
17
18
  export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
18
19
  export const MAX_SYSTEM_PROMPT_CHARS = 16_384;
19
20
  export const MAX_OUTPUT_SCHEMA_CHARS = 32_768;
@@ -39,15 +40,15 @@ export function validateUserSkills(skills) {
39
40
  if (s[key] !== undefined && !skillToolListOk(s[key]))
40
41
  return `skill.${key} must be an array of strings (permission-rule spellings: a tool name, mcp__<server> / mcp__<server>__*, Bash(<prefix>:*), Read(//<dir>/**))`;
41
42
  }
42
- const unreachable = unreachableSkillToolListEntryOf(s);
43
- if (unreachable !== undefined)
44
- return describeUnreachableSkillToolListEntry(unreachable);
43
+ const listRefusal = skillToolListRefusal(s);
44
+ if (listRefusal !== undefined)
45
+ return listRefusal;
45
46
  }
46
47
  return null;
47
48
  }
48
49
  export const SKILL_MANIFEST_RETIRED = "skill.manifest was retired in core 7.41.0 — a skill's tools are its allowedTools / disallowedTools now (an allow-rule and a deny-rule source for the run the skill is activated in, never a fence); move manifest.allowTools to allowedTools. To fence tools use deny rules (settings.permissions.deny) or a delegated agent's allowTools; to fence write paths use the session rules' allowDirs";
49
50
  const MAX_IMAGES_PER_REQUEST = 20;
50
- const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
51
+ const MAX_IMAGE_BASE64_BYTES = MCP_IMAGE_MAX_BASE64;
51
52
  function mentionableNames(config, catalog) {
52
53
  const allowlist = config.atModelAllowlist ?? [];
53
54
  return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist, config.modelRefusals));
@@ -238,7 +239,7 @@ export function createAdmission(ctx) {
238
239
  for (const img of body.images) {
239
240
  const data = img.data;
240
241
  if (typeof data === "string" && Buffer.byteLength(data, "utf8") > MAX_IMAGE_BASE64_BYTES) {
241
- sendError(res, 413, "request.payload_too_large", `each inline image must be at most ${MAX_IMAGE_BASE64_BYTES} base64 bytes`);
242
+ sendError(res, 413, "request.payload_too_large", `each inline image must be at most ${MAX_IMAGE_BASE64_BYTES} base64 bytes (the engine's inline-image limit — the model API's 5 MB image cap)`);
242
243
  return null;
243
244
  }
244
245
  }
@@ -470,6 +471,11 @@ export function createAdmission(ctx) {
470
471
  sendError(res, 400, "request.field_invalid", "interactiveTools must be a boolean");
471
472
  return null;
472
473
  }
474
+ const approvalReach = body.approvalReach;
475
+ if (approvalReach !== undefined && !isApprovalReach(approvalReach)) {
476
+ sendError(res, 400, "request.field_invalid", `approvalReach must be ${APPROVAL_REACH_WORDS} when present (the run declares that nobody on it can be asked to approve; omit the key otherwise)`);
477
+ return null;
478
+ }
473
479
  if (body.oneShot !== undefined && typeof body.oneShot !== "boolean") {
474
480
  sendError(res, 400, "request.field_invalid", "oneShot must be a boolean");
475
481
  return null;
@@ -8,7 +8,7 @@ import { hasServiceAuth } from "./dispatch.js";
8
8
  import { gatedPrincipal } from "./principal-gate.js";
9
9
  import { readinessOf, billableRefusalOf } from "./readiness.js";
10
10
  import { readResumeCtx } from "./resume-ctx-read.js";
11
- import { allowListReadingUnstated } from "./checkpoint-ctx.js";
11
+ import { storedBodyReplayRefusal } from "./checkpoint-ctx.js";
12
12
  const BLOCKED_SCALAR_KEYS = ["errorCode", "blockedBy", "field", "reason", "count", "cap", "index"];
13
13
  function blockedScalars(body) {
14
14
  const out = {};
@@ -66,21 +66,24 @@ export function createApprovalSweeps(ctx) {
66
66
  return false;
67
67
  }
68
68
  };
69
- const allowListUnservable = async (sessionId) => {
69
+ const storedBodyUnservable = async (sessionId) => {
70
70
  const read = await readResumeCtx(cs.getCtx(sessionId));
71
71
  if (read.kind === "corrupt")
72
- return false;
73
- return read.kind === "ok" && allowListReadingUnstated(read.ctx);
72
+ return undefined;
73
+ if (read.kind === "absent")
74
+ return undefined;
75
+ return storedBodyReplayRefusal(read.ctx);
74
76
  };
75
77
  let parkedSkipped = 0;
76
- const allowListSkipped = [];
78
+ const replaySkipped = [];
77
79
  for (const { sessionId } of expired) {
78
80
  if (await isParkedOwned(sessionId)) {
79
81
  parkedSkipped += 1;
80
82
  continue;
81
83
  }
82
- if (await allowListUnservable(sessionId)) {
83
- allowListSkipped.push(sessionId);
84
+ const unservable = await storedBodyUnservable(sessionId);
85
+ if (unservable !== undefined) {
86
+ replaySkipped.push({ sessionId, kind: unservable.kind });
84
87
  continue;
85
88
  }
86
89
  await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", { decidedBy: "sla_timeout" }).then((r) => { if (r.status >= 400)
@@ -88,11 +91,11 @@ export function createApprovalSweeps(ctx) {
88
91
  }
89
92
  if (parkedSkipped > 0)
90
93
  deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
91
- if (allowListSkipped.length > 0) {
92
- deps.logger?.warn?.("deny_sweep_allow_list_reading_skipped", {
93
- count: allowListSkipped.length,
94
- sessionIds: allowListSkipped,
95
- note: "these expired approvals belong to tasks suspended by a server version that read settings.permissions.allow as a whitelist; this version does not replay them, so the SLA deny cannot be delivered. They stay suspended (session lock held) until the reaper collects them at their terminal time — cancel them (POST /v1/runs/:id/cancel) to release the sessions now",
94
+ if (replaySkipped.length > 0) {
95
+ deps.logger?.warn?.("deny_sweep_stored_body_skipped", {
96
+ count: replaySkipped.length,
97
+ skipped: replaySkipped,
98
+ note: "these expired approvals belong to tasks whose stored request this version does not replay (kind allow_list_reading: settings.permissions.allow written under an earlier reading; kind skill_deny_inert: a skill disallowedTools entry the engine now reads as empty), so the SLA deny cannot be delivered. They stay suspended until the terminal backstop reaps them or someone cancels them; nothing was retried",
96
99
  });
97
100
  }
98
101
  }
@@ -147,7 +150,7 @@ export function createApprovalSweeps(ctx) {
147
150
  ...(req.gateBoundInputHash !== null ? { boundInputHash: req.gateBoundInputHash } : {}),
148
151
  ...(req.updatedInput !== undefined ? { updatedInput: req.updatedInput } : {}),
149
152
  };
150
- const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, hostDecisionFacts(req.settledBy, req.principal), req.httpReq, undefined, binding, undefined, true, { principal: req.principal, explicitOperator: false });
153
+ const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, hostDecisionFacts(req.decidedBy, req.principal), req.httpReq, undefined, binding, undefined, true, { principal: req.principal, explicitOperator: false });
151
154
  return { status: out.status, body: out.body };
152
155
  }
153
156
  return { denyExpiredApprovals, redeemParkedAsk };
@@ -1,43 +1,57 @@
1
1
  /**
2
- * 续跑重建输入(`checkpoint_ctx` 的行体,{@link CheckpointCtx})的**写形**,以及持久化体里 `settings.permissions.allow` 的**读法记号** —— S-1034(7.115.0)。
2
+ * 续跑重建输入(`checkpoint_ctx` 的行体,{@link CheckpointCtx})的**写形**,以及持久化体里 `settings.permissions.allow` 的**读法记号**。
3
3
  *
4
- * ## 病
5
- * 7.115.0 把 `settings.permissions.allow` 从「名单」(名单外的工具一律拒)改成「allow 规则」(命中 = 带理由的放行,未命中 = 不表态、照常判)。
6
- * 续跑家族重建 spec 时把**持久化的提交体**原样重放进 `resolveSpec`(`resume-legs.ts` 的 `resolveResumeSpec`)——
7
- * 一条在 ≤7.114.0 上停驻、体里带非空 `allow` 的行,升级后续跑就被按新读法读:名单外的工具不再被拒。持久面上的静默语义翻转(放宽方向)。
8
- * 坏形 `allow`(`Task` / `mcp__figma` / `Bash(npm:*)` …)的旧行则每次续跑都 409、SLA 拒扫腿逐拍重试直到收割 —— 一张本来放不开任何门的停驻被砖化。
4
+ * ## 病(两代,同一形)
5
+ * `settings.permissions.allow` 的读法换过两次:≤7.114.0 是「名单」(名单外的工具一律拒);7.115.0–7.116.x 是「表态」(只回答没人表态的那一问,
6
+ * 写 / shell 这类普通问照问);7.117.0 起整份交给引擎的 allow 层(`TaskSpec.allowedTools`:表里的调用免问 —— 普通问与没人表态两类都清,内容形也收)。
7
+ * 续跑家族重建 spec 时把**持久化的提交体**原样重放进 `resolveSpec`(`resume-legs.ts` 的 `resolveResumeSpec`)—— 一条在旧读法下停驻、体里带非空 `allow`
8
+ * 的行,升级后续跑就被按新读法读,每一次换代都是放宽方向(名单 → 表态:名单外的工具放开;表态 → allow 层:普通问不再问)。持久面上的静默语义翻转。
9
9
  *
10
10
  * ## 规则(一条;持久数据的「响亮拒 + 一次性迁移」,零双读)
11
- * 续跑重放持久化体之前问一句:**体里的 `allow` 在生效(非空数组)而本行没有「按表态读」的记号 ⇒ 拒**。
11
+ * 续跑重放持久化体之前问一句:**体里的 `allow` 在生效(非空数组)而本行的读法记号不是当前这一个词 ⇒ 拒**。
12
12
  * 行留停驻、字节不动(拒发生在认领之前,run 行、卡、ctx 一个字节都不写);出路 = 作为新任务重提,这条任务若还停驻着先取消(`task_done` 唤醒停驻与 workflow 宿主那条腿没有可取消的 run)。
13
- * **没有**「按旧名单语义继续读」的臂:那是第二种读法,正是本规则要消灭的东西。
14
- * · 记号 = ≥7.115.0 写下的行在体里带非空 `allow` 时,ctx 上的 `permissionsAllowReading: "statement"`(闭集一词)。
13
+ * **没有**「按旧读法继续读」的臂(那是第二种读法),也**没有**「按新读法静默续跑」的臂(那是对一条已提交任务的静默放宽)。
14
+ * · 记号 = 当前写点在体里带非空 `allow` 时写在 ctx 上的 `permissionsAllowReading`,闭集只有当前读法一个词({@link PERMISSIONS_ALLOW_READINGS})。
15
15
  * 写点 = 每一次**新的**持久化写({@link buildCheckpointCtx} 是它们的唯一构造口);续跑腿的写回传的是读进来的那只 ctx(或其展开),记号随行,不重新判。
16
- * · 体里没有 `allow`(或空数组 —— 7.114.0 的名单判据 `acceptedRuleList` 对空数组同样读成「没有名单」)的行不写记号、不受本规则影响。
16
+ * · 拒句按「这行是在哪种读法下写的」分两句({@link ALLOW_LIST_READING_ERAS} 穷举):无记号(或记号认不得)= 名单时代;`"statement"` = 表态时代。
17
+ * · 体里没有 `allow`(或空数组 —— 三代都把空数组读成「没有」)的行不写记号、不受本规则影响。
17
18
  *
18
- * ## 判据与 7.114.0 的关系(如实)
19
- * 7.114.0 只在三桶整体合法时才读名单;判据这里只看「`allow` 是非空数组」,比 7.114.0 的「名单生效」宽一格(坏桶的行 7.114.0 也不读名单)——
20
- * 方向 = 多拒,不会漏拒;而坏桶的体当年就被 fresh 受理门拒在门外,盘上本不该有。
19
+ * ## 第二问(7.117.0,引擎 7.42 逐字读规则条目):技能拒侧读成空的存量体
20
+ * 同一面上的第二个「新提交会被拒、存量体重放不能放过」:提交体里技能(顶层 / 子代)的 `disallowedTools` 有引擎读成空的条目(`"Bash "` 这类 7.41 去白后有效、
21
+ * 7.42 什么也不拒的条目)⇒ 提交 400(`resolveSpec`,拒点);存量停驻体重放 ⇒ 409 `resume_blocked_by_policy`。判据函数 = `rule-entry-list.ts` `skillDenyInertEntryOf`。
22
+ *
23
+ * ## 一只判据、五个问点({@link storedBodyReplayRefusal} / {@link storedBodyReplayAnswer})
24
+ * 「这条停驻行的持久化体在本版上能否重放」只有**一个**读法:上面两问合在 {@link storedBodyReplayRefusal} 一处,答复合在 {@link storedBodyReplayAnswer} 一处。
25
+ * 问点五个,同调同答:续跑驱动段(`resume-legs.ts` `resolveResumeSpecOrBlocked`,另四条腿的唯一判点)· plan_review 的 prepare 段(读类拒,在 nonce 消费之前)·
26
+ * SLA 拒扫腿的跳过臂(`approval-sweeps.ts`)· 终态 run 的 steer 铸唤醒卡之前(`routes/runs.ts`:铸卡前先证明它赎得回,证不出 ⇒ 不铸、当场同一个拒)·
27
+ * `resolveSpec` 本身留作真正的拒点(提交腿 400;续跑腿在到它之前已被本判据答掉)。再开第六个问点必须调这两只,不许各判各的 —— 此前铸卡前只判了
28
+ * allow 记号,技能拒侧那一问漏在外面,卡铸出来了、`/wake` 恒 409([ref] 合并复审两腿同报)。
21
29
  *
22
30
  * ## 何时删(一次性迁移,不是长期双读)
23
- * 本判据、拒臂、记号写点与 `CheckpointCtx.permissionsAllowReading` 一起删,删除条件 = 所有 ≤7.114.0 停驻的行都已结束或被收割
24
- * (停驻行的终局时刻 = 落盘起 `TERMINAL_BACKSTOP_MS`,见 `plugins/checkpoint-store-sql.ts`)之后的那个版本。删的时候记号的读者一起删:
25
- * 届时盘上不会再有没记号却带 `allow` 的行,而有记号的行读不读它都一样。
31
+ * 本判据、拒臂、记号写点与 `CheckpointCtx.permissionsAllowReading` 一起删,删除条件与窗登记在 `plugins/one-time-migrations.ts` 的
32
+ * 一次性迁移登记表(本条那一行):所有旧读法下停驻的行都已结束或被收割(停驻行的终局 = 落盘起 `TERMINAL_BACKSTOP_MS`,见
33
+ * `plugins/checkpoint-store-sql.ts`)之后的那个版本。删的时候记号的读者一起删。
26
34
  */
27
35
  import type { CheckpointCtx } from "./route-ctx.js";
28
36
  import type { TaskRequestBody } from "./wire-types.js";
29
- /** 读法记号的闭集(一词):这一行体里的 `settings.permissions.allow` 是按「allow 规则(表态)」写下、也该按它读的。 */
30
- export declare const PERMISSIONS_ALLOW_READINGS: readonly ["statement"];
37
+ import { type SkillDenyInertEntry } from "../rule-entry-list.js";
38
+ /** 读法记号的闭集(一词 = 当前读法):这一行体里的 `settings.permissions.allow` 是按「交给引擎的 allow 层」写下、也该按它读的
39
+ * (与能力位 `settings.permissions.allowLayer` 同一个说法)。 */
40
+ export declare const PERMISSIONS_ALLOW_READINGS: readonly ["allow_layer"];
31
41
  export type PermissionsAllowReading = (typeof PERMISSIONS_ALLOW_READINGS)[number];
42
+ /** 被拒的行是在哪种读法下写的(闭集;拒句按它穷举):`list` = 无记号或记号认不得(≤7.114.0 的名单读法;认不得的记号不是任何一代的证据,
43
+ * 按最早那一代说话);`statement` = 7.115.0–7.116.x 写下的表态读法。 */
44
+ export declare const ALLOW_LIST_READING_ERAS: readonly ["list", "statement"];
45
+ export type AllowListReadingEra = (typeof ALLOW_LIST_READING_ERAS)[number];
32
46
  /**
33
- * 体里的 `settings.permissions.allow` 是否**在生效**:非空数组(与 7.114.0 名单判据 `acceptedRuleList` 同一个「在」——空数组 = 没有)。
47
+ * 体里的 `settings.permissions.allow` 是否**在生效**:非空数组(三代同一个「在」—— 空数组 = 没有)。
34
48
  * 入参是持久化的 JSON(可能来自任何旧版本),逐层判形,不信型面。
35
49
  */
36
50
  export declare function bodyCarriesAllowList(body: TaskRequestBody): boolean;
37
51
  /**
38
52
  * 一次**新的**持久化写的 ctx(提交腿 / `POST /v1/runs` 族 / leader worker)—— 写记号的唯一口。
39
53
  *
40
- * 调用方是「本进程刚按表态读过这份体」的写点:fresh 提交(受理门按 7.115.0 读过 `allow`),或已经过了续跑拒臂的重放腿
54
+ * 调用方是「本进程刚按当前读法读过这份体」的写点:fresh 提交(受理门按当前读法读过 `allow`),或已经过了续跑拒臂的重放腿
41
55
  * (`startFreshHostTurn` 把宿主的存量体交给 `POST /v1/runs` 同一条链 —— 它在那之前调过 `resolveResumeSpecOrBlocked`,没记号却带 `allow` 的体到不了这里)。
42
56
  * 续跑腿的**写回**不经本函数:它传读进来的 ctx(或其展开),记号随行;在写回口重新判会把一条没记号的旧行「洗」成新行。
43
57
  */
@@ -47,17 +61,20 @@ export declare function buildCheckpointCtx(fields: {
47
61
  unattendedLeg?: true;
48
62
  }): CheckpointCtx;
49
63
  /**
50
- * 本行能否被按 7.115.0 的读法重放:体里 `allow` 在生效而行上没有记号 ⇒ **不能**(`true` = 拒)。
51
- * 记号的值不在闭集里(手改 / 将来的写者)⇒ 同样拒:认不得的记号不是证据。
64
+ * 本行能否被按当前读法重放:体里 `allow` 在生效而行上的记号不是当前这一个词 ⇒ **不能**(`true` = 拒)。
65
+ * 记号缺席、是旧词(`"statement"`)或认不得(手改 / 将来的写者)⇒ 同样拒:只有当前这个词是「按当前读法写下」的证据。
52
66
  */
53
67
  export declare function allowListReadingUnstated(ctx: CheckpointCtx): boolean;
68
+ /** 被拒的行是哪一代写的(只在 {@link allowListReadingUnstated} 为真时有意义):记号是 `"statement"` ⇒ 表态时代;其余(缺席 / 认不得)⇒ 名单时代。 */
69
+ export declare function allowListReadingEraOf(ctx: CheckpointCtx): AllowListReadingEra;
54
70
  /**
55
71
  * 拒的答复 —— 码与落点照「退役 mode-seed 行」的先例(`checkpoint.unsupported_version`,409,`retriable:true` = 那张卡仍 pending;**不是**「原样重试会成功」)。
56
- * 拒在认领之前,所以没有 `taskId`(行没抢到就不造键)。句子说原因(这条任务停在 `allow` 还是名单的版本上)与出路(作为新任务重提,仍停驻的先取消;要「只许这些工具」用会话规则 `toolAllow` / 每请求 `excludeTools`)。
72
+ * 拒在认领之前,所以没有 `taskId`(行没抢到就不造键)。句子按这一行是哪一代写的分两句(闭集穷举,加一代 ⇒ 编译红):说原因(这条任务停在哪种读法上、
73
+ * 照当前读法续跑会放宽什么)与出路(作为新任务重提,仍停驻的先取消)。
57
74
  *
58
75
  * 🔴 形是给冻结门看的(`api-error-text-freeze` 的 resume 家族收集器):`status: <三位数>, body: { error: "<整句双引号字面>", errorCode: "<码>"`,中间只许有空白。
59
76
  */
60
- export declare function allowListReadingRefusal(): {
77
+ export declare function allowListReadingRefusal(era: AllowListReadingEra): {
61
78
  status: number;
62
79
  body: {
63
80
  error: string;
@@ -65,4 +82,46 @@ export declare function allowListReadingRefusal(): {
65
82
  retriable: true;
66
83
  };
67
84
  };
85
+ /** 一条停驻行的持久化体在本版上**不能**重放时,是哪一条规则拒的(闭集;加一条 ⇒ {@link storedBodyReplayAnswer} 的 switch 编译红)。 */
86
+ export type StoredBodyReplayRefusal = {
87
+ readonly kind: "allow_list_reading";
88
+ readonly era: AllowListReadingEra;
89
+ } | {
90
+ readonly kind: "skill_deny_inert";
91
+ readonly entry: SkillDenyInertEntry;
92
+ };
93
+ /**
94
+ * 「存量体能否重放」的**唯一**判据(文件头「一只判据、五个问点」):不能 ⇒ 返回拒的那条规则;能 ⇒ `undefined`。
95
+ * 顺序:先 allow 读法记号,再技能拒侧读成空(两问都中时答第一问 —— 任一问中都不重放,句子只需一句)。
96
+ */
97
+ export declare function storedBodyReplayRefusal(ctx: CheckpointCtx): StoredBodyReplayRefusal | undefined;
98
+ /** 续跑家族交回的拒答复形(`{ status, body }`,由 `sendResumeOutcome` / 各路由发送;`body` 至少 `error` + `errorCode`)。 */
99
+ export interface ResumeRefusalAnswer {
100
+ readonly status: number;
101
+ readonly body: {
102
+ readonly error: string;
103
+ readonly errorCode: string;
104
+ } & Record<string, unknown>;
105
+ }
106
+ /**
107
+ * 「续跑被一条准入策略挡住」的**唯一**铸点:`resolveSpec` 在重放存量体时抛出的 `HttpError`(400 族)折成 409 `resume_blocked_by_policy`,
108
+ * `blockedBy` = 那条 400 的码(属主 `http/send.ts` `httpErrorCode`),`extra` 原样随体。{@link storedBodyReplayAnswer} 的技能拒侧那一臂与
109
+ * `resume-legs.ts` 的折叠口都调它 —— 句子与码只在这里写一次(冻结门 `api-error-text-freeze` 的模板骨架表锚这一处)。
110
+ */
111
+ export declare function resumeBlockedByPolicyAnswer(message: string, blockedBy: string, extra?: Record<string, unknown> | undefined): ResumeRefusalAnswer;
112
+ /**
113
+ * {@link storedBodyReplayRefusal} 的**日志字段**(三处问点同一形,各自只再加坐标键 `sessionId` / `taskId`):`kind` 判别,allow 臂带 `era`、技能臂带 `layer`,`note` 一句出路。
114
+ * 事件名三个(问点各一:`resume_refused_stored_body` / `steer_wake_park_refused_stored_body` / `deny_sweep_stored_body_skipped`),字段形只有这一只。
115
+ */
116
+ export declare function storedBodyReplayLogFields(refusal: StoredBodyReplayRefusal): {
117
+ kind: StoredBodyReplayRefusal["kind"];
118
+ era?: AllowListReadingEra;
119
+ layer?: SkillDenyInertEntry["layer"];
120
+ note: string;
121
+ };
122
+ /**
123
+ * {@link storedBodyReplayRefusal} 的答复:每条规则一只既有铸点(allow 记号 ⇒ {@link allowListReadingRefusal};技能拒侧 ⇒ {@link resumeBlockedByPolicyAnswer},与提交腿的 400
124
+ * `request.field_invalid` 同码同句 —— 体上只有这一个问题时;存量体同时还踩中 `resolveSpec` 更早的拒〔三桶超帽 / 托管姿态〕时,提交侧答更早那条、续跑侧答本句,两边都是拒,指路不同)。
125
+ */
126
+ export declare function storedBodyReplayAnswer(refusal: StoredBodyReplayRefusal): ResumeRefusalAnswer;
68
127
  //# sourceMappingURL=checkpoint-ctx.d.ts.map
@@ -1,4 +1,6 @@
1
- export const PERMISSIONS_ALLOW_READINGS = ["statement"];
1
+ import { skillDenyInertEntryOf } from "../rule-entry-list.js";
2
+ export const PERMISSIONS_ALLOW_READINGS = ["allow_layer"];
3
+ export const ALLOW_LIST_READING_ERAS = ["list", "statement"];
2
4
  export function bodyCarriesAllowList(body) {
3
5
  const settings = body?.settings;
4
6
  if (settings === null || typeof settings !== "object" || Array.isArray(settings))
@@ -14,7 +16,7 @@ export function buildCheckpointCtx(fields) {
14
16
  body: fields.body,
15
17
  ...(fields.memoryScope !== undefined ? { memoryScope: fields.memoryScope } : {}),
16
18
  ...(fields.unattendedLeg === true ? { unattendedLeg: true } : {}),
17
- ...(bodyCarriesAllowList(fields.body) ? { permissionsAllowReading: "statement" } : {}),
19
+ ...(bodyCarriesAllowList(fields.body) ? { permissionsAllowReading: "allow_layer" } : {}),
18
20
  };
19
21
  }
20
22
  export function allowListReadingUnstated(ctx) {
@@ -23,14 +25,72 @@ export function allowListReadingUnstated(ctx) {
23
25
  const reading = ctx.permissionsAllowReading;
24
26
  return !(typeof reading === "string" && PERMISSIONS_ALLOW_READINGS.includes(reading));
25
27
  }
26
- export function allowListReadingRefusal() {
28
+ export function allowListReadingEraOf(ctx) {
29
+ const reading = ctx.permissionsAllowReading;
30
+ return reading === "statement" ? "statement" : "list";
31
+ }
32
+ export function allowListReadingRefusal(era) {
33
+ switch (era) {
34
+ case "list":
35
+ return {
36
+ status: 409,
37
+ body: {
38
+ error: "this task was suspended by an earlier server version that did not record how it read settings.permissions.allow (7.114.0 and earlier read it as a whitelist: every tool not on the list was refused); this version hands that list to the engine's allow layer, where each entry is a standing yes for the calls it covers, so continuing would give the task more tools than it was submitted with. Nothing was decided and nothing was resumed, and retrying will be refused the same way: submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel). To allow only certain tools, use the session rules' toolAllow (PUT /v1/sessions/:id/policy) or the request's excludeTools.",
39
+ errorCode: "checkpoint.unsupported_version",
40
+ retriable: true,
41
+ },
42
+ };
43
+ case "statement":
44
+ return {
45
+ status: 409,
46
+ body: {
47
+ error: "this task was suspended by an earlier server version (7.115.0 to 7.116.x) that read settings.permissions.allow as a statement that the named tools are allowed, which answered only the question nobody else had answered and never cleared an ordinary question such as a file write or a shell command; this version hands that list to the engine's allow layer, where an entry also clears those ordinary questions, so continuing would run calls without asking that the task was submitted to ask about. Nothing was decided and nothing was resumed, and retrying will be refused the same way: submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel).",
48
+ errorCode: "checkpoint.unsupported_version",
49
+ retriable: true,
50
+ },
51
+ };
52
+ default: {
53
+ const unhandled = era;
54
+ throw new Error(`allow-list reading era ${String(unhandled)} has no refusal sentence`);
55
+ }
56
+ }
57
+ }
58
+ export function storedBodyReplayRefusal(ctx) {
59
+ if (allowListReadingUnstated(ctx))
60
+ return { kind: "allow_list_reading", era: allowListReadingEraOf(ctx) };
61
+ const entry = skillDenyInertEntryOf(ctx.body);
62
+ if (entry !== undefined)
63
+ return { kind: "skill_deny_inert", entry };
64
+ return undefined;
65
+ }
66
+ export function resumeBlockedByPolicyAnswer(message, blockedBy, extra) {
27
67
  return {
28
68
  status: 409,
29
- body: {
30
- error: "this task was suspended by an earlier server version, which read settings.permissions.allow as a whitelist (every tool not on the list was refused); this version reads that list as a statement that the named tools are allowed, not as a whitelist, so continuing would give the task more tools than it was submitted with. Nothing was decided and nothing was resumed, and retrying will be refused the same way: submit the work again as a new task, and if this task is still suspended cancel it first (POST /v1/runs/:id/cancel). To allow only certain tools, use the session rules' toolAllow (PUT /v1/sessions/:id/policy) or the request's excludeTools.",
31
- errorCode: "checkpoint.unsupported_version",
32
- retriable: true,
33
- },
69
+ body: { error: `resume blocked by an admission policy change: ${message}`, errorCode: "resume_blocked_by_policy", blockedBy, ...(extra ?? {}) },
34
70
  };
35
71
  }
72
+ export function storedBodyReplayLogFields(refusal) {
73
+ switch (refusal.kind) {
74
+ case "allow_list_reading":
75
+ return { kind: refusal.kind, era: refusal.era, note: "this session's stored request carries settings.permissions.allow written under an earlier reading (list era: a whitelist; statement era: a statement that cleared only the unstated question); it is not replayed under the current reading — the task stays suspended until someone cancels it" };
76
+ case "skill_deny_inert":
77
+ return { kind: refusal.kind, layer: refusal.entry.layer, note: "this session's stored request carries a skill disallowedTools entry the engine now reads as empty (an earlier engine trimmed it into a working deny); it is not replayed — the task stays suspended until someone cancels it and resubmits with the entry fixed" };
78
+ default: {
79
+ const unhandled = refusal;
80
+ throw new Error(`stored-body replay refusal ${JSON.stringify(unhandled)} has no log fields`);
81
+ }
82
+ }
83
+ }
84
+ export function storedBodyReplayAnswer(refusal) {
85
+ switch (refusal.kind) {
86
+ case "allow_list_reading":
87
+ return allowListReadingRefusal(refusal.era);
88
+ case "skill_deny_inert":
89
+ return resumeBlockedByPolicyAnswer(refusal.entry.message, "request.field_invalid");
90
+ default: {
91
+ const unhandled = refusal;
92
+ throw new Error(`stored-body replay refusal ${JSON.stringify(unhandled)} has no answer`);
93
+ }
94
+ }
95
+ }
36
96
  //# sourceMappingURL=checkpoint-ctx.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * S-431 —— 一条 HTTP 请求愿意等**一次远端 MCP 拨号**多久:**一只数、一只 helper**,两口共读。
3
+ *
4
+ * 同一类动作(连一台远端 MCP 服务器 + 列它的工具)在本仓有两张 HTTP 面:
5
+ * · `GET /v1/sessions/:id/mcp` 面板(`routes/sessions.ts`)—— 物化部署缺省场景的申报,到点答 200 `degraded:true`;
6
+ * · `POST /v1/sessions/:id/mcp/reconnect`(`routes/mcp-reconnect.ts`)—— 在飞 run 上重拨一台,到点答 503。
7
+ * 修前面板有期限、reconnect 是裸 `await`(S-431):同一件事两个读法,一台拨不通 / 半开的远端能把后者挂很久。
8
+ * 两口各自**到点答什么**不同(面板是只读的部署面,有一个诚实的空形;reconnect 是一次交易,没有结果就没有
9
+ * 一个能当 `outcome` 的词)—— 但**等多久**是一个事实,所以只有一个数。
10
+ *
11
+ * 🔴 **这是常量,不是旋钮**:没有 env、不进 config-catalog。要调窗就改这一处,两口一起动。
12
+ * (底层拨号自己的上界是 core 的启动期钟 —— env `MCP_TIMEOUT`,未设落 SDK 缺省 —— 与本数无关:本数只界
13
+ * **HTTP 应答**,不界拨号;见 {@link raceDeadline}。)
14
+ *
15
+ * helper 收的是期限**参数**而不是自己读上面那个常量:调用点把 {@link MCP_DIAL_DEADLINE_MS} 显式交下来,
16
+ * `test/mcp-reconnect-deadline.test.ts` 的静态钉就是「每个远端 MCP 拨号调用点都把它交给 `raceDeadline`」。
17
+ */
18
+ /** 一次远端 MCP 拨号的 HTTP 应答期限(毫秒)。面板与 reconnect 共读;**全仓只有这一处写它**。 */
19
+ export declare const MCP_DIAL_DEADLINE_MS = 10000;
20
+ /** `work` 没在期限内落定。**具名型**:调用腿据它分诊,不比对错误文案。 */
21
+ export declare class DeadlineExceeded extends Error {
22
+ readonly boundMs: number;
23
+ constructor(boundMs: number);
24
+ }
25
+ /**
26
+ * 让**应答**不再等 `work`:`work` 先落定就原样交出(resolve / reject 都不变形),期限先到就 reject
27
+ * {@link DeadlineExceeded}。
28
+ *
29
+ * 🔴 **只界应答,不取消 `work`**:本函数不持有任何取消把手 —— core 的 `TaskStream.reconnectMcpServer` 与
30
+ * `materializeMcpTools` 都不收 `AbortSignal`(d.ts 逐字),所以期限到点后底层拨号**继续跑**,落定时
31
+ * 才释放它占的连接 / 子进程。调用点必须把这句话交代给调用方(reconnect 的 503 文案、契约 §G.7c)。
32
+ * 迟到的落定(尤其迟到的 reject)挂一条吞掉,免得没人 await 它时变成未处理拒绝。
33
+ */
34
+ export declare function raceDeadline<T>(work: Promise<T>, deadlineMs: number): Promise<T>;
35
+ //# sourceMappingURL=mcp-dial-deadline.d.ts.map
@@ -0,0 +1,21 @@
1
+ export const MCP_DIAL_DEADLINE_MS = 10_000;
2
+ export class DeadlineExceeded extends Error {
3
+ boundMs;
4
+ constructor(boundMs) {
5
+ super(`did not settle within ${boundMs}ms`);
6
+ this.boundMs = boundMs;
7
+ this.name = "DeadlineExceeded";
8
+ }
9
+ }
10
+ export function raceDeadline(work, deadlineMs) {
11
+ let timer;
12
+ const deadline = new Promise((_resolve, reject) => {
13
+ timer = setTimeout(() => reject(new DeadlineExceeded(deadlineMs)), deadlineMs);
14
+ });
15
+ work.catch(() => undefined);
16
+ return Promise.race([work, deadline]).finally(() => {
17
+ if (timer !== undefined)
18
+ clearTimeout(timer);
19
+ });
20
+ }
21
+ //# sourceMappingURL=mcp-dial-deadline.js.map
@@ -10,21 +10,24 @@
10
10
  *
11
11
  * ## 规则(三层,与 operator 面既有层序逐字同序)
12
12
  * ① core 的**本口专属**码(闭集穷举 `switch`,见 {@link sentCoreCode})—— 码与状态两面同;文案按受众(下一节,一条规则);
13
- * ② `memory.target_outside_store`(core 7.31.1 #1094 跨面 store 状态码)—— 409;
13
+ * ② **店形拒**两码 `memory.target_outside_store`(core 7.31.1 #1094)/ `memory.scope_home_not_directory`(core 7.44.0 #1368)—— 跨面 store 状态码,409、码原样
14
+ * (判据 `memory-store-refusal.ts` 一只;运维行 `memory_store_form_refused { code, path?, form? }` 两受众同一只铸点,路径只在那里);
14
15
  * ③ `ControlPlaneCorruptError` —— typed 500 `internal.memory_control_plane_corrupt`(fail-closed 信号,一码两义见下);
15
16
  * 其余 ⇒ `false`(未分类,调用方发它自己那句 500 且**不回显**内部文本)。
16
17
  *
17
18
  * ## 文案按受众分(码与状态不分)—— **一条规则,三层同施,不按码挑**(F9,7.108.0 合并复审 opus 腿 O-9)
18
- * core 原句是写给运维的诊断材料:② ③ 点名**宿主路径**(store 根、链解析到哪 / 控制面账本文件),① 的 `selector_mismatch` 带**首个请求的
19
+ * core 原句是写给运维的诊断材料:③ 点名**宿主路径**(控制面账本文件;② 自 core 7.44.0 起句子不带路径、路径在 `detail.path` ⇒ 运维行),① 的 `selector_mismatch` 带**首个请求的
19
20
  * 选择子哈希**(`pinned set <selectHash>`,无盐 sha256 —— operator 选择子可字典还原,首个请求未必是这位会话所有者发的):
20
21
  * · operator 受众 ⇒ 原句上 wire(过了 operator 门,这些材料对这个受众不是泄露);
21
22
  * · 会话所有者受众 ⇒ **一律**一句固定指路(每码一句,只说「怎么办」),原句整句交 `withheld` 落运维 warn —— 不逐码判断哪句安全
22
23
  * (逐码挑 = core 改一句文案就可能静默漏出一样新东西)。② ③ 的固定句**两形都真**:
23
- * ② 链先于请求在场 ⇒ 提交点之前拒、什么都没做;提交点之后才换上的链 ⇒ 已删的几条算数、journal 留盘(codex 一审 C1);
24
+ * ② 链先于请求在场 ⇒ 提交点之前拒、什么都没做;提交点之后才换上的链 ⇒ 已删的几条算数、journal 留盘(codex 一审 C1);scope home 不是真目录那一句
25
+ * 同样不许诺零删除(home 被换掉的时刻本层判不出),只指路「运维放回真目录后用同一个 requestId 重发」;
24
26
  * ③ 一码两义 —— 操作前账本读不出 ⇒ 什么都没发生;落定后残留枚举失败 ⇒ 抹除已提交、重放收敛(core 原话)。两句都不许诺零删除,
25
27
  * 都指路「运维修好后用同一个 requestId 重发」(两义下这句都对)。本层**不**嗅 core 文案分辨前后(拿上游自由文本当判别式是第二真源)。
26
28
  */
27
29
  import type { ServerResponse } from "node:http";
30
+ import type { Logger } from "../observability/logger.js";
28
31
  /**
29
32
  * 已分类拒 → HTTP 状态的**唯一**码表 = 自铸 wire 值登记处的 `sessionMemoryEraseStatus` 集(F3:wire 变更清单从同一张表 diff,状态改一处、
30
33
  * 清单跟着列)。两面同读(S-930:码与状态只判一次)。键写错 ⇒ tsc 红(索引的是 `as const` 表的键)。
@@ -36,6 +39,9 @@ export type MemoryComplianceAudience = {
36
39
  readonly kind: "session_owner";
37
40
  readonly withheld: (coreMessage: string) => void;
38
41
  };
39
- /** 按上面三层应答;返回 `true` = 已应答,`false` = 未分类(一个字节都没写,调用方发它自己那句 500 并落它自己的运维 warn)。 */
40
- export declare function sentMemoryComplianceFailure(res: ServerResponse, err: unknown, audience: MemoryComplianceAudience): boolean;
42
+ /**
43
+ * 按上面三层应答;返回 `true` = 已应答,`false` = 未分类(一个字节都没写,调用方发它自己那句 500 并落它自己的运维 warn)。
44
+ * `logger` = 店形拒那一条运维行的去处(两受众同一只;缺席 ⇒ 不落)。
45
+ */
46
+ export declare function sentMemoryComplianceFailure(res: ServerResponse, err: unknown, audience: MemoryComplianceAudience, logger: Pick<Logger, "warn"> | undefined): boolean;
41
47
  //# sourceMappingURL=memory-compliance-failure-reply.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { ControlPlaneCorruptError } from "@sema-agent/core";
2
- import { targetOutsideStoreOf } from "../memory-store-refusal.js";
2
+ import { logStoreFormRefusal, storeFormRefusalOf } from "../memory-store-refusal.js";
3
3
  import { sentMemoryStoreRefusal } from "./memory-store-refusal-reply.js";
4
4
  import { sendError } from "./send.js";
5
5
  import { assertNever } from "./route-ctx.js";
@@ -38,19 +38,28 @@ function codeOf(err) {
38
38
  const { code } = err;
39
39
  return typeof code === "string" ? code : undefined;
40
40
  }
41
- export function sentMemoryComplianceFailure(res, err, audience) {
41
+ export function sentMemoryComplianceFailure(res, err, audience, logger) {
42
42
  const coreCode = codeOf(err);
43
43
  if (coreCode !== undefined && sentCoreCode(res, coreCode, err instanceof Error ? err.message : String(err), audience))
44
44
  return true;
45
- const outside = targetOutsideStoreOf(err);
46
- if (outside !== undefined) {
45
+ const storeForm = storeFormRefusalOf(err);
46
+ if (storeForm !== undefined) {
47
47
  switch (audience.kind) {
48
48
  case "operator":
49
- return sentMemoryStoreRefusal(res, err);
49
+ return sentMemoryStoreRefusal(res, err, logger);
50
50
  case "session_owner":
51
- audience.withheld(outside.message);
52
- 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");
53
- return true;
51
+ logStoreFormRefusal(logger, storeForm);
52
+ audience.withheld(storeForm.message);
53
+ switch (storeForm.code) {
54
+ case "memory.target_outside_store":
55
+ 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");
56
+ return true;
57
+ case "memory.scope_home_not_directory":
58
+ sendError(res, 409, "memory.scope_home_not_directory", "session memory erasure refused — a scope directory in this deployment's memory store is not a real directory (it was replaced by a link or a file), so this erasure did not complete; ask the operator to put the real directory back, then resend with the same requestId");
59
+ return true;
60
+ default:
61
+ return assertNever(storeForm.code);
62
+ }
54
63
  default:
55
64
  return assertNever(audience);
56
65
  }
@@ -1,24 +1,27 @@
1
1
  /**
2
- * core 7.31.1 #1094 `memory.target_outside_store` 的 **operator 面 HTTP 映射** —— 唯一一处字面量(判据在
3
- * `src/memory-store-refusal.ts`)。挂在每一个触 store 树的 operator 记忆口的已分类收尾上:
2
+ * **店形拒**两码(`memory.target_outside_store` / `memory.scope_home_not_directory`,判据在 `src/memory-store-refusal.ts`)的
3
+ * **operator 面 HTTP 映射** + 两受众共用的**运维行** —— 唯一一处码字面量。挂在每一个触 store 树的 operator 记忆口的已分类收尾上:
4
4
  * `routes/memory-compliance.ts`(出处 / 抹除;经 `http/memory-compliance-failure-reply.ts` 的 operator 受众臂)· `routes/memory-origin.ts`(外源列表 / 清标;**不含**只读控制面的清标账列表 ——
5
5
  * core 的 wire 表逐字:只碰控制面的面不判,给它挂一条永远收不到的码路径正是附录 A 活触发门要防的形)·
6
- * `routes/memory-bundle.ts`(导出 / 导入)。会话所有者的自助抹除口**不走这里**(S-930 起经 `http/memory-compliance-failure-reply.ts` 的会话所有者受众臂:同码、固定文案)。
6
+ * `routes/memory-bundle.ts`(导出 / 导入)。会话所有者的自助抹除口**不走这里的 wire 句**(经 `http/memory-compliance-failure-reply.ts` 的会话所有者受众臂:同码、固定文案),
7
+ * 但运维行({@link logStoreFormRefusal},铸点在 `src/memory-store-refusal.ts`)两受众同一只。
7
8
  * `POST /v1/admin/memory/consolidation/run` 也不走这里:那口把 core 的**任何**有码拒一律收成 409 `memory.consolidation_refused`
8
- * (码进文案,附录 A 该行),本码已在那条臂上,不再给它第二个码。
9
+ * (码进文案,附录 A 该行),这一族已在那条臂上,不再给它第二个码。
9
10
  *
10
- * ## 为什么 409
11
- * core 的读法:operator 要处置的拒(去掉 store 里那只通到外面的目录链),不是可重试的 IO 故障,也不是对调用方的裁决。与
11
+ * ## 为什么 409(两码同一条)
12
+ * core 的读法:operator 要处置的盘形(去掉 store 里那只通到外面的目录链 / 把 scope home 放回真目录),不是可重试的 IO 故障,也不是对调用方的裁决。与
12
13
  * `memory.erasure_census_incomplete` 同一类(**当前 store 状态**下这次做不了;状态修好后重发即改判)⇒ 409,不 500(服务器没坏)、
13
14
  * 不 4xx 请求族(请求没写错)。抹除面:链先于请求在场 ⇒ core 在提交点之前拒,零删除、零 journal(真 core 格钉在
14
15
  * `test/memory-compliance-http.test.ts`);链在提交点之后才换上 ⇒ 已删的几条算数、journal 留盘 —— core 原句会带
15
16
  * `after the commit point; the journal is kept`,本层原样转发、不嗅它分类(codex 一审 C1)。
16
17
  *
17
- * ## 文案原样
18
- * core 的诊断句点名动作、路径与解析到哪 —— 那是运维找到那只链的唯一材料,与本族其它 core 码同一条「码与文案都不改写」纪律。
19
- * 只对 operator 面这么做:这几口都过了 operator 门,宿主路径对这个受众不是泄露。
18
+ * ## 文案原样,路径进运维行
19
+ * operator 面上 wire 的是 core 原句(码领头;core 7.44.0 起句子里**没有**宿主路径 —— 路径在 `detail.path`)。路径不上 wire(两受众同,裁定:运维腿的
20
+ * 句子不带服务器路径,路径只进日志):每次应答这一族,{@link logStoreFormRefusal} 落一条运维 warn `memory_store_form_refused { code, path?, form? }` ——
21
+ * 运维找到那只链 / 那只坏 home 的唯一材料。
20
22
  */
21
23
  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
+ import type { Logger } from "../observability/logger.js";
25
+ /** `err` 是店形拒 ⇒ 409 带码 + core 原句 + 运维行,返回 true(已应答);否则 false,一个字节都不写。 */
26
+ export declare function sentMemoryStoreRefusal(res: ServerResponse, err: unknown, logger: Pick<Logger, "warn"> | undefined): boolean;
24
27
  //# sourceMappingURL=memory-store-refusal-reply.d.ts.map
@@ -1,10 +1,21 @@
1
- import { targetOutsideStoreOf } from "../memory-store-refusal.js";
1
+ import { logStoreFormRefusal, storeFormRefusalOf } from "../memory-store-refusal.js";
2
2
  import { sendError } from "./send.js";
3
- export function sentMemoryStoreRefusal(res, err) {
4
- const refusal = targetOutsideStoreOf(err);
3
+ export function sentMemoryStoreRefusal(res, err, logger) {
4
+ const refusal = storeFormRefusalOf(err);
5
5
  if (refusal === undefined)
6
6
  return false;
7
- sendError(res, 409, "memory.target_outside_store", refusal.message);
8
- return true;
7
+ logStoreFormRefusal(logger, refusal);
8
+ switch (refusal.code) {
9
+ case "memory.target_outside_store":
10
+ sendError(res, 409, "memory.target_outside_store", refusal.message);
11
+ return true;
12
+ case "memory.scope_home_not_directory":
13
+ sendError(res, 409, "memory.scope_home_not_directory", refusal.message);
14
+ return true;
15
+ default: {
16
+ const unhandled = refusal.code;
17
+ throw new Error(`unhandled store-form refusal word: ${String(unhandled)}`);
18
+ }
19
+ }
9
20
  }
10
21
  //# sourceMappingURL=memory-store-refusal-reply.js.map