@cassiomc1/forgeloop 2.0.0-rc.2 → 2.0.0-rc.4
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 +5 -4
- package/docs/AGENT_PROTOCOL_SUMMARY.md +1 -1
- package/docs/RELEASE_CHECKLIST.md +31 -2
- package/docs/TROUBLESHOOTING.md +33 -0
- package/package.json +1 -1
- package/src/core/completion-artifacts.js +14 -7
- package/src/repository-index/binary-manager.js +8 -2
- package/src/storage/migration-validation.js +3 -3
- package/src/storage/project-replacement.js +3 -3
- package/src/storage/project-restore.js +3 -3
- package/src/storage/restore-execution-provenance.js +26 -0
- package/src/storage/restore-replacement-archive.js +1 -1
- package/src/storage/restore-replacement.js +1 -1
- package/src/storage/unit-of-work.js +6 -2
package/README.md
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
[](https://github.com/cassiomc1/forgeloop/actions/workflows/package-smoke.yml)
|
|
12
12
|
[](https://github.com/cassiomc1/forgeloop/actions/workflows/release-notes.yml)
|
|
13
13
|
|
|
14
|
-
ForgeLoop
|
|
14
|
+
ForgeLoop governs AI-assisted engineering. Jev supplies bounded semantic input; the host model implements changes. ForgeLoop owns lifecycle, claims, gates, evidence, completion, recovery, and publication truth.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Sources: [`DOCS_INDEX.md`](./DOCS_INDEX.md); canonical [`LOOP_ENGINEERING.md`](./LOOP_ENGINEERING.md); discovery [`PROTOCOL_INTEGRATION.md`](./PROTOCOL_INTEGRATION.md); project facts [`PROJECT_PROFILE.md`](./PROJECT_PROFILE.md); guides [`GUIDE_ROUTER.md`](./GUIDE_ROUTER.md).
|
|
17
17
|
|
|
18
18
|
## Where should I start?
|
|
19
19
|
|
|
@@ -33,7 +33,6 @@ Operational sources: [`DOCS_INDEX.md`](./DOCS_INDEX.md); [`LOOP_ENGINEERING.md`]
|
|
|
33
33
|
- **Troubleshooting & error codes** → [`docs/TROUBLESHOOTING.md`](./docs/TROUBLESHOOTING.md)
|
|
34
34
|
- **Code attestation & revision coverage** → [`docs/CODE_ATTESTATION.md`](./docs/CODE_ATTESTATION.md)
|
|
35
35
|
- **System architecture & safety** → [`LOOP_SYSTEM_DESIGN.md`](./LOOP_SYSTEM_DESIGN.md) & [`THREAT_MODEL.md`](./THREAT_MODEL.md)
|
|
36
|
-
- **Documentation index & ownership** → [`DOCS_INDEX.md`](./DOCS_INDEX.md)
|
|
37
36
|
|
|
38
37
|
## Real execution proof
|
|
39
38
|
|
|
@@ -78,7 +77,9 @@ not establish project identity. Public project evidence remains v1.
|
|
|
78
77
|
## Distribution and license
|
|
79
78
|
|
|
80
79
|
GitHub hosts source and issues. The core npm package is publicly available;
|
|
81
|
-
the MCP companion retains restricted distribution.
|
|
80
|
+
the MCP companion retains restricted distribution. The owner-selected default
|
|
81
|
+
`latest` channel targets `2.0.0-rc.2`, which remains a release candidate with
|
|
82
|
+
incomplete stable SQLite acceptance.
|
|
82
83
|
ForgeLoop-owned source and documentation are licensed under the
|
|
83
84
|
[PolyForm Noncommercial License 1.0.0](./LICENSE). Commercial use is not granted
|
|
84
85
|
by this license. Third-party notices are documented in
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
ForgeLoop is a portable protocol and support CLI for verifiable engineering workflows. It records and validates task state, contracts, routing, checks, evidence, continuity, and optional code attestations. It does not become an agent scheduler, delegation service, source-control authority, or secret manager.
|
|
8
8
|
|
|
9
9
|
Protocol version: 1
|
|
10
|
-
Package version: 2.0.0-rc.
|
|
10
|
+
Package version: 2.0.0-rc.4
|
|
11
11
|
|
|
12
12
|
## Canonical loop
|
|
13
13
|
|
|
@@ -4,11 +4,30 @@ This is the current release checklist for the PolyForm Noncommercial-licensed
|
|
|
4
4
|
`@cassiomc1/forgeloop` package. It is a preparation and verification checklist;
|
|
5
5
|
it does not authorize publication.
|
|
6
6
|
|
|
7
|
+
## Runtime corrections after RC2
|
|
8
|
+
|
|
9
|
+
RC3 and later candidates with runtime changes require fresh full repository
|
|
10
|
+
verification and the clean-user audit on Windows, macOS and Linux. The bounded
|
|
11
|
+
source-equivalence shortcut below applies only to RC1 and RC2. Stable SQLite
|
|
12
|
+
acceptance remains separate; a passing new-user audit does not change
|
|
13
|
+
`soleSQLiteWriter` or authorize a stable 2.x release. Candidates publish to
|
|
14
|
+
`next` under the existing release-channel policy.
|
|
15
|
+
|
|
16
|
+
The `Published npm new-user SQLite audit` workflow supports `candidate` and
|
|
17
|
+
`npm` package sources. Before a PR, test the packed candidate in an isolated
|
|
18
|
+
installation, home directory and project on each platform. After publication,
|
|
19
|
+
dispatch the same workflow with source `npm` and the exact new version. Require
|
|
20
|
+
every check to pass, including live Jev health, concurrent task creation,
|
|
21
|
+
SQLite integrity and a completed-task backup restored to a fresh directory.
|
|
22
|
+
Supply `FORGELOOP_NPM_AUDIT_JEV_KEY` through GitHub Actions secrets and remove
|
|
23
|
+
temporary credentials after the runs finish.
|
|
24
|
+
|
|
7
25
|
## Bounded 2.0.0-rc.2 publication
|
|
8
26
|
|
|
9
27
|
The owner approved short verification and an RC fallback on 2026-10-13. This
|
|
10
|
-
candidate is published
|
|
11
|
-
`
|
|
28
|
+
candidate is published to npm `next`. The owner subsequently authorized moving
|
|
29
|
+
`latest` to the existing RC2 through a guarded tag-only promotion. Publishing a
|
|
30
|
+
new stable version remains blocked while `soleSQLiteWriter` is false. A release candidate does not assert completed
|
|
12
31
|
migration acceptance or production readiness.
|
|
13
32
|
|
|
14
33
|
After npm rejected restricted RC1 publication with `E402`, the owner authorized
|
|
@@ -50,6 +69,16 @@ confirmation `PUBLISH PUBLIC PACKAGE`, then verify npm version, Git commit,
|
|
|
50
69
|
tarball integrity, and provenance. Stable publishing retains full checks and
|
|
51
70
|
additionally refuses incomplete sole-writer acceptance.
|
|
52
71
|
|
|
72
|
+
## Owner-authorized RC2 default channel
|
|
73
|
+
|
|
74
|
+
Dispatch `npm-publish.yml` on `main` with operation `promote-latest` and
|
|
75
|
+
confirmation `POINT LATEST TO RC2`. This moves only `latest` to the already
|
|
76
|
+
published public `2.0.0-rc.2`; it neither republishes nor modifies the RC2 tag.
|
|
77
|
+
The promotion checks the exact release commit and downloaded SHA-512, refuses
|
|
78
|
+
unexpected existing tags, and requires npm trusted-publisher `manageDistTags`
|
|
79
|
+
authority. No credentials are logged or stored. The version remains an RC;
|
|
80
|
+
stable SQLite acceptance and production readiness are still incomplete.
|
|
81
|
+
|
|
53
82
|
## ForgeLoop 2.0.0 candidate scope
|
|
54
83
|
|
|
55
84
|
The 2.0.0 candidate is not release-ready while SQLite migration plan §10 has
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# ForgeLoop Troubleshooting Guide
|
|
2
2
|
|
|
3
|
+
## Windows Repository Index ZIP extraction
|
|
4
|
+
|
|
5
|
+
When `init` reports `E_REPOSITORY_INDEX_ENGINE_EXTRACTION_FAILED` and
|
|
6
|
+
`tar: This does not look like a tar archive`, a GNU tar from Git may be
|
|
7
|
+
shadowing Windows' ZIP-capable system tool. RC3 selects
|
|
8
|
+
`%SystemRoot%\System32\tar.exe` explicitly for inspection and extraction.
|
|
9
|
+
Archive and binary checksum verification and unsafe-entry rejection still
|
|
10
|
+
apply. A missing system tool remains an explicit extraction failure; ForgeLoop
|
|
11
|
+
does not install replacement software automatically.
|
|
12
|
+
|
|
13
|
+
## Restoring completed tasks into another directory
|
|
14
|
+
|
|
15
|
+
Use an attachment-inclusive `storage-backup` and `storage-restore` with
|
|
16
|
+
externally stopped writers. Historical execution records keep their original
|
|
17
|
+
project root and exact bytes. RC3 accepts them only when they exactly match
|
|
18
|
+
the snapshot bound to the destination's restore journal and storage marker.
|
|
19
|
+
Keep `.forgeloop/storage-restores/<operation-id>/snapshot` and its journal;
|
|
20
|
+
they are retained provenance evidence, not a disposable search cache.
|
|
21
|
+
An altered record, missing snapshot or broken binding remains rejected.
|
|
22
|
+
|
|
23
|
+
Storage restore does not install project code or instruction adapters. Restore
|
|
24
|
+
the project files separately, then run `forgeloop init` in the destination to
|
|
25
|
+
install the kit before expecting the complete `doctor` report to be healthy.
|
|
26
|
+
|
|
27
|
+
## Concurrent task creation
|
|
28
|
+
|
|
29
|
+
Task creation revalidates write claims atomically. If the catalog changes while
|
|
30
|
+
a command is prepared, RC3 refreshes authoritative claim inputs with bounded
|
|
31
|
+
backoff and jitter without replaying the command or its external effects.
|
|
32
|
+
Overlapping claims still fail. Sustained contention can exhaust the retry
|
|
33
|
+
bound and return a catalog-change refusal; inspect fresh task discovery
|
|
34
|
+
before deciding whether to retry.
|
|
35
|
+
|
|
3
36
|
This guide provides symptom-first recovery procedures for common ForgeLoop protocol, state, and verification issues.
|
|
4
37
|
|
|
5
38
|
---
|
package/package.json
CHANGED
|
@@ -124,6 +124,19 @@ function commandProvenanceError(message = "Observed command evidence requires a
|
|
|
124
124
|
return artifactError("E_COMMAND_PROVENANCE_UNATTESTED", message, [ARTIFACT_PATHS.receipt]);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
async function validateExecutionProjectRoot({ target, taskId, check, execution, protocolProjectRoot, allowForeignCwd, executionArtifacts }) {
|
|
128
|
+
if (allowForeignCwd || path.resolve(protocolProjectRoot) === path.resolve(target)) return;
|
|
129
|
+
const { isRetainedRestoreExecution } = await import("../storage/restore-execution-provenance.js");
|
|
130
|
+
const restored = !executionArtifacts && await isRetainedRestoreExecution(target, execution, { taskId });
|
|
131
|
+
if (!restored) {
|
|
132
|
+
throw artifactError(
|
|
133
|
+
"E_EXECUTION_REF_INVALID",
|
|
134
|
+
"Execution artifact protocol project root does not match the current target",
|
|
135
|
+
[taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
127
140
|
/**
|
|
128
141
|
* Revalidate the ForgeLoop-owned execution artifact behind an observed command
|
|
129
142
|
* check. This is intentionally asynchronous so completion and audit can verify
|
|
@@ -194,13 +207,7 @@ export async function validateCheckExecutionProvenance(check, {
|
|
|
194
207
|
[taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
|
|
195
208
|
);
|
|
196
209
|
}
|
|
197
|
-
|
|
198
|
-
throw artifactError(
|
|
199
|
-
"E_EXECUTION_REF_INVALID",
|
|
200
|
-
"Execution artifact protocol project root does not match the current target",
|
|
201
|
-
[taskId ? taskExecutionPath(taskId, check.executionRef) : ARTIFACT_PATHS.executionDirectory],
|
|
202
|
-
);
|
|
203
|
-
}
|
|
210
|
+
await validateExecutionProjectRoot({ target, taskId, check, execution, protocolProjectRoot, allowForeignCwd, executionArtifacts });
|
|
204
211
|
if (check.status === "passed" && (execution.status !== "passed" || execution.exitCode !== 0)) {
|
|
205
212
|
throw artifactError(
|
|
206
213
|
"E_EXECUTION_REF_INVALID",
|
|
@@ -181,8 +181,14 @@ export function validateArchiveEntry(entry) {
|
|
|
181
181
|
return normalized;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
export function resolveArchiveTool(archive, { platform = process.platform, env = process.env } = {}) {
|
|
185
|
+
// Windows ships a ZIP-capable bsdtar. Git's GNU tar can shadow it on PATH.
|
|
186
|
+
if (platform === "win32") return path.win32.join(env.SystemRoot || "C:\\Windows", "System32", "tar.exe");
|
|
187
|
+
return archive === "zip" ? "unzip" : "tar";
|
|
188
|
+
}
|
|
189
|
+
|
|
184
190
|
async function listArchiveEntries(archivePath, { archive = "tar.gz", execFileImpl = nodeExecFile } = {}) {
|
|
185
|
-
const listCommand = archive
|
|
191
|
+
const listCommand = resolveArchiveTool(archive);
|
|
186
192
|
const archiveName = path.basename(archivePath);
|
|
187
193
|
const listArgs = listCommand === "unzip" ? ["-Z1", archiveName] : ["-tf", archiveName];
|
|
188
194
|
try {
|
|
@@ -195,7 +201,7 @@ async function listArchiveEntries(archivePath, { archive = "tar.gz", execFileImp
|
|
|
195
201
|
|
|
196
202
|
async function extractArchive(archivePath, extractionDirectory, { archive = "tar.gz", execFileImpl = nodeExecFile } = {}) {
|
|
197
203
|
await listArchiveEntries(archivePath, { archive, execFileImpl });
|
|
198
|
-
const extractCommand = archive
|
|
204
|
+
const extractCommand = resolveArchiveTool(archive);
|
|
199
205
|
const destination = path.relative(path.dirname(archivePath), extractionDirectory).replaceAll("\\", "/") || ".";
|
|
200
206
|
const extractArgs = extractCommand === "unzip"
|
|
201
207
|
? ["-q", path.basename(archivePath), "-d", destination]
|
|
@@ -26,7 +26,7 @@ function artifactSchema(row) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/** Validate imported payloads with their existing protocol schemas and rules. */
|
|
29
|
-
export async function validateMigrationDatabase(db, { target, packageRoot }) {
|
|
29
|
+
export async function validateMigrationDatabase(db, { target, packageRoot, archivedExecutions = false }) {
|
|
30
30
|
const schemas = new Map();
|
|
31
31
|
const validate = async (text, name, label, taskId = null) => {
|
|
32
32
|
if (!name) throw Object.assign(new Error(`Unmapped operational artifact: ${label}`), { code: "E_STORAGE_MIGRATION_UNMAPPED_SOURCE" });
|
|
@@ -55,13 +55,13 @@ export async function validateMigrationDatabase(db, { target, packageRoot }) {
|
|
|
55
55
|
validator?.(value);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
-
return withOperationalStore({ db, target }, async () => {
|
|
58
|
+
return withOperationalStore({ db, target, readOnly: true }, async () => {
|
|
59
59
|
const tasks = [];
|
|
60
60
|
for (const row of db.prepare("SELECT task_id FROM tasks ORDER BY task_id").iterate()) {
|
|
61
61
|
const state = await readWorkState(target, { taskId: row.task_id, packageRoot });
|
|
62
62
|
for (const check of state?.checks ?? []) {
|
|
63
63
|
if (check.provenance === "FORGELOOP_EXECUTED") {
|
|
64
|
-
await validateCheckExecutionProvenance(check, { target, packageRoot, taskId: row.task_id });
|
|
64
|
+
await validateCheckExecutionProvenance(check, { target, packageRoot, taskId: row.task_id, allowForeignCwd: archivedExecutions });
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
const ledger = await validateEventLedger(target, packageRoot, { taskId: row.task_id });
|
|
@@ -77,7 +77,7 @@ export async function resumeProjectReplacementPreparation(target, { operationId,
|
|
|
77
77
|
if (intent.phase === "PREPARING") {
|
|
78
78
|
await ensurePreparedRestoreSnapshot(target, current.root, intent, { journalFilename: "replacement-intent.json" });
|
|
79
79
|
await withVerifiedProjectStorageBackup(await assertSafePath(current.root, "snapshot"), async retained => {
|
|
80
|
-
await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot });
|
|
80
|
+
await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot, archivedExecutions: true });
|
|
81
81
|
await withVerifiedProjectStorageBackup(intent.source, source => {
|
|
82
82
|
if (canonicalFingerprint(source.manifest) !== intent.sourceFingerprint
|
|
83
83
|
|| canonicalFingerprint(logicalSnapshot(source.db)) !== canonicalFingerprint(logicalSnapshot(retained.db))) throw invalid("Replacement recovery snapshot differs from recorded source");
|
|
@@ -99,7 +99,7 @@ export async function resumeProjectReplacementPreparation(target, { operationId,
|
|
|
99
99
|
export async function replaceActiveProjectStorage(source, target, { writersQuiesced = false, packageRoot = getPackageRoot() } = {}) {
|
|
100
100
|
if (writersQuiesced !== true) throw Object.assign(new Error("Exclude writers and close their database connections before active replacement"), { code: "E_STORAGE_MIGRATION_QUIESCENCE_REQUIRED" });
|
|
101
101
|
return withVerifiedProjectStorageBackup(source, async ({ db, root, manifest }) => {
|
|
102
|
-
await validateMigrationDatabase(db, { target: root, packageRoot });
|
|
102
|
+
await validateMigrationDatabase(db, { target: root, packageRoot, archivedExecutions: true });
|
|
103
103
|
const relative = path.relative(path.resolve(target), root).split(path.sep).join("/");
|
|
104
104
|
if (REPLACEMENT_ACTIVE_ROOTS.some(name => relative === name || relative.startsWith(`${name}/`))) throw invalid("Replacement source cannot be archived outgoing operational state");
|
|
105
105
|
return withStorageMaintenance(target, async () => {
|
|
@@ -119,7 +119,7 @@ export async function replaceActiveProjectStorage(source, target, { writersQuies
|
|
|
119
119
|
const snapshot = await assertSafePath(operationRoot, "snapshot");
|
|
120
120
|
await backupProjectStorage(db, root, snapshot);
|
|
121
121
|
await withVerifiedProjectStorageBackup(snapshot, async retained => {
|
|
122
|
-
await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot });
|
|
122
|
+
await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot, archivedExecutions: true });
|
|
123
123
|
await withVerifiedProjectStorageBackup(root, current => {
|
|
124
124
|
if (canonicalFingerprint(current.manifest) !== intent.sourceFingerprint
|
|
125
125
|
|| canonicalFingerprint(logicalSnapshot(current.db)) !== canonicalFingerprint(logicalSnapshot(retained.db))) throw invalid("Incoming source changed during replacement preparation");
|
|
@@ -92,7 +92,7 @@ export async function restoreProjectStorageToFreshProject(source, target, { writ
|
|
|
92
92
|
if (canonicalFingerprint(current.manifest) !== canonicalFingerprint(sourceManifest)
|
|
93
93
|
|| canonicalFingerprint(logicalSnapshot(current.db)) !== canonicalFingerprint(logicalSnapshot(snapshot))) throw invalid("Source backup changed during restore preparation");
|
|
94
94
|
});
|
|
95
|
-
await validateMigrationDatabase(snapshot, { target: retained, packageRoot });
|
|
95
|
+
await validateMigrationDatabase(snapshot, { target: retained, packageRoot, archivedExecutions: true });
|
|
96
96
|
const binding = { operationId, databaseSchemaVersion: readStorageMeta(snapshot).schema_version,
|
|
97
97
|
sourceInventoryFingerprint: canonicalFingerprint({ database: manifest.databaseSha256, inventory: manifest.inventorySha256, references: manifest.references }) };
|
|
98
98
|
Object.assign(journal, { phase: "READY", binding, snapshotFingerprint: canonicalFingerprint(manifest) });
|
|
@@ -111,7 +111,7 @@ async function publishReadyRestore(target, operationRoot, journal, { packageRoot
|
|
|
111
111
|
const persist = () => writeFileAtomic(journalPath, `${JSON.stringify(journal, null, 2)}\n`);
|
|
112
112
|
const { operationId, binding } = journal;
|
|
113
113
|
return withVerifiedProjectStorageBackup(retained, async ({ db: snapshot, manifest }) => {
|
|
114
|
-
await validateMigrationDatabase(snapshot, { target: retained, packageRoot });
|
|
114
|
+
await validateMigrationDatabase(snapshot, { target: retained, packageRoot, archivedExecutions: true });
|
|
115
115
|
await assertOwnedStorageMaintenance(target);
|
|
116
116
|
await verifyProjectRestoreJournal(target, operationId, { expectedOwnerId: await assertOwnedStorageMaintenance(target) });
|
|
117
117
|
await assertRestorePublicationLayout(target, snapshot);
|
|
@@ -186,7 +186,7 @@ async function validatePreparedRestore(target, root, journal, packageRoot) {
|
|
|
186
186
|
await withVerifiedProjectStorageBackup(journal.source, async source => {
|
|
187
187
|
if (canonicalFingerprint(source.manifest) !== journal.sourceFingerprint
|
|
188
188
|
|| canonicalFingerprint(logicalSnapshot(source.db)) !== canonicalFingerprint(logicalSnapshot(snapshot))) throw invalid("Prepared restore differs from its recorded source backup");
|
|
189
|
-
await validateMigrationDatabase(snapshot, { target: snapshotPath, packageRoot });
|
|
189
|
+
await validateMigrationDatabase(snapshot, { target: snapshotPath, packageRoot, archivedExecutions: true });
|
|
190
190
|
});
|
|
191
191
|
await assertOwnedStorageMaintenance(target);
|
|
192
192
|
Object.assign(journal, { phase: "READY", snapshotFingerprint: canonicalFingerprint(manifest), binding: {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "../core/artifacts.js";
|
|
2
|
+
import { readStorageVersionMarker } from "./storage-marker.js";
|
|
3
|
+
import { verifyProjectRestoreJournal } from "./restore-journal.js";
|
|
4
|
+
import { withVerifiedProjectStorageBackup } from "./project-backup.js";
|
|
5
|
+
import { operationalContext } from "./operational-context.js";
|
|
6
|
+
|
|
7
|
+
/** Historical roots remain valid only for exact records in the bound snapshot. */
|
|
8
|
+
export async function isRetainedRestoreExecution(target, execution, { taskId } = {}) {
|
|
9
|
+
// Replacement journals also audit incoming archives. Those read-only audits
|
|
10
|
+
// must not select their snapshot as the caller's live operational project.
|
|
11
|
+
return operationalContext.run(null, () => verifyRetainedExecution(target, execution, taskId));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function verifyRetainedExecution(target, execution, taskId) {
|
|
15
|
+
const marker = await readStorageVersionMarker(target);
|
|
16
|
+
if (!marker?.operationId) return false;
|
|
17
|
+
let retained;
|
|
18
|
+
try { retained = await verifyProjectRestoreJournal(target, marker.operationId); }
|
|
19
|
+
catch (error) { if (error.code === "ENOENT") return false; throw error; }
|
|
20
|
+
if (!retained.snapshotVerified || !["PUBLISHING", "ACTIVE"].includes(retained.journal.phase)) return false;
|
|
21
|
+
return withVerifiedProjectStorageBackup(retained.snapshot, ({ db }) => {
|
|
22
|
+
const row = db.prepare("SELECT payload_json FROM executions WHERE task_id = ? AND execution_id = ?")
|
|
23
|
+
.get(taskId ?? execution.taskId, execution.executionId);
|
|
24
|
+
return Boolean(row && canonicalFingerprint(JSON.parse(row.payload_json)) === canonicalFingerprint(execution));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -22,7 +22,7 @@ const belongs = (name, root) => name === root || name.startsWith(`${root}/`);
|
|
|
22
22
|
async function incomingSnapshot(target, operationId, packageRoot) {
|
|
23
23
|
const root = await assertSafePath(target, `.forgeloop/storage-restores/${operationId}/snapshot`);
|
|
24
24
|
return withVerifiedProjectStorageBackup(root, async ({ db, manifest }) => {
|
|
25
|
-
await validateMigrationDatabase(db, { target: root, packageRoot });
|
|
25
|
+
await validateMigrationDatabase(db, { target: root, packageRoot, archivedExecutions: true });
|
|
26
26
|
return { root, fingerprint: canonicalFingerprint(manifest) };
|
|
27
27
|
});
|
|
28
28
|
}
|
|
@@ -80,7 +80,7 @@ export async function prepareActiveStorageReplacement(target, operationId, { wri
|
|
|
80
80
|
const backup = await assertSafePath(root, "backup");
|
|
81
81
|
await backupProjectStorage(db, target, backup);
|
|
82
82
|
await withVerifiedProjectStorageBackup(backup, async retained => {
|
|
83
|
-
await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot });
|
|
83
|
+
await validateMigrationDatabase(retained.db, { target: retained.root, packageRoot, archivedExecutions: true });
|
|
84
84
|
const logicalFingerprint = canonicalFingerprint(logicalSnapshot(retained.db));
|
|
85
85
|
if (logicalFingerprint !== canonicalFingerprint(logicalSnapshot(db))) throw invalid("Outgoing state changed while its retained backup was prepared");
|
|
86
86
|
Object.assign(manifest, { backupFingerprint: canonicalFingerprint(retained.manifest), logicalFingerprint });
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { publishAttachmentFile, verifyAttachmentFile } from "./attachment-files.js";
|
|
2
2
|
import { insertVerifiedAttachmentReference, validateAttachmentReference } from "./attachment-references.js";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { randomUUID, createHash } from "node:crypto";
|
|
4
|
+
import { randomUUID, createHash, randomInt } from "node:crypto";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
6
|
import { operationalContext, getOperationalStore } from "./operational-context.js";
|
|
6
7
|
import { canonicalFingerprint } from "../core/artifacts.js";
|
|
7
8
|
import { taskStorageKey } from "../core/task-identity.js";
|
|
@@ -547,7 +548,10 @@ async function commitPreparedStore(store, packageRoot) {
|
|
|
547
548
|
for (let refresh = 0; ; refresh += 1) {
|
|
548
549
|
try { store.commit(); return; }
|
|
549
550
|
catch (error) {
|
|
550
|
-
if (error.code !== "E_STORAGE_CATALOG_CHANGED" || refresh >=
|
|
551
|
+
if (error.code !== "E_STORAGE_CATALOG_CHANGED" || refresh >= 16) throw error;
|
|
552
|
+
// Spread simultaneous creators before refreshing their authoritative
|
|
553
|
+
// claim inputs. The prepared command and external effects are not replayed.
|
|
554
|
+
await delay(Math.min(5 * (2 ** refresh), 100) + randomInt(0, 20));
|
|
551
555
|
store.reads.delete("task-catalog");
|
|
552
556
|
const { discoverTasks } = await import("../core/task-discovery.js");
|
|
553
557
|
const { assertNoScopeConflicts } = await import("../core/task-scope.js");
|