@bastani/atomic 0.9.19-alpha.5 → 0.9.19-alpha.6

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 (109) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  3. package/dist/builtin/intercom/broker/client.ts +19 -1
  4. package/dist/builtin/intercom/index.bundle.mjs +8 -1
  5. package/dist/builtin/intercom/package.json +1 -1
  6. package/dist/builtin/mcp/package.json +1 -1
  7. package/dist/builtin/subagents/CHANGELOG.md +6 -0
  8. package/dist/builtin/subagents/README.md +3 -1
  9. package/dist/builtin/subagents/agents/codebase-online-researcher.md +7 -7
  10. package/dist/builtin/subagents/package.json +1 -1
  11. package/dist/builtin/subagents/skills/subagent/SKILL.md +3 -1
  12. package/dist/builtin/subagents/src/extension/index.bundle.mjs +2 -1
  13. package/dist/builtin/web-access/CHANGELOG.md +7 -0
  14. package/dist/builtin/web-access/README.md +31 -24
  15. package/dist/builtin/web-access/index.bundle.mjs +16099 -77
  16. package/dist/builtin/web-access/package.json +2 -1
  17. package/dist/builtin/workflows/CHANGELOG.md +6 -0
  18. package/dist/builtin/workflows/package.json +1 -1
  19. package/dist/builtin/workflows/src/extension/index.bundle.mjs +31 -2
  20. package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
  21. package/dist/core/agent-session-tool-registry.js +10 -0
  22. package/dist/core/agent-session-tool-registry.js.map +1 -1
  23. package/dist/core/bash-executor.js.map +1 -1
  24. package/dist/core/tools/bash.d.ts +1 -1
  25. package/dist/core/tools/bash.d.ts.map +1 -1
  26. package/dist/core/tools/bash.js +3 -2
  27. package/dist/core/tools/bash.js.map +1 -1
  28. package/dist/core/tools/edit.d.ts +3 -0
  29. package/dist/core/tools/edit.d.ts.map +1 -1
  30. package/dist/core/tools/edit.js +91 -12
  31. package/dist/core/tools/edit.js.map +1 -1
  32. package/dist/core/tools/file-mutation-coordinator.d.ts +255 -0
  33. package/dist/core/tools/file-mutation-coordinator.d.ts.map +1 -0
  34. package/dist/core/tools/file-mutation-coordinator.js +300 -0
  35. package/dist/core/tools/file-mutation-coordinator.js.map +1 -0
  36. package/dist/core/tools/file-mutation-queue.d.ts +14 -1
  37. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  38. package/dist/core/tools/file-mutation-queue.js +15 -3
  39. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  40. package/dist/core/tools/hashline.d.ts +9 -0
  41. package/dist/core/tools/hashline.d.ts.map +1 -1
  42. package/dist/core/tools/hashline.js +12 -1
  43. package/dist/core/tools/hashline.js.map +1 -1
  44. package/dist/core/tools/index.d.ts +9 -1
  45. package/dist/core/tools/index.d.ts.map +1 -1
  46. package/dist/core/tools/index.js +67 -12
  47. package/dist/core/tools/index.js.map +1 -1
  48. package/dist/core/tools/non-minting-snapshot-store.d.ts +49 -0
  49. package/dist/core/tools/non-minting-snapshot-store.d.ts.map +1 -0
  50. package/dist/core/tools/non-minting-snapshot-store.js +64 -0
  51. package/dist/core/tools/non-minting-snapshot-store.js.map +1 -0
  52. package/dist/core/tools/powershell.d.ts +2 -2
  53. package/dist/core/tools/powershell.d.ts.map +1 -1
  54. package/dist/core/tools/powershell.js +2 -5
  55. package/dist/core/tools/powershell.js.map +1 -1
  56. package/dist/core/tools/read-document-extract.js.map +1 -1
  57. package/dist/core/tools/render-utils.js.map +1 -1
  58. package/dist/core/tools/write.d.ts +30 -1
  59. package/dist/core/tools/write.d.ts.map +1 -1
  60. package/dist/core/tools/write.js +97 -8
  61. package/dist/core/tools/write.js.map +1 -1
  62. package/dist/index.d.ts +1 -1
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js.map +1 -1
  65. package/dist/modes/interactive/components/bash-execution.js.map +1 -1
  66. package/dist/modes/interactive/interactive-mode-base.d.ts +1 -0
  67. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  68. package/dist/modes/interactive/interactive-mode-base.js +3 -1
  69. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  70. package/dist/modes/interactive/interactive-mode-surface.d.ts +1 -1
  71. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  72. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  73. package/dist/modes/interactive/interactive-render-chat.js +28 -4
  74. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  75. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  76. package/dist/modes/interactive/interactive-startup.js +3 -0
  77. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  78. package/dist/modes/interactive-engine/activity-watchdog.d.ts +2 -2
  79. package/dist/modes/interactive-engine/activity-watchdog.d.ts.map +1 -1
  80. package/dist/modes/interactive-engine/activity-watchdog.js.map +1 -1
  81. package/dist/modes/interactive-engine/engine-diagnostic-view.d.ts +3 -1
  82. package/dist/modes/interactive-engine/engine-diagnostic-view.d.ts.map +1 -1
  83. package/dist/modes/interactive-engine/engine-diagnostic-view.js +11 -0
  84. package/dist/modes/interactive-engine/engine-diagnostic-view.js.map +1 -1
  85. package/dist/modes/interactive-engine/isolated-runtime.js +2 -1
  86. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  87. package/dist/modes/rpc/rpc-client-process.d.ts +2 -0
  88. package/dist/modes/rpc/rpc-client-process.d.ts.map +1 -1
  89. package/dist/modes/rpc/rpc-client-process.js +23 -1
  90. package/dist/modes/rpc/rpc-client-process.js.map +1 -1
  91. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  92. package/dist/modes/rpc/rpc-client.js +20 -3
  93. package/dist/modes/rpc/rpc-client.js.map +1 -1
  94. package/dist/utils/markit.d.ts +2 -0
  95. package/dist/utils/markit.d.ts.map +1 -1
  96. package/dist/utils/markit.js +65 -5
  97. package/dist/utils/markit.js.map +1 -1
  98. package/docs/background-tasks.md +14 -10
  99. package/docs/containerization.md +7 -5
  100. package/docs/development.md +11 -0
  101. package/docs/docs.json +1 -0
  102. package/docs/subagents.md +3 -1
  103. package/docs/tools.md +26 -0
  104. package/docs/web-access.md +38 -0
  105. package/examples/extensions/gondolin/index.ts +116 -148
  106. package/examples/extensions/gondolin/package.json +1 -1
  107. package/examples/extensions/ssh.ts +56 -34
  108. package/npm-shrinkwrap.json +33 -33
  109. package/package.json +4 -4
