jg-agent-hq 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +4 -4
- package/api/README.md +51 -8
- package/dist/cli/cli.js +35 -3
- package/dist/cli/utilities.d.ts +3 -0
- package/dist/cli/utilities.js +30 -0
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/types.d.ts +102 -0
- package/dist/contracts/types.js +24 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/plans/authority.d.ts +2 -0
- package/dist/plans/authority.js +8 -0
- package/dist/plans/documents.d.ts +17 -0
- package/dist/plans/documents.js +24 -2
- package/dist/plans/files.js +3 -1
- package/dist/plans/plans.js +3 -1
- package/dist/plans/types.d.ts +5 -0
- package/dist/providers/claude/claude.js +2 -0
- package/dist/providers/claude/history/types.d.ts +23 -0
- package/dist/providers/claude/transformers.js +14 -1
- package/dist/providers/codex/codex.js +4 -0
- package/dist/providers/codex/transformers.js +30 -3
- package/dist/providers/codex/types.d.ts +34 -1
- package/dist/providers/codex/types.js +11 -2
- package/dist/runtime/runtime.d.ts +2 -0
- package/dist/runtime/runtime.js +2 -0
- package/dist/runtime/state/index.d.ts +1 -1
- package/dist/runtime/state/index.js +1 -1
- package/dist/runtime/state/state.d.ts +16 -1
- package/dist/runtime/state/state.js +94 -3
- package/dist/snapshots/capture.d.ts +14 -0
- package/dist/snapshots/capture.js +142 -0
- package/dist/snapshots/database.d.ts +2 -0
- package/dist/snapshots/database.js +15 -51
- package/dist/snapshots/fences.d.ts +15 -0
- package/dist/snapshots/fences.js +64 -0
- package/dist/snapshots/index.d.ts +2 -0
- package/dist/snapshots/index.js +3 -0
- package/dist/snapshots/snapshots.d.ts +4 -1
- package/dist/snapshots/snapshots.js +27 -113
- package/dist/snapshots/snapshots.worker.js +5 -1
- package/dist/snapshots/types.d.ts +11 -0
- package/dist/snapshots/types.js +5 -1
- package/dist/storage/index.d.ts +2 -1
- package/dist/storage/index.js +2 -1
- package/dist/storage/schema/constants.d.ts +2 -0
- package/dist/storage/schema/constants.js +3 -0
- package/dist/storage/schema/index.d.ts +4 -0
- package/dist/storage/schema/index.js +5 -0
- package/dist/storage/schema/schema.d.ts +16 -0
- package/dist/storage/schema/schema.js +73 -0
- package/dist/storage/schema/types.d.ts +17 -0
- package/dist/storage/schema/types.js +8 -0
- package/dist/storage/schema/v1-to-v2.sql +1 -0
- package/dist/storage/schema/v1.sql +538 -0
- package/dist/storage/types.d.ts +0 -5
- package/dist/storage/types.js +0 -6
- package/dist/storage/upgrade/index.d.ts +2 -0
- package/dist/storage/upgrade/index.js +2 -0
- package/dist/storage/upgrade/types.d.ts +61 -0
- package/dist/storage/upgrade/types.js +42 -0
- package/dist/storage/upgrade/upgrade.d.ts +33 -0
- package/dist/storage/upgrade/upgrade.js +196 -0
- package/dist/storage/upgrade/upgrade.worker.d.ts +1 -0
- package/dist/storage/upgrade/upgrade.worker.js +220 -0
- package/dist/store/schema.d.ts +2 -0
- package/dist/store/schema.js +16 -25
- package/dist/store/schema.sql +2 -1
- package/dist/store/store.d.ts +2 -0
- package/dist/store/store.js +2 -0
- package/docs/recovery.md +15 -1
- package/errors/README.md +34 -28
- package/package.json +2 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ErrorKindSchema } from '../../contracts/index.js';
|
|
4
|
+
const PathSchema = z
|
|
5
|
+
.string()
|
|
6
|
+
.min(1)
|
|
7
|
+
.max(4096)
|
|
8
|
+
.refine((path) => isAbsolute(path) && resolve(path) === path);
|
|
9
|
+
const HashSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
10
|
+
// explicit offline operator inputs; stored workspace roots never grant authority
|
|
11
|
+
export const UpgradeOptionsSchema = z.strictObject({
|
|
12
|
+
stateRoot: PathSchema,
|
|
13
|
+
backupDirectory: PathSchema,
|
|
14
|
+
allowedRoots: z.array(PathSchema).min(1).max(128),
|
|
15
|
+
coordinationRoot: PathSchema.optional(),
|
|
16
|
+
});
|
|
17
|
+
// current is an idempotent observation, with no new backup or reconciliation
|
|
18
|
+
export const UpgradeResultSchema = z.object({
|
|
19
|
+
stateRoot: PathSchema,
|
|
20
|
+
state: z.enum(['upgraded', 'current']),
|
|
21
|
+
fromVersion: z.number().int().positive(),
|
|
22
|
+
toVersion: z.number().int().positive(),
|
|
23
|
+
fromSchemaHash: HashSchema,
|
|
24
|
+
toSchemaHash: HashSchema,
|
|
25
|
+
backupDirectory: PathSchema.nullable(),
|
|
26
|
+
});
|
|
27
|
+
// the parent pins the key observed before lifetime ownership was acquired
|
|
28
|
+
export const UpgradeWorkerInputSchema = UpgradeOptionsSchema.extend({ actionKeyHash: HashSchema });
|
|
29
|
+
export const UpgradeWorkerResponseSchema = z.discriminatedUnion('kind', [
|
|
30
|
+
z.object({ kind: z.literal('result'), result: UpgradeResultSchema }),
|
|
31
|
+
z.object({
|
|
32
|
+
kind: z.literal('error'),
|
|
33
|
+
code: z.string().regex(/^JG_AGENT_HQ_[A-Z_]+$/),
|
|
34
|
+
message: z.string().max(16384),
|
|
35
|
+
cause: z.instanceof(Error).optional(),
|
|
36
|
+
data: z.object({
|
|
37
|
+
kind: ErrorKindSchema,
|
|
38
|
+
phase: z.string().max(64),
|
|
39
|
+
backupDirectory: PathSchema.nullable(),
|
|
40
|
+
}),
|
|
41
|
+
}),
|
|
42
|
+
]);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type IHqStorageUpgradeOptions, type IHqStorageUpgradeResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Upgrades existing offline storage only after a fully verified local backup.
|
|
4
|
+
* Stop HQ/native children and all independent database/plan writers first. No owner is reclaimed.
|
|
5
|
+
* An uncertain error requires explicit reinspection; this operation never reverses or replays DDL.
|
|
6
|
+
* @returns The committed upgrade or an untouched current-version observation, after worker exit.
|
|
7
|
+
* @throws
|
|
8
|
+
* - JG_AGENT_HQ_REQUEST_INVALID: The HQ request is invalid
|
|
9
|
+
* - JG_AGENT_HQ_STATE_DIRECTORY_UNSAFE: The HQ state directory is unsafe
|
|
10
|
+
* - JG_AGENT_HQ_STATE_FILE_UNSAFE: The HQ state file is unsafe
|
|
11
|
+
* - JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE: The HQ action key is unavailable
|
|
12
|
+
* - JG_AGENT_HQ_RUNTIME_ALREADY_OWNED: The HQ runtime is already owned
|
|
13
|
+
* - JG_AGENT_HQ_RUNTIME_OWNER_UNVERIFIABLE: The HQ runtime owner is unverifiable
|
|
14
|
+
* - JG_AGENT_HQ_RUNTIME_NOT_RUNNING: The HQ runtime is not running
|
|
15
|
+
* - JG_AGENT_HQ_LOCK_UNSUPPORTED_PLATFORM: The HQ lock requires Linux or macOS
|
|
16
|
+
* - JG_AGENT_HQ_RUNTIME_DIRECTORY_INVALID: The HQ runtime directory must be a real directory
|
|
17
|
+
* - JG_AGENT_HQ_RUNTIME_DIRECTORY_PERMISSIONS: The HQ runtime directory must be private and owned by the current user
|
|
18
|
+
* - JG_AGENT_HQ_LOCK_FILE_INVALID: The HQ lock must be a regular file
|
|
19
|
+
* - JG_AGENT_HQ_LOCK_FILE_PERMISSIONS: The HQ lock must be private and owned by the current user
|
|
20
|
+
* - JG_AGENT_HQ_LOCK_OWNER_OVERSIZED: The HQ lock owner exceeds the supported size
|
|
21
|
+
* - JG_AGENT_HQ_LOCK_OWNER_UNREADABLE: The HQ lock owner could not be read. Inspect .runtime/hq.lock
|
|
22
|
+
* - JG_AGENT_HQ_LOCK_OWNER_INCOMPLETE: The HQ lock owner is incomplete. Inspect .runtime/hq.lock and remove it only after its owner stopped
|
|
23
|
+
* - JG_AGENT_HQ_LOCK_OWNER_INACTIVE: The HQ lock has an inactive owner
|
|
24
|
+
* - JG_AGENT_HQ_LOCK_OWNER_UNVERIFIABLE: The HQ lock owner could not be verified
|
|
25
|
+
* - JG_AGENT_HQ_LOCK_TIMEOUT: Timed out waiting for the HQ lock
|
|
26
|
+
* - JG_AGENT_HQ_STORAGE_INVALID: The HQ storage is invalid
|
|
27
|
+
* - JG_AGENT_HQ_STORAGE_VERSION_UNSUPPORTED: The HQ storage version is unsupported
|
|
28
|
+
* - JG_AGENT_HQ_STORAGE_UPGRADE_FAILED: The HQ storage upgrade failed
|
|
29
|
+
* - JG_AGENT_HQ_STORAGE_UPGRADE_UNCERTAIN: The HQ storage upgrade requires reconciliation
|
|
30
|
+
* - JG_AGENT_HQ_SNAPSHOT_INVALID: The HQ snapshot is invalid
|
|
31
|
+
* - JG_AGENT_HQ_SNAPSHOT_FAILED: The HQ snapshot operation failed
|
|
32
|
+
*/
|
|
33
|
+
export declare const upgradeHqStorage: (options: IHqStorageUpgradeOptions) => Promise<IHqStorageUpgradeResult>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { Worker } from 'node:worker_threads';
|
|
4
|
+
import { hashValue } from 'jg-crypto';
|
|
5
|
+
import { Exception, extractMessage, hasErrorCode } from 'jg-exception';
|
|
6
|
+
import { assertPrivateDirectory } from 'jg-fs';
|
|
7
|
+
import { isPrivateOwner } from '../../locks/index.js';
|
|
8
|
+
import { claimRuntimeOwner, isWithinWorkspace, readExistingState, resolveAllowedRoots, } from '../../runtime/state/index.js';
|
|
9
|
+
import { UpgradeOptionsSchema, UpgradeWorkerResponseSchema, } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Waits for actual exit, including when a worker reports an error. There is no size-based deadline.
|
|
12
|
+
* @returns The validated result after confirmed worker shutdown.
|
|
13
|
+
* @throws
|
|
14
|
+
* - JG_AGENT_HQ_REQUEST_INVALID: The HQ request is invalid
|
|
15
|
+
* - JG_AGENT_HQ_STATE_DIRECTORY_UNSAFE: The HQ state directory is unsafe
|
|
16
|
+
* - JG_AGENT_HQ_STATE_FILE_UNSAFE: The HQ state file is unsafe
|
|
17
|
+
* - JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE: The HQ action key is unavailable
|
|
18
|
+
* - JG_AGENT_HQ_LOCK_UNSUPPORTED_PLATFORM: The HQ lock requires Linux or macOS
|
|
19
|
+
* - JG_AGENT_HQ_RUNTIME_DIRECTORY_INVALID: The HQ runtime directory must be a real directory
|
|
20
|
+
* - JG_AGENT_HQ_RUNTIME_DIRECTORY_PERMISSIONS: The HQ runtime directory must be private and owned by the current user
|
|
21
|
+
* - JG_AGENT_HQ_LOCK_FILE_INVALID: The HQ lock must be a regular file
|
|
22
|
+
* - JG_AGENT_HQ_LOCK_FILE_PERMISSIONS: The HQ lock must be private and owned by the current user
|
|
23
|
+
* - JG_AGENT_HQ_LOCK_OWNER_OVERSIZED: The HQ lock owner exceeds the supported size
|
|
24
|
+
* - JG_AGENT_HQ_LOCK_OWNER_UNREADABLE: The HQ lock owner could not be read. Inspect .runtime/hq.lock
|
|
25
|
+
* - JG_AGENT_HQ_LOCK_OWNER_INCOMPLETE: The HQ lock owner is incomplete. Inspect .runtime/hq.lock and remove it only after its owner stopped
|
|
26
|
+
* - JG_AGENT_HQ_LOCK_OWNER_INACTIVE: The HQ lock has an inactive owner
|
|
27
|
+
* - JG_AGENT_HQ_LOCK_OWNER_UNVERIFIABLE: The HQ lock owner could not be verified
|
|
28
|
+
* - JG_AGENT_HQ_LOCK_TIMEOUT: Timed out waiting for the HQ lock
|
|
29
|
+
* - JG_AGENT_HQ_STORAGE_INVALID: The HQ storage is invalid
|
|
30
|
+
* - JG_AGENT_HQ_STORAGE_VERSION_UNSUPPORTED: The HQ storage version is unsupported
|
|
31
|
+
* - JG_AGENT_HQ_STORAGE_UPGRADE_FAILED: The HQ storage upgrade failed
|
|
32
|
+
* - JG_AGENT_HQ_STORAGE_UPGRADE_UNCERTAIN: The HQ storage upgrade requires reconciliation
|
|
33
|
+
* - JG_AGENT_HQ_SNAPSHOT_INVALID: The HQ snapshot is invalid
|
|
34
|
+
* - JG_AGENT_HQ_SNAPSHOT_FAILED: The HQ snapshot operation failed
|
|
35
|
+
*/
|
|
36
|
+
const runUpgradeWorker = async (input) => {
|
|
37
|
+
let worker;
|
|
38
|
+
try {
|
|
39
|
+
worker = new Worker(new URL('./upgrade.worker.js', import.meta.url), {
|
|
40
|
+
execArgv: process.execArgv.filter((argument, index, args) => argument !== '--input-type' &&
|
|
41
|
+
!argument.startsWith('--input-type=') &&
|
|
42
|
+
args[index - 1] !== '--input-type'),
|
|
43
|
+
workerData: input,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (cause) {
|
|
47
|
+
throw new Exception(`The HQ storage upgrade failed: ${extractMessage(cause)}`, {
|
|
48
|
+
code: 'JG_AGENT_HQ_STORAGE_UPGRADE_FAILED',
|
|
49
|
+
data: { kind: 'operation-failed', phase: 'worker-start', backupDirectory: null },
|
|
50
|
+
cause,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return new Promise((resolveResult, rejectResult) => {
|
|
54
|
+
let response;
|
|
55
|
+
let failure;
|
|
56
|
+
let hasDuplicateResponse = false;
|
|
57
|
+
worker.on('message', (message) => {
|
|
58
|
+
const parsed = UpgradeWorkerResponseSchema.safeParse(message);
|
|
59
|
+
if (response)
|
|
60
|
+
hasDuplicateResponse = true;
|
|
61
|
+
else if (!parsed.success)
|
|
62
|
+
failure = parsed.error;
|
|
63
|
+
else
|
|
64
|
+
response = parsed.data;
|
|
65
|
+
});
|
|
66
|
+
worker.once('error', (cause) => {
|
|
67
|
+
failure = cause;
|
|
68
|
+
});
|
|
69
|
+
worker.once('exit', (code) => {
|
|
70
|
+
if (failure || hasDuplicateResponse || code !== 0 || !response)
|
|
71
|
+
rejectResult(new Exception(`The HQ storage upgrade requires reconciliation: ${failure ? extractMessage(failure) : hasDuplicateResponse ? 'Duplicate upgrade worker result' : `Worker exited (${code})`}`, {
|
|
72
|
+
code: 'JG_AGENT_HQ_STORAGE_UPGRADE_UNCERTAIN',
|
|
73
|
+
data: {
|
|
74
|
+
kind: 'operation-failed',
|
|
75
|
+
phase: 'worker-exit',
|
|
76
|
+
backupDirectory: input.backupDirectory,
|
|
77
|
+
},
|
|
78
|
+
cause: failure,
|
|
79
|
+
}));
|
|
80
|
+
else if (response.kind === 'error')
|
|
81
|
+
rejectResult(new Exception(response.message, {
|
|
82
|
+
code: response.code,
|
|
83
|
+
data: response.data,
|
|
84
|
+
cause: response.cause,
|
|
85
|
+
}));
|
|
86
|
+
else
|
|
87
|
+
resolveResult(response.result);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Upgrades existing offline storage only after a fully verified local backup.
|
|
93
|
+
* Stop HQ/native children and all independent database/plan writers first. No owner is reclaimed.
|
|
94
|
+
* An uncertain error requires explicit reinspection; this operation never reverses or replays DDL.
|
|
95
|
+
* @returns The committed upgrade or an untouched current-version observation, after worker exit.
|
|
96
|
+
* @throws
|
|
97
|
+
* - JG_AGENT_HQ_REQUEST_INVALID: The HQ request is invalid
|
|
98
|
+
* - JG_AGENT_HQ_STATE_DIRECTORY_UNSAFE: The HQ state directory is unsafe
|
|
99
|
+
* - JG_AGENT_HQ_STATE_FILE_UNSAFE: The HQ state file is unsafe
|
|
100
|
+
* - JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE: The HQ action key is unavailable
|
|
101
|
+
* - JG_AGENT_HQ_RUNTIME_ALREADY_OWNED: The HQ runtime is already owned
|
|
102
|
+
* - JG_AGENT_HQ_RUNTIME_OWNER_UNVERIFIABLE: The HQ runtime owner is unverifiable
|
|
103
|
+
* - JG_AGENT_HQ_RUNTIME_NOT_RUNNING: The HQ runtime is not running
|
|
104
|
+
* - JG_AGENT_HQ_LOCK_UNSUPPORTED_PLATFORM: The HQ lock requires Linux or macOS
|
|
105
|
+
* - JG_AGENT_HQ_RUNTIME_DIRECTORY_INVALID: The HQ runtime directory must be a real directory
|
|
106
|
+
* - JG_AGENT_HQ_RUNTIME_DIRECTORY_PERMISSIONS: The HQ runtime directory must be private and owned by the current user
|
|
107
|
+
* - JG_AGENT_HQ_LOCK_FILE_INVALID: The HQ lock must be a regular file
|
|
108
|
+
* - JG_AGENT_HQ_LOCK_FILE_PERMISSIONS: The HQ lock must be private and owned by the current user
|
|
109
|
+
* - JG_AGENT_HQ_LOCK_OWNER_OVERSIZED: The HQ lock owner exceeds the supported size
|
|
110
|
+
* - JG_AGENT_HQ_LOCK_OWNER_UNREADABLE: The HQ lock owner could not be read. Inspect .runtime/hq.lock
|
|
111
|
+
* - JG_AGENT_HQ_LOCK_OWNER_INCOMPLETE: The HQ lock owner is incomplete. Inspect .runtime/hq.lock and remove it only after its owner stopped
|
|
112
|
+
* - JG_AGENT_HQ_LOCK_OWNER_INACTIVE: The HQ lock has an inactive owner
|
|
113
|
+
* - JG_AGENT_HQ_LOCK_OWNER_UNVERIFIABLE: The HQ lock owner could not be verified
|
|
114
|
+
* - JG_AGENT_HQ_LOCK_TIMEOUT: Timed out waiting for the HQ lock
|
|
115
|
+
* - JG_AGENT_HQ_STORAGE_INVALID: The HQ storage is invalid
|
|
116
|
+
* - JG_AGENT_HQ_STORAGE_VERSION_UNSUPPORTED: The HQ storage version is unsupported
|
|
117
|
+
* - JG_AGENT_HQ_STORAGE_UPGRADE_FAILED: The HQ storage upgrade failed
|
|
118
|
+
* - JG_AGENT_HQ_STORAGE_UPGRADE_UNCERTAIN: The HQ storage upgrade requires reconciliation
|
|
119
|
+
* - JG_AGENT_HQ_SNAPSHOT_INVALID: The HQ snapshot is invalid
|
|
120
|
+
* - JG_AGENT_HQ_SNAPSHOT_FAILED: The HQ snapshot operation failed
|
|
121
|
+
*/
|
|
122
|
+
export const upgradeHqStorage = async (options) => {
|
|
123
|
+
const parsed = UpgradeOptionsSchema.safeParse(options);
|
|
124
|
+
if (!parsed.success)
|
|
125
|
+
throw new Exception('The HQ request is invalid', {
|
|
126
|
+
code: 'JG_AGENT_HQ_REQUEST_INVALID',
|
|
127
|
+
data: { kind: 'invalid-request' },
|
|
128
|
+
cause: parsed.error,
|
|
129
|
+
});
|
|
130
|
+
const selected = parsed.data;
|
|
131
|
+
const state = await readExistingState(selected.stateRoot);
|
|
132
|
+
const allowedRoots = await resolveAllowedRoots(selected.allowedRoots);
|
|
133
|
+
const coordinationRoot = selected.coordinationRoot
|
|
134
|
+
? (await resolveAllowedRoots([selected.coordinationRoot]))[0]
|
|
135
|
+
: state.stateRoot;
|
|
136
|
+
try {
|
|
137
|
+
await assertPrivateDirectory(dirname(selected.backupDirectory));
|
|
138
|
+
if ((await realpath(dirname(selected.backupDirectory))) !== dirname(selected.backupDirectory) ||
|
|
139
|
+
!isPrivateOwner(await lstat(dirname(selected.backupDirectory))) ||
|
|
140
|
+
isWithinWorkspace(state.stateRoot, selected.backupDirectory) ||
|
|
141
|
+
isWithinWorkspace(selected.backupDirectory, state.stateRoot))
|
|
142
|
+
throw new Exception('The HQ snapshot is invalid', {
|
|
143
|
+
code: 'JG_AGENT_HQ_SNAPSHOT_INVALID',
|
|
144
|
+
data: { kind: 'invalid-request' },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
catch (cause) {
|
|
148
|
+
if (hasErrorCode(cause, 'JG_AGENT_HQ_SNAPSHOT_INVALID'))
|
|
149
|
+
throw cause;
|
|
150
|
+
throw new Exception(`The HQ snapshot is invalid: ${extractMessage(cause)}`, {
|
|
151
|
+
code: 'JG_AGENT_HQ_SNAPSHOT_INVALID',
|
|
152
|
+
data: { kind: 'invalid-request' },
|
|
153
|
+
cause,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const ownership = await claimRuntimeOwner(state.stateRoot, 'upgrade-storage');
|
|
157
|
+
let result;
|
|
158
|
+
let failure;
|
|
159
|
+
try {
|
|
160
|
+
const current = await readExistingState(state.stateRoot);
|
|
161
|
+
if (hashValue(current.actionKey) !== hashValue(state.actionKey))
|
|
162
|
+
throw new Exception('The HQ action key is unavailable', {
|
|
163
|
+
code: 'JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE',
|
|
164
|
+
data: { kind: 'state-conflict' },
|
|
165
|
+
});
|
|
166
|
+
result = await runUpgradeWorker({
|
|
167
|
+
...selected,
|
|
168
|
+
allowedRoots,
|
|
169
|
+
coordinationRoot,
|
|
170
|
+
actionKeyHash: hashValue(current.actionKey),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (cause) {
|
|
174
|
+
failure = cause;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
// runUpgradeWorker settles only after exit; a worker that has not exited keeps this owner
|
|
178
|
+
await ownership.release();
|
|
179
|
+
}
|
|
180
|
+
catch (cause) {
|
|
181
|
+
throw new Exception(`The HQ storage upgrade requires reconciliation: ${extractMessage(cause)}`, {
|
|
182
|
+
code: 'JG_AGENT_HQ_STORAGE_UPGRADE_UNCERTAIN',
|
|
183
|
+
data: {
|
|
184
|
+
kind: 'operation-failed',
|
|
185
|
+
phase: 'owner-release',
|
|
186
|
+
backupDirectory: result?.backupDirectory ?? selected.backupDirectory,
|
|
187
|
+
},
|
|
188
|
+
cause: failure
|
|
189
|
+
? new AggregateError([failure, cause], 'Upgrade owner release failed')
|
|
190
|
+
: cause,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (failure)
|
|
194
|
+
throw failure;
|
|
195
|
+
return result;
|
|
196
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { DatabaseSync, backup } from 'node:sqlite';
|
|
3
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
4
|
+
import { hashValue } from 'jg-crypto';
|
|
5
|
+
import { Exception, extractMessage, isException } from 'jg-exception';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { getErrorKind } from '../../errors/index.js';
|
|
8
|
+
import { acquireHqLock } from '../../locks/index.js';
|
|
9
|
+
import { isWithinWorkspace, readExistingState } from '../../runtime/state/index.js';
|
|
10
|
+
import { captureSnapshot, verifySnapshot } from '../../snapshots/index.js';
|
|
11
|
+
import { CURRENT_SCHEMA_VERSION, STORAGE_MIGRATIONS, STORAGE_SCHEMAS, readStorageSchema, } from '../schema/index.js';
|
|
12
|
+
import { STORAGE_CONNECTION_SETTINGS } from '../constants.js';
|
|
13
|
+
import { UpgradeWorkerInputSchema, UpgradeResultSchema, } from './types.js';
|
|
14
|
+
const port = parentPort;
|
|
15
|
+
if (!port)
|
|
16
|
+
throw new Exception('The HQ storage upgrade failed', {
|
|
17
|
+
code: 'JG_AGENT_HQ_STORAGE_UPGRADE_FAILED',
|
|
18
|
+
});
|
|
19
|
+
let phase = 'preflight';
|
|
20
|
+
let verifiedBackup = null;
|
|
21
|
+
let database;
|
|
22
|
+
let release;
|
|
23
|
+
let hasCommitted = false;
|
|
24
|
+
let result;
|
|
25
|
+
let failure;
|
|
26
|
+
try {
|
|
27
|
+
const input = UpgradeWorkerInputSchema.parse(workerData);
|
|
28
|
+
const state = await readExistingState(input.stateRoot);
|
|
29
|
+
if (hashValue(state.actionKey) !== input.actionKeyHash)
|
|
30
|
+
throw new Exception('The HQ action key is unavailable', {
|
|
31
|
+
code: 'JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE',
|
|
32
|
+
data: { kind: 'state-conflict' },
|
|
33
|
+
});
|
|
34
|
+
database = new DatabaseSync(state.databasePath, {
|
|
35
|
+
readOnly: true,
|
|
36
|
+
enableForeignKeyConstraints: true,
|
|
37
|
+
timeout: STORAGE_CONNECTION_SETTINGS.busyTimeoutMs,
|
|
38
|
+
});
|
|
39
|
+
const observed = readStorageSchema(database, input.actionKeyHash);
|
|
40
|
+
if (!observed)
|
|
41
|
+
throw new Exception('The HQ storage is invalid', {
|
|
42
|
+
code: 'JG_AGENT_HQ_STORAGE_INVALID',
|
|
43
|
+
data: { kind: 'state-conflict' },
|
|
44
|
+
});
|
|
45
|
+
const target = STORAGE_SCHEMAS.find((entry) => entry.version === CURRENT_SCHEMA_VERSION);
|
|
46
|
+
const base = {
|
|
47
|
+
stateRoot: state.stateRoot,
|
|
48
|
+
fromVersion: observed.schema.version,
|
|
49
|
+
toVersion: target.version,
|
|
50
|
+
fromSchemaHash: observed.schema.hash,
|
|
51
|
+
toSchemaHash: target.hash,
|
|
52
|
+
};
|
|
53
|
+
if (observed.schema.version === target.version) {
|
|
54
|
+
result = { ...base, state: 'current', backupDirectory: null };
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// validate every selected source before reserving output; pages remain bounded for growing registries
|
|
58
|
+
let after = '';
|
|
59
|
+
for (;;) {
|
|
60
|
+
const page = database
|
|
61
|
+
.prepare('SELECT directory FROM hq_plan_registration WHERE directory > ? ORDER BY directory LIMIT 128')
|
|
62
|
+
.all(after);
|
|
63
|
+
for (const row of page) {
|
|
64
|
+
const directory = z.string().parse(row.directory);
|
|
65
|
+
if (isWithinWorkspace(input.backupDirectory, directory) ||
|
|
66
|
+
isWithinWorkspace(directory, input.backupDirectory))
|
|
67
|
+
throw new Exception('The HQ snapshot is invalid', {
|
|
68
|
+
code: 'JG_AGENT_HQ_SNAPSHOT_INVALID',
|
|
69
|
+
data: { kind: 'invalid-request' },
|
|
70
|
+
});
|
|
71
|
+
after = directory;
|
|
72
|
+
}
|
|
73
|
+
if (page.length < 128)
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
database.close();
|
|
77
|
+
database = undefined;
|
|
78
|
+
phase = 'metadata-lock';
|
|
79
|
+
release = await acquireHqLock(input.coordinationRoot ?? state.stateRoot, 'upgrade-storage', {
|
|
80
|
+
waitTimeoutMs: 0,
|
|
81
|
+
});
|
|
82
|
+
const checkpoint = await readExistingState(state.stateRoot);
|
|
83
|
+
if (hashValue(checkpoint.actionKey) !== input.actionKeyHash)
|
|
84
|
+
throw new Exception('The HQ action key is unavailable', {
|
|
85
|
+
code: 'JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE',
|
|
86
|
+
data: { kind: 'state-conflict' },
|
|
87
|
+
});
|
|
88
|
+
database = new DatabaseSync(state.databasePath, {
|
|
89
|
+
enableForeignKeyConstraints: true,
|
|
90
|
+
timeout: STORAGE_CONNECTION_SETTINGS.busyTimeoutMs,
|
|
91
|
+
});
|
|
92
|
+
database.exec(`PRAGMA journal_mode = ${STORAGE_CONNECTION_SETTINGS.journalMode}; PRAGMA synchronous = ${STORAGE_CONNECTION_SETTINGS.synchronous}; BEGIN IMMEDIATE`);
|
|
93
|
+
const locked = readStorageSchema(database, input.actionKeyHash);
|
|
94
|
+
if (locked?.schema.version !== observed.schema.version ||
|
|
95
|
+
locked.schema.hash !== observed.schema.hash)
|
|
96
|
+
throw new Exception('The HQ storage is invalid', {
|
|
97
|
+
code: 'JG_AGENT_HQ_STORAGE_INVALID',
|
|
98
|
+
data: { kind: 'state-conflict' },
|
|
99
|
+
});
|
|
100
|
+
phase = 'backup';
|
|
101
|
+
const writer = database;
|
|
102
|
+
await captureSnapshot({
|
|
103
|
+
stateRoot: state.stateRoot,
|
|
104
|
+
coordinationRoot: input.coordinationRoot ?? state.stateRoot,
|
|
105
|
+
allowedRoots: input.allowedRoots,
|
|
106
|
+
actionKeyHash: input.actionKeyHash,
|
|
107
|
+
schemaHash: observed.schema.hash,
|
|
108
|
+
readPlans: async (cursor) => writer
|
|
109
|
+
.prepare('SELECT directory FROM hq_plan_registration WHERE directory > ? ORDER BY directory LIMIT 128')
|
|
110
|
+
.all(cursor)
|
|
111
|
+
.map((row) => z.string().parse(row.directory)),
|
|
112
|
+
backupDatabase: async (destination) => {
|
|
113
|
+
// an active writer cannot be the source of sqlite3_backup; readers see the locked committed state
|
|
114
|
+
const source = new DatabaseSync(state.databasePath, {
|
|
115
|
+
readOnly: true,
|
|
116
|
+
enableForeignKeyConstraints: true,
|
|
117
|
+
});
|
|
118
|
+
try {
|
|
119
|
+
await backup(source, destination);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
source.close();
|
|
123
|
+
}
|
|
124
|
+
const copy = new DatabaseSync(destination);
|
|
125
|
+
try {
|
|
126
|
+
copy.exec('PRAGMA journal_mode = DELETE');
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
copy.close();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
verify: verifySnapshot,
|
|
133
|
+
}, input.backupDirectory);
|
|
134
|
+
verifiedBackup = input.backupDirectory;
|
|
135
|
+
phase = 'migration';
|
|
136
|
+
const edge = STORAGE_MIGRATIONS.find((entry) => entry.fromVersion === observed.schema.version && entry.toVersion === target.version);
|
|
137
|
+
if (!edge)
|
|
138
|
+
throw new Exception('The HQ storage version is unsupported', {
|
|
139
|
+
code: 'JG_AGENT_HQ_STORAGE_VERSION_UNSUPPORTED',
|
|
140
|
+
data: { kind: 'state-conflict' },
|
|
141
|
+
});
|
|
142
|
+
database.exec(await readFile(edge.source, 'utf8'));
|
|
143
|
+
database
|
|
144
|
+
.prepare('UPDATE hq_schema SET schema_version = ?, schema_hash = ? WHERE id = 1')
|
|
145
|
+
.run(target.version, target.hash);
|
|
146
|
+
const migrated = readStorageSchema(database, input.actionKeyHash);
|
|
147
|
+
if (migrated?.schema.version !== target.version || migrated.schema.hash !== target.hash)
|
|
148
|
+
throw new Exception('The HQ storage is invalid', {
|
|
149
|
+
code: 'JG_AGENT_HQ_STORAGE_INVALID',
|
|
150
|
+
data: { kind: 'state-conflict' },
|
|
151
|
+
});
|
|
152
|
+
const integrity = database.prepare('PRAGMA integrity_check').all();
|
|
153
|
+
if (integrity.length !== 1 ||
|
|
154
|
+
integrity[0]?.integrity_check !== 'ok' ||
|
|
155
|
+
database.prepare('PRAGMA foreign_key_check').get())
|
|
156
|
+
throw new Exception('The HQ storage is invalid', {
|
|
157
|
+
code: 'JG_AGENT_HQ_STORAGE_INVALID',
|
|
158
|
+
data: { kind: 'state-conflict' },
|
|
159
|
+
});
|
|
160
|
+
phase = 'commit';
|
|
161
|
+
database.exec('COMMIT');
|
|
162
|
+
hasCommitted = true;
|
|
163
|
+
result = { ...base, state: 'upgraded', backupDirectory: verifiedBackup };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (cause) {
|
|
167
|
+
failure = cause;
|
|
168
|
+
// a post-COMMIT exception can precede the assignment above; autocommit distinguishes that uncertainty
|
|
169
|
+
if (phase === 'commit' && database && !database.isTransaction)
|
|
170
|
+
hasCommitted = true;
|
|
171
|
+
if (database?.isTransaction) {
|
|
172
|
+
try {
|
|
173
|
+
database.exec('ROLLBACK');
|
|
174
|
+
}
|
|
175
|
+
catch (cleanup) {
|
|
176
|
+
hasCommitted = true;
|
|
177
|
+
failure = new AggregateError([cause, cleanup], 'Upgrade rollback could not be confirmed');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
try {
|
|
183
|
+
database?.close();
|
|
184
|
+
await release?.();
|
|
185
|
+
}
|
|
186
|
+
catch (cause) {
|
|
187
|
+
hasCommitted = true;
|
|
188
|
+
failure = failure ? new AggregateError([failure, cause], 'Upgrade cleanup failed') : cause;
|
|
189
|
+
phase = 'cleanup';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
if (failure) {
|
|
194
|
+
const error = hasCommitted
|
|
195
|
+
? new Exception(`The HQ storage upgrade requires reconciliation: ${extractMessage(failure)}`, {
|
|
196
|
+
code: 'JG_AGENT_HQ_STORAGE_UPGRADE_UNCERTAIN',
|
|
197
|
+
data: { kind: 'operation-failed' },
|
|
198
|
+
cause: failure,
|
|
199
|
+
})
|
|
200
|
+
: isException(failure) && failure.code.startsWith('JG_AGENT_HQ_')
|
|
201
|
+
? failure
|
|
202
|
+
: new Exception(`The HQ storage upgrade failed: ${extractMessage(failure)}`, {
|
|
203
|
+
code: 'JG_AGENT_HQ_STORAGE_UPGRADE_FAILED',
|
|
204
|
+
data: { kind: 'operation-failed' },
|
|
205
|
+
cause: failure,
|
|
206
|
+
});
|
|
207
|
+
port.postMessage({
|
|
208
|
+
kind: 'error',
|
|
209
|
+
code: error.code,
|
|
210
|
+
message: extractMessage(error),
|
|
211
|
+
cause: failure instanceof Error ? failure : undefined,
|
|
212
|
+
data: { kind: getErrorKind(error), phase, backupDirectory: verifiedBackup },
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
else
|
|
216
|
+
port.postMessage({ kind: 'result', result: UpgradeResultSchema.parse(result) });
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
port.close();
|
|
220
|
+
}
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
4
4
|
* Fresh schema and stamp creation share one transaction; application objects are not converted.
|
|
5
5
|
* @returns The worker-owned connection and complete HQ schema hash.
|
|
6
6
|
* @throws
|
|
7
|
+
* - JG_AGENT_HQ_STORAGE_UPGRADE_REQUIRED: The HQ storage requires an explicit upgrade
|
|
8
|
+
* - JG_AGENT_HQ_STORAGE_VERSION_UNSUPPORTED: The HQ storage version is unsupported
|
|
7
9
|
* - JG_AGENT_HQ_STORAGE_INVALID: The HQ storage is invalid
|
|
8
10
|
* - JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE: The HQ action key is unavailable
|
|
9
11
|
*/
|
package/dist/store/schema.js
CHANGED
|
@@ -1,49 +1,40 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
2
|
import { hashValue } from 'jg-crypto';
|
|
3
3
|
import { Exception, isException } from 'jg-exception';
|
|
4
|
-
import { STORAGE_CONNECTION_SETTINGS
|
|
4
|
+
import { STORAGE_CONNECTION_SETTINGS } from '../storage/index.js';
|
|
5
|
+
import { CURRENT_SCHEMA_VERSION, readStorageSchema } from '../storage/schema/index.js';
|
|
5
6
|
/**
|
|
6
7
|
* Recognizes an empty schema or validates its singleton stamp without issuing writes.
|
|
7
8
|
* @returns Whether initialization is required.
|
|
8
9
|
* @throws
|
|
10
|
+
* - JG_AGENT_HQ_STORAGE_UPGRADE_REQUIRED: The HQ storage requires an explicit upgrade
|
|
11
|
+
* - JG_AGENT_HQ_STORAGE_VERSION_UNSUPPORTED: The HQ storage version is unsupported
|
|
9
12
|
* - JG_AGENT_HQ_STORAGE_INVALID: The HQ storage is invalid
|
|
10
13
|
* - JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE: The HQ action key is unavailable
|
|
11
14
|
*/
|
|
12
15
|
const validateSchemaStamp = (database, schemaHash, actionKeyHash) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
code: 'JG_AGENT_HQ_STORAGE_INVALID',
|
|
23
|
-
data: { kind: 'state-conflict' },
|
|
24
|
-
});
|
|
25
|
-
if (stamp.data.action_key_hash !== actionKeyHash)
|
|
26
|
-
throw new Exception('The HQ action key is unavailable', {
|
|
27
|
-
code: 'JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE',
|
|
28
|
-
data: { kind: 'state-conflict' },
|
|
29
|
-
});
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
catch (cause) {
|
|
33
|
-
if (isException(cause))
|
|
34
|
-
throw cause;
|
|
16
|
+
const observed = readStorageSchema(database, actionKeyHash);
|
|
17
|
+
if (!observed)
|
|
18
|
+
return true;
|
|
19
|
+
if (observed.schema.version !== CURRENT_SCHEMA_VERSION)
|
|
20
|
+
throw new Exception('The HQ storage requires an explicit upgrade', {
|
|
21
|
+
code: 'JG_AGENT_HQ_STORAGE_UPGRADE_REQUIRED',
|
|
22
|
+
data: { kind: 'state-conflict' },
|
|
23
|
+
});
|
|
24
|
+
if (observed.schema.hash !== schemaHash)
|
|
35
25
|
throw new Exception('The HQ storage is invalid', {
|
|
36
26
|
code: 'JG_AGENT_HQ_STORAGE_INVALID',
|
|
37
27
|
data: { kind: 'state-conflict' },
|
|
38
|
-
cause,
|
|
39
28
|
});
|
|
40
|
-
|
|
29
|
+
return false;
|
|
41
30
|
};
|
|
42
31
|
/**
|
|
43
32
|
* Validates through a read-only connection before opening a writer that can checkpoint a WAL.
|
|
44
33
|
* Fresh schema and stamp creation share one transaction; application objects are not converted.
|
|
45
34
|
* @returns The worker-owned connection and complete HQ schema hash.
|
|
46
35
|
* @throws
|
|
36
|
+
* - JG_AGENT_HQ_STORAGE_UPGRADE_REQUIRED: The HQ storage requires an explicit upgrade
|
|
37
|
+
* - JG_AGENT_HQ_STORAGE_VERSION_UNSUPPORTED: The HQ storage version is unsupported
|
|
47
38
|
* - JG_AGENT_HQ_STORAGE_INVALID: The HQ storage is invalid
|
|
48
39
|
* - JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE: The HQ action key is unavailable
|
|
49
40
|
*/
|
package/dist/store/schema.sql
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
CREATE TABLE hq_schema (
|
|
2
2
|
id INTEGER NOT NULL CONSTRAINT hq_pk_schema__id PRIMARY KEY CHECK (id = 1),
|
|
3
3
|
schema_hash TEXT NOT NULL CHECK (length(schema_hash) = 64),
|
|
4
|
-
action_key_hash TEXT NOT NULL CHECK (length(action_key_hash) = 64)
|
|
4
|
+
action_key_hash TEXT NOT NULL CHECK (length(action_key_hash) = 64),
|
|
5
|
+
schema_version INTEGER NOT NULL DEFAULT 2 CHECK (schema_version > 0)
|
|
5
6
|
) STRICT;
|
|
6
7
|
|
|
7
8
|
CREATE TABLE hq_native_history_index (
|
package/dist/store/store.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { type IStore } from './types.js';
|
|
|
4
4
|
* @returns A store whose close waits for worker exit.
|
|
5
5
|
* @throws
|
|
6
6
|
* - JG_AGENT_HQ_STORE_FAILED: The HQ store failed
|
|
7
|
+
* - JG_AGENT_HQ_STORAGE_UPGRADE_REQUIRED: The HQ storage requires an explicit upgrade
|
|
8
|
+
* - JG_AGENT_HQ_STORAGE_VERSION_UNSUPPORTED: The HQ storage version is unsupported
|
|
7
9
|
* - JG_AGENT_HQ_STORAGE_INVALID: The HQ storage is invalid
|
|
8
10
|
* - JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE: The HQ action key is unavailable
|
|
9
11
|
*/
|
package/dist/store/store.js
CHANGED
|
@@ -15,6 +15,8 @@ import { StoreRequestSchema, StoreResponseSchema, } from './types.js';
|
|
|
15
15
|
* @returns A store whose close waits for worker exit.
|
|
16
16
|
* @throws
|
|
17
17
|
* - JG_AGENT_HQ_STORE_FAILED: The HQ store failed
|
|
18
|
+
* - JG_AGENT_HQ_STORAGE_UPGRADE_REQUIRED: The HQ storage requires an explicit upgrade
|
|
19
|
+
* - JG_AGENT_HQ_STORAGE_VERSION_UNSUPPORTED: The HQ storage version is unsupported
|
|
18
20
|
* - JG_AGENT_HQ_STORAGE_INVALID: The HQ storage is invalid
|
|
19
21
|
* - JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE: The HQ action key is unavailable
|
|
20
22
|
*/
|
package/docs/recovery.md
CHANGED
|
@@ -12,10 +12,24 @@ A successful snapshot has exact file hashes and a final completion marker. The d
|
|
|
12
12
|
|
|
13
13
|
Stop all connections to the proposed destination and choose a new recovery workspace. Restore validates source and staged copies and refuses every existing target. It creates separate state and plan recovery directories, a streamed recovery.jsonl inventory and recovered.json completion record. Never use output left incomplete by a crash. Caught failures clean only the output that operation created.
|
|
14
14
|
|
|
15
|
-
Start HQ with the returned stateRoot and explicit allowedRoots. Source root permissions are not imported. The runtime gets a new epoch; automation stays paused, dispatch is disabled, skill grants are revoked, approvals expire and native ownership is uncertain. Read stored reports without native histories. Inspect wait sets, continuation reservations and uncertain receipts without replaying input.
|
|
15
|
+
Inspect the snapshot schema version. A restored published 2.0.0 database remains version 1; upgrade it explicitly while offline before starting current HQ. Start HQ with the returned stateRoot and explicit allowedRoots. Source root permissions are not imported. The runtime gets a new epoch; automation stays paused, dispatch is disabled, skill grants are revoked, approvals expire and native ownership is uncertain. Read stored reports without native histories. Inspect wait sets, continuation reservations and uncertain receipts without replaying input.
|
|
16
16
|
|
|
17
17
|
Recover repositories and native files separately. Source paths and plan bindings are retained as evidence. Compare the selected plan companions against current files before copying them into an explicitly chosen location; never blindly replace repository files. Rebind plan references and obtain fresh authority where needed. Reconcile a pending document intent only using its original ID and unchanged/desired byte guards. Reconcile native ownership and make current-revision host lifecycle decisions before enabling execution. The snapshot does not promise path translation or restore authentication.
|
|
18
18
|
|
|
19
|
+
## Upgrade and rollback
|
|
20
|
+
|
|
21
|
+
Stop HQ and its native children, close every independent application database writer and pause independent plan writers. Keep the existing database and 32-byte action.key together. Normal startup never upgrades them. Choose an absent backup destination beneath an existing private canonical parent:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
jg-agent-hq upgrade-storage --state-root /path/to/state --backup-directory /private/backups/before-upgrade --workspace-root /path/to/workspace
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Repeat --workspace-root for additional trusted roots; pass --coordination-root only when the installation uses a separate existing coordination directory. The Node upgradeHqStorage API performs the same operation. Both produce a fully verified format-1 backup before migration, preserving host tables, exact reports and registered plan companions. The operation does not start a runtime, resume automation or redeem any usage reset.
|
|
28
|
+
|
|
29
|
+
Retain a verified backup even after a later migration failure. Before commit, a confirmed rollback preserves logical state; WAL files need not remain byte-identical after opening a writer. An uncertain commit, lost result or cleanup error requires explicit reconciliation. Confirm workers and all independent writers have stopped, inspect the retained owner/backup evidence, then explicitly retry against the durable stamp. Current returns without another backup or reconciliation. Never blindly delete an owner or replay a migration. A current-version call leaves the supplied backup destination untouched.
|
|
30
|
+
|
|
31
|
+
Rollback uses the pre-upgrade snapshot restored into a new recovery workspace with compatible software. Do not overwrite the upgraded state root. Version 1 restores need the old compatible package or the explicit upgrade before current startup. Downgrade accepts losing every change made after that snapshot, including host changes and later HQ receipts. Reconcile native ownership and obtain fresh authority before enabling work. No down-migration or automatic reverse operation exists.
|
|
32
|
+
|
|
19
33
|
## Off-machine durability
|
|
20
34
|
|
|
21
35
|
The development helper examples/shared-storage.ts demonstrates a host-owned application table, maintenance, capture and offline restore using public exports. Capture already returns verified metadata, so the helper avoids an immediate extra full-database inspection. Its application transaction commits independently of capture. Inspect the restored values before starting a recovered runtime with explicitly chosen roots.
|