@sema-agent/core 7.49.5 → 7.50.1

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 (200) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/agents/observer.js +3 -10
  3. package/dist/agents/subagent-steps.d.ts +6 -14
  4. package/dist/agents/subagent-steps.js +3 -17
  5. package/dist/brain/stream-shared.d.ts +1 -1
  6. package/dist/core/allow-entry.d.ts +1 -1
  7. package/dist/core/allow-entry.js +1 -1
  8. package/dist/core/ask-origin.d.ts +15 -11
  9. package/dist/core/ask-origin.js +6 -14
  10. package/dist/core/auto-mode-prompt.js +6 -2
  11. package/dist/core/checkpoint-store.d.ts +6 -4
  12. package/dist/core/checkpoint-store.js +1 -1
  13. package/dist/core/context-edit.d.ts +2 -2
  14. package/dist/core/decision-receipt.d.ts +2 -2
  15. package/dist/core/decision-receipt.js +30 -11
  16. package/dist/core/delegation-frames.d.ts +66 -18
  17. package/dist/core/delegation-frames.js +22 -0
  18. package/dist/core/engine-notice.d.ts +7 -5
  19. package/dist/core/frame-settlement.d.ts +6 -1
  20. package/dist/core/gate-fold.js +34 -19
  21. package/dist/core/gate-lanes.js +15 -13
  22. package/dist/core/gate-outcome.d.ts +2 -2
  23. package/dist/core/governance-codes.js +2 -0
  24. package/dist/core/hooks.d.ts +28 -20
  25. package/dist/core/human-input-projection.js +2 -0
  26. package/dist/core/lsp-diagnostics.d.ts +3 -0
  27. package/dist/core/lsp-diagnostics.js +6 -0
  28. package/dist/core/lsp-session.d.ts +39 -17
  29. package/dist/core/lsp-session.js +118 -58
  30. package/dist/core/lsp.d.ts +83 -3
  31. package/dist/core/lsp.js +146 -16
  32. package/dist/core/mcp.js +2 -1
  33. package/dist/core/media-byte-cap.d.ts +14 -0
  34. package/dist/core/media-byte-cap.js +15 -3
  35. package/dist/core/memory-engine/consolidation-driver.d.ts +23 -1
  36. package/dist/core/memory-engine/consolidation-driver.js +54 -6
  37. package/dist/core/memory-engine/consolidation-write-gate.d.ts +46 -0
  38. package/dist/core/memory-engine/consolidation-write-gate.js +82 -0
  39. package/dist/core/memory-engine/consolidation.d.ts +17 -0
  40. package/dist/core/memory-engine/consolidation.js +1 -0
  41. package/dist/core/memory-engine/distiller.d.ts +8 -2
  42. package/dist/core/memory-engine/distiller.js +8 -6
  43. package/dist/core/memory-engine/engine.d.ts +37 -4
  44. package/dist/core/memory-engine/engine.js +127 -38
  45. package/dist/core/memory-engine/file-backend.d.ts +8 -3
  46. package/dist/core/memory-engine/file-backend.js +44 -16
  47. package/dist/core/memory-engine/index.d.ts +2 -2
  48. package/dist/core/memory-engine/index.js +1 -1
  49. package/dist/core/memory-engine/landing-admission.d.ts +65 -0
  50. package/dist/core/memory-engine/landing-admission.js +68 -0
  51. package/dist/core/memory-engine/types.d.ts +23 -0
  52. package/dist/core/mode-policy.d.ts +3 -2
  53. package/dist/core/mode-policy.js +2 -0
  54. package/dist/core/permission-rule-model.d.ts +19 -13
  55. package/dist/core/permission-rule-model.js +7 -4
  56. package/dist/core/permission-rule-provider.d.ts +11 -0
  57. package/dist/core/permission-rules.d.ts +1 -1
  58. package/dist/core/permission-rules.js +5 -2
  59. package/dist/core/persisted-rule-arms.d.ts +15 -7
  60. package/dist/core/persisted-rule-arms.js +13 -6
  61. package/dist/core/reminder-mint.d.ts +5 -0
  62. package/dist/core/reminder-mint.js +1 -0
  63. package/dist/core/retired-keys.d.ts +8 -0
  64. package/dist/core/retired-keys.js +4 -0
  65. package/dist/core/rule-lane-reads.d.ts +4 -4
  66. package/dist/core/rule-lane-reads.js +10 -1
  67. package/dist/core/runner/construction-doors.d.ts +10 -4
  68. package/dist/core/runner/construction-doors.js +25 -2
  69. package/dist/core/runner/contracts.d.ts +20 -3
  70. package/dist/core/runner/env-capability-census.js +1 -1
  71. package/dist/core/runner/gate-exit.js +2 -1
  72. package/dist/core/runner/memory-consolidation-driver.d.ts +6 -4
  73. package/dist/core/runner/memory-consolidation-driver.js +5 -2
  74. package/dist/core/runner/memory-consolidation-write-judge.d.ts +17 -0
  75. package/dist/core/runner/memory-consolidation-write-judge.js +14 -0
  76. package/dist/core/runner/permission-rule-lanes.d.ts +16 -22
  77. package/dist/core/runner/permission-rule-lanes.js +13 -43
  78. package/dist/core/runner/person-path-rows.d.ts +244 -0
  79. package/dist/core/runner/person-path-rows.js +387 -0
  80. package/dist/core/runner/policy-throw.d.ts +34 -0
  81. package/dist/core/runner/policy-throw.js +25 -0
  82. package/dist/core/runner/prepare-acquire-reconcile.js +8 -1
  83. package/dist/core/runner/prepare-artifact.d.ts +5 -3
  84. package/dist/core/runner/prepare-artifact.js +2 -8
  85. package/dist/core/runner/prepare-ask-lane.d.ts +2 -1
  86. package/dist/core/runner/prepare-ask-lane.js +14 -16
  87. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
  88. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  89. package/dist/core/runner/prepare-context-lane.js +2 -2
  90. package/dist/core/runner/prepare-gate-stations.d.ts +3 -3
  91. package/dist/core/runner/prepare-gate-stations.js +1 -1
  92. package/dist/core/runner/prepare-hands-readface.d.ts +19 -10
  93. package/dist/core/runner/prepare-hands-readface.js +57 -43
  94. package/dist/core/runner/prepare-lsp.d.ts +15 -13
  95. package/dist/core/runner/prepare-lsp.js +11 -11
  96. package/dist/core/runner/prepare-policy-chain.d.ts +8 -4
  97. package/dist/core/runner/prepare-policy-chain.js +11 -10
  98. package/dist/core/runner/prepare-run-refs.js +1 -1
  99. package/dist/core/runner/prepare-task.js +24 -16
  100. package/dist/core/runner/prepare-turn-wiring.d.ts +13 -6
  101. package/dist/core/runner/prepare-turn-wiring.js +19 -8
  102. package/dist/core/runner/read-root-grant.d.ts +1 -25
  103. package/dist/core/runner/read-root-grant.js +1 -17
  104. package/dist/core/runner/retired-runner-deps.d.ts +2 -2
  105. package/dist/core/runner/run-brain-sinks.d.ts +1 -5
  106. package/dist/core/runner/run-brain-sinks.js +2 -4
  107. package/dist/core/runner/run-harness-handlers.js +5 -4
  108. package/dist/core/runner/run-identity-wiring.d.ts +4 -4
  109. package/dist/core/runner/run-identity-wiring.js +10 -1
  110. package/dist/core/runner/run-leg.d.ts +1 -1
  111. package/dist/core/runner/run-leg.js +22 -9
  112. package/dist/core/runner/run-settle-and-teardown.d.ts +0 -4
  113. package/dist/core/runner/run-settle-and-teardown.js +7 -4
  114. package/dist/core/runner/run-terminal-adoption.d.ts +1 -1
  115. package/dist/core/runner/run-terminal-adoption.js +1 -1
  116. package/dist/core/runner/run-turn-boundary.js +2 -2
  117. package/dist/core/runner/runtask.js +8 -8
  118. package/dist/core/runner/session-file-state-replay.d.ts +14 -4
  119. package/dist/core/runner/session-file-state-replay.js +8 -0
  120. package/dist/core/runner/stream-steer-verb.js +3 -0
  121. package/dist/core/runner/tool-end-body.d.ts +5 -4
  122. package/dist/core/runner/tool-end-body.js +4 -4
  123. package/dist/core/runner/tool-output-projection.d.ts +6 -4
  124. package/dist/core/runner/tool-output-projection.js +66 -47
  125. package/dist/core/runner-deps.d.ts +6 -3
  126. package/dist/core/shared-memory/types.d.ts +2 -2
  127. package/dist/core/shell-read-silence.d.ts +23 -0
  128. package/dist/core/shell-read-silence.js +18 -0
  129. package/dist/core/synthetic-read-verdict.d.ts +80 -21
  130. package/dist/core/synthetic-read-verdict.js +89 -18
  131. package/dist/core/task-event.d.ts +113 -25
  132. package/dist/core/task-spec.d.ts +44 -0
  133. package/dist/core/task-tool-shape.d.ts +1 -1
  134. package/dist/core/tool-catalog-entries.js +16 -2
  135. package/dist/core/tool-policy.d.ts +25 -15
  136. package/dist/core/tool-policy.js +9 -13
  137. package/dist/core/trace.d.ts +3 -2
  138. package/dist/core/types.d.ts +3 -3
  139. package/dist/core/types.js +1 -1
  140. package/dist/core/untrusted-text.d.ts +2 -2
  141. package/dist/core/user-attachment.d.ts +164 -0
  142. package/dist/core/user-attachment.js +437 -0
  143. package/dist/engine/compaction/compaction.js +10 -3
  144. package/dist/engine/compaction/utils.js +6 -0
  145. package/dist/engine/execution-env/node-execution-env.d.ts +19 -1
  146. package/dist/engine/execution-env/node-execution-env.js +125 -0
  147. package/dist/engine/harness/agent-harness.d.ts +12 -2
  148. package/dist/engine/harness/agent-harness.js +15 -3
  149. package/dist/engine/harness/types.d.ts +21 -0
  150. package/dist/engine/llm/content-block-render.d.ts +15 -0
  151. package/dist/engine/llm/content-block-render.js +10 -0
  152. package/dist/engine/llm/index.d.ts +1 -0
  153. package/dist/engine/llm/index.js +1 -0
  154. package/dist/engine/llm/types.d.ts +80 -4
  155. package/dist/engine/llm/types.js +43 -1
  156. package/dist/engine/session/import-validate.js +2 -0
  157. package/dist/index.d.ts +6 -4
  158. package/dist/index.js +2 -1
  159. package/dist/internal/harness-types.d.ts +1 -1
  160. package/dist/internal/llm.d.ts +2 -2
  161. package/dist/internal/llm.js +1 -1
  162. package/dist/orchestration/workflow.d.ts +4 -5
  163. package/dist/orchestration/workflow.js +2 -2
  164. package/dist/prompts/default.d.ts +1 -1
  165. package/dist/tools/artifact/artifact-tool.js +1 -1
  166. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  167. package/dist/tools/fs/dir-entries.d.ts +4 -10
  168. package/dist/tools/fs/dir-entries.js +4 -11
  169. package/dist/tools/fs/file-read-check.js +3 -3
  170. package/dist/tools/fs/fs-pdf.js +2 -4
  171. package/dist/tools/fs/fs-read.d.ts +47 -4
  172. package/dist/tools/fs/fs-read.js +63 -19
  173. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  174. package/dist/tools/fs/fs-search-tools.js +7 -2
  175. package/dist/tools/fs/fs-write.d.ts +1 -1
  176. package/dist/tools/fs/fs-write.js +7 -6
  177. package/dist/tools/fs/index.d.ts +7 -1
  178. package/dist/tools/fs/index.js +56 -34
  179. package/dist/tools/fs/read-deny-home.d.ts +19 -1
  180. package/dist/tools/fs/read-deny-home.js +32 -22
  181. package/dist/tools/fs/read-deny.d.ts +14 -0
  182. package/dist/tools/fs/read-deny.js +8 -0
  183. package/dist/tools/fs/repo-map.d.ts +3 -2
  184. package/dist/tools/fs/repo-map.js +11 -3
  185. package/dist/tools/fs/safety.d.ts +41 -2
  186. package/dist/tools/fs/safety.js +12 -3
  187. package/dist/tools/fs/shell-read-check.d.ts +12 -3
  188. package/dist/tools/fs/shell-read-check.js +44 -24
  189. package/dist/tools/fs/shell-read-device.d.ts +1 -1
  190. package/dist/tools/fs/shell-read-face.d.ts +15 -0
  191. package/dist/tools/fs/shell-read-face.js +5 -0
  192. package/dist/tools/fs/shell-read-judge.d.ts +9 -4
  193. package/dist/tools/fs/shell-read-judge.js +1 -1
  194. package/dist/tools/loop-tick.d.ts +2 -2
  195. package/package.json +3 -1
  196. package/test/export-surface.snapshot.json +39 -1
  197. package/wire-changes/core@7.50.0.json +1699 -0
  198. package/wire-changes/core@7.50.1.json +65 -0
  199. package/dist/core/runner/persisted-path-rows.d.ts +0 -88
  200. package/dist/core/runner/persisted-path-rows.js +0 -108
