narraeon 0.6.0 → 0.6.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.
Files changed (41) hide show
  1. package/README.md +11 -0
  2. package/README.zh-CN.md +8 -0
  3. package/dist/node/protocol/v1.js +0 -2
  4. package/dist/node/runtime/FileNativeExclusiveLink.js +39 -0
  5. package/dist/node/runtime/V1Runtime.js +1 -5
  6. package/dist/node/runtime/archive/WorldArchives.js +14 -46
  7. package/dist/node/runtime/artifact/ArtifactStoreFiles.js +330 -0
  8. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +495 -1535
  9. package/dist/node/runtime/artifact/LegacyArtifactStoreMigration.js +260 -0
  10. package/dist/node/runtime/config/AppPreferencesStore.js +0 -1
  11. package/dist/node/runtime/config/ProtagonistTemplateStore.js +0 -1
  12. package/dist/node/runtime/config/dataRootLock.js +108 -0
  13. package/dist/node/runtime/content/ContentWorkspace.js +0 -10
  14. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +7 -44
  15. package/dist/node/runtime/extension/PackageScriptPermissions.js +0 -8
  16. package/dist/node/runtime/extension/WorldExtensionControls.js +0 -8
  17. package/dist/node/runtime/model/ModelConnectionStore.js +0 -1
  18. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +3 -14
  19. package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +32 -207
  20. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +29 -239
  21. package/dist/node/runtime/play/PlayCallChain.js +338 -366
  22. package/dist/node/runtime/play/PlayDocumentTools.js +8 -125
  23. package/dist/node/runtime/play/PlayFollowupRequests.js +2 -2
  24. package/dist/node/runtime/play/PlayTraceRetention.js +8 -36
  25. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +3 -15
  26. package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +213 -1033
  27. package/dist/node/runtime/world/FileNativeWorldStore.js +230 -781
  28. package/dist/node/runtime/world/{FileNativeAuthorityV3.js → LegacyAuthorityV3Reader.js} +13 -404
  29. package/dist/node/runtime/world/WorldActivity.js +14 -22
  30. package/dist/node/runtime/world/WorldDocumentMaintenance.js +2 -2
  31. package/dist/node/runtime/world/WorldLedgerAuthority.js +340 -0
  32. package/dist/node/runtime/world/WorldOperationCoordinator.js +59 -401
  33. package/dist/node/runtime/world/WorldQueue.js +46 -0
  34. package/dist/node/runtime/world/WorldTurnLedger.js +353 -0
  35. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +0 -12
  36. package/dist/node/server/mobile.js +20 -0
  37. package/dist/node/server/startWebServer.js +15 -8
  38. package/dist/web/assets/{index-BgBL8G-A.js → index-7GzLSUpU.js} +2 -2
  39. package/dist/web/index.html +1 -1
  40. package/package.json +2 -1
  41. package/dist/node/runtime/FileNativePhysicalClone.js +0 -86
@@ -4,60 +4,30 @@ import { isPlayFailureKind, } from "../../protocol/playFailure.js";
4
4
  import { isNarrativeOrigin, } from "../history/HistoryBackground.js";
5
5
  import { encodePlayPromptRun, decodePlayPromptRun, } from "./PlayPromptRun.js";
6
6
  import { createHash, randomUUID } from "node:crypto";
