stratagate-dsh 0.3.3 → 0.3.5

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
@@ -3727,6 +3727,123 @@ var SqliteStorage = class {
3727
3727
 
3728
3728
  // packages/core/src/topics.ts
3729
3729
  import { createHash, randomUUID } from "node:crypto";
3730
+
3731
+ // packages/core/src/topic-diagnostics.ts
3732
+ var categories = [
3733
+ "max-tokens",
3734
+ "schema-invalid",
3735
+ "validation-failed",
3736
+ "timeout",
3737
+ "provider-failed",
3738
+ "worker-failed",
3739
+ "source-changed"
3740
+ ];
3741
+ var TOPIC_SAFE_REASONS = /* @__PURE__ */ new Set([
3742
+ "A topic may be updated only once per projection.",
3743
+ "Invalid duplicate chapter label conflicts with an existing topic.",
3744
+ "Invalid duplicate chapter label in one projection.",
3745
+ "Invalid duplicate chapter label matches an unexposed existing topic.",
3746
+ "Invalid or duplicate topic section title.",
3747
+ "Invalid section backfill: explicit sections must cover the existing chapter.",
3748
+ "Invalid topic membership source events.",
3749
+ "Invalid topic overview section.",
3750
+ "Invalid topic overview source events.",
3751
+ "Invalid topic overview title.",
3752
+ "Invalid topic section source events.",
3753
+ "Invalid topic sections.",
3754
+ "Invalid topic title or description.",
3755
+ "New topic overview must match a final topic section.",
3756
+ "Old overview evidence must be preserved verbatim; new sections need batch events.",
3757
+ "Old section membership must be preserved; reassignment needs batch events.",
3758
+ "Overview references must belong to returned topic membership.",
3759
+ "Section references must belong to returned topic membership.",
3760
+ "Topic projection lease expired; stale result rejected.",
3761
+ "Topic projection must contain at most 8 overview paragraphs per chapter per batch.",
3762
+ "Topic projection must return 1-12 topics.",
3763
+ "Topic projection omitted batch events.",
3764
+ "Topic projection source version changed; stale result rejected.",
3765
+ "Topic sections omitted batch events.",
3766
+ "Truncated topic sources permit navigation scope only, not factual overview.",
3767
+ "Unknown or unexposed topic id.",
3768
+ "duplicate chapter label; combine its batch assignments",
3769
+ "duplicate section title",
3770
+ "every returned topic must cover a supplied batch Event",
3771
+ "every supplied batch Event must be assigned to a topic",
3772
+ "every topic batch Event must be assigned to a section",
3773
+ "expected only topics, with at most 12 entries",
3774
+ "new overview must match a declared or inherited section",
3775
+ "old overview evidence may only be preserved verbatim; rewritten entries must cite batch Events only",
3776
+ "overview must contain 0-8 source-grounded entries",
3777
+ "overview.sourceEventIds must contain unique, supplied Event ids (at most 12)",
3778
+ "overview.text must be nonempty and at most 600 characters",
3779
+ "overview.title must be nonempty and at most 80 characters",
3780
+ "section backfill must update only its existing chapter",
3781
+ "section.sourceEventIds must contain unique, supplied Event ids",
3782
+ "section.title must be nonempty and at most 80 characters",
3783
+ "sections must assign batch Events independently of overview",
3784
+ "topic.description must be nonempty and at most 400 characters",
3785
+ "topic.sourceEventIds must contain unique, supplied Event ids",
3786
+ "topic.title must be nonempty and at most 120 characters",
3787
+ "topicId must be nonempty and at most 200 characters",
3788
+ "truncated Event evidence may only support scope entries",
3789
+ "unexpected overview field",
3790
+ "unexpected section field",
3791
+ "unexpected topic field",
3792
+ "unknown or repeated topicId",
3793
+ "unknown overview kind",
3794
+ "topic input exceeds model context capacity",
3795
+ "output token limit reached",
3796
+ "structured topic schema mismatch",
3797
+ "tool arguments are not valid JSON",
3798
+ "expected exactly one structured topic call",
3799
+ "topic semantic validation failed",
3800
+ "structured model task timed out",
3801
+ "provider or route error",
3802
+ "topic worker failed",
3803
+ "source version changed; stale result rejected",
3804
+ "topic projection lease expired"
3805
+ ]);
3806
+ function safeTopicDiagnostics(value) {
3807
+ if (!value || typeof value !== "object") return void 0;
3808
+ const input = value;
3809
+ if (!categories.includes(input.category)) return void 0;
3810
+ const result = {
3811
+ category: input.category,
3812
+ reason: typeof input.reason === "string" && TOPIC_SAFE_REASONS.has(input.reason) ? input.reason : "topic worker failed"
3813
+ };
3814
+ for (const key of [
3815
+ "eventCount",
3816
+ "candidateTopicCount",
3817
+ "estimatedInputTokens",
3818
+ "requestedOutputTokens",
3819
+ "maxOutputTokens",
3820
+ "modelCalls",
3821
+ "attempt",
3822
+ "splitDepth"
3823
+ ]) {
3824
+ const number = input[key];
3825
+ if (typeof number === "number" && Number.isSafeInteger(number) && number >= 0) result[key] = number;
3826
+ }
3827
+ if (["stop", "max-tokens", "error", "aborted", "tool-calls", "unknown"].includes(String(input.finishReason))) {
3828
+ result.finishReason = input.finishReason;
3829
+ }
3830
+ if (["requested-unverified", "reasoning-observed", "fallback", "unavailable"].includes(String(input.reasoningOff))) {
3831
+ result.reasoningOff = input.reasoningOff;
3832
+ }
3833
+ if (typeof input.reasoningObserved === "boolean") result.reasoningObserved = input.reasoningObserved;
3834
+ return result;
3835
+ }
3836
+ var TopicProjectionError = class extends Error {
3837
+ diagnostics;
3838
+ constructor(category, reason, metadata = {}) {
3839
+ const diagnostics = safeTopicDiagnostics({ ...metadata, category, reason });
3840
+ super(`${diagnostics.category}: ${diagnostics.reason}`);
3841
+ this.name = "TopicProjectionError";
3842
+ this.diagnostics = diagnostics;
3843
+ }
3844
+ };
3845
+
3846
+ // packages/core/src/topics.ts
3730
3847
  var MEMORY_TOPIC_PROJECTOR_VERSION = 3;
3731
3848
  var TOPIC_BATCH_LIMIT = 12;
3732
3849
  var TOPIC_CANDIDATE_LIMIT = 12;
@@ -3937,6 +4054,13 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3937
4054
  job.superseded = true;
3938
4055
  job.lastError = "source-changed";
3939
4056
  job.updatedAt = now;
4057
+ job.diagnostics = safeTopicDiagnostics({
4058
+ ...job.diagnostics,
4059
+ category: "source-changed",
4060
+ reason: "source version changed; stale result rejected",
4061
+ eventCount: job.sourceEventIds.length,
4062
+ attempt: job.attempts
4063
+ });
3940
4064
  }
3941
4065
  const finished = this.state.jobs.filter((job) => job.status === "completed" || job.status === "failed" && job.nextRetryAt === null);
3942
4066
  const retain = new Set([...finished.slice(-64), ...finished.filter((job) => job.status === "failed" && !job.superseded && job.attempts >= TOPIC_MAX_ATTEMPTS)].map((job) => job.id));
@@ -3962,6 +4086,14 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3962
4086
  }
