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.
- package/api/src/http-api-transport.js +4 -0
- package/api/src/protocol.js +2 -0
- package/dist/blob-store.d.ts +42 -11
- package/dist/blob-store.d.ts.map +1 -1
- package/dist/blob-store.js +163 -53
- package/dist/blob-store.js.map +1 -1
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +231 -0
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +62 -0
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +81 -4
- package/dist/cms.js.map +1 -1
- package/dist/distiller-tools.d.ts +9 -0
- package/dist/distiller-tools.d.ts.map +1 -0
- package/dist/distiller-tools.js +102 -0
- package/dist/distiller-tools.js.map +1 -0
- package/dist/footprint.d.ts +129 -0
- package/dist/footprint.d.ts.map +1 -0
- package/dist/footprint.js +254 -0
- package/dist/footprint.js.map +1 -0
- package/dist/inspect-tools.d.ts.map +1 -1
- package/dist/inspect-tools.js +63 -2
- package/dist/inspect-tools.js.map +1 -1
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +140 -14
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts +32 -0
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +93 -0
- package/dist/management-client.js.map +1 -1
- package/dist/orchestration/agents.d.ts +9 -0
- package/dist/orchestration/agents.d.ts.map +1 -1
- package/dist/orchestration/agents.js +15 -0
- package/dist/orchestration/agents.js.map +1 -1
- package/dist/orchestration/index.d.ts +2 -2
- package/dist/orchestration/index.js +1 -1
- package/dist/orchestration/lifecycle.d.ts +1 -0
- package/dist/orchestration/lifecycle.d.ts.map +1 -1
- package/dist/orchestration/lifecycle.js +348 -1
- package/dist/orchestration/lifecycle.js.map +1 -1
- package/dist/orchestration/queue.d.ts.map +1 -1
- package/dist/orchestration/queue.js +29 -1
- package/dist/orchestration/queue.js.map +1 -1
- package/dist/orchestration/runtime.d.ts +1 -1
- package/dist/orchestration/runtime.d.ts.map +1 -1
- package/dist/orchestration/runtime.js +31 -1
- package/dist/orchestration/runtime.js.map +1 -1
- package/dist/orchestration/state.d.ts +27 -0
- package/dist/orchestration/state.d.ts.map +1 -1
- package/dist/orchestration/state.js +10 -0
- package/dist/orchestration/state.js.map +1 -1
- package/dist/orchestration/turn.d.ts.map +1 -1
- package/dist/orchestration/turn.js +42 -0
- package/dist/orchestration/turn.js.map +1 -1
- package/dist/orchestration-registry.d.ts.map +1 -1
- package/dist/orchestration-registry.js +6 -2
- package/dist/orchestration-registry.js.map +1 -1
- package/dist/orchestration-version.d.ts +1 -1
- package/dist/orchestration-version.js +1 -1
- package/dist/orchestration.d.ts +2 -2
- package/dist/orchestration.js +1 -1
- package/dist/orchestration_1_0_46.d.ts +1 -1
- package/dist/orchestration_1_0_47.d.ts +1 -1
- package/dist/orchestration_1_0_48.d.ts +1 -1
- package/dist/orchestration_1_0_49.d.ts +1 -1
- package/dist/orchestration_1_0_50.d.ts +1 -1
- package/dist/orchestration_1_0_66/agents.d.ts +52 -0
- package/dist/orchestration_1_0_66/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_66/agents.js +825 -0
- package/dist/orchestration_1_0_66/agents.js.map +1 -0
- package/dist/orchestration_1_0_66/index.d.ts +24 -0
- package/dist/orchestration_1_0_66/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_66/index.js +13 -0
- package/dist/orchestration_1_0_66/index.js.map +1 -0
- package/dist/orchestration_1_0_66/lifecycle.d.ts +48 -0
- package/dist/orchestration_1_0_66/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_66/lifecycle.js +573 -0
- package/dist/orchestration_1_0_66/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_66/queue.d.ts +7 -0
- package/dist/orchestration_1_0_66/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_66/queue.js +776 -0
- package/dist/orchestration_1_0_66/queue.js.map +1 -0
- package/dist/orchestration_1_0_66/runtime.d.ts +29 -0
- package/dist/orchestration_1_0_66/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_66/runtime.js +198 -0
- package/dist/orchestration_1_0_66/runtime.js.map +1 -0
- package/dist/orchestration_1_0_66/state.d.ts +149 -0
- package/dist/orchestration_1_0_66/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_66/state.js +148 -0
- package/dist/orchestration_1_0_66/state.js.map +1 -0
- package/dist/orchestration_1_0_66/turn.d.ts +34 -0
- package/dist/orchestration_1_0_66/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_66/turn.js +1169 -0
- package/dist/orchestration_1_0_66/turn.js.map +1 -0
- package/dist/orchestration_1_0_66/utils.d.ts +39 -0
- package/dist/orchestration_1_0_66/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_66/utils.js +316 -0
- package/dist/orchestration_1_0_66/utils.js.map +1 -0
- package/dist/orchestration_1_0_67/agents.d.ts +52 -0
- package/dist/orchestration_1_0_67/agents.d.ts.map +1 -0
- package/dist/orchestration_1_0_67/agents.js +825 -0
- package/dist/orchestration_1_0_67/agents.js.map +1 -0
- package/dist/orchestration_1_0_67/index.d.ts +28 -0
- package/dist/orchestration_1_0_67/index.d.ts.map +1 -0
- package/dist/orchestration_1_0_67/index.js +13 -0
- package/dist/orchestration_1_0_67/index.js.map +1 -0
- package/dist/orchestration_1_0_67/lifecycle.d.ts +49 -0
- package/dist/orchestration_1_0_67/lifecycle.d.ts.map +1 -0
- package/dist/orchestration_1_0_67/lifecycle.js +807 -0
- package/dist/orchestration_1_0_67/lifecycle.js.map +1 -0
- package/dist/orchestration_1_0_67/queue.d.ts +7 -0
- package/dist/orchestration_1_0_67/queue.d.ts.map +1 -0
- package/dist/orchestration_1_0_67/queue.js +804 -0
- package/dist/orchestration_1_0_67/queue.js.map +1 -0
- package/dist/orchestration_1_0_67/runtime.d.ts +29 -0
- package/dist/orchestration_1_0_67/runtime.d.ts.map +1 -0
- package/dist/orchestration_1_0_67/runtime.js +228 -0
- package/dist/orchestration_1_0_67/runtime.js.map +1 -0
- package/dist/orchestration_1_0_67/state.d.ts +176 -0
- package/dist/orchestration_1_0_67/state.d.ts.map +1 -0
- package/dist/orchestration_1_0_67/state.js +158 -0
- package/dist/orchestration_1_0_67/state.js.map +1 -0
- package/dist/orchestration_1_0_67/turn.d.ts +34 -0
- package/dist/orchestration_1_0_67/turn.d.ts.map +1 -0
- package/dist/orchestration_1_0_67/turn.js +1208 -0
- package/dist/orchestration_1_0_67/turn.js.map +1 -0
- package/dist/orchestration_1_0_67/utils.d.ts +39 -0
- package/dist/orchestration_1_0_67/utils.d.ts.map +1 -0
- package/dist/orchestration_1_0_67/utils.js +316 -0
- package/dist/orchestration_1_0_67/utils.js.map +1 -0
- package/dist/regen-worker.d.ts +167 -0
- package/dist/regen-worker.d.ts.map +1 -0
- package/dist/regen-worker.js +512 -0
- package/dist/regen-worker.js.map +1 -0
- package/dist/resourcemgr-tools.d.ts.map +1 -1
- package/dist/resourcemgr-tools.js +33 -2
- package/dist/resourcemgr-tools.js.map +1 -1
- package/dist/session-lifecycle.d.ts +8 -0
- package/dist/session-lifecycle.d.ts.map +1 -1
- package/dist/session-lifecycle.js +35 -17
- package/dist/session-lifecycle.js.map +1 -1
- package/dist/session-manager.d.ts +21 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +104 -11
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +34 -0
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +407 -5
- package/dist/session-proxy.js.map +1 -1
- package/dist/session-store.d.ts +50 -15
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js +143 -30
- package/dist/session-store.js.map +1 -1
- package/dist/snapshot-protocol.d.ts +34 -3
- package/dist/snapshot-protocol.d.ts.map +1 -1
- package/dist/snapshot-protocol.js +25 -0
- package/dist/snapshot-protocol.js.map +1 -1
- package/dist/types.d.ts +84 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/web/web-management-client.d.ts +11 -0
- package/dist/web/web-management-client.d.ts.map +1 -1
- package/dist/web/web-management-client.js +8 -0
- package/dist/web/web-management-client.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +7 -0
- package/dist/worker.js.map +1 -1
- 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
|
}
|
package/api/src/protocol.js
CHANGED
|
@@ -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." },
|
package/dist/blob-store.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|
package/dist/blob-store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"blob-store.d.ts","sourceRoot":"","sources":["../src/blob-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAEH,eAAe,EACf,0BAA0B,EAI7B,MAAM,qBAAqB,CAAC;AAO7B,OAAO,
|
|
1
|
+
{"version":3,"file":"blob-store.d.ts","sourceRoot":"","sources":["../src/blob-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAEH,eAAe,EACf,0BAA0B,EAI7B,MAAM,qBAAqB,CAAC;AAO7B,OAAO,EAGH,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAC9B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAEH,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EAErB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,qBAAqB,EAY1B,KAAK,aAAa,EACrB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,aAAa,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASzB;AA0CD;;;;;;;GAOG;AACH,MAAM,WAAW,4BAA4B;IACzC,eAAe,EAAE,eAAe,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,0BAA0B,GAAG,IAAI,CAAC;IACxD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,gBAAiB,YAAW,iBAAiB,EAAE,aAAa,EAAE,sBAAsB;IAC7F,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,UAAU,CAA2C;IAC7D,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,qBAAqB,CAA6B;gBAGtD,wBAAwB,EAAE,MAAM,GAAG,4BAA4B,EAC/D,aAAa,GAAE,MAA2B,EAC1C,eAAe,CAAC,EAAE,MAAM;IA6B5B;;;OAGG;IACG,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA+FjG;;;OAGG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8C/D;;;OAGG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoElE,kGAAkG;IAClG,OAAO,CAAC,YAAY;IAId,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAsD1F,4DAA4D;IACtD,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqBjE,2FAA2F;IACrF,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA0B9D;;;;;OAKG;IACG,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BvD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAK;IAChD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAqB;IAElE,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,iBAAiB;YAqBX,YAAY;IAWpB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAKxE,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA8H5G,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAkDxF,OAAO,CAAC,gBAAgB;IAMxB;;;OAGG;IACG,cAAc,CAChB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,MAAM,EACxB,WAAW,CAAC,EAAE,MAAM,EACpB,IAAI,GAAE,qBAA0B,GACjC,OAAO,CAAC,gBAAgB,CAAC;IAiB5B;;;;OAIG;IACG,sBAAsB,CACxB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,EACpB,IAAI,GAAE,qBAA0B,GACjC,OAAO,CAAC,gBAAgB,CAAC;IAiB5B;;;OAGG;IACG,YAAY,CACd,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EACnB,IAAI,GAAE,qBAA0B,GACjC,OAAO,CAAC,gBAAgB,CAAC;IAS5B;;;OAGG;IACG,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAsBtF,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC;IAqBnF,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAalG,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAYhF;;;OAGG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAqB7D,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAM3E;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK3E;;;;;;;;OAQG;IACH,sBAAsB,CAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,aAAa,SAAI,GAClB,MAAM;IAuCT;;;;OAIG;IACG,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,OAAO,CAAC,MAAM,CAAC;CAUpG;AA+BD;;;;;;;;GAQG;AACH,MAAM,WAAW,mBAAmB;IAChC;;;;;;;;;;;;OAYG;IACH,oCAAoC,CAAC,EAAE,MAAM,CAAC;IAC9C;;;;;;;;;;;;;;;OAeG;IACH,+BAA+B,CAAC,EAAE,MAAM,CAAC;IACzC,uEAAuE;IACvE,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,+BAA+B,CAAC,EAAE,MAAM,CAAC;IACzC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,sBAAsB,CAClC,GAAG,EAAE,mBAAmB,EACxB,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAA;CAAO,GACxC,gBAAgB,GAAG,IAAI,CA2CzB"}
|
package/dist/blob-store.js
CHANGED
|
@@ -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
|
-
|
|
328
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
521
|
-
|
|
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,
|