dsh-plugin-soulbridge 0.0.114 → 0.0.116

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 +72 -4
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -694,11 +694,16 @@ export function buildFactHooks(text, options = {}) {
694
694
  * When the question names a subject, the entity timeline is the answer and the
695
695
  * related-pairs search is the contamination: it is how "上次和 a 吃饭" ends up
696
696
  * answered with something about b. So the plan chooses one or the other.
697
+ *
698
+ * A time question is different: the window block answers "what happened last week",
699
+ * and the relevance block stays on because it is where relative/unknown facts and
700
+ * not-yet-extracted conversation still surface (0027 §八: they must never be dropped
701
+ * just because their date cannot be located).
697
702
  */
698
703
  export function planBlocks(plan) {
699
- if (!plan) return { related: true, timeline: false };
700
- if (plan.primitive === 'entity') return { related: false, timeline: true };
701
- return { related: true, timeline: false };
704
+ if (!plan) return { related: true, timeline: false, window: false };
705
+ if (plan.primitive === 'entity') return { related: false, timeline: true, window: false };
706
+ return { related: true, timeline: false, window: Boolean(plan.window) };
702
707
  }
703
708
 
704
709
  const PROFILE_CORE_DIMENSION = '价值观';
@@ -755,7 +760,55 @@ export function renderRulesBlock(rules) {
755
760
  return `<rules fired="${ids}">\n${lines}\n</rules>`;
756
761
  }
757
762
 
758
- function buildRelatedContext(text, state = {}) {
763
+ /** How many facts one window may put in the prompt before it is cut. */
764
+ const TIME_WINDOW_LIMIT = 10;
765
+
766
+ /**
767
+ * A calendar day in the unit the store writes: the UTC date of a day instant.
768
+ *
769
+ * Not the local date: facts store days as UTC midnights, so the local reading would
770
+ * disagree in any timezone west of UTC — and agree only by luck in ours.
771
+ */
772
+ function isoDay(ms) {
773
+ return new Date(ms).toISOString().slice(0, 10);
774
+ }
775
+
776
+ /** When a fact happened, as the injection may say it: a located date, or the words said. */
777
+ function factWhen(fact) {
778
+ // A range ("10 月下旬") is not a day: printing its start as a date would claim an
779
+ // exactness the model never had. The original words are the honest answer.
780
+ if (fact.validFromPrecision === 'range' && fact.validFromText) return fact.validFromText;
781
+ return fact.validFrom ? isoDay(fact.validFrom) : fact.validFromText ?? '';
782
+ }
783
+
784
+ /**
785
+ * The facts that happened in one window (0027 §八, reader 1).
786
+ *
787
+ * Only located dates reach this block — the query already excluded the rest. The dates
788
+ * are shown because "when" is the question; an empty window says so explicitly rather
789
+ * than leaving the model to fill the gap. Truncation is stated, never silent: a block
790
+ * that looks complete but is not is worse than a short one.
791
+ */
792
+ export function renderTimeWindow(window, facts, options = {}) {
793
+ const cue = escapeAttr(String(window.cue ?? ''));
794
+ // `to` is exclusive; the last day in the window is the millisecond before it.
795
+ const head = `<time-window cue="${cue}" from="${isoDay(window.from)}" to="${isoDay(window.to - 1)}">`;
796
+ if (!Array.isArray(facts) || facts.length === 0) {
797
+ return `${head}\n(${escapeBlockText(String(window.cue ?? '这段时间'))}没有记录)\n</time-window>`;
798
+ }
799
+ let sawInferred = false;
800
+ const lines = facts.map((fact) => {
801
+ const marker = fact.source === 'inferred' ? INFERRED_MARKER : '';
802
+ if (marker) sawInferred = true;
803
+ const when = escapeBlockText(String(factWhen(fact)));
804
+ return `- ${marker}[${fact.id}]${when ? ` ${when}` : ''} ${escapeBlockText(fact.statement)}`;
805
+ });
806
+ if (options.truncated) lines.push(`- (只显示最早 ${facts.length} 条)`);
807
+ const block = `${head}\n${lines.join('\n')}\n</time-window>`;
808
+ return sawInferred ? `${block}\n${SOURCE_NOTES}` : block;
809
+ }
810
+
811
+ export function buildRelatedContext(text, state = {}) {
759
812
  const parts = [];
760
813
  const used = [];
761
814
  // The profile is the merged description of the person; it is the only block that
@@ -824,6 +877,21 @@ function buildRelatedContext(text, state = {}) {
824
877
  log(`timeline failed: ${error instanceof Error ? error.message : String(error)}`);
825
878
  }
826
879
  }
880
+ if (policy.window && plan?.window) {
881
+ try {
882
+ // +1 is a probe, not a fact: when it comes back there were more rows than the
883
+ // block may carry, and the renderer says so instead of pretending completeness.
884
+ const hits = ensureSoul().factsInWindow(plan.window, { limit: TIME_WINDOW_LIMIT + 1 });
885
+ const facts = hits.slice(0, TIME_WINDOW_LIMIT);
886
+ const truncated = hits.length > TIME_WINDOW_LIMIT;
887
+ parts.push(renderTimeWindow(plan.window, facts, { truncated }));
888
+ used.push(...facts.map((fact) => fact.id));
889
+ trace({ event: 'time_window', cue: plan.window.cue, from: plan.window.from, to: plan.window.to, hits: facts.length, truncated });
890
+ log(`time-window: ${plan.window.cue} hits=${facts.length}${truncated ? ' (truncated)' : ''}`);
891
+ } catch (error) {
892
+ log(`time window failed: ${error instanceof Error ? error.message : String(error)}`);
893
+ }
894
+ }
827
895
  if (policy.related) {
828
896
  const pairs = ensureSoul().recallTurnPairs(text);
829
897
  if (pairs.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-soulbridge",
3
- "version": "0.0.114",
3
+ "version": "0.0.116",
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.112 <0.1.0"
45
+ "soulbridge": ">=0.0.115 <0.1.0"
46
46
  }
47
47
  }