Capture M16 gap execution artifacts
This commit is contained in:
BIN
web/app/public/vendor/blender/web_engine.wasm
vendored
BIN
web/app/public/vendor/blender/web_engine.wasm
vendored
Binary file not shown.
BIN
web/app/src/vendor/blender/web_engine.wasm
vendored
BIN
web/app/src/vendor/blender/web_engine.wasm
vendored
Binary file not shown.
@@ -9,7 +9,7 @@ export type EditorRegionKind = "HEADER" | "MAIN" | "TOOLBAR" | "SIDEBAR" | "FOOT
|
||||
export type EditorMode = "OBJECT" | "EDIT" | "POSE";
|
||||
|
||||
export interface EditorRegionIR { id: string; kind: EditorRegionKind; visible: boolean }
|
||||
export interface EditorAreaIR { id: string; editor: EditorTypeIR; regions: EditorRegionIR[]; rect: { x: number; y: number; width: number; height: number }; maximized: boolean }
|
||||
export interface EditorAreaIR { id: string; editor: EditorTypeIR; regions: EditorRegionIR[]; rect: { x: number; y: number; width: number; height: number }; maximized: boolean; filterText?: string }
|
||||
export interface EditorWorkspaceIR { id: string; name: string; areas: EditorAreaIR[]; activeAreaId: string; revision: number }
|
||||
export interface EditorContextIR { workspaceId: string; activeAreaId: string; activeEditor: EditorTypeIR; mode: EditorMode; activeObjectId: string | null; selection: string[]; viewLayer: string; pinnedData: string | null; revision: number }
|
||||
export interface KeymapBindingIR {
|
||||
@@ -65,7 +65,9 @@ export function parseEditorWorkflow(value: unknown): EditorWorkflowIR {
|
||||
const areaId = text(areaValue.id, `${areaName}.id`); if (areaIds.has(areaId)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `Duplicate area ${areaId}`); areaIds.add(areaId);
|
||||
regionCount += areaValue.regions.length; if (regionCount > EDITOR_WORKFLOW_BUDGET.maxRegions) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_BUDGET_EXCEEDED", "Region count exceeds the budget");
|
||||
const regionIds = new Set<string>(); const regions = areaValue.regions.map((regionValue, regionIndex): EditorRegionIR => { const regionName = `${areaName}.regions[${regionIndex}]`; if (!record(regionValue) || !EDITOR_REGION_KINDS.has(regionValue.kind as EditorRegionKind)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${regionName} is invalid`); const regionId = text(regionValue.id, `${regionName}.id`); if (regionIds.has(regionId)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `Duplicate region ${regionId}`); regionIds.add(regionId); return { id: regionId, kind: regionValue.kind as EditorRegionKind, visible: regionValue.visible !== false }; });
|
||||
return { id: areaId, editor: areaValue.editor as EditorTypeIR, regions, rect: rect(areaValue.rect, `${areaName}.rect`), maximized: areaValue.maximized === true };
|
||||
const filterText = areaValue.filterText;
|
||||
if (filterText !== undefined && (typeof filterText !== "string" || filterText.length > 64)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${areaName}.filterText is invalid`);
|
||||
return { id: areaId, editor: areaValue.editor as EditorTypeIR, regions, rect: rect(areaValue.rect, `${areaName}.rect`), maximized: areaValue.maximized === true, filterText: filterText as string | undefined };
|
||||
});
|
||||
const activeAreaId = text(workspaceValue.activeAreaId, `${name}.activeAreaId`); if (!areaIds.has(activeAreaId)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name}.activeAreaId is missing`);
|
||||
for (let index = 0; index < areas.length; index++) for (let other = index + 1; other < areas.length; other++) if (!areas[index].maximized && !areas[other].maximized && overlap(areas[index].rect, areas[other].rect)) throw new EditorWorkflowValidationError("EDITOR_LAYOUT_INVALID", `${name} has overlapping areas`);
|
||||
|
||||
@@ -491,7 +491,7 @@ export interface AnimationIR {
|
||||
targetId: string;
|
||||
frameStart: number;
|
||||
frameEnd: number;
|
||||
channels: Array<{ path: string; interpolation?: "CONSTANT" | "LINEAR" | "BEZIER" | "MIXED"; keyframes: Array<{ frame: number; value: number[]; interpolation?: "CONSTANT" | "LINEAR" | "BEZIER" }> }>;
|
||||
channels: Array<{ path: string; editable?: boolean; enabled?: boolean; selected?: boolean; group?: string; expanded?: boolean; interpolation?: "CONSTANT" | "LINEAR" | "BEZIER" | "MIXED"; keyframes: Array<{ frame: number; value: number[]; interpolation?: "CONSTANT" | "LINEAR" | "BEZIER" }> }>;
|
||||
}
|
||||
|
||||
export interface CollectionIR {
|
||||
|
||||
@@ -36,6 +36,18 @@ export interface StreamConsumerOptions {
|
||||
requireCompletion?: boolean;
|
||||
isComplete?: (progress: StreamProgress) => boolean;
|
||||
onCheckpoint?: (checkpoint: AtomicCheckpoint) => Promise<void> | void;
|
||||
/**
|
||||
* Recreate the response after a transport-level read failure. The returned
|
||||
* stream must start at `progress.byteCount`; already consumed bytes are
|
||||
* retained and are never published twice.
|
||||
*/
|
||||
reconnect?: (progress: StreamProgress, error: unknown) =>
|
||||
| (ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>)
|
||||
| Promise<ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>>;
|
||||
/** Maximum number of reconnect attempts after the initial stream. */
|
||||
maxRetries?: number;
|
||||
/** Optional delay between reconnect attempts. */
|
||||
retryDelayMs?: number;
|
||||
}
|
||||
|
||||
export interface StreamConsumeResult extends StreamProgress {
|
||||
@@ -49,6 +61,16 @@ async function sha256Hex(bytes: Uint8Array): Promise<string> {
|
||||
return [...new Uint8Array(digest)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function concatParts(parts: Uint8Array[]): Uint8Array {
|
||||
const result = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0));
|
||||
let offset = 0;
|
||||
for (const part of parts) {
|
||||
result.set(part, offset);
|
||||
offset += part.byteLength;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function asStream(source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>): ReadableStream<Uint8Array> {
|
||||
if (typeof (source as ReadableStream<Uint8Array>).getReader === "function") return source as ReadableStream<Uint8Array>;
|
||||
const iterable = source as AsyncIterable<Uint8Array>;
|
||||
@@ -92,24 +114,79 @@ async function checkpoint(options: StreamConsumerOptions, state: "STREAMING" | "
|
||||
catch (error) { throw new StreamConsumeError("STREAM_CHECKPOINT_FAILED", "checkpoint publication failed", byteCount, chunkCount, { cause: error }); }
|
||||
}
|
||||
|
||||
function waitForRetry(delayMs: number, signal?: AbortSignal): Promise<boolean> {
|
||||
if (delayMs <= 0) return Promise.resolve(signal?.aborted !== true);
|
||||
return new Promise((resolve) => {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
const finish = (ready: boolean): void => {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
resolve(ready);
|
||||
};
|
||||
const onAbort = (): void => finish(false);
|
||||
timer = setTimeout(() => finish(signal?.aborted !== true), delayMs);
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
if (signal?.aborted) finish(false);
|
||||
});
|
||||
}
|
||||
|
||||
/** Consume a UTF-8 byte stream without replacement characters or partial durable output. */
|
||||
export async function consumeUtf8Stream(source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>, options: StreamConsumerOptions = {}): Promise<StreamConsumeResult> {
|
||||
const reader = asStream(source).getReader();
|
||||
const maxRetries = options.maxRetries ?? (options.reconnect ? 1 : 0);
|
||||
if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) {
|
||||
throw new TypeError("STREAM_RETRY_INVALID: maxRetries must be a non-negative safe integer");
|
||||
}
|
||||
if (options.retryDelayMs !== undefined && (!Number.isFinite(options.retryDelayMs) || options.retryDelayMs < 0)) {
|
||||
throw new TypeError("STREAM_RETRY_INVALID: retryDelayMs must be a non-negative number");
|
||||
}
|
||||
if (maxRetries && !options.reconnect) {
|
||||
throw new TypeError("STREAM_RETRY_INVALID: reconnect is required when maxRetries is non-zero");
|
||||
}
|
||||
let reader = asStream(source).getReader();
|
||||
const decoder = new TextDecoder("utf-8", { fatal: true });
|
||||
const parts: Uint8Array[] = [];
|
||||
let text = "";
|
||||
let byteCount = 0;
|
||||
let chunkCount = 0;
|
||||
let completed = false;
|
||||
let retryCount = 0;
|
||||
let readerLockHeld = true;
|
||||
const aborted = (): boolean => options.signal?.aborted === true;
|
||||
const cancel = (): void => { void reader.cancel("STREAM_CANCELLED").catch(() => undefined); };
|
||||
options.signal?.addEventListener("abort", cancel, { once: true });
|
||||
const reconnect = async (error: unknown): Promise<boolean> => {
|
||||
if (!options.reconnect || retryCount >= maxRetries || aborted()) return false;
|
||||
const progress = { byteCount, chunkCount, sha256: await sha256Hex(concatParts(parts)) };
|
||||
const previousReader = reader;
|
||||
retryCount += 1;
|
||||
await previousReader.cancel("STREAM_RECONNECT").catch(() => undefined);
|
||||
previousReader.releaseLock();
|
||||
readerLockHeld = false;
|
||||
if (!await waitForRetry(options.retryDelayMs ?? 0, options.signal)) return false;
|
||||
if (aborted()) return false;
|
||||
let replacement;
|
||||
try { replacement = await options.reconnect(progress, error); }
|
||||
catch { return false; }
|
||||
if (!replacement) return false;
|
||||
try { reader = asStream(replacement).getReader(); }
|
||||
catch { return false; }
|
||||
if (aborted()) {
|
||||
await reader.cancel("STREAM_CANCELLED").catch(() => undefined);
|
||||
reader.releaseLock();
|
||||
readerLockHeld = false;
|
||||
return false;
|
||||
}
|
||||
readerLockHeld = true;
|
||||
return true;
|
||||
};
|
||||
try {
|
||||
while (true) {
|
||||
if (aborted()) throw new StreamConsumeError("STREAM_CANCELLED", "stream consumption was cancelled", byteCount, chunkCount);
|
||||
let next: ReadableStreamReadResult<Uint8Array>;
|
||||
try { next = await reader.read(); }
|
||||
catch (error) {
|
||||
if (aborted()) throw new StreamConsumeError("STREAM_CANCELLED", "stream consumption was cancelled", byteCount, chunkCount, { cause: error });
|
||||
if (await reconnect(error)) continue;
|
||||
if (aborted()) throw new StreamConsumeError("STREAM_CANCELLED", "stream consumption was cancelled", byteCount, chunkCount, { cause: error });
|
||||
throw new StreamConsumeError("STREAM_DISCONNECTED", `stream disconnected before completion: ${errorMessage(error)}`, byteCount, chunkCount, { cause: error });
|
||||
}
|
||||
@@ -122,21 +199,21 @@ export async function consumeUtf8Stream(source: ReadableStream<Uint8Array> | Asy
|
||||
chunkCount += 1;
|
||||
try { text += decoder.decode(bytes, { stream: true }); }
|
||||
catch (error) { throw new StreamConsumeError("STREAM_UTF8_INVALID", "stream ended or yielded an invalid UTF-8 sequence", byteCount, chunkCount, { cause: error }); }
|
||||
await checkpoint(options, "STREAMING", Uint8Array.from(parts.flatMap((part) => [...part])), byteCount, chunkCount);
|
||||
await checkpoint(options, "STREAMING", concatParts(parts), byteCount, chunkCount);
|
||||
}
|
||||
try { text += decoder.decode(); }
|
||||
catch (error) { throw new StreamConsumeError("STREAM_UTF8_INVALID", "stream ended with an incomplete UTF-8 sequence", byteCount, chunkCount, { cause: error }); }
|
||||
const progress = { byteCount, chunkCount, sha256: await sha256Hex(Uint8Array.from(parts.flatMap((part) => [...part]))) };
|
||||
const progress = { byteCount, chunkCount, sha256: await sha256Hex(concatParts(parts)) };
|
||||
if ((options.requireCompletion && !options.isComplete) || (options.isComplete && !options.isComplete(progress))) {
|
||||
throw new StreamConsumeError("STREAM_DISCONNECTED", "stream disconnected before completion", byteCount, chunkCount);
|
||||
}
|
||||
await checkpoint(options, "COMPLETED", Uint8Array.from(parts.flatMap((part) => [...part])), byteCount, chunkCount);
|
||||
await checkpoint(options, "COMPLETED", concatParts(parts), byteCount, chunkCount);
|
||||
completed = true;
|
||||
return { text, ...progress };
|
||||
} catch (error) {
|
||||
if (!completed && error instanceof StreamConsumeError && options.onCheckpoint) {
|
||||
try {
|
||||
const bytes = Uint8Array.from(parts.flatMap((part) => [...part]));
|
||||
const bytes = concatParts(parts);
|
||||
await options.onCheckpoint(createAtomicCheckpoint({ state: "FAILED", byteCount, chunkCount, sha256: await sha256Hex(bytes) }));
|
||||
} catch {
|
||||
// Preserve the original transport/UTF-8 error; failed checkpoints are advisory.
|
||||
@@ -146,7 +223,7 @@ export async function consumeUtf8Stream(source: ReadableStream<Uint8Array> | Asy
|
||||
} finally {
|
||||
options.signal?.removeEventListener("abort", cancel);
|
||||
if (!completed) await reader.cancel("STREAM_CONSUMER_STOPPED").catch(() => undefined);
|
||||
reader.releaseLock();
|
||||
if (readerLockHeld) reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,3 +56,10 @@ test("governance reports a serialized context envelope that exceeds its reserved
|
||||
const result = validateContextBundle(oversized);
|
||||
assert.ok(result.violations.some(({ code }) => code === "CONTEXT_ENVELOPE_OVER_BUDGET"));
|
||||
});
|
||||
|
||||
test("explicit task handoff remains auditable after the queue advances", () => {
|
||||
const bundle = buildTaskContext("M16-GAP-00145");
|
||||
const result = validateContextBundle(bundle, { current: false });
|
||||
assert.deepEqual(result.violations, []);
|
||||
assert.equal(bundle.context.nextTask, "M16-GAP-00146");
|
||||
});
|
||||
|
||||
@@ -67,6 +67,77 @@ test("stream consumer classifies a transport/body decoding disconnect as incompl
|
||||
);
|
||||
});
|
||||
|
||||
test("stream consumer reconnects from the confirmed byte cursor after a transport disconnect", async () => {
|
||||
let reconnects = 0;
|
||||
const result = await stream.consumeUtf8Stream(
|
||||
streamOf([bytes(0x61)], new Error("Transport error: network error: error decoding response body")),
|
||||
{
|
||||
maxRetries: 1,
|
||||
reconnect: (progress, error) => {
|
||||
reconnects += 1;
|
||||
assert.equal(progress.byteCount, 1);
|
||||
assert.equal(progress.chunkCount, 1);
|
||||
assert.equal(progress.sha256, digest(Buffer.from("a")));
|
||||
assert.match(error.message, /error decoding response body/u);
|
||||
return streamOf([bytes(0x62)]);
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.equal(reconnects, 1);
|
||||
assert.equal(result.text, "ab");
|
||||
assert.equal(result.byteCount, 2);
|
||||
assert.equal(result.chunkCount, 2);
|
||||
});
|
||||
|
||||
test("stream consumer gives a reconnect callback one bounded retry by default", async () => {
|
||||
const result = await stream.consumeUtf8Stream(
|
||||
streamOf([], new Error("error decoding response body")),
|
||||
{ reconnect: () => streamOf([bytes(0x6f, 0x6b)]) },
|
||||
);
|
||||
assert.equal(result.text, "ok");
|
||||
});
|
||||
|
||||
test("stream consumer reports the final transport error when reconnect attempts are exhausted", async () => {
|
||||
let reconnects = 0;
|
||||
await assert.rejects(
|
||||
stream.consumeUtf8Stream(streamOf([], new Error("error decoding response body")), {
|
||||
maxRetries: 1,
|
||||
reconnect: () => {
|
||||
reconnects += 1;
|
||||
return streamOf([], new Error("connection reset"));
|
||||
},
|
||||
}),
|
||||
(error) => error.code === "STREAM_DISCONNECTED" && /connection reset/u.test(error.message),
|
||||
);
|
||||
assert.equal(reconnects, 1);
|
||||
});
|
||||
|
||||
test("stream consumer keeps the disconnect diagnostic when reconnect creation fails", async () => {
|
||||
await assert.rejects(
|
||||
stream.consumeUtf8Stream(streamOf([], new Error("error decoding response body")), {
|
||||
maxRetries: 1,
|
||||
reconnect: () => { throw new Error("retry unavailable"); },
|
||||
}),
|
||||
(error) => error.code === "STREAM_DISCONNECTED" && /error decoding response body/u.test(error.message),
|
||||
);
|
||||
});
|
||||
|
||||
test("stream consumer turns an abort during retry backoff into cancellation", async () => {
|
||||
const controller = new AbortController();
|
||||
let reconnects = 0;
|
||||
const promise = stream.consumeUtf8Stream(streamOf([], new Error("error decoding response body")), {
|
||||
signal: controller.signal,
|
||||
retryDelayMs: 50,
|
||||
reconnect: () => {
|
||||
reconnects += 1;
|
||||
return streamOf([bytes(0x6f, 0x6b)]);
|
||||
},
|
||||
});
|
||||
setTimeout(() => controller.abort(), 1);
|
||||
await assert.rejects(promise, (error) => error.code === "STREAM_CANCELLED");
|
||||
assert.equal(reconnects, 0);
|
||||
});
|
||||
|
||||
test("stream consumer bounds a provider error body before surfacing it", async () => {
|
||||
const detail = "x".repeat(10_000);
|
||||
await assert.rejects(
|
||||
|
||||
@@ -24,11 +24,12 @@ test("current task context is bounded and follows the parent pointer", () => {
|
||||
test("printed context reserves envelope space and omits verbose exclusion audit", () => {
|
||||
const bundle = buildTaskContext();
|
||||
const compact = compactTaskContext(bundle.context);
|
||||
const excludedReasons = {};
|
||||
for (const item of bundle.context.inputSelection.excluded) {
|
||||
excludedReasons[item.reason] = (excludedReasons[item.reason] ?? 0) + 1;
|
||||
}
|
||||
assert.equal(compact.inputSelection.excludedCount, bundle.context.inputSelection.excluded.length);
|
||||
assert.deepEqual(compact.inputSelection.excludedReasons, {
|
||||
EVIDENCE_BYTE_BUDGET: 3,
|
||||
GENERATED_EVIDENCE_OUTPUT: 3,
|
||||
});
|
||||
assert.deepEqual(compact.inputSelection.excludedReasons, excludedReasons);
|
||||
assert.ok(!Object.hasOwn(compact.inputSelection, "excluded"));
|
||||
assert.ok(contextSizeReport(bundle).serializedContextTokens <= CONTEXT_LIMITS.contextEnvelopeTokens);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user