7
- import { link, mkdir, open, readFile, rename, rm, writeFile, } from "node:fs/promises";
7
+ import { constants } from "node:fs";
8
+ import { chmod, copyFile, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
9
+ import { linkExclusive } from "../FileNativeExclusiveLink.js";
8
10
  import { dirname, join, resolve } from "node:path";
9
11
  import { isDeepStrictEqual } from "node:util";
10
- import { cloneFilePhysically } from "../FileNativePhysicalClone.js";
11
12
  export class FileNativePlayTimelineStore {
12
13
  #worldsRoot;
13
14
  constructor(dataRoot) {
14
15
  this.#worldsRoot = join(resolve(dataRoot), "worlds-file-native");
15
16
  }
16
- /**
17
- * Convert the cumulative call-chain record written by every released
18
- * Narraeon version through v0.1.1 into immutable context segments. The
19
- * source digest makes interrupted retries reproduce the same generation and
20
- * identities; callers publish the world-wide storage marker only after this
21
- * method has completed.
22
- */
23
- async migrateReleasedRecord(worldId, value, sourceDigest) {
24
- assertIdentity(worldId, "World ID");
25
- if (!/^[a-f0-9]{64}$/u.test(sourceDigest))
26
- throw new TypeError("Released call-chain source digest is invalid");
27
- assertReleasedPersistedPlayCallChain(value, worldId);
28
- const contexts = [
29
- ...(value.previousContexts ?? []).map((context) => structuredClone(context)),
30
- releasedCurrentContext(value),
31
- ];
32
- const seen = new Set();
33
- const generation = `released:${sourceDigest}`;
34
- let previousChainId = null;
35
- for (const context of contexts) {
36
- assertIdentity(context.chainId, "Call-chain ID");
37
- if (seen.has(context.chainId))
38
- throw new Error("Released play-call-chain contexts contain a duplicate identity");
39
- seen.add(context.chainId);
40
- await this.persist({
41
- ...structuredClone(context),
42
- events: context.events.map(normalizePlayEvent),
43
- schemaVersion: 3,
44
- kind: "play_call_chain",
45
- worldId,
46
- previousChainId,
47
- timelineGeneration: generation,
48
- documentAuthorizationCheckpoints: structuredClone(context.documentAuthorizationCheckpoints ?? []),
49
- });
50
- previousChainId = context.chainId;
51
- }
52
- const migrated = await this.readAllContexts(worldId);
53
- if (migrated.length !== contexts.length ||
54
- migrated.some((context, index) => context.chainId !== contexts[index]?.chainId))
55
- throw new Error("Released play-call-chain migration did not reproduce its context chain");
56
- }
57
17
  async persist(value, cursor) {
58
18
  return this.#persistAtRuntimeRoot(this.#worldRuntimeRoot(value.worldId), value, cursor);
59
19
  }
60
20
  /** Write a sealed context segment without making it the active timeline. */
21
+ /** Physically remove the segments of contexts a rewind discarded. */
22
+ async discardContexts(worldId, chainIds) {
23
+ for (const chainId of chainIds) {
24
+ assertIdentity(chainId, "Call-chain ID");
25
+ await rm(this.#contextRoot(worldId, chainId), {
26
+ recursive: true,
27
+ force: true,
28
+ });
29
+ }
30
+ }
61
31
  async persistDetached(value, cursor) {
62
32
  return this.#persistAtRuntimeRoot(this.#worldRuntimeRoot(value.worldId), value, cursor, false);
63
33
  }
@@ -73,7 +43,6 @@ export class FileNativePlayTimelineStore {
73
43
  const transcriptCount = prefixLength(input.source.transcript, input.target.transcript);
74
44
  const eventCount = prefixLength(input.source.events, input.target.events);
75
45
  const completedToolCount = prefixLength(input.source.completedTools, input.target.completedTools);
76
- const authorizationCheckpointCount = prefixLength(input.source.documentAuthorizationCheckpoints ?? [], input.target.documentAuthorizationCheckpoints ?? []);
77
46
  if (input.retainCompleteContext ||
78
47
  input.target.events.some((event) => event.kind === "assistant" && event.status === "completed")) {
79
48
  const encoding = await this.readInitialEncoding(input.sourceWorldId, input.source.chainId).catch(() => null);
@@ -114,19 +83,11 @@ export class FileNativePlayTimelineStore {
114
83
  count: completedToolCount,
115
84
  immutable: true,
116
85
  }),
117
- cloneNumberedPrefix({
118
- sourceRoot,
119
- targetRoot,
120
- directory: "authorization",
121
- count: authorizationCheckpointCount,
122
- immutable: true,
123
- }),
124
86
  ]);
125
87
  return this.persistStaged(input.targetWorldRoot, input.target, {
126
88
  transcriptCount,
127
89
  eventCount,
128
90
  completedToolCount,
129
- authorizationCheckpointCount,
130
91
  });
131
92
  }
132
93
  async #persistAtRuntimeRoot(runtimeRoot, value, cursor, publishHead = true) {
