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.
Files changed (75) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +4 -4
  3. package/api/README.md +51 -8
  4. package/dist/cli/cli.js +35 -3
  5. package/dist/cli/utilities.d.ts +3 -0
  6. package/dist/cli/utilities.js +30 -0
  7. package/dist/contracts/index.d.ts +2 -2
  8. package/dist/contracts/index.js +1 -1
  9. package/dist/contracts/types.d.ts +102 -0
  10. package/dist/contracts/types.js +24 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +2 -0
  13. package/dist/plans/authority.d.ts +2 -0
  14. package/dist/plans/authority.js +8 -0
  15. package/dist/plans/documents.d.ts +17 -0
  16. package/dist/plans/documents.js +24 -2
  17. package/dist/plans/files.js +3 -1
  18. package/dist/plans/plans.js +3 -1
  19. package/dist/plans/types.d.ts +5 -0
  20. package/dist/providers/claude/claude.js +2 -0
  21. package/dist/providers/claude/history/types.d.ts +23 -0
  22. package/dist/providers/claude/transformers.js +14 -1
  23. package/dist/providers/codex/codex.js +4 -0
  24. package/dist/providers/codex/transformers.js +30 -3
  25. package/dist/providers/codex/types.d.ts +34 -1
  26. package/dist/providers/codex/types.js +11 -2
  27. package/dist/runtime/runtime.d.ts +2 -0
  28. package/dist/runtime/runtime.js +2 -0
  29. package/dist/runtime/state/index.d.ts +1 -1
  30. package/dist/runtime/state/index.js +1 -1
  31. package/dist/runtime/state/state.d.ts +16 -1
  32. package/dist/runtime/state/state.js +94 -3
  33. package/dist/snapshots/capture.d.ts +14 -0
  34. package/dist/snapshots/capture.js +142 -0
  35. package/dist/snapshots/database.d.ts +2 -0
  36. package/dist/snapshots/database.js +15 -51
  37. package/dist/snapshots/fences.d.ts +15 -0
  38. package/dist/snapshots/fences.js +64 -0
  39. package/dist/snapshots/index.d.ts +2 -0
  40. package/dist/snapshots/index.js +3 -0
  41. package/dist/snapshots/snapshots.d.ts +4 -1
  42. package/dist/snapshots/snapshots.js +27 -113
  43. package/dist/snapshots/snapshots.worker.js +5 -1
  44. package/dist/snapshots/types.d.ts +11 -0
  45. package/dist/snapshots/types.js +5 -1
  46. package/dist/storage/index.d.ts +2 -1
  47. package/dist/storage/index.js +2 -1
  48. package/dist/storage/schema/constants.d.ts +2 -0
  49. package/dist/storage/schema/constants.js +3 -0
  50. package/dist/storage/schema/index.d.ts +4 -0
  51. package/dist/storage/schema/index.js +5 -0
  52. package/dist/storage/schema/schema.d.ts +16 -0
  53. package/dist/storage/schema/schema.js +73 -0
  54. package/dist/storage/schema/types.d.ts +17 -0
  55. package/dist/storage/schema/types.js +8 -0
  56. package/dist/storage/schema/v1-to-v2.sql +1 -0
  57. package/dist/storage/schema/v1.sql +538 -0
  58. package/dist/storage/types.d.ts +0 -5
  59. package/dist/storage/types.js +0 -6
  60. package/dist/storage/upgrade/index.d.ts +2 -0
  61. package/dist/storage/upgrade/index.js +2 -0
  62. package/dist/storage/upgrade/types.d.ts +61 -0
  63. package/dist/storage/upgrade/types.js +42 -0
  64. package/dist/storage/upgrade/upgrade.d.ts +33 -0
  65. package/dist/storage/upgrade/upgrade.js +196 -0
  66. package/dist/storage/upgrade/upgrade.worker.d.ts +1 -0
  67. package/dist/storage/upgrade/upgrade.worker.js +220 -0
  68. package/dist/store/schema.d.ts +2 -0
  69. package/dist/store/schema.js +16 -25
  70. package/dist/store/schema.sql +2 -1
  71. package/dist/store/store.d.ts +2 -0
  72. package/dist/store/store.js +2 -0
  73. package/docs/recovery.md +15 -1
  74. package/errors/README.md +34 -28
  75. package/package.json +2 -2
@@ -1,6 +1,6 @@
1
1
  import { hashValue as fingerprintSource } from 'jg-crypto';
2
2
  import { Exception } from 'jg-exception';
3
- import { fingerprintPlan, parseMilestones, parsePlan, preparePlanDocumentWrite, preparePlanUpdate, } from 'jg-plans';
3
+ import { checkPlan, fingerprintPlan, parseMilestones, parsePlan, preparePlanDocumentWrite, preparePlanUpdate, } from 'jg-plans';
4
4
  import { z } from 'zod';
5
5
  import { PlanMetadataSchema } from '../contracts/index.js';
6
6
  const administrativeKeys = ['created_at', 'updated_at', 'revision', 'status'];
@@ -10,13 +10,35 @@ const bindingSchema = z.object({
10
10
  plan_content_sha256: z.string().regex(/^[a-f0-9]{64}$/),
11
11
  });
12
12
  const metadataSchema = z.record(z.string(), z.json());
13
- const invalid = (cause) => {
13
+ /**
14
+ * Rejects a plan HQ cannot read or act on.
15
+ * @param cause The validation failure, when one exists.
16
+ * @throws
17
+ * - JG_AGENT_HQ_PLAN_INVALID: The HQ plan is invalid
18
+ */
19
+ export const invalid = (cause) => {
14
20
  throw new Exception('The HQ plan is invalid', {
15
21
  code: 'JG_AGENT_HQ_PLAN_INVALID',
16
22
  data: { kind: 'invalid-request' },
17
23
  cause,
18
24
  });
19
25
  };
26
+ /**
27
+ * Checks a protocol 0.2 plan with jg-plans for the stages HQ gates. Plan approval needs plan.md
28
+ * to conform alone; finalization, sequence approval, authorization, and execution need it to
29
+ * conform with its milestones. Plans without the 0.2 marker always conform, as before.
30
+ * @param plan Exact plan.md source.
31
+ * @param milestones Exact milestones.md source, or null when absent.
32
+ * @returns The conformance of each stage.
33
+ */
34
+ export const observePlanConformance = (plan, milestones) => {
35
+ if (parsePlan(plan).protocol !== '0.2')
36
+ return { plan: true, sequence: true };
37
+ return {
38
+ plan: checkPlan({ plan }).conforms,
39
+ sequence: checkPlan({ plan, ...(milestones === null ? {} : { milestones }) }).conforms,
40
+ };
41
+ };
20
42
  /** Metadata must be available as plain JSON; absence alone supplies an empty mapping. */
21
43
  const metadataOf = (document) => {
22
44
  if (!document.frontmatter)
@@ -6,7 +6,7 @@ import { Exception, extractMessage, hasErrorCode, isException } from 'jg-excepti
6
6
  import { PlanCoordinationSchema, } from '../contracts/index.js';
7
7
  import { resolveWorkspaceTarget } from '../runtime/state/index.js';
8
8
  import { computeObservedPlanRevision, hashPlanContent, projectPlanMilestones, } from './utilities.js';
9
- import { observePlanContract, observeSkillsBinding, observeSequence } from './documents.js';
9
+ import { observePlanConformance, observePlanContract, observeSkillsBinding, observeSequence, } from './documents.js';
10
10
  const MAX_PLAN_BYTES = 1_048_576;
11
11
  /**
12
12
  * Reads one bounded regular artifact without following a file symlink.
@@ -168,6 +168,8 @@ export const readPlanArtifacts = async (target, roots, parseContract) => {
168
168
  milestones: milestones === null ? null : hashPlanContent(milestones),
169
169
  coordination: coordinationText === null ? null : hashPlanContent(coordinationText),
170
170
  },
171
+ // observed, not enforced: reads and revisions continue while stage gates use the result
172
+ conformance: observePlanConformance(plan, milestones),
171
173
  };
172
174
  };
173
175
  /** Flushes fixed-name creation/removal before journal completion. */
@@ -9,7 +9,7 @@ import { resolveWorkspaceTarget } from '../runtime/state/index.js';
9
9
  import { canExecutePlanUnit, describePlan, updatePlanAuthority } from './authority.js';
10
10
  import { createPlanAdmission } from './admission.js';
11
11
  import { readPlanPage } from './projections.js';
12
- import { prepareAggregateProgress, prepareSequenceMarkers } from './documents.js';
12
+ import { invalid, prepareAggregateProgress, prepareSequenceMarkers } from './documents.js';
13
13
  import { finalizeInitialSequence } from './finalization.js';
14
14
  import { beginPlanRevision, finishPlanCheckpoint, recordPlanCompletion, refreshPlanBaseline, } from './revisions.js';
15
15
  import { capturePlanWorktrees } from './evidence.js';
@@ -594,6 +594,8 @@ export const createPlans = (coordinationRoot, roots, store, runtimeEpoch, parseC
594
594
  }
595
595
  }
596
596
  else if (command.kind === 'finalize') {
597
+ if (!artifacts.conformance.sequence)
598
+ invalid();
597
599
  if (artifacts.skillsBinding === 'stale')
598
600
  conflict();
599
601
  const workflow = coordination.revision;
@@ -22,12 +22,17 @@ export type IPlanArtifacts = {
22
22
  milestones: string | null;
23
23
  coordination: string | null;
24
24
  };
25
+ conformance: IPlanConformance;
25
26
  };
26
27
  export type IPlanContract = {
27
28
  body: string;
28
29
  metadata: IPlanMetadata;
29
30
  milestones: string | null;
30
31
  };
32
+ export type IPlanConformance = {
33
+ plan: boolean;
34
+ sequence: boolean;
35
+ };
31
36
  export declare const PlanWriteIntentSchema: z.ZodObject<{
32
37
  id: z.ZodUUID;
33
38
  digest: z.ZodString;
@@ -25,6 +25,8 @@ export const createClaudeProvider = (store) => {
25
25
  observedAt: null,
26
26
  ordinaryUsageAllowed: null,
27
27
  windows: [],
28
+ credits: null,
29
+ resetCredits: null,
28
30
  });
29
31
  let latest = unavailable();
30
32
  const connect = async () => {
@@ -4285,7 +4285,30 @@ export declare const ClaudeHistoryReadSchema: z.ZodUnion<readonly [z.ZodDiscrimi
4285
4285
  remainingPercent: z.ZodNumber;
4286
4286
  durationMinutes: z.ZodNullable<z.ZodNumber>;
4287
4287
  resetsAt: z.ZodNullable<z.ZodNumber>;
4288
+ period: z.ZodOptional<z.ZodEnum<{
4289
+ "five-hour": "five-hour";
4290
+ "seven-day": "seven-day";
4291
+ other: "other";
4292
+ }>>;
4293
+ nativeKind: z.ZodOptional<z.ZodNullable<z.ZodString>>;
4294
+ group: z.ZodOptional<z.ZodNullable<z.ZodString>>;
4295
+ scope: z.ZodOptional<z.ZodNullable<z.ZodObject<{
4296
+ model: z.ZodNullable<z.ZodObject<{
4297
+ id: z.ZodNullable<z.ZodString>;
4298
+ displayName: z.ZodString;
4299
+ }, z.core.$strip>>;
4300
+ surface: z.ZodNullable<z.ZodString>;
4301
+ }, z.core.$strip>>>;
4288
4302
  }, z.core.$strip>>;
4303
+ credits: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
4304
+ bucket: z.ZodString;
4305
+ hasCredits: z.ZodBoolean;
4306
+ isUnlimited: z.ZodBoolean;
4307
+ balance: z.ZodNullable<z.ZodString>;
4308
+ }, z.core.$strip>>>>;
4309
+ resetCredits: z.ZodOptional<z.ZodNullable<z.ZodObject<{
4310
+ availableCount: z.ZodNumber;
4311
+ }, z.core.$strip>>>;
4289
4312
  }, z.core.$strip>;
4290
4313
  }, z.core.$strip>, z.ZodObject<{
4291
4314
  kind: z.ZodLiteral<"skills">;
@@ -27,8 +27,19 @@ export const toClaudeUsageSnapshot = (result, observedAt) => {
27
27
  window: row.group === 'session' ? 'primary' : 'secondary',
28
28
  usedPercent: row.percent,
29
29
  remainingPercent: Math.max(0, 100 - row.percent),
30
- durationMinutes: null,
30
+ durationMinutes: row.group === 'session' ? 300 : row.group === 'weekly' ? 10080 : null,
31
31
  resetsAt: row.resets_at === null ? null : Date.parse(row.resets_at),
32
+ period: row.group === 'session' ? 'five-hour' : row.group === 'weekly' ? 'seven-day' : 'other',
33
+ nativeKind: row.kind,
34
+ group: row.group,
35
+ scope: row.scope
36
+ ? {
37
+ model: row.scope.model
38
+ ? { id: row.scope.model.id, displayName: row.scope.model.display_name }
39
+ : null,
40
+ surface: row.scope.surface,
41
+ }
42
+ : null,
32
43
  });
33
44
  }
34
45
  return {
@@ -37,6 +48,8 @@ export const toClaudeUsageSnapshot = (result, observedAt) => {
37
48
  observedAt,
38
49
  ordinaryUsageAllowed: null,
39
50
  windows,
51
+ credits: null,
52
+ resetCredits: null,
40
53
  state: !result.rate_limits_available || windows.length === 0
41
54
  ? 'unavailable'
42
55
  : result.rate_limits?.limits.some((row) => row.is_active && row.percent !== null && row.percent >= 100)
@@ -26,6 +26,8 @@ export const createCodexProvider = async (extraSkillRoots = [], agentTools) => {
26
26
  observedAt: null,
27
27
  ordinaryUsageAllowed: null,
28
28
  windows: [],
29
+ credits: null,
30
+ resetCredits: null,
29
31
  };
30
32
  let rpc;
31
33
  let refresh;
@@ -218,6 +220,8 @@ export const createCodexProvider = async (extraSkillRoots = [], agentTools) => {
218
220
  observedAt: null,
219
221
  ordinaryUsageAllowed: null,
220
222
  windows: [],
223
+ credits: null,
224
+ resetCredits: null,
221
225
  };
222
226
  }
223
227
  if (method === 'account/rateLimits/updated' &&
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { UsageSnapshotSchema, ThreadSummarySchema, TurnSummarySchema, HistoryItemSchema, } from '../../contracts/index.js';
2
+ import { UsageSnapshotSchema, UsageCreditsSchema, ThreadSummarySchema, TurnSummarySchema, HistoryItemSchema, } from '../../contracts/index.js';
3
3
  import { parseNative } from './validations.js';
4
4
  import { nativeProvenance } from '../../sessions/evidence/index.js';
5
5
  /** Preserves exact native tree identities independently of managed-agent relationships. */
@@ -122,9 +122,25 @@ export const toHistoryItem = ({ turnId, item }, fullBody = false) => {
122
122
  * - JG_AGENT_HQ_CODEX_FAILED: The Codex integration failed
123
123
  */
124
124
  export const toUsageSnapshot = (usage, observedAt) => {
125
- const buckets = usage.rateLimitsByLimitId ?? {};
125
+ const nativeBuckets = usage.rateLimitsByLimitId;
126
+ const buckets = nativeBuckets && Object.keys(nativeBuckets).length > 0
127
+ ? nativeBuckets
128
+ : usage.rateLimits
129
+ ? { [usage.rateLimits.limitId ?? 'legacy']: usage.rateLimits }
130
+ : {};
126
131
  const windows = [];
127
- for (const [bucketId, bucket] of Object.entries(buckets))
132
+ const credits = [];
133
+ for (const [bucketId, bucket] of Object.entries(buckets)) {
134
+ if (bucket.credits) {
135
+ const observed = UsageCreditsSchema.safeParse({
136
+ bucket: bucketId,
137
+ hasCredits: bucket.credits.hasCredits,
138
+ isUnlimited: bucket.credits.unlimited,
139
+ balance: bucket.credits.balance,
140
+ });
141
+ if (observed.success)
142
+ credits.push(observed.data);
143
+ }
128
144
  for (const window of ['primary', 'secondary']) {
129
145
  const entry = bucket[window];
130
146
  if (entry)
@@ -137,8 +153,17 @@ export const toUsageSnapshot = (usage, observedAt) => {
137
153
  remainingPercent: Math.max(0, 100 - entry.usedPercent),
138
154
  durationMinutes: entry.windowDurationMins ?? null,
139
155
  resetsAt: entry.resetsAt == null ? null : entry.resetsAt * 1000,
156
+ period: entry.windowDurationMins === 300
157
+ ? 'five-hour'
158
+ : entry.windowDurationMins === 10080
159
+ ? 'seven-day'
160
+ : 'other',
161
+ nativeKind: null,
162
+ group: null,
163
+ scope: null,
140
164
  });
141
165
  }
166
+ }
142
167
  const reached = Object.values(buckets).some((bucket) => bucket.rateLimitReachedType != null);
143
168
  const state = windows.length === 0
144
169
  ? 'unavailable'
@@ -152,5 +177,7 @@ export const toUsageSnapshot = (usage, observedAt) => {
152
177
  observedAt,
153
178
  ordinaryUsageAllowed: usage.ordinaryUsageAllowed ?? null,
154
179
  windows,
180
+ credits: credits.length > 0 ? credits : null,
181
+ resetCredits: usage.rateLimitResetCredits ?? null,
155
182
  });
156
183
  };
@@ -99,9 +99,14 @@ export declare const NativeBucketSchema: z.ZodObject<{
99
99
  resetsAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
100
100
  }, z.core.$strip>>>;
101
101
  rateLimitReachedType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
102
+ credits: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodObject<{
103
+ hasCredits: z.ZodBoolean;
104
+ unlimited: z.ZodBoolean;
105
+ balance: z.ZodNullable<z.ZodString>;
106
+ }, z.core.$strip>>>>;
102
107
  }, z.core.$strip>;
103
108
  export declare const NativeUsageSchema: z.ZodObject<{
104
- rateLimitsByLimitId: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodObject<{
109
+ rateLimitsByLimitId: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodObject<{
105
110
  limitId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
106
111
  limitName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
107
112
  primary: z.ZodOptional<z.ZodNullable<z.ZodObject<{
@@ -115,7 +120,35 @@ export declare const NativeUsageSchema: z.ZodObject<{
115
120
  resetsAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
116
121
  }, z.core.$strip>>>;
117
122
  rateLimitReachedType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
123
+ credits: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodObject<{
124
+ hasCredits: z.ZodBoolean;
125
+ unlimited: z.ZodBoolean;
126
+ balance: z.ZodNullable<z.ZodString>;
127
+ }, z.core.$strip>>>>;
128
+ }, z.core.$strip>>>>;
129
+ rateLimits: z.ZodOptional<z.ZodNullable<z.ZodObject<{
130
+ limitId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
131
+ limitName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
132
+ primary: z.ZodOptional<z.ZodNullable<z.ZodObject<{
133
+ usedPercent: z.ZodNumber;
134
+ windowDurationMins: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
135
+ resetsAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
136
+ }, z.core.$strip>>>;
137
+ secondary: z.ZodOptional<z.ZodNullable<z.ZodObject<{
138
+ usedPercent: z.ZodNumber;
139
+ windowDurationMins: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
140
+ resetsAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
141
+ }, z.core.$strip>>>;
142
+ rateLimitReachedType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
143
+ credits: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodObject<{
144
+ hasCredits: z.ZodBoolean;
145
+ unlimited: z.ZodBoolean;
146
+ balance: z.ZodNullable<z.ZodString>;
147
+ }, z.core.$strip>>>>;
118
148
  }, z.core.$strip>>>;
149
+ rateLimitResetCredits: z.ZodCatch<z.ZodOptional<z.ZodNullable<z.ZodObject<{
150
+ availableCount: z.ZodNumber;
151
+ }, z.core.$strip>>>>;
119
152
  ordinaryUsageAllowed: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
120
153
  }, z.core.$strip>;
121
154
  export type INativeThread = z.infer<typeof NativeThreadSchema>;
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { MAX_PAGE_SIZE, } from '../../contracts/index.js';
2
+ import { MAX_PAGE_SIZE, UsageCreditsSchema, UsageResetCreditsSchema, } from '../../contracts/index.js';
3
3
  // additive native responses are narrowed to fields this adapter actually uses
4
4
  export const NativeThreadSchema = z.object({
5
5
  id: z.string().min(1).max(4096),
@@ -41,15 +41,24 @@ const NativeWindowSchema = z.object({
41
41
  windowDurationMins: z.number().int().positive().nullable().optional(),
42
42
  resetsAt: z.number().int().safe().nullable().optional(),
43
43
  });
44
+ // optional billing metadata may be unavailable without invalidating measured quota windows
45
+ const NativeCreditsSchema = z.object({
46
+ hasCredits: UsageCreditsSchema.shape.hasCredits,
47
+ unlimited: UsageCreditsSchema.shape.isUnlimited,
48
+ balance: UsageCreditsSchema.shape.balance,
49
+ });
44
50
  export const NativeBucketSchema = z.object({
45
51
  limitId: z.string().nullable().optional(),
46
52
  limitName: z.string().nullable().optional(),
47
53
  primary: NativeWindowSchema.nullable().optional(),
48
54
  secondary: NativeWindowSchema.nullable().optional(),
49
55
  rateLimitReachedType: z.string().nullable().optional(),
56
+ credits: NativeCreditsSchema.nullable().optional().catch(null),
50
57
  });
51
58
  export const NativeUsageSchema = z.object({
52
- rateLimitsByLimitId: z.record(z.string(), NativeBucketSchema).nullable(),
59
+ rateLimitsByLimitId: z.record(z.string(), NativeBucketSchema).nullable().optional(),
60
+ rateLimits: NativeBucketSchema.nullable().optional(),
61
+ rateLimitResetCredits: UsageResetCreditsSchema.nullable().optional().catch(null),
53
62
  ordinaryUsageAllowed: z.boolean().nullable().optional(),
54
63
  });
55
64
  // native discovery is unpaged; limit inventory size and retain only display/invocation metadata
@@ -15,6 +15,8 @@ import { type IAgentHq, type IAgentHqOptions } from './types.js';
15
15
  * - JG_AGENT_HQ_RUNTIME_NOT_RUNNING: The HQ runtime is not running
16
16
  * - JG_AGENT_HQ_TRANSPORT_FAILED: The HQ transport failed
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
  */
20
22
  export declare const createAgentHq: (options: IAgentHqOptions) => Promise<IAgentHq>;
@@ -39,6 +39,8 @@ const UnconfirmedCleanupSchema = z.object({ cleanupUnconfirmed: z.literal(true)
39
39
  * - JG_AGENT_HQ_RUNTIME_NOT_RUNNING: The HQ runtime is not running
40
40
  * - JG_AGENT_HQ_TRANSPORT_FAILED: The HQ transport failed
41
41
  * - JG_AGENT_HQ_STORE_FAILED: The HQ store failed
42
+ * - JG_AGENT_HQ_STORAGE_UPGRADE_REQUIRED: The HQ storage requires an explicit upgrade
43
+ * - JG_AGENT_HQ_STORAGE_VERSION_UNSUPPORTED: The HQ storage version is unsupported
42
44
  * - JG_AGENT_HQ_STORAGE_INVALID: The HQ storage is invalid
43
45
  */
44
46
  export const createAgentHq = async (options) => {
@@ -1,2 +1,2 @@
1
- export { prepareStateRoot, resolveAllowedRoots, isWithinWorkspace, resolveWorkspaceTarget, readRuntimeOwner, claimRuntimeOwner, prepareDatabase, prepareActionKey, } from './state.js';
1
+ export { prepareStateRoot, readExistingState, resolveAllowedRoots, isWithinWorkspace, resolveWorkspaceTarget, readRuntimeOwner, claimRuntimeOwner, prepareDatabase, prepareActionKey, } from './state.js';
2
2
  export { RUNTIME_OWNER_FILE, DATABASE_FILE } from './constants.js';
@@ -1,3 +1,3 @@
1
1
  // state ownership and trusted paths
2
- export { prepareStateRoot, resolveAllowedRoots, isWithinWorkspace, resolveWorkspaceTarget, readRuntimeOwner, claimRuntimeOwner, prepareDatabase, prepareActionKey, } from './state.js';
2
+ export { prepareStateRoot, readExistingState, resolveAllowedRoots, isWithinWorkspace, resolveWorkspaceTarget, readRuntimeOwner, claimRuntimeOwner, prepareDatabase, prepareActionKey, } from './state.js';
3
3
  export { RUNTIME_OWNER_FILE, DATABASE_FILE } from './constants.js';
@@ -56,7 +56,7 @@ export declare const readRuntimeOwner: (stateRoot: string) => Promise<{
56
56
  * - JG_AGENT_HQ_RUNTIME_OWNER_UNVERIFIABLE: The HQ runtime owner is unverifiable
57
57
  * - JG_AGENT_HQ_RUNTIME_NOT_RUNNING: The HQ runtime is not running
58
58
  */
59
- export declare const claimRuntimeOwner: (stateRoot: string) => Promise<{
59
+ export declare const claimRuntimeOwner: (stateRoot: string, command?: "serve" | "upgrade-storage") => Promise<{
60
60
  owner: {
61
61
  pid: number;
62
62
  nonce: string;
@@ -72,3 +72,18 @@ export declare const claimRuntimeOwner: (stateRoot: string) => Promise<{
72
72
  * - JG_AGENT_HQ_STATE_FILE_UNSAFE: The HQ state file is unsafe
73
73
  */
74
74
  export declare const prepareDatabase: (stateRoot: string) => Promise<string>;
75
+ /**
76
+ * Validates existing offline storage without creating directories, database files or keys.
77
+ * @returns Canonical state and the exact existing private key.
78
+ * @throws
79
+ * - JG_AGENT_HQ_LOCK_UNSUPPORTED_PLATFORM: The HQ lock requires Linux or macOS
80
+ * - JG_AGENT_HQ_REQUEST_INVALID: The HQ request is invalid
81
+ * - JG_AGENT_HQ_STATE_DIRECTORY_UNSAFE: The HQ state directory is unsafe
82
+ * - JG_AGENT_HQ_STATE_FILE_UNSAFE: The HQ state file is unsafe
83
+ * - JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE: The HQ action key is unavailable
84
+ */
85
+ export declare const readExistingState: (stateRoot: string) => Promise<{
86
+ stateRoot: string;
87
+ databasePath: string;
88
+ actionKey: Uint8Array;
89
+ }>;
@@ -1,7 +1,8 @@
1
1
  import { constants } from 'node:fs';
2
2
  import { lstat, open, realpath, rm } from 'node:fs/promises';
3
- import { isAbsolute, join, relative, sep } from 'node:path';
3
+ import { isAbsolute, join, relative, sep, parse, resolve } from 'node:path';
4
4
  import { randomBytes, randomUUID } from 'node:crypto';
5
+ import { assertNoLinksBelow } from 'jg-fs';
5
6
  import { Exception, extractMessage, hasErrorCode, isException } from 'jg-exception';
6
7
  import { ensurePrivateRuntimeDirectory, HqLockOwnerSchema, isPrivateOwner, } from '../../locks/index.js';
7
8
  import { ACTION_KEY_FILE, DATABASE_FILE, RUNTIME_OWNER_FILE } from './constants.js';
@@ -215,11 +216,11 @@ export const readRuntimeOwner = async (stateRoot) => {
215
216
  * - JG_AGENT_HQ_RUNTIME_OWNER_UNVERIFIABLE: The HQ runtime owner is unverifiable
216
217
  * - JG_AGENT_HQ_RUNTIME_NOT_RUNNING: The HQ runtime is not running
217
218
  */
218
- export const claimRuntimeOwner = async (stateRoot) => {
219
+ export const claimRuntimeOwner = async (stateRoot, command = 'serve') => {
219
220
  const owner = {
220
221
  pid: process.pid,
221
222
  nonce: randomUUID(),
222
- command: 'serve',
223
+ command,
223
224
  createdAt: new Date().toISOString(),
224
225
  };
225
226
  const content = `${JSON.stringify(owner)}\n`;
@@ -294,3 +295,93 @@ export const prepareDatabase = async (stateRoot) => {
294
295
  }
295
296
  return path;
296
297
  };
298
+ /**
299
+ * Validates existing offline storage without creating directories, database files or keys.
300
+ * @returns Canonical state and the exact existing private key.
301
+ * @throws
302
+ * - JG_AGENT_HQ_LOCK_UNSUPPORTED_PLATFORM: The HQ lock requires Linux or macOS
303
+ * - JG_AGENT_HQ_REQUEST_INVALID: The HQ request is invalid
304
+ * - JG_AGENT_HQ_STATE_DIRECTORY_UNSAFE: The HQ state directory is unsafe
305
+ * - JG_AGENT_HQ_STATE_FILE_UNSAFE: The HQ state file is unsafe
306
+ * - JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE: The HQ action key is unavailable
307
+ */
308
+ export const readExistingState = async (stateRoot) => {
309
+ if (process.platform !== 'linux' && process.platform !== 'darwin')
310
+ throw new Exception('The HQ lock requires Linux or macOS', {
311
+ code: 'JG_AGENT_HQ_LOCK_UNSUPPORTED_PLATFORM',
312
+ });
313
+ if (!isAbsolute(stateRoot) || resolve(stateRoot) !== stateRoot)
314
+ throw new Exception('The HQ request is invalid', {
315
+ code: 'JG_AGENT_HQ_REQUEST_INVALID',
316
+ data: { kind: 'invalid-request' },
317
+ });
318
+ try {
319
+ await assertNoLinksBelow(parse(stateRoot).root, stateRoot);
320
+ const directory = await lstat(stateRoot);
321
+ if (!directory.isDirectory() ||
322
+ !isPrivateOwner(directory) ||
323
+ (await realpath(stateRoot)) !== stateRoot)
324
+ throw new Exception('The HQ state directory is unsafe', {
325
+ code: 'JG_AGENT_HQ_STATE_DIRECTORY_UNSAFE',
326
+ data: { kind: 'state-conflict' },
327
+ });
328
+ }
329
+ catch (cause) {
330
+ if (hasErrorCode(cause, 'JG_AGENT_HQ_STATE_DIRECTORY_UNSAFE'))
331
+ throw cause;
332
+ throw new Exception(`The HQ state directory is unsafe: ${extractMessage(cause)}`, {
333
+ code: 'JG_AGENT_HQ_STATE_DIRECTORY_UNSAFE',
334
+ data: { kind: 'state-conflict' },
335
+ cause,
336
+ });
337
+ }
338
+ const databasePath = join(stateRoot, DATABASE_FILE);
339
+ for (const filename of [
340
+ databasePath,
341
+ `${databasePath}-wal`,
342
+ `${databasePath}-shm`,
343
+ `${databasePath}-journal`,
344
+ ]) {
345
+ try {
346
+ const details = await lstat(filename);
347
+ if (!details.isFile() ||
348
+ details.isSymbolicLink() ||
349
+ details.nlink !== 1 ||
350
+ !isPrivateOwner(details))
351
+ throw new Exception('The HQ state file is unsafe', {
352
+ code: 'JG_AGENT_HQ_STATE_FILE_UNSAFE',
353
+ data: { kind: 'state-conflict' },
354
+ });
355
+ }
356
+ catch (cause) {
357
+ if (filename !== databasePath && hasErrorCode(cause, 'ENOENT'))
358
+ continue;
359
+ if (hasErrorCode(cause, 'JG_AGENT_HQ_STATE_FILE_UNSAFE'))
360
+ throw cause;
361
+ throw new Exception(`The HQ state file is unsafe: ${extractMessage(cause)}`, {
362
+ code: 'JG_AGENT_HQ_STATE_FILE_UNSAFE',
363
+ data: { kind: 'state-conflict' },
364
+ cause,
365
+ });
366
+ }
367
+ }
368
+ let actionKey;
369
+ try {
370
+ actionKey = await readPrivateBytes(join(stateRoot, ACTION_KEY_FILE), 32);
371
+ }
372
+ catch (cause) {
373
+ if (isException(cause))
374
+ throw cause;
375
+ throw new Exception(`The HQ action key is unavailable: ${extractMessage(cause)}`, {
376
+ code: 'JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE',
377
+ data: { kind: 'state-conflict' },
378
+ cause,
379
+ });
380
+ }
381
+ if (actionKey.length !== 32)
382
+ throw new Exception('The HQ action key is unavailable', {
383
+ code: 'JG_AGENT_HQ_ACTION_KEY_UNAVAILABLE',
384
+ data: { kind: 'state-conflict' },
385
+ });
386
+ return { stateRoot, databasePath, actionKey };
387
+ };
@@ -0,0 +1,14 @@
1
+ import { type ISnapshotSource, type IHqSnapshotInspection } from './types.js';
2
+ /**
3
+ * Streams the shared format-1 capture and verifies it before transferring destination ownership.
4
+ * The caller must hold its database/metadata checkpoint; no cache or full inventory is retained.
5
+ * @returns The fully verified snapshot, retained by the caller after this resolves.
6
+ * @throws
7
+ * - JG_AGENT_HQ_STORE_FAILED: The HQ store failed
8
+ * - JG_PLANS_LOCK_BUSY: The plan document is locked
9
+ * - JG_PLANS_LOCK_FAILED: The plan document lock operation failed
10
+ * - JG_AGENT_HQ_SNAPSHOT_INVALID: The HQ snapshot is invalid
11
+ * - JG_AGENT_HQ_SNAPSHOT_FAILED: The HQ snapshot operation failed
12
+ * - JG_AGENT_HQ_STORAGE_VERSION_UNSUPPORTED: The HQ storage version is unsupported
13
+ */
14
+ export declare const captureSnapshot: (context: ISnapshotSource, destination: string) => Promise<IHqSnapshotInspection>;