narraeon 0.8.0 → 0.8.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.
@@ -31,6 +31,9 @@ export class FileNativeSettingImprovementStore {
31
31
  createId() {
32
32
  return `setting-${randomUUID()}`;
33
33
  }
34
+ get sessionScope() {
35
+ return this.#sessionsRoot;
36
+ }
34
37
  recordGoogleHop(sessionId, requestId, hop) {
35
38
  assertSessionId(sessionId);
36
39
  return writeGoogleHop(join(this.#sessionsRoot, ".google-hops", sessionId), requestId, hop, () => stat(this.#path(sessionId)).then(() => true, () => false));
@@ -84,10 +87,65 @@ export class FileNativeSettingImprovementStore {
84
87
  save(session) {
85
88
  return this.#mutate(async () => {
86
89
  validateStoredSession(session);
90
+ await this.#preservePreviousSchema(session);
87
91
  await publishJson(this.#path(session.sessionId), session);
88
92
  await this.#recordSavedSession(session);
89
93
  });
90
94
  }
95
+ async preparePendingEdit(session) {
96
+ if (session.pendingEdit === undefined)
97
+ throw new Error("No pending setting conversation edit");
98
+ await publishJson(this.#pendingEditPath(session.packageId), {
99
+ schemaVersion: 1,
100
+ packageId: session.packageId,
101
+ sessionId: session.sessionId,
102
+ });
103
+ await this.save(session);
104
+ }
105
+ async recoverPendingEdits(packageId, lease) {
106
+ const path = this.#pendingEditPath(packageId);
107
+ let marker;
108
+ try {
109
+ marker = JSON.parse(await readFile(path, "utf8"));
110
+ }
111
+ catch (error) {
112
+ if (isNodeError(error) && error.code === "ENOENT")
113
+ return;
114
+ throw error;
115
+ }
116
+ if (!isRecord(marker) ||
117
+ !hasExactKeys(marker, ["schemaVersion", "packageId", "sessionId"]) ||
118
+ marker.schemaVersion !== 1 ||
119
+ marker.packageId !== packageId ||
120
+ typeof marker.sessionId !== "string")
121
+ throw new Error("Pending authoring edit marker is damaged");
122
+ assertSessionId(marker.sessionId);
123
+ const raw = JSON.parse(await readFile(this.#path(marker.sessionId), "utf8"));
124
+ const session = validateStoredSession(raw);
125
+ if (session.sessionId !== marker.sessionId ||
126
+ session.packageId !== packageId)
127
+ throw new Error("Pending edit identity does not match its package");
128
+ if (session.pendingEdit === undefined) {
129
+ await rm(path, { force: true });
130
+ return;
131
+ }
132
+ await this.finishPendingEdit(session, lease);
133
+ }
134
+ async finishPendingEdit(session, lease) {
135
+ const pending = session.pendingEdit;
136
+ if (pending === undefined)
137
+ throw new Error("No pending setting conversation edit");
138
+ const fingerprint = contentTreeFingerprint(lease.package.files);
139
+ if (fingerprint === pending.beforeFingerprint &&
140
+ fingerprint !== pending.afterFingerprint)
141
+ await lease.replace(pending.afterFiles);
142
+ else if (fingerprint !== pending.afterFingerprint)
143
+ throw new Error("Cannot recover the conversation edit because the content package changed; no further changes were made");
144
+ await publishJson(this.#path(session.sessionId), pending.next);
145
+ await rm(this.#pendingEditPath(session.packageId), { force: true });
146
+ await this.#recordSavedSession(pending.next);
147
+ return structuredClone(pending.next);
148
+ }
91
149
  saveIfUnchanged(expected, replacement) {
92
150
  return this.#mutate(async () => {
93
151
  if (expected.sessionId !== replacement.sessionId)
@@ -96,6 +154,7 @@ export class FileNativeSettingImprovementStore {
96
154
  const current = await this.#read(expected.sessionId);
97
155
  if (!isDeepStrictEqual(current, expected))
98
156
  return false;
157
+ await this.#preservePreviousSchema(replacement);
99
158
  await publishJson(this.#path(replacement.sessionId), replacement);
100
159
  await this.#recordSavedSession(replacement);
101
160
  return true;
@@ -114,6 +173,8 @@ export class FileNativeSettingImprovementStore {
114
173
  await rm(join(this.#sessionsRoot, `${sessionId}.schema-v1.json`), {
115
174
  force: true,
116
175
  });
176
+ for (const version of [2, 3, 4])
177
+ await rm(join(this.#sessionsRoot, `${sessionId}.schema-v${version}.json`), { force: true });
117
178
  await this.#recordDeletedSession(session);
118
179
  });
119
180
  }
@@ -184,7 +245,9 @@ export class FileNativeSettingImprovementStore {
184
245
  session.googleIdentity?.sessionId !== sessionId ||
185
246
  session.googleIdentity.packageId !== session.packageId ||
186
247
  session.googleIdentity.creationRequestId !== session.creationRequestId ||
187
- session.requests?.[0]?.requestId !== session.creationRequestId ||
248
+ (session.requests?.[0]?.requestId !== session.creationRequestId &&
249
+ !(session.schemaVersion === 5 &&
250
+ session.editReceipts?.some((receipt) => receipt.requestId === session.requests?.[0]?.requestId))) ||
188
251
  !/^package-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(session.packageId) ||
189
252
  !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(session.creationRequestId) ||
190
253
  hasPendingToolCalls(session))
@@ -292,6 +355,27 @@ export class FileNativeSettingImprovementStore {
292
355
  request.wireRequest = await readGoogleHops(join(this.#sessionsRoot, ".google-hops", sessionId), request.requestId, request.wireRequest);
293
356
  return session;
294
357
  }
358
+ async #preservePreviousSchema(next) {
359
+ if (next.schemaVersion < 4)
360
+ return;
361
+ let source;
362
+ try {
363
+ source = await readFile(this.#path(next.sessionId), "utf8");
364
+ }
365
+ catch (error) {
366
+ if (isNodeError(error) && error.code === "ENOENT")
367
+ return;
368
+ throw error;
369
+ }
370
+ const previous = validateStoredSession(JSON.parse(source));
371
+ if (previous.sessionId !== next.sessionId)
372
+ throw new Error("Setting-improvement conversation identity does not match its file");
373
+ if (previous.schemaVersion === next.schemaVersion)
374
+ return;
375
+ if (previous.schemaVersion > next.schemaVersion)
376
+ throw new Error("Cannot downgrade a setting-improvement conversation");
377
+ await publishImmutableSource(join(this.#sessionsRoot, `${next.sessionId}.schema-v${previous.schemaVersion}.json`), source);
378
+ }
295
379
  async #migrateLegacy(sessionId, source, value) {
296
380
  if (!validLegacySession(value))
297
381
  throw new Error("Legacy setting-improvement conversation is damaged");
@@ -338,6 +422,11 @@ export class FileNativeSettingImprovementStore {
338
422
  assertSessionId(sessionId);
339
423
  return join(this.#sessionsRoot, `${sessionId}.json`);
340
424
  }
425
+ #pendingEditPath(packageId) {
426
+ if (!/^package-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(packageId))
427
+ throw new Error("Invalid content package ID");
428
+ return join(this.#sessionsRoot, ".pending-edits", `${packageId}.json`);
429
+ }
341
430
  }
342
431
  function compareSummaries(left, right) {
343
432
  return (right.updatedAt - left.updatedAt ||
@@ -380,7 +469,8 @@ export function summarizeStoredSettingImprovementSession(session) {
380
469
  changedFileCount: changedPaths.size,
381
470
  exchange: session.exchange,
382
471
  toolCalls: session.toolCalls,
383
- needsRecovery: session.pendingSettlement !== null ||
472
+ needsRecovery: session.pendingEdit !== undefined ||
473
+ session.pendingSettlement !== null ||
384
474
  session.runStatus === "running" ||
385
475
  hasPendingToolCalls(session),
386
476
  };
@@ -439,15 +529,43 @@ function validateStoredSession(value) {
439
529
  "requests",
440
530
  "googleOutcomeUnknown",
441
531
  "googleIdentity",
532
+ "retryDispatch",
533
+ "pendingEdit",
534
+ "editReceipts",
442
535
  ]) ||
443
- (value.schemaVersion !== 2 && value.schemaVersion !== 3) ||
536
+ (value.schemaVersion !== 2 &&
537
+ value.schemaVersion !== 3 &&
538
+ value.schemaVersion !== 4 &&
539
+ value.schemaVersion !== 5) ||
540
+ (value.schemaVersion !== 4 &&
541
+ value.schemaVersion !== 5 &&
542
+ value.retryDispatch !== undefined) ||
543
+ (value.schemaVersion !== 5 && value.pendingEdit !== undefined) ||
544
+ (value.schemaVersion !== 5 && value.editReceipts !== undefined) ||
545
+ (value.editReceipts !== undefined &&
546
+ !validEditReceipts(value.editReceipts)) ||
547
+ (value.pendingEdit !== undefined &&
548
+ !validPendingEdit(value.pendingEdit, value)) ||
444
549
  (value.schemaVersion === 2
445
550
  ? value.requests !== undefined
446
- : !validAuthoringRequests(value.requests, validPromptCompilation, {
447
- modelItems: value.modelItems,
448
- activeRequestId: value.activeRequestId,
449
- completedRequestIds: value.completedRequestIds,
450
- })) ||
551
+ : (value.schemaVersion === 4 || value.schemaVersion === 5) &&
552
+ Array.isArray(value.requests) &&
553
+ value.requests.length === 0 &&
554
+ ((value.schemaVersion === 5 && value.pendingEdit !== undefined) ||
555
+ (value.runStatus === "ready" &&
556
+ Array.isArray(value.modelItems) &&
557
+ value.modelItems.length === 0 &&
558
+ Array.isArray(value.messages) &&
559
+ value.messages.length === 0 &&
560
+ value.activeRequestId === null &&
561
+ Array.isArray(value.completedRequestIds) &&
562
+ value.completedRequestIds.length === 0))
563
+ ? false
564
+ : !validAuthoringRequests(value.requests, validPromptCompilation, {
565
+ modelItems: value.modelItems,
566
+ activeRequestId: value.activeRequestId,
567
+ completedRequestIds: value.completedRequestIds,
568
+ })) ||
451
569
  typeof value.sessionId !== "string" ||
452
570
  typeof value.packageId !== "string" ||
453
571
  !validOptionalTitle(value.contentPackageTitle) ||
@@ -458,6 +576,27 @@ function validateStoredSession(value) {
458
576
  typeof value.creationRequestId !== "string" ||
459
577
  !validPromptCompilation(value.bootstrap) ||
460
578
  !validModelBinding(value.modelBinding) ||
579
+ (value.retryDispatch !== undefined &&
580
+ (!isRecord(value.retryDispatch) ||
581
+ !hasExactKeys(value.retryDispatch, [
582
+ "requestId",
583
+ "exchange",
584
+ "modelItemCount",
585
+ "state",
586
+ ]) ||
587
+ typeof value.retryDispatch.requestId !== "string" ||
588
+ !validCount(value.retryDispatch.exchange) ||
589
+ !validCount(value.retryDispatch.modelItemCount) ||
590
+ !["dispatching", "retryable", "blocked"].includes(String(value.retryDispatch.state)) ||
591
+ (value.schemaVersion !== 4 && value.schemaVersion !== 5) ||
592
+ lastRequestId(value.requests) !== value.retryDispatch.requestId ||
593
+ (value.runStatus !== "running" && value.runStatus !== "interrupted") ||
594
+ value.pendingSettlement !== null ||
595
+ !validCount(value.exchange) ||
596
+ (value.retryDispatch.exchange !== value.exchange &&
597
+ value.retryDispatch.exchange !== Number(value.exchange) + 1) ||
598
+ !Array.isArray(value.modelItems) ||
599
+ value.retryDispatch.modelItemCount !== value.modelItems.length)) ||
461
600
  (value.playPreset !== undefined &&
462
601
  !isPlayPresetBinding(value.playPreset)) ||
463
602
  !Array.isArray(value.modelItems) ||
@@ -502,6 +641,69 @@ function validateStoredSession(value) {
502
641
  throw new Error("Setting-improvement transcript is damaged");
503
642
  return structuredClone(value);
504
643
  }
644
+ function lastRequestId(requests) {
645
+ if (!Array.isArray(requests))
646
+ return undefined;
647
+ const last = requests.at(-1);
648
+ return isRecord(last) ? last.requestId : undefined;
649
+ }
650
+ function validEditReceipts(value) {
651
+ if (!Array.isArray(value))
652
+ return false;
653
+ const ids = new Set();
654
+ return value.every((entry) => {
655
+ if (!isRecord(entry) ||
656
+ !hasExactKeys(entry, [
657
+ "requestId",
658
+ "messageId",
659
+ "message",
660
+ "filePolicy",
661
+ ]) ||
662
+ typeof entry.requestId !== "string" ||
663
+ !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(entry.requestId) ||
664
+ ids.has(entry.requestId) ||
665
+ typeof entry.messageId !== "string" ||
666
+ typeof entry.message !== "string" ||
667
+ entry.message.trim().length === 0 ||
668
+ entry.message.length > 64 * 1024 ||
669
+ (entry.filePolicy !== "keep" && entry.filePolicy !== "revert"))
670
+ return false;
671
+ ids.add(entry.requestId);
672
+ return true;
673
+ });
674
+ }
675
+ function validPendingEdit(value, original) {
676
+ if (!isRecord(value) ||
677
+ !hasExactKeys(value, [
678
+ "beforeFingerprint",
679
+ "afterFingerprint",
680
+ "afterFiles",
681
+ "next",
682
+ ]) ||
683
+ typeof value.beforeFingerprint !== "string" ||
684
+ !/^[0-9a-f]{64}$/u.test(value.beforeFingerprint) ||
685
+ typeof value.afterFingerprint !== "string" ||
686
+ !/^[0-9a-f]{64}$/u.test(value.afterFingerprint) ||
687
+ !validContentTreeFiles(value.afterFiles) ||
688
+ contentTreeFingerprint(value.afterFiles) !== value.afterFingerprint ||
689
+ !isRecord(value.next) ||
690
+ value.next.pendingEdit !== undefined ||
691
+ value.next.schemaVersion !== 5 ||
692
+ value.next.sessionId !== original.sessionId ||
693
+ value.next.packageId !== original.packageId ||
694
+ value.next.creationRequestId !== original.creationRequestId ||
695
+ value.next.runStatus !== "running" ||
696
+ value.next.activeRequestId === null ||
697
+ original.pendingSettlement !== null)
698
+ return false;
699
+ try {
700
+ validateStoredSession(value.next);
701
+ return true;
702
+ }
703
+ catch {
704
+ return false;
705
+ }
706
+ }
505
707
  function migrateLegacySession(value, applyOutcome) {
506
708
  if (!validLegacySession(value))
507
709
  throw new Error("Legacy setting-improvement conversation is damaged");