3963
4087
  recover(now) {
3964
4088
  for (const job of this.state.jobs) if (job.status === "running" && Date.parse(job.leaseUntil ?? job.updatedAt) <= Date.parse(now)) {
4089
+ job.diagnostics = safeTopicDiagnostics({
4090
+ category: "timeout",
4091
+ reason: "topic projection lease expired",
4092
+ eventCount: job.sourceEventIds.length,
4093
+ candidateTopicCount: job.context?.existingTopics.length,
4094
+ attempt: job.attempts,
4095
+ splitDepth: job.splitDepth ?? 0
4096
+ });
3965
4097
  job.status = "failed";
3966
4098
  job.lastError = "timeout";
3967
4099
  job.context = null;
@@ -3999,7 +4131,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3999
4131
  const versions = sourceVersions(sources2);
4000
4132
  const running = this.state.jobs.find((job) => job.status === "running");
4001
4133
  if (running) return Date.parse(running.leaseUntil ?? running.updatedAt) <= now || !jobInputsCurrent(running, versions);
4002
- return this.pendingWork(events).some(({ event, sectionBackfillTopicId }) => this.matchesMode([event.id], versions, mode, sectionBackfillTopicId)) || this.state.jobs.some((job) => !job.superseded && job.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION && job.attempts < TOPIC_MAX_ATTEMPTS && (job.status === "pending" || job.status === "failed" && job.nextRetryAt !== null && Date.parse(job.nextRetryAt) <= now) && jobInputsCurrent(job, versions) && this.matchesMode(job.sourceEventIds, versions, mode, job.sectionBackfillTopicId));
4134
+ return this.pendingWork(events).some(({ event, sectionBackfillTopicId }) => this.matchesMode([event.id], versions, mode, sectionBackfillTopicId)) || this.state.jobs.some((job) => !job.superseded && job.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION && job.attempts < TOPIC_MAX_ATTEMPTS && (job.status === "pending" && (job.nextRetryAt === null || Date.parse(job.nextRetryAt) <= now) || job.status === "failed" && job.nextRetryAt !== null && Date.parse(job.nextRetryAt) <= now) && jobInputsCurrent(job, versions) && this.matchesMode(job.sourceEventIds, versions, mode, job.sectionBackfillTopicId));
4003
4135
  }
4004
4136
  claim(events, now, mode = "all") {
4005
4137
  if (this.state.bootstrap?.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION) {
@@ -4010,7 +4142,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4010
4142
  if (this.state.jobs.some((job2) => job2.status === "running")) return null;
4011
4143
  const sources2 = new Map(events.filter(visible).map((event) => [event.id, event]));
4012
4144
  const versions = sourceVersions(sources2);
4013
- const eligible = this.state.jobs.filter((job2) => !job2.superseded && job2.attempts < TOPIC_MAX_ATTEMPTS && (job2.status === "pending" || job2.status === "failed" && job2.nextRetryAt !== null && Date.parse(job2.nextRetryAt) <= Date.parse(now)) && jobInputsCurrent(job2, versions) && this.matchesMode(job2.sourceEventIds, versions, mode, job2.sectionBackfillTopicId)).sort((a, b) => Number(this.isBootstrap(a.sourceEventIds[0], versions.get(a.sourceEventIds[0]), a.sectionBackfillTopicId)) - Number(this.isBootstrap(b.sourceEventIds[0], versions.get(b.sourceEventIds[0]), b.sectionBackfillTopicId)))[0];
4145
+ const eligible = this.state.jobs.filter((job2) => !job2.superseded && job2.attempts < TOPIC_MAX_ATTEMPTS && (job2.status === "pending" && (job2.nextRetryAt === null || Date.parse(job2.nextRetryAt) <= Date.parse(now)) || job2.status === "failed" && job2.nextRetryAt !== null && Date.parse(job2.nextRetryAt) <= Date.parse(now)) && jobInputsCurrent(job2, versions) && this.matchesMode(job2.sourceEventIds, versions, mode, job2.sectionBackfillTopicId)).sort((a, b) => Number(this.isBootstrap(a.sourceEventIds[0], versions.get(a.sourceEventIds[0]), a.sectionBackfillTopicId)) - Number(this.isBootstrap(b.sourceEventIds[0], versions.get(b.sourceEventIds[0]), b.sectionBackfillTopicId)))[0];
4014
4146
  let job = eligible;
4015
4147
  if (job && this.isBootstrap(job.sourceEventIds[0], versions.get(job.sourceEventIds[0]), job.sectionBackfillTopicId) && mode === "all" && this.pendingWork(events).some(({ event, sectionBackfillTopicId }) => !this.isBootstrap(event.id, versions.get(event.id), sectionBackfillTopicId))) job = void 0;
4016
4148
  if (job?.status === "failed") {
@@ -4134,6 +4266,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4134
4266
  ...!storedById.get(topic.id).invalidated ? Object.keys(storedById.get(topic.id).dependencyVersions ?? {}).filter((id) => sources2.has(id)) : []
4135
4267
  ])]);
4136
4268
  job.dependencyVersions = Object.fromEntries(dependencyIds.map((id) => [id, versions.get(id)]));
4269
+ job.diagnostics = void 0;
4137
4270
  job.status = "running";
4138
4271
  job.attempts += 1;
4139
4272
  job.lastError = null;
@@ -4149,15 +4282,15 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4149
4282
  const job = this.requireJob(id);
4150
4283
  if (job.status === "completed") return { topicIds: [...job.topicIds] };
4151
4284
  if (job.status !== "running" || !job.context) throw new Error(`Topic projection ${id} is not running`);
4152
- if (Date.parse(job.leaseUntil ?? job.updatedAt) <= Date.parse(now)) throw new Error("Topic projection lease expired; stale result rejected.");
4285
+ if (Date.parse(job.leaseUntil ?? job.updatedAt) <= Date.parse(now)) throw new TopicProjectionError("source-changed", "Topic projection lease expired; stale result rejected.");
4153
4286
  const sources2 = new Map(events.filter(visible).map((event) => [event.id, event]));
4154
4287
  const versions = sourceVersions(sources2);
4155
4288
  if (!jobInputsCurrent(job, versions) || Object.entries(job.candidateVersions).some(([topicId, version]) => digest(this.state.topics.find((topic) => topic.id === topicId)) !== version)) {
4156
- throw new Error("Topic projection source version changed; stale result rejected.");
4289
+ throw new TopicProjectionError("source-changed", "Topic projection source version changed; stale result rejected.");
4157
4290
  }
4158
- if (!Array.isArray(result?.topics) || result.topics.length < 1 || result.topics.length > 12) throw new Error("Topic projection must return 1-12 topics.");
4291
+ if (!Array.isArray(result?.topics) || result.topics.length < 1 || result.topics.length > 12) throw new TopicProjectionError("validation-failed", "Topic projection must return 1-12 topics.");
4159
4292
  if (job.sectionBackfillTopicId && (result.topics.length !== 1 || result.topics[0].topicId !== job.sectionBackfillTopicId || result.topics[0].sections === void 0)) {
4160
- throw new Error("Invalid section backfill: explicit sections must cover the existing chapter.");
4293
+ throw new TopicProjectionError("validation-failed", "Invalid section backfill: explicit sections must cover the existing chapter.");
4161
4294
  }
4162
4295
  const candidates = new Map(job.context.existingTopics.map((topic) => [topic.id, topic]));
4163
4296
  const allowed = /* @__PURE__ */ new Set([...job.sourceEventIds, ...job.context.existingTopics.flatMap((topic) => topic.sourceEventIds)]);
@@ -4167,7 +4300,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4167
4300
  const labels = /* @__PURE__ */ new Set();
4168
4301
  const proposals = [];
4169
4302
  const checkIds = (ids, label) => {
4170
- if (!Array.isArray(ids) || ids.length === 0 || unique2(ids).length !== ids.length || ids.some((eventId) => typeof eventId !== "string" || !allowed.has(eventId))) throw new Error(`Invalid topic ${label} source events.`);
4303
+ if (!Array.isArray(ids) || ids.length === 0 || unique2(ids).length !== ids.length || ids.some((eventId) => typeof eventId !== "string" || !allowed.has(eventId))) throw new TopicProjectionError("validation-failed", `Invalid topic ${label} source events.`);
4171
4304
  return ids;
4172
4305
  };
