bunnyquery 1.9.0 → 1.9.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/bunnyquery.js +58 -14
- package/dist/engine.cjs +57 -13
- package/dist/engine.cjs.map +1 -1
- package/dist/engine.mjs +57 -13
- package/dist/engine.mjs.map +1 -1
- package/package.json +1 -1
- package/src/engine/indexing_groups.ts +88 -12
package/package.json
CHANGED
|
@@ -289,6 +289,23 @@ export type RunStubInfo = {
|
|
|
289
289
|
platform?: 'claude' | 'openai';
|
|
290
290
|
};
|
|
291
291
|
|
|
292
|
+
/** Canonical form of an indexing key for COMPARISON only (never for storage).
|
|
293
|
+
*
|
|
294
|
+
* The two sides of every stub-vs-group comparison come from one string but by two
|
|
295
|
+
* routes: the run:: record id keeps `attachment.storagePath` verbatim
|
|
296
|
+
* (requests.ts runIndexUniqueId), while the group's key is recovered from the pass
|
|
297
|
+
* prompt and trimmed on the way (history.ts parseIndexingRequestText). A path whose
|
|
298
|
+
* first or last character is whitespace therefore produced two keys for one file, the
|
|
299
|
+
* stub was never suppressed, and the file drew two rows.
|
|
300
|
+
*
|
|
301
|
+
* Trim only. NOT NFC: both keys derive from the same string with no normalization on
|
|
302
|
+
* either side, so no NFC/NFD divergence is reachable here, and normalizing a key that
|
|
303
|
+
* is also an S3 object key elsewhere is how storage paths get silently renamed.
|
|
304
|
+
*/
|
|
305
|
+
export function canonIndexKey(s?: string): string {
|
|
306
|
+
return typeof s === 'string' && s ? s.trim() : '';
|
|
307
|
+
}
|
|
308
|
+
|
|
292
309
|
/** A 'working' run record older than this with no live-queue confirmation is
|
|
293
310
|
* treated as unknown rather than live: a chain that died without reaching any
|
|
294
311
|
* error path leaves 'working' dangling, and a row must not spin forever on a
|
|
@@ -413,6 +430,10 @@ export function buildChatDisplayList(
|
|
|
413
430
|
var openRunOfKey: { [key: string]: string } = {};
|
|
414
431
|
var runsOfKey: { [key: string]: string[] } = {};
|
|
415
432
|
var keyOfRun: { [runId: string]: string } = {};
|
|
433
|
+
// Newest pass timestamp seen for each open run, so a FIRST pass can be told from a
|
|
434
|
+
// re-index (it arrives AFTER the run it follows) apart from a misordered older pass
|
|
435
|
+
// (it arrives before). Only the second must not re-open.
|
|
436
|
+
var newestTsOfRun: { [runId: string]: number } = {};
|
|
416
437
|
var runSeq = 0;
|
|
417
438
|
|
|
418
439
|
for (var i = 0; i < list.length; i++) {
|
|
@@ -421,7 +442,18 @@ export function buildChatDisplayList(
|
|
|
421
442
|
|
|
422
443
|
var runId: string | undefined;
|
|
423
444
|
var ref = msg.role === 'user' ? readFileRef(msg) : null;
|
|
424
|
-
|
|
445
|
+
// A pass already attributed to a run stays with it, whatever its label says.
|
|
446
|
+
// Reading the label FIRST made attribution depend on walk order: a first pass
|
|
447
|
+
// ("A new file has just been uploaded") re-opened its file's run at the line
|
|
448
|
+
// below, so if history delivered a run's continuations before its first pass -
|
|
449
|
+
// which paging and the bg merge can do - the continuations opened a run, the
|
|
450
|
+
// late first pass deleted it and opened a second, and the abandoned one was
|
|
451
|
+
// force-finished. One run then rendered as TWO rows for the same file: a green
|
|
452
|
+
// "complete" and a yellow "in progress". The same applies to a duplicate copy
|
|
453
|
+
// of a pass. The id is the stable identity here; the label is a description.
|
|
454
|
+
if (msg._serverItemId && runByItemId[msg._serverItemId]) {
|
|
455
|
+
runId = runByItemId[msg._serverItemId];
|
|
456
|
+
} else if (ref) {
|
|
425
457
|
// Path first — a file can be re-uploaded under a name that already
|
|
426
458
|
// exists elsewhere. But a pass that supplied no path (a compact
|
|
427
459
|
// continuation label recovered from an old history cache) is still the
|
|
@@ -431,7 +463,24 @@ export function buildChatDisplayList(
|
|
|
431
463
|
// A FIRST pass ("A new file has just been uploaded") when this file
|
|
432
464
|
// already has a run open starts a new one: it is a re-index, or a
|
|
433
465
|
// re-upload over the same storage path. Continuations join the run.
|
|
434
|
-
|
|
466
|
+
//
|
|
467
|
+
// ...but only when this pass is genuinely NEW. A first pass whose id is
|
|
468
|
+
// already attributed is the same pass reached twice (a duplicate bubble, or
|
|
469
|
+
// a walk that reaches it after the continuations it opened), and re-opening
|
|
470
|
+
// on it splits one run in half.
|
|
471
|
+
var alreadySeen = !!(msg._serverItemId && runByItemId[msg._serverItemId]);
|
|
472
|
+
// ...and only when it is actually LATER than the run it would replace. A
|
|
473
|
+
// re-index's first pass follows the previous run; a misordered one precedes
|
|
474
|
+
// the continuations already walked, and re-opening on it abandons a live run
|
|
475
|
+
// (rendered green "complete") beside the fragment (yellow "in progress").
|
|
476
|
+
// Timestamps decide it when both are known; without them the old
|
|
477
|
+
// label-only behaviour stands, so the re-index case is unchanged.
|
|
478
|
+
var openId = openRunOfKey[key];
|
|
479
|
+
var notLater = false;
|
|
480
|
+
if (openId && typeof msg._ts === 'number' && typeof newestTsOfRun[openId] === 'number') {
|
|
481
|
+
notLater = (msg._ts as number) <= newestTsOfRun[openId];
|
|
482
|
+
}
|
|
483
|
+
if (!ref.continued && !alreadySeen && !notLater && openRunOfKey[key]) delete openRunOfKey[key];
|
|
435
484
|
runId = openRunOfKey[key];
|
|
436
485
|
if (!runId) {
|
|
437
486
|
runId = 'run' + (runSeq++);
|
|
@@ -439,8 +488,6 @@ export function buildChatDisplayList(
|
|
|
439
488
|
keyOfRun[runId] = key;
|
|
440
489
|
(runsOfKey[key] || (runsOfKey[key] = [])).push(runId);
|
|
441
490
|
}
|
|
442
|
-
} else if (msg._serverItemId && runByItemId[msg._serverItemId]) {
|
|
443
|
-
runId = runByItemId[msg._serverItemId];
|
|
444
491
|
} else if (msg.role !== 'user') {
|
|
445
492
|
// ADJACENT only. Both paths that create a pass emit its request and
|
|
446
493
|
// response bubbles together, so an id-less response belongs to the
|
|
@@ -455,6 +502,10 @@ export function buildChatDisplayList(
|
|
|
455
502
|
// or a response whose request is not loaded) stays an ordinary message
|
|
456
503
|
// rather than being folded into whichever group happens to be nearest.
|
|
457
504
|
if (!runId) continue;
|
|
505
|
+
if (typeof msg._ts === 'number') {
|
|
506
|
+
var prevTs = newestTsOfRun[runId];
|
|
507
|
+
if (typeof prevTs !== 'number' || (msg._ts as number) > prevTs) newestTsOfRun[runId] = msg._ts as number;
|
|
508
|
+
}
|
|
458
509
|
|
|
459
510
|
var g = groups[runId];
|
|
460
511
|
if (!g) {
|
|
@@ -780,16 +831,18 @@ export function buildChatDisplayList(
|
|
|
780
831
|
var coveredPathlessNames: { [k: string]: boolean } = {};
|
|
781
832
|
for (var ci = 0; ci < order.length; ci++) {
|
|
782
833
|
var cg = groups[order[ci]];
|
|
783
|
-
if (cg.path) {
|
|
784
|
-
|
|
785
|
-
|
|
834
|
+
if (cg.path) {
|
|
835
|
+
coveredPaths[canonIndexKey(cg.path)] = true;
|
|
836
|
+
if (cg.key) coveredPaths[canonIndexKey(cg.key)] = true;
|
|
837
|
+
} else if (cg.name) coveredPathlessNames[canonIndexKey(cg.name)] = true;
|
|
838
|
+
else if (cg.key) coveredPaths[canonIndexKey(cg.key)] = true;
|
|
786
839
|
}
|
|
787
840
|
var now = opts && typeof opts.now === 'number' ? opts.now : Date.now();
|
|
788
841
|
var stubClearedAt = (opts && typeof opts.stubClearedAt === 'number' && opts.stubClearedAt > 0)
|
|
789
842
|
? opts.stubClearedAt : 0;
|
|
790
843
|
for (var sp in runStubs) {
|
|
791
844
|
var rec = runStubs[sp];
|
|
792
|
-
if (!sp || !rec || !rec.status || coveredPaths[sp]) continue;
|
|
845
|
+
if (!sp || !rec || !rec.status || coveredPaths[canonIndexKey(sp)]) continue;
|
|
793
846
|
var fname = rec.filename || sp.split('/').pop() || sp;
|
|
794
847
|
// A PATHLESS group (legacy compact label) can only be matched by
|
|
795
848
|
// name, and that is still the right match: the file already has a
|
|
@@ -797,7 +850,7 @@ export function buildChatDisplayList(
|
|
|
797
850
|
// void. What is gone is the reverse — a group WITH a path no longer
|
|
798
851
|
// writes into the name namespace, so it can no longer delete a
|
|
799
852
|
// same-basename stub from another folder.
|
|
800
|
-
if (coveredPathlessNames[fname]) continue;
|
|
853
|
+
if (coveredPathlessNames[canonIndexKey(fname)]) continue;
|
|
801
854
|
// A run recorded under the OTHER platform's chat belongs to that
|
|
802
855
|
// conversation: its passes live in that history (so no real group
|
|
803
856
|
// can ever cover this stub) and the queue probe is platform-scoped
|
|
@@ -808,7 +861,26 @@ export function buildChatDisplayList(
|
|
|
808
861
|
// whatever the record says (a lagged update must not paint a verdict over
|
|
809
862
|
// visible work), then the record's own terminal statuses, then the
|
|
810
863
|
// queue's authoritative ABSENCE, and only then the stated wait.
|
|
811
|
-
|
|
864
|
+
// The bare-NAME disjunct is load-bearing: a legacy pass prompt may carry no
|
|
865
|
+
// storage path at all, so the name is the only key its run can appear under.
|
|
866
|
+
// But it also matched ANY live file sharing this basename, painting a
|
|
867
|
+
// finished run's stub yellow because a same-named file in another folder was
|
|
868
|
+
// indexing. Take the name hit only when it cannot belong to a different
|
|
869
|
+
// file - i.e. no OTHER live key is a path ending in this basename.
|
|
870
|
+
var live = !!liveIndexKeys[sp] || !!liveIndexKeys[canonIndexKey(sp)];
|
|
871
|
+
if (!live && (liveIndexKeys[fname] || liveIndexKeys[canonIndexKey(fname)])) {
|
|
872
|
+
var claimedByOther = false;
|
|
873
|
+
for (var lk in liveIndexKeys) {
|
|
874
|
+
if (!liveIndexKeys[lk]) continue;
|
|
875
|
+
var lkc = canonIndexKey(lk);
|
|
876
|
+
if (lkc === canonIndexKey(sp)) continue;
|
|
877
|
+
if (lkc.length > fname.length && lkc.slice(-(fname.length + 1)) === '/' + fname) {
|
|
878
|
+
claimedByOther = true;
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
live = !claimedByOther;
|
|
883
|
+
}
|
|
812
884
|
// Cleared-history horizon: a run that ended at or before the clear is
|
|
813
885
|
// part of what the user asked to forget. A live queue hit survives it
|
|
814
886
|
// (see BuildDisplayListOptions.stubClearedAt). A record carrying NO
|
|
@@ -825,7 +897,8 @@ export function buildChatDisplayList(
|
|
|
825
897
|
// A done:: marker from the same sweep is as terminal as the record's
|
|
826
898
|
// own 'done' — it settles a dangling 'working' the chain never
|
|
827
899
|
// flipped (mirrors the real-group doneKeys disjunct).
|
|
828
|
-
if (rec.status === 'done' || doneKeys[sp] || doneKeys[
|
|
900
|
+
if (rec.status === 'done' || doneKeys[sp] || doneKeys[canonIndexKey(sp)]
|
|
901
|
+
|| doneKeys[fname] || doneKeys[canonIndexKey(fname)]) { st = 'done'; fin = true; }
|
|
829
902
|
else if (rec.status === 'error') { st = 'error'; fin = true; }
|
|
830
903
|
else if (rec.status === 'cancelled') { st = 'cancelled'; fin = true; }
|
|
831
904
|
else if (liveIndexChecked) {
|
|
@@ -903,11 +976,14 @@ export function buildChatDisplayList(
|
|
|
903
976
|
// the first pass paged in, and again when paging ended. A record with a
|
|
904
977
|
// `started` places the run at ONE spot for as long as the record exists.
|
|
905
978
|
var suppressAnchor: { [runId: string]: boolean } = {};
|
|
979
|
+
var stubByCanon: { [k: string]: RunStubInfo } = {};
|
|
980
|
+
if (runStubs) for (var ck in runStubs) stubByCanon[canonIndexKey(ck)] = runStubs[ck];
|
|
906
981
|
if (runStubs) {
|
|
907
982
|
for (var ti2 = 0; ti2 < order.length; ti2++) {
|
|
908
983
|
var tg = groups[order[ti2]];
|
|
909
984
|
if (!newestRunOfKey[order[ti2]]) continue;
|
|
910
|
-
var trec = (tg.path && runStubs[tg.path]
|
|
985
|
+
var trec = (tg.path && (runStubs[tg.path] || stubByCanon[canonIndexKey(tg.path)]))
|
|
986
|
+
|| runStubs[tg.key] || stubByCanon[canonIndexKey(tg.key)];
|
|
911
987
|
if (!trec || typeof trec.started !== 'number') continue;
|
|
912
988
|
if (stubPlatform && trec.platform && trec.platform !== stubPlatform) continue;
|
|
913
989
|
suppressAnchor[order[ti2]] = true;
|