@mikode13/harness 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +285 -20
  2. package/dist/agent/domain/agent.d.ts +12 -0
  3. package/dist/agent/domain/agent.js.map +1 -1
  4. package/dist/agent/domain/approval.d.ts +14 -0
  5. package/dist/agent/domain/approval.js +28 -3
  6. package/dist/agent/domain/approval.js.map +1 -1
  7. package/dist/agent/domain/runContext.d.ts +31 -0
  8. package/dist/agent/domain/runContext.js +47 -0
  9. package/dist/agent/domain/runContext.js.map +1 -0
  10. package/dist/diff/domain/unifiedDiff.d.ts +19 -0
  11. package/dist/diff/domain/unifiedDiff.js +166 -0
  12. package/dist/diff/domain/unifiedDiff.js.map +1 -0
  13. package/dist/engines/domain/model/historyFollower.d.ts +41 -0
  14. package/dist/engines/domain/model/historyFollower.js +158 -0
  15. package/dist/engines/domain/model/historyFollower.js.map +1 -0
  16. package/dist/engines/domain/model/llmAgent.d.ts +42 -4
  17. package/dist/engines/domain/model/llmAgent.js +224 -36
  18. package/dist/engines/domain/model/llmAgent.js.map +1 -1
  19. package/dist/factory/infrastructure/agentFactory.d.ts +14 -0
  20. package/dist/factory/infrastructure/agentFactory.js +20 -2
  21. package/dist/factory/infrastructure/agentFactory.js.map +1 -1
  22. package/dist/factory/infrastructure/agentLLMFactory.d.ts +119 -14
  23. package/dist/factory/infrastructure/agentLLMFactory.js +284 -57
  24. package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
  25. package/dist/index.d.ts +11 -3
  26. package/dist/index.js +5 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/llm/domain/conversation.d.ts +44 -4
  29. package/dist/llm/domain/conversation.js +56 -7
  30. package/dist/llm/domain/conversation.js.map +1 -1
  31. package/dist/llm/domain/llm.d.ts +3 -1
  32. package/dist/llm/domain/llm.js.map +1 -1
  33. package/dist/llm/domain/tool.d.ts +13 -0
  34. package/dist/llm/domain/tool.js +12 -1
  35. package/dist/llm/domain/tool.js.map +1 -1
  36. package/dist/llm/infrastructure/claudeLLMClient.d.ts +3 -2
  37. package/dist/llm/infrastructure/claudeLLMClient.js +38 -10
  38. package/dist/llm/infrastructure/claudeLLMClient.js.map +1 -1
  39. package/dist/llm/infrastructure/openAILLMClient.d.ts +2 -1
  40. package/dist/llm/infrastructure/openAILLMClient.js +66 -16
  41. package/dist/llm/infrastructure/openAILLMClient.js.map +1 -1
  42. package/dist/orchestration/domain/model/orchestratorAgent.d.ts +11 -2
  43. package/dist/orchestration/domain/model/orchestratorAgent.js +43 -4
  44. package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
  45. package/dist/recovery/domain/fileChanges.d.ts +31 -0
  46. package/dist/recovery/domain/fileChanges.js +48 -0
  47. package/dist/recovery/domain/fileChanges.js.map +1 -0
  48. package/dist/recovery/domain/history.d.ts +114 -0
  49. package/dist/recovery/domain/history.js +62 -0
  50. package/dist/recovery/domain/history.js.map +1 -0
  51. package/dist/recovery/domain/recoveryStore.d.ts +233 -0
  52. package/dist/recovery/domain/recoveryStore.js +32 -0
  53. package/dist/recovery/domain/recoveryStore.js.map +1 -0
  54. package/dist/recovery/domain/retention.d.ts +33 -0
  55. package/dist/recovery/domain/retention.js +90 -0
  56. package/dist/recovery/domain/retention.js.map +1 -0
  57. package/dist/recovery/domain/runChanges.d.ts +37 -0
  58. package/dist/recovery/domain/runChanges.js +109 -0
  59. package/dist/recovery/domain/runChanges.js.map +1 -0
  60. package/dist/recovery/domain/runTree.d.ts +32 -0
  61. package/dist/recovery/domain/runTree.js +90 -0
  62. package/dist/recovery/domain/runTree.js.map +1 -0
  63. package/dist/recovery/infrastructure/createHistory.d.ts +11 -0
  64. package/dist/recovery/infrastructure/createHistory.js +17 -0
  65. package/dist/recovery/infrastructure/createHistory.js.map +1 -0
  66. package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +133 -0
  67. package/dist/recovery/infrastructure/fileRecoveryStore.js +1043 -0
  68. package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -0
  69. package/dist/recovery/infrastructure/workspaceFiles.d.ts +30 -0
  70. package/dist/recovery/infrastructure/workspaceFiles.js +125 -0
  71. package/dist/recovery/infrastructure/workspaceFiles.js.map +1 -0
  72. package/dist/tools/domain/accessPolicy.d.ts +30 -0
  73. package/dist/tools/domain/accessPolicy.js +11 -0
  74. package/dist/tools/domain/accessPolicy.js.map +1 -0
  75. package/dist/tools/domain/commands.d.ts +93 -0
  76. package/dist/tools/domain/commands.js +191 -0
  77. package/dist/tools/domain/commands.js.map +1 -0
  78. package/dist/tools/domain/editResponse.d.ts +14 -0
  79. package/dist/tools/domain/editResponse.js +79 -0
  80. package/dist/tools/domain/editResponse.js.map +1 -0
  81. package/dist/tools/domain/fileEdits.d.ts +64 -0
  82. package/dist/tools/domain/fileEdits.js +21 -0
  83. package/dist/tools/domain/fileEdits.js.map +1 -0
  84. package/dist/tools/domain/lineLength.d.ts +7 -0
  85. package/dist/tools/domain/lineLength.js +26 -0
  86. package/dist/tools/domain/lineLength.js.map +1 -0
  87. package/dist/tools/domain/preparedCall.d.ts +31 -0
  88. package/dist/tools/domain/preparedCall.js +14 -0
  89. package/dist/tools/domain/preparedCall.js.map +1 -0
  90. package/dist/tools/domain/readRegistry.d.ts +46 -0
  91. package/dist/tools/domain/readRegistry.js +62 -0
  92. package/dist/tools/domain/readRegistry.js.map +1 -0
  93. package/dist/tools/domain/textLines.d.ts +26 -0
  94. package/dist/tools/domain/textLines.js +44 -0
  95. package/dist/tools/domain/textLines.js.map +1 -0
  96. package/dist/tools/domain/v4aPatch.d.ts +17 -0
  97. package/dist/tools/domain/v4aPatch.js +236 -0
  98. package/dist/tools/domain/v4aPatch.js.map +1 -0
  99. package/dist/tools/infrastructure/boundedWorkspace.d.ts +11 -2
  100. package/dist/tools/infrastructure/boundedWorkspace.js +9 -1
  101. package/dist/tools/infrastructure/boundedWorkspace.js.map +1 -1
  102. package/dist/tools/infrastructure/commandCheckpoint.d.ts +55 -0
  103. package/dist/tools/infrastructure/commandCheckpoint.js +178 -0
  104. package/dist/tools/infrastructure/commandCheckpoint.js.map +1 -0
  105. package/dist/tools/infrastructure/commandProcess.d.ts +47 -0
  106. package/dist/tools/infrastructure/commandProcess.js +217 -0
  107. package/dist/tools/infrastructure/commandProcess.js.map +1 -0
  108. package/dist/tools/infrastructure/commandTool.d.ts +36 -0
  109. package/dist/tools/infrastructure/commandTool.js +254 -0
  110. package/dist/tools/infrastructure/commandTool.js.map +1 -0
  111. package/dist/tools/infrastructure/createWorkspace.d.ts +10 -0
  112. package/dist/tools/infrastructure/createWorkspace.js +11 -3
  113. package/dist/tools/infrastructure/createWorkspace.js.map +1 -1
  114. package/dist/tools/infrastructure/defineTool.d.ts +14 -0
  115. package/dist/tools/infrastructure/defineTool.js +28 -5
  116. package/dist/tools/infrastructure/defineTool.js.map +1 -1
  117. package/dist/tools/infrastructure/editFormats.d.ts +22 -0
  118. package/dist/tools/infrastructure/editFormats.js +211 -0
  119. package/dist/tools/infrastructure/editFormats.js.map +1 -0
  120. package/dist/tools/infrastructure/fileEditor.d.ts +64 -0
  121. package/dist/tools/infrastructure/fileEditor.js +306 -0
  122. package/dist/tools/infrastructure/fileEditor.js.map +1 -0
  123. package/dist/tools/infrastructure/fileTools.d.ts +81 -0
  124. package/dist/tools/infrastructure/fileTools.js +152 -0
  125. package/dist/tools/infrastructure/fileTools.js.map +1 -0
  126. package/dist/tools/infrastructure/gitIgnoreRules.d.ts +15 -0
  127. package/dist/tools/infrastructure/gitIgnoreRules.js +74 -0
  128. package/dist/tools/infrastructure/gitIgnoreRules.js.map +1 -0
  129. package/dist/tools/infrastructure/gitWorkspace.d.ts +1 -1
  130. package/dist/tools/infrastructure/gitWorkspace.js +1 -1
  131. package/dist/tools/infrastructure/gitWorkspace.js.map +1 -1
  132. package/dist/tools/infrastructure/ignoreFileChanges.d.ts +14 -0
  133. package/dist/tools/infrastructure/ignoreFileChanges.js +62 -0
  134. package/dist/tools/infrastructure/ignoreFileChanges.js.map +1 -0
  135. package/dist/tools/infrastructure/policyWorkspace.d.ts +52 -0
  136. package/dist/tools/infrastructure/policyWorkspace.js +32 -0
  137. package/dist/tools/infrastructure/policyWorkspace.js.map +1 -0
  138. package/dist/tools/infrastructure/ripgrepWorkspace.d.ts +2 -1
  139. package/dist/tools/infrastructure/ripgrepWorkspace.js +1 -1
  140. package/dist/tools/infrastructure/ripgrepWorkspace.js.map +1 -1
  141. package/dist/tools/infrastructure/rootsAccessPolicy.d.ts +100 -0
  142. package/dist/tools/infrastructure/rootsAccessPolicy.js +301 -0
  143. package/dist/tools/infrastructure/rootsAccessPolicy.js.map +1 -0
  144. package/dist/tools/infrastructure/runProcess.d.ts +4 -1
  145. package/dist/tools/infrastructure/runProcess.js +4 -2
  146. package/dist/tools/infrastructure/runProcess.js.map +1 -1
  147. package/dist/tools/infrastructure/secretPaths.d.ts +8 -0
  148. package/dist/tools/infrastructure/secretPaths.js +76 -0
  149. package/dist/tools/infrastructure/secretPaths.js.map +1 -0
  150. package/dist/tools/infrastructure/showChangesTool.d.ts +12 -0
  151. package/dist/tools/infrastructure/showChangesTool.js +43 -0
  152. package/dist/tools/infrastructure/showChangesTool.js.map +1 -0
  153. package/dist/tools/infrastructure/trackedEdits.d.ts +33 -0
  154. package/dist/tools/infrastructure/trackedEdits.js +70 -0
  155. package/dist/tools/infrastructure/trackedEdits.js.map +1 -0
  156. package/dist/tools/infrastructure/trackedReadFile.d.ts +21 -0
  157. package/dist/tools/infrastructure/trackedReadFile.js +78 -0
  158. package/dist/tools/infrastructure/trackedReadFile.js.map +1 -0
  159. package/dist/tools/infrastructure/workspaceTools.d.ts +28 -0
  160. package/dist/tools/infrastructure/workspaceTools.js +53 -40
  161. package/dist/tools/infrastructure/workspaceTools.js.map +1 -1
  162. package/dist/tools/infrastructure/workspaceWrites.d.ts +23 -0
  163. package/dist/tools/infrastructure/workspaceWrites.js +42 -0
  164. package/dist/tools/infrastructure/workspaceWrites.js.map +1 -0
  165. package/dist/tools/infrastructure/writeSession.d.ts +35 -0
  166. package/dist/tools/infrastructure/writeSession.js +81 -0
  167. package/dist/tools/infrastructure/writeSession.js.map +1 -0
  168. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recoveryStore.js","sourceRoot":"","sources":["../../../src/recovery/domain/recoveryStore.ts"],"names":[],"mappings":"AAwHA,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/**\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"]}
@@ -0,0 +1,33 @@
1
+ import type { JournalEntry, Revision, RunRecord } from './recoveryStore.ts';
2
+ /** One step towards keeping at most a given number of runs. */
3
+ export type PruneStep =
4
+ /** An abandoned run nothing depends on: it goes whole. */
5
+ {
6
+ kind: 'remove';
7
+ runId: string;
8
+ }
9
+ /** A run on the current line: its changes are chained into `into`, its child on that line. */
10
+ | {
11
+ kind: 'chain';
12
+ runId: string;
13
+ into: string;
14
+ };
15
+ /**
16
+ * The line the user works along: from the first run after the start to the run the workspace is
17
+ * at, then on through the runs `redo` would take, so what can be redone is kept too.
18
+ */
19
+ export declare function currentLine(runs: RunRecord[], revisions: Revision[], head: string | undefined): string[];
20
+ /**
21
+ * The next step that brings the history down to `keep` runs, or `undefined` once it is there or
22
+ * nothing more can go. Abandoned branches go first, a leaf at a time, oldest first. Then the
23
+ * oldest run on the current line other than the one the workspace is at is chained into the
24
+ * next one, so the start of the history, before any run, always stays reachable.
25
+ */
26
+ export declare function nextPruneStep(runs: RunRecord[], revisions: Revision[], head: string | undefined, keep: number): PruneStep | undefined;
27
+ /**
28
+ * The changes of `older` followed by those of `newer`, as one run's: each file's chain reduced to
29
+ * its net, from before its first change to after its last, so the contents in between are no
30
+ * longer referenced. A break in a chain, where someone wrote to the file between two changes,
31
+ * is kept as a second change, so undoing still leaves that work alone.
32
+ */
33
+ export declare function chainEntries(older: JournalEntry[], newer: JournalEntry[]): JournalEntry[];
@@ -0,0 +1,90 @@
1
+ import { sameState } from "./fileChanges.js";
2
+ import { redoTarget } from "./runTree.js";
3
+ /**
4
+ * The line the user works along: from the first run after the start to the run the workspace is
5
+ * at, then on through the runs `redo` would take, so what can be redone is kept too.
6
+ */
7
+ export function currentLine(runs, revisions, head) {
8
+ const byId = new Map(runs.map(run => [run.runId, run]));
9
+ const line = [];
10
+ for (let runId = head; runId !== undefined; runId = byId.get(runId)?.parentRunId) {
11
+ line.unshift(runId);
12
+ }
13
+ for (let next = redoTarget(runs, revisions, head); next !== undefined;) {
14
+ line.push(next);
15
+ next = redoTarget(runs, revisions, next);
16
+ }
17
+ return line;
18
+ }
19
+ /**
20
+ * The next step that brings the history down to `keep` runs, or `undefined` once it is there or
21
+ * nothing more can go. Abandoned branches go first, a leaf at a time, oldest first. Then the
22
+ * oldest run on the current line other than the one the workspace is at is chained into the
23
+ * next one, so the start of the history, before any run, always stays reachable.
24
+ */
25
+ export function nextPruneStep(runs, revisions, head, keep) {
26
+ if (runs.length <= keep)
27
+ return undefined;
28
+ const line = currentLine(runs, revisions, head);
29
+ const parents = new Set(runs.map(run => run.parentRunId));
30
+ const leaf = runs
31
+ .map(run => run.runId)
32
+ .filter(runId => !line.includes(runId) && !parents.has(runId))
33
+ .sort()[0];
34
+ if (leaf)
35
+ return { kind: 'remove', runId: leaf };
36
+ for (let index = 0; index + 1 < line.length; index++) {
37
+ const runId = line[index];
38
+ const into = line[index + 1];
39
+ if (runId !== undefined && into !== undefined && runId !== head) {
40
+ return { kind: 'chain', runId, into };
41
+ }
42
+ }
43
+ return undefined;
44
+ }
45
+ /**
46
+ * The changes of `older` followed by those of `newer`, as one run's: each file's chain reduced to
47
+ * its net, from before its first change to after its last, so the contents in between are no
48
+ * longer referenced. A break in a chain, where someone wrote to the file between two changes,
49
+ * is kept as a second change, so undoing still leaves that work alone.
50
+ */
51
+ export function chainEntries(older, newer) {
52
+ const byPath = new Map();
53
+ for (const entry of [...older, ...newer]) {
54
+ if (entry.status === 'abandoned')
55
+ continue;
56
+ const segments = byPath.get(entry.path) ?? [];
57
+ const last = segments.at(-1);
58
+ const previous = last?.at(-1);
59
+ if (last && previous && sameState(previous.after, entry.before))
60
+ last.push(entry);
61
+ else
62
+ segments.push([entry]);
63
+ byPath.set(entry.path, segments);
64
+ }
65
+ const chained = [];
66
+ for (const [path, segments] of byPath) {
67
+ for (const segment of segments) {
68
+ const first = segment[0];
69
+ const last = segment.at(-1);
70
+ if (!first || !last)
71
+ continue;
72
+ const createdFolders = [...new Set(segment.flatMap(entry => entry.createdFolders ?? []))];
73
+ // Seen around commands only when no tool of the harness made any part of it.
74
+ const observers = new Set(segment.map(entry => entry.observedBy));
75
+ const observedBy = observers.has(undefined) ? undefined : [...observers].join(', ');
76
+ chained.push({
77
+ sequence: chained.length + 1,
78
+ path,
79
+ before: first.before,
80
+ after: last.after,
81
+ ...(createdFolders.length > 0 ? { createdFolders } : {}),
82
+ ...(observedBy === undefined ? {} : { observedBy }),
83
+ // One change nobody could settle leaves the whole chain unsettled.
84
+ status: segment.some(entry => entry.status === 'prepared') ? 'prepared' : 'applied',
85
+ });
86
+ }
87
+ }
88
+ return chained;
89
+ }
90
+ //# sourceMappingURL=retention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retention.js","sourceRoot":"","sources":["../../../src/recovery/domain/retention.ts"],"names":[],"mappings":"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","sourcesContent":["import { sameState } from './fileChanges.ts';\nimport type { JournalEntry, Revision, RunRecord } from './recoveryStore.ts';\nimport { redoTarget } from './runTree.ts';\n\n/** One step towards keeping at most a given number of runs. */\nexport type PruneStep =\n\t/** An abandoned run nothing depends on: it goes whole. */\n\t| { kind: 'remove'; runId: string }\n\t/** A run on the current line: its changes are chained into `into`, its child on that line. */\n\t| { kind: 'chain'; runId: string; into: string };\n\n/**\n * The line the user works along: from the first run after the start to the run the workspace is\n * at, then on through the runs `redo` would take, so what can be redone is kept too.\n */\nexport function currentLine(\n\truns: RunRecord[],\n\trevisions: Revision[],\n\thead: string | undefined,\n): string[] {\n\tconst byId = new Map(runs.map(run => [run.runId, run]));\n\tconst line: string[] = [];\n\tfor (let runId = head; runId !== undefined; runId = byId.get(runId)?.parentRunId) {\n\t\tline.unshift(runId);\n\t}\n\tfor (let next = redoTarget(runs, revisions, head); next !== undefined;) {\n\t\tline.push(next);\n\t\tnext = redoTarget(runs, revisions, next);\n\t}\n\treturn line;\n}\n\n/**\n * The next step that brings the history down to `keep` runs, or `undefined` once it is there or\n * nothing more can go. Abandoned branches go first, a leaf at a time, oldest first. Then the\n * oldest run on the current line other than the one the workspace is at is chained into the\n * next one, so the start of the history, before any run, always stays reachable.\n */\nexport function nextPruneStep(\n\truns: RunRecord[],\n\trevisions: Revision[],\n\thead: string | undefined,\n\tkeep: number,\n): PruneStep | undefined {\n\tif (runs.length <= keep) return undefined;\n\tconst line = currentLine(runs, revisions, head);\n\n\tconst parents = new Set(runs.map(run => run.parentRunId));\n\tconst leaf = runs\n\t\t.map(run => run.runId)\n\t\t.filter(runId => !line.includes(runId) && !parents.has(runId))\n\t\t.sort()[0];\n\tif (leaf) return { kind: 'remove', runId: leaf };\n\n\tfor (let index = 0; index + 1 < line.length; index++) {\n\t\tconst runId = line[index];\n\t\tconst into = line[index + 1];\n\t\tif (runId !== undefined && into !== undefined && runId !== head) {\n\t\t\treturn { kind: 'chain', runId, into };\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The changes of `older` followed by those of `newer`, as one run's: each file's chain reduced to\n * its net, from before its first change to after its last, so the contents in between are no\n * longer referenced. A break in a chain, where someone wrote to the file between two changes,\n * is kept as a second change, so undoing still leaves that work alone.\n */\nexport function chainEntries(older: JournalEntry[], newer: JournalEntry[]): JournalEntry[] {\n\tconst byPath = new Map<string, JournalEntry[][]>();\n\tfor (const entry of [...older, ...newer]) {\n\t\tif (entry.status === 'abandoned') continue;\n\t\tconst segments = byPath.get(entry.path) ?? [];\n\t\tconst last = segments.at(-1);\n\t\tconst previous = last?.at(-1);\n\t\tif (last && previous && sameState(previous.after, entry.before)) last.push(entry);\n\t\telse segments.push([entry]);\n\t\tbyPath.set(entry.path, segments);\n\t}\n\n\tconst chained: JournalEntry[] = [];\n\tfor (const [path, segments] of byPath) {\n\t\tfor (const segment of segments) {\n\t\t\tconst first = segment[0];\n\t\t\tconst last = segment.at(-1);\n\t\t\tif (!first || !last) continue;\n\t\t\tconst createdFolders = [...new Set(segment.flatMap(entry => entry.createdFolders ?? []))];\n\t\t\t// Seen around commands only when no tool of the harness made any part of it.\n\t\t\tconst observers = new Set(segment.map(entry => entry.observedBy));\n\t\t\tconst observedBy = observers.has(undefined) ? undefined : [...observers].join(', ');\n\t\t\tchained.push({\n\t\t\t\tsequence: chained.length + 1,\n\t\t\t\tpath,\n\t\t\t\tbefore: first.before,\n\t\t\t\tafter: last.after,\n\t\t\t\t...(createdFolders.length > 0 ? { createdFolders } : {}),\n\t\t\t\t...(observedBy === undefined ? {} : { observedBy }),\n\t\t\t\t// One change nobody could settle leaves the whole chain unsettled.\n\t\t\t\tstatus: segment.some(entry => entry.status === 'prepared') ? 'prepared' : 'applied',\n\t\t\t});\n\t\t}\n\t}\n\treturn chained;\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import { type FileChange } from './fileChanges.ts';
2
+ import type { JournalEntry, RecoveryStore, RunRecord } from './recoveryStore.ts';
3
+ export interface ShowChangesOptions {
4
+ /**
5
+ * The most lines of diff shown. What does not fit is named instead, in at most two more lines.
6
+ */
7
+ maxLines?: number;
8
+ /**
9
+ * Whether a file, by its real path, may be shown. A file it may not show is neither shown nor
10
+ * named: only how many there were is said. Defaults to every file.
11
+ */
12
+ visible?: (path: string) => Promise<boolean>;
13
+ }
14
+ /**
15
+ * What a run changed, as a git-style diff per file, in the order the run first touched each one.
16
+ * It shows each file from before the run's first change to after its last, so a file the run
17
+ * created and then deleted does not appear. A change a live run has prepared but not made yet
18
+ * is named, not shown. It reports the run's recorded effects, not a comparison of the whole
19
+ * repository.
20
+ */
21
+ export declare function showChanges(store: RecoveryStore, runId: string, { maxLines, visible }?: ShowChangesOptions): Promise<string>;
22
+ /**
23
+ * The files a run changed, in the order it first touched each one. A file left as it was, such
24
+ * as one the run created and then deleted, is not a change; one it may or may not have changed,
25
+ * or is changing now, is.
26
+ */
27
+ export declare function changedFiles(record: RunRecord, entries: JournalEntry[]): FileChange[];
28
+ /**
29
+ * Git's test, a NUL byte near the start, plus content that is not valid UTF-8: decoding would
30
+ * replace its bytes, and two different files could then show no difference.
31
+ */
32
+ export declare function isText(content: Buffer): boolean;
33
+ /**
34
+ * Relative to the run's root when inside it; a file in another root keeps its real path. Both
35
+ * are real absolute paths, with `/` as the separator on every supported platform.
36
+ */
37
+ export declare function displayPath(root: string, path: string): string;
@@ -0,0 +1,109 @@
1
+ import { unifiedDiff } from '#src/diff/domain/unifiedDiff';
2
+ import { fileChanges, sameState } from "./fileChanges.js";
3
+ const binaryProbeBytes = 8000;
4
+ /**
5
+ * What a run changed, as a git-style diff per file, in the order the run first touched each one.
6
+ * It shows each file from before the run's first change to after its last, so a file the run
7
+ * created and then deleted does not appear. A change a live run has prepared but not made yet
8
+ * is named, not shown. It reports the run's recorded effects, not a comparison of the whole
9
+ * repository.
10
+ */
11
+ export async function showChanges(store, runId, { maxLines = 1000, visible } = {}) {
12
+ const { record, entries } = await store.readRun(runId);
13
+ const all = changedFiles(record, entries);
14
+ const changes = [];
15
+ for (const change of all)
16
+ if (!visible || (await visible(change.path)))
17
+ changes.push(change);
18
+ const output = [];
19
+ const hidden = all.length - changes.length;
20
+ if (hidden > 0) {
21
+ output.push(`# ${String(hidden)} changed ${hidden === 1 ? 'file is' : 'files are'} not shown: you may not read ${hidden === 1 ? 'it' : 'them'}`);
22
+ }
23
+ for (const [index, change] of changes.entries()) {
24
+ const name = displayPath(record.root, change.path);
25
+ const lines = await renderFile(store, name, change);
26
+ const room = maxLines - output.length;
27
+ if (lines.length <= room) {
28
+ output.push(...lines);
29
+ continue;
30
+ }
31
+ const rest = changes.slice(index + 1).map(next => displayPath(record.root, next.path));
32
+ const shown = Math.max(0, room);
33
+ output.push(...lines.slice(0, shown));
34
+ output.push(`... ${String(lines.length - shown)} more lines of the diff of ${name} not shown`);
35
+ if (rest.length > 0) {
36
+ output.push(`... ${String(rest.length)} more changed files not shown: ${rest.join(', ')}`);
37
+ }
38
+ break;
39
+ }
40
+ return output.join('\n');
41
+ }
42
+ /**
43
+ * The files a run changed, in the order it first touched each one. A file left as it was, such
44
+ * as one the run created and then deleted, is not a change; one it may or may not have changed,
45
+ * or is changing now, is.
46
+ */
47
+ export function changedFiles(record, entries) {
48
+ return fileChanges(entries, record.status !== 'running').filter(change => change.uncertain || change.pending || !sameState(change.before, change.after));
49
+ }
50
+ async function renderFile(store, name, change) {
51
+ const { before, after } = change;
52
+ const lines = [];
53
+ if (change.uncertain) {
54
+ lines.push(`# ${name}: the run stopped while changing this file; it may not hold this change`);
55
+ }
56
+ if (change.observedBy.length > 0) {
57
+ const commands = change.observedBy.map(command => `"${command}"`).join(', ');
58
+ lines.push(`# ${name}: changed while the command ${commands} ran, seen by comparing before and after`);
59
+ }
60
+ if (change.pending) {
61
+ lines.push(`# ${name}: the run is changing this file now; that change is not shown`);
62
+ if (sameState(before, after))
63
+ return lines;
64
+ }
65
+ lines.push(`diff --git a/${name} b/${name}`);
66
+ if (!before.exists && after.exists)
67
+ lines.push(`new file mode ${gitMode(after.mode)}`);
68
+ if (before.exists && !after.exists)
69
+ lines.push(`deleted file mode ${gitMode(before.mode)}`);
70
+ if (before.exists && after.exists && before.mode !== after.mode) {
71
+ lines.push(`old mode ${gitMode(before.mode)}`, `new mode ${gitMode(after.mode)}`);
72
+ }
73
+ if (before.exists && after.exists && before.hash === after.hash)
74
+ return lines;
75
+ const oldContent = before.exists ? await store.readContent(before.hash) : Buffer.alloc(0);
76
+ const newContent = after.exists ? await store.readContent(after.hash) : Buffer.alloc(0);
77
+ const oldName = before.exists ? `a/${name}` : '/dev/null';
78
+ const newName = after.exists ? `b/${name}` : '/dev/null';
79
+ if (!isText(oldContent) || !isText(newContent)) {
80
+ lines.push(`Binary files ${oldName} and ${newName} differ`);
81
+ return lines;
82
+ }
83
+ const hunks = unifiedDiff(oldContent.toString('utf8'), newContent.toString('utf8'));
84
+ // An empty file created or deleted has no hunks, as in git.
85
+ if (hunks !== '')
86
+ lines.push(`--- ${oldName}`, `+++ ${newName}`, ...hunks.split('\n'));
87
+ return lines;
88
+ }
89
+ /**
90
+ * Git's test, a NUL byte near the start, plus content that is not valid UTF-8: decoding would
91
+ * replace its bytes, and two different files could then show no difference.
92
+ */
93
+ export function isText(content) {
94
+ if (content.subarray(0, binaryProbeBytes).includes(0))
95
+ return false;
96
+ return Buffer.from(content.toString('utf8'), 'utf8').equals(content);
97
+ }
98
+ /** A regular file's mode as git writes it, such as `100644`. */
99
+ function gitMode(mode) {
100
+ return (0o100000 | mode).toString(8);
101
+ }
102
+ /**
103
+ * Relative to the run's root when inside it; a file in another root keeps its real path. Both
104
+ * are real absolute paths, with `/` as the separator on every supported platform.
105
+ */
106
+ export function displayPath(root, path) {
107
+ return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;
108
+ }
109
+ //# sourceMappingURL=runChanges.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,32 @@
1
+ import type { Revision, RunRecord } from './recoveryStore.ts';
2
+ /** The runs to undo, newest first, then the runs to redo, oldest first. */
3
+ export interface Route {
4
+ undo: string[];
5
+ redo: string[];
6
+ }
7
+ /** A run named in the history is not kept any more, or never existed. */
8
+ export declare class UnknownRunError extends Error {
9
+ readonly runId: string;
10
+ constructor(runId: string);
11
+ }
12
+ /**
13
+ * Retention removed a run: an abandoned one whole, or one on the current line by chaining its
14
+ * changes into the next run. Either way the state right after it is gone.
15
+ */
16
+ export declare class HistoryExpiredError extends Error {
17
+ readonly runId: string;
18
+ /** The run that now holds its changes, when it was chained rather than removed. */
19
+ readonly keptIn: string | undefined;
20
+ constructor(runId: string, keptIn?: string);
21
+ }
22
+ /**
23
+ * The runs from `from` to `to` through their closest common ancestor. `undefined` is the start
24
+ * of the history, before any run.
25
+ */
26
+ export declare function route(runs: RunRecord[], from: string | undefined, to: string | undefined): Route;
27
+ /**
28
+ * The run `redo` goes to from `head`: the child of `head` towards the run the workspace was at
29
+ * most recently, so going back and forward again returns where it was. Without one, the newest
30
+ * child. `undefined` when `head` has no children.
31
+ */
32
+ export declare function redoTarget(runs: RunRecord[], revisions: Revision[], head: string | undefined): string | undefined;
@@ -0,0 +1,90 @@
1
+ /** A run named in the history is not kept any more, or never existed. */
2
+ export class UnknownRunError extends Error {
3
+ runId;
4
+ constructor(runId) {
5
+ super(`Run ${runId} is not in the history of this workspace`);
6
+ this.name = 'UnknownRunError';
7
+ this.runId = runId;
8
+ }
9
+ }
10
+ /**
11
+ * Retention removed a run: an abandoned one whole, or one on the current line by chaining its
12
+ * changes into the next run. Either way the state right after it is gone.
13
+ */
14
+ export class HistoryExpiredError extends Error {
15
+ runId;
16
+ /** The run that now holds its changes, when it was chained rather than removed. */
17
+ keptIn;
18
+ constructor(runId, keptIn) {
19
+ super(keptIn
20
+ ? `Run ${runId} is no longer kept: its changes are now part of run ${keptIn}`
21
+ : `Run ${runId} is no longer kept`);
22
+ this.name = 'HistoryExpiredError';
23
+ this.runId = runId;
24
+ this.keptIn = keptIn;
25
+ }
26
+ }
27
+ /**
28
+ * The runs from `from` to `to` through their closest common ancestor. `undefined` is the start
29
+ * of the history, before any run.
30
+ */
31
+ export function route(runs, from, to) {
32
+ const byId = new Map(runs.map(run => [run.runId, run]));
33
+ const back = ancestry(byId, from);
34
+ const forward = ancestry(byId, to);
35
+ // Both lines end at the start, so they share at least that much.
36
+ const shared = forward.filter(runId => back.includes(runId));
37
+ const meeting = shared[0];
38
+ return {
39
+ undo: until(back, meeting),
40
+ redo: until(forward, meeting).reverse(),
41
+ };
42
+ }
43
+ /**
44
+ * The run `redo` goes to from `head`: the child of `head` towards the run the workspace was at
45
+ * most recently, so going back and forward again returns where it was. Without one, the newest
46
+ * child. `undefined` when `head` has no children.
47
+ */
48
+ export function redoTarget(runs, revisions, head) {
49
+ const byId = new Map(runs.map(run => [run.runId, run]));
50
+ const children = runs.filter(run => run.parentRunId === head).map(run => run.runId);
51
+ if (children.length === 0)
52
+ return undefined;
53
+ for (const revision of [...revisions].reverse()) {
54
+ for (const visited of [revision.from, revision.to]) {
55
+ // Up from where the workspace was, until one of the children, if it is below `head`.
56
+ for (let current = visited; current !== undefined; current = byId.get(current)?.parentRunId) {
57
+ if (children.includes(current))
58
+ return current;
59
+ }
60
+ }
61
+ }
62
+ return children.sort().at(-1);
63
+ }
64
+ /**
65
+ * `runId` and its ancestors, newest first. A parent that is not kept any more makes the line
66
+ * unknown, because the workspace could not be moved across it.
67
+ */
68
+ function ancestry(byId, runId) {
69
+ const line = [];
70
+ for (let current = runId; current !== undefined;) {
71
+ const run = byId.get(current);
72
+ if (!run)
73
+ throw new UnknownRunError(current);
74
+ line.push(current);
75
+ current = run.parentRunId;
76
+ }
77
+ line.push(undefined);
78
+ return line;
79
+ }
80
+ /** The runs of `line` before `meeting`. */
81
+ function until(line, meeting) {
82
+ const result = [];
83
+ for (const runId of line) {
84
+ if (runId === meeting || runId === undefined)
85
+ break;
86
+ result.push(runId);
87
+ }
88
+ return result;
89
+ }
90
+ //# sourceMappingURL=runTree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runTree.js","sourceRoot":"","sources":["../../../src/recovery/domain/runTree.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Revision, RunRecord } from './recoveryStore.ts';\n\n/** The runs to undo, newest first, then the runs to redo, oldest first. */\nexport interface Route {\n\tundo: string[];\n\tredo: string[];\n}\n\n/** A run named in the history is not kept any more, or never existed. */\nexport class UnknownRunError extends Error {\n\treadonly runId: string;\n\n\tconstructor(runId: string) {\n\t\tsuper(`Run ${runId} is not in the history of this workspace`);\n\t\tthis.name = 'UnknownRunError';\n\t\tthis.runId = runId;\n\t}\n}\n\n/**\n * Retention removed a run: an abandoned one whole, or one on the current line by chaining its\n * changes into the next run. Either way the state right after it is gone.\n */\nexport class HistoryExpiredError extends Error {\n\treadonly runId: string;\n\t/** The run that now holds its changes, when it was chained rather than removed. */\n\treadonly keptIn: string | undefined;\n\n\tconstructor(runId: string, keptIn?: string) {\n\t\tsuper(\n\t\t\tkeptIn\n\t\t\t\t? `Run ${runId} is no longer kept: its changes are now part of run ${keptIn}`\n\t\t\t\t: `Run ${runId} is no longer kept`,\n\t\t);\n\t\tthis.name = 'HistoryExpiredError';\n\t\tthis.runId = runId;\n\t\tthis.keptIn = keptIn;\n\t}\n}\n\n/**\n * The runs from `from` to `to` through their closest common ancestor. `undefined` is the start\n * of the history, before any run.\n */\nexport function route(runs: RunRecord[], from: string | undefined, to: string | undefined): Route {\n\tconst byId = new Map(runs.map(run => [run.runId, run]));\n\tconst back = ancestry(byId, from);\n\tconst forward = ancestry(byId, to);\n\t// Both lines end at the start, so they share at least that much.\n\tconst shared = forward.filter(runId => back.includes(runId));\n\tconst meeting = shared[0];\n\n\treturn {\n\t\tundo: until(back, meeting),\n\t\tredo: until(forward, meeting).reverse(),\n\t};\n}\n\n/**\n * The run `redo` goes to from `head`: the child of `head` towards the run the workspace was at\n * most recently, so going back and forward again returns where it was. Without one, the newest\n * child. `undefined` when `head` has no children.\n */\nexport function redoTarget(\n\truns: RunRecord[],\n\trevisions: Revision[],\n\thead: string | undefined,\n): string | undefined {\n\tconst byId = new Map(runs.map(run => [run.runId, run]));\n\tconst children = runs.filter(run => run.parentRunId === head).map(run => run.runId);\n\tif (children.length === 0) return undefined;\n\n\tfor (const revision of [...revisions].reverse()) {\n\t\tfor (const visited of [revision.from, revision.to]) {\n\t\t\t// Up from where the workspace was, until one of the children, if it is below `head`.\n\t\t\tfor (let current = visited; current !== undefined; current = byId.get(current)?.parentRunId) {\n\t\t\t\tif (children.includes(current)) return current;\n\t\t\t}\n\t\t}\n\t}\n\treturn children.sort().at(-1);\n}\n\n/**\n * `runId` and its ancestors, newest first. A parent that is not kept any more makes the line\n * unknown, because the workspace could not be moved across it.\n */\nfunction ancestry(byId: Map<string, RunRecord>, runId: string | undefined): (string | undefined)[] {\n\tconst line: (string | undefined)[] = [];\n\tfor (let current = runId; current !== undefined;) {\n\t\tconst run = byId.get(current);\n\t\tif (!run) throw new UnknownRunError(current);\n\t\tline.push(current);\n\t\tcurrent = run.parentRunId;\n\t}\n\tline.push(undefined);\n\treturn line;\n}\n\n/** The runs of `line` before `meeting`. */\nfunction until(line: (string | undefined)[], meeting: string | undefined): string[] {\n\tconst result: string[] = [];\n\tfor (const runId of line) {\n\t\tif (runId === meeting || runId === undefined) break;\n\t\tresult.push(runId);\n\t}\n\treturn result;\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import { type History } from '../domain/history.ts';
2
+ /**
3
+ * The history of the runs that wrote to `root`, kept outside it under the platform's state
4
+ * directory. Only runs of a model-backed agent with write tools are recorded: the Agent SDK
5
+ * engines change files through their own tools, and their changes are not in it.
6
+ */
7
+ export declare function createHistory({ root, stateDirectory, }: {
8
+ root: string;
9
+ /** Where the history is kept. Defaults to the platform's state directory. */
10
+ stateDirectory?: string;
11
+ }): Promise<History>;
@@ -0,0 +1,17 @@
1
+ import { realpath } from 'node:fs/promises';
2
+ import { StoreHistory } from "../domain/history.js";
3
+ import { FileRecoveryStore } from "./fileRecoveryStore.js";
4
+ /**
5
+ * The history of the runs that wrote to `root`, kept outside it under the platform's state
6
+ * directory. Only runs of a model-backed agent with write tools are recorded: the Agent SDK
7
+ * engines change files through their own tools, and their changes are not in it.
8
+ */
9
+ export async function createHistory({ root, stateDirectory, }) {
10
+ const store = await FileRecoveryStore.open({
11
+ root,
12
+ ...(stateDirectory === undefined ? {} : { directory: stateDirectory }),
13
+ });
14
+ // The store keys the workspace by its real path, and records real paths.
15
+ return new StoreHistory({ store, root: await realpath(root) });
16
+ }
17
+ //# sourceMappingURL=createHistory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createHistory.js","sourceRoot":"","sources":["../../../src/recovery/infrastructure/createHistory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAgB,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EACnC,IAAI,EACJ,cAAc,GAKd;IACA,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAAC;QAC1C,IAAI;QACJ,GAAG,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,cAAc,EAAE,CAAC;KACtE,CAAC,CAAC;IACH,yEAAyE;IACzE,OAAO,IAAI,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAChE,CAAC","sourcesContent":["import { realpath } from 'node:fs/promises';\nimport { type History, StoreHistory } from '../domain/history.ts';\nimport { FileRecoveryStore } from './fileRecoveryStore.ts';\n\n/**\n * The history of the runs that wrote to `root`, kept outside it under the platform's state\n * directory. Only runs of a model-backed agent with write tools are recorded: the Agent SDK\n * engines change files through their own tools, and their changes are not in it.\n */\nexport async function createHistory({\n\troot,\n\tstateDirectory,\n}: {\n\troot: string;\n\t/** Where the history is kept. Defaults to the platform's state directory. */\n\tstateDirectory?: string;\n}): Promise<History> {\n\tconst store = await FileRecoveryStore.open({\n\t\troot,\n\t\t...(stateDirectory === undefined ? {} : { directory: stateDirectory }),\n\t});\n\t// The store keys the workspace by its real path, and records real paths.\n\treturn new StoreHistory({ store, root: await realpath(root) });\n}\n"]}