4173
4306
  for (const proposal of result.topics) {
@@ -4177,45 +4310,45 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4177
4310
  const label = normalizedLabel;
4178
4311
  const sameLabel = typeof proposal.title === "string" ? this.state.topics.filter((topic) => !topic.invalidated && label(topic.title) === label(proposal.title)) : [];
4179
4312
  if (sameLabel.some((topic) => !candidates.has(topic.id))) {
4180
- throw new Error("Invalid duplicate chapter label matches an unexposed existing topic.");
4313
+ throw new TopicProjectionError("validation-failed", "Invalid duplicate chapter label matches an unexposed existing topic.");
4181
4314
  }
4182
4315
  const existing = proposal.topicId ? this.state.topics.find((topic) => topic.id === proposal.topicId) : inferred ?? (sameLabel.length === 1 ? sameLabel[0] : void 0);
4183
4316
  if (sameLabel.some((topic) => topic.id !== existing?.id)) {
4184
- throw new Error("Invalid duplicate chapter label conflicts with an existing topic.");
4317
+ throw new TopicProjectionError("validation-failed", "Invalid duplicate chapter label conflicts with an existing topic.");
4185
4318
  }
4186
4319
  if (typeof proposal.title === "string") {
4187
4320
  const key = label(proposal.title);
4188
- if (labels.has(key)) throw new Error("Invalid duplicate chapter label in one projection.");
4321
+ if (labels.has(key)) throw new TopicProjectionError("validation-failed", "Invalid duplicate chapter label in one projection.");
4189
4322
  labels.add(key);
4190
4323
  }
4191
- if (proposal.topicId && (!existing || !candidates.has(proposal.topicId))) throw new Error("Unknown or unexposed topic id.");
4324
+ if (proposal.topicId && (!existing || !candidates.has(proposal.topicId))) throw new TopicProjectionError("validation-failed", "Unknown or unexposed topic id.");
4192
4325
  const topicId = existing?.id ?? `topic_${randomUUID()}`;
4193
- if (touched.has(topicId)) throw new Error("A topic may be updated only once per projection.");
4326
+ if (touched.has(topicId)) throw new TopicProjectionError("validation-failed", "A topic may be updated only once per projection.");
4194
4327
  touched.add(topicId);
4195
- if (typeof proposal.title !== "string" || !proposal.title.trim() || [...proposal.title].length > 120 || typeof proposal.description !== "string" || !proposal.description.trim() || [...proposal.description].length > 400) throw new Error("Invalid topic title or description.");
4328
+ if (typeof proposal.title !== "string" || !proposal.title.trim() || [...proposal.title].length > 120 || typeof proposal.description !== "string" || !proposal.description.trim() || [...proposal.description].length > 400) throw new TopicProjectionError("validation-failed", "Invalid topic title or description.");
4196
4329
  for (const eventId of ids) if (batchIds.has(eventId)) covered.add(eventId);
4197
- if (!Array.isArray(proposal.overview) || proposal.overview.length > 8) throw new Error("Topic projection must contain at most 8 overview paragraphs per chapter per batch.");
4330
+ if (!Array.isArray(proposal.overview) || proposal.overview.length > 8) throw new TopicProjectionError("validation-failed", "Topic projection must contain at most 8 overview paragraphs per chapter per batch.");
4198
4331
  for (const part of proposal.overview) {
4199
- if (part.title !== void 0 && (typeof part.title !== "string" || !part.title.trim() || [...part.title].length > 80)) throw new Error("Invalid topic overview title.");
4200
- if (!["history", "decision", "change", "open-question", "scope"].includes(part.kind) || typeof part.text !== "string" || !part.text.trim() || [...part.text].length > 600) throw new Error("Invalid topic overview section.");
4332
+ if (part.title !== void 0 && (typeof part.title !== "string" || !part.title.trim() || [...part.title].length > 80)) throw new TopicProjectionError("validation-failed", "Invalid topic overview title.");
4333
+ if (!["history", "decision", "change", "open-question", "scope"].includes(part.kind) || typeof part.text !== "string" || !part.text.trim() || [...part.text].length > 600) throw new TopicProjectionError("validation-failed", "Invalid topic overview section.");
4201
4334
  checkIds(part.sourceEventIds, "overview");
4202
4335
  if (part.kind !== "scope" && part.sourceEventIds.some((eventId) => job.context.truncatedEventIds?.includes(eventId))) {
4203
- throw new Error("Truncated topic sources permit navigation scope only, not factual overview.");
4336
+ throw new TopicProjectionError("validation-failed", "Truncated topic sources permit navigation scope only, not factual overview.");
4204
4337
  }
4205
- if (part.sourceEventIds.length > 12 || part.sourceEventIds.some((eventId) => !ids.includes(eventId))) throw new Error("Overview references must belong to returned topic membership.");
4206
- if (part.sourceEventIds.some((eventId) => !batchIds.has(eventId)) && !candidates.get(topicId)?.overview.some((old) => digest(old) === digest(part))) throw new Error("Old overview evidence must be preserved verbatim; new sections need batch events.");
4338
+ if (part.sourceEventIds.length > 12 || part.sourceEventIds.some((eventId) => !ids.includes(eventId))) throw new TopicProjectionError("validation-failed", "Overview references must belong to returned topic membership.");
4339
+ if (part.sourceEventIds.some((eventId) => !batchIds.has(eventId)) && !candidates.get(topicId)?.overview.some((old) => digest(old) === digest(part))) throw new TopicProjectionError("validation-failed", "Old overview evidence must be preserved verbatim; new sections need batch events.");
4207
4340
  }
4208
- if (proposal.sections !== void 0 && !Array.isArray(proposal.sections)) throw new Error("Invalid topic sections.");
4341
+ if (proposal.sections !== void 0 && !Array.isArray(proposal.sections)) throw new TopicProjectionError("validation-failed", "Invalid topic sections.");
4209
4342
  const assignments = memoryTopicMembershipSections(proposal);
4210
4343
  const sectionLabels = /* @__PURE__ */ new Set();
4211
4344
  for (const section of assignments) {
4212
- if (!section || typeof section.title !== "string" || !section.title.trim() || [...section.title].length > 80 || sectionLabels.has(label(section.title))) throw new Error("Invalid or duplicate topic section title.");
4345
+ if (!section || typeof section.title !== "string" || !section.title.trim() || [...section.title].length > 80 || sectionLabels.has(label(section.title))) throw new TopicProjectionError("validation-failed", "Invalid or duplicate topic section title.");
4213
4346
  sectionLabels.add(label(section.title));
4214
4347
  checkIds(section.sourceEventIds, "section");
4215
- if (section.sourceEventIds.some((eventId) => !ids.includes(eventId))) throw new Error("Section references must belong to returned topic membership.");
4216
- if (section.sourceEventIds.some((eventId) => !batchIds.has(eventId)) && !memoryTopicMembershipSections(existing ?? { overview: [] }).some((old) => label(old.title) === label(section.title) && section.sourceEventIds.filter((eventId) => !batchIds.has(eventId)).every((eventId) => old.sourceEventIds.includes(eventId)))) throw new Error("Old section membership must be preserved; reassignment needs batch events.");
4348
+ if (section.sourceEventIds.some((eventId) => !ids.includes(eventId))) throw new TopicProjectionError("validation-failed", "Section references must belong to returned topic membership.");
4349
+ if (section.sourceEventIds.some((eventId) => !batchIds.has(eventId)) && !memoryTopicMembershipSections(existing ?? { overview: [] }).some((old) => label(old.title) === label(section.title) && section.sourceEventIds.filter((eventId) => !batchIds.has(eventId)).every((eventId) => old.sourceEventIds.includes(eventId)))) throw new TopicProjectionError("validation-failed", "Old section membership must be preserved; reassignment needs batch events.");
4217
4350
  }
4218
- if (proposal.sections !== void 0 && ids.some((eventId) => batchIds.has(eventId) && !assignments.some((section) => section.sourceEventIds.includes(eventId)))) throw new Error("Topic sections omitted batch events.");
4351
+ if (proposal.sections !== void 0 && ids.some((eventId) => batchIds.has(eventId) && !assignments.some((section) => section.sourceEventIds.includes(eventId)))) throw new TopicProjectionError("validation-failed", "Topic sections omitted batch events.");
4219
4352
  const membership = unique2([...(existing?.sourceEventIds ?? []).filter((eventId) => sources2.has(eventId)), ...ids]);
4220
4353
  const assignedIds = new Set(proposal.sections === void 0 ? [] : assignments.flatMap((section) => section.sourceEventIds).filter((eventId) => batchIds.has(eventId)));
4221
4354
  const sections = existing && !existing.invalidated ? memoryTopicMembershipSections(existing) : [];
@@ -4231,7 +4364,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4231
4364
  const inherited = existing && !existing.invalidated ? existing.overview : [];
4232
4365
  if (proposal.sections !== void 0) for (const part of proposal.overview) {
4233
4366
  if (!inherited.some((old) => digest(old) === digest(part)) && !memoryTopicOverviewMatchesSection(part, titles)) {
4234
- throw new Error("New topic overview must match a final topic section.");
4367
+ throw new TopicProjectionError("validation-failed", "New topic overview must match a final topic section.");
4235
4368
  }
4236
4369
  }
4237
4370
  const overview2 = unique2(proposal.overview.filter((part) => memoryTopicOverviewMatchesSection(part, titles)).map((part) => JSON.stringify(part))).map((part) => JSON.parse(part));
@@ -4251,7 +4384,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4251
4384
  updatedAt: now
4252
4385
  });
4253
4386
  }