@@ -21,11 +21,13 @@ import { exitGate, passAskFace } from "./runner/gate-exit.js";
21
21
  import { readOrgRuleLane, readPersistedRuleLane } from "./rule-lane-reads.js";
22
22
  import { traceHookCrash } from "./gate-crash-trace.js";
23
23
  import { createGateDeadline, awaitWriteProtectionJudgment, withGateTimeout } from "./gate-bounded-await.js";
24
- import { thrownText } from "./thrown-value.js";
24
+ import { policyThrowLine, reportPolicyThrow } from "./runner/policy-throw.js";
25
25
  const QUESTION_TOOL_NAMES = new Set([ASK_USER_QUESTION_TOOL_NAME]);
26
26
  function parkWith(suspendAsk, parkArgs, carry) {
27
27
  return suspendAsk(...parkArgs, carry);
28
28
  }
29
+ const ruleCallUnreadableOf = (lane) => lane?.unreadable === true && lane.reason !== undefined;
30
+ const ruleStoreUnreadableOf = (lane) => lane?.unreadable === true && lane.reason === undefined;
29
31
  export async function runGateLanes(pass) {
30
32
  const { input, toolName, toolCallId, callSignal, ledger, notifier, screening, adjudicate, resolveAsk, suspendAsk } = pass;
31
33
  const boundedAwaitDeadlineMs = createGateDeadline(input, notifier);
@@ -42,11 +44,12 @@ export async function runGateLanes(pass) {
42
44
  targetUnresolved: pass.targetUnresolved === true,
43
45
  });
