stratagate-dsh 0.2.38 → 0.2.39

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/index.js CHANGED
@@ -1263,7 +1263,7 @@ function normalizeRetrievalAssessment(input, batchEvidenceRefs) {
1263
1263
  }
1264
1264
 
1265
1265
  // packages/core/src/storage.ts
1266
- var STRATAGATE_STORAGE_SCHEMA_VERSION = 9;
1266
+ var STRATAGATE_STORAGE_SCHEMA_VERSION = 10;
1267
1267
  var KNOWLEDGE_GRAPH_PROJECTOR_VERSION = 1;
1268
1268
  var StorageConflictError = class extends Error {
1269
1269
  constructor(namespace, expectedRevision, actualRevision) {
@@ -1387,7 +1387,15 @@ function normalizeSnapshot(value) {
1387
1387
  schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1388
1388
  blocks: readyLegacyBlocks(legacy.blocks),
1389
1389
  summaryJobs: [],
1390
- extractionJobs: legacyExtractionJobs(legacy.extractionJobs)
1390
+ extractionJobs: legacyExtractionJobs(legacy.extractionJobs),
1391
+ externalMemoryImportJobs: []
1392
+ };
1393
+ } else if (schemaVersion === 9) {
1394
+ const legacy = value;
1395
+ snapshot = {
1396
+ ...structuredClone(legacy),
1397
+ schemaVersion: STRATAGATE_STORAGE_SCHEMA_VERSION,
1398
+ externalMemoryImportJobs: []
1391
1399
  };
1392
1400
  } else if (schemaVersion === STRATAGATE_STORAGE_SCHEMA_VERSION) {
1393
1401
  snapshot = structuredClone(value);
@@ -1403,7 +1411,8 @@ function normalizeSnapshot(value) {
1403
1411
  if (!Number.isFinite(snapshot.blockDecayLambda) || snapshot.blockDecayLambda < 0) {
1404
1412
  throw new TypeError("Invalid StrataGate snapshot: blockDecayLambda must be a non-negative finite number");
1405
1413
  }
1406
- for (const key of ["openTail", "blocks", "summaryJobs", "events", "graphNodes", "graphEdges", "graphProjectionJobs", "elements", "extractionJobs", "elementProjectionJobs", "usageReceipts", "ingestionReceipts"]) {
1414
+ if (!Array.isArray(snapshot.externalMemoryImportJobs)) snapshot.externalMemoryImportJobs = [];
1415
+ for (const key of ["openTail", "blocks", "summaryJobs", "events", "graphNodes", "graphEdges", "graphProjectionJobs", "elements", "extractionJobs", "elementProjectionJobs", "usageReceipts", "ingestionReceipts", "externalMemoryImportJobs"]) {
1407
1416
  if (!Array.isArray(snapshot[key])) throw new TypeError(`Invalid StrataGate snapshot: ${key} must be an array`);
1408
1417
  }
1409
1418
  if (!Array.isArray(snapshot.successfulModelResponses)) snapshot.successfulModelResponses = [];
@@ -1817,6 +1826,16 @@ CREATE TABLE IF NOT EXISTS ingestion_receipts (
1817
1826
  PRIMARY KEY (namespace, receipt_id),
1818
1827
  FOREIGN KEY (namespace) REFERENCES memory_spaces(namespace) ON DELETE CASCADE
1819
1828
  ) STRICT;
1829
+
1830
+ CREATE TABLE IF NOT EXISTS external_memory_import_jobs (
1831
+ namespace TEXT NOT NULL,
1832
+ id TEXT NOT NULL,
1833
+ payload_json TEXT NOT NULL,
1834
+ created_at TEXT NOT NULL,
1835
+ updated_at TEXT NOT NULL,
1836
+ PRIMARY KEY (namespace, id),
1837
+ FOREIGN KEY (namespace) REFERENCES memory_spaces(namespace) ON DELETE CASCADE
1838
+ ) STRICT;
1820
1839
  `;
1821
1840
  var THREAD_INDEXES = `
1822
1841
  CREATE INDEX IF NOT EXISTS messages_thread_idx ON messages(namespace, thread_id, position);
@@ -2092,6 +2111,10 @@ var SqliteStorage = class {
2092
2111
  id: row.receipt_id,
2093
2112
  createdAt: row.created_at
2094
2113
  }));
2114
+ const externalMemoryImportJobs = this.database.prepare(`
2115
+ SELECT id, payload_json FROM external_memory_import_jobs
2116
+ WHERE namespace = ? ORDER BY created_at, id
2117
+ `).all(key).map((row) => parseJson(row.payload_json, "external_memory_import_jobs.payload_json"));
2095
2118
  const graphState = this.database.prepare(`
2096
2119
  SELECT nodes_json, edges_json, jobs_json FROM graph_state WHERE namespace = ?
2097
2120
  `).get(key);
@@ -2115,6 +2138,7 @@ var SqliteStorage = class {
2115
2138
  elementProjectionJobs,
2116
2139
  usageReceipts,
2117
2140
  ingestionReceipts,
2141
+ externalMemoryImportJobs,
2118
2142
  successfulModelResponses
2119
2143
  };
2120
2144
  assertValidSnapshot(snapshot);
@@ -2498,6 +2522,14 @@ var SqliteStorage = class {
2498
2522
  for (const receipt of snapshot.ingestionReceipts) {
2499
2523
  insertIngestionReceipt.run(namespace, receipt.id, receipt.createdAt);
2500
2524
  }
2525
+ this.database.prepare("DELETE FROM external_memory_import_jobs WHERE namespace = ?").run(namespace);
2526
+ const insertExternalMemoryImportJob = this.database.prepare(`
2527
+ INSERT INTO external_memory_import_jobs (namespace, id, payload_json, created_at, updated_at)
2528
+ VALUES (?, ?, ?, ?, ?)
2529
+ `);
2530
+ for (const job of snapshot.externalMemoryImportJobs) {
2531
+ insertExternalMemoryImportJob.run(namespace, job.id, JSON.stringify(job), job.createdAt, job.updatedAt);
2532
+ }
2501
2533
  this.database.prepare("DELETE FROM model_response_history WHERE namespace = ?").run(namespace);
2502
2534
  const insertSuccessfulModelResponse = this.database.prepare(`
2503
2535
  INSERT INTO model_response_history (namespace, id, kind, response, created_at)
@@ -2646,6 +2678,10 @@ function errorMessage(error) {
2646
2678
  return error instanceof Error ? error.message : String(error);
2647
2679
  }
2648
2680
  var STRATAGATE_CONSTRUCTOR_TOKEN = /* @__PURE__ */ Symbol("StrataGate constructor");
2681
+ var EXTERNAL_MEMORY_AUTO_APPLY_CONFIDENCE = 0.85;
2682
+ function externalMemoryFingerprint(value) {
2683
+ return `${normalizeSearchText(value.title)}\0${normalizeSearchText(value.summary)}`;
2684
+ }
2649
2685
  var DERIVATION_MAX_ATTEMPTS = 3;
2650
2686
  var DERIVATION_BACKOFF_MS = 1e3;
2651
2687
  var StrataGate = class _StrataGate {
@@ -2673,6 +2709,7 @@ var StrataGate = class _StrataGate {
2673
2709
  usageReceipts = /* @__PURE__ */ new Map();
2674
2710
  successfulModelResponses = [];
2675
2711
  ingestionReceipts = /* @__PURE__ */ new Map();
2712
+ externalMemoryImportJobs = /* @__PURE__ */ new Map();
2676
2713
  currentTurn = 0;
2677
2714
  storage;
2678
2715
  namespace;
@@ -2846,6 +2883,25 @@ var StrataGate = class _StrataGate {
2846
2883
  get storageRevision() {
2847
2884
  return this.revision;
2848
2885
  }
2886
+ /** Replace an out-of-date in-memory view with the latest durable namespace snapshot. */
2887
+ async reloadFromStorage() {
2888
+ if (!this.storage || !this.namespace) return false;
2889
+ const previous = this.mutationQueue;
2890
+ let release;
2891
+ this.mutationQueue = new Promise((resolve2) => {
2892
+ release = resolve2;
2893
+ });
2894
+ await previous;
2895
+ try {
2896
+ const loaded = await this.storage.load(this.namespace);
2897
+ if (!loaded || loaded.revision === this.revision) return false;
2898
+ this.restoreSnapshot(loaded.snapshot);
2899
+ this.revision = loaded.revision;
2900
+ return true;
2901
+ } finally {
2902
+ release();
2903
+ }
2904
+ }
2849
2905
  get blockTurnSize() {
2850
2906
  return this.blockTurnSizeValue;
2851
2907
  }
@@ -2907,6 +2963,9 @@ var StrataGate = class _StrataGate {
2907
2963
  listSuccessfulModelResponses() {
2908
2964
  return this.successfulModelResponses;
2909
2965
  }
2966
+ listExternalMemoryImportJobs() {
2967
+ return [...this.externalMemoryImportJobs.values()].map((job) => structuredClone(job));
2968
+ }
2910
2969
  async recordSuccessfulModelResponses(responses) {
2911
2970
  if (responses.length === 0) return;
2912
2971
  await this.commitMutation(() => {
@@ -2937,6 +2996,7 @@ var StrataGate = class _StrataGate {
2937
2996
  elementProjectionJobs: [...this.elementProjectionJobs.values()],
2938
2997
  usageReceipts: [...this.usageReceipts.values()],
2939
2998
  ingestionReceipts: [...this.ingestionReceipts.values()],
2999
+ externalMemoryImportJobs: [...this.externalMemoryImportJobs.values()],
2940
3000
  successfulModelResponses: this.successfulModelResponses
2941
3001
  });
2942
3002
  }
@@ -3044,7 +3104,41 @@ var StrataGate = class _StrataGate {
3044
3104
  * overwritten: MERGE and SUPERSEDE create a new canonical Event that points
3045
3105
  * back to the older Events.
3046
3106
  */
3047
- async importExternalMemory(options) {
3107
+ normalizeExternalMemoryDecision(candidate, matches, decision, forceConfirmation = false) {
3108
+ const allowed = new Set(matches.map(({ event }) => event.id));
3109
+ const exact = this.events.find((event) => event.status !== "forgotten" && event.status !== "archived" && externalMemoryFingerprint(event) === externalMemoryFingerprint(candidate));
3110
+ if (exact) allowed.add(exact.id);
3111
+ const existingEventIds = [...new Set((decision.existingEventIds ?? []).filter((id) => allowed.has(id)))];
3112
+ const requestedAction = this.normalizeExternalAction(decision.action);
3113
+ const missingTarget = requestedAction !== "ADD" && requestedAction !== "IGNORE" && existingEventIds.length === 0;
3114
+ const action = missingTarget ? "IGNORE" : requestedAction;
3115
+ const confidence2 = missingTarget ? 0.5 : Number.isFinite(decision.confidence) ? Math.max(0, Math.min(1, decision.confidence)) : 0.5;
3116
+ return {
3117
+ candidate: structuredClone(candidate),
3118
+ action,
3119
+ existingEventIds,
3120
+ matches: structuredClone([...matches]),
3121
+ confidence: confidence2,
3122
+ requiresConfirmation: forceConfirmation || confidence2 < EXTERNAL_MEMORY_AUTO_APPLY_CONFIDENCE,
3123
+ ...decision.mergedCandidate ? { mergedCandidate: structuredClone(decision.mergedCandidate) } : {},
3124
+ ...missingTarget ? { reason: "\u6A21\u578B\u672A\u5173\u8054\u5230\u5141\u8BB8\u8303\u56F4\u5185\u7684\u73B0\u6709\u8BB0\u5FC6\uFF0C\u5DF2\u5B89\u5168\u964D\u7EA7\u4E3A\u5FFD\u7565" } : typeof decision.reason === "string" && decision.reason.trim() ? { reason: decision.reason.trim().slice(0, 500) } : {}
3125
+ };
3126
+ }
3127
+ async decideExternalMemoryCandidate(candidate, priorFingerprints, decider, topK, forceConfirmation = false) {
3128
+ const fingerprint = externalMemoryFingerprint(candidate);
3129
+ const exact = this.events.find((event) => event.status !== "forgotten" && event.status !== "archived" && externalMemoryFingerprint(event) === fingerprint);
3130
+ const duplicateInImport = priorFingerprints.has(fingerprint);
3131
+ const query = `${candidate.title} ${candidate.summary} ${(candidate.tags ?? []).join(" ")}`.trim();
3132
+ const matches = await this.searchEvents(query, { limit: topK, trackRetrieval: false });
3133
+ const decision = exact || duplicateInImport ? {
3134
+ action: "IGNORE",
3135
+ existingEventIds: exact ? [exact.id] : [],
3136
+ reason: exact ? "\u4E0E\u73B0\u6709\u8BB0\u5FC6\u5B8C\u5168\u91CD\u590D" : "\u4E0E\u672C\u6279\u6B21\u4E2D\u7684\u5019\u9009\u5B8C\u5168\u91CD\u590D",
3137
+ confidence: 1
3138
+ } : await decider({ candidate: structuredClone(candidate), matches: structuredClone(matches) });
3139
+ return this.normalizeExternalMemoryDecision(candidate, matches, decision, forceConfirmation);
3140
+ }
3141
+ async previewExternalMemoryImport(options) {
3048
3142
  const text3 = options.text.trim();
3049
3143
  if (!text3) throw new TypeError("External memory text must not be empty");
3050
3144
  if (typeof options.decider !== "function") {
@@ -3055,28 +3149,198 @@ var StrataGate = class _StrataGate {
3055
3149
  const extracted = await extractor({ text: text3, importedAt });
3056
3150
  const candidates = Array.isArray(extracted?.candidates) ? extracted.candidates.slice(0, 200) : [];
3057
3151
  const topK = Math.max(1, Math.min(20, Math.floor(options.topK ?? 5)));
3058
- const source = await this.commitMutation(() => this.createExternalSourceBlock(text3, importedAt));
3059
- const prepared = [];
3152
+ const seenFingerprints = /* @__PURE__ */ new Set();
3153
+ const decisions = [];
3060
3154
  for (const candidate of candidates) {
3061
3155
  if (!candidate || typeof candidate.title !== "string" || typeof candidate.summary !== "string") continue;
3062
- const query = `${candidate.title} ${candidate.summary} ${(candidate.tags ?? []).join(" ")}`.trim();
3063
- const matches = await this.searchEvents(query, { limit: topK });
3064
- const decision = await options.decider({ candidate, matches });
3065
- const allowed = new Set(matches.map(({ event }) => event.id));
3066
- const existingEventIds = [...new Set((decision.existingEventIds ?? []).filter((id) => allowed.has(id)))];
3067
- prepared.push({ candidate, decision, existingEventIds });
3156
+ const fingerprint = externalMemoryFingerprint(candidate);
3157
+ const preview = await this.decideExternalMemoryCandidate(candidate, seenFingerprints, options.decider, topK);
3158
+ seenFingerprints.add(fingerprint);
3159
+ decisions.push(preview);
3160
+ }
3161
+ return { importedAt, baseRevision: this.revision, decisions };
3162
+ }
3163
+ getExternalMemoryImportJob(jobId) {
3164
+ const job = this.externalMemoryImportJobs.get(jobId.trim());
3165
+ return job ? structuredClone(job) : null;
3166
+ }
3167
+ async createExternalMemoryImportJob(text3) {
3168
+ const normalized = text3.trim();
3169
+ if (!normalized) throw new TypeError("External memory text must not be empty");
3170
+ const now = toUtc8Iso(this.now());
3171
+ let candidates = [];
3172
+ let parseError = null;
3173
+ try {
3174
+ candidates = parseExternalMemoryExport(normalized).candidates;
3175
+ } catch (error) {
3176
+ parseError = errorMessage(error).slice(0, 2e3);
3177
+ }
3178
+ const job = {
3179
+ id: `import_${crypto.randomUUID()}`,
3180
+ text: normalized,
3181
+ importedAt: now,
3182
+ status: parseError ? "extracting" : candidates.length > 0 ? "processing" : "ready",
3183
+ candidates: structuredClone(candidates),
3184
+ decisions: [],
3185
+ processedCount: 0,
3186
+ totalCount: candidates.length,
3187
+ recoveredFromInvalidJson: false,
3188
+ parseError,
3189
+ lastError: null,
3190
+ sourceBlockId: null,
3191
+ importedCount: 0,
3192
+ createdAt: now,
3193
+ updatedAt: now
3194
+ };
3195
+ await this.commitMutation(() => this.externalMemoryImportJobs.set(job.id, structuredClone(job)));
3196
+ return structuredClone(job);
3197
+ }
3198
+ async completeExternalMemoryFallback(jobId, result) {
3199
+ const candidates = (Array.isArray(result.candidates) ? result.candidates : []).filter((candidate) => candidate && typeof candidate.title === "string" && typeof candidate.summary === "string").slice(0, 200);
3200
+ return this.commitMutation(() => {
3201
+ const job = this.requireExternalMemoryImportJob(jobId);
3202
+ if (job.status !== "extracting") return structuredClone(job);
3203
+ job.candidates = structuredClone(candidates);
3204
+ job.decisions = [];
3205
+ job.processedCount = 0;
3206
+ job.totalCount = candidates.length;
3207
+ job.recoveredFromInvalidJson = true;
3208
+ job.status = candidates.length > 0 ? "processing" : "failed";
3209
+ job.lastError = candidates.length > 0 ? null : "\u6A21\u578B\u672A\u80FD\u4ECE\u4E0D\u5408\u683C\u5185\u5BB9\u4E2D\u6062\u590D\u51FA\u4EFB\u4F55\u5019\u9009\u8BB0\u5FC6";
3210
+ job.updatedAt = toUtc8Iso(this.now());
3211
+ return structuredClone(job);
3212
+ });
3213
+ }
3214
+ async processNextExternalMemoryImport(jobId, decider, topK = 5) {
3215
+ if (typeof decider !== "function") throw new TypeError("External memory decider is required");
3216
+ const work = await this.prepareNextExternalMemoryImport(jobId, topK);
3217
+ if (!work) {
3218
+ const current = this.requireExternalMemoryImportJob(jobId);
3219
+ return structuredClone(current);
3220
+ }
3221
+ const decision = work.deterministicDecision ?? await decider({
3222
+ candidate: structuredClone(work.candidate),
3223
+ matches: structuredClone(work.matches)
3224
+ });
3225
+ return this.completeNextExternalMemoryImport(
3226
+ work.jobId,
3227
+ work.index,
3228
+ decision,
3229
+ work.matches,
3230
+ work.forceConfirmation
3231
+ );
3232
+ }
3233
+ async prepareNextExternalMemoryImport(jobId, topK = 5) {
3234
+ const current = this.requireExternalMemoryImportJob(jobId);
3235
+ if (current.status !== "processing") return null;
3236
+ const index = current.processedCount;
3237
+ const candidate = current.candidates[index];
3238
+ if (!candidate) return null;
3239
+ const priorFingerprints = new Set(current.candidates.slice(0, index).map(externalMemoryFingerprint));
3240
+ const fingerprint = externalMemoryFingerprint(candidate);
3241
+ const exact = this.events.find((event) => event.status !== "forgotten" && event.status !== "archived" && externalMemoryFingerprint(event) === fingerprint);
3242
+ const duplicateInImport = priorFingerprints.has(fingerprint);
3243
+ const query = `${candidate.title} ${candidate.summary} ${(candidate.tags ?? []).join(" ")}`.trim();
3244
+ const matches = await this.searchEvents(query, {
3245
+ limit: Math.max(1, Math.min(20, Math.floor(topK))),
3246
+ trackRetrieval: false
3247
+ });
3248
+ const deterministicDecision = exact || duplicateInImport ? {
3249
+ action: "IGNORE",
3250
+ existingEventIds: exact ? [exact.id] : [],
3251
+ reason: exact ? "\u4E0E\u73B0\u6709\u8BB0\u5FC6\u5B8C\u5168\u91CD\u590D" : "\u4E0E\u672C\u6279\u6B21\u4E2D\u7684\u5019\u9009\u5B8C\u5168\u91CD\u590D",
3252
+ confidence: 1
3253
+ } : void 0;
3254
+ return {
3255
+ jobId: current.id,
3256
+ index,
3257
+ candidate: structuredClone(candidate),
3258
+ matches: structuredClone(matches),
3259
+ forceConfirmation: current.recoveredFromInvalidJson,
3260
+ ...deterministicDecision ? { deterministicDecision } : {}
3261
+ };
3262
+ }
3263
+ async completeNextExternalMemoryImport(jobId, index, decision, matches, forceConfirmation = false) {
3264
+ return this.commitMutation(() => {
3265
+ const job = this.requireExternalMemoryImportJob(jobId);
3266
+ if (job.processedCount > index) return structuredClone(job);
3267
+ if (job.status !== "processing" || job.processedCount !== index) {
3268
+ throw new Error(`External memory import ${jobId} is no longer at candidate ${index}`);
3269
+ }
3270
+ const candidate = job.candidates[index];
3271
+ if (!candidate) throw new Error(`External memory import ${jobId} has no candidate ${index}`);
3272
+ const normalized = this.normalizeExternalMemoryDecision(candidate, matches, decision, forceConfirmation);
3273
+ job.decisions.push(structuredClone(normalized));
3274
+ job.processedCount += 1;
3275
+ if (job.processedCount >= job.totalCount) {
3276
+ job.status = job.decisions.some(({ requiresConfirmation }) => requiresConfirmation) ? "awaiting_confirmation" : "ready";
3277
+ }
3278
+ job.lastError = null;
3279
+ job.updatedAt = toUtc8Iso(this.now());
3280
+ return structuredClone(job);
3281
+ });
3282
+ }
3283
+ async failExternalMemoryImportJob(jobId, error) {
3284
+ return this.commitMutation(() => {
3285
+ const job = this.requireExternalMemoryImportJob(jobId);
3286
+ job.status = "failed";
3287
+ job.lastError = errorMessage(error).slice(0, 2e3);
3288
+ job.updatedAt = toUtc8Iso(this.now());
3289
+ return structuredClone(job);
3290
+ });
3291
+ }
3292
+ async retryExternalMemoryImportJob(jobId) {
3293
+ return this.commitMutation(() => {
3294
+ const job = this.requireExternalMemoryImportJob(jobId);
3295
+ if (job.status !== "failed") return structuredClone(job);
3296
+ job.status = job.candidates.length === 0 && job.parseError ? "extracting" : "processing";
3297
+ job.lastError = null;
3298
+ job.updatedAt = toUtc8Iso(this.now());
3299
+ return structuredClone(job);
3300
+ });
3301
+ }
3302
+ async completeExternalMemoryImportJob(jobId, result) {
3303
+ return this.commitMutation(() => {
3304
+ const job = this.requireExternalMemoryImportJob(jobId);
3305
+ job.status = "committed";
3306
+ job.sourceBlockId = result.sourceBlockId;
3307
+ job.importedCount = result.addedEvents.length;
3308
+ job.updatedAt = toUtc8Iso(this.now());
3309
+ return structuredClone(job);
3310
+ });
3311
+ }
3312
+ async markExternalMemoryImportUndone(jobId) {
3313
+ return this.commitMutation(() => {
3314
+ const job = this.requireExternalMemoryImportJob(jobId);
3315
+ job.status = "undone";
3316
+ job.updatedAt = toUtc8Iso(this.now());
3317
+ return structuredClone(job);
3318
+ });
3319
+ }
3320
+ async commitExternalMemoryImport(options) {
3321
+ const text3 = options.text.trim();
3322
+ if (!text3) throw new TypeError("External memory text must not be empty");
3323
+ if (options.candidates.length !== options.decisions.length) {
3324
+ throw new TypeError("External memory candidates and decisions must have the same length");
3068
3325
  }
3069
3326
  return this.commitMutation(() => {
3327
+ if (this.revision !== options.baseRevision) {
3328
+ throw new Error(`External memory preview is stale: expected revision ${options.baseRevision}, found ${this.revision}`);
3329
+ }
3330
+ const importedAt = toUtc8Iso(options.importedAt);
3331
+ const source = this.createExternalSourceBlock(text3, importedAt);
3070
3332
  const addedEvents = [];
3071
3333
  const changedEventIds = /* @__PURE__ */ new Set();
3072
3334
  const decisions = [];
3073
- for (const item of prepared) {
3074
- const action = this.normalizeExternalAction(item.decision.action);
3075
- const targets = item.existingEventIds;
3076
- const reason = typeof item.decision.reason === "string" ? item.decision.reason.trim().slice(0, 500) : void 0;
3335
+ for (const [index, item] of options.decisions.entries()) {
3336
+ const action = this.normalizeExternalAction(item.action);
3337
+ const allowed = new Set(item.matches.map(({ event }) => event.id));
3338
+ const targets = [...new Set(item.existingEventIds.filter((id) => allowed.has(id)))];
3339
+ const reason = typeof item.reason === "string" ? item.reason.trim().slice(0, 500) : void 0;
3077
3340
  let createdEvent;
3078
- const proposed = item.decision.mergedCandidate;
3079
- const candidate = proposed && typeof proposed.title === "string" && typeof proposed.summary === "string" ? proposed : item.candidate;
3341
+ const proposed = item.mergedCandidate;
3342
+ const original = options.candidates[index];
3343
+ const candidate = proposed && typeof proposed.title === "string" && typeof proposed.summary === "string" ? proposed : original;
3080
3344
  if ((action === "ADD" || action === "MERGE" || action === "SUPERSEDE" || action === "CONFLICT") && (action === "ADD" || targets.length > 0)) {
3081
3345
  const temporal = {
3082
3346
  ...candidate.temporal ?? {},
@@ -3102,11 +3366,12 @@ var StrataGate = class _StrataGate {
3102
3366
  }
3103
3367
  }
3104
3368
  const audit2 = {
3105
- candidate: structuredClone(item.candidate),
3369
+ candidate: structuredClone(original),
3106
3370
  action,
3107
3371
  existingEventIds: targets,
3108
3372
  ...createdEvent ? { createdEventId: createdEvent.id } : {},
3109
- ...reason ? { reason } : {}
3373
+ ...reason ? { reason } : {},
3374
+ ...typeof item.confidence === "number" ? { confidence: item.confidence } : {}
3110
3375
  };
3111
3376
  decisions.push(audit2);
3112
3377
  }
@@ -3123,6 +3388,92 @@ var StrataGate = class _StrataGate {
3123
3388
  };
3124
3389
  });
3125
3390
  }
3391
+ async importExternalMemory(options) {
3392
+ const preview = await this.previewExternalMemoryImport(options);
3393
+ return this.commitExternalMemoryImport({
3394
+ text: options.text,
3395
+ importedAt: preview.importedAt,
3396
+ baseRevision: preview.baseRevision,
3397
+ candidates: preview.decisions.map(({ candidate }) => candidate),
3398
+ decisions: preview.decisions
3399
+ });
3400
+ }
3401
+ async undoExternalMemoryImport(sourceBlockId) {
3402
+ const id = sourceBlockId.trim();
3403
+ if (!id) throw new TypeError("External memory source block ID must not be empty");
3404
+ return this.commitMutation(() => {
3405
+ const sourceIndex = this.blocks.findIndex((block) => block.id === id && block.l0Tags?.includes("external-memory-import"));
3406
+ if (sourceIndex < 0) throw new Error(`Unknown external memory import: ${id}`);
3407
+ const source = this.blocks[sourceIndex];
3408
+ const sourceMessageIds = new Set(source.l5Raw.map(({ id: id2 }) => id2));
3409
+ const importedEventIds = new Set(this.events.filter((event) => event.sourceBlockId === id).map(({ id: id2 }) => id2));
3410
+ const restoredEventIds = /* @__PURE__ */ new Set();
3411
+ const now = toUtc8Iso(this.now());
3412
+ this.events.splice(0, this.events.length, ...this.events.filter((event) => !importedEventIds.has(event.id)));
3413
+ for (const event of this.events) {
3414
+ for (const field of ["conflictsWithEventIds", "supersedesEventIds", "beforeEventIds", "afterEventIds", "relatedEventIds"]) {
3415
+ const previous = event.temporal[field] ?? [];
3416
+ const filtered = previous.filter((target) => !importedEventIds.has(target));
3417
+ if (filtered.length !== previous.length) {
3418
+ event.temporal[field] = filtered;
3419
+ restoredEventIds.add(event.id);
3420
+ }
3421
+ }
3422
+ if (event.temporal.sameEventId && importedEventIds.has(event.temporal.sameEventId)) {
3423
+ delete event.temporal.sameEventId;
3424
+ restoredEventIds.add(event.id);
3425
+ }
3426
+ if (event.supersededBy && importedEventIds.has(event.supersededBy)) {
3427
+ const replacement = this.events.find((candidate) => candidate.id !== event.id && (candidate.temporal.supersedesEventIds ?? []).includes(event.id));
3428
+ event.status = replacement ? "superseded" : "active";
3429
+ event.supersededBy = replacement?.id ?? null;
3430
+ if (!replacement && event.weight.forcedCap === 0.1) event.weight.forcedCap = null;
3431
+ restoredEventIds.add(event.id);
3432
+ }
3433
+ if (restoredEventIds.has(event.id)) event.updatedAt = now;
3434
+ }
3435
+ for (const [jobId, job] of this.elementProjectionJobs) {
3436
+ if (job.sourceEventIds.some((eventId) => importedEventIds.has(eventId))) this.elementProjectionJobs.delete(jobId);
3437
+ }
3438
+ for (const [jobId, job] of this.graphProjectionJobs) {
3439
+ if (job.sourceEventIds.some((eventId) => importedEventIds.has(eventId))) this.graphProjectionJobs.delete(jobId);
3440
+ }
3441
+ for (const element of this.elements) {
3442
+ element.sourceEventIds = element.sourceEventIds.filter((eventId) => !importedEventIds.has(eventId));
3443
+ element.sourceMessageIds = element.sourceMessageIds.filter((messageId) => !sourceMessageIds.has(messageId));
3444
+ element.facts = element.facts.flatMap((fact) => {
3445
+ fact.sourceEventIds = fact.sourceEventIds.filter((eventId) => !importedEventIds.has(eventId));
3446
+ return fact.sourceEventIds.length > 0 ? [fact] : [];
3447
+ });
3448
+ const current = [...element.facts].reverse().find((fact) => fact.status === "active" && fact.mode === "state");
3449
+ element.currentState = current ? Array.isArray(current.value) ? current.value.join("\u3001") : current.value : "";
3450
+ }
3451
+ this.elements.splice(0, this.elements.length, ...this.elements.filter((element) => element.sourceEventIds.length > 0 || element.facts.length > 0));
3452
+ for (const node of this.graphNodes) {
3453
+ node.sourceEventIds = node.sourceEventIds.filter((eventId) => !importedEventIds.has(eventId));
3454
+ node.facts = node.facts.flatMap((fact) => {
3455
+ fact.sourceEventIds = fact.sourceEventIds.filter((eventId) => !importedEventIds.has(eventId));
3456
+ return fact.sourceEventIds.length > 0 ? [fact] : [];
3457
+ });
3458
+ }
3459
+ const removedNodeIds = new Set(this.graphNodes.filter((node) => node.sourceEventIds.length === 0 && node.facts.length === 0).map(({ id: id2 }) => id2));
3460
+ this.graphNodes.splice(0, this.graphNodes.length, ...this.graphNodes.filter((node) => !removedNodeIds.has(node.id)));
3461
+ for (const edge of this.graphEdges) {
3462
+ edge.sourceEventIds = edge.sourceEventIds.filter((eventId) => !importedEventIds.has(eventId));
3463
+ }
3464
+ this.graphEdges.splice(0, this.graphEdges.length, ...this.graphEdges.filter((edge) => edge.sourceEventIds.length > 0 && !removedNodeIds.has(edge.fromNodeId) && !removedNodeIds.has(edge.toNodeId)));
3465
+ for (const [receiptId, receipt] of this.usageReceipts) {
3466
+ receipt.eventIds = receipt.eventIds.filter((eventId) => !importedEventIds.has(eventId));
3467
+ if (receipt.eventIds.length === 0 && receipt.elementIds.length === 0) this.usageReceipts.delete(receiptId);
3468
+ }
3469
+ this.blocks.splice(sourceIndex, 1);
3470
+ return {
3471
+ sourceBlockId: id,
3472
+ removedEventIds: [...importedEventIds],
3473
+ restoredEventIds: [...restoredEventIds]
3474
+ };
3475
+ });
3476
+ }
3126
3477
  async searchEvents(query, options = {}) {
3127
3478
  const limit = Math.max(1, Math.min(20, options.limit ?? 6));
3128
3479
  const participants = (options.participants ?? []).map(normalizeSearchText).filter(Boolean);
@@ -3176,7 +3527,7 @@ var StrataGate = class _StrataGate {
3176
3527
  rankings.push(structured(candidates));
3177
3528
  }
3178
3529
  const ranked = rrfRank(rankings).slice(0, limit).map(({ item: event, score }) => ({ event, score }));
3179
- if (ranked.length > 0) {
3530
+ if (ranked.length > 0 && options.trackRetrieval !== false) {
3180
3531
  const now = toUtc8Iso(this.now());
3181
3532
  await this.commitMutation(() => {
3182
3533
  for (const { event } of ranked) event.weight.lastRetrievedAt = now;
@@ -3513,6 +3864,11 @@ var StrataGate = class _StrataGate {
3513
3864
  const action = typeof value === "string" ? value.trim().toUpperCase() : "";
3514
3865
  return action === "ADD" || action === "MERGE" || action === "SUPERSEDE" || action === "CONFLICT" || action === "IGNORE" ? action : "IGNORE";
3515
3866
  }
3867
+ requireExternalMemoryImportJob(id) {
3868
+ const job = this.externalMemoryImportJobs.get(id.trim());
3869
+ if (!job) throw new Error(`Unknown external memory import job: ${id}`);
3870
+ return job;
3871
+ }
3516
3872
  createExternalSourceBlock(text3, importedAt) {
3517
3873
  const blockId = this.idFactory("blk");
3518
3874
  const threadId = `external-import:${blockId}`;
@@ -3527,7 +3883,7 @@ var StrataGate = class _StrataGate {
3527
3883
  const block = {
3528
3884
  id: blockId,
3529
3885
  threadId,
3530
- sequence: this.blocks.length + 1,
3886
+ sequence: Math.max(0, ...this.blocks.map(({ sequence }) => sequence)) + 1,
3531
3887
  startTurn: 1,
3532
3888
  endTurn: 1,
3533
3889
  createdAt: importedAt,
@@ -4059,6 +4415,8 @@ var StrataGate = class _StrataGate {
4059
4415
  for (const receipt of copy.usageReceipts) this.usageReceipts.set(receipt.id, receipt);
4060
4416
  this.ingestionReceipts.clear();
4061
4417
  for (const receipt of copy.ingestionReceipts) this.ingestionReceipts.set(receipt.id, receipt);
4418
+ this.externalMemoryImportJobs.clear();
4419
+ for (const job of copy.externalMemoryImportJobs) this.externalMemoryImportJobs.set(job.id, job);
4062
4420
  this.successfulModelResponses.splice(0, this.successfulModelResponses.length, ...copy.successfulModelResponses ?? []);
4063
4421
  this.validateReferences();
4064
4422
  }
@@ -4327,7 +4685,9 @@ var STRUCTURED_FIELDS = {
4327
4685
  summarizer: ["l0Title", "l0Tags", "l1Summary", "l2Keypoints", "shouldExtract"],
4328
4686
  extractor: ["shouldExtract", "reason", "events"],
4329
4687
  projector: ["reason", "changes"],
4330
- graphProjector: ["reason", "nodes", "edges"]
4688
+ graphProjector: ["reason", "nodes", "edges"],
4689
+ externalMemoryExtractor: ["reason", "candidates"],
4690
+ externalMemoryDecider: ["action", "reason", "confidence"]
4331
4691
  };
4332
4692
  var STRING_ARRAY = { type: "array", items: { type: "string" } };
4333
4693
  var OPEN_OBJECT = { type: "object", additionalProperties: true };
@@ -4435,6 +4795,17 @@ var GRAPH_PROJECTOR_PARAMETERS = {
4435
4795
  nodes: { type: "array", items: GRAPH_NODE, required: true },
4436
4796
  edges: { type: "array", items: GRAPH_EDGE, required: true }
4437
4797
  };
4798
+ var EXTERNAL_MEMORY_DECIDER_PARAMETERS = {
4799
+ action: { type: "string", enum: ["ADD", "MERGE", "SUPERSEDE", "CONFLICT", "IGNORE"], required: true },
4800
+ existingEventIds: STRING_ARRAY,
4801
+ mergedCandidate: OPEN_OBJECT,
4802
+ reason: { type: "string", required: true },
4803
+ confidence: { type: "number", required: true }
4804
+ };
4805
+ var EXTERNAL_MEMORY_EXTRACTOR_PARAMETERS = {
4806
+ reason: { type: "string", required: true },
4807
+ candidates: { type: "array", items: OPEN_OBJECT, required: true }
4808
+ };
4438
4809
  var STRUCTURED_TOOLS = {
4439
4810
  summarizer: {
4440
4811
  name: "stratagate_summarize_block",
@@ -4455,6 +4826,16 @@ var STRUCTURED_TOOLS = {
4455
4826
  name: "stratagate_project_knowledge_graph",
4456
4827
  description: "Project stable graph nodes and directed edges from supplied event evidence.",
4457
4828
  parameters: GRAPH_PROJECTOR_PARAMETERS
4829
+ },
4830
+ externalMemoryDecider: {
4831
+ name: "stratagate_decide_external_memory",
4832
+ description: "Decide how one external memory candidate relates to retrieved local events.",
4833
+ parameters: EXTERNAL_MEMORY_DECIDER_PARAMETERS
4834
+ },
4835
+ externalMemoryExtractor: {
4836
+ name: "stratagate_recover_external_memory",
4837
+ description: "Recover structured external-memory candidates from malformed JSON or plain text.",
4838
+ parameters: EXTERNAL_MEMORY_EXTRACTOR_PARAMETERS
4458
4839
  }
4459
4840
  };
4460
4841
  function toolSchema(kind) {
@@ -4483,7 +4864,10 @@ var DshModelBridge = class {
4483
4864
  successfulResponses = [];
4484
4865
  offCapabilities = /* @__PURE__ */ new Map();
4485
4866
  run(session, operation) {
4486
- return this.sessions.run(session, operation);
4867
+ return this.sessions.run({ session, sessionId: session.id }, operation);
4868
+ }
4869
+ runDetached(sessionId, operation) {
4870
+ return this.sessions.run({ sessionId }, operation);
4487
4871
  }
4488
4872
  takeSuccessfulResponses() {
4489
4873
  const responses = this.successfulResponses.splice(0, this.successfulResponses.length);
@@ -4625,10 +5009,43 @@ var DshModelBridge = class {
4625
5009
  });
4626
5010
  return { reason: text2(raw.reason, "Projected Event evidence into the Knowledge Graph."), nodes, edges };
4627
5011
  };
5012
+ externalMemoryDecider = async (context) => {
5013
+ const raw = object(await this.callStructured(
5014
+ "externalMemoryDecider",
5015
+ `${EXTERNAL_MEMORY_DECIDER_PROMPT_ZH_CN}
5016
+
5017
+ \u8C03\u7528 ${STRUCTURED_TOOLS.externalMemoryDecider.name} \u6070\u597D\u4E00\u6B21\uFF0C\u4E0D\u8981\u8FD4\u56DE\u666E\u901A\u6587\u672C\u3002confidence \u5FC5\u987B\u662F 0 \u5230 1\uFF0C\u8868\u793A\u8BE5 action \u5224\u65AD\u7684\u628A\u63E1\u7A0B\u5EA6\u3002`,
5018
+ context
5019
+ ));
5020
+ const action = text2(raw.action).toUpperCase();
5021
+ const allowedActions = /* @__PURE__ */ new Set(["ADD", "MERGE", "SUPERSEDE", "CONFLICT", "IGNORE"]);
5022
+ const proposed = object(raw.mergedCandidate);
5023
+ const mergedCandidate = text2(proposed.title) && text2(proposed.summary) ? proposed : void 0;
5024
+ return {
5025
+ action: allowedActions.has(action) ? action : "IGNORE",
5026
+ existingEventIds: strings2(raw.existingEventIds),
5027
+ ...mergedCandidate ? { mergedCandidate } : {},
5028
+ reason: text2(raw.reason, "\u6A21\u578B\u672A\u63D0\u4F9B\u88C1\u51B3\u7406\u7531\u3002").slice(0, 500),
5029
+ confidence: typeof raw.confidence === "number" ? Math.max(0, Math.min(1, raw.confidence)) : 0.5
5030
+ };
5031
+ };
5032
+ externalMemoryExtractor = async ({ text: source, importedAt }) => {
5033
+ const raw = object(await this.callStructured(
5034
+ "externalMemoryExtractor",
5035
+ `Recover durable memory candidates from the supplied malformed external-memory export. Use only facts present in sourceText; never invent missing facts. Preserve uncertainty and omit unsupported dates. Call ${STRUCTURED_TOOLS.externalMemoryExtractor.name} exactly once with reason and candidates. Each candidate needs a concise title and self-contained summary. Do not return ordinary text.`,
5036
+ { sourceText: source, importedAt }
5037
+ ));
5038
+ const parsed = parseExternalMemoryExport(JSON.stringify({
5039
+ schemaVersion: "stratagate.external-memory.v2",
5040
+ sourceType: "external_ai_memory_export",
5041
+ candidates: Array.isArray(raw.candidates) ? raw.candidates : []
5042
+ }));
5043
+ return { candidates: parsed.candidates, reason: text2(raw.reason, parsed.reason) };
5044
+ };
4628
5045
  async callStructured(kind, system, payload) {
4629
- const session = this.sessions.getStore();
4630
- if (!session) throw new Error("StrataGate model callback ran without a DSH session");
4631
- const baseRoute = this.resolveRoute(session);
5046
+ const execution = this.sessions.getStore();
5047
+ if (!execution) throw new Error("StrataGate model callback ran without an execution context");
5048
+ const baseRoute = this.resolveRoute(execution.session);
4632
5049
  const routeKey = `${baseRoute.provider}\0${baseRoute.model}`;
4633
5050
  let useOff = await this.shouldUseOff(baseRoute);
4634
5051
  let lastError;
@@ -4656,7 +5073,7 @@ ${JSON_RETRY_INSTRUCTION}`,
4656
5073
  function: { name: STRUCTURED_TOOLS[kind].name }
4657
5074
  },
4658
5075
  maxTokens: this.config.maxOutputTokens,
4659
- sessionId: session.id,
5076
+ sessionId: execution.sessionId,
4660
5077
  purpose: "compaction"
4661
5078
  };
4662
5079
  try {
@@ -4805,7 +5222,7 @@ ${JSON_RETRY_INSTRUCTION}`,
4805
5222
  }
4806
5223
  }
4807
5224
  resolveRoute(session) {
4808
- const request = session.requestHeader()?.config;
5225
+ const request = session?.requestHeader()?.config;
4809
5226
  if (this.config.provider && this.config.model) {
4810
5227
  return { provider: this.config.provider, model: this.config.model };
4811
5228
  }
@@ -5052,6 +5469,7 @@ var StrataGateRuntime = class {
5052
5469
  derivationTimers = /* @__PURE__ */ new Map();
5053
5470
  derivationRuns = /* @__PURE__ */ new Map();
5054
5471
  adminSnapshotCache = /* @__PURE__ */ new Map();
5472
+ externalImportRuns = /* @__PURE__ */ new Map();
5055
5473
  ingestTail = Promise.resolve();
5056
5474
  settingsTail = Promise.resolve();
5057
5475
  batchSequence = 0;
@@ -5393,6 +5811,7 @@ var StrataGateRuntime = class {
5393
5811
  }
5394
5812
  const settled = await Promise.allSettled(this.spaces.values());
5395
5813
  await Promise.allSettled(this.derivationRuns.values());
5814
+ await Promise.allSettled(this.externalImportRuns.values());
5396
5815
  await Promise.all(settled.flatMap((result) => result.status === "fulfilled" ? [result.value.close()] : []));
5397
5816
  if (flushError !== void 0) throw flushError;
5398
5817
  }
@@ -5500,36 +5919,213 @@ var StrataGateRuntime = class {
5500
5919
  await storage.close();
5501
5920
  }
5502
5921
  }
5503
- /** Import a v2 external-AI export from the admin UI. */
5504
- async adminImportExternalMemory(namespace, text3) {
5922
+ externalImportView(job) {
5923
+ return {
5924
+ jobId: job.id,
5925
+ status: job.status,
5926
+ processedCount: job.processedCount,
5927
+ totalCount: job.totalCount,
5928
+ recoveredFromInvalidJson: job.recoveredFromInvalidJson,
5929
+ parseError: job.parseError,
5930
+ lastError: job.lastError,
5931
+ sourceBlockId: job.sourceBlockId,
5932
+ importedCount: job.importedCount,
5933
+ createdAt: job.createdAt,
5934
+ updatedAt: job.updatedAt,
5935
+ decisions: job.decisions.map(({ matches: _matches, mergedCandidate: _mergedCandidate, ...decision }) => decision),
5936
+ requiresConfirmationCount: job.decisions.filter(({ requiresConfirmation }) => requiresConfirmation).length
5937
+ };
5938
+ }
5939
+ async refreshExternalImportMemory(namespace, memory) {
5940
+ if (this.config.database === ":memory:" || !existsSync(this.config.database)) return;
5941
+ const storage = new SqliteStorage({ filename: this.config.database, readonly: true });
5942
+ try {
5943
+ const head = storage.listNamespaceRevisions().find((entry) => entry.namespace === namespace);
5944
+ if (head && head.revision !== memory.storageRevision) await memory.reloadFromStorage();
5945
+ } finally {
5946
+ await storage.close();
5947
+ }
5948
+ }
5949
+ async refreshActiveExternalImportMemory(namespace) {
5950
+ const opening = this.spaces.get(namespace);
5951
+ if (!opening) return;
5952
+ await (await opening).reloadFromStorage();
5953
+ }
5954
+ async retryExternalImportWrite(namespace, operation) {
5955
+ let lastConflict;
5956
+ for (let attempt = 0; attempt < 8; attempt += 1) {
5957
+ const { memory, owned } = await this.openAdminMemory(namespace);
5958
+ try {
5959
+ await this.refreshExternalImportMemory(namespace, memory);
5960
+ const result = await operation(memory);
5961
+ if (owned) await this.refreshActiveExternalImportMemory(namespace);
5962
+ return result;
5963
+ } catch (error) {
5964
+ if (!(error instanceof StorageConflictError)) throw error;
5965
+ lastConflict = error;
5966
+ if (!owned) await memory.reloadFromStorage();
5967
+ } finally {
5968
+ if (owned) await memory.close();
5969
+ }
5970
+ }
5971
+ throw lastConflict ?? new Error(`Unable to update external memory import ${namespace}`);
5972
+ }
5973
+ takeSuccessfulResponses() {
5974
+ return typeof this.models.takeSuccessfulResponses === "function" ? this.models.takeSuccessfulResponses() : [];
5975
+ }
5976
+ async persistExternalImportResponses(namespace, responses) {
5977
+ if (responses.length === 0) return;
5978
+ await this.retryExternalImportWrite(namespace, (memory) => memory.recordSuccessfulModelResponses(responses));
5979
+ }
5980
+ scheduleExternalMemoryImport(namespace, jobId) {
5981
+ if (this.closed || this.externalImportRuns.has(jobId)) return;
5982
+ const run = (async () => {
5983
+ while (!this.closed) {
5984
+ let job = null;
5985
+ let work = null;
5986
+ const prepared = await this.openAdminMemory(namespace);
5987
+ try {
5988
+ await this.refreshExternalImportMemory(namespace, prepared.memory);
5989
+ job = prepared.memory.getExternalMemoryImportJob(jobId);
5990
+ if (!job) return;
5991
+ if (job.status === "processing") {
5992
+ work = await prepared.memory.prepareNextExternalMemoryImport(jobId);
5993
+ } else if (job.status !== "extracting") return;
5994
+ } finally {
5995
+ if (prepared.owned) await prepared.memory.close();
5996
+ }
5997
+ try {
5998
+ if (job.status === "extracting") {
5999
+ const recovered = await this.models.runDetached(`admin-import-recovery:${jobId}`, () => this.models.externalMemoryExtractor({ text: job.text, importedAt: job.importedAt }));
6000
+ const responses2 = this.takeSuccessfulResponses();
6001
+ await this.retryExternalImportWrite(namespace, (memory) => memory.completeExternalMemoryFallback(jobId, recovered));
6002
+ await this.persistExternalImportResponses(namespace, responses2);
6003
+ continue;
6004
+ }
6005
+ if (!work) return;
6006
+ let decision;
6007
+ let responses = [];
6008
+ if (work.deterministicDecision) {
6009
+ decision = work.deterministicDecision;
6010
+ } else {
6011
+ decision = await this.models.runDetached(`admin-import:${jobId}`, () => this.models.externalMemoryDecider({
6012
+ candidate: structuredClone(work.candidate),
6013
+ matches: structuredClone(work.matches)
6014
+ }));
6015
+ responses = this.takeSuccessfulResponses();
6016
+ }
6017
+ await this.retryExternalImportWrite(namespace, (memory) => memory.completeNextExternalMemoryImport(
6018
+ work.jobId,
6019
+ work.index,
6020
+ decision,
6021
+ work.matches,
6022
+ work.forceConfirmation
6023
+ ));
6024
+ await this.persistExternalImportResponses(namespace, responses);
6025
+ } catch (error) {
6026
+ if (error instanceof StorageConflictError) {
6027
+ this.onIngestError(error);
6028
+ return;
6029
+ }
6030
+ try {
6031
+ await this.retryExternalImportWrite(namespace, (memory) => memory.failExternalMemoryImportJob(jobId, error));
6032
+ } catch (persistError) {
6033
+ this.onIngestError(persistError);
6034
+ }
6035
+ return;
6036
+ }
6037
+ }
6038
+ })().catch((error) => this.onIngestError(error)).finally(() => {
6039
+ this.externalImportRuns.delete(jobId);
6040
+ });
6041
+ this.externalImportRuns.set(jobId, run);
6042
+ }
6043
+ /** Create a durable analysis job and return before model-backed work begins. */
6044
+ async adminPreviewExternalMemory(namespace, text3) {
5505
6045
  const key = namespace.trim();
5506
6046
  if (!key) throw new TypeError("StrataGate admin namespace must not be empty");
6047
+ if (!text3.trim()) throw new TypeError("External memory text must not be empty");
5507
6048
  await this.flush();
5508
- const active = this.spaces.get(key);
5509
- let memory;
5510
- let owned = false;
5511
- if (active) {
5512
- memory = await active;
5513
- } else {
5514
- if (this.config.database === ":memory:" || !existsSync(this.config.database)) {
5515
- throw new Error(`Unknown StrataGate namespace: ${key}`);
6049
+ const { memory, owned } = await this.openAdminMemory(key);
6050
+ try {
6051
+ await this.refreshExternalImportMemory(key, memory);
6052
+ const job = await memory.createExternalMemoryImportJob(text3);
6053
+ this.scheduleExternalMemoryImport(key, job.id);
6054
+ return this.externalImportView(job);
6055
+ } finally {
6056
+ if (owned) await memory.close();
6057
+ }
6058
+ }
6059
+ async adminExternalMemoryStatus(namespace, jobId) {
6060
+ const key = namespace.trim();
6061
+ if (!key) throw new TypeError("StrataGate admin namespace must not be empty");
6062
+ const { memory, owned } = await this.openAdminMemory(key);
6063
+ try {
6064
+ await this.refreshExternalImportMemory(key, memory);
6065
+ const jobs = memory.listExternalMemoryImportJobs();
6066
+ const job = jobId ? jobs.find(({ id }) => id === jobId) : [...jobs].sort((left, right) => right.createdAt.localeCompare(left.createdAt))[0];
6067
+ if (!job) return { job: null };
6068
+ if (job.status === "extracting" || job.status === "processing") this.scheduleExternalMemoryImport(key, job.id);
6069
+ return this.externalImportView(job);
6070
+ } finally {
6071
+ if (owned) await memory.close();
6072
+ }
6073
+ }
6074
+ async adminRetryExternalMemory(namespace, jobId) {
6075
+ const key = namespace.trim();
6076
+ let lastConflict;
6077
+ for (let attempt = 0; attempt < 4; attempt += 1) {
6078
+ const { memory, owned } = await this.openAdminMemory(key);
6079
+ try {
6080
+ await this.refreshExternalImportMemory(key, memory);
6081
+ const job = await memory.retryExternalMemoryImportJob(jobId);
6082
+ this.scheduleExternalMemoryImport(key, job.id);
6083
+ return this.externalImportView(job);
6084
+ } catch (error) {
6085
+ if (!(error instanceof StorageConflictError)) throw error;
6086
+ lastConflict = error;
6087
+ if (!owned) await memory.reloadFromStorage();
6088
+ } finally {
6089
+ if (owned) await memory.close();
5516
6090
  }
5517
- memory = await StrataGate.open({
5518
- database: this.config.database,
5519
- namespace: key,
5520
- blockTurnSize: this.blockTurnSize,
5521
- blockDecayLambda: this.blockDecayLambda,
5522
- graphProjector: this.models.graphProjector,
5523
- disableElementProjection: true
5524
- });
5525
- owned = true;
5526
6091
  }
6092
+ throw lastConflict;
6093
+ }
6094
+ async adminCommitExternalMemory(namespace, jobId, choices) {
6095
+ const key = namespace.trim();
6096
+ const { memory, owned } = await this.openAdminMemory(key);
5527
6097
  try {
5528
- const result = await memory.importExternalMemory({
5529
- text: text3,
5530
- decider: async () => ({ action: "ADD" })
6098
+ await this.refreshExternalImportMemory(key, memory);
6099
+ const job = memory.getExternalMemoryImportJob(jobId);
6100
+ if (!job) throw new Error("\u627E\u4E0D\u5230\u5BFC\u5165\u4EFB\u52A1");
6101
+ if (job.status !== "ready" && job.status !== "awaiting_confirmation") {
6102
+ throw new Error("\u5BFC\u5165\u5206\u6790\u5C1A\u672A\u5B8C\u6210");
6103
+ }
6104
+ const selected = new Map(choices.filter(({ index, action }) => Number.isSafeInteger(index) && ["ADD", "MERGE", "SUPERSEDE", "CONFLICT", "IGNORE"].includes(action)).map(({ index, action }) => [index, action]));
6105
+ const decisions = job.decisions.map((decision, index) => {
6106
+ if (!decision.requiresConfirmation) return decision;
6107
+ const action = selected.get(index) ?? "IGNORE";
6108
+ const needsTarget = action === "MERGE" || action === "SUPERSEDE" || action === "CONFLICT";
6109
+ if (needsTarget && decision.existingEventIds.length === 0) {
6110
+ return { ...decision, action: "IGNORE", existingEventIds: [], reason: "\u7528\u6237\u9009\u62E9\u7684\u64CD\u4F5C\u6CA1\u6709\u53EF\u5173\u8054\u65E7\u8BB0\u5FC6\uFF0C\u5DF2\u5B89\u5168\u5FFD\u7565" };
6111
+ }
6112
+ return {
6113
+ ...decision,
6114
+ action,
6115
+ existingEventIds: action === "ADD" || action === "IGNORE" ? [] : decision.existingEventIds,
6116
+ reason: `\u7528\u6237\u786E\u8BA4\uFF1A${action}`
6117
+ };
5531
6118
  });
6119
+ const result = await memory.commitExternalMemoryImport({
6120
+ text: job.text,
6121
+ importedAt: job.importedAt,
6122
+ baseRevision: memory.storageRevision,
6123
+ candidates: job.candidates,
6124
+ decisions
6125
+ });
6126
+ const completed = await memory.completeExternalMemoryImportJob(job.id, result);
5532
6127
  return {
6128
+ ...this.externalImportView(completed),
5533
6129
  sourceBlockId: result.sourceBlockId,
5534
6130
  decisions: result.decisions,
5535
6131
  importedCount: result.addedEvents.length,
@@ -5539,6 +6135,19 @@ var StrataGateRuntime = class {
5539
6135
  if (owned) await memory.close();
5540
6136
  }
5541
6137
  }
6138
+ async adminUndoExternalMemory(namespace, sourceBlockId) {
6139
+ const key = namespace.trim();
6140
+ const { memory, owned } = await this.openAdminMemory(key);
6141
+ try {
6142
+ await this.refreshExternalImportMemory(key, memory);
6143
+ const result = await memory.undoExternalMemoryImport(sourceBlockId);
6144
+ const job = memory.listExternalMemoryImportJobs().find((candidate) => candidate.sourceBlockId === sourceBlockId);
6145
+ if (job) await memory.markExternalMemoryImportUndone(job.id);
6146
+ return result;
6147
+ } finally {
6148
+ if (owned) await memory.close();
6149
+ }
6150
+ }
5542
6151
  adminWorkspaceName(namespace) {
5543
6152
  const remembered = this.workspaceNames.get(namespace);
5544
6153
  if (remembered) return remembered;
@@ -5784,6 +6393,24 @@ var StrataGateRuntime = class {
5784
6393
  timer.unref?.();
5785
6394
  this.migrationTimers.set(namespace, timer);
5786
6395
  }
6396
+ async openAdminMemory(namespace) {
6397
+ const active = this.spaces.get(namespace);
6398
+ if (active) return { memory: await active, owned: false };
6399
+ if (this.config.database === ":memory:" || !existsSync(this.config.database)) {
6400
+ throw new Error(`Unknown StrataGate namespace: ${namespace}`);
6401
+ }
6402
+ return {
6403
+ memory: await StrataGate.open({
6404
+ database: this.config.database,
6405
+ namespace,
6406
+ blockTurnSize: this.blockTurnSize,
6407
+ blockDecayLambda: this.blockDecayLambda,
6408
+ graphProjector: this.models.graphProjector,
6409
+ disableElementProjection: true
6410
+ }),
6411
+ owned: true
6412
+ };
6413
+ }
5787
6414
  rememberWorkspace(namespace, cwd) {
5788
6415
  const name2 = workspaceDisplayName(cwd);
5789
6416
  this.workspaceNames.set(namespace, name2);
@@ -6654,10 +7281,41 @@ async function importExternalMemory(runtime, req) {
6654
7281
  throw new AdminHttpError(400, "\u5BFC\u5165\u8BF7\u6C42\u7F3A\u5C11 JSON body");
6655
7282
  }
6656
7283
  const namespace = typeof body.namespace === "string" ? body.namespace.trim() : "";
6657
- const text3 = typeof body.text === "string" ? body.text.trim() : "";
6658
7284
  if (!namespace) throw new AdminHttpError(400, "namespace is required");
6659
- if (!text3) throw new AdminHttpError(400, "text is required");
6660
- return runtime.adminImportExternalMemory(namespace, text3);
7285
+ const operation = typeof body.operation === "string" ? body.operation : "preview";
7286
+ if (operation === "preview") {
7287
+ const text3 = typeof body.text === "string" ? body.text.trim() : "";
7288
+ if (!text3) throw new AdminHttpError(400, "text is required");
7289
+ return runtime.adminPreviewExternalMemory(namespace, text3);
7290
+ }
7291
+ if (operation === "status") {
7292
+ const jobId = typeof body.jobId === "string" ? body.jobId.trim() : void 0;
7293
+ return runtime.adminExternalMemoryStatus(namespace, jobId);
7294
+ }
7295
+ if (operation === "retry") {
7296
+ const jobId = typeof body.jobId === "string" ? body.jobId.trim() : "";
7297
+ if (!jobId) throw new AdminHttpError(400, "jobId is required");
7298
+ return runtime.adminRetryExternalMemory(namespace, jobId);
7299
+ }
7300
+ if (operation === "commit") {
7301
+ const jobId = typeof body.jobId === "string" ? body.jobId.trim() : "";
7302
+ if (!jobId) throw new AdminHttpError(400, "jobId is required");
7303
+ const allowed = /* @__PURE__ */ new Set(["ADD", "MERGE", "SUPERSEDE", "CONFLICT", "IGNORE"]);
7304
+ const choices = Array.isArray(body.choices) ? body.choices.flatMap((value) => {
7305
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
7306
+ const item = value;
7307
+ const index = item.index;
7308
+ const action = typeof item.action === "string" ? item.action.toUpperCase() : "IGNORE";
7309
+ return Number.isSafeInteger(index) && allowed.has(action) ? [{ index, action }] : [];
7310
+ }) : [];
7311
+ return runtime.adminCommitExternalMemory(namespace, jobId, choices);
7312
+ }
7313
+ if (operation === "undo") {
7314
+ const sourceBlockId = typeof body.sourceBlockId === "string" ? body.sourceBlockId.trim() : "";
7315
+ if (!sourceBlockId) throw new AdminHttpError(400, "sourceBlockId is required");
7316
+ return runtime.adminUndoExternalMemory(namespace, sourceBlockId);
7317
+ }
7318
+ throw new AdminHttpError(400, "operation must be preview, status, retry, commit, or undo");
6661
7319
  }
6662
7320
  function externalMemoryPrompt() {
6663
7321
  return { prompt: EXTERNAL_MEMORY_EXPORT_PROMPT_ZH_CN, schemaVersion: "stratagate.external-memory.v2" };
@@ -7084,8 +7742,17 @@ async function handleAdminRequest(runtime, req, res) {
7084
7742
  if (req.method !== "PATCH") throw new AdminHttpError(405, "StrataGate Block expansion requires PATCH");
7085
7743
  sendJson(res, 200, await expandBlock(runtime, url));
7086
7744
  } else if (path === "/api/stratagate/import") {
7087
- if (req.method === "GET") sendJson(res, 200, externalMemoryPrompt());
7088
- else if (req.method === "POST") sendJson(res, 200, await importExternalMemory(runtime, req));
7745
+ if (req.method === "GET") {
7746
+ const operation = url.searchParams.get("operation");
7747
+ if (operation === "status") {
7748
+ const namespace = url.searchParams.get("namespace")?.trim() ?? "";
7749
+ if (!namespace) throw new AdminHttpError(400, "namespace is required");
7750
+ const jobId = url.searchParams.get("jobId")?.trim() || void 0;
7751
+ sendJson(res, 200, await runtime.adminExternalMemoryStatus(namespace, jobId));
7752
+ } else {
7753
+ sendJson(res, 200, externalMemoryPrompt());
7754
+ }
7755
+ } else if (req.method === "POST") sendJson(res, 200, await importExternalMemory(runtime, req));
7089
7756
  else throw new AdminHttpError(405, "External memory import requires GET or POST");
7090
7757
  } else if (req.method !== "GET") throw new AdminHttpError(405, "StrataGate memory data is read-only");
7091
7758
  else if (path === "/api/stratagate/dashboard") sendDashboard(res, await dashboard(runtime, url, requestHeader(req, "if-none-match")));