4254
- if (job.sourceEventIds.some((eventId) => !covered.has(eventId))) throw new Error("Topic projection omitted batch events.");
4387
+ if (job.sourceEventIds.some((eventId) => !covered.has(eventId))) throw new TopicProjectionError("validation-failed", "every supplied batch Event must be assigned to a topic");
4255
4388
  for (const topic of proposals) {
4256
4389
  const index = this.state.topics.findIndex((current) => current.id === topic.id);
4257
4390
  if (index < 0) this.state.topics.push(topic);
@@ -4281,13 +4414,49 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4281
4414
  fail(id, error, now) {
4282
4415
  const job = this.state.jobs.find((candidate) => candidate.id === id);
4283
4416
  if (!job || job.status !== "running") return;
4417
+ const diagnostic = error instanceof TopicProjectionError ? safeTopicDiagnostics(error.diagnostics) : void 0;
4418
+ const message = error instanceof Error ? error.message : String(error);
4419
+ const category = diagnostic?.category ?? (/timeout|timed out/i.test(message) ? "timeout" : /source.*changed|stale|not running/i.test(message) ? "source-changed" : "worker-failed");
4420
+ job.diagnostics = safeTopicDiagnostics({
4421
+ ...diagnostic,
4422
+ category,
4423
+ reason: diagnostic?.reason ?? (category === "timeout" ? "structured model task timed out" : category === "source-changed" ? "source version changed; stale result rejected" : "topic worker failed"),
4424
+ eventCount: job.sourceEventIds.length,
4425
+ candidateTopicCount: diagnostic?.candidateTopicCount ?? job.context?.existingTopics.length,
4426
+ attempt: job.attempts,
4427
+ splitDepth: job.splitDepth ?? 0
4428
+ });
4284
4429
  job.status = "failed";
4285
4430
  job.context = null;
4286
4431
  job.leaseUntil = null;
4287
- const message = error instanceof Error ? error.message : String(error);
4288
- job.lastError = /timeout|timed out/i.test(message) ? "timeout" : /source.*changed|stale|not running/i.test(message) ? "source-changed" : /invalid|omitted|unknown|overview|projection must|truncat/i.test(message) ? "invalid-output" : "worker-failed";
4432
+ job.lastError = category === "validation-failed" ? `${category}: ${job.diagnostics.reason}` : category;
4289
4433
  job.nextRetryAt = job.attempts < TOPIC_MAX_ATTEMPTS ? new Date(Date.parse(now) + 3e4 * 2 ** Math.max(0, job.attempts - 1)).toISOString() : null;
4290
4434
  job.updatedAt = now;
4435
+ if (category === "max-tokens" && diagnostic?.finishReason === "max-tokens" && job.sourceEventIds.length > 1 && (job.splitDepth ?? 0) < 4) {
4436
+ const middle = Math.ceil(job.sourceEventIds.length / 2);
4437
+ for (const ids of [job.sourceEventIds.slice(0, middle), job.sourceEventIds.slice(middle)]) {
4438
+ this.state.jobs.push({
4439
+ ...structuredClone(job),
4440
+ id: `tproj_${randomUUID()}`,
4441
+ sourceEventIds: ids,
4442
+ sourceVersions: Object.fromEntries(ids.map((id2) => [id2, job.sourceVersions[id2]])),
4443
+ dependencyVersions: {},
4444
+ candidateVersions: {},
4445
+ status: "pending",
4446
+ attempts: 0,
4447
+ diagnostics: void 0,
4448
+ splitDepth: (job.splitDepth ?? 0) + 1,
4449
+ topicIds: [],
4450
+ superseded: false,
4451
+ lastError: null,
4452
+ nextRetryAt: job.nextRetryAt,
4453
+ createdAt: now,
4454
+ updatedAt: now
4455
+ });
4456
+ }
4457
+ job.superseded = true;
4458
+ job.nextRetryAt = null;
4459
+ }
4291
4460
  }
4292
4461
  requireJob(id) {
4293
4462
  const job = this.state.jobs.find((candidate) => candidate.id === id);
@@ -4311,6 +4480,8 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4311
4480
  dependencyVersions: {},
4312
4481
  candidateVersions: {},
4313
4482
  topicIds: [],
4483
+ diagnostics: void 0,
4484
+ splitDepth: 0,
4314
4485
  lastError: null,
4315
4486
  nextRetryAt: null,
4316
4487
  leaseUntil: null,
@@ -7754,7 +7925,7 @@ ${JSON_RETRY_INSTRUCTION}`, tool: toolSchema("topicProjector"), payload }));
7754
7925
  }
7755
7926
  function parseTopicProjection(value, context) {
7756
7927
  const fail = (message) => {
7757
- throw new Error(`Topic projection validation failed: ${message}`);
7928
+ throw new TopicProjectionError("validation-failed", message);
7758
7929
  };
7759
7930
  const raw = object(value);
7760
7931
  if (Object.keys(raw).some((key) => key !== "topics") || !Array.isArray(raw.topics) || raw.topics.length > 12) {
@@ -7841,6 +8012,7 @@ function parseTopicProjection(value, context) {
7841
8012
  if ([...eventIds].some((id) => !covered.has(id))) fail("every supplied batch Event must be assigned to a topic");
7842
8013
  return { topics };
7843
8014
  }
8015
+ var TOPIC_OUTPUT_TOKEN_BUDGET = 32768;
7844
8016
  var DshModelBridge = class {
7845
8017
  constructor(ctx, config, liveStructuredReasoningEffort) {
7846
8018
  this.ctx = ctx;
@@ -7849,12 +8021,14 @@ var DshModelBridge = class {
7849
8021
  this.structuredReasoningEffort = config.structuredReasoningEffort ?? "auto";
7850
8022
  this.ctx.on?.("llm/adapters-updated", () => {
7851
8023
  this.offCapabilities.clear();
8024
+ this.contextWindows.clear();
7852
8025
  for (const listener of this.adaptersUpdatedListeners) listener();
7853
8026
  });
7854
8027
  }
7855
8028
  ctx;
7856
8029
  config;
7857
8030
  liveStructuredReasoningEffort;
8031
+ contextWindows = /* @__PURE__ */ new Map();
7858
8032
  sessions = new AsyncLocalStorage();
7859
8033
  successfulResponses = [];
7860
8034
  offCapabilities = /* @__PURE__ */ new Map();
@@ -8084,7 +8258,7 @@ Use project scope for repository decisions, user scope for stable preferences/id
8084
8258
  return { reason: text2(raw.reason, "Projected Event evidence into the Knowledge Graph."), nodes, edges };
8085
8259
  };
8086
8260
  topicProjector = async (context) => {
8087
- const { payload, shownContext } = topicProjectionPayload(context, this.config.maxOutputTokens);
8261
+ const { payload, shownContext } = topicProjectionPayload(context, TOPIC_OUTPUT_TOKEN_BUDGET);
8088
8262
  try {
8089
8263
  const raw = await this.callStructured(
8090
8264
  "topicProjector",
@@ -8092,13 +8266,16 @@ Use project scope for repository decisions, user scope for stable preferences/id
8092
8266
  payload,
8093
8267
  (value) => {
8094
8268
  parseTopicProjection(value, shownContext);
8095
- }
8269
+ },
8270
+ { eventCount: shownContext.events.length, candidateTopicCount: shownContext.existingTopics.length }
8096
8271
  );
8097
8272
  return parseTopicProjection(raw, shownContext);
8098
8273
  } catch (error) {
8099
- if (error instanceof ModelJsonResponseError) throw new Error(error.message.split("\nRaw response preview")[0]);
8100
- if (error instanceof Error && /^StrataGate structured model task timed out after \d+ms$/.test(error.message)) throw new Error(error.message);
8101
- throw new Error("Topic projection model call failed (provider or route error); raw details omitted from diagnostics");
8274
+ if (error instanceof TopicProjectionError) throw error;
8275
+ if (error instanceof Error && /^StrataGate structured model task timed out after \d+ms$/.test(error.message)) {
8276
+ throw new TopicProjectionError("timeout", "structured model task timed out");
8277
+ }
8278
+ throw new TopicProjectionError("provider-failed", "provider or route error");
8102
8279
  }
8103
8280
  };
8104
8281
  externalMemoryDecider = async (context) => {
@@ -8150,17 +8327,25 @@ Use project scope for repository decisions, user scope for stable preferences/id
8150
8327
  }
8151
8328
  return proposed;
8152
8329
  }
8153
- async callStructured(kind, system, payload, validate) {
8330
+ async callStructured(kind, system, payload, validate, topicMetadata) {
8154
8331
  const execution = this.sessions.getStore();
8155
8332
  if (!execution) throw new Error("StrataGate model callback ran without an execution context");
8156
8333
  const baseRoute = this.resolveRoute(execution.session);
8157
8334
  const routeKey = `${baseRoute.provider}\0${baseRoute.model}`;
8158
8335
  let useOff = await this.shouldUseOff(baseRoute);
8336
+ const isTopic = kind === "topicProjector";
8337
+ const topicDiagnostics = {
8338
+ ...topicMetadata,
8339
+ requestedOutputTokens: TOPIC_OUTPUT_TOKEN_BUDGET,
8340
+ modelCalls: 0
8341
+ };
8342
+ const topicError = (category, reason) => new TopicProjectionError(category, reason, topicDiagnostics);
8159
8343
  let lastError;
8160
8344
  let lastResponse = "";
8161
8345
  let attemptsUsed = 0;
8162
8346
  let noAdapterRetried = false;
8163
8347
  let graphRetryFeedback = "";
8348
+ let offFallback = false;
8164
8349
  for (let attempt = 1; attempt <= JSON_RESPONSE_ATTEMPTS; attempt += 1) {
8165
8350
  attemptsUsed = attempt;
8166
8351
  const message = createUserMessage({
@@ -8184,10 +8369,33 @@ ${JSON_RETRY_INSTRUCTION}${graphRetryFeedback}`,
8184
8369
  type: "function",
8185
8370
  function: { name: STRUCTURED_TOOLS[kind].name }
8186
8371
  },
8187
- maxTokens: kind === "profileMaintenance" ? Math.max(this.config.maxOutputTokens, Math.min(12e3, 512 + 2 * Array.from(JSON.stringify(payload)).length)) : this.config.maxOutputTokens,
8372
+ maxTokens: kind === "profileMaintenance" ? Math.max(this.config.maxOutputTokens, Math.min(12e3, 512 + 2 * Array.from(JSON.stringify(payload)).length)) : isTopic ? TOPIC_OUTPUT_TOKEN_BUDGET : this.config.maxOutputTokens,
8188
8373
  sessionId: execution.sessionId,
8189
8374
  purpose: "compaction"
8190
8375
  };
