@bastani/atomic 0.9.19-alpha.5 → 0.9.19-alpha.7
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.
- package/CHANGELOG.md +32 -0
- package/README.md +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +6 -0
- package/dist/builtin/intercom/broker/client.ts +19 -1
- package/dist/builtin/intercom/index.bundle.mjs +8 -1
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +6 -0
- package/dist/builtin/subagents/README.md +3 -1
- package/dist/builtin/subagents/agents/codebase-online-researcher.md +7 -7
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/skills/subagent/SKILL.md +3 -1
- package/dist/builtin/subagents/src/extension/index.bundle.mjs +2 -1
- package/dist/builtin/web-access/CHANGELOG.md +13 -0
- package/dist/builtin/web-access/README.md +31 -24
- package/dist/builtin/web-access/index.bundle.mjs +16121 -80
- package/dist/builtin/web-access/package.json +2 -1
- package/dist/builtin/workflows/CHANGELOG.md +15 -0
- package/dist/builtin/workflows/README.md +1 -1
- package/dist/builtin/workflows/builtin/adversarial-verification.js +2 -2
- package/dist/builtin/workflows/builtin/{chunk-vhmnzdj9.js → chunk-1jth021m.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-p8jjwfg6.js → chunk-2377r57g.js} +4 -2
- package/dist/builtin/workflows/builtin/{chunk-hbnwv56f.js → chunk-31j2gs7h.js} +2 -2
- package/dist/builtin/workflows/builtin/{chunk-n8zysf1t.js → chunk-ngkqkzej.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-1nbq96fh.js → chunk-nkrafh9s.js} +1 -1
- package/dist/builtin/workflows/builtin/{chunk-1kx4aggw.js → chunk-wf741bv3.js} +2 -2
- package/dist/builtin/workflows/builtin/{chunk-t6x2d78s.js → chunk-z29qbehr.js} +1 -1
- package/dist/builtin/workflows/builtin/generate-and-filter.js +2 -2
- package/dist/builtin/workflows/builtin/goal.js +3 -3
- package/dist/builtin/workflows/builtin/index.js +7 -7
- package/dist/builtin/workflows/builtin/ralph.js +3 -3
- package/dist/builtin/workflows/builtin/tournament.js +2 -2
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/index.bundle.mjs +339 -31
- package/dist/builtin/workflows/src/index.js +67 -19
- package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +2 -0
- package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
- package/dist/core/agent-session-tool-registry.js +10 -0
- package/dist/core/agent-session-tool-registry.js.map +1 -1
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/tools/bash.d.ts +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +3 -2
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts +3 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +91 -12
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/file-mutation-coordinator.d.ts +255 -0
- package/dist/core/tools/file-mutation-coordinator.d.ts.map +1 -0
- package/dist/core/tools/file-mutation-coordinator.js +300 -0
- package/dist/core/tools/file-mutation-coordinator.js.map +1 -0
- package/dist/core/tools/file-mutation-queue.d.ts +14 -1
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +15 -3
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/hashline.d.ts +9 -0
- package/dist/core/tools/hashline.d.ts.map +1 -1
- package/dist/core/tools/hashline.js +12 -1
- package/dist/core/tools/hashline.js.map +1 -1
- package/dist/core/tools/index.d.ts +9 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +67 -12
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/non-minting-snapshot-store.d.ts +49 -0
- package/dist/core/tools/non-minting-snapshot-store.d.ts.map +1 -0
- package/dist/core/tools/non-minting-snapshot-store.js +64 -0
- package/dist/core/tools/non-minting-snapshot-store.js.map +1 -0
- package/dist/core/tools/powershell.d.ts +2 -2
- package/dist/core/tools/powershell.d.ts.map +1 -1
- package/dist/core/tools/powershell.js +2 -5
- package/dist/core/tools/powershell.js.map +1 -1
- package/dist/core/tools/read-document-extract.js.map +1 -1
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/write.d.ts +30 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +97 -8
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/task-live-transcript.d.ts +1 -1
- package/dist/modes/interactive/components/task-live-transcript.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-live-transcript.js +19 -11
- package/dist/modes/interactive/components/task-live-transcript.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-base.js +3 -1
- package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
- package/dist/modes/interactive/interactive-render-chat.js +28 -4
- package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
- package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-startup.js +3 -0
- package/dist/modes/interactive/interactive-startup.js.map +1 -1
- package/dist/modes/interactive-engine/activity-watchdog.d.ts +2 -2
- package/dist/modes/interactive-engine/activity-watchdog.d.ts.map +1 -1
- package/dist/modes/interactive-engine/activity-watchdog.js.map +1 -1
- package/dist/modes/interactive-engine/engine-diagnostic-view.d.ts +3 -1
- package/dist/modes/interactive-engine/engine-diagnostic-view.d.ts.map +1 -1
- package/dist/modes/interactive-engine/engine-diagnostic-view.js +11 -0
- package/dist/modes/interactive-engine/engine-diagnostic-view.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +2 -1
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/rpc/rpc-client-process.d.ts +2 -0
- package/dist/modes/rpc/rpc-client-process.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client-process.js +23 -1
- package/dist/modes/rpc/rpc-client-process.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +20 -3
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/utils/markit.d.ts +2 -0
- package/dist/utils/markit.d.ts.map +1 -1
- package/dist/utils/markit.js +65 -5
- package/dist/utils/markit.js.map +1 -1
- package/docs/background-tasks.md +15 -11
- package/docs/build.md +51 -0
- package/docs/changelog.mdx +8 -8
- package/docs/compaction/reference.md +267 -0
- package/docs/compaction.md +36 -219
- package/docs/computer-use.md +35 -0
- package/docs/containerization.md +9 -5
- package/docs/custom-provider/api-reference.md +145 -0
- package/docs/custom-provider/oauth.md +131 -0
- package/docs/custom-provider/override.md +34 -0
- package/docs/custom-provider/registration.md +163 -0
- package/docs/custom-provider/streaming.md +214 -0
- package/docs/custom-provider.md +43 -607
- package/docs/development.md +87 -0
- package/docs/docs.json +199 -76
- package/docs/environment-variables.md +1 -1
- package/docs/extensions/api-reference.md +1001 -0
- package/docs/extensions/authoring.md +676 -0
- package/docs/extensions/events.md +793 -0
- package/docs/extensions/examples.md +94 -0
- package/docs/extensions/ui.md +470 -0
- package/docs/extensions.md +270 -2768
- package/docs/getting-started/authentication.md +55 -0
- package/docs/getting-started/first-session.md +172 -0
- package/docs/getting-started/installation.md +154 -0
- package/docs/getting-started/project-instructions.md +43 -0
- package/docs/guides/configuration.md +65 -0
- package/docs/guides/intercom.md +24 -0
- package/docs/guides/non-interactive.md +33 -0
- package/docs/guides/subagents.md +24 -0
- package/docs/guides/workflows.md +27 -0
- package/docs/guides.md +41 -0
- package/docs/index.md +13 -45
- package/docs/intercom/operations.md +178 -0
- package/docs/intercom/reference.md +146 -0
- package/docs/intercom.md +54 -259
- package/docs/json.md +17 -0
- package/docs/llama-cpp.md +5 -0
- package/docs/models/artificial-analysis-index.md +89 -0
- package/docs/models/evals.md +2 -2
- package/docs/models/model-selection.md +3 -3
- package/docs/models/pareto-efficiency.md +2 -2
- package/docs/models/reference.md +601 -0
- package/docs/models.md +45 -550
- package/docs/packages/authoring.md +88 -0
- package/docs/packages/reference.md +43 -0
- package/docs/packages.md +28 -107
- package/docs/programmatic.md +29 -0
- package/docs/prompt-templates.md +11 -0
- package/docs/providers/reference.md +23 -0
- package/docs/providers.md +10 -15
- package/docs/quickstart.md +44 -238
- package/docs/reference/cli.md +252 -0
- package/docs/reference.md +52 -0
- package/docs/rpc/examples.md +64 -0
- package/docs/rpc/extension-ui.md +198 -0
- package/docs/rpc/protocol.md +1341 -0
- package/docs/rpc.md +109 -1346
- package/docs/sdk/reference.md +1090 -0
- package/docs/sdk.md +56 -1002
- package/docs/session-format.md +2 -0
- package/docs/sessions.md +6 -10
- package/docs/settings.md +6 -45
- package/docs/skills/authoring.md +95 -0
- package/docs/skills/reference.md +60 -0
- package/docs/skills.md +20 -122
- package/docs/subagents/authoring.md +37 -0
- package/docs/subagents/reference.md +60 -0
- package/docs/subagents.md +42 -96
- package/docs/terminal-setup.md +1 -1
- package/docs/themes/reference.md +176 -0
- package/docs/themes.md +21 -135
- package/docs/tmux.md +5 -0
- package/docs/tools/edit.md +5 -0
- package/docs/tools.md +31 -0
- package/docs/tui/reference.md +343 -0
- package/docs/tui.md +23 -286
- package/docs/usage.md +26 -209
- package/docs/web-access.md +40 -0
- package/docs/windows.md +2 -1
- package/docs/workflows/api-reference.md +11 -4
- package/docs/workflows/authoring.md +4 -2
- package/docs/workflows/builtins.md +2 -2
- package/docs/workflows/operations.md +11 -7
- package/docs/workflows/reliable-design.md +21 -3
- package/docs/workflows/verification.md +8 -0
- package/examples/extensions/gondolin/index.ts +116 -148
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/ssh.ts +56 -34
- package/npm-shrinkwrap.json +33 -33
- package/package.json +4 -4
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { type SnapshotStore } from "./hashline-engine/index.ts";
|
|
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 declare const FILE_MUTATION_CONFLICT_CODE = "FILE_MUTATION_CONFLICT";
|
|
15
|
+
/**
|
|
16
|
+
* Reasons report a {@link FileMutationLiveState} wherever the live target can be described,
|
|
17
|
+
* which is everywhere except `target_unreadable`: that one exists precisely because the target
|
|
18
|
+
* could not be read, so claiming a size or a tag for it would be invention.
|
|
19
|
+
*
|
|
20
|
+
* Only the reasons that have something to compare against also report a
|
|
21
|
+
* {@link FileMutationConflictEvidence} diff: `no_prior_observation` never read the file, and
|
|
22
|
+
* `foreign_snapshot` presented a tag naming content this session has never seen, so in both
|
|
23
|
+
* cases there is no prior side to diff.
|
|
24
|
+
*/
|
|
25
|
+
export type FileMutationConflictReason =
|
|
26
|
+
/** Live content moved between the mutation being prepared and the write being attempted. */
|
|
27
|
+
"changed_before_write"
|
|
28
|
+
/** Overwrite attempted with no prior read of this file in this session. */
|
|
29
|
+
| "no_prior_observation"
|
|
30
|
+
/** Overwrite attempted, but what this session observed is no longer the live content. */
|
|
31
|
+
| "changed_since_observation"
|
|
32
|
+
/** Exclusive create lost: the target already exists. */
|
|
33
|
+
| "target_exists"
|
|
34
|
+
/** The target no longer exists: removed between preparing the mutation and writing it. */
|
|
35
|
+
| "target_missing"
|
|
36
|
+
/**
|
|
37
|
+
* The target still exists but can no longer be read: replaced by a directory, made
|
|
38
|
+
* unreadable, locked, or rewritten as content this reader cannot parse. Distinct from a
|
|
39
|
+
* content change because there is nothing to diff, and from a deletion because the path is
|
|
40
|
+
* still occupied.
|
|
41
|
+
*/
|
|
42
|
+
| "target_unreadable"
|
|
43
|
+
/** The presented tag was never minted in this session's snapshot store. */
|
|
44
|
+
| "foreign_snapshot";
|
|
45
|
+
/**
|
|
46
|
+
* Correlation only.
|
|
47
|
+
*
|
|
48
|
+
* Flora, 2026-08-13: identity "must never grant permission or identify a current owner or
|
|
49
|
+
* culprit". So this describes the writer being *rejected*, never a winner, and nothing in
|
|
50
|
+
* this module reads it to decide whether a mutation is admitted. A record of the last Atomic
|
|
51
|
+
* writer could not prove ownership anyway, because shell commands, external programs, other
|
|
52
|
+
* Atomic processes and symlink changes all bypass this coordinator.
|
|
53
|
+
*/
|
|
54
|
+
export interface MutationRequester {
|
|
55
|
+
readonly sessionId: string;
|
|
56
|
+
/** Session that spawned this one, from the child's `supervisor.supervisorSessionId`. */
|
|
57
|
+
readonly parentSessionId?: string;
|
|
58
|
+
readonly workflowRunId?: string;
|
|
59
|
+
readonly workflowStageId?: string;
|
|
60
|
+
readonly workflowStageName?: string;
|
|
61
|
+
readonly subagentRunId?: string;
|
|
62
|
+
readonly subagentAgent?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Position within a fan-out. Needed to tell siblings apart: a child's intercom address is
|
|
65
|
+
* `agent + runId + index` (`resolveSubagentIntercomTarget`), so a stage that spawns several
|
|
66
|
+
* children of one agent onto one file yields identical identity without it.
|
|
67
|
+
*/
|
|
68
|
+
readonly subagentIndex?: number;
|
|
69
|
+
/** Opaque per-attempt UUID. Diagnostic only; never an authority. */
|
|
70
|
+
readonly attemptId?: string;
|
|
71
|
+
/** The specific tool call rejected. Narrower than the attempt, which makes many calls. */
|
|
72
|
+
readonly toolCallId?: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolved per execution rather than captured once.
|
|
76
|
+
*
|
|
77
|
+
* Mirrors `sessionTempDir` in the session tool registry, which is a thunk "so bash spill
|
|
78
|
+
* files follow the live transcript session across fork/branch/resume". A requester captured
|
|
79
|
+
* at tool-construction time would keep a stale session id through exactly those transitions,
|
|
80
|
+
* which is the relaunch case this feature exists to make legible.
|
|
81
|
+
*
|
|
82
|
+
* Takes the tool call id rather than leaving callers to merge it in, because it has a
|
|
83
|
+
* different lifetime from the rest: session identity belongs to the session, the call id is
|
|
84
|
+
* an argument of the single `execute` being rejected.
|
|
85
|
+
*/
|
|
86
|
+
export type MutationRequesterResolver = (toolCallId: string) => MutationRequester | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* What actually diverged, so a conflict can be acted on without a second round trip.
|
|
89
|
+
*
|
|
90
|
+
* A reason and a path are not evidence; they say a race happened, not what it did. Whatever acts
|
|
91
|
+
* on the rejection, a workflow applying its own control behavior or a human reading the error,
|
|
92
|
+
* needs the divergence itself before it can decide anything.
|
|
93
|
+
*
|
|
94
|
+
* The span is computed by trimming the common prefix and suffix rather than comparing line by
|
|
95
|
+
* line positionally, so inserting one line at the top reports one changed region and not every
|
|
96
|
+
* following line.
|
|
97
|
+
*/
|
|
98
|
+
export interface FileMutationConflictEvidence {
|
|
99
|
+
/** 1-based line of the first divergence. */
|
|
100
|
+
readonly line: number;
|
|
101
|
+
/** Lines the rejected mutation assumed occupied the divergent span. */
|
|
102
|
+
readonly assumedLines: number;
|
|
103
|
+
/** Lines actually occupying it now. */
|
|
104
|
+
readonly foundLines: number;
|
|
105
|
+
/** First assumed line, clamped. Absent when the span is pure insertion. */
|
|
106
|
+
readonly assumed?: string;
|
|
107
|
+
/** First live line, clamped. Absent when the span is pure deletion. */
|
|
108
|
+
readonly found?: string;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The target as it stood at the moment of refusal, independent of any comparison.
|
|
112
|
+
*
|
|
113
|
+
* Kept separate from {@link FileMutationConflictEvidence} because the two answer different
|
|
114
|
+
* questions and only one of them can be unanswerable. A conflict may have nothing to diff
|
|
115
|
+
* against, as when the session never read the file, but there is still always something worth
|
|
116
|
+
* reporting about what the mutation was aimed at. Size in particular is the difference between
|
|
117
|
+
* clobbering an empty placeholder and clobbering four hundred lines of production code.
|
|
118
|
+
*/
|
|
119
|
+
export interface FileMutationLiveState {
|
|
120
|
+
/** Lines currently in the file. Zero when it is empty or does not exist. */
|
|
121
|
+
readonly lines: number;
|
|
122
|
+
/** Tag of the live content, so a rejected tag can be told apart from the real one. */
|
|
123
|
+
readonly tag?: string;
|
|
124
|
+
/** First line, clamped. Absent when the file is missing, empty, or starts with a blank line. */
|
|
125
|
+
readonly firstLine?: string;
|
|
126
|
+
}
|
|
127
|
+
/** Describe live content, or a missing target when `live` is `undefined`. */
|
|
128
|
+
export declare function computeLiveState(live: string | undefined): FileMutationLiveState;
|
|
129
|
+
/**
|
|
130
|
+
* Compare what a mutation assumed against what is live, returning `undefined` when they agree.
|
|
131
|
+
*
|
|
132
|
+
* Both sides must already share a line-ending convention. `Snapshot.text` is normalized to LF,
|
|
133
|
+
* so a caller passing raw live bytes on Windows would report every line as divergent.
|
|
134
|
+
*/
|
|
135
|
+
export declare function computeConflictEvidence(assumed: string, found: string): FileMutationConflictEvidence | undefined;
|
|
136
|
+
export interface FileMutationConflictDetails {
|
|
137
|
+
readonly reason: FileMutationConflictReason;
|
|
138
|
+
/** Display path, as the model referred to it. */
|
|
139
|
+
readonly path: string;
|
|
140
|
+
/** Symlink-resolved key, from `canonicalMutationKey`. */
|
|
141
|
+
readonly canonicalKey: string;
|
|
142
|
+
readonly requester?: MutationRequester;
|
|
143
|
+
/** The tag the rejected mutation presented, when it had one. */
|
|
144
|
+
readonly presentedTag?: string;
|
|
145
|
+
/** Absent when the reason carries no comparison, as with `no_prior_observation`. */
|
|
146
|
+
readonly evidence?: FileMutationConflictEvidence;
|
|
147
|
+
/** What the mutation was aimed at. Absent only when it could not be read at all. */
|
|
148
|
+
readonly liveState?: FileMutationLiveState;
|
|
149
|
+
/**
|
|
150
|
+
* Filesystem error code that classified the conflict, such as `EISDIR` or `EACCES`. For a
|
|
151
|
+
* target that became unreadable this is the whole diagnosis, since nothing else about the
|
|
152
|
+
* file can be reported once the read fails.
|
|
153
|
+
*/
|
|
154
|
+
readonly causeCode?: string;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Raised instead of a bare `Error` so a conflict is countable rather than merely readable.
|
|
158
|
+
* The message is the wire format; see {@link FILE_MUTATION_CONFLICT_CODE}.
|
|
159
|
+
*/
|
|
160
|
+
export declare class FileMutationConflict extends Error {
|
|
161
|
+
readonly reason: FileMutationConflictReason;
|
|
162
|
+
readonly path: string;
|
|
163
|
+
readonly canonicalKey: string;
|
|
164
|
+
readonly requester: MutationRequester | undefined;
|
|
165
|
+
readonly presentedTag: string | undefined;
|
|
166
|
+
/** Structured alongside the message so a ledger can record it without re-parsing text. */
|
|
167
|
+
readonly evidence: FileMutationConflictEvidence | undefined;
|
|
168
|
+
readonly liveState: FileMutationLiveState | undefined;
|
|
169
|
+
readonly causeCode: string | undefined;
|
|
170
|
+
constructor(details: FileMutationConflictDetails);
|
|
171
|
+
/**
|
|
172
|
+
* Ordered by what must survive truncation: the code first, then the reason and guidance,
|
|
173
|
+
* then the divergence, and the live-state summary last. The tail is the most expendable
|
|
174
|
+
* because it is the part a reader can always recover by opening the file.
|
|
175
|
+
*/
|
|
176
|
+
static formatMessage(details: FileMutationConflictDetails): string;
|
|
177
|
+
}
|
|
178
|
+
/** Whether a thrown value is an exclusive-create collision reported by the filesystem. */
|
|
179
|
+
export declare function isExclusiveCreateCollision(error: unknown): boolean;
|
|
180
|
+
/**
|
|
181
|
+
* Whether a thrown value says the target is gone. `ENOTDIR` counts because a parent directory
|
|
182
|
+
* replaced by a file makes the path unreachable for the same reason a deletion does.
|
|
183
|
+
*/
|
|
184
|
+
export declare function isMissingTargetError(error: unknown): boolean;
|
|
185
|
+
/** The `code` a filesystem rejection carries, when it carries one. */
|
|
186
|
+
export declare function filesystemErrorCode(error: unknown): string | undefined;
|
|
187
|
+
/**
|
|
188
|
+
* Guard for the window between preparing a mutation and committing it, called under the
|
|
189
|
+
* mutation queue.
|
|
190
|
+
*
|
|
191
|
+
* `prepared` is the content the mutation was computed against, which for a hashline edit is
|
|
192
|
+
* the file as `prepare` read it, NOT what the session originally observed, since `prepare`
|
|
193
|
+
* re-reads and may have recovered onto drifted content. Naming that honestly matters: this
|
|
194
|
+
* closes a time-of-check window, it does not prove the session read the file.
|
|
195
|
+
*
|
|
196
|
+
* Full text rather than tags: a hashline tag is 16 bits and the engine documents that it can
|
|
197
|
+
* collide, so it is a label for the model to recognize, never proof of identity.
|
|
198
|
+
*/
|
|
199
|
+
export declare function assertLiveMatchesPrepared(args: {
|
|
200
|
+
readonly canonicalKey: string;
|
|
201
|
+
readonly path: string;
|
|
202
|
+
readonly prepared: string;
|
|
203
|
+
readonly live: string;
|
|
204
|
+
readonly requester?: MutationRequester;
|
|
205
|
+
readonly presentedTag?: string;
|
|
206
|
+
}): void;
|
|
207
|
+
/**
|
|
208
|
+
* Guard for `write` overwriting an existing file: this session must already have observed the
|
|
209
|
+
* exact content it is about to replace.
|
|
210
|
+
*
|
|
211
|
+
* Asks the session's own store and never another session's, which is what keeps cross-session
|
|
212
|
+
* rejection strict. `byHashAndText` answers both halves of the requirement at once, since a
|
|
213
|
+
* hit means this session recorded exactly this content for this path.
|
|
214
|
+
*
|
|
215
|
+
* `storeKey` is the snapshot store's own key convention, the normalized absolute path, which
|
|
216
|
+
* is NOT symlink-resolved unlike `canonicalKey`. Passing the wrong one makes a symlinked path
|
|
217
|
+
* miss and report a false `no_prior_observation`.
|
|
218
|
+
*
|
|
219
|
+
* The tag is derived here rather than accepted. Unlike the engine's own use of
|
|
220
|
+
* `byHashAndText`, where the hash is the independent claim a section presented, `write` has no
|
|
221
|
+
* presented tag: it replaces the whole file. Any tag is therefore just `live` hashed, and
|
|
222
|
+
* taking it as a parameter only creates a way for the two to disagree silently.
|
|
223
|
+
*/
|
|
224
|
+
export declare function assertPriorSessionObservation(args: {
|
|
225
|
+
readonly canonicalKey: string;
|
|
226
|
+
readonly storeKey: string;
|
|
227
|
+
readonly path: string;
|
|
228
|
+
readonly store: SnapshotStore;
|
|
229
|
+
readonly live: string;
|
|
230
|
+
readonly requester?: MutationRequester;
|
|
231
|
+
}): void;
|
|
232
|
+
/**
|
|
233
|
+
* Build a requester from parts already present on a session. Kept pure so it is testable
|
|
234
|
+
* without constructing an `AgentSession`, and so this module never imports one.
|
|
235
|
+
*/
|
|
236
|
+
export declare function buildMutationRequester(parts: {
|
|
237
|
+
readonly sessionId: string;
|
|
238
|
+
readonly orchestration?: {
|
|
239
|
+
readonly kind: string;
|
|
240
|
+
readonly workflowRunId: string;
|
|
241
|
+
readonly workflowStageId: string;
|
|
242
|
+
readonly workflowStageName: string;
|
|
243
|
+
};
|
|
244
|
+
readonly intercom?: {
|
|
245
|
+
readonly runId: string;
|
|
246
|
+
readonly agent: string;
|
|
247
|
+
readonly index?: number;
|
|
248
|
+
readonly supervisor?: {
|
|
249
|
+
readonly supervisorSessionId: string;
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
readonly attemptId?: string;
|
|
253
|
+
readonly toolCallId?: string;
|
|
254
|
+
}): MutationRequester;
|
|
255
|
+
//# sourceMappingURL=file-mutation-coordinator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-mutation-coordinator.d.ts","sourceRoot":"","sources":["../../../src/core/tools/file-mutation-coordinator.ts"],"names":[],"mappings":"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"}
|
|
@@ -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"]}
|