narraeon 0.3.1 → 0.5.0

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 (76) hide show
  1. package/dist/node/protocol/appPreferences.js +25 -0
  2. package/dist/node/protocol/conversationObservation.js +67 -0
  3. package/dist/node/protocol/v1.js +188 -34
  4. package/dist/node/protocol/worldExtensions.js +16 -0
  5. package/dist/node/protocol/worldMaintenance.js +77 -0
  6. package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
  7. package/dist/node/runtime/ConversationChanges.js +24 -0
  8. package/dist/node/runtime/V1Runtime.js +447 -121
  9. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
  10. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
  11. package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
  12. package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
  13. package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
  14. package/dist/node/runtime/content/ContentWorkspace.js +43 -13
  15. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
  16. package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
  17. package/dist/node/runtime/content/PackageFollowups.js +80 -0
  18. package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
  19. package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
  20. package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
  21. package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
  22. package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
  23. package/dist/node/runtime/model/ModelConnectionStore.js +44 -10
  24. package/dist/node/runtime/model/ModelHost.js +31 -1
  25. package/dist/node/runtime/model/ModelHostStream.js +51 -5
  26. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
  27. package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
  28. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
  29. package/dist/node/runtime/play/OrderedFollowups.js +71 -0
  30. package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
  31. package/dist/node/runtime/play/PlayCallChain.js +814 -61
  32. package/dist/node/runtime/play/PlayContinuity.js +88 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
  36. package/dist/node/runtime/play/PlayPromptRun.js +86 -0
  37. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
  38. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
  39. package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
  40. package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
  41. package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
  42. package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
  43. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
  44. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
  45. package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
  46. package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
  47. package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
  48. package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
  49. package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
  50. package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
  51. package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
  52. package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
  53. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
  54. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
  55. package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
  56. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
  57. package/dist/node/server/ConversationStream.js +109 -0
  58. package/dist/node/server/createServer.js +5 -1
  59. package/dist/node/shared/artifact-payload-contract.js +123 -0
  60. package/dist/node/shared/default-play-prompts.js +25 -45
  61. package/dist/node/shared/default-preset-host.js +114 -138
  62. package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
  63. package/dist/node/shared/display-regex.js +114 -0
  64. package/dist/node/shared/first-party-action-choices.js +1 -6
  65. package/dist/node/shared/first-party-player-view.js +1 -6
  66. package/dist/node/shared/ordered-author-prompts.js +64 -0
  67. package/dist/node/shared/ordered-followups.js +42 -0
  68. package/dist/node/shared/ordered-play-prompts.js +73 -0
  69. package/dist/web/assets/index-BXfelCi9.js +1207 -0
  70. package/dist/web/assets/index-BYnhUxlk.css +1 -0
  71. package/dist/web/index.html +2 -2
  72. package/package.json +1 -1
  73. package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
  74. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
  75. package/dist/web/assets/index-D5VqDvjY.js +0 -1192
  76. package/dist/web/assets/index-Xa0s35CJ.css +0 -1
@@ -1,26 +1,36 @@
1
+ import { PackageScriptPermissions } from "./extension/PackageScriptPermissions.js";
2
+ import { comparePromptPrefixes } from "./prompt/WorldPromptDiagnostics.js";
1
3
  import { join } from "node:path";
2
4
  import { maxPortableContentArchiveBase64Characters, maxPortableContentArchiveBytes, } from "../protocol/contentTree.js";
3
5
  import { V1ProtocolError, v1Protocol, } from "../protocol/v1.js";
4
6
  import { defaultAppLocale, } from "../protocol/appPreferences.js";
5
7
  import { ContentPackageImportError, ContentWorkspace, InvalidContentTreeError, } from "./content/ContentWorkspace.js";
8
+ import { contentTreeFingerprint } from "./content/ContentTreeFingerprint.js";
6
9
  import { inspectContentPackageCurrentTree } from "./content/FileNativeContentTree.js";
7
- import { FileNativeModelHost, FileNativeSettingAuthorProvider, } from "./model/FileNativeModelAdapters.js";
8
- import {} from "./model/ModelHost.js";
10
+ import { FileNativeModelHost } from "./model/FileNativeModelAdapters.js";
11
+ import { equalModelHostBinding, } from "./model/ModelHost.js";
9
12
  import { ModelConnectionStore } from "./model/ModelConnectionStore.js";
10
13
  import { FileNativeContinuityCorrection } from "./play/FileNativeContinuityCorrection.js";
14
+ import { fingerprintControl } from "./play/PlayDocumentTools.js";
11
15
  import { PlayCallChain, PlayCallChainError, } from "./play/PlayCallChain.js";
12
- import { FileNativePlayPresetError, FileNativePlayPresetStore, presetHostBinding, settingImprovementPromptForBinding, } from "./play/FileNativePlayPresetStore.js";
16
+ import { FileNativePlayPresetError, FileNativePlayPresetStore, presetHostBinding, } from "./play/FileNativePlayPresetStore.js";
13
17
  import { buildPlayPresetWorkbenchSnapshot } from "./play/PlayPresetWorkbench.js";
14
18
  import { FileNativeArtifactStore } from "./artifact/FileNativeArtifactStore.js";
15
19
  import { projectArtifactForFrontend, projectDebugArtifactForFrontend, } from "./extension/FrontendExtensionBundle.js";
16
20
  import { projectPlayerViewPanels, } from "./extension/PlayerViewPanelProjector.js";
17
21
  import { FileNativePromptCompiler } from "./prompt/FileNativePromptCompiler.js";
