stratagate-dsh 0.2.95 → 0.2.97

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/plugin.js CHANGED
@@ -2,7 +2,6 @@
2
2
  import { mkdir as mkdir2 } from "node:fs/promises";
3
3
  import { existsSync as existsSync2 } from "node:fs";
4
4
  import { dirname as dirname3 } from "node:path";
5
- import { createUserMessage as createUserMessage3 } from "@deepseek-ai/dsh-llm";
6
5
 
7
6
  // src/config.ts
8
7
  import z from "@deepseek-ai/schemastery";
@@ -834,6 +833,41 @@ function applyGraphProjection(options) {
834
833
  }
835
834
 
836
835
  // packages/core/src/events.ts
836
+ function normalizeEventTemporal(value) {
837
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
838
+ const temporal = { ...value };
839
+ for (const field of [
840
+ "mentionedAt",
841
+ "happenedStart",
842
+ "happenedEnd",
843
+ "originalText",
844
+ "precision",
845
+ "basis",
846
+ "status",
847
+ "eventType",
848
+ "threadId",
849
+ "sameEventId"
850
+ ]) {
851
+ if (typeof temporal[field] !== "string") delete temporal[field];
852
+ }
853
+ for (const field of [
854
+ "participants",
855
+ "participantNodeIds",
856
+ "beforeEventIds",
857
+ "afterEventIds",
858
+ "supersedesEventIds",
859
+ "conflictsWithEventIds",
860
+ "relatedEventIds"
861
+ ]) {
862
+ const values = temporal[field];
863
+ if (Array.isArray(values) && [...values].every((item) => typeof item === "string")) {
864
+ temporal[field] = [...values];
865
+ } else {
866
+ delete temporal[field];
867
+ }
868
+ }
869
+ return temporal;
870
+ }
837
871
  function normalizeStandardEventType(value) {
838
872
  const normalized = normalizeSearchText(value ?? "").replace(/[\s_-]+/g, "");
839
873
  const aliases = {
@@ -1632,13 +1666,16 @@ var PROFILE_FIELDS = {
1632
1666
  assistantPreferredName: { label: "Assistant preferred name", maxLength: 100 },
1633
1667
  preferredLanguage: { label: "Preferred answer language", maxLength: 100 },
1634
1668
  reasoningLanguage: { label: "Preferred visible reasoning language", maxLength: 100 },
1669
+ defaultLocation: { label: "Default location (when the task specifies no location)", maxLength: 200 },
1670
+ homeCity: { label: "Usual city of residence", maxLength: 100 },
1671
+ currentCity: { label: "Current city (until updated or cleared)", maxLength: 100 },
1635
1672
  responsePreferences: { label: "Response preferences", maxLength: 1e3 },
1636
1673
  standingInstructions: { label: "Standing instructions", maxLength: 1e3 },
1637
1674
  userBackground: { label: "User background", maxLength: 1500 },
1638
1675
  longTermGoals: { label: "Long-term goals", maxLength: 1e3 },
1639
1676
  persistentNotes: { label: "Persistent notes", maxLength: 1200 }
1640
1677
  };
1641
- var PROFILE_PROTECTED_SHORT_FIELDS = ["userPreferredName", "assistantPreferredName", "preferredLanguage", "reasoningLanguage"];
1678
+ var PROFILE_PROTECTED_SHORT_FIELDS = ["userPreferredName", "assistantPreferredName", "preferredLanguage", "reasoningLanguage", "defaultLocation", "homeCity", "currentCity"];
1642
1679
  var PROFILE_TOTAL_MAX_LENGTH = 6e3;
1643
1680
  var PROFILE_MAINTENANCE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1644
1681
  var PROFILE_MAINTENANCE_TOTAL_THRESHOLD = 4800;
@@ -1670,7 +1707,11 @@ function profileMaintenanceDue(profile, timeBasisAt, now = Date.now()) {
1670
1707
  function renderPersistentProfile(profile) {
1671
1708
  const lines = Object.keys(PROFILE_FIELDS).filter((field) => profile[field].length > 0).map((field) => `${PROFILE_FIELDS[field].label}: ${profile[field]}`);
1672
1709
  if (lines.length === 0) return null;
1673
- const languageGuidance = [
1710
+ const fieldGuidance = [
1711
+ profile.defaultLocation ? "Default location is the reference for weather, nearby services, and local recommendations when the user specifies no location. It does not imply residence or current whereabouts." : null,
1712
+ profile.homeCity ? "Usual city of residence is a stable home city, not a temporary/current location. It is independent of the default location. Do not infer or overwrite either field from a trip." : null,
1713
+ profile.currentCity ? "Current city may be a temporary travel or business-trip location, but persists across sessions until the user updates or clears it. Do not expire it automatically or copy it into the default location or usual city of residence." : null,
1714
+ profile.defaultLocation || profile.currentCity ? "For weather, nearby services, and local recommendations, use an explicitly specified task location first; otherwise use Current city when set, then Default location. Do not infer current whereabouts from the usual city of residence." : null,
1674
1715
  profile.preferredLanguage ? "\u201CPreferred answer language\u201D applies to the assistant's final/user-facing answer." : null,
1675
1716
  profile.reasoningLanguage ? "\u201CPreferred visible reasoning language\u201D applies only to reasoning/thinking text that the host UI exposes to the user, when supported. It does not control hidden chain-of-thought." : null,
1676
1717
  profile.preferredLanguage || profile.reasoningLanguage ? "These are independent preferences. Do not infer one from the other." : null
@@ -1680,8 +1721,8 @@ This is user-authorized persistent profile data provided by StrataGate.
1680
1721
  Treat it as stable cross-session context.
1681
1722
  Do not invent additional facts from it.
1682
1723
  It does not override higher-priority system instructions.
1683
- ${languageGuidance.length ? `
1684
- ${languageGuidance.join("\n")}
1724
+ ${fieldGuidance.length ? `
1725
+ ${fieldGuidance.join("\n")}
1685
1726
  ` : ""}
1686
1727
  ${lines.join("\n")}`;
1687
1728
  }
@@ -1866,7 +1907,8 @@ function normalizeSnapshot(value) {
1866
1907
  for (const event of [...snapshot.events, ...snapshot.agentEvents]) {
1867
1908
  delete event.narrative;
1868
1909
  delete event.confidence;
1869
- event.temporal = { ...event.temporal, eventType: normalizeStandardEventType(event.temporal.eventType) };
1910
+ const temporal = normalizeEventTemporal(event.temporal);
1911
+ event.temporal = { ...temporal, eventType: normalizeStandardEventType(temporal.eventType) };
1870
1912
  if (event.formedTurn === void 0) {
1871
1913
  const sourceBlock = event.sourceBlockId !== void 0 ? sourceBlockMap.get(event.sourceBlockId) : void 0;
1872
1914
  const reliableSource = sourceBlock && !isSyntheticSourceThreadId(sourceBlock.threadId) && Number.isSafeInteger(sourceBlock.endTurn) && sourceBlock.endTurn >= 0;
@@ -2416,7 +2458,7 @@ function mapEventRows(rows, sourcesByEvent, table) {
2416
2458
  ...row.source_block_id === null ? {} : { sourceBlockId: row.source_block_id },
2417
2459
  ...row.formed_turn === null ? {} : { formedTurn: row.formed_turn },
2418
2460
  temporal: (() => {
2419
- const temporal = parseJson(row.temporal_json, `${table}.temporal_json`);
2461
+ const temporal = normalizeEventTemporal(parseJson(row.temporal_json, `${table}.temporal_json`));
2420
2462
  return { ...temporal, eventType: normalizeStandardEventType(temporal.eventType) };
2421
2463
  })(),
2422
2464
  scope: row.scope,
@@ -4632,6 +4674,7 @@ var StrataGate = class _StrataGate {
4632
4674
  }
4633
4675
  /** Rank one event pool (BM25 + structured filters fused by RRF), sliced to `limit`. */
4634
4676
  rankEventPool(candidates, query, options, limit) {
4677
+ for (const event of candidates) event.temporal = normalizeEventTemporal(event.temporal);
4635
4678
  const participants = (options.participants ?? []).map(normalizeSearchText).filter(Boolean);
4636
4679
  const eventType = normalizeSearchText(options.eventType ?? "");
4637
4680
  const from = options.happenedFrom ? Date.parse(options.happenedFrom) : Number.NEGATIVE_INFINITY;
@@ -4757,7 +4800,7 @@ var StrataGate = class _StrataGate {
4757
4800
  event.title,
4758
4801
  event.summary,
4759
4802
  event.tags.join(" "),
4760
- (event.temporal.participants ?? []).join(" ")
4803
+ (normalizeEventTemporal(event.temporal).participants ?? []).join(" ")
4761
4804
  ].join(" ")).join(" "));
4762
4805
  const effectiveViews = this.graphNodes.flatMap((node) => effectiveGraphNodeView(node, this.graphEdges, this.listAllEvents()) ?? []);
4763
4806
  const effectiveNodes = effectiveViews.map(({ node }) => node);
@@ -5390,6 +5433,7 @@ var StrataGate = class _StrataGate {
5390
5433
  });
5391
5434
  }
5392
5435
  storeEventCard(input, pool, parts) {
5436
+ const temporal = normalizeEventTemporal(input.temporal);
5393
5437
  const event = {
5394
5438
  id: input.id ?? this.idFactory("evt"),
5395
5439
  formedTurn: parts.formedTurn,
@@ -5400,8 +5444,8 @@ var StrataGate = class _StrataGate {
5400
5444
  sourceMessageIds: parts.sourceMessageIds,
5401
5445
  ...parts.sourceBlockId !== void 0 ? { sourceBlockId: parts.sourceBlockId } : {},
5402
5446
  temporal: {
5403
- ...input.temporal ? { ...input.temporal } : { mentionedAt: parts.now },
5404
- eventType: normalizeStandardEventType(input.temporal?.eventType)
5447
+ ...input.temporal ? temporal : { mentionedAt: parts.now },
5448
+ eventType: normalizeStandardEventType(temporal.eventType)
5405
5449
  },
5406
5450
  scope: input.scope ?? "user",
5407
5451
  criticality: parts.criticality,
@@ -5930,7 +5974,7 @@ var StrataGate = class _StrataGate {
5930
5974
  previous: threadBlocks.slice(0, targetIndex).reverse().find((block) => block.l2Keypoints !== void 0) ?? null,
5931
5975
  target,
5932
5976
  next,
5933
- timeline: [...timelineEvents.values()].map((event) => ({ id: event.id, title: event.title, temporal: structuredClone(event.temporal) }))
5977
+ timeline: [...timelineEvents.values()].map((event) => ({ id: event.id, title: event.title, temporal: normalizeEventTemporal(structuredClone(event.temporal)) }))
5934
5978
  });
5935
5979
  } catch (error) {
5936
5980
  await this.commitMutation(() => {
@@ -6713,7 +6757,7 @@ var STRUCTURED_TOOLS = {
6713
6757
  },
6714
6758
  profileMaintenance: {
6715
6759
  name: "stratagate_maintain_profile",
6716
- description: "Return the same nine Persistent Profile fields with only safe wording and redundancy cleanup; preserve the four short fields apart from necessary whitespace cleanup.",
6760
+ description: `Return the same ${Object.keys(PROFILE_FIELDS).length} Persistent Profile fields with only safe wording and redundancy cleanup; preserve ${PROFILE_PROTECTED_SHORT_FIELDS.join(", ")} apart from necessary whitespace cleanup. Default location and usual city of residence are independent stable fields, not temporary/current location. Current city persists until updated or cleared; never expire or merge it with those stable fields.`,
6717
6761
  parameters: PROFILE_MAINTENANCE_PARAMETERS
6718
6762
  }
6719
6763
  };
@@ -6865,7 +6909,7 @@ Use project scope for repository decisions, user scope for stable preferences/id
6865
6909
  quotes: strings2(item.quotes).slice(0, 12),
6866
6910
  sourceMessageIds,
6867
6911
  sourceBlockId: context.target.id,
6868
- temporal: object(item.temporal),
6912
+ temporal: normalizeEventTemporal(item.temporal),
6869
6913
  scope,
6870
6914
  criticality
6871
6915
  };
@@ -7021,7 +7065,7 @@ Use project scope for repository decisions, user scope for stable preferences/id
7021
7065
  async maintainProfile(profile) {
7022
7066
  const raw = object(await this.callStructured(
7023
7067
  "profileMaintenance",
7024
- `You maintain only the supplied StrataGate Persistent Profile. Call ${STRUCTURED_TOOLS.profileMaintenance.name} exactly once with all nine string fields. You may deduplicate, merge repeated meaning, shorten redundant wording, and improve organization. Preserve every unique fact, uncertainty, constraint, and instruction. Never infer or add facts, broaden meaning, or read Event, Graph, or conversation history. If two statements might conflict or cannot safely merge, retain both. Keep userPreferredName, assistantPreferredName, preferredLanguage, and reasoningLanguage unchanged except necessary whitespace cleanup. preferredLanguage and reasoningLanguage are independent: never infer, copy, or merge either language field into the other. reasoningLanguage is only for user-visible reasoning/thinking text when supported, not hidden chain-of-thought. Character limits (Unicode code points): ${JSON.stringify(Object.fromEntries(Object.entries(PROFILE_FIELDS).map(([field, spec]) => [field, spec.maxLength])))}. Total maximum: 6000. If safe compression is impossible, return the original value.`,
7068
+ `You maintain only the supplied StrataGate Persistent Profile. Call ${STRUCTURED_TOOLS.profileMaintenance.name} exactly once with all ${Object.keys(PROFILE_FIELDS).length} string fields. You may deduplicate, merge repeated meaning, shorten redundant wording, and improve organization. Preserve every unique fact, uncertainty, constraint, and instruction. Never infer or add facts, broaden meaning, or read Event, Graph, or conversation history. If two statements might conflict or cannot safely merge, retain both. Keep ${PROFILE_PROTECTED_SHORT_FIELDS.join(", ")} unchanged except necessary whitespace cleanup. defaultLocation is the reference when a task specifies no location and no currentCity is set; homeCity is the stable city of residence. They are independent, neither means temporary/current location, and travel must not overwrite either. currentCity is the current location, possibly temporary travel or business travel; it persists across sessions until the user updates or clears it. Never expire currentCity automatically, clear it because a trip seems temporary, or copy it into either stable field. preferredLanguage and reasoningLanguage are independent: never infer, copy, or merge either language field into the other. reasoningLanguage is only for user-visible reasoning/thinking text when supported, not hidden chain-of-thought. Character limits (Unicode code points): ${JSON.stringify(Object.fromEntries(Object.entries(PROFILE_FIELDS).map(([field, spec]) => [field, spec.maxLength])))}. Total maximum: 6000. If safe compression is impossible, return the original value.`,
7025
7069
  { profile, fieldDefinitions: PROFILE_FIELDS }
7026
7070
  ));
7027
7071
  if (Object.keys(raw).length !== Object.keys(PROFILE_FIELDS).length || Object.keys(raw).some((field) => !(field in PROFILE_FIELDS))) {
@@ -9843,17 +9887,21 @@ function sessionOf(exec) {
9843
9887
  function registerMemoryTools(ctx, runtime) {
9844
9888
  ctx.tools.register(defineTool({
9845
9889
  name: "memory_profile_update",
9846
- description: `This tool is provided by the StrataGate plugin. \u4FEE\u6539\u7528\u6237\u5E38\u9A7B\u753B\u50CF\u4E2D\u7684\u4E00\u4E2A\u56FA\u5B9A\u5B57\u6BB5\uFF1A\u7528\u6237\u79F0\u547C\u3001\u52A9\u624B\u540D\u5B57\u3001\u9ED8\u8BA4\u56DE\u7B54\u8BED\u8A00\u3001\u601D\u8003\u8FC7\u7A0B\u8BED\u8A00\u3001\u56DE\u590D\u65B9\u5F0F\u548C\u98CE\u683C\u504F\u597D\u3001\u957F\u671F\u6301\u7EED\u751F\u6548\u7684\u8981\u6C42\u3001\u7A33\u5B9A\u7684\u7528\u6237\u80CC\u666F\u3001\u957F\u671F\u76EE\u6807\uFF0C\u6216\u5176\u4ED6\u5FC5\u987B\u5E38\u9A7B\u7684\u4FE1\u606F\u3002
9890
+ description: `This tool is provided by the StrataGate plugin. \u4FEE\u6539\u7528\u6237\u5E38\u9A7B\u753B\u50CF\u4E2D\u7684\u4E00\u4E2A\u56FA\u5B9A\u5B57\u6BB5\uFF1A\u7528\u6237\u79F0\u547C\u3001\u52A9\u624B\u540D\u5B57\u3001\u9ED8\u8BA4\u56DE\u7B54\u8BED\u8A00\u3001\u601D\u8003\u8FC7\u7A0B\u8BED\u8A00\u3001\u5E38\u9A7B/\u9ED8\u8BA4\u5730\u5740\u3001\u5F53\u524D\u6240\u5728\u57CE\u5E02\u3001\u539F\u6709\u5E38\u9A7B\u57CE\u5E02\u3001\u56DE\u590D\u65B9\u5F0F\u548C\u98CE\u683C\u504F\u597D\u3001\u957F\u671F\u6301\u7EED\u751F\u6548\u7684\u8981\u6C42\u3001\u7A33\u5B9A\u7684\u7528\u6237\u80CC\u666F\u3001\u957F\u671F\u76EE\u6807\uFF0C\u6216\u5176\u4ED6\u5FC5\u987B\u5E38\u9A7B\u7684\u4FE1\u606F\u3002
9847
9891
 
9848
9892
  \u8FD9\u4E9B\u4FE1\u606F\u4F1A\u81EA\u52A8\u63D0\u4F9B\u7ED9\u540E\u7EED\u6BCF\u6B21\u5BF9\u8BDD\uFF0C\u65E0\u9700\u68C0\u7D22\u3002\u53EA\u6709\u786E\u5B9E\u9700\u8981\u6301\u7EED\u653E\u5728\u4E0A\u4E0B\u6587\u4E2D\u7684\u4FE1\u606F\uFF0C\u624D\u9002\u5408\u5199\u5165\u753B\u50CF\u3002
9849
9893
 
9894
+ \u7A33\u5B9A\u7684\u9ED8\u8BA4\u5730\u70B9\uFF08defaultLocation\uFF09\u548C\u5E38\u9A7B\u57CE\u5E02\uFF08homeCity\uFF09\u4F18\u5148\u5C5E\u4E8E Profile\uFF1A\u9ED8\u8BA4\u5730\u70B9\u662F\u7528\u6237\u672A\u6307\u5B9A\u5730\u70B9\u65F6\uFF0C\u5929\u6C14\u3001\u9644\u8FD1\u670D\u52A1\u3001\u672C\u5730\u63A8\u8350\u7B49\u4EFB\u52A1\u7684\u9ED8\u8BA4\u53C2\u8003\uFF1B\u5E38\u9A7B\u57CE\u5E02\u662F\u7528\u6237\u7A33\u5B9A\u5C45\u4F4F\u6216\u5E38\u9A7B\u7684\u57CE\u5E02\u3002\u4E24\u4E2A\u5B57\u6BB5\u72EC\u7ACB\uFF0C\u4E0D\u80FD\u4E92\u76F8\u63A8\u65AD\uFF0C\u4E5F\u4E0D\u4EE3\u8868\u5F53\u524D\u6240\u5728\u5730\u3002\u4E00\u6B21\u65C5\u884C\u6216\u5F53\u524D\u4E34\u65F6\u4F4D\u7F6E\u4E0D\u80FD\u81EA\u52A8\u8986\u76D6\u7A33\u5B9A\u5B57\u6BB5\uFF1B\u66F4\u65B0\u5730\u70B9\u5B57\u6BB5\u540C\u6837\u9075\u5B88\u4E0B\u8FF0\u660E\u786E\u8BF7\u6C42\u6216\u786E\u8BA4\u6388\u6743\u89C4\u5219\u3002
9895
+
9896
+ \u5F53\u524D\u6240\u5728\u57CE\u5E02\uFF08currentCity\uFF09\u4E5F\u5C5E\u4E8E Profile\uFF1A\u53EF\u4EE5\u662F\u65C5\u6E38\u3001\u51FA\u5DEE\u65F6\u7684\u4E34\u65F6\u57CE\u5E02\uFF0C\u4F46\u4F1A\u8DE8\u4F1A\u8BDD\u6301\u7EED\u6CE8\u5165\uFF0C\u4FDD\u5B58\u5230\u7528\u6237\u4E0B\u6B21\u4FEE\u6539\u6216\u6E05\u7A7A\u4E3A\u6B62\uFF0C\u4E0D\u81EA\u52A8\u8FC7\u671F\u3002\u201C\u4E34\u65F6\u201D\u63CF\u8FF0\u6240\u5728\u5730\u7684\u6027\u8D28\uFF0C\u4E0D\u662F\u4FDD\u5B58\u65F6\u957F\u3002\u7528\u6237\u8981\u6C42\u8BB0\u4F4F\u5F53\u524D\u65C5\u884C/\u51FA\u5DEE\u57CE\u5E02\u65F6\uFF0C\u66F4\u65B0 currentCity\uFF0C\u4E0D\u4FEE\u6539 defaultLocation \u6216 homeCity\uFF1B\u8BA1\u5212\u5C06\u6765\u53BB\u3001\u5DF2\u7ECF\u7ED3\u675F\u7684\u65C5\u884C\u6216\u5386\u53F2\u5730\u70B9\u4E0D\u80FD\u5F53\u4F5C\u5F53\u524D\u57CE\u5E02\u3002\u5BF9\u672A\u6307\u5B9A\u5730\u70B9\u7684\u5929\u6C14\u3001\u9644\u8FD1\u670D\u52A1\u3001\u672C\u5730\u63A8\u8350\u7B49\u4EFB\u52A1\uFF0C\u4F18\u5148\u53C2\u8003\u5DF2\u8BBE\u7F6E\u7684 currentCity\uFF0C\u7A7A\u503C\u65F6\u518D\u53C2\u8003 defaultLocation\uFF1B\u4EFB\u52A1\u660E\u786E\u6307\u5B9A\u7684\u5730\u70B9\u4F18\u5148\u3002\u66F4\u65B0\u6216\u6E05\u7A7A currentCity \u4ECD\u9075\u5B88\u540C\u4E00 Profile \u6388\u6743\u89C4\u5219\u3002
9897
+
9850
9898
  \u9ED8\u8BA4\u56DE\u7B54\u8BED\u8A00\u53EA\u63A7\u5236\u6700\u7EC8\u9762\u5411\u7528\u6237\u7684\u56DE\u7B54\uFF1B\u601D\u8003\u8FC7\u7A0B\u8BED\u8A00\u53EA\u63A7\u5236\u5BBF\u4E3B\u754C\u9762\u652F\u6301\u65F6\u7528\u6237\u53EF\u89C1\u7684\u601D\u8003\u6587\u672C\uFF0C\u4E0D\u63A7\u5236\u9690\u85CF\u63A8\u7406\u3002\u4E24\u8005\u662F\u72EC\u7ACB\u5B57\u6BB5\uFF0C\u53EA\u4FEE\u6539\u7528\u6237\u6307\u5B9A\u7684\u5B57\u6BB5\u3002\u4F8B\u5982\u201C\u4EE5\u540E\u90FD\u7528\u4E2D\u6587\u56DE\u7B54\u6211\u201D\u53EA\u4FEE\u6539 preferredLanguage\uFF0C\u201C\u4EE5\u540E\u601D\u8003\u8FC7\u7A0B\u7528\u4E2D\u6587\u201D\u53EA\u4FEE\u6539 reasoningLanguage\uFF1B\u4E24\u8005\u90FD\u8981\u6C42\u65F6\u5206\u522B\u8C03\u7528\u4E24\u6B21\u3002
9851
9899
 
9852
9900
  \u6BCF\u6B21\u8C03\u7528\u53EA\u4FEE\u6539\u4E00\u4E2A\u5DF2\u6709\u5B57\u6BB5\uFF0C\u4E0D\u65B0\u589E\u3001\u5220\u9664\u6216\u6539\u540D\u5B57\u6BB5\uFF0C\u4E5F\u4E0D\u4E3A\u4FEE\u6539\u4E00\u4E2A\u5B57\u6BB5\u91CD\u5199\u6574\u4EFD\u753B\u50CF\u3002\u7528\u6237\u660E\u786E\u8981\u6C42\u4FEE\u6539\u65F6\u53EF\u76F4\u63A5\u6267\u884C\uFF1B\u5982\u679C\u662F\u4F60\u4E3B\u52A8\u5EFA\u8BAE\u4FEE\u6539\uFF0C\u987B\u5148\u8BF4\u660E\u5177\u4F53\u5B57\u6BB5\u548C\u65B0\u503C\u3002\u53EA\u6709\u7528\u6237\u7D27\u63A5\u7740\u660E\u786E\u56DE\u590D\u201C\u540C\u610F\u201D\uFF0C\u624D\u6388\u6743\u8FD9\u4E00\u6B21\u4FEE\u6539\uFF1B\u6C89\u9ED8\u3001\u62D2\u7EDD\u3001\u6362\u8BDD\u9898\u6216\u63D0\u51FA\u4E0D\u540C\u4FEE\u6539\u90FD\u4E0D\u7B97\u6388\u6743\u3002
9853
9901
 
9854
9902
  \u53EA\u9700\u5728\u76F8\u5173\u60C5\u5883\u4E2D\u60F3\u8D77\u7684\u9879\u76EE\u4E8B\u5B9E\u3001\u7ECF\u5386\u3001\u51B3\u5B9A\u6216\u504F\u597D\uFF0C\u8BF7\u4F7F\u7528 memory_remember\uFF1B\u4EC5\u672C\u6B21\u6709\u6548\u7684\u8981\u6C42\u4E0D\u5199\u5165\u8BB0\u5FC6\u3002\u4E0D\u80FD\u56E0\u4E3A\u7528\u6237\u8BF4\u4E86\u201C\u8BB0\u4F4F\u201D\u5C31\u81EA\u52A8\u9009\u62E9\u672C\u5DE5\u5177\uFF0C\u4E5F\u4E0D\u80FD\u5728 memory_remember \u4E0D\u53EF\u7528\u65F6\u628A Event \u4FE1\u606F\u6539\u5199\u6210\u5E38\u9A7B\u753B\u50CF\u3002`,
9855
9903
  parameters: {
9856
- field: { type: "string", required: true, enum: ["userPreferredName", "assistantPreferredName", "preferredLanguage", "reasoningLanguage", "responsePreferences", "standingInstructions", "userBackground", "longTermGoals", "persistentNotes"] },
9904
+ field: { type: "string", required: true, enum: ["userPreferredName", "assistantPreferredName", "preferredLanguage", "reasoningLanguage", "defaultLocation", "homeCity", "currentCity", "responsePreferences", "standingInstructions", "userBackground", "longTermGoals", "persistentNotes"] },
9857
9905
  value: { type: "string", required: true }
9858
9906
  },
9859
9907
  output: jsonOutput,
@@ -9986,7 +10034,7 @@ function registerMemoryTools(ctx, runtime) {
9986
10034
  }));
9987
10035
  ctx.tools.register(defineTool({
9988
10036
  name: "memory_assess",
9989
- description: "This tool is provided by the StrataGate plugin. Apply StrataGate Evidence Gate to a retrieval batch. Pass batch_id from the retrieval result; omitting it remains compatible with sequential flows and selects the latest batch. The response reports every input ref that was not adopted and why.",
10037
+ description: "This tool is provided by the StrataGate plugin. Apply StrataGate Evidence Gate to a retrieval batch BEFORE the final user answer. Assess evidence, close all batches with memory_record_use, then output the user answer. Pass batch_id from the retrieval result; omitting it remains compatible with sequential flows and selects the latest batch. The response reports every input ref that was not adopted and why.",
9990
10038
  parameters: {
9991
10039
  batch_id: { type: "string", description: "The batchId returned by the retrieval to assess. Omit only in a strictly sequential flow." },
9992
10040
  verdict: { type: "string", enum: ["sufficient", "partial", "wrong"], required: true },
@@ -10004,7 +10052,7 @@ function registerMemoryTools(ctx, runtime) {
10004
10052
  }));
10005
10053
  ctx.tools.register(defineTool({
10006
10054
  name: "memory_record_use",
10007
- description: "This tool is provided by the StrataGate plugin. Close one StrataGate retrieval batch. Pass its batch_id and exactly the evidenceRefs from that batch actually used in the answer, or [] when none were used. Non-empty refs require that batch's sufficient assessment. The host renders successful selections as answer-tail citations, so do not write a manual citation list. Omitting batch_id selects the latest batch for sequential compatibility.",
10055
+ description: "This tool is provided by the StrataGate plugin. Close one StrataGate retrieval batch BEFORE outputting the final user answer. Complete memory_assess / memory_record_use as pre-answer evidence processing for ALL batches, then generate the final user answer. Pass its batch_id and exactly the evidenceRefs from that batch actually adopted for the answer, or [] when none are used. Non-empty refs require that batch's sufficient assessment. Never write the answer first and record usage afterwards. Do not turn this tool's internal receipt or batch status into a user-facing answer. The host renders successful selections as answer-tail citations, so do not write a manual citation list. Omitting batch_id selects the latest batch for sequential compatibility.",
10008
10056
  parameters: {
10009
10057
  batch_id: { type: "string", description: "The batchId to close. Omit only in a strictly sequential flow." },
10010
10058
  evidence_refs: { type: "array", items: { type: "string" }, required: true }
@@ -11387,6 +11435,78 @@ function registerAdminRoutes(ctx, runtime) {
11387
11435
  });
11388
11436
  }
11389
11437
 
11438
+ // src/retrieval-finalization.ts
11439
+ import { createUserMessage as createUserMessage3 } from "@deepseek-ai/dsh-llm";
11440
+ var MAX_ANSWER_ATTEMPTS = 3;
11441
+ function isCompleteAnswer(data) {
11442
+ if (data.interrupted || data.message.content.some((block) => block.type === "tool-call")) return false;
11443
+ if (!data.message.content.some((block) => block.type === "text" && block.text.trim().length > 0)) return false;
11444
+ const stream = data.stream ?? [];
11445
+ for (let index = stream.length - 1; index >= 0; index--) {
11446
+ const record2 = stream[index];
11447
+ if (record2.type === "chunk" && record2.chunk.type === "finish") return record2.chunk.reason.kind === "stop";
11448
+ }
11449
+ return true;
11450
+ }
11451
+ function answerFailure() {
11452
+ return new Error(`StrataGate final answer recovery failed after ${MAX_ANSWER_ATTEMPTS} attempts: no complete non-empty user-visible answer was confirmed. Retry the turn.`);
11453
+ }
11454
+ var FINAL_ANSWER_INSTRUCTION = "Resume the original real user request in this turn, not the plugin instructions. All omitted memory receipts have now been handled. Now produce the complete final user-facing answer as the last assistant text. If you already gave the answer before the receipts, restate it in full, preserving its supported facts, conclusions, and requested deliverables; do not merely refer to the earlier answer. Use only evidence actually adopted for that answer. Do not perform new retrieval or other tool work. Do not report batch closure, evidence-use bookkeeping, unresolvedBatchIds, or other StrataGate internal status; these are not the user's answer.";
11455
+ function installRetrievalFinalization(ctx, runtime) {
11456
+ const recoveries = /* @__PURE__ */ new WeakMap();
11457
+ const steer = (agent, text3) => agent.steer(createUserMessage3({
11458
+ content: [{ type: "text", text: text3 }],
11459
+ source: dshMessageSource("instructions")
11460
+ }));
11461
+ ctx.on("agent/turn-stopping", ({ agent, turn, signal }) => {
11462
+ if (signal?.aborted) return;
11463
+ const pending = runtime.pendingBatchIds(agent.session);
11464
+ const recovery = recoveries.get(agent.session);
11465
+ if (pending.length === 0) {
11466
+ if (!recovery) return;
11467
+ if (recovery.phase === "answer-complete") return;
11468
+ if (recovery.answerAttempts >= MAX_ANSWER_ATTEMPTS) throw answerFailure();
11469
+ steer(agent, FINAL_ANSWER_INSTRUCTION);
11470
+ return;
11471
+ }
11472
+ if (recovery && recovery.phase !== "closing") {
11473
+ throw new Error("StrataGate final answer recovery created a new retrieval batch; final answer cannot be confirmed.");
11474
+ }
11475
+ if (recovery && (recovery.remainingStops === 0 || ![...recovery.pending].some((id) => !pending.includes(id)))) {
11476
+ throw new Error("StrataGate retrieval cleanup made no progress; unresolved batches remain. Retry the turn to complete memory_record_use.");
11477
+ }
11478
+ recoveries.set(agent.session, { turn, phase: "closing", pending: new Set(pending), remainingStops: recovery ? recovery.remainingStops - 1 : pending.length, answerAttempts: 0 });
11479
+ steer(agent, `StrataGate retrieval batches are still unresolved: ${pending.join(", ")}. Before answering, close every one with memory_record_use using its batch_id and exactly the evidence_refs actually adopted for the user answer, or [] if none were used. Non-empty refs still require that batch's sufficient memory_assess; do not reinforce evidence merely because it was retrieved. This is only the evidence-processing phase. Do not write a final answer or a batch-status explanation yet. After all receipts succeed, you will receive an instruction to produce the complete user answer again.`);
11480
+ });
11481
+ ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
11482
+ const assembled = await next();
11483
+ const recovery = context.agent && recoveries.get(context.agent.session);
11484
+ if (!recovery || !context.agent || context.signal?.aborted) return assembled;
11485
+ if (runtime.needsRecordUse(context.agent.session)) {
11486
+ if (recovery.phase !== "closing") throw new Error("StrataGate final answer recovery created a new retrieval batch; final answer cannot be confirmed.");
11487
+ return assembled;
11488
+ }
11489
+ if (recovery.phase !== "answer-complete" && recovery.answerAttempts >= MAX_ANSWER_ATTEMPTS) throw answerFailure();
11490
+ return { ...assembled, tools: [], contexts: [...assembled.contexts, { name: "stratagate:final-answer-recovery", text: FINAL_ANSWER_INSTRUCTION }] };
11491
+ });
11492
+ ctx.on("session/event", (session, event) => {
11493
+ const recovery = recoveries.get(session);
11494
+ if (!recovery) return;
11495
+ if (event.type === "turn/end" && event.data.turn === recovery.turn) {
11496
+ recoveries.delete(session);
11497
+ } else if (event.type === "step/start" && event.data.turn === recovery.turn) {
11498
+ if (recovery.phase === "closing" && runtime.needsRecordUse(session)) return;
11499
+ recovery.phase = "awaiting-answer";
11500
+ recovery.answerStep = event.data.step;
11501
+ recovery.answerAttempts++;
11502
+ } else if (event.type === "assistant/message" && event.data.turn === recovery.turn && event.data.step === recovery.answerStep) {
11503
+ recovery.phase = isCompleteAnswer(event.data) ? "answer-complete" : "awaiting-answer";
11504
+ } else if (event.type === "assistant/attempt" && event.data.turn === recovery.turn && event.data.step === recovery.answerStep) {
11505
+ recovery.phase = "awaiting-answer";
11506
+ }
11507
+ });
11508
+ }
11509
+
11390
11510
  // src/legacy-session.ts
11391
11511
  import * as zlib from "node:zlib";
11392
11512
  import { constants as fsConstants } from "node:fs";
@@ -11736,11 +11856,14 @@ Memory use:
11736
11856
  - For explicit retrieval, treat relevance and sufficiency separately. Mark evidence sufficient only when it directly supports all material parts needed for the answer; partial when relevant evidence exists but important facts, time, relationships, or source details are missing; wrong when the retrieved evidence does not support the requested claim or refers to a different subject.
11737
11857
  - If evidence is partial or wrong, follow nextStrategy with a targeted next step: refine the Event or Graph search, expand the relevant Event, Graph node, or Block, or inspect raw memory. Do not repeat the same failed search unchanged, and do not present uncertain memory as fact.
11738
11858
  - Only evidence actually used in the final answer or action may be reinforced.
11859
+ - Every explicit retrieval batch must be closed BEFORE you begin outputting the final user-facing answer. The required order is retrieval \u2192 Evidence Gate (memory_assess) \u2192 memory_record_use for every batch \u2192 final user answer. Assessment and usage recording are pre-answer evidence processing. Decide which evidence the answer will actually adopt, assess it, and record only those refs; close unused batches with evidence_refs = []. Never write the user answer first and append receipts afterwards. After memory_record_use, generate the user answer; never turn its internal batch/receipt status into the final answer.
11739
11860
 
11740
11861
  Memory writing:
11741
11862
 
11742
11863
  - Choose by scope rather than the word "remember": memory_profile_update is for always-on global Profile fields supplied to future conversations without retrieval; memory_remember is for durable information that should surface when relevant; information that matters only to the current turn needs neither. Store the same information in one place by default.
11743
11864
  - Use memory_profile_update only for information that belongs in a Profile field. Explicit user requests may be applied directly; inferred changes must follow the tool's consent rule. Final-answer language and visible-reasoning language are independent fields. Do not use memory_remember to bypass Profile consent.
11865
+ - Stable default location (defaultLocation: reference for weather, nearby services, or local recommendations when no location is specified) and usual city of residence (homeCity) belong preferentially in the always-on Profile. They are independent: do not infer either from the other or from a current/temporary location or a trip. Temporary travel must not automatically overwrite these stable fields. Follow the same Profile consent rule.
11866
+ - Current city (currentCity) also belongs in the always-on Profile. Travel or business-trip location can be temporary while its saved value persists across sessions until the user updates or clears it; do not expire it automatically. A user request to remember their current trip city belongs here, not in retrieval-dependent memory_remember. Future trip plans, completed trips, and historical locations do not establish current whereabouts. Never copy currentCity into defaultLocation or homeCity. For local tasks use an explicit task location first, otherwise currentCity when set, then defaultLocation. Follow the same Profile consent rule for updates and clearing.
11744
11867
  - Use memory_remember for durable project facts, past decisions, corrections, experiences, and context-specific preferences. Record one self-contained, grounded fact per call, with necessary project, time, and scope. Never record speculation, secrets, credentials, or transient task state.`;
11745
11868
  var FEEDBACK_PROTOCOL = `[StrataGate feedback policy]
11746
11869
  The feedback_prepare tool creates a local draft for the user to review; it never submits the draft.
@@ -11834,16 +11957,7 @@ async function apply(ctx, config) {
11834
11957
  if (feedbackSuggestion) contexts.push({ name: "stratagate:feedback-suggestion", text: feedbackSuggestion });
11835
11958
  return { ...assembled, contexts };
11836
11959
  });
11837
- ctx.on("agent/turn-stopping", ({ agent }) => {
11838
- if (!runtime.needsRecordUse(agent.session)) return;
11839
- agent.steer(createUserMessage3({
11840
- content: [{
11841
- type: "text",
11842
- text: `StrataGate retrieval batches are still unresolved: ${runtime.pendingBatchIds(agent.session).join(", ")}. Before ending this turn, close each one with memory_record_use using its batch_id and evidence_refs set to exactly the refs from that batch used in the answer, or [] if none were used.`
11843
- }],
11844
- source: dshMessageSource("instructions")
11845
- }));
11846
- });
11960
+ installRetrievalFinalization(ctx, runtime);
11847
11961
  registerMemoryTools(ctx, runtime);
11848
11962
  ctx.on("tools/result", (exec, result) => {
11849
11963
  if (!result.isError || !exec.agent) return;