8376
+ if (isTopic) {
8377
+ const estimateInput = () => estimateTokens(JSON.stringify({ system: request.system, tools: request.tools, messages: request.messages }));
8378
+ let inputTokens = estimateInput();
8379
+ const window = this.contextWindows.get(routeKey);
8380
+ if (window !== void 0 && window - inputTokens - 1024 < 256) {
8381
+ Object.assign(topicDiagnostics, { estimatedInputTokens: inputTokens, maxOutputTokens: 0 });
8382
+ throw topicError("validation-failed", "topic input exceeds model context capacity");
8383
+ }
8384
+ if (window !== void 0) request.maxTokens = Math.min(TOPIC_OUTPUT_TOKEN_BUDGET, Math.floor(window - inputTokens - 1024));
8385
+ request.messages = [{ ...message, content: [{ type: "text", text: JSON.stringify({
8386
+ ...payload,
8387
+ outputTokenBudget: request.maxTokens
8388
+ }) }] }];
8389
+ inputTokens = estimateInput();
8390
+ delete topicDiagnostics.finishReason;
8391
+ delete topicDiagnostics.reasoningObserved;
8392
+ Object.assign(topicDiagnostics, {
8393
+ estimatedInputTokens: inputTokens,
8394
+ maxOutputTokens: request.maxTokens,
8395
+ modelCalls: (topicDiagnostics.modelCalls ?? 0) + 1,
8396
+ reasoningOff: useOff ? "requested-unverified" : offFallback ? "fallback" : "unavailable"
8397
+ });
8398
+ }
8191
8399
  try {
8192
8400
  await this.consumeStructuredStream(request, assembler);
8193
8401
  } catch (error) {
@@ -8199,13 +8407,19 @@ ${JSON_RETRY_INSTRUCTION}${graphRetryFeedback}`,
8199
8407
  if (useOff && isOffRejection(error)) {
8200
8408
  this.offCapabilities.set(routeKey, "unsupported");
8201
8409
  useOff = false;
8410
+ offFallback = true;
8202
8411
  this.warnOffFallbackOnce(routeKey, `${baseRoute.provider}/${baseRoute.model} rejected reasoningEffort=off; retrying once without it`);
8203
8412
  attempt -= 1;
8204
8413
  continue;
8205
8414
  }
8415
+ if (isTopic) {
8416
+ const timedOut = error instanceof Error && /^StrataGate structured model task timed out after \d+ms$/.test(error.message);
8417
+ throw topicError(timedOut ? "timeout" : "provider-failed", timedOut ? "structured model task timed out" : "provider or route error");
8418
+ }
8206
8419
  throw error;
8207
8420
  }
8208
8421
  const finish = assembler.finish;
8422
+ if (isTopic) topicDiagnostics.finishReason = finish.kind;
8209
8423
  if (finish.kind === "error" || finish.kind === "aborted") {
8210
8424
  const failure = new Error(`StrataGate model call failed [${finish.failure.code}]: ${finish.failure.message}`);
8211
8425
  if (!noAdapterRetried && isNoAdapter(finish.failure)) {
@@ -8216,26 +8430,34 @@ ${JSON_RETRY_INSTRUCTION}${graphRetryFeedback}`,
8216
8430
  if (useOff && isOffRejection(finish.failure)) {
8217
8431
  this.offCapabilities.set(routeKey, "unsupported");
8218
8432
  useOff = false;
8433
+ offFallback = true;
8219
8434
  this.warnOffFallbackOnce(routeKey, `${baseRoute.provider}/${baseRoute.model} rejected reasoningEffort=off; retrying once without it`);
8220
8435
  attempt -= 1;
8221
8436
  continue;
8222
8437
  }
8438
+ if (isTopic) throw topicError("provider-failed", "provider or route error");
8223
8439
  throw failure;
8224
8440
  }
8225
8441
  if (useOff) this.offCapabilities.set(routeKey, "supported");
8226
8442
  const blocks = assembler.blocks();
8227
8443
  const calls = blocks.filter((block) => block.type === "tool-call");
