dsh-plugin-soulbridge 0.0.111 → 0.0.112

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.
Files changed (2) hide show
  1. package/index.js +44 -8
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -1098,6 +1098,17 @@ function bumpExtractAttempt(turnId) {
1098
1098
  );
1099
1099
  }
1100
1100
 
1101
+ /**
1102
+ * After a thread was extracted, every queued turn of that thread is already covered.
1103
+ * Leaving them would extract the same topic again, turn by turn (0025).
1104
+ */
1105
+ function removeExtractItemsOfThread(threadId, turnIds) {
1106
+ if (!threadId) return;
1107
+ writeExtractQueue(
1108
+ readExtractQueue().filter((item) => !turnIds.includes(item.turnId)),
1109
+ );
1110
+ }
1111
+
1101
1112
  /**
1102
1113
  * Add one turn to the extraction queue.
1103
1114
  *
@@ -1131,7 +1142,7 @@ function hasSubstance(text) {
1131
1142
  return !/^(好的?|嗯+|哦+|可以|行|ok|okay|收到|明白|继续|好)[。!!.\s]*$/i.test(trimmed);
1132
1143
  }
1133
1144
 
1134
- async function runExtraction(text, turnId, turnNo, origin = 'live') {
1145
+ async function runExtraction(text, turnId, turnNo, origin = 'live', thread = null) {
1135
1146
  const config = readConfig();
1136
1147
  if (config.extract === 'off') return 'skipped-off';
1137
1148
  if (typeof text !== 'string' || !hasSubstance(text)) {
@@ -1153,6 +1164,10 @@ async function runExtraction(text, turnId, turnNo, origin = 'live') {
1153
1164
  const previousThreadId = soul.threadBefore(turnRow.sessionId, turnNo);
1154
1165
  if (previousThreadId) context.previousThreadId = previousThreadId;
1155
1166
  }
1167
+ // 0025: the unit is the whole topic when the turn belongs to one. The turns array is
1168
+ // numbered the way the prompt numbers them, and each fact may name its own turn.
1169
+ const turns = thread?.turns ?? null;
1170
+ const turnIds = turns ? Object.fromEntries(turns.map((entry) => [entry.turnNo, entry.id])) : undefined;
1156
1171
  const draft = await soul.extract(text, {
1157
1172
  llm: useLlm ? injectedLlm : undefined,
1158
1173
  mode: useLlm ? 'auto' : 'rules',
@@ -1162,8 +1177,9 @@ async function runExtraction(text, turnId, turnNo, origin = 'live') {
1162
1177
  // The exchange, not the message (ADR 0035): my reply is where promises and
1163
1178
  // decisions live, and it is the only way "我的承诺" can ever be extracted.
1164
1179
  assistantText: turnRow?.assistantText ?? undefined,
1180
+ turns: turns ?? undefined,
1165
1181
  });
1166
- const result = soul.commitDrafts(draft, { turnId });
1182
+ const result = soul.commitDrafts(draft, { turnId, turnIds });
1167
1183
  // Attribution is written even when nothing was extracted: a turn that says
1168
1184
  // "无" (nothing worth remembering) can still be part of a thread.
1169
1185
  const attribution = soul.applyThread(draft.thread, {
@@ -1183,6 +1199,7 @@ async function runExtraction(text, turnId, turnNo, origin = 'live') {
1183
1199
  ` gaps=${result.gaps.length} skipped=${result.skipped.length}` +
1184
1200
  ` slots->text=${result.droppedSlots.length} holder->world=${result.holderFallbacks.length}` +
1185
1201
  ` inferred=${inferred}` +
1202
+ ` unit=${turns ? `${turns.length}turns` : '1turn'} threadId=${thread?.id ?? '-'}` +
1186
1203
  ` ms=${ms} turn=${turnNo} origin=${origin}` +
1187
1204
  ` thread=${attribution.threadId}${attribution.created ? '(new)' : ''}` +
1188
1205
  ` join=${attribution.joinReason} threadLine=${draft.threadLine ?? 'rules'}` +
@@ -1276,9 +1293,21 @@ export async function drainExtractionQueue() {
1276
1293
  try {
1277
1294
  const item = pickExtractionItem(queue);
1278
1295
  let text = '';
1296
+ let thread = null;
1279
1297
  try {
1280
- const turn = ensureSoul().turn(item.turnId);
1298
+ const soul = ensureSoul();
1299
+ const turn = soul.turn(item.turnId);
1281
1300
  text = turn?.userText ?? '';
1301
+ // A turn is a trigger, the topic is the unit (0025). When the turn is already in a
1302
+ // thread, extract that thread's turns instead of this message alone.
1303
+ const threadId = soul.turnThreadId(item.turnId);
1304
+ if (threadId) {
1305
+ const turns = soul.threadTurns(threadId);
1306
+ if (turns.length > 0) {
1307
+ thread = { id: threadId, turns };
1308
+ text = turns[turns.length - 1].userText;
1309
+ }
1310
+ }
1282
1311
  } catch {
1283
1312
  text = '';
1284
1313
  }
@@ -1288,14 +1317,21 @@ export async function drainExtractionQueue() {
1288
1317
  return;
1289
1318
  }
1290
1319
 
1291
- const outcome = await runExtraction(text, item.turnId, item.turnNo, item.origin ?? 'live');
1320
+ const outcome = await runExtraction(text, item.turnId, item.turnNo, item.origin ?? 'live', thread);
1292
1321
  const ok =
1293
1322
  outcome === 'ok' || outcome === 'skipped-off' || outcome === 'skipped-empty' || outcome === 'rules-mode';
1294
1323
  if (ok) {
1295
- removeExtractItem(item.turnId);
1296
- // Promotion is decided after the facts exist: a corrected turn's lessons enter
1297
- // the classification queue here, which is the only live producer it has.
1298
- feedClassifyFromTurn(item.turnId);
1324
+ if (thread) {
1325
+ removeExtractItemsOfThread(thread.id, thread.turns.map((entry) => entry.id));
1326
+ // Promotion is decided after the facts exist; a whole thread can hold several
1327
+ // corrected turns, so every turn is offered.
1328
+ for (const entry of thread.turns) feedClassifyFromTurn(entry.id);
1329
+ } else {
1330
+ removeExtractItem(item.turnId);
1331
+ // Promotion is decided after the facts exist: a corrected turn's lessons enter
1332
+ // the classification queue here, which is the only live producer it has.
1333
+ feedClassifyFromTurn(item.turnId);
1334
+ }
1299
1335
  trace({ event: 'extract-queue', drained: 1, remaining: readExtractQueue().length, outcome });
1300
1336
  return true;
1301
1337
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-soulbridge",
3
- "version": "0.0.111",
3
+ "version": "0.0.112",
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.108 <0.1.0"
45
+ "soulbridge": ">=0.0.112 <0.1.0"
46
46
  }
47
47
  }