narraeon 0.1.1 → 0.3.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.
- package/dist/node/protocol/modelConnections.js +49 -0
- package/dist/node/protocol/modelUsage.js +39 -0
- package/dist/node/protocol/v1.js +35 -0
- package/dist/node/runtime/FileNativePhysicalClone.js +86 -0
- package/dist/node/runtime/V1Runtime.js +66 -29
- package/dist/node/runtime/model/FileNativeModelAdapters.js +730 -287
- package/dist/node/runtime/model/ModelConnectionStore.js +123 -2
- package/dist/node/runtime/model/ModelHost.js +81 -9
- package/dist/node/runtime/model/ModelHostStream.js +61 -1
- package/dist/node/runtime/model/SettingAuthorStream.js +16 -113
- package/dist/node/runtime/play/FileNativePlayAdvanceStore.js +269 -0
- package/dist/node/runtime/play/FileNativePlayTimelineStore.js +1128 -0
- package/dist/node/runtime/play/PlayCallChain.js +904 -369
- package/dist/node/runtime/play/PlayDocumentTools.js +35 -0
- package/dist/node/runtime/play/PlayFollowupRequests.js +43 -26
- package/dist/node/runtime/prompt/FileNativePromptCompiler.js +50 -35
- package/dist/node/runtime/prompt/FileNativeToolRegistry.js +55 -29
- package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +9 -4
- package/dist/node/runtime/world/FileNativeAuthorityV3.js +971 -0
- package/dist/node/runtime/world/FileNativeHistoryProjection.js +39 -0
- package/dist/node/runtime/world/FileNativeWorldStorageMigrator.js +1067 -0
- package/dist/node/runtime/world/FileNativeWorldStore.js +615 -784
- package/dist/node/server/createServer.js +10 -2
- package/dist/web/assets/index-D5VqDvjY.js +1192 -0
- package/dist/web/assets/index-Xa0s35CJ.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CRGTZ9Ak.css +0 -1
- package/dist/web/assets/index-DDx1itzT.js +0 -1192
|
@@ -1,3 +1,52 @@
|
|
|
1
|
+
/** CLIProxyAPI gives these model-name suffixes priority over body effort. */
|
|
2
|
+
export function hasCLIProxyThinkingSuffix(modelId) {
|
|
3
|
+
return /\((?:-1|none|auto|minimal|low|medium|high|xhigh|max|\d+)\)$/iu.test(modelId);
|
|
4
|
+
}
|
|
5
|
+
/** One validation authority shared by storage and the wire adapter boundary. */
|
|
6
|
+
export function modelReasoningPolicyIssue(input) {
|
|
7
|
+
const thinking = input.thinking ?? "provider_default";
|
|
8
|
+
const thinkingBudgetTokens = input.thinkingBudgetTokens ?? null;
|
|
9
|
+
const thinkingControlledByCLIProxySuffix = input.provider === "anthropic_messages" &&
|
|
10
|
+
input.dialect === "cliproxyapi" &&
|
|
11
|
+
hasCLIProxyThinkingSuffix(input.modelId);
|
|
12
|
+
if (input.provider === "anthropic_messages" &&
|
|
13
|
+
(input.effort === "none" || input.effort === "minimal"))
|
|
14
|
+
return "Anthropic Messages effort supports low, medium, high, xhigh, and max; use Thinking disabled instead of effort none";
|
|
15
|
+
if (input.provider !== "anthropic_messages" &&
|
|
16
|
+
thinking !== "provider_default")
|
|
17
|
+
return "Only Anthropic Messages defines a separate thinking mode; OpenAI-shaped protocols control reasoning through effort";
|
|
18
|
+
if (input.provider !== "anthropic_messages" && thinkingBudgetTokens !== null)
|
|
19
|
+
return "Only Anthropic Messages manual thinking accepts a thinking token budget";
|
|
20
|
+
if (thinking === "enabled") {
|
|
21
|
+
if (thinkingBudgetTokens === null ||
|
|
22
|
+
!Number.isSafeInteger(thinkingBudgetTokens) ||
|
|
23
|
+
thinkingBudgetTokens < 1_024)
|
|
24
|
+
return "Anthropic manual thinking requires an integer budget of at least 1024 tokens";
|
|
25
|
+
if (input.maxOutputTokens !== undefined &&
|
|
26
|
+
thinkingBudgetTokens >= input.maxOutputTokens)
|
|
27
|
+
return "Anthropic thinking budget must be lower than maximum output tokens";
|
|
28
|
+
}
|
|
29
|
+
else if (thinkingBudgetTokens !== null) {
|
|
30
|
+
return "A thinking token budget is valid only when Anthropic Thinking is set to manual enabled";
|
|
31
|
+
}
|
|
32
|
+
if (input.provider === "anthropic_messages" &&
|
|
33
|
+
(thinking === "provider_default" || thinking === "disabled") &&
|
|
34
|
+
input.summary !== "provider_default" &&
|
|
35
|
+
!(thinking === "provider_default" && thinkingControlledByCLIProxySuffix))
|
|
36
|
+
return "Anthropic returned thinking can be configured only when Thinking is adaptive or manual enabled, or when a CLIProxyAPI model suffix controls Thinking";
|
|
37
|
+
if (input.provider !== "openai_responses" &&
|
|
38
|
+
(input.summary === "concise" || input.summary === "detailed"))
|
|
39
|
+
return "Concise and detailed reasoning summaries are defined only by OpenAI Responses";
|
|
40
|
+
if (input.provider === "chat_completions" &&
|
|
41
|
+
input.dialect === "standard" &&
|
|
42
|
+
input.summary !== "provider_default")
|
|
43
|
+
return "Standard Chat Completions has no reasoning-summary parameter; use provider default or select the CLIProxyAPI dialect";
|
|
44
|
+
if (input.dialect === "cliproxyapi" &&
|
|
45
|
+
(input.effort !== "provider_default" || thinking !== "provider_default") &&
|
|
46
|
+
hasCLIProxyThinkingSuffix(input.modelId))
|
|
47
|
+
return "CLIProxyAPI model thinking suffixes override request effort and thinking; use either the model suffix with provider defaults or explicit request fields, not both";
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
1
50
|
export const modelProviderPresets = [
|
|
2
51
|
{
|
|
3
52
|
id: "custom",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const modelUsageKeys = [
|
|
2
|
+
"inputTokens",
|
|
3
|
+
"uncachedInputTokens",
|
|
4
|
+
"cacheReadTokens",
|
|
5
|
+
"cacheWriteTokens",
|
|
6
|
+
"reasoningTokens",
|
|
7
|
+
"outputTokens",
|
|
8
|
+
"totalTokens",
|
|
9
|
+
];
|
|
10
|
+
/** A zero baseline used only before any Provider exchange has completed. */
|
|
11
|
+
export function emptyAggregatedModelUsage() {
|
|
12
|
+
return usageWithValue(0, "derived_provider_fields");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Sum complete Provider usage records without treating an unreported field as
|
|
16
|
+
* zero. Once any exchange lacks a field, that aggregate remains unavailable.
|
|
17
|
+
*/
|
|
18
|
+
export function aggregateModelUsage(current, next) {
|
|
19
|
+
const result = {};
|
|
20
|
+
for (const key of modelUsageKeys) {
|
|
21
|
+
const right = next?.[key] ?? null;
|
|
22
|
+
result[key] =
|
|
23
|
+
current[key] === null || right === null ? null : current[key] + right;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
...result,
|
|
27
|
+
provenance: Object.fromEntries(modelUsageKeys.map((key) => [
|
|
28
|
+
key,
|
|
29
|
+
result[key] === null ? "unavailable" : "derived_provider_fields",
|
|
30
|
+
])),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function usageWithValue(value, source) {
|
|
34
|
+
const values = Object.fromEntries(modelUsageKeys.map((key) => [key, value]));
|
|
35
|
+
return {
|
|
36
|
+
...values,
|
|
37
|
+
provenance: Object.fromEntries(modelUsageKeys.map((key) => [key, source])),
|
|
38
|
+
};
|
|
39
|
+
}
|
package/dist/node/protocol/v1.js
CHANGED
|
@@ -106,7 +106,20 @@ const requiredFields = {
|
|
|
106
106
|
exchangeId: "string",
|
|
107
107
|
playerText: "string",
|
|
108
108
|
},
|
|
109
|
+
"play.chain.cancel": {
|
|
110
|
+
worldId: "string",
|
|
111
|
+
chainId: "string",
|
|
112
|
+
exchangeId: "string",
|
|
113
|
+
},
|
|
109
114
|
"play.chain.inspect": { worldId: "string" },
|
|
115
|
+
"play.timeline.page": { worldId: "string", limit: "number" },
|
|
116
|
+
"play.timeline.detail": {
|
|
117
|
+
worldId: "string",
|
|
118
|
+
chainId: "string",
|
|
119
|
+
eventId: "number",
|
|
120
|
+
},
|
|
121
|
+
"world.surface.read": { worldId: "string", surface: "string" },
|
|
122
|
+
"world.play-decorations.read": { worldId: "string" },
|
|
110
123
|
"correction.begin": { worldId: "string", operationId: "string" },
|
|
111
124
|
"correction.read": { candidateId: "string", document: "string" },
|
|
112
125
|
"correction.patch": {
|
|
@@ -205,6 +218,23 @@ function validateRequestFields(request) {
|
|
|
205
218
|
request.operationId !== undefined &&
|
|
206
219
|
(typeof request.operationId !== "string" || request.operationId === ""))
|
|
207
220
|
throw new V1ProtocolError("invalid_request", "artifacts.debug.operationId is invalid");
|
|
221
|
+
if (request.type === "play.timeline.page") {
|
|
222
|
+
if (!Number.isSafeInteger(request.limit) ||
|
|
223
|
+
Number(request.limit) < 1 ||
|
|
224
|
+
Number(request.limit) > 100 ||
|
|
225
|
+
(request.cursor !== undefined &&
|
|
226
|
+
(typeof request.cursor !== "string" || request.cursor === "")))
|
|
227
|
+
throw new V1ProtocolError("invalid_request", "play.timeline.page pagination is invalid");
|
|
228
|
+
}
|
|
229
|
+
if (request.type === "play.timeline.detail" &&
|
|
230
|
+
(!Number.isSafeInteger(request.eventId) || Number(request.eventId) < 1))
|
|
231
|
+
throw new V1ProtocolError("invalid_request", "play.timeline.detail.eventId is invalid");
|
|
232
|
+
if (request.type === "world.surface.read" &&
|
|
233
|
+
request.surface !== "state" &&
|
|
234
|
+
request.surface !== "control" &&
|
|
235
|
+
request.surface !== "history" &&
|
|
236
|
+
request.surface !== "runtime")
|
|
237
|
+
throw new V1ProtocolError("invalid_request", "world.surface.read.surface is invalid");
|
|
208
238
|
if (request.type !== "setting-improvement.start")
|
|
209
239
|
return;
|
|
210
240
|
if (request.mode !== "plan_first" && request.mode !== "direct_candidate")
|
|
@@ -264,7 +294,12 @@ const requestTypes = new Set([
|
|
|
264
294
|
"play.chain.revise-player",
|
|
265
295
|
"play.chain.start",
|
|
266
296
|
"play.chain.append",
|
|
297
|
+
"play.chain.cancel",
|
|
267
298
|
"play.chain.inspect",
|
|
299
|
+
"play.timeline.page",
|
|
300
|
+
"play.timeline.detail",
|
|
301
|
+
"world.surface.read",
|
|
302
|
+
"world.play-decorations.read",
|
|
268
303
|
"correction.begin",
|
|
269
304
|
"correction.read",
|
|
270
305
|
"correction.patch",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { copyFile, link, mkdir } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
const hardlinkFallbackCodes = new Set([
|
|
5
|
+
"EXDEV",
|
|
6
|
+
"EPERM",
|
|
7
|
+
"EACCES",
|
|
8
|
+
"EMLINK",
|
|
9
|
+
"ENOTSUP",
|
|
10
|
+
"EOPNOTSUPP",
|
|
11
|
+
"ENOSYS",
|
|
12
|
+
]);
|
|
13
|
+
const reflinkFallbackCodes = new Set([
|
|
14
|
+
"EXDEV",
|
|
15
|
+
"EPERM",
|
|
16
|
+
"EACCES",
|
|
17
|
+
"ENOTSUP",
|
|
18
|
+
"EOPNOTSUPP",
|
|
19
|
+
"EINVAL",
|
|
20
|
+
"ENOSYS",
|
|
21
|
+
]);
|
|
22
|
+
/**
|
|
23
|
+
* Retains one file without aliasing mutable bytes. Immutable files prefer a
|
|
24
|
+
* hard link; every other case walks reflink -> exclusive byte copy.
|
|
25
|
+
*/
|
|
26
|
+
export async function cloneFilePhysically(input) {
|
|
27
|
+
await mkdir(dirname(input.target), { recursive: true, mode: 0o700 });
|
|
28
|
+
const strategy = process.env.NARRAEON_INTERNAL_TEST_CLONE_STRATEGY;
|
|
29
|
+
if (input.immutable && strategy !== "reflink" && strategy !== "copy") {
|
|
30
|
+
try {
|
|
31
|
+
await link(input.source, input.target);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (await acceptExistingTarget(error, input))
|
|
36
|
+
return;
|
|
37
|
+
if (!isFallbackError(error, hardlinkFallbackCodes))
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (strategy !== "copy") {
|
|
42
|
+
try {
|
|
43
|
+
await copyFileAsReflink(input.source, input.target);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (await acceptExistingTarget(error, input))
|
|
48
|
+
return;
|
|
49
|
+
if (!isFallbackError(error, reflinkFallbackCodes))
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await copyFile(input.source, input.target, constants.COPYFILE_EXCL);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (await acceptExistingTarget(error, input))
|
|
58
|
+
return;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function copyFileAsReflink(source, target) {
|
|
63
|
+
const injectedErrorCode = process.env.NARRAEON_INTERNAL_TEST_REFLINK_ERROR_CODE;
|
|
64
|
+
if (injectedErrorCode !== undefined) {
|
|
65
|
+
const error = new Error(`${injectedErrorCode}: injected copy-on-write clone failure`);
|
|
66
|
+
error.code = injectedErrorCode;
|
|
67
|
+
error.path = source;
|
|
68
|
+
error.syscall = "copyfile";
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
await copyFile(source, target, constants.COPYFILE_FICLONE_FORCE);
|
|
72
|
+
}
|
|
73
|
+
async function acceptExistingTarget(error, input) {
|
|
74
|
+
if (!isNodeError(error) ||
|
|
75
|
+
error.code !== "EEXIST" ||
|
|
76
|
+
input.onTargetExists === undefined)
|
|
77
|
+
return false;
|
|
78
|
+
await input.onTargetExists(input.source, input.target);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
function isFallbackError(error, acceptedCodes) {
|
|
82
|
+
return isNodeError(error) && acceptedCodes.has(error.code ?? "");
|
|
83
|
+
}
|
|
84
|
+
function isNodeError(error) {
|
|
85
|
+
return error instanceof Error && "code" in error;
|
|
86
|
+
}
|
|
@@ -239,7 +239,8 @@ export class V1Runtime {
|
|
|
239
239
|
: this.#playPresets.bindRevision(request.playPresetId, request.playPresetRevision),
|
|
240
240
|
]);
|
|
241
241
|
const hostBinding = presetHostBinding(playPreset);
|
|
242
|
-
|
|
242
|
+
const modelHost = new FileNativeModelHost(connection);
|
|
243
|
+
const preview = this.#compiler.preview({
|
|
243
244
|
endpoint: {
|
|
244
245
|
id: `content:${request.packageId}`,
|
|
245
246
|
commit: "current",
|
|
@@ -251,8 +252,32 @@ export class V1Runtime {
|
|
|
251
252
|
}), `content:${request.packageId}.message.genesis.narrator`, "content-preview"),
|
|
252
253
|
playerInputPlacement: "append",
|
|
253
254
|
playerInput: request.playerInput,
|
|
254
|
-
modelBinding:
|
|
255
|
+
modelBinding: modelHost.binding(),
|
|
255
256
|
}, playPreset);
|
|
257
|
+
const tools = structuredClone(preview.compilation.toolUniverse ?? preview.compilation.tools);
|
|
258
|
+
return {
|
|
259
|
+
...preview,
|
|
260
|
+
wireRequest: modelHost.previewRequest({
|
|
261
|
+
bootstrap: structuredClone(preview.compilation),
|
|
262
|
+
tools: structuredClone(tools),
|
|
263
|
+
toolUniverse: structuredClone(tools),
|
|
264
|
+
allowedTools: tools.map(({ name }) => name),
|
|
265
|
+
toolStrategy: preview.compilation.toolStrategy,
|
|
266
|
+
appended: preview.initialAppend === undefined
|
|
267
|
+
? []
|
|
268
|
+
: [
|
|
269
|
+
{
|
|
270
|
+
kind: "player",
|
|
271
|
+
text: preview.initialAppend.logical.text,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
requestId: "play_call_chain",
|
|
275
|
+
operationId: "prompt-preview",
|
|
276
|
+
requestAttempt: 1,
|
|
277
|
+
exchange: 1,
|
|
278
|
+
maxOutputTokens: modelHost.binding().maxOutputTokens,
|
|
279
|
+
}),
|
|
280
|
+
};
|
|
256
281
|
}
|
|
257
282
|
case "world.create": {
|
|
258
283
|
const [package_, connection, preset] = await Promise.all([
|
|
@@ -280,38 +305,42 @@ export class V1Runtime {
|
|
|
280
305
|
return this.#worlds.deleteWorld(request.worldId);
|
|
281
306
|
});
|
|
282
307
|
case "world.read": {
|
|
283
|
-
await this.#reconcileArtifacts(request.worldId);
|
|
284
308
|
const head = await this.#worlds.currentHead(request.worldId);
|
|
285
309
|
const playerViews = await this.#worlds.renderPlayerViewsAtHead(request.worldId, head);
|
|
286
|
-
const [
|
|
287
|
-
this.#worlds.readSurface(request.worldId, "state"),
|
|
288
|
-
this.#worlds.readSurface(request.worldId, "control"),
|
|
289
|
-
this.#worlds.readSurface(request.worldId, "history"),
|
|
290
|
-
this.#worlds.readSurface(request.worldId, "runtime"),
|
|
291
|
-
this.#worlds.recoverEndpoint(request.worldId, head),
|
|
292
|
-
this.#worlds.readAuthorityHistory(request.worldId),
|
|
293
|
-
this.#frontendProjection(request.worldId),
|
|
294
|
-
this.#artifacts.readExtensionSummaries(request.worldId),
|
|
310
|
+
const [playerViewPanels, playTimeline] = await Promise.all([
|
|
295
311
|
this.#frontendPlayerViewPanels(request.worldId, head, playerViews),
|
|
312
|
+
this.#worlds.playTimeline.readPage(request.worldId, 40),
|
|
296
313
|
]);
|
|
297
314
|
return {
|
|
298
315
|
worldId: request.worldId,
|
|
299
316
|
head,
|
|
300
|
-
state,
|
|
301
|
-
control,
|
|
302
|
-
history,
|
|
303
|
-
runtime,
|
|
317
|
+
state: [],
|
|
318
|
+
control: [],
|
|
319
|
+
history: [],
|
|
320
|
+
runtime: { head, surfaces: "lazy" },
|
|
304
321
|
playerViews,
|
|
305
|
-
committedMessages:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
})),
|
|
309
|
-
artifacts,
|
|
310
|
-
extensions,
|
|
322
|
+
committedMessages: [],
|
|
323
|
+
artifacts: [],
|
|
324
|
+
extensions: [],
|
|
311
325
|
playerViewPanels,
|
|
312
|
-
|
|
326
|
+
artifactDebug: [],
|
|
327
|
+
playCallChain: null,
|
|
328
|
+
playTimeline,
|
|
313
329
|
};
|
|
314
330
|
}
|
|
331
|
+
case "world.surface.read":
|
|
332
|
+
return request.surface === "runtime"
|
|
333
|
+
? this.#worlds.readSurface(request.worldId, "runtime")
|
|
334
|
+
: this.#worlds.readSurface(request.worldId, request.surface);
|
|
335
|
+
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 };
|
|
343
|
+
}
|
|
315
344
|
case "artifacts.read":
|
|
316
345
|
await this.#reconcileArtifacts(request.worldId);
|
|
317
346
|
return this.#frontendProjection(request.worldId, request.channel);
|
|
@@ -345,17 +374,12 @@ export class V1Runtime {
|
|
|
345
374
|
}
|
|
346
375
|
case "world.derive": {
|
|
347
376
|
const preset = await this.#playPresets.bindCurrent();
|
|
348
|
-
const result = await this.#
|
|
377
|
+
const result = await this.#playCallChains.deriveWorld({
|
|
349
378
|
operationId: request.operationId,
|
|
350
379
|
sourceWorldId: request.sourceWorldId,
|
|
351
380
|
sourceHead: request.sourceHead,
|
|
352
381
|
hostPresetId: preset.id,
|
|
353
382
|
});
|
|
354
|
-
await this.#playCallChains.forkToDerivedWorld({
|
|
355
|
-
sourceWorldId: request.sourceWorldId,
|
|
356
|
-
sourceHead: request.sourceHead,
|
|
357
|
-
targetWorldId: result.world.worldId,
|
|
358
|
-
});
|
|
359
383
|
await this.#reconcileResultArtifacts(result);
|
|
360
384
|
return result;
|
|
361
385
|
}
|
|
@@ -398,8 +422,20 @@ export class V1Runtime {
|
|
|
398
422
|
? {}
|
|
399
423
|
: { observer: playCallChainObserver }),
|
|
400
424
|
}));
|
|
425
|
+
case "play.chain.cancel":
|
|
426
|
+
return playCall(() => this.#playCallChains.cancel({
|
|
427
|
+
worldId: request.worldId,
|
|
428
|
+
chainId: request.chainId,
|
|
429
|
+
exchangeId: request.exchangeId,
|
|
430
|
+
}));
|
|
401
431
|
case "play.chain.inspect":
|
|
402
432
|
return this.#playCallChains.inspectWorld(request.worldId);
|
|
433
|
+
case "play.timeline.page":
|
|
434
|
+
await this.#worlds.ensureCurrentStorage(request.worldId);
|
|
435
|
+
return this.#worlds.playTimeline.readPage(request.worldId, request.limit, request.cursor);
|
|
436
|
+
case "play.timeline.detail":
|
|
437
|
+
await this.#worlds.ensureCurrentStorage(request.worldId);
|
|
438
|
+
return this.#worlds.playTimeline.readDetail(request.worldId, request.chainId, request.eventId);
|
|
403
439
|
case "correction.begin":
|
|
404
440
|
return this.#corrections.begin({
|
|
405
441
|
worldId: request.worldId,
|
|
@@ -437,6 +473,7 @@ export class V1Runtime {
|
|
|
437
473
|
this.#worlds.currentHeadOperationId(worldId),
|
|
438
474
|
]);
|
|
439
475
|
await this.#artifacts.reconcileHead(worldId, head, currentOperationId ?? undefined);
|
|
476
|
+
return head;
|
|
440
477
|
}
|
|
441
478
|
async #frontendProjection(worldId, channel) {
|
|
442
479
|
const artifacts = await this.#artifacts.readActiveProjection(worldId, channel);
|