@@ -0,0 +1,300 @@
1
+ import { computeFileHash } from "./hashline-engine/index.js";
2
+ /**
3
+ * Stable token carried inside every conflict message.
4
+ *
5
+ * It lives in the message text, not only on the class, because nothing typed survives the
6
+ * trip from a tool to a workflow: a subagent result reports `cause`/`error` as strings, a
7
+ * workflow task result has no error field at all, and a workflow runner reduces a failure to
8
+ * `err.message`. Matching this token is how a consumer downstream of that flattening tells a
9
+ * mutation conflict apart from an ordinary tool error.
10
+ *
11
+ * It is placed at the start of the message so it survives truncation of the tail as the
12
+ * message is relayed. Never change it without updating every consumer that matches on it.
13
+ */
14
+ export const FILE_MUTATION_CONFLICT_CODE = "FILE_MUTATION_CONFLICT";
15
+ /**
16
+ * Excerpts are clamped because a conflict travels as message text through channels that
17
+ * truncate, and because file content copied into an error outlives the turn in ledger records.
18
+ * One line per side is enough to tell the model whether its plan still holds.
19
+ */
20
+ const EVIDENCE_LINE_CHARS = 120;
21
+ function clampLine(line) {
22
+ return line.length <= EVIDENCE_LINE_CHARS ? line : `${line.slice(0, EVIDENCE_LINE_CHARS)}...`;
23
+ }
24
+ /** Describe live content, or a missing target when `live` is `undefined`. */
25
+ export function computeLiveState(live) {
26
+ if (live === undefined)
27
+ return { lines: 0 };
28
+ // Trailing newline terminates the last line rather than starting an empty one.
29
+ const lines = live === "" ? [] : live.replace(/\n$/, "").split("\n");
30
+ const first = lines[0];
31
+ return {
32
+ lines: lines.length,
33
+ tag: computeFileHash(live),
34
+ ...(first ? { firstLine: clampLine(first) } : {}),
35
+ };
36
+ }
37
+ /**
38
+ * Compare what a mutation assumed against what is live, returning `undefined` when they agree.
39
+ *
40
+ * Both sides must already share a line-ending convention. `Snapshot.text` is normalized to LF,
41
+ * so a caller passing raw live bytes on Windows would report every line as divergent.
42
+ */
43
+ export function computeConflictEvidence(assumed, found) {
44
+ if (assumed === found)
45
+ return undefined;
46
+ const assumedLines = assumed.split("\n");
47
+ const foundLines = found.split("\n");
48
+ let start = 0;
49
+ while (start < assumedLines.length && start < foundLines.length && assumedLines[start] === foundLines[start]) {
50
+ start++;
51
+ }
52
+ let endAssumed = assumedLines.length;
53
+ let endFound = foundLines.length;
54
+ while (endAssumed > start && endFound > start && assumedLines[endAssumed - 1] === foundLines[endFound - 1]) {
55
+ endAssumed--;
56
+ endFound--;
57
+ }
58
+ return {
59
+ line: start + 1,
60
+ assumedLines: endAssumed - start,
61
+ foundLines: endFound - start,
62
+ ...(start < endAssumed ? { assumed: clampLine(assumedLines[start] ?? "") } : {}),
63
+ ...(start < endFound ? { found: clampLine(foundLines[start] ?? "") } : {}),
64
+ };
65
+ }
66
+ /**
67
+ * Paired so the instruction cannot contradict the diagnosis it follows.
68
+ *
69
+ * A single shared instruction cannot be true of all six reasons. "Read the file again" is
70
+ * false for the two that fail precisely because the file was never read, and impossible for
71
+ * `target_missing`, where there is nothing left to read.
72
+ */
73
+ const REASON_COPY = {
74
+ changed_before_write: {
75
+ diagnosis: "the file changed after this edit was prepared",
76
+ instruction: "Read the file again and rebuild this edit from what it says now; do not resend it unchanged.",
77
+ },
78
+ no_prior_observation: {
79
+ diagnosis: "this session has not read the file it is overwriting",
80
+ instruction: "Read the file first, then decide whether this overwrite is still the change you want.",
81
+ },
82
+ changed_since_observation: {
83
+ diagnosis: "the file changed since this session last read it",
84
+ instruction: "Read the file again and reconcile this change with what is there now.",
85
+ },
86
+ target_exists: {
87
+ diagnosis: "the file already exists",
88
+ instruction: "Read the existing file and edit it in place, or write to a different path.",
89
+ },
90
+ target_missing: {
91
+ diagnosis: "the file no longer exists",
92
+ instruction: "Do not read it; confirm the removal was intended, then recreate the file or drop this change.",
93
+ },
94
+ target_unreadable: {
95
+ diagnosis: "the file still exists but could no longer be read",
96
+ instruction: "Something replaced or locked the path rather than editing it. Inspect what is there now before retrying; a second identical edit will fail the same way.",
97
+ },
98
+ foreign_snapshot: {
99
+ diagnosis: "the snapshot tag was not issued in this session",
100
+ instruction: "Read the file in this session to obtain a valid tag before editing it.",
101
+ },
102
+ };
103
+ function describeRequester(requester) {
104
+ if (!requester)
105
+ return "";
106
+ const parts = [`session=${requester.sessionId}`];
107
+ if (requester.parentSessionId)
108
+ parts.push(`parent=${requester.parentSessionId}`);
109
+ if (requester.workflowRunId)
110
+ parts.push(`run=${requester.workflowRunId}`);
111
+ if (requester.workflowStageId)
112
+ parts.push(`stage=${requester.workflowStageId}`);
113
+ // Quoted: a stage name is free text and the field separator here is a space.
114
+ if (requester.workflowStageName)
115
+ parts.push(`stageName="${requester.workflowStageName}"`);
116
+ if (requester.subagentRunId)
117
+ parts.push(`child=${requester.subagentRunId}`);
118
+ if (requester.subagentAgent)
119
+ parts.push(`agent=${requester.subagentAgent}`);
120
+ // Presence, not truthiness: the first child of a fan-out is index 0.
121
+ if (requester.subagentIndex !== undefined)
122
+ parts.push(`index=${requester.subagentIndex}`);
123
+ if (requester.attemptId)
124
+ parts.push(`attempt=${requester.attemptId}`);
125
+ if (requester.toolCallId)
126
+ parts.push(`call=${requester.toolCallId}`);
127
+ return ` [${parts.join(" ")}]`;
128
+ }
129
+ /**
130
+ * Quote an excerpt of file content for inclusion in a single-line message.
131
+ *
132
+ * `JSON.stringify` rather than wrapping in backticks or quotes of our own: source lines
133
+ * routinely contain both, and a template literal in a TypeScript file would otherwise close
134
+ * the delimiter early and leave the excerpt unreadable. It also escapes control characters,
135
+ * so a stray CR from a file this module was handed unnormalized shows up as `\r` rather than
136
+ * silently rearranging the message.
137
+ */
138
+ function quoteExcerpt(text) {
139
+ return JSON.stringify(text);
140
+ }
141
+ function describeEvidence(evidence) {
142
+ if (!evidence)
143
+ return "";
144
+ const span = evidence.assumedLines === 1 && evidence.foundLines === 1
145
+ ? ""
146
+ : ` (${evidence.assumedLines} lines replaced by ${evidence.foundLines})`;
147
+ const at = ` First divergence at line ${evidence.line}`;
148
+ if (evidence.assumed !== undefined && evidence.found !== undefined) {
149
+ return `${at}: assumed ${quoteExcerpt(evidence.assumed)}, found ${quoteExcerpt(evidence.found)}${span}.`;
150
+ }
151
+ if (evidence.found !== undefined) {
152
+ return `${at}: found ${quoteExcerpt(evidence.found)} where this mutation expected nothing${span}.`;
153
+ }
154
+ return `${at}: assumed ${quoteExcerpt(evidence.assumed ?? "")}, which is no longer present${span}.`;
155
+ }
156
+ function describeLiveState(state) {
157
+ if (!state)
158
+ return "";
159
+ if (!state.tag)
160
+ return " The target does not exist.";
161
+ const first = state.firstLine ? `, starting ${quoteExcerpt(state.firstLine)}` : "";
162
+ return ` Target now holds ${state.lines} lines, #${state.tag}${first}.`;
163
+ }
164
+ /**
165
+ * Raised instead of a bare `Error` so a conflict is countable rather than merely readable.
166
+ * The message is the wire format; see {@link FILE_MUTATION_CONFLICT_CODE}.
167
+ */
168
+ export class FileMutationConflict extends Error {
169
+ constructor(details) {
170
+ super(FileMutationConflict.formatMessage(details));
171
+ // Sub-classing Error does not set this, and consumers match on it.
172
+ this.name = "FileMutationConflict";
173
+ this.reason = details.reason;
174
+ this.path = details.path;
175
+ this.canonicalKey = details.canonicalKey;
176
+ this.requester = details.requester;
177
+ this.presentedTag = details.presentedTag;
178
+ this.evidence = details.evidence;
179
+ this.liveState = details.liveState;
180
+ this.causeCode = details.causeCode;
181
+ }
182
+ /**
183
+ * Ordered by what must survive truncation: the code first, then the reason and guidance,
184
+ * then the divergence, and the live-state summary last. The tail is the most expendable
185
+ * because it is the part a reader can always recover by opening the file.
186
+ */
187
+ static formatMessage(details) {
188
+ const tag = details.presentedTag ? ` (presented #${details.presentedTag})` : "";
189
+ const copy = REASON_COPY[details.reason];
190
+ const cause = details.causeCode ? ` (${details.causeCode})` : "";
191
+ return (`${FILE_MUTATION_CONFLICT_CODE}:${details.reason} ${details.path}${tag}: ` +
192
+ `${copy.diagnosis}${cause}.${describeRequester(details.requester)} ` +
193
+ copy.instruction +
194
+ describeEvidence(details.evidence) +
195
+ describeLiveState(details.liveState));
196
+ }
197
+ }
198
+ /** Whether a thrown value is an exclusive-create collision reported by the filesystem. */
199
+ export function isExclusiveCreateCollision(error) {
200
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
201
+ }
202
+ /**
203
+ * Whether a thrown value says the target is gone. `ENOTDIR` counts because a parent directory
204
+ * replaced by a file makes the path unreachable for the same reason a deletion does.
205
+ */
206
+ export function isMissingTargetError(error) {
207
+ const code = filesystemErrorCode(error);
208
+ return code === "ENOENT" || code === "ENOTDIR";
209
+ }
210
+ /** The `code` a filesystem rejection carries, when it carries one. */
211
+ export function filesystemErrorCode(error) {
212
+ if (typeof error !== "object" || error === null || !("code" in error))
213
+ return undefined;
214
+ const code = error.code;
215
+ return typeof code === "string" ? code : undefined;
216
+ }
217
+ /**
218
+ * Guard for the window between preparing a mutation and committing it, called under the
219
+ * mutation queue.
220
+ *
221
+ * `prepared` is the content the mutation was computed against, which for a hashline edit is
222
+ * the file as `prepare` read it, NOT what the session originally observed, since `prepare`
223
+ * re-reads and may have recovered onto drifted content. Naming that honestly matters: this
224
+ * closes a time-of-check window, it does not prove the session read the file.
225
+ *
226
+ * Full text rather than tags: a hashline tag is 16 bits and the engine documents that it can
227
+ * collide, so it is a label for the model to recognize, never proof of identity.
228
+ */
229
+ export function assertLiveMatchesPrepared(args) {
230
+ if (args.live === args.prepared)
231
+ return;
232
+ const evidence = computeConflictEvidence(args.prepared, args.live);
233
+ throw new FileMutationConflict({
234
+ reason: "changed_before_write",
235
+ path: args.path,
236
+ canonicalKey: args.canonicalKey,
237
+ requester: args.requester,
238
+ presentedTag: args.presentedTag,
239
+ liveState: computeLiveState(args.live),
240
+ ...(evidence ? { evidence } : {}),
241
+ });
242
+ }
243
+ /**
244
+ * Guard for `write` overwriting an existing file: this session must already have observed the
245
+ * exact content it is about to replace.
246
+ *
247
+ * Asks the session's own store and never another session's, which is what keeps cross-session
248
+ * rejection strict. `byHashAndText` answers both halves of the requirement at once, since a
249
+ * hit means this session recorded exactly this content for this path.
250
+ *
251
+ * `storeKey` is the snapshot store's own key convention, the normalized absolute path, which
252
+ * is NOT symlink-resolved unlike `canonicalKey`. Passing the wrong one makes a symlinked path
253
+ * miss and report a false `no_prior_observation`.
254
+ *
255
+ * The tag is derived here rather than accepted. Unlike the engine's own use of
256
+ * `byHashAndText`, where the hash is the independent claim a section presented, `write` has no
257
+ * presented tag: it replaces the whole file. Any tag is therefore just `live` hashed, and
258
+ * taking it as a parameter only creates a way for the two to disagree silently.
259
+ */
260
+ export function assertPriorSessionObservation(args) {
261
+ if (args.store.byHashAndText(args.storeKey, computeFileHash(args.live), args.live))
262
+ return;
263
+ // The last version this session recorded is what it believed it was overwriting, so the
264
+ // divergence from live is the evidence. Without one there is nothing to diff, but the live
265
+ // state still reports what was about to be clobbered sight unseen, which is the part
266
+ // whoever reviews the rejection actually needs.
267
+ const head = args.store.head(args.storeKey);
268
+ const evidence = head ? computeConflictEvidence(head.text, args.live) : undefined;
269
+ throw new FileMutationConflict({
270
+ reason: head ? "changed_since_observation" : "no_prior_observation",
271
+ path: args.path,
272
+ canonicalKey: args.canonicalKey,
273
+ requester: args.requester,
274
+ liveState: computeLiveState(args.live),
275
+ ...(evidence ? { evidence } : {}),
276
+ });
277
+ }
278
+ /**
279
+ * Build a requester from parts already present on a session. Kept pure so it is testable
280
+ * without constructing an `AgentSession`, and so this module never imports one.
281
+ */
282
+ export function buildMutationRequester(parts) {
283
+ const workflow = parts.orchestration?.kind === "workflow-stage" ? parts.orchestration : undefined;
284
+ const supervisorSessionId = parts.intercom?.supervisor?.supervisorSessionId;
285
+ const subagentIndex = parts.intercom?.index;
286
+ return {
287
+ sessionId: parts.sessionId,
288
+ ...(supervisorSessionId ? { parentSessionId: supervisorSessionId } : {}),
289
+ ...(workflow ? { workflowRunId: workflow.workflowRunId } : {}),
290
+ ...(workflow ? { workflowStageId: workflow.workflowStageId } : {}),
291
+ ...(workflow ? { workflowStageName: workflow.workflowStageName } : {}),
292
+ ...(parts.intercom ? { subagentRunId: parts.intercom.runId } : {}),
293
+ ...(parts.intercom ? { subagentAgent: parts.intercom.agent } : {}),
294
+ // Presence, not truthiness: index 0 is the first child, and `0 ? … : {}` drops it.
295
+ ...(subagentIndex !== undefined ? { subagentIndex } : {}),
296
+ ...(parts.attemptId ? { attemptId: parts.attemptId } : {}),
297
+ ...(parts.toolCallId ? { toolCallId: parts.toolCallId } : {}),
298
+ };
299
+ }
300
+ //# sourceMappingURL=file-mutation-coordinator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-mutation-coordinator.js","sourceRoot":"","sources":["../../../src/core/tools/file-mutation-coordinator.ts"],"names":[],"mappings":"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","sourcesContent":["import { computeFileHash, type SnapshotStore } from \"./hashline-engine/index.ts\";\n\n/**\n * Stable token carried inside every conflict message.\n *\n * It lives in the message text, not only on the class, because nothing typed survives the\n * trip from a tool to a workflow: a subagent result reports `cause`/`error` as strings, a\n * workflow task result has no error field at all, and a workflow runner reduces a failure to\n * `err.message`. Matching this token is how a consumer downstream of that flattening tells a\n * mutation conflict apart from an ordinary tool error.\n *\n * It is placed at the start of the message so it survives truncation of the tail as the\n * message is relayed. Never change it without updating every consumer that matches on it.\n */\nexport const FILE_MUTATION_CONFLICT_CODE = \"FILE_MUTATION_CONFLICT\";\n\n/**\n * Reasons report a {@link FileMutationLiveState} wherever the live target can be described,\n * which is everywhere except `target_unreadable`: that one exists precisely because the target\n * could not be read, so claiming a size or a tag for it would be invention.\n *\n * Only the reasons that have something to compare against also report a\n * {@link FileMutationConflictEvidence} diff: `no_prior_observation` never read the file, and\n * `foreign_snapshot` presented a tag naming content this session has never seen, so in both\n * cases there is no prior side to diff.\n */\nexport type FileMutationConflictReason =\n\t/** Live content moved between the mutation being prepared and the write being attempted. */\n\t| \"changed_before_write\"\n\t/** Overwrite attempted with no prior read of this file in this session. */\n\t| \"no_prior_observation\"\n\t/** Overwrite attempted, but what this session observed is no longer the live content. */\n\t| \"changed_since_observation\"\n\t/** Exclusive create lost: the target already exists. */\n\t| \"target_exists\"\n\t/** The target no longer exists: removed between preparing the mutation and writing it. */\n\t| \"target_missing\"\n\t/**\n\t * The target still exists but can no longer be read: replaced by a directory, made\n\t * unreadable, locked, or rewritten as content this reader cannot parse. Distinct from a\n\t * content change because there is nothing to diff, and from a deletion because the path is\n\t * still occupied.\n\t */\n\t| \"target_unreadable\"\n\t/** The presented tag was never minted in this session's snapshot store. */\n\t| \"foreign_snapshot\";\n\n/**\n * Correlation only.\n *\n * Flora, 2026-08-13: identity \"must never grant permission or identify a current owner or\n * culprit\". So this describes the writer being *rejected*, never a winner, and nothing in\n * this module reads it to decide whether a mutation is admitted. A record of the last Atomic\n * writer could not prove ownership anyway, because shell commands, external programs, other\n * Atomic processes and symlink changes all bypass this coordinator.\n */\nexport interface MutationRequester {\n\treadonly sessionId: string;\n\t/** Session that spawned this one, from the child's `supervisor.supervisorSessionId`. */\n\treadonly parentSessionId?: string;\n\treadonly workflowRunId?: string;\n\treadonly workflowStageId?: string;\n\treadonly workflowStageName?: string;\n\treadonly subagentRunId?: string;\n\treadonly subagentAgent?: string;\n\t/**\n\t * Position within a fan-out. Needed to tell siblings apart: a child's intercom address is\n\t * `agent + runId + index` (`resolveSubagentIntercomTarget`), so a stage that spawns several\n\t * children of one agent onto one file yields identical identity without it.\n\t */\n\treadonly subagentIndex?: number;\n\t/** Opaque per-attempt UUID. Diagnostic only; never an authority. */\n\treadonly attemptId?: string;\n\t/** The specific tool call rejected. Narrower than the attempt, which makes many calls. */\n\treadonly toolCallId?: string;\n}\n\n/**\n * Resolved per execution rather than captured once.\n *\n * Mirrors `sessionTempDir` in the session tool registry, which is a thunk \"so bash spill\n * files follow the live transcript session across fork/branch/resume\". A requester captured\n * at tool-construction time would keep a stale session id through exactly those transitions,\n * which is the relaunch case this feature exists to make legible.\n *\n * Takes the tool call id rather than leaving callers to merge it in, because it has a\n * different lifetime from the rest: session identity belongs to the session, the call id is\n * an argument of the single `execute` being rejected.\n */\nexport type MutationRequesterResolver = (toolCallId: string) => MutationRequester | undefined;\n\n/**\n * What actually diverged, so a conflict can be acted on without a second round trip.\n *\n * A reason and a path are not evidence; they say a race happened, not what it did. Whatever acts\n * on the rejection, a workflow applying its own control behavior or a human reading the error,\n * needs the divergence itself before it can decide anything.\n *\n * The span is computed by trimming the common prefix and suffix rather than comparing line by\n * line positionally, so inserting one line at the top reports one changed region and not every\n * following line.\n */\nexport interface FileMutationConflictEvidence {\n\t/** 1-based line of the first divergence. */\n\treadonly line: number;\n\t/** Lines the rejected mutation assumed occupied the divergent span. */\n\treadonly assumedLines: number;\n\t/** Lines actually occupying it now. */\n\treadonly foundLines: number;\n\t/** First assumed line, clamped. Absent when the span is pure insertion. */\n\treadonly assumed?: string;\n\t/** First live line, clamped. Absent when the span is pure deletion. */\n\treadonly found?: string;\n}\n\n/**\n * Excerpts are clamped because a conflict travels as message text through channels that\n * truncate, and because file content copied into an error outlives the turn in ledger records.\n * One line per side is enough to tell the model whether its plan still holds.\n */\nconst EVIDENCE_LINE_CHARS = 120;\n\nfunction clampLine(line: string): string {\n\treturn line.length <= EVIDENCE_LINE_CHARS ? line : `${line.slice(0, EVIDENCE_LINE_CHARS)}...`;\n}\n\n/**\n * The target as it stood at the moment of refusal, independent of any comparison.\n *\n * Kept separate from {@link FileMutationConflictEvidence} because the two answer different\n * questions and only one of them can be unanswerable. A conflict may have nothing to diff\n * against, as when the session never read the file, but there is still always something worth\n * reporting about what the mutation was aimed at. Size in particular is the difference between\n * clobbering an empty placeholder and clobbering four hundred lines of production code.\n */\nexport interface FileMutationLiveState {\n\t/** Lines currently in the file. Zero when it is empty or does not exist. */\n\treadonly lines: number;\n\t/** Tag of the live content, so a rejected tag can be told apart from the real one. */\n\treadonly tag?: string;\n\t/** First line, clamped. Absent when the file is missing, empty, or starts with a blank line. */\n\treadonly firstLine?: string;\n}\n\n/** Describe live content, or a missing target when `live` is `undefined`. */\nexport function computeLiveState(live: string | undefined): FileMutationLiveState {\n\tif (live === undefined) return { lines: 0 };\n\t// Trailing newline terminates the last line rather than starting an empty one.\n\tconst lines = live === \"\" ? [] : live.replace(/\\n$/, \"\").split(\"\\n\");\n\tconst first = lines[0];\n\treturn {\n\t\tlines: lines.length,\n\t\ttag: computeFileHash(live),\n\t\t...(first ? { firstLine: clampLine(first) } : {}),\n\t};\n}\n\n/**\n * Compare what a mutation assumed against what is live, returning `undefined` when they agree.\n *\n * Both sides must already share a line-ending convention. `Snapshot.text` is normalized to LF,\n * so a caller passing raw live bytes on Windows would report every line as divergent.\n */\nexport function computeConflictEvidence(assumed: string, found: string): FileMutationConflictEvidence | undefined {\n\tif (assumed === found) return undefined;\n\tconst assumedLines = assumed.split(\"\\n\");\n\tconst foundLines = found.split(\"\\n\");\n\tlet start = 0;\n\twhile (start < assumedLines.length && start < foundLines.length && assumedLines[start] === foundLines[start]) {\n\t\tstart++;\n\t}\n\tlet endAssumed = assumedLines.length;\n\tlet endFound = foundLines.length;\n\twhile (endAssumed > start && endFound > start && assumedLines[endAssumed - 1] === foundLines[endFound - 1]) {\n\t\tendAssumed--;\n\t\tendFound--;\n\t}\n\treturn {\n\t\tline: start + 1,\n\t\tassumedLines: endAssumed - start,\n\t\tfoundLines: endFound - start,\n\t\t...(start < endAssumed ? { assumed: clampLine(assumedLines[start] ?? \"\") } : {}),\n\t\t...(start < endFound ? { found: clampLine(foundLines[start] ?? \"\") } : {}),\n\t};\n}\n\nexport interface FileMutationConflictDetails {\n\treadonly reason: FileMutationConflictReason;\n\t/** Display path, as the model referred to it. */\n\treadonly path: string;\n\t/** Symlink-resolved key, from `canonicalMutationKey`. */\n\treadonly canonicalKey: string;\n\treadonly requester?: MutationRequester;\n\t/** The tag the rejected mutation presented, when it had one. */\n\treadonly presentedTag?: string;\n\t/** Absent when the reason carries no comparison, as with `no_prior_observation`. */\n\treadonly evidence?: FileMutationConflictEvidence;\n\t/** What the mutation was aimed at. Absent only when it could not be read at all. */\n\treadonly liveState?: FileMutationLiveState;\n\t/**\n\t * Filesystem error code that classified the conflict, such as `EISDIR` or `EACCES`. For a\n\t * target that became unreadable this is the whole diagnosis, since nothing else about the\n\t * file can be reported once the read fails.\n\t */\n\treadonly causeCode?: string;\n}\n\ninterface ReasonCopy {\n\t/** Why the mutation was refused. */\n\treadonly diagnosis: string;\n\t/** What to do about it. */\n\treadonly instruction: string;\n}\n\n/**\n * Paired so the instruction cannot contradict the diagnosis it follows.\n *\n * A single shared instruction cannot be true of all six reasons. \"Read the file again\" is\n * false for the two that fail precisely because the file was never read, and impossible for\n * `target_missing`, where there is nothing left to read.\n */\nconst REASON_COPY: Record<FileMutationConflictReason, ReasonCopy> = {\n\tchanged_before_write: {\n\t\tdiagnosis: \"the file changed after this edit was prepared\",\n\t\tinstruction: \"Read the file again and rebuild this edit from what it says now; do not resend it unchanged.\",\n\t},\n\tno_prior_observation: {\n\t\tdiagnosis: \"this session has not read the file it is overwriting\",\n\t\tinstruction: \"Read the file first, then decide whether this overwrite is still the change you want.\",\n\t},\n\tchanged_since_observation: {\n\t\tdiagnosis: \"the file changed since this session last read it\",\n\t\tinstruction: \"Read the file again and reconcile this change with what is there now.\",\n\t},\n\ttarget_exists: {\n\t\tdiagnosis: \"the file already exists\",\n\t\tinstruction: \"Read the existing file and edit it in place, or write to a different path.\",\n\t},\n\ttarget_missing: {\n\t\tdiagnosis: \"the file no longer exists\",\n\t\tinstruction: \"Do not read it; confirm the removal was intended, then recreate the file or drop this change.\",\n\t},\n\ttarget_unreadable: {\n\t\tdiagnosis: \"the file still exists but could no longer be read\",\n\t\tinstruction:\n\t\t\t\"Something replaced or locked the path rather than editing it. Inspect what is there now before retrying; a second identical edit will fail the same way.\",\n\t},\n\tforeign_snapshot: {\n\t\tdiagnosis: \"the snapshot tag was not issued in this session\",\n\t\tinstruction: \"Read the file in this session to obtain a valid tag before editing it.\",\n\t},\n};\n\nfunction describeRequester(requester: MutationRequester | undefined): string {\n\tif (!requester) return \"\";\n\tconst parts = [`session=${requester.sessionId}`];\n\tif (requester.parentSessionId) parts.push(`parent=${requester.parentSessionId}`);\n\tif (requester.workflowRunId) parts.push(`run=${requester.workflowRunId}`);\n\tif (requester.workflowStageId) parts.push(`stage=${requester.workflowStageId}`);\n\t// Quoted: a stage name is free text and the field separator here is a space.\n\tif (requester.workflowStageName) parts.push(`stageName=\"${requester.workflowStageName}\"`);\n\tif (requester.subagentRunId) parts.push(`child=${requester.subagentRunId}`);\n\tif (requester.subagentAgent) parts.push(`agent=${requester.subagentAgent}`);\n\t// Presence, not truthiness: the first child of a fan-out is index 0.\n\tif (requester.subagentIndex !== undefined) parts.push(`index=${requester.subagentIndex}`);\n\tif (requester.attemptId) parts.push(`attempt=${requester.attemptId}`);\n\tif (requester.toolCallId) parts.push(`call=${requester.toolCallId}`);\n\treturn ` [${parts.join(\" \")}]`;\n}\n\n/**\n * Quote an excerpt of file content for inclusion in a single-line message.\n *\n * `JSON.stringify` rather than wrapping in backticks or quotes of our own: source lines\n * routinely contain both, and a template literal in a TypeScript file would otherwise close\n * the delimiter early and leave the excerpt unreadable. It also escapes control characters,\n * so a stray CR from a file this module was handed unnormalized shows up as `\\r` rather than\n * silently rearranging the message.\n */\nfunction quoteExcerpt(text: string): string {\n\treturn JSON.stringify(text);\n}\n\nfunction describeEvidence(evidence: FileMutationConflictEvidence | undefined): string {\n\tif (!evidence) return \"\";\n\tconst span =\n\t\tevidence.assumedLines === 1 && evidence.foundLines === 1\n\t\t\t? \"\"\n\t\t\t: ` (${evidence.assumedLines} lines replaced by ${evidence.foundLines})`;\n\tconst at = ` First divergence at line ${evidence.line}`;\n\tif (evidence.assumed !== undefined && evidence.found !== undefined) {\n\t\treturn `${at}: assumed ${quoteExcerpt(evidence.assumed)}, found ${quoteExcerpt(evidence.found)}${span}.`;\n\t}\n\tif (evidence.found !== undefined) {\n\t\treturn `${at}: found ${quoteExcerpt(evidence.found)} where this mutation expected nothing${span}.`;\n\t}\n\treturn `${at}: assumed ${quoteExcerpt(evidence.assumed ?? \"\")}, which is no longer present${span}.`;\n}\n\nfunction describeLiveState(state: FileMutationLiveState | undefined): string {\n\tif (!state) return \"\";\n\tif (!state.tag) return \" The target does not exist.\";\n\tconst first = state.firstLine ? `, starting ${quoteExcerpt(state.firstLine)}` : \"\";\n\treturn ` Target now holds ${state.lines} lines, #${state.tag}${first}.`;\n}\n\n/**\n * Raised instead of a bare `Error` so a conflict is countable rather than merely readable.\n * The message is the wire format; see {@link FILE_MUTATION_CONFLICT_CODE}.\n */\nexport class FileMutationConflict extends Error {\n\treadonly reason: FileMutationConflictReason;\n\treadonly path: string;\n\treadonly canonicalKey: string;\n\treadonly requester: MutationRequester | undefined;\n\treadonly presentedTag: string | undefined;\n\t/** Structured alongside the message so a ledger can record it without re-parsing text. */\n\treadonly evidence: FileMutationConflictEvidence | undefined;\n\treadonly liveState: FileMutationLiveState | undefined;\n\treadonly causeCode: string | undefined;\n\n\tconstructor(details: FileMutationConflictDetails) {\n\t\tsuper(FileMutationConflict.formatMessage(details));\n\t\t// Sub-classing Error does not set this, and consumers match on it.\n\t\tthis.name = \"FileMutationConflict\";\n\t\tthis.reason = details.reason;\n\t\tthis.path = details.path;\n\t\tthis.canonicalKey = details.canonicalKey;\n\t\tthis.requester = details.requester;\n\t\tthis.presentedTag = details.presentedTag;\n\t\tthis.evidence = details.evidence;\n\t\tthis.liveState = details.liveState;\n\t\tthis.causeCode = details.causeCode;\n\t}\n\n\t/**\n\t * Ordered by what must survive truncation: the code first, then the reason and guidance,\n\t * then the divergence, and the live-state summary last. The tail is the most expendable\n\t * because it is the part a reader can always recover by opening the file.\n\t */\n\tstatic formatMessage(details: FileMutationConflictDetails): string {\n\t\tconst tag = details.presentedTag ? ` (presented #${details.presentedTag})` : \"\";\n\t\tconst copy = REASON_COPY[details.reason];\n\t\tconst cause = details.causeCode ? ` (${details.causeCode})` : \"\";\n\t\treturn (\n\t\t\t`${FILE_MUTATION_CONFLICT_CODE}:${details.reason} ${details.path}${tag}: ` +\n\t\t\t`${copy.diagnosis}${cause}.${describeRequester(details.requester)} ` +\n\t\t\tcopy.instruction +\n\t\t\tdescribeEvidence(details.evidence) +\n\t\t\tdescribeLiveState(details.liveState)\n\t\t);\n\t}\n}\n\n/** Whether a thrown value is an exclusive-create collision reported by the filesystem. */\nexport function isExclusiveCreateCollision(error: unknown): boolean {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error && error.code === \"EEXIST\";\n}\n\n/**\n * Whether a thrown value says the target is gone. `ENOTDIR` counts because a parent directory\n * replaced by a file makes the path unreachable for the same reason a deletion does.\n */\nexport function isMissingTargetError(error: unknown): boolean {\n\tconst code = filesystemErrorCode(error);\n\treturn code === \"ENOENT\" || code === \"ENOTDIR\";\n}\n\n/** The `code` a filesystem rejection carries, when it carries one. */\nexport function filesystemErrorCode(error: unknown): string | undefined {\n\tif (typeof error !== \"object\" || error === null || !(\"code\" in error)) return undefined;\n\tconst code = (error as { code?: unknown }).code;\n\treturn typeof code === \"string\" ? code : undefined;\n}\n\n/**\n * Guard for the window between preparing a mutation and committing it, called under the\n * mutation queue.\n *\n * `prepared` is the content the mutation was computed against, which for a hashline edit is\n * the file as `prepare` read it, NOT what the session originally observed, since `prepare`\n * re-reads and may have recovered onto drifted content. Naming that honestly matters: this\n * closes a time-of-check window, it does not prove the session read the file.\n *\n * Full text rather than tags: a hashline tag is 16 bits and the engine documents that it can\n * collide, so it is a label for the model to recognize, never proof of identity.\n */\nexport function assertLiveMatchesPrepared(args: {\n\treadonly canonicalKey: string;\n\treadonly path: string;\n\treadonly prepared: string;\n\treadonly live: string;\n\treadonly requester?: MutationRequester;\n\treadonly presentedTag?: string;\n}): void {\n\tif (args.live === args.prepared) return;\n\tconst evidence = computeConflictEvidence(args.prepared, args.live);\n\tthrow new FileMutationConflict({\n\t\treason: \"changed_before_write\",\n\t\tpath: args.path,\n\t\tcanonicalKey: args.canonicalKey,\n\t\trequester: args.requester,\n\t\tpresentedTag: args.presentedTag,\n\t\tliveState: computeLiveState(args.live),\n\t\t...(evidence ? { evidence } : {}),\n\t});\n}\n\n/**\n * Guard for `write` overwriting an existing file: this session must already have observed the\n * exact content it is about to replace.\n *\n * Asks the session's own store and never another session's, which is what keeps cross-session\n * rejection strict. `byHashAndText` answers both halves of the requirement at once, since a\n * hit means this session recorded exactly this content for this path.\n *\n * `storeKey` is the snapshot store's own key convention, the normalized absolute path, which\n * is NOT symlink-resolved unlike `canonicalKey`. Passing the wrong one makes a symlinked path\n * miss and report a false `no_prior_observation`.\n *\n * The tag is derived here rather than accepted. Unlike the engine's own use of\n * `byHashAndText`, where the hash is the independent claim a section presented, `write` has no\n * presented tag: it replaces the whole file. Any tag is therefore just `live` hashed, and\n * taking it as a parameter only creates a way for the two to disagree silently.\n */\nexport function assertPriorSessionObservation(args: {\n\treadonly canonicalKey: string;\n\treadonly storeKey: string;\n\treadonly path: string;\n\treadonly store: SnapshotStore;\n\treadonly live: string;\n\treadonly requester?: MutationRequester;\n}): void {\n\tif (args.store.byHashAndText(args.storeKey, computeFileHash(args.live), args.live)) return;\n\t// The last version this session recorded is what it believed it was overwriting, so the\n\t// divergence from live is the evidence. Without one there is nothing to diff, but the live\n\t// state still reports what was about to be clobbered sight unseen, which is the part\n\t// whoever reviews the rejection actually needs.\n\tconst head = args.store.head(args.storeKey);\n\tconst evidence = head ? computeConflictEvidence(head.text, args.live) : undefined;\n\tthrow new FileMutationConflict({\n\t\treason: head ? \"changed_since_observation\" : \"no_prior_observation\",\n\t\tpath: args.path,\n\t\tcanonicalKey: args.canonicalKey,\n\t\trequester: args.requester,\n\t\tliveState: computeLiveState(args.live),\n\t\t...(evidence ? { evidence } : {}),\n\t});\n}\n\n/**\n * Build a requester from parts already present on a session. Kept pure so it is testable\n * without constructing an `AgentSession`, and so this module never imports one.\n */\nexport function buildMutationRequester(parts: {\n\treadonly sessionId: string;\n\treadonly orchestration?: {\n\t\treadonly kind: string;\n\t\treadonly workflowRunId: string;\n\t\treadonly workflowStageId: string;\n\t\treadonly workflowStageName: string;\n\t};\n\treadonly intercom?: {\n\t\treadonly runId: string;\n\t\treadonly agent: string;\n\t\treadonly index?: number;\n\t\treadonly supervisor?: { readonly supervisorSessionId: string };\n\t};\n\treadonly attemptId?: string;\n\treadonly toolCallId?: string;\n}): MutationRequester {\n\tconst workflow = parts.orchestration?.kind === \"workflow-stage\" ? parts.orchestration : undefined;\n\tconst supervisorSessionId = parts.intercom?.supervisor?.supervisorSessionId;\n\tconst subagentIndex = parts.intercom?.index;\n\treturn {\n\t\tsessionId: parts.sessionId,\n\t\t...(supervisorSessionId ? { parentSessionId: supervisorSessionId } : {}),\n\t\t...(workflow ? { workflowRunId: workflow.workflowRunId } : {}),\n\t\t...(workflow ? { workflowStageId: workflow.workflowStageId } : {}),\n\t\t...(workflow ? { workflowStageName: workflow.workflowStageName } : {}),\n\t\t...(parts.intercom ? { subagentRunId: parts.intercom.runId } : {}),\n\t\t...(parts.intercom ? { subagentAgent: parts.intercom.agent } : {}),\n\t\t// Presence, not truthiness: index 0 is the first child, and `0 ? … : {}` drops it.\n\t\t...(subagentIndex !== undefined ? { subagentIndex } : {}),\n\t\t...(parts.attemptId ? { attemptId: parts.attemptId } : {}),\n\t\t...(parts.toolCallId ? { toolCallId: parts.toolCallId } : {}),\n\t};\n}\n"]}
@@ -1,6 +1,19 @@
1
+ /**
2
+ * Canonical key for a mutation target: absolute, and symlink-resolved when the path
3
+ * exists so an alias and its target share one queue. A not-yet-created file falls back to
4
+ * the resolved path, which is what lets `write` create files.
5
+ *
6
+ * Exported so callers that need to name the same target (diagnostics, coordination) cannot
7
+ * drift into a second normalizer.
8
+ */
9
+ export declare function canonicalMutationKey(filePath: string): Promise<string>;
1
10
  /**
2
11
  * Serialize file mutation operations targeting the same file.
3
12
  * Operations for different files still run in parallel.
13
+ *
14
+ * The holder is handed the canonical key this call registered under. Registration already
15
+ * paid for the `realpath`, so a holder that has to name its own target in a diagnostic takes
16
+ * the answer from here instead of resolving the path a second time under the lock.
4
17
  */