18
- import { DocumentCandidateSettingImprovement } from "./setting/DocumentCandidateSettingImprovement.js";
22
+ import { FileNativeSettingImprovementStore } from "./setting/FileNativeSettingImprovementStore.js";
23
+ import { SettingImprovementRollbackError, SettingImprovementSession, } from "./setting/SettingImprovementSession.js";
19
24
  import { FileNativeWorldStore } from "./world/FileNativeWorldStore.js";
20
25
  import { WorldDocumentStore } from "./world/WorldDocumentStore.js";
21
26
  import { FileNativeAiFailureLog } from "./model/AiFailureLog.js";
22
27
  import { AppPreferencesStore } from "./config/AppPreferencesStore.js";
28
+ import { FileNativeWorldRevisionStore } from "./world-revision/FileNativeWorldRevisionStore.js";
29
+ import { WorldRevisionSession } from "./world-revision/WorldRevisionSession.js";
30
+ import { WorldRevisionWorkspace, WorldRevisionWorkspaceError, } from "./world-revision/WorldRevisionWorkspace.js";
31
+ import { ConversationChanges } from "./ConversationChanges.js";
23
32
  export class V1Runtime {
33
+ #conversationChanges = new ConversationChanges();
24
34
  #content;
25
35
  #playPresets;
26
36
  #worlds;
@@ -30,7 +40,8 @@ export class V1Runtime {
30
40
  #playCallChains;
31
41
  #failureLog;
32
42
  #preferences;
33
- #settingImprovements = new Map();
43
+ #settingImprovements;
44
+ #worldRevisions;
34
45
  #compiler = new FileNativePromptCompiler();
35
46
  #locale = defaultAppLocale;
36
47
  constructor(input) {
@@ -47,9 +58,75 @@ export class V1Runtime {
47
58
  input.logRoot === undefined
48
59
  ? undefined
49
60
  : new FileNativeAiFailureLog(join(input.logRoot, "ai-failures"));
50
- this.#playCallChains = new PlayCallChain(this.#worlds, this.#compiler, this.#artifacts, this.#failureLog);
61
+ this.#playCallChains = new PlayCallChain(this.#worlds, this.#compiler, this.#artifacts, this.#failureLog, (id) => this.#conversationChanges.publish(`play:${id}`));
51
62
  this.#models = new ModelConnectionStore(input.configRoot);
52
- this.#corrections = new FileNativeContinuityCorrection(this.#worlds);
63
+ this.#corrections = new FileNativeContinuityCorrection(this.#worlds, {
64
+ compiler: this.#compiler,
65
+ });
66
+ this.#settingImprovements = new SettingImprovementSession({
67
+ changed: (id, durable) => this.#conversationChanges.publish(`setting:${id}`, durable),
68
+ store: new FileNativeSettingImprovementStore(input.dataRoot, {
69
+ content: this.#content,
70
+ }),
71
+ content: this.#content,
72
+ compiler: this.#compiler,
73
+ locale: () => this.#locale,
74
+ bindModelHost: () => this.#modelHost(),
75
+ bindExistingModelHost: (binding) => this.#matchingModelHost(binding),
76
+ bindPlayPreset: () => this.#playPresets.freeze(),
77
+ preview: (snapshot, modelBinding, playPreset) => this.#compiler.preview({
78
+ endpoint: { id: "setting-authoring", commit: "current-tree" },
79
+ hostBinding: playPreset === undefined
80
+ ? {
81
+ hostPresetId: "legacy-setting-authoring",
82
+ files: defaultSettingPreviewHost(),
83
+ }
84
+ : presetHostBinding(playPreset),
85
+ world: previewWorld(snapshot, "setting-authoring.message.genesis.narrator", "setting-authoring"),
86
+ playerInputPlacement: "append",
87
+ playerInput: this.#locale === "zh-CN"
88
+ ? "检查内容包当前树。"
89
+ : "Inspect the content package current tree.",
90
+ modelBinding,
91
+ }, playPreset),
92
+ });
93
+ const worldRevisionStore = new FileNativeWorldRevisionStore(input.dataRoot);
94
+ const worldRevisionWorkspace = new WorldRevisionWorkspace({
95
+ store: worldRevisionStore,
96
+ worlds: this.#worlds,
97
+ });
98
+ this.#worldRevisions = new WorldRevisionSession({
99
+ changed: (id, durable) => this.#conversationChanges.publish(`revision:${id}`, durable),
100
+ store: worldRevisionStore,
101
+ workspace: worldRevisionWorkspace,
102
+ worlds: this.#worlds,
103
+ compiler: this.#compiler,
104
+ locale: () => this.#locale,
105
+ bindModelHost: () => this.#modelHost(),
106
+ bindExistingModelHost: (binding) => this.#matchingModelHost(binding),
107
+ bindPlayPreset: () => this.#playPresets.freeze(),
108
+ preview: ({ snapshot, modelBinding, playPreset, binding, epoch, maintenance, }) => this.#compiler.preview({
109
+ endpoint: {
110
+ id: `${epoch.worldId}:${epoch.baseHead}:revision`,
111
+ commit: `worktree:${contentTreeFingerprint(snapshot.files)}`,
112
+ operationId: epoch.epochId,
113
+ },
114
+ hostBinding: presetHostBinding(playPreset),
115
+ world: {
116
+ controlFingerprint: fingerprintControl(Object.fromEntries(snapshot.files.map(({ path, contents }) => [path, contents]))),
117
+ documentSnapshot: snapshot,
118
+ history: structuredClone(binding.history),
119
+ narrativeCheckpoint: binding.narrativeCheckpoint,
120
+ ...maintenance,
121
+ additionalMaterials: structuredClone(binding.additionalMaterials),
122
+ },
123
+ playerInputPlacement: "append",
124
+ playerInput: this.#locale === "zh-CN"
125
+ ? "检查世界修订工作树。"
126
+ : "Inspect the world-revision worktree.",
127
+ modelBinding,
128
+ }, playPreset),
129
+ });
53
130
  }
54
131
  async initialize() {
55
132
  this.#locale = (await this.#preferences.view()).locale;
@@ -57,8 +134,58 @@ export class V1Runtime {
57
134
  await this.#playPresets.initialize();
58
135
  }
59
136
  async handle(request, playCallChainObserver) {
60
- const result = await this.#dispatch(request, playCallChainObserver);
61
- return { protocol: v1Protocol, result };
137
+ try {
138
+ const result = await this.#dispatch(request, playCallChainObserver);
139
+ return { protocol: v1Protocol, result };
140
+ }
141
+ finally {
142
+ // Explicit commands may partially settle before failing. Readers recover
143
+ // through the same Runtime projection, never infer success from this signal.
144
+ if (!/\.(read|status|overview|inspect|page|detail|list|preview)$/.test(request.type)) {
145
+ if ("packageId" in request)
146
+ this.#conversationChanges.publish(`setting:${request.packageId}`);
147
+ if ("worldId" in request) {
148
+ this.#conversationChanges.publish(`play:${request.worldId}`);
149
+ this.#conversationChanges.publish(`revision:${request.worldId}`);
150
+ }
151
+ }
152
+ }
153
+ }
154
+ subscribeConversation(target, changed) {
155
+ return this.#conversationChanges.subscribe(`${target.kind}:${target.id}`, changed);
156
+ }
157
+ async readConversation(target, previous) {
158
+ switch (target.kind) {
159
+ case "play":
160
+ return {
161
+ kind: "play",
162
+ value: await this.#playCallChains.observeWorld(target.id),
163
+ extensionsRevision: await this.#worlds.extensionControlsRevision(target.id),
164
+ };
165
+ case "setting": {
166
+ if (previous?.kind === "setting")
167
+ return {
168
+ kind: "setting",
169
+ value: this.#settingImprovements.observeProgress(previous.value),
170
+ };
171
+ const status = await this.#settingImprovements.status(target.id, target.sessionId);
172
+ const content = await this.#content.readCurrentTreeContentPackage(target.id);
173
+ return {
174
+ kind: "setting",
175
+ value: {
176
+ ...status,
177
+ revision: `${status.revision}:${contentTreeFingerprint(content.files)}`,
178
+ },
179
+ };
180
+ }
181
+ case "revision":
182
+ return {
183
+ kind: "revision",
184
+ value: previous?.kind === "revision"
185
+ ? this.#worldRevisions.observeProgress(previous.value)
186
+ : await this.#worldRevisions.status(target.id, target.sessionId),
187
+ };
188
+ }
62
189
  }
63
190
  async #dispatch(request, playCallChainObserver) {
64
191
  switch (request.type) {
@@ -74,7 +201,12 @@ export class V1Runtime {
74
201
  case "preferences.read":
75
202
  return this.#preferences.view();
76
203
  case "preferences.save": {
77
- const preferences = await this.#preferences.save(request.locale);
204
+ const preferences = await this.#preferences.save({
205
+ ...(request.locale === undefined ? {} : { locale: request.locale }),
206
+ ...(request.reading === undefined
207
+ ? {}
208
+ : { reading: request.reading }),
209
+ });
78
210
  this.#locale = preferences.locale;
79
211
  this.#compiler.setLocale(this.#locale);
80
212
  await this.#playPresets.syncBuiltinDefaultLocale();
@@ -84,6 +216,8 @@ export class V1Runtime {
84
216
  return this.#models.view();
85
217
  case "model.save":
86
218
  return this.#models.save(request.connection);
219
+ case "model.copy":
220
+ return this.#models.copy(request.connectionId, request.name);
87
221
  case "model.select":
88
222
  return this.#models.select(request.connectionId);
89
223
  case "model.delete":
@@ -92,6 +226,14 @@ export class V1Runtime {
92
226
  return this.#models.listModels(request);
93
227
  case "content.create":
94
228
  return this.#content.createCurrentTreeContentPackage();
229
+ case "content.scripts.read":
230
+ return this.#content.packageScriptPermissions(request.packageId);
231
+ case "content.scripts.set":
232
+ return this.#content.packageScriptPermissions(request.packageId, request.enabled);
233
+ case "world.package-scripts.read":
234
+ return this.#worlds.packageScriptPermissions(request.worldId);
235
+ case "world.package-scripts.set":
236
+ return this.#worlds.packageScriptPermissions(request.worldId, request.enabled);
95
237
  case "content.read":
96
238
  return this.#content.readCurrentTreeContentPackage(request.packageId);
97
239
  case "content.replace":
@@ -106,7 +248,7 @@ export class V1Runtime {
106
248
  case "content.import": {
107
249
  const archive = decodePortableContentArchive(request.archiveBase64);
108
250
  try {
109
- return await this.#content.importPortableContentPackageArchive(archive);
251
+ return await this.#content.importPortableContentPackageArchive(archive, request.title === undefined ? {} : { title: request.title });
110
252
  }
111
253
  catch (error) {
112
254
  if (error instanceof ContentPackageImportError ||
@@ -124,82 +266,33 @@ export class V1Runtime {
124
266
  base64: exported.archive.toString("base64"),
125
267
  };
126
268
  }
127
- case "setting-improvement.start": {
128
- if (this.#settingImprovements.has(request.improvementId))
129
- throw new Error("Setting-improvement ID already exists");
130
- const [package_, connection, preset] = await Promise.all([
131
- this.#content.readCurrentTreeContentPackage(request.packageId),
132
- this.#models.bind(),
133
- this.#playPresets.bindCurrent(),
134
- ]);
135
- const improvement = new DocumentCandidateSettingImprovement({
136
- files: package_.files,
137
- adapter: new FileNativeSettingAuthorProvider(connection, fetch, {
138
- ...(this.#failureLog === undefined
139
- ? {}
140
- : { failureLog: this.#failureLog }),
141
- operationId: request.improvementId,
142
- locale: this.#locale,
143
- }),
144
- authorPrompt: settingImprovementPromptForBinding(preset, this.#locale),
145
- locale: this.#locale,
146
- ...(this.#failureLog === undefined
147
- ? {}
148
- : { failureLog: this.#failureLog }),
149
- preview: (snapshot) => this.#compiler.preview({
150
- endpoint: { id: "setting-candidate", commit: "candidate" },
151
- hostBinding: {
152
- hostPresetId: "setting-candidate",
153
- files: defaultSettingPreviewHost(),
154
- },
155
- world: previewWorld(snapshot, "setting-candidate.message.genesis.narrator", "setting-candidate"),
156
- playerInputPlacement: "bootstrap",
157
- playerInput: this.#locale === "zh-CN"
158
- ? "预览设定候选。"
159
- : "Preview the setting candidate.",
160
- modelBinding: new FileNativeModelHost(connection).binding(),
161
- }),
162
- });
163
- if (this.#settingImprovements.has(request.improvementId))
164
- throw new Error("Setting-improvement ID already exists");
165
- this.#settingImprovements.set(request.improvementId, {
166
- packageId: request.packageId,
167
- improvement,
168
- });
269
+ case "setting-improvement.preview":
270
+ return this.#settingImprovements.preview(request.packageId, request.sessionId);
271
+ case "setting-improvement.read":
272
+ return this.#settingImprovements.read(request.packageId);
273
+ case "setting-improvement.status":
274
+ return this.#settingImprovements.status(request.packageId, request.sessionId);
275
+ case "setting-improvement.overview":
276
+ return this.#settingImprovements.overview(request.packageId);
277
+ case "setting-improvement.session.read":
278
+ return this.#settingImprovements.readSession(request.packageId, request.sessionId);
279
+ case "setting-improvement.session.delete":
280
+ return this.#settingImprovements.deleteSession(request.packageId, request.sessionId);
281
+ case "setting-improvement.rollback":
169
282
  try {
170
- return await improvement.start({
171
- goal: request.goal,
172
- contextPaths: request.contextPaths,
173
- mode: request.mode,
174
- });
283
+ return await this.#settingImprovements.rollback(request);
175
284
  }
176
285
  catch (error) {
177
- this.#settingImprovements.delete(request.improvementId);
286
+ if (error instanceof SettingImprovementRollbackError)
287
+ throw new V1ProtocolError("invalid_request", error.message, {
288
+ cause: error,
289
+ });
178
290
  throw error;
179
291
  }
180
- }
181
- case "setting-improvement.confirm":
182
- return this.#settingImprovement(request.improvementId).improvement.confirmPlan();
183
- // Answers while start/confirm is still awaiting the provider: those calls
184
- // yield the event loop, so this read observes the run in flight.
185
- case "setting-improvement.progress":
186
- return this.#settingImprovement(request.improvementId).improvement.progress();
187
- case "setting-improvement.revise-plan":
188
- return this.#settingImprovement(request.improvementId).improvement.revisePlan(request.feedback);
189
- case "setting-improvement.revise-candidate":
190
- return this.#settingImprovement(request.improvementId).improvement.reviseCandidate(request.feedback);
191
- case "setting-improvement.apply": {
192
- const active = this.#settingImprovement(request.improvementId);
193
- await active.improvement.apply(async (files) => {
194
- await this.#content.replaceCurrentTreeContentPackage(active.packageId, files);
195
- });
196
- this.#settingImprovements.delete(request.improvementId);
197
- return { applied: true };
198
- }
199
- case "setting-improvement.discard":
200
- this.#settingImprovement(request.improvementId).improvement.discard();
201
- this.#settingImprovements.delete(request.improvementId);
202
- return { discarded: true };
292
+ case "setting-improvement.message":
293
+ return this.#settingImprovements.send(request);
294
+ case "setting-improvement.cancel":
295
+ return this.#settingImprovements.cancel(request.sessionId);
203
296
  case "play.read":
204
297
  return this.#playPresets.list();
205
298
  case "play.create":
@@ -228,7 +321,11 @@ export class V1Runtime {
228
321
  const binding = request.presetId === undefined
229
322
  ? await this.#playPresets.bindCurrent()
230
323
  : await this.#playPresets.bindRevision(request.presetId, request.revision);
231
- return buildPlayPresetWorkbenchSnapshot(binding);
324
+ if (request.worldId === undefined)
325
+ return buildPlayPresetWorkbenchSnapshot(binding, undefined, request.draft);
326
+ const head = await this.#worlds.currentHead(request.worldId);
327
+ const playerViews = await this.#worlds.renderPlayerViewsAtHead(request.worldId, head);
328
+ return buildPlayPresetWorkbenchSnapshot(binding, { worldId: request.worldId, head, playerViews }, request.draft);
232
329
  }
233
330
  case "prompt.preview": {
234
331
  const [package_, connection, playPreset] = await Promise.all([
@@ -266,6 +363,9 @@ export class V1Runtime {
266
363
  appended: preview.initialAppend === undefined
267
364
  ? []
268
365
  : [
366
+ ...(preview.initialAppend.beforePlayer === undefined
367
+ ? []
368
+ : [preview.initialAppend.beforePlayer.logical]),
269
369
  {
270
370
  kind: "player",
271
371
  text: preview.initialAppend.logical.text,
@@ -288,6 +388,8 @@ export class V1Runtime {
288
388
  return this.#worlds.createFromContentPackage({
289
389
  operationId: request.operationId,
290
390
  sourcePackageId: request.packageId,
391
+ sourcePackageTitle: package_.title,
392
+ packageScriptGrants: await this.#content.readPackageScriptGrants(request.packageId),
291
393
  packageFiles: package_.files,
292
394
  prompt: {
293
395
  hostBinding: presetHostBinding(preset),
@@ -301,15 +403,18 @@ export class V1Runtime {
301
403
  return this.#worlds.renameWorld(request.worldId, request.name);
302
404
  case "world.delete":
303
405
  return playCall(async () => {
406
+ await this.#worlds.assertWorldRevisionUnlocked(request.worldId);
304
407
  this.#playCallChains.forgetWorld(request.worldId);
305
408
  return this.#worlds.deleteWorld(request.worldId);
306
409
  });
307
410
  case "world.read": {
308
411
  const head = await this.#worlds.currentHead(request.worldId);
309
412
  const playerViews = await this.#worlds.renderPlayerViewsAtHead(request.worldId, head);
310
- const [playerViewPanels, playTimeline] = await Promise.all([
311
- this.#frontendPlayerViewPanels(request.worldId, head, playerViews),
413
+ const extensionControls = await this.#currentExtensionControls(request.worldId);
414
+ const [playerViewPanels, playTimeline, revisionEpoch] = await Promise.all([
415
+ this.#frontendPlayerViewPanels(request.worldId, head, playerViews, extensionControls),
312
416
  this.#worlds.playTimeline.readPage(request.worldId, 40),
417
+ this.#worldRevisions.activeEpoch(request.worldId),
313
418
  ]);
314
419
  return {
315
420
  worldId: request.worldId,
@@ -323,27 +428,54 @@ export class V1Runtime {
323
428
  artifacts: [],
324
429
  extensions: [],
325
430
  playerViewPanels,
431
+ extensionControls,
432
+ suppressedPlayerViewIds: await this.#suppressedPlayerViewIds(extensionControls),
326
433
  artifactDebug: [],
327
434
  playCallChain: null,
328
435
  playTimeline,
436
+ worldRevision: revisionEpoch,
329
437
  };
330
438
  }
331
439
  case "world.surface.read":
332
440
  return request.surface === "runtime"
333
441
  ? this.#worlds.readSurface(request.worldId, "runtime")
334
442
  : this.#worlds.readSurface(request.worldId, request.surface);
443
+ case "world.extensions.read":
444
+ return this.#worlds.extensionControls(request.worldId, await this.#playPresets.bindCurrent(), this.#locale);
445
+ case "world.extensions.set": {
446
+ const result = await this.#worlds.extensionControls(request.worldId, await this.#playPresets.bindCurrent(), this.#locale, request);
447
+ return result;
448
+ }
335
449
  case "world.play-decorations.read": {
336
- const head = await this.#reconcileArtifacts(request.worldId);
337
- const [artifacts, extensions, artifactDebug] = await Promise.all([
338
- this.#frontendProjection(request.worldId),
339
- this.#artifacts.readExtensionSummaries(request.worldId),
340
- this.#frontendDebug(request.worldId),
341
- ]);
342
- return { head, artifacts, extensions, artifactDebug };
450
+ for (;;) {
451
+ const extensionControls = await this.#currentExtensionControls(request.worldId);
452
+ const head = await this.#reconcileArtifacts(request.worldId);
453
+ const [projection, artifactDebug] = await Promise.all([
454
+ this.#frontendDecorations(request.worldId),
455
+ this.#frontendDebug(request.worldId),
456
+ ]);
457
+ const playerViews = await this.#worlds.renderPlayerViewsAtHead(request.worldId, head);
458
+ const playerViewPanels = await this.#frontendPlayerViewPanels(request.worldId, head, playerViews, extensionControls);
459
+ const suppressedPlayerViewIds = await this.#suppressedPlayerViewIds(extensionControls);
460
+ // Never attach a newer controls revision to a projection read before
461
+ // that choice was accepted. Concurrent toggles restart this read.
462
+ if ((await this.#worlds.extensionControlsRevision(request.worldId)) !==
463
+ extensionControls.revision)
464
+ continue;
465
+ return {
466
+ head,
467
+ ...projection,
468
+ artifactDebug,
469
+ playerViews,
470
+ playerViewPanels,
471
+ extensionControls,
472
+ suppressedPlayerViewIds,
473
+ };
474
+ }
343
475
  }
344
476
  case "artifacts.read":
345
477
  await this.#reconcileArtifacts(request.worldId);
346
- return this.#frontendProjection(request.worldId, request.channel);
478
+ return (await this.#frontendDecorations(request.worldId, request.channel)).artifacts;
347
479
  case "artifacts.debug":
348
480
  await this.#reconcileArtifacts(request.worldId);
349
481
  return this.#frontendDebug(request.worldId, request.operationId);
@@ -353,6 +485,7 @@ export class V1Runtime {
353
485
  return result;
354
486
  }
355
487
  case "world.control-draft.save":
488
+ await this.#worlds.assertWorldRevisionUnlocked(request.worldId);
356
489
  return this.#worlds.saveControlDraft(request.worldId, request.files);
357
490
  case "world.control-draft.preview": {
358
491
  const prompt = {
@@ -372,6 +505,33 @@ export class V1Runtime {
372
505
  };
373
506
  return this.#worlds.applyControlDraft(request.worldId, prompt);
374
507
  }
508
+ case "world.revision.open":
509
+ return this.#worldRevisionCall(() => this.#worldRevisions.open(request.worldId));
510
+ case "world.revision.overview":
511
+ return this.#worldRevisionCall(() => this.#worldRevisions.overview(request.worldId));
512
+ case "world.revision.preview":
513
+ return this.#worldRevisions.preview(request.worldId, request.sessionId);
514
+ case "world.revision.status":
515
+ return this.#worldRevisionCall(() => this.#worldRevisions.status(request.worldId, request.sessionId));
516
+ case "world.revision.session.read":
517
+ return this.#worldRevisionCall(() => this.#worldRevisions.readSession(request.worldId, request.sessionId));
518
+ case "world.revision.session.delete":
519
+ return this.#worldRevisionCall(() => this.#worldRevisions.deleteSession(request.worldId, request.sessionId));
520
+ case "world.revision.message":
521
+ return this.#worldRevisionCall(() => this.#worldRevisions.send(request));
522
+ case "world.revision.cancel":
523
+ return this.#worldRevisionCall(() => this.#worldRevisions.cancel(request.sessionId));
524
+ case "world.revision.files.replace":
525
+ return this.#worldRevisionCall(() => this.#worldRevisions.replaceFiles(request));
526
+ case "world.revision.rollback":
527
+ return this.#worldRevisionCall(() => this.#worldRevisions.rollback(request));
528
+ case "world.revision.apply": {
529
+ const result = await this.#worldRevisionCall(() => this.#worldRevisions.apply(request));
530
+ await this.#reconcileArtifacts(request.worldId);
531
+ return result;
532
+ }
533
+ case "world.revision.discard":
534
+ return this.#worldRevisionCall(() => this.#worldRevisions.discard(request));
375
535
  case "world.derive": {
376
536
  const preset = await this.#playPresets.bindCurrent();
377
537
  const result = await this.#playCallChains.deriveWorld({
@@ -384,18 +544,33 @@ export class V1Runtime {
384
544
  return result;
385
545
  }
386
546
  case "play.chain.revise-player": {
387
- const result = await playCall(() => this.#playCallChains.revisePlayer({
547
+ const base = {
388
548
  operationId: request.operationId,
389
549
  worldId: request.worldId,
390
550
  chainId: request.chainId,
391
551
  eventId: request.eventId,
392
552
  replacementExchangeId: request.replacementExchangeId,
393
553
  replacementText: request.replacementText,
394
- }));
554
+ };
555
+ const result = await playCall(async () => {
556
+ await this.#worlds.assertWorldRevisionUnlocked(request.worldId);
557
+ if (request.continuation === "continue_context")
558
+ return this.#playCallChains.revisePlayer({
559
+ ...base,
560
+ continuation: request.continuation,
561
+ });
562
+ const { hostBinding, playPreset, modelBinding } = await this.#continuousBinding();
563
+ return this.#playCallChains.revisePlayer({
564
+ ...base,
565
+ continuation: request.continuation,
566
+ freshContext: { hostBinding, playPreset, modelBinding },
567
+ });
568
+ });
395
569
  await this.#reconcileArtifacts(request.worldId);
396
570
  return result;
397
571
  }
398
572
  case "play.chain.start": {
573
+ await this.#worlds.assertWorldRevisionUnlocked(request.worldId);
399
574
  const { modelHost, hostBinding, playPreset, modelBinding } = await this.#continuousBinding();
400
575
  return playCall(() => this.#playCallChains.start({
401
576
  worldId: request.worldId,
@@ -412,16 +587,20 @@ export class V1Runtime {
412
587
  }));
413
588
  }
414
589
  case "play.chain.append":
415
- return playCall(async () => this.#playCallChains.append({
416
- worldId: request.worldId,
417
- chainId: request.chainId,
418
- exchangeId: request.exchangeId,
419
- playerText: request.playerText,
420
- modelHost: await this.#modelHost(),
421
- ...(playCallChainObserver === undefined
422
- ? {}
423
- : { observer: playCallChainObserver }),
424
- }));
590
+ return playCall(async () => {
591
+ await this.#worlds.assertWorldRevisionUnlocked(request.worldId);
592
+ return this.#playCallChains.append({
593
+ worldId: request.worldId,
594
+ chainId: request.chainId,
595
+ exchangeId: request.exchangeId,
596
+ playerText: request.playerText,
597
+ modelHost: await this.#modelHost(),
598
+ resolvePrompt: () => this.#continuousBinding(),
599
+ ...(playCallChainObserver === undefined
600
+ ? {}
601
+ : { observer: playCallChainObserver }),
602
+ });
603
+ });
425
604
  case "play.chain.cancel":
426
605
  return playCall(() => this.#playCallChains.cancel({
427
606
  worldId: request.worldId,
@@ -436,6 +615,88 @@ export class V1Runtime {
436
615
  case "play.timeline.detail":
437
616
  await this.#worlds.ensureCurrentStorage(request.worldId);
438
617
  return this.#worlds.playTimeline.readDetail(request.worldId, request.chainId, request.eventId);
618
+ case "world.play-context.read": {
619
+ const binding = await this.#worlds.bindPlayCallChain(request.worldId);
620
+ const currentContext = await this.#playCallChains.inspectReading(request.worldId, binding.parentHead);
621
+ const model = await this.#models.view();
622
+ if (!model.configured)
623
+ return {
624
+ worldId: request.worldId,
625
+ worldHead: binding.parentHead,
626
+ currentContext,
627
+ nextFreshContext: null,
628
+ };
629
+ const { modelHost, hostBinding, playPreset, modelBinding } = await this.#continuousBinding();
630
+ const persisted = currentContext === null
631
+ ? null
632
+ : await this.#worlds.playTimeline.readCurrent(request.worldId);
633
+ const frozenBinding = persisted?.value.modelBinding;
634
+ const continueContext = currentContext !== null &&
635
+ !currentContext.stale &&
636
+ frozenBinding !== undefined &&
637
+ equalModelHostBinding(frozenBinding, modelBinding);
638
+ const baseline = continueContext
639
+ ? await this.#worlds.bindPlayCallChainAt(request.worldId, currentContext.baselineHead)
640
+ : binding;
641
+ const preview = this.#compiler.preview({
642
+ extensionControls: await this.#worlds.extensionControls(request.worldId, playPreset, this.#locale),
643
+ endpoint: {
644
+ id: `${request.worldId}:${binding.parentHead}`,
645
+ commit: binding.parentHead,
646
+ },
647
+ hostBinding,
648
+ world: {
649
+ controlFingerprint: fingerprintControl(binding.files),
650
+ documentSnapshot: WorldDocumentStore.open({
651
+ layout: "world_state",
652
+ files: Object.entries(binding.files).map(([path, contents]) => ({ path, contents })),
653
+ }),
654
+ additionalMaterials: structuredClone(binding.additionalMaterials),
655
+ history: structuredClone(binding.history),
656
+ narrativeCheckpoint: baseline.narrativeCheckpoint,
657
+ historyAlreadyAppended: Object.keys(binding.history).filter((key) => !(key in baseline.history)),
658
+ ...(await this.#worlds.inspectDocumentMaintenance(request.worldId, binding.parentHead)),
659
+ },
660
+ playerInputPlacement: "append",
661
+ playerInput: "",
662
+ modelBinding,
663
+ }, playPreset);
664
+ const tools = preview.compilation.toolUniverse;
665
+ const nextEncoding = modelHost.previewRequest({
666
+ bootstrap: preview.compilation,
667
+ tools,
668
+ toolUniverse: tools,
669
+ allowedTools: tools.map(({ name }) => name),
670
+ toolStrategy: preview.compilation.toolStrategy,
671
+ appended: continueContext ? (persisted?.value.transcript ?? []) : [],
672
+ operationId: continueContext
673
+ ? currentContext.chainId
674
+ : "next-fresh-context-preview",
675
+ maxOutputTokens: modelBinding.maxOutputTokens,
676
+ });
677
+ const currentEncoding = currentContext === null
678
+ ? null
679
+ : await this.#worlds.playTimeline
680
+ .readInitialEncoding(request.worldId, currentContext.chainId, currentContext.requestExchange)
681
+ .catch(() => null);
682
+ const prefixDiagnostics = comparePromptPrefixes(currentContext === null
683
+ ? null
684
+ : {
685
+ bootstrap: currentContext.bootstrap,
686
+ encoding: currentEncoding,
687
+ }, { bootstrap: preview.compilation, encoding: nextEncoding });
688
+ return {
689
+ worldId: request.worldId,
690
+ worldHead: binding.parentHead,
691
+ currentContext,
692
+ nextFreshContext: {
693
+ contextMode: continueContext ? "append" : "fresh",
694
+ head: binding.parentHead,
695
+ preview,
696
+ prefixDiagnostics,
697
+ },
698
+ };
699
+ }
439
700
  case "correction.begin":
440
701
  return this.#corrections.begin({
441
702
  worldId: request.worldId,
@@ -448,12 +709,14 @@ export class V1Runtime {
448
709
  return this.#corrections.patchDocument(request);
449
710
  case "correction.replace":
450
711
  return this.#corrections.replaceDocument(request);
451
- case "correction.preview":
712
+ case "correction.preview": {
713
+ const { hostBinding, modelBinding, playPreset } = await this.#continuousBinding();
452
714
  return this.#corrections.preview({
453
715
  candidateId: request.candidateId,
454
716
  expectedVersion: request.expectedVersion,
455
- prompt: await this.#promptBinding(),
717
+ prompt: { hostBinding, modelBinding, playPreset },
456
718
  });
719
+ }
457
720
  case "correction.apply": {
458
721
  const result = await this.#corrections.apply(request);
459
722
  await this.#reconcileResultArtifacts(result);
@@ -467,6 +730,21 @@ export class V1Runtime {
467
730
  async #modelHost() {
468
731
  return new FileNativeModelHost(await this.#models.bind(), fetch, this.#failureLog);
469
732
  }
733
+ async #worldRevisionCall(operation) {
734
+ try {
735
+ return await operation();
736
+ }
737
+ catch (error) {
738
+ if (error instanceof WorldRevisionWorkspaceError)
739
+ throw new V1ProtocolError("invalid_request", error.message, {
740
+ cause: error,
741
+ });
742
+ throw error;
743
+ }
744
+ }
745
+ async #matchingModelHost(binding) {
746
+ return new FileNativeModelHost(await this.#models.bindMatching(binding), fetch, this.#failureLog);
747
+ }
470
748
  async #reconcileArtifacts(worldId) {
471
749
  const [head, currentOperationId] = await Promise.all([
472
750
  this.#worlds.currentHead(worldId),
@@ -475,19 +753,71 @@ export class V1Runtime {
475
753
  await this.#artifacts.reconcileHead(worldId, head, currentOperationId ?? undefined);
476
754
  return head;
477
755
  }
478
- async #frontendProjection(worldId, channel) {
479
- const artifacts = await this.#artifacts.readActiveProjection(worldId, channel);
480
- return Promise.all(artifacts.map(async (artifact) => {
756
+ async #frontendDecorations(worldId, channel) {
757
+ await this.#currentExtensionControls(worldId);
758
+ const summaries = await this.#artifacts.readExtensionSummaries(worldId);
759
+ const targets = await this.#worlds.playTimeline.resolveReplies(worldId, summaries.flatMap((item) => item.attachment === undefined ? [] : [item.attachment]));
760
+ const matches = (attachment) => targets.find((target) => target.contextId === attachment.contextId &&
761
+ target.eventId === attachment.eventId &&
762
+ target.head === attachment.head);
763
+ const extensions = summaries
764
+ .filter((item) => item.attachment === undefined ||
765
+ matches(item.attachment) !== undefined)
766
+ .sort((left, right) => Number(left.head?.replace("commit:", "") ?? 0) -
767
+ Number(right.head?.replace("commit:", "") ?? 0));
768
+ const artifacts = await this.#artifacts.readActiveProjection(worldId, channel, targets);
769
+ const projected = (await Promise.all(artifacts.map(async (artifact) => {
481
770
  const { renderer, ...safeArtifact } = artifact;
482
771
  void renderer;
483
772
  const resolved = await this.#frontendBinding(artifact.playPresetId, artifact.playPresetRevision);
484
773
  return {
485
774
  ...safeArtifact,
486
- frontend: projectArtifactForFrontend(artifact, resolved.binding, resolved.failure),
775
+ ...(artifact.attachment === undefined
776
+ ? {}
777
+ : {
778
+ reply: matches(artifact.attachment),
779
+ }),
780
+ frontend: projectArtifactForFrontend(artifact, resolved.binding, resolved.failure, artifact.requestId.startsWith("package:") &&
781
+ artifact.frozenPresentation !== undefined &&
782
+ (await this.#worlds.readPackageScriptGrants(worldId)).includes(PackageScriptPermissions.fingerprint(artifact.frozenPresentation.files))),
487
783
  };
488
- }));
784
+ }))).filter((artifact) => artifact.frontend.mount !== "story" || artifact.reply !== undefined);
785
+ return { artifacts: projected, extensions };
786
+ }
787
+ async #currentExtensionControls(worldId) {
788
+ try {
789
+ return await this.#worlds.extensionControls(worldId, await this.#playPresets.bindCurrent(), this.#locale);
790
+ }
791
+ catch (error) {
792
+ if (!(error instanceof FileNativePlayPresetError))
793
+ throw error;
794
+ return {
795
+ revision: await this.#worlds.extensionControlsRevision(worldId),
796
+ items: [],
797
+ };
798
+ }
799
+ }
800
+ async #suppressedPlayerViewIds(controls) {
801
+ let binding;
802
+ try {
803
+ binding = await this.#playPresets.bindCurrent();
804
+ }
805
+ catch (error) {
806
+ if (error instanceof FileNativePlayPresetError)
807
+ return [];
808
+ throw error;
809
+ }
810
+ // Every declared panel owns its view's fallback even while hidden.
811
+ return [
812
+ ...new Set([
813
+ ...binding.definition.playerViewPanels.map((panel) => panel.source.view),
814
+ ...controls.items
815
+ .filter((item) => item.kind === "view" && !item.enabled)
816
+ .map((item) => item.id),
817
+ ]),
818
+ ];
489
819
  }
490
- async #frontendPlayerViewPanels(worldId, head, playerViews) {
820
+ async #frontendPlayerViewPanels(worldId, head, playerViews, controls) {
491
821
  try {
492
822
  const binding = await this.#playPresets.bindCurrent();
493
823
  return projectPlayerViewPanels({
@@ -495,7 +825,7 @@ export class V1Runtime {
495
825
  head,
496
826
  playerViews,
497
827
  binding,
498
- });
828
+ }).filter((panel) => controls.items.some((item) => item.kind === "panel" && item.id === panel.panelId && item.enabled));
499
829
  }
500
830
  catch (error) {
501
831
  // Panels are decoration on top of a world read. Any unusable preset —
@@ -514,7 +844,9 @@ export class V1Runtime {
514
844
  const resolved = await this.#frontendBinding(record.playPresetId, record.playPresetRevision);
515
845
  return {
516
846
  ...safeRecord,
517
- frontend: projectDebugArtifactForFrontend(record, resolved.binding, resolved.failure),
847
+ frontend: projectDebugArtifactForFrontend(record, resolved.binding, resolved.failure, record.requestId.startsWith("package:") &&
848
+ record.frozenPresentation !== undefined &&
849
+ (await this.#worlds.readPackageScriptGrants(worldId)).includes(PackageScriptPermissions.fingerprint(record.frozenPresentation.files))),
518
850
  };
519
851
  }));
520
852
  }
@@ -554,12 +886,6 @@ export class V1Runtime {
554
886
  return;
555
887
  await this.#reconcileArtifacts(worldId);
556
888
  }
557
- #settingImprovement(improvementId) {
558
- const active = this.#settingImprovements.get(improvementId);
559
- if (active === undefined)
560
- throw new Error("Setting-improvement session does not exist");
561
- return active;
562
- }
563
889
  async #promptBinding() {
564
890
  const [preset, connection] = await Promise.all([
565
891
  this.#playPresets.bindCurrent(),