@mengine/medeo-tool 1.0.1-alpha.2 → 1.2.1-alpha.10
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 +118 -8
- package/dist/entity-contract-C5HHaYKk.d.mts +228 -0
- package/dist/index.d.mts +320 -54
- package/dist/index.mjs +1672 -129
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +476 -643
- package/dist/script-session-B4fLNe-p.mjs +1942 -0
- package/dist/script-session-B4fLNe-p.mjs.map +1 -0
- package/dist/worker-entry.d.mts +6 -3
- package/dist/worker-entry.mjs +316 -8
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +4 -3
- package/dist/script-session-B8fc9Ccb.mjs +0 -439
- package/dist/script-session-B8fc9Ccb.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { c as renderPreview, l as renderCompactProjection, n as EntitySandbox, o as isMediaAssetVariantKind, r as toDslRows, s as collectAffectedPartIds, t as EditSandboxSession } from "./script-session-B4fLNe-p.mjs";
|
|
2
|
+
import { EntityGraphHttpClient, ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, encodeDocVersionMark, migrateLegacyTimelineToEntities, 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,7 +34,9 @@ function runEditScript(options) {
|
|
|
33
34
|
document: options.document,
|
|
34
35
|
script: options.script,
|
|
35
36
|
inputs: options.inputs,
|
|
36
|
-
|
|
37
|
+
entityState: options.entityState,
|
|
38
|
+
idLabel: options.idLabel,
|
|
39
|
+
entityOnly: options.entityOnly
|
|
37
40
|
},
|
|
38
41
|
execArgv: resolveRegisterUrl.pathname.endsWith(".ts") ? [
|
|
39
42
|
"--experimental-transform-types",
|
|
@@ -54,6 +57,7 @@ function runEditScript(options) {
|
|
|
54
57
|
error: { message: `edit script exceeded timeout of ${timeoutMs}ms` },
|
|
55
58
|
partial: {
|
|
56
59
|
ops: ops.slice(),
|
|
60
|
+
entityCommands: entityCommands.slice(),
|
|
57
61
|
logs: logs.slice()
|
|
58
62
|
}
|
|
59
63
|
});
|
|
@@ -81,6 +85,10 @@ function runEditScript(options) {
|
|
|
81
85
|
ops.push(message.entry);
|
|
82
86
|
return;
|
|
83
87
|
}
|
|
88
|
+
if (message.t === "entity-entry") {
|
|
89
|
+
entityCommands.push(message.command);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
84
92
|
if (message.t === "log") {
|
|
85
93
|
logs.push(message.line);
|
|
86
94
|
return;
|
|
@@ -89,14 +97,19 @@ function runEditScript(options) {
|
|
|
89
97
|
ops.length = Math.max(0, Math.min(message.index, ops.length));
|
|
90
98
|
return;
|
|
91
99
|
}
|
|
100
|
+
if (message.t === "entity-truncate") {
|
|
101
|
+
entityCommands.length = Math.max(0, Math.min(message.index, entityCommands.length));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
92
104
|
if (message.t === "done") {
|
|
93
|
-
if (message.opsCount !== ops.length) {
|
|
105
|
+
if (message.opsCount !== ops.length || message.entityCommandsCount !== entityCommands.length) {
|
|
94
106
|
finish({
|
|
95
107
|
ok: false,
|
|
96
108
|
phase: "runtime",
|
|
97
|
-
error: { message: `
|
|
109
|
+
error: { message: `journal count mismatch: worker reported timeline=${message.opsCount}, entities=${message.entityCommandsCount}; host collected timeline=${ops.length}, entities=${entityCommands.length}` },
|
|
98
110
|
partial: {
|
|
99
111
|
ops: ops.slice(),
|
|
112
|
+
entityCommands: entityCommands.slice(),
|
|
100
113
|
logs: logs.slice()
|
|
101
114
|
}
|
|
102
115
|
});
|
|
@@ -105,9 +118,15 @@ function runEditScript(options) {
|
|
|
105
118
|
finish({
|
|
106
119
|
ok: true,
|
|
107
120
|
plan: {
|
|
121
|
+
plan_kind: message.planKind,
|
|
108
122
|
doc_id: options.document.meta.draft_id ?? "",
|
|
109
123
|
base_version: options.baseVersion,
|
|
110
124
|
ops: ops.slice(),
|
|
125
|
+
entity_base_revision: message.entityBaseRevision,
|
|
126
|
+
entity_commands: entityCommands.slice(),
|
|
127
|
+
...message.entityRows !== void 0 ? { entity_rows: message.entityRows } : {},
|
|
128
|
+
deleted_entity_ids: message.deletedEntityIds,
|
|
129
|
+
deleted_relation_ids: message.deletedRelationIds,
|
|
111
130
|
preview: message.preview,
|
|
112
131
|
logs: logs.slice()
|
|
113
132
|
},
|
|
@@ -121,6 +140,7 @@ function runEditScript(options) {
|
|
|
121
140
|
error: message.error,
|
|
122
141
|
partial: {
|
|
123
142
|
ops: ops.slice(),
|
|
143
|
+
entityCommands: entityCommands.slice(),
|
|
124
144
|
logs: logs.slice()
|
|
125
145
|
}
|
|
126
146
|
});
|
|
@@ -137,6 +157,7 @@ function runEditScript(options) {
|
|
|
137
157
|
},
|
|
138
158
|
partial: {
|
|
139
159
|
ops: ops.slice(),
|
|
160
|
+
entityCommands: entityCommands.slice(),
|
|
140
161
|
logs: logs.slice()
|
|
141
162
|
}
|
|
142
163
|
});
|
|
@@ -150,6 +171,7 @@ function runEditScript(options) {
|
|
|
150
171
|
error: { message: `worker exited with code ${code ?? "null"} before completion` },
|
|
151
172
|
partial: {
|
|
152
173
|
ops: ops.slice(),
|
|
174
|
+
entityCommands: entityCommands.slice(),
|
|
153
175
|
logs: logs.slice()
|
|
154
176
|
}
|
|
155
177
|
});
|
|
@@ -157,22 +179,1047 @@ function runEditScript(options) {
|
|
|
157
179
|
});
|
|
158
180
|
}
|
|
159
181
|
//#endregion
|
|
182
|
+
//#region src/entity/entity-contract.ts
|
|
183
|
+
const KNOWN_ENTITY_KINDS = [
|
|
184
|
+
"axvideo",
|
|
185
|
+
"timeline",
|
|
186
|
+
"track",
|
|
187
|
+
"clip",
|
|
188
|
+
"asset",
|
|
189
|
+
"video",
|
|
190
|
+
"audio",
|
|
191
|
+
"voice",
|
|
192
|
+
"image",
|
|
193
|
+
"sequence-marker",
|
|
194
|
+
"viewport",
|
|
195
|
+
"audio-script",
|
|
196
|
+
"phonetic-script",
|
|
197
|
+
"caption"
|
|
198
|
+
];
|
|
199
|
+
const KNOWN_RELATION_KINDS = [
|
|
200
|
+
"timeline-track",
|
|
201
|
+
"track-clip",
|
|
202
|
+
"clip-marker",
|
|
203
|
+
"marker-content",
|
|
204
|
+
"axvideo-marker",
|
|
205
|
+
"marker-timeline",
|
|
206
|
+
"physical-asset",
|
|
207
|
+
"generated",
|
|
208
|
+
"phonetic-script-provenance",
|
|
209
|
+
"caption-provenance",
|
|
210
|
+
"caption-alignment",
|
|
211
|
+
"clip-anchor",
|
|
212
|
+
"audio-script-render"
|
|
213
|
+
];
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/entity/entity-http-client.ts
|
|
216
|
+
const API_PREFIX = "/api/mengine/v1";
|
|
217
|
+
const entityKinds = new Set(KNOWN_ENTITY_KINDS);
|
|
218
|
+
const relationKinds = new Set(KNOWN_RELATION_KINDS);
|
|
219
|
+
var MengineEntityHttpRequestError = class extends Error {
|
|
220
|
+
status;
|
|
221
|
+
payload;
|
|
222
|
+
constructor(status, payload) {
|
|
223
|
+
super(`mengine entity-state request failed: ${status}`);
|
|
224
|
+
this.status = status;
|
|
225
|
+
this.payload = payload;
|
|
226
|
+
this.name = "MengineEntityHttpRequestError";
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
/** Narrow authenticated client for the entity-store CAS endpoint. */
|
|
230
|
+
var EntityHttpClient = class {
|
|
231
|
+
options;
|
|
232
|
+
fetchImpl;
|
|
233
|
+
constructor(options) {
|
|
234
|
+
this.options = options;
|
|
235
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
236
|
+
}
|
|
237
|
+
async fetchState() {
|
|
238
|
+
return toSnapshot(await this.requestJson({ method: "GET" }), this.options.docId);
|
|
239
|
+
}
|
|
240
|
+
async commit(expectedRevision, state, deletions = {}) {
|
|
241
|
+
return toSnapshot(await this.requestJson({
|
|
242
|
+
method: "POST",
|
|
243
|
+
body: JSON.stringify({
|
|
244
|
+
expected_revision: expectedRevision,
|
|
245
|
+
rows: {
|
|
246
|
+
entities: state.entities,
|
|
247
|
+
relations: state.relations
|
|
248
|
+
},
|
|
249
|
+
deleted_entity_ids: [...deletions.deleted_entity_ids ?? []],
|
|
250
|
+
deleted_relation_ids: [...deletions.deleted_relation_ids ?? []]
|
|
251
|
+
})
|
|
252
|
+
}), this.options.docId);
|
|
253
|
+
}
|
|
254
|
+
async requestJson(init) {
|
|
255
|
+
const response = await this.fetchImpl(this.endpoint(), {
|
|
256
|
+
...init,
|
|
257
|
+
headers: this.headers()
|
|
258
|
+
});
|
|
259
|
+
const payload = await safeReadJson(response);
|
|
260
|
+
if (!response.ok) throw new MengineEntityHttpRequestError(response.status, payload);
|
|
261
|
+
return payload;
|
|
262
|
+
}
|
|
263
|
+
headers() {
|
|
264
|
+
const headers = new Headers({
|
|
265
|
+
accept: "application/json",
|
|
266
|
+
"content-type": "application/json"
|
|
267
|
+
});
|
|
268
|
+
const authToken = typeof this.options.authToken === "function" ? this.options.authToken() : this.options.authToken;
|
|
269
|
+
if (authToken != null && authToken !== "") headers.set("authorization", `Bearer ${authToken}`);
|
|
270
|
+
const userId = typeof this.options.userId === "function" ? this.options.userId() : this.options.userId;
|
|
271
|
+
if (userId != null && userId !== "") headers.set("medeo-user-id", userId);
|
|
272
|
+
return headers;
|
|
273
|
+
}
|
|
274
|
+
endpoint() {
|
|
275
|
+
return `${this.options.httpOrigin.replace(/\/$/, "")}${API_PREFIX}/docs/${encodeURIComponent(this.options.docId)}/entity-state`;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
function toSnapshot(value, expectedDocId) {
|
|
279
|
+
if (!isRecord$2(value) || typeof value.doc_id !== "string" || !isNonNegativeInteger(value.revision)) throw new Error("invalid entity-state response envelope");
|
|
280
|
+
if (value.doc_id !== expectedDocId) throw new Error(`entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
281
|
+
if (!isRecord$2(value.rows) || !Array.isArray(value.rows.entities) || !Array.isArray(value.rows.relations)) throw new Error("invalid entity-state response rows");
|
|
282
|
+
const response = value;
|
|
283
|
+
return {
|
|
284
|
+
revision: response.revision,
|
|
285
|
+
entities: response.rows.entities.map(parseEntity),
|
|
286
|
+
relations: response.rows.relations.map(parseRelation)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function parseEntity(value) {
|
|
290
|
+
if (!isRecord$2(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");
|
|
291
|
+
return structuredClone(value);
|
|
292
|
+
}
|
|
293
|
+
function parseRelation(value) {
|
|
294
|
+
if (!isRecord$2(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");
|
|
295
|
+
return structuredClone(value);
|
|
296
|
+
}
|
|
297
|
+
function isJsonObject(value) {
|
|
298
|
+
return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$2(value);
|
|
299
|
+
}
|
|
300
|
+
function isJsonValue(value, ancestors) {
|
|
301
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
302
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
303
|
+
if (typeof value !== "object" || ancestors.has(value)) return false;
|
|
304
|
+
const prototype = Object.getPrototypeOf(value);
|
|
305
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
|
|
306
|
+
ancestors.add(value);
|
|
307
|
+
const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
|
|
308
|
+
ancestors.delete(value);
|
|
309
|
+
return valid;
|
|
310
|
+
}
|
|
311
|
+
function isRecord$2(value) {
|
|
312
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
313
|
+
}
|
|
314
|
+
function isTrimmed(value) {
|
|
315
|
+
return typeof value === "string" && value.length > 0 && value.trim() === value;
|
|
316
|
+
}
|
|
317
|
+
function isNonNegativeInteger(value) {
|
|
318
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
319
|
+
}
|
|
320
|
+
async function safeReadJson(response) {
|
|
321
|
+
const text = await response.text();
|
|
322
|
+
if (text.length === 0) return null;
|
|
323
|
+
try {
|
|
324
|
+
return JSON.parse(text);
|
|
325
|
+
} catch {
|
|
326
|
+
return text;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/entity/generation-sync.ts
|
|
331
|
+
/**
|
|
332
|
+
* External systems whose asset entities carry a factual Memota identity.
|
|
333
|
+
* Voice results use the speech system; every other medium uses `memota`.
|
|
334
|
+
*/
|
|
335
|
+
const ASSET_SYSTEMS = new Set(["memota", "memota-speech"]);
|
|
336
|
+
/** Bounded CAS retry budget for the sync commit after a concurrent winner. */
|
|
337
|
+
const MAX_COMMIT_ATTEMPTS = 3;
|
|
338
|
+
/** Validate host-supplied facts; a malformed record fails the whole query. */
|
|
339
|
+
function parseGenerationFacts(value) {
|
|
340
|
+
if (!Array.isArray(value)) throw new Error("generation facts must be an array");
|
|
341
|
+
return value.map((item) => {
|
|
342
|
+
if (!isRecord$1(item)) throw new Error("each generation fact must be an object");
|
|
343
|
+
const { outputAssetId, inputAssetIds } = item;
|
|
344
|
+
if (typeof outputAssetId !== "string" || outputAssetId.length === 0 || outputAssetId.trim() !== outputAssetId) throw new Error("generation fact outputAssetId must be a non-empty trimmed string");
|
|
345
|
+
if (!Array.isArray(inputAssetIds)) throw new Error("generation fact inputAssetIds must be an array (explicit [] means text-only)");
|
|
346
|
+
const inputs = inputAssetIds;
|
|
347
|
+
for (const input of inputs) if (typeof input !== "string" || input.length === 0 || input.trim() !== input) throw new Error("generation fact inputAssetIds entries must be non-empty trimmed strings");
|
|
348
|
+
return {
|
|
349
|
+
outputAssetId,
|
|
350
|
+
inputAssetIds: [...inputs]
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function planGenerationScope(base, commands, state) {
|
|
355
|
+
const touchedIds = /* @__PURE__ */ new Set();
|
|
356
|
+
for (const command of commands) if (command.kind === "create-entity" && isMediaAssetVariantKind(command.entity.entity_kind)) touchedIds.add(command.entity.entity_id);
|
|
357
|
+
const beforeByKey = resolveMediaByAssetKey(base);
|
|
358
|
+
const scoped = /* @__PURE__ */ new Set();
|
|
359
|
+
const queryKeys = /* @__PURE__ */ new Set();
|
|
360
|
+
for (const [key, mediaIds] of resolveMediaByAssetKey(state)) {
|
|
361
|
+
const previousIds = new Set(beforeByKey.get(key) ?? []);
|
|
362
|
+
for (const id of mediaIds) {
|
|
363
|
+
if (!touchedIds.has(id) || previousIds.has(id)) continue;
|
|
364
|
+
scoped.add(id);
|
|
365
|
+
queryKeys.add(key);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
scopedMediaIds: scoped,
|
|
370
|
+
queryAssetKeys: [...queryKeys].sort()
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Ordered generated(output,input) Relations missing from `state` for the given
|
|
375
|
+
* factual records. Both endpoints must already exist and match their own Asset
|
|
376
|
+
* identities, and the pair must involve a media Entity the plan
|
|
377
|
+
* newly fact-exposed (`scopedMediaIds`): lineage scopes to the commit's diff,
|
|
378
|
+
* so a pair the user deleted between untouched entities stays deleted. A pair
|
|
379
|
+
* the facts already resolved against the plan's base state is likewise skipped.
|
|
380
|
+
* One-sided facts, text-only records, self pairs, and already-linked pairs are
|
|
381
|
+
* skipped. Duplicate records collapse to one Relation.
|
|
382
|
+
*/
|
|
383
|
+
function planGeneratedRelations(input) {
|
|
384
|
+
const { state, facts } = input;
|
|
385
|
+
const scoped = input.scopedMediaIds;
|
|
386
|
+
const factKeys = new Set(facts.flatMap((fact) => [fact.outputAssetId, ...fact.inputAssetIds]));
|
|
387
|
+
const mediaByAssetKey = resolveMediaByAssetKey(state, factKeys);
|
|
388
|
+
const baseResolvable = new Set(resolvablePairs(resolveMediaByAssetKey(input.baseState, factKeys), facts));
|
|
389
|
+
const linkedPairs = new Set(state.relations.filter((relation) => relation.relation_kind === "generated").map((relation) => pairKey(relation.endpoint_0_entity_id, relation.endpoint_1_entity_id)));
|
|
390
|
+
const relations = [];
|
|
391
|
+
for (const fact of facts) for (const outputId of mediaByAssetKey.get(fact.outputAssetId) ?? []) for (const inputAssetId of fact.inputAssetIds) for (const inputId of mediaByAssetKey.get(inputAssetId) ?? []) {
|
|
392
|
+
if (outputId === inputId) continue;
|
|
393
|
+
if (!scoped.has(outputId) && !scoped.has(inputId)) continue;
|
|
394
|
+
const pair = pairKey(outputId, inputId);
|
|
395
|
+
if (linkedPairs.has(pair) || baseResolvable.has(pair)) continue;
|
|
396
|
+
linkedPairs.add(pair);
|
|
397
|
+
relations.push({
|
|
398
|
+
relation_id: input.newRelationId(),
|
|
399
|
+
relation_kind: "generated",
|
|
400
|
+
endpoint_0_entity_id: outputId,
|
|
401
|
+
endpoint_1_entity_id: inputId,
|
|
402
|
+
metadata: {},
|
|
403
|
+
trace: { synced_by: "generation-sync" }
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return relations;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Sync generation lineage after a confirmed entity commit. Any failure is
|
|
410
|
+
* returned as a `failed` outcome instead of thrown, so the already-durable
|
|
411
|
+
* commit result is never masked; a successful query that finds nothing is
|
|
412
|
+
* `current`. Asset identities are immutable, so facts are queried once. A
|
|
413
|
+
* revision conflict re-reads current entities and relations, re-plans, and
|
|
414
|
+
* retries within `MAX_COMMIT_ATTEMPTS`; deleted endpoints are never recreated.
|
|
415
|
+
*/
|
|
416
|
+
async function syncGeneratedRelations(input) {
|
|
417
|
+
const { client, docId, baseState, entityCommands, loadFacts } = input;
|
|
418
|
+
try {
|
|
419
|
+
let state = await client.fetchState();
|
|
420
|
+
let scope = planGenerationScope(baseState, entityCommands, state);
|
|
421
|
+
if (scope.queryAssetKeys.length === 0) return { status: "current" };
|
|
422
|
+
const facts = parseGenerationFacts(await loadFacts(docId, scope.queryAssetKeys));
|
|
423
|
+
for (let attempt = 1; attempt <= MAX_COMMIT_ATTEMPTS; attempt++) {
|
|
424
|
+
if (scope.queryAssetKeys.length === 0) return { status: "current" };
|
|
425
|
+
const relations = planGeneratedRelations({
|
|
426
|
+
baseState,
|
|
427
|
+
state,
|
|
428
|
+
scopedMediaIds: scope.scopedMediaIds,
|
|
429
|
+
facts,
|
|
430
|
+
newRelationId: mintRelationId
|
|
431
|
+
});
|
|
432
|
+
if (relations.length === 0) return { status: "current" };
|
|
433
|
+
try {
|
|
434
|
+
await client.commit(state.revision, {
|
|
435
|
+
...state,
|
|
436
|
+
relations: [...state.relations, ...relations]
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
status: "applied",
|
|
440
|
+
created_relation_ids: relations.map((relation) => relation.relation_id)
|
|
441
|
+
};
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (!(error instanceof MengineEntityHttpRequestError && error.status === 409) || attempt === MAX_COMMIT_ATTEMPTS) return {
|
|
444
|
+
status: "failed",
|
|
445
|
+
message: `generation lineage sync commit failed: ${errorMessage(error)}`
|
|
446
|
+
};
|
|
447
|
+
state = await client.fetchState();
|
|
448
|
+
scope = planGenerationScope(baseState, entityCommands, state);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
status: "failed",
|
|
453
|
+
message: "generation lineage sync exhausted its retry budget"
|
|
454
|
+
};
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return {
|
|
457
|
+
status: "failed",
|
|
458
|
+
message: `generation lineage query failed: ${errorMessage(error)}`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function assetKeyOf(entity) {
|
|
463
|
+
if (!isMediaAssetVariantKind(entity.entity_kind)) return void 0;
|
|
464
|
+
const external = entity.payload?.external;
|
|
465
|
+
if (external == null || typeof external !== "object" || Array.isArray(external)) return void 0;
|
|
466
|
+
const { system, key } = external;
|
|
467
|
+
if (typeof system !== "string" || !ASSET_SYSTEMS.has(system)) return void 0;
|
|
468
|
+
if (typeof key !== "string" || key.length === 0 || key.trim() !== key) return void 0;
|
|
469
|
+
return key;
|
|
470
|
+
}
|
|
471
|
+
/** Media variants own their Asset locator; generation lookup never follows Relations. */
|
|
472
|
+
function resolveMediaByAssetKey(state, factKeys) {
|
|
473
|
+
const systemByKey = /* @__PURE__ */ new Map();
|
|
474
|
+
for (const entity of state.entities) {
|
|
475
|
+
const key = assetKeyOf(entity);
|
|
476
|
+
if (key === void 0 || !factKeys?.has(key)) continue;
|
|
477
|
+
const system = entity.payload.external.system;
|
|
478
|
+
if (systemByKey.has(key) && systemByKey.get(key) !== system) throw new Error(`Ambiguous generation asset id ${key} across media and speech namespaces`);
|
|
479
|
+
systemByKey.set(key, system);
|
|
480
|
+
}
|
|
481
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
482
|
+
for (const entity of state.entities) {
|
|
483
|
+
if (!isMediaAssetVariantKind(entity.entity_kind)) continue;
|
|
484
|
+
const key = assetKeyOf(entity);
|
|
485
|
+
if (key === void 0) continue;
|
|
486
|
+
const matches = resolved.get(key) ?? [];
|
|
487
|
+
matches.push(entity.entity_id);
|
|
488
|
+
resolved.set(key, matches);
|
|
489
|
+
}
|
|
490
|
+
return resolved;
|
|
491
|
+
}
|
|
492
|
+
function pairKey(endpoint0, endpoint1) {
|
|
493
|
+
return `${endpoint0}\u0000${endpoint1}`;
|
|
494
|
+
}
|
|
495
|
+
/** Pair keys the facts already resolve to under the given base bindings. */
|
|
496
|
+
function resolvablePairs(mediaByAssetKey, facts) {
|
|
497
|
+
const pairs = [];
|
|
498
|
+
for (const fact of facts) for (const outputId of mediaByAssetKey.get(fact.outputAssetId) ?? []) for (const inputAssetId of fact.inputAssetIds) for (const inputId of mediaByAssetKey.get(inputAssetId) ?? []) if (outputId !== inputId) pairs.push(pairKey(outputId, inputId));
|
|
499
|
+
return pairs;
|
|
500
|
+
}
|
|
501
|
+
function mintRelationId() {
|
|
502
|
+
return `relation_${randomUUID()}`;
|
|
503
|
+
}
|
|
504
|
+
function errorMessage(error) {
|
|
505
|
+
return error instanceof Error ? error.message : String(error);
|
|
506
|
+
}
|
|
507
|
+
function isRecord$1(value) {
|
|
508
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/migration-input.ts
|
|
512
|
+
/** Validate host-recalled facts without accepting a caller-controlled snapshot or VV. */
|
|
513
|
+
function parseMigrationAssetFacts(value) {
|
|
514
|
+
if (!Array.isArray(value)) throw new Error("asset_facts is required for migrate-legacy and must be an array");
|
|
515
|
+
return value.map((item) => {
|
|
516
|
+
if (!record(item)) throw new Error("Each asset_facts entry must be an object");
|
|
517
|
+
const { assetId, kind, durationMs, storageKey, voice } = item;
|
|
518
|
+
if (!nonempty(assetId)) throw new Error("asset_facts.assetId must be a non-empty trimmed string");
|
|
519
|
+
if (kind !== "image" && kind !== "video" && kind !== "audio" && kind !== "voice") throw new Error("asset_facts.kind must be image, video, audio, or voice");
|
|
520
|
+
if (Object.keys(item).some((key) => ![
|
|
521
|
+
"assetId",
|
|
522
|
+
"kind",
|
|
523
|
+
"durationMs",
|
|
524
|
+
"storageKey",
|
|
525
|
+
"voice"
|
|
526
|
+
].includes(key))) throw new Error("Unknown asset_facts field");
|
|
527
|
+
if (storageKey !== void 0 && !nonempty(storageKey)) throw new Error("asset_facts.storageKey must be non-empty");
|
|
528
|
+
if (kind === "image") {
|
|
529
|
+
if (durationMs !== void 0 || voice !== void 0) throw new Error("Image facts cannot declare duration or voice");
|
|
530
|
+
return {
|
|
531
|
+
assetId,
|
|
532
|
+
kind,
|
|
533
|
+
...storageKey === void 0 ? {} : { storageKey }
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (typeof durationMs !== "number" || !Number.isSafeInteger(durationMs) || durationMs <= 0) throw new Error("asset_facts.durationMs must be factual positive whole milliseconds");
|
|
537
|
+
if (kind === "video") {
|
|
538
|
+
if (voice !== void 0) throw new Error("Video facts cannot declare voice");
|
|
539
|
+
return {
|
|
540
|
+
assetId,
|
|
541
|
+
kind,
|
|
542
|
+
durationMs,
|
|
543
|
+
...storageKey === void 0 ? {} : { storageKey }
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
if (!nonempty(storageKey)) throw new Error("Audio and Voice facts require their physical storageKey");
|
|
547
|
+
if (kind === "audio") {
|
|
548
|
+
if (voice !== void 0) throw new Error("Audio facts cannot declare a Voice descriptor");
|
|
549
|
+
return {
|
|
550
|
+
assetId,
|
|
551
|
+
kind,
|
|
552
|
+
durationMs,
|
|
553
|
+
storageKey
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
if (!record(voice) || voice.system !== "voice-library" || !nonempty(voice.key) || voice.name !== void 0 && typeof voice.name !== "string" || Object.keys(voice).some((key) => ![
|
|
557
|
+
"system",
|
|
558
|
+
"key",
|
|
559
|
+
"name"
|
|
560
|
+
].includes(key))) throw new Error("Voice facts require an explicit voice-library descriptor");
|
|
561
|
+
return {
|
|
562
|
+
assetId,
|
|
563
|
+
kind,
|
|
564
|
+
durationMs,
|
|
565
|
+
storageKey,
|
|
566
|
+
voice: {
|
|
567
|
+
system: "voice-library",
|
|
568
|
+
key: voice.key,
|
|
569
|
+
...voice.name === void 0 ? {} : { name: voice.name }
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function record(value) {
|
|
575
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
576
|
+
}
|
|
577
|
+
function nonempty(value) {
|
|
578
|
+
return typeof value === "string" && value.length > 0 && value.trim() === value;
|
|
579
|
+
}
|
|
580
|
+
//#endregion
|
|
581
|
+
//#region src/sandbox/generated/entity-edit-sandbox-model-context.ts
|
|
582
|
+
/** @generated by gen:sandbox-dts. DO NOT EDIT. */
|
|
583
|
+
const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
584
|
+
"/** @generated by gen:sandbox-dts. Entity-native editor contract; DO NOT EDIT. */",
|
|
585
|
+
"export interface AudioMediaAssetFact {",
|
|
586
|
+
" readonly assetId: string;",
|
|
587
|
+
" readonly kind: 'audio';",
|
|
588
|
+
" readonly durationMs: number;",
|
|
589
|
+
" readonly storageKey: string;",
|
|
590
|
+
"}",
|
|
591
|
+
"export interface BoundedDerivedSequencePayload extends JsonObject {",
|
|
592
|
+
" extent: {",
|
|
593
|
+
" kind: 'bounded';",
|
|
594
|
+
" start: number;",
|
|
595
|
+
" end: number;",
|
|
596
|
+
" };",
|
|
597
|
+
" sampling: 'derived';",
|
|
598
|
+
" coordinateSpace: JsonValue;",
|
|
599
|
+
"}",
|
|
600
|
+
"export interface BoundedNativeSequencePayload extends JsonObject {",
|
|
601
|
+
" /** Factual coordinates from recalled media metadata; never invent an end/duration. */",
|
|
602
|
+
" extent: {",
|
|
603
|
+
" kind: 'bounded';",
|
|
604
|
+
" start: number;",
|
|
605
|
+
" end: number;",
|
|
606
|
+
" };",
|
|
607
|
+
" sampling: 'native';",
|
|
608
|
+
" coordinateSpace: JsonValue;",
|
|
609
|
+
"}",
|
|
610
|
+
"export interface CaptionFontDescriptor {",
|
|
611
|
+
" readonly system: 'font-library';",
|
|
612
|
+
" readonly key: string;",
|
|
613
|
+
"}",
|
|
614
|
+
"export interface CaptionStyleFields {",
|
|
615
|
+
" readonly font?: CaptionFontDescriptor;",
|
|
616
|
+
" readonly fontSize?: number;",
|
|
617
|
+
" readonly fontColor?: string;",
|
|
618
|
+
" readonly fontWeight?: number;",
|
|
619
|
+
" readonly entranceAnimation?: string;",
|
|
620
|
+
" readonly entranceAnimationDurationMs?: number;",
|
|
621
|
+
" readonly strokeColor?: string;",
|
|
622
|
+
" readonly strokeWidth?: number;",
|
|
623
|
+
" readonly positionX?: number;",
|
|
624
|
+
" readonly positionY?: number;",
|
|
625
|
+
"}",
|
|
626
|
+
"export type ClipEntityId = EntityId;",
|
|
627
|
+
"export type ClipPlacement =",
|
|
628
|
+
" | {",
|
|
629
|
+
" readonly kind: 'sequential';",
|
|
630
|
+
" readonly order: number;",
|
|
631
|
+
" }",
|
|
632
|
+
" | {",
|
|
633
|
+
" readonly kind: 'absolute';",
|
|
634
|
+
" readonly targetRange: SequenceRange<number>;",
|
|
635
|
+
" }",
|
|
636
|
+
" | {",
|
|
637
|
+
" readonly kind: 'anchored';",
|
|
638
|
+
" readonly hostClipEntityId: string;",
|
|
639
|
+
" readonly anchorOffset: number;",
|
|
640
|
+
" };",
|
|
641
|
+
"export type CreateEntityInput = {",
|
|
642
|
+
" [K in KnownEntityKind]: {",
|
|
643
|
+
" entity_id?: string;",
|
|
644
|
+
" entity_kind: K;",
|
|
645
|
+
" payload: EntityPayloadByKind[K];",
|
|
646
|
+
" };",
|
|
647
|
+
"}[KnownEntityKind];",
|
|
648
|
+
"export interface DeleteBgmInput {",
|
|
649
|
+
" readonly timelineEntityId: string;",
|
|
650
|
+
"}",
|
|
651
|
+
"export interface DeleteClipInput {",
|
|
652
|
+
" readonly clipEntityId: string;",
|
|
653
|
+
"}",
|
|
654
|
+
"export interface DeleteClipTreeInput {",
|
|
655
|
+
" readonly clipEntityIds: readonly string[];",
|
|
656
|
+
" readonly onAnchored: 'cascade' | 'detach';",
|
|
657
|
+
"}",
|
|
658
|
+
"export interface DeleteEntityInput {",
|
|
659
|
+
" entity_id: string;",
|
|
660
|
+
"}",
|
|
661
|
+
"export interface DeleteVoiceoverInput {",
|
|
662
|
+
" readonly voiceoverClipEntityIds: readonly string[];",
|
|
663
|
+
"}",
|
|
664
|
+
"export type EmptyRelationKind =",
|
|
665
|
+
" | 'timeline-track'",
|
|
666
|
+
" | 'track-clip'",
|
|
667
|
+
" | 'clip-marker'",
|
|
668
|
+
" | 'marker-content'",
|
|
669
|
+
" | 'axvideo-marker'",
|
|
670
|
+
" | 'marker-timeline';",
|
|
671
|
+
"export interface EntityFacade {",
|
|
672
|
+
" list(): SandboxEntity[];",
|
|
673
|
+
" get(entityId: string): SandboxEntity | null;",
|
|
674
|
+
" /** Find document resources by external Memota asset id, including directly composed media variants. */",
|
|
675
|
+
" findByAssetId(assetId: string): SandboxEntity<ResourceEntityKind>[];",
|
|
676
|
+
" create(input: CreateEntityInput): string;",
|
|
677
|
+
" /** Replace one Entity's owned payload without changing its identity or kind. */",
|
|
678
|
+
" update(input: UpdateEntityInput): void;",
|
|
679
|
+
" /** Delete an Entity only after all of its incident Relations have been explicitly unlinked. */",
|
|
680
|
+
" delete(input: DeleteEntityInput): void;",
|
|
681
|
+
" /** Get or create one typed Asset by factual external id and return its single content identity. Never creates a Clip. */",
|
|
682
|
+
" ensureMedia(fact: MediaAssetFact): {",
|
|
683
|
+
" contentEntityId: string;",
|
|
684
|
+
" };",
|
|
685
|
+
"}",
|
|
686
|
+
"export type EntityId = string;",
|
|
687
|
+
"export interface EntityPayloadByKind {",
|
|
688
|
+
" axvideo: BoundedDerivedSequencePayload;",
|
|
689
|
+
" timeline: JsonObject;",
|
|
690
|
+
" track: JsonObject & {",
|
|
691
|
+
" hidden?: boolean;",
|
|
692
|
+
" role?: string;",
|
|
693
|
+
" };",
|
|
694
|
+
" clip: JsonObject;",
|
|
695
|
+
" /** Inline caption text; external media uses a typed Asset variant. */",
|
|
696
|
+
" asset: JsonObject;",
|
|
697
|
+
" video: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
698
|
+
" audio: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
699
|
+
" voice: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
700
|
+
" image: UnboundedConstantSequencePayload & MediaAssetPayload;",
|
|
701
|
+
" 'sequence-marker': JsonObject & {",
|
|
702
|
+
" sourceRange: {",
|
|
703
|
+
" start: number;",
|
|
704
|
+
" end: number;",
|
|
705
|
+
" };",
|
|
706
|
+
" targetRange?: {",
|
|
707
|
+
" start: number;",
|
|
708
|
+
" end: number;",
|
|
709
|
+
" };",
|
|
710
|
+
" duration:",
|
|
711
|
+
" | {",
|
|
712
|
+
" mode: 'from-source';",
|
|
713
|
+
" }",
|
|
714
|
+
" | {",
|
|
715
|
+
" mode: 'fixed';",
|
|
716
|
+
" value: number;",
|
|
717
|
+
" };",
|
|
718
|
+
" timeRemapping?: JsonValue;",
|
|
719
|
+
" anchorOffset?: number;",
|
|
720
|
+
" durationPolicy?: 'timeline';",
|
|
721
|
+
" };",
|
|
722
|
+
" viewport: JsonObject;",
|
|
723
|
+
" 'audio-script': JsonObject & {",
|
|
724
|
+
" segments: ScriptTextSegment[];",
|
|
725
|
+
" };",
|
|
726
|
+
" 'phonetic-script': JsonObject & {",
|
|
727
|
+
" segments: ScriptTextSegment[];",
|
|
728
|
+
" };",
|
|
729
|
+
" caption: BoundedNativeSequencePayload;",
|
|
730
|
+
"}",
|
|
731
|
+
"export interface EntityStoreSnapshot {",
|
|
732
|
+
" revision: number;",
|
|
733
|
+
" entities: SandboxEntity[];",
|
|
734
|
+
" relations: SandboxRelation[];",
|
|
735
|
+
"}",
|
|
736
|
+
"export interface ImageMediaAssetFact {",
|
|
737
|
+
" readonly assetId: string;",
|
|
738
|
+
" readonly kind: 'image';",
|
|
739
|
+
" readonly storageKey?: string;",
|
|
740
|
+
"}",
|
|
741
|
+
"export interface InsertClipInput {",
|
|
742
|
+
" readonly trackEntityId: string;",
|
|
743
|
+
" /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */",
|
|
744
|
+
" readonly contentEntityId: string;",
|
|
745
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
746
|
+
" readonly duration: SequenceDuration<number>;",
|
|
747
|
+
" readonly targetRange?: SequenceRange<number>;",
|
|
748
|
+
" readonly clipPayload?: JsonObject;",
|
|
749
|
+
"}",
|
|
750
|
+
"export interface InsertMediaClipInput {",
|
|
751
|
+
" readonly timelineEntityId: string;",
|
|
752
|
+
" readonly clipEntityId?: string;",
|
|
753
|
+
" readonly media: VisualMediaAssetFact;",
|
|
754
|
+
" /** Source/display window in whole milliseconds. Images use this as their finite display span. */",
|
|
755
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
756
|
+
" readonly placement: ClipPlacement;",
|
|
757
|
+
" readonly volume?: number;",
|
|
758
|
+
"}",
|
|
759
|
+
"export interface InsertMediaClipsInput {",
|
|
760
|
+
" readonly timelineEntityId: string;",
|
|
761
|
+
" readonly clips: readonly ReplacementMediaClipInput[];",
|
|
762
|
+
" /** One placement decision for the whole input-ordered block. */",
|
|
763
|
+
" readonly insertion: MediaClipInsertion;",
|
|
764
|
+
"}",
|
|
765
|
+
"export interface InsertPlacedClipInput {",
|
|
766
|
+
" readonly trackEntityId: string;",
|
|
767
|
+
" readonly contentEntityId: string;",
|
|
768
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
769
|
+
" readonly duration: SequenceDuration<number>;",
|
|
770
|
+
" readonly placement: ClipPlacement;",
|
|
771
|
+
" readonly clipPayload?: JsonObject;",
|
|
772
|
+
" /** Stable caller-owned placement identity, when one already exists outside the graph. */",
|
|
773
|
+
" readonly clipEntityId?: string;",
|
|
774
|
+
"}",
|
|
775
|
+
"export interface JsonObject {",
|
|
776
|
+
" [key: string]: JsonValue;",
|
|
777
|
+
"}",
|
|
778
|
+
"export type JsonPrimitive = string | number | boolean | null;",
|
|
779
|
+
"export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];",
|
|
780
|
+
"export type KnownEntityKind =",
|
|
781
|
+
" | 'axvideo'",
|
|
782
|
+
" | 'timeline'",
|
|
783
|
+
" | 'track'",
|
|
784
|
+
" | 'clip'",
|
|
785
|
+
" | 'asset'",
|
|
786
|
+
" | 'video'",
|
|
787
|
+
" | 'audio'",
|
|
788
|
+
" | 'voice'",
|
|
789
|
+
" | 'image'",
|
|
790
|
+
" | 'sequence-marker'",
|
|
791
|
+
" | 'viewport'",
|
|
792
|
+
" | 'audio-script'",
|
|
793
|
+
" | 'phonetic-script'",
|
|
794
|
+
" | 'caption';",
|
|
795
|
+
"export type KnownRelationKind =",
|
|
796
|
+
" | 'timeline-track'",
|
|
797
|
+
" | 'track-clip'",
|
|
798
|
+
" | 'clip-marker'",
|
|
799
|
+
" | 'marker-content'",
|
|
800
|
+
" | 'axvideo-marker'",
|
|
801
|
+
" | 'marker-timeline'",
|
|
802
|
+
" | 'physical-asset'",
|
|
803
|
+
" | 'generated'",
|
|
804
|
+
" | 'phonetic-script-provenance'",
|
|
805
|
+
" | 'caption-provenance'",
|
|
806
|
+
" | 'caption-alignment'",
|
|
807
|
+
" | 'clip-anchor'",
|
|
808
|
+
" | 'audio-script-render';",
|
|
809
|
+
"export interface LinearClipSpeed {",
|
|
810
|
+
" readonly kind: 'linear';",
|
|
811
|
+
" readonly rate: number;",
|
|
812
|
+
" readonly mode?: string;",
|
|
813
|
+
"}",
|
|
814
|
+
"export interface LinkAudioScriptRenderRelationInput {",
|
|
815
|
+
" relation_id?: string;",
|
|
816
|
+
" output_entity_id: string;",
|
|
817
|
+
" script_entity_id: string;",
|
|
818
|
+
" trace?: JsonObject;",
|
|
819
|
+
"}",
|
|
820
|
+
"export interface LinkClipAnchorRelationInput {",
|
|
821
|
+
" relation_id?: string;",
|
|
822
|
+
" child_clip_entity_id: string;",
|
|
823
|
+
" host_clip_entity_id: string;",
|
|
824
|
+
" trace?: JsonObject;",
|
|
825
|
+
"}",
|
|
826
|
+
"export interface LinkGeneratedRelationInput {",
|
|
827
|
+
" relation_id?: string;",
|
|
828
|
+
" output_entity_id: string;",
|
|
829
|
+
" input_entity_id: string;",
|
|
830
|
+
" trace?: JsonObject;",
|
|
831
|
+
"}",
|
|
832
|
+
"interface LinkRelationBase {",
|
|
833
|
+
" relation_id?: string;",
|
|
834
|
+
" endpoint_0_entity_id: string;",
|
|
835
|
+
" endpoint_1_entity_id: string;",
|
|
836
|
+
" trace?: JsonObject;",
|
|
837
|
+
"}",
|
|
838
|
+
"export type LinkRelationInput =",
|
|
839
|
+
" | (LinkRelationBase & {",
|
|
840
|
+
" relation_kind: EmptyRelationKind;",
|
|
841
|
+
" metadata?: {",
|
|
842
|
+
" [key: string]: never;",
|
|
843
|
+
" };",
|
|
844
|
+
" })",
|
|
845
|
+
" | (LinkRelationBase & {",
|
|
846
|
+
" relation_kind: 'physical-asset';",
|
|
847
|
+
" metadata?: JsonObject;",
|
|
848
|
+
" })",
|
|
849
|
+
" | (LinkRelationBase & {",
|
|
850
|
+
" relation_kind: 'phonetic-script-provenance' | 'caption-provenance';",
|
|
851
|
+
" metadata: JsonObject & {",
|
|
852
|
+
" segmentAlignment: JsonValue;",
|
|
853
|
+
" };",
|
|
854
|
+
" })",
|
|
855
|
+
" | (LinkRelationBase & {",
|
|
856
|
+
" relation_kind: 'caption-alignment';",
|
|
857
|
+
" metadata: JsonObject & {",
|
|
858
|
+
" alignment: JsonValue;",
|
|
859
|
+
" };",
|
|
860
|
+
" });",
|
|
861
|
+
"/** Facts resolved from media storage. A trim window never substitutes for intrinsic duration. */",
|
|
862
|
+
"export type MediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact | AudioMediaAssetFact | VoiceMediaAssetFact;",
|
|
863
|
+
"export type MediaAssetPayload = JsonObject & {",
|
|
864
|
+
" external: {",
|
|
865
|
+
" system: 'memota' | 'memota-speech';",
|
|
866
|
+
" key: string;",
|
|
867
|
+
" };",
|
|
868
|
+
" storageKey?: string;",
|
|
869
|
+
"};",
|
|
870
|
+
"export type MediaClipInsertion =",
|
|
871
|
+
" | {",
|
|
872
|
+
" readonly kind: 'before';",
|
|
873
|
+
" readonly clipEntityId: string;",
|
|
874
|
+
" }",
|
|
875
|
+
" | {",
|
|
876
|
+
" readonly kind: 'after';",
|
|
877
|
+
" readonly clipEntityId: string;",
|
|
878
|
+
" }",
|
|
879
|
+
" | {",
|
|
880
|
+
" readonly kind: 'firstStart';",
|
|
881
|
+
" readonly startMs: number;",
|
|
882
|
+
" };",
|
|
883
|
+
"export interface MoveClipInput {",
|
|
884
|
+
" readonly clipEntityId: string;",
|
|
885
|
+
" readonly trackEntityId: string;",
|
|
886
|
+
"}",
|
|
887
|
+
"export interface MoveClipsToStartsInput {",
|
|
888
|
+
" readonly moves: readonly {",
|
|
889
|
+
" readonly clipEntityId: string;",
|
|
890
|
+
" readonly newStartMs: number;",
|
|
891
|
+
" }[];",
|
|
892
|
+
" /** Absolute-time drags preserve every voiceover's current visible landing. */",
|
|
893
|
+
" readonly onAnchored: 'keepAbsolute';",
|
|
894
|
+
"}",
|
|
895
|
+
"export interface MoveSequentialClipsInput {",
|
|
896
|
+
" readonly clipEntityIds: readonly string[];",
|
|
897
|
+
" readonly anchor: SequentialClipAnchor;",
|
|
898
|
+
" readonly onAnchored: 'follow' | 'keepAbsolute';",
|
|
899
|
+
"}",
|
|
900
|
+
"export interface MoveVoiceoverInput {",
|
|
901
|
+
" readonly voiceoverClipEntityId: string;",
|
|
902
|
+
" /** Absolute requested timeline start; MEngine resolves and persists the host relation. */",
|
|
903
|
+
" readonly newStartMs: number;",
|
|
904
|
+
"}",
|
|
905
|
+
"export interface PatchCaptionStyleInput {",
|
|
906
|
+
" readonly timelineEntityId: string;",
|
|
907
|
+
" readonly style: CaptionStyleFields;",
|
|
908
|
+
"}",
|
|
909
|
+
"export interface RelationFacade {",
|
|
910
|
+
" list(): SandboxRelation[];",
|
|
911
|
+
" /** Incident lookup is endpoint-agnostic; persisted endpoint positions stay unchanged. */",
|
|
912
|
+
" of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
|
|
913
|
+
" /** physical-asset binds inline Caption text, never external media. */",
|
|
914
|
+
" link(input: LinkRelationInput): string;",
|
|
915
|
+
" /** Author ordered generated(output,input); generic link() deliberately rejects this kind. */",
|
|
916
|
+
" linkGenerated(input: LinkGeneratedRelationInput): string;",
|
|
917
|
+
" /** Author ordered clip-anchor(child,host) without positional endpoint ambiguity. */",
|
|
918
|
+
" linkClipAnchor(input: LinkClipAnchorRelationInput): string;",
|
|
919
|
+
" /** Author ordered audio-script-render(output,script) without positional endpoint ambiguity. */",
|
|
920
|
+
" linkAudioScriptRender(input: LinkAudioScriptRenderRelationInput): string;",
|
|
921
|
+
" /** Remove a Relation by identity; endpoint replacement is an explicit unlink plus link. */",
|
|
922
|
+
" unlink(input: UnlinkRelationInput): void;",
|
|
923
|
+
"}",
|
|
924
|
+
"export interface ReplaceClipContentInput {",
|
|
925
|
+
" readonly clipEntityId: string;",
|
|
926
|
+
" /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */",
|
|
927
|
+
" readonly contentEntityId: string;",
|
|
928
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
929
|
+
" readonly duration: SequenceDuration<number>;",
|
|
930
|
+
" readonly targetRange?: SequenceRange<number>;",
|
|
931
|
+
" readonly timeRemapping?: JsonValue;",
|
|
932
|
+
"}",
|
|
933
|
+
"export interface ReplaceMediaClipInput {",
|
|
934
|
+
" readonly clipEntityId: string;",
|
|
935
|
+
" readonly media: VisualMediaAssetFact;",
|
|
936
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
937
|
+
"}",
|
|
938
|
+
"export interface ReplaceSequentialClipsInput {",
|
|
939
|
+
" readonly timelineEntityId: string;",
|
|
940
|
+
" readonly oldClipEntityIds: readonly string[];",
|
|
941
|
+
" readonly newClips: readonly ReplacementMediaClipInput[];",
|
|
942
|
+
" readonly onAnchored: 'remap' | 'cascade';",
|
|
943
|
+
"}",
|
|
944
|
+
"export interface ReplacementMediaClipInput {",
|
|
945
|
+
" readonly clipEntityId?: string;",
|
|
946
|
+
" readonly media: VisualMediaAssetFact;",
|
|
947
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
948
|
+
" readonly volume?: number;",
|
|
949
|
+
"}",
|
|
950
|
+
"/** Asset identity, either an old physical-only row or a directly composed media variant. */",
|
|
951
|
+
"export type ResourceEntityKind = 'image' | 'video' | 'audio' | 'voice';",
|
|
952
|
+
"export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
|
|
953
|
+
" entity_id: string;",
|
|
954
|
+
" entity_kind: K;",
|
|
955
|
+
" payload: EntityPayloadByKind[K];",
|
|
956
|
+
"}",
|
|
957
|
+
"export interface SandboxRelation {",
|
|
958
|
+
" relation_id: string;",
|
|
959
|
+
" relation_kind: KnownRelationKind;",
|
|
960
|
+
" endpoint_0_entity_id: string;",
|
|
961
|
+
" endpoint_1_entity_id: string;",
|
|
962
|
+
" metadata: JsonObject;",
|
|
963
|
+
" trace: JsonObject;",
|
|
964
|
+
"}",
|
|
965
|
+
"export type ScriptTextSegment = JsonObject & {",
|
|
966
|
+
" segmentId: string;",
|
|
967
|
+
" text: string;",
|
|
968
|
+
" language?: string;",
|
|
969
|
+
"};",
|
|
970
|
+
"export type SequenceDuration<Span = unknown> =",
|
|
971
|
+
" | {",
|
|
972
|
+
" readonly mode: 'from-source';",
|
|
973
|
+
" }",
|
|
974
|
+
" | {",
|
|
975
|
+
" readonly mode: 'fixed';",
|
|
976
|
+
" readonly value: Span;",
|
|
977
|
+
" };",
|
|
978
|
+
"export interface SequenceRange<Point = unknown> {",
|
|
979
|
+
" readonly start: Point;",
|
|
980
|
+
" readonly end: Point;",
|
|
981
|
+
"}",
|
|
982
|
+
"export type SequentialClipAnchor =",
|
|
983
|
+
" | {",
|
|
984
|
+
" readonly position: 'before' | 'after';",
|
|
985
|
+
" readonly clipEntityId: string;",
|
|
986
|
+
" }",
|
|
987
|
+
" | {",
|
|
988
|
+
" readonly position: 'trackStart';",
|
|
989
|
+
" };",
|
|
990
|
+
"export interface SetBgmInput {",
|
|
991
|
+
" readonly timelineEntityId: string;",
|
|
992
|
+
" readonly bgmClipEntityId: string;",
|
|
993
|
+
" readonly media: AudioMediaAssetFact;",
|
|
994
|
+
" readonly volume: number;",
|
|
995
|
+
"}",
|
|
996
|
+
"export interface SetCaptionVisibilityInput {",
|
|
997
|
+
" readonly timelineEntityId: string;",
|
|
998
|
+
" readonly hidden: boolean;",
|
|
999
|
+
"}",
|
|
1000
|
+
"export interface SetClipPlacementInput {",
|
|
1001
|
+
" readonly clipEntityId: string;",
|
|
1002
|
+
" readonly placement: ClipPlacement;",
|
|
1003
|
+
"}",
|
|
1004
|
+
"export interface SetClipSpeedInput {",
|
|
1005
|
+
" readonly clipEntityId: string;",
|
|
1006
|
+
" readonly timeRemapping: LinearClipSpeed | null;",
|
|
1007
|
+
"}",
|
|
1008
|
+
"export interface SetClipVolumeInput {",
|
|
1009
|
+
" readonly clipEntityId: string;",
|
|
1010
|
+
" /** Playback gain in decibels. */",
|
|
1011
|
+
" readonly volume: number;",
|
|
1012
|
+
"}",
|
|
1013
|
+
"export interface TrimClipInput {",
|
|
1014
|
+
" readonly clipEntityId: string;",
|
|
1015
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
1016
|
+
"}",
|
|
1017
|
+
"export interface UnboundedConstantSequencePayload extends JsonObject {",
|
|
1018
|
+
" extent: {",
|
|
1019
|
+
" kind: 'unbounded';",
|
|
1020
|
+
" start: number;",
|
|
1021
|
+
" };",
|
|
1022
|
+
" sampling: 'constant';",
|
|
1023
|
+
" coordinateSpace: JsonValue;",
|
|
1024
|
+
"}",
|
|
1025
|
+
"export interface UnlinkRelationInput {",
|
|
1026
|
+
" relation_id: string;",
|
|
1027
|
+
"}",
|
|
1028
|
+
"export interface UpdateClipInput {",
|
|
1029
|
+
" readonly clipEntityId: string;",
|
|
1030
|
+
" /** Complete replacement for the Clip-owned payload. */",
|
|
1031
|
+
" readonly payload: JsonObject;",
|
|
1032
|
+
"}",
|
|
1033
|
+
"export interface UpdateClipMarkerInput {",
|
|
1034
|
+
" readonly clipEntityId: string;",
|
|
1035
|
+
" readonly sourceRange?: SequenceRange<number>;",
|
|
1036
|
+
" /** Passing `undefined` explicitly removes the optional target range. */",
|
|
1037
|
+
" readonly targetRange?: SequenceRange<number> | undefined;",
|
|
1038
|
+
" readonly duration?: SequenceDuration<number>;",
|
|
1039
|
+
" /** Passing `undefined` explicitly removes the optional remapping value. */",
|
|
1040
|
+
" readonly timeRemapping?: JsonValue | undefined;",
|
|
1041
|
+
"}",
|
|
1042
|
+
"export interface UpdateEntityInput {",
|
|
1043
|
+
" entity_id: string;",
|
|
1044
|
+
" payload: JsonObject;",
|
|
1045
|
+
"}",
|
|
1046
|
+
"export interface VideoMediaAssetFact {",
|
|
1047
|
+
" readonly assetId: string;",
|
|
1048
|
+
" readonly kind: 'video';",
|
|
1049
|
+
" readonly durationMs: number;",
|
|
1050
|
+
" readonly storageKey?: string;",
|
|
1051
|
+
"}",
|
|
1052
|
+
"export type VisualMediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact;",
|
|
1053
|
+
"export interface VoiceDescriptor {",
|
|
1054
|
+
" readonly system: 'voice-library';",
|
|
1055
|
+
" readonly key: string;",
|
|
1056
|
+
" readonly name?: string;",
|
|
1057
|
+
"}",
|
|
1058
|
+
"export interface VoiceMediaAssetFact {",
|
|
1059
|
+
" /** Stable external speech result id, independent of the placed Clip id. */",
|
|
1060
|
+
" readonly assetId: string;",
|
|
1061
|
+
" readonly kind: 'voice';",
|
|
1062
|
+
" readonly durationMs: number;",
|
|
1063
|
+
" readonly storageKey: string;",
|
|
1064
|
+
" readonly voice: VoiceDescriptor;",
|
|
1065
|
+
"}",
|
|
1066
|
+
"export interface VoiceoverCaptionFact {",
|
|
1067
|
+
" /** Stable placed caption identity supplied by the materialized side effect. */",
|
|
1068
|
+
" readonly captionClipEntityId: string;",
|
|
1069
|
+
" readonly text: string;",
|
|
1070
|
+
" readonly startMs: number;",
|
|
1071
|
+
" readonly durationMs: number;",
|
|
1072
|
+
" readonly style?: CaptionStyleFields;",
|
|
1073
|
+
"}",
|
|
1074
|
+
"export interface VoiceoverTakeInput {",
|
|
1075
|
+
" readonly timelineEntityId: string;",
|
|
1076
|
+
" /** Stable placed speech identity, distinct from media.assetId. */",
|
|
1077
|
+
" readonly voiceoverClipEntityId: string;",
|
|
1078
|
+
" readonly hostClipEntityId: string;",
|
|
1079
|
+
" readonly anchorOffset: number;",
|
|
1080
|
+
" readonly media: VoiceMediaAssetFact;",
|
|
1081
|
+
" /** Complete spoken text; the editor owns the deterministic local script segment identity. */",
|
|
1082
|
+
" readonly scriptText: string;",
|
|
1083
|
+
" readonly volume: number;",
|
|
1084
|
+
" readonly captions: readonly VoiceoverCaptionFact[];",
|
|
1085
|
+
"}",
|
|
1086
|
+
"export interface VoiceoverTakeResult {",
|
|
1087
|
+
" readonly voiceoverClipEntityId: string;",
|
|
1088
|
+
" readonly voiceEntityId: string;",
|
|
1089
|
+
" readonly audioScriptEntityId: string;",
|
|
1090
|
+
" readonly captionClipEntityIds: readonly string[];",
|
|
1091
|
+
"}",
|
|
1092
|
+
"/** Timeline writes accept existing media Entity ids, never Memota asset ids or URLs. */",
|
|
1093
|
+
"export interface EditApi {",
|
|
1094
|
+
" insertClip(input: InsertClipInput): ClipEntityId;",
|
|
1095
|
+
" insertPlacedClip(input: InsertPlacedClipInput): ClipEntityId;",
|
|
1096
|
+
" updateClipMarker(input: UpdateClipMarkerInput): void;",
|
|
1097
|
+
" setClipPlacement(input: SetClipPlacementInput): void;",
|
|
1098
|
+
" moveSequentialClips(input: MoveSequentialClipsInput): void;",
|
|
1099
|
+
" moveClip(input: MoveClipInput): void;",
|
|
1100
|
+
" replaceClipContent(input: ReplaceClipContentInput): void;",
|
|
1101
|
+
" insertMediaClip(input: InsertMediaClipInput): ClipEntityId;",
|
|
1102
|
+
" insertMediaClips(input: InsertMediaClipsInput): readonly ClipEntityId[];",
|
|
1103
|
+
" replaceMediaClip(input: ReplaceMediaClipInput): void;",
|
|
1104
|
+
" setClipVolume(input: SetClipVolumeInput): void;",
|
|
1105
|
+
" setClipSpeed(input: SetClipSpeedInput): void;",
|
|
1106
|
+
" trimClip(input: TrimClipInput): void;",
|
|
1107
|
+
" replaceSequentialClips(input: ReplaceSequentialClipsInput): readonly ClipEntityId[];",
|
|
1108
|
+
" deleteClip(input: DeleteClipInput): void;",
|
|
1109
|
+
" deleteClipTree(input: DeleteClipTreeInput): void;",
|
|
1110
|
+
" updateClip(input: UpdateClipInput): void;",
|
|
1111
|
+
" upsertVoiceoverTake(input: VoiceoverTakeInput): VoiceoverTakeResult;",
|
|
1112
|
+
" moveVoiceover(input: MoveVoiceoverInput): void;",
|
|
1113
|
+
" moveClipsToStarts(input: MoveClipsToStartsInput): void;",
|
|
1114
|
+
" deleteVoiceover(input: DeleteVoiceoverInput): void;",
|
|
1115
|
+
" setBgm(input: SetBgmInput): ClipEntityId;",
|
|
1116
|
+
" deleteBgm(input: DeleteBgmInput): void;",
|
|
1117
|
+
" setCaptionVisibility(input: SetCaptionVisibilityInput): void;",
|
|
1118
|
+
" patchCaptionStyle(input: PatchCaptionStyleInput): void;",
|
|
1119
|
+
"}",
|
|
1120
|
+
"export interface TimelineApi {",
|
|
1121
|
+
" snapshot(): EntityStoreSnapshot;",
|
|
1122
|
+
"}",
|
|
1123
|
+
"export interface SandboxCheckpoint {",
|
|
1124
|
+
" readonly index: number;",
|
|
1125
|
+
"}",
|
|
1126
|
+
"export declare const edit: EditApi;",
|
|
1127
|
+
"export declare const timeline: TimelineApi;",
|
|
1128
|
+
"export declare const entities: EntityFacade;",
|
|
1129
|
+
"export declare const relations: RelationFacade;",
|
|
1130
|
+
"export declare function checkpoint(): SandboxCheckpoint;",
|
|
1131
|
+
"export declare function rollbackTo(cp: SandboxCheckpoint): void;",
|
|
1132
|
+
"export declare const inputs: Readonly<Record<string, unknown>>;",
|
|
1133
|
+
""
|
|
1134
|
+
].join("\n");
|
|
1135
|
+
//#endregion
|
|
160
1136
|
//#region src/prompt.ts
|
|
161
1137
|
const MEDEO_TOOL_DESCRIPTION = `
|
|
162
|
-
Edit
|
|
1138
|
+
Edit the authoritative Medeo Entity/Relation graph through a deterministic, side-effect-free JavaScript sandbox. Timeline objects and edit targets are Entities, not Memota assets or legacy parts.
|
|
163
1139
|
|
|
164
1140
|
Operations:
|
|
165
|
-
- snapshot: return the
|
|
166
|
-
-
|
|
167
|
-
-
|
|
1141
|
+
- snapshot: initialize missing fixed editor structure, then return the Entity/Relation state summary and opaque base version. Initialization is idempotent and may advance the entity revision once; unchanged snapshots do not write.
|
|
1142
|
+
- migrate-legacy: explicitly migrate an existing legacy timeline using recalled asset_facts. MEngine reads the canonical document and version, verifies that editing facts are preserved, and commits migration alone. Then take a fresh snapshot before any edit; never pass a caller-created legacy snapshot or version.
|
|
1143
|
+
- run-edit-script: inspect timeline.snapshot(), entities.*, and relations.*; edit.* operates existing Entity ids and creates the required Clip/SequenceMarker structural graph. Asset import, media Entity creation and timeline edits belong in ONE plan. The sandbox has no network, storage or generation access. Pass recalled asset facts through inputs; generation history is not a script input — the host program queries it itself after each commit. A successful run returns preview, logs, base revision and plan_id.
|
|
1144
|
+
- commit-plan: commit the complete Entity/Relation plan through revision CAS. The server derives the read-only timeline projection in the same transaction. There is no separate writable timeline plan and no preflight replay into a legacy editor. A failed transport is unconfirmed, never committed; retry the same plan_id.
|
|
168
1145
|
|
|
169
1146
|
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.
|
|
1147
|
+
|
|
1148
|
+
Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts, call entities.ensureMedia(fact), then pass its contentEntityId to edit.insertClip. A raw external asset id or URL is not valid contentEntityId. Image/Video/Audio/Voice are logical variants of Asset: a resource has ONE identity and ONE typed row owning both media fields and external {system,key}/storageKey, with no separate Asset row or physical-asset relation. ensureMedia returns contentEntityId and reuses that single identity by external asset id. Each placement still gets its own Clip and SequenceMarker. Generation lineage is program-synced: after each successful commit the tool connects existing typed Assets from host-recalled generation facts (endpoint 0 output, endpoint 1 input) that the host queries itself — do not pass generation history through inputs. Do not author generated Relations yourself, and never create an Entity merely to backfill or represent lineage; media variants the edit itself legitimately needs are still created normally. Text-only generation has no input and no lineage edge. relations.of(entityId) is endpoint-agnostic.
|
|
1149
|
+
`.trim();
|
|
1150
|
+
const MEDEO_TOOL_EXECUTION_RULES = `
|
|
1151
|
+
The host supplies the current document. Do not ask for, invent, or pass a document id.
|
|
1152
|
+
timeline.snapshot() returns the Entity/Relation graph with its revision, not a legacy VideoDraft. Inspect Timeline, Track, Clip, SequenceMarker and their relations to plan edits.
|
|
1153
|
+
Generation lineage is not a model input: the host program queries it via loadGenerationFacts and syncs generated Relations after each successful commit. Memota asset facts are host-provided through inputs. Never invent an asset id, Entity kind, or peer Entity id.
|
|
1154
|
+
Use entities.ensureMedia(fact) to get or create the canonical typed Asset. Native media placement helpers use the same resolver. findByAssetId includes directly composed media variants, including Voice. Every Image/Video/Audio/Voice must own its external identity; separate Asset+media graphs are invalid. A typed Asset's external identity cannot be removed or rewritten: to replace its source, ensureMedia for the new Asset and replace the Clip's content. Conflicting facts fail closed. Asset generation itself still creates no editor Entities.
|
|
1155
|
+
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.
|
|
1156
|
+
physical-asset represents inline Caption text only; Image/Video/Audio/Voice cannot use this relation. Generated lineage Relations are not model-authored: the tool derives them from host generation facts after a successful commit, so do not call relations.linkGenerated to record provenance. relations.of remains endpoint-agnostic for lookup.
|
|
1157
|
+
The compatibility reader supports the existing four Track roles: video_clip (Image/Video), speech (Voice), caption (Caption), and bgm (Audio), one of each. Clip.volume is decibels (-60 to 20, 0 = original). Marker.sourceRange is the selected source interval; Marker.duration is effective display/playback duration. Coordinates and duration are whole milliseconds for this reader, not a global DSL restriction. Placement is exactly one of Clip.order, Marker.targetRange, or clip-anchor(child,host) plus Marker.anchorOffset. Use the native move/delete/voiceover helpers so placement and cascade decisions are in the same entity plan. Reading the graph never rebinds anchors or invents empty clips. Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'} and agrees with rounded source span divided by rate. Image remains unbounded/constant; an explicit linear rate scales its display window, not an invented media extent. Nonlinear speed and multiple visual overlay tracks are unsupported.
|
|
1158
|
+
Voice links to AudioScript through audio-script-render(output,script). Caption owns text/style and is placed through a Clip anchored to the Voice Clip; caption alignment/provenance agree with that Voice/AudioScript. BGM Audio keeps factual source duration and its Marker declares durationPolicy:'timeline'. External Asset identity and storageKey are distinct from the placed Clip identity. Never introduce a speech entity kind.
|
|
1159
|
+
Create only the known entity kinds. The host initializes one Timeline and four fixed Tracks before editing; inspect and reuse their IDs from timeline.snapshot(), never create another Timeline or Track for each operation. If snapshot reports legacy migration is required, recall the listed asset facts and call migrate-legacy first. Missing facts, unsupported layouts, and version conflicts fail closed; never fall back to an old timeline method or raw update endpoint.
|
|
1160
|
+
Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
|
|
1161
|
+
`.trim();
|
|
1162
|
+
/** Render the complete MEngine-owned context injected before one model call. */
|
|
1163
|
+
function renderMedeoModelContext(input) {
|
|
1164
|
+
const updated = input.updatedSincePreviousModelCall == null ? "unknown (first model call)" : String(input.updatedSincePreviousModelCall);
|
|
1165
|
+
return `
|
|
1166
|
+
${MEDEO_TOOL_DESCRIPTION}
|
|
1167
|
+
|
|
1168
|
+
${MEDEO_TOOL_EXECUTION_RULES}
|
|
1169
|
+
|
|
1170
|
+
Current MEngine document state (sampled dynamically immediately before this model call):
|
|
1171
|
+
- document_version: ${JSON.stringify(input.documentVersion)}
|
|
1172
|
+
- updated_since_previous_model_call: ${updated}
|
|
1173
|
+
|
|
1174
|
+
When updated_since_previous_model_call is true, the document changed after the previous model call. The change may have come from this tool or another editor, so take a fresh snapshot before planning further edits.
|
|
1175
|
+
|
|
1176
|
+
Sandbox TypeScript interface:
|
|
1177
|
+
\`\`\`ts
|
|
1178
|
+
${ENTITY_EDIT_SANDBOX_API_DTS}
|
|
1179
|
+
\`\`\`
|
|
170
1180
|
`.trim();
|
|
1181
|
+
}
|
|
171
1182
|
//#endregion
|
|
172
1183
|
//#region src/schema.ts
|
|
173
1184
|
const MEDEO_TOOL_NAME = "medeo";
|
|
1185
|
+
const assetFactProperties = {
|
|
1186
|
+
assetId: {
|
|
1187
|
+
type: "string",
|
|
1188
|
+
minLength: 1
|
|
1189
|
+
},
|
|
1190
|
+
kind: {
|
|
1191
|
+
type: "string",
|
|
1192
|
+
enum: [
|
|
1193
|
+
"image",
|
|
1194
|
+
"video",
|
|
1195
|
+
"audio",
|
|
1196
|
+
"voice"
|
|
1197
|
+
]
|
|
1198
|
+
},
|
|
1199
|
+
durationMs: {
|
|
1200
|
+
type: "integer",
|
|
1201
|
+
minimum: 1
|
|
1202
|
+
},
|
|
1203
|
+
storageKey: {
|
|
1204
|
+
type: "string",
|
|
1205
|
+
minLength: 1
|
|
1206
|
+
},
|
|
1207
|
+
voice: {
|
|
1208
|
+
type: "object",
|
|
1209
|
+
additionalProperties: false,
|
|
1210
|
+
required: ["system", "key"],
|
|
1211
|
+
properties: {
|
|
1212
|
+
system: { const: "voice-library" },
|
|
1213
|
+
key: {
|
|
1214
|
+
type: "string",
|
|
1215
|
+
minLength: 1
|
|
1216
|
+
},
|
|
1217
|
+
name: { type: "string" }
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
174
1221
|
/**
|
|
175
|
-
* JSON Schema for the host-facing
|
|
1222
|
+
* JSON Schema for the host-facing `medeo` tool surface.
|
|
176
1223
|
*
|
|
177
1224
|
* The schema intentionally does not return or accept the full op journal:
|
|
178
1225
|
* journals stay in the tool process and are referenced by `plan_id`. This keeps
|
|
@@ -188,6 +1235,7 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
188
1235
|
type: "string",
|
|
189
1236
|
enum: [
|
|
190
1237
|
"snapshot",
|
|
1238
|
+
"migrate-legacy",
|
|
191
1239
|
"run-edit-script",
|
|
192
1240
|
"commit-plan"
|
|
193
1241
|
],
|
|
@@ -201,11 +1249,73 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
201
1249
|
script: {
|
|
202
1250
|
type: "string",
|
|
203
1251
|
minLength: 1,
|
|
204
|
-
description: "JavaScript body for run-edit-script.
|
|
1252
|
+
description: "JavaScript body for run-edit-script. Use edit, timeline, entities, relations, checkpoint, rollbackTo, inputs, and console. Asset import, media relations, and timeline entity edits share one entity plan."
|
|
205
1253
|
},
|
|
206
1254
|
inputs: {
|
|
207
1255
|
type: "object",
|
|
208
|
-
description: "Pre-materialized, side-effect-free values passed into the script. Generation and network IO must happen in the host before this call."
|
|
1256
|
+
description: "Pre-materialized, side-effect-free values passed into the script, including recalled asset facts. Generation history is never an input: the host queries lineage itself and syncs generated Relations after each commit. Generation and network IO must happen in the host before this call."
|
|
1257
|
+
},
|
|
1258
|
+
asset_facts: {
|
|
1259
|
+
type: "array",
|
|
1260
|
+
description: "Factual asset metadata recalled by the host for migrate-legacy only. The package reads the canonical legacy snapshot and version itself; never supply a clip trim window as media duration.",
|
|
1261
|
+
items: { oneOf: [
|
|
1262
|
+
{
|
|
1263
|
+
type: "object",
|
|
1264
|
+
additionalProperties: false,
|
|
1265
|
+
required: ["assetId", "kind"],
|
|
1266
|
+
properties: {
|
|
1267
|
+
assetId: assetFactProperties.assetId,
|
|
1268
|
+
kind: { const: "image" },
|
|
1269
|
+
storageKey: assetFactProperties.storageKey
|
|
1270
|
+
}
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
type: "object",
|
|
1274
|
+
additionalProperties: false,
|
|
1275
|
+
required: [
|
|
1276
|
+
"assetId",
|
|
1277
|
+
"kind",
|
|
1278
|
+
"durationMs"
|
|
1279
|
+
],
|
|
1280
|
+
properties: {
|
|
1281
|
+
assetId: assetFactProperties.assetId,
|
|
1282
|
+
kind: { const: "video" },
|
|
1283
|
+
durationMs: assetFactProperties.durationMs,
|
|
1284
|
+
storageKey: assetFactProperties.storageKey
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
type: "object",
|
|
1289
|
+
additionalProperties: false,
|
|
1290
|
+
required: [
|
|
1291
|
+
"assetId",
|
|
1292
|
+
"kind",
|
|
1293
|
+
"durationMs",
|
|
1294
|
+
"storageKey"
|
|
1295
|
+
],
|
|
1296
|
+
properties: {
|
|
1297
|
+
assetId: assetFactProperties.assetId,
|
|
1298
|
+
kind: { const: "audio" },
|
|
1299
|
+
durationMs: assetFactProperties.durationMs,
|
|
1300
|
+
storageKey: assetFactProperties.storageKey
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
type: "object",
|
|
1305
|
+
additionalProperties: false,
|
|
1306
|
+
required: [
|
|
1307
|
+
"assetId",
|
|
1308
|
+
"kind",
|
|
1309
|
+
"durationMs",
|
|
1310
|
+
"storageKey",
|
|
1311
|
+
"voice"
|
|
1312
|
+
],
|
|
1313
|
+
properties: {
|
|
1314
|
+
...assetFactProperties,
|
|
1315
|
+
kind: { const: "voice" }
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
] }
|
|
209
1319
|
},
|
|
210
1320
|
timeout_ms: {
|
|
211
1321
|
type: "integer",
|
|
@@ -228,11 +1338,24 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
228
1338
|
},
|
|
229
1339
|
validation: {
|
|
230
1340
|
type: "string",
|
|
231
|
-
enum: ["version"
|
|
232
|
-
description: "
|
|
1341
|
+
enum: ["version"],
|
|
1342
|
+
description: "Commit with Entity revision CAS; reject concurrent changes."
|
|
233
1343
|
}
|
|
234
1344
|
},
|
|
235
1345
|
oneOf: [
|
|
1346
|
+
{
|
|
1347
|
+
required: [
|
|
1348
|
+
"op",
|
|
1349
|
+
"doc_id",
|
|
1350
|
+
"asset_facts"
|
|
1351
|
+
],
|
|
1352
|
+
properties: {
|
|
1353
|
+
op: { const: "migrate-legacy" },
|
|
1354
|
+
doc_id: { $ref: "#/properties/doc_id" },
|
|
1355
|
+
asset_facts: { $ref: "#/properties/asset_facts" }
|
|
1356
|
+
},
|
|
1357
|
+
additionalProperties: false
|
|
1358
|
+
},
|
|
236
1359
|
{
|
|
237
1360
|
required: ["op", "doc_id"],
|
|
238
1361
|
properties: {
|
|
@@ -277,39 +1400,37 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
277
1400
|
//#endregion
|
|
278
1401
|
//#region src/session/commit-plan.ts
|
|
279
1402
|
/**
|
|
280
|
-
* Replay a sandbox journal into a
|
|
281
|
-
*
|
|
1403
|
+
* Replay a sandbox journal into a manually-synchronized document and push the
|
|
1404
|
+
* whole plan as one causally complete update.
|
|
282
1405
|
*
|
|
283
|
-
* - Default / `{ validation: 'version' }`: if
|
|
284
|
-
* `plan.base_version`, reject with zero writes.
|
|
1406
|
+
* - Default / `{ validation: 'version' }`: if the current document mark differs
|
|
1407
|
+
* from `plan.base_version`, reject with zero writes.
|
|
285
1408
|
* - `{ validation: 'preflight' }`: skip the version gate; revalidate each op
|
|
286
1409
|
* against a PlainMemoryAdapter seeded from the current live snapshot, then
|
|
287
1410
|
* replay for real. A SchemaValidator failure becomes `op_conflict` with the
|
|
288
1411
|
* failing entry's index. Journal integrity errors (unrecorded/unconsumed
|
|
289
1412
|
* ids) still propagate as throws in both modes.
|
|
290
1413
|
*/
|
|
291
|
-
async function commitPlan(
|
|
292
|
-
if (options?.validation === "preflight") return commitPlanPreflight(
|
|
293
|
-
const actual =
|
|
294
|
-
|
|
1414
|
+
async function commitPlan(doc, plan, options) {
|
|
1415
|
+
if (options?.validation === "preflight") return commitPlanPreflight(doc, plan);
|
|
1416
|
+
const actual = encodeDocVersionMark(doc.versionMark());
|
|
1417
|
+
const expected = decodeDocVersionMark(plan.base_version);
|
|
1418
|
+
if (expected == null || doc.hasChangedSince(expected)) return {
|
|
295
1419
|
kind: "rejected",
|
|
296
1420
|
reason: "version_mismatch",
|
|
297
1421
|
expected: plan.base_version,
|
|
298
1422
|
actual
|
|
299
1423
|
};
|
|
300
|
-
await replayJournal(
|
|
301
|
-
return
|
|
302
|
-
kind: "committed",
|
|
303
|
-
ops_applied: plan.ops.length
|
|
304
|
-
};
|
|
1424
|
+
await doc.replayJournal(plan.ops);
|
|
1425
|
+
return retryPlanPush(doc, plan.ops.length);
|
|
305
1426
|
}
|
|
306
1427
|
/**
|
|
307
1428
|
* Phase-2 path: scratch revalidation then real replay. Each entry is driven
|
|
308
1429
|
* through `replayJournal` alone so a ValidationError maps to a stable index;
|
|
309
1430
|
* integrity throws are not wrapped.
|
|
310
1431
|
*/
|
|
311
|
-
async function commitPlanPreflight(
|
|
312
|
-
const scratch = createPlainMemoryAdapter(
|
|
1432
|
+
async function commitPlanPreflight(doc, plan) {
|
|
1433
|
+
const scratch = createPlainMemoryAdapter(doc.snapshot());
|
|
313
1434
|
for (let index = 0; index < plan.ops.length; index++) {
|
|
314
1435
|
const entry = plan.ops[index];
|
|
315
1436
|
if (entry == null) continue;
|
|
@@ -324,15 +1445,41 @@ async function commitPlanPreflight(session, plan) {
|
|
|
324
1445
|
const entry = plan.ops[index];
|
|
325
1446
|
if (entry == null) continue;
|
|
326
1447
|
try {
|
|
327
|
-
await replayJournal(
|
|
1448
|
+
await doc.replayJournal([entry]);
|
|
328
1449
|
} catch (error) {
|
|
329
1450
|
if (error instanceof ValidationError) return opConflict(index, entry.kind, `real replay: ${error.message}`);
|
|
330
1451
|
throw error;
|
|
331
1452
|
}
|
|
332
1453
|
}
|
|
1454
|
+
return retryPlanPush(doc, plan.ops.length);
|
|
1455
|
+
}
|
|
1456
|
+
/** Push an already-replayed plan again without replaying or re-running its version gate. */
|
|
1457
|
+
async function retryPlanPush(doc, opsApplied) {
|
|
1458
|
+
const result = await doc.push();
|
|
1459
|
+
if (result.kind === "ack" || result.kind === "duplicate" || result.kind === "nothing_to_push") {
|
|
1460
|
+
const reconciled = result.collaborated ? await doc.pull() : void 0;
|
|
1461
|
+
const warnings = reconciled != null && !reconciled.ok ? [{
|
|
1462
|
+
kind: "pull_failed",
|
|
1463
|
+
message: reconciled.error.message
|
|
1464
|
+
}] : void 0;
|
|
1465
|
+
return {
|
|
1466
|
+
kind: "committed",
|
|
1467
|
+
ops_applied: opsApplied,
|
|
1468
|
+
collaborated: result.collaborated,
|
|
1469
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
if (result.kind === "rejected") return {
|
|
1473
|
+
kind: "rejected",
|
|
1474
|
+
reason: "push_rejected",
|
|
1475
|
+
...result.code !== void 0 ? { code: result.code } : {},
|
|
1476
|
+
message: result.error?.message ?? "mengine rejected the sandbox plan"
|
|
1477
|
+
};
|
|
333
1478
|
return {
|
|
334
|
-
kind: "
|
|
335
|
-
|
|
1479
|
+
kind: "unconfirmed",
|
|
1480
|
+
reason: "push_failed",
|
|
1481
|
+
ops_applied: opsApplied,
|
|
1482
|
+
message: result.error?.message ?? "mengine push failed"
|
|
336
1483
|
};
|
|
337
1484
|
}
|
|
338
1485
|
function opConflict(index, op_kind, message) {
|
|
@@ -347,6 +1494,7 @@ function opConflict(index, op_kind, message) {
|
|
|
347
1494
|
//#endregion
|
|
348
1495
|
//#region src/host-tool.ts
|
|
349
1496
|
const DEFAULT_MAX_PLANS = 16;
|
|
1497
|
+
const DEFAULT_MAX_MODEL_CONTEXTS = 128;
|
|
350
1498
|
function isRecord(value) {
|
|
351
1499
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
352
1500
|
}
|
|
@@ -359,6 +1507,112 @@ function requiredContext(value, docId, field) {
|
|
|
359
1507
|
if (resolved == null || resolved.length === 0) throw new Error(`${field} must resolve to a non-empty string for doc ${docId}`);
|
|
360
1508
|
return resolved;
|
|
361
1509
|
}
|
|
1510
|
+
function renderEntitySnapshot(state) {
|
|
1511
|
+
const rows = [...state.entities.map((entity) => JSON.stringify(entity)), ...state.relations.map((relation) => JSON.stringify(relation))];
|
|
1512
|
+
const shown = rows.slice(0, 200);
|
|
1513
|
+
return [
|
|
1514
|
+
`Entity revision=${state.revision} entities=${state.entities.length} relations=${state.relations.length}`,
|
|
1515
|
+
...shown,
|
|
1516
|
+
...shown.length < rows.length ? ["[truncated; inspect entities/relations in the sandbox]"] : []
|
|
1517
|
+
].join("\n");
|
|
1518
|
+
}
|
|
1519
|
+
function migrationNotice(document, state) {
|
|
1520
|
+
if (state.entities.some((row) => row.entity_kind === "timeline") || Object.keys(document.part_library ?? {}).length === 0) return "";
|
|
1521
|
+
const assetIds = new Set(Object.values(document.part_library ?? {}).flatMap((part) => {
|
|
1522
|
+
const id = part.video_clip?.origin_media_id ?? part.bgm?.origin_media_id;
|
|
1523
|
+
return typeof id === "string" && id !== "" ? [id] : [];
|
|
1524
|
+
}));
|
|
1525
|
+
return `\nLegacy timeline migration required. Recall factual media metadata for ${JSON.stringify([...assetIds])}, then call migrate-legacy with asset_facts. Speech facts are read from the canonical legacy document. Take a fresh snapshot after migration before editing.`;
|
|
1526
|
+
}
|
|
1527
|
+
async function commitEntityPlan(client, plan) {
|
|
1528
|
+
const rows = plan.entity_rows;
|
|
1529
|
+
if (rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1530
|
+
try {
|
|
1531
|
+
const committed = await client.commit(plan.entity_base_revision, rows, {
|
|
1532
|
+
deleted_entity_ids: plan.deleted_entity_ids ?? [],
|
|
1533
|
+
deleted_relation_ids: plan.deleted_relation_ids ?? []
|
|
1534
|
+
});
|
|
1535
|
+
return {
|
|
1536
|
+
kind: "committed",
|
|
1537
|
+
ops_applied: plan.entity_commands.length,
|
|
1538
|
+
collaborated: false,
|
|
1539
|
+
entity_revision: committed.revision
|
|
1540
|
+
};
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
if (error instanceof MengineEntityHttpRequestError) {
|
|
1543
|
+
if (error.status === 409 && isRevisionConflictPayload(error.payload)) {
|
|
1544
|
+
const actualFromPayload = revisionConflictActual(error.payload);
|
|
1545
|
+
try {
|
|
1546
|
+
const current = await client.fetchState();
|
|
1547
|
+
if (current.revision === plan.entity_base_revision + 1 && entityRowsEquivalent(current, rows)) return {
|
|
1548
|
+
kind: "committed",
|
|
1549
|
+
ops_applied: plan.entity_commands.length,
|
|
1550
|
+
collaborated: false,
|
|
1551
|
+
entity_revision: current.revision
|
|
1552
|
+
};
|
|
1553
|
+
return {
|
|
1554
|
+
kind: "rejected",
|
|
1555
|
+
reason: "entity_revision_mismatch",
|
|
1556
|
+
expected: plan.entity_base_revision,
|
|
1557
|
+
actual: current.revision
|
|
1558
|
+
};
|
|
1559
|
+
} catch {
|
|
1560
|
+
if (actualFromPayload !== void 0) return {
|
|
1561
|
+
kind: "rejected",
|
|
1562
|
+
reason: "entity_revision_mismatch",
|
|
1563
|
+
expected: plan.entity_base_revision,
|
|
1564
|
+
actual: actualFromPayload
|
|
1565
|
+
};
|
|
1566
|
+
return {
|
|
1567
|
+
kind: "unconfirmed",
|
|
1568
|
+
reason: "push_failed",
|
|
1569
|
+
ops_applied: plan.entity_commands.length,
|
|
1570
|
+
message: "entity-state conflict could not be reconciled"
|
|
1571
|
+
};
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return {
|
|
1575
|
+
kind: "rejected",
|
|
1576
|
+
reason: "entity_state_rejected",
|
|
1577
|
+
status: error.status,
|
|
1578
|
+
message: entityHttpErrorMessage(error.payload)
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
kind: "unconfirmed",
|
|
1583
|
+
reason: "push_failed",
|
|
1584
|
+
ops_applied: plan.entity_commands.length,
|
|
1585
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function revisionConflictActual(payload) {
|
|
1590
|
+
if (!isRecord(payload)) return void 0;
|
|
1591
|
+
const actual = payload.actual_revision;
|
|
1592
|
+
return typeof actual === "number" && Number.isSafeInteger(actual) && actual >= 0 ? actual : void 0;
|
|
1593
|
+
}
|
|
1594
|
+
function isRevisionConflictPayload(payload) {
|
|
1595
|
+
return isRecord(payload) && payload.code === "revision_conflict";
|
|
1596
|
+
}
|
|
1597
|
+
function entityHttpErrorMessage(payload) {
|
|
1598
|
+
if (isRecord(payload) && typeof payload.message === "string" && payload.message.length > 0) return payload.message;
|
|
1599
|
+
return typeof payload === "string" && payload.length > 0 ? payload : "mengine rejected the entity-state plan";
|
|
1600
|
+
}
|
|
1601
|
+
function commitWarnings(result) {
|
|
1602
|
+
return result.kind === "committed" && "warnings" in result && result.warnings !== void 0 ? [...result.warnings] : void 0;
|
|
1603
|
+
}
|
|
1604
|
+
function entityRowsEquivalent(left, right) {
|
|
1605
|
+
const normalize = (state) => ({
|
|
1606
|
+
entities: [...state.entities].sort((a, b) => a.entity_id.localeCompare(b.entity_id)).map((entity) => canonicalJson(entity)),
|
|
1607
|
+
relations: [...state.relations].sort((a, b) => a.relation_id.localeCompare(b.relation_id)).map((relation) => canonicalJson(relation))
|
|
1608
|
+
});
|
|
1609
|
+
return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
|
|
1610
|
+
}
|
|
1611
|
+
function canonicalJson(value) {
|
|
1612
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
1613
|
+
if (!isRecord(value)) return value;
|
|
1614
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalJson(value[key])]));
|
|
1615
|
+
}
|
|
362
1616
|
function parseInput(value) {
|
|
363
1617
|
if (!isRecord(value)) throw new Error("input must be an object");
|
|
364
1618
|
const op = value.op;
|
|
@@ -369,6 +1623,18 @@ function parseInput(value) {
|
|
|
369
1623
|
op,
|
|
370
1624
|
doc_id: docId
|
|
371
1625
|
};
|
|
1626
|
+
if (op === "migrate-legacy") {
|
|
1627
|
+
if (Object.keys(value).some((key) => ![
|
|
1628
|
+
"op",
|
|
1629
|
+
"doc_id",
|
|
1630
|
+
"asset_facts"
|
|
1631
|
+
].includes(key))) throw new Error("migrate-legacy accepts asset_facts only; the package reads the canonical document and version");
|
|
1632
|
+
return {
|
|
1633
|
+
op,
|
|
1634
|
+
doc_id: docId,
|
|
1635
|
+
asset_facts: parseMigrationAssetFacts(value.asset_facts)
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
372
1638
|
if (op === "run-edit-script") {
|
|
373
1639
|
if (typeof value.script !== "string" || value.script.length === 0) throw new Error("script must be a non-empty string");
|
|
374
1640
|
if (value.inputs !== void 0 && !isRecord(value.inputs)) throw new Error("inputs must be an object");
|
|
@@ -402,56 +1668,78 @@ function parseInput(value) {
|
|
|
402
1668
|
/**
|
|
403
1669
|
* Create the self-contained Medeo LLM tool.
|
|
404
1670
|
*
|
|
405
|
-
* The package owns
|
|
1671
|
+
* The package owns document construction, compact projection, sandbox execution,
|
|
406
1672
|
* plan caching, commit, document get-or-create, and shutdown. The host supplies
|
|
407
1673
|
* environment facts plus the authoritative legacy draft loader used only when
|
|
408
1674
|
* Mengine has no document yet.
|
|
409
1675
|
*/
|
|
410
1676
|
function createMedeoTool(options) {
|
|
411
|
-
const
|
|
1677
|
+
const documents = /* @__PURE__ */ new Map();
|
|
1678
|
+
const entityClients = /* @__PURE__ */ new Map();
|
|
1679
|
+
const documentTails = /* @__PURE__ */ new Map();
|
|
1680
|
+
const pendingPushes = /* @__PURE__ */ new Map();
|
|
412
1681
|
const plans = /* @__PURE__ */ new Map();
|
|
1682
|
+
const modelContextVersions = /* @__PURE__ */ new Map();
|
|
413
1683
|
const maxPlans = options.maxPlans ?? DEFAULT_MAX_PLANS;
|
|
1684
|
+
const maxModelContexts = options.maxModelContexts ?? DEFAULT_MAX_MODEL_CONTEXTS;
|
|
414
1685
|
let closed = false;
|
|
415
|
-
async function
|
|
1686
|
+
async function getDocument(docId) {
|
|
416
1687
|
if (closed) throw new Error("medeo tool is closed");
|
|
417
|
-
const existing =
|
|
1688
|
+
const existing = documents.get(docId);
|
|
418
1689
|
if (existing != null) return await existing;
|
|
419
1690
|
const created = (async () => {
|
|
420
|
-
|
|
1691
|
+
return await getOrCreateDocument(new MengineHttpClient({
|
|
421
1692
|
docId,
|
|
422
1693
|
httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
|
|
423
1694
|
...options.authToken !== void 0 ? { authToken: () => optionalContext(options.authToken, docId) } : {},
|
|
424
1695
|
...options.userId !== void 0 ? { userId: () => optionalContext(options.userId, docId) } : {},
|
|
425
1696
|
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
426
|
-
});
|
|
427
|
-
const peerId = optionalContext(options.peerId, docId);
|
|
428
|
-
await getOrCreateDocument(client, docId, peerId);
|
|
429
|
-
const session = new MengineDocSession({
|
|
430
|
-
docId,
|
|
431
|
-
client,
|
|
432
|
-
...peerId !== void 0 ? { peerId } : {},
|
|
433
|
-
...options.sseReconnectDelayMs !== void 0 ? { sseReconnectDelayMs: options.sseReconnectDelayMs } : {}
|
|
434
|
-
});
|
|
435
|
-
try {
|
|
436
|
-
await session.start();
|
|
437
|
-
return session;
|
|
438
|
-
} catch (error) {
|
|
439
|
-
session.destroy();
|
|
440
|
-
throw error;
|
|
441
|
-
}
|
|
1697
|
+
}), docId, optionalContext(options.peerId, docId));
|
|
442
1698
|
})();
|
|
443
|
-
|
|
1699
|
+
documents.set(docId, created);
|
|
444
1700
|
try {
|
|
445
1701
|
return await created;
|
|
446
1702
|
} catch (error) {
|
|
447
|
-
if (
|
|
1703
|
+
if (documents.get(docId) === created) documents.delete(docId);
|
|
448
1704
|
throw error;
|
|
449
1705
|
}
|
|
450
1706
|
}
|
|
1707
|
+
function getEntityClient(docId) {
|
|
1708
|
+
if (closed) throw new Error("medeo tool is closed");
|
|
1709
|
+
const existing = entityClients.get(docId);
|
|
1710
|
+
if (existing != null) return existing;
|
|
1711
|
+
const client = new EntityHttpClient({
|
|
1712
|
+
docId,
|
|
1713
|
+
httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
|
|
1714
|
+
...options.authToken !== void 0 ? { authToken: () => optionalContext(options.authToken, docId) } : {},
|
|
1715
|
+
...options.userId !== void 0 ? { userId: () => optionalContext(options.userId, docId) } : {},
|
|
1716
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
1717
|
+
});
|
|
1718
|
+
entityClients.set(docId, client);
|
|
1719
|
+
return client;
|
|
1720
|
+
}
|
|
1721
|
+
async function runExclusive(docId, use) {
|
|
1722
|
+
const previous = documentTails.get(docId) ?? Promise.resolve();
|
|
1723
|
+
let release;
|
|
1724
|
+
const gate = new Promise((resolve) => {
|
|
1725
|
+
release = resolve;
|
|
1726
|
+
});
|
|
1727
|
+
const tail = previous.catch(() => {}).then(() => gate);
|
|
1728
|
+
documentTails.set(docId, tail);
|
|
1729
|
+
await previous.catch(() => {});
|
|
1730
|
+
try {
|
|
1731
|
+
return await use(await getDocument(docId));
|
|
1732
|
+
} finally {
|
|
1733
|
+
release();
|
|
1734
|
+
if (documentTails.get(docId) === tail) documentTails.delete(docId);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
451
1737
|
async function getOrCreateDocument(client, docId, peerId) {
|
|
452
1738
|
try {
|
|
453
|
-
await
|
|
454
|
-
|
|
1739
|
+
return await ManualSyncDoc.open({
|
|
1740
|
+
client,
|
|
1741
|
+
...peerId !== void 0 ? { peerId } : {}
|
|
1742
|
+
});
|
|
455
1743
|
} catch (error) {
|
|
456
1744
|
if (!(error instanceof MengineHttpRequestError) || error.status !== 404) throw error;
|
|
457
1745
|
if (options.loadInitialDraft === void 0) throw error;
|
|
@@ -464,99 +1752,349 @@ function createMedeoTool(options) {
|
|
|
464
1752
|
await client.bootstrapSnapshot(seed.export({ mode: "snapshot" }));
|
|
465
1753
|
} catch (error) {
|
|
466
1754
|
if (!(error instanceof MengineHttpRequestError) || error.status !== 400) throw error;
|
|
467
|
-
await client.fetchSnapshot();
|
|
468
1755
|
}
|
|
1756
|
+
return await ManualSyncDoc.open({
|
|
1757
|
+
client,
|
|
1758
|
+
...peerId !== void 0 ? { peerId } : {}
|
|
1759
|
+
});
|
|
469
1760
|
}
|
|
470
|
-
function rememberPlan(docId, plan) {
|
|
1761
|
+
function rememberPlan(docId, plan, baseState) {
|
|
471
1762
|
const planId = randomUUID();
|
|
472
1763
|
plans.set(planId, {
|
|
473
1764
|
docId,
|
|
474
|
-
plan
|
|
1765
|
+
plan,
|
|
1766
|
+
...plan.plan_kind === "entities" ? { baseState: baseState && structuredClone(baseState) } : {}
|
|
475
1767
|
});
|
|
476
1768
|
while (plans.size > maxPlans) {
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
plans.
|
|
1769
|
+
const protectedPlanIds = new Set([...pendingPushes.values()].map((pending) => pending.planId));
|
|
1770
|
+
protectedPlanIds.add(planId);
|
|
1771
|
+
const oldestEvictable = [...plans.keys()].find((candidate) => !protectedPlanIds.has(candidate));
|
|
1772
|
+
if (oldestEvictable === void 0) break;
|
|
1773
|
+
plans.delete(oldestEvictable);
|
|
480
1774
|
}
|
|
481
1775
|
return planId;
|
|
482
1776
|
}
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
return {
|
|
487
|
-
ok: true,
|
|
488
|
-
op: "snapshot",
|
|
489
|
-
doc_id: input.doc_id,
|
|
490
|
-
version: session.version(),
|
|
491
|
-
preview: renderCompactProjection(document)
|
|
492
|
-
};
|
|
1777
|
+
function assertNoPendingPush(docId) {
|
|
1778
|
+
const pending = pendingPushes.get(docId);
|
|
1779
|
+
if (pending != null) throw new Error(`doc ${docId} has an unconfirmed push; retry plan_id ${pending.planId} before continuing`);
|
|
493
1780
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
1781
|
+
function recordPushResult(docId, planId, plan, result, baseState) {
|
|
1782
|
+
if (result.kind === "unconfirmed") {
|
|
1783
|
+
pendingPushes.set(docId, plan.plan_kind === "timeline" ? {
|
|
1784
|
+
kind: "timeline",
|
|
1785
|
+
planId,
|
|
1786
|
+
plan,
|
|
1787
|
+
opsApplied: result.ops_applied
|
|
1788
|
+
} : {
|
|
1789
|
+
kind: "entities",
|
|
1790
|
+
planId,
|
|
1791
|
+
plan,
|
|
1792
|
+
...baseState !== void 0 ? { baseState } : {}
|
|
1793
|
+
});
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
pendingPushes.delete(docId);
|
|
1797
|
+
if (plan.plan_kind === "timeline" && result.kind === "rejected" && result.reason === "push_rejected") documents.delete(docId);
|
|
1798
|
+
}
|
|
1799
|
+
async function fetchEntityStateForSandbox(docId, doc, pull) {
|
|
1800
|
+
const client = getEntityClient(docId);
|
|
1801
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1802
|
+
const state = await client.fetchState();
|
|
1803
|
+
if (pendingPushes.has(docId)) return state;
|
|
1804
|
+
const document = doc.snapshot();
|
|
1805
|
+
const hasTimeline = state.entities.some((row) => row.entity_kind === "timeline");
|
|
1806
|
+
const hasLegacyContent = Object.keys(document.part_library ?? {}).length > 0 || (document.tracks ?? []).some((track) => (track.items ?? []).length > 0);
|
|
1807
|
+
if (!hasTimeline && hasLegacyContent) return state;
|
|
1808
|
+
if (!hasTimeline) {
|
|
1809
|
+
if (pull.warnings !== void 0) throw new Error("Editor initialization requires a fresh canonical snapshot; retry snapshot");
|
|
1810
|
+
const baseRows = toDslRows(state);
|
|
1811
|
+
const migrated = migrateLegacyTimelineToEntities(document, [], baseRows);
|
|
1812
|
+
try {
|
|
1813
|
+
await getGraphClient(docId).commit({
|
|
1814
|
+
revision: state.revision,
|
|
1815
|
+
rows: baseRows
|
|
1816
|
+
}, migrated, { migrationBaseVv: encodeDocVersionMark(doc.versionMark()) });
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
if (!(error instanceof MengineHttpRequestError) || error.status !== 409) throw new Error(`Editor initialization was not confirmed; retry snapshot to reconcile state: ${error instanceof Error ? error.message : String(error)}`);
|
|
1819
|
+
}
|
|
1820
|
+
pull = await observePull(doc);
|
|
1821
|
+
continue;
|
|
515
1822
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
1823
|
+
const sandbox = new EntitySandbox({
|
|
1824
|
+
state,
|
|
1825
|
+
idFactory: (prefix) => `${prefix}_${randomUUID()}`
|
|
1826
|
+
});
|
|
1827
|
+
sandbox.ensureFoundation();
|
|
1828
|
+
if (sandbox.commandCount === 0) return state;
|
|
1829
|
+
if (pull.warnings !== void 0) throw new Error("Editor initialization requires a fresh canonical snapshot; retry snapshot");
|
|
1830
|
+
try {
|
|
1831
|
+
const committed = await client.commit(state.revision, sandbox.buildPlan().rows);
|
|
1832
|
+
await doc.pull();
|
|
1833
|
+
return committed;
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
if (!(error instanceof MengineEntityHttpRequestError) || error.status !== 409) throw new Error(`Editor initialization was not confirmed; retry snapshot to reconcile state: ${error instanceof Error ? error.message : String(error)}`);
|
|
1836
|
+
pull = await observePull(doc);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
throw new Error("Editor initialization conflicted repeatedly; take a fresh snapshot");
|
|
1840
|
+
}
|
|
1841
|
+
function getGraphClient(docId) {
|
|
1842
|
+
return new EntityGraphHttpClient({
|
|
1843
|
+
docId,
|
|
1844
|
+
httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
|
|
1845
|
+
...options.authToken === void 0 ? {} : { authToken: () => optionalContext(options.authToken, docId) },
|
|
1846
|
+
...options.userId === void 0 ? {} : { userId: () => optionalContext(options.userId, docId) },
|
|
1847
|
+
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
async function commitCachedPlan(docId, _doc, plan, validation, baseState) {
|
|
1851
|
+
if (plan.plan_kind === "timeline") throw new Error("Legacy timeline plans are not editable; use an Entity/Relation plan");
|
|
1852
|
+
if (validation === "preflight") throw new Error("Entity plans use revision CAS; validation=preflight is not supported");
|
|
1853
|
+
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1854
|
+
const client = getEntityClient(docId);
|
|
1855
|
+
const preCommitState = baseState ?? (options.loadGenerationFacts !== void 0 ? await client.fetchState() : void 0);
|
|
1856
|
+
return await attachGenerationSync(docId, plan, await commitEntityPlan(client, plan), preCommitState);
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* After a confirmed entity commit, connect fact-matched generated Relations
|
|
1860
|
+
* from host-recalled lineage. The commit is already durable, so a sync
|
|
1861
|
+
* failure never fails the op; it is attached to the result and surfaced as a
|
|
1862
|
+
* warning instead. The plan's diff against `baseState` scopes the sync:
|
|
1863
|
+
* newly created media Asset identities — not untouched pairs or placement-only edits.
|
|
1864
|
+
* One-sided facts are skipped silently inside the sync.
|
|
1865
|
+
*/
|
|
1866
|
+
async function attachGenerationSync(docId, plan, result, baseState) {
|
|
1867
|
+
if (result.kind !== "committed" || options.loadGenerationFacts === void 0 || baseState === void 0) return result;
|
|
1868
|
+
let outcome;
|
|
1869
|
+
try {
|
|
1870
|
+
outcome = await syncGeneratedRelations({
|
|
1871
|
+
client: getEntityClient(docId),
|
|
1872
|
+
docId,
|
|
1873
|
+
baseState,
|
|
1874
|
+
entityCommands: plan.entity_commands,
|
|
1875
|
+
loadFacts: options.loadGenerationFacts
|
|
1876
|
+
});
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
outcome = {
|
|
1879
|
+
status: "failed",
|
|
1880
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
const warnings = outcome.status === "failed" ? [{
|
|
1884
|
+
kind: "generation_sync_failed",
|
|
1885
|
+
message: outcome.message ?? "generation lineage sync failed"
|
|
1886
|
+
}] : void 0;
|
|
531
1887
|
return {
|
|
532
|
-
...
|
|
533
|
-
|
|
534
|
-
|
|
1888
|
+
...result,
|
|
1889
|
+
generation_sync: outcome,
|
|
1890
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
535
1891
|
};
|
|
536
1892
|
}
|
|
537
|
-
async function
|
|
538
|
-
const
|
|
539
|
-
if (
|
|
540
|
-
const session = await getSession(input.doc_id);
|
|
541
|
-
const commitOptions = input.validation === void 0 ? void 0 : { validation: input.validation };
|
|
542
|
-
const result = await commitPlan(session, cached.plan, commitOptions);
|
|
1893
|
+
async function observePull(doc) {
|
|
1894
|
+
const result = await doc.pull();
|
|
1895
|
+
if (result.ok) return { collaborated: result.changed };
|
|
543
1896
|
return {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
result
|
|
1897
|
+
collaborated: false,
|
|
1898
|
+
warnings: [{
|
|
1899
|
+
kind: "pull_failed",
|
|
1900
|
+
message: result.error.message
|
|
1901
|
+
}]
|
|
550
1902
|
};
|
|
551
1903
|
}
|
|
1904
|
+
function mergeWarnings(...groups) {
|
|
1905
|
+
const warnings = groups.flatMap((group) => group ?? []);
|
|
1906
|
+
return warnings.length > 0 ? warnings : void 0;
|
|
1907
|
+
}
|
|
1908
|
+
async function getModelContext(input) {
|
|
1909
|
+
const docId = input.doc_id.trim();
|
|
1910
|
+
const contextId = input.context_id.trim();
|
|
1911
|
+
if (docId.length === 0) throw new Error("doc_id must be a non-empty string");
|
|
1912
|
+
if (contextId.length === 0) throw new Error("context_id must be a non-empty string");
|
|
1913
|
+
return await runExclusive(docId, async (doc) => {
|
|
1914
|
+
const entityState = await fetchEntityStateForSandbox(docId, doc, await observePull(doc));
|
|
1915
|
+
const documentVersion = `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`;
|
|
1916
|
+
const baselineKey = `${contextId}\u0000${docId}`;
|
|
1917
|
+
const previousVersion = modelContextVersions.get(baselineKey);
|
|
1918
|
+
const updatedSincePreviousModelCall = previousVersion == null ? null : previousVersion !== documentVersion;
|
|
1919
|
+
modelContextVersions.delete(baselineKey);
|
|
1920
|
+
modelContextVersions.set(baselineKey, documentVersion);
|
|
1921
|
+
while (modelContextVersions.size > maxModelContexts) {
|
|
1922
|
+
const oldest = modelContextVersions.keys().next().value;
|
|
1923
|
+
if (oldest === void 0) break;
|
|
1924
|
+
modelContextVersions.delete(oldest);
|
|
1925
|
+
}
|
|
1926
|
+
return {
|
|
1927
|
+
prompt: renderMedeoModelContext({
|
|
1928
|
+
documentVersion,
|
|
1929
|
+
updatedSincePreviousModelCall
|
|
1930
|
+
}),
|
|
1931
|
+
document_version: documentVersion,
|
|
1932
|
+
updated_since_previous_model_call: updatedSincePreviousModelCall
|
|
1933
|
+
};
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
async function snapshot(input) {
|
|
1937
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
1938
|
+
assertNoPendingPush(input.doc_id);
|
|
1939
|
+
const pull = await observePull(doc);
|
|
1940
|
+
const entityState = await fetchEntityStateForSandbox(input.doc_id, doc, pull);
|
|
1941
|
+
return {
|
|
1942
|
+
ok: true,
|
|
1943
|
+
op: "snapshot",
|
|
1944
|
+
doc_id: input.doc_id,
|
|
1945
|
+
version: `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`,
|
|
1946
|
+
preview: renderEntitySnapshot(entityState) + migrationNotice(doc.snapshot(), entityState),
|
|
1947
|
+
collaborated: pull.collaborated,
|
|
1948
|
+
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1949
|
+
};
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
async function migrate(input) {
|
|
1953
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
1954
|
+
assertNoPendingPush(input.doc_id);
|
|
1955
|
+
const client = getGraphClient(input.doc_id);
|
|
1956
|
+
const base = await client.fetchState();
|
|
1957
|
+
if (base.rows.entities.some((row) => row.entityKind === "timeline")) return {
|
|
1958
|
+
ok: true,
|
|
1959
|
+
op: "migrate-legacy",
|
|
1960
|
+
doc_id: input.doc_id,
|
|
1961
|
+
migration_status: "already_entity",
|
|
1962
|
+
entity_revision: base.revision,
|
|
1963
|
+
next_action: "snapshot"
|
|
1964
|
+
};
|
|
1965
|
+
const pull = await doc.pull();
|
|
1966
|
+
if (!pull.ok) throw new Error(`Migration requires a fresh canonical snapshot: ${pull.error.message}`);
|
|
1967
|
+
const migrationBaseVv = encodeDocVersionMark(doc.versionMark());
|
|
1968
|
+
const nextRows = migrateLegacyTimelineToEntities(doc.snapshot(), input.asset_facts, base.rows);
|
|
1969
|
+
let revision;
|
|
1970
|
+
try {
|
|
1971
|
+
revision = (await client.commit(base, nextRows, { migrationBaseVv })).revision;
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
if (error instanceof MengineHttpRequestError) throw new Error(`Migration rejected (HTTP ${error.status}): ${entityHttpErrorMessage(error.payload)}; take a fresh snapshot before retrying`);
|
|
1974
|
+
throw new Error("Migration submission is unconfirmed; take a fresh snapshot and retry migrate-legacy to inspect whether the Entity timeline already exists");
|
|
1975
|
+
}
|
|
1976
|
+
documents.delete(input.doc_id);
|
|
1977
|
+
for (const [id, cached] of plans) if (cached.docId === input.doc_id) plans.delete(id);
|
|
1978
|
+
return {
|
|
1979
|
+
ok: true,
|
|
1980
|
+
op: "migrate-legacy",
|
|
1981
|
+
doc_id: input.doc_id,
|
|
1982
|
+
migration_status: "committed",
|
|
1983
|
+
entity_revision: revision,
|
|
1984
|
+
next_action: "snapshot"
|
|
1985
|
+
};
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
async function run(input) {
|
|
1989
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
1990
|
+
assertNoPendingPush(input.doc_id);
|
|
1991
|
+
const pull = await observePull(doc);
|
|
1992
|
+
const entityState = await fetchEntityStateForSandbox(input.doc_id, doc, pull);
|
|
1993
|
+
const document = doc.snapshot();
|
|
1994
|
+
const baseVersion = encodeDocVersionMark(doc.versionMark());
|
|
1995
|
+
const result = await runEditScript({
|
|
1996
|
+
document,
|
|
1997
|
+
baseVersion,
|
|
1998
|
+
entityState,
|
|
1999
|
+
entityOnly: true,
|
|
2000
|
+
script: input.script,
|
|
2001
|
+
...input.inputs !== void 0 ? { inputs: input.inputs } : {},
|
|
2002
|
+
timeoutMs: input.timeout_ms ?? options.sandbox?.timeoutMs,
|
|
2003
|
+
memoryLimitMb: input.memory_limit_mb ?? options.sandbox?.memoryLimitMb
|
|
2004
|
+
});
|
|
2005
|
+
if (!result.ok) return {
|
|
2006
|
+
ok: false,
|
|
2007
|
+
op: "run-edit-script",
|
|
2008
|
+
doc_id: input.doc_id,
|
|
2009
|
+
phase: result.phase,
|
|
2010
|
+
error: result.error,
|
|
2011
|
+
partial: {
|
|
2012
|
+
ops_count: result.partial.ops.length + result.partial.entityCommands.length,
|
|
2013
|
+
logs: result.partial.logs
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
const plan = {
|
|
2017
|
+
...result.plan,
|
|
2018
|
+
doc_id: input.doc_id
|
|
2019
|
+
};
|
|
2020
|
+
const planId = rememberPlan(input.doc_id, plan, entityState);
|
|
2021
|
+
const base = {
|
|
2022
|
+
ok: true,
|
|
2023
|
+
op: "run-edit-script",
|
|
2024
|
+
doc_id: input.doc_id,
|
|
2025
|
+
plan_id: planId,
|
|
2026
|
+
plan_kind: plan.plan_kind,
|
|
2027
|
+
base_version: baseVersion,
|
|
2028
|
+
entity_base_revision: plan.entity_base_revision,
|
|
2029
|
+
ops_count: plan.ops.length + plan.entity_commands.length,
|
|
2030
|
+
preview: plan.preview,
|
|
2031
|
+
logs: plan.logs,
|
|
2032
|
+
duration_ms: result.durationMs,
|
|
2033
|
+
collaborated: pull.collaborated,
|
|
2034
|
+
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
2035
|
+
};
|
|
2036
|
+
if (input.auto_commit !== true) return base;
|
|
2037
|
+
const commit = await commitCachedPlan(input.doc_id, doc, plan, void 0, entityState);
|
|
2038
|
+
recordPushResult(input.doc_id, planId, plan, commit, entityState);
|
|
2039
|
+
const warnings = mergeWarnings(pull.warnings, commitWarnings(commit));
|
|
2040
|
+
return {
|
|
2041
|
+
...base,
|
|
2042
|
+
committed: commit.kind === "committed",
|
|
2043
|
+
commit_result: commit,
|
|
2044
|
+
collaborated: pull.collaborated || commit.kind === "committed" && commit.collaborated,
|
|
2045
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
2046
|
+
};
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
async function commit(input) {
|
|
2050
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
2051
|
+
const pending = pendingPushes.get(input.doc_id);
|
|
2052
|
+
if (pending != null) {
|
|
2053
|
+
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}`);
|
|
2054
|
+
const result = pending.kind === "timeline" ? await retryPlanPush(doc, pending.opsApplied) : await commitCachedPlan(input.doc_id, doc, pending.plan, input.validation, pending.baseState);
|
|
2055
|
+
recordPushResult(input.doc_id, input.plan_id, pending.plan, result, pending.kind === "entities" ? pending.baseState : void 0);
|
|
2056
|
+
const warnings = commitWarnings(result);
|
|
2057
|
+
return {
|
|
2058
|
+
ok: true,
|
|
2059
|
+
op: "commit-plan",
|
|
2060
|
+
doc_id: input.doc_id,
|
|
2061
|
+
plan_id: input.plan_id,
|
|
2062
|
+
plan_kind: pending.plan.plan_kind,
|
|
2063
|
+
committed: result.kind === "committed",
|
|
2064
|
+
result,
|
|
2065
|
+
collaborated: result.kind === "committed" && result.collaborated,
|
|
2066
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
const cached = plans.get(input.plan_id);
|
|
2070
|
+
if (cached == null || cached.docId !== input.doc_id) throw new Error(`plan_id ${input.plan_id} is not available for doc ${input.doc_id}`);
|
|
2071
|
+
const pull = cached.plan.plan_kind === "timeline" ? await observePull(doc) : { collaborated: false };
|
|
2072
|
+
const result = await commitCachedPlan(input.doc_id, doc, cached.plan, input.validation, cached.baseState);
|
|
2073
|
+
recordPushResult(input.doc_id, input.plan_id, cached.plan, result, cached.baseState);
|
|
2074
|
+
const warnings = mergeWarnings(pull.warnings, commitWarnings(result));
|
|
2075
|
+
return {
|
|
2076
|
+
ok: true,
|
|
2077
|
+
op: "commit-plan",
|
|
2078
|
+
doc_id: input.doc_id,
|
|
2079
|
+
plan_id: input.plan_id,
|
|
2080
|
+
plan_kind: cached.plan.plan_kind,
|
|
2081
|
+
committed: result.kind === "committed",
|
|
2082
|
+
result,
|
|
2083
|
+
collaborated: pull.collaborated || result.kind === "committed" && result.collaborated,
|
|
2084
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
2085
|
+
};
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
552
2088
|
return {
|
|
553
2089
|
name: MEDEO_TOOL_NAME,
|
|
554
2090
|
description: MEDEO_TOOL_DESCRIPTION,
|
|
555
2091
|
parameters: MEDEO_TOOL_PARAMETERS,
|
|
2092
|
+
getModelContext,
|
|
556
2093
|
async handle(input) {
|
|
557
2094
|
try {
|
|
558
2095
|
const parsed = parseInput(input);
|
|
559
2096
|
if (parsed.op === "snapshot") return await snapshot(parsed);
|
|
2097
|
+
if (parsed.op === "migrate-legacy") return await migrate(parsed);
|
|
560
2098
|
if (parsed.op === "run-edit-script") return await run(parsed);
|
|
561
2099
|
return await commit(parsed);
|
|
562
2100
|
} catch (error) {
|
|
@@ -569,17 +2107,22 @@ function createMedeoTool(options) {
|
|
|
569
2107
|
},
|
|
570
2108
|
async close() {
|
|
571
2109
|
closed = true;
|
|
572
|
-
|
|
573
|
-
|
|
2110
|
+
await Promise.allSettled(documentTails.values());
|
|
2111
|
+
const opening = [...documents.values()];
|
|
2112
|
+
documents.clear();
|
|
2113
|
+
entityClients.clear();
|
|
2114
|
+
documentTails.clear();
|
|
2115
|
+
pendingPushes.clear();
|
|
574
2116
|
plans.clear();
|
|
2117
|
+
modelContextVersions.clear();
|
|
575
2118
|
const errors = [];
|
|
576
|
-
for (const
|
|
577
|
-
|
|
2119
|
+
for (const documentPromise of opening) try {
|
|
2120
|
+
await documentPromise;
|
|
578
2121
|
} catch (error) {
|
|
579
2122
|
errors.push(error);
|
|
580
2123
|
}
|
|
581
2124
|
if (errors.length === 1) throw errors[0];
|
|
582
|
-
if (errors.length > 1) throw new AggregateError(errors, "failed to close medeo tool
|
|
2125
|
+
if (errors.length > 1) throw new AggregateError(errors, "failed to close medeo tool documents");
|
|
583
2126
|
}
|
|
584
2127
|
};
|
|
585
2128
|
}
|