5
- export declare function withFileMutationQueue<T>(filePath: string, fn: () => Promise<T>): Promise<T>;
18
+ export declare function withFileMutationQueue<T>(filePath: string, fn: (canonicalKey: string) => Promise<T>): Promise<T>;
6
19
  //# sourceMappingURL=file-mutation-queue.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"file-mutation-queue.d.ts","sourceRoot":"","sources":["../../../src/core/tools/file-mutation-queue.ts"],"names":[],"mappings":"AA2BA;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA6BjG"}
1
+ {"version":3,"file":"file-mutation-queue.d.ts","sourceRoot":"","sources":["../../../src/core/tools/file-mutation-queue.ts"],"names":[],"mappings":"AAeA;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAU5E;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA6BrH"}
@@ -8,7 +8,15 @@ function isMissingPathError(error) {
8
8
  "code" in error &&
9
9
  (error.code === "ENOENT" || error.code === "ENOTDIR"));
10
10
  }
11
- async function getMutationQueueKey(filePath) {
11
+ /**
12
+ * Canonical key for a mutation target: absolute, and symlink-resolved when the path
13
+ * exists so an alias and its target share one queue. A not-yet-created file falls back to
14
+ * the resolved path, which is what lets `write` create files.
15
+ *
16
+ * Exported so callers that need to name the same target (diagnostics, coordination) cannot
17
+ * drift into a second normalizer.
18
+ */
19
+ export async function canonicalMutationKey(filePath) {
12
20
  const resolvedPath = resolve(filePath);
13
21
  try {
14
22
  return await realpath(resolvedPath);
@@ -23,10 +31,14 @@ async function getMutationQueueKey(filePath) {
23
31
  /**
24
32
  * Serialize file mutation operations targeting the same file.
25
33
  * Operations for different files still run in parallel.
34
+ *
35
+ * The holder is handed the canonical key this call registered under. Registration already
36
+ * paid for the `realpath`, so a holder that has to name its own target in a diagnostic takes
37
+ * the answer from here instead of resolving the path a second time under the lock.
26
38
  */
27
39
  export async function withFileMutationQueue(filePath, fn) {
28
40
  const registration = registrationQueue.then(async () => {
29
- const key = await getMutationQueueKey(filePath);
41
+ const key = await canonicalMutationKey(filePath);
30
42
  const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();
31
43
  let releaseNext;
32
44
  const nextQueue = new Promise((resolveQueue) => {
@@ -40,7 +52,7 @@ export async function withFileMutationQueue(filePath, fn) {
40
52
  const { key, currentQueue, chainedQueue, releaseNext } = await registration;
41
53
  await currentQueue;
42
54
  try {
43
- return await fn();
55
+ return await fn(key);
44
56
  }
45
57
  finally {
46
58
  releaseNext();
@@ -1 +1 @@
1
- {"version":3,"file":"file-mutation-queue.js","sourceRoot":"","sources":["../../../src/core/tools/file-mutation-queue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAyB,CAAC;AAC5D,IAAI,iBAAiB,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;AAE1C,SAAS,kBAAkB,CAAC,KAAc;IACzC,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,MAAM,IAAI,KAAK;QACf,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CACrD,CAAC;AACH,CAAC;AAED,KAAK,UAAU,mBAAmB,CAAC,QAAgB;IAClD,MAAM,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,CAAC;QACJ,OAAO,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,YAAY,CAAC;QACrB,CAAC;QACD,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAI,QAAgB,EAAE,EAAoB;IACpF,MAAM,YAAY,GAAG,iBAAiB,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;QACtD,MAAM,GAAG,GAAG,MAAM,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,YAAY,GAAG,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QAEtE,IAAI,WAAwB,CAAC;QAC7B,MAAM,SAAS,GAAG,IAAI,OAAO,CAAO,CAAC,YAAY,EAAE,EAAE;YACpD,WAAW,GAAG,YAAY,CAAC;QAC5B,CAAC,CAAC,CAAC;QACH,MAAM,YAAY,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACxD,kBAAkB,CAAC,GAAG,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QAE1C,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,CAAC;IACzD,CAAC,CAAC,CAAC;IACH,iBAAiB,GAAG,YAAY,CAAC,IAAI,CACpC,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CACf,CAAC;IAEF,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,GAAG,MAAM,YAAY,CAAC;IAC5E,MAAM,YAAY,CAAC;IACnB,IAAI,CAAC;QACJ,OAAO,MAAM,EAAE,EAAE,CAAC;IACnB,CAAC;YAAS,CAAC;QACV,WAAW,EAAE,CAAC;QACd,IAAI,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,YAAY,EAAE,CAAC;YAClD,kBAAkB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;IACF,CAAC;AACF,CAAC","sourcesContent":["import { realpath } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\n\nconst fileMutationQueues = new Map<string, Promise<void>>();\nlet registrationQueue = Promise.resolve();\n\nfunction isMissingPathError(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === \"object\" &&\n\t\terror !== null &&\n\t\t\"code\" in error &&\n\t\t(error.code === \"ENOENT\" || error.code === \"ENOTDIR\")\n\t);\n}\n\nasync function getMutationQueueKey(filePath: string): Promise<string> {\n\tconst resolvedPath = resolve(filePath);\n\ttry {\n\t\treturn await realpath(resolvedPath);\n\t} catch (error) {\n\t\tif (isMissingPathError(error)) {\n\t\t\treturn resolvedPath;\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Serialize file mutation operations targeting the same file.\n * Operations for different files still run in parallel.\n */\nexport async function withFileMutationQueue<T>(filePath: string, fn: () => Promise<T>): Promise<T> {\n\tconst registration = registrationQueue.then(async () => {\n\t\tconst key = await getMutationQueueKey(filePath);\n\t\tconst currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();\n\n\t\tlet releaseNext!: () => void;\n\t\tconst nextQueue = new Promise<void>((resolveQueue) => {\n\t\t\treleaseNext = resolveQueue;\n\t\t});\n\t\tconst chainedQueue = currentQueue.then(() => nextQueue);\n\t\tfileMutationQueues.set(key, chainedQueue);\n\n\t\treturn { key, currentQueue, chainedQueue, releaseNext };\n\t});\n\tregistrationQueue = registration.then(\n\t\t() => undefined,\n\t\t() => undefined,\n\t);\n\n\tconst { key, currentQueue, chainedQueue, releaseNext } = await registration;\n\tawait currentQueue;\n\ttry {\n\t\treturn await fn();\n\t} finally {\n\t\treleaseNext();\n\t\tif (fileMutationQueues.get(key) === chainedQueue) {\n\t\t\tfileMutationQueues.delete(key);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"file-mutation-queue.js","sourceRoot":"","sources":["../../../src/core/tools/file-mutation-queue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAyB,CAAC;AAC5D,IAAI,iBAAiB,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;AAE1C,SAAS,kBAAkB,CAAC,KAAc;IACzC,OAAO,CACN,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,MAAM,IAAI,KAAK;QACf,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,CACrD,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAgB;IAC1D,MAAM,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACvC,IAAI,CAAC;QACJ,OAAO,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC;IACrC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,kBAAkB,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,YAAY,CAAC;QACrB,CAAC;QACD,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAI,QAAgB,EAAE,EAAwC;IACxG,MAAM,YAAY,GAAG,iBAAiB,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;QACtD,MAAM,GAAG,GAAG,MAAM,oBAAoB,CAAC,QAAQ,CAAC,CAAC;QACjD,MAAM,YAAY,GAAG,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QAEtE,IAAI,WAAwB,CAAC;QAC7B,MAAM,SAAS,GAAG,IAAI,OAAO,CAAO,CAAC,YAAY,EAAE,EAAE;YACpD,WAAW,GAAG,YAAY,CAAC;QAC5B,CAAC,CAAC,CAAC;QACH,MAAM,YAAY,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACxD,kBAAkB,CAAC,GAAG,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;QAE1C,OAAO,EAAE,GAAG,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,CAAC;IACzD,CAAC,CAAC,CAAC;IACH,iBAAiB,GAAG,YAAY,CAAC,IAAI,CACpC,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CACf,CAAC;IAEF,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,GAAG,MAAM,YAAY,CAAC;IAC5E,MAAM,YAAY,CAAC;IACnB,IAAI,CAAC;QACJ,OAAO,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC;IACtB,CAAC;YAAS,CAAC;QACV,WAAW,EAAE,CAAC;QACd,IAAI,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,YAAY,EAAE,CAAC;YAClD,kBAAkB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChC,CAAC;IACF,CAAC;AACF,CAAC","sourcesContent":["import { realpath } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\n\nconst fileMutationQueues = new Map<string, Promise<void>>();\nlet registrationQueue = Promise.resolve();\n\nfunction isMissingPathError(error: unknown): boolean {\n\treturn (\n\t\ttypeof error === \"object\" &&\n\t\terror !== null &&\n\t\t\"code\" in error &&\n\t\t(error.code === \"ENOENT\" || error.code === \"ENOTDIR\")\n\t);\n}\n\n/**\n * Canonical key for a mutation target: absolute, and symlink-resolved when the path\n * exists so an alias and its target share one queue. A not-yet-created file falls back to\n * the resolved path, which is what lets `write` create files.\n *\n * Exported so callers that need to name the same target (diagnostics, coordination) cannot\n * drift into a second normalizer.\n */\nexport async function canonicalMutationKey(filePath: string): Promise<string> {\n\tconst resolvedPath = resolve(filePath);\n\ttry {\n\t\treturn await realpath(resolvedPath);\n\t} catch (error) {\n\t\tif (isMissingPathError(error)) {\n\t\t\treturn resolvedPath;\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Serialize file mutation operations targeting the same file.\n * Operations for different files still run in parallel.\n *\n * The holder is handed the canonical key this call registered under. Registration already\n * paid for the `realpath`, so a holder that has to name its own target in a diagnostic takes\n * the answer from here instead of resolving the path a second time under the lock.\n */\nexport async function withFileMutationQueue<T>(filePath: string, fn: (canonicalKey: string) => Promise<T>): Promise<T> {\n\tconst registration = registrationQueue.then(async () => {\n\t\tconst key = await canonicalMutationKey(filePath);\n\t\tconst currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();\n\n\t\tlet releaseNext!: () => void;\n\t\tconst nextQueue = new Promise<void>((resolveQueue) => {\n\t\t\treleaseNext = resolveQueue;\n\t\t});\n\t\tconst chainedQueue = currentQueue.then(() => nextQueue);\n\t\tfileMutationQueues.set(key, chainedQueue);\n\n\t\treturn { key, currentQueue, chainedQueue, releaseNext };\n\t});\n\tregistrationQueue = registration.then(\n\t\t() => undefined,\n\t\t() => undefined,\n\t);\n\n\tconst { key, currentQueue, chainedQueue, releaseNext } = await registration;\n\tawait currentQueue;\n\ttry {\n\t\treturn await fn(key);\n\t} finally {\n\t\treleaseNext();\n\t\tif (fileMutationQueues.get(key) === chainedQueue) {\n\t\t\tfileMutationQueues.delete(key);\n\t\t}\n\t}\n}\n"]}
@@ -11,6 +11,15 @@ export interface HashlineSnapshotStore {
11
11
  findByHeader(displayPath: string, tag: string): HashlineSnapshot | undefined;
12
12
  }
13
13
  export declare function hashlineDisplayPath(absolutePath: string, cwd: string): string;
14
+ /**
15
+ * The key a path's snapshots are filed under in the underlying {@link SnapshotStore}.
16
+ *
17
+ * `record` below is the only place that decides this, so anything asking the store about a
18
+ * path it did not itself record goes through here rather than re-deriving the convention and
19
+ * drifting from it. Note this is `path.normalize`, not `realpath`: two symlinked spellings of
20
+ * one file are two keys here, unlike `canonicalMutationKey`.
21
+ */
22
+ export declare function hashlineStoreKey(absolutePath: string): string;
14
23
  export declare function normalizeHashlineContent(content: string): string;
15
24
  export declare function computeHashlineTag(content: string): string;
16
25
  export declare function createHashlineSnapshotStore(): HashlineSnapshotStore;
@@ -1 +1 @@
1
- {"version":3,"file":"hashline.d.ts","sourceRoot":"","sources":["../../../src/core/tools/hashline.ts"],"names":[],"mappings":"AACA,OAAO,EAKN,KAAK,aAAa,EAClB,MAAM,4BAA4B,CAAC;AAEpC,MAAM,WAAW,gBAAgB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAC7E,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAAC;CAC7E;AAMD,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAI7E;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,2BAA2B,IAAI,qBAAqB,CAkBnE;AAED,wBAAgB,sBAAsB,CACrC,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,qBAAqB,GAC1B,gBAAgB,CAElB;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,SAAmB,EAAE,SAAS,SAAI,GAAG,MAAM,CAKnH;AAED,MAAM,WAAW,uBAAuB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,uCAAuC,CACtD,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,qBAAqB,EAC5B,WAAW,EAAE,MAAM,GACjB,uBAAuB,CA2FzB;AAED,wBAAgB,+BAA+B,CAC9C,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,qBAAqB,EAC5B,WAAW,EAAE,MAAM,GACjB,MAAM,CAER;AAED,wBAAgB,+BAA+B,CAC9C,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,EAClD,QAAQ,GAAE,SAAS,MAAM,EAAO,GAC9B,MAAM,CAMR"}
1
+ {"version":3,"file":"hashline.d.ts","sourceRoot":"","sources":["../../../src/core/tools/hashline.ts"],"names":[],"mappings":"AACA,OAAO,EAKN,KAAK,aAAa,EAClB,MAAM,4BAA4B,CAAC;AAEpC,MAAM,WAAW,gBAAgB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,MAAM,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAAC;IAC7E,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAAC;CAC7E;AAMD,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAI7E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,2BAA2B,IAAI,qBAAqB,CAkBnE;AAED,wBAAgB,sBAAsB,CACrC,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,qBAAqB,GAC1B,gBAAgB,CAElB;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,SAAmB,EAAE,SAAS,SAAI,GAAG,MAAM,CAKnH;AAED,MAAM,WAAW,uBAAuB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,wBAAgB,uCAAuC,CACtD,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,qBAAqB,EAC5B,WAAW,EAAE,MAAM,GACjB,uBAAuB,CA2FzB;AAED,wBAAgB,+BAA+B,CAC9C,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,qBAAqB,EAC5B,WAAW,EAAE,MAAM,GACjB,MAAM,CAER;AAED,wBAAgB,+BAA+B,CAC9C,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,EAClD,QAAQ,GAAE,SAAS,MAAM,EAAO,GAC9B,MAAM,CAMR"}
@@ -9,6 +9,17 @@ export function hashlineDisplayPath(absolutePath, cwd) {
9
9
  return toPosixPath(relativePath);
10
10
  return toPosixPath(absolutePath);
11
11
  }
12
+ /**
13
+ * The key a path's snapshots are filed under in the underlying {@link SnapshotStore}.
14
+ *
15
+ * `record` below is the only place that decides this, so anything asking the store about a
16
+ * path it did not itself record goes through here rather than re-deriving the convention and
17
+ * drifting from it. Note this is `path.normalize`, not `realpath`: two symlinked spellings of
18
+ * one file are two keys here, unlike `canonicalMutationKey`.
19
+ */
20
+ export function hashlineStoreKey(absolutePath) {
21
+ return normalizePath(absolutePath);
22
+ }
12
23
  export function normalizeHashlineContent(content) {
13
24
  return content
14
25
  .replace(/^\uFEFF/, "")
@@ -24,7 +35,7 @@ export function createHashlineSnapshotStore() {
24
35
  return {
25
36
  snapshots,
26
37
  record(absolutePath, cwd, content) {
27
- const normalizedPath = normalizePath(absolutePath);
38
+ const normalizedPath = hashlineStoreKey(absolutePath);
28
39
  const normalized = normalizeHashlineContent(content);
29
40
  const displayPath = hashlineDisplayPath(normalizedPath, cwd);
30
41
  const tag = snapshots.record(normalizedPath, normalized);
@@ -1 +1 @@
1
- {"version":3,"file":"hashline.js","sourceRoot":"","sources":["../../../src/core/tools/hashline.ts"],"names":[],"mappings":"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","sourcesContent":["import { isAbsolute, normalize as normalizePath, relative, sep } from \"node:path\";\nimport {\n\tcomputeFileHash,\n\tformatHashlineHeader,\n\tformatNumberedLines,\n\tInMemorySnapshotStore,\n\ttype SnapshotStore,\n} from \"./hashline-engine/index.ts\";\n\nexport interface HashlineSnapshot {\n\tabsolutePath: string;\n\tdisplayPath: string;\n\ttag: string;\n\tcontent: string;\n}\n\nexport interface HashlineSnapshotStore {\n\treadonly snapshots: SnapshotStore;\n\trecord(absolutePath: string, cwd: string, content: string): HashlineSnapshot;\n\tfindByHeader(displayPath: string, tag: string): HashlineSnapshot | undefined;\n}\n\nfunction toPosixPath(filePath: string): string {\n\treturn filePath.split(sep).join(\"/\");\n}\n\nexport function hashlineDisplayPath(absolutePath: string, cwd: string): string {\n\tconst relativePath = relative(cwd, absolutePath);\n\tif (relativePath && !relativePath.startsWith(\"..\") && !isAbsolute(relativePath)) return toPosixPath(relativePath);\n\treturn toPosixPath(absolutePath);\n}\n\nexport function normalizeHashlineContent(content: string): string {\n\treturn content\n\t\t.replace(/^\\uFEFF/, \"\")\n\t\t.replace(/\\r\\n/g, \"\\n\")\n\t\t.replace(/\\r/g, \"\\n\");\n}\n\nexport function computeHashlineTag(content: string): string {\n\treturn computeFileHash(normalizeHashlineContent(content));\n}\n\nexport function createHashlineSnapshotStore(): HashlineSnapshotStore {\n\tconst snapshots = new InMemorySnapshotStore();\n\tconst headers = new Map<string, HashlineSnapshot>();\n\treturn {\n\t\tsnapshots,\n\t\trecord(absolutePath: string, cwd: string, content: string): HashlineSnapshot {\n\t\t\tconst normalizedPath = normalizePath(absolutePath);\n\t\t\tconst normalized = normalizeHashlineContent(content);\n\t\t\tconst displayPath = hashlineDisplayPath(normalizedPath, cwd);\n\t\t\tconst tag = snapshots.record(normalizedPath, normalized);\n\t\t\tconst snapshot = { absolutePath: normalizedPath, displayPath, tag, content: normalized };\n\t\t\theaders.set(`${displayPath}\\0${tag}`, snapshot);\n\t\t\treturn snapshot;\n\t\t},\n\t\tfindByHeader(displayPath: string, tag: string): HashlineSnapshot | undefined {\n\t\t\treturn headers.get(`${displayPath}\\0${tag.toUpperCase()}`);\n\t\t},\n\t};\n}\n\nexport function recordHashlineSnapshot(\n\tabsolutePath: string,\n\tcwd: string,\n\tcontent: string,\n\tstore: HashlineSnapshotStore,\n): HashlineSnapshot {\n\treturn store.record(absolutePath, cwd, content);\n}\n\nexport function formatHashlineContent(snapshot: HashlineSnapshot, content = snapshot.content, startLine = 1): string {\n\treturn [\n\t\tformatHashlineHeader(snapshot.displayPath, snapshot.tag),\n\t\tformatNumberedLines(normalizeHashlineContent(content), startLine),\n\t].join(\"\\n\");\n}\n\nexport interface StrippedHashlineContent {\n\tcontent: string;\n\tstripped: boolean;\n}\n\nexport function stripKnownHashlineCopiedContentWithMeta(\n\tcontent: string,\n\tabsolutePath: string,\n\tcwd: string,\n\tstore: HashlineSnapshotStore,\n\temittedPath: string,\n): StrippedHashlineContent {\n\tconst normalized = normalizeHashlineContent(content);\n\tconst lines = normalized.split(\"\\n\");\n\tconst headerIndex = lines.findIndex(\n\t\t(line, index) =>\n\t\t\t/^\\[[^\\]\\n]+#[0-9A-Fa-f]{4}\\]$/.test(line) &&\n\t\t\tlines.slice(0, index).every((prefix) => prefix.trim() === \"\" || /^#\\s+.+\\/?$/.test(prefix)),\n\t);\n\tif (headerIndex < 0) return { content, stripped: false };\n\tconst header = (lines[headerIndex] ?? \"\").match(/^\\[([^\\]\\n]+)#([0-9A-Fa-f]{4})\\]$/);\n\tif (!header) return { content, stripped: false };\n\tconst snapshot = store.findByHeader(header[1] ?? \"\", header[2] ?? \"\");\n\tif (!snapshot) return { content, stripped: false };\n\tconst body = lines.slice(headerIndex + 1);\n\tif (body.length === 0) return { content: snapshot.content, stripped: true };\n\t// The write tool emits its confirmation from the raw `path` argument it was\n\t// called with (`Successfully wrote to ${path}`), not from the resolved path,\n\t// so `emittedPath` is the authoritative anchor: it is the only form that is\n\t// guaranteed to round-trip. The resolved and cwd-relative forms are accepted\n\t// because a caller may have named the file either way, and the snapshot's own\n\t// paths are accepted because a copied confirmation may belong to the write\n\t// that produced the snapshot being copied. Every candidate is a complete\n\t// path — a bare basename is deliberately NOT accepted, since `Successfully\n\t// wrote to notes.md` is ordinary prose when the target is `deep/dir/notes.md`,\n\t// and treating it as chrome destroys that line and everything after it.\n\tconst knownConfirmationPaths = new Set<string>();\n\tconst addKnownPath = (filePath: string): void => {\n\t\tif (!filePath) return;\n\t\tknownConfirmationPaths.add(filePath);\n\t\tknownConfirmationPaths.add(filePath.replaceAll(\"\\\\\", \"/\"));\n\t\tknownConfirmationPaths.add(filePath.replaceAll(\"/\", \"\\\\\"));\n\t};\n\taddKnownPath(emittedPath);\n\taddKnownPath(absolutePath);\n\tif (absolutePath) addKnownPath(relative(cwd, absolutePath));\n\taddKnownPath(snapshot.absolutePath);\n\taddKnownPath(snapshot.displayPath);\n\tconst isKnownWriteConfirmation = (line: string): boolean => {\n\t\tconst match = line.match(/^Successfully wrote (?:\\d+ bytes )?to (.+)$/);\n\t\treturn match !== null && knownConfirmationPaths.has(match[1] ?? \"\");\n\t};\n\tconst stripped: string[] = [];\n\tconst snapshotLines = snapshot.content.split(\"\\n\");\n\tlet sawRow = false;\n\tlet lastCopiedLineNumber: number | undefined;\n\t// Trailing tool chrome a model is likely to copy along with the hashline\n\t// body: the read/search continuation footers, the write tool's own\n\t// confirmation (and its stripped-note), and `Resolved …` conflict footers.\n\t// A write confirmation is chrome only when it names the exact path the write\n\t// was asked for; a matching prefix or basename alone may be user content.\n\t// The optional byte count keeps pre-e583b290 confirmations matchable.\n\t// These footers never carry a line number, so they mark the end of the\n\t// numbered body instead of aborting stripping like an arbitrary line.\n\tconst isToolFooter = (line: string): boolean =>\n\t\tline.trim() === \"\" ||\n\t\t/^\\[\\d+ more lines in file\\./.test(line) ||\n\t\t/^\\[Showing lines /.test(line) ||\n\t\tisKnownWriteConfirmation(line) ||\n\t\t/^Resolved \\d+ conflicts?/.test(line) ||\n\t\t/^Resolved conflict \\d+/.test(line) ||\n\t\t/^Note: stripped copied hashline/.test(line) ||\n\t\t/^\\[[^\\]\\n]+#[0-9A-Fa-f]{4}\\]$/.test(line);\n\tlet onlyFooter = true;\n\tfor (const line of body) {\n\t\tif (isToolFooter(line)) {\n\t\t\t// A footer after the numbered body ends the body; a footer before any\n\t\t\t// row (leading blanks / a copied snapshot header) is just skipped.\n\t\t\tif (sawRow) break;\n\t\t\tcontinue;\n\t\t}\n\t\tonlyFooter = false;\n\t\tconst match = line.match(/^[* ]?(\\d+):(.*)$/s);\n\t\tif (!match) return { content, stripped: false };\n\t\tsawRow = true;\n\t\tconst lineNumber = Number.parseInt(match[1] ?? \"0\", 10);\n\t\tconst strippedLine = match[2] ?? \"\";\n\t\tif (snapshotLines[lineNumber - 1] !== strippedLine) return { content, stripped: false };\n\t\tlastCopiedLineNumber = lineNumber;\n\t\tstripped.push(strippedLine);\n\t}\n\tif (sawRow) {\n\t\tlet strippedContent = stripped.join(\"\\n\");\n\t\tif (snapshot.content.endsWith(\"\\n\") && lastCopiedLineNumber === snapshotLines.length - 1) strippedContent += \"\\n\";\n\t\treturn { content: strippedContent, stripped: true };\n\t}\n\n\t// Header + only tool chrome (e.g. a copied write confirmation\n\t// `Successfully wrote to <path>`) names a known snapshot with no numbered\n\t// body to recover — resolve to the snapshot's stored content.\n\tif (onlyFooter) return { content: snapshot.content, stripped: true };\n\treturn { content, stripped: false };\n}\n\nexport function stripKnownHashlineCopiedContent(\n\tcontent: string,\n\tabsolutePath: string,\n\tcwd: string,\n\tstore: HashlineSnapshotStore,\n\temittedPath: string,\n): string {\n\treturn stripKnownHashlineCopiedContentWithMeta(content, absolutePath, cwd, store, emittedPath).content;\n}\n\nexport function formatCompactHashlineEditResult(\n\tsnapshot: HashlineSnapshot,\n\tdiff: { diff?: string; firstChangedLine?: number },\n\tmessages: readonly string[] = [],\n): string {\n\treturn [\n\t\tformatHashlineHeader(snapshot.displayPath, snapshot.tag),\n\t\t...messages,\n\t\tdiff.diff?.trim() || `First changed line: ${diff.firstChangedLine ?? 1}`,\n\t].join(\"\\n\");\n}\n"]}
1
+ {"version":3,"file":"hashline.js","sourceRoot":"","sources":["../../../src/core/tools/hashline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,IAAI,aAAa,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAClF,OAAO,EACN,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,qBAAqB,GAErB,MAAM,4BAA4B,CAAC;AAepC,SAAS,WAAW,CAAC,QAAgB;IACpC,OAAO,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACtC,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,YAAoB,EAAE,GAAW;IACpE,MAAM,YAAY,GAAG,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACjD,IAAI,YAAY,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO,WAAW,CAAC,YAAY,CAAC,CAAC;IAClH,OAAO,WAAW,CAAC,YAAY,CAAC,CAAC;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,YAAoB;IACpD,OAAO,aAAa,CAAC,YAAY,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,OAAe;IACvD,OAAO,OAAO;SACZ,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;SACtB,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC;SACtB,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAAe;IACjD,OAAO,eAAe,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,UAAU,2BAA2B;IAC1C,MAAM,SAAS,GAAG,IAAI,qBAAqB,EAAE,CAAC;IAC9C,MAAM,OAAO,GAAG,IAAI,GAAG,EAA4B,CAAC;IACpD,OAAO;QACN,SAAS;QACT,MAAM,CAAC,YAAoB,EAAE,GAAW,EAAE,OAAe;YACxD,MAAM,cAAc,GAAG,gBAAgB,CAAC,YAAY,CAAC,CAAC;YACtD,MAAM,UAAU,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC;YACrD,MAAM,WAAW,GAAG,mBAAmB,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC;YAC7D,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;YACzD,MAAM,QAAQ,GAAG,EAAE,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;YACzF,OAAO,CAAC,GAAG,CAAC,GAAG,WAAW,KAAK,GAAG,EAAE,EAAE,QAAQ,CAAC,CAAC;YAChD,OAAO,QAAQ,CAAC;QACjB,CAAC;QACD,YAAY,CAAC,WAAmB,EAAE,GAAW;YAC5C,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,WAAW,KAAK,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC5D,CAAC;KACD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,sBAAsB,CACrC,YAAoB,EACpB,GAAW,EACX,OAAe,EACf,KAA4B;IAE5B,OAAO,KAAK,CAAC,MAAM,CAAC,YAAY,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,QAA0B,EAAE,OAAO,GAAG,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,CAAC;IAC1G,OAAO;QACN,oBAAoB,CAAC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,CAAC;QACxD,mBAAmB,CAAC,wBAAwB,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;KACjE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC;AAOD,MAAM,UAAU,uCAAuC,CACtD,OAAe,EACf,YAAoB,EACpB,GAAW,EACX,KAA4B,EAC5B,WAAmB;IAEnB,MAAM,UAAU,GAAG,wBAAwB,CAAC,OAAO,CAAC,CAAC;IACrD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACrC,MAAM,WAAW,GAAG,KAAK,CAAC,SAAS,CAClC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CACf,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC;QAC1C,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAC5F,CAAC;IACF,IAAI,WAAW,GAAG,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACzD,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACrF,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACjD,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACtE,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IACnD,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC;IAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5E,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,6EAA6E;IAC7E,8EAA8E;IAC9E,2EAA2E;IAC3E,yEAAyE;IACzE,2EAA2E;IAC3E,+EAA+E;IAC/E,wEAAwE;IACxE,MAAM,sBAAsB,GAAG,IAAI,GAAG,EAAU,CAAC;IACjD,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAQ,EAAE;QAC/C,IAAI,CAAC,QAAQ;YAAE,OAAO;QACtB,sBAAsB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrC,sBAAsB,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC;QAC3D,sBAAsB,CAAC,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;IAC5D,CAAC,CAAC;IACF,YAAY,CAAC,WAAW,CAAC,CAAC;IAC1B,YAAY,CAAC,YAAY,CAAC,CAAC;IAC3B,IAAI,YAAY;QAAE,YAAY,CAAC,QAAQ,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC;IAC5D,YAAY,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IACpC,YAAY,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACnC,MAAM,wBAAwB,GAAG,CAAC,IAAY,EAAW,EAAE;QAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;QACxE,OAAO,KAAK,KAAK,IAAI,IAAI,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACrE,CAAC,CAAC;IACF,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,MAAM,aAAa,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnD,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,oBAAwC,CAAC;IAC7C,yEAAyE;IACzE,mEAAmE;IACnE,2EAA2E;IAC3E,6EAA6E;IAC7E,0EAA0E;IAC1E,sEAAsE;IACtE,uEAAuE;IACvE,sEAAsE;IACtE,MAAM,YAAY,GAAG,CAAC,IAAY,EAAW,EAAE,CAC9C,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;QAClB,6BAA6B,CAAC,IAAI,CAAC,IAAI,CAAC;QACxC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC;QAC9B,wBAAwB,CAAC,IAAI,CAAC;QAC9B,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC;QACrC,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC;QACnC,iCAAiC,CAAC,IAAI,CAAC,IAAI,CAAC;QAC5C,+BAA+B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,UAAU,GAAG,IAAI,CAAC;IACtB,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACzB,IAAI,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,sEAAsE;YACtE,mEAAmE;YACnE,IAAI,MAAM;gBAAE,MAAM;YAClB,SAAS;QACV,CAAC;QACD,UAAU,GAAG,KAAK,CAAC;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;QAC/C,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QAChD,MAAM,GAAG,IAAI,CAAC;QACd,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;QACxD,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACpC,IAAI,aAAa,CAAC,UAAU,GAAG,CAAC,CAAC,KAAK,YAAY;YAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;QACxF,oBAAoB,GAAG,UAAU,CAAC;QAClC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC7B,CAAC;IACD,IAAI,MAAM,EAAE,CAAC;QACZ,IAAI,eAAe,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,QAAQ,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,oBAAoB,KAAK,aAAa,CAAC,MAAM,GAAG,CAAC;YAAE,eAAe,IAAI,IAAI,CAAC;QAClH,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACrD,CAAC;IAED,8DAA8D;IAC9D,0EAA0E;IAC1E,8DAA8D;IAC9D,IAAI,UAAU;QAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACrE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,+BAA+B,CAC9C,OAAe,EACf,YAAoB,EACpB,GAAW,EACX,KAA4B,EAC5B,WAAmB;IAEnB,OAAO,uCAAuC,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,KAAK,EAAE,WAAW,CAAC,CAAC,OAAO,CAAC;AACxG,CAAC;AAED,MAAM,UAAU,+BAA+B,CAC9C,QAA0B,EAC1B,IAAkD,EAClD,QAAQ,GAAsB,EAAE;IAEhC,OAAO;QACN,oBAAoB,CAAC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,CAAC;QACxD,GAAG,QAAQ;QACX,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,uBAAuB,IAAI,CAAC,gBAAgB,IAAI,CAAC,EAAE;KACxE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACd,CAAC","sourcesContent":["import { isAbsolute, normalize as normalizePath, relative, sep } from \"node:path\";\nimport {\n\tcomputeFileHash,\n\tformatHashlineHeader,\n\tformatNumberedLines,\n\tInMemorySnapshotStore,\n\ttype SnapshotStore,\n} from \"./hashline-engine/index.ts\";\n\nexport interface HashlineSnapshot {\n\tabsolutePath: string;\n\tdisplayPath: string;\n\ttag: string;\n\tcontent: string;\n}\n\nexport interface HashlineSnapshotStore {\n\treadonly snapshots: SnapshotStore;\n\trecord(absolutePath: string, cwd: string, content: string): HashlineSnapshot;\n\tfindByHeader(displayPath: string, tag: string): HashlineSnapshot | undefined;\n}\n\nfunction toPosixPath(filePath: string): string {\n\treturn filePath.split(sep).join(\"/\");\n}\n\nexport function hashlineDisplayPath(absolutePath: string, cwd: string): string {\n\tconst relativePath = relative(cwd, absolutePath);\n\tif (relativePath && !relativePath.startsWith(\"..\") && !isAbsolute(relativePath)) return toPosixPath(relativePath);\n\treturn toPosixPath(absolutePath);\n}\n\n/**\n * The key a path's snapshots are filed under in the underlying {@link SnapshotStore}.\n *\n * `record` below is the only place that decides this, so anything asking the store about a\n * path it did not itself record goes through here rather than re-deriving the convention and\n * drifting from it. Note this is `path.normalize`, not `realpath`: two symlinked spellings of\n * one file are two keys here, unlike `canonicalMutationKey`.\n */\nexport function hashlineStoreKey(absolutePath: string): string {\n\treturn normalizePath(absolutePath);\n}\n\nexport function normalizeHashlineContent(content: string): string {\n\treturn content\n\t\t.replace(/^\\uFEFF/, \"\")\n\t\t.replace(/\\r\\n/g, \"\\n\")\n\t\t.replace(/\\r/g, \"\\n\");\n}\n\nexport function computeHashlineTag(content: string): string {\n\treturn computeFileHash(normalizeHashlineContent(content));\n}\n\nexport function createHashlineSnapshotStore(): HashlineSnapshotStore {\n\tconst snapshots = new InMemorySnapshotStore();\n\tconst headers = new Map<string, HashlineSnapshot>();\n\treturn {\n\t\tsnapshots,\n\t\trecord(absolutePath: string, cwd: string, content: string): HashlineSnapshot {\n\t\t\tconst normalizedPath = hashlineStoreKey(absolutePath);\n\t\t\tconst normalized = normalizeHashlineContent(content);\n\t\t\tconst displayPath = hashlineDisplayPath(normalizedPath, cwd);\n\t\t\tconst tag = snapshots.record(normalizedPath, normalized);\n\t\t\tconst snapshot = { absolutePath: normalizedPath, displayPath, tag, content: normalized };\n\t\t\theaders.set(`${displayPath}\\0${tag}`, snapshot);\n\t\t\treturn snapshot;\n\t\t},\n\t\tfindByHeader(displayPath: string, tag: string): HashlineSnapshot | undefined {\n\t\t\treturn headers.get(`${displayPath}\\0${tag.toUpperCase()}`);\n\t\t},\n\t};\n}\n\nexport function recordHashlineSnapshot(\n\tabsolutePath: string,\n\tcwd: string,\n\tcontent: string,\n\tstore: HashlineSnapshotStore,\n): HashlineSnapshot {\n\treturn store.record(absolutePath, cwd, content);\n}\n\nexport function formatHashlineContent(snapshot: HashlineSnapshot, content = snapshot.content, startLine = 1): string {\n\treturn [\n\t\tformatHashlineHeader(snapshot.displayPath, snapshot.tag),\n\t\tformatNumberedLines(normalizeHashlineContent(content), startLine),\n\t].join(\"\\n\");\n}\n\nexport interface StrippedHashlineContent {\n\tcontent: string;\n\tstripped: boolean;\n}\n\nexport function stripKnownHashlineCopiedContentWithMeta(\n\tcontent: string,\n\tabsolutePath: string,\n\tcwd: string,\n\tstore: HashlineSnapshotStore,\n\temittedPath: string,\n): StrippedHashlineContent {\n\tconst normalized = normalizeHashlineContent(content);\n\tconst lines = normalized.split(\"\\n\");\n\tconst headerIndex = lines.findIndex(\n\t\t(line, index) =>\n\t\t\t/^\\[[^\\]\\n]+#[0-9A-Fa-f]{4}\\]$/.test(line) &&\n\t\t\tlines.slice(0, index).every((prefix) => prefix.trim() === \"\" || /^#\\s+.+\\/?$/.test(prefix)),\n\t);\n\tif (headerIndex < 0) return { content, stripped: false };\n\tconst header = (lines[headerIndex] ?? \"\").match(/^\\[([^\\]\\n]+)#([0-9A-Fa-f]{4})\\]$/);\n\tif (!header) return { content, stripped: false };\n\tconst snapshot = store.findByHeader(header[1] ?? \"\", header[2] ?? \"\");\n\tif (!snapshot) return { content, stripped: false };\n\tconst body = lines.slice(headerIndex + 1);\n\tif (body.length === 0) return { content: snapshot.content, stripped: true };\n\t// The write tool emits its confirmation from the raw `path` argument it was\n\t// called with (`Successfully wrote to ${path}`), not from the resolved path,\n\t// so `emittedPath` is the authoritative anchor: it is the only form that is\n\t// guaranteed to round-trip. The resolved and cwd-relative forms are accepted\n\t// because a caller may have named the file either way, and the snapshot's own\n\t// paths are accepted because a copied confirmation may belong to the write\n\t// that produced the snapshot being copied. Every candidate is a complete\n\t// path — a bare basename is deliberately NOT accepted, since `Successfully\n\t// wrote to notes.md` is ordinary prose when the target is `deep/dir/notes.md`,\n\t// and treating it as chrome destroys that line and everything after it.\n\tconst knownConfirmationPaths = new Set<string>();\n\tconst addKnownPath = (filePath: string): void => {\n\t\tif (!filePath) return;\n\t\tknownConfirmationPaths.add(filePath);\n\t\tknownConfirmationPaths.add(filePath.replaceAll(\"\\\\\", \"/\"));\n\t\tknownConfirmationPaths.add(filePath.replaceAll(\"/\", \"\\\\\"));\n\t};\n\taddKnownPath(emittedPath);\n\taddKnownPath(absolutePath);\n\tif (absolutePath) addKnownPath(relative(cwd, absolutePath));\n\taddKnownPath(snapshot.absolutePath);\n\taddKnownPath(snapshot.displayPath);\n\tconst isKnownWriteConfirmation = (line: string): boolean => {\n\t\tconst match = line.match(/^Successfully wrote (?:\\d+ bytes )?to (.+)$/);\n\t\treturn match !== null && knownConfirmationPaths.has(match[1] ?? \"\");\n\t};\n\tconst stripped: string[] = [];\n\tconst snapshotLines = snapshot.content.split(\"\\n\");\n\tlet sawRow = false;\n\tlet lastCopiedLineNumber: number | undefined;\n\t// Trailing tool chrome a model is likely to copy along with the hashline\n\t// body: the read/search continuation footers, the write tool's own\n\t// confirmation (and its stripped-note), and `Resolved …` conflict footers.\n\t// A write confirmation is chrome only when it names the exact path the write\n\t// was asked for; a matching prefix or basename alone may be user content.\n\t// The optional byte count keeps pre-e583b290 confirmations matchable.\n\t// These footers never carry a line number, so they mark the end of the\n\t// numbered body instead of aborting stripping like an arbitrary line.\n\tconst isToolFooter = (line: string): boolean =>\n\t\tline.trim() === \"\" ||\n\t\t/^\\[\\d+ more lines in file\\./.test(line) ||\n\t\t/^\\[Showing lines /.test(line) ||\n\t\tisKnownWriteConfirmation(line) ||\n\t\t/^Resolved \\d+ conflicts?/.test(line) ||\n\t\t/^Resolved conflict \\d+/.test(line) ||\n\t\t/^Note: stripped copied hashline/.test(line) ||\n\t\t/^\\[[^\\]\\n]+#[0-9A-Fa-f]{4}\\]$/.test(line);\n\tlet onlyFooter = true;\n\tfor (const line of body) {\n\t\tif (isToolFooter(line)) {\n\t\t\t// A footer after the numbered body ends the body; a footer before any\n\t\t\t// row (leading blanks / a copied snapshot header) is just skipped.\n\t\t\tif (sawRow) break;\n\t\t\tcontinue;\n\t\t}\n\t\tonlyFooter = false;\n\t\tconst match = line.match(/^[* ]?(\\d+):(.*)$/s);\n\t\tif (!match) return { content, stripped: false };\n\t\tsawRow = true;\n\t\tconst lineNumber = Number.parseInt(match[1] ?? \"0\", 10);\n\t\tconst strippedLine = match[2] ?? \"\";\n\t\tif (snapshotLines[lineNumber - 1] !== strippedLine) return { content, stripped: false };\n\t\tlastCopiedLineNumber = lineNumber;\n\t\tstripped.push(strippedLine);\n\t}\n\tif (sawRow) {\n\t\tlet strippedContent = stripped.join(\"\\n\");\n\t\tif (snapshot.content.endsWith(\"\\n\") && lastCopiedLineNumber === snapshotLines.length - 1) strippedContent += \"\\n\";\n\t\treturn { content: strippedContent, stripped: true };\n\t}\n\n\t// Header + only tool chrome (e.g. a copied write confirmation\n\t// `Successfully wrote to <path>`) names a known snapshot with no numbered\n\t// body to recover — resolve to the snapshot's stored content.\n\tif (onlyFooter) return { content: snapshot.content, stripped: true };\n\treturn { content, stripped: false };\n}\n\nexport function stripKnownHashlineCopiedContent(\n\tcontent: string,\n\tabsolutePath: string,\n\tcwd: string,\n\tstore: HashlineSnapshotStore,\n\temittedPath: string,\n): string {\n\treturn stripKnownHashlineCopiedContentWithMeta(content, absolutePath, cwd, store, emittedPath).content;\n}\n\nexport function formatCompactHashlineEditResult(\n\tsnapshot: HashlineSnapshot,\n\tdiff: { diff?: string; firstChangedLine?: number },\n\tmessages: readonly string[] = [],\n): string {\n\treturn [\n\t\tformatHashlineHeader(snapshot.displayPath, snapshot.tag),\n\t\t...messages,\n\t\tdiff.diff?.trim() || `First changed line: ${diff.firstChangedLine ?? 1}`,\n\t].join(\"\\n\");\n}\n"]}