pilotswarm-sdk 0.5.20 → 0.5.22

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 (169) hide show
  1. package/api/src/http-api-transport.js +4 -0
  2. package/api/src/protocol.js +2 -0
  3. package/dist/blob-store.d.ts +42 -11
  4. package/dist/blob-store.d.ts.map +1 -1
  5. package/dist/blob-store.js +163 -53
  6. package/dist/blob-store.js.map +1 -1
  7. package/dist/cms-migrations.d.ts.map +1 -1
  8. package/dist/cms-migrations.js +231 -0
  9. package/dist/cms-migrations.js.map +1 -1
  10. package/dist/cms.d.ts +62 -0
  11. package/dist/cms.d.ts.map +1 -1
  12. package/dist/cms.js +81 -4
  13. package/dist/cms.js.map +1 -1
  14. package/dist/distiller-tools.d.ts +9 -0
  15. package/dist/distiller-tools.d.ts.map +1 -0
  16. package/dist/distiller-tools.js +102 -0
  17. package/dist/distiller-tools.js.map +1 -0
  18. package/dist/footprint.d.ts +129 -0
  19. package/dist/footprint.d.ts.map +1 -0
  20. package/dist/footprint.js +254 -0
  21. package/dist/footprint.js.map +1 -0
  22. package/dist/inspect-tools.d.ts.map +1 -1
  23. package/dist/inspect-tools.js +63 -2
  24. package/dist/inspect-tools.js.map +1 -1
  25. package/dist/managed-session.d.ts.map +1 -1
  26. package/dist/managed-session.js +140 -14
  27. package/dist/managed-session.js.map +1 -1
  28. package/dist/management-client.d.ts +32 -0
  29. package/dist/management-client.d.ts.map +1 -1
  30. package/dist/management-client.js +93 -0
  31. package/dist/management-client.js.map +1 -1
  32. package/dist/orchestration/agents.d.ts +9 -0
  33. package/dist/orchestration/agents.d.ts.map +1 -1
  34. package/dist/orchestration/agents.js +15 -0
  35. package/dist/orchestration/agents.js.map +1 -1
  36. package/dist/orchestration/index.d.ts +2 -2
  37. package/dist/orchestration/index.js +1 -1
  38. package/dist/orchestration/lifecycle.d.ts +1 -0
  39. package/dist/orchestration/lifecycle.d.ts.map +1 -1
  40. package/dist/orchestration/lifecycle.js +348 -1
  41. package/dist/orchestration/lifecycle.js.map +1 -1
  42. package/dist/orchestration/queue.d.ts.map +1 -1
  43. package/dist/orchestration/queue.js +29 -1
  44. package/dist/orchestration/queue.js.map +1 -1
  45. package/dist/orchestration/runtime.d.ts +1 -1
  46. package/dist/orchestration/runtime.d.ts.map +1 -1
  47. package/dist/orchestration/runtime.js +31 -1
  48. package/dist/orchestration/runtime.js.map +1 -1
  49. package/dist/orchestration/state.d.ts +27 -0
  50. package/dist/orchestration/state.d.ts.map +1 -1
  51. package/dist/orchestration/state.js +10 -0
  52. package/dist/orchestration/state.js.map +1 -1
  53. package/dist/orchestration/turn.d.ts.map +1 -1
  54. package/dist/orchestration/turn.js +42 -0
  55. package/dist/orchestration/turn.js.map +1 -1
  56. package/dist/orchestration-registry.d.ts.map +1 -1
  57. package/dist/orchestration-registry.js +6 -2
  58. package/dist/orchestration-registry.js.map +1 -1
  59. package/dist/orchestration-version.d.ts +1 -1
  60. package/dist/orchestration-version.js +1 -1
  61. package/dist/orchestration.d.ts +2 -2
  62. package/dist/orchestration.js +1 -1
  63. package/dist/orchestration_1_0_46.d.ts +1 -1
  64. package/dist/orchestration_1_0_47.d.ts +1 -1
  65. package/dist/orchestration_1_0_48.d.ts +1 -1
  66. package/dist/orchestration_1_0_49.d.ts +1 -1
  67. package/dist/orchestration_1_0_50.d.ts +1 -1
  68. package/dist/orchestration_1_0_66/agents.d.ts +52 -0
  69. package/dist/orchestration_1_0_66/agents.d.ts.map +1 -0
  70. package/dist/orchestration_1_0_66/agents.js +825 -0
  71. package/dist/orchestration_1_0_66/agents.js.map +1 -0
  72. package/dist/orchestration_1_0_66/index.d.ts +24 -0
  73. package/dist/orchestration_1_0_66/index.d.ts.map +1 -0
  74. package/dist/orchestration_1_0_66/index.js +13 -0
  75. package/dist/orchestration_1_0_66/index.js.map +1 -0
  76. package/dist/orchestration_1_0_66/lifecycle.d.ts +48 -0
  77. package/dist/orchestration_1_0_66/lifecycle.d.ts.map +1 -0
  78. package/dist/orchestration_1_0_66/lifecycle.js +573 -0
  79. package/dist/orchestration_1_0_66/lifecycle.js.map +1 -0
  80. package/dist/orchestration_1_0_66/queue.d.ts +7 -0
  81. package/dist/orchestration_1_0_66/queue.d.ts.map +1 -0
  82. package/dist/orchestration_1_0_66/queue.js +776 -0
  83. package/dist/orchestration_1_0_66/queue.js.map +1 -0
  84. package/dist/orchestration_1_0_66/runtime.d.ts +29 -0
  85. package/dist/orchestration_1_0_66/runtime.d.ts.map +1 -0
  86. package/dist/orchestration_1_0_66/runtime.js +198 -0
  87. package/dist/orchestration_1_0_66/runtime.js.map +1 -0
  88. package/dist/orchestration_1_0_66/state.d.ts +149 -0
  89. package/dist/orchestration_1_0_66/state.d.ts.map +1 -0
  90. package/dist/orchestration_1_0_66/state.js +148 -0
  91. package/dist/orchestration_1_0_66/state.js.map +1 -0
  92. package/dist/orchestration_1_0_66/turn.d.ts +34 -0
  93. package/dist/orchestration_1_0_66/turn.d.ts.map +1 -0
  94. package/dist/orchestration_1_0_66/turn.js +1169 -0
  95. package/dist/orchestration_1_0_66/turn.js.map +1 -0
  96. package/dist/orchestration_1_0_66/utils.d.ts +39 -0
  97. package/dist/orchestration_1_0_66/utils.d.ts.map +1 -0
  98. package/dist/orchestration_1_0_66/utils.js +316 -0
  99. package/dist/orchestration_1_0_66/utils.js.map +1 -0
  100. package/dist/orchestration_1_0_67/agents.d.ts +52 -0
  101. package/dist/orchestration_1_0_67/agents.d.ts.map +1 -0
  102. package/dist/orchestration_1_0_67/agents.js +825 -0
  103. package/dist/orchestration_1_0_67/agents.js.map +1 -0
  104. package/dist/orchestration_1_0_67/index.d.ts +28 -0
  105. package/dist/orchestration_1_0_67/index.d.ts.map +1 -0
  106. package/dist/orchestration_1_0_67/index.js +13 -0
  107. package/dist/orchestration_1_0_67/index.js.map +1 -0
  108. package/dist/orchestration_1_0_67/lifecycle.d.ts +49 -0
  109. package/dist/orchestration_1_0_67/lifecycle.d.ts.map +1 -0
  110. package/dist/orchestration_1_0_67/lifecycle.js +807 -0
  111. package/dist/orchestration_1_0_67/lifecycle.js.map +1 -0
  112. package/dist/orchestration_1_0_67/queue.d.ts +7 -0
  113. package/dist/orchestration_1_0_67/queue.d.ts.map +1 -0
  114. package/dist/orchestration_1_0_67/queue.js +804 -0
  115. package/dist/orchestration_1_0_67/queue.js.map +1 -0
  116. package/dist/orchestration_1_0_67/runtime.d.ts +29 -0
  117. package/dist/orchestration_1_0_67/runtime.d.ts.map +1 -0
  118. package/dist/orchestration_1_0_67/runtime.js +228 -0
  119. package/dist/orchestration_1_0_67/runtime.js.map +1 -0
  120. package/dist/orchestration_1_0_67/state.d.ts +176 -0
  121. package/dist/orchestration_1_0_67/state.d.ts.map +1 -0
  122. package/dist/orchestration_1_0_67/state.js +158 -0
  123. package/dist/orchestration_1_0_67/state.js.map +1 -0
  124. package/dist/orchestration_1_0_67/turn.d.ts +34 -0
  125. package/dist/orchestration_1_0_67/turn.d.ts.map +1 -0
  126. package/dist/orchestration_1_0_67/turn.js +1208 -0
  127. package/dist/orchestration_1_0_67/turn.js.map +1 -0
  128. package/dist/orchestration_1_0_67/utils.d.ts +39 -0
  129. package/dist/orchestration_1_0_67/utils.d.ts.map +1 -0
  130. package/dist/orchestration_1_0_67/utils.js +316 -0
  131. package/dist/orchestration_1_0_67/utils.js.map +1 -0
  132. package/dist/regen-worker.d.ts +167 -0
  133. package/dist/regen-worker.d.ts.map +1 -0
  134. package/dist/regen-worker.js +512 -0
  135. package/dist/regen-worker.js.map +1 -0
  136. package/dist/resourcemgr-tools.d.ts.map +1 -1
  137. package/dist/resourcemgr-tools.js +33 -2
  138. package/dist/resourcemgr-tools.js.map +1 -1
  139. package/dist/session-lifecycle.d.ts +8 -0
  140. package/dist/session-lifecycle.d.ts.map +1 -1
  141. package/dist/session-lifecycle.js +35 -17
  142. package/dist/session-lifecycle.js.map +1 -1
  143. package/dist/session-manager.d.ts +21 -0
  144. package/dist/session-manager.d.ts.map +1 -1
  145. package/dist/session-manager.js +104 -11
  146. package/dist/session-manager.js.map +1 -1
  147. package/dist/session-proxy.d.ts +34 -0
  148. package/dist/session-proxy.d.ts.map +1 -1
  149. package/dist/session-proxy.js +407 -5
  150. package/dist/session-proxy.js.map +1 -1
  151. package/dist/session-store.d.ts +50 -15
  152. package/dist/session-store.d.ts.map +1 -1
  153. package/dist/session-store.js +143 -30
  154. package/dist/session-store.js.map +1 -1
  155. package/dist/snapshot-protocol.d.ts +34 -3
  156. package/dist/snapshot-protocol.d.ts.map +1 -1
  157. package/dist/snapshot-protocol.js +25 -0
  158. package/dist/snapshot-protocol.js.map +1 -1
  159. package/dist/types.d.ts +84 -0
  160. package/dist/types.d.ts.map +1 -1
  161. package/dist/types.js.map +1 -1
  162. package/dist/web/web-management-client.d.ts +11 -0
  163. package/dist/web/web-management-client.d.ts.map +1 -1
  164. package/dist/web/web-management-client.js +8 -0
  165. package/dist/web/web-management-client.js.map +1 -1
  166. package/dist/worker.d.ts.map +1 -1
  167. package/dist/worker.js +7 -0
  168. package/dist/worker.js.map +1 -1
  169. package/package.json +1 -1
