narraeon 0.4.0 → 0.5.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 (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 +186 -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 +445 -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 +13 -0
  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-DG85hpWC.css +1 -0
  70. package/dist/web/assets/index-DfxvCjb6.js +1207 -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-D8YKfmNG.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();
@@ -94,6 +226,14 @@ export class V1Runtime {
94
226
  return this.#models.listModels(request);
95
227
  case "content.create":
96
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);
97
237
  case "content.read":
98
238
  return this.#content.readCurrentTreeContentPackage(request.packageId);
99
239
  case "content.replace":
@@ -108,7 +248,7 @@ export class V1Runtime {
108
248
  case "content.import": {
109
249
  const archive = decodePortableContentArchive(request.archiveBase64);
110
250
  try {
111
- return await this.#content.importPortableContentPackageArchive(archive);
251
+ return await this.#content.importPortableContentPackageArchive(archive, request.title === undefined ? {} : { title: request.title });
112
252
  }
113
253
  catch (error) {
114
254
  if (error instanceof ContentPackageImportError ||
@@ -126,82 +266,33 @@ export class V1Runtime {
126
266
  base64: exported.archive.toString("base64"),
127
267
  };
128
268
  }
129
- case "setting-improvement.start": {
130
- if (this.#settingImprovements.has(request.improvementId))
131
- throw new Error("Setting-improvement ID already exists");
132
- const [package_, connection, preset] = await Promise.all([
133
- this.#content.readCurrentTreeContentPackage(request.packageId),
134
- this.#models.bind(),
135
- this.#playPresets.bindCurrent(),
136
- ]);
137
- const improvement = new DocumentCandidateSettingImprovement({
138
- files: package_.files,
139
- adapter: new FileNativeSettingAuthorProvider(connection, fetch, {
140
- ...(this.#failureLog === undefined
141
- ? {}
142
- : { failureLog: this.#failureLog }),
143
- operationId: request.improvementId,
144
- locale: this.#locale,
145
- }),
146
- authorPrompt: settingImprovementPromptForBinding(preset, this.#locale),
147
- locale: this.#locale,
148
- ...(this.#failureLog === undefined
149
- ? {}
150
- : { failureLog: this.#failureLog }),
151
- preview: (snapshot) => this.#compiler.preview({
152
- endpoint: { id: "setting-candidate", commit: "candidate" },
153
- hostBinding: {
154
- hostPresetId: "setting-candidate",
155
- files: defaultSettingPreviewHost(),
156
- },
157
- world: previewWorld(snapshot, "setting-candidate.message.genesis.narrator", "setting-candidate"),
158
- playerInputPlacement: "bootstrap",
159
- playerInput: this.#locale === "zh-CN"
160
- ? "预览设定候选。"
161
- : "Preview the setting candidate.",
162
- modelBinding: new FileNativeModelHost(connection).binding(),
163
- }),
164
- });
165
- if (this.#settingImprovements.has(request.improvementId))
166
- throw new Error("Setting-improvement ID already exists");
167
- this.#settingImprovements.set(request.improvementId, {
168
- packageId: request.packageId,
169
- improvement,
170
- });
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":
171
282
  try {
172
- return await improvement.start({
173
- goal: request.goal,
174
- contextPaths: request.contextPaths,
175
- mode: request.mode,
176
- });
283
+ return await this.#settingImprovements.rollback(request);
177
284
  }
178
285
  catch (error) {
179
- this.#settingImprovements.delete(request.improvementId);
286
+ if (error instanceof SettingImprovementRollbackError)
287
+ throw new V1ProtocolError("invalid_request", error.message, {
288
+ cause: error,
289
+ });
180
290
  throw error;
181
291
  }
182
- }
183
- case "setting-improvement.confirm":
184
- return this.#settingImprovement(request.improvementId).improvement.confirmPlan();
185
- // Answers while start/confirm is still awaiting the provider: those calls
186
- // yield the event loop, so this read observes the run in flight.
187
- case "setting-improvement.progress":
188
- return this.#settingImprovement(request.improvementId).improvement.progress();
189
- case "setting-improvement.revise-plan":
190
- return this.#settingImprovement(request.improvementId).improvement.revisePlan(request.feedback);
191
- case "setting-improvement.revise-candidate":
192
- return this.#settingImprovement(request.improvementId).improvement.reviseCandidate(request.feedback);
193
- case "setting-improvement.apply": {
194
- const active = this.#settingImprovement(request.improvementId);
195
- await active.improvement.apply(async (files) => {
196
- await this.#content.replaceCurrentTreeContentPackage(active.packageId, files);
197
- });
198
- this.#settingImprovements.delete(request.improvementId);
199
- return { applied: true };
200
- }
201
- case "setting-improvement.discard":
202
- this.#settingImprovement(request.improvementId).improvement.discard();
203
- this.#settingImprovements.delete(request.improvementId);
204
- 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);
205
296
  case "play.read":
206
297
  return this.#playPresets.list();
207
298
  case "play.create":
@@ -230,7 +321,11 @@ export class V1Runtime {
230
321
  const binding = request.presetId === undefined
231
322
  ? await this.#playPresets.bindCurrent()
232
323
  : await this.#playPresets.bindRevision(request.presetId, request.revision);
233
- 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);
234
329
  }
235
330
  case "prompt.preview": {
236
331
  const [package_, connection, playPreset] = await Promise.all([
@@ -268,6 +363,9 @@ export class V1Runtime {
268
363
  appended: preview.initialAppend === undefined
269
364
  ? []
270
365
  : [
366
+ ...(preview.initialAppend.beforePlayer === undefined
367
+ ? []
368
+ : [preview.initialAppend.beforePlayer.logical]),
271
369
  {
272
370
  kind: "player",
273
371
  text: preview.initialAppend.logical.text,
@@ -290,6 +388,8 @@ export class V1Runtime {
290
388
  return this.#worlds.createFromContentPackage({
291
389
  operationId: request.operationId,
292
390
  sourcePackageId: request.packageId,
391
+ sourcePackageTitle: package_.title,
392
+ packageScriptGrants: await this.#content.readPackageScriptGrants(request.packageId),
293
393
  packageFiles: package_.files,
294
394
  prompt: {
295
395
  hostBinding: presetHostBinding(preset),
@@ -303,15 +403,18 @@ export class V1Runtime {
303
403
  return this.#worlds.renameWorld(request.worldId, request.name);
304
404
  case "world.delete":
305
405
  return playCall(async () => {
406
+ await this.#worlds.assertWorldRevisionUnlocked(request.worldId);
306
407
  this.#playCallChains.forgetWorld(request.worldId);
307
408
  return this.#worlds.deleteWorld(request.worldId);
308
409
  });
309
410
  case "world.read": {
310
411
  const head = await this.#worlds.currentHead(request.worldId);
311
412
  const playerViews = await this.#worlds.renderPlayerViewsAtHead(request.worldId, head);
312
- const [playerViewPanels, playTimeline] = await Promise.all([
313
- 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),
314
416
  this.#worlds.playTimeline.readPage(request.worldId, 40),
417
+ this.#worldRevisions.activeEpoch(request.worldId),
315
418
  ]);
316
419
  return {
317
420
  worldId: request.worldId,
@@ -325,27 +428,54 @@ export class V1Runtime {
325
428
  artifacts: [],
326
429
  extensions: [],
327
430
  playerViewPanels,
431
+ extensionControls,
432
+ suppressedPlayerViewIds: await this.#suppressedPlayerViewIds(extensionControls),
328
433
  artifactDebug: [],
329
434
  playCallChain: null,
330
435
  playTimeline,
436
+ worldRevision: revisionEpoch,
331
437
  };
332
438
  }
333
439
  case "world.surface.read":
334
440
  return request.surface === "runtime"
335
441
  ? this.#worlds.readSurface(request.worldId, "runtime")
336
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
+ }
337
449
  case "world.play-decorations.read": {
338
- const head = await this.#reconcileArtifacts(request.worldId);
339
- const [artifacts, extensions, artifactDebug] = await Promise.all([
340
- this.#frontendProjection(request.worldId),
341
- this.#artifacts.readExtensionSummaries(request.worldId),
342
- this.#frontendDebug(request.worldId),
343
- ]);
344
- 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
+ }
345
475
  }
346
476
  case "artifacts.read":
347
477
  await this.#reconcileArtifacts(request.worldId);
348
- return this.#frontendProjection(request.worldId, request.channel);
478
+ return (await this.#frontendDecorations(request.worldId, request.channel)).artifacts;
349
479
  case "artifacts.debug":
350
480
  await this.#reconcileArtifacts(request.worldId);
351
481
  return this.#frontendDebug(request.worldId, request.operationId);
@@ -355,6 +485,7 @@ export class V1Runtime {
355
485
  return result;
356
486
  }
357
487
  case "world.control-draft.save":
488
+ await this.#worlds.assertWorldRevisionUnlocked(request.worldId);
358
489
  return this.#worlds.saveControlDraft(request.worldId, request.files);
359
490
  case "world.control-draft.preview": {
360
491
  const prompt = {
@@ -374,6 +505,33 @@ export class V1Runtime {
374
505
  };
375
506
  return this.#worlds.applyControlDraft(request.worldId, prompt);
376
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));
377
535
  case "world.derive": {
378
536
  const preset = await this.#playPresets.bindCurrent();
379
537
  const result = await this.#playCallChains.deriveWorld({
@@ -386,18 +544,33 @@ export class V1Runtime {
386
544
  return result;
387
545
  }
388
546
  case "play.chain.revise-player": {
389
- const result = await playCall(() => this.#playCallChains.revisePlayer({
547
+ const base = {
390
548
  operationId: request.operationId,
391
549
  worldId: request.worldId,
392
550
  chainId: request.chainId,
393
551
  eventId: request.eventId,
394
552
  replacementExchangeId: request.replacementExchangeId,
395
553
  replacementText: request.replacementText,
396
- }));
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
+ });
397
569
  await this.#reconcileArtifacts(request.worldId);
398
570
  return result;
399
571
  }
400
572
  case "play.chain.start": {
573
+ await this.#worlds.assertWorldRevisionUnlocked(request.worldId);
401
574
  const { modelHost, hostBinding, playPreset, modelBinding } = await this.#continuousBinding();
402
575
  return playCall(() => this.#playCallChains.start({
403
576
  worldId: request.worldId,
@@ -414,16 +587,20 @@ export class V1Runtime {
414
587
  }));
415
588
  }
416
589
  case "play.chain.append":
417
- return playCall(async () => this.#playCallChains.append({
418
- worldId: request.worldId,
419
- chainId: request.chainId,
420
- exchangeId: request.exchangeId,
421
- playerText: request.playerText,
422
- modelHost: await this.#modelHost(),
423
- ...(playCallChainObserver === undefined
424
- ? {}
425
- : { observer: playCallChainObserver }),
426
- }));
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
+ });
427
604
  case "play.chain.cancel":
428
605
  return playCall(() => this.#playCallChains.cancel({
429
606
  worldId: request.worldId,
@@ -438,6 +615,88 @@ export class V1Runtime {
438
615
  case "play.timeline.detail":
439
616
  await this.#worlds.ensureCurrentStorage(request.worldId);
440
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
+ }
441
700
  case "correction.begin":
442
701
  return this.#corrections.begin({
443
702
  worldId: request.worldId,
@@ -450,12 +709,14 @@ export class V1Runtime {
450
709
  return this.#corrections.patchDocument(request);
451
710
  case "correction.replace":
452
711
  return this.#corrections.replaceDocument(request);
453
- case "correction.preview":
712
+ case "correction.preview": {
713
+ const { hostBinding, modelBinding, playPreset } = await this.#continuousBinding();
454
714
  return this.#corrections.preview({
455
715
  candidateId: request.candidateId,
456
716
  expectedVersion: request.expectedVersion,
457
- prompt: await this.#promptBinding(),
717
+ prompt: { hostBinding, modelBinding, playPreset },
458
718
  });
719
+ }
459
720
  case "correction.apply": {
460
721
  const result = await this.#corrections.apply(request);
461
722
  await this.#reconcileResultArtifacts(result);
@@ -469,6 +730,21 @@ export class V1Runtime {
469
730
  async #modelHost() {
470
731
  return new FileNativeModelHost(await this.#models.bind(), fetch, this.#failureLog);
471
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
+ }
472
748
  async #reconcileArtifacts(worldId) {
473
749
  const [head, currentOperationId] = await Promise.all([
474
750
  this.#worlds.currentHead(worldId),
@@ -477,19 +753,71 @@ export class V1Runtime {
477
753
  await this.#artifacts.reconcileHead(worldId, head, currentOperationId ?? undefined);
478
754
  return head;
479
755
  }
480
- async #frontendProjection(worldId, channel) {
481
- const artifacts = await this.#artifacts.readActiveProjection(worldId, channel);
482
- 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) => {
483
770
  const { renderer, ...safeArtifact } = artifact;
484
771
  void renderer;
485
772
  const resolved = await this.#frontendBinding(artifact.playPresetId, artifact.playPresetRevision);
486
773
  return {
487
774
  ...safeArtifact,
488
- 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))),
489
783
  };
490
- }));
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
+ ];
491
819
  }
492
- async #frontendPlayerViewPanels(worldId, head, playerViews) {
820
+ async #frontendPlayerViewPanels(worldId, head, playerViews, controls) {
493
821
  try {
494
822
  const binding = await this.#playPresets.bindCurrent();
495
823
  return projectPlayerViewPanels({
@@ -497,7 +825,7 @@ export class V1Runtime {
497
825
  head,
498
826
  playerViews,
499
827
  binding,
500
- });
828
+ }).filter((panel) => controls.items.some((item) => item.kind === "panel" && item.id === panel.panelId && item.enabled));
501
829
  }
502
830
  catch (error) {
503
831
  // Panels are decoration on top of a world read. Any unusable preset —
@@ -516,7 +844,9 @@ export class V1Runtime {
516
844
  const resolved = await this.#frontendBinding(record.playPresetId, record.playPresetRevision);
517
845
  return {
518
846
  ...safeRecord,
519
- 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))),
520
850
  };
521
851
  }));
522
852
  }
@@ -556,12 +886,6 @@ export class V1Runtime {
556
886
  return;
557
887
  await this.#reconcileArtifacts(worldId);
558
888
  }
559
- #settingImprovement(improvementId) {
560
- const active = this.#settingImprovements.get(improvementId);
561
- if (active === undefined)
562
- throw new Error("Setting-improvement session does not exist");
563
- return active;
564
- }
565
889
  async #promptBinding() {
566
890
  const [preset, connection] = await Promise.all([
567
891
  this.#playPresets.bindCurrent(),