8228
- const responseForError = `${renderBlocksForDiagnostics(blocks, finish.kind)}
8444
+ if (isTopic) {
8445
+ topicDiagnostics.reasoningObserved = blocks.some((block) => block.type === "reasoning" && block.text.trim().length > 0);
8446
+ if (topicDiagnostics.reasoningObserved && useOff) topicDiagnostics.reasoningOff = "reasoning-observed";
8447
+ if (finish.kind === "max-tokens") throw topicError("max-tokens", "output token limit reached");
8448
+ }
8449
+ const responseForError = isTopic ? "" : `${renderBlocksForDiagnostics(blocks, finish.kind)}
8229
8450
  [finish=${finish.kind}; toolCalls=${calls.length}]`;
8230
8451
  lastResponse = responseForError;
8231
8452
  try {
8232
8453
  const expectedTool = STRUCTURED_TOOLS[kind].name;
8233
8454
  let parsed;
8234
8455
  if (calls.length !== 1 || calls[0]?.name !== expectedTool) {
8235
- const textFallback = blocks.filter((block) => block.type === "text" || block.type === "reasoning").map((block) => block.text).join("\n");
8456
+ const textFallback = blocks.filter((block) => block.type === "text" || !isTopic && block.type === "reasoning").map((block) => block.text).join("\n");
8236
8457
  try {
8237
8458
  parsed = parseJsonResponse(textFallback, STRUCTURED_FIELDS[kind]);
8238
8459
  } catch {
8460
+ if (isTopic) throw topicError("schema-invalid", "expected exactly one structured topic call");
8239
8461
  throw new ModelJsonResponseError(
8240
8462
  `StrataGate model response did not call ${expectedTool} exactly once`,
8241
8463
  { response: responseForError }
@@ -8245,6 +8467,7 @@ ${JSON_RETRY_INSTRUCTION}${graphRetryFeedback}`,
8245
8467
  try {
8246
8468
  parsed = JSON.parse(calls[0].arguments);
8247
8469
  } catch {
8470
+ if (isTopic) throw topicError("schema-invalid", "tool arguments are not valid JSON");
8248
8471
  throw new ModelJsonResponseError(
8249
8472
  `StrataGate ${expectedTool} arguments were not valid JSON`,
8250
8473
  { response: responseForError }
@@ -8254,13 +8477,14 @@ ${JSON_RETRY_INSTRUCTION}${graphRetryFeedback}`,
8254
8477
  if (kind === "graphProjector") parsed = normalizeGraphNameProvenance(parsed);
8255
8478
  const violations = validateArgs(STRUCTURED_TOOLS[kind].parameters, parsed);
8256
8479
  if (violations.length > 0) {
8480
+ if (isTopic) throw topicError("schema-invalid", "structured topic schema mismatch");
8257
8481
  if (kind === "graphProjector") {
8258
8482
  const paths = violations.flatMap((violation) => violation.match(/nodes\[\d+\]\.metadataProvenance\.name(?:\[\d+\])?/g) ?? []);
8259
8483
  graphRetryFeedback = paths.length > 0 ? `
8260
8484
  Correct these canonical-name provenance fields: ${[...new Set(paths)].slice(0, 24).join(", ")}. metadataProvenance.name must be an array of supplied Event ID strings, e.g. ["evt_x"], supporting the actual node.name. Only aliases/tags use { value, sourceEventIds } objects. Do not substitute provenance for a different name.` : "";
8261
8485
  }
8262
8486
  throw new ModelJsonResponseError(
8263
- `StrataGate ${expectedTool} arguments were invalid: ${kind === "topicProjector" ? "structured topic schema mismatch" : violations.join("; ")}`,
8487
+ `StrataGate ${expectedTool} arguments were invalid: ${violations.join("; ")}`,
8264
8488
  { response: responseForError }
8265
8489
  );
8266
8490
  }
@@ -8276,6 +8500,7 @@ Correct these canonical-name provenance fields: ${[...new Set(paths)].slice(0, 2
8276
8500
  try {
8277
8501
  validate?.(parsed);
8278
8502
  } catch (error) {
8503
+ if (isTopic) throw topicError("validation-failed", error instanceof TopicProjectionError ? error.diagnostics.reason : "topic semantic validation failed");
8279
8504
  throw new ModelJsonResponseError(
8280
8505
  `StrataGate ${expectedTool} arguments were invalid: ${error instanceof Error ? error.message : String(error)}`,
8281
8506
  { cause: error, response: responseForError }
@@ -8292,7 +8517,7 @@ Correct these canonical-name provenance fields: ${[...new Set(paths)].slice(0, 2
8292
8517
  }
8293
8518
  return parsed;
8294
8519
  } catch (error) {
8295
- if (!(error instanceof ModelJsonResponseError)) throw error;
8520
+ if (!(error instanceof ModelJsonResponseError) && !(isTopic && error instanceof TopicProjectionError)) throw error;
8296
8521
  lastError = finish.kind === "max-tokens" ? new ModelJsonResponseError(
8297
8522
  `StrataGate ${STRUCTURED_TOOLS[kind].name} call was truncated before valid arguments`,
8298
8523
  { cause: error, response: responseForError }
@@ -8303,6 +8528,7 @@ Correct these canonical-name provenance fields: ${[...new Set(paths)].slice(0, 2
8303
8528
  }
8304
8529
  }
8305
8530
  }
8531
+ if (isTopic && lastError instanceof TopicProjectionError) throw lastError;
8306
8532
  const validationDetail = lastError?.message ? `: ${lastError.message}` : "";
8307
8533
  throw new ModelJsonResponseError(
8308
8534
  `StrataGate model did not produce a valid ${STRUCTURED_TOOLS[kind].name} call after ${attemptsUsed} attempt${attemptsUsed === 1 ? "" : "s"}${validationDetail}`,
@@ -8339,6 +8565,7 @@ Correct these canonical-name provenance fields: ${[...new Set(paths)].slice(0, 2
8339
8565
  timer.unref?.();
8340
8566
  })
8341
8567
  ]);
8568
+ if (Number.isFinite(info.context?.contextWindow) && info.context.contextWindow > 0) this.contextWindows.set(key, info.context.contextWindow);
8342
8569
  if (!info.reasoning) return this.currentStructuredReasoningEffort() === "force-off";
8343
8570
  const supported = info.reasoning.efforts.some(({ id }) => String(id) === "off");
8344
8571
  this.offCapabilities.set(key, supported ? "supported" : "unsupported");
@@ -8790,11 +9017,11 @@ function topicNavigation(topics, events) {
8790
9017
  coverage: topic.coverage,
8791
9018
  isFallback: topic.isFallback === true
8792
9019
  }));
8793
- const categories = CATEGORIES.map((category) => ({
9020
+ const categories2 = CATEGORIES.map((category) => ({
8794
9021
  ...category,
8795
9022
  count: entries.filter((entry) => entry.category === category.id).length
8796
9023
  })).filter(({ count }) => count > 0);
8797
- return { entries, categories };
9024
+ return { entries, categories: categories2 };
8798
9025
  }
8799
9026
  function topicPage(topics, events, options = {}) {
8800
9027
  const navigation = topicNavigation(topics, events);
@@ -8818,27 +9045,58 @@ function lineText(value, maximum) {
8818
9045
  return value.replace(/\s+/g, " ").trim().slice(0, maximum);
8819
9046
  }
8820
9047
  function renderMemoryDirectory(topics, events) {
8821
- const { entries, categories } = topicNavigation(topics, events);
9048
+ const { entries, categories: categories2 } = topicNavigation(topics, events);
8822
9049
  if (entries.length === 0) return "";
8823
- const header = "[StrataGate \u8BB0\u5FC6\u76EE\u5F55]\n\u4EC5\u4F9B\u5BFC\u822A\uFF0C\u6982\u89C8\u4E0D\u662F\u4E8B\u5B9E\u8BC1\u636E\u3002\u9700\u8981\u4E3B\u9898\u8109\u7EDC\u65F6\u7528 memory_expand_topic\uFF1B\u9700\u8981\u4E8B\u5B9E\u8BC1\u636E\u53EF\u76F4\u63A5 memory_search_events(topic_id)\uFF0C\u4E5F\u53EF\u76F4\u63A5\u641C\u7D22\u4E8B\u4EF6/\u56FE\u8C31\u3002";
8824
- const lines = entries.map((entry) => `- ${entry.id}\uFF1A${lineText(entry.title, 64)}${entry.description ? `\uFF1B${lineText(entry.description, 48)}` : ""}`);
8825
- const complete = `${header}
8826
- ${lines.join("\n")}`;
9050
+ const header = "[StrataGate \u8BB0\u5FC6\u76EE\u5F55]\n\u4EC5\u4F9B\u5BFC\u822A\uFF0C\u4E0D\u662F\u4E8B\u5B9E\u8BC1\u636E\u3002\u9700\u8981\u4E3B\u9898\u8109\u7EDC\u65F6\u7528 memory_expand_topic\uFF1B\u9700\u8981\u4E8B\u5B9E\u8BC1\u636E\u53EF\u76F4\u63A5 memory_search_events(topic_id)\uFF0C\u4E5F\u53EF\u76F4\u63A5\u641C\u7D22\u4E8B\u4EF6/\u56FE\u8C31\u3002";
9051
+ const byId = new Map(topics.map((topic) => [topic.id, topic]));
9052
+ const chapters = entries.map((entry) => ({
9053
+ ...entry,
9054
+ line: `- ${entry.id}\uFF1A${lineText(entry.title, 64)}${entry.description ? `\uFF1B${lineText(entry.description, 48)}` : ""}`,
9055
+ // Reuse the book's stable section order, including summary-free and legacy sections.
9056
+ sections: memoryTopicSections(byId.get(entry.id)).map(({ title }) => ` - ${lineText(title, 64)}`)
9057
+ }));
9058
+ const render = (prefix, selected2) => `${prefix}
9059
+ ${selected2.flatMap(({ line, sections }) => [line, ...sections]).join("\n")}`.trim();
9060
+ const complete = render(header, chapters);
8827
9061
  if (estimateTokens(complete) <= MEMORY_DIRECTORY_TOKEN_BUDGET) return complete;
8828
- const branches = categories.map(({ id, label, count }) => `${id}\uFF08${label} ${count}\uFF09`).join("\uFF1B");
9062
+ const branches = categories2.map(({ id, label, count }) => `${id}\uFF08${label} ${count}\uFF09`).join("\uFF1B");
8829
9063
  const navigation = `${header}
8830
- \u5171 ${entries.length} \u9879\uFF1B\u5206\u7C7B\uFF1A${branches}\u3002\u5B8C\u6574\u76EE\u5F55\u7528 memory_list_topics(category, offset) \u5206\u9875\uFF1Bquery \u53EF\u67E5\u4E3B\u9898\u3002\u4E0B\u5217\u4E3A\u90E8\u5206\u5165\u53E3\uFF1A`;
8831
- const selected = [];
8832
- for (const category of categories) {
8833
- const entry = entries.find((item) => item.category === category.id);
8834
- const line = `- ${entry.id}\uFF1A${lineText(entry.title, 40)}`;
8835
- if (estimateTokens(`${navigation}
8836
- ${[...selected.map(({ line: line2 }) => line2), line].join("\n")}`) <= MEMORY_DIRECTORY_TOKEN_BUDGET) selected.push({ id: entry.id, line });
9064
+ \u5171 ${entries.length} \u9879\uFF1B\u5206\u7C7B\uFF1A${branches}\u3002\u76EE\u5F55\u5DF2\u88C1\u526A\uFF1B\u5B8C\u6574\u76EE\u5F55\u7528 memory_list_topics(category, offset) \u5206\u9875\uFF1Bquery \u53EF\u67E5\u4E3B\u9898\uFF0C\u5C0F\u8282\u7528 memory_expand_topic(id)\u3002`;
9065
+ let selected = chapters.map((chapter) => ({ ...chapter, sections: [] }));
9066
+ if (estimateTokens(render(navigation, selected)) > MEMORY_DIRECTORY_TOKEN_BUDGET) {
9067
+ selected = [];
9068
+ const admit = (chapter) => {
9069
+ for (const line of [chapter.line, `- ${chapter.id}\uFF1A${lineText(chapter.title, 64)}`]) {
9070
+ const candidate = { ...chapter, line, sections: [] };
9071
+ if (estimateTokens(render(navigation, [...selected, candidate])) <= MEMORY_DIRECTORY_TOKEN_BUDGET) {
9072
+ selected.push(candidate);
9073
+ return;
9074
+ }
9075
+ }
9076
+ };
9077
+ for (const category of categories2) admit(chapters.find((chapter) => chapter.category === category.id));
9078
+ for (const chapter of chapters) if (!selected.some(({ id }) => id === chapter.id)) admit(chapter);
8837
9079
  }
8838
9080
  const order = new Map(entries.map(({ id }, index) => [id, index]));
8839
9081
  selected.sort((a, b) => order.get(a.id) - order.get(b.id));
8840
- return `${navigation}
8841
- ${selected.map(({ line }) => line).join("\n")}`.trim();
9082
+ const sectionLines = new Map(chapters.map(({ id, sections }) => [id, sections]));
9083
+ const exhausted = /* @__PURE__ */ new Set();
9084
+ for (let index = 0; exhausted.size < selected.length; index++) {
9085
+ for (const chapter of selected) {
9086
+ if (exhausted.has(chapter.id)) continue;
9087
+ const line = sectionLines.get(chapter.id)[index];
9088
+ if (line === void 0) {
9089
+ exhausted.add(chapter.id);
9090
+ continue;
9091
+ }
9092
+ chapter.sections.push(line);
9093
+ if (estimateTokens(render(navigation, selected)) > MEMORY_DIRECTORY_TOKEN_BUDGET) {
9094
+ chapter.sections.pop();
9095
+ exhausted.add(chapter.id);
9096
+ }
9097
+ }
9098
+ }
9099
+ return render(navigation, selected);
8842
9100
  }
8843
9101
  function boundedTopic(topic, envelope = {}) {
8844
9102
  const { sections: _sections, ...summaryTopic } = topic;
@@ -9384,9 +9642,21 @@ var StrataGateRuntime = class {
9384
9642
  }
9385
9643
  async buildMemoryDirectory(session) {
9386
9644
  await this.flush();
9387
- const memory = await this.space(session);
9388
- await this.refreshExternalImportMemory(this.namespaceFor(session), memory);
9389
- return renderMemoryDirectory(this.visibleTopics(memory), memory.listAllEvents());
9645
+ const namespace = this.namespaceFor(session);
9646
+ const active = this.spaces.get(namespace);
9647
+ if (active) {
9648
+ const memory = await active;
9649
+ await this.refreshExternalImportMemory(namespace, memory);
9650
+ return renderMemoryDirectory(this.visibleTopics(memory), memory.listAllEvents());
9651
+ }
9652
+ if (this.config.database === ":memory:" || !existsSync(this.config.database)) return "";
9653
+ const storage = new SqliteStorage({ filename: this.config.database, readonly: true });
9654
+ try {
9655
+ const memory = await StrataGate.openWithStorage({ storage, namespace });
9656
+ return renderMemoryDirectory(this.visibleTopics(memory), memory.listAllEvents());
9657
+ } finally {
9658
+ await storage.close();
9659
+ }
9390
9660
  }
9391
9661
  visibleTopics(memory) {
9392
9662
  if (this.agentMemoryRetrievalWeight > 0) return memory.listMemoryTopics();
@@ -9663,6 +9933,7 @@ var StrataGateRuntime = class {
9663
9933
  async buildAutoContext(session) {
9664
9934
  await this.flush();
9665
9935
  const memory = await this.space(session);
9936
+ await this.refreshExternalImportMemory(this.namespaceFor(session), memory);
9666
9937
  const threadId = String(session.id);
9667
9938
  const blockContexts = memory.getBlockContext(threadId);
9668
9939
  if (this.syncDecayedBlockSurface(session, memory, blockContexts)) {
@@ -10467,6 +10738,7 @@ var StrataGateRuntime = class {
10467
10738
  const jobId = id.trim();
10468
10739
  if (!key) throw new TypeError("StrataGate admin namespace must not be empty");
10469
10740
  if (!jobId) throw new TypeError("StrataGate job id must not be empty");
10741
+ if (kind === "topic-projection") return this.adminRetryTopicProjection(key, jobId);
10470
10742
  const retryKey = `${key}\0${kind}\0${jobId}`;
10471
10743
  const existing = this.adminJobRetryRuns.get(retryKey);
10472
10744
  if (existing) return existing;
@@ -11976,6 +12248,7 @@ function topicDirectoryProjection(snapshot, agentMemoryWeight) {
11976
12248
  const failedWork = /* @__PURE__ */ new Set();
11977
12249
  const failures = state.jobs.flatMap((job) => {
11978
12250
  if (job.superseded || job.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION || job.status !== "failed" || job.attempts < TOPIC_MAX_ATTEMPTS || !Object.entries({ ...job.sourceVersions, ...job.dependencyVersions }).every(([id, version]) => versions.get(id) === version)) return [];
12251
+ const diagnostics = safeTopicDiagnostics(job.diagnostics);
11979
12252
  const ids = job.sourceEventIds.filter((id) => {
11980
12253
  const key = relationKey(job.sectionBackfillTopicId, id, job.sourceVersions[id]);
11981
12254
  if (!outstandingWork.has(key)) return false;
@@ -11987,7 +12260,8 @@ function topicDirectoryProjection(snapshot, agentMemoryWeight) {
11987
12260
  eventIds: ids,
11988
12261
  attempts: job.attempts,
11989
12262
  // Topic failures store reason codes, never raw model output.
11990
- lastError: ["timeout", "source-changed", "invalid-output", "worker-failed"].includes(job.lastError ?? "") ? job.lastError : "worker-failed",
12263
+ diagnostics,
12264
+ lastError: diagnostics ? `${diagnostics.category}: ${diagnostics.reason}` : ["timeout", "source-changed", "invalid-output", "worker-failed"].includes(job.lastError ?? "") ? job.lastError : "worker-failed",
11991
12265
  updatedAt: job.updatedAt
11992
12266
  }] : [];
11993
12267
  });
@@ -12203,17 +12477,41 @@ async function overview(runtime, cachedEntries) {
12203
12477
  sourceEventIds: job.sourceEventIds
12204
12478
  };
12205
12479
  };
12480
+ const topicJobs = (snapshot.memoryTopicState?.jobs ?? []).filter((job) => !job.superseded);
12481
+ const describeTopicJob = (job) => {
12482
+ const diagnostics = safeTopicDiagnostics(job.diagnostics);
12483
+ const lastError = diagnostics ? `${diagnostics.category}: ${diagnostics.reason}` : ["timeout", "source-changed", "invalid-output", "worker-failed"].includes(job.lastError ?? "") ? job.lastError : "worker-failed";
12484
+ return {
12485
+ id: job.id,
12486
+ kind: "topic-projection",
12487
+ state: derivationJobState(job, job.status === "completed"),
12488
+ status: job.status,
12489
+ attempts: job.attempts,
12490
+ nextRetryAt: job.nextRetryAt,
12491
+ updatedAt: job.updatedAt,
12492
+ lastError,
12493
+ lastErrorFull: lastError,
12494
+ diagnostics,
12495
+ eventCount: job.sourceEventIds.length,
12496
+ blockIds: [],
12497
+ threadIds: [],
12498
+ blockDetails: []
12499
+ };
12500
+ };
12501
+ const topicStatus = summarizeDerivationJobs(topicJobs, (job) => job.status === "completed");
12206
12502
  const summaryStatus = summarizeDerivationJobs(snapshot.summaryJobs, (job) => job.status === "succeeded");
12207
12503
  const extractionStatus = summarizeDerivationJobs(snapshot.extractionJobs, (job) => job.status === "succeeded" || job.status === "skipped");
12208
12504
  const graphStatus = summarizeDerivationJobs(snapshot.graphProjectionJobs, (job) => job.status === "completed");
12209
- const failedJobs = summaryStatus.terminalFailed + extractionStatus.terminalFailed + graphStatus.terminalFailed;
12210
- const processingJobs = summaryStatus.processing + extractionStatus.processing + graphStatus.processing;
12505
+ const failedJobs = summaryStatus.terminalFailed + extractionStatus.terminalFailed + graphStatus.terminalFailed + topicStatus.terminalFailed;
12506
+ const processingJobs = summaryStatus.processing + extractionStatus.processing + graphStatus.processing + topicStatus.processing;
12211
12507
  const failedJobDetails = [
12508
+ ...topicJobs.filter((job) => job.status === "failed").map(describeTopicJob),
12212
12509
  ...snapshot.summaryJobs.filter(({ status }) => status === "failed").map((job) => describeBlockJob("block-summary", job)),
12213
12510
  ...snapshot.extractionJobs.filter(({ status }) => status === "failed").map((job) => describeBlockJob("event-extraction", job)),
12214
12511
  ...snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").map(describeGraphJob)
12215
12512
  ];
12216
12513
  const processingJobDetails = [
12514
+ ...topicJobs.filter((job) => ["processing", "retryable"].includes(derivationJobState(job, job.status === "completed"))).map(describeTopicJob),
12217
12515
  ...snapshot.summaryJobs.filter((job) => derivationJobState(job, job.status === "succeeded") === "processing" || derivationJobState(job, job.status === "succeeded") === "retryable").map((job) => describeBlockJob("block-summary", job)),
12218
12516
  ...snapshot.extractionJobs.filter((job) => derivationJobState(job, job.status === "succeeded" || job.status === "skipped") === "processing" || derivationJobState(job, job.status === "succeeded" || job.status === "skipped") === "retryable").map((job) => describeBlockJob("event-extraction", job)),
12219
12517
  ...snapshot.graphProjectionJobs.filter(graphProjectionIsProcessing).map(describeGraphJob)
@@ -12243,7 +12541,8 @@ async function overview(runtime, cachedEntries) {
12243
12541
  taskStatus: {
12244
12542
  blockSummary: summaryStatus,
12245
12543
  eventExtraction: extractionStatus,
12246
- graphProjection: graphStatus
12544
+ graphProjection: graphStatus,
12545
+ topicProjection: topicStatus
12247
12546
  },
12248
12547
  graphMigration: (() => {
12249
12548
  const projected = new Set(snapshot.graphProjectionJobs.filter(({ status, projectorVersion }) => status === "completed" && projectorVersion === KNOWLEDGE_GRAPH_PROJECTOR_VERSION).flatMap(({ sourceEventIds }) => sourceEventIds)).size;
@@ -12884,13 +13183,13 @@ async function retryJob(runtime, url) {
12884
13183
  const kind = url.searchParams.get("kind")?.trim() ?? "";
12885
13184
  const jobId = url.searchParams.get("jobId")?.trim() ?? "";
12886
13185
  if (!namespace) throw new AdminHttpError(400, "namespace is required");
12887
- if (!["block-summary", "event-extraction", "graph-projection"].includes(kind)) {
12888
- throw new AdminHttpError(400, "kind must be block-summary, event-extraction, or graph-projection");
13186
+ if (!["block-summary", "event-extraction", "graph-projection", "topic-projection"].includes(kind)) {
13187
+ throw new AdminHttpError(400, "kind must be block-summary, event-extraction, graph-projection, or topic-projection");
12889
13188
  }
12890
13189
  if (!jobId) throw new AdminHttpError(400, "jobId is required");
12891
13190
  if (jobId.startsWith("virtual:")) throw new AdminHttpError(409, "Recovered legacy fragments cannot be retried");
12892
13191
  const snapshot = await requiredSnapshot(runtime, namespace);
12893
- const job = kind === "block-summary" ? snapshot.summaryJobs.find(({ blockId }) => blockId === jobId) : kind === "event-extraction" ? snapshot.extractionJobs.find(({ blockId }) => blockId === jobId) : snapshot.graphProjectionJobs.find(({ id }) => id === jobId);
13192
+ const job = kind === "block-summary" ? snapshot.summaryJobs.find(({ blockId }) => blockId === jobId) : kind === "event-extraction" ? snapshot.extractionJobs.find(({ blockId }) => blockId === jobId) : kind === "topic-projection" ? snapshot.memoryTopicState?.jobs.find(({ id }) => id === jobId) : snapshot.graphProjectionJobs.find(({ id }) => id === jobId);
12894
13193
  if (!job) throw new AdminHttpError(404, `Unknown ${kind} job: ${jobId}`);
12895
13194
  if (job.status !== "failed") throw new AdminHttpError(409, `${kind} job is ${job.status}, not failed`);
12896
13195
  try {
@@ -12900,6 +13199,7 @@ async function retryJob(runtime, url) {
12900
13199
  jobId
12901
13200
  );
12902
13201
  } catch (error) {
13202
+ if (kind === "topic-projection" && error instanceof Error && /Topic retry conflict|Unknown topic projection/.test(error.message)) throw new AdminHttpError(409, "\u6765\u6E90\u6216\u4EFB\u52A1\u5DF2\u66F4\u65B0\uFF0C\u8BF7\u91CD\u65B0\u8BFB\u53D6\u76EE\u5F55");
12903
13203
  throw new AdminHttpError(422, error instanceof Error ? error.message : String(error));
12904
13204
  }
12905
13205
  }
@@ -13607,20 +13907,22 @@ async function apply(ctx, config) {
13607
13907
  }
13608
13908
  const session = context.agent?.session;
13609
13909
  if (!session) return { ...assembled, contexts };
13910
+ let autoMemory;
13610
13911
  try {
13611
- const directory = await runtime.buildMemoryDirectory(session);
13612
- if (directory) contexts.push({ name: "stratagate:memory-directory", text: directory });
13912
+ const text3 = await runtime.buildAutoContext(session);
13913
+ autoMemory = { name: "stratagate:auto-memory", text: text3 };
13613
13914
  } catch (error) {
13614
- ctx.logger.warn(`stratagate-memory directory failed: ${renderError(error)}`);
13915
+ ctx.logger.warn(`stratagate-memory auto-context failed: ${renderError(error)}`);
13615
13916
  runtime.notePluginError(session, error);
13616
13917
  }
13617
13918
  try {
13618
- const text3 = await runtime.buildAutoContext(session);
13619
- contexts.push({ name: "stratagate:auto-memory", text: text3 });
13919
+ const directory = await runtime.buildMemoryDirectory(session);
13920
+ if (directory) contexts.push({ name: "stratagate:memory-directory", text: directory });
13620
13921
  } catch (error) {
13621
- ctx.logger.warn(`stratagate-memory auto-context failed: ${renderError(error)}`);
13922
+ ctx.logger.warn(`stratagate-memory directory failed: ${renderError(error)}`);
13622
13923
  runtime.notePluginError(session, error);
13623
13924
  }
13925
+ if (autoMemory) contexts.push(autoMemory);
13624
13926
  const feedbackSuggestion = runtime.takeFeedbackSuggestion(session);
13625
13927
  if (feedbackSuggestion) contexts.push({ name: "stratagate:feedback-suggestion", text: feedbackSuggestion });
13626
13928
  return { ...assembled, contexts };