@mikode13/harness 11.0.0 → 11.1.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.
- package/README.md +138 -6
- package/dist/agent/domain/agent.d.ts +24 -3
- package/dist/agent/domain/agent.js.map +1 -1
- package/dist/agent/domain/approval.d.ts +20 -0
- package/dist/agent/domain/approval.js.map +1 -1
- package/dist/agent/domain/inbox.d.ts +42 -0
- package/dist/agent/domain/inbox.js +81 -0
- package/dist/agent/domain/inbox.js.map +1 -0
- package/dist/agent/domain/oneRunAtATime.d.ts +17 -0
- package/dist/agent/domain/oneRunAtATime.js +32 -0
- package/dist/agent/domain/oneRunAtATime.js.map +1 -0
- package/dist/agent/domain/runContext.d.ts +4 -0
- package/dist/agent/domain/runContext.js +5 -0
- package/dist/agent/domain/runContext.js.map +1 -1
- package/dist/agent/domain/session.d.ts +15 -0
- package/dist/agent/domain/session.js +80 -0
- package/dist/agent/domain/session.js.map +1 -0
- package/dist/engines/claude/infrastructure/model/claudeAgent.d.ts +6 -2
- package/dist/engines/claude/infrastructure/model/claudeAgent.js +20 -5
- package/dist/engines/claude/infrastructure/model/claudeAgent.js.map +1 -1
- package/dist/engines/codex/infrastructure/model/codexAgent.d.ts +5 -1
- package/dist/engines/codex/infrastructure/model/codexAgent.js +13 -2
- package/dist/engines/codex/infrastructure/model/codexAgent.js.map +1 -1
- package/dist/engines/domain/model/backgroundAgents.d.ts +5 -2
- package/dist/engines/domain/model/backgroundAgents.js +11 -3
- package/dist/engines/domain/model/backgroundAgents.js.map +1 -1
- package/dist/engines/domain/model/llmAgent.d.ts +5 -0
- package/dist/engines/domain/model/llmAgent.js +68 -10
- package/dist/engines/domain/model/llmAgent.js.map +1 -1
- package/dist/factory/infrastructure/agentLLMFactory.d.ts +18 -5
- package/dist/factory/infrastructure/agentLLMFactory.js +31 -8
- package/dist/factory/infrastructure/agentLLMFactory.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/orchestration/domain/model/orchestratorAgent.d.ts +9 -1
- package/dist/orchestration/domain/model/orchestratorAgent.js +20 -3
- package/dist/orchestration/domain/model/orchestratorAgent.js.map +1 -1
- package/dist/recovery/domain/history.d.ts +10 -3
- package/dist/recovery/domain/history.js +1 -0
- package/dist/recovery/domain/history.js.map +1 -1
- package/dist/recovery/domain/recoveryStore.d.ts +14 -2
- package/dist/recovery/domain/recoveryStore.js.map +1 -1
- package/dist/recovery/infrastructure/fileRecoveryStore.d.ts +21 -2
- package/dist/recovery/infrastructure/fileRecoveryStore.js +58 -6
- package/dist/recovery/infrastructure/fileRecoveryStore.js.map +1 -1
- package/dist/recovery/infrastructure/folderClaims.d.ts +27 -0
- package/dist/recovery/infrastructure/folderClaims.js +124 -0
- package/dist/recovery/infrastructure/folderClaims.js.map +1 -0
- package/dist/shared/domain/session.d.ts +12 -0
- package/dist/shared/domain/session.js +2 -0
- package/dist/shared/domain/session.js.map +1 -0
- package/dist/tools/domain/preparedCall.d.ts +3 -1
- package/dist/tools/domain/preparedCall.js.map +1 -1
- package/dist/tools/infrastructure/commandTool.js +2 -0
- package/dist/tools/infrastructure/commandTool.js.map +1 -1
- package/dist/tools/infrastructure/exploreTool.js +1 -1
- package/dist/tools/infrastructure/exploreTool.js.map +1 -1
- package/dist/tools/infrastructure/fileTools.d.ts +2 -0
- package/dist/tools/infrastructure/fileTools.js +9 -1
- package/dist/tools/infrastructure/fileTools.js.map +1 -1
- package/dist/tools/infrastructure/removeWorktreeTool.d.ts +23 -0
- package/dist/tools/infrastructure/removeWorktreeTool.js +172 -0
- package/dist/tools/infrastructure/removeWorktreeTool.js.map +1 -0
- package/dist/tools/infrastructure/workspaceWrites.d.ts +4 -0
- package/dist/tools/infrastructure/workspaceWrites.js +30 -16
- package/dist/tools/infrastructure/workspaceWrites.js.map +1 -1
- package/dist/tools/infrastructure/worktrees.d.ts +49 -0
- package/dist/tools/infrastructure/worktrees.js +154 -0
- package/dist/tools/infrastructure/worktrees.js.map +1 -0
- package/dist/tools/infrastructure/writeSession.d.ts +5 -1
- package/dist/tools/infrastructure/writeSession.js +7 -3
- package/dist/tools/infrastructure/writeSession.js.map +1 -1
- package/dist/workflow/domain/workflow.d.ts +3 -0
- package/dist/workflow/domain/workflow.js.map +1 -1
- package/dist/workflow/domain/workflowRunner.js +8 -2
- package/dist/workflow/domain/workflowRunner.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestratorAgent.js","sourceRoot":"","sources":["../../../../src/orchestration/domain/model/orchestratorAgent.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tAgent,\n\tAgentResponse,\n\tProgressAgent,\n\tProgressEvent,\n\tRunOptions,\n} from '#src/agent/domain/agent';\nimport { addTokens, type Tokens } from '#src/shared/domain/tokens';\nimport { RecoverableError, UnrecoverableError, withSpentTokens } from '#src/shared/domain/errors';\nimport type { ReviewerDecision } from './reviewerDecision.ts';\nimport type { Validator } from '#src/shared/domain/validator';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport {\n\tclassifyHostFailure,\n\tdescribeFailure,\n\ttreatErrors,\n} from '#src/shared/domain/providerFailure';\nimport {\n\tRunContext,\n\ttype RunEnd,\n\trunContextOf,\n\twithRunContext,\n} from '#src/agent/domain/runContext';\nimport { isAbortError } from '#src/shared/domain/isAbortError';\n\n// Each role's standing instructions, kept apart from the data of a round so an agent with a\n// system prompt can hold them there instead of receiving them again in every prompt. An agent\n// without one gets them at the head of each prompt; see `InstructedAgent`.\nconst conciseCommunication =\n\t'Communicate tersely and professionally. In reasoning updates, state only the next concrete action or a verified finding, in one short sentence. Do not narrate routine intentions, uncertainty, self-reflection, encouragement, or a desire to be thorough. Do not restate tool results. Once you decide what to do, act immediately. Use only the detail required by your role in the final response.';\n\nexport const plannerInstructions = `You are the planner agent. Read the relevant repository code and create a concise, engineering-grade plan for the executor. Cover the requested behaviour, structural issues, affected files or symbols, API contracts and boundaries, important failure paths, and meaningful tests. Keep the plan focused on the current request and do not require an architecture redesign yet. ${conciseCommunication}`;\n\nconst executorTask = `You are the executor agent. Read the relevant repository code and implement the original user request according to the planner's current plan. Preserve contracts and boundaries, handle important failure paths, keep the change focused, and add or update meaningful tests. Inspect and change the code; do not merely describe what should be done. ${conciseCommunication}`;\n\n// For an executor that can run the project's tests and checks: an Agent SDK one, which has a\n// shell, or a model-API one given commands. The harness cannot tell which command runs the\n// tests, so any command counts.\nexport const executorInstructions = `${executorTask} Before you finish, run the project's tests and checks, and finish only when they pass. Once they pass and the plan is carried out, finish at once: do not add work beyond the plan, because the reviewer asks for what is missing. If you cannot make them pass, finish by saying which ones fail and why.`;\n\n// For a model-API executor with no command, which cannot run anything: told so, it neither\n// spends its steps looking for a way to test nor claims a result it could not have.\nexport const executorInstructionsWithoutCommands = `${executorTask} You have no command that runs the project's tests or checks, so you cannot run them. Once the plan is carried out, finish at once: do not add work beyond the plan, because the reviewer asks for what is missing. Say in your summary that the tests and checks were not run.`;\n\nconst reviewerTask = `Review the code. Independently inspect the repository, the current diff, and relevant surrounding code; do not rely on the executor's narrative. Judge it against the original request, and any issue it names, first: check that the plan is right for it, and that the change fulfills it. Check correctness and logic errors, important failure paths, unused or artificial abstractions, races or shared state, API contract breaks, boundary violations, regressions, scope, and test quality. If it is correct, respond with JSON only: {\"decision\":\"approved\"}. If it is not correct, respond with JSON only: {\"decision\":\"rejected\",\"feedback\":\"list concrete, prioritized findings and the required direction for each\"}. The feedback must contain actionable engineering findings, not a general summary. ${conciseCommunication}`;\n\n// Each goes with the executor instructions of the same name. The executor may finish with\n// failing tests, saying so, and nothing in the harness checks which: the reviewer takes it\n// from the executor's response.\nexport const reviewerInstructions = `You are the reviewer agent. The executor was told to run the project's tests and checks before finishing, and to finish only when they pass, or to say which ones fail and why. Whether they pass is the one thing to take from its response: do not run them yourself, and do not approve a change whose response says that any of them fail. ${reviewerTask}`;\n\nexport const reviewerInstructionsWithoutCommands = `You are the reviewer agent. Nobody has run the tests or checks: the executor had no command for them. Judge from the code whether the tests it added or changed would pass, and do not approve a change whose tests could not. ${reviewerTask}`;\n\nconst getPlannerPrompt = (userPrompt: string, previousFailureReason?: string) => {\n\tconst feedback = previousFailureReason\n\t\t? `\\n\\nFeedback from the previous attempt:\\n---\\n${previousFailureReason}\\n---`\n\t\t: '';\n\n\treturn `Original user request:\n---\n${userPrompt}\n---${feedback}`;\n};\n\nconst getExecutorPrompt = (userPrompt: string, plannerPrompt: string) =>\n\t`Original user request:\n---\n${userPrompt}\n---\n\nCurrent implementation plan:\n---\n${plannerPrompt}\n---`;\n\nconst getReviewerPrompt = (\n\tuserPrompt: string,\n\tplannerPrompt: string,\n\texecutorResult: string,\n\tparseFailureReason?: string,\n) => {\n\tconst retryNotice = parseFailureReason\n\t\t? `\\n\\nYour previous response could not be used: ${parseFailureReason} Respond with JSON only, matching the schema exactly, with no surrounding text and no markdown code fences.`\n\t\t: '';\n\n\treturn `Original user request:\n---\n${userPrompt}\n---\n\nPlanner's current plan:\n---\n${plannerPrompt}\n---\n\nExecutor response (context only; verify it independently):\n---\n${executorResult}\n---${retryNotice}`;\n};\n\ninterface RunTotals {\n\ttokens: Tokens | undefined;\n\t/** A role answered without usage, so the run's total cannot be known. */\n\tunreported: boolean;\n}\n\n// Every role's response counts, even an empty one that starts another round: it was billed.\nfunction addToTotals(totals: RunTotals, response: AgentResponse): void {\n\tif (!response.tokens) totals.unreported = true;\n\ttotals.tokens = addTokens(totals.tokens, response.tokens);\n}\n\n/** Unknown rather than partial: a sum missing a billed call must not look complete. */\nfunction runTokens(totals: RunTotals): Tokens | undefined {\n\treturn totals.unreported ? undefined : totals.tokens;\n}\n\nfunction stripCodeFence(text: string): string {\n\tconst match = /^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/.exec(text.trim());\n\treturn match?.[1] ?? text;\n}\n\nfunction parseReviewerDecision(\n\tresponse: AgentResponse,\n\treviewerDecisionValidator: Validator<ReviewerDecision>,\n): ReviewerDecision {\n\tif (!response.response) {\n\t\tthrow new RecoverableError(\"There's no decision from the reviewer, something went wrong\", {\n\t\t\tcause: 'Reviewer decision is missing.',\n\t\t});\n\t}\n\n\tlet parsedResponse: unknown;\n\ttry {\n\t\tparsedResponse = JSON.parse(stripCodeFence(response.response));\n\t} catch {\n\t\tthrow new RecoverableError('Reviewer returned an invalid decision', {\n\t\t\tcause: 'Reviewer response must be valid JSON.',\n\t\t});\n\t}\n\n\tconst decision = reviewerDecisionValidator.validate(parsedResponse);\n\tif (!decision) {\n\t\tthrow new RecoverableError('Reviewer returned an invalid decision', {\n\t\t\tcause: 'Reviewer response did not match the decision schema.',\n\t\t});\n\t}\n\n\treturn decision;\n}\n\nexport class OrchestratorAgent implements Agent {\n\tprivate plannerAgent: Agent;\n\tprivate executorAgent: Agent;\n\tprivate reviewerAgent: Agent;\n\tprivate reviewerDecisionValidator: Validator<ReviewerDecision>;\n\tprivate maxAttempts: number;\n\tprivate logger: ILogger;\n\n\tconstructor({\n\t\tplannerAgent,\n\t\texecutorAgent,\n\t\treviewerAgent,\n\t\treviewerDecisionValidator,\n\t\tmaxAttempts = 3,\n\t\tlogger,\n\t}: {\n\t\tplannerAgent: Agent;\n\t\texecutorAgent: Agent;\n\t\treviewerAgent: Agent;\n\t\treviewerDecisionValidator: Validator<ReviewerDecision>;\n\t\tmaxAttempts?: number;\n\t\tlogger: ILogger;\n\t}) {\n\t\tif (!Number.isInteger(maxAttempts) || maxAttempts < 1) {\n\t\t\tthrow new RangeError('maxAttempts must be a positive integer');\n\t\t}\n\n\t\tthis.plannerAgent = plannerAgent;\n\t\tthis.executorAgent = executorAgent;\n\t\tthis.reviewerAgent = reviewerAgent;\n\t\tthis.reviewerDecisionValidator = reviewerDecisionValidator;\n\t\tthis.maxAttempts = maxAttempts;\n\t\tthis.logger = logger;\n\t}\n\n\t/**\n\t * Every role, round and retry of one run shares one run context, so what the executor writes\n\t * is one run of the workspace's history, and the planner's and reviewer's turns are tied to\n\t * it. The orchestrator ends that context with the run, unless an outer agent owns it.\n\t */\n\tasync run(prompt: string, options: RunOptions): Promise<AgentResponse> {\n\t\tconst start = Date.now();\n\t\t// Per invocation, not per instance: the CLI keeps one orchestrator for a whole session.\n\t\tconst totals: RunTotals = { tokens: undefined, unreported: false };\n\t\tconst inherited = runContextOf(options);\n\t\tconst context = inherited ?? new RunContext();\n\t\tlet end: RunEnd = 'failed';\n\n\t\ttry {\n\t\t\tawait this.runRounds(prompt, inherited ? options : withRunContext(options, context), totals);\n\t\t\tend = 'completed';\n\t\t} catch (error) {\n\t\t\tif (isAbortError(error)) end = 'cancelled';\n\t\t\t// A failing role carries its own tokens; the roles before it are in the totals.\n\t\t\tthrow withSpentTokens(error, totals.tokens, totals.unreported);\n\t\t} finally {\n\t\t\tif (!inherited) await this.finishContext(context, end);\n\t\t}\n\n\t\t// Read once the run ended: a run that changed nothing left the history then.\n\t\tconst { runId } = context.historyRun;\n\t\treturn {\n\t\t\tresponse: 'All job has finished',\n\t\t\ttokens: runTokens(totals),\n\t\t\t// The whole run's wall clock, so it matches what the consumer waited.\n\t\t\tduration: (Date.now() - start) / 1000,\n\t\t\t...(!inherited && runId ? { runId } : {}),\n\t\t};\n\t}\n\n\t/** The run's records end here, but its result stands: what it changed is already on disk. */\n\tprivate async finishContext(context: RunContext, end: RunEnd): Promise<void> {\n\t\ttry {\n\t\t\tawait context.finish(end);\n\t\t} catch (error) {\n\t\t\tthis.warn(`The run could not close its records: ${describeFailure(error)}`);\n\t\t}\n\t}\n\n\tprivate async runRounds(prompt: string, options: RunOptions, totals: RunTotals): Promise<void> {\n\t\tlet lastFailureReason: string | undefined;\n\n\t\tfor (let attempt = 1; attempt <= this.maxAttempts; attempt++) {\n\t\t\tconst isLastAttempt = attempt === this.maxAttempts;\n\n\t\t\tconst plannerResponse = await this.plannerAgent.run(\n\t\t\t\tgetPlannerPrompt(prompt, lastFailureReason),\n\t\t\t\tthis.optionsForRole(options, 'planner'),\n\t\t\t);\n\t\t\taddToTotals(totals, plannerResponse);\n\n\t\t\tif (!plannerResponse.response) {\n\t\t\t\tlastFailureReason = 'The planner produced no response.';\n\t\t\t\tif (isLastAttempt) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t\t}\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the planner produced no response; starting another round`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst executorResponse = await this.executorAgent.run(\n\t\t\t\tgetExecutorPrompt(prompt, plannerResponse.response),\n\t\t\t\tthis.optionsForRole(options, 'executor'),\n\t\t\t);\n\t\t\taddToTotals(totals, executorResponse);\n\n\t\t\tif (!executorResponse.response) {\n\t\t\t\tlastFailureReason = 'The executor produced no response.';\n\t\t\t\tif (isLastAttempt) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t\t}\n\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the executor produced no response; starting another round`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst reviewerDecision = await this.getReviewerDecision(\n\t\t\t\tprompt,\n\t\t\t\tplannerResponse.response,\n\t\t\t\texecutorResponse.response,\n\t\t\t\toptions,\n\t\t\t\ttotals,\n\t\t\t);\n\n\t\t\tif (reviewerDecision.decision === 'approved') return;\n\n\t\t\tlastFailureReason = reviewerDecision.feedback;\n\t\t\tif (isLastAttempt) {\n\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t}\n\n\t\t\tthis.warn(\n\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the reviewer rejected the round; starting another with its feedback`,\n\t\t\t\tlastFailureReason,\n\t\t\t);\n\t\t}\n\n\t\tthrow new UnrecoverableError('Max attempts exhausted', {\n\t\t\tcause: lastFailureReason ?? 'Unknown failure.',\n\t\t});\n\t}\n\n\t// Retries only the reviewer call on a malformed decision, instead of redoing\n\t// planning/execution — a bad or unparsable reviewer response is not evidence\n\t// the plan or the implementation were wrong.\n\tprivate async getReviewerDecision(\n\t\tuserPrompt: string,\n\t\tplannerPrompt: string,\n\t\texecutorResult: string,\n\t\toptions: RunOptions,\n\t\ttotals: RunTotals,\n\t): Promise<ReviewerDecision> {\n\t\tlet parseFailureReason: string | undefined;\n\n\t\tfor (let attempt = 1; attempt <= this.maxAttempts; attempt++) {\n\t\t\tconst reviewerResponse = await this.reviewerAgent.run(\n\t\t\t\tgetReviewerPrompt(userPrompt, plannerPrompt, executorResult, parseFailureReason),\n\t\t\t\tthis.optionsForRole(options, 'reviewer'),\n\t\t\t);\n\n\t\t\taddToTotals(totals, reviewerResponse);\n\n\t\t\ttry {\n\t\t\t\treturn parseReviewerDecision(reviewerResponse, this.reviewerDecisionValidator);\n\t\t\t} catch (error) {\n\t\t\t\tif (!(error instanceof RecoverableError)) throw error;\n\n\t\t\t\tparseFailureReason = error.cause;\n\t\t\t\tif (attempt === this.maxAttempts) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: parseFailureReason });\n\t\t\t\t}\n\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Reviewer decision ${String(attempt)}/${String(this.maxAttempts)} was unusable; asking the reviewer again`,\n\t\t\t\t\tparseFailureReason,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tthrow new UnrecoverableError('Max attempts exhausted', {\n\t\t\tcause: parseFailureReason ?? 'Unknown failure.',\n\t\t});\n\t}\n\n\tprivate warn(...args: unknown[]): void {\n\t\ttreatErrors(\n\t\t\t() => {\n\t\t\t\tthis.logger.warn(...args);\n\t\t\t},\n\t\t\tclassifyHostFailure,\n\t\t\t'Orchestrator logger failed while reporting a retry',\n\t\t);\n\t}\n\n\t/**\n\t * Keep the options object extensible and the private run context intact while making the\n\t * orchestrator's attribution authoritative over anything a nested agent supplied.\n\t */\n\tprivate optionsForRole(options: RunOptions, agent: ProgressAgent): RunOptions {\n\t\tconst onProgress = options.onProgress;\n\t\treturn {\n\t\t\t...options,\n\t\t\tonProgress: onProgress\n\t\t\t\t? (event: ProgressEvent) => {\n\t\t\t\t\t\ttreatErrors(\n\t\t\t\t\t\t\t() => {\n\t\t\t\t\t\t\t\tonProgress({ ...event, agent });\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tclassifyHostFailure,\n\t\t\t\t\t\t\t'Orchestrator progress callback failed',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\t};\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"orchestratorAgent.js","sourceRoot":"","sources":["../../../../src/orchestration/domain/model/orchestratorAgent.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tAgent,\n\tAgentResponse,\n\tProgressAgent,\n\tProgressEvent,\n\tRunOptions,\n} from '#src/agent/domain/agent';\nimport { addTokens, type Tokens } from '#src/shared/domain/tokens';\nimport { RecoverableError, UnrecoverableError, withSpentTokens } from '#src/shared/domain/errors';\nimport type { ReviewerDecision } from './reviewerDecision.ts';\nimport type { Validator } from '#src/shared/domain/validator';\nimport type { ILogger } from '#src/shared/domain/logger';\nimport {\n\tclassifyHostFailure,\n\tdescribeFailure,\n\ttreatErrors,\n} from '#src/shared/domain/providerFailure';\nimport {\n\tRunContext,\n\ttype RunEnd,\n\trunContextOf,\n\twithRunContext,\n} from '#src/agent/domain/runContext';\nimport { isAbortError } from '#src/shared/domain/isAbortError';\nimport { OneRunAtATime } from '#src/agent/domain/oneRunAtATime';\nimport { newSession, sessionOf } from '#src/agent/domain/session';\n\n// Each role's standing instructions, kept apart from the data of a round so an agent with a\n// system prompt can hold them there instead of receiving them again in every prompt. An agent\n// without one gets them at the head of each prompt; see `InstructedAgent`.\nconst conciseCommunication =\n\t'Communicate tersely and professionally. In reasoning updates, state only the next concrete action or a verified finding, in one short sentence. Do not narrate routine intentions, uncertainty, self-reflection, encouragement, or a desire to be thorough. Do not restate tool results. Once you decide what to do, act immediately. Use only the detail required by your role in the final response.';\n\nexport const plannerInstructions = `You are the planner agent. Read the relevant repository code and create a concise, engineering-grade plan for the executor. Cover the requested behaviour, structural issues, affected files or symbols, API contracts and boundaries, important failure paths, and meaningful tests. Keep the plan focused on the current request and do not require an architecture redesign yet. ${conciseCommunication}`;\n\nconst executorTask = `You are the executor agent. Read the relevant repository code and implement the original user request according to the planner's current plan. Preserve contracts and boundaries, handle important failure paths, keep the change focused, and add or update meaningful tests. Inspect and change the code; do not merely describe what should be done. ${conciseCommunication}`;\n\n// For an executor that can run the project's tests and checks: an Agent SDK one, which has a\n// shell, or a model-API one given commands. The harness cannot tell which command runs the\n// tests, so any command counts.\nexport const executorInstructions = `${executorTask} Before you finish, run the project's tests and checks, and finish only when they pass. Once they pass and the plan is carried out, finish at once: do not add work beyond the plan, because the reviewer asks for what is missing. If you cannot make them pass, finish by saying which ones fail and why.`;\n\n// For a model-API executor with no command, which cannot run anything: told so, it neither\n// spends its steps looking for a way to test nor claims a result it could not have.\nexport const executorInstructionsWithoutCommands = `${executorTask} You have no command that runs the project's tests or checks, so you cannot run them. Once the plan is carried out, finish at once: do not add work beyond the plan, because the reviewer asks for what is missing. Say in your summary that the tests and checks were not run.`;\n\nconst reviewerTask = `Review the code. Independently inspect the repository, the current diff, and relevant surrounding code; do not rely on the executor's narrative. Judge it against the original request, and any issue it names, first: check that the plan is right for it, and that the change fulfills it. Check correctness and logic errors, important failure paths, unused or artificial abstractions, races or shared state, API contract breaks, boundary violations, regressions, scope, and test quality. If it is correct, respond with JSON only: {\"decision\":\"approved\"}. If it is not correct, respond with JSON only: {\"decision\":\"rejected\",\"feedback\":\"list concrete, prioritized findings and the required direction for each\"}. The feedback must contain actionable engineering findings, not a general summary. ${conciseCommunication}`;\n\n// Each goes with the executor instructions of the same name. The executor may finish with\n// failing tests, saying so, and nothing in the harness checks which: the reviewer takes it\n// from the executor's response.\nexport const reviewerInstructions = `You are the reviewer agent. The executor was told to run the project's tests and checks before finishing, and to finish only when they pass, or to say which ones fail and why. Whether they pass is the one thing to take from its response: do not run them yourself, and do not approve a change whose response says that any of them fail. ${reviewerTask}`;\n\nexport const reviewerInstructionsWithoutCommands = `You are the reviewer agent. Nobody has run the tests or checks: the executor had no command for them. Judge from the code whether the tests it added or changed would pass, and do not approve a change whose tests could not. ${reviewerTask}`;\n\nconst getPlannerPrompt = (userPrompt: string, previousFailureReason?: string) => {\n\tconst feedback = previousFailureReason\n\t\t? `\\n\\nFeedback from the previous attempt:\\n---\\n${previousFailureReason}\\n---`\n\t\t: '';\n\n\treturn `Original user request:\n---\n${userPrompt}\n---${feedback}`;\n};\n\nconst getExecutorPrompt = (userPrompt: string, plannerPrompt: string) =>\n\t`Original user request:\n---\n${userPrompt}\n---\n\nCurrent implementation plan:\n---\n${plannerPrompt}\n---`;\n\nconst getReviewerPrompt = (\n\tuserPrompt: string,\n\tplannerPrompt: string,\n\texecutorResult: string,\n\tparseFailureReason?: string,\n) => {\n\tconst retryNotice = parseFailureReason\n\t\t? `\\n\\nYour previous response could not be used: ${parseFailureReason} Respond with JSON only, matching the schema exactly, with no surrounding text and no markdown code fences.`\n\t\t: '';\n\n\treturn `Original user request:\n---\n${userPrompt}\n---\n\nPlanner's current plan:\n---\n${plannerPrompt}\n---\n\nExecutor response (context only; verify it independently):\n---\n${executorResult}\n---${retryNotice}`;\n};\n\ninterface RunTotals {\n\ttokens: Tokens | undefined;\n\t/** A role answered without usage, so the run's total cannot be known. */\n\tunreported: boolean;\n}\n\n// Every role's response counts, even an empty one that starts another round: it was billed.\nfunction addToTotals(totals: RunTotals, response: AgentResponse): void {\n\tif (!response.tokens) totals.unreported = true;\n\ttotals.tokens = addTokens(totals.tokens, response.tokens);\n}\n\n/** Unknown rather than partial: a sum missing a billed call must not look complete. */\nfunction runTokens(totals: RunTotals): Tokens | undefined {\n\treturn totals.unreported ? undefined : totals.tokens;\n}\n\nfunction stripCodeFence(text: string): string {\n\tconst match = /^```(?:json)?\\s*([\\s\\S]*?)\\s*```$/.exec(text.trim());\n\treturn match?.[1] ?? text;\n}\n\nfunction parseReviewerDecision(\n\tresponse: AgentResponse,\n\treviewerDecisionValidator: Validator<ReviewerDecision>,\n): ReviewerDecision {\n\tif (!response.response) {\n\t\tthrow new RecoverableError(\"There's no decision from the reviewer, something went wrong\", {\n\t\t\tcause: 'Reviewer decision is missing.',\n\t\t});\n\t}\n\n\tlet parsedResponse: unknown;\n\ttry {\n\t\tparsedResponse = JSON.parse(stripCodeFence(response.response));\n\t} catch {\n\t\tthrow new RecoverableError('Reviewer returned an invalid decision', {\n\t\t\tcause: 'Reviewer response must be valid JSON.',\n\t\t});\n\t}\n\n\tconst decision = reviewerDecisionValidator.validate(parsedResponse);\n\tif (!decision) {\n\t\tthrow new RecoverableError('Reviewer returned an invalid decision', {\n\t\t\tcause: 'Reviewer response did not match the decision schema.',\n\t\t});\n\t}\n\n\treturn decision;\n}\n\nexport class OrchestratorAgent implements Agent {\n\tprivate plannerAgent: Agent;\n\tprivate executorAgent: Agent;\n\tprivate reviewerAgent: Agent;\n\tprivate reviewerDecisionValidator: Validator<ReviewerDecision>;\n\tprivate maxAttempts: number;\n\tprivate logger: ILogger;\n\tprivate readonly runs = new OneRunAtATime();\n\t// The session its runs belong to when their options name none.\n\tprivate readonly session = newSession();\n\n\tconstructor({\n\t\tplannerAgent,\n\t\texecutorAgent,\n\t\treviewerAgent,\n\t\treviewerDecisionValidator,\n\t\tmaxAttempts = 3,\n\t\tlogger,\n\t}: {\n\t\tplannerAgent: Agent;\n\t\texecutorAgent: Agent;\n\t\treviewerAgent: Agent;\n\t\treviewerDecisionValidator: Validator<ReviewerDecision>;\n\t\tmaxAttempts?: number;\n\t\tlogger: ILogger;\n\t}) {\n\t\tif (!Number.isInteger(maxAttempts) || maxAttempts < 1) {\n\t\t\tthrow new RangeError('maxAttempts must be a positive integer');\n\t\t}\n\n\t\tthis.plannerAgent = plannerAgent;\n\t\tthis.executorAgent = executorAgent;\n\t\tthis.reviewerAgent = reviewerAgent;\n\t\tthis.reviewerDecisionValidator = reviewerDecisionValidator;\n\t\tthis.maxAttempts = maxAttempts;\n\t\tthis.logger = logger;\n\t}\n\n\t/**\n\t * Every role, round and retry of one run shares one run context, so what the executor writes\n\t * is one run of the workspace's history, and the planner's and reviewer's turns are tied to\n\t * it. The orchestrator ends that context with the run, unless an outer agent owns it.\n\t *\n\t * @throws {RunInProgressError} when another run on it is still working, before any role\n\t * starts.\n\t */\n\trun(prompt: string, options: RunOptions): Promise<AgentResponse> {\n\t\treturn this.runs.run(() => this.runAlone(prompt, options));\n\t}\n\n\tprivate async runAlone(prompt: string, options: RunOptions): Promise<AgentResponse> {\n\t\tconst start = Date.now();\n\t\t// Per invocation, not per instance: the CLI keeps one orchestrator for a whole session.\n\t\tconst totals: RunTotals = { tokens: undefined, unreported: false };\n\t\tconst session = sessionOf(options) ?? this.session;\n\t\tconst inherited = runContextOf(options);\n\t\tconst context = inherited ?? new RunContext(session);\n\t\tlet end: RunEnd = 'failed';\n\n\t\ttry {\n\t\t\tawait this.runRounds(prompt, inherited ? options : withRunContext(options, context), totals);\n\t\t\tend = 'completed';\n\t\t} catch (error) {\n\t\t\tif (isAbortError(error)) end = 'cancelled';\n\t\t\t// A failing role carries its own tokens; the roles before it are in the totals.\n\t\t\tthrow withSpentTokens(error, totals.tokens, totals.unreported);\n\t\t} finally {\n\t\t\tif (!inherited) await this.finishContext(context, end);\n\t\t}\n\n\t\t// Read once the run ended: a run that changed nothing left the history then.\n\t\tconst { runId } = context.historyRun;\n\t\treturn {\n\t\t\tresponse: 'All job has finished',\n\t\t\ttokens: runTokens(totals),\n\t\t\t// The whole run's wall clock, so it matches what the consumer waited.\n\t\t\tduration: (Date.now() - start) / 1000,\n\t\t\t...(!inherited && runId ? { runId } : {}),\n\t\t\t// A copy: a consumer changing it must not change what later runs record.\n\t\t\tsession: { ...session },\n\t\t};\n\t}\n\n\t/** The run's records end here, but its result stands: what it changed is already on disk. */\n\tprivate async finishContext(context: RunContext, end: RunEnd): Promise<void> {\n\t\ttry {\n\t\t\tawait context.finish(end);\n\t\t} catch (error) {\n\t\t\tthis.warn(`The run could not close its records: ${describeFailure(error)}`);\n\t\t}\n\t}\n\n\tprivate async runRounds(prompt: string, options: RunOptions, totals: RunTotals): Promise<void> {\n\t\tlet lastFailureReason: string | undefined;\n\n\t\tfor (let attempt = 1; attempt <= this.maxAttempts; attempt++) {\n\t\t\tconst isLastAttempt = attempt === this.maxAttempts;\n\n\t\t\tconst plannerResponse = await this.plannerAgent.run(\n\t\t\t\tgetPlannerPrompt(prompt, lastFailureReason),\n\t\t\t\tthis.optionsForRole(options, 'planner'),\n\t\t\t);\n\t\t\taddToTotals(totals, plannerResponse);\n\n\t\t\tif (!plannerResponse.response) {\n\t\t\t\tlastFailureReason = 'The planner produced no response.';\n\t\t\t\tif (isLastAttempt) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t\t}\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the planner produced no response; starting another round`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst executorResponse = await this.executorAgent.run(\n\t\t\t\tgetExecutorPrompt(prompt, plannerResponse.response),\n\t\t\t\tthis.optionsForRole(options, 'executor'),\n\t\t\t);\n\t\t\taddToTotals(totals, executorResponse);\n\n\t\t\tif (!executorResponse.response) {\n\t\t\t\tlastFailureReason = 'The executor produced no response.';\n\t\t\t\tif (isLastAttempt) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t\t}\n\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the executor produced no response; starting another round`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst reviewerDecision = await this.getReviewerDecision(\n\t\t\t\tprompt,\n\t\t\t\tplannerResponse.response,\n\t\t\t\texecutorResponse.response,\n\t\t\t\toptions,\n\t\t\t\ttotals,\n\t\t\t);\n\n\t\t\tif (reviewerDecision.decision === 'approved') return;\n\n\t\t\tlastFailureReason = reviewerDecision.feedback;\n\t\t\tif (isLastAttempt) {\n\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: lastFailureReason });\n\t\t\t}\n\n\t\t\tthis.warn(\n\t\t\t\t`Attempt ${String(attempt)}/${String(this.maxAttempts)}: the reviewer rejected the round; starting another with its feedback`,\n\t\t\t\tlastFailureReason,\n\t\t\t);\n\t\t}\n\n\t\tthrow new UnrecoverableError('Max attempts exhausted', {\n\t\t\tcause: lastFailureReason ?? 'Unknown failure.',\n\t\t});\n\t}\n\n\t// Retries only the reviewer call on a malformed decision, instead of redoing\n\t// planning/execution — a bad or unparsable reviewer response is not evidence\n\t// the plan or the implementation were wrong.\n\tprivate async getReviewerDecision(\n\t\tuserPrompt: string,\n\t\tplannerPrompt: string,\n\t\texecutorResult: string,\n\t\toptions: RunOptions,\n\t\ttotals: RunTotals,\n\t): Promise<ReviewerDecision> {\n\t\tlet parseFailureReason: string | undefined;\n\n\t\tfor (let attempt = 1; attempt <= this.maxAttempts; attempt++) {\n\t\t\tconst reviewerResponse = await this.reviewerAgent.run(\n\t\t\t\tgetReviewerPrompt(userPrompt, plannerPrompt, executorResult, parseFailureReason),\n\t\t\t\tthis.optionsForRole(options, 'reviewer'),\n\t\t\t);\n\n\t\t\taddToTotals(totals, reviewerResponse);\n\n\t\t\ttry {\n\t\t\t\treturn parseReviewerDecision(reviewerResponse, this.reviewerDecisionValidator);\n\t\t\t} catch (error) {\n\t\t\t\tif (!(error instanceof RecoverableError)) throw error;\n\n\t\t\t\tparseFailureReason = error.cause;\n\t\t\t\tif (attempt === this.maxAttempts) {\n\t\t\t\t\tthrow new UnrecoverableError('Max attempts exhausted', { cause: parseFailureReason });\n\t\t\t\t}\n\n\t\t\t\tthis.warn(\n\t\t\t\t\t`Reviewer decision ${String(attempt)}/${String(this.maxAttempts)} was unusable; asking the reviewer again`,\n\t\t\t\t\tparseFailureReason,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tthrow new UnrecoverableError('Max attempts exhausted', {\n\t\t\tcause: parseFailureReason ?? 'Unknown failure.',\n\t\t});\n\t}\n\n\tprivate warn(...args: unknown[]): void {\n\t\ttreatErrors(\n\t\t\t() => {\n\t\t\t\tthis.logger.warn(...args);\n\t\t\t},\n\t\t\tclassifyHostFailure,\n\t\t\t'Orchestrator logger failed while reporting a retry',\n\t\t);\n\t}\n\n\t/**\n\t * Keep the options object extensible and the private run context intact while making the\n\t * orchestrator's attribution authoritative over anything a nested agent supplied. No role\n\t * reads the inbox, which only a single model-backed agent reads: what was sent stays for the\n\t * next run.\n\t */\n\tprivate optionsForRole(options: RunOptions, agent: ProgressAgent): RunOptions {\n\t\tconst onProgress = options.onProgress;\n\t\treturn {\n\t\t\t...options,\n\t\t\tinbox: undefined,\n\t\t\tonProgress: onProgress\n\t\t\t\t? (event: ProgressEvent) => {\n\t\t\t\t\t\ttreatErrors(\n\t\t\t\t\t\t\t() => {\n\t\t\t\t\t\t\t\tonProgress({ ...event, agent });\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tclassifyHostFailure,\n\t\t\t\t\t\t\t'Orchestrator progress callback failed',\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\t};\n\t}\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Session } from '#src/shared/domain/session';
|
|
1
2
|
import type { MoveOptions, RecoveryStore, RunStatus } from './recoveryStore.ts';
|
|
2
3
|
/** One run that wrote to the workspace. */
|
|
3
4
|
export interface HistoryRun {
|
|
@@ -17,6 +18,12 @@ export interface HistoryRun {
|
|
|
17
18
|
* real path of a file outside it. A host decides whether to show them.
|
|
18
19
|
*/
|
|
19
20
|
files: string[];
|
|
21
|
+
/**
|
|
22
|
+
* The session it came from, so a host can warn before undoing another session's run. Absent
|
|
23
|
+
* for a run recorded before runs recorded one. A run retention chained into this one keeps
|
|
24
|
+
* no session of its own.
|
|
25
|
+
*/
|
|
26
|
+
session?: Session;
|
|
20
27
|
}
|
|
21
28
|
/** One move of the workspace through its history. */
|
|
22
29
|
export interface Move {
|
|
@@ -69,7 +76,7 @@ export interface History {
|
|
|
69
76
|
* host that showed the user what a move will do passes where the workspace was, so a run that
|
|
70
77
|
* ended meanwhile cannot make it do something else.
|
|
71
78
|
*
|
|
72
|
-
* @throws {WorkspaceBusyError} when a run
|
|
79
|
+
* @throws {WorkspaceBusyError} when a run, another move or the removal of its worktree holds the workspace.
|
|
73
80
|
* @throws {WorkspaceMovedError} when the workspace is not at `from`.
|
|
74
81
|
* @throws {UnknownRunError} when `target` is not in the history.
|
|
75
82
|
* @throws {HistoryExpiredError} when retention chained `target` into a later run.
|
|
@@ -79,7 +86,7 @@ export interface History {
|
|
|
79
86
|
* Moves the workspace to the run before the one it is at.
|
|
80
87
|
*
|
|
81
88
|
* @throws {NothingToMoveError} at the start of the history.
|
|
82
|
-
* @throws {WorkspaceBusyError} when a run
|
|
89
|
+
* @throws {WorkspaceBusyError} when a run, another move or the removal of its worktree holds the workspace.
|
|
83
90
|
* @throws {WorkspaceMovedError} when the workspace is not at `from`.
|
|
84
91
|
*/
|
|
85
92
|
undo(options?: MoveOptions): Promise<Move>;
|
|
@@ -88,7 +95,7 @@ export interface History {
|
|
|
88
95
|
* branch.
|
|
89
96
|
*
|
|
90
97
|
* @throws {NothingToMoveError} when no run follows the one it is at.
|
|
91
|
-
* @throws {WorkspaceBusyError} when a run
|
|
98
|
+
* @throws {WorkspaceBusyError} when a run, another move or the removal of its worktree holds the workspace.
|
|
92
99
|
* @throws {WorkspaceMovedError} when the workspace is not at `from`.
|
|
93
100
|
*/
|
|
94
101
|
redo(options?: MoveOptions): Promise<Move>;
|
|
@@ -33,6 +33,7 @@ export class StoreHistory {
|
|
|
33
33
|
startedAt: record.startedAt,
|
|
34
34
|
...(record.finishedAt ? { finishedAt: record.finishedAt } : {}),
|
|
35
35
|
files: [...new Set(files)].map(path => displayPath(record.root, path)),
|
|
36
|
+
...(record.session ? { session: { ...record.session } } : {}),
|
|
36
37
|
});
|
|
37
38
|
}
|
|
38
39
|
return { ...(head ? { head } : {}), runs: listed };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"history.js","sourceRoot":"","sources":["../../../src/recovery/domain/history.ts"],"names":[],"mappings":"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","sourcesContent":["import { changedFiles, displayPath, showChanges } from './runChanges.ts';\nimport type { MoveOptions, RecoveryStore, Revision, RunStatus } from './recoveryStore.ts';\nimport { HistoryExpiredError } from './runTree.ts';\n\n/** One run that wrote to the workspace. */\nexport interface HistoryRun {\n\trunId: string;\n\t/** The run the workspace was at when this one started; absent when it started at the start. */\n\tparentRunId?: string;\n\t/**\n\t * Runs whose changes retention chained into this one, oldest first: going back past this run\n\t * goes back past them too. An id an earlier `AgentResponse` gave may be found here.\n\t */\n\tabsorbed?: string[];\n\tstatus: RunStatus;\n\tstartedAt: string;\n\tfinishedAt?: string;\n\t/**\n\t * The files it changed, in the order it first touched each one: relative to the root, or the\n\t * real path of a file outside it. A host decides whether to show them.\n\t */\n\tfiles: string[];\n}\n\n/** One move of the workspace through its history. */\nexport interface Move {\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\treason?: string;\n\t/** Every file reached the state the history expected. Otherwise `conflicts` names those left. */\n\tcomplete: boolean;\n\t/**\n\t * The files left as they were, and the run whose change could not be undone or redone: one\n\t * that did not hold what the history expected, such as one the user edited since, or one a\n\t * command changed into, or from, something the history cannot copy, such as a file over 64 MB.\n\t */\n\tconflicts: { path: string; runId: string }[];\n}\n\n/**\n * The history of the runs that wrote to one workspace: what each run changed, and moving the\n * workspace back and forward through them. Runs form a tree: going back and writing again\n * starts a new branch, and the earlier one stays reachable with `goTo`.\n */\nexport interface History {\n\t/**\n\t * Every run kept, oldest first, and the run the workspace is at (`head`), absent at the start\n\t * of the history. A run whose process died while writing is `interrupted`.\n\t */\n\tlist(): Promise<{ head?: string; runs: HistoryRun[] }>;\n\t/**\n\t * What a run changed, as a git-style diff per file. What does not fit in `maxLines`, 1000 by\n\t * default, is named instead.\n\t *\n\t * @throws {UnknownRunError} when `runId` was never a run of this workspace.\n\t * @throws {HistoryExpiredError} when retention removed it or chained it into a later run.\n\t */\n\tchanges(runId: string, options?: { maxLines?: number }): Promise<string>;\n\t/**\n\t * Moves the workspace to the state right after `target`, or to how it was before any run with\n\t * `historyStart`. A file that does not hold what the history expects is left as it is, and the\n\t * move is then not `complete`. With `from`, it moves only if the workspace is still there: a\n\t * host that showed the user what a move will do passes where the workspace was, so a run that\n\t * ended meanwhile cannot make it do something else.\n\t *\n\t * @throws {WorkspaceBusyError} when a run or another move holds the workspace.\n\t * @throws {WorkspaceMovedError} when the workspace is not at `from`.\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<Move>;\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 * @throws {WorkspaceBusyError} when a run or another move holds the workspace.\n\t * @throws {WorkspaceMovedError} when the workspace is not at `from`.\n\t */\n\tundo(options?: MoveOptions): Promise<Move>;\n\t/**\n\t * Moves the workspace to the next run: towards where it was most recently, or else the newest\n\t * branch.\n\t *\n\t * @throws {NothingToMoveError} when no run follows the one it is at.\n\t * @throws {WorkspaceBusyError} when a run or another move holds the workspace.\n\t * @throws {WorkspaceMovedError} when the workspace is not at `from`.\n\t */\n\tredo(options?: MoveOptions): Promise<Move>;\n}\n\n/** `History` over a recovery store, with paths shown relative to the workspace's real root. */\nexport class StoreHistory implements History {\n\tprivate readonly store: RecoveryStore;\n\tprivate readonly root: string;\n\n\tconstructor({ store, root }: { store: RecoveryStore; root: string }) {\n\t\tthis.store = store;\n\t\tthis.root = root;\n\t}\n\n\tasync list(): Promise<{ head?: string; runs: HistoryRun[] }> {\n\t\tconst { head, runs } = await this.store.listRuns();\n\t\tconst listed: HistoryRun[] = [];\n\t\tfor (const run of runs) {\n\t\t\tconst read = await this.store.readRun(run.runId).catch((error: unknown) => {\n\t\t\t\t// Retention took it since it was listed: it is no longer in the history.\n\t\t\t\tif (error instanceof HistoryExpiredError) return undefined;\n\t\t\t\tthrow error;\n\t\t\t});\n\t\t\tif (!read) continue;\n\t\t\tconst { record, entries, unrecorded } = read;\n\t\t\tconst files = [\n\t\t\t\t...changedFiles(record, entries).map(change => change.path),\n\t\t\t\t...unrecorded.map(change => change.path),\n\t\t\t];\n\t\t\tlisted.push({\n\t\t\t\trunId: record.runId,\n\t\t\t\t...(record.parentRunId ? { parentRunId: record.parentRunId } : {}),\n\t\t\t\t...(record.absorbed ? { absorbed: [...record.absorbed] } : {}),\n\t\t\t\tstatus: record.status,\n\t\t\t\tstartedAt: record.startedAt,\n\t\t\t\t...(record.finishedAt ? { finishedAt: record.finishedAt } : {}),\n\t\t\t\tfiles: [...new Set(files)].map(path => displayPath(record.root, path)),\n\t\t\t});\n\t\t}\n\t\treturn { ...(head ? { head } : {}), runs: listed };\n\t}\n\n\tchanges(runId: string, options: { maxLines?: number } = {}): Promise<string> {\n\t\treturn showChanges(this.store, runId, options);\n\t}\n\n\tasync goTo(target: string, options: MoveOptions = {}): Promise<Move> {\n\t\treturn this.moveOf(await this.store.goTo(target, options));\n\t}\n\n\tasync undo(options: MoveOptions = {}): Promise<Move> {\n\t\treturn this.moveOf(await this.store.undo(options));\n\t}\n\n\tasync redo(options: MoveOptions = {}): Promise<Move> {\n\t\treturn this.moveOf(await this.store.redo(options));\n\t}\n\n\tprivate moveOf(revision: Revision): Move {\n\t\treturn {\n\t\t\t...(revision.from ? { from: revision.from } : {}),\n\t\t\t...(revision.to ? { to: revision.to } : {}),\n\t\t\tat: revision.at,\n\t\t\t...(revision.reason ? { reason: revision.reason } : {}),\n\t\t\tcomplete: revision.complete,\n\t\t\tconflicts: revision.conflicts.map(({ path, runId }) => ({\n\t\t\t\tpath: displayPath(this.root, path),\n\t\t\t\trunId,\n\t\t\t})),\n\t\t};\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"history.js","sourceRoot":"","sources":["../../../src/recovery/domain/history.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Session } from '#src/shared/domain/session';\nimport { changedFiles, displayPath, showChanges } from './runChanges.ts';\nimport type { MoveOptions, RecoveryStore, Revision, RunStatus } from './recoveryStore.ts';\nimport { HistoryExpiredError } from './runTree.ts';\n\n/** One run that wrote to the workspace. */\nexport interface HistoryRun {\n\trunId: string;\n\t/** The run the workspace was at when this one started; absent when it started at the start. */\n\tparentRunId?: string;\n\t/**\n\t * Runs whose changes retention chained into this one, oldest first: going back past this run\n\t * goes back past them too. An id an earlier `AgentResponse` gave may be found here.\n\t */\n\tabsorbed?: string[];\n\tstatus: RunStatus;\n\tstartedAt: string;\n\tfinishedAt?: string;\n\t/**\n\t * The files it changed, in the order it first touched each one: relative to the root, or the\n\t * real path of a file outside it. A host decides whether to show them.\n\t */\n\tfiles: string[];\n\t/**\n\t * The session it came from, so a host can warn before undoing another session's run. Absent\n\t * for a run recorded before runs recorded one. A run retention chained into this one keeps\n\t * no session of its own.\n\t */\n\tsession?: Session;\n}\n\n/** One move of the workspace through its history. */\nexport interface Move {\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\treason?: string;\n\t/** Every file reached the state the history expected. Otherwise `conflicts` names those left. */\n\tcomplete: boolean;\n\t/**\n\t * The files left as they were, and the run whose change could not be undone or redone: one\n\t * that did not hold what the history expected, such as one the user edited since, or one a\n\t * command changed into, or from, something the history cannot copy, such as a file over 64 MB.\n\t */\n\tconflicts: { path: string; runId: string }[];\n}\n\n/**\n * The history of the runs that wrote to one workspace: what each run changed, and moving the\n * workspace back and forward through them. Runs form a tree: going back and writing again\n * starts a new branch, and the earlier one stays reachable with `goTo`.\n */\nexport interface History {\n\t/**\n\t * Every run kept, oldest first, and the run the workspace is at (`head`), absent at the start\n\t * of the history. A run whose process died while writing is `interrupted`.\n\t */\n\tlist(): Promise<{ head?: string; runs: HistoryRun[] }>;\n\t/**\n\t * What a run changed, as a git-style diff per file. What does not fit in `maxLines`, 1000 by\n\t * default, is named instead.\n\t *\n\t * @throws {UnknownRunError} when `runId` was never a run of this workspace.\n\t * @throws {HistoryExpiredError} when retention removed it or chained it into a later run.\n\t */\n\tchanges(runId: string, options?: { maxLines?: number }): Promise<string>;\n\t/**\n\t * Moves the workspace to the state right after `target`, or to how it was before any run with\n\t * `historyStart`. A file that does not hold what the history expects is left as it is, and the\n\t * move is then not `complete`. With `from`, it moves only if the workspace is still there: a\n\t * host that showed the user what a move will do passes where the workspace was, so a run that\n\t * ended meanwhile cannot make it do something else.\n\t *\n\t * @throws {WorkspaceBusyError} when a run, another move or the removal of its worktree holds the workspace.\n\t * @throws {WorkspaceMovedError} when the workspace is not at `from`.\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<Move>;\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 * @throws {WorkspaceBusyError} when a run, another move or the removal of its worktree holds the workspace.\n\t * @throws {WorkspaceMovedError} when the workspace is not at `from`.\n\t */\n\tundo(options?: MoveOptions): Promise<Move>;\n\t/**\n\t * Moves the workspace to the next run: towards where it was most recently, or else the newest\n\t * branch.\n\t *\n\t * @throws {NothingToMoveError} when no run follows the one it is at.\n\t * @throws {WorkspaceBusyError} when a run, another move or the removal of its worktree holds the workspace.\n\t * @throws {WorkspaceMovedError} when the workspace is not at `from`.\n\t */\n\tredo(options?: MoveOptions): Promise<Move>;\n}\n\n/** `History` over a recovery store, with paths shown relative to the workspace's real root. */\nexport class StoreHistory implements History {\n\tprivate readonly store: RecoveryStore;\n\tprivate readonly root: string;\n\n\tconstructor({ store, root }: { store: RecoveryStore; root: string }) {\n\t\tthis.store = store;\n\t\tthis.root = root;\n\t}\n\n\tasync list(): Promise<{ head?: string; runs: HistoryRun[] }> {\n\t\tconst { head, runs } = await this.store.listRuns();\n\t\tconst listed: HistoryRun[] = [];\n\t\tfor (const run of runs) {\n\t\t\tconst read = await this.store.readRun(run.runId).catch((error: unknown) => {\n\t\t\t\t// Retention took it since it was listed: it is no longer in the history.\n\t\t\t\tif (error instanceof HistoryExpiredError) return undefined;\n\t\t\t\tthrow error;\n\t\t\t});\n\t\t\tif (!read) continue;\n\t\t\tconst { record, entries, unrecorded } = read;\n\t\t\tconst files = [\n\t\t\t\t...changedFiles(record, entries).map(change => change.path),\n\t\t\t\t...unrecorded.map(change => change.path),\n\t\t\t];\n\t\t\tlisted.push({\n\t\t\t\trunId: record.runId,\n\t\t\t\t...(record.parentRunId ? { parentRunId: record.parentRunId } : {}),\n\t\t\t\t...(record.absorbed ? { absorbed: [...record.absorbed] } : {}),\n\t\t\t\tstatus: record.status,\n\t\t\t\tstartedAt: record.startedAt,\n\t\t\t\t...(record.finishedAt ? { finishedAt: record.finishedAt } : {}),\n\t\t\t\tfiles: [...new Set(files)].map(path => displayPath(record.root, path)),\n\t\t\t\t...(record.session ? { session: { ...record.session } } : {}),\n\t\t\t});\n\t\t}\n\t\treturn { ...(head ? { head } : {}), runs: listed };\n\t}\n\n\tchanges(runId: string, options: { maxLines?: number } = {}): Promise<string> {\n\t\treturn showChanges(this.store, runId, options);\n\t}\n\n\tasync goTo(target: string, options: MoveOptions = {}): Promise<Move> {\n\t\treturn this.moveOf(await this.store.goTo(target, options));\n\t}\n\n\tasync undo(options: MoveOptions = {}): Promise<Move> {\n\t\treturn this.moveOf(await this.store.undo(options));\n\t}\n\n\tasync redo(options: MoveOptions = {}): Promise<Move> {\n\t\treturn this.moveOf(await this.store.redo(options));\n\t}\n\n\tprivate moveOf(revision: Revision): Move {\n\t\treturn {\n\t\t\t...(revision.from ? { from: revision.from } : {}),\n\t\t\t...(revision.to ? { to: revision.to } : {}),\n\t\t\tat: revision.at,\n\t\t\t...(revision.reason ? { reason: revision.reason } : {}),\n\t\t\tcomplete: revision.complete,\n\t\t\tconflicts: revision.conflicts.map(({ path, runId }) => ({\n\t\t\t\tpath: displayPath(this.root, path),\n\t\t\t\trunId,\n\t\t\t})),\n\t\t};\n\t}\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Session } from '#src/shared/domain/session';
|
|
1
2
|
/**
|
|
2
3
|
* What one path held at one moment: nothing, or a regular file with this content and mode.
|
|
3
4
|
* `hash` names the content in the store, so the bytes themselves never travel in a record.
|
|
@@ -95,6 +96,8 @@ export interface RunRecord {
|
|
|
95
96
|
unsettled?: true;
|
|
96
97
|
/** The process that ran it, so a run whose process died can be told from a live one. */
|
|
97
98
|
pid: number;
|
|
99
|
+
/** The session it came from. Absent for a run recorded before runs recorded one. */
|
|
100
|
+
session?: Session;
|
|
98
101
|
}
|
|
99
102
|
/**
|
|
100
103
|
* The recovery record of one run that writes. Every method returns only once what it records
|
|
@@ -209,14 +212,23 @@ export interface RecoveryStore {
|
|
|
209
212
|
* same one.
|
|
210
213
|
*/
|
|
211
214
|
readonly workspaceId: string;
|
|
215
|
+
/**
|
|
216
|
+
* What its runs may write in, as real paths: its root and the other folders it was opened
|
|
217
|
+
* with. A run claims them while it holds the workspace, so no worktree they lie in is removed
|
|
218
|
+
* meanwhile.
|
|
219
|
+
*/
|
|
220
|
+
readonly writeFolders: readonly string[];
|
|
212
221
|
/**
|
|
213
222
|
* Starts recording a run and holds the workspace for it until `finish`. The new run's parent
|
|
214
223
|
* is the run the workspace is at, and the workspace is at the new run from then on. Runs a
|
|
215
224
|
* dead process left `running` are settled first.
|
|
216
225
|
*
|
|
217
|
-
* @throws {WorkspaceBusyError} when another live run is writing to the workspace
|
|
226
|
+
* @throws {WorkspaceBusyError} when another live run is writing to the workspace, or a
|
|
227
|
+
* worktree it may write in is being removed.
|
|
218
228
|
*/
|
|
219
|
-
startRun(
|
|
229
|
+
startRun(options?: {
|
|
230
|
+
session?: Session;
|
|
231
|
+
}): Promise<RunJournal>;
|
|
220
232
|
/**
|
|
221
233
|
* Every run, oldest first, and the run the workspace is at (`head`), absent before the first.
|
|
222
234
|
* A run still recorded as `running` whose process is gone is listed as `interrupted`.
|
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
1
|
+
{"version":3,"file":"recoveryStore.js","sourceRoot":"","sources":["../../../src/recovery/domain/recoveryStore.ts"],"names":[],"mappings":"AAoKA,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;AA4ED,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":["import type { Session } from '#src/shared/domain/session';\n\n/**\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\t/** The session it came from. Absent for a run recorded before runs recorded one. */\n\tsession?: Session;\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 * What its runs may write in, as real paths: its root and the other folders it was opened\n\t * with. A run claims them while it holds the workspace, so no worktree they lie in is removed\n\t * meanwhile.\n\t */\n\treadonly writeFolders: readonly 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, or a\n\t * worktree it may write in is being removed.\n\t */\n\tstartRun(options?: { session?: Session }): 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"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Session } from '#src/shared/domain/session';
|
|
1
2
|
import type { ILogger } from '#src/shared/domain/logger';
|
|
2
3
|
import { type JournalEntry, type MoveOptions, type Revision, type RecoveryStore, type RunJournal, type RunRecord, type UnrecordedChange } from '../domain/recoveryStore.ts';
|
|
3
4
|
/** Where an application keeps state that is neither configuration nor cache, per platform. */
|
|
@@ -23,12 +24,15 @@ export declare function defaultStateDirectory(env?: NodeJS.ProcessEnv, platform?
|
|
|
23
24
|
*/
|
|
24
25
|
export declare class FileRecoveryStore implements RecoveryStore {
|
|
25
26
|
readonly workspaceId: string;
|
|
27
|
+
readonly writeFolders: readonly string[];
|
|
26
28
|
private readonly root;
|
|
27
29
|
private readonly directory;
|
|
28
30
|
private readonly keepRuns;
|
|
29
31
|
private readonly logger;
|
|
32
|
+
private readonly claims;
|
|
33
|
+
private readonly releases;
|
|
30
34
|
private constructor();
|
|
31
|
-
static open({ root, directory, keepRuns, logger, }: {
|
|
35
|
+
static open({ root, directory, keepRuns, logger, writeRoots, }: {
|
|
32
36
|
root: string;
|
|
33
37
|
/** Defaults to the platform's state directory. */
|
|
34
38
|
directory?: string;
|
|
@@ -40,8 +44,15 @@ export declare class FileRecoveryStore implements RecoveryStore {
|
|
|
40
44
|
keepRuns?: number;
|
|
41
45
|
/** Where it reports a failure it absorbs. Defaults to standard error. */
|
|
42
46
|
logger?: ILogger;
|
|
47
|
+
/**
|
|
48
|
+
* The folders besides `root` that its runs may write in, such as the other `write` roots,
|
|
49
|
+
* so no worktree they lie in is removed while a run writes there.
|
|
50
|
+
*/
|
|
51
|
+
writeRoots?: readonly string[];
|
|
43
52
|
}): Promise<FileRecoveryStore>;
|
|
44
|
-
startRun(
|
|
53
|
+
startRun({ session }?: {
|
|
54
|
+
session?: Session;
|
|
55
|
+
}): Promise<RunJournal>;
|
|
45
56
|
listRuns(): Promise<{
|
|
46
57
|
head?: string;
|
|
47
58
|
runs: RunRecord[];
|
|
@@ -69,6 +80,14 @@ export declare class FileRecoveryStore implements RecoveryStore {
|
|
|
69
80
|
* most the step in progress, and that step is safe to take again.
|
|
70
81
|
*/
|
|
71
82
|
private takeSteps;
|
|
83
|
+
/**
|
|
84
|
+
* Claims each file the runs on a route changed outside the folders this store claims: a run
|
|
85
|
+
* may have written in a folder this store was not opened with, such as a second root a history
|
|
86
|
+
* reopened by its first root alone does not know. A worktree holding one is not removed while
|
|
87
|
+
* the move writes there.
|
|
88
|
+
*/
|
|
89
|
+
private claimFiles;
|
|
90
|
+
private stepAlong;
|
|
72
91
|
/** Takes one run's changes back to its parent, or forward again, file by file. */
|
|
73
92
|
private step;
|
|
74
93
|
/**
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
2
|
import { link, mkdir, open, readdir, readFile, realpath, rename, rm, stat, truncate, unlink, } from 'node:fs/promises';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { dirname, isAbsolute, join } from 'node:path';
|
|
4
|
+
import { dirname, isAbsolute, join, sep } from 'node:path';
|
|
5
|
+
import { FolderClaims } from "./folderClaims.js";
|
|
5
6
|
import { classifyHostFailure, describeFailure, treatErrors, } from '#src/shared/domain/providerFailure';
|
|
6
7
|
import { Logger } from '#src/shared/infrastructure/logger';
|
|
7
8
|
import { fileChanges, sameState } from "../domain/fileChanges.js";
|
|
8
9
|
import { historyStart, NothingToMoveError, WorkspaceBusyError, WorkspaceMovedError, } from "../domain/recoveryStore.js";
|
|
9
10
|
import { chainEntries, nextPruneStep } from "../domain/retention.js";
|
|
10
|
-
import { HistoryExpiredError, redoTarget, route, UnknownRunError } from "../domain/runTree.js";
|
|
11
|
+
import { HistoryExpiredError, redoTarget, route, UnknownRunError, } from "../domain/runTree.js";
|
|
11
12
|
import { currentState, hasRealParents, removeEmptyFolders, removeFile, removeTemporary, temporaryPath, writeFileState, } from "./workspaceFiles.js";
|
|
12
13
|
const appName = 'mikode-harness';
|
|
13
14
|
// The store holds source code the agent touched: only its owner may read it.
|
|
@@ -149,18 +150,24 @@ async function storeContent(contentDirectory, content) {
|
|
|
149
150
|
*/
|
|
150
151
|
export class FileRecoveryStore {
|
|
151
152
|
workspaceId;
|
|
153
|
+
writeFolders;
|
|
152
154
|
root;
|
|
153
155
|
directory;
|
|
154
156
|
keepRuns;
|
|
155
157
|
logger;
|
|
156
|
-
|
|
158
|
+
claims;
|
|
159
|
+
// What releases the claim of each run or move holding the workspace.
|
|
160
|
+
releases = new Map();
|
|
161
|
+
constructor(root, directory, keepRuns, logger, workspaceId, { claims, folders }) {
|
|
157
162
|
this.root = root;
|
|
158
163
|
this.directory = directory;
|
|
159
164
|
this.keepRuns = keepRuns;
|
|
160
165
|
this.logger = logger;
|
|
161
166
|
this.workspaceId = workspaceId;
|
|
167
|
+
this.claims = claims;
|
|
168
|
+
this.writeFolders = folders;
|
|
162
169
|
}
|
|
163
|
-
static async open({ root, directory = defaultStateDirectory(), keepRuns = defaultKeepRuns, logger = new Logger(), }) {
|
|
170
|
+
static async open({ root, directory = defaultStateDirectory(), keepRuns = defaultKeepRuns, logger = new Logger(), writeRoots = [], }) {
|
|
164
171
|
if (!Number.isInteger(keepRuns) || keepRuns < 3) {
|
|
165
172
|
throw new RangeError(`keepRuns must be an integer of at least 3; got ${String(keepRuns)}`);
|
|
166
173
|
}
|
|
@@ -170,10 +177,17 @@ export class FileRecoveryStore {
|
|
|
170
177
|
await makeDirectory(join(workspace, 'runs'));
|
|
171
178
|
await makeDirectory(join(workspace, 'content'));
|
|
172
179
|
await writeDurably(join(workspace, 'workspace.json'), JSON.stringify({ root: realRoot }));
|
|
180
|
+
const folders = [
|
|
181
|
+
realRoot,
|
|
182
|
+
...(await Promise.all(writeRoots.map(path => realpath(path).catch(() => path)))),
|
|
183
|
+
];
|
|
173
184
|
// Real, so a state directory reached through a link is the history it holds.
|
|
174
|
-
return new FileRecoveryStore(realRoot, workspace, keepRuns, logger, await realpath(workspace)
|
|
185
|
+
return new FileRecoveryStore(realRoot, workspace, keepRuns, logger, await realpath(workspace), {
|
|
186
|
+
claims: new FolderClaims(await realpath(directory)),
|
|
187
|
+
folders,
|
|
188
|
+
});
|
|
175
189
|
}
|
|
176
|
-
async startRun() {
|
|
190
|
+
async startRun({ session } = {}) {
|
|
177
191
|
const runId = newRunId();
|
|
178
192
|
try {
|
|
179
193
|
// Inside, because taking the lock can fail after publishing it. Unlocking removes only a
|
|
@@ -196,6 +210,7 @@ export class FileRecoveryStore {
|
|
|
196
210
|
startedAt: new Date().toISOString(),
|
|
197
211
|
status: 'running',
|
|
198
212
|
pid: process.pid,
|
|
213
|
+
...(session ? { session } : {}),
|
|
199
214
|
};
|
|
200
215
|
await writeDurably(join(runDirectory, 'run.json'), JSON.stringify(record));
|
|
201
216
|
// After the record, so the head never names a run that does not exist.
|
|
@@ -399,6 +414,34 @@ export class FileRecoveryStore {
|
|
|
399
414
|
*/
|
|
400
415
|
async takeSteps(head, runs, pending) {
|
|
401
416
|
const { undo, redo } = route(runs, head.runId, pending.to);
|
|
417
|
+
const release = await this.claimFiles([...undo, ...redo]);
|
|
418
|
+
try {
|
|
419
|
+
return await this.stepAlong(head, runs, pending, { undo, redo });
|
|
420
|
+
}
|
|
421
|
+
finally {
|
|
422
|
+
await release();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Claims each file the runs on a route changed outside the folders this store claims: a run
|
|
427
|
+
* may have written in a folder this store was not opened with, such as a second root a history
|
|
428
|
+
* reopened by its first root alone does not know. A worktree holding one is not removed while
|
|
429
|
+
* the move writes there.
|
|
430
|
+
*/
|
|
431
|
+
async claimFiles(runIds) {
|
|
432
|
+
const outside = new Set();
|
|
433
|
+
for (const runId of runIds) {
|
|
434
|
+
const { entries } = await this.readRun(runId);
|
|
435
|
+
for (const { path } of fileChanges(entries, true)) {
|
|
436
|
+
if (!this.writeFolders.some(folder => within(path, folder)))
|
|
437
|
+
outside.add(path);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (outside.size === 0)
|
|
441
|
+
return () => Promise.resolve();
|
|
442
|
+
return this.claims.claimWriting(`files-${newRunId()}`, [...outside]);
|
|
443
|
+
}
|
|
444
|
+
async stepAlong(head, runs, pending, { undo, redo }) {
|
|
402
445
|
const parents = new Map(runs.map(run => [run.runId, run.parentRunId]));
|
|
403
446
|
let reached = head.runId;
|
|
404
447
|
const save = () => this.writeHead({ ...(reached ? { runId: reached } : {}), revision: head.revision, pending });
|
|
@@ -931,6 +974,9 @@ export class FileRecoveryStore {
|
|
|
931
974
|
// Written whole before it appears, so a lock that exists always names its holder.
|
|
932
975
|
await writeDurably(path, JSON.stringify({ runId, pid: process.pid }), { exclusive: true });
|
|
933
976
|
heldHere.add(runId);
|
|
977
|
+
// Under the lock: a removal of a worktree this run may write in keeps it out. The
|
|
978
|
+
// caller unlocks when this throws.
|
|
979
|
+
this.releases.set(runId, await this.claims.claimWriting(runId, this.writeFolders));
|
|
934
980
|
return;
|
|
935
981
|
}
|
|
936
982
|
catch (error) {
|
|
@@ -958,6 +1004,9 @@ export class FileRecoveryStore {
|
|
|
958
1004
|
async unlock(runId) {
|
|
959
1005
|
// First: the run has ended even if removing its lock fails, so that lock is stale.
|
|
960
1006
|
heldHere.delete(runId);
|
|
1007
|
+
const release = this.releases.get(runId);
|
|
1008
|
+
this.releases.delete(runId);
|
|
1009
|
+
await release?.();
|
|
961
1010
|
const path = join(this.directory, 'lock');
|
|
962
1011
|
// Only its own lock: one cleared as stale and taken by another run is not this run's.
|
|
963
1012
|
if ((await readLock(path))?.runId === runId)
|
|
@@ -1049,6 +1098,9 @@ async function readLock(path) {
|
|
|
1049
1098
|
}
|
|
1050
1099
|
return { runId: 'unknown' };
|
|
1051
1100
|
}
|
|
1101
|
+
function within(path, folder) {
|
|
1102
|
+
return path === folder || path.startsWith(`${folder}${sep}`);
|
|
1103
|
+
}
|
|
1052
1104
|
function contentPath(contentDirectory, hash) {
|
|
1053
1105
|
// Two levels, so no folder holds every file a workspace ever stored.
|
|
1054
1106
|
return join(contentDirectory, hash.slice(0, 2), hash);
|