@vue-skuilder/db 0.1.35 → 0.1.36
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/core/index.d.cts +46 -0
- package/dist/core/index.d.ts +46 -0
- package/dist/core/index.js +163 -12
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +163 -12
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +163 -12
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +163 -12
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +163 -12
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +163 -12
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +32 -2
- package/dist/index.d.ts +32 -2
- package/dist/index.js +229 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +229 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/core/navigators/Pipeline.ts +5 -2
- package/src/core/navigators/PipelineDebugger.ts +238 -8
- package/src/study/SessionController.ts +74 -11
- package/src/study/SessionDebugger.ts +8 -0
package/dist/index.mjs
CHANGED
|
@@ -854,6 +854,7 @@ var PipelineDebugger_exports = {};
|
|
|
854
854
|
__export(PipelineDebugger_exports, {
|
|
855
855
|
buildRunReport: () => buildRunReport,
|
|
856
856
|
captureRun: () => captureRun,
|
|
857
|
+
clearRunHistory: () => clearRunHistory,
|
|
857
858
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
858
859
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
859
860
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -861,6 +862,9 @@ __export(PipelineDebugger_exports, {
|
|
|
861
862
|
function registerPipelineForDebug(pipeline) {
|
|
862
863
|
_activePipeline = pipeline;
|
|
863
864
|
}
|
|
865
|
+
function clearRunHistory() {
|
|
866
|
+
runHistory.length = 0;
|
|
867
|
+
}
|
|
864
868
|
function getOrigin(card) {
|
|
865
869
|
const firstEntry = card.provenance[0];
|
|
866
870
|
if (!firstEntry) return "unknown";
|
|
@@ -889,7 +893,7 @@ function parseCardElo(provenance) {
|
|
|
889
893
|
}
|
|
890
894
|
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
|
|
891
895
|
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
892
|
-
const
|
|
896
|
+
const toReport = (card) => ({
|
|
893
897
|
cardId: card.cardId,
|
|
894
898
|
courseId: card.courseId,
|
|
895
899
|
origin: getOrigin(card),
|
|
@@ -899,7 +903,47 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
899
903
|
provenance: card.provenance,
|
|
900
904
|
tags: card.tags,
|
|
901
905
|
selected: selectedIds.has(card.cardId)
|
|
902
|
-
})
|
|
906
|
+
});
|
|
907
|
+
const selectedReported = [];
|
|
908
|
+
const nearMissReported = [];
|
|
909
|
+
const discardedTailCards = [];
|
|
910
|
+
let nonSelectedSeen = 0;
|
|
911
|
+
for (const card of allCards) {
|
|
912
|
+
if (selectedIds.has(card.cardId)) {
|
|
913
|
+
selectedReported.push(toReport(card));
|
|
914
|
+
} else if (nonSelectedSeen < DISCARDED_KEEP_TOP) {
|
|
915
|
+
nearMissReported.push(toReport(card));
|
|
916
|
+
nonSelectedSeen++;
|
|
917
|
+
} else {
|
|
918
|
+
discardedTailCards.push(card);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
const cards = [...selectedReported, ...nearMissReported];
|
|
922
|
+
let discardedTail;
|
|
923
|
+
if (discardedTailCards.length > 0) {
|
|
924
|
+
let scoreMin = Infinity;
|
|
925
|
+
let scoreMax = -Infinity;
|
|
926
|
+
let eloMin = Infinity;
|
|
927
|
+
let eloMax = -Infinity;
|
|
928
|
+
let eloSeen = false;
|
|
929
|
+
for (const c of discardedTailCards) {
|
|
930
|
+
if (c.score < scoreMin) scoreMin = c.score;
|
|
931
|
+
if (c.score > scoreMax) scoreMax = c.score;
|
|
932
|
+
const elo = parseCardElo(c.provenance);
|
|
933
|
+
if (elo !== void 0) {
|
|
934
|
+
eloSeen = true;
|
|
935
|
+
if (elo < eloMin) eloMin = elo;
|
|
936
|
+
if (elo > eloMax) eloMax = elo;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
const eloFragment = eloSeen ? `, ELO ${eloMin}\u2013${eloMax}` : "";
|
|
940
|
+
discardedTail = {
|
|
941
|
+
count: discardedTailCards.length,
|
|
942
|
+
scoreRange: [scoreMin, scoreMax],
|
|
943
|
+
eloRange: eloSeen ? [eloMin, eloMax] : void 0,
|
|
944
|
+
note: `${discardedTailCards.length} additional candidate(s) scored below the top ${DISCARDED_KEEP_TOP} near-misses and were not retained (score ${scoreMin.toExponential(2)}\u2013${scoreMax.toExponential(2)}${eloFragment}). Likely ELO-window pull remnants filtered out by hierarchy/lesson/priority gates.`
|
|
945
|
+
};
|
|
946
|
+
}
|
|
903
947
|
const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
|
|
904
948
|
const newSelected = selectedCards.filter((c) => getOrigin(c) === "new").length;
|
|
905
949
|
return {
|
|
@@ -914,7 +958,8 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
914
958
|
finalCount: selectedCards.length,
|
|
915
959
|
reviewsSelected,
|
|
916
960
|
newSelected,
|
|
917
|
-
cards
|
|
961
|
+
cards,
|
|
962
|
+
discardedTail
|
|
918
963
|
};
|
|
919
964
|
}
|
|
920
965
|
function formatProvenance(provenance) {
|
|
@@ -950,6 +995,44 @@ function printRunSummary(run) {
|
|
|
950
995
|
);
|
|
951
996
|
console.groupEnd();
|
|
952
997
|
}
|
|
998
|
+
function escapeHtml(s) {
|
|
999
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1000
|
+
}
|
|
1001
|
+
function escapeAttr(s) {
|
|
1002
|
+
return escapeHtml(s).replace(/"/g, """);
|
|
1003
|
+
}
|
|
1004
|
+
function copyTextToClipboard(text, btn) {
|
|
1005
|
+
const done = () => {
|
|
1006
|
+
if (!btn) return;
|
|
1007
|
+
const orig = btn.textContent ?? "Copy";
|
|
1008
|
+
btn.textContent = "Copied!";
|
|
1009
|
+
btn.classList.add("copied");
|
|
1010
|
+
setTimeout(() => {
|
|
1011
|
+
btn.textContent = orig;
|
|
1012
|
+
btn.classList.remove("copied");
|
|
1013
|
+
}, 1200);
|
|
1014
|
+
};
|
|
1015
|
+
const fallback = () => {
|
|
1016
|
+
const ta = document.createElement("textarea");
|
|
1017
|
+
ta.value = text;
|
|
1018
|
+
ta.style.position = "fixed";
|
|
1019
|
+
ta.style.opacity = "0";
|
|
1020
|
+
document.body.appendChild(ta);
|
|
1021
|
+
ta.select();
|
|
1022
|
+
try {
|
|
1023
|
+
document.execCommand("copy");
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
logger.warn(`[Pipeline Debug] Copy failed: ${e}`);
|
|
1026
|
+
}
|
|
1027
|
+
document.body.removeChild(ta);
|
|
1028
|
+
done();
|
|
1029
|
+
};
|
|
1030
|
+
if (navigator.clipboard?.writeText) {
|
|
1031
|
+
navigator.clipboard.writeText(text).then(done).catch(fallback);
|
|
1032
|
+
} else {
|
|
1033
|
+
fallback();
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
953
1036
|
function renderUI() {
|
|
954
1037
|
if (!_uiContainer) return;
|
|
955
1038
|
const runs = runHistory;
|
|
@@ -1008,6 +1091,13 @@ function renderUI() {
|
|
|
1008
1091
|
#sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
|
|
1009
1092
|
#sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
|
|
1010
1093
|
#sk-pipeline-debugger .provenance { font-size: 12px; color: #666; margin-top: 0.25rem; white-space: pre-wrap; font-family: monospace; background: #f8f9fa; padding: 0.5rem; border-radius: 4px; }
|
|
1094
|
+
#sk-pipeline-debugger .run-label { display: inline-block; margin-top: 0.25rem; padding: 0.1rem 0.4rem; background: #fff3cd; color: #664d03; border-radius: 3px; font-family: monospace; font-size: 11px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
|
|
1095
|
+
#sk-pipeline-debugger .label-banner { display: inline-block; padding: 0.25rem 0.6rem; background: #fff3cd; color: #664d03; border-radius: 4px; font-family: monospace; font-size: 13px; margin: 0 0 0.75rem 0; }
|
|
1096
|
+
#sk-pipeline-debugger .copy-btn { background: #0d6efd; color: white; border: none; padding: 0.25rem 0.6rem; border-radius: 3px; cursor: pointer; font-size: 12px; margin-left: 0.5rem; }
|
|
1097
|
+
#sk-pipeline-debugger .copy-btn:hover { background: #0b5ed7; }
|
|
1098
|
+
#sk-pipeline-debugger .copy-btn.copied { background: #198754; }
|
|
1099
|
+
#sk-pipeline-debugger .section-head { display: flex; align-items: center; justify-content: space-between; margin-top: 1rem; }
|
|
1100
|
+
#sk-pipeline-debugger .section-head h3 { margin: 0; }
|
|
1011
1101
|
`;
|
|
1012
1102
|
const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
|
|
1013
1103
|
(r, i) => `
|
|
@@ -1015,6 +1105,7 @@ function renderUI() {
|
|
|
1015
1105
|
<strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
|
|
1016
1106
|
<small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
|
|
1017
1107
|
<small>${r.finalCount} cards selected</small>
|
|
1108
|
+
${r.hints?._label ? `<br/><span class="run-label" title="${escapeAttr(r.hints._label)}">${escapeHtml(r.hints._label)}</span>` : ""}
|
|
1018
1109
|
</div>
|
|
1019
1110
|
`
|
|
1020
1111
|
).join("");
|
|
@@ -1023,11 +1114,13 @@ function renderUI() {
|
|
|
1023
1114
|
const filteredCards = selectedRun.cards.filter(
|
|
1024
1115
|
(c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
|
|
1025
1116
|
);
|
|
1117
|
+
const labelText = selectedRun.hints?._label ?? "(no label)";
|
|
1026
1118
|
detailsHtml = `
|
|
1027
1119
|
<h2>Run: ${selectedRun.runId}</h2>
|
|
1120
|
+
<div class="label-banner" title="${escapeAttr(labelText)}">${escapeHtml(labelText)}</div>
|
|
1028
1121
|
<p>
|
|
1029
|
-
<strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
|
|
1030
|
-
<strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
|
|
1122
|
+
<strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
|
|
1123
|
+
<strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
|
|
1031
1124
|
<strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
|
|
1032
1125
|
</p>
|
|
1033
1126
|
|
|
@@ -1042,7 +1135,10 @@ function renderUI() {
|
|
|
1042
1135
|
</table>
|
|
1043
1136
|
|
|
1044
1137
|
${selectedRun.hints ? `
|
|
1045
|
-
<
|
|
1138
|
+
<div class="section-head">
|
|
1139
|
+
<h3>Ephemeral Hints</h3>
|
|
1140
|
+
<button class="copy-btn" onclick="window.skuilder.pipeline._copyConfig('${selectedRun.runId}', this)">Copy config</button>
|
|
1141
|
+
</div>
|
|
1046
1142
|
<table>
|
|
1047
1143
|
${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
|
|
1048
1144
|
${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
|
|
@@ -1066,7 +1162,10 @@ function renderUI() {
|
|
|
1066
1162
|
</tbody>
|
|
1067
1163
|
</table>
|
|
1068
1164
|
|
|
1069
|
-
<
|
|
1165
|
+
<div class="section-head">
|
|
1166
|
+
<h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
|
|
1167
|
+
<button class="copy-btn" onclick="window.skuilder.pipeline._copyResults('${selectedRun.runId}', this)">Copy results</button>
|
|
1168
|
+
</div>
|
|
1070
1169
|
<input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
|
|
1071
1170
|
|
|
1072
1171
|
<table>
|
|
@@ -1112,7 +1211,7 @@ function mountPipelineDebugger() {
|
|
|
1112
1211
|
win.skuilder = win.skuilder || {};
|
|
1113
1212
|
win.skuilder.pipeline = pipelineDebugAPI;
|
|
1114
1213
|
}
|
|
1115
|
-
var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
|
|
1214
|
+
var _activePipeline, MAX_RUNS, runHistory, DISCARDED_KEEP_TOP, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
|
|
1116
1215
|
var init_PipelineDebugger = __esm({
|
|
1117
1216
|
"src/core/navigators/PipelineDebugger.ts"() {
|
|
1118
1217
|
"use strict";
|
|
@@ -1121,6 +1220,7 @@ var init_PipelineDebugger = __esm({
|
|
|
1121
1220
|
_activePipeline = null;
|
|
1122
1221
|
MAX_RUNS = 10;
|
|
1123
1222
|
runHistory = [];
|
|
1223
|
+
DISCARDED_KEEP_TOP = 25;
|
|
1124
1224
|
_uiContainer = null;
|
|
1125
1225
|
_selectedRunIndex = null;
|
|
1126
1226
|
_cardSearchQuery = "";
|
|
@@ -1185,7 +1285,14 @@ var init_PipelineDebugger = __esm({
|
|
|
1185
1285
|
return;
|
|
1186
1286
|
}
|
|
1187
1287
|
}
|
|
1188
|
-
|
|
1288
|
+
const runsWithTails = runHistory.filter((r) => r.discardedTail && r.discardedTail.count > 0);
|
|
1289
|
+
if (runsWithTails.length > 0) {
|
|
1290
|
+
logger.info(
|
|
1291
|
+
`[Pipeline Debug] Card '${cardId}' not found in retained cards. ${runsWithTails.length} run(s) have discarded tails that were not retained \u2014 the card may have been a low-score candidate. See run.discardedTail for ranges.`
|
|
1292
|
+
);
|
|
1293
|
+
} else {
|
|
1294
|
+
logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
|
|
1295
|
+
}
|
|
1189
1296
|
},
|
|
1190
1297
|
/**
|
|
1191
1298
|
* Explain why reviews may or may not have been selected.
|
|
@@ -1490,6 +1597,50 @@ var init_PipelineDebugger = __esm({
|
|
|
1490
1597
|
_cardSearchQuery = query;
|
|
1491
1598
|
renderUI();
|
|
1492
1599
|
},
|
|
1600
|
+
/**
|
|
1601
|
+
* Internal UI helpers
|
|
1602
|
+
* @internal
|
|
1603
|
+
*/
|
|
1604
|
+
_copyConfig(runId, btn) {
|
|
1605
|
+
const run = runHistory.find((r) => r.runId === runId);
|
|
1606
|
+
if (!run) return;
|
|
1607
|
+
const payload = {
|
|
1608
|
+
runId: run.runId,
|
|
1609
|
+
timestamp: run.timestamp.toISOString(),
|
|
1610
|
+
courseId: run.courseId,
|
|
1611
|
+
courseName: run.courseName,
|
|
1612
|
+
hints: run.hints ?? null
|
|
1613
|
+
};
|
|
1614
|
+
copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
|
|
1615
|
+
},
|
|
1616
|
+
/**
|
|
1617
|
+
* Internal UI helpers
|
|
1618
|
+
* @internal
|
|
1619
|
+
*
|
|
1620
|
+
* Copies an "abridged" view of results: just the selected cards with their
|
|
1621
|
+
* generator, origin, final score, and the top provenance reason. Designed
|
|
1622
|
+
* for pasting into bug reports without flooding with full provenance.
|
|
1623
|
+
*/
|
|
1624
|
+
_copyResults(runId, btn) {
|
|
1625
|
+
const run = runHistory.find((r) => r.runId === runId);
|
|
1626
|
+
if (!run) return;
|
|
1627
|
+
const selected = run.cards.filter((c) => c.selected).sort((a, b) => b.finalScore - a.finalScore).map((c) => ({
|
|
1628
|
+
cardId: c.cardId,
|
|
1629
|
+
generator: c.generator,
|
|
1630
|
+
origin: c.origin,
|
|
1631
|
+
score: Number(c.finalScore.toFixed(3)),
|
|
1632
|
+
topReason: c.provenance[0]?.reason ?? ""
|
|
1633
|
+
}));
|
|
1634
|
+
const payload = {
|
|
1635
|
+
runId: run.runId,
|
|
1636
|
+
label: run.hints?._label ?? null,
|
|
1637
|
+
finalCount: run.finalCount,
|
|
1638
|
+
newSelected: run.newSelected,
|
|
1639
|
+
reviewsSelected: run.reviewsSelected,
|
|
1640
|
+
selected
|
|
1641
|
+
};
|
|
1642
|
+
copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
|
|
1643
|
+
},
|
|
1493
1644
|
/**
|
|
1494
1645
|
* Show help.
|
|
1495
1646
|
*/
|
|
@@ -5804,12 +5955,12 @@ ${e.stack}` : JSON.stringify(e);
|
|
|
5804
5955
|
async getWeightedCards(limit) {
|
|
5805
5956
|
const u = await this._getCurrentUser();
|
|
5806
5957
|
try {
|
|
5807
|
-
const
|
|
5958
|
+
const navigator2 = await this.createNavigator(u);
|
|
5808
5959
|
if (this._pendingHints) {
|
|
5809
|
-
|
|
5960
|
+
navigator2.setEphemeralHints(this._pendingHints);
|
|
5810
5961
|
this._pendingHints = null;
|
|
5811
5962
|
}
|
|
5812
|
-
return
|
|
5963
|
+
return navigator2.getWeightedCards(limit);
|
|
5813
5964
|
} catch (e) {
|
|
5814
5965
|
logger.error(`[courseDB] Error getting weighted cards: ${e}`);
|
|
5815
5966
|
throw e;
|
|
@@ -8853,12 +9004,12 @@ var init_courseDB2 = __esm({
|
|
|
8853
9004
|
}
|
|
8854
9005
|
async getWeightedCards(limit) {
|
|
8855
9006
|
try {
|
|
8856
|
-
const
|
|
9007
|
+
const navigator2 = await this.createNavigator(this.userDB);
|
|
8857
9008
|
if (this._pendingHints) {
|
|
8858
|
-
|
|
9009
|
+
navigator2.setEphemeralHints(this._pendingHints);
|
|
8859
9010
|
this._pendingHints = null;
|
|
8860
9011
|
}
|
|
8861
|
-
return
|
|
9012
|
+
return navigator2.getWeightedCards(limit);
|
|
8862
9013
|
} catch (e) {
|
|
8863
9014
|
logger.error(`[static/courseDB] Error getting weighted cards: ${e}`);
|
|
8864
9015
|
throw e;
|
|
@@ -12810,10 +12961,12 @@ mountMixerDebugger();
|
|
|
12810
12961
|
|
|
12811
12962
|
// src/study/SessionDebugger.ts
|
|
12812
12963
|
init_logger();
|
|
12964
|
+
init_PipelineDebugger();
|
|
12813
12965
|
var activeSession = null;
|
|
12814
12966
|
var sessionHistory = [];
|
|
12815
12967
|
var MAX_HISTORY = 5;
|
|
12816
12968
|
function startSessionTracking(reviewQLength, newQLength, failedQLength) {
|
|
12969
|
+
clearRunHistory();
|
|
12817
12970
|
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
12818
12971
|
activeSession = {
|
|
12819
12972
|
sessionId,
|
|
@@ -13271,8 +13424,19 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13271
13424
|
/**
|
|
13272
13425
|
* Request a mid-session replan. Re-runs the pipeline with current user state
|
|
13273
13426
|
* and atomically replaces the newQ contents. Safe to call at any time during
|
|
13274
|
-
* a session
|
|
13275
|
-
*
|
|
13427
|
+
* a session.
|
|
13428
|
+
*
|
|
13429
|
+
* Concurrency policy:
|
|
13430
|
+
* - Two unhinted auto-replans never run in parallel; the second coalesces
|
|
13431
|
+
* into the first (returns the same promise).
|
|
13432
|
+
* - A hint-bearing replan that arrives while another replan is in flight
|
|
13433
|
+
* is queued to run **after** the in-flight one rather than dropped.
|
|
13434
|
+
* This preserves caller intent (label, requireCards, excludeTags,
|
|
13435
|
+
* limit, minFollowUpCards) instead of silently discarding it. Without
|
|
13436
|
+
* queueing, a background auto-replan that started just before a
|
|
13437
|
+
* completion-triggered replan would clobber the queue with unhinted
|
|
13438
|
+
* results (e.g. surfacing another gpc-intro card right after one
|
|
13439
|
+
* completed, skipping the prescribed `c-wst-*` follow-up).
|
|
13276
13440
|
*
|
|
13277
13441
|
* Does NOT affect reviewQ or failedQ.
|
|
13278
13442
|
*
|
|
@@ -13288,10 +13452,55 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13288
13452
|
if (opts.hints || opts.label || opts.limit) {
|
|
13289
13453
|
this._depletionReplanAttempted = false;
|
|
13290
13454
|
}
|
|
13455
|
+
const hasIntent = this._replanHasIntent(opts);
|
|
13291
13456
|
if (this._replanPromise) {
|
|
13292
|
-
|
|
13293
|
-
|
|
13457
|
+
if (!hasIntent) {
|
|
13458
|
+
this.log("Replan already in progress, coalescing unhinted auto-replan");
|
|
13459
|
+
return this._replanPromise;
|
|
13460
|
+
}
|
|
13461
|
+
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
13462
|
+
this.log(
|
|
13463
|
+
`Replan in progress; queueing hint-bearing replan${labelTag} behind in-flight run`
|
|
13464
|
+
);
|
|
13465
|
+
const inflight = this._replanPromise;
|
|
13466
|
+
const queued = inflight.catch(() => void 0).then(() => this._runReplan(opts));
|
|
13467
|
+
this._replanPromise = queued.finally(() => {
|
|
13468
|
+
if (this._replanPromise === queued) this._replanPromise = null;
|
|
13469
|
+
});
|
|
13470
|
+
return queued;
|
|
13294
13471
|
}
|
|
13472
|
+
const run = this._runReplan(opts);
|
|
13473
|
+
this._replanPromise = run.finally(() => {
|
|
13474
|
+
if (this._replanPromise === run) this._replanPromise = null;
|
|
13475
|
+
});
|
|
13476
|
+
await run;
|
|
13477
|
+
}
|
|
13478
|
+
/**
|
|
13479
|
+
* True when a requestReplan call carries caller intent that must not be
|
|
13480
|
+
* silently dropped. Bare unhinted auto-replans (depletion / quality
|
|
13481
|
+
* triggers in nextCard) return false and may coalesce.
|
|
13482
|
+
*/
|
|
13483
|
+
_replanHasIntent(opts) {
|
|
13484
|
+
if (opts.label) return true;
|
|
13485
|
+
if (opts.limit !== void 0) return true;
|
|
13486
|
+
if (opts.minFollowUpCards !== void 0) return true;
|
|
13487
|
+
if (opts.mode && opts.mode !== "replace") return true;
|
|
13488
|
+
if (opts.hints && Object.keys(opts.hints).length > 0) return true;
|
|
13489
|
+
return false;
|
|
13490
|
+
}
|
|
13491
|
+
/**
|
|
13492
|
+
* Body of a single replan: populate auto-excludes, stash hints on
|
|
13493
|
+
* sources, log, then run the pipeline. Extracted so it can be invoked
|
|
13494
|
+
* either immediately (no in-flight replan) or queued (chained after
|
|
13495
|
+
* the in-flight one resolves).
|
|
13496
|
+
*
|
|
13497
|
+
* IMPORTANT: hint stash and the queue-state snapshot used to build
|
|
13498
|
+
* excludeCards happen at *invocation* time, not at *queue* time. For a
|
|
13499
|
+
* queued replan that means excludes reflect the state after the prior
|
|
13500
|
+
* replan landed — which is what we want, since the prior replan's
|
|
13501
|
+
* newQ.peek(0) is the imminent draw we need to exclude.
|
|
13502
|
+
*/
|
|
13503
|
+
async _runReplan(opts) {
|
|
13295
13504
|
if (!opts.hints) opts.hints = {};
|
|
13296
13505
|
const hints = opts.hints;
|
|
13297
13506
|
const excludeSet = new Set(hints.excludeCards ?? []);
|
|
@@ -13319,12 +13528,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13319
13528
|
this._minCardsGuarantee = Math.max(this._minCardsGuarantee, opts.minFollowUpCards);
|
|
13320
13529
|
this.log(`[Replan] Card guarantee set to ${this._minCardsGuarantee}`);
|
|
13321
13530
|
}
|
|
13322
|
-
|
|
13323
|
-
try {
|
|
13324
|
-
await this._replanPromise;
|
|
13325
|
-
} finally {
|
|
13326
|
-
this._replanPromise = null;
|
|
13327
|
-
}
|
|
13531
|
+
await this._executeReplan(opts);
|
|
13328
13532
|
}
|
|
13329
13533
|
/**
|
|
13330
13534
|
* Normalise the requestReplan argument. Accepts either a ReplanOptions
|