44
46
  const readFaceNow = input.readFace?.();
45
- const askOriginFacts = (org, ruleCallUnreadable) => ({
47
+ const askOriginFacts = (org, lane) => ({
46
48
  contentQuestion: heldBy("lanes.contentQuestionClass", QUESTION_TOOL_NAMES, pass.req),
47
49
  markedUnresolvable: markedUnresolvable(),
48
50
  org,
49
- ...(ruleCallUnreadable ? { ruleCallUnreadable } : {}),
51
+ ...(ruleCallUnreadableOf(lane) ? { ruleCallUnreadable: true } : {}),
52
+ ...(ruleStoreUnreadableOf(lane) ? { ruleStoreUnreadable: true } : {}),
50
53
  tightened: pass.tightenedBy,
51
54
  mandateMarks: mandateMarksNow(),
52
55
  ...(input.toolMarkSources !== undefined ? { toolMarkSources: input.toolMarkSources } : {}),
@@ -118,13 +121,12 @@ export async function runGateLanes(pass) {
118
121
  };
119
122
  const orgAnswer = await applyOrgLayer(pass.decision, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput);
120
123
  pass.decision = orgAnswer.decision;
121
- const ruleCallUnreadableOf = (lane) => lane?.unreadable === true && lane.reason !== undefined;
122
124
  const readPersistedRules = (req) => readPersistedRuleLane(input.persistedRules, req, input.abortSignal);
123
125
  const reread = pass.policyRewrite !== undefined ? withArgsReading(pass.req, pass.policyRewrite, pass.callSignal ?? input.abortSignal) : undefined;
124
126
  const laneReq = reread !== undefined ? await reread : { ...pass.req, ...(pass.policyRewrite !== undefined ? { args: pass.policyRewrite } : {}) };
125
127
  const laneAnswer = input.persistedRules !== undefined && wireOf("lanes.persistedLaneSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME ? await readPersistedRules(laneReq) : undefined;
126
128
  {
127
- const tightened = applyPersistedTightening(pass.decision, laneAnswer);
129
+ const tightened = applyPersistedTightening(pass.decision, laneAnswer, laneReq);
128
130
  pass.decision = tightened.decision;
129
131
  if (tightened.tightened === "deny")
130
132
  pass.deniedBy = "persisted_rule";
@@ -146,7 +148,7 @@ export async function runGateLanes(pass) {
146
148
  pass.req.targetReading = laneReq.targetReading;
147
149
  }
148
150
  const ruleMayClear = () => ruleClearanceOf(pass.decision.action === "ask" ? pass.decision : undefined, mandateMarksNow()).ruleMayClear;
149
- const classMayClear = () => pass.decision.action === "ask" && classifyAsk(pass.decision, askOriginFacts(orgAnswer.org, ruleCallUnreadableOf(laneAnswer))).row.ruleMayClear;
151
+ const classMayClear = () => pass.decision.action === "ask" && classifyAsk(pass.decision, askOriginFacts(orgAnswer.org, laneAnswer)).row.ruleMayClear;
150
152
  let personalEvidence = { dotsAbsent: input.persistedRules === undefined ? "not_wired" : "not_adjudicated" };
151
153
  if (laneAnswer?.unreadable === true)
152
154
  personalEvidence = { dotsAbsent: "unavailable" };
@@ -234,7 +236,7 @@ export async function runGateLanes(pass) {
234
236
  const { allowRuleShadowed: _claimed, ...standing } = pass.decision;
235
237
  pass.decision = shadows.length > 0 ? { ...standing, allowRuleShadowed: shadows } : standing;
236
238
  }
237
- const originFacts = askOriginFacts(orgAnswer.org, ruleCallUnreadableOf(laneAnswer));
239
+ const originFacts = askOriginFacts(orgAnswer.org, laneAnswer);
238
240
  let noRulingMinted = false;
239
241
  const fallbackAskOf = (standingAsk, fallback, latestBlockedAction) => {
240
242
  const { classifierNoRuling: _why, ...standing } = standingAsk;
@@ -553,9 +555,10 @@ export async function runGateLanes(pass) {
553
555
  recheck = refuseOutOfContractDecision(await (callSignal !== undefined ? adjudicate(editReq, callSignal) : adjudicate(editReq)));
554
556
  }
555
557
  catch (err) {
558
+ reportPolicyThrow((e) => traceHookCrash(input, e, notifier), "the tool policy's re-check of the approved edit", { toolName, toolCallId }, err, callSignal ?? input.abortSignal);
556
559
  recheck = {
557
560
  action: "deny",
558
- message: `policy re-check of the approved edit for "${toolName}" errored: ${thrownText(err)}`,
561
+ message: `policy re-check of the approved edit for "${toolName}" errored: ${policyThrowLine(err)}`,
559
562
  };
560
563
  }
561
564
  }
@@ -582,13 +585,12 @@ export async function runGateLanes(pass) {
582
585
  editDenied = recheck;
583
586
  break;
584
587
  }
585
- let editRuleCallUnreadable = false;
588
+ let editLane;
586
589
  const editReread = withArgsReading(pass.req, editArgs, callSignal ?? input.abortSignal);
587
590
  const editLaneReq = editReread !== undefined ? await editReread : { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
588
591
  if (input.persistedRules !== undefined && wireOf("lanes.persistedEditSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME) {
589
- const editLane = await readPersistedRules(editLaneReq);
590
- editRuleCallUnreadable = ruleCallUnreadableOf(editLane);
591
- const editTightened = applyPersistedTightening(recheck, editLane);
592
+ editLane = await readPersistedRules(editLaneReq);
593
+ const editTightened = applyPersistedTightening(recheck, editLane, editLaneReq);
592
594
  recheck = editTightened.decision;
593
595
  if (editTightened.tightened === "deny")
594
596
  pass.deniedBy = "persisted_rule";
@@ -619,7 +621,7 @@ export async function runGateLanes(pass) {
619
621
  }
620
622
  const editAskSnapshot = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
621
623
  markedThisRound = undefined;
622
- const editFacts = askOriginFacts(editOrg.org, editRuleCallUnreadable);
624
+ const editFacts = askOriginFacts(editOrg.org, editLane);
623
625
  const editAskDecision = stampAskOrigin(editAskSnapshot, editFacts, classifyStanding(editAskSnapshot, editFacts));
624
626
  if (input.permissionMode !== undefined && pass.askClassification !== undefined) {
625
627
  const editVerdict = arbitrateByMode(input.permissionMode(), pass.askClassification, { ...(editAskDecision.origin !== undefined ? { origin: editAskDecision.origin } : {}), ...(editAskDecision.mandate !== undefined ? { mandate: editAskDecision.mandate } : {}) });
@@ -360,8 +360,8 @@ export type GateDisposition = {
360
360
  * to anyone: the mode, the disclosed class word (`askClass`, the first member of the folded set in `ASK_CLASSES`
361
361
  * order — one of {@link releasableAskClassesFor} on the call's face), the whole set, and the origin / mandate words the card would
362
362
  * have shown. `reason` is a member of {@link import("./ask-origin.js").MODE_RELEASE_REASONS} (`isModeReleaseReason`):
363
- * `"rule_unreadable"` marks a release over a rule the call could not be read against, `"target_unresolved"` a release (the
364
- * shell face) over a write target whose reading could not say where it leads; absent, the release names none. The record carries no face, so the
363
+ * `"target_unresolved"` marks a release (the shell face) over a write target whose reading could not say where it leads;
364
+ * absent, the release names none. The record carries no face, so the
365
365
  * screen asks that SOME face releases the class.
366
366
  */
367
367
  export interface ModeRelease {
@@ -47,6 +47,8 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
47
47
  "memory.consolidation_lease_unavailable",
48
48
  "memory.consolidation_backend_failed",
49
49
  "memory.consolidation_gate_failed",
50
+ "memory.consolidation_write_refused",
51
+ "memory.landing_unstable",
50
52
  "memory.active_set_collapse",
51
53
  "memory.challenge_ledger_oversize",
52
54
  "memory.control_plane_rebuilt",
@@ -939,6 +939,14 @@ export interface UserPromptSubmitContext {
939
939
  * {@link source}, and absence means "anonymous caller", exactly as on the event face.
940
940
  */
941
941
  actor?: ActorAssertion;
942
+ /**
943
+ * The files declared on this objective (`TaskSpec.userAttachments`), in order — present only on `source:"objective"`
944
+ * when the objective declares any. A path entry shows its path; a host entry its name and byte count, never its
945
+ * bytes. Blocking the objective reads and delivers none of them. A hook does not govern the engine's reading of an
946
+ * allowed attachment (PreToolUse does not run for it either): a deployment that must keep a path from the model
947
+ * writes a deny rule, which the attachment read honors like the Read tool does.
948
+ */
949
+ userAttachments?: readonly import("./user-attachment.js").UserAttachmentDeclaration[];
942
950
  }
943
951
  /** #281 件A — the context of a {@link Hooks.postToolBatch} invocation (third parameter, additive —
944
952
  * deliberately NOT folded into `meta`, whose presence already means "the engine injected here"). */
@@ -1105,35 +1113,35 @@ export interface PersistedRuleHitRule {
1105
1113
  * deny > ask > allow — already chose it). `rules` is non-empty by contract — an empty set covers nothing
1106
1114
  * and is not a hit (the normalizer reads it as a clean negative).
1107
1115
  *
1108
- * Why dots and not an id: a persisted rule HAS no scalar id — it is a (behavior, text, scope) group whose
1109
- * identity is the set of add dots that redeemed it, and two concurrently approved records for one text
1110
- * each carry their own. The set is the identity; a joined string would not be one.
1116
+ * Why dots and not an id: a persisted rule HAS no scalar id — it is a (behavior, text, scope) group whose identity is
1117
+ * the set of add dots that redeemed it, and two concurrently approved records for one text each carry their own.
1111
1118
  *
1112
- * The pre-375 single-rule object shape (`{ rule, dots? }`) and the pre-behavior `{ rules }` shape are
1113
- * NOT accepted: on this loosening seam an unrecognized answer degrades to a clean negative (more asks),
1114
- * never to a match — the safe failure shape for a lane that missed the same-window upgrade.
1119
+ * The pre-375 single-rule object shape (`{ rule, dots? }`) and the pre-behavior `{ rules }` shape are NOT accepted:
1120
+ * on this loosening seam an unrecognized answer degrades to a clean negative (more asks), never to a match.
1115
1121
  */
1116
1122
  export interface PersistedRuleHit {
1117
1123
  /** What the deciding rule(s) say about the call — the closed set's word. */
1118
1124
  readonly behavior: import("./permission-rule-model.js").RuleBehavior;
1119
1125
  /** The rule set the verdict stands on. Non-empty. */
1120
1126
  readonly rules: readonly PersistedRuleHitRule[];
1127
+ /** A path DENY's coordinates (where the reaching row met the call): the gate's exits mint the refusal's card from it. */
1128
+ readonly denial?: import("../tools/fs/safety.js").PathDenialEvidence;
1121
1129
  }
1122
1130
  /**
1123
- * design/252 G-2 — the answer a lane gives when it could not READ its store (no `reason`; the CALL form is `reason`'s).
1131
+ * design/252 G-2 — the answer a lane gives when it could not READ its store, or a partition of it (no `reason`; the CALL form is `reason`'s).
1124
1132
  *
1125
- * The store form's DECISION meaning is identical to "no rule": the store's rows — allow, ask and deny alike — take
1126
- * no part in this call's decision (skipped, the way a settings file with errors is skipped whole; the lane announces
1127
- * it, and the rows apply again once a read succeeds). What it changes is the RECORD: without it, an unreadable store
1128
- * and a store that holds no matching rule are the same answer (`undefined`), and the ask's evidence would report one
1129
- * as the other — `"no_match"` ("we looked and found nothing") vs `"unavailable"` ("we could not look").
1133
+ * The store form fails CLOSED: a deny or ask row inside what could not be read may forbid this call and an allow beside it
1134
+ * is no yes, so the decision becomes a real-approval ask (origin `rule_store_unavailable`, class `governance_unavailable`:
1135
+ * no automatic lane clears it, `bypassPermissions` keeps it for a person, headless it is refused) until a read succeeds
1136
+ * again; a deny or ask row that WAS read still stands. The RECORD says `"unavailable"`, not `"no_match"`; the lane
1137
+ * announces the skipped partition once per run.
1130
1138
  */
1131
1139
  export interface PersistedRuleUnreadable {
1132
1140
  readonly unreadable: true;
1133
- /** WHY the lane could not read: absent for a store read failure (its rows take no part); for a CALL the lane could
1134
- * not read against the person's standing deny/ask rows (an expansion where a rule reads a word, an unterminated quote,
1135
- * a syntax error — `programRunReachOf`'s `unreadable` word), the lexer's reason — the call then fails closed: an allow
1136
- * tightens to a real-approval ask of origin `rule_store_unavailable`, the reason in its message. */
1141
+ /** WHY the lane could not read: absent for a store (or partition) read failure — the store form, a real-approval ask of
1142
+ * class `governance_unavailable`; for a CALL the lane could not read against the person's standing deny/ask rows (an
1143
+ * expansion where a rule reads a word, an unterminated quote, a syntax error) the lexer's reason — the call form, an allow
1144
+ * tightens to a real-approval ask of class `rule_unreadable`. Both carry the origin `rule_store_unavailable`. */
1137
1145
  readonly reason?: string;
1138
1146
  }
1139
1147
  /**
@@ -1610,10 +1618,10 @@ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, All
1610
1618
  persistedRules?: {
1611
1619
  /** The lane's verdict for this call — a {@link PersistedRuleHit} naming the behavior that spoke and
1612
1620
  * the rule set it stands on, a coverage-carrying clean negative, an unreadable report, or
1613
- * `undefined` (the store was READ and no rule speaks). Must not throw — and a store that cannot be
1614
- * read answers `{ unreadable: true }`, not `undefined`: the DECISION is the same (its rows take no part, as
1615
- * for a thrown or timed-out lane) but the evidence records `"unavailable"`, not `"no_match"`; the lane owns
1616
- * announcing the skip. A call unreadable against the rows answers `{ unreadable: true, reason }` (fails closed).
1621
+ * `undefined` (the store was READ and no rule speaks). Must not throw — and a store (or a partition of it) that cannot
1622
+ * be read answers `{ unreadable: true }`, not `undefined`: it fails closed (a row nobody could read may forbid the call;
1623
+ * the decision becomes a real-approval ask) and the evidence records `"unavailable"`, not `"no_match"`; the lane owns
1624
+ * announcing the skip. A call unreadable against the rows answers `{ unreadable: true, reason }` (the call form).
1617
1625
  *
1618
1626
  * For an ALLOW the set is the COVERAGE SET — each member's canonical text plus its add DOTS. A
1619
1627
  * single rule admitting the whole command is a one-element set; a compound admitted by the
@@ -31,6 +31,7 @@ const HANDLED_HUMAN_INPUT_KEYS = {
31
31
  delivery: "forwarded",
32
32
  principal: "forwarded",
33
33
  entryId: "forwarded",
34
+ attachments: "forwarded",
34
35
  };
35
36
  export function buildHumanInputEvent(input) {
36
37
  void HANDLED_HUMAN_INPUT_KEYS;
@@ -45,6 +46,7 @@ export function buildHumanInputEvent(input) {
45
46
  ...(input.actor !== undefined ? { actor: snapshotActorAssertion(input.actor) } : {}),
46
47
  ...(input.principal !== undefined ? { principal: input.principal } : {}),
47
48
  ...(input.entryId !== undefined ? { entryId: input.entryId } : {}),
49
+ ...(input.attachments !== undefined && input.attachments.length > 0 ? { attachments: input.attachments.map((a) => ({ ...a })) } : {}),
48
50
  };
49
51
  }
50
52
  export function buildAppliedHumanInputEvent(input) {
@@ -79,6 +79,9 @@ export declare class LspDiagnosticsRegistry {
79
79
  */
80
80
  drain(runIdent: string): LspFileDiagnostics[];
81
81
  }
82
+ /** A drain with every file the run's read face withholds left out (`withheldOf` answers the batch: a withheld file is in
83
+ * the map; a judgment that fails withholds them all). The run's drain already marked them delivered, so they do not come back. */
84
+ export declare function withholdListedDiagnostics(files: LspFileDiagnostics[], withheldOf: (uris: readonly string[]) => Promise<ReadonlyMap<string, unknown>>): Promise<LspFileDiagnostics[]>;
82
85
  /**
83
86
  * CC 198 `formatDiagnosticsSummary` (198:320480-320499), verbatim shape:
84
87
  * ```
@@ -74,6 +74,12 @@ export class LspDiagnosticsRegistry {
74
74
  return out;
75
75
  }
76
76
  }
77
+ export async function withholdListedDiagnostics(files, withheldOf) {
78
+ if (files.length === 0)
79
+ return files;
80
+ const withheld = await withheldOf(files.map((f) => f.uri)).catch(() => undefined);
81
+ return withheld === undefined ? [] : files.filter((f) => !withheld.has(f.uri));
82
+ }
77
83
  function basenameOfUri(uri) {
78
84
  const path = uri.replace(/^file:\/\//, "");
79
85
  const idx = path.lastIndexOf("/");
@@ -1,4 +1,4 @@
1
- import type { LspSession, LspOperation, LspRequestParams, LspResult, LspTransport, LspReadText } from "./lsp.js";
1
+ import type { LspSession, LspOperation, LspRequestParams, LspResult, LspTransport, LspReadText, LspReadGate } from "./lsp.js";
2
2
  import type { LspDiagnosticsRegistry } from "./lsp-diagnostics.js";
3
3
  export interface SessionWarmup {
4
4
  /** Empty results within this window after session creation are retried (a freshly-spawned server answers
@@ -24,9 +24,14 @@ export declare class TransportLspSession implements LspSession {
24
24
  * we ourselves sent removes the round-trip entirely (`uriToPath` stays the fallback for a server that
25
25
  * re-spells the URI). */
26
26
  private readonly openedUris;
27
- /** In-flight per-file syncs — concurrent ops on the same new file share ONE read/didOpen. Each entry owns a
28
- * {@link SharedAbortScope} so ONE caller's abort no longer cancels the read the others are waiting on
29
- * (HRD-LSP-12). */
27
+ /** Files carried over from a session that died ({@link warmOpen}): not read yet — the next gated sync opens each one
28
+ * its caller's gate admits, before that caller's query, and drops the rest. */
29
+ private readonly carried;
30
+ /** Per file, the generation of the newest sync started for it — the only one that may still publish. */
31
+ private readonly syncGen;
32
+ /** Per file, the newest sync in flight (dropped when it settles); concurrent by-name syncs share ONE read/didOpen.
33
+ * Each entry owns a {@link SharedAbortScope} so ONE caller's abort no longer cancels the read the others are waiting on
34
+ * (HRD-LSP-12). `byName` = a read with no gate (the only kind another caller may share). */
30
35
  private readonly syncing;
31
36
  private readonly createdAt;
32
37
  /** HRD-LSP-11: the warm-up barrier as a shared JOB, not a boolean flag set before the await (which let the
@@ -47,7 +52,7 @@ export declare class TransportLspSession implements LspSession {
47
52
  * closing one mid-op turns a healthy answer into `server_terminated` for a caller that did nothing wrong. */
48
53
  get inFlight(): number;
49
54
  private activeRequests;
50
- request(op: LspOperation, params: LspRequestParams, signal?: AbortSignal): Promise<LspResult>;
55
+ request(op: LspOperation, params: LspRequestParams, signal?: AbortSignal, gate?: LspReadGate): Promise<LspResult>;
51
56
  private requestOnce;
52
57
  /**
53
58
  * HRD-LSP-11: run (or join) this session's ONE warm-up barrier.
@@ -60,13 +65,28 @@ export declare class TransportLspSession implements LspSession {
60
65
  * and one caller's abort neither cancels the shared barrier nor makes the others wait for it.
61
66
  */
62
67
  private warmupBarrierOnce;
63
- /** Re-sync every opened file (+ the queried one) with its CURRENT on-disk text, in parallel. Returns the
64
- * QUERIED file's outcome — the one the op's honesty depends on (HRD-LSP-7). */
68
+ /** Re-sync every opened and carried file (+ the queried one) with its CURRENT on-disk text, in parallel, each through
69
+ * the caller's gate. Returns the QUERIED file's outcome — the one the op's honesty depends on (HRD-LSP-7). */
65
70
  private syncOpenedFiles;
66
- /** didOpen a new file / didChange (full-text replace) an opened one whose disk content moved. Never throws:
67
- * it REPORTS failure instead (`{ok:false}`) — an unreadable file no longer looks like a clean sync to the
68
- * caller. Concurrent callers share one in-flight sync, each keeping its OWN cancellation (HRD-LSP-12). */
69
- private syncOne;
71
+ /**
72
+ * The session's one read port. Every sync of a document takes the next generation of that document and publishes only if
73
+ * it is still the newest one started when its read settles — so a sync that publishes after another started after that
74
+ * publication and opened its object after it: an older object's text never lands after a newer one's. No sync ever waits
75
+ * for another's read (a read that never settles, abandoned or not, holds up nobody else). A superseded sync publishes nothing: its waiters take the newest sync's outcome, or sync
76
+ * again when that one is gone or abandoned. With a gate, the file is read only from the object the gate admitted and
77
+ * holds, never by its name again; a file the newest gate does not admit is closed (not read, not re-sent, its text
78
+ * dropped from the server's open documents). With none (a caller with no read face), by name through `readText` — and
79
+ * such a sync shares a by-name read already in flight for the document, each joiner keeping its OWN cancellation
80
+ * (HRD-LSP-12); a gated read is its caller's admission and is never shared.
81
+ */
82
+ private syncGated;
83
+ private syncInTurn;
84
+ /** didClose a document the caller's gate refused and forget it (a later caller whose gate admits it opens it afresh). */
85
+ private closeRefused;
86
+ /** didOpen a new file / didChange (full-text replace) an opened one whose disk content moved, from the text `read`
87
+ * returns — only while `isNewest()` holds when the read settles (else {@link SUPERSEDED}, nothing sent). Never throws: it
88
+ * REPORTS failure instead (`{ok:false}`) — an unreadable file no longer looks like a clean sync to the caller. */
89
+ private publishRead;
70
90
  /** Structural fallback classification for errors carrying no {@link lspFailureOf} brand (a
71
91
  * third-party transport, e.g. the TOB WS lane) — abort signal and transport liveness are the only
72
92
  * transport-neutral facts available; everything else is a server fault. Cannot produce
@@ -78,19 +98,21 @@ export declare class TransportLspSession implements LspSession {
78
98
  * project config only "sees" its open files (e.g. tsserver inferred project), so without the carry-over,
79
99
  * cross-file ops (references/calls) silently degrade to single-file after a heal. */
80
100
  openedFiles(): string[];
81
- /** Re-open carried files on a fresh session (heal). Best-effort per file (a deleted file just skips).
82
- * Returns how many files did NOT make it, so the manager's heal log can say when a carry-over was lost
83
- * instead of leaving a silently single-file server behind (HRD-LSP-1 disclosure half). */
84
- warmOpen(filePaths: string[], signal?: AbortSignal): Promise<{
101
+ /** Carry files over to a fresh session (heal): nothing is read here — no caller's read face is in hand — and the next
102
+ * gated sync (an op, a change notice) opens each file its caller's gate admits before that caller's query, so no op
103
+ * is answered from a document set missing an admitted carried file (HRD-LSP-1). `deferred` counts the files carried;
104
+ * a carried file that later cannot be read is logged by that sync (`lsp_sync_skipped`). */
105
+ warmOpen(filePaths: string[], _signal?: AbortSignal): Promise<{
85
106
  opened: number;
86
107
  failed: number;
108
+ deferred: number;
87
109
  }>;
88
110
  /**
89
111
  * design/121: push the file's CURRENT text to the server WITHOUT an op (didOpen/didChange as
90
112
  * appropriate). The agent's edit/write tools call this fire-and-forget so the server re-analyzes
91
113
  * and pushes publishDiagnostics — the session's normal lazy sync only runs on the next LSP op,
92
- * which may never come. Never throws (same best-effort contract as syncOne).
114
+ * which may never come. Through the caller's `gate`, as every read. Never throws (same best-effort contract as publishRead).
93
115
  */
94
- notifyFileChanged(filePath: string, signal?: AbortSignal): Promise<void>;
116
+ notifyFileChanged(filePath: string, signal?: AbortSignal, gate?: LspReadGate): Promise<void>;
95
117
  close(): Promise<void>;
96
118
  }
@@ -1,6 +1,14 @@
1
1
  import { uriToPath, buildRequest, callHierarchyMethod, parseResult, pathToUri } from "./lsp-protocol.js";
2
- import { lspFailureOf, SharedAbortScope, settleOnAbort } from "./lsp.js";
2
+ import { lspFailureOf, MAX_LSP_FILE_BYTES, SharedAbortScope, settleOnAbort } from "./lsp.js";
3
3
  import { thrownText } from "./thrown-value.js";
4
+ const UTF8 = new TextDecoder("utf-8", { ignoreBOM: true });
5
+ async function readHeldText(held, signal) {
6
+ const bytes = await held.readBytes(MAX_LSP_FILE_BYTES, signal);
7
+ if (!bytes.ok)
8
+ throw bytes.error;
9
+ return UTF8.decode(bytes.value);
10
+ }
11
+ const SUPERSEDED = Symbol("superseded");
4
12
  const WARMUP_MAX_ATTEMPTS = 3;
5
13
  export class TransportLspSession {
6
14
  transport;
@@ -10,6 +18,8 @@ export class TransportLspSession {
10
18
  warmup;
11
19
  opened = new Map();
12
20
  openedUris = new Map();
21
+ carried = new Set();
22
+ syncGen = new Map();
13
23
  syncing = new Map();
14
24
  createdAt = Date.now();
15
25
  warmupBarrier;
@@ -44,20 +54,20 @@ export class TransportLspSession {
44
54
  return this.activeRequests;
45
55
  }
46
56
  activeRequests = 0;
47
- async request(op, params, signal) {
57
+ async request(op, params, signal, gate) {
48
58
  this.activeRequests += 1;
49
59
  try {
50
- return await this.requestOnce(op, params, signal);
60
+ return await this.requestOnce(op, params, signal, gate);
51
61
  }
52
62
  finally {
53
63
  this.activeRequests -= 1;
54
64
  }
55
65
  }
56
- async requestOnce(op, params, signal) {
66
+ async requestOnce(op, params, signal, gate) {
57
67
  const built = buildRequest(op, params);
58
68
  if (!built)
59
69
  return { kind: "none", reason: "unsupported_operation" };
60
- const sync = await this.syncOpenedFiles(params.filePath, signal);
70
+ const sync = await this.syncOpenedFiles(params.filePath, signal, gate);
61
71
  if (!sync.ok) {
62
72
  const reason = signal?.aborted ? "cancelled" : (sync.reason ?? "file_sync_failed");
63
73
  this.log("lsp_op_degraded", { op, file: params.filePath, reason, err: sync.error });
@@ -131,64 +141,109 @@ export class TransportLspSession {
131
141
  entry.scope.join(signal);
132
142
  await settleOnAbort(entry.job, signal, () => undefined);
133
143
  }
134
- async syncOpenedFiles(queried, signal) {
135
- const files = new Set(this.opened.keys());
144
+ async syncOpenedFiles(queried, signal, gate) {
145
+ const files = new Set([...this.opened.keys(), ...this.carried]);
136
146
  files.add(queried);
137
- const outcomes = await Promise.all([...files].map(async (fp) => [fp, await this.syncOne(fp, signal)]));
147
+ const outcomes = await Promise.all([...files].map(async (fp) => [fp, await this.syncGated(fp, signal, gate)]));
138
148
  return outcomes.find(([fp]) => fp === queried)?.[1] ?? { ok: true };
139
149
  }
140
- syncOne(filePath, signal) {
141
- const inflight = this.syncing.get(filePath);
142
- if (inflight && !inflight.scope.signal.aborted) {
143
- inflight.scope.join(signal);
144
- return settleOnAbort(inflight.job, signal, () => ({ ok: false, error: "aborted by the caller" }));
150
+ async syncGated(filePath, signal, gate) {
151
+ const outcome = await this.syncInTurn(filePath, signal, gate);
152
+ if (outcome.ok || signal?.aborted !== true)
153
+ this.carried.delete(filePath);
154
+ return outcome;
155
+ }
156
+ syncInTurn(filePath, signal, gate) {
157
+ const aborted = () => ({ ok: false, error: "aborted by the caller" });
158
+ const newest = this.syncing.get(filePath);
159
+ if (gate === undefined && newest?.byName === true && !newest.scope.signal.aborted) {
160
+ newest.scope.join(signal);
161
+ return settleOnAbort(newest.job, signal, aborted);
145
162
  }
163
+ const gen = (this.syncGen.get(filePath) ?? 0) + 1;
164
+ this.syncGen.set(filePath, gen);
165
+ const isNewest = () => this.syncGen.get(filePath) === gen;
146
166
  const scope = new SharedAbortScope();
147
167
  scope.join(signal);
148
- const entry = {
149
- scope,
150
- job: (async () => {
151
- let text;
152
- try {
153
- text = await this.readText(filePath, scope.signal);
154
- }
155
- catch (e) {
156
- const error = thrownText(e);
157
- this.log("lsp_sync_skipped", { file: filePath, error, opened: this.opened.has(filePath) });
158
- return { ok: false, error };
168
+ const entry = { scope, byName: gate === undefined, job: Promise.resolve({ ok: true }) };
169
+ entry.job = (async () => {
170
+ let published;
171
+ if (gate === undefined)
172
+ published = await this.publishRead(filePath, () => this.readText(filePath, scope.signal), isNewest);
173
+ else {
174
+ const admission = await gate(filePath, scope.signal).catch((e) => ({ ok: false, reason: `the read boundary could not judge this file (${thrownText(e)})` }));
175
+ if (!admission.ok) {
176
+ if (isNewest() && !scope.signal.aborted)
177
+ this.closeRefused(filePath);
178
+ return { ok: false, error: admission.reason };
159
179
  }
160
- const cur = this.opened.get(filePath);
161
- const uri = pathToUri(filePath);
162
- if (!cur) {
163
- this.transport.notify("textDocument/didOpen", {
164
- textDocument: { uri, languageId: this.languageId, version: 1, text },
165
- });
166
- if (this.transport.closed === true) {
167
- this.log("lsp_sync_notify_dropped", { file: filePath, kind: "didOpen" });
168
- return { ok: false, error: "didOpen was not delivered (transport closed)", reason: "server_terminated" };
169
- }
170
- this.opened.set(filePath, { version: 1, text });
171
- this.openedUris.set(uri, filePath);
172
- }
173
- else if (cur.text !== text) {
174
- const version = cur.version + 1;
175
- this.transport.notify("textDocument/didChange", { textDocument: { uri, version }, contentChanges: [{ text }] });
176
- if (this.transport.closed === true) {
177
- this.log("lsp_sync_notify_dropped", { file: filePath, kind: "didChange", version });
178
- return { ok: false, error: "didChange was not delivered (transport closed)", reason: "server_terminated" };
179
- }
180
- this.opened.set(filePath, { version, text });
181
- this.log("lsp_file_resync", { file: filePath, version });
182
- }
183
- return { ok: true };
184
- })(),
185
- };
180
+ const held = admission.object;
181
+ published = await this.publishRead(filePath, () => readHeldText(held, scope.signal).finally(() => held.close().catch(() => undefined)), isNewest);
182
+ }
183
+ if (published !== SUPERSEDED)
184
+ return published;
185
+ if (scope.signal.aborted)
186
+ return aborted();
187
+ const successor = this.syncing.get(filePath);
188
+ if (successor !== undefined && successor !== entry && !successor.scope.signal.aborted) {
189
+ successor.scope.join(scope.signal);
190
+ return successor.job;
191
+ }
192
+ return this.syncInTurn(filePath, scope.signal, gate);
193
+ })();
186
194
  this.syncing.set(filePath, entry);
187
195
  const settled = entry.job.finally(() => {
188
196
  if (this.syncing.get(filePath) === entry)
189
197
  this.syncing.delete(filePath);
190
198
  });
191
- return settleOnAbort(settled, signal, () => ({ ok: false, error: "aborted by the caller" }));
199
+ return settleOnAbort(settled, signal, aborted);
200
+ }
201
+ closeRefused(filePath) {
202
+ this.carried.delete(filePath);
203
+ if (!this.opened.has(filePath))
204
+ return;
205
+ const uri = pathToUri(filePath);
206
+ this.opened.delete(filePath);
207
+ this.openedUris.delete(uri);
208
+ this.transport.notify("textDocument/didClose", { textDocument: { uri } });
209
+ this.log("lsp_doc_closed_refused", { file: filePath });
210
+ }
211
+ async publishRead(filePath, read, isNewest) {
212
+ let text;
213
+ try {
214
+ text = await read();
215
+ }
216
+ catch (e) {
217
+ const error = thrownText(e);
218
+ this.log("lsp_sync_skipped", { file: filePath, error, opened: this.opened.has(filePath) });
219
+ return { ok: false, error };
220
+ }
221
+ if (!isNewest())
222
+ return SUPERSEDED;
223
+ const cur = this.opened.get(filePath);
224
+ const uri = pathToUri(filePath);
225
+ if (!cur) {
226
+ this.transport.notify("textDocument/didOpen", {
227
+ textDocument: { uri, languageId: this.languageId, version: 1, text },
228
+ });
229
+ if (this.transport.closed === true) {
230
+ this.log("lsp_sync_notify_dropped", { file: filePath, kind: "didOpen" });
231
+ return { ok: false, error: "didOpen was not delivered (transport closed)", reason: "server_terminated" };
232
+ }
233
+ this.opened.set(filePath, { version: 1, text });
234
+ this.openedUris.set(uri, filePath);
235
+ }
236
+ else if (cur.text !== text) {
237
+ const version = cur.version + 1;
238
+ this.transport.notify("textDocument/didChange", { textDocument: { uri, version }, contentChanges: [{ text }] });
239
+ if (this.transport.closed === true) {
240
+ this.log("lsp_sync_notify_dropped", { file: filePath, kind: "didChange", version });
241
+ return { ok: false, error: "didChange was not delivered (transport closed)", reason: "server_terminated" };
242
+ }
243
+ this.opened.set(filePath, { version, text });
244
+ this.log("lsp_file_resync", { file: filePath, version });
245
+ }
246
+ return { ok: true };
192
247
  }
193
248
  static classifyUnbranded(signal, transport) {
194
249
  if (signal?.aborted)
@@ -201,15 +256,20 @@ export class TransportLspSession {
201
256
  return this.transport.closed === true;
202
257
  }
203
258
  openedFiles() {
204
- return [...this.opened.keys()];
259
+ return [...new Set([...this.opened.keys(), ...this.carried])];
205
260
  }
206
- async warmOpen(filePaths, signal) {
207
- const outcomes = await Promise.all(filePaths.map((fp) => this.syncOne(fp, signal)));
208
- const failed = outcomes.filter((o) => !o.ok).length;
209
- return { opened: outcomes.length - failed, failed };
261
+ async warmOpen(filePaths, _signal) {
262
+ let deferred = 0;
263
+ for (const fp of filePaths) {
264
+ if (this.opened.has(fp) || this.carried.has(fp))
265
+ continue;
266
+ this.carried.add(fp);
267
+ deferred += 1;
268
+ }
269
+ return { opened: 0, failed: 0, deferred };
210
270
  }
211
- notifyFileChanged(filePath, signal) {
212
- return this.syncOne(filePath, signal).then(() => undefined);
271
+ notifyFileChanged(filePath, signal, gate) {
272
+ return this.syncGated(filePath, signal, gate).then(() => undefined);
213
273
  }
214
274
  close() {
215
275
  return this.transport.close();