Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions opencode.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
"type": "remote",
"url": "http://127.0.0.1:29173/index-mcp/streamable-http",
"enabled": true
},
"mcp-steroid": {
"type": "remote",
"url": "http://127.0.0.1:6315/mcp",
"enabled": true
}
}
}
19 changes: 14 additions & 5 deletions src/setup/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,7 @@ function receiptFailureResult(
changed: boolean,
receiptPath: string | null,
diagnostic: string,
manualActions?: string[],
): SetupResult {
return {
status: 'failed',
Expand All @@ -1417,9 +1418,9 @@ function receiptFailureResult(
target: paths.targetRoot,
steps: [{ name: 'Apply receipt-backed setup transaction', outcome: 'failed' }],
diagnostics: [diagnostic],
manual_actions: changed
manual_actions: manualActions ?? (changed
? ['Inspect the verified in-progress receipt before retrying or rolling back.']
: ['No setup target change remains; review the failed receipt before retrying.'],
: ['No setup target change remains; review the failed receipt before retrying.']),
receipt: receiptPath,
};
}
Expand Down Expand Up @@ -2027,14 +2028,22 @@ async function executeOpenCodeSetup(
receiptKeyPath,
receiptTrustedRoot,
);
const busyTarget = applied.filesystem.diagnostics.includes('filesystem-target-busy');
let failureDiagnostic = 'Setup transaction failed and did not complete.';
if (busyTarget) {
failureDiagnostic = 'OpenCode setup could not replace its managed files because OpenCode or another process keeps them busy or inaccessible.';
} else if (applied.filesystem.remainingArtifacts.length > 0) {
failureDiagnostic = 'Setup transaction failed with an unresolved temporary filesystem artifact.';
}
return receiptFailureResult(
request,
paths,
applied.filesystem.changed,
applied.initialReceiptPersisted ? receiptPaths.receiptPath : null,
applied.filesystem.remainingArtifacts.length > 0
? 'Setup transaction failed with an unresolved temporary filesystem artifact.'
: 'Setup transaction failed and did not complete.',
failureDiagnostic,
busyTarget
? ['Close every OpenCode process and retry setup; do not delete the thoth-mem plugin manually.']
: undefined,
);
}

Expand Down
13 changes: 11 additions & 2 deletions src/setup/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export async function applyAtomicFilesystemChanges(
remainingArtifacts: [],
diagnostics: [],
};
} catch {
} catch (error) {
const remainingArtifacts = await cleanupArtifacts(activeArtifacts, options);
const { restored, unrestored } = await restoreAppliedChanges(applied, options);
const cleanupIncomplete = remainingArtifacts.length > 0;
Expand All @@ -208,7 +208,7 @@ export async function applyAtomicFilesystemChanges(
? 'filesystem-artifact-cleanup-incomplete'
: unrestored.length > 0
? 'filesystem-apply-failed-restoration-incomplete'
: 'filesystem-apply-failed-restored',
: filesystemApplyDiagnostic(error),
);
}
}
Expand Down Expand Up @@ -974,6 +974,15 @@ async function cleanupArtifacts(
return [...artifacts].sort();
}

function filesystemApplyDiagnostic(error: unknown): string {
const code = error instanceof Error && 'code' in error
? (error as NodeJS.ErrnoException).code
: undefined;
return ['EPERM', 'EBUSY', 'EACCES', 'ETXTBSY'].includes(code ?? '')
? 'filesystem-target-busy'
: 'filesystem-apply-failed-restored';
}

function failedResult(
backups: FilesystemBackup[],
restored: string[],
Expand Down
26 changes: 26 additions & 0 deletions tests/setup/rollback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,32 @@ describe('write-ahead setup receipts', () => {
});
});

it('reports a privacy-safe busy-target action when OpenCode replacement is blocked', async () => {
await withTemporaryFixture(async (fixture) => {
const privateError = 'EPERM private target path C:\\Users\\secret\\OpenCode';
let faultInjected = false;
const result = await runSetup(fixture, fixture.request, {
ids: ['busy-target'],
filesystemFault: ({ point }) => {
if (point === 'atomic-rename' && !faultInjected) {
faultInjected = true;
throw Object.assign(new Error(privateError), { code: 'EPERM' });
}
},
});

expect(result).toMatchObject({ status: 'failed', changed: false });
expect(faultInjected).toBe(true);
expect(result.diagnostics).toEqual([
'OpenCode setup could not replace its managed files because OpenCode or another process keeps them busy or inaccessible.',
]);
expect(result.manual_actions).toEqual([
'Close every OpenCode process and retry setup; do not delete the thoth-mem plugin manually.',
]);
expect(JSON.stringify(result)).not.toContain(privateError);
});
});

it('uses target-bound transient OpenCode journals and removes successful rollback evidence', async () => {
await withTemporaryFixture(async (fixture) => {
const result = await runSetup(fixture, fixture.request, { ids: ['transient-journal'] });
Expand Down
Loading