@@ -156,7 +117,6 @@ export class FileNativePlayTimelineStore {
156
117
  eventCount: Math.min(supplied.eventCount, value.events.length),
157
118
  transcriptCount: Math.min(supplied.transcriptCount, value.transcript.length),
158
119
  completedToolCount: Math.min(supplied.completedToolCount, value.completedTools.length),
159
- authorizationCheckpointCount: Math.min(supplied.authorizationCheckpointCount, value.documentAuthorizationCheckpoints?.length ?? 0),
160
120
  };
161
121
  const lastRequestDigest = value.lastRequest === null
162
122
  ? null
@@ -187,9 +147,6 @@ export class FileNativePlayTimelineStore {
187
147
  }
188
148
  for (let index = previous.completedToolCount; index < value.completedTools.length; index += 1)
189
149
  await publishImmutableJson(join(root, "completed-tools", numbered(index + 1)), value.completedTools[index]);
190
- const checkpoints = value.documentAuthorizationCheckpoints ?? [];
191
- for (let index = previous.authorizationCheckpointCount; index < checkpoints.length; index += 1)
192
- await publishImmutableJson(join(root, "authorization", numbered(index + 1)), checkpoints[index]);
193
150
  const promptRuns = value.promptRuns ?? [];
194
151
  for (let index = supplied.promptRunCount ?? 0; index < promptRuns.length; index += 1) {
195
152
  await publishImmutableJson(join(root, "prompt-runs", numbered(index + 1)), encodePlayPromptRun(promptRuns[index]));
@@ -203,7 +160,6 @@ export class FileNativePlayTimelineStore {
203
160
  eventCount: value.events.length,
204
161
  transcriptCount: value.transcript.length,
205
162
  completedToolCount: value.completedTools.length,
206
- authorizationCheckpointCount: checkpoints.length,
207
163
  promptRunCount: promptRuns.length,
208
164
  changedDocuments: structuredClone(value.changedDocuments),
209
165
  nextEventId: value.nextEventId,
@@ -228,7 +184,6 @@ export class FileNativePlayTimelineStore {
228
184
  eventCount: value.events.length,
229
185
  transcriptCount: value.transcript.length,
230
186
  completedToolCount: value.completedTools.length,
231
- authorizationCheckpointCount: checkpoints.length,
232
187
  promptRunCount: promptRuns.length,
233
188
  };
234
189
  }
@@ -263,16 +218,14 @@ export class FileNativePlayTimelineStore {
263
218
  if (metadata === null)
264
219
  return null;
265
220
  const { index: indexValue, state: stateValue, root } = metadata;
266
- const [baseValue, continuationValue, transcript, rawEvents, completedTools, checkpoints, promptRuns,] = await Promise.all([
221
+ const [baseValue, continuationValue, transcript, events, completedTools, promptRuns,] = await Promise.all([
267
222
  readJson(join(root, "base.json")),
268
223
  readJson(join(root, "continuations", `${stateValue.continuationDigest}.json`)),
269
224
  readNumbered(join(root, "transcript"), stateValue.transcriptCount),
270
225
  readNumberedValidated(join(root, "events"), stateValue.eventCount, assertPlayEvent),
271
226
  readNumbered(join(root, "completed-tools"), stateValue.completedToolCount),
272
- readNumbered(join(root, "authorization"), stateValue.authorizationCheckpointCount),
273
227
  readNumbered(join(root, "prompt-runs"), stateValue.promptRunCount ?? 0).then((records) => records.map(decodePlayPromptRun)),
274
228
  ]);
275
- const events = rawEvents.map(normalizePlayEvent);
276
229
  assertContextBase(baseValue);
277
230
  assertContextContinuation(continuationValue);
278
231
  if (baseValue.worldId !== worldId ||
@@ -335,7 +288,6 @@ export class FileNativePlayTimelineStore {
335
288
  transcript,
336
289
  events,
337
290
  completedTools,
338
- documentAuthorizationCheckpoints: checkpoints,
339
291
  promptRuns,
340
292
  changedDocuments: structuredClone(stateValue.changedDocuments),
341
293
  nextMaterials: structuredClone(continuationValue.nextMaterials),
@@ -360,7 +312,6 @@ export class FileNativePlayTimelineStore {
360
312
  eventCount: stateValue.eventCount,
361
313
  transcriptCount: stateValue.transcriptCount,
362
314
  completedToolCount: stateValue.completedToolCount,
363
- authorizationCheckpointCount: stateValue.authorizationCheckpointCount,
364
315
  promptRunCount: stateValue.promptRunCount ?? 0,
365
316
  },
366
317
  };
@@ -547,7 +498,7 @@ export class FileNativePlayTimelineStore {
547
498
  assertPlayEvent(event);
548
499
  if (event.id !== eventId)
549
500
  throw new Error("Play timeline event identity is inconsistent");
550
- return projectLegacyModelBindingFailure(normalizePlayEvent(event));
501
+ return projectLegacyModelBindingFailure(event);
551
502
  }
552
503
  newGeneration() {
553
504
  return `timeline:${randomUUID()}`;
@@ -761,7 +712,6 @@ function emptyPersistenceCursor() {
761
712
  eventCount: 0,
762
713
  transcriptCount: 0,
763
714
  completedToolCount: 0,
764
- authorizationCheckpointCount: 0,
765
715
  };
766
716
  }
767
717
  function numbered(index) {
@@ -830,7 +780,6 @@ function assertContextState(value) {
830
780
  !validCount(value.eventCount) ||
831
781
  !validCount(value.transcriptCount) ||
832
782
  !validCount(value.completedToolCount) ||
833
- !validCount(value.authorizationCheckpointCount) ||
834
783
  (value.promptRunCount !== undefined && !validCount(value.promptRunCount)) ||
835
784
  !Array.isArray(value.changedDocuments) ||
836
785
  !validCount(value.nextEventId) ||
@@ -865,65 +814,6 @@ function assertTimelineHead(value) {
865
814
  typeof value.generation !== "string")
866
815
  throw new Error("Play timeline head has an invalid shape");
867
816
  }
868
- function assertReleasedPersistedPlayCallChain(value, worldId) {
869
- if (!isRecord(value) ||
870
- (value.schemaVersion !== 1 && value.schemaVersion !== 2) ||
871
- value.kind !== "play_call_chain" ||
872
- value.worldId !== worldId ||
873
- !isReleasedPlayContext(value) ||
874
- (value.previousContexts !== undefined &&
875
- (!Array.isArray(value.previousContexts) ||
876
- value.previousContexts.some((context) => !isReleasedPlayContext(context)))))
877
- throw new Error("Released play-call-chain durable data has an invalid shape");
878
- }
879
- function isReleasedPlayContext(value) {
880
- if (!isRecord(value))
881
- return false;
882
- if (typeof value.chainId !== "string" ||
883
- typeof value.baselineHead !== "string" ||
884
- (value.continuityContextId !== undefined &&
885
- (typeof value.continuityContextId !== "string" ||
886
- value.continuityContextId.length === 0)) ||
887
- (value.baselineHistoryLength !== undefined &&
888
- !validCount(value.baselineHistoryLength)) ||
889
- typeof value.parentHead !== "string" ||
890
- !validReleasedDerivedFrom(value.derivedFrom) ||
891
- !validReleasedBranchedBeforePlayer(value.branchedBeforePlayer) ||
892
- !isRecord(value.playPreset) ||
893
- (value.followups !== undefined && !Array.isArray(value.followups)) ||
894
- (value.playPresetScriptsEnabled !== undefined &&
895
- typeof value.playPresetScriptsEnabled !== "boolean") ||
896
- (value.modelBinding !== undefined &&
897
- !validModelHostBinding(value.modelBinding)) ||
898
- (value.status !== "ready" &&
899
- value.status !== "running" &&
900
- value.status !== "interrupted") ||
901
- typeof value.canRetry !== "boolean" ||
902
- !isRecord(value.bootstrap) ||
903
- !Array.isArray(value.tools) ||
904
- !Array.isArray(value.transcript) ||
905
- !Array.isArray(value.events) ||
906
- !Array.isArray(value.completedTools) ||
907
- !Array.isArray(value.changedDocuments) ||
908
- !Array.isArray(value.nextMaterials) ||
909
- !validCount(value.nextEventId) ||
910
- !validCount(value.exchange) ||
911
- (value.lastRequest !== null && !isRecord(value.lastRequest)) ||
912
- !validCount(value.lastRequestAttempt) ||
913
- (value.lastFailure !== null && typeof value.lastFailure !== "string") ||
914
- (value.lastFailureKind !== undefined &&
915
- !isPlayFailureKind(value.lastFailureKind)) ||
916
- typeof value.updatedAt !== "number")
917
- return false;
918
- try {
919
- for (const event of value.events)
920
- assertPlayEvent(event);
921
- }
922
- catch {
923
- return false;
924
- }
925
- return validReleasedAuthorizationCheckpoints(value.documentAuthorizationCheckpoints, value.events);
926
- }
927
817
  function validModelHostBinding(value) {
928
818
  return (isRecord(value) &&
929
819
  (value.provider === "chat_completions" ||
@@ -946,54 +836,6 @@ function validModelHostBinding(value) {
946
836
  value.cacheStrategy === "explicit_cliproxyapi_message" ||
947
837
  value.cacheStrategy === "provider_managed"));
948
838
  }
949
- function validReleasedDerivedFrom(value) {
950
- return (value === undefined ||
951
- (isRecord(value) &&
952
- typeof value.worldId === "string" &&
953
- typeof value.chainId === "string" &&
954
- typeof value.head === "string"));
955
- }
956
- function validReleasedBranchedBeforePlayer(value) {
957
- return (value === undefined ||
958
- (isRecord(value) &&
959
- typeof value.worldId === "string" &&
960
- typeof value.chainId === "string" &&
961
- Number.isSafeInteger(value.eventId) &&
962
- Number(value.eventId) > 0));
963
- }
964
- function validReleasedAuthorizationCheckpoints(value, events) {
965
- if (value === undefined)
966
- return true;
967
- if (!Array.isArray(value) || value.length > 100_000)
968
- return false;
969
- const eventIds = new Set(events.flatMap((event) => isRecord(event) && Number.isSafeInteger(event.id) && Number(event.id) > 0
970
- ? [Number(event.id)]
971
- : []));
972
- let previousEventId = -1;
973
- for (const checkpoint of value) {
974
- if (!isRecord(checkpoint) ||
975
- Object.keys(checkpoint).length !== 2 ||
976
- !Object.hasOwn(checkpoint, "afterEventId") ||
977
- !Object.hasOwn(checkpoint, "authorization") ||
978
- !Number.isSafeInteger(checkpoint.afterEventId) ||
979
- Number(checkpoint.afterEventId) < 0 ||
980
- Number(checkpoint.afterEventId) <= previousEventId ||
981
- (Number(checkpoint.afterEventId) !== 0 &&
982
- !eventIds.has(Number(checkpoint.afterEventId))) ||
983
- !isRecord(checkpoint.authorization))
984
- return false;
985
- previousEventId = Number(checkpoint.afterEventId);
986
- }
987
- return true;
988
- }
989
- function releasedCurrentContext(value) {
990
- const { schemaVersion: _schemaVersion, kind: _kind, worldId: _worldId, previousContexts: _previousContexts, ...context } = value;
991
- void _schemaVersion;
992
- void _kind;
993
- void _worldId;
994
- void _previousContexts;
995
- return structuredClone(context);
996
- }
997
839
  function assertPlayEvent(value) {
998
840
  if (!isRecord(value) ||
999
841
  !Number.isSafeInteger(value.id) ||
@@ -1140,43 +982,7 @@ function validOptionalString(value) {
1140
982
  return value === undefined || typeof value === "string";
1141
983
  }
1142
984
  function validOptionalUsage(value) {
1143
- return (value === undefined ||
1144
- (isRecord(value) &&
1145
- validUsageToken(value.inputTokens) &&
1146
- validUsageToken(value.outputTokens) &&
1147
- (value.provenance === undefined || validFullUsage(value))));
1148
- }
1149
- /**
1150
- * v0.1.1 stored only input/output usage. Keep that released shape readable,
1151
- * but expose one current protocol projection with honest unavailable fields.
1152
- */
1153
- function normalizePlayEvent(event) {
1154
- if ((event.kind !== "assistant" && event.kind !== "followup") ||
1155
- event.usage === undefined ||
1156
- validFullUsage(event.usage))
1157
- return structuredClone(event);
1158
- const legacy = event.usage;
1159
- const unavailable = "unavailable";
1160
- const provider = (value) => value === null ? unavailable : "provider";
1161
- const usage = {
1162
- inputTokens: legacy.inputTokens,
1163
- uncachedInputTokens: null,
1164
- cacheReadTokens: null,
1165
- cacheWriteTokens: null,
1166
- reasoningTokens: null,
1167
- outputTokens: legacy.outputTokens,
1168
- totalTokens: null,
1169
- provenance: {
1170
- inputTokens: provider(legacy.inputTokens),
1171
- uncachedInputTokens: unavailable,
1172
- cacheReadTokens: unavailable,
1173
- cacheWriteTokens: unavailable,
1174
- reasoningTokens: unavailable,
1175
- outputTokens: provider(legacy.outputTokens),
1176
- totalTokens: unavailable,
1177
- },
1178
- };
1179
- return { ...structuredClone(event), usage };
985
+ return value === undefined || validFullUsage(value);
1180
986
  }
1181
987
  function validFullUsage(value) {
1182
988
  if (!isRecord(value) || !isRecord(value.provenance))
@@ -1232,12 +1038,18 @@ async function cloneNumberedPrefix(input) {
1232
1038
  await cloneTimelineFile(join(input.sourceRoot, input.directory, numbered(index)), join(input.targetRoot, input.directory, numbered(index)), input.immutable);
1233
1039
  }
1234
1040
  async function cloneTimelineFile(source, target, immutable) {
1235
- await cloneFilePhysically({
1236
- source,
1237
- target,
1238
- immutable,
1239
- onTargetExists: assertSameFileBytes,
1240
- });
1041
+ await mkdir(dirname(target), { recursive: true });
1042
+ try {
1043
+ await copyFile(source, target, constants.COPYFILE_EXCL);
1044
+ }
1045
+ catch (error) {
1046
+ if (!isNodeError(error) || error.code !== "EEXIST")
1047
+ throw error;
1048
+ await assertSameFileBytes(source, target);
1049
+ return;
1050
+ }
1051
+ if (immutable)
1052
+ await chmod(target, 0o444);
1241
1053
  }
1242
1054
  async function assertSameFileBytes(source, target) {
1243
1055
  const [sourceBytes, targetBytes] = await Promise.all([
@@ -1279,9 +1091,7 @@ async function publishJson(path, value) {
1279
1091
  encoding: "utf8",
1280
1092
  mode: 0o600,
1281
1093
  });
1282
- await syncFile(temporary);
1283
1094
  await rename(temporary, path);
1284
- await syncDirectory(dirname(path));
1285
1095
  }
1286
1096
  async function publishImmutableJson(path, value) {
1287
1097
  const contents = `${JSON.stringify(value, null, 2)}\n`;
@@ -1293,10 +1103,8 @@ async function publishImmutableJson(path, value) {
1293
1103
  flag: "wx",
1294
1104
  mode: 0o600,
1295
1105
  });
1296
- await syncFile(temporary);
1297
1106
  try {
1298
- await link(temporary, path);
1299
- await syncDirectory(dirname(path));
1107
+ await linkExclusive(temporary, path);
1300
1108
  }
1301
1109
  catch (error) {
1302
1110
  if (!isNodeError(error) || error.code !== "EEXIST")
@@ -1311,21 +1119,3 @@ async function publishImmutableJson(path, value) {
1311
1119
  await rm(temporary, { force: true });
1312
1120
  }
1313
1121
  }
1314
- async function syncFile(path) {
1315
- const handle = await open(path, "r");
1316
- try {
1317
- await handle.sync();
1318
- }
1319
- finally {
1320
- await handle.close();
1321
- }
1322
- }
1323
- async function syncDirectory(path) {
1324
- const handle = await open(path, "r");
1325
- try {
1326
- await handle.sync();
1327
- }
1328
- finally {
1329
- await handle.close();
1330
- }
1331
- }