@neta-art/cohub-cli 10.8.0 → 10.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/board-kernel.js
CHANGED
|
@@ -27527,21 +27527,9 @@ function fitTextToWidth(text, value, width) {
|
|
|
27527
27527
|
}
|
|
27528
27528
|
//#endregion
|
|
27529
27529
|
//#region ../sdk/src/board/render/text-resolution.ts
|
|
27530
|
-
const
|
|
27531
|
-
const
|
|
27532
|
-
const
|
|
27533
|
-
function getBoardResolution(size, devicePixelRatio = globalThis.devicePixelRatio) {
|
|
27534
|
-
const density = Number.isFinite(devicePixelRatio) && devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
27535
|
-
const pixels = size ? size.width * size.height : 0;
|
|
27536
|
-
const budget = pixels > 0 ? Math.sqrt(MAX_BOARD_PIXELS / pixels) : MAX_BOARD_RESOLUTION;
|
|
27537
|
-
return Math.min(density, MAX_BOARD_RESOLUTION, budget);
|
|
27538
|
-
}
|
|
27539
|
-
/**
|
|
27540
|
-
* Zoom buckets for text re-rasterisation. Pixi Text is a bitmap: at high zoom it
|
|
27541
|
-
* looks soft unless we raise its resolution. We quantise zoom into a few
|
|
27542
|
-
* buckets so small zoom changes don't thrash texture regeneration.
|
|
27543
|
-
*/
|
|
27544
|
-
const TEXT_ZOOM_BUCKETS = [
|
|
27530
|
+
const MIN_TEXT_RESOLUTION = .125;
|
|
27531
|
+
const MAX_TEXT_EDGE = 4096;
|
|
27532
|
+
const ZOOM_BUCKETS = [
|
|
27545
27533
|
.5,
|
|
27546
27534
|
1,
|
|
27547
27535
|
1.5,
|
|
@@ -27551,23 +27539,17 @@ const TEXT_ZOOM_BUCKETS = [
|
|
|
27551
27539
|
];
|
|
27552
27540
|
function textZoomBucket(zoom) {
|
|
27553
27541
|
const clamped = Math.max(.1, Math.min(4, zoom));
|
|
27554
|
-
for (const bucket of
|
|
27555
|
-
return
|
|
27542
|
+
for (const bucket of ZOOM_BUCKETS) if (clamped <= bucket) return bucket;
|
|
27543
|
+
return ZOOM_BUCKETS[ZOOM_BUCKETS.length - 1] ?? 4;
|
|
27556
27544
|
}
|
|
27557
27545
|
function boardCardDetailed(zoom) {
|
|
27558
27546
|
return textZoomBucket(zoom) >= 1;
|
|
27559
27547
|
}
|
|
27560
|
-
|
|
27561
|
-
|
|
27562
|
-
const
|
|
27563
|
-
|
|
27564
|
-
|
|
27565
|
-
/** Update a Pixi text texture only when zoom crosses a resolution bucket. */
|
|
27566
|
-
function syncTextResolution(text, state, zoom) {
|
|
27567
|
-
const resolution = textResolutionForZoom(zoom);
|
|
27568
|
-
if (resolution === state.resolution && resolution === text.resolution) return;
|
|
27569
|
-
text.resolution = resolution;
|
|
27570
|
-
state.resolution = resolution;
|
|
27548
|
+
function syncTextResolution(text, resolution) {
|
|
27549
|
+
const { width, height } = text.bounds;
|
|
27550
|
+
const limit = MAX_TEXT_EDGE / Math.max(1, Math.ceil(width + text.style.padding * 2), Math.ceil(height + text.style.padding * 2));
|
|
27551
|
+
const next = Math.min(limit, Math.max(MIN_TEXT_RESOLUTION, resolution));
|
|
27552
|
+
if (text.resolution !== next) text.resolution = next;
|
|
27571
27553
|
}
|
|
27572
27554
|
/** Defer expensive Pixi word-wrap rasterisation during a live resize. */
|
|
27573
27555
|
function syncTextWrapWidth(text, state, width, defer) {
|
|
@@ -27749,13 +27731,15 @@ function createCardTag() {
|
|
|
27749
27731
|
icon,
|
|
27750
27732
|
text: null,
|
|
27751
27733
|
sig: "",
|
|
27752
|
-
resolution: 0,
|
|
27753
27734
|
width: 0
|
|
27754
27735
|
};
|
|
27755
27736
|
}
|
|
27737
|
+
function tagScale(zoom) {
|
|
27738
|
+
return Math.min(MAX_GROWTH, Math.max(1, 1 / Math.max(zoom, .01)));
|
|
27739
|
+
}
|
|
27756
27740
|
function zoomCardTag(tag, zoom) {
|
|
27757
27741
|
if (!tag.text || !tag.sig) return;
|
|
27758
|
-
const scale =
|
|
27742
|
+
const scale = tagScale(zoom);
|
|
27759
27743
|
const span = 17 + tag.text.width;
|
|
27760
27744
|
tag.root.scale.set(scale);
|
|
27761
27745
|
tag.root.position.set(0, -18 * scale);
|
|
@@ -27769,7 +27753,6 @@ function syncCardTag(tag, input, context) {
|
|
|
27769
27753
|
return;
|
|
27770
27754
|
}
|
|
27771
27755
|
if (!tag.text) {
|
|
27772
|
-
tag.resolution = textResolutionForZoom(context.zoom);
|
|
27773
27756
|
tag.text = new Text({
|
|
27774
27757
|
text: "",
|
|
27775
27758
|
style: {
|
|
@@ -27777,30 +27760,27 @@ function syncCardTag(tag, input, context) {
|
|
|
27777
27760
|
fontSize: 11,
|
|
27778
27761
|
fontWeight: "500"
|
|
27779
27762
|
},
|
|
27780
|
-
resolution: tag.resolution,
|
|
27781
27763
|
roundPixels: true
|
|
27782
27764
|
});
|
|
27783
27765
|
tag.root.addChild(tag.text);
|
|
27784
27766
|
}
|
|
27785
|
-
syncTextResolution(tag.text, tag, context.zoom);
|
|
27786
27767
|
const sig = [
|
|
27787
27768
|
input.glyph,
|
|
27788
27769
|
input.text,
|
|
27789
27770
|
input.width,
|
|
27790
27771
|
context.themeKey
|
|
27791
27772
|
].join("|");
|
|
27792
|
-
if (sig
|
|
27793
|
-
|
|
27794
|
-
|
|
27795
|
-
|
|
27796
|
-
|
|
27797
|
-
|
|
27798
|
-
|
|
27799
|
-
|
|
27800
|
-
|
|
27801
|
-
|
|
27802
|
-
|
|
27803
|
-
tag.text.position.set(17, (ICON - tag.text.height) / 2);
|
|
27773
|
+
if (sig !== tag.sig) {
|
|
27774
|
+
tag.sig = sig;
|
|
27775
|
+
const color = context.palette.muted;
|
|
27776
|
+
const pixel = context.style.icons === "pixel";
|
|
27777
|
+
drawCardGlyph(tag.icon.clear(), input.glyph, color, pixel);
|
|
27778
|
+
tag.text.style.fill = color;
|
|
27779
|
+
tag.text.style.fontFamily = context.style.font;
|
|
27780
|
+
fitTextToWidth(tag.text, input.text, Math.max(1, input.width - ICON - GAP$2));
|
|
27781
|
+
tag.text.position.set(17, (ICON - tag.text.height) / 2);
|
|
27782
|
+
}
|
|
27783
|
+
syncTextResolution(tag.text, context.textResolution * tagScale(context.zoom));
|
|
27804
27784
|
zoomCardTag(tag, context.zoom);
|
|
27805
27785
|
}
|
|
27806
27786
|
//#endregion
|
|
@@ -28111,9 +28091,9 @@ function cardElevation(context, id) {
|
|
|
28111
28091
|
}
|
|
28112
28092
|
//#endregion
|
|
28113
28093
|
//#region ../sdk/src/board/render/renderers/base-card-renderer.ts
|
|
28114
|
-
function createLabel(text, style) {
|
|
28094
|
+
function createLabel(text, style, resolution) {
|
|
28115
28095
|
return new Text({
|
|
28116
|
-
resolution
|
|
28096
|
+
resolution,
|
|
28117
28097
|
roundPixels: true,
|
|
28118
28098
|
text,
|
|
28119
28099
|
style: {
|
|
@@ -28270,12 +28250,15 @@ function audioKeyCenter(width, height) {
|
|
|
28270
28250
|
function sync$13(container, item, context) {
|
|
28271
28251
|
const parts = partsByContainer$13.get(container);
|
|
28272
28252
|
if (!parts) return;
|
|
28253
|
+
syncContent$1(parts, item, context);
|
|
28254
|
+
syncTextResolution(parts.title, context.textResolution);
|
|
28255
|
+
}
|
|
28256
|
+
function syncContent$1(parts, item, context) {
|
|
28273
28257
|
positionShell(parts.root, item);
|
|
28274
28258
|
const { width, height } = item.frame;
|
|
28275
28259
|
const title = item.props.snapshot?.title ?? getMediaResourceTitle(item.props.src);
|
|
28276
28260
|
const durationMs = item.props.snapshot?.durationMs;
|
|
28277
28261
|
const detailed = boardCardDetailed(context.zoom);
|
|
28278
|
-
syncTextResolution(parts.title, parts, context.zoom);
|
|
28279
28262
|
const elevation = cardElevation(context, item.id);
|
|
28280
28263
|
const sig = [
|
|
28281
28264
|
width,
|
|
@@ -28328,14 +28311,13 @@ const audioCardRenderer = {
|
|
|
28328
28311
|
const root = new Container();
|
|
28329
28312
|
const plate = new Graphics();
|
|
28330
28313
|
const wave = new Graphics();
|
|
28331
|
-
const resolution = textResolutionForZoom(context.zoom);
|
|
28332
28314
|
const title = new Text({
|
|
28333
28315
|
text: "",
|
|
28334
28316
|
style: {
|
|
28335
28317
|
fontSize: 13,
|
|
28336
28318
|
fontWeight: "600"
|
|
28337
28319
|
},
|
|
28338
|
-
resolution,
|
|
28320
|
+
resolution: context.textResolution,
|
|
28339
28321
|
roundPixels: true
|
|
28340
28322
|
});
|
|
28341
28323
|
root.addChild(plate, wave, title);
|
|
@@ -28344,8 +28326,7 @@ const audioCardRenderer = {
|
|
|
28344
28326
|
plate,
|
|
28345
28327
|
wave,
|
|
28346
28328
|
title,
|
|
28347
|
-
sig: ""
|
|
28348
|
-
resolution
|
|
28329
|
+
sig: ""
|
|
28349
28330
|
});
|
|
28350
28331
|
if (item.type === "audio") sync$13(root, item, context);
|
|
28351
28332
|
return root;
|
|
@@ -28506,7 +28487,6 @@ function sync$11(container, item, context) {
|
|
|
28506
28487
|
parts.line.alpha = itemAlpha(context, style.stroke);
|
|
28507
28488
|
const color = itemColor(context, style.stroke, "brand");
|
|
28508
28489
|
const size = style.strokeWidth ?? 2.5;
|
|
28509
|
-
syncTextResolution(parts.label, parts, context.zoom);
|
|
28510
28490
|
const resolved = resolveSceneArrow(item, context.scene);
|
|
28511
28491
|
const path = trimPolyline(resolved.path, style.trim);
|
|
28512
28492
|
const lineSig = [
|
|
@@ -28573,6 +28553,7 @@ function sync$11(container, item, context) {
|
|
|
28573
28553
|
border: context.palette.border
|
|
28574
28554
|
});
|
|
28575
28555
|
}
|
|
28556
|
+
syncTextResolution(parts.label, context.textResolution);
|
|
28576
28557
|
const visible = props.label.length > 0 && (style.trim === void 0 || style.trim >= .5);
|
|
28577
28558
|
parts.label.visible = visible;
|
|
28578
28559
|
parts.backdrop.visible = visible;
|
|
@@ -28586,7 +28567,6 @@ const arrowCardRenderer = {
|
|
|
28586
28567
|
create: (item, context) => {
|
|
28587
28568
|
const root = new Container();
|
|
28588
28569
|
const line = new Graphics();
|
|
28589
|
-
const resolution = textResolutionForZoom(context.zoom);
|
|
28590
28570
|
const label = new Text({
|
|
28591
28571
|
text: "",
|
|
28592
28572
|
style: {
|
|
@@ -28595,7 +28575,7 @@ const arrowCardRenderer = {
|
|
|
28595
28575
|
fontSize: 14,
|
|
28596
28576
|
fontWeight: "500"
|
|
28597
28577
|
},
|
|
28598
|
-
resolution,
|
|
28578
|
+
resolution: context.textResolution,
|
|
28599
28579
|
roundPixels: true
|
|
28600
28580
|
});
|
|
28601
28581
|
label.anchor.set(.5);
|
|
@@ -28607,8 +28587,7 @@ const arrowCardRenderer = {
|
|
|
28607
28587
|
backdrop,
|
|
28608
28588
|
label,
|
|
28609
28589
|
lineSig: "",
|
|
28610
|
-
labelSig: ""
|
|
28611
|
-
resolution
|
|
28590
|
+
labelSig: ""
|
|
28612
28591
|
});
|
|
28613
28592
|
if (item.type === "arrow") sync$11(root, item, context);
|
|
28614
28593
|
return root;
|
|
@@ -28618,7 +28597,7 @@ const arrowCardRenderer = {
|
|
|
28618
28597
|
},
|
|
28619
28598
|
resolution: (container, _item, context) => {
|
|
28620
28599
|
const parts = partsByContainer$11.get(container);
|
|
28621
|
-
if (parts) syncTextResolution(parts.label,
|
|
28600
|
+
if (parts) syncTextResolution(parts.label, context.textResolution);
|
|
28622
28601
|
},
|
|
28623
28602
|
renderFar: (graphics, item, context) => {
|
|
28624
28603
|
if (item.type !== "arrow") return;
|
|
@@ -29003,6 +28982,14 @@ function syncClip(parts, rect, style) {
|
|
|
29003
28982
|
function sync$8(container, item, context) {
|
|
29004
28983
|
const parts = partsByContainer$8.get(container);
|
|
29005
28984
|
if (!parts) return;
|
|
28985
|
+
syncContent(parts, item, context);
|
|
28986
|
+
for (const text of [
|
|
28987
|
+
parts.title,
|
|
28988
|
+
parts.excerpt,
|
|
28989
|
+
parts.typeMark
|
|
28990
|
+
]) syncTextResolution(text, context.textResolution);
|
|
28991
|
+
}
|
|
28992
|
+
function syncContent(parts, item, context) {
|
|
29006
28993
|
positionShell(parts.root, item);
|
|
29007
28994
|
const { width, height } = item.frame;
|
|
29008
28995
|
const detail = detailFor(context.zoom);
|
|
@@ -29020,9 +29007,6 @@ function sync$8(container, item, context) {
|
|
|
29020
29007
|
const kind = filePreviewKind(item.props.snapshot);
|
|
29021
29008
|
const category = fileCategory(item.props.src, item.props.snapshot?.mimeType);
|
|
29022
29009
|
const accent = fileCategoryAccent(category, context.palette);
|
|
29023
|
-
syncTextResolution(parts.title, parts.titleRes, context.zoom);
|
|
29024
|
-
syncTextResolution(parts.excerpt, parts.excerptRes, context.zoom);
|
|
29025
|
-
syncTextResolution(parts.typeMark, parts.typeMarkRes, context.zoom);
|
|
29026
29010
|
const visualSig = [
|
|
29027
29011
|
key ?? "",
|
|
29028
29012
|
width,
|
|
@@ -29146,7 +29130,7 @@ const fileCardRenderer = {
|
|
|
29146
29130
|
const cover = new Sprite(Texture.EMPTY);
|
|
29147
29131
|
const coverMask = new Graphics();
|
|
29148
29132
|
cover.mask = coverMask;
|
|
29149
|
-
const resolution =
|
|
29133
|
+
const resolution = context.textResolution;
|
|
29150
29134
|
const title = new Text({
|
|
29151
29135
|
text: "",
|
|
29152
29136
|
style: {
|
|
@@ -29203,10 +29187,7 @@ const fileCardRenderer = {
|
|
|
29203
29187
|
title,
|
|
29204
29188
|
excerpt,
|
|
29205
29189
|
visualSig: "",
|
|
29206
|
-
textSig: ""
|
|
29207
|
-
titleRes: { resolution },
|
|
29208
|
-
excerptRes: { resolution },
|
|
29209
|
-
typeMarkRes: { resolution }
|
|
29190
|
+
textSig: ""
|
|
29210
29191
|
});
|
|
29211
29192
|
if (item.type === "file") sync$8(root, item, context);
|
|
29212
29193
|
return root;
|
|
@@ -29244,7 +29225,6 @@ function sync$7(container, item, context) {
|
|
|
29244
29225
|
if (!parts) return;
|
|
29245
29226
|
const { frame, style, props, locked } = item;
|
|
29246
29227
|
positionShell(parts.root, item);
|
|
29247
|
-
syncTextResolution(parts.label, parts, context.zoom);
|
|
29248
29228
|
const fill = itemColor(context, style.fill ?? style.stroke, "neutral", "fill");
|
|
29249
29229
|
const fillAlpha = (style.fillOpacity ?? (style.fill ? 1 : DEFAULT_WASH)) * itemAlpha(context, style.fill ?? style.stroke);
|
|
29250
29230
|
const baseStroke = itemColor(context, style.stroke, "neutral");
|
|
@@ -29300,6 +29280,7 @@ function sync$7(container, item, context) {
|
|
|
29300
29280
|
parts.label.style.fill = labelColor;
|
|
29301
29281
|
parts.label.style.fontFamily = context.style.font;
|
|
29302
29282
|
}
|
|
29283
|
+
syncTextResolution(parts.label, context.textResolution);
|
|
29303
29284
|
parts.label.position.set(4, -18);
|
|
29304
29285
|
}
|
|
29305
29286
|
const frameCardRenderer = {
|
|
@@ -29313,15 +29294,14 @@ const frameCardRenderer = {
|
|
|
29313
29294
|
fontFamily: BOARD_FONT_STACK,
|
|
29314
29295
|
fontSize: 12,
|
|
29315
29296
|
fontWeight: "600"
|
|
29316
|
-
});
|
|
29297
|
+
}, context.textResolution);
|
|
29317
29298
|
root.addChild(box, label);
|
|
29318
29299
|
partsByContainer$7.set(root, {
|
|
29319
29300
|
root,
|
|
29320
29301
|
box,
|
|
29321
29302
|
label,
|
|
29322
29303
|
visualSig: "",
|
|
29323
|
-
textSig: ""
|
|
29324
|
-
resolution: label.resolution
|
|
29304
|
+
textSig: ""
|
|
29325
29305
|
});
|
|
29326
29306
|
if (item.type === "frame") sync$7(root, item, context);
|
|
29327
29307
|
return root;
|
|
@@ -29331,7 +29311,7 @@ const frameCardRenderer = {
|
|
|
29331
29311
|
},
|
|
29332
29312
|
resolution: (container, _item, context) => {
|
|
29333
29313
|
const parts = partsByContainer$7.get(container);
|
|
29334
|
-
if (parts) syncTextResolution(parts.label,
|
|
29314
|
+
if (parts) syncTextResolution(parts.label, context.textResolution);
|
|
29335
29315
|
},
|
|
29336
29316
|
renderFar: (graphics, item, context) => {
|
|
29337
29317
|
if (item.type !== "frame" || item.props.decoration === "none") return;
|
|
@@ -29440,7 +29420,6 @@ function sync$6(container, item, context) {
|
|
|
29440
29420
|
const fillOpacity = (style.fillOpacity ?? (style.fill ? 1 : 0)) * itemAlpha(context, style.fill ?? style.stroke);
|
|
29441
29421
|
const strokeAlpha = itemAlpha(context, style.stroke ?? style.fill);
|
|
29442
29422
|
const strokeWidth = style.strokeWidth ?? 1.75;
|
|
29443
|
-
syncTextResolution(parts.label, parts, context.zoom);
|
|
29444
29423
|
const visualSig = [
|
|
29445
29424
|
width,
|
|
29446
29425
|
height,
|
|
@@ -29499,6 +29478,7 @@ function sync$6(container, item, context) {
|
|
|
29499
29478
|
parts.label.style.align = props.align;
|
|
29500
29479
|
}
|
|
29501
29480
|
syncTextWrapWidth(parts.label, parts, Math.max(1, width - 16), context.resizingIds.has(item.id));
|
|
29481
|
+
syncTextResolution(parts.label, context.textResolution);
|
|
29502
29482
|
parts.label.position.set(width / 2, height / 2);
|
|
29503
29483
|
}
|
|
29504
29484
|
const shapeCardRenderer = {
|
|
@@ -29507,7 +29487,6 @@ const shapeCardRenderer = {
|
|
|
29507
29487
|
create: (item, context) => {
|
|
29508
29488
|
const root = new Container();
|
|
29509
29489
|
const shape = new Graphics();
|
|
29510
|
-
const resolution = textResolutionForZoom(context.zoom);
|
|
29511
29490
|
const label = new Text({
|
|
29512
29491
|
text: "",
|
|
29513
29492
|
style: {
|
|
@@ -29519,7 +29498,7 @@ const shapeCardRenderer = {
|
|
|
29519
29498
|
wordWrap: true,
|
|
29520
29499
|
lineHeight: 28
|
|
29521
29500
|
},
|
|
29522
|
-
resolution,
|
|
29501
|
+
resolution: context.textResolution,
|
|
29523
29502
|
roundPixels: true
|
|
29524
29503
|
});
|
|
29525
29504
|
label.anchor.set(.5);
|
|
@@ -29530,8 +29509,7 @@ const shapeCardRenderer = {
|
|
|
29530
29509
|
label,
|
|
29531
29510
|
visualSig: "",
|
|
29532
29511
|
textSig: "",
|
|
29533
|
-
wrapWidth: 0
|
|
29534
|
-
resolution
|
|
29512
|
+
wrapWidth: 0
|
|
29535
29513
|
});
|
|
29536
29514
|
if (item.type === "shape") sync$6(root, item, context);
|
|
29537
29515
|
return root;
|
|
@@ -29541,7 +29519,7 @@ const shapeCardRenderer = {
|
|
|
29541
29519
|
},
|
|
29542
29520
|
resolution: (container, _item, context) => {
|
|
29543
29521
|
const parts = partsByContainer$6.get(container);
|
|
29544
|
-
if (parts) syncTextResolution(parts.label,
|
|
29522
|
+
if (parts) syncTextResolution(parts.label, context.textResolution);
|
|
29545
29523
|
},
|
|
29546
29524
|
renderFar: (graphics, item, context) => {
|
|
29547
29525
|
if (item.type !== "shape") return;
|
|
@@ -29568,7 +29546,6 @@ function sync$5(container, item, context) {
|
|
|
29568
29546
|
parts.id = item.id;
|
|
29569
29547
|
}
|
|
29570
29548
|
positionShell(parts.root, item);
|
|
29571
|
-
syncTextResolution(parts.label, parts.label, context.zoom);
|
|
29572
29549
|
const { width, height } = item.frame;
|
|
29573
29550
|
const texture = context.scripts?.frame(item, context.time) ?? null;
|
|
29574
29551
|
const error = context.scripts?.error(item.id) ?? null;
|
|
@@ -29586,19 +29563,21 @@ function sync$5(container, item, context) {
|
|
|
29586
29563
|
message,
|
|
29587
29564
|
context.themeKey
|
|
29588
29565
|
].join("|");
|
|
29589
|
-
if (sig
|
|
29590
|
-
|
|
29591
|
-
|
|
29592
|
-
|
|
29593
|
-
|
|
29594
|
-
|
|
29595
|
-
|
|
29596
|
-
|
|
29597
|
-
|
|
29598
|
-
|
|
29599
|
-
|
|
29600
|
-
|
|
29601
|
-
|
|
29566
|
+
if (sig !== parts.sig) {
|
|
29567
|
+
parts.sig = sig;
|
|
29568
|
+
parts.placeholder.clear();
|
|
29569
|
+
if (!ready || error) parts.placeholder.rect(0, 0, width, height).stroke({
|
|
29570
|
+
color: context.palette.border,
|
|
29571
|
+
width: 1,
|
|
29572
|
+
alpha: .8
|
|
29573
|
+
});
|
|
29574
|
+
parts.label.visible = !ready || Boolean(error);
|
|
29575
|
+
parts.label.text = message;
|
|
29576
|
+
parts.label.style.fill = error ? context.colors.rose.stroke : context.palette.muted;
|
|
29577
|
+
parts.label.style.wordWrapWidth = Math.max(40, width - 16);
|
|
29578
|
+
parts.label.position.set(width / 2, height / 2);
|
|
29579
|
+
}
|
|
29580
|
+
syncTextResolution(parts.label, context.textResolution);
|
|
29602
29581
|
}
|
|
29603
29582
|
const customCardRenderer = {
|
|
29604
29583
|
id: "custom-card",
|
|
@@ -29617,7 +29596,8 @@ const customCardRenderer = {
|
|
|
29617
29596
|
wordWrap: true,
|
|
29618
29597
|
breakWords: true
|
|
29619
29598
|
},
|
|
29620
|
-
resolution:
|
|
29599
|
+
resolution: context.textResolution,
|
|
29600
|
+
roundPixels: true
|
|
29621
29601
|
});
|
|
29622
29602
|
label.anchor.set(.5);
|
|
29623
29603
|
root.addChild(sprite, placeholder, label);
|
|
@@ -29637,7 +29617,7 @@ const customCardRenderer = {
|
|
|
29637
29617
|
},
|
|
29638
29618
|
resolution: (container, _item, context) => {
|
|
29639
29619
|
const parts = partsByContainer$5.get(container);
|
|
29640
|
-
if (parts) syncTextResolution(parts.label,
|
|
29620
|
+
if (parts) syncTextResolution(parts.label, context.textResolution);
|
|
29641
29621
|
},
|
|
29642
29622
|
renderFar: (graphics, item, context) => {
|
|
29643
29623
|
drawFarPlate(graphics, item.frame, {
|
|
@@ -30047,10 +30027,6 @@ function sync$3(container, item, context) {
|
|
|
30047
30027
|
const detailText = !full || !surface ? "" : status === "failed" ? snapshot.errorExcerpt ?? "" : working ? snapshot.promptExcerpt ?? "" : "";
|
|
30048
30028
|
const textArtifact = kind === "text" && artifact?.type === "text";
|
|
30049
30029
|
const bodyText = textArtifact ? artifact.textExcerpt : counting ? `${label} · ${elapsed}` : label;
|
|
30050
|
-
syncTextResolution(parts.body, parts, context.zoom);
|
|
30051
|
-
syncTextResolution(parts.detail, parts, context.zoom);
|
|
30052
|
-
syncTextResolution(parts.meta, parts, context.zoom);
|
|
30053
|
-
syncTextResolution(parts.badge, parts, context.zoom);
|
|
30054
30030
|
const textSig = [
|
|
30055
30031
|
counting ? label : bodyText,
|
|
30056
30032
|
detailText,
|
|
@@ -30081,6 +30057,12 @@ function sync$3(container, item, context) {
|
|
|
30081
30057
|
parts.badge.style.fill = context.palette.text;
|
|
30082
30058
|
if (parts.badge.text !== count) parts.badge.text = count;
|
|
30083
30059
|
}
|
|
30060
|
+
for (const text of [
|
|
30061
|
+
parts.body,
|
|
30062
|
+
parts.detail,
|
|
30063
|
+
parts.meta,
|
|
30064
|
+
parts.badge
|
|
30065
|
+
]) syncTextResolution(text, context.textResolution);
|
|
30084
30066
|
const badgeWidth = Math.max(22, parts.badge.width + 12);
|
|
30085
30067
|
const visualSig = [
|
|
30086
30068
|
width,
|
|
@@ -30198,7 +30180,6 @@ const taskCardRenderer = {
|
|
|
30198
30180
|
const previewMask = new Graphics();
|
|
30199
30181
|
const shimmer = new Graphics();
|
|
30200
30182
|
const previewArt = new Graphics();
|
|
30201
|
-
const resolution = textResolutionForZoom(context.zoom);
|
|
30202
30183
|
const text = (fontFamily, fontSize, fontWeight) => new Text({
|
|
30203
30184
|
text: "",
|
|
30204
30185
|
style: {
|
|
@@ -30208,7 +30189,7 @@ const taskCardRenderer = {
|
|
|
30208
30189
|
wordWrap: true,
|
|
30209
30190
|
breakWords: true
|
|
30210
30191
|
},
|
|
30211
|
-
resolution,
|
|
30192
|
+
resolution: context.textResolution,
|
|
30212
30193
|
roundPixels: true
|
|
30213
30194
|
});
|
|
30214
30195
|
const body = text(BOARD_FONT_STACK, 11.5, "500");
|
|
@@ -30242,7 +30223,6 @@ const taskCardRenderer = {
|
|
|
30242
30223
|
assetKey: null,
|
|
30243
30224
|
visualSig: "",
|
|
30244
30225
|
textSig: "",
|
|
30245
|
-
resolution,
|
|
30246
30226
|
motion: {
|
|
30247
30227
|
itemId: "",
|
|
30248
30228
|
width: 0,
|
|
@@ -30319,7 +30299,6 @@ function sync$2(container, item, context) {
|
|
|
30319
30299
|
const anchorX = props.align === "center" ? .5 : props.align === "right" ? 1 : 0;
|
|
30320
30300
|
parts.body.anchor.set(anchorX, 0);
|
|
30321
30301
|
parts.body.position.set(item.frame.width * anchorX, 0);
|
|
30322
|
-
syncTextResolution(parts.body, parts, context.zoom * Math.max(1, previewScale));
|
|
30323
30302
|
parts.body.alpha = itemAlpha(context, item.style.fill);
|
|
30324
30303
|
parts.caret.alpha = parts.body.alpha;
|
|
30325
30304
|
const ink = itemColor(context, item.style.fill, "neutral");
|
|
@@ -30349,6 +30328,7 @@ function sync$2(container, item, context) {
|
|
|
30349
30328
|
breakWords: true
|
|
30350
30329
|
});
|
|
30351
30330
|
}
|
|
30331
|
+
syncTextResolution(parts.body, context.textResolution * previewScale);
|
|
30352
30332
|
syncCaret(parts, item, context);
|
|
30353
30333
|
}
|
|
30354
30334
|
const textCardRenderer = {
|
|
@@ -30356,11 +30336,10 @@ const textCardRenderer = {
|
|
|
30356
30336
|
canRender: (item) => item.type === "text",
|
|
30357
30337
|
create: (item, context) => {
|
|
30358
30338
|
const root = new Container();
|
|
30359
|
-
const resolution = textResolutionForZoom(context.zoom);
|
|
30360
30339
|
const body = new Text({
|
|
30361
30340
|
text: "",
|
|
30362
30341
|
style: { fontFamily: BOARD_FONT_STACKS.sans },
|
|
30363
|
-
resolution,
|
|
30342
|
+
resolution: context.textResolution,
|
|
30364
30343
|
roundPixels: true
|
|
30365
30344
|
});
|
|
30366
30345
|
const caret = new Graphics();
|
|
@@ -30369,7 +30348,6 @@ const textCardRenderer = {
|
|
|
30369
30348
|
root,
|
|
30370
30349
|
body,
|
|
30371
30350
|
caret,
|
|
30372
|
-
resolution,
|
|
30373
30351
|
contentSig: ""
|
|
30374
30352
|
});
|
|
30375
30353
|
if (item.type === "text") sync$2(root, item, context);
|
|
@@ -30380,7 +30358,7 @@ const textCardRenderer = {
|
|
|
30380
30358
|
},
|
|
30381
30359
|
resolution: (container, _item, context) => {
|
|
30382
30360
|
const parts = partsByContainer$2.get(container);
|
|
30383
|
-
if (parts) syncTextResolution(parts.body,
|
|
30361
|
+
if (parts) syncTextResolution(parts.body, context.textResolution * parts.body.scale.x);
|
|
30384
30362
|
},
|
|
30385
30363
|
renderFar: (graphics, item, context) => {
|
|
30386
30364
|
if (item.type !== "text") return;
|
|
@@ -30403,7 +30381,6 @@ function sync$1(container, item, context) {
|
|
|
30403
30381
|
positionShell(parts.root, item);
|
|
30404
30382
|
const { width, height } = item.frame;
|
|
30405
30383
|
const realType = item.type;
|
|
30406
|
-
syncTextResolution(parts.label, parts, context.zoom);
|
|
30407
30384
|
const elevation = cardElevation(context, item.id);
|
|
30408
30385
|
const visualSig = [
|
|
30409
30386
|
width,
|
|
@@ -30438,6 +30415,7 @@ function sync$1(container, item, context) {
|
|
|
30438
30415
|
parts.label.text = realType;
|
|
30439
30416
|
parts.label.style.fill = context.palette.muted;
|
|
30440
30417
|
}
|
|
30418
|
+
syncTextResolution(parts.label, context.textResolution);
|
|
30441
30419
|
parts.label.position.set(width / 2, height / 2);
|
|
30442
30420
|
}
|
|
30443
30421
|
const unknownCardRenderer = {
|
|
@@ -30447,7 +30425,6 @@ const unknownCardRenderer = {
|
|
|
30447
30425
|
const root = new Container();
|
|
30448
30426
|
const box = new Graphics();
|
|
30449
30427
|
box.context.batchMode = "batch";
|
|
30450
|
-
const resolution = textResolutionForZoom(context.zoom);
|
|
30451
30428
|
const label = new Text({
|
|
30452
30429
|
text: "",
|
|
30453
30430
|
style: {
|
|
@@ -30455,7 +30432,7 @@ const unknownCardRenderer = {
|
|
|
30455
30432
|
fontSize: 11,
|
|
30456
30433
|
fontWeight: "600"
|
|
30457
30434
|
},
|
|
30458
|
-
resolution,
|
|
30435
|
+
resolution: context.textResolution,
|
|
30459
30436
|
roundPixels: true
|
|
30460
30437
|
});
|
|
30461
30438
|
label.anchor.set(.5);
|
|
@@ -30465,8 +30442,7 @@ const unknownCardRenderer = {
|
|
|
30465
30442
|
box,
|
|
30466
30443
|
label,
|
|
30467
30444
|
visualSig: "",
|
|
30468
|
-
textSig: ""
|
|
30469
|
-
resolution
|
|
30445
|
+
textSig: ""
|
|
30470
30446
|
});
|
|
30471
30447
|
sync$1(root, item, context);
|
|
30472
30448
|
return root;
|
|
@@ -30476,7 +30452,7 @@ const unknownCardRenderer = {
|
|
|
30476
30452
|
},
|
|
30477
30453
|
resolution: (container, _item, context) => {
|
|
30478
30454
|
const parts = partsByContainer$1.get(container);
|
|
30479
|
-
if (parts) syncTextResolution(parts.label,
|
|
30455
|
+
if (parts) syncTextResolution(parts.label, context.textResolution);
|
|
30480
30456
|
},
|
|
30481
30457
|
renderFar: (graphics, item, context) => {
|
|
30482
30458
|
drawFarPlate(graphics, item.frame, {
|
|
@@ -31642,6 +31618,7 @@ function buildContext(input) {
|
|
|
31642
31618
|
themeKey: "export",
|
|
31643
31619
|
rendererType: "canvas",
|
|
31644
31620
|
zoom: input.scale,
|
|
31621
|
+
textResolution: input.scale,
|
|
31645
31622
|
reducedMotion: true,
|
|
31646
31623
|
labels: DEFAULT_BOARD_RENDER_LABELS,
|
|
31647
31624
|
taskRun: () => null,
|
|
@@ -4,6 +4,7 @@ import { RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, runtimeCommandSchema
|
|
|
4
4
|
import { describeError } from "../network-error.js";
|
|
5
5
|
import { executeTurn } from "./native/execution.js";
|
|
6
6
|
import { ContextRequiredError } from "./native/results.js";
|
|
7
|
+
import { SizeLimitError } from "./native/transcript.js";
|
|
7
8
|
import { ProcessCleanupUncertainError } from "./process-group.js";
|
|
8
9
|
import { serializeDiagnosticError, } from "./diagnostics.js";
|
|
9
10
|
export async function serveRuntime(options) {
|
|
@@ -11,6 +12,11 @@ export async function serveRuntime(options) {
|
|
|
11
12
|
let backoff = 500;
|
|
12
13
|
let attempt = 0;
|
|
13
14
|
let conflictSince = null;
|
|
15
|
+
// Executions outlive a single transport so a dropped socket reconnects immediately
|
|
16
|
+
// instead of waiting for in-flight turns to finish.
|
|
17
|
+
const executions = new Map();
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const announce = { current: null };
|
|
14
20
|
const uploads = new AbortController();
|
|
15
21
|
const uploadSignal = AbortSignal.any([options.signal, uploads.signal]);
|
|
16
22
|
const log = (level, event, data, context) => options.diagnostics?.log(level, event, data, context);
|
|
@@ -33,12 +39,16 @@ export async function serveRuntime(options) {
|
|
|
33
39
|
while (!options.signal.aborted) {
|
|
34
40
|
attempt += 1;
|
|
35
41
|
let readyAt = 0;
|
|
42
|
+
const connectStartedAt = Date.now();
|
|
36
43
|
let outcome;
|
|
37
44
|
try {
|
|
38
45
|
outcome = await connect({
|
|
39
46
|
...options,
|
|
40
47
|
runtimeId,
|
|
41
48
|
attempt,
|
|
49
|
+
executions,
|
|
50
|
+
seen,
|
|
51
|
+
announce,
|
|
42
52
|
onReady: () => {
|
|
43
53
|
readyAt = Date.now();
|
|
44
54
|
conflictSince = null;
|
|
@@ -72,18 +82,40 @@ export async function serveRuntime(options) {
|
|
|
72
82
|
attempt: attempt || 1,
|
|
73
83
|
delayMs: backoff,
|
|
74
84
|
outcome: outcome.kind,
|
|
85
|
+
connectedMs: Date.now() - connectStartedAt,
|
|
86
|
+
readyMs: readyAt ? Date.now() - readyAt : null,
|
|
87
|
+
activeExecutions: executions.size,
|
|
75
88
|
});
|
|
76
89
|
await delay(backoff / 2 + Math.random() * backoff / 2, undefined, { signal: options.signal }).catch(() => undefined);
|
|
77
90
|
backoff = Math.min(30_000, backoff * 2);
|
|
78
91
|
}
|
|
79
92
|
}
|
|
80
93
|
finally {
|
|
94
|
+
for (const execution of executions.values())
|
|
95
|
+
execution.controller.abort();
|
|
96
|
+
await Promise.allSettled([...executions.values()].map((entry) => entry.promise));
|
|
81
97
|
clearInterval(timer);
|
|
82
98
|
uploads.abort();
|
|
83
99
|
await flush();
|
|
84
100
|
log("info", "runtime.stopped", { runtimeId });
|
|
85
101
|
}
|
|
86
102
|
}
|
|
103
|
+
function frameIdentity(frame) {
|
|
104
|
+
const value = frame;
|
|
105
|
+
const event = value?.event;
|
|
106
|
+
const pick = (input) => typeof input === "string" ? input : undefined;
|
|
107
|
+
return {
|
|
108
|
+
frameType: pick(value?.type),
|
|
109
|
+
requestId: pick(value?.requestId),
|
|
110
|
+
eventType: pick(event?.type),
|
|
111
|
+
sessionId: pick(event?.sessionId) ?? pick(event?.input?.sessionId),
|
|
112
|
+
turnId: pick(event?.turnId) ?? pick(event?.input?.turnId),
|
|
113
|
+
harness: pick(event?.input?.harness),
|
|
114
|
+
nativeSessionId: pick(event?.input?.nativeSessionId),
|
|
115
|
+
turnCount: Array.isArray(event?.input?.turns) ? event.input.turns.length : undefined,
|
|
116
|
+
turnIds: Array.isArray(event?.input?.turns) ? event.input.turns.slice(0, 10).map((turn) => pick(turn?.turnId)).filter(Boolean) : undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
87
119
|
function executionContext(connectionId, input) {
|
|
88
120
|
return {
|
|
89
121
|
connectionId,
|
|
@@ -112,11 +144,13 @@ async function connect(options) {
|
|
|
112
144
|
const runtimeUrl = new URL(options.url);
|
|
113
145
|
runtimeUrl.searchParams.set("runtimeId", options.runtimeId);
|
|
114
146
|
const socket = new WebSocket(runtimeUrl.toString());
|
|
115
|
-
const active =
|
|
116
|
-
const seen =
|
|
147
|
+
const active = options.executions;
|
|
148
|
+
const seen = options.seen;
|
|
117
149
|
const disconnected = new AbortController();
|
|
118
150
|
const contexts = new Map();
|
|
119
151
|
let lastHeartbeat = Date.now();
|
|
152
|
+
let openedAt = 0;
|
|
153
|
+
let readyAt = 0;
|
|
120
154
|
let connectionId = null;
|
|
121
155
|
let fatal = false;
|
|
122
156
|
let fatalCode = 0;
|
|
@@ -137,8 +171,10 @@ async function connect(options) {
|
|
|
137
171
|
const data = JSON.stringify(frame);
|
|
138
172
|
const bytes = Buffer.byteLength(data);
|
|
139
173
|
if (bytes > RUNTIME_MAX_FRAME_BYTES) {
|
|
140
|
-
|
|
141
|
-
|
|
174
|
+
const identity = frameIdentity(frame);
|
|
175
|
+
const execution = identity.requestId ? active.get(identity.requestId) : undefined;
|
|
176
|
+
log("error", "runtime.frame_too_large", { bytes, maxBytes: RUNTIME_MAX_FRAME_BYTES, ...identity }, execution ? executionContext(connectionId, execution) : { connectionId });
|
|
177
|
+
throw new SizeLimitError("Runtime frame exceeds transfer limit");
|
|
142
178
|
}
|
|
143
179
|
socket.send(data);
|
|
144
180
|
};
|
|
@@ -173,8 +209,13 @@ async function connect(options) {
|
|
|
173
209
|
log("error", "runtime.heartbeat_timeout", {
|
|
174
210
|
ageMs: heartbeatAgeMs,
|
|
175
211
|
activeExecutions: active.size,
|
|
212
|
+
readyState: socket.readyState,
|
|
213
|
+
bufferedBytes: socket.bufferedAmount,
|
|
176
214
|
}, { connectionId });
|
|
177
215
|
closeTransport();
|
|
216
|
+
// A half-open TCP path may never deliver the close event; stop waiting so reconnect can start.
|
|
217
|
+
if (heartbeatAgeMs > 45_000)
|
|
218
|
+
handleClose({ code: 1006, reason: "Runtime heartbeat timed out", wasClean: false }, true);
|
|
178
219
|
}
|
|
179
220
|
else if (connectionId) {
|
|
180
221
|
try {
|
|
@@ -206,52 +247,108 @@ async function connect(options) {
|
|
|
206
247
|
});
|
|
207
248
|
}
|
|
208
249
|
}, 10_000);
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
250
|
+
const announceHere = (execution) => {
|
|
251
|
+
try {
|
|
252
|
+
send({ type: "runtime.recovery", executions: [execution] });
|
|
253
|
+
log("info", "runtime.detached_result_announced", undefined, executionContext(connectionId, execution));
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
// The next ready connection reports every pending receipt.
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
/** Replays a saved result, or answers explicitly that none is available; never leaves the server waiting. */
|
|
260
|
+
const replaySaved = async (requestId, input, context, executionRequestId) => {
|
|
261
|
+
let saved = null;
|
|
262
|
+
try {
|
|
263
|
+
saved = await options.executor.results.recover(input, executionRequestId);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
log("error", "runtime.recovery_failed", { error: serializeDiagnosticError(error) }, context);
|
|
267
|
+
}
|
|
268
|
+
if (saved)
|
|
269
|
+
for (const event of saved.events)
|
|
270
|
+
send({ type: "runtime.event", requestId, event });
|
|
271
|
+
else
|
|
272
|
+
send({ type: "runtime.event", requestId, event: { type: "turn.error", uncertain: true, message: "Result unavailable; files retained" } });
|
|
273
|
+
};
|
|
274
|
+
let closeHandled = false;
|
|
275
|
+
let resolveClosed = () => { };
|
|
276
|
+
const closed = new Promise((resolve) => { resolveClosed = resolve; });
|
|
277
|
+
const handleClose = (event, forced = false) => {
|
|
278
|
+
if (closeHandled)
|
|
279
|
+
return;
|
|
280
|
+
closeHandled = true;
|
|
281
|
+
unauthorized = event.code === 4401;
|
|
282
|
+
fatal = [4400, 4403].includes(event.code);
|
|
283
|
+
if (fatal) {
|
|
284
|
+
fatalCode = event.code;
|
|
285
|
+
fatalReason = event.reason;
|
|
286
|
+
}
|
|
287
|
+
conflict = event.code === 4409;
|
|
288
|
+
disconnected.abort();
|
|
289
|
+
const now = Date.now();
|
|
290
|
+
log(options.signal.aborted ? "debug" : fatal || conflict ? "error" : "warn", fatal ? "runtime.websocket.rejected" : "runtime.websocket.closed", {
|
|
291
|
+
code: event.code,
|
|
292
|
+
reason: event.reason,
|
|
293
|
+
wasClean: event.wasClean,
|
|
294
|
+
forced,
|
|
295
|
+
durationMs: now - connectedAt,
|
|
296
|
+
openMs: openedAt ? now - openedAt : null,
|
|
297
|
+
readyMs: readyAt ? now - readyAt : null,
|
|
298
|
+
lastInboundAgeMs: now - lastHeartbeat,
|
|
299
|
+
bufferedBytes: socket.bufferedAmount,
|
|
300
|
+
activeExecutions: active.size,
|
|
301
|
+
fatal,
|
|
302
|
+
conflict,
|
|
303
|
+
}, { connectionId });
|
|
304
|
+
nativeChannelReady = false;
|
|
305
|
+
for (const pending of nativePending.values()) {
|
|
306
|
+
clearTimeout(pending.timer);
|
|
307
|
+
pending.reject(new Error("Runtime native channel disconnected"));
|
|
308
|
+
}
|
|
309
|
+
nativePending.clear();
|
|
310
|
+
options.onDisconnected?.();
|
|
311
|
+
const executionMustStop = options.signal.aborted || fatal || conflict;
|
|
312
|
+
if (options.announce.current === announceHere)
|
|
313
|
+
options.announce.current = null;
|
|
314
|
+
for (const [requestId, execution] of active) {
|
|
315
|
+
if (executionMustStop) {
|
|
316
|
+
log("error", "runtime.execution_transport_invalidated", { code: event.code, reason: event.reason }, executionContext(connectionId, execution));
|
|
317
|
+
execution.controller.abort();
|
|
216
318
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
code: event.code,
|
|
221
|
-
reason: event.reason,
|
|
222
|
-
durationMs: Date.now() - connectedAt,
|
|
223
|
-
activeExecutions: active.size,
|
|
224
|
-
fatal,
|
|
225
|
-
conflict,
|
|
226
|
-
}, { connectionId });
|
|
227
|
-
nativeChannelReady = false;
|
|
228
|
-
for (const pending of nativePending.values()) {
|
|
229
|
-
clearTimeout(pending.timer);
|
|
230
|
-
pending.reject(new Error("Runtime native channel disconnected"));
|
|
319
|
+
else if (execution.recovery) {
|
|
320
|
+
// The server re-asks on the next connection; a stale waiter must not hold a slot or its request id.
|
|
321
|
+
execution.controller.abort();
|
|
231
322
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
execution.controller.abort();
|
|
323
|
+
else if (!execution.detached) {
|
|
324
|
+
execution.detached = true;
|
|
325
|
+
log("warn", "runtime.execution_transport_detached", { code: event.code, reason: event.reason }, executionContext(connectionId, execution));
|
|
326
|
+
// The server may have stopped asking by the time this finishes; a saved result is announced again.
|
|
327
|
+
void execution.promise.finally(() => {
|
|
328
|
+
if (!execution.result || active.get(requestId) !== execution)
|
|
329
|
+
return;
|
|
330
|
+
options.announce.current?.({ sessionId: execution.sessionId, turnId: execution.turnId, harness: execution.harness });
|
|
331
|
+
});
|
|
242
332
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
333
|
+
}
|
|
334
|
+
resolveClosed();
|
|
335
|
+
};
|
|
336
|
+
socket.addEventListener("close", (event) => handleClose(event), { once: true });
|
|
246
337
|
socket.addEventListener("error", (event) => {
|
|
247
|
-
const detail = event
|
|
338
|
+
const detail = event;
|
|
248
339
|
log("warn", "runtime.websocket.error", {
|
|
249
340
|
type: event.type,
|
|
250
|
-
|
|
341
|
+
message: typeof detail.message === "string" && detail.message ? detail.message : undefined,
|
|
342
|
+
readyState: socket.readyState,
|
|
343
|
+
opened: openedAt > 0,
|
|
344
|
+
ready: readyAt > 0,
|
|
345
|
+
lastInboundAgeMs: Date.now() - lastHeartbeat,
|
|
346
|
+
error: serializeDiagnosticError(detail.error ?? new Error("WebSocket error")),
|
|
251
347
|
}, { connectionId });
|
|
252
348
|
socket.close();
|
|
253
349
|
});
|
|
254
350
|
socket.addEventListener("open", () => {
|
|
351
|
+
openedAt = Date.now();
|
|
255
352
|
try {
|
|
256
353
|
log("info", "runtime.websocket.open", {
|
|
257
354
|
attempt: options.attempt,
|
|
@@ -289,14 +386,17 @@ async function connect(options) {
|
|
|
289
386
|
if (connectionId)
|
|
290
387
|
throw new Error("Runtime connection identity changed");
|
|
291
388
|
connectionId = frame.connectionId;
|
|
389
|
+
readyAt = Date.now();
|
|
292
390
|
clearTimeout(readyTimer);
|
|
293
391
|
log("info", "runtime.ready", {
|
|
294
392
|
connectionId,
|
|
295
|
-
durationMs:
|
|
393
|
+
durationMs: readyAt - connectedAt,
|
|
394
|
+
detachedExecutions: [...active.values()].filter((entry) => entry.detached).length,
|
|
296
395
|
}, { connectionId });
|
|
297
396
|
nativeChannelReady = true;
|
|
298
397
|
options.onNativeChannel?.(sendNative);
|
|
299
398
|
options.onReady();
|
|
399
|
+
options.announce.current = announceHere;
|
|
300
400
|
let recoveryBatch = 0;
|
|
301
401
|
for await (const executions of options.executor.results.pendingBatches()) {
|
|
302
402
|
recoveryBatch += 1;
|
|
@@ -391,10 +491,7 @@ async function connect(options) {
|
|
|
391
491
|
if (previous.sessionId !== identity.sessionId || previous.turnId !== identity.turnId || previous.harness !== identity.harness)
|
|
392
492
|
throw new Error("Runtime recovery identity changed");
|
|
393
493
|
await previous.promise;
|
|
394
|
-
|
|
395
|
-
if (saved)
|
|
396
|
-
for (const event of saved.events)
|
|
397
|
-
send({ type: "runtime.event", requestId: frame.requestId, event });
|
|
494
|
+
await replaySaved(frame.requestId, identity, context);
|
|
398
495
|
return;
|
|
399
496
|
}
|
|
400
497
|
const running = [...active].find(([, entry]) => entry.turnId === identity.turnId && entry.sessionId === identity.sessionId && entry.harness === identity.harness);
|
|
@@ -404,14 +501,26 @@ async function connect(options) {
|
|
|
404
501
|
traceContext: frame.traceContext,
|
|
405
502
|
controller: new AbortController(),
|
|
406
503
|
promise: Promise.resolve(),
|
|
504
|
+
recovery: true,
|
|
407
505
|
};
|
|
408
506
|
active.set(frame.requestId, recovery);
|
|
409
507
|
recovery.promise = (async () => {
|
|
410
508
|
try {
|
|
411
509
|
if (running) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
510
|
+
// A detached execution was not superseded by this host; wait for its result instead of discarding it.
|
|
511
|
+
if (running[1].detached) {
|
|
512
|
+
log("info", "runtime.recovery_waiting_detached", { runningRequestId: running[0] }, context);
|
|
513
|
+
// A recovery the server gave up on must not hold an execution slot until the Turn ends.
|
|
514
|
+
const stopped = recovery.controller.signal;
|
|
515
|
+
await Promise.race([running[1].promise, new Promise((resolve) => stopped.addEventListener("abort", () => resolve(), { once: true }))]);
|
|
516
|
+
stopped.throwIfAborted();
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
running[1].controller.abort();
|
|
520
|
+
await running[1].promise;
|
|
521
|
+
}
|
|
522
|
+
if (active.get(running[0]) === running[1])
|
|
523
|
+
active.delete(running[0]);
|
|
415
524
|
}
|
|
416
525
|
const saved = await options.executor.results.recover(identity);
|
|
417
526
|
recovery.controller.signal.throwIfAborted();
|
|
@@ -433,7 +542,8 @@ async function connect(options) {
|
|
|
433
542
|
// The next connection can read the same result.
|
|
434
543
|
}
|
|
435
544
|
}
|
|
436
|
-
active.
|
|
545
|
+
if (active.get(frame.requestId) === recovery)
|
|
546
|
+
active.delete(frame.requestId);
|
|
437
547
|
}
|
|
438
548
|
})();
|
|
439
549
|
return;
|
|
@@ -449,11 +559,9 @@ async function connect(options) {
|
|
|
449
559
|
if (previous) {
|
|
450
560
|
if (previous.sessionId !== frame.input.sessionId || previous.turnId !== frame.input.turnId || previous.harness !== frame.input.harness)
|
|
451
561
|
throw new Error("Runtime execution identity changed");
|
|
562
|
+
log("info", "runtime.execution_resumed", { detached: previous.detached === true, settled: Boolean(previous.result) }, executionContext(connectionId, frame.input));
|
|
452
563
|
await previous.promise;
|
|
453
|
-
|
|
454
|
-
if (saved)
|
|
455
|
-
for (const event of saved.events)
|
|
456
|
-
send({ type: "runtime.event", requestId: frame.requestId, event });
|
|
564
|
+
await replaySaved(frame.requestId, frame.input, context, frame.requestId);
|
|
457
565
|
return;
|
|
458
566
|
}
|
|
459
567
|
const requestContext = async (executionSignal) => {
|
|
@@ -601,7 +709,6 @@ async function connect(options) {
|
|
|
601
709
|
shutdown();
|
|
602
710
|
try {
|
|
603
711
|
await closed;
|
|
604
|
-
await Promise.allSettled([...active.values()].map((entry) => entry.promise));
|
|
605
712
|
}
|
|
606
713
|
finally {
|
|
607
714
|
clearTimeout(readyTimer);
|
|
@@ -118,6 +118,8 @@ export declare class NativeIngest {
|
|
|
118
118
|
private readonly reads;
|
|
119
119
|
private readonly timers;
|
|
120
120
|
private readonly failures;
|
|
121
|
+
/** File versions that hit a size limit; they are not read again until the file changes. */
|
|
122
|
+
private readonly oversized;
|
|
121
123
|
private readonly aliveAt;
|
|
122
124
|
private readonly runningPaths;
|
|
123
125
|
/** Files a live Cohub execution is writing in a format that cannot mark its Turn. */
|
|
@@ -210,14 +212,19 @@ export declare class NativeIngest {
|
|
|
210
212
|
};
|
|
211
213
|
transcriptFor(sessionId: string, harness: Harness, headTurnId: string | null): TranscriptState | null;
|
|
212
214
|
scan(): Promise<void>;
|
|
213
|
-
|
|
215
|
+
/** `resume` is automatic resumption: it skips file versions that already hit a size limit, which only an explicit import retries. */
|
|
216
|
+
plan(filter?: ImportFilter, signal?: AbortSignal, options?: {
|
|
217
|
+
resume?: boolean;
|
|
218
|
+
}): Promise<Array<NativeCandidate & {
|
|
214
219
|
nativeSessionId?: string;
|
|
215
220
|
}>>;
|
|
216
221
|
/**
|
|
217
222
|
* Start or resume a backfill. What was imported is tracked, so resuming simply plans again; the
|
|
218
223
|
* server recognizes any Turn it already has, so an interrupted batch is never duplicated.
|
|
219
224
|
*/
|
|
220
|
-
startImport(filter?: ImportFilter, concurrency?: number
|
|
225
|
+
startImport(filter?: ImportFilter, concurrency?: number, options?: {
|
|
226
|
+
resume?: boolean;
|
|
227
|
+
}): Promise<ImportJob>;
|
|
221
228
|
pauseImport(): ImportJob;
|
|
222
229
|
watch(signal: AbortSignal): Promise<void>;
|
|
223
230
|
private watchHarness;
|
|
@@ -231,6 +238,7 @@ export declare class NativeIngest {
|
|
|
231
238
|
private report;
|
|
232
239
|
private sync;
|
|
233
240
|
private read;
|
|
241
|
+
private readVersion;
|
|
234
242
|
private crossesBoundary;
|
|
235
243
|
private blank;
|
|
236
244
|
private harnessFor;
|
|
@@ -2,6 +2,7 @@ import { appendFile, mkdir, open, readFile, readdir, rename, stat } from "node:f
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { NATIVE_INGEST_MAX_TURNS, SETTLED_TURN_STATUSES } from "@neta-art/cohub";
|
|
4
4
|
import { acceptsDirectory, acceptsTranscript, readSessionHeader, readTranscript, transcriptRoots } from "./adapters.js";
|
|
5
|
+
import { SizeLimitError } from "./transcript.js";
|
|
5
6
|
import { nativeTurnId } from "./identity.js";
|
|
6
7
|
import { harnessAdapter, transcriptFormat } from "../harnesses/index.js";
|
|
7
8
|
import { containsNativePath } from "./paths.js";
|
|
@@ -27,6 +28,7 @@ const RETRY_MAX_MS = 5 * 60_000;
|
|
|
27
28
|
*/
|
|
28
29
|
const INGEST_BATCH_TURNS = Math.min(50, NATIVE_INGEST_MAX_TURNS);
|
|
29
30
|
const IMPORT_FAILURES_KEPT = 100;
|
|
31
|
+
const fileVersion = (info) => `${info.ino}:${info.size}:${info.mtimeMs}`;
|
|
30
32
|
/** Headers are read this many at a time; each costs one small read. */
|
|
31
33
|
const HEADER_CONCURRENCY = 16;
|
|
32
34
|
/** Whether a session in `cwd` belongs to the directory bound at `root`. */
|
|
@@ -105,6 +107,8 @@ export class NativeIngest {
|
|
|
105
107
|
reads = new Set();
|
|
106
108
|
timers = new Map();
|
|
107
109
|
failures = new Map();
|
|
110
|
+
/** File versions that hit a size limit; they are not read again until the file changes. */
|
|
111
|
+
oversized = new Map();
|
|
108
112
|
aliveAt = new Map();
|
|
109
113
|
runningPaths = new Set();
|
|
110
114
|
/** Files a live Cohub execution is writing in a format that cannot mark its Turn. */
|
|
@@ -320,21 +324,23 @@ export class NativeIngest {
|
|
|
320
324
|
}
|
|
321
325
|
this.drainLive();
|
|
322
326
|
if (this.job.state === "running")
|
|
323
|
-
await this.startImport(this.job.filter, this.job.concurrency);
|
|
327
|
+
await this.startImport(this.job.filter, this.job.concurrency, { resume: true });
|
|
324
328
|
}
|
|
325
|
-
|
|
329
|
+
/** `resume` is automatic resumption: it skips file versions that already hit a size limit, which only an explicit import retries. */
|
|
330
|
+
async plan(filter = {}, signal, options = {}) {
|
|
326
331
|
const harnesses = this.harnesses.filter((harness) => !filter.harnesses?.length || filter.harnesses.includes(harness));
|
|
327
332
|
const candidates = await discoverTranscripts(this.options.cwd, harnesses, { headers: true, nested: this.nested, signal });
|
|
328
333
|
// A transcript whose first read failed is still untracked (size -1), so a later import retries it.
|
|
329
334
|
return candidates.filter((candidate) => (this.states.get(candidate.path)?.size ?? -1) < 0 && !this.live.has(candidate.path)
|
|
335
|
+
&& !(options.resume && this.oversized.get(candidate.path) === fileVersion(candidate))
|
|
330
336
|
&& (!filter.nativeSessionId || candidate.nativeSessionId === filter.nativeSessionId));
|
|
331
337
|
}
|
|
332
338
|
/**
|
|
333
339
|
* Start or resume a backfill. What was imported is tracked, so resuming simply plans again; the
|
|
334
340
|
* server recognizes any Turn it already has, so an interrupted batch is never duplicated.
|
|
335
341
|
*/
|
|
336
|
-
async startImport(filter = {}, concurrency = DEFAULT_IMPORT_CONCURRENCY) {
|
|
337
|
-
const queue = await this.plan(filter);
|
|
342
|
+
async startImport(filter = {}, concurrency = DEFAULT_IMPORT_CONCURRENCY, options = {}) {
|
|
343
|
+
const queue = await this.plan(filter, undefined, options);
|
|
338
344
|
const resuming = this.job.state === "running" || this.job.state === "paused";
|
|
339
345
|
const same = resuming && JSON.stringify(this.job.filter) === JSON.stringify(filter);
|
|
340
346
|
const base = same ? this.job : { ...this.job, done: 0, turns: 0, bytes: 0, failed: [], startedAt: new Date().toISOString() };
|
|
@@ -450,7 +456,7 @@ export class NativeIngest {
|
|
|
450
456
|
}
|
|
451
457
|
retryLater(path, error) {
|
|
452
458
|
this.report(path, error);
|
|
453
|
-
if (this.stopped || !this.transport)
|
|
459
|
+
if (this.stopped || !this.transport || error instanceof SizeLimitError)
|
|
454
460
|
return;
|
|
455
461
|
const attempts = (this.failures.get(path) ?? 0) + 1;
|
|
456
462
|
if (attempts > RETRY_ATTEMPTS) {
|
|
@@ -508,6 +514,20 @@ export class NativeIngest {
|
|
|
508
514
|
return null; throw error; });
|
|
509
515
|
if (!info?.isFile())
|
|
510
516
|
return;
|
|
517
|
+
const version = fileVersion(info);
|
|
518
|
+
if (lane === "live" && this.oversized.get(path) === version)
|
|
519
|
+
return;
|
|
520
|
+
this.oversized.delete(path);
|
|
521
|
+
try {
|
|
522
|
+
await this.readVersion(path, lane, force, info);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
if (error instanceof SizeLimitError)
|
|
526
|
+
this.oversized.set(path, version);
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async readVersion(path, lane, force, info) {
|
|
511
531
|
const previous = this.states.get(path);
|
|
512
532
|
// Unchanged bytes can still hold a Turn whose end the control plane has only now reported.
|
|
513
533
|
if (previous && this.unchanged(previous, info) && !(force && previous.runningTurnId))
|
|
@@ -41,6 +41,10 @@ export type TranscriptLine = {
|
|
|
41
41
|
};
|
|
42
42
|
type Line = TranscriptLine;
|
|
43
43
|
export declare const iso: (value: unknown) => string;
|
|
44
|
+
/** A size limit that the same bytes will hit again; retrying before the file changes is wasted work. */
|
|
45
|
+
export declare class SizeLimitError extends Error {
|
|
46
|
+
name: string;
|
|
47
|
+
}
|
|
44
48
|
export declare const NATIVE_MAX_RECORD_BYTES: number;
|
|
45
49
|
export declare const NATIVE_MAX_TRANSCRIPT_BYTES: number;
|
|
46
50
|
export declare const codexSessionsDirectory: () => string;
|
|
@@ -19,6 +19,10 @@ export const iso = (value) => {
|
|
|
19
19
|
return date.toISOString();
|
|
20
20
|
};
|
|
21
21
|
const missing = (error) => error?.code === "ENOENT";
|
|
22
|
+
/** A size limit that the same bytes will hit again; retrying before the file changes is wasted work. */
|
|
23
|
+
export class SizeLimitError extends Error {
|
|
24
|
+
name = "SizeLimitError";
|
|
25
|
+
}
|
|
22
26
|
export const NATIVE_MAX_RECORD_BYTES = 256 * 1024 * 1024;
|
|
23
27
|
export const NATIVE_MAX_TRANSCRIPT_BYTES = 1024 * 1024 * 1024;
|
|
24
28
|
const NATIVE_HEADER_MAX_BYTES = 1024 * 1024;
|
|
@@ -203,7 +207,7 @@ export async function ensurePlainCodexRollout(path, cacheDirectory, signal, maxB
|
|
|
203
207
|
transform(chunk, _encoding, push) {
|
|
204
208
|
written += chunk.length;
|
|
205
209
|
if (written > maxBytes) {
|
|
206
|
-
push(new
|
|
210
|
+
push(new SizeLimitError("Decompressed rollout exceeds the local limit"));
|
|
207
211
|
return;
|
|
208
212
|
}
|
|
209
213
|
push(null, chunk);
|
|
@@ -231,9 +235,9 @@ export async function readTranscriptLines(stream) {
|
|
|
231
235
|
pendingBytes += bytes.length;
|
|
232
236
|
checksum.update(bytes);
|
|
233
237
|
if (pendingBytes > NATIVE_MAX_RECORD_BYTES)
|
|
234
|
-
throw new
|
|
238
|
+
throw new SizeLimitError("Native record exceeds the 256 MiB local parse limit; original retained");
|
|
235
239
|
if (offset + pendingBytes > NATIVE_MAX_TRANSCRIPT_BYTES)
|
|
236
|
-
throw new
|
|
240
|
+
throw new SizeLimitError("Native transcript exceeds the 1 GiB local parse limit; original retained");
|
|
237
241
|
};
|
|
238
242
|
for await (const chunk of stream) {
|
|
239
243
|
let start = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "10.8.
|
|
3
|
+
"version": "10.8.1",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"NOTICE"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@neta-art/cohub": "12.
|
|
21
|
+
"@neta-art/cohub": "12.1.0",
|
|
22
22
|
"@neta-art/generation": "^0.1.40",
|
|
23
23
|
"commander": "^15.0.0",
|
|
24
24
|
"sharp": "^0.35.4",
|