@gajae-code/coding-agent 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/dist/types/config/model-registry.d.ts +17 -10
- package/dist/types/config/models-config-schema.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +5 -0
- package/dist/types/edit/diff.d.ts +16 -0
- package/dist/types/edit/modes/replace.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
- package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
- package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
- package/dist/types/extensibility/skills.d.ts +9 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -2
- package/dist/types/harness-control-plane/storage.d.ts +7 -0
- package/dist/types/lsp/client.d.ts +1 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +19 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
- package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/sdk.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +10 -0
- package/dist/types/session/blob-store.d.ts +17 -0
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-storage.d.ts +6 -0
- package/dist/types/skill-state/active-state.d.ts +13 -0
- package/dist/types/thinking.d.ts +3 -2
- package/dist/types/tools/index.d.ts +3 -0
- package/package.json +9 -7
- package/src/cli.ts +14 -0
- package/src/commands/harness.ts +192 -7
- package/src/commands/ultragoal.ts +1 -21
- package/src/config/model-equivalence.ts +1 -1
- package/src/config/model-registry.ts +32 -5
- package/src/config/models-config-schema.ts +7 -2
- package/src/config/settings-schema.ts +4 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +19 -23
- package/src/defaults/gjc/skills/ralplan/SKILL.md +7 -7
- package/src/discovery/claude-plugins.ts +25 -5
- package/src/edit/diff.ts +64 -1
- package/src/edit/modes/replace.ts +60 -2
- package/src/extensibility/gjc-plugins/activation.ts +87 -0
- package/src/extensibility/gjc-plugins/index.ts +9 -0
- package/src/extensibility/gjc-plugins/injection.ts +114 -0
- package/src/extensibility/gjc-plugins/loader.ts +131 -0
- package/src/extensibility/gjc-plugins/paths.ts +66 -0
- package/src/extensibility/gjc-plugins/schema.ts +79 -0
- package/src/extensibility/gjc-plugins/state.ts +29 -0
- package/src/extensibility/gjc-plugins/tools.ts +47 -0
- package/src/extensibility/gjc-plugins/types.ts +97 -0
- package/src/extensibility/gjc-plugins/validation.ts +76 -0
- package/src/extensibility/skills.ts +39 -7
- package/src/gjc-runtime/state-runtime.ts +93 -2
- package/src/gjc-runtime/state-writer.ts +17 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
- package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
- package/src/gjc-runtime/workflow-manifest.ts +2 -2
- package/src/harness-control-plane/storage.ts +144 -2
- package/src/hashline/hash.ts +23 -0
- package/src/hooks/skill-state.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +7 -0
- package/src/modes/acp/acp-agent.ts +25 -2
- package/src/modes/bridge/bridge-mode.ts +124 -2
- package/src/modes/controllers/input-controller.ts +14 -2
- package/src/modes/prompt-action-autocomplete.ts +49 -10
- package/src/modes/rpc/rpc-client.ts +79 -3
- package/src/modes/rpc/rpc-mode.ts +67 -0
- package/src/modes/rpc/rpc-types.ts +224 -2
- package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
- package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
- package/src/modes/shared/agent-wire/command-validation.ts +25 -1
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
- package/src/modes/shared/agent-wire/handshake.ts +43 -3
- package/src/modes/shared/agent-wire/protocol.ts +7 -0
- package/src/modes/shared/agent-wire/responses.ts +2 -2
- package/src/modes/shared/agent-wire/scopes.ts +2 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
- package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/system/system-prompt.md +9 -0
- package/src/runtime-mcp/client.ts +7 -4
- package/src/runtime-mcp/manager.ts +45 -13
- package/src/runtime-mcp/transports/http.ts +40 -14
- package/src/runtime-mcp/transports/stdio.ts +11 -10
- package/src/sdk.ts +47 -0
- package/src/session/agent-session.ts +211 -2
- package/src/session/blob-store.ts +84 -0
- package/src/session/messages.ts +3 -0
- package/src/session/session-manager.ts +390 -33
- package/src/session/session-storage.ts +26 -0
- package/src/setup/provider-onboarding.ts +2 -2
- package/src/skill-state/active-state.ts +89 -1
- package/src/task/discovery.ts +7 -1
- package/src/task/executor.ts +16 -2
- package/src/thinking.ts +8 -2
- package/src/tools/ask.ts +39 -9
- package/src/tools/index.ts +3 -0
- package/src/tools/skill.ts +15 -3
- package/src/utils/edit-mode.ts +1 -1
|
@@ -38,8 +38,12 @@ import {
|
|
|
38
38
|
externalizeImageDataUrlSync,
|
|
39
39
|
isBlobRef,
|
|
40
40
|
isImageDataUrl,
|
|
41
|
+
MemoryBlobStore,
|
|
41
42
|
resolveImageData,
|
|
43
|
+
resolveImageDataSync,
|
|
42
44
|
resolveImageDataUrl,
|
|
45
|
+
resolveImageDataUrlSync,
|
|
46
|
+
resolveTextBlobSync,
|
|
43
47
|
} from "./blob-store";
|
|
44
48
|
import {
|
|
45
49
|
type BashExecutionMessage,
|
|
@@ -825,23 +829,44 @@ export async function loadEntriesFromFile(
|
|
|
825
829
|
* Resolve blob references in loaded entries, restoring both session image blocks and persisted
|
|
826
830
|
* provider image URLs back to the inline data expected by downstream transports. Mutates entries in place.
|
|
827
831
|
*/
|
|
828
|
-
function hasImageUrl(value: unknown): value is { image_url: string } {
|
|
829
|
-
return typeof value === "object" && value !== null && "image_url" in value
|
|
832
|
+
function hasImageUrl(value: unknown): value is { image_url: string | { url?: string } } {
|
|
833
|
+
return typeof value === "object" && value !== null && "image_url" in value;
|
|
830
834
|
}
|
|
831
835
|
|
|
832
|
-
async function
|
|
836
|
+
async function resolvePersistedBlobRefs(value: unknown, blobStore: BlobStore, key?: string): Promise<void> {
|
|
833
837
|
if (Array.isArray(value)) {
|
|
834
|
-
await Promise.all(value.map(item =>
|
|
838
|
+
await Promise.all(value.map(item => resolvePersistedBlobRefs(item, blobStore, key)));
|
|
835
839
|
return;
|
|
836
840
|
}
|
|
837
841
|
|
|
838
842
|
if (typeof value !== "object" || value === null) return;
|
|
839
843
|
|
|
840
|
-
if (
|
|
841
|
-
value.
|
|
844
|
+
if (isImageBlock(value) && isBlobRef(value.data)) {
|
|
845
|
+
value.data = await resolveImageData(blobStore, value.data);
|
|
842
846
|
}
|
|
843
847
|
|
|
844
|
-
|
|
848
|
+
if (hasImageUrl(value)) {
|
|
849
|
+
if (typeof value.image_url === "string" && isBlobRef(value.image_url)) {
|
|
850
|
+
value.image_url = await resolveImageDataUrl(blobStore, value.image_url);
|
|
851
|
+
} else if (
|
|
852
|
+
typeof value.image_url === "object" &&
|
|
853
|
+
value.image_url !== null &&
|
|
854
|
+
typeof value.image_url.url === "string" &&
|
|
855
|
+
isBlobRef(value.image_url.url)
|
|
856
|
+
) {
|
|
857
|
+
value.image_url.url = await resolveImageDataUrl(blobStore, value.image_url.url);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
await Promise.all(
|
|
862
|
+
Object.entries(value).map(async ([childKey, item]) => {
|
|
863
|
+
if (childKey === "data" && typeof item === "string" && isBlobRef(item) && key !== TEXT_CONTENT_KEY) {
|
|
864
|
+
(value as Record<string, unknown>)[childKey] = await resolveImageDataUrl(blobStore, item);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
await resolvePersistedBlobRefs(item, blobStore, childKey);
|
|
868
|
+
}),
|
|
869
|
+
);
|
|
845
870
|
}
|
|
846
871
|
|
|
847
872
|
async function resolveBlobRefsInEntries(entries: FileEntry[], blobStore: BlobStore): Promise<void> {
|
|
@@ -869,7 +894,7 @@ async function resolveBlobRefsInEntries(entries: FileEntry[], blobStore: BlobSto
|
|
|
869
894
|
}
|
|
870
895
|
}
|
|
871
896
|
|
|
872
|
-
promises.push(
|
|
897
|
+
promises.push(resolvePersistedBlobRefs(entry, blobStore));
|
|
873
898
|
}
|
|
874
899
|
|
|
875
900
|
await Promise.all(promises);
|
|
@@ -1073,6 +1098,30 @@ const TRUNCATION_NOTICE = "\n\n[Session persistence truncated large content]";
|
|
|
1073
1098
|
/** Minimum base64 length to externalize to blob store (skip tiny inline images) */
|
|
1074
1099
|
const BLOB_EXTERNALIZE_THRESHOLD = 1024;
|
|
1075
1100
|
const TEXT_CONTENT_KEY = "content";
|
|
1101
|
+
const RESIDENT_BLOB_SENTINEL_KEY = "__gjcResidentBlob";
|
|
1102
|
+
type ResidentBlobKind = "text" | "imageUrl" | "imageData";
|
|
1103
|
+
interface ResidentBlobSentinel {
|
|
1104
|
+
[RESIDENT_BLOB_SENTINEL_KEY]: true;
|
|
1105
|
+
kind: ResidentBlobKind;
|
|
1106
|
+
ref: string;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function residentBlobSentinel(kind: ResidentBlobKind, ref: string): ResidentBlobSentinel {
|
|
1110
|
+
return { [RESIDENT_BLOB_SENTINEL_KEY]: true, kind, ref };
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function isResidentBlobSentinel(value: unknown): value is ResidentBlobSentinel {
|
|
1114
|
+
return (
|
|
1115
|
+
typeof value === "object" &&
|
|
1116
|
+
value !== null &&
|
|
1117
|
+
(value as { [RESIDENT_BLOB_SENTINEL_KEY]?: unknown })[RESIDENT_BLOB_SENTINEL_KEY] === true &&
|
|
1118
|
+
((value as { kind?: unknown }).kind === "text" ||
|
|
1119
|
+
(value as { kind?: unknown }).kind === "imageUrl" ||
|
|
1120
|
+
(value as { kind?: unknown }).kind === "imageData") &&
|
|
1121
|
+
typeof (value as { ref?: unknown }).ref === "string" &&
|
|
1122
|
+
isBlobRef((value as { ref: string }).ref)
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1076
1125
|
|
|
1077
1126
|
/**
|
|
1078
1127
|
* Recursively truncate large strings in an object for session persistence.
|
|
@@ -1104,10 +1153,138 @@ function isImageBlock(value: unknown): value is { type: "image"; data: string; m
|
|
|
1104
1153
|
);
|
|
1105
1154
|
}
|
|
1106
1155
|
|
|
1156
|
+
const RESIDENT_EXTERNALIZE_STRING_EXCLUDED_KEYS = new Set([
|
|
1157
|
+
"id",
|
|
1158
|
+
"type",
|
|
1159
|
+
"parentId",
|
|
1160
|
+
"timestamp",
|
|
1161
|
+
"role",
|
|
1162
|
+
"provider",
|
|
1163
|
+
"model",
|
|
1164
|
+
"api",
|
|
1165
|
+
"customType",
|
|
1166
|
+
"mode",
|
|
1167
|
+
"mimeType",
|
|
1168
|
+
"stopReason",
|
|
1169
|
+
"toolName",
|
|
1170
|
+
"targetId",
|
|
1171
|
+
"firstKeptEntryId",
|
|
1172
|
+
]);
|
|
1173
|
+
|
|
1174
|
+
function shouldExternalizeResidentString(key: string | undefined): boolean {
|
|
1175
|
+
return !key || !RESIDENT_EXTERNALIZE_STRING_EXCLUDED_KEYS.has(key);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function externalizeResidentValueSync(obj: unknown, blobStore: BlobStore, key?: string): unknown {
|
|
1179
|
+
if (obj === null || obj === undefined) return obj;
|
|
1180
|
+
if (typeof obj === "string") {
|
|
1181
|
+
if (key === "image_url" && isImageDataUrl(obj) && obj.length >= BLOB_EXTERNALIZE_THRESHOLD)
|
|
1182
|
+
return residentBlobSentinel("imageUrl", externalizeImageDataUrlSync(blobStore, obj));
|
|
1183
|
+
if (shouldExternalizeResidentString(key) && obj.length >= BLOB_EXTERNALIZE_THRESHOLD)
|
|
1184
|
+
return residentBlobSentinel("text", blobStore.putSync(Buffer.from(obj, "utf8")).ref);
|
|
1185
|
+
return obj;
|
|
1186
|
+
}
|
|
1187
|
+
if (Array.isArray(obj)) {
|
|
1188
|
+
let changed = false;
|
|
1189
|
+
const result: unknown[] = new Array(obj.length);
|
|
1190
|
+
for (let i = 0; i < obj.length; i++) {
|
|
1191
|
+
const item = obj[i];
|
|
1192
|
+
if (
|
|
1193
|
+
key === TEXT_CONTENT_KEY &&
|
|
1194
|
+
isImageBlock(item) &&
|
|
1195
|
+
!isBlobRef(item.data) &&
|
|
1196
|
+
item.data.length >= BLOB_EXTERNALIZE_THRESHOLD
|
|
1197
|
+
) {
|
|
1198
|
+
changed = true;
|
|
1199
|
+
result[i] = {
|
|
1200
|
+
...item,
|
|
1201
|
+
data: residentBlobSentinel("imageData", externalizeImageDataSync(blobStore, item.data)),
|
|
1202
|
+
};
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
const newItem = externalizeResidentValueSync(item, blobStore, key);
|
|
1206
|
+
if (newItem !== item) changed = true;
|
|
1207
|
+
result[i] = newItem;
|
|
1208
|
+
}
|
|
1209
|
+
return changed ? result : obj;
|
|
1210
|
+
}
|
|
1211
|
+
if (typeof obj === "object") {
|
|
1212
|
+
let changed = false;
|
|
1213
|
+
const entries: Array<readonly [string, unknown]> = [];
|
|
1214
|
+
for (const [childKey, value] of Object.entries(obj)) {
|
|
1215
|
+
const newValue = externalizeResidentValueSync(value, blobStore, childKey);
|
|
1216
|
+
if (newValue !== value) changed = true;
|
|
1217
|
+
entries.push([childKey, newValue]);
|
|
1218
|
+
}
|
|
1219
|
+
return changed ? Object.fromEntries(entries) : obj;
|
|
1220
|
+
}
|
|
1221
|
+
return obj;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function prepareEntryForResidentSync(entry: FileEntry, blobStore: BlobStore): FileEntry {
|
|
1225
|
+
return externalizeResidentValueSync(entry, blobStore) as FileEntry;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function materializeResidentValueSync(
|
|
1229
|
+
obj: unknown,
|
|
1230
|
+
blobStore: BlobStore,
|
|
1231
|
+
key?: string,
|
|
1232
|
+
cache = new Map<string, string>(),
|
|
1233
|
+
): unknown {
|
|
1234
|
+
if (obj === null || obj === undefined) return obj;
|
|
1235
|
+
if (typeof obj === "string") return obj;
|
|
1236
|
+
if (isResidentBlobSentinel(obj)) {
|
|
1237
|
+
const cacheKey = `${obj.kind}:${obj.ref}`;
|
|
1238
|
+
const cached = cache.get(cacheKey);
|
|
1239
|
+
if (cached !== undefined) return cached;
|
|
1240
|
+
const resolved =
|
|
1241
|
+
obj.kind === "imageUrl"
|
|
1242
|
+
? resolveImageDataUrlSync(blobStore, obj.ref)
|
|
1243
|
+
: obj.kind === "imageData"
|
|
1244
|
+
? resolveImageDataSync(blobStore, obj.ref)
|
|
1245
|
+
: resolveTextBlobSync(blobStore, obj.ref);
|
|
1246
|
+
cache.set(cacheKey, resolved);
|
|
1247
|
+
return resolved;
|
|
1248
|
+
}
|
|
1249
|
+
if (Array.isArray(obj)) {
|
|
1250
|
+
let changed = false;
|
|
1251
|
+
const result = obj.map(item => {
|
|
1252
|
+
const newItem = materializeResidentValueSync(item, blobStore, key, cache);
|
|
1253
|
+
if (newItem !== item) changed = true;
|
|
1254
|
+
return newItem;
|
|
1255
|
+
});
|
|
1256
|
+
return changed ? result : obj;
|
|
1257
|
+
}
|
|
1258
|
+
if (typeof obj === "object") {
|
|
1259
|
+
let changed = false;
|
|
1260
|
+
const entries = Object.entries(obj).map(([childKey, value]) => {
|
|
1261
|
+
const newValue = materializeResidentValueSync(value, blobStore, childKey, cache);
|
|
1262
|
+
if (newValue !== value) changed = true;
|
|
1263
|
+
return [childKey, newValue] as const;
|
|
1264
|
+
});
|
|
1265
|
+
return changed ? Object.fromEntries(entries) : obj;
|
|
1266
|
+
}
|
|
1267
|
+
return obj;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function materializeResidentEntrySync<T extends FileEntry | SessionEntry>(
|
|
1271
|
+
entry: T,
|
|
1272
|
+
blobStore: BlobStore,
|
|
1273
|
+
cache: Map<string, string>,
|
|
1274
|
+
): T {
|
|
1275
|
+
return materializeResidentValueSync(entry, blobStore, undefined, cache) as T;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function materializeResidentEntriesSync<T extends FileEntry | SessionEntry>(entries: T[], blobStore: BlobStore): T[] {
|
|
1279
|
+
const cache = new Map<string, string>();
|
|
1280
|
+
return entries.map(entry => materializeResidentEntrySync(entry, blobStore, cache));
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1107
1283
|
async function truncateForPersistence(obj: FileEntry, blobStore: BlobStore, key?: string): Promise<FileEntry>;
|
|
1108
1284
|
async function truncateForPersistence(obj: string, blobStore: BlobStore, key?: string): Promise<string>;
|
|
1109
1285
|
async function truncateForPersistence(obj: unknown[], blobStore: BlobStore, key?: string): Promise<unknown[]>;
|
|
1110
1286
|
async function truncateForPersistence(obj: object, blobStore: BlobStore, key?: string): Promise<object>;
|
|
1287
|
+
async function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string): Promise<unknown>;
|
|
1111
1288
|
async function truncateForPersistence(
|
|
1112
1289
|
obj: null | undefined,
|
|
1113
1290
|
blobStore: BlobStore,
|
|
@@ -1117,7 +1294,7 @@ async function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?:
|
|
|
1117
1294
|
if (obj === null || obj === undefined) return obj;
|
|
1118
1295
|
|
|
1119
1296
|
if (typeof obj === "string") {
|
|
1120
|
-
if (key === "image_url" && isImageDataUrl(obj)) {
|
|
1297
|
+
if ((key === "image_url" || key === "image_url.url") && isImageDataUrl(obj)) {
|
|
1121
1298
|
return externalizeImageDataUrl(blobStore, obj);
|
|
1122
1299
|
}
|
|
1123
1300
|
|
|
@@ -1139,7 +1316,9 @@ async function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?:
|
|
|
1139
1316
|
let changed = false;
|
|
1140
1317
|
const result = await Promise.all(
|
|
1141
1318
|
obj.map(async item => {
|
|
1142
|
-
//
|
|
1319
|
+
// Keep durable JSONL bounded and lossless for large images. Resident
|
|
1320
|
+
// sentinels are materialized before this serializer runs, so persistence
|
|
1321
|
+
// still owns the existing blob-ref-on-disk contract.
|
|
1143
1322
|
if (key === TEXT_CONTENT_KEY && isImageBlock(item)) {
|
|
1144
1323
|
if (!isBlobRef(item.data) && item.data.length >= BLOB_EXTERNALIZE_THRESHOLD) {
|
|
1145
1324
|
changed = true;
|
|
@@ -1147,7 +1326,6 @@ async function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?:
|
|
|
1147
1326
|
return { ...item, data: blobRef };
|
|
1148
1327
|
}
|
|
1149
1328
|
}
|
|
1150
|
-
|
|
1151
1329
|
const newItem = await truncateForPersistence(item, blobStore, key);
|
|
1152
1330
|
if (newItem !== item) changed = true;
|
|
1153
1331
|
return newItem;
|
|
@@ -1170,6 +1348,30 @@ async function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?:
|
|
|
1170
1348
|
|
|
1171
1349
|
return [
|
|
1172
1350
|
(async () => {
|
|
1351
|
+
if (
|
|
1352
|
+
childKey === "image_url" &&
|
|
1353
|
+
typeof value === "object" &&
|
|
1354
|
+
value !== null &&
|
|
1355
|
+
typeof (value as { url?: unknown }).url === "string"
|
|
1356
|
+
) {
|
|
1357
|
+
let imageUrlChanged = false;
|
|
1358
|
+
const imageUrlEntries = await Promise.all(
|
|
1359
|
+
Object.entries(value).map(async ([imageUrlKey, imageUrlValue]) => {
|
|
1360
|
+
const persistenceKey = imageUrlKey === "url" ? "image_url.url" : imageUrlKey;
|
|
1361
|
+
const newImageUrlValue = await truncateForPersistence(
|
|
1362
|
+
imageUrlValue,
|
|
1363
|
+
blobStore,
|
|
1364
|
+
persistenceKey,
|
|
1365
|
+
);
|
|
1366
|
+
if (newImageUrlValue !== imageUrlValue) imageUrlChanged = true;
|
|
1367
|
+
return [imageUrlKey, newImageUrlValue] as const;
|
|
1368
|
+
}),
|
|
1369
|
+
);
|
|
1370
|
+
if (imageUrlChanged) {
|
|
1371
|
+
changed = true;
|
|
1372
|
+
return [childKey, Object.fromEntries(imageUrlEntries)] as const;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1173
1375
|
const newValue = await truncateForPersistence(value, blobStore, childKey);
|
|
1174
1376
|
if (newValue !== value) changed = true;
|
|
1175
1377
|
return [childKey, newValue] as const;
|
|
@@ -1219,7 +1421,7 @@ function truncateForPersistenceSync(obj: unknown, blobStore: BlobStore, key?: st
|
|
|
1219
1421
|
if (obj === null || obj === undefined) return obj;
|
|
1220
1422
|
|
|
1221
1423
|
if (typeof obj === "string") {
|
|
1222
|
-
if (key === "image_url" && isImageDataUrl(obj)) {
|
|
1424
|
+
if ((key === "image_url" || key === "image_url.url") && isImageDataUrl(obj)) {
|
|
1223
1425
|
return externalizeImageDataUrlSync(blobStore, obj);
|
|
1224
1426
|
}
|
|
1225
1427
|
if (obj.length > MAX_PERSIST_CHARS) {
|
|
@@ -1260,6 +1462,25 @@ function truncateForPersistenceSync(obj: unknown, blobStore: BlobStore, key?: st
|
|
|
1260
1462
|
changed = true;
|
|
1261
1463
|
continue;
|
|
1262
1464
|
}
|
|
1465
|
+
if (
|
|
1466
|
+
childKey === "image_url" &&
|
|
1467
|
+
typeof value === "object" &&
|
|
1468
|
+
value !== null &&
|
|
1469
|
+
typeof (value as { url?: unknown }).url === "string"
|
|
1470
|
+
) {
|
|
1471
|
+
let imageUrlChanged = false;
|
|
1472
|
+
const imageUrlEntries = Object.entries(value).map(([imageUrlKey, imageUrlValue]) => {
|
|
1473
|
+
const persistenceKey = imageUrlKey === "url" ? "image_url.url" : imageUrlKey;
|
|
1474
|
+
const newImageUrlValue = truncateForPersistenceSync(imageUrlValue, blobStore, persistenceKey);
|
|
1475
|
+
if (newImageUrlValue !== imageUrlValue) imageUrlChanged = true;
|
|
1476
|
+
return [imageUrlKey, newImageUrlValue] as const;
|
|
1477
|
+
});
|
|
1478
|
+
if (imageUrlChanged) {
|
|
1479
|
+
changed = true;
|
|
1480
|
+
entries.push([childKey, Object.fromEntries(imageUrlEntries)]);
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1263
1484
|
const newValue = truncateForPersistenceSync(value, blobStore, childKey);
|
|
1264
1485
|
if (newValue !== value) changed = true;
|
|
1265
1486
|
entries.push([childKey, newValue]);
|
|
@@ -1430,6 +1651,13 @@ class NdjsonFileWriter {
|
|
|
1430
1651
|
isOpen(): boolean {
|
|
1431
1652
|
return !this.#closed && !this.#closing;
|
|
1432
1653
|
}
|
|
1654
|
+
|
|
1655
|
+
closeSync(): void {
|
|
1656
|
+
if (this.#closed) return;
|
|
1657
|
+
this.#closed = true;
|
|
1658
|
+
this.#closing = true;
|
|
1659
|
+
this.#writer.close().catch(() => {});
|
|
1660
|
+
}
|
|
1433
1661
|
}
|
|
1434
1662
|
|
|
1435
1663
|
/** Get recent sessions for display in welcome screen */
|
|
@@ -1820,6 +2048,7 @@ export class SessionManager {
|
|
|
1820
2048
|
#inMemoryArtifacts: Map<string, string> | null = null;
|
|
1821
2049
|
#inMemoryArtifactCounter = 0;
|
|
1822
2050
|
readonly #blobStore: BlobStore;
|
|
2051
|
+
readonly #residentBlobStore = new MemoryBlobStore();
|
|
1823
2052
|
|
|
1824
2053
|
private constructor(
|
|
1825
2054
|
private cwd: string,
|
|
@@ -1827,7 +2056,7 @@ export class SessionManager {
|
|
|
1827
2056
|
private readonly persist: boolean,
|
|
1828
2057
|
private readonly storage: SessionStorage,
|
|
1829
2058
|
) {
|
|
1830
|
-
this.#blobStore = new BlobStore(getBlobsDir());
|
|
2059
|
+
this.#blobStore = persist ? new BlobStore(getBlobsDir()) : this.#residentBlobStore;
|
|
1831
2060
|
if (persist && sessionDir) {
|
|
1832
2061
|
this.storage.ensureDirSync(sessionDir);
|
|
1833
2062
|
}
|
|
@@ -1900,8 +2129,11 @@ export class SessionManager {
|
|
|
1900
2129
|
this.#titleSource = header?.titleSource;
|
|
1901
2130
|
|
|
1902
2131
|
this.#needsFullRewriteOnNextPersist = migrateToCurrentVersion(this.#fileEntries);
|
|
1903
|
-
|
|
1904
2132
|
await resolveBlobRefsInEntries(this.#fileEntries, this.#blobStore);
|
|
2133
|
+
|
|
2134
|
+
this.#fileEntries = this.#fileEntries.map(entry =>
|
|
2135
|
+
prepareEntryForResidentSync(entry, this.#residentBlobStore),
|
|
2136
|
+
);
|
|
1905
2137
|
this.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
1906
2138
|
|
|
1907
2139
|
this.#buildIndex();
|
|
@@ -2212,6 +2444,14 @@ export class SessionManager {
|
|
|
2212
2444
|
this.#persistWriterPath = undefined;
|
|
2213
2445
|
}
|
|
2214
2446
|
|
|
2447
|
+
#closePersistWriterInternalSync(): void {
|
|
2448
|
+
if (this.#persistWriter) {
|
|
2449
|
+
this.#persistWriter.closeSync();
|
|
2450
|
+
this.#persistWriter = undefined;
|
|
2451
|
+
}
|
|
2452
|
+
this.#persistWriterPath = undefined;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2215
2455
|
async #closePersistWriter(): Promise<void> {
|
|
2216
2456
|
await this.#queuePersistTask(
|
|
2217
2457
|
async () => {
|
|
@@ -2227,6 +2467,49 @@ export class SessionManager {
|
|
|
2227
2467
|
// shared `*.bak` glob on both real and in-memory storage backends and promote it back to
|
|
2228
2468
|
// the primary on the next session-dir scan.
|
|
2229
2469
|
|
|
2470
|
+
#replaceSessionFileAfterEpermSync(tempPath: string, targetPath: string, renameError: unknown): void {
|
|
2471
|
+
const dir = path.resolve(targetPath, "..");
|
|
2472
|
+
const backupPath = path.join(dir, `${path.basename(targetPath)}.${Snowflake.next()}.bak`);
|
|
2473
|
+
try {
|
|
2474
|
+
this.storage.renameSync(targetPath, backupPath);
|
|
2475
|
+
} catch (err) {
|
|
2476
|
+
if (isEnoent(err)) {
|
|
2477
|
+
this.storage.renameSync(tempPath, targetPath);
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
throw toError(renameError);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
try {
|
|
2484
|
+
this.storage.renameSync(tempPath, targetPath);
|
|
2485
|
+
} catch (err) {
|
|
2486
|
+
const replaceError = toError(err);
|
|
2487
|
+
const originalError = toError(renameError);
|
|
2488
|
+
try {
|
|
2489
|
+
this.storage.renameSync(backupPath, targetPath);
|
|
2490
|
+
} catch (rollbackErr) {
|
|
2491
|
+
const rollbackError = toError(rollbackErr);
|
|
2492
|
+
throw new Error(
|
|
2493
|
+
`Failed to replace session file after EPERM (original: ${originalError.message}; retry: ${replaceError.message}); rollback from ${backupPath} also failed: ${rollbackError.message}`,
|
|
2494
|
+
{ cause: originalError },
|
|
2495
|
+
);
|
|
2496
|
+
}
|
|
2497
|
+
throw replaceError;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
try {
|
|
2501
|
+
this.storage.unlinkSync(backupPath);
|
|
2502
|
+
} catch (err) {
|
|
2503
|
+
if (!isEnoent(err)) {
|
|
2504
|
+
logger.warn("Failed to remove session rewrite backup", {
|
|
2505
|
+
sessionFile: targetPath,
|
|
2506
|
+
backupPath,
|
|
2507
|
+
error: toError(err).message,
|
|
2508
|
+
});
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2230
2513
|
async #replaceSessionFileAfterEperm(tempPath: string, targetPath: string, renameError: unknown): Promise<void> {
|
|
2231
2514
|
const dir = path.resolve(targetPath, "..");
|
|
2232
2515
|
const backupPath = path.join(dir, `${path.basename(targetPath)}.${Snowflake.next()}.bak`);
|
|
@@ -2278,6 +2561,37 @@ export class SessionManager {
|
|
|
2278
2561
|
await this.#replaceSessionFileAfterEperm(tempPath, targetPath, err);
|
|
2279
2562
|
}
|
|
2280
2563
|
}
|
|
2564
|
+
|
|
2565
|
+
#replaceSessionFileSync(tempPath: string, targetPath: string): void {
|
|
2566
|
+
try {
|
|
2567
|
+
this.storage.renameSync(tempPath, targetPath);
|
|
2568
|
+
} catch (err) {
|
|
2569
|
+
if (hasFsCode(err, "EPERM")) {
|
|
2570
|
+
this.#replaceSessionFileAfterEpermSync(tempPath, targetPath, err);
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
throw toError(err);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
#writeEntriesAtomicallySync(entries: FileEntry[]): void {
|
|
2579
|
+
if (!this.#sessionFile) return;
|
|
2580
|
+
const dir = path.resolve(this.#sessionFile, "..");
|
|
2581
|
+
const tempPath = path.join(dir, `.${path.basename(this.#sessionFile)}.${Snowflake.next()}.tmp`);
|
|
2582
|
+
const writer = new NdjsonFileWriter(this.storage, tempPath, { flags: "w" });
|
|
2583
|
+
try {
|
|
2584
|
+
for (const entry of entries) {
|
|
2585
|
+
writer.writeSync(entry);
|
|
2586
|
+
}
|
|
2587
|
+
writer.closeSync();
|
|
2588
|
+
this.#replaceSessionFileSync(tempPath, this.#sessionFile);
|
|
2589
|
+
} catch (err) {
|
|
2590
|
+
writer.closeSync();
|
|
2591
|
+
void this.storage.unlink(tempPath).catch(() => {});
|
|
2592
|
+
throw toError(err);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2281
2595
|
async #writeEntriesAtomically(entries: FileEntry[]): Promise<void> {
|
|
2282
2596
|
if (!this.#sessionFile) return;
|
|
2283
2597
|
const dir = path.resolve(this.#sessionFile, "..");
|
|
@@ -2311,14 +2625,29 @@ export class SessionManager {
|
|
|
2311
2625
|
await this.#queuePersistTask(async () => {
|
|
2312
2626
|
await this.#closePersistWriterInternal();
|
|
2313
2627
|
const entries = await Promise.all(
|
|
2314
|
-
this.#fileEntries.map(entry =>
|
|
2628
|
+
materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStore).map(entry =>
|
|
2629
|
+
prepareEntryForPersistence(entry, this.#blobStore),
|
|
2630
|
+
),
|
|
2315
2631
|
);
|
|
2316
2632
|
await this.#writeEntriesAtomically(entries);
|
|
2317
2633
|
this.#needsFullRewriteOnNextPersist = false;
|
|
2318
2634
|
this.#flushed = true;
|
|
2635
|
+
this.#ensuredOnDisk = true;
|
|
2319
2636
|
});
|
|
2320
2637
|
}
|
|
2321
2638
|
|
|
2639
|
+
#rewriteFileSync(): void {
|
|
2640
|
+
if (!this.persist || !this.#sessionFile) return;
|
|
2641
|
+
this.#closePersistWriterInternalSync();
|
|
2642
|
+
const entries = materializeResidentEntriesSync(this.#fileEntries, this.#residentBlobStore).map(entry =>
|
|
2643
|
+
prepareEntryForPersistenceSync(entry, this.#blobStore),
|
|
2644
|
+
);
|
|
2645
|
+
this.#writeEntriesAtomicallySync(entries);
|
|
2646
|
+
this.#needsFullRewriteOnNextPersist = false;
|
|
2647
|
+
this.#flushed = true;
|
|
2648
|
+
this.#ensuredOnDisk = true;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2322
2651
|
isPersisted(): boolean {
|
|
2323
2652
|
return this.persist;
|
|
2324
2653
|
}
|
|
@@ -2580,6 +2909,7 @@ export class SessionManager {
|
|
|
2580
2909
|
if (!hasAssistant) {
|
|
2581
2910
|
// Mark as not flushed so when assistant arrives, all entries get written.
|
|
2582
2911
|
this.#flushed = false;
|
|
2912
|
+
this.#ensuredOnDisk = false;
|
|
2583
2913
|
return;
|
|
2584
2914
|
}
|
|
2585
2915
|
}
|
|
@@ -2590,7 +2920,12 @@ export class SessionManager {
|
|
|
2590
2920
|
// `#persistChain` → `#recordPersistError`; we swallow the rejection
|
|
2591
2921
|
// here to avoid an unhandled rejection when the persist dir races with
|
|
2592
2922
|
// test-level tempDir cleanup.
|
|
2593
|
-
|
|
2923
|
+
try {
|
|
2924
|
+
this.#rewriteFileSync();
|
|
2925
|
+
} catch (err) {
|
|
2926
|
+
this.#recordPersistError(err);
|
|
2927
|
+
throw this.#persistError ?? toError(err);
|
|
2928
|
+
}
|
|
2594
2929
|
return;
|
|
2595
2930
|
}
|
|
2596
2931
|
|
|
@@ -2609,18 +2944,21 @@ export class SessionManager {
|
|
|
2609
2944
|
this.#rewriteFile().catch(() => {});
|
|
2610
2945
|
return;
|
|
2611
2946
|
}
|
|
2612
|
-
const
|
|
2947
|
+
const materializedEntry = materializeResidentEntrySync(entry, this.#residentBlobStore, new Map());
|
|
2948
|
+
const persistedEntry = prepareEntryForPersistenceSync(materializedEntry, this.#blobStore);
|
|
2613
2949
|
writer.writeSync(persistedEntry);
|
|
2614
2950
|
} catch (err) {
|
|
2615
2951
|
this.#recordPersistError(err);
|
|
2952
|
+
throw this.#persistError ?? toError(err);
|
|
2616
2953
|
}
|
|
2617
2954
|
}
|
|
2618
2955
|
|
|
2619
2956
|
#appendEntry(entry: SessionEntry): void {
|
|
2620
|
-
this.#
|
|
2621
|
-
this.#
|
|
2622
|
-
this.#
|
|
2623
|
-
this.
|
|
2957
|
+
const residentEntry = prepareEntryForResidentSync(entry, this.#residentBlobStore) as SessionEntry;
|
|
2958
|
+
this.#fileEntries.push(residentEntry);
|
|
2959
|
+
this.#byId.set(residentEntry.id, residentEntry);
|
|
2960
|
+
this.#leafId = residentEntry.id;
|
|
2961
|
+
this._persist(residentEntry);
|
|
2624
2962
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
2625
2963
|
const usage = entry.message.usage;
|
|
2626
2964
|
this.#usageStatistics.input += usage.input;
|
|
@@ -2894,7 +3232,9 @@ export class SessionManager {
|
|
|
2894
3232
|
}
|
|
2895
3233
|
|
|
2896
3234
|
getLeafEntry(): SessionEntry | undefined {
|
|
2897
|
-
|
|
3235
|
+
if (!this.#leafId) return undefined;
|
|
3236
|
+
const entry = this.#byId.get(this.#leafId);
|
|
3237
|
+
return entry ? materializeResidentEntrySync(entry, this.#residentBlobStore, new Map()) : undefined;
|
|
2898
3238
|
}
|
|
2899
3239
|
|
|
2900
3240
|
/**
|
|
@@ -2913,17 +3253,19 @@ export class SessionManager {
|
|
|
2913
3253
|
}
|
|
2914
3254
|
|
|
2915
3255
|
getEntry(id: string): SessionEntry | undefined {
|
|
2916
|
-
|
|
3256
|
+
const entry = this.#byId.get(id);
|
|
3257
|
+
return entry ? materializeResidentEntrySync(entry, this.#residentBlobStore, new Map()) : undefined;
|
|
2917
3258
|
}
|
|
2918
3259
|
|
|
2919
3260
|
/**
|
|
2920
3261
|
* Get all direct children of an entry.
|
|
2921
3262
|
*/
|
|
2922
3263
|
getChildren(parentId: string): SessionEntry[] {
|
|
3264
|
+
const cache = new Map<string, string>();
|
|
2923
3265
|
const children: SessionEntry[] = [];
|
|
2924
3266
|
for (const entry of this.#byId.values()) {
|
|
2925
3267
|
if (entry.parentId === parentId) {
|
|
2926
|
-
children.push(entry);
|
|
3268
|
+
children.push(materializeResidentEntrySync(entry, this.#residentBlobStore, cache));
|
|
2927
3269
|
}
|
|
2928
3270
|
}
|
|
2929
3271
|
return children;
|
|
@@ -2968,11 +3310,12 @@ export class SessionManager {
|
|
|
2968
3310
|
* Use buildSessionContext() to get the resolved messages for the LLM.
|
|
2969
3311
|
*/
|
|
2970
3312
|
getBranch(fromId?: string): SessionEntry[] {
|
|
3313
|
+
const cache = new Map<string, string>();
|
|
2971
3314
|
const path: SessionEntry[] = [];
|
|
2972
3315
|
const startId = fromId ?? this.#leafId;
|
|
2973
3316
|
let current = startId ? this.#byId.get(startId) : undefined;
|
|
2974
3317
|
while (current) {
|
|
2975
|
-
path.unshift(current);
|
|
3318
|
+
path.unshift(materializeResidentEntrySync(current, this.#residentBlobStore, cache));
|
|
2976
3319
|
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
2977
3320
|
}
|
|
2978
3321
|
return path;
|
|
@@ -2983,7 +3326,7 @@ export class SessionManager {
|
|
|
2983
3326
|
* Uses tree traversal from current leaf.
|
|
2984
3327
|
*/
|
|
2985
3328
|
buildSessionContext(): SessionContext {
|
|
2986
|
-
return buildSessionContext(this.getEntries(), this.#leafId
|
|
3329
|
+
return buildSessionContext(this.getEntries(), this.#leafId);
|
|
2987
3330
|
}
|
|
2988
3331
|
|
|
2989
3332
|
/** Strip stale OpenAI Responses assistant replay metadata from loaded in-memory entries. */
|
|
@@ -3020,7 +3363,10 @@ export class SessionManager {
|
|
|
3020
3363
|
* change the leaf pointer. Entries cannot be modified or deleted.
|
|
3021
3364
|
*/
|
|
3022
3365
|
getEntries(): SessionEntry[] {
|
|
3023
|
-
return
|
|
3366
|
+
return materializeResidentEntriesSync(
|
|
3367
|
+
this.#fileEntries.filter((e): e is SessionEntry => e.type !== "session"),
|
|
3368
|
+
this.#residentBlobStore,
|
|
3369
|
+
);
|
|
3024
3370
|
}
|
|
3025
3371
|
|
|
3026
3372
|
/**
|
|
@@ -3159,7 +3505,7 @@ export class SessionManager {
|
|
|
3159
3505
|
const lines: string[] = [];
|
|
3160
3506
|
lines.push(JSON.stringify(header));
|
|
3161
3507
|
for (const entry of pathWithoutLabels) {
|
|
3162
|
-
lines.push(JSON.stringify(entry));
|
|
3508
|
+
lines.push(JSON.stringify(prepareEntryForPersistenceSync(entry, this.#blobStore)));
|
|
3163
3509
|
}
|
|
3164
3510
|
// Write fresh label entries at the end
|
|
3165
3511
|
const lastEntryId = pathWithoutLabels[pathWithoutLabels.length - 1]?.id || null;
|
|
@@ -3174,13 +3520,19 @@ export class SessionManager {
|
|
|
3174
3520
|
targetId,
|
|
3175
3521
|
label,
|
|
3176
3522
|
};
|
|
3177
|
-
lines.push(JSON.stringify(labelEntry));
|
|
3523
|
+
lines.push(JSON.stringify(prepareEntryForPersistenceSync(labelEntry, this.#blobStore)));
|
|
3178
3524
|
pathEntryIds.add(labelEntry.id);
|
|
3179
3525
|
labelEntries.push(labelEntry);
|
|
3180
3526
|
parentId = labelEntry.id;
|
|
3181
3527
|
}
|
|
3182
3528
|
this.storage.writeTextSync(newSessionFile, `${lines.join("\n")}\n`);
|
|
3183
|
-
this.#fileEntries = [
|
|
3529
|
+
this.#fileEntries = [
|
|
3530
|
+
header,
|
|
3531
|
+
...pathWithoutLabels.map(
|
|
3532
|
+
entry => prepareEntryForResidentSync(entry, this.#residentBlobStore) as SessionEntry,
|
|
3533
|
+
),
|
|
3534
|
+
...labelEntries,
|
|
3535
|
+
];
|
|
3184
3536
|
this.#sessionId = newSessionId;
|
|
3185
3537
|
this.#sessionFile = newSessionFile;
|
|
3186
3538
|
this.#flushed = true;
|
|
@@ -3203,7 +3555,11 @@ export class SessionManager {
|
|
|
3203
3555
|
labelEntries.push(labelEntry);
|
|
3204
3556
|
parentId = labelEntry.id;
|
|
3205
3557
|
}
|
|
3206
|
-
this.#fileEntries = [
|
|
3558
|
+
this.#fileEntries = [
|
|
3559
|
+
header,
|
|
3560
|
+
...pathWithoutLabels.map(entry => prepareEntryForResidentSync(entry, this.#residentBlobStore) as SessionEntry),
|
|
3561
|
+
...labelEntries,
|
|
3562
|
+
];
|
|
3207
3563
|
this.#sessionId = newSessionId;
|
|
3208
3564
|
this.#buildIndex();
|
|
3209
3565
|
return undefined;
|
|
@@ -3247,8 +3603,9 @@ export class SessionManager {
|
|
|
3247
3603
|
const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
|
|
3248
3604
|
migrateToCurrentVersion(forkEntries);
|
|
3249
3605
|
await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
|
|
3250
|
-
|
|
3251
|
-
const
|
|
3606
|
+
manager.#fileEntries = forkEntries.map(entry => prepareEntryForResidentSync(entry, manager.#residentBlobStore));
|
|
3607
|
+
const sourceHeader = manager.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
3608
|
+
const historyEntries = manager.#fileEntries.filter(entry => entry.type !== "session") as SessionEntry[];
|
|
3252
3609
|
manager.#newSessionSync({ parentSession: sourceHeader?.id });
|
|
3253
3610
|
const newHeader = manager.#fileEntries[0] as SessionHeader;
|
|
3254
3611
|
newHeader.title = sourceHeader?.title;
|