@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/README.md +36 -6
- package/dist/entity-contract-B3txrzTt.d.mts +174 -0
- package/dist/index.d.mts +87 -17
- package/dist/index.mjs +740 -149
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +176 -3
- package/dist/script-session-BF44uKv_.mjs +1501 -0
- package/dist/script-session-BF44uKv_.mjs.map +1 -0
- package/dist/worker-entry.d.mts +2 -0
- package/dist/worker-entry.mjs +23 -4
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +4 -3
- package/dist/script-session-DdPA4tTf.mjs +0 -447
- package/dist/script-session-DdPA4tTf.mjs.map +0 -1
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-
|
|
2
|
-
import {
|
|
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: `
|
|
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
|
|
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
|
|
938
|
-
- commit-plan:
|
|
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
|
|
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
|
|
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
|
|
1077
|
-
*
|
|
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
|
|
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(
|
|
1088
|
-
if (options?.validation === "preflight") return commitPlanPreflight(
|
|
1089
|
-
const actual =
|
|
1090
|
-
|
|
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(
|
|
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(
|
|
1108
|
-
const scratch = createPlainMemoryAdapter(
|
|
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(
|
|
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: "
|
|
1131
|
-
|
|
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
|
|
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
|
|
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
|
|
1682
|
+
async function getDocument(docId) {
|
|
1215
1683
|
if (closed) throw new Error("medeo tool is closed");
|
|
1216
|
-
const existing =
|
|
1684
|
+
const existing = documents.get(docId);
|
|
1217
1685
|
if (existing != null) return await existing;
|
|
1218
1686
|
const created = (async () => {
|
|
1219
|
-
|
|
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
|
-
|
|
1695
|
+
documents.set(docId, created);
|
|
1243
1696
|
try {
|
|
1244
1697
|
return await created;
|
|
1245
1698
|
} catch (error) {
|
|
1246
|
-
if (
|
|
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
|
|
1253
|
-
|
|
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
|
|
1277
|
-
|
|
1278
|
-
plans.
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
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
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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
|
-
|
|
1402
|
-
|
|
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
|
|
1407
|
-
|
|
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
|
|
2003
|
+
if (errors.length > 1) throw new AggregateError(errors, "failed to close medeo tool documents");
|
|
1413
2004
|
}
|
|
1414
2005
|
};
|
|
1415
2006
|
}
|