@mengine/medeo-tool 1.2.1-alpha.0 → 1.2.1-alpha.7

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/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { i as renderCompactProjection, n as collectAffectedPartIds, r as renderPreview, t as EditSandboxSession } from "./script-session-DdPA4tTf.mjs";
2
- import { MengineDocSession, MengineHttpClient, MengineHttpRequestError, ValidationError, createMirrorVideoDocument, createPlainMemoryAdapter, replayJournal, toVideoDocument } from "@mengine/medeo-client";
1
+ import { i as renderCompactProjection, n as collectAffectedPartIds, r as renderPreview, t as EditSandboxSession } from "./script-session-BF44uKv_.mjs";
2
+ import { ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, encodeDocVersionMark, replayJournal, toVideoDocument } from "@mengine/medeo-client";
3
3
  import { Worker } from "node:worker_threads";
4
4
  import { randomUUID } from "node:crypto";
5
5
  //#region src/sandbox/node-host.ts
@@ -23,6 +23,7 @@ function runEditScript(options) {
23
23
  const workerEntryUrl = options.workerEntryUrl ?? sourceSibling("worker-entry");
24
24
  const resolveRegisterUrl = sourceSibling("node-esm-resolve-register");
25
25
  const ops = [];
26
+ const entityCommands = [];
26
27
  const logs = [];
27
28
  return new Promise((resolve) => {
28
29
  let settled = false;
@@ -33,6 +34,7 @@ function runEditScript(options) {
33
34
  document: options.document,
34
35
  script: options.script,
35
36
  inputs: options.inputs,
37
+ entityState: options.entityState,
36
38
  idLabel: options.idLabel
37
39
  },
38
40
  execArgv: resolveRegisterUrl.pathname.endsWith(".ts") ? [
@@ -54,6 +56,7 @@ function runEditScript(options) {
54
56
  error: { message: `edit script exceeded timeout of ${timeoutMs}ms` },
55
57
  partial: {
56
58
  ops: ops.slice(),
59
+ entityCommands: entityCommands.slice(),
57
60
  logs: logs.slice()
58
61
  }
59
62
  });
@@ -81,6 +84,10 @@ function runEditScript(options) {
81
84
  ops.push(message.entry);
82
85
  return;
83
86
  }
87
+ if (message.t === "entity-entry") {
88
+ entityCommands.push(message.command);
89
+ return;
90
+ }
84
91
  if (message.t === "log") {
85
92
  logs.push(message.line);
86
93
  return;
@@ -89,14 +96,19 @@ function runEditScript(options) {
89
96
  ops.length = Math.max(0, Math.min(message.index, ops.length));
90
97
  return;
91
98
  }
99
+ if (message.t === "entity-truncate") {
100
+ entityCommands.length = Math.max(0, Math.min(message.index, entityCommands.length));
101
+ return;
102
+ }
92
103
  if (message.t === "done") {
93
- if (message.opsCount !== ops.length) {
104
+ if (message.opsCount !== ops.length || message.entityCommandsCount !== entityCommands.length) {
94
105
  finish({
95
106
  ok: false,
96
107
  phase: "runtime",
97
- error: { message: `opsCount mismatch: worker reported ${message.opsCount}, host collected ${ops.length}` },
108
+ error: { message: `journal count mismatch: worker reported timeline=${message.opsCount}, entities=${message.entityCommandsCount}; host collected timeline=${ops.length}, entities=${entityCommands.length}` },
98
109
  partial: {
99
110
  ops: ops.slice(),
111
+ entityCommands: entityCommands.slice(),
100
112
  logs: logs.slice()
101
113
  }
102
114
  });
@@ -105,9 +117,13 @@ function runEditScript(options) {
105
117
  finish({
106
118
  ok: true,
107
119
  plan: {
120
+ plan_kind: message.planKind,
108
121
  doc_id: options.document.meta.draft_id ?? "",
109
122
  base_version: options.baseVersion,
110
123
  ops: ops.slice(),
124
+ entity_base_revision: message.entityBaseRevision,
125
+ entity_commands: entityCommands.slice(),
126
+ ...message.entityRows !== void 0 ? { entity_rows: message.entityRows } : {},
111
127
  preview: message.preview,
112
128
  logs: logs.slice()
113
129
  },
@@ -121,6 +137,7 @@ function runEditScript(options) {
121
137
  error: message.error,
122
138
  partial: {
123
139
  ops: ops.slice(),
140
+ entityCommands: entityCommands.slice(),
124
141
  logs: logs.slice()
125
142
  }
126
143
  });
@@ -137,6 +154,7 @@ function runEditScript(options) {
137
154
  },
138
155
  partial: {
139
156
  ops: ops.slice(),
157
+ entityCommands: entityCommands.slice(),
140
158
  logs: logs.slice()
141
159
  }
142
160
  });
@@ -150,6 +168,7 @@ function runEditScript(options) {
150
168
  error: { message: `worker exited with code ${code ?? "null"} before completion` },
151
169
  partial: {
152
170
  ops: ops.slice(),
171
+ entityCommands: entityCommands.slice(),
153
172
  logs: logs.slice()
154
173
  }
155
174
  });
@@ -157,6 +176,150 @@ function runEditScript(options) {
157
176
  });
158
177
  }
159
178
  //#endregion
179
+ //#region src/entity/entity-contract.ts
180
+ const KNOWN_ENTITY_KINDS = [
181
+ "axvideo",
182
+ "timeline",
183
+ "track",
184
+ "clip",
185
+ "asset",
186
+ "video",
187
+ "audio",
188
+ "voice",
189
+ "image",
190
+ "sequence-marker",
191
+ "viewport",
192
+ "audio-script",
193
+ "phonetic-script",
194
+ "caption"
195
+ ];
196
+ const KNOWN_RELATION_KINDS = [
197
+ "timeline-track",
198
+ "track-clip",
199
+ "clip-marker",
200
+ "marker-content",
201
+ "axvideo-marker",
202
+ "marker-timeline",
203
+ "physical-asset",
204
+ "generated",
205
+ "phonetic-script-provenance",
206
+ "caption-provenance",
207
+ "caption-alignment"
208
+ ];
209
+ //#endregion
210
+ //#region src/entity/entity-http-client.ts
211
+ const API_PREFIX = "/api/mengine/v1";
212
+ const entityKinds = new Set(KNOWN_ENTITY_KINDS);
213
+ const relationKinds = new Set(KNOWN_RELATION_KINDS);
214
+ var MengineEntityHttpRequestError = class extends Error {
215
+ status;
216
+ payload;
217
+ constructor(status, payload) {
218
+ super(`mengine entity-state request failed: ${status}`);
219
+ this.status = status;
220
+ this.payload = payload;
221
+ this.name = "MengineEntityHttpRequestError";
222
+ }
223
+ };
224
+ /** Narrow authenticated client for the entity-store CAS endpoint. */
225
+ var EntityHttpClient = class {
226
+ options;
227
+ fetchImpl;
228
+ constructor(options) {
229
+ this.options = options;
230
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
231
+ }
232
+ async fetchState() {
233
+ return toSnapshot(await this.requestJson({ method: "GET" }), this.options.docId);
234
+ }
235
+ async commit(expectedRevision, state) {
236
+ return toSnapshot(await this.requestJson({
237
+ method: "POST",
238
+ body: JSON.stringify({
239
+ expected_revision: expectedRevision,
240
+ rows: {
241
+ entities: state.entities,
242
+ relations: state.relations
243
+ }
244
+ })
245
+ }), this.options.docId);
246
+ }
247
+ async requestJson(init) {
248
+ const response = await this.fetchImpl(this.endpoint(), {
249
+ ...init,
250
+ headers: this.headers()
251
+ });
252
+ const payload = await safeReadJson(response);
253
+ if (!response.ok) throw new MengineEntityHttpRequestError(response.status, payload);
254
+ return payload;
255
+ }
256
+ headers() {
257
+ const headers = new Headers({
258
+ accept: "application/json",
259
+ "content-type": "application/json"
260
+ });
261
+ const authToken = typeof this.options.authToken === "function" ? this.options.authToken() : this.options.authToken;
262
+ if (authToken != null && authToken !== "") headers.set("authorization", `Bearer ${authToken}`);
263
+ const userId = typeof this.options.userId === "function" ? this.options.userId() : this.options.userId;
264
+ if (userId != null && userId !== "") headers.set("medeo-user-id", userId);
265
+ return headers;
266
+ }
267
+ endpoint() {
268
+ return `${this.options.httpOrigin.replace(/\/$/, "")}${API_PREFIX}/docs/${encodeURIComponent(this.options.docId)}/entity-state`;
269
+ }
270
+ };
271
+ function toSnapshot(value, expectedDocId) {
272
+ if (!isRecord$1(value) || typeof value.doc_id !== "string" || !isNonNegativeInteger(value.revision)) throw new Error("invalid entity-state response envelope");
273
+ if (value.doc_id !== expectedDocId) throw new Error(`entity-state response doc_id mismatch: expected "${expectedDocId}"`);
274
+ if (!isRecord$1(value.rows) || !Array.isArray(value.rows.entities) || !Array.isArray(value.rows.relations)) throw new Error("invalid entity-state response rows");
275
+ const response = value;
276
+ return {
277
+ revision: response.revision,
278
+ entities: response.rows.entities.map(parseEntity),
279
+ relations: response.rows.relations.map(parseRelation)
280
+ };
281
+ }
282
+ function parseEntity(value) {
283
+ if (!isRecord$1(value) || !isTrimmed(value.entity_id) || typeof value.entity_kind !== "string" || !entityKinds.has(value.entity_kind) || !isJsonObject(value.payload)) throw new Error("invalid Entity row in entity-state response");
284
+ return structuredClone(value);
285
+ }
286
+ function parseRelation(value) {
287
+ if (!isRecord$1(value) || !isTrimmed(value.relation_id) || typeof value.relation_kind !== "string" || !relationKinds.has(value.relation_kind) || !isTrimmed(value.endpoint_0_entity_id) || !isTrimmed(value.endpoint_1_entity_id) || !isJsonObject(value.metadata) || !isJsonObject(value.trace)) throw new Error("invalid Relation row in entity-state response");
288
+ return structuredClone(value);
289
+ }
290
+ function isJsonObject(value) {
291
+ return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$1(value);
292
+ }
293
+ function isJsonValue(value, ancestors) {
294
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
295
+ if (typeof value === "number") return Number.isFinite(value);
296
+ if (typeof value !== "object" || ancestors.has(value)) return false;
297
+ const prototype = Object.getPrototypeOf(value);
298
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
299
+ ancestors.add(value);
300
+ const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
301
+ ancestors.delete(value);
302
+ return valid;
303
+ }
304
+ function isRecord$1(value) {
305
+ return value !== null && typeof value === "object" && !Array.isArray(value);
306
+ }
307
+ function isTrimmed(value) {
308
+ return typeof value === "string" && value.length > 0 && value.trim() === value;
309
+ }
310
+ function isNonNegativeInteger(value) {
311
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
312
+ }
313
+ async function safeReadJson(response) {
314
+ const text = await response.text();
315
+ if (text.length === 0) return null;
316
+ try {
317
+ return JSON.parse(text);
318
+ } catch {
319
+ return text;
320
+ }
321
+ }
322
+ //#endregion
160
323
  //#region src/sandbox/generated/edit-sandbox-model-context.ts
161
324
  /**
162
325
  * @generated by gen:sandbox-dts — DO NOT EDIT MANUALLY
@@ -359,6 +522,9 @@ const EDIT_SANDBOX_API_DTS = [
359
522
  " }[];",
360
523
  "}",
361
524
  "",
525
+ "/**",
526
+ " * Reorder a set of main-track clips relative to a reference clip.",
527
+ " */",
362
528
  "export interface MoveVideoClipsByAnchorInput {",
363
529
  " /**",
364
530
  " * Clips to move as one block, keeping their relative order. Need not be contiguous on the track.",
@@ -585,6 +751,9 @@ const EDIT_SANDBOX_API_DTS = [
585
751
  " }[];",
586
752
  "}",
587
753
  "",
754
+ "/**",
755
+ " * Replace a contiguous run of main-track clips with a new run.",
756
+ " */",
588
757
  "export interface ReplaceVideoClipSequenceInput {",
589
758
  " /**",
590
759
  " * The clips being replaced: a contiguous main-track run, listed in timeline order",
@@ -840,6 +1009,7 @@ const EDIT_SANDBOX_API_DTS = [
840
1009
  "/** Agent write surface — one method per SemanticOp kind. */",
841
1010
  "export interface EditApi {",
842
1011
  " moveVideoClips(input: MoveVideoClipsInput): Promise<void>;",
1012
+ " /** Reorder a set of main-track clips relative to a reference clip. */",
843
1013
  " moveVideoClipsByAnchor(input: MoveVideoClipsByAnchorInput): Promise<void>;",
844
1014
  " deleteVideoClips(input: DeleteVideoClipsInput): Promise<void>;",
845
1015
  " /** Add video clips to a track. */",
@@ -849,6 +1019,7 @@ const EDIT_SANDBOX_API_DTS = [
849
1019
  " setVideoClipSpeedShift(input: SetVideoClipSpeedShiftInput): Promise<void>;",
850
1020
  " /** Replace the media backing existing video clips. */",
851
1021
  " replaceVideoClipContent(input: ReplaceVideoClipContentInput): Promise<void>;",
1022
+ " /** Replace a contiguous run of main-track clips with a new run. */",
852
1023
  " replaceVideoClipSequence(input: ReplaceVideoClipSequenceInput): Promise<void>;",
853
1024
  " /** Re-trim existing video clips (the user-facing \"adjust duration\" gesture is a trim of the source window). */",
854
1025
  " adjustVideoClipDuration(input: AdjustVideoClipDurationInput): Promise<void>;",
@@ -873,6 +1044,185 @@ const EDIT_SANDBOX_API_DTS = [
873
1044
  " adjustBgmVolume(input: AdjustBgmVolumeInput): Promise<void>;",
874
1045
  "}",
875
1046
  "",
1047
+ "export type JsonPrimitive = string | number | boolean | null;",
1048
+ "export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];",
1049
+ "export interface JsonObject {",
1050
+ " [key: string]: JsonValue;",
1051
+ "}",
1052
+ "",
1053
+ "export type KnownEntityKind =",
1054
+ " | 'axvideo'",
1055
+ " | 'timeline'",
1056
+ " | 'track'",
1057
+ " | 'clip'",
1058
+ " | 'asset'",
1059
+ " | 'video'",
1060
+ " | 'audio'",
1061
+ " | 'voice'",
1062
+ " | 'image'",
1063
+ " | 'sequence-marker'",
1064
+ " | 'viewport'",
1065
+ " | 'audio-script'",
1066
+ " | 'phonetic-script'",
1067
+ " | 'caption';",
1068
+ "",
1069
+ "export type KnownRelationKind =",
1070
+ " | 'timeline-track'",
1071
+ " | 'track-clip'",
1072
+ " | 'clip-marker'",
1073
+ " | 'marker-content'",
1074
+ " | 'axvideo-marker'",
1075
+ " | 'marker-timeline'",
1076
+ " | 'physical-asset'",
1077
+ " | 'generated'",
1078
+ " | 'phonetic-script-provenance'",
1079
+ " | 'caption-provenance'",
1080
+ " | 'caption-alignment';",
1081
+ "",
1082
+ "export interface BoundedNativeSequencePayload extends JsonObject {",
1083
+ " /** Use factual recalled coordinates; never invent an end or duration. */",
1084
+ " extent: { kind: 'bounded'; start: number; end: number };",
1085
+ " sampling: 'native';",
1086
+ " coordinateSpace: JsonValue;",
1087
+ "}",
1088
+ "export interface UnboundedConstantSequencePayload extends JsonObject {",
1089
+ " extent: { kind: 'unbounded'; start: number };",
1090
+ " sampling: 'constant';",
1091
+ " coordinateSpace: JsonValue;",
1092
+ "}",
1093
+ "export interface BoundedDerivedSequencePayload extends JsonObject {",
1094
+ " extent: { kind: 'bounded'; start: number; end: number };",
1095
+ " sampling: 'derived';",
1096
+ " coordinateSpace: JsonValue;",
1097
+ "}",
1098
+ "export type ScriptTextSegment = JsonObject & {",
1099
+ " segmentId: string;",
1100
+ " text: string;",
1101
+ " language?: string;",
1102
+ "};",
1103
+ "",
1104
+ "export interface EntityPayloadByKind {",
1105
+ " axvideo: BoundedDerivedSequencePayload;",
1106
+ " timeline: JsonObject;",
1107
+ " track: JsonObject & { hidden?: boolean; role?: string };",
1108
+ " clip: JsonObject;",
1109
+ " asset: JsonObject;",
1110
+ " video: BoundedNativeSequencePayload;",
1111
+ " audio: BoundedNativeSequencePayload;",
1112
+ " voice: BoundedNativeSequencePayload;",
1113
+ " image: UnboundedConstantSequencePayload;",
1114
+ " 'sequence-marker': JsonObject & {",
1115
+ " sourceRange: { start: number; end: number };",
1116
+ " targetRange?: { start: number; end: number };",
1117
+ " duration: { mode: 'from-source' } | { mode: 'fixed'; value: number };",
1118
+ " timeRemapping?: JsonValue;",
1119
+ " };",
1120
+ " viewport: JsonObject;",
1121
+ " 'audio-script': JsonObject & { segments: ScriptTextSegment[] };",
1122
+ " 'phonetic-script': JsonObject & { segments: ScriptTextSegment[] };",
1123
+ " caption: BoundedNativeSequencePayload;",
1124
+ "}",
1125
+ "",
1126
+ "export type CreateEntityInput = {",
1127
+ " [K in KnownEntityKind]: {",
1128
+ " entity_id?: string;",
1129
+ " entity_kind: K;",
1130
+ " payload: EntityPayloadByKind[K];",
1131
+ " };",
1132
+ "}[KnownEntityKind];",
1133
+ "",
1134
+ "export interface ImportAssetInput {",
1135
+ " asset_id: string;",
1136
+ " entity_id?: string;",
1137
+ " payload?: JsonObject;",
1138
+ "}",
1139
+ "",
1140
+ "export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
1141
+ " entity_id: string;",
1142
+ " entity_kind: K;",
1143
+ " payload: EntityPayloadByKind[K];",
1144
+ "}",
1145
+ "",
1146
+ "export interface SandboxRelation {",
1147
+ " relation_id: string;",
1148
+ " relation_kind: KnownRelationKind;",
1149
+ " endpoint_0_entity_id: string;",
1150
+ " endpoint_1_entity_id: string;",
1151
+ " metadata: JsonObject;",
1152
+ " trace: JsonObject;",
1153
+ "}",
1154
+ "",
1155
+ "export type EmptyRelationKind =",
1156
+ " | 'timeline-track'",
1157
+ " | 'track-clip'",
1158
+ " | 'clip-marker'",
1159
+ " | 'marker-content'",
1160
+ " | 'axvideo-marker'",
1161
+ " | 'marker-timeline';",
1162
+ "export type LinkRelationInput =",
1163
+ " | {",
1164
+ " relation_id?: string;",
1165
+ " relation_kind: EmptyRelationKind;",
1166
+ " endpoint_0_entity_id: string;",
1167
+ " endpoint_1_entity_id: string;",
1168
+ " metadata?: { [key: string]: never };",
1169
+ " trace?: JsonObject;",
1170
+ " }",
1171
+ " | {",
1172
+ " relation_id?: string;",
1173
+ " relation_kind: 'physical-asset';",
1174
+ " /** Canonical endpoint 0 is sequence media; endpoint 1 is Asset. */",
1175
+ " endpoint_0_entity_id: string;",
1176
+ " endpoint_1_entity_id: string;",
1177
+ " metadata?: JsonObject;",
1178
+ " trace?: JsonObject;",
1179
+ " }",
1180
+ " | {",
1181
+ " relation_id?: string;",
1182
+ " relation_kind: 'phonetic-script-provenance' | 'caption-provenance';",
1183
+ " endpoint_0_entity_id: string;",
1184
+ " endpoint_1_entity_id: string;",
1185
+ " metadata: JsonObject & { segmentAlignment: JsonValue };",
1186
+ " trace?: JsonObject;",
1187
+ " }",
1188
+ " | {",
1189
+ " relation_id?: string;",
1190
+ " relation_kind: 'caption-alignment';",
1191
+ " endpoint_0_entity_id: string;",
1192
+ " endpoint_1_entity_id: string;",
1193
+ " metadata: JsonObject & { alignment: JsonValue };",
1194
+ " trace?: JsonObject;",
1195
+ " };",
1196
+ "",
1197
+ "export interface LinkGeneratedRelationInput {",
1198
+ " relation_id?: string;",
1199
+ " /** Generated output media Entity; persisted as endpoint 0. */",
1200
+ " output_entity_id: string;",
1201
+ " /** Input media Entity used to generate the output; persisted as endpoint 1. */",
1202
+ " input_entity_id: string;",
1203
+ " trace?: JsonObject;",
1204
+ "}",
1205
+ "",
1206
+ "/** Explicit Entity authoring. Assets and media Entities are not one-to-one. */",
1207
+ "export interface EntityApi {",
1208
+ " list(): SandboxEntity[];",
1209
+ " get(entityId: string): SandboxEntity | null;",
1210
+ " /** Call before importAsset; inspect every match and decide whether to reuse one. */",
1211
+ " findByAssetId(assetId: string): SandboxEntity<'asset'>[];",
1212
+ " create(input: CreateEntityInput): string;",
1213
+ " /** Create only an Asset Entity when no existing match should be reused; this does not infer media. */",
1214
+ " importAsset(input: ImportAssetInput): string;",
1215
+ "}",
1216
+ "",
1217
+ "/** Incident reads ignore endpoint position; relation semantics preserve it. */",
1218
+ "export interface RelationApi {",
1219
+ " list(): SandboxRelation[];",
1220
+ " of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
1221
+ " link(input: LinkRelationInput): string;",
1222
+ " /** Author ordered generated(output,input). */",
1223
+ " linkGenerated(input: LinkGeneratedRelationInput): string;",
1224
+ "}",
1225
+ "",
876
1226
  "/** Clip hit from `clipsInRange`. */",
877
1227
  "export interface TimelineClipDescriptor {",
878
1228
  " id: string;",
@@ -918,30 +1268,38 @@ const EDIT_SANDBOX_API_DTS = [
918
1268
  "",
919
1269
  "export declare const edit: EditApi;",
920
1270
  "export declare const timeline: TimelineApi;",
1271
+ "export declare const entities: EntityApi;",
1272
+ "export declare const relations: RelationApi;",
921
1273
  "",
922
1274
  "/** Capture a rollback point. */",
923
1275
  "export declare function checkpoint(): SandboxCheckpoint;",
924
1276
  "/** Roll the sandbox document back to a prior checkpoint. */",
925
1277
  "export declare function rollbackTo(cp: SandboxCheckpoint): void;",
926
- "/** Host-injected script arguments (opaque). */",
927
- "export declare const inputs: unknown;",
1278
+ "/** Host-injected, pre-materialized facts. Validate each field before use. */",
1279
+ "export declare const inputs: Readonly<Record<string, unknown>>;",
928
1280
  ""
929
1281
  ].join("\n");
930
1282
  //#endregion
931
1283
  //#region src/prompt.ts
932
1284
  const MEDEO_TOOL_DESCRIPTION = `
933
- Edit a Medeo video document through a deterministic, side-effect-free JavaScript sandbox.
1285
+ Edit a Medeo video document and its explicit Entity/Relation state through a deterministic, side-effect-free JavaScript sandbox.
934
1286
 
935
1287
  Operations:
936
1288
  - snapshot: return the compact timeline projection and opaque base version.
937
- - run-edit-script: execute JavaScript against a forked snapshot. Inspect timeline.*, compute coordinates, and call edit.* methods in one script. The sandbox has no network, storage, clock, or generation access. Pass materialized asset/speech facts through inputs. A successful run returns preview, logs, base_version, and plan_id — not the full op journal.
938
- - commit-plan: replay a cached plan_id into the live MengineDocSession through SemanticEditor. Use validation=version for all-or-nothing commit, or preflight to localize an op conflict after concurrent edits.
1289
+ - run-edit-script: execute JavaScript against forked timeline and Entity/Relation snapshots. Inspect timeline.*, entities.*, and relations.*; call edit.* for timeline mutations or the explicit entity APIs for domain mutations. The sandbox has no network, storage, clock, or generation access. Pass recalled generation/asset facts through inputs. A successful run returns preview, logs, plan_kind, base versions, and plan_id — not the full journals.
1290
+ - commit-plan: commit a cached plan_id. Timeline plans replay into ManualSyncDoc and push one causally complete update; Entity plans replace the authoritative row set through revision CAS. validation=preflight is timeline-only. A failed transport is unconfirmed, never committed; retry the same plan_id.
939
1291
 
940
1292
  Default flow: snapshot → run-edit-script with auto_commit=false → inspect preview → commit-plan. Use auto_commit=true only for low-risk edits when the host does not need user confirmation. On version mismatch, rerun snapshot and the script; never try to patch a rejected journal by hand.
1293
+
1294
+ One plan must mutate exactly one store: timeline or Entity/Relation state. If both are needed, author and commit two separate plans. There is no automatic Asset→Entity projection: select the relevant recalled fact, explicitly import an Asset if useful, explicitly create only known typed Entities, and author relations. Asset and media Entity identity are not one-to-one. relations.linkGenerated({ output_entity_id, input_entity_id }) means generated(output,input); incident lookup with relations.of(entityId) is endpoint-agnostic.
941
1295
  `.trim();
942
1296
  const MEDEO_TOOL_EXECUTION_RULES = `
943
1297
  The host supplies the current document. Do not ask for, invent, or pass a document id.
944
1298
  Use timeline.snapshot() for the whole draft projection. Its duration is timeline.snapshot().timeline?.duration_ms; there is no top-level duration_ms.
1299
+ Generation lineage and Memota asset facts are host-provided through inputs. Never invent an asset id, Entity kind, or peer Entity id.
1300
+ Before importing an Asset, call entities.findByAssetId(assetId), inspect every match, and decide whether an existing Entity represents the intended logical asset. Multiple matches are valid; do not assume Asset↔media is one-to-one.
1301
+ For recalled video/audio/voice, create a bounded/native payload whose extent end comes from factual media duration/coordinates in inputs; never fabricate a duration. Image uses unbounded/constant semantics and has no invented end. If required facts are absent, do not create the media Entity yet.
1302
+ For physical-asset authoring, use sequence media as endpoint_0_entity_id and Asset as endpoint_1_entity_id. For generated lineage, use linkGenerated so endpoint 0 is output and endpoint 1 is input. relations.of remains endpoint-agnostic for lookup.
945
1303
  Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
946
1304
  `.trim();
947
1305
  /** Render the complete MEngine-owned context injected before one model call. */
@@ -997,11 +1355,11 @@ const MEDEO_TOOL_PARAMETERS = {
997
1355
  script: {
998
1356
  type: "string",
999
1357
  minLength: 1,
1000
- description: "JavaScript body for run-edit-script. It receives edit, timeline, checkpoint, rollbackTo, inputs, and console; perform all calculations in the script."
1358
+ description: "JavaScript body for run-edit-script. It receives edit, timeline, entities, relations, checkpoint, rollbackTo, inputs, and console. A plan may mutate the timeline or Entity/Relation state, never both."
1001
1359
  },
1002
1360
  inputs: {
1003
1361
  type: "object",
1004
- description: "Pre-materialized, side-effect-free values passed into the script. Generation and network IO must happen in the host before this call."
1362
+ description: "Pre-materialized, side-effect-free values passed into the script, including recalled generation lineage and asset facts. Generation and network IO must happen in the host before this call."
1005
1363
  },
1006
1364
  timeout_ms: {
1007
1365
  type: "integer",
@@ -1025,7 +1383,7 @@ const MEDEO_TOOL_PARAMETERS = {
1025
1383
  validation: {
1026
1384
  type: "string",
1027
1385
  enum: ["version", "preflight"],
1028
- description: "commit-plan mode: version rejects any concurrent change; preflight revalidates each op against the current snapshot."
1386
+ description: "Timeline commit mode: version rejects any concurrent change; preflight revalidates each op. Entity plans always use revision CAS and reject preflight."
1029
1387
  }
1030
1388
  },
1031
1389
  oneOf: [
@@ -1073,39 +1431,37 @@ const MEDEO_TOOL_PARAMETERS = {
1073
1431
  //#endregion
1074
1432
  //#region src/session/commit-plan.ts
1075
1433
  /**
1076
- * Replay a sandbox journal into a live session through its document adapter
1077
- * (SemanticEditor Loro mengine-server).
1434
+ * Replay a sandbox journal into a manually-synchronized document and push the
1435
+ * whole plan as one causally complete update.
1078
1436
  *
1079
- * - Default / `{ validation: 'version' }`: if `session.version()`
1080
- * `plan.base_version`, reject with zero writes.
1437
+ * - Default / `{ validation: 'version' }`: if the current document mark differs
1438
+ * from `plan.base_version`, reject with zero writes.
1081
1439
  * - `{ validation: 'preflight' }`: skip the version gate; revalidate each op
1082
1440
  * against a PlainMemoryAdapter seeded from the current live snapshot, then
1083
1441
  * replay for real. A SchemaValidator failure becomes `op_conflict` with the
1084
1442
  * failing entry's index. Journal integrity errors (unrecorded/unconsumed
1085
1443
  * ids) still propagate as throws in both modes.
1086
1444
  */
1087
- async function commitPlan(session, plan, options) {
1088
- if (options?.validation === "preflight") return commitPlanPreflight(session, plan);
1089
- const actual = session.version();
1090
- if (actual !== plan.base_version) return {
1445
+ async function commitPlan(doc, plan, options) {
1446
+ if (options?.validation === "preflight") return commitPlanPreflight(doc, plan);
1447
+ const actual = encodeDocVersionMark(doc.versionMark());
1448
+ const expected = decodeDocVersionMark(plan.base_version);
1449
+ if (expected == null || doc.hasChangedSince(expected)) return {
1091
1450
  kind: "rejected",
1092
1451
  reason: "version_mismatch",
1093
1452
  expected: plan.base_version,
1094
1453
  actual
1095
1454
  };
1096
- await replayJournal(session.documentAdapter, plan.ops);
1097
- return {
1098
- kind: "committed",
1099
- ops_applied: plan.ops.length
1100
- };
1455
+ await doc.replayJournal(plan.ops);
1456
+ return retryPlanPush(doc, plan.ops.length);
1101
1457
  }
1102
1458
  /**
1103
1459
  * Phase-2 path: scratch revalidation then real replay. Each entry is driven
1104
1460
  * through `replayJournal` alone so a ValidationError maps to a stable index;
1105
1461
  * integrity throws are not wrapped.
1106
1462
  */
1107
- async function commitPlanPreflight(session, plan) {
1108
- const scratch = createPlainMemoryAdapter(session.snapshot());
1463
+ async function commitPlanPreflight(doc, plan) {
1464
+ const scratch = createPlainMemoryAdapter(doc.snapshot());
1109
1465
  for (let index = 0; index < plan.ops.length; index++) {
1110
1466
  const entry = plan.ops[index];
1111
1467
  if (entry == null) continue;
@@ -1120,15 +1476,41 @@ async function commitPlanPreflight(session, plan) {
1120
1476
  const entry = plan.ops[index];
1121
1477
  if (entry == null) continue;
1122
1478
  try {
1123
- await replayJournal(session.documentAdapter, [entry]);
1479
+ await doc.replayJournal([entry]);
1124
1480
  } catch (error) {
1125
1481
  if (error instanceof ValidationError) return opConflict(index, entry.kind, `real replay: ${error.message}`);
1126
1482
  throw error;
1127
1483
  }
1128
1484
  }
1485
+ return retryPlanPush(doc, plan.ops.length);
1486
+ }
1487
+ /** Push an already-replayed plan again without replaying or re-running its version gate. */
1488
+ async function retryPlanPush(doc, opsApplied) {
1489
+ const result = await doc.push();
1490
+ if (result.kind === "ack" || result.kind === "duplicate" || result.kind === "nothing_to_push") {
1491
+ const reconciled = result.collaborated ? await doc.pull() : void 0;
1492
+ const warnings = reconciled != null && !reconciled.ok ? [{
1493
+ kind: "pull_failed",
1494
+ message: reconciled.error.message
1495
+ }] : void 0;
1496
+ return {
1497
+ kind: "committed",
1498
+ ops_applied: opsApplied,
1499
+ collaborated: result.collaborated,
1500
+ ...warnings !== void 0 ? { warnings } : {}
1501
+ };
1502
+ }
1503
+ if (result.kind === "rejected") return {
1504
+ kind: "rejected",
1505
+ reason: "push_rejected",
1506
+ ...result.code !== void 0 ? { code: result.code } : {},
1507
+ message: result.error?.message ?? "mengine rejected the sandbox plan"
1508
+ };
1129
1509
  return {
1130
- kind: "committed",
1131
- ops_applied: plan.ops.length
1510
+ kind: "unconfirmed",
1511
+ reason: "push_failed",
1512
+ ops_applied: opsApplied,
1513
+ message: result.error?.message ?? "mengine push failed"
1132
1514
  };
1133
1515
  }
1134
1516
  function opConflict(index, op_kind, message) {
@@ -1156,6 +1538,89 @@ function requiredContext(value, docId, field) {
1156
1538
  if (resolved == null || resolved.length === 0) throw new Error(`${field} must resolve to a non-empty string for doc ${docId}`);
1157
1539
  return resolved;
1158
1540
  }
1541
+ async function commitEntityPlan(client, plan) {
1542
+ const rows = plan.entity_rows;
1543
+ if (rows === void 0) throw new Error("entity plan is missing its authoritative rows");
1544
+ try {
1545
+ const committed = await client.commit(plan.entity_base_revision, rows);
1546
+ return {
1547
+ kind: "committed",
1548
+ ops_applied: plan.entity_commands.length,
1549
+ collaborated: false,
1550
+ entity_revision: committed.revision
1551
+ };
1552
+ } catch (error) {
1553
+ if (error instanceof MengineEntityHttpRequestError) {
1554
+ if (error.status === 409) {
1555
+ const actualFromPayload = revisionConflictActual(error.payload);
1556
+ try {
1557
+ const current = await client.fetchState();
1558
+ if (current.revision === plan.entity_base_revision + 1 && entityRowsEquivalent(current, rows)) return {
1559
+ kind: "committed",
1560
+ ops_applied: plan.entity_commands.length,
1561
+ collaborated: false,
1562
+ entity_revision: current.revision
1563
+ };
1564
+ return {
1565
+ kind: "rejected",
1566
+ reason: "entity_revision_mismatch",
1567
+ expected: plan.entity_base_revision,
1568
+ actual: current.revision
1569
+ };
1570
+ } catch {
1571
+ if (actualFromPayload !== void 0) return {
1572
+ kind: "rejected",
1573
+ reason: "entity_revision_mismatch",
1574
+ expected: plan.entity_base_revision,
1575
+ actual: actualFromPayload
1576
+ };
1577
+ return {
1578
+ kind: "unconfirmed",
1579
+ reason: "push_failed",
1580
+ ops_applied: plan.entity_commands.length,
1581
+ message: "entity-state conflict could not be reconciled"
1582
+ };
1583
+ }
1584
+ }
1585
+ return {
1586
+ kind: "rejected",
1587
+ reason: "entity_state_rejected",
1588
+ status: error.status,
1589
+ message: entityHttpErrorMessage(error.payload)
1590
+ };
1591
+ }
1592
+ return {
1593
+ kind: "unconfirmed",
1594
+ reason: "push_failed",
1595
+ ops_applied: plan.entity_commands.length,
1596
+ message: error instanceof Error ? error.message : String(error)
1597
+ };
1598
+ }
1599
+ }
1600
+ function revisionConflictActual(payload) {
1601
+ if (!isRecord(payload)) return void 0;
1602
+ const actual = payload.actual_revision;
1603
+ return typeof actual === "number" && Number.isSafeInteger(actual) && actual >= 0 ? actual : void 0;
1604
+ }
1605
+ function entityHttpErrorMessage(payload) {
1606
+ if (isRecord(payload) && typeof payload.message === "string" && payload.message.length > 0) return payload.message;
1607
+ return typeof payload === "string" && payload.length > 0 ? payload : "mengine rejected the entity-state plan";
1608
+ }
1609
+ function commitWarnings(result) {
1610
+ return result.kind === "committed" && "warnings" in result && result.warnings !== void 0 ? [...result.warnings] : void 0;
1611
+ }
1612
+ function entityRowsEquivalent(left, right) {
1613
+ const normalize = (state) => ({
1614
+ entities: [...state.entities].sort((a, b) => a.entity_id.localeCompare(b.entity_id)).map((entity) => canonicalJson(entity)),
1615
+ relations: [...state.relations].sort((a, b) => a.relation_id.localeCompare(b.relation_id)).map((relation) => canonicalJson(relation))
1616
+ });
1617
+ return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
1618
+ }
1619
+ function canonicalJson(value) {
1620
+ if (Array.isArray(value)) return value.map(canonicalJson);
1621
+ if (!isRecord(value)) return value;
1622
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalJson(value[key])]));
1623
+ }
1159
1624
  function parseInput(value) {
1160
1625
  if (!isRecord(value)) throw new Error("input must be an object");
1161
1626
  const op = value.op;
@@ -1199,58 +1664,78 @@ function parseInput(value) {
1199
1664
  /**
1200
1665
  * Create the self-contained Medeo LLM tool.
1201
1666
  *
1202
- * The package owns session construction, compact projection, sandbox execution,
1667
+ * The package owns document construction, compact projection, sandbox execution,
1203
1668
  * plan caching, commit, document get-or-create, and shutdown. The host supplies
1204
1669
  * environment facts plus the authoritative legacy draft loader used only when
1205
1670
  * Mengine has no document yet.
1206
1671
  */
1207
1672
  function createMedeoTool(options) {
1208
- const sessions = /* @__PURE__ */ new Map();
1673
+ const documents = /* @__PURE__ */ new Map();
1674
+ const entityClients = /* @__PURE__ */ new Map();
1675
+ const documentTails = /* @__PURE__ */ new Map();
1676
+ const pendingPushes = /* @__PURE__ */ new Map();
1209
1677
  const plans = /* @__PURE__ */ new Map();
1210
1678
  const modelContextVersions = /* @__PURE__ */ new Map();
1211
1679
  const maxPlans = options.maxPlans ?? DEFAULT_MAX_PLANS;
1212
1680
  const maxModelContexts = options.maxModelContexts ?? DEFAULT_MAX_MODEL_CONTEXTS;
1213
1681
  let closed = false;
1214
- async function getSession(docId) {
1682
+ async function getDocument(docId) {
1215
1683
  if (closed) throw new Error("medeo tool is closed");
1216
- const existing = sessions.get(docId);
1684
+ const existing = documents.get(docId);
1217
1685
  if (existing != null) return await existing;
1218
1686
  const created = (async () => {
1219
- const client = new MengineHttpClient({
1687
+ return await getOrCreateDocument(new MengineHttpClient({
1220
1688
  docId,
1221
1689
  httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
1222
1690
  ...options.authToken !== void 0 ? { authToken: () => optionalContext(options.authToken, docId) } : {},
1223
1691
  ...options.userId !== void 0 ? { userId: () => optionalContext(options.userId, docId) } : {},
1224
1692
  ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
1225
- });
1226
- const peerId = optionalContext(options.peerId, docId);
1227
- await getOrCreateDocument(client, docId, peerId);
1228
- const session = new MengineDocSession({
1229
- docId,
1230
- client,
1231
- ...peerId !== void 0 ? { peerId } : {},
1232
- ...options.sseReconnectDelayMs !== void 0 ? { sseReconnectDelayMs: options.sseReconnectDelayMs } : {}
1233
- });
1234
- try {
1235
- await session.start();
1236
- return session;
1237
- } catch (error) {
1238
- session.destroy();
1239
- throw error;
1240
- }
1693
+ }), docId, optionalContext(options.peerId, docId));
1241
1694
  })();
1242
- sessions.set(docId, created);
1695
+ documents.set(docId, created);
1243
1696
  try {
1244
1697
  return await created;
1245
1698
  } catch (error) {
1246
- if (sessions.get(docId) === created) sessions.delete(docId);
1699
+ if (documents.get(docId) === created) documents.delete(docId);
1247
1700
  throw error;
1248
1701
  }
1249
1702
  }
1703
+ function getEntityClient(docId) {
1704
+ if (closed) throw new Error("medeo tool is closed");
1705
+ const existing = entityClients.get(docId);
1706
+ if (existing != null) return existing;
1707
+ const client = new EntityHttpClient({
1708
+ docId,
1709
+ httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
1710
+ ...options.authToken !== void 0 ? { authToken: () => optionalContext(options.authToken, docId) } : {},
1711
+ ...options.userId !== void 0 ? { userId: () => optionalContext(options.userId, docId) } : {},
1712
+ ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
1713
+ });
1714
+ entityClients.set(docId, client);
1715
+ return client;
1716
+ }
1717
+ async function runExclusive(docId, use) {
1718
+ const previous = documentTails.get(docId) ?? Promise.resolve();
1719
+ let release;
1720
+ const gate = new Promise((resolve) => {
1721
+ release = resolve;
1722
+ });
1723
+ const tail = previous.catch(() => {}).then(() => gate);
1724
+ documentTails.set(docId, tail);
1725
+ await previous.catch(() => {});
1726
+ try {
1727
+ return await use(await getDocument(docId));
1728
+ } finally {
1729
+ release();
1730
+ if (documentTails.get(docId) === tail) documentTails.delete(docId);
1731
+ }
1732
+ }
1250
1733
  async function getOrCreateDocument(client, docId, peerId) {
1251
1734
  try {
1252
- await client.fetchSnapshot();
1253
- return;
1735
+ return await ManualSyncDoc.open({
1736
+ client,
1737
+ ...peerId !== void 0 ? { peerId } : {}
1738
+ });
1254
1739
  } catch (error) {
1255
1740
  if (!(error instanceof MengineHttpRequestError) || error.status !== 404) throw error;
1256
1741
  if (options.loadInitialDraft === void 0) throw error;
@@ -1263,8 +1748,11 @@ function createMedeoTool(options) {
1263
1748
  await client.bootstrapSnapshot(seed.export({ mode: "snapshot" }));
1264
1749
  } catch (error) {
1265
1750
  if (!(error instanceof MengineHttpRequestError) || error.status !== 400) throw error;
1266
- await client.fetchSnapshot();
1267
1751
  }
1752
+ return await ManualSyncDoc.open({
1753
+ client,
1754
+ ...peerId !== void 0 ? { peerId } : {}
1755
+ });
1268
1756
  }
1269
1757
  function rememberPlan(docId, plan) {
1270
1758
  const planId = randomUUID();
@@ -1273,109 +1761,208 @@ function createMedeoTool(options) {
1273
1761
  plan
1274
1762
  });
1275
1763
  while (plans.size > maxPlans) {
1276
- const oldest = plans.keys().next().value;
1277
- if (oldest === void 0) break;
1278
- plans.delete(oldest);
1764
+ const protectedPlanIds = new Set([...pendingPushes.values()].map((pending) => pending.planId));
1765
+ protectedPlanIds.add(planId);
1766
+ const oldestEvictable = [...plans.keys()].find((candidate) => !protectedPlanIds.has(candidate));
1767
+ if (oldestEvictable === void 0) break;
1768
+ plans.delete(oldestEvictable);
1279
1769
  }
1280
1770
  return planId;
1281
1771
  }
1772
+ function assertNoPendingPush(docId) {
1773
+ const pending = pendingPushes.get(docId);
1774
+ if (pending != null) throw new Error(`doc ${docId} has an unconfirmed push; retry plan_id ${pending.planId} before continuing`);
1775
+ }
1776
+ function recordPushResult(docId, planId, plan, result) {
1777
+ if (result.kind === "unconfirmed") {
1778
+ pendingPushes.set(docId, plan.plan_kind === "timeline" ? {
1779
+ kind: "timeline",
1780
+ planId,
1781
+ plan,
1782
+ opsApplied: result.ops_applied
1783
+ } : {
1784
+ kind: "entities",
1785
+ planId,
1786
+ plan
1787
+ });
1788
+ return;
1789
+ }
1790
+ pendingPushes.delete(docId);
1791
+ if (plan.plan_kind === "timeline" && result.kind === "rejected" && result.reason === "push_rejected") documents.delete(docId);
1792
+ }
1793
+ async function fetchEntityStateForSandbox(docId) {
1794
+ try {
1795
+ return await getEntityClient(docId).fetchState();
1796
+ } catch (error) {
1797
+ if (error instanceof MengineEntityHttpRequestError && error.status === 404) return {
1798
+ revision: 0,
1799
+ entities: [],
1800
+ relations: []
1801
+ };
1802
+ throw error;
1803
+ }
1804
+ }
1805
+ async function commitCachedPlan(docId, doc, plan, validation) {
1806
+ if (plan.plan_kind === "timeline") return await commitPlan(doc, plan, validation === void 0 ? void 0 : { validation });
1807
+ if (validation === "preflight") throw new Error("validation=preflight applies only to timeline plans; entity plans use revision CAS");
1808
+ if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
1809
+ return await commitEntityPlan(getEntityClient(docId), plan);
1810
+ }
1811
+ async function observePull(doc) {
1812
+ const result = await doc.pull();
1813
+ if (result.ok) return { collaborated: result.changed };
1814
+ return {
1815
+ collaborated: false,
1816
+ warnings: [{
1817
+ kind: "pull_failed",
1818
+ message: result.error.message
1819
+ }]
1820
+ };
1821
+ }
1822
+ function mergeWarnings(...groups) {
1823
+ const warnings = groups.flatMap((group) => group ?? []);
1824
+ return warnings.length > 0 ? warnings : void 0;
1825
+ }
1282
1826
  async function getModelContext(input) {
1283
1827
  const docId = input.doc_id.trim();
1284
1828
  const contextId = input.context_id.trim();
1285
1829
  if (docId.length === 0) throw new Error("doc_id must be a non-empty string");
1286
1830
  if (contextId.length === 0) throw new Error("context_id must be a non-empty string");
1287
- const documentVersion = (await getSession(docId)).version();
1288
- const baselineKey = `${contextId}\u0000${docId}`;
1289
- const previousVersion = modelContextVersions.get(baselineKey);
1290
- const updatedSincePreviousModelCall = previousVersion == null ? null : previousVersion !== documentVersion;
1291
- modelContextVersions.delete(baselineKey);
1292
- modelContextVersions.set(baselineKey, documentVersion);
1293
- while (modelContextVersions.size > maxModelContexts) {
1294
- const oldest = modelContextVersions.keys().next().value;
1295
- if (oldest === void 0) break;
1296
- modelContextVersions.delete(oldest);
1297
- }
1298
- return {
1299
- prompt: renderMedeoModelContext({
1300
- documentVersion,
1301
- updatedSincePreviousModelCall
1302
- }),
1303
- document_version: documentVersion,
1304
- updated_since_previous_model_call: updatedSincePreviousModelCall
1305
- };
1306
- }
1307
- async function confirmCommitted(session, result) {
1308
- if (result.kind === "committed" && result.ops_applied > 0) await session.waitForServerAck();
1309
- return result;
1831
+ return await runExclusive(docId, async (doc) => {
1832
+ await observePull(doc);
1833
+ const documentVersion = encodeDocVersionMark(doc.versionMark());
1834
+ const baselineKey = `${contextId}\u0000${docId}`;
1835
+ const previousVersion = modelContextVersions.get(baselineKey);
1836
+ const updatedSincePreviousModelCall = previousVersion == null ? null : previousVersion !== documentVersion;
1837
+ modelContextVersions.delete(baselineKey);
1838
+ modelContextVersions.set(baselineKey, documentVersion);
1839
+ while (modelContextVersions.size > maxModelContexts) {
1840
+ const oldest = modelContextVersions.keys().next().value;
1841
+ if (oldest === void 0) break;
1842
+ modelContextVersions.delete(oldest);
1843
+ }
1844
+ return {
1845
+ prompt: renderMedeoModelContext({
1846
+ documentVersion,
1847
+ updatedSincePreviousModelCall
1848
+ }),
1849
+ document_version: documentVersion,
1850
+ updated_since_previous_model_call: updatedSincePreviousModelCall
1851
+ };
1852
+ });
1310
1853
  }
1311
1854
  async function snapshot(input) {
1312
- const session = await getSession(input.doc_id);
1313
- const document = session.snapshot();
1314
- return {
1315
- ok: true,
1316
- op: "snapshot",
1317
- doc_id: input.doc_id,
1318
- version: session.version(),
1319
- preview: renderCompactProjection(document)
1320
- };
1855
+ return runExclusive(input.doc_id, async (doc) => {
1856
+ assertNoPendingPush(input.doc_id);
1857
+ const pull = await observePull(doc);
1858
+ return {
1859
+ ok: true,
1860
+ op: "snapshot",
1861
+ doc_id: input.doc_id,
1862
+ version: encodeDocVersionMark(doc.versionMark()),
1863
+ preview: renderCompactProjection(doc.snapshot()),
1864
+ collaborated: pull.collaborated,
1865
+ ...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
1866
+ };
1867
+ });
1321
1868
  }
1322
1869
  async function run(input) {
1323
- const session = await getSession(input.doc_id);
1324
- const document = session.snapshot();
1325
- const baseVersion = session.version();
1326
- const result = await runEditScript({
1327
- document,
1328
- baseVersion,
1329
- script: input.script,
1330
- ...input.inputs !== void 0 ? { inputs: input.inputs } : {},
1331
- timeoutMs: input.timeout_ms ?? options.sandbox?.timeoutMs,
1332
- memoryLimitMb: input.memory_limit_mb ?? options.sandbox?.memoryLimitMb
1870
+ return runExclusive(input.doc_id, async (doc) => {
1871
+ assertNoPendingPush(input.doc_id);
1872
+ const [pull, entityState] = await Promise.all([observePull(doc), fetchEntityStateForSandbox(input.doc_id)]);
1873
+ const document = doc.snapshot();
1874
+ const baseVersion = encodeDocVersionMark(doc.versionMark());
1875
+ const result = await runEditScript({
1876
+ document,
1877
+ baseVersion,
1878
+ entityState,
1879
+ script: input.script,
1880
+ ...input.inputs !== void 0 ? { inputs: input.inputs } : {},
1881
+ timeoutMs: input.timeout_ms ?? options.sandbox?.timeoutMs,
1882
+ memoryLimitMb: input.memory_limit_mb ?? options.sandbox?.memoryLimitMb
1883
+ });
1884
+ if (!result.ok) return {
1885
+ ok: false,
1886
+ op: "run-edit-script",
1887
+ doc_id: input.doc_id,
1888
+ phase: result.phase,
1889
+ error: result.error,
1890
+ partial: {
1891
+ ops_count: result.partial.ops.length + result.partial.entityCommands.length,
1892
+ logs: result.partial.logs
1893
+ }
1894
+ };
1895
+ const plan = {
1896
+ ...result.plan,
1897
+ doc_id: input.doc_id
1898
+ };
1899
+ const planId = rememberPlan(input.doc_id, plan);
1900
+ const base = {
1901
+ ok: true,
1902
+ op: "run-edit-script",
1903
+ doc_id: input.doc_id,
1904
+ plan_id: planId,
1905
+ plan_kind: plan.plan_kind,
1906
+ base_version: baseVersion,
1907
+ entity_base_revision: plan.entity_base_revision,
1908
+ ops_count: plan.ops.length + plan.entity_commands.length,
1909
+ preview: plan.preview,
1910
+ logs: plan.logs,
1911
+ duration_ms: result.durationMs,
1912
+ collaborated: pull.collaborated,
1913
+ ...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
1914
+ };
1915
+ if (input.auto_commit !== true) return base;
1916
+ const commit = await commitCachedPlan(input.doc_id, doc, plan);
1917
+ recordPushResult(input.doc_id, planId, plan, commit);
1918
+ const warnings = mergeWarnings(pull.warnings, commitWarnings(commit));
1919
+ return {
1920
+ ...base,
1921
+ committed: commit.kind === "committed",
1922
+ commit_result: commit,
1923
+ collaborated: pull.collaborated || commit.kind === "committed" && commit.collaborated,
1924
+ ...warnings !== void 0 ? { warnings } : {}
1925
+ };
1333
1926
  });
1334
- if (!result.ok) return {
1335
- ok: false,
1336
- op: "run-edit-script",
1337
- doc_id: input.doc_id,
1338
- phase: result.phase,
1339
- error: result.error,
1340
- partial: {
1341
- ops_count: result.partial.ops.length,
1342
- logs: result.partial.logs
1343
- }
1344
- };
1345
- const planId = rememberPlan(input.doc_id, result.plan);
1346
- const base = {
1347
- ok: true,
1348
- op: "run-edit-script",
1349
- doc_id: input.doc_id,
1350
- plan_id: planId,
1351
- base_version: baseVersion,
1352
- ops_count: result.plan.ops.length,
1353
- preview: result.plan.preview,
1354
- logs: result.plan.logs,
1355
- duration_ms: result.durationMs
1356
- };
1357
- if (input.auto_commit !== true) return base;
1358
- const commit = await confirmCommitted(session, await commitPlan(session, result.plan));
1359
- return {
1360
- ...base,
1361
- committed: commit.kind === "committed",
1362
- commit_result: commit
1363
- };
1364
1927
  }
1365
1928
  async function commit(input) {
1366
- const cached = plans.get(input.plan_id);
1367
- if (cached == null || cached.docId !== input.doc_id) throw new Error(`plan_id ${input.plan_id} is not available for doc ${input.doc_id}`);
1368
- const session = await getSession(input.doc_id);
1369
- const commitOptions = input.validation === void 0 ? void 0 : { validation: input.validation };
1370
- const result = await confirmCommitted(session, await commitPlan(session, cached.plan, commitOptions));
1371
- return {
1372
- ok: true,
1373
- op: "commit-plan",
1374
- doc_id: input.doc_id,
1375
- plan_id: input.plan_id,
1376
- committed: result.kind === "committed",
1377
- result
1378
- };
1929
+ return runExclusive(input.doc_id, async (doc) => {
1930
+ const pending = pendingPushes.get(input.doc_id);
1931
+ if (pending != null) {
1932
+ if (pending.planId !== input.plan_id) throw new Error(`doc ${input.doc_id} has an unconfirmed push for plan_id ${pending.planId}; retry it before ${input.plan_id}`);
1933
+ const result = pending.kind === "timeline" ? await retryPlanPush(doc, pending.opsApplied) : await commitCachedPlan(input.doc_id, doc, pending.plan, input.validation);
1934
+ recordPushResult(input.doc_id, input.plan_id, pending.plan, result);
1935
+ const warnings = commitWarnings(result);
1936
+ return {
1937
+ ok: true,
1938
+ op: "commit-plan",
1939
+ doc_id: input.doc_id,
1940
+ plan_id: input.plan_id,
1941
+ plan_kind: pending.plan.plan_kind,
1942
+ committed: result.kind === "committed",
1943
+ result,
1944
+ collaborated: result.kind === "committed" && result.collaborated,
1945
+ ...warnings !== void 0 ? { warnings } : {}
1946
+ };
1947
+ }
1948
+ const cached = plans.get(input.plan_id);
1949
+ if (cached == null || cached.docId !== input.doc_id) throw new Error(`plan_id ${input.plan_id} is not available for doc ${input.doc_id}`);
1950
+ const pull = cached.plan.plan_kind === "timeline" ? await observePull(doc) : { collaborated: false };
1951
+ const result = await commitCachedPlan(input.doc_id, doc, cached.plan, input.validation);
1952
+ recordPushResult(input.doc_id, input.plan_id, cached.plan, result);
1953
+ const warnings = mergeWarnings(pull.warnings, commitWarnings(result));
1954
+ return {
1955
+ ok: true,
1956
+ op: "commit-plan",
1957
+ doc_id: input.doc_id,
1958
+ plan_id: input.plan_id,
1959
+ plan_kind: cached.plan.plan_kind,
1960
+ committed: result.kind === "committed",
1961
+ result,
1962
+ collaborated: pull.collaborated || result.kind === "committed" && result.collaborated,
1963
+ ...warnings !== void 0 ? { warnings } : {}
1964
+ };
1965
+ });
1379
1966
  }
1380
1967
  return {
1381
1968
  name: MEDEO_TOOL_NAME,
@@ -1398,18 +1985,22 @@ function createMedeoTool(options) {
1398
1985
  },
1399
1986
  async close() {
1400
1987
  closed = true;
1401
- const opening = [...sessions.values()];
1402
- sessions.clear();
1988
+ await Promise.allSettled(documentTails.values());
1989
+ const opening = [...documents.values()];
1990
+ documents.clear();
1991
+ entityClients.clear();
1992
+ documentTails.clear();
1993
+ pendingPushes.clear();
1403
1994
  plans.clear();
1404
1995
  modelContextVersions.clear();
1405
1996
  const errors = [];
1406
- for (const sessionPromise of opening) try {
1407
- (await sessionPromise).destroy();
1997
+ for (const documentPromise of opening) try {
1998
+ await documentPromise;
1408
1999
  } catch (error) {
1409
2000
  errors.push(error);
1410
2001
  }
1411
2002
  if (errors.length === 1) throw errors[0];
1412
- if (errors.length > 1) throw new AggregateError(errors, "failed to close medeo tool sessions");
2003
+ if (errors.length > 1) throw new AggregateError(errors, "failed to close medeo tool documents");
1413
2004
  }
1414
2005
  };
1415
2006
  }