@@ -150,6 +150,10 @@ export class HttpApiTransport {
150
150
  return this.api.call("setSessionModel", { sessionId, options });
151
151
  }
152
152
 
153
+ async regenerateSession(sessionId, options = {}) {
154
+ return this.api.call("regenerateSession", { sessionId, options });
155
+ }
156
+
153
157
  async stopSessionTurn(sessionId, options = {}) {
154
158
  return this.api.call("stopSessionTurn", { sessionId, options });
155
159
  }
@@ -104,6 +104,8 @@ export const OPERATIONS = [
104
104
  { name: "getSessionEvents", access: "session:read", method: "GET", path: "/management/sessions/:sessionId/events", params: { sessionId: path("sessionId"), afterSeq: query("number"), limit: query("number"), eventTypes: query("json") }, summary: "Session events after a sequence number (reconnect catch-up). Optional eventTypes (JSON string array) narrows to those event types server-side." },
105
105
  { name: "getSessionEventsBefore", access: "session:read", method: "GET", path: "/management/sessions/:sessionId/events-before", params: { sessionId: path("sessionId"), beforeSeq: query("number"), limit: query("number"), eventTypes: query("json") }, summary: "Older session events for history paging. Optional eventTypes (JSON string array) narrows to those event types server-side (chat transcript paging)." },
106
106
  { name: "getSessionMetricSummary", access: "session:read", method: "GET", path: "/management/sessions/:sessionId/metric-summary", params: { sessionId: path("sessionId") }, summary: "Per-session metric summary." },
107
+ { name: "getSessionFootprint", access: "session:read", method: "GET", path: "/management/sessions/:sessionId/footprint", params: { sessionId: path("sessionId") }, summary: "Session footprint: context/compaction health, event-log and snapshot sizes, and an assessment (ok/elevated/degraded) with a recommendation. Control-plane only; TTL-cached." },
108
+ { name: "regenerateSession", access: "session:manage", method: "POST", path: "/management/sessions/:sessionId/regenerate", params: { sessionId: path("sessionId"), options: body() }, summary: "Regenerate the session's transcript in place (epoch rebirth): archive, distill, and recreate the Copilot session at a turn boundary. Enqueue-then-observe; outcomes arrive as session.regenerate_* events." },
107
109
  { name: "getSessionTokensByModel", access: "session:read", method: "GET", path: "/management/sessions/:sessionId/tokens-by-model", params: { sessionId: path("sessionId") }, summary: "Token totals grouped by model." },
108
110
  { name: "getSessionTreeStats", access: "session:read", method: "GET", path: "/management/sessions/:sessionId/tree-stats", params: { sessionId: path("sessionId") }, summary: "Stats rolled up across the spawn tree." },
109
111
  { name: "getSessionSkillUsage", access: "session:read", method: "GET", path: "/management/sessions/:sessionId/skill-usage", params: { sessionId: path("sessionId"), since: query("string") }, summary: "Skill usage for one session." },
@@ -28,7 +28,29 @@
28
28
  */
29
29
  import { ContainerClient, StorageSharedKeyCredential } from "@azure/storage-blob";
30
30
  import { type SnapshotCommitInput, type SnapshotCommitResult, type SnapshotHydrateResult, type SnapshotProbe, type VersionedSnapshotStore } from "./snapshot-protocol.js";
31
- import { type ArtifactDownloadResult, type ArtifactMetadata, type SessionStateStore, type ArtifactStore, type ArtifactUploadOptions } from "./session-store.js";
31
+ import { type ArtifactDownloadResult, type ArtifactMetadata, type SessionStateStore, type ArtifactStore, type ArtifactUploadOptions, type SnapshotCodec } from "./session-store.js";
32
+ /**
33
+ * Epoch-chain snapshot blob: `S.e<E>.tar.br`, one blob per chain. The name
34
+ * must NEVER end `.tar.gz` — that is the only shape shipped resource-manager
35
+ * purge binaries collect as delete candidates, and this name invisibility
36
+ * (not fail-closed parsing in new code) is what protects retained epochs
37
+ * from an old binary. See the key-shape invariant in snapshot-protocol.ts.
38
+ */
39
+ export declare function epochSnapshotBlobName(sessionId: string, epoch: number): string;
40
+ /**
41
+ * Snapshot-blob metadata for a CAS commit, written atomically with the
42
+ * content by single-shot Put Blob. Epoch chains additionally carry
43
+ * `psepoch` — the key already scopes them; the field makes listings
44
+ * self-describing.
45
+ */
46
+ export declare function snapshotCommitBlobMetadata(args: {
47
+ version: number;
48
+ turnKey: string;
49
+ contentHash: string;
50
+ codec: SnapshotCodec;
51
+ rawSizeBytes: number;
52
+ epoch?: number;
53
+ }): Record<string, string>;
32
54
  /**
33
55
  * Configuration for constructing a {@link SessionBlobStore} against an
34
56
  * already-built `ContainerClient`. Used by the managed-identity path
@@ -82,30 +104,39 @@ export declare class SessionBlobStore implements SessionStateStore, ArtifactStor
82
104
  * Dehydrate a session: tar, upload, remove local files.
83
105
  * Frees the worker slot for another session.
84
106
  */
85
- dehydrate(sessionId: string, meta?: Record<string, unknown>): Promise<void>;
107
+ dehydrate(sessionId: string, meta?: Record<string, unknown>, epoch?: number): Promise<void>;
86
108
  /**
87
109
  * Hydrate a session: download tar from blob, extract to local disk.
88
110
  * No-op if local session files already exist.
89
111
  */
90
- hydrate(sessionId: string): Promise<void>;
112
+ hydrate(sessionId: string, epoch?: number): Promise<void>;
91
113
  /**
92
114
  * Checkpoint: upload current session state to blob without removing local files.
93
115
  * Used for crash resilience — the session stays warm in memory.
94
116
  */
95
- checkpoint(sessionId: string): Promise<void>;
96
- getSnapshotSizeBytes(sessionId: string): Promise<number | undefined>;
117
+ checkpoint(sessionId: string, epoch?: number): Promise<void>;
118
+ /** Size-cache key: legacy family caches under the bare id, epoch chains under their blob name. */
119
+ private sizeCacheKey;
120
+ getSnapshotSizeBytes(sessionId: string, epoch?: number): Promise<number | undefined>;
97
121
  /** Check if a dehydrated session exists in blob storage. */
98
- exists(sessionId: string): Promise<boolean>;
99
- /** Delete a dehydrated session from blob storage. */
100
- delete(sessionId: string): Promise<void>;
122
+ exists(sessionId: string, epoch?: number): Promise<boolean>;
123
+ /** Delete a dehydrated session from blob storage (epoch >= 1: only that epoch's chain). */
124
+ delete(sessionId: string, epoch?: number): Promise<void>;
125
+ /**
126
+ * Remove the legacy family AND every epoch chain for the session (real
127
+ * session deletion). Epoch objects are enumerated by prefix and deleted
128
+ * ONLY when the fail-closed parser accepts the name — anything else
129
+ * under the prefix is logged and left alone.
130
+ */
131
+ deleteAllEpochs(sessionId: string): Promise<void>;
101
132
  private static readonly COMMIT_MAX_ATTEMPTS;
102
133
  private static readonly SINGLE_SHOT_MAX_BYTES;
103
134
  private snapshotBlobName;
104
135
  private probeFromMetadata;
105
136
  private headSnapshot;
106
- probeSnapshot(sessionId: string): Promise<SnapshotProbe>;
107
- commitSnapshot(sessionId: string, input: SnapshotCommitInput): Promise<SnapshotCommitResult>;
108
- hydrateSnapshot(sessionId: string): Promise<SnapshotHydrateResult>;
137
+ probeSnapshot(sessionId: string, epoch?: number): Promise<SnapshotProbe>;
138
+ commitSnapshot(sessionId: string, input: SnapshotCommitInput, epoch?: number): Promise<SnapshotCommitResult>;
139
+ hydrateSnapshot(sessionId: string, epoch?: number): Promise<SnapshotHydrateResult>;
109
140
  private artifactBlobPath;
110
141
  /**
111
142
  * Upload an artifact file (e.g. .md) to blob storage.
@@ -1 +1 @@
1
- {"version":3,"file":"blob-store.d.ts","sourceRoot":"","sources":["../src/blob-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"blob-store.d.ts","sourceRoot":"","sources":["../src/blob-store.ts"],"names":[],"mappings":"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"}
@@ -33,8 +33,34 @@ import fs from "node:fs";
33
33
  import os from "node:os";
34
34
  import path from "node:path";
35
35
  import { faultPoint } from "./fault-injection.js";
36
- import { SnapshotConflictError, } from "./snapshot-protocol.js";
37
- import { DEFAULT_SESSION_STATE_DIR, DEFAULT_SNAPSHOT_CODEC, archiveSessionDir, buildMetadata, extractSessionArchive, isBinaryArtifactContentType, normalizeArtifactContentType, resolveArtifactFileUpload, resolveArtifactUpload, resolveSnapshotCodec, waitForSessionSnapshot, } from "./session-store.js";
36
+ import { SnapshotConflictError, isLegacyEpoch, } from "./snapshot-protocol.js";
37
+ import { DEFAULT_SESSION_STATE_DIR, DEFAULT_SNAPSHOT_CODEC, archiveSessionDir, buildMetadata, extractSessionArchive, isBinaryArtifactContentType, normalizeArtifactContentType, parseEpochSnapshotName, resolveArtifactFileUpload, resolveArtifactUpload, resolveSnapshotCodec, waitForSessionSnapshot, } from "./session-store.js";
38
+ /**
39
+ * Epoch-chain snapshot blob: `S.e<E>.tar.br`, one blob per chain. The name
40
+ * must NEVER end `.tar.gz` — that is the only shape shipped resource-manager
41
+ * purge binaries collect as delete candidates, and this name invisibility
42
+ * (not fail-closed parsing in new code) is what protects retained epochs
43
+ * from an old binary. See the key-shape invariant in snapshot-protocol.ts.
44
+ */
45
+ export function epochSnapshotBlobName(sessionId, epoch) {
46
+ return `${sessionId}.e${epoch}.tar.br`;
47
+ }
48
+ /**
49
+ * Snapshot-blob metadata for a CAS commit, written atomically with the
50
+ * content by single-shot Put Blob. Epoch chains additionally carry
51
+ * `psepoch` — the key already scopes them; the field makes listings
52
+ * self-describing.
53
+ */
54
+ export function snapshotCommitBlobMetadata(args) {
55
+ return {
56
+ psver: String(args.version),
57
+ psturnkey: args.turnKey,
58
+ pssha: args.contentHash,
59
+ pscodec: args.codec,
60
+ psraw: String(args.rawSizeBytes),
61
+ ...(isLegacyEpoch(args.epoch) ? {} : { psepoch: String(args.epoch) }),
62
+ };
63
+ }
38
64
  function formatBlobLogValue(value) {
39
65
  if (value == null)
40
66
  return "";
@@ -126,19 +152,20 @@ export class SessionBlobStore {
126
152
  * Dehydrate a session: tar, upload, remove local files.
127
153
  * Frees the worker slot for another session.
128
154
  */
129
- async dehydrate(sessionId, meta) {
155
+ async dehydrate(sessionId, meta, epoch) {
130
156
  const sessionDir = path.join(this.sessionStateDir, sessionId);
131
157
  logBlobStore("info", sessionId, "dehydrate start", {
132
158
  container: this.containerName,
133
159
  dir: sessionDir,
134
160
  reason: meta?.reason,
161
+ ...(isLegacyEpoch(epoch) ? {} : { epoch }),
135
162
  });
136
163
  // Versioned-snapshot fence: the committed chain (lifecycle protocol)
137
164
  // already holds this session's durable state, and an unconditional
138
165
  // legacy Put would replace both content and the CAS metadata
139
166
  // (psver/psturnkey/pssha). Degrade to release: free local files.
140
167
  try {
141
- const head = await this.headSnapshot(sessionId);
168
+ const head = await this.headSnapshot(sessionId, epoch);
142
169
  if (head.exists && !head.legacy) {
143
170
  logBlobStore("warn", sessionId, "dehydrate skipped upload: versioned snapshot exists; releasing local files only", {
144
171
  container: this.containerName,
@@ -154,6 +181,11 @@ export class SessionBlobStore {
154
181
  error: errorMessage(probeErr),
155
182
  });
156
183
  }
184
+ // Epoch chains are versioned-only: the Put below writes the legacy
185
+ // (epoch-0) family (`S.tar.gz`) and must never run for epoch >= 1.
186
+ if (!isLegacyEpoch(epoch)) {
187
+ throw new Error(`dehydrate(${sessionId}) reached the legacy upload path with epoch ${epoch}; epoch chains commit via commitSnapshot`);
188
+ }
157
189
  const snapshot = await waitForSessionSnapshot(this.sessionStateDir, sessionId);
158
190
  if (!snapshot.ready) {
159
191
  logBlobStore("warn", sessionId, "dehydrate snapshot not ready", {
@@ -216,7 +248,12 @@ export class SessionBlobStore {
216
248
  * Hydrate a session: download tar from blob, extract to local disk.
217
249
  * No-op if local session files already exist.
218
250
  */
219
- async hydrate(sessionId) {
251
+ async hydrate(sessionId, epoch) {
252
+ // Legacy whole-dir restore; epoch chains hydrate via hydrateSnapshot
253
+ // (atomic swap), so this path never sees them.
254
+ if (!isLegacyEpoch(epoch)) {
255
+ throw new Error(`hydrate(${sessionId}) is the legacy path; epoch ${epoch} chains hydrate via hydrateSnapshot`);
256
+ }
220
257
  const sessionDir = path.join(this.sessionStateDir, sessionId);
221
258
  logBlobStore("info", sessionId, "hydrate start", {
222
259
  container: this.containerName,
@@ -260,7 +297,7 @@ export class SessionBlobStore {
260
297
  * Checkpoint: upload current session state to blob without removing local files.
261
298
  * Used for crash resilience — the session stays warm in memory.
262
299
  */
263
- async checkpoint(sessionId) {
300
+ async checkpoint(sessionId, epoch) {
264
301
  const sessionDir = path.join(this.sessionStateDir, sessionId);
265
302
  if (!fs.existsSync(sessionDir)) {
266
303
  logBlobStore("info", sessionId, "checkpoint skipped", {
@@ -272,7 +309,7 @@ export class SessionBlobStore {
272
309
  // Versioned-snapshot fence (see dehydrate): never clobber the
273
310
  // CAS-protected chain with an unversioned legacy write.
274
311
  try {
275
- const head = await this.headSnapshot(sessionId);
312
+ const head = await this.headSnapshot(sessionId, epoch);
276
313
  if (head.exists && !head.legacy) {
277
314
  logBlobStore("warn", sessionId, "checkpoint skipped: versioned snapshot exists", {
278
315
  container: this.containerName,
@@ -287,6 +324,10 @@ export class SessionBlobStore {
287
324
  error: errorMessage(probeErr),
288
325
  });
289
326
  }
327
+ // Epoch chains are versioned-only (see dehydrate).
328
+ if (!isLegacyEpoch(epoch)) {
329
+ throw new Error(`checkpoint(${sessionId}) reached the legacy upload path with epoch ${epoch}; epoch chains commit via commitSnapshot`);
330
+ }
290
331
  const codec = DEFAULT_SNAPSHOT_CODEC;
291
332
  const tarPath = path.join(os.tmpdir(), `${sessionId}.tar`);
292
333
  try {
@@ -324,10 +365,37 @@ export class SessionBlobStore {
324
365
  catch { }
325
366
  }
326
367
  }
327
- async getSnapshotSizeBytes(sessionId) {
328
- const cached = this.snapshotSizeBySession.get(sessionId);
368
+ /** Size-cache key: legacy family caches under the bare id, epoch chains under their blob name. */
369
+ sizeCacheKey(sessionId, epoch) {
370
+ return isLegacyEpoch(epoch) ? sessionId : epochSnapshotBlobName(sessionId, epoch);
371
+ }
372
+ async getSnapshotSizeBytes(sessionId, epoch) {
373
+ const cached = this.snapshotSizeBySession.get(this.sizeCacheKey(sessionId, epoch));
329
374
  if (Number.isFinite(cached))
330
375
  return cached;
376
+ // Epoch chains keep no meta.json mirror (key-shape invariant) — the
377
+ // blob's own content length is the stored size.
378
+ if (!isLegacyEpoch(epoch)) {
379
+ const blobName = epochSnapshotBlobName(sessionId, epoch);
380
+ try {
381
+ const props = await this.containerClient.getBlockBlobClient(blobName).getProperties();
382
+ const sizeBytes = Number(props.contentLength);
383
+ if (Number.isFinite(sizeBytes)) {
384
+ this.snapshotSizeBySession.set(this.sizeCacheKey(sessionId, epoch), sizeBytes);
385
+ return sizeBytes;
386
+ }
387
+ }
388
+ catch (error) {
389
+ if (error?.statusCode !== 404) {
390
+ logBlobStore("warn", sessionId, "snapshot size read failed", {
391
+ container: this.containerName,
392
+ blob: blobName,
393
+ error: errorMessage(error),
394
+ });
395
+ }
396
+ }
397
+ return undefined;
398
+ }
331
399
  const metaBlob = this.containerClient.getBlockBlobClient(`${sessionId}.meta.json`);
332
400
  try {
333
401
  if (!(await metaBlob.exists())) {
@@ -355,13 +423,14 @@ export class SessionBlobStore {
355
423
  return undefined;
356
424
  }
357
425
  /** Check if a dehydrated session exists in blob storage. */
358
- async exists(sessionId) {
359
- const tarBlob = this.containerClient.getBlockBlobClient(`${sessionId}.tar.gz`);
426
+ async exists(sessionId, epoch) {
427
+ const blobName = this.snapshotBlobName(sessionId, epoch);
428
+ const tarBlob = this.containerClient.getBlockBlobClient(blobName);
360
429
  try {
361
430
  const exists = await tarBlob.exists();
362
431
  logBlobStore("info", sessionId, "exists probe", {
363
432
  container: this.containerName,
364
- blob: `${sessionId}.tar.gz`,
433
+ blob: blobName,
365
434
  exists,
366
435
  });
367
436
  return exists;
@@ -369,22 +438,30 @@ export class SessionBlobStore {
369
438
  catch (error) {
370
439
  logBlobStore("warn", sessionId, "exists probe failed", {
371
440
  container: this.containerName,
372
- blob: `${sessionId}.tar.gz`,
441
+ blob: blobName,
373
442
  error: errorMessage(error),
374
443
  });
375
444
  throw error;
376
445
  }
377
446
  }
378
- /** Delete a dehydrated session from blob storage. */
379
- async delete(sessionId) {
447
+ /** Delete a dehydrated session from blob storage (epoch >= 1: only that epoch's chain). */
448
+ async delete(sessionId, epoch) {
380
449
  logBlobStore("info", sessionId, "delete start", {
381
450
  container: this.containerName,
451
+ ...(isLegacyEpoch(epoch) ? {} : { epoch }),
382
452
  });
383
453
  try {
384
- await this.containerClient.getBlockBlobClient(`${sessionId}.tar.gz`).deleteIfExists();
385
- await this.containerClient.getBlockBlobClient(`${sessionId}.meta.json`).deleteIfExists();
454
+ if (isLegacyEpoch(epoch)) {
455
+ await this.containerClient.getBlockBlobClient(`${sessionId}.tar.gz`).deleteIfExists();
456
+ await this.containerClient.getBlockBlobClient(`${sessionId}.meta.json`).deleteIfExists();
457
+ }
458
+ else {
459
+ // One blob per epoch chain, no meta.json mirror — nothing else to collect.
460
+ await this.containerClient.getBlockBlobClient(epochSnapshotBlobName(sessionId, epoch)).deleteIfExists();
461
+ }
386
462
  logBlobStore("info", sessionId, "delete complete", {
387
463
  container: this.containerName,
464
+ ...(isLegacyEpoch(epoch) ? {} : { epoch }),
388
465
  });
389
466
  }
390
467
  catch (error) {
@@ -395,6 +472,27 @@ export class SessionBlobStore {
395
472
  throw error;
396
473
  }
397
474
  }
475
+ /**
476
+ * Remove the legacy family AND every epoch chain for the session (real
477
+ * session deletion). Epoch objects are enumerated by prefix and deleted
478
+ * ONLY when the fail-closed parser accepts the name — anything else
479
+ * under the prefix is logged and left alone.
480
+ */
481
+ async deleteAllEpochs(sessionId) {
482
+ await this.delete(sessionId);
483
+ const prefix = `${sessionId}.e`;
484
+ for await (const blob of this.containerClient.listBlobsFlat({ prefix })) {
485
+ const parsed = parseEpochSnapshotName(sessionId, blob.name);
486
+ if (!parsed) {
487
+ logBlobStore("warn", sessionId, "deleteAllEpochs leaving unparseable name alone", {
488
+ container: this.containerName,
489
+ blob: blob.name,
490
+ });
491
+ continue;
492
+ }
493
+ await this.containerClient.getBlockBlobClient(blob.name).deleteIfExists();
494
+ }
495
+ }
398
496
  // ─── Versioned CAS contract (session-lifecycle-protocol §3.1) ───
399
497
  //
400
498
  // The version is an explicit counter in the tar blob's metadata
@@ -408,8 +506,8 @@ export class SessionBlobStore {
408
506
  // Metadata for version bumps (it would decouple counter from content).
409
507
  static COMMIT_MAX_ATTEMPTS = 5;
410
508
  static SINGLE_SHOT_MAX_BYTES = 256 * 1024 * 1024;
411
- snapshotBlobName(sessionId) {
412
- return `${sessionId}.tar.gz`;
509
+ snapshotBlobName(sessionId, epoch) {
510
+ return isLegacyEpoch(epoch) ? `${sessionId}.tar.gz` : epochSnapshotBlobName(sessionId, epoch);
413
511
  }
414
512
  probeFromMetadata(metadata, etag) {
415
513
  const codec = resolveSnapshotCodec(metadata?.pscodec);
@@ -428,8 +526,8 @@ export class SessionBlobStore {
428
526
  ...(etag ? { etag } : {}),
429
527
  };
430
528
  }
431
- async headSnapshot(sessionId) {
432
- const blob = this.containerClient.getBlockBlobClient(this.snapshotBlobName(sessionId));
529
+ async headSnapshot(sessionId, epoch) {
530
+ const blob = this.containerClient.getBlockBlobClient(this.snapshotBlobName(sessionId, epoch));
433
531
  try {
434
532
  const props = await blob.getProperties();
435
533
  return this.probeFromMetadata(props.metadata, props.etag);
@@ -440,18 +538,20 @@ export class SessionBlobStore {
440
538
  throw error;
441
539
  }
442
540
  }
443
- async probeSnapshot(sessionId) {
444
- const { etag: _etag, ...probe } = await this.headSnapshot(sessionId);
541
+ async probeSnapshot(sessionId, epoch) {
542
+ const { etag: _etag, ...probe } = await this.headSnapshot(sessionId, epoch);
445
543
  return probe;
446
544
  }
447
- async commitSnapshot(sessionId, input) {
545
+ async commitSnapshot(sessionId, input, epoch) {
448
546
  const sessionDir = path.join(this.sessionStateDir, sessionId);
449
547
  const snapshot = await waitForSessionSnapshot(this.sessionStateDir, sessionId);
450
548
  if (!snapshot.ready) {
451
549
  throw new Error(`Session state directory not ready during commit: ${sessionId} (${sessionDir}). ` +
452
550
  `Missing: ${snapshot.missing.join(", ") || "unknown"}`);
453
551
  }
454
- const codec = DEFAULT_SNAPSHOT_CODEC;
552
+ // Epoch chains pin brotli — their names bake `.tar.br` (key-shape
553
+ // invariant) — rather than tracking the default.
554
+ const codec = isLegacyEpoch(epoch) ? DEFAULT_SNAPSHOT_CODEC : "brotli";
455
555
  const tarPath = path.join(os.tmpdir(), `ps-commit-${sessionId}-${process.pid}-${Date.now()}.tar`);
456
556
  try {
457
557
  const { rawSizeBytes } = await archiveSessionDir(this.sessionStateDir, sessionId, tarPath, codec);
@@ -462,9 +562,9 @@ export class SessionBlobStore {
462
562
  `Audit the session workspace for files that belong in tar excludes.`);
463
563
  }
464
564
  const contentHash = crypto.createHash("sha256").update(body).digest("hex");
465
- const blob = this.containerClient.getBlockBlobClient(this.snapshotBlobName(sessionId));
565
+ const blob = this.containerClient.getBlockBlobClient(this.snapshotBlobName(sessionId, epoch));
466
566
  for (let attempt = 1; attempt <= SessionBlobStore.COMMIT_MAX_ATTEMPTS; attempt++) {
467
- const head = await this.headSnapshot(sessionId);
567
+ const head = await this.headSnapshot(sessionId, epoch);
468
568
  if (head.exists && head.version === input.baseVersion + 1 && head.turnKey === input.turnKey) {
469
569
  logBlobStore("info", sessionId, "commit already landed (idempotent retry)", {
470
570
  version: head.version,
@@ -491,36 +591,44 @@ export class SessionBlobStore {
491
591
  try {
492
592
  await blob.upload(body, body.length, {
493
593
  conditions,
494
- metadata: {
495
- psver: String(version),
496
- psturnkey: input.turnKey,
497
- pssha: contentHash,
498
- pscodec: codec,
499
- psraw: String(rawSizeBytes),
500
- },
501
- });
502
- faultPoint("store.commit.after-write");
503
- // Legacy compat: keep <sessionId>.meta.json fresh so
504
- // getSnapshotSizeBytes and older tooling keep working.
505
- // Unconditional + best-effort — it is informational only.
506
- try {
507
- const metadata = {
508
- ...buildMetadata(tarPath, sessionId, { reason: "turn-commit" }),
594
+ metadata: snapshotCommitBlobMetadata({
509
595
  version,
510
596
  turnKey: input.turnKey,
511
597
  contentHash,
512
598
  codec,
513
599
  rawSizeBytes,
514
- };
515
- this.snapshotSizeBySession.set(sessionId, metadata.sizeBytes);
516
- const metaBlob = this.containerClient.getBlockBlobClient(`${sessionId}.meta.json`);
517
- const metaJson = JSON.stringify(metadata);
518
- await metaBlob.upload(metaJson, metaJson.length);
600
+ epoch,
601
+ }),
602
+ });
603
+ faultPoint("store.commit.after-write");
604
+ if (isLegacyEpoch(epoch)) {
605
+ // Legacy compat: keep <sessionId>.meta.json fresh so
606
+ // getSnapshotSizeBytes and older tooling keep working.
607
+ // Unconditional + best-effort — it is informational only.
608
+ // Epoch chains keep NO meta.json mirror: nothing
609
+ // epoch-scoped may end `.meta.json` (key-shape invariant).
610
+ try {
611
+ const metadata = {
612
+ ...buildMetadata(tarPath, sessionId, { reason: "turn-commit" }),
613
+ version,
614
+ turnKey: input.turnKey,
615
+ contentHash,
616
+ codec,
617
+ rawSizeBytes,
618
+ };
619
+ this.snapshotSizeBySession.set(sessionId, metadata.sizeBytes);
620
+ const metaBlob = this.containerClient.getBlockBlobClient(`${sessionId}.meta.json`);
621
+ const metaJson = JSON.stringify(metadata);
622
+ await metaBlob.upload(metaJson, metaJson.length);
623
+ }
624
+ catch (metaErr) {
625
+ logBlobStore("warn", sessionId, "commit meta.json refresh failed (non-fatal)", {
626
+ error: errorMessage(metaErr),
627
+ });
628
+ }
519
629
  }
520
- catch (metaErr) {
521
- logBlobStore("warn", sessionId, "commit meta.json refresh failed (non-fatal)", {
522
- error: errorMessage(metaErr),
523
- });
630
+ else {
631
+ this.snapshotSizeBySession.set(this.sizeCacheKey(sessionId, epoch), body.length);
524
632
  }
525
633
  logBlobStore("info", sessionId, "commit complete", {
526
634
  version,
@@ -529,6 +637,7 @@ export class SessionBlobStore {
529
637
  rawSizeBytes,
530
638
  codec,
531
639
  attempt,
640
+ ...(isLegacyEpoch(epoch) ? {} : { epoch }),
532
641
  });
533
642
  return { version, contentHash, sizeBytes: body.length, rawSizeBytes, alreadyCommitted: false };
534
643
  }
@@ -558,9 +667,9 @@ export class SessionBlobStore {
558
667
  catch { }
559
668
  }
560
669
  }
561
- async hydrateSnapshot(sessionId) {
670
+ async hydrateSnapshot(sessionId, epoch) {
562
671
  const sessionDir = path.join(this.sessionStateDir, sessionId);
563
- const blob = this.containerClient.getBlockBlobClient(this.snapshotBlobName(sessionId));
672
+ const blob = this.containerClient.getBlockBlobClient(this.snapshotBlobName(sessionId, epoch));
564
673
  const tarPath = path.join(os.tmpdir(), `ps-hydrate-${sessionId}-${process.pid}-${Date.now()}.tar`);
565
674
  // One download response carries body + metadata consistently.
566
675
  let metadata;
@@ -599,6 +708,7 @@ export class SessionBlobStore {
599
708
  logBlobStore("info", sessionId, "versioned hydrate complete", {
600
709
  version: probe.version,
601
710
  legacy: probe.legacy,
711
+ ...(isLegacyEpoch(epoch) ? {} : { epoch }),
602
712
  });
603
713
  return {
604
714
  version: probe.version,