dsh-plugin-soulbridge 0.0.116 → 0.0.118
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/index.js +86 -3
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -233,6 +233,70 @@ export function createTurnCostTracker({ threshold = TURN_REQUEST_ALERT, maxTurns
|
|
|
233
233
|
};
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/**
|
|
237
|
+
* When a session has grown past the point where starting a new one is cheaper (0026).
|
|
238
|
+
*
|
|
239
|
+
* The signal is prompt tokens per request — how much history every single call re-buys —
|
|
240
|
+
* with the turn count as the second axis. 300k is where the old session degraded; 100
|
|
241
|
+
* turns is where continuing stops being cheap even at a smaller context.
|
|
242
|
+
*/
|
|
243
|
+
const SWITCH_SESSION = { contextPerRequest: 300_000, turns: 100 };
|
|
244
|
+
|
|
245
|
+
/** The reminder the model relays: the reply is the only channel the user reads. */
|
|
246
|
+
export function renderSwitchSession(stats, perRequest) {
|
|
247
|
+
const thousands = Math.round(perRequest / 1000);
|
|
248
|
+
return (
|
|
249
|
+
`<switch-session turns="${stats.turns}" context-per-request="${Math.round(perRequest)}">` +
|
|
250
|
+
`这个会话已经很大(每请求约 ${thousands}k token 上下文,共 ${stats.turns} 轮)。` +
|
|
251
|
+
`请提醒用户:新开一个会话更省——先把进度写进 docs/state.md 的接手须知,` +
|
|
252
|
+
`再到新会话用「读 docs/state.md 的接手须知」开头。` +
|
|
253
|
+
`</switch-session>`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Per-session cost ledger. It answers one question: has this session crossed the point
|
|
259
|
+
* where a new one is cheaper, and has the user been told yet?
|
|
260
|
+
*
|
|
261
|
+
* Not a nag: the reminder fires once per level, and a level is one more notch of the
|
|
262
|
+
* same threshold (600k/req or 200 turns is level 2). Crossing again by a doubling is
|
|
263
|
+
* worth saying again; saying it every turn is wallpaper.
|
|
264
|
+
*/
|
|
265
|
+
export function createSessionLedger({ thresholds = SWITCH_SESSION } = {}) {
|
|
266
|
+
const cost = new Map();
|
|
267
|
+
const reminded = new Map();
|
|
268
|
+
return {
|
|
269
|
+
/** Feed one finished turn — the turn-cost report. */
|
|
270
|
+
note(record) {
|
|
271
|
+
const id = String(record?.session ?? 'default');
|
|
272
|
+
const stats = cost.get(id) ?? { turns: 0, requests: 0, prompt: 0 };
|
|
273
|
+
stats.turns += 1;
|
|
274
|
+
stats.requests += Number(record?.requests) || 0;
|
|
275
|
+
stats.prompt +=
|
|
276
|
+
(Number(record?.input) || 0) + (Number(record?.cacheHit) || 0) + (Number(record?.cacheWrite) || 0);
|
|
277
|
+
cost.set(id, stats);
|
|
278
|
+
// Bounded like the tracker: a long-lived host must not accumulate sessions forever.
|
|
279
|
+
if (cost.size > 16) cost.delete(cost.keys().next().value);
|
|
280
|
+
},
|
|
281
|
+
/** The reminder block when a new level is crossed; '' otherwise. */
|
|
282
|
+
due(sessionId) {
|
|
283
|
+
const id = String(sessionId ?? 'default');
|
|
284
|
+
const stats = cost.get(id);
|
|
285
|
+
if (!stats || stats.requests === 0) return '';
|
|
286
|
+
const perRequest = stats.prompt / stats.requests;
|
|
287
|
+
const level = Math.floor(
|
|
288
|
+
Math.max(perRequest / thresholds.contextPerRequest, stats.turns / thresholds.turns),
|
|
289
|
+
);
|
|
290
|
+
if (level < 1 || level <= (reminded.get(id) ?? 0)) return '';
|
|
291
|
+
reminded.set(id, level);
|
|
292
|
+
return renderSwitchSession(stats, perRequest);
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** One ledger for the running host: a session is remembered across pre-steps. */
|
|
298
|
+
const sessionLedger = createSessionLedger();
|
|
299
|
+
|
|
236
300
|
/** Prompt assembly can run several times per step; do not hit SQLite every time. */
|
|
237
301
|
|
|
238
302
|
/**
|
|
@@ -1147,7 +1211,10 @@ export function createPreStepHandler(deps) {
|
|
|
1147
1211
|
// previous session: one session can hold several topics, and a topic can
|
|
1148
1212
|
// span sessions. Injected once per session, when it is a different one.
|
|
1149
1213
|
const handoff = takeHandoff(sessionId);
|
|
1150
|
-
|
|
1214
|
+
// 0026: when this session has already grown past the point where a new one is
|
|
1215
|
+
// cheaper, say so once per level — the reply is the only channel the user reads.
|
|
1216
|
+
const switchHint = sessionLedger.due(sessionId);
|
|
1217
|
+
const block = [handoff, switchHint, related].filter(Boolean).join('\n\n');
|
|
1151
1218
|
injectedTurn.set(sessionId, turn);
|
|
1152
1219
|
if (injectedTurn.size > 16) {
|
|
1153
1220
|
// Keep the map bounded: drop the oldest entry, not the newest.
|
|
@@ -1168,6 +1235,10 @@ export function createPreStepHandler(deps) {
|
|
|
1168
1235
|
const idPart = used.length > 0 ? ` ids=[${used.join(',')}]` : '';
|
|
1169
1236
|
log(`prestep: inject turn=${turn} chars=${block.length}${idPart} handoff=${handoff ? 'yes' : 'no'}`);
|
|
1170
1237
|
trace({ event: 'inject', session: sessionId, turn, chars: block.length, ids: used, handoff: Boolean(handoff) });
|
|
1238
|
+
if (switchHint) {
|
|
1239
|
+
log(`switch-session: remind turn=${turn}`);
|
|
1240
|
+
trace({ event: 'switch_session', session: sessionId, turn });
|
|
1241
|
+
}
|
|
1171
1242
|
return { ...decision, messages: [...decision.messages, note] };
|
|
1172
1243
|
} catch (error) {
|
|
1173
1244
|
log(`prestep: skipped after error: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1426,7 +1497,7 @@ async function runExtraction(text, turnId, turnNo, origin = 'live', thread = nul
|
|
|
1426
1497
|
const inferred = result.facts.filter((fact) => fact.source === 'inferred').length;
|
|
1427
1498
|
log(
|
|
1428
1499
|
`extract ok: by=${draft.by} reason=${draft.reason ?? '-'} entities=${result.entities.length} facts=${result.facts.length}` +
|
|
1429
|
-
` gaps=${result.gaps.length} skipped=${result.skipped.length}` +
|
|
1500
|
+
` gaps=${result.gaps.length} skipped=${result.skipped.length} draftSkipped=${(draft.skipped ?? []).length}` +
|
|
1430
1501
|
` slots->text=${result.droppedSlots.length} holder->world=${result.holderFallbacks.length}` +
|
|
1431
1502
|
` inferred=${inferred}` +
|
|
1432
1503
|
` unit=${turns ? `${turns.length}turns` : '1turn'} threadId=${thread?.id ?? '-'}` +
|
|
@@ -1438,6 +1509,12 @@ async function runExtraction(text, turnId, turnNo, origin = 'live', thread = nul
|
|
|
1438
1509
|
for (const skipped of result.skipped.slice(0, 3)) {
|
|
1439
1510
|
log(` extract skipped: ${skipped.what} (${skipped.reason})`);
|
|
1440
1511
|
}
|
|
1512
|
+
// Draft-level drops are the extractor's own decisions (a sentence it could not
|
|
1513
|
+
// structure, an item the model wrote and the gate refused). Visible here so the
|
|
1514
|
+
// floor cannot throw work away silently.
|
|
1515
|
+
for (const dropped of (draft.skipped ?? []).slice(0, 3)) {
|
|
1516
|
+
log(` extract dropped: ${dropped.what} (${dropped.reason})`);
|
|
1517
|
+
}
|
|
1441
1518
|
for (const slot of result.droppedSlots.slice(0, 3)) {
|
|
1442
1519
|
log(` extract slot kept as text: ${slot.slot}=${slot.value} (undeclared)`);
|
|
1443
1520
|
}
|
|
@@ -1450,6 +1527,7 @@ async function runExtraction(text, turnId, turnNo, origin = 'live', thread = nul
|
|
|
1450
1527
|
facts: result.facts.length,
|
|
1451
1528
|
gaps: result.gaps.length,
|
|
1452
1529
|
skipped: result.skipped,
|
|
1530
|
+
draftSkipped: (draft.skipped ?? []).slice(0, 5),
|
|
1453
1531
|
droppedSlots: result.droppedSlots,
|
|
1454
1532
|
holderFallbacks: result.holderFallbacks,
|
|
1455
1533
|
thread: attribution.threadId,
|
|
@@ -2516,7 +2594,12 @@ export function apply(ctx) {
|
|
|
2516
2594
|
// reaches the threshold also gets one line in plugin.log. Attached before the
|
|
2517
2595
|
// event listener so no turn can be missed.
|
|
2518
2596
|
turnCostTracker = createTurnCostTracker({
|
|
2519
|
-
onReport: (record) =>
|
|
2597
|
+
onReport: (record) => {
|
|
2598
|
+
trace(record);
|
|
2599
|
+
// 0026 reminder: the same record that makes a turn visible also answers
|
|
2600
|
+
// "is this session too big to keep going".
|
|
2601
|
+
sessionLedger.note(record);
|
|
2602
|
+
},
|
|
2520
2603
|
onAlert: (line) => log(line),
|
|
2521
2604
|
});
|
|
2522
2605
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-soulbridge",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.118",
|
|
4
4
|
"description": "Inject soulbridge context into DSH sessions and feed user turns back as observations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"setup": "node scripts/install-hooks.mjs"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"soulbridge": ">=0.0.
|
|
45
|
+
"soulbridge": ">=0.0.116 <0.1.0"
|
|
46
46
|
}
|
|
47
47
|
}
|