@mikode13/harness 9.2.0 → 9.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +6 -3
  2. package/dist/engines/domain/model/historyFollower.d.ts +1 -1
  3. package/dist/engines/domain/model/historyFollower.js +14 -3
  4. package/dist/engines/domain/model/historyFollower.js.map +1 -1
  5. package/dist/factory/infrastructure/agentLLMFactory.d.ts +2 -2
  6. package/dist/factory/infrastructure/agentLLMFactory.js +5 -5
  7. package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
  8. package/dist/recovery/domain/history.d.ts +3 -2
  9. package/dist/recovery/domain/history.js +6 -2
  10. package/dist/recovery/domain/history.js.map +1 -1
  11. package/dist/recovery/domain/recoveryStore.d.ts +47 -5
  12. package/dist/recovery/domain/recoveryStore.js.map +1 -1
  13. package/dist/recovery/domain/runChanges.js +11 -2
  14. package/dist/recovery/domain/runChanges.js.map +1 -1
  15. package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +41 -9
  16. package/dist/recovery/infrastructure/fileRecoveryStore.js +217 -65
  17. package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -1
  18. package/dist/tools/domain/editResponse.d.ts +7 -2
  19. package/dist/tools/domain/editResponse.js +33 -11
  20. package/dist/tools/domain/editResponse.js.map +1 -1
  21. package/dist/tools/domain/v4aPatch.d.ts +6 -6
  22. package/dist/tools/domain/v4aPatch.js +59 -21
  23. package/dist/tools/domain/v4aPatch.js.map +1 -1
  24. package/dist/tools/infrastructure/commandCheckpoint.d.ts +3 -2
  25. package/dist/tools/infrastructure/commandCheckpoint.js +21 -9
  26. package/dist/tools/infrastructure/commandCheckpoint.js.map +1 -1
  27. package/dist/tools/infrastructure/commandTool.js +1 -1
  28. package/dist/tools/infrastructure/commandTool.js.map +1 -1
  29. package/dist/tools/infrastructure/fileTools.d.ts +1 -1
  30. package/dist/tools/infrastructure/fileTools.js +3 -3
  31. package/dist/tools/infrastructure/fileTools.js.map +1 -1
  32. package/dist/tools/infrastructure/trackedEdits.d.ts +3 -3
  33. package/dist/tools/infrastructure/trackedEdits.js +17 -11
  34. package/dist/tools/infrastructure/trackedEdits.js.map +1 -1
  35. package/dist/tools/infrastructure/workspaceWrites.d.ts +8 -5
  36. package/dist/tools/infrastructure/workspaceWrites.js +19 -7
  37. package/dist/tools/infrastructure/workspaceWrites.js.map +1 -1
  38. package/package.json +1 -1
@@ -36,6 +36,22 @@ export interface JournalEntry {
36
36
  */
37
37
  export interface Manifest {
38
38
  files: Record<string, FileState>;
39
+ /**
40
+ * Covered paths it could not copy, such as a folder, a link or a file too large: what they
41
+ * held is unknown, so a change to one can be named but not undone.
42
+ */
43
+ uncopyable?: string[];
44
+ }
45
+ /**
46
+ * A path a command changed into, or from, something the history cannot copy, such as a file
47
+ * over the size limit: what it held before or after is unknown. Undo and redo leave it as it is,
48
+ * and report it as a conflict.
49
+ */
50
+ export interface UnrecordedChange {
51
+ /** The real path, absolute. */
52
+ path: string;
53
+ /** The command it was seen around. */
54
+ by: string;
39
55
  }
40
56
  /** What one observation cost: before and after together. */
41
57
  export interface ObservationMeasure {
@@ -71,6 +87,12 @@ export interface RunRecord {
71
87
  startedAt: string;
72
88
  finishedAt?: string;
73
89
  status: RunStatus;
90
+ /**
91
+ * It finished with an observation still open, such as a command whose changes could not be
92
+ * compared after it ran: its journal may not hold all it changed yet. The next run or move
93
+ * settles it, as for an interrupted run, and keeps its status.
94
+ */
95
+ unsettled?: true;
74
96
  /** The process that ran it, so a run whose process died can be told from a live one. */
75
97
  pid: number;
76
98
  }
@@ -103,6 +125,11 @@ export interface RunJournal {
103
125
  * files cost, is kept with it.
104
126
  */
105
127
  observed(sequence: number, measure?: ObservationMeasure): Promise<void>;
128
+ /**
129
+ * Records that `by` changed `path` into, or from, something the history cannot copy. A move
130
+ * over the run leaves it as it is, and is not complete.
131
+ */
132
+ unrecorded(path: string, by: string): Promise<void>;
106
133
  /** How many bytes of content this run stored that the store did not hold yet. */
107
134
  readonly storedBytes: number;
108
135
  /** The path of every change it recorded as applied, in order: each one reached a file. */
@@ -114,20 +141,27 @@ export interface RunJournal {
114
141
  */
115
142
  contentState(): string;
116
143
  /**
117
- * Whether a change it recorded may have reached a file: one prepared and not abandoned. It
118
- * holds whatever happens to the record afterwards.
144
+ * Whether a change it recorded may have reached a file: one prepared and not abandoned, or one
145
+ * it could not copy. It holds whatever happens to the record afterwards, except once `finish`
146
+ * settled an observation the run left open and found that the run changed nothing: the run
147
+ * left the history, so it is `false`.
119
148
  */
120
149
  readonly changed: boolean;
121
150
  /**
122
151
  * Ends the run and releases the workspace for the next writer. A run that `changed` nothing
123
152
  * leaves the history, and the workspace is back at the run before it. If removing it fails,
124
- * the store still treats it as gone, and the next run or move finishes removing it.
153
+ * the store still treats it as gone, and the next run or move finishes removing it. A run that
154
+ * ends with an observation open is settled from its manifest now, or by the next run or move
155
+ * if that fails.
125
156
  */
126
157
  finish(status: Exclude<RunStatus, 'running' | 'interrupted'>): Promise<void>;
127
158
  }
128
159
  /** The point before any run: going there takes the workspace back to how it was. */
129
160
  export declare const historyStart = "start";
130
- /** A file a move left as it was, because it did not hold what the history expected there. */
161
+ /**
162
+ * A file a move left as it was: it did not hold what the history expected there, or a command
163
+ * changed it into, or from, something the history cannot copy.
164
+ */
131
165
  export interface MoveConflict {
132
166
  path: string;
133
167
  /** The run whose change could not be undone or redone. */
@@ -169,6 +203,12 @@ export declare class NothingToMoveError extends Error {
169
203
  * workspace itself. Only one run writes to a workspace at a time.
170
204
  */
171
205
  export interface RecoveryStore {
206
+ /**
207
+ * The history on disk this store keeps, from the real paths of its root and its state
208
+ * directory: two stores opened over one history, under any spelling of those paths, have the
209
+ * same one.
210
+ */
211
+ readonly workspaceId: string;
172
212
  /**
173
213
  * Starts recording a run and holds the workspace for it until `finish`. The new run's parent
174
214
  * is the run the workspace is at, and the workspace is at the new run from then on. Runs a
@@ -215,7 +255,8 @@ export interface RecoveryStore {
215
255
  /** Every move of the workspace, oldest first. */
216
256
  listRevisions(): Promise<Revision[]>;
217
257
  /**
218
- * A run's record and its changes, in the order they were prepared.
258
+ * A run's record, its changes, in the order they were prepared, and the changes it could not
259
+ * copy.
219
260
  *
220
261
  * @throws {UnknownRunError} when `runId` was never a run of this workspace.
221
262
  * @throws {HistoryExpiredError} when retention removed `runId` or chained it into a later run.
@@ -223,6 +264,7 @@ export interface RecoveryStore {
223
264
  readRun(runId: string): Promise<{
224
265
  record: RunRecord;
225
266
  entries: JournalEntry[];
267
+ unrecorded: UnrecordedChange[];
226
268
  }>;
227
269
  /** The bytes a hash names. */
228
270
  readContent(hash: string): Promise<Buffer>;
@@ -1 +1 @@
1
- {"version":3,"file":"recoveryStore.js","sourceRoot":"","sources":["../../../src/recovery/domain/recoveryStore.ts"],"names":[],"mappings":"AAgIA,oFAAoF;AACpF,MAAM,CAAC,MAAM,YAAY,GAAG,OAAO,CAAC;AAsCpC,2FAA2F;AAC3F,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC5C,YAAY,SAA0B;QACrC,KAAK,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC;QACpF,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IAClC,CAAC;CACD;AA4DD,uFAAuF;AACvF,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC7C,uEAAuE;IAC9D,QAAQ,CAAS;IAC1B,wDAAwD;IAC/C,MAAM,CAAS;IAExB,YAAY,QAAgB,EAAE,MAAc;QAC3C,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;QACxF,KAAK,CAAC,iCAAiC,IAAI,CAAC,QAAQ,CAAC,cAAc,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACnF,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;CACD;AAED,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACnC,KAAK,CAAS;IAEvB,YAAY,KAAa;QACxB,KAAK,CAAC,0BAA0B,KAAK,mBAAmB,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACpB,CAAC;CACD","sourcesContent":["/**\n * What one path held at one moment: nothing, or a regular file with this content and mode.\n * `hash` names the content in the store, so the bytes themselves never travel in a record.\n */\nexport type FileState = { exists: false } | { exists: true; hash: string; mode: number };\n\n/**\n * One change to one file. It is recorded as `prepared` before the file is touched, and only\n * then applied, so a run that stops halfway always leaves a record of what it may have done.\n * `abandoned` means the change was prepared but never happened.\n */\nexport interface JournalEntry {\n\tsequence: number;\n\t/** The file's real path, absolute: a run may write to more than one root of its workspace. */\n\tpath: string;\n\tbefore: FileState;\n\tafter: FileState;\n\t/** Folders the change creates for a new file, outermost first: undo removes them if empty. */\n\tcreatedFolders?: string[];\n\t/**\n\t * The command whose run the change was seen around, for a change no tool of the harness made\n\t * itself. It was found by comparing the file before and after, not journaled before it was made.\n\t */\n\tobservedBy?: string;\n\tstatus: 'prepared' | 'applied' | 'abandoned';\n}\n\n/**\n * What a group of files held right before something outside the journal ran, such as a\n * command: the content of each, by its real path, absolute. It is stored as content, and the\n * journal names its hash before the command starts.\n */\nexport interface Manifest {\n\tfiles: Record<string, FileState>;\n}\n\n/** What one observation cost: before and after together. */\nexport interface ObservationMeasure {\n\t/** Files captured before, and compared after. */\n\tfiles: number;\n\tbytesRead: number;\n\t/** New content stored, which the store did not hold already. */\n\tbytesStored: number;\n\t/** Spent capturing and comparing, not running what was observed. */\n\tdurationMs: number;\n}\n\n/**\n * `interrupted` is a run whose process died while it was writing. The next run to take the\n * workspace settles each change it left `prepared`, from what the file holds now.\n */\nexport type RunStatus = 'running' | 'completed' | 'failed' | 'cancelled' | 'interrupted';\n\nexport interface RunRecord {\n\trunId: string;\n\t/** The real path of the workspace root the run wrote to. */\n\troot: string;\n\t/**\n\t * The run the workspace was at when this one started, so runs form a tree: going back to an\n\t * earlier run and writing again starts a new branch. Absent for the first run.\n\t */\n\tparentRunId?: string;\n\t/**\n\t * Runs whose changes retention chained into this one, oldest first. They are no longer in\n\t * the history: this run now goes back to where the oldest of them started.\n\t */\n\tabsorbed?: string[];\n\t/** The journal file that holds the run's changes. Absent while it is the first one written. */\n\tjournal?: string;\n\tstartedAt: string;\n\tfinishedAt?: string;\n\tstatus: RunStatus;\n\t/** The process that ran it, so a run whose process died can be told from a live one. */\n\tpid: number;\n}\n\n/**\n * The recovery record of one run that writes. Every method returns only once what it records\n * is on disk: a change is never made before the record that would undo it exists. Once a step\n * fails to reach the journal, the journal refuses every later step, because a line written\n * after a broken one could not be read back.\n */\nexport interface RunJournal {\n\treadonly runId: string;\n\t/** Stores `content` and returns the hash that names it. Content stored before is reused. */\n\tsaveContent(content: Buffer): Promise<string>;\n\t/** Records a change about to be made, and returns its sequence number. */\n\tprepare(change: Omit<JournalEntry, 'sequence' | 'status'>): Promise<number>;\n\t/** The change was made. */\n\tapplied(sequence: number): Promise<void>;\n\t/** The change was not made, and the file still holds its `before`. */\n\tabandoned(sequence: number): Promise<void>;\n\t/**\n\t * Records that `by`, such as a command, is about to change files the journal cannot see it\n\t * change, and that the stored `manifest` holds what they hold now. Each change it makes is\n\t * then recorded as its own entry, prepared and applied at once, before `observed`. If the run\n\t * stops first, the next run settles it: every file of the manifest that changed since is\n\t * recorded as changed by `by`. A file it created is not in the manifest, and is then missed.\n\t */\n\tobserve(manifest: string, by: string): Promise<number>;\n\t/**\n\t * Every change the observation found is recorded. `measure`, what capturing and comparing the\n\t * files cost, is kept with it.\n\t */\n\tobserved(sequence: number, measure?: ObservationMeasure): Promise<void>;\n\t/** How many bytes of content this run stored that the store did not hold yet. */\n\treadonly storedBytes: number;\n\t/** The path of every change it recorded as applied, in order: each one reached a file. */\n\treadonly appliedPaths: readonly string[];\n\t/**\n\t * What the files this run changed hold now, as one hash of their paths and states. A file back\n\t * to what it held before the run's first change to it counts as unchanged, so a change undone\n\t * gives the state from before it.\n\t */\n\tcontentState(): string;\n\t/**\n\t * Whether a change it recorded may have reached a file: one prepared and not abandoned. It\n\t * holds whatever happens to the record afterwards.\n\t */\n\treadonly changed: boolean;\n\t/**\n\t * Ends the run and releases the workspace for the next writer. A run that `changed` nothing\n\t * leaves the history, and the workspace is back at the run before it. If removing it fails,\n\t * the store still treats it as gone, and the next run or move finishes removing it.\n\t */\n\tfinish(status: Exclude<RunStatus, 'running' | 'interrupted'>): Promise<void>;\n}\n\n/** The point before any run: going there takes the workspace back to how it was. */\nexport const historyStart = 'start';\n\n/** A file a move left as it was, because it did not hold what the history expected there. */\nexport interface MoveConflict {\n\tpath: string;\n\t/** The run whose change could not be undone or redone. */\n\trunId: string;\n}\n\n/**\n * One move of the workspace through its history, kept so an agent that runs next can tell\n * that its conversation is about a state the workspace has left.\n */\nexport interface Revision {\n\t/** Counts the moves of this workspace, from 1. */\n\trevision: number;\n\t/** The run the workspace was at; absent for the start of the history. */\n\tfrom?: string;\n\t/** The run the workspace is at now; absent for the start of the history. */\n\tto?: string;\n\tat: string;\n\t/** Why the user went there, when the host asked. */\n\treason?: string;\n\t/** Every file reached the state the history expected. Otherwise `conflicts` names those that did not. */\n\tcomplete: boolean;\n\tconflicts: MoveConflict[];\n}\n\nexport interface MoveOptions {\n\t/** Why the user is moving the workspace, recorded with the move. */\n\treason?: string;\n\t/**\n\t * Where the workspace must be for the move to happen: a run, or `historyStart`. Checked under\n\t * the workspace lock, so a move confirmed against one state never runs from another.\n\t */\n\tfrom?: string;\n}\n\n/** There is nothing to undo at the start of the history, or nothing to redo at its tip. */\nexport class NothingToMoveError extends Error {\n\tconstructor(direction: 'undo' | 'redo') {\n\t\tsuper(direction === 'undo' ? 'There is no run to undo' : 'There is no run to redo');\n\t\tthis.name = 'NothingToMoveError';\n\t}\n}\n\n/**\n * Where the runs that write to one workspace keep what is needed to undo them, outside the\n * workspace itself. Only one run writes to a workspace at a time.\n */\nexport interface RecoveryStore {\n\t/**\n\t * Starts recording a run and holds the workspace for it until `finish`. The new run's parent\n\t * is the run the workspace is at, and the workspace is at the new run from then on. Runs a\n\t * dead process left `running` are settled first.\n\t *\n\t * @throws {WorkspaceBusyError} when another live run is writing to the workspace.\n\t */\n\tstartRun(): Promise<RunJournal>;\n\t/**\n\t * Every run, oldest first, and the run the workspace is at (`head`), absent before the first.\n\t * A run still recorded as `running` whose process is gone is listed as `interrupted`.\n\t */\n\tlistRuns(): Promise<{ head?: string; runs: RunRecord[] }>;\n\t/**\n\t * Moves the workspace to the state right after `target`, or to how it was before any run\n\t * with `historyStart`. It undoes runs one by one back to the closest common ancestor, then\n\t * redoes runs one by one forward to `target`. Each file is written only if it holds the\n\t * state the history expects there; one that does not is left as it is and reported, and the\n\t * move is then not `complete`. A move the process did not finish is finished by the next\n\t * move or run.\n\t *\n\t * @throws {WorkspaceBusyError} when a run or another move holds the workspace.\n\t * @throws {WorkspaceMovedError} when `from` was given and the workspace is elsewhere.\n\t * @throws {UnknownRunError} when `target` is not in the history.\n\t * @throws {HistoryExpiredError} when retention chained `target` into a later run.\n\t */\n\tgoTo(target: string, options?: MoveOptions): Promise<Revision>;\n\t/**\n\t * Moves the workspace to the run before the one it is at.\n\t *\n\t * @throws {NothingToMoveError} at the start of the history.\n\t */\n\tundo(options?: MoveOptions): Promise<Revision>;\n\t/**\n\t * Moves the workspace to the next run: towards where it was most recently, or the newest\n\t * branch.\n\t *\n\t * @throws {NothingToMoveError} when no run follows the one it is at.\n\t */\n\tredo(options?: MoveOptions): Promise<Revision>;\n\t/** Every move of the workspace, oldest first. */\n\tlistRevisions(): Promise<Revision[]>;\n\t/**\n\t * A run's record and its changes, in the order they were prepared.\n\t *\n\t * @throws {UnknownRunError} when `runId` was never a run of this workspace.\n\t * @throws {HistoryExpiredError} when retention removed `runId` or chained it into a later run.\n\t */\n\treadRun(runId: string): Promise<{ record: RunRecord; entries: JournalEntry[] }>;\n\t/** The bytes a hash names. */\n\treadContent(hash: string): Promise<Buffer>;\n}\n\n/** A move was asked from one point of the history, and the workspace is at another. */\nexport class WorkspaceMovedError extends Error {\n\t/** Where the move expected the workspace: a run, or `historyStart`. */\n\treadonly expected: string;\n\t/** Where the workspace is: a run, or `historyStart`. */\n\treadonly actual: string;\n\n\tconstructor(expected: string, actual: string) {\n\t\tconst name = (point: string) => (point === historyStart ? 'the start' : `run ${point}`);\n\t\tsuper(`The workspace is no longer at ${name(expected)}: it is at ${name(actual)}`);\n\t\tthis.name = 'WorkspaceMovedError';\n\t\tthis.expected = expected;\n\t\tthis.actual = actual;\n\t}\n}\n\nexport class WorkspaceBusyError extends Error {\n\treadonly runId: string;\n\n\tconstructor(runId: string) {\n\t\tsuper(`The workspace is busy: ${runId} is writing to it`);\n\t\tthis.name = 'WorkspaceBusyError';\n\t\tthis.runId = runId;\n\t}\n}\n"]}
1
+ {"version":3,"file":"recoveryStore.js","sourceRoot":"","sources":["../../../src/recovery/domain/recoveryStore.ts"],"names":[],"mappings":"AAgKA,oFAAoF;AACpF,MAAM,CAAC,MAAM,YAAY,GAAG,OAAO,CAAC;AAyCpC,2FAA2F;AAC3F,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC5C,YAAY,SAA0B;QACrC,KAAK,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC;QACpF,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IAClC,CAAC;CACD;AAqED,uFAAuF;AACvF,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC7C,uEAAuE;IAC9D,QAAQ,CAAS;IAC1B,wDAAwD;IAC/C,MAAM,CAAS;IAExB,YAAY,QAAgB,EAAE,MAAc;QAC3C,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,CAAC,KAAK,KAAK,YAAY,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;QACxF,KAAK,CAAC,iCAAiC,IAAI,CAAC,QAAQ,CAAC,cAAc,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACnF,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;CACD;AAED,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACnC,KAAK,CAAS;IAEvB,YAAY,KAAa;QACxB,KAAK,CAAC,0BAA0B,KAAK,mBAAmB,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACpB,CAAC;CACD","sourcesContent":["/**\n * What one path held at one moment: nothing, or a regular file with this content and mode.\n * `hash` names the content in the store, so the bytes themselves never travel in a record.\n */\nexport type FileState = { exists: false } | { exists: true; hash: string; mode: number };\n\n/**\n * One change to one file. It is recorded as `prepared` before the file is touched, and only\n * then applied, so a run that stops halfway always leaves a record of what it may have done.\n * `abandoned` means the change was prepared but never happened.\n */\nexport interface JournalEntry {\n\tsequence: number;\n\t/** The file's real path, absolute: a run may write to more than one root of its workspace. */\n\tpath: string;\n\tbefore: FileState;\n\tafter: FileState;\n\t/** Folders the change creates for a new file, outermost first: undo removes them if empty. */\n\tcreatedFolders?: string[];\n\t/**\n\t * The command whose run the change was seen around, for a change no tool of the harness made\n\t * itself. It was found by comparing the file before and after, not journaled before it was made.\n\t */\n\tobservedBy?: string;\n\tstatus: 'prepared' | 'applied' | 'abandoned';\n}\n\n/**\n * What a group of files held right before something outside the journal ran, such as a\n * command: the content of each, by its real path, absolute. It is stored as content, and the\n * journal names its hash before the command starts.\n */\nexport interface Manifest {\n\tfiles: Record<string, FileState>;\n\t/**\n\t * Covered paths it could not copy, such as a folder, a link or a file too large: what they\n\t * held is unknown, so a change to one can be named but not undone.\n\t */\n\tuncopyable?: string[];\n}\n\n/**\n * A path a command changed into, or from, something the history cannot copy, such as a file\n * over the size limit: what it held before or after is unknown. Undo and redo leave it as it is,\n * and report it as a conflict.\n */\nexport interface UnrecordedChange {\n\t/** The real path, absolute. */\n\tpath: string;\n\t/** The command it was seen around. */\n\tby: string;\n}\n\n/** What one observation cost: before and after together. */\nexport interface ObservationMeasure {\n\t/** Files captured before, and compared after. */\n\tfiles: number;\n\tbytesRead: number;\n\t/** New content stored, which the store did not hold already. */\n\tbytesStored: number;\n\t/** Spent capturing and comparing, not running what was observed. */\n\tdurationMs: number;\n}\n\n/**\n * `interrupted` is a run whose process died while it was writing. The next run to take the\n * workspace settles each change it left `prepared`, from what the file holds now.\n */\nexport type RunStatus = 'running' | 'completed' | 'failed' | 'cancelled' | 'interrupted';\n\nexport interface RunRecord {\n\trunId: string;\n\t/** The real path of the workspace root the run wrote to. */\n\troot: string;\n\t/**\n\t * The run the workspace was at when this one started, so runs form a tree: going back to an\n\t * earlier run and writing again starts a new branch. Absent for the first run.\n\t */\n\tparentRunId?: string;\n\t/**\n\t * Runs whose changes retention chained into this one, oldest first. They are no longer in\n\t * the history: this run now goes back to where the oldest of them started.\n\t */\n\tabsorbed?: string[];\n\t/** The journal file that holds the run's changes. Absent while it is the first one written. */\n\tjournal?: string;\n\tstartedAt: string;\n\tfinishedAt?: string;\n\tstatus: RunStatus;\n\t/**\n\t * It finished with an observation still open, such as a command whose changes could not be\n\t * compared after it ran: its journal may not hold all it changed yet. The next run or move\n\t * settles it, as for an interrupted run, and keeps its status.\n\t */\n\tunsettled?: true;\n\t/** The process that ran it, so a run whose process died can be told from a live one. */\n\tpid: number;\n}\n\n/**\n * The recovery record of one run that writes. Every method returns only once what it records\n * is on disk: a change is never made before the record that would undo it exists. Once a step\n * fails to reach the journal, the journal refuses every later step, because a line written\n * after a broken one could not be read back.\n */\nexport interface RunJournal {\n\treadonly runId: string;\n\t/** Stores `content` and returns the hash that names it. Content stored before is reused. */\n\tsaveContent(content: Buffer): Promise<string>;\n\t/** Records a change about to be made, and returns its sequence number. */\n\tprepare(change: Omit<JournalEntry, 'sequence' | 'status'>): Promise<number>;\n\t/** The change was made. */\n\tapplied(sequence: number): Promise<void>;\n\t/** The change was not made, and the file still holds its `before`. */\n\tabandoned(sequence: number): Promise<void>;\n\t/**\n\t * Records that `by`, such as a command, is about to change files the journal cannot see it\n\t * change, and that the stored `manifest` holds what they hold now. Each change it makes is\n\t * then recorded as its own entry, prepared and applied at once, before `observed`. If the run\n\t * stops first, the next run settles it: every file of the manifest that changed since is\n\t * recorded as changed by `by`. A file it created is not in the manifest, and is then missed.\n\t */\n\tobserve(manifest: string, by: string): Promise<number>;\n\t/**\n\t * Every change the observation found is recorded. `measure`, what capturing and comparing the\n\t * files cost, is kept with it.\n\t */\n\tobserved(sequence: number, measure?: ObservationMeasure): Promise<void>;\n\t/**\n\t * Records that `by` changed `path` into, or from, something the history cannot copy. A move\n\t * over the run leaves it as it is, and is not complete.\n\t */\n\tunrecorded(path: string, by: string): Promise<void>;\n\t/** How many bytes of content this run stored that the store did not hold yet. */\n\treadonly storedBytes: number;\n\t/** The path of every change it recorded as applied, in order: each one reached a file. */\n\treadonly appliedPaths: readonly string[];\n\t/**\n\t * What the files this run changed hold now, as one hash of their paths and states. A file back\n\t * to what it held before the run's first change to it counts as unchanged, so a change undone\n\t * gives the state from before it.\n\t */\n\tcontentState(): string;\n\t/**\n\t * Whether a change it recorded may have reached a file: one prepared and not abandoned, or one\n\t * it could not copy. It holds whatever happens to the record afterwards, except once `finish`\n\t * settled an observation the run left open and found that the run changed nothing: the run\n\t * left the history, so it is `false`.\n\t */\n\treadonly changed: boolean;\n\t/**\n\t * Ends the run and releases the workspace for the next writer. A run that `changed` nothing\n\t * leaves the history, and the workspace is back at the run before it. If removing it fails,\n\t * the store still treats it as gone, and the next run or move finishes removing it. A run that\n\t * ends with an observation open is settled from its manifest now, or by the next run or move\n\t * if that fails.\n\t */\n\tfinish(status: Exclude<RunStatus, 'running' | 'interrupted'>): Promise<void>;\n}\n\n/** The point before any run: going there takes the workspace back to how it was. */\nexport const historyStart = 'start';\n\n/**\n * A file a move left as it was: it did not hold what the history expected there, or a command\n * changed it into, or from, something the history cannot copy.\n */\nexport interface MoveConflict {\n\tpath: string;\n\t/** The run whose change could not be undone or redone. */\n\trunId: string;\n}\n\n/**\n * One move of the workspace through its history, kept so an agent that runs next can tell\n * that its conversation is about a state the workspace has left.\n */\nexport interface Revision {\n\t/** Counts the moves of this workspace, from 1. */\n\trevision: number;\n\t/** The run the workspace was at; absent for the start of the history. */\n\tfrom?: string;\n\t/** The run the workspace is at now; absent for the start of the history. */\n\tto?: string;\n\tat: string;\n\t/** Why the user went there, when the host asked. */\n\treason?: string;\n\t/** Every file reached the state the history expected. Otherwise `conflicts` names those that did not. */\n\tcomplete: boolean;\n\tconflicts: MoveConflict[];\n}\n\nexport interface MoveOptions {\n\t/** Why the user is moving the workspace, recorded with the move. */\n\treason?: string;\n\t/**\n\t * Where the workspace must be for the move to happen: a run, or `historyStart`. Checked under\n\t * the workspace lock, so a move confirmed against one state never runs from another.\n\t */\n\tfrom?: string;\n}\n\n/** There is nothing to undo at the start of the history, or nothing to redo at its tip. */\nexport class NothingToMoveError extends Error {\n\tconstructor(direction: 'undo' | 'redo') {\n\t\tsuper(direction === 'undo' ? 'There is no run to undo' : 'There is no run to redo');\n\t\tthis.name = 'NothingToMoveError';\n\t}\n}\n\n/**\n * Where the runs that write to one workspace keep what is needed to undo them, outside the\n * workspace itself. Only one run writes to a workspace at a time.\n */\nexport interface RecoveryStore {\n\t/**\n\t * The history on disk this store keeps, from the real paths of its root and its state\n\t * directory: two stores opened over one history, under any spelling of those paths, have the\n\t * same one.\n\t */\n\treadonly workspaceId: string;\n\t/**\n\t * Starts recording a run and holds the workspace for it until `finish`. The new run's parent\n\t * is the run the workspace is at, and the workspace is at the new run from then on. Runs a\n\t * dead process left `running` are settled first.\n\t *\n\t * @throws {WorkspaceBusyError} when another live run is writing to the workspace.\n\t */\n\tstartRun(): Promise<RunJournal>;\n\t/**\n\t * Every run, oldest first, and the run the workspace is at (`head`), absent before the first.\n\t * A run still recorded as `running` whose process is gone is listed as `interrupted`.\n\t */\n\tlistRuns(): Promise<{ head?: string; runs: RunRecord[] }>;\n\t/**\n\t * Moves the workspace to the state right after `target`, or to how it was before any run\n\t * with `historyStart`. It undoes runs one by one back to the closest common ancestor, then\n\t * redoes runs one by one forward to `target`. Each file is written only if it holds the\n\t * state the history expects there; one that does not is left as it is and reported, and the\n\t * move is then not `complete`. A move the process did not finish is finished by the next\n\t * move or run.\n\t *\n\t * @throws {WorkspaceBusyError} when a run or another move holds the workspace.\n\t * @throws {WorkspaceMovedError} when `from` was given and the workspace is elsewhere.\n\t * @throws {UnknownRunError} when `target` is not in the history.\n\t * @throws {HistoryExpiredError} when retention chained `target` into a later run.\n\t */\n\tgoTo(target: string, options?: MoveOptions): Promise<Revision>;\n\t/**\n\t * Moves the workspace to the run before the one it is at.\n\t *\n\t * @throws {NothingToMoveError} at the start of the history.\n\t */\n\tundo(options?: MoveOptions): Promise<Revision>;\n\t/**\n\t * Moves the workspace to the next run: towards where it was most recently, or the newest\n\t * branch.\n\t *\n\t * @throws {NothingToMoveError} when no run follows the one it is at.\n\t */\n\tredo(options?: MoveOptions): Promise<Revision>;\n\t/** Every move of the workspace, oldest first. */\n\tlistRevisions(): Promise<Revision[]>;\n\t/**\n\t * A run's record, its changes, in the order they were prepared, and the changes it could not\n\t * copy.\n\t *\n\t * @throws {UnknownRunError} when `runId` was never a run of this workspace.\n\t * @throws {HistoryExpiredError} when retention removed `runId` or chained it into a later run.\n\t */\n\treadRun(\n\t\trunId: string,\n\t): Promise<{ record: RunRecord; entries: JournalEntry[]; unrecorded: UnrecordedChange[] }>;\n\t/** The bytes a hash names. */\n\treadContent(hash: string): Promise<Buffer>;\n}\n\n/** A move was asked from one point of the history, and the workspace is at another. */\nexport class WorkspaceMovedError extends Error {\n\t/** Where the move expected the workspace: a run, or `historyStart`. */\n\treadonly expected: string;\n\t/** Where the workspace is: a run, or `historyStart`. */\n\treadonly actual: string;\n\n\tconstructor(expected: string, actual: string) {\n\t\tconst name = (point: string) => (point === historyStart ? 'the start' : `run ${point}`);\n\t\tsuper(`The workspace is no longer at ${name(expected)}: it is at ${name(actual)}`);\n\t\tthis.name = 'WorkspaceMovedError';\n\t\tthis.expected = expected;\n\t\tthis.actual = actual;\n\t}\n}\n\nexport class WorkspaceBusyError extends Error {\n\treadonly runId: string;\n\n\tconstructor(runId: string) {\n\t\tsuper(`The workspace is busy: ${runId} is writing to it`);\n\t\tthis.name = 'WorkspaceBusyError';\n\t\tthis.runId = runId;\n\t}\n}\n"]}
@@ -9,17 +9,26 @@ const binaryProbeBytes = 8000;
9
9
  * repository.
10
10
  */
11
11
  export async function showChanges(store, runId, { maxLines = 1000, visible } = {}) {
12
- const { record, entries } = await store.readRun(runId);
12
+ const { record, entries, unrecorded } = await store.readRun(runId);
13
13
  const all = changedFiles(record, entries);
14
14
  const changes = [];
15
15
  for (const change of all)
16
16
  if (!visible || (await visible(change.path)))
17
17
  changes.push(change);
18
+ const uncopied = [];
19
+ for (const change of unrecorded) {
20
+ if (!visible || (await visible(change.path)))
21
+ uncopied.push(change);
22
+ }
18
23
  const output = [];
19
- const hidden = all.length - changes.length;
24
+ const hidden = all.length + unrecorded.length - changes.length - uncopied.length;
20
25
  if (hidden > 0) {
21
26
  output.push(`# ${String(hidden)} changed ${hidden === 1 ? 'file is' : 'files are'} not shown: you may not read ${hidden === 1 ? 'it' : 'them'}`);
22
27
  }
28
+ // Named first: no diff can show them, and they must not be cut away with the diffs.
29
+ for (const { path, by } of uncopied) {
30
+ output.push(`# ${displayPath(record.root, path)}: changed while the command "${by}" ran, into or from something the history cannot copy (a folder, a link, or a file over 64 MB); undo leaves it as it is`);
31
+ }
23
32
  for (const [index, change] of changes.entries()) {
24
33
  const name = displayPath(record.root, change.path);
25
34
  const lines = await renderFile(store, name, change);
@@ -1 +1 @@
1
- {"version":3,"file":"runChanges.js","sourceRoot":"","sources":["../../../src/recovery/domain/runChanges.ts"],"names":[],"mappings":"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","sourcesContent":["import { unifiedDiff } from '#src/diff/domain/unifiedDiff';\nimport { type FileChange, fileChanges, sameState } from './fileChanges.ts';\nimport type { JournalEntry, RecoveryStore, RunRecord } from './recoveryStore.ts';\n\nconst binaryProbeBytes = 8000;\n\nexport interface ShowChangesOptions {\n\t/**\n\t * The most lines of diff shown. What does not fit is named instead, in at most two more lines.\n\t */\n\tmaxLines?: number;\n\t/**\n\t * Whether a file, by its real path, may be shown. A file it may not show is neither shown nor\n\t * named: only how many there were is said. Defaults to every file.\n\t */\n\tvisible?: (path: string) => Promise<boolean>;\n}\n\n/**\n * What a run changed, as a git-style diff per file, in the order the run first touched each one.\n * It shows each file from before the run's first change to after its last, so a file the run\n * created and then deleted does not appear. A change a live run has prepared but not made yet\n * is named, not shown. It reports the run's recorded effects, not a comparison of the whole\n * repository.\n */\nexport async function showChanges(\n\tstore: RecoveryStore,\n\trunId: string,\n\t{ maxLines = 1000, visible }: ShowChangesOptions = {},\n): Promise<string> {\n\tconst { record, entries } = await store.readRun(runId);\n\tconst all = changedFiles(record, entries);\n\tconst changes: FileChange[] = [];\n\tfor (const change of all) if (!visible || (await visible(change.path))) changes.push(change);\n\tconst output: string[] = [];\n\tconst hidden = all.length - changes.length;\n\tif (hidden > 0) {\n\t\toutput.push(\n\t\t\t`# ${String(hidden)} changed ${hidden === 1 ? 'file is' : 'files are'} not shown: you may not read ${hidden === 1 ? 'it' : 'them'}`,\n\t\t);\n\t}\n\n\tfor (const [index, change] of changes.entries()) {\n\t\tconst name = displayPath(record.root, change.path);\n\t\tconst lines = await renderFile(store, name, change);\n\t\tconst room = maxLines - output.length;\n\t\tif (lines.length <= room) {\n\t\t\toutput.push(...lines);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst rest = changes.slice(index + 1).map(next => displayPath(record.root, next.path));\n\t\tconst shown = Math.max(0, room);\n\t\toutput.push(...lines.slice(0, shown));\n\t\toutput.push(`... ${String(lines.length - shown)} more lines of the diff of ${name} not shown`);\n\t\tif (rest.length > 0) {\n\t\t\toutput.push(`... ${String(rest.length)} more changed files not shown: ${rest.join(', ')}`);\n\t\t}\n\t\tbreak;\n\t}\n\n\treturn output.join('\\n');\n}\n\n/**\n * The files a run changed, in the order it first touched each one. A file left as it was, such\n * as one the run created and then deleted, is not a change; one it may or may not have changed,\n * or is changing now, is.\n */\nexport function changedFiles(record: RunRecord, entries: JournalEntry[]): FileChange[] {\n\treturn fileChanges(entries, record.status !== 'running').filter(\n\t\tchange => change.uncertain || change.pending || !sameState(change.before, change.after),\n\t);\n}\n\nasync function renderFile(\n\tstore: RecoveryStore,\n\tname: string,\n\tchange: FileChange,\n): Promise<string[]> {\n\tconst { before, after } = change;\n\tconst lines: string[] = [];\n\tif (change.uncertain) {\n\t\tlines.push(`# ${name}: the run stopped while changing this file; it may not hold this change`);\n\t}\n\tif (change.observedBy.length > 0) {\n\t\tconst commands = change.observedBy.map(command => `\"${command}\"`).join(', ');\n\t\tlines.push(\n\t\t\t`# ${name}: changed while the command ${commands} ran, seen by comparing before and after`,\n\t\t);\n\t}\n\tif (change.pending) {\n\t\tlines.push(`# ${name}: the run is changing this file now; that change is not shown`);\n\t\tif (sameState(before, after)) return lines;\n\t}\n\tlines.push(`diff --git a/${name} b/${name}`);\n\tif (!before.exists && after.exists) lines.push(`new file mode ${gitMode(after.mode)}`);\n\tif (before.exists && !after.exists) lines.push(`deleted file mode ${gitMode(before.mode)}`);\n\tif (before.exists && after.exists && before.mode !== after.mode) {\n\t\tlines.push(`old mode ${gitMode(before.mode)}`, `new mode ${gitMode(after.mode)}`);\n\t}\n\tif (before.exists && after.exists && before.hash === after.hash) return lines;\n\n\tconst oldContent = before.exists ? await store.readContent(before.hash) : Buffer.alloc(0);\n\tconst newContent = after.exists ? await store.readContent(after.hash) : Buffer.alloc(0);\n\tconst oldName = before.exists ? `a/${name}` : '/dev/null';\n\tconst newName = after.exists ? `b/${name}` : '/dev/null';\n\n\tif (!isText(oldContent) || !isText(newContent)) {\n\t\tlines.push(`Binary files ${oldName} and ${newName} differ`);\n\t\treturn lines;\n\t}\n\tconst hunks = unifiedDiff(oldContent.toString('utf8'), newContent.toString('utf8'));\n\t// An empty file created or deleted has no hunks, as in git.\n\tif (hunks !== '') lines.push(`--- ${oldName}`, `+++ ${newName}`, ...hunks.split('\\n'));\n\treturn lines;\n}\n\n/**\n * Git's test, a NUL byte near the start, plus content that is not valid UTF-8: decoding would\n * replace its bytes, and two different files could then show no difference.\n */\nexport function isText(content: Buffer): boolean {\n\tif (content.subarray(0, binaryProbeBytes).includes(0)) return false;\n\treturn Buffer.from(content.toString('utf8'), 'utf8').equals(content);\n}\n\n/** A regular file's mode as git writes it, such as `100644`. */\nfunction gitMode(mode: number): string {\n\treturn (0o100000 | mode).toString(8);\n}\n\n/**\n * Relative to the run's root when inside it; a file in another root keeps its real path. Both\n * are real absolute paths, with `/` as the separator on every supported platform.\n */\nexport function displayPath(root: string, path: string): string {\n\treturn path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;\n}\n"]}
1
+ {"version":3,"file":"runChanges.js","sourceRoot":"","sources":["../../../src/recovery/domain/runChanges.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAmB,WAAW,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAG3E,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAc9B;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAChC,KAAoB,EACpB,KAAa,EACb,EAAE,QAAQ,GAAG,IAAI,EAAE,OAAO,KAAyB,EAAE;IAErD,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACnE,MAAM,GAAG,GAAG,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAiB,EAAE,CAAC;IACjC,KAAK,MAAM,MAAM,IAAI,GAAG;QAAE,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7F,MAAM,QAAQ,GAAsB,EAAE,CAAC;IACvC,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;QACjC,IAAI,CAAC,OAAO,IAAI,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;IACjF,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QAChB,MAAM,CAAC,IAAI,CACV,KAAK,MAAM,CAAC,MAAM,CAAC,YAAY,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,gCAAgC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CACnI,CAAC;IACH,CAAC;IAED,oFAAoF;IACpF,KAAK,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,QAAQ,EAAE,CAAC;QACrC,MAAM,CAAC,IAAI,CACV,KAAK,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,gCAAgC,EAAE,yHAAyH,CAC9L,CAAC;IACH,CAAC;IACD,KAAK,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QACjD,MAAM,IAAI,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;QACnD,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;QACtC,IAAI,KAAK,CAAC,MAAM,IAAI,IAAI,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;YACtB,SAAS;QACV,CAAC;QAED,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACvF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC,8BAA8B,IAAI,YAAY,CAAC,CAAC;QAC/F,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,MAAM,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,kCAAkC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC5F,CAAC;QACD,MAAM;IACP,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,MAAiB,EAAE,OAAuB;IACtE,OAAO,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,MAAM,CAC9D,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,OAAO,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CACvF,CAAC;AACH,CAAC;AAED,KAAK,UAAU,UAAU,CACxB,KAAoB,EACpB,IAAY,EACZ,MAAkB;IAElB,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;IACjC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,yEAAyE,CAAC,CAAC;IAChG,CAAC;IACD,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,OAAO,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7E,KAAK,CAAC,IAAI,CACT,KAAK,IAAI,+BAA+B,QAAQ,0CAA0C,CAC1F,CAAC;IACH,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,+DAA+D,CAAC,CAAC;QACrF,IAAI,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;IAC5C,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,gBAAgB,IAAI,MAAM,IAAI,EAAE,CAAC,CAAC;IAC7C,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,iBAAiB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvF,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,qBAAqB,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC5F,IAAI,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC;QACjE,KAAK,CAAC,IAAI,CAAC,YAAY,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,YAAY,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACnF,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IAE9E,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1F,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACxF,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;IAC1D,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;IAEzD,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,gBAAgB,OAAO,QAAQ,OAAO,SAAS,CAAC,CAAC;QAC5D,OAAO,KAAK,CAAC;IACd,CAAC;IACD,MAAM,KAAK,GAAG,WAAW,CAAC,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;IACpF,4DAA4D;IAC5D,IAAI,KAAK,KAAK,EAAE;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,OAAO,EAAE,EAAE,OAAO,OAAO,EAAE,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACvF,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,MAAM,CAAC,OAAe;IACrC,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IACpE,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AACtE,CAAC;AAED,gEAAgE;AAChE,SAAS,OAAO,CAAC,IAAY;IAC5B,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;AACtC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,IAAY;IACrD,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACzE,CAAC","sourcesContent":["import { unifiedDiff } from '#src/diff/domain/unifiedDiff';\nimport { type FileChange, fileChanges, sameState } from './fileChanges.ts';\nimport type { JournalEntry, RecoveryStore, RunRecord } from './recoveryStore.ts';\n\nconst binaryProbeBytes = 8000;\n\nexport interface ShowChangesOptions {\n\t/**\n\t * The most lines of diff shown. What does not fit is named instead, in at most two more lines.\n\t */\n\tmaxLines?: number;\n\t/**\n\t * Whether a file, by its real path, may be shown. A file it may not show is neither shown nor\n\t * named: only how many there were is said. Defaults to every file.\n\t */\n\tvisible?: (path: string) => Promise<boolean>;\n}\n\n/**\n * What a run changed, as a git-style diff per file, in the order the run first touched each one.\n * It shows each file from before the run's first change to after its last, so a file the run\n * created and then deleted does not appear. A change a live run has prepared but not made yet\n * is named, not shown. It reports the run's recorded effects, not a comparison of the whole\n * repository.\n */\nexport async function showChanges(\n\tstore: RecoveryStore,\n\trunId: string,\n\t{ maxLines = 1000, visible }: ShowChangesOptions = {},\n): Promise<string> {\n\tconst { record, entries, unrecorded } = await store.readRun(runId);\n\tconst all = changedFiles(record, entries);\n\tconst changes: FileChange[] = [];\n\tfor (const change of all) if (!visible || (await visible(change.path))) changes.push(change);\n\tconst uncopied: typeof unrecorded = [];\n\tfor (const change of unrecorded) {\n\t\tif (!visible || (await visible(change.path))) uncopied.push(change);\n\t}\n\tconst output: string[] = [];\n\tconst hidden = all.length + unrecorded.length - changes.length - uncopied.length;\n\tif (hidden > 0) {\n\t\toutput.push(\n\t\t\t`# ${String(hidden)} changed ${hidden === 1 ? 'file is' : 'files are'} not shown: you may not read ${hidden === 1 ? 'it' : 'them'}`,\n\t\t);\n\t}\n\n\t// Named first: no diff can show them, and they must not be cut away with the diffs.\n\tfor (const { path, by } of uncopied) {\n\t\toutput.push(\n\t\t\t`# ${displayPath(record.root, path)}: changed while the command \"${by}\" ran, into or from something the history cannot copy (a folder, a link, or a file over 64 MB); undo leaves it as it is`,\n\t\t);\n\t}\n\tfor (const [index, change] of changes.entries()) {\n\t\tconst name = displayPath(record.root, change.path);\n\t\tconst lines = await renderFile(store, name, change);\n\t\tconst room = maxLines - output.length;\n\t\tif (lines.length <= room) {\n\t\t\toutput.push(...lines);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst rest = changes.slice(index + 1).map(next => displayPath(record.root, next.path));\n\t\tconst shown = Math.max(0, room);\n\t\toutput.push(...lines.slice(0, shown));\n\t\toutput.push(`... ${String(lines.length - shown)} more lines of the diff of ${name} not shown`);\n\t\tif (rest.length > 0) {\n\t\t\toutput.push(`... ${String(rest.length)} more changed files not shown: ${rest.join(', ')}`);\n\t\t}\n\t\tbreak;\n\t}\n\n\treturn output.join('\\n');\n}\n\n/**\n * The files a run changed, in the order it first touched each one. A file left as it was, such\n * as one the run created and then deleted, is not a change; one it may or may not have changed,\n * or is changing now, is.\n */\nexport function changedFiles(record: RunRecord, entries: JournalEntry[]): FileChange[] {\n\treturn fileChanges(entries, record.status !== 'running').filter(\n\t\tchange => change.uncertain || change.pending || !sameState(change.before, change.after),\n\t);\n}\n\nasync function renderFile(\n\tstore: RecoveryStore,\n\tname: string,\n\tchange: FileChange,\n): Promise<string[]> {\n\tconst { before, after } = change;\n\tconst lines: string[] = [];\n\tif (change.uncertain) {\n\t\tlines.push(`# ${name}: the run stopped while changing this file; it may not hold this change`);\n\t}\n\tif (change.observedBy.length > 0) {\n\t\tconst commands = change.observedBy.map(command => `\"${command}\"`).join(', ');\n\t\tlines.push(\n\t\t\t`# ${name}: changed while the command ${commands} ran, seen by comparing before and after`,\n\t\t);\n\t}\n\tif (change.pending) {\n\t\tlines.push(`# ${name}: the run is changing this file now; that change is not shown`);\n\t\tif (sameState(before, after)) return lines;\n\t}\n\tlines.push(`diff --git a/${name} b/${name}`);\n\tif (!before.exists && after.exists) lines.push(`new file mode ${gitMode(after.mode)}`);\n\tif (before.exists && !after.exists) lines.push(`deleted file mode ${gitMode(before.mode)}`);\n\tif (before.exists && after.exists && before.mode !== after.mode) {\n\t\tlines.push(`old mode ${gitMode(before.mode)}`, `new mode ${gitMode(after.mode)}`);\n\t}\n\tif (before.exists && after.exists && before.hash === after.hash) return lines;\n\n\tconst oldContent = before.exists ? await store.readContent(before.hash) : Buffer.alloc(0);\n\tconst newContent = after.exists ? await store.readContent(after.hash) : Buffer.alloc(0);\n\tconst oldName = before.exists ? `a/${name}` : '/dev/null';\n\tconst newName = after.exists ? `b/${name}` : '/dev/null';\n\n\tif (!isText(oldContent) || !isText(newContent)) {\n\t\tlines.push(`Binary files ${oldName} and ${newName} differ`);\n\t\treturn lines;\n\t}\n\tconst hunks = unifiedDiff(oldContent.toString('utf8'), newContent.toString('utf8'));\n\t// An empty file created or deleted has no hunks, as in git.\n\tif (hunks !== '') lines.push(`--- ${oldName}`, `+++ ${newName}`, ...hunks.split('\\n'));\n\treturn lines;\n}\n\n/**\n * Git's test, a NUL byte near the start, plus content that is not valid UTF-8: decoding would\n * replace its bytes, and two different files could then show no difference.\n */\nexport function isText(content: Buffer): boolean {\n\tif (content.subarray(0, binaryProbeBytes).includes(0)) return false;\n\treturn Buffer.from(content.toString('utf8'), 'utf8').equals(content);\n}\n\n/** A regular file's mode as git writes it, such as `100644`. */\nfunction gitMode(mode: number): string {\n\treturn (0o100000 | mode).toString(8);\n}\n\n/**\n * Relative to the run's root when inside it; a file in another root keeps its real path. Both\n * are real absolute paths, with `/` as the separator on every supported platform.\n */\nexport function displayPath(root: string, path: string): string {\n\treturn path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;\n}\n"]}
@@ -1,4 +1,5 @@
1
- import { type JournalEntry, type MoveOptions, type Revision, type RecoveryStore, type RunJournal, type RunRecord } from '../domain/recoveryStore.ts';
1
+ import type { ILogger } from '#src/shared/domain/logger';
2
+ import { type JournalEntry, type MoveOptions, type Revision, type RecoveryStore, type RunJournal, type RunRecord, type UnrecordedChange } from '../domain/recoveryStore.ts';
2
3
  /** Where an application keeps state that is neither configuration nor cache, per platform. */
3
4
  export declare function defaultStateDirectory(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, home?: string): string;
4
5
  /**
@@ -21,11 +22,13 @@ export declare function defaultStateDirectory(env?: NodeJS.ProcessEnv, platform?
21
22
  * same instant could both believe they hold it.
22
23
  */
23
24
  export declare class FileRecoveryStore implements RecoveryStore {
25
+ readonly workspaceId: string;
24
26
  private readonly root;
25
27
  private readonly directory;
26
28
  private readonly keepRuns;
29
+ private readonly logger;
27
30
  private constructor();
28
- static open({ root, directory, keepRuns, }: {
31
+ static open({ root, directory, keepRuns, logger, }: {
29
32
  root: string;
30
33
  /** Defaults to the platform's state directory. */
31
34
  directory?: string;
@@ -35,6 +38,8 @@ export declare class FileRecoveryStore implements RecoveryStore {
35
38
  * no next run to be chained into, so pruning can leave two runs besides the new one.
36
39
  */
37
40
  keepRuns?: number;
41
+ /** Where it reports a failure it absorbs. Defaults to standard error. */
42
+ logger?: ILogger;
38
43
  }): Promise<FileRecoveryStore>;
39
44
  startRun(): Promise<RunJournal>;
40
45
  listRuns(): Promise<{
@@ -44,6 +49,7 @@ export declare class FileRecoveryStore implements RecoveryStore {
44
49
  readRun(runId: string): Promise<{
45
50
  record: RunRecord;
46
51
  entries: JournalEntry[];
52
+ unrecorded: UnrecordedChange[];
47
53
  }>;
48
54
  private readRecord;
49
55
  private runDirectory;
@@ -102,24 +108,50 @@ export declare class FileRecoveryStore implements RecoveryStore {
102
108
  * saved are freed by the next prune.
103
109
  */
104
110
  private discardRun;
105
- /** Removes the stored contents no kept run refers to. */
111
+ /**
112
+ * Removes the stored contents no kept run refers to. An observation still open refers to its
113
+ * manifest and to every content in it, which settling it needs.
114
+ */
106
115
  private collectContent;
107
116
  private appendRevision;
108
117
  /**
109
- * Marks every other run still recorded as `running` as `interrupted`: this run holds the lock,
110
- * so their processes are gone. Each change one left `prepared` is settled from what its file
111
- * holds now: its `after` means it was made, its `before` that it was not. A file that holds
112
- * neither stays `prepared`, because nothing tells whether the change reached it.
118
+ * Settles every other run left unfinished: one still recorded as `running` is `interrupted`,
119
+ * since this run holds the lock and its process is gone, and one that finished `unsettled`
120
+ * keeps its status.
113
121
  */
114
122
  private settleInterrupted;
123
+ /**
124
+ * Settles a run that finished with an observation open, under its own lock, before it releases
125
+ * the workspace. If that fails, its record stays `unsettled`, and the next run or move settles
126
+ * it, so the failure is reported and not thrown.
127
+ */
128
+ private settleAtFinish;
129
+ /**
130
+ * Settles what a run left unfinished. Each change it left `prepared` is settled from what its
131
+ * file holds now: its `after` means it was made, its `before` that it was not. A file that holds
132
+ * neither stays `prepared`, because nothing tells whether the change reached it. Each
133
+ * observation it left open is settled from its manifest. A run still `running` becomes
134
+ * `interrupted`; any other keeps its status. Whether the run is still in the history: one that
135
+ * changed nothing leaves it if it was still `running`, or if it is `finishing`, which unties
136
+ * its turns from it. One that finished is kept otherwise, since the turns of a conversation may
137
+ * name it.
138
+ */
139
+ private settleRun;
115
140
  /**
116
141
  * The lines that record what a command an interrupted run was observing changed: every file of
117
142
  * its manifest that holds something else now, as changed by that command, and then the end of
118
143
  * the observation. A file recorded after the observation began is left alone: the run
119
- * recorded it itself. One that is now a folder, a link or too large to record is left out.
144
+ * recorded it itself. One that is now a folder, a link or too large to record, or that was one
145
+ * and is no longer, or that cannot be read, is recorded as a change the history could not copy.
120
146
  */
121
147
  private settleObservation;
122
- /** What `path` holds now, with its content stored, or `undefined` for what no entry records. */
148
+ private warnUnreadable;
149
+ /**
150
+ * What `path` holds now, with its content stored, or `undefined` for what no entry records.
151
+ * A file it cannot read is `unreadable`, and reported: it cannot be undone either, and settling
152
+ * never waits on it, or every later run would fail to start. A failure to store its content is
153
+ * thrown, so the observation stays open and settling it is tried again.
154
+ */
123
155
  private saveState;
124
156
  private readEntries;
125
157
  /**