stratagate-dsh 0.3.2 → 0.3.4

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;
@@ -3762,8 +3879,25 @@ function memoryTopicSectionTitle(part) {
3762
3879
  return part.title?.trim() || SECTION_TITLES[part.kind] || "\u4E3B\u9898\u6982\u89C8";
3763
3880
  }
3764
3881
  var normalizedLabel = (value) => value.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
3882
+ function memoryTopicOverviewMatchesSection(part, titles) {
3883
+ const display = normalizedLabel(memoryTopicSectionTitle(part));
3884
+ return titles.some((title) => normalizedLabel(title) === display);
3885
+ }
3886
+ function memoryTopicMembershipSections(topic) {
3887
+ if (topic.sections !== void 0) return structuredClone(topic.sections);
3888
+ const sections = /* @__PURE__ */ new Map();
3889
+ for (const part of topic.overview) {
3890
+ const title = memoryTopicSectionTitle(part);
3891
+ const key = normalizedLabel(title);
3892
+ const section = sections.get(key) ?? { title, sourceEventIds: [] };
3893
+ section.sourceEventIds = unique2([...section.sourceEventIds, ...part.sourceEventIds]);
3894
+ sections.set(key, section);
3895
+ }
3896
+ return [...sections.values()];
3897
+ }
3765
3898
  function sectionTitles(topic) {
3766
3899
  const titles = /* @__PURE__ */ new Map();
3900
+ for (const section of memoryTopicMembershipSections(topic)) titles.set(normalizedLabel(section.title), section.title);
3767
3901
  for (const part of topic.overview) {
3768
3902
  const title = memoryTopicSectionTitle(part);
3769
3903
  if (!titles.has(normalizedLabel(title))) titles.set(normalizedLabel(title), title);
@@ -3788,6 +3922,7 @@ function topicView(topic, events, versions) {
3788
3922
  description: valid ? topic.description : "\u6982\u89C8\u7B49\u5F85\u91CD\u65B0\u6574\u7406\uFF0C\u8BF7\u67E5\u770B\u5173\u8054\u4E8B\u4EF6\u3002",
3789
3923
  sourceEventIds: ids,
3790
3924
  overview: overview2,
3925
+ sections: valid ? memoryTopicMembershipSections(topic) : [],
3791
3926
  createdAt: topic.createdAt,
3792
3927
  updatedAt: topic.updatedAt,
3793
3928
  coverage: { totalEvents: ids.length, summarizedEvents: summarized.size, omittedEvents: ids.length - summarized.size },
@@ -3816,6 +3951,9 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3816
3951
  state = { topics: [], jobs: [], projectedVersions: {} };
3817
3952
  restore(state) {
3818
3953
  this.state = state ? structuredClone(state) : { topics: [], jobs: [], projectedVersions: {} };
3954
+ for (const topic of this.state.topics) if (topic.sections === void 0) {
3955
+ topic.sections = memoryTopicMembershipSections(topic);
3956
+ }
3819
3957
  if (state && state.rebuildVersions === void 0 && state.topics.some((topic) => topic.invalidated)) {
3820
3958
  this.state.rebuildVersions = Object.fromEntries(state.topics.filter((topic) => topic.invalidated).flatMap((topic) => Object.entries(topic.sourceVersions).filter(([id, version]) => state.projectedVersions[id] !== version)));
3821
3959
  }
@@ -3851,9 +3989,23 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3851
3989
  bootstrap() {
3852
3990
  return this.state.bootstrap ? structuredClone(this.state.bootstrap) : null;
3853
3991
  }
3992
+ needsInitialization() {
3993
+ return this.state.bootstrap?.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION || this.state.sectionMembershipVersion !== 1;
3994
+ }
3854
3995
  initializeBootstrap(events, now) {
3855
- if (this.state.bootstrap?.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION) return;
3856
3996
  this.synchronize(events, now);
3997
+ if (this.state.sectionMembershipVersion !== 1) {
3998
+ const versions = sourceVersions(new Map(events.filter(visible).map((event) => [event.id, event])));
3999
+ const missing = {};
4000
+ for (const topic of this.state.topics.filter((topic2) => !topic2.invalidated)) {
4001
+ const assigned = new Set(memoryTopicMembershipSections(topic).flatMap((section) => section.sourceEventIds));
4002
+ const ids = topic.sourceEventIds.filter((id) => !assigned.has(id) && versions.has(id) && this.state.projectedVersions[id] === versions.get(id));
4003
+ if (ids.length) missing[topic.id] = Object.fromEntries(ids.map((id) => [id, versions.get(id)]));
4004
+ }
4005
+ this.state.sectionBackfill = missing;
4006
+ this.state.sectionMembershipVersion = 1;
4007
+ }
4008
+ if (this.state.bootstrap?.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION) return;
3857
4009
  this.state.bootstrap = {
3858
4010
  projectorVersion: MEMORY_TOPIC_PROJECTOR_VERSION,
3859
4011
  sourceVersions: Object.fromEntries(sourceVersions(new Map(events.filter(visible).map((event) => [event.id, event])))),
@@ -3869,7 +4021,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3869
4021
  const versions = sourceVersions(sources2);
3870
4022
  if (this.state.bootstrap && this.state.bootstrap.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION || this.state.topics.some((topic) => topic.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION) || this.state.jobs.some((job) => job.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION)) {
3871
4023
  const fromV2 = (!this.state.bootstrap || this.state.bootstrap.projectorVersion === 2) && this.state.topics.every((topic) => topic.projectorVersion === 2) && this.state.jobs.every((job) => job.projectorVersion === 2);
3872
- const chapters = fromV2 ? this.state.topics.filter((topic) => !topic.invalidated && topic.title.trim() && topic.description.trim() && topic.sourceEventIds.length > 0 && topic.sourceEventIds.every((id) => sources2.has(id) && topic.sourceVersions[id] === versions.get(id)) && Object.entries(topic.dependencyVersions ?? topic.sourceVersions).every(([id, version]) => versions.get(id) === version)).map((topic) => ({ ...topic, overview: [], projectorVersion: MEMORY_TOPIC_PROJECTOR_VERSION, updatedAt: now })) : [];
4024
+ const chapters = fromV2 ? this.state.topics.filter((topic) => !topic.invalidated && topic.title.trim() && topic.description.trim() && topic.sourceEventIds.length > 0 && topic.sourceEventIds.every((id) => sources2.has(id) && topic.sourceVersions[id] === versions.get(id)) && Object.entries(topic.dependencyVersions ?? topic.sourceVersions).every(([id, version]) => versions.get(id) === version)).map((topic) => ({ ...topic, overview: [], sections: [], projectorVersion: MEMORY_TOPIC_PROJECTOR_VERSION, updatedAt: now })) : [];
3873
4025
  this.state = { topics: chapters, jobs: [], projectedVersions: {} };
3874
4026
  }
3875
4027
  for (const topic of this.state.topics) {
@@ -3878,6 +4030,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3878
4030
  topic.title = "";
3879
4031
  topic.description = "";
3880
4032
  topic.overview = [];
4033
+ topic.sections = [];
3881
4034
  for (const id of topic.sourceEventIds) if (sources2.has(id)) {
3882
4035
  const version = versions.get(id);
3883
4036
  if (topic.sourceVersions[id] === version && this.state.projectedVersions[id] === version) {
@@ -3901,6 +4054,13 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3901
4054
  job.superseded = true;
3902
4055
  job.lastError = "source-changed";
3903
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
+ });
3904
4064
  }
3905
4065
  const finished = this.state.jobs.filter((job) => job.status === "completed" || job.status === "failed" && job.nextRetryAt === null);
3906
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));
@@ -3908,6 +4068,12 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3908
4068
  for (const [id, version] of Object.entries(this.state.rebuildVersions ?? {})) {
3909
4069
  if (versions.get(id) !== version || this.state.projectedVersions[id] === version) delete this.state.rebuildVersions[id];
3910
4070
  }
4071
+ for (const [topicId, pending] of Object.entries(this.state.sectionBackfill ?? {})) {
4072
+ const topic = this.state.topics.find((topic2) => topic2.id === topicId && !topic2.invalidated);
4073
+ const assigned = new Set(topic ? memoryTopicMembershipSections(topic).flatMap((section) => section.sourceEventIds) : []);
4074
+ for (const [id, version] of Object.entries(pending)) if (!topic || !topic.sourceEventIds.includes(id) || versions.get(id) !== version || assigned.has(id)) delete pending[id];
4075
+ if (!Object.keys(pending).length) delete this.state.sectionBackfill[topicId];
4076
+ }
3911
4077
  const bootstrap = this.state.bootstrap;
3912
4078
  if (bootstrap && bootstrap.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION) {
3913
4079
  const outstanding = Object.entries(bootstrap.sourceVersions).filter(([id, version]) => versions.get(id) === version && this.state.projectedVersions[id] !== version);
@@ -3920,6 +4086,14 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3920
4086
  }
3921
4087
  recover(now) {
3922
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
+ });
3923
4097
  job.status = "failed";
3924
4098
  job.lastError = "timeout";
3925
4099
  job.context = null;
@@ -3928,29 +4102,36 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3928
4102
  job.updatedAt = now;
3929
4103
  }
3930
4104
  }
3931
- pendingEvents(events) {
4105
+ pendingWork(events) {
3932
4106
  const sources2 = new Map(events.filter(visible).map((event) => [event.id, event]));
3933
4107
  const versions = sourceVersions(sources2);
3934
4108
  const dirtyIds = new Set(this.state.topics.filter((topic) => topic.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION || Object.entries(topic.dependencyVersions ?? topic.sourceVersions).some(([id, version]) => versions.get(id) !== version)).flatMap((topic) => topic.sourceEventIds));
3935
- return events.filter(visible).filter((event) => {
4109
+ const blocked = (id, version, topicId) => this.state.jobs.some((job) => !job.superseded && job.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION && job.sectionBackfillTopicId === topicId && job.sourceVersions[id] === version && jobInputsCurrent(job, versions) && (job.status === "running" || job.status === "pending" || job.status === "failed" && (job.attempts >= TOPIC_MAX_ATTEMPTS || job.nextRetryAt !== null)));
4110
+ return events.filter(visible).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)).flatMap((event) => {
3936
4111
  const version = versions.get(event.id);
3937
- if (this.state.projectedVersions[event.id] === version && !dirtyIds.has(event.id)) return false;
3938
- return !this.state.jobs.some((job) => !job.superseded && job.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION && job.sourceVersions[event.id] === version && jobInputsCurrent(job, versions) && (job.status === "running" || job.status === "pending" || job.status === "failed" && (job.attempts >= TOPIC_MAX_ATTEMPTS || job.nextRetryAt !== null)));
3939
- }).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
4112
+ const work = [];
4113
+ if ((this.state.projectedVersions[event.id] !== version || dirtyIds.has(event.id)) && !blocked(event.id, version)) {
4114
+ work.push({ event });
4115
+ }
4116
+ for (const [topicId, pending] of Object.entries(this.state.sectionBackfill ?? {})) {
4117
+ if (pending[event.id] === version && !blocked(event.id, version, topicId)) work.push({ event, sectionBackfillTopicId: topicId });
4118
+ }
4119
+ return work;
4120
+ });
3940
4121
  }
3941
- isBootstrap(id, version) {
4122
+ isBootstrap(id, version, sectionBackfillTopicId) {
3942
4123
  const bootstrap = this.state.bootstrap;
3943
- return this.state.rebuildVersions?.[id] === version || !bootstrap || bootstrap.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION || bootstrap.status !== "completed" && bootstrap.sourceVersions[id] === version;
4124
+ return sectionBackfillTopicId !== void 0 || this.state.rebuildVersions?.[id] === version || !bootstrap || bootstrap.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION || bootstrap.status !== "completed" && bootstrap.sourceVersions[id] === version;
3944
4125
  }
3945
- matchesMode(ids, versions, mode) {
3946
- return mode === "all" || ids.some((id) => this.isBootstrap(id, versions.get(id)) === (mode === "bootstrap"));
4126
+ matchesMode(ids, versions, mode, sectionBackfillTopicId) {
4127
+ return mode === "all" || ids.some((id) => this.isBootstrap(id, versions.get(id), sectionBackfillTopicId) === (mode === "bootstrap"));
3947
4128
  }
3948
4129
  hasPending(events, now, mode = "all") {
3949
4130
  const sources2 = new Map(events.filter(visible).map((event) => [event.id, event]));
3950
4131
  const versions = sourceVersions(sources2);
3951
4132
  const running = this.state.jobs.find((job) => job.status === "running");
3952
4133
  if (running) return Date.parse(running.leaseUntil ?? running.updatedAt) <= now || !jobInputsCurrent(running, versions);
3953
- return this.pendingEvents(events).some((event) => this.matchesMode([event.id], versions, mode)) || 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));
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));
3954
4135
  }
3955
4136
  claim(events, now, mode = "all") {
3956
4137
  if (this.state.bootstrap?.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION) {
@@ -3961,9 +4142,9 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3961
4142
  if (this.state.jobs.some((job2) => job2.status === "running")) return null;
3962
4143
  const sources2 = new Map(events.filter(visible).map((event) => [event.id, event]));
3963
4144
  const versions = sourceVersions(sources2);
3964
- 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)).sort((a, b) => Number(this.isBootstrap(a.sourceEventIds[0], versions.get(a.sourceEventIds[0]))) - Number(this.isBootstrap(b.sourceEventIds[0], versions.get(b.sourceEventIds[0]))))[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];
3965
4146
  let job = eligible;
3966
- if (job && this.isBootstrap(job.sourceEventIds[0], versions.get(job.sourceEventIds[0])) && mode === "all" && this.pendingEvents(events).some((event) => !this.isBootstrap(event.id, versions.get(event.id)))) job = void 0;
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;
3967
4148
  if (job?.status === "failed") {
3968
4149
  const previous = job;
3969
4150
  previous.nextRetryAt = null;
@@ -3983,9 +4164,10 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3983
4164
  this.state.jobs.push(job);
3984
4165
  }
3985
4166
  if (!job) {
3986
- const pending = this.pendingEvents(events).filter((event) => this.matchesMode([event.id], versions, mode)).sort((a, b) => Number(this.isBootstrap(a.id, versions.get(a.id))) - Number(this.isBootstrap(b.id, versions.get(b.id)))).filter((event) => !this.state.jobs.some((candidate) => candidate.status === "failed" && candidate.attempts < TOPIC_MAX_ATTEMPTS && candidate.nextRetryAt !== null && candidate.sourceVersions[event.id] === versions.get(event.id)));
4167
+ const pending = this.pendingWork(events).filter(({ event, sectionBackfillTopicId }) => this.matchesMode([event.id], versions, mode, sectionBackfillTopicId)).sort((a, b) => Number(this.isBootstrap(a.event.id, versions.get(a.event.id), a.sectionBackfillTopicId)) - Number(this.isBootstrap(b.event.id, versions.get(b.event.id), b.sectionBackfillTopicId)));
3987
4168
  const first = pending[0];
3988
- const sameKind = pending.filter((event) => first && this.isBootstrap(event.id, versions.get(event.id)) === this.isBootstrap(first.id, versions.get(first.id))).slice(0, TOPIC_BATCH_LIMIT);
4169
+ const backfillTopicId = first?.sectionBackfillTopicId;
4170
+ const sameKind = pending.filter(({ event, sectionBackfillTopicId }) => first && sectionBackfillTopicId === backfillTopicId && this.isBootstrap(event.id, versions.get(event.id), sectionBackfillTopicId) === this.isBootstrap(first.event.id, versions.get(first.event.id), backfillTopicId)).slice(0, TOPIC_BATCH_LIMIT).map(({ event }) => event);
3989
4171
  const batch2 = [];
3990
4172
  let eventBudget = 16e3;
3991
4173
  for (const event of sameKind) {
@@ -4012,6 +4194,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4012
4194
  createdAt: now,
4013
4195
  updatedAt: now
4014
4196
  };
4197
+ if (backfillTopicId) job.sectionBackfillTopicId = backfillTopicId;
4015
4198
  this.state.jobs.push(job);
4016
4199
  }
4017
4200
  const batch = job.sourceEventIds.map((id) => sources2.get(id));
@@ -4030,21 +4213,30 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4030
4213
  }).join(" "), 4]
4031
4214
  ])).map(({ item, score }) => [item.id, score]));
4032
4215
  const overlap = (topic) => topic.sourceEventIds.filter((id) => batchIds.has(id)).length;
4033
- const candidates = eligibleTopics.sort((a, b) => overlap(b) - overlap(a) || (lexicalScores.get(b.id) ?? 0) - (lexicalScores.get(a.id) ?? 0) || b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id)).slice(0, TOPIC_CANDIDATE_LIMIT);
4216
+ const candidates = eligibleTopics.filter((topic) => !job.sectionBackfillTopicId || topic.id === job.sectionBackfillTopicId).sort((a, b) => overlap(b) - overlap(a) || (lexicalScores.get(b.id) ?? 0) - (lexicalScores.get(a.id) ?? 0) || b.updatedAt.localeCompare(a.updatedAt) || a.id.localeCompare(b.id)).slice(0, TOPIC_CANDIDATE_LIMIT);
4034
4217
  const existingTopics = [];
4035
4218
  let remaining = 3e4 - JSON.stringify(batch.map(compactEvent)).length;
4036
4219
  for (const topic of candidates) {
4037
- const overview2 = topic.overview.slice(0, 4);
4220
+ const overview2 = job.sectionBackfillTopicId ? [] : topic.overview.slice(0, 4);
4038
4221
  const exposed = unique2([
4039
4222
  ...overview2.flatMap((part) => part.sourceEventIds),
4040
4223
  ...topic.sourceEventIds.filter((id) => batchIds.has(id))
4041
4224
  ]);
4225
+ const allTitles = sectionTitles(topic);
4226
+ let titleBudget = 4e3;
4227
+ const shownTitles = job.sectionBackfillTopicId ? allTitles.filter((title, index) => {
4228
+ const cost2 = JSON.stringify(title).length;
4229
+ if (index >= 120 || cost2 > titleBudget) return false;
4230
+ titleBudget -= cost2;
4231
+ return true;
4232
+ }) : allTitles;
4042
4233
  const candidate = {
4043
4234
  id: topic.id,
4044
4235
  title: topic.title,
4045
4236
  description: topic.description,
4046
4237
  overview: overview2,
4047
- sectionTitles: sectionTitles(topic),
4238
+ sectionTitles: shownTitles,
4239
+ ...shownTitles.length < allTitles.length ? { sectionTitlesOmitted: allTitles.length - shownTitles.length } : {},
4048
4240
  sourceEventIds: exposed,
4049
4241
  totalSourceEvents: topic.sourceEventIds.length
4050
4242
  };
@@ -4057,7 +4249,16 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4057
4249
  const compact = compactEvent(event);
4058
4250
  return compact.title !== event.title || compact.summary !== event.summary || JSON.stringify(compact.temporal) !== JSON.stringify(topicTemporal(event)) || JSON.stringify(compact.tags) !== JSON.stringify(event.tags);
4059
4251
  }).map((event) => event.id);
4060
- job.context = { jobId: job.id, events: batch.map(compactEvent), existingTopics, truncatedEventIds };
4252
+ if (job.sectionBackfillTopicId && !existingTopics.some((topic) => topic.id === job.sectionBackfillTopicId)) {
4253
+ throw new Error("Section backfill chapter cannot fit the projection input budget.");
4254
+ }
4255
+ job.context = {
4256
+ jobId: job.id,
4257
+ events: batch.map(compactEvent),
4258
+ existingTopics,
4259
+ truncatedEventIds,
4260
+ ...job.sectionBackfillTopicId ? { sectionBackfillTopicId: job.sectionBackfillTopicId } : {}
4261
+ };
4061
4262
  const storedById = new Map(this.state.topics.map((topic) => [topic.id, topic]));
4062
4263
  job.candidateVersions = Object.fromEntries(existingTopics.map((topic) => [topic.id, digest(storedById.get(topic.id))]));
4063
4264
  const dependencyIds = unique2([...job.sourceEventIds, ...existingTopics.flatMap((topic) => [
@@ -4065,12 +4266,13 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4065
4266
  ...!storedById.get(topic.id).invalidated ? Object.keys(storedById.get(topic.id).dependencyVersions ?? {}).filter((id) => sources2.has(id)) : []
4066
4267
  ])]);
4067
4268
  job.dependencyVersions = Object.fromEntries(dependencyIds.map((id) => [id, versions.get(id)]));
4269
+ job.diagnostics = void 0;
4068
4270
  job.status = "running";
4069
4271
  job.attempts += 1;
4070
4272
  job.lastError = null;
4071
4273
  job.nextRetryAt = null;
4072
4274
  job.updatedAt = now;
4073
- if (this.state.bootstrap.status === "pending" && this.matchesMode(job.sourceEventIds, versions, "bootstrap")) {
4275
+ if (this.state.bootstrap.status === "pending" && this.matchesMode(job.sourceEventIds, versions, "bootstrap", job.sectionBackfillTopicId)) {
4074
4276
  this.state.bootstrap.status = "running";
4075
4277
  }
4076
4278
  job.leaseUntil = new Date(Date.parse(now) + TOPIC_LEASE_MS).toISOString();
@@ -4080,13 +4282,16 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4080
4282
  const job = this.requireJob(id);
4081
4283
  if (job.status === "completed") return { topicIds: [...job.topicIds] };
4082
4284
  if (job.status !== "running" || !job.context) throw new Error(`Topic projection ${id} is not running`);
4083
- 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.");
4084
4286
  const sources2 = new Map(events.filter(visible).map((event) => [event.id, event]));
4085
4287
  const versions = sourceVersions(sources2);
4086
4288
  if (!jobInputsCurrent(job, versions) || Object.entries(job.candidateVersions).some(([topicId, version]) => digest(this.state.topics.find((topic) => topic.id === topicId)) !== version)) {
4087
- 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.");
4290
+ }
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.");
4292
+ if (job.sectionBackfillTopicId && (result.topics.length !== 1 || result.topics[0].topicId !== job.sectionBackfillTopicId || result.topics[0].sections === void 0)) {
4293
+ throw new TopicProjectionError("validation-failed", "Invalid section backfill: explicit sections must cover the existing chapter.");
4088
4294
  }
4089
- if (!Array.isArray(result?.topics) || result.topics.length < 1 || result.topics.length > 12) throw new Error("Topic projection must return 1-12 topics.");
4090
4295
  const candidates = new Map(job.context.existingTopics.map((topic) => [topic.id, topic]));
4091
4296
  const allowed = /* @__PURE__ */ new Set([...job.sourceEventIds, ...job.context.existingTopics.flatMap((topic) => topic.sourceEventIds)]);
4092
4297
  const batchIds = new Set(job.sourceEventIds);
@@ -4095,7 +4300,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4095
4300
  const labels = /* @__PURE__ */ new Set();
4096
4301
  const proposals = [];
4097
4302
  const checkIds = (ids, label) => {
4098
- 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.`);
4099
4304
  return ids;
4100
4305
  };
4101
4306
  for (const proposal of result.topics) {
@@ -4105,44 +4310,72 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4105
4310
  const label = normalizedLabel;
4106
4311
  const sameLabel = typeof proposal.title === "string" ? this.state.topics.filter((topic) => !topic.invalidated && label(topic.title) === label(proposal.title)) : [];
4107
4312
  if (sameLabel.some((topic) => !candidates.has(topic.id))) {
4108
- 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.");
4109
4314
  }
4110
4315
  const existing = proposal.topicId ? this.state.topics.find((topic) => topic.id === proposal.topicId) : inferred ?? (sameLabel.length === 1 ? sameLabel[0] : void 0);
4111
4316
  if (sameLabel.some((topic) => topic.id !== existing?.id)) {
4112
- 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.");
4113
4318
  }
4114
4319
  if (typeof proposal.title === "string") {
4115
4320
  const key = label(proposal.title);
4116
- 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.");
4117
4322
  labels.add(key);
4118
4323
  }
4119
- 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.");
4120
4325
  const topicId = existing?.id ?? `topic_${randomUUID()}`;
4121
- 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.");
4122
4327
  touched.add(topicId);
4123
- 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.");
4124
4329
  for (const eventId of ids) if (batchIds.has(eventId)) covered.add(eventId);
4125
- 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.");
4126
4331
  for (const part of proposal.overview) {
4127
- if (part.title !== void 0 && (typeof part.title !== "string" || !part.title.trim() || [...part.title].length > 80)) throw new Error("Invalid topic overview title.");
4128
- 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.");
4129
4334
  checkIds(part.sourceEventIds, "overview");
4130
4335
  if (part.kind !== "scope" && part.sourceEventIds.some((eventId) => job.context.truncatedEventIds?.includes(eventId))) {
4131
- 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.");
4132
4337
  }
4133
- if (part.sourceEventIds.length > 12 || part.sourceEventIds.some((eventId) => !ids.includes(eventId))) throw new Error("Overview references must belong to returned topic membership.");
4134
- 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.");
4135
- }
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.");
4340
+ }
4341
+ if (proposal.sections !== void 0 && !Array.isArray(proposal.sections)) throw new TopicProjectionError("validation-failed", "Invalid topic sections.");
4342
+ const assignments = memoryTopicMembershipSections(proposal);
4343
+ const sectionLabels = /* @__PURE__ */ new Set();
4344
+ for (const section of assignments) {
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.");
4346
+ sectionLabels.add(label(section.title));
4347
+ checkIds(section.sourceEventIds, "section");
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.");
4350
+ }
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.");
4136
4352
  const membership = unique2([...(existing?.sourceEventIds ?? []).filter((eventId) => sources2.has(eventId)), ...ids]);
4353
+ const assignedIds = new Set(proposal.sections === void 0 ? [] : assignments.flatMap((section) => section.sourceEventIds).filter((eventId) => batchIds.has(eventId)));
4354
+ const sections = existing && !existing.invalidated ? memoryTopicMembershipSections(existing) : [];
4355
+ const memberIds = new Set(membership);
4356
+ for (const section of sections) section.sourceEventIds = section.sourceEventIds.filter((eventId) => memberIds.has(eventId) && !assignedIds.has(eventId));
4357
+ for (const assignment of assignments) {
4358
+ const section = sections.find((current) => label(current.title) === label(assignment.title));
4359
+ if (section) section.sourceEventIds = unique2([...section.sourceEventIds, ...assignment.sourceEventIds]);
4360
+ else sections.push({ title: assignment.title.trim(), sourceEventIds: [...assignment.sourceEventIds] });
4361
+ }
4362
+ const finalSections = sections.filter((section) => section.sourceEventIds.length > 0);
4363
+ const titles = finalSections.map((section) => section.title);
4137
4364
  const inherited = existing && !existing.invalidated ? existing.overview : [];
4138
- const overview2 = unique2(proposal.overview.map((part) => JSON.stringify(part))).map((part) => JSON.parse(part));
4139
- for (const part of inherited) if (!overview2.some((current) => digest(current) === digest(part))) overview2.push(structuredClone(part));
4365
+ if (proposal.sections !== void 0) for (const part of proposal.overview) {
4366
+ if (!inherited.some((old) => digest(old) === digest(part)) && !memoryTopicOverviewMatchesSection(part, titles)) {
4367
+ throw new TopicProjectionError("validation-failed", "New topic overview must match a final topic section.");
4368
+ }
4369
+ }
4370
+ const overview2 = unique2(proposal.overview.filter((part) => memoryTopicOverviewMatchesSection(part, titles)).map((part) => JSON.stringify(part))).map((part) => JSON.parse(part));
4371
+ for (const part of inherited) if (memoryTopicOverviewMatchesSection(part, titles) && !overview2.some((current) => digest(current) === digest(part))) overview2.push(structuredClone(part));
4140
4372
  proposals.push({
4141
4373
  id: topicId,
4142
- title: proposal.title.trim(),
4143
- description: proposal.description.trim(),
4374
+ title: job.sectionBackfillTopicId ? existing.title : proposal.title.trim(),
4375
+ description: job.sectionBackfillTopicId ? existing.description : proposal.description.trim(),
4144
4376
  sourceEventIds: membership,
4145
4377
  overview: overview2,
4378
+ sections: finalSections,
4146
4379
  sourceVersions: Object.fromEntries(membership.map((eventId) => [eventId, versions.get(eventId)])),
4147
4380
  dependencyVersions: { ...!existing?.invalidated ? existing?.dependencyVersions ?? {} : {}, ...job.dependencyVersions },
4148
4381
  projectorVersion: MEMORY_TOPIC_PROJECTOR_VERSION,
@@ -4151,11 +4384,16 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4151
4384
  updatedAt: now
4152
4385
  });
4153
4386
  }
4154
- 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");
4155
4388
  for (const topic of proposals) {
4156
4389
  const index = this.state.topics.findIndex((current) => current.id === topic.id);
4157
4390
  if (index < 0) this.state.topics.push(topic);
4158
4391
  else this.state.topics[index] = topic;
4392
+ const pending = this.state.sectionBackfill?.[topic.id];
4393
+ if (pending) {
4394
+ for (const eventId of memoryTopicMembershipSections(topic).flatMap((section) => section.sourceEventIds)) delete pending[eventId];
4395
+ if (!Object.keys(pending).length) delete this.state.sectionBackfill[topic.id];
4396
+ }
4159
4397
  }
4160
4398
  Object.assign(this.state.projectedVersions, job.sourceVersions);
4161
4399
  for (const [eventId, version] of Object.entries(job.sourceVersions)) {
@@ -4176,13 +4414,49 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4176
4414
  fail(id, error, now) {
4177
4415
  const job = this.state.jobs.find((candidate) => candidate.id === id);
4178
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
+ });
4179
4429
  job.status = "failed";
4180
4430
  job.context = null;
4181
4431
  job.leaseUntil = null;
4182
- const message = error instanceof Error ? error.message : String(error);
4183
- 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;
4184
4433
  job.nextRetryAt = job.attempts < TOPIC_MAX_ATTEMPTS ? new Date(Date.parse(now) + 3e4 * 2 ** Math.max(0, job.attempts - 1)).toISOString() : null;
4185
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
+ }
4186
4460
  }
4187
4461
  requireJob(id) {
4188
4462
  const job = this.state.jobs.find((candidate) => candidate.id === id);
@@ -4193,7 +4467,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4193
4467
  const previous = this.requireJob(id);
4194
4468
  const sources2 = new Map(events.filter(visible).map((event) => [event.id, event]));
4195
4469
  const versions = sourceVersions(sources2);
4196
- if (previous.superseded || previous.status !== "failed" || previous.attempts < TOPIC_MAX_ATTEMPTS || previous.nextRetryAt !== null || !jobInputsCurrent(previous, versions) || previous.sourceEventIds.some((eventId) => this.state.projectedVersions[eventId] === versions.get(eventId))) {
4470
+ if (previous.superseded || previous.status !== "failed" || previous.attempts < TOPIC_MAX_ATTEMPTS || previous.nextRetryAt !== null || !jobInputsCurrent(previous, versions) || previous.sourceEventIds.some((eventId) => this.state.projectedVersions[eventId] === versions.get(eventId) && (!previous.sectionBackfillTopicId || this.state.sectionBackfill?.[previous.sectionBackfillTopicId]?.[eventId] !== versions.get(eventId)))) {
4197
4471
  throw new Error("Topic retry conflict: failure is no longer current or retryable.");
4198
4472
  }
4199
4473
  const job = {
@@ -4206,6 +4480,8 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4206
4480
  dependencyVersions: {},
4207
4481
  candidateVersions: {},
4208
4482
  topicIds: [],
4483
+ diagnostics: void 0,
4484
+ splitDepth: 0,
4209
4485
  lastError: null,
4210
4486
  nextRetryAt: null,
4211
4487
  leaseUntil: null,
@@ -4518,7 +4794,7 @@ var StrataGate = class _StrataGate {
4518
4794
  }
4519
4795
  async initializeTopicBootstrap() {
4520
4796
  for (let attempt = 0; attempt < 3; attempt += 1) {
4521
- if (this.topicDirectory.bootstrap()?.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION) return;
4797
+ if (!this.topicDirectory.needsInitialization()) return;
4522
4798
  try {
4523
4799
  await this.commitMutation(() => this.topicDirectory.initializeBootstrap(this.listAllEvents(), toUtc8Iso(this.now())));
4524
4800
  return;
@@ -7411,7 +7687,7 @@ var TOPIC_OVERVIEW = {
7411
7687
  additionalProperties: false,
7412
7688
  properties: {
7413
7689
  kind: { type: "string", enum: TOPIC_OVERVIEW_KINDS, required: true },
7414
- title: { type: "string", description: "Lasting category inside the chapter, e.g. defect diagnosis and fixes, releases and installation, or architecture decisions. Never a single Event, bug, version, date or task title; at most 80 characters." },
7690
+ title: { type: "string", description: "Lasting category inside the chapter. Must match a declared or inherited section title; if omitted, the kind default must match one. Never a single Event, bug, version, date or task title; at most 80 characters." },
7415
7691
  text: { type: "string", description: "Source-grounded scope, historical progress, decision, change, or unresolved question; preserve time, uncertainty, plans, cancellation, and disputes.", required: true },
7416
7692
  sourceEventIds: { ...STRING_ARRAY, required: true }
7417
7693
  }
@@ -7428,6 +7704,14 @@ var TOPIC_PROJECTOR_PARAMETERS = {
7428
7704
  title: { type: "string", description: "A broad lasting chapter label (project, life area, relationship), not a single task, release, UI page, or factual conclusion.", required: true },
7429
7705
  description: { type: "string", description: "Describe which records this topic covers; navigation only, not a claim about current truth.", required: true },
7430
7706
  sourceEventIds: { ...STRING_ARRAY, description: "Exact supplied Event ids assigned to this topic; existing unseen topic members are retained by the store.", required: true },
7707
+ sections: { type: "array", required: true, items: {
7708
+ type: "object",
7709
+ additionalProperties: false,
7710
+ properties: {
7711
+ title: { type: "string", description: "Reuse a lasting section title from sectionTitles when applicable.", required: true },
7712
+ sourceEventIds: { ...STRING_ARRAY, description: "Batch Event ids routed to this section, regardless of overview citations.", required: true }
7713
+ }
7714
+ } },
7431
7715
  overview: { type: "array", items: TOPIC_OVERVIEW, required: true }
7432
7716
  }
7433
7717
  }
@@ -7441,7 +7725,7 @@ events.catalogHints \u4EC5\u662F\u53EF\u9009 routing hint\uFF08\u5206\u7C7B\u5BF
7441
7725
  \u8FD9\u4E9B\u4E8B\u4EF6\u5361\u662F\u6709\u754C\u7684\u5BFC\u822A\u6750\u6599\uFF0C\u6807\u9898\u6216\u6458\u8981\u53EF\u80FD\u7701\u7565\u5C3E\u90E8\uFF1B\u4E0D\u8981\u56E0\u672A\u770B\u5230\u5426\u5B9A\u3001\u540E\u7EED\u66F4\u65B0\u6216\u9650\u5236\u6761\u4EF6\u5C31\u63A8\u65AD\u5F53\u524D\u4E8B\u5B9E\u3001\u5B8C\u6210\u72B6\u6001\u6216\u6CA1\u6709\u4E89\u8BAE\u3002\u5B8C\u6574\u4E8B\u5B9E\u4ECD\u987B\u5C55\u5F00\u4E8B\u4EF6\u548C\u539F\u6587\u53D6\u8BC1\u3002status\u3001supersededBy\u3001temporal.status \u53CA\u5176\u660E\u786E\u5173\u7CFB\u63D0\u4F9B\u7684\u5386\u53F2\u3001\u8BA1\u5212\u3001\u53D6\u6D88\u548C\u51B2\u7A81\u6807\u8BB0\u5FC5\u987B\u4FDD\u7559\uFF1B\u6750\u6599\u4E0D\u8DB3\u65F6\u53EA\u5199\u8986\u76D6\u8303\u56F4\u6216\u5F85\u786E\u8BA4\uFF0C\u4E0D\u8865\u5199\u7ED3\u8BBA\u3002
7442
7726
  truncatedEventIds \u660E\u786E\u5217\u51FA\u672A\u5B8C\u6574\u63D0\u4F9B\u7684\u4E8B\u4EF6\uFF1A\u5F15\u7528\u5176\u4E2D\u4EFB\u4F55\u4E8B\u4EF6\u7684\u6982\u8981\u6BB5\u53EA\u80FD\u662F scope\uFF0C\u53EA\u8BF4\u660E\u8D44\u6599\u8303\u56F4\uFF0C\u4E0D\u5199\u5386\u53F2\u3001\u51B3\u5B9A\u3001\u53D8\u5316\u6216\u5F85\u786E\u8BA4\u7684\u4E8B\u5B9E\uFF1B\u6DF7\u5408\u5F15\u7528\u5176\u4ED6\u4E8B\u4EF6\u4E5F\u4E0D\u80FD\u653E\u5BBD\u6B64\u9650\u5236\u3002
7443
7727
 
7444
- Topic \u662F\u957F\u671F\u7684\u5927\u7AE0\u8282\uFF0C\u6309\u9879\u76EE\u3001\u751F\u6D3B\u9886\u57DF\u6216\u957F\u671F\u5173\u7CFB\u7EC4\u7EC7\uFF0C\u800C\u4E0D\u662F\u6BCF\u4E2A\u4E8B\u4EF6\u6216\u5B50\u4EFB\u52A1\u4E00\u7AE0\u3002\u4F8B\u5982 StrataGate UI\u3001DSH \u517C\u5BB9\u3001Topic Directory\u3001Retrieval \u5E94\u5F52\u5165\u201CStrataGate\u201D\u540C\u4E00\u7AE0\uFF1B\u6C42\u804C\u6295\u9012\u3001\u9762\u8BD5\u548C\u5B9E\u4E60\u8FDB\u5C55\u5F52\u5165\u201C\u6C42\u804C\u4E0E\u804C\u4E1A\u53D1\u5C55\u201D\u3002existingTopics.sectionTitles \u5217\u51FA\u8BE5\u7AE0\u5168\u90E8\u5DF2\u6709\u5C0F\u8282\u6807\u9898\uFF0C\u4EC5\u4F9B\u5BFC\u822A\uFF0C\u4E0D\u662F\u6B63\u6587\u6216\u4E8B\u5B9E\u8BC1\u636E\u3002\u5DF2\u6709\u5C0F\u8282\u80FD\u5BB9\u7EB3\u672C\u6279\u4E8B\u9879\u65F6\u5FC5\u987B\u9010\u5B57\u590D\u7528\u539F title\uFF0C\u4E0D\u8981\u8FD1\u4E49\u6539\u540D\uFF1B\u4E0D\u80FD\u56E0\u4E3A overview \u53EA\u5C55\u793A\u524D\u51E0\u6BB5\u5C31\u91CD\u590D\u521B\u5EFA\u5C0F\u8282\u3002\u786E\u5B9E\u5C5E\u4E8E\u5DF2\u6709\u7C7B\u522B\u65E0\u6CD5\u5BB9\u7EB3\u7684\u65B0\u957F\u671F\u7C7B\u522B\u624D\u65B0\u589E\u5C0F\u8282\uFF1B\u5DF2\u6709\u7AE0\u4E5F\u5E94\u590D\u7528\u539F\u7AE0\u540D\u548C topicId\u3002\u5728\u7AE0\u5185\u7528 overview.title \u533A\u5206\u957F\u671F\u7C7B\u522B\uFF0C\u4F8B\u5982\u201C\u7F3A\u9677\u6392\u67E5\u4E0E\u4FEE\u590D\u201D\u201C\u7248\u672C\u53D1\u5E03\u4E0E\u5B89\u88C5\u201D\u201C\u67B6\u6784\u4E0E\u914D\u7F6E\u51B3\u7B56\u201D\u201C\u754C\u9762\u4E0E\u4EA4\u4E92\u201D\u201CDSH \u517C\u5BB9\u201D\uFF1Bkind \u8868\u793A\u6BB5\u843D\u5185\u5BB9\u6027\u8D28\uFF0C\u4E0D\u662F\u62C6\u7AE0\u6216\u62C6\u8282\u4F9D\u636E\u3002\u4E0D\u540C\u9879\u76EE\u6216\u4E0D\u76F8\u5173\u9886\u57DF\u4ECD\u5206\u5F00\uFF0C\u4E0D\u80FD\u4E3A\u51CF\u5C11\u7AE0\u6570\u786C\u5408\u5E76\u3002\u6BCF\u6279\u901A\u5E38\u53EA\u65B0\u589E 0-2 \u4E2A\u5927\u7AE0\u8282\uFF1B12 \u662F\u4E92\u4E0D\u76F8\u5173\u9886\u57DF\u7684\u5B89\u5168\u4E0A\u9650\uFF0C\u4E0D\u662F\u521B\u5EFA\u76EE\u6807\u3002
7728
+ Topic \u662F\u957F\u671F\u7684\u5927\u7AE0\u8282\uFF0C\u6309\u9879\u76EE\u3001\u751F\u6D3B\u9886\u57DF\u6216\u957F\u671F\u5173\u7CFB\u7EC4\u7EC7\uFF0C\u800C\u4E0D\u662F\u6BCF\u4E2A\u4E8B\u4EF6\u6216\u5B50\u4EFB\u52A1\u4E00\u7AE0\u3002\u4F8B\u5982 StrataGate UI\u3001DSH \u517C\u5BB9\u3001Topic Directory\u3001Retrieval \u5E94\u5F52\u5165\u201CStrataGate\u201D\u540C\u4E00\u7AE0\uFF1B\u6C42\u804C\u6295\u9012\u3001\u9762\u8BD5\u548C\u5B9E\u4E60\u8FDB\u5C55\u5F52\u5165\u201C\u6C42\u804C\u4E0E\u804C\u4E1A\u53D1\u5C55\u201D\u3002existingTopics.sectionTitles \u5217\u51FA\u8BE5\u7AE0\u5168\u90E8\u5DF2\u6709\u5C0F\u8282\u6807\u9898\uFF0C\u4EC5\u4F9B\u5BFC\u822A\uFF0C\u4E0D\u662F\u6B63\u6587\u6216\u4E8B\u5B9E\u8BC1\u636E\u3002\u5DF2\u6709\u5C0F\u8282\u80FD\u5BB9\u7EB3\u672C\u6279\u4E8B\u9879\u65F6\u5FC5\u987B\u9010\u5B57\u590D\u7528\u539F title\uFF0C\u4E0D\u8981\u8FD1\u4E49\u6539\u540D\uFF1B\u4E0D\u80FD\u56E0\u4E3A overview \u53EA\u5C55\u793A\u524D\u51E0\u6BB5\u5C31\u91CD\u590D\u521B\u5EFA\u5C0F\u8282\u3002\u786E\u5B9E\u5C5E\u4E8E\u5DF2\u6709\u7C7B\u522B\u65E0\u6CD5\u5BB9\u7EB3\u7684\u65B0\u957F\u671F\u7C7B\u522B\u624D\u65B0\u589E\u5C0F\u8282\uFF1B\u5DF2\u6709\u7AE0\u4E5F\u5E94\u590D\u7528\u539F\u7AE0\u540D\u548C topicId\u3002\u5728\u7AE0\u5185\u7528 sections.title \u533A\u5206\u957F\u671F\u7C7B\u522B\uFF0C\u4F8B\u5982\u201C\u7F3A\u9677\u6392\u67E5\u4E0E\u4FEE\u590D\u201D\u201C\u7248\u672C\u53D1\u5E03\u4E0E\u5B89\u88C5\u201D\u201C\u67B6\u6784\u4E0E\u914D\u7F6E\u51B3\u7B56\u201D\u201C\u754C\u9762\u4E0E\u4EA4\u4E92\u201D\u201CDSH \u517C\u5BB9\u201D\uFF1Boverview.title \u4F7F\u7528\u5BF9\u5E94\u5C0F\u8282\u6807\u9898\uFF0Ckind \u8868\u793A\u6BB5\u843D\u5185\u5BB9\u6027\u8D28\uFF0C\u4E0D\u662F\u62C6\u7AE0\u6216\u62C6\u8282\u4F9D\u636E\u3002\u4E0D\u540C\u9879\u76EE\u6216\u4E0D\u76F8\u5173\u9886\u57DF\u4ECD\u5206\u5F00\uFF0C\u4E0D\u80FD\u4E3A\u51CF\u5C11\u7AE0\u6570\u786C\u5408\u5E76\u3002\u6BCF\u6279\u901A\u5E38\u53EA\u65B0\u589E 0-2 \u4E2A\u5927\u7AE0\u8282\uFF1B12 \u662F\u4E92\u4E0D\u76F8\u5173\u9886\u57DF\u7684\u5B89\u5168\u4E0A\u9650\uFF0C\u4E0D\u662F\u521B\u5EFA\u76EE\u6807\u3002
7445
7729
  \u6BCF\u4E2A\u672C\u6279\u4E8B\u4EF6\u5FC5\u987B\u81F3\u5C11\u5206\u914D\u7ED9\u4E00\u4E2A\u7AE0\u8282\uFF1B\u5373\u4F7F\u5C1A\u672A\u8FDB\u5165\u56FE\u8C31\u4E5F\u8981\u4FDD\u7559\u5165\u53E3\u3002\u540C\u4E00\u9879\u76EE\u4E0B\u4E0D\u540C\u4E8B\u4EF6\u3001\u65F6\u95F4\u548C\u51B3\u5B9A\u53EF\u4EE5\u5171\u7528\u7AE0\u8282\uFF0C\u5F52\u7C7B\u4E0D\u7B49\u4E8E\u628A\u4E8B\u5B9E\u5408\u5E76\u6216\u8BA4\u5B9A\u5B83\u4EEC\u76F8\u540C\u3002\u5148\u68C0\u67E5\u6240\u6709 existingTopics \u7684\u8303\u56F4\uFF0C\u80FD\u5BB9\u7EB3\u672C\u6279\u4E8B\u9879\u5C31\u4F18\u5148\u590D\u7528 topicId\uFF0C\u5373\u4F7F\u6807\u9898\u6CA1\u6709\u8BE5\u5B50\u4EFB\u52A1\u5173\u952E\u8BCD\uFF1B\u4E0D\u8981\u4E3A\u5B50\u529F\u80FD\u3001\u7248\u672C\u3001\u4E00\u6B21\u6295\u9012\u6216\u53D1\u5E03\u53E6\u5EFA\u7AE0\u8282\u3002\u540C\u540D\u7AE0\u8282\u53EA\u80FD\u8FD4\u56DE\u4E00\u6B21\u3002\u65B0\u589E\u7AE0\u8282\u7701\u7565 topicId\uFF1B\u65E7\u7AE0\u8282\u672A\u5C55\u793A\u7684\u6210\u5458\u7531\u5B58\u50A8\u5C42\u4FDD\u7559\uFF0C\u4E0D\u8981\u731C\u6D4B\u6216\u8865\u9F50\u5176\u5185\u5BB9\u3002\u6BCF\u4E2A\u7AE0\u8282 sourceEventIds \u53EA\u5199\u5B9E\u9645\u7ED9\u51FA\u7684\u4E8B\u4EF6\u7F16\u53F7\uFF1B\u6BCF\u6BB5\u6765\u6E90\u5FC5\u987B\u5C5E\u4E8E\u8BE5\u7AE0\u8282\u7684 sourceEventIds\u3002candidateTopicsOmitted \u8868\u793A\u53D7\u8F93\u5165\u9884\u7B97\u9650\u5236\u672A\u5C55\u793A\u7684\u5019\u9009\u6570\uFF0C\u4E0D\u80FD\u63A8\u65AD\u88AB\u7701\u7565\u7AE0\u8282\u7684\u5185\u5BB9\u3002
7446
7730
 
7447
7731
  \u5BFC\u822A\u5C42\u7EA7\u662F\u7AE0 \u2192 \u8282 \u2192 Event\u3002Event \u4FDD\u6301\u5177\u4F53\u3001\u72EC\u7ACB\uFF1B\u8282\u5FC5\u987B\u80FD\u6301\u7EED\u5BB9\u7EB3\u540C\u7C7B\u4E8B\u4EF6\u3002\u5355\u4E2A bug\u3001\u7248\u672C\u53F7\u3001\u67D0\u5929\u8FDB\u5C55\u3001\u4E00\u6B21\u5B89\u88C5\u6216\u4E00\u4E2A\u4FEE\u590D\u65B9\u6848\u4E0D\u80FD\u5355\u72EC\u6210\u8282\uFF0C\u4E5F\u4E0D\u8981\u7167\u6284 Event \u6807\u9898\u4F5C\u4E3A\u8282\u6807\u9898\u3002\u4F8B\u5982\u201C0.2.4 \u63D0\u53D6\u6839\u56E0\u66B4\u9732\u8FC7\u7A0B\u201D\u201C0.2.6 \u63D0\u53D6\u4ECD 0 \u4EA7\u51FA\u201D\u201C0.2.7 \u4FEE\u590D\u65B9\u6848\u201D\u201C\u5C01\u5757\u5F02\u5E38\u5F85\u6392\u67E5\u201D\u90FD\u8FDB\u5165\u201C\u7F3A\u9677\u6392\u67E5\u4E0E\u4FEE\u590D\u201D\uFF1B\u201C\u4ECA\u65E5\u7248\u672C\u8FED\u4EE3 0.2.1\u21920.2.7\u201D\u201C0.2.8 \u5B89\u88C5\u4E0E\u9A8C\u8BC1\u201D\u8FDB\u5165\u201C\u7248\u672C\u53D1\u5E03\u4E0E\u5B89\u88C5\u201D\u3002\u5177\u4F53\u7248\u672C\u3001\u65F6\u95F4\u3001\u6839\u56E0\u3001\u5904\u7F6E\u8FC7\u7A0B\u548C\u72B6\u6001\u653E\u5728\u8282\u5185\u6982\u8981\u53CA\u539F Event \u4E2D\u3002\u201C\u8BA1\u5212\u4FEE\u590D\u201D\u201C\u4FEE\u590D\u4E2D\u201D\u201C\u5DF2\u4FEE\u590D\u201D\u5C5E\u4E8E\u540C\u4E00\u7C7B\u522B\u7684\u4E0D\u540C\u72B6\u6001\uFF0C\u4E0D\u53E6\u8D77\u8282\u3002\u540C\u4E00\u7C7B\u522B\u53EF\u4EE5\u6709\u591A\u4E2A\u6709\u6765\u6E90\u7684\u6BB5\u843D\uFF0C\u4E0D\u8981\u4E3A\u6BB5\u843D\u53E6\u8D77\u6807\u9898\u3002
@@ -7449,7 +7733,10 @@ Topic \u662F\u957F\u671F\u7684\u5927\u7AE0\u8282\uFF0C\u6309\u9879\u76EE\u3001\u
7449
7733
 
7450
7734
  title \u662F\u53EF\u8BC6\u522B\u7684\u4E3B\u9898\u540D\u79F0\uFF1Bdescription \u7528\u4E00\u53E5\u8BDD\u8BF4\u660E\u8FD9\u91CC\u5B58\u6709\u54EA\u4E9B\u8D44\u6599\uFF0C\u4F8B\u5982\u201C\u5305\u542B\u90E8\u7F72\u9009\u578B\u3001\u8FC1\u79FB\u7ECF\u8FC7\u548C\u9057\u7559\u95EE\u9898\u201D\uFF0C\u4E0D\u8981\u628A\u5386\u53F2\u7ED3\u8BBA\u5199\u6210\u5F53\u524D\u4E8B\u5B9E\u3002overview \u7684 kind \u4EC5\u5141\u8BB8 scope\uFF08\u80CC\u666F\u6216\u8986\u76D6\u8303\u56F4\uFF09\u3001history\uFF08\u5386\u53F2\u8FDB\u5C55\uFF09\u3001decision\uFF08\u5F53\u65F6\u7684\u51B3\u5B9A\uFF09\u3001change\uFF08\u6709\u4F9D\u636E\u7684\u53D8\u5316\uFF09\u3001open-question\uFF08\u5F85\u786E\u8BA4\u4E8B\u9879\uFF09\u3002\u4FDD\u7559\u660E\u786E\u65F6\u70B9\u3001\u8FC7\u53BB\u5F0F\u3001\u8BA1\u5212\u3001\u53D6\u6D88\u3001\u4E89\u8BAE\u548C\u4E0D\u786E\u5B9A\u6027\uFF1B\u6CA1\u6709\u65B0\u7684\u652F\u6301\u4E0D\u80FD\u628A\u65E7\u4E8B\u5B9E\u5347\u683C\u4E3A\u5F53\u524D\u7ED3\u8BBA\uFF0C\u4E0D\u80FD\u628A\u8BA1\u5212\u5199\u6210\u5DF2\u5B8C\u6210\u3001\u5EFA\u8BAE\u5199\u6210\u5DF2\u51B3\u5B9A\u3001\u63A8\u6D4B\u5199\u6210\u539F\u56E0\u3002\u51B3\u5B9A\u66FE\u7ECF\u6210\u7ACB\u4E0D\u7B49\u4E8E\u73B0\u5728\u4ECD\u6709\u6548\u3002\u76F8\u5173\u4E8B\u4EF6\u77DB\u76FE\u65F6\u4FDD\u7559\u77DB\u76FE\u548C\u5F85\u786E\u8BA4\u72B6\u6001\u3002
7451
7735
 
7452
- outputTokenBudget \u662F\u6574\u4E2A\u54CD\u5E94\u7684\u8F93\u51FA\u9884\u7B97\u3002\u4F18\u5148\u5B8C\u6574\u5206\u914D\u672C\u6279\u6240\u6709\u4E8B\u4EF6\uFF0C\u518D\u5199\u5FC5\u8981\u7684\u7B80\u77ED\u540D\u79F0\u3001\u8303\u56F4\u8BF4\u660E\u548C\u65B0\u589E\u6BB5\u3002\u5B58\u50A8\u5C42\u4F1A\u81EA\u52A8\u7EE7\u627F\u6709\u6548\u65E7\u6982\u8981\uFF0C\u65E0\u9700\u8F93\u51FA\u590D\u8FF0\uFF1B\u901A\u5E38\u53EA\u9700 0-2 \u6BB5\u65B0\u589E\u6982\u8981\uFF0C\u9884\u7B97\u4E0D\u8DB3\u65F6 overview \u53EF\u4E3A\u7A7A\uFF0C\u53EA\u5EFA\u7ACB\u76EE\u5F55\u5165\u53E3\uFF0C\u4E0D\u622A\u65AD JSON \u6216\u9057\u6F0F\u4E8B\u4EF6\u3002
7736
+ sections \u72EC\u7ACB\u4FDD\u5B58\u4E8B\u4EF6\u7684\u5C0F\u8282\u5F52\u5C5E\uFF0C\u4E0E overview \u7684\u6765\u6E90\u5F15\u7528\u5206\u5F00\u3002\u6BCF\u4E2A\u5F52\u5165\u672C\u7AE0\u7684\u672C\u6279\u4E8B\u4EF6\u5FC5\u987B\u81F3\u5C11\u5206\u914D\u5230\u4E00\u4E2A sections \u5C0F\u8282\uFF0C\u5373\u4F7F\u603B\u89C8\u6CA1\u6709\u5F15\u7528\u5B83\u3002sections \u53EA\u5199\u672C\u6279\u4E8B\u4EF6\u7F16\u53F7\uFF1B\u65E7\u5F52\u5C5E\u7531\u5B58\u50A8\u5C42\u7EE7\u627F\u3002\u4F18\u5148\u9010\u5B57\u590D\u7528 existingTopics.sectionTitles \u4E2D\u9002\u5408\u7684\u6807\u9898\uFF1B\u5C0F\u8282\u6CBF\u7528\u4E0A\u8FF0\u957F\u671F\u7C7B\u522B\u89C4\u5219\u3002\u65B0 overview \u7684\u5C55\u793A\u6807\u9898\u5FC5\u987B\u5339\u914D\u672C\u8F6E sections.title \u6216\u91CD\u5206\u7C7B\u540E\u4ECD\u5B58\u5728\u7684\u5DF2\u6709\u5C0F\u8282\u6807\u9898\uFF0C\u4E0D\u80FD\u9760\u603B\u89C8\u5355\u72EC\u521B\u5EFA\u5C0F\u8282\uFF1B\u7701\u7565 title \u65F6\uFF0C\u5176 kind \u7684\u9ED8\u8BA4\u5C55\u793A\u6807\u9898\u4E5F\u5FC5\u987B\u6709\u5BF9\u5E94\u7684\u6700\u7EC8\u5C0F\u8282\u3002\u82E5\u91CD\u5206\u7C7B\u79FB\u8D70\u65E7\u5C0F\u8282\u7684\u5168\u90E8\u4E8B\u4EF6\uFF0C\u5B58\u50A8\u5C42\u4E22\u5F03\u8BE5\u8282\u7684\u65E7\u603B\u89C8\uFF0C\u5373\u4F7F\u672C\u8F6E\u539F\u6837\u56DE\u4F20\u4E5F\u4E0D\u4F1A\u4FDD\u7559\u3002overview.title \u53EA\u51B3\u5B9A\u6982\u62EC\u6587\u5B57\u663E\u793A\u5728\u54EA\u4E2A\u5C0F\u8282\uFF0C\u4E0D\u51B3\u5B9A\u4E8B\u4EF6\u5F52\u5C5E\uFF1B\u65E0\u9700\u4E3A\u6BCF\u6761\u4E8B\u4EF6\u5199\u603B\u89C8\uFF0C\u4E5F\u4E0D\u8981\u4E3A\u4E86\u8986\u76D6\u76EE\u5F55\u800C\u865A\u589E\u6765\u6E90\u5F15\u7528\u3002
7737
+ \u82E5\u8F93\u5165\u542B sectionBackfillTopicId\uFF0C\u8FD9\u662F\u5347\u7EA7\u540E\u7684\u65E7\u4E8B\u4EF6\u8865\u5F52\u7C7B\uFF1A\u53EA\u8FD4\u56DE\u8BE5 topicId \u7684\u4E00\u4E2A\u7AE0\u8282\uFF0C\u4FDD\u7559\u539F\u7AE0\u540D\u548C\u63CF\u8FF0\uFF0C\u4E3A\u5168\u90E8\u672C\u6279\u4E8B\u4EF6\u5199\u5165\u9002\u5F53\u7684\u5C0F\u8282\uFF1B\u4E0D\u5F97\u6362\u7AE0\u6216\u65B0\u5EFA\u7AE0\u3002\u5DF2\u6709\u5C0F\u8282\u6807\u9898\u53EF\u80FD\u53EA\u5C55\u793A\u524D 120 \u4E2A\uFF0C\u4F18\u5148\u590D\u7528\u9002\u5408\u7684\u5DF2\u6709\u6807\u9898\u3002\u6B64\u4EFB\u52A1\u65E0\u9700\u91CD\u5199\u603B\u89C8\uFF0Coverview \u53EF\u4EE5\u4E3A\u7A7A\uFF0C\u5B58\u50A8\u5C42\u4FDD\u7559\u6709\u6548\u65E7\u603B\u89C8\u3002
7738
+
7739
+ outputTokenBudget \u662F\u6574\u4E2A\u54CD\u5E94\u7684\u8F93\u51FA\u9884\u7B97\u3002\u4F18\u5148\u5B8C\u6574\u5206\u914D\u672C\u6279\u6240\u6709\u4E8B\u4EF6\u5230\u7AE0\u8282\u548C\u5C0F\u8282\uFF0C\u518D\u5199\u5FC5\u8981\u7684\u7B80\u77ED\u540D\u79F0\u3001\u8303\u56F4\u8BF4\u660E\u548C\u65B0\u589E\u6BB5\u3002\u5B58\u50A8\u5C42\u4F1A\u5728\u6700\u7EC8\u5C0F\u8282\u4ECD\u5B58\u5728\u65F6\u81EA\u52A8\u7EE7\u627F\u6709\u6548\u65E7\u6982\u8981\uFF0C\u65E0\u9700\u8F93\u51FA\u590D\u8FF0\uFF1B\u901A\u5E38\u53EA\u9700 0-2 \u6BB5\u65B0\u589E\u6982\u8981\uFF0C\u9884\u7B97\u4E0D\u8DB3\u65F6 overview \u53EF\u4E3A\u7A7A\uFF0Csections \u4ECD\u5FC5\u987B\u5B8C\u6574\u5F52\u7C7B\u672C\u6279\u4E8B\u4EF6\uFF0C\u4E0D\u622A\u65AD JSON \u6216\u9057\u6F0F\u4E8B\u4EF6\u3002
7453
7740
  \u6BCF\u6279\u6700\u591A 12 \u4E2A\u4E3B\u9898\uFF1Btitle \u6700\u591A 120 \u4E2A\u5B57\u7B26\uFF0Cdescription \u6700\u591A 400 \u4E2A\u5B57\u7B26\uFF0C\u4F46\u5C3D\u91CF\u7528\u77ED\u540D\u79F0\u548C\u4E00\u53E5\u8303\u56F4\u8BF4\u660E\uFF1B\u6BCF\u4E2A\u4E3B\u9898\u5728\u672C\u6B21\u54CD\u5E94\u6700\u591A 8 \u6BB5\u6982\u8981\uFF1B\u8FD9\u4E0D\u662F\u7D2F\u8BA1\u7AE0\u8282\u7684\u6BB5\u6570\u6216\u8282\u6570\u4E0A\u9650\uFF0C\u6709\u6548\u65E7\u6BB5\u7531\u5B58\u50A8\u5C42\u4FDD\u7559\u3002\u6BCF\u6BB5\u6700\u591A 600 \u4E2A\u5B57\u7B26\u300112 \u4E2A\u6765\u6E90\u3002\u4E0D\u8981\u91CD\u590D\u6807\u9898\u3001\u76EE\u5F55\u8BF4\u660E\u3001\u5DF2\u6709\u6982\u8981\u6216\u65E0\u5173\u80CC\u666F\u3002\u4E0D\u8981\u751F\u6210\u7ECF\u9A8C\u5C42\u6216\u53E6\u5199\u65B0\u7684\u4E8B\u4EF6\u3002`;
7454
7741
  var MAX_TOPIC_INPUT_TOKENS = 2e4;
7455
7742
  var EXTERNAL_MEMORY_DECIDER_PARAMETERS = {
@@ -7594,6 +7881,9 @@ function topicProjectionPayload(context, outputTokenBudget) {
7594
7881
  if (new Set(context.existingTopics.map(({ id }) => id)).size !== context.existingTopics.length) {
7595
7882
  throw new Error("Topic projection input contains duplicate candidate topic ids");
7596
7883
  }
7884
+ if (context.sectionBackfillTopicId && !context.existingTopics.some(({ id }) => id === context.sectionBackfillTopicId)) {
7885
+ throw new Error("Topic section backfill input must include its existing chapter");
7886
+ }
7597
7887
  const events = context.events.map((event) => ({
7598
7888
  id: event.id,
7599
7889
  title: event.title,
@@ -7608,6 +7898,7 @@ function topicProjectionPayload(context, outputTokenBudget) {
7608
7898
  for (; ; ) {
7609
7899
  const payload = {
7610
7900
  jobId: context.jobId,
7901
+ ...context.sectionBackfillTopicId ? { sectionBackfillTopicId: context.sectionBackfillTopicId } : {},
7611
7902
  evidenceCompleteness: "bounded-navigation-cards; retrieve Event and original-source evidence before relying on factual content",
7612
7903
  outputTokenBudget,
7613
7904
  events,
@@ -7619,7 +7910,8 @@ function topicProjectionPayload(context, outputTokenBudget) {
7619
7910
  overview: topic.overview,
7620
7911
  ...topic.sectionTitles ? { sectionTitles: topic.sectionTitles } : {},
7621
7912
  sourceEventIds: topic.sourceEventIds,
7622
- totalSourceEvents: topic.totalSourceEvents
7913
+ totalSourceEvents: topic.totalSourceEvents,
7914
+ ...topic.sectionTitlesOmitted ? { sectionTitlesOmitted: topic.sectionTitlesOmitted } : {}
7623
7915
  })),
7624
7916
  candidateTopicsOmitted: context.existingTopics.length - existingTopics.length
7625
7917
  };
@@ -7627,13 +7919,13 @@ function topicProjectionPayload(context, outputTokenBudget) {
7627
7919
 
7628
7920
  ${JSON_RETRY_INSTRUCTION}`, tool: toolSchema("topicProjector"), payload }));
7629
7921
  if (inputTokens <= MAX_TOPIC_INPUT_TOKENS) return { payload, shownContext: { ...context, existingTopics } };
7630
- if (existingTopics.length > 0) existingTopics.pop();
7922
+ if (existingTopics.length > 0 && existingTopics.at(-1).id !== context.sectionBackfillTopicId) existingTopics.pop();
7631
7923
  else throw new Error(`Topic projection input exceeds ${MAX_TOPIC_INPUT_TOKENS} estimated tokens; split the Event batch instead of truncating evidence`);
7632
7924
  }
7633
7925
  }
7634
7926
  function parseTopicProjection(value, context) {
7635
7927
  const fail = (message) => {
7636
- throw new Error(`Topic projection validation failed: ${message}`);
7928
+ throw new TopicProjectionError("validation-failed", message);
7637
7929
  };
7638
7930
  const raw = object(value);
7639
7931
  if (Object.keys(raw).some((key) => key !== "topics") || !Array.isArray(raw.topics) || raw.topics.length > 12) {
@@ -7655,14 +7947,18 @@ function parseTopicProjection(value, context) {
7655
7947
  }
7656
7948
  return value2;
7657
7949
  };
7658
- const topics = raw.topics.map((candidate) => {
7950
+ const rawTopics = raw.topics;
7951
+ const topics = rawTopics.map((candidate) => {
7659
7952
  const item = object(candidate);
7660
7953
  const title = boundedText(item.title, "topic.title", 120);
7661
7954
  const label = title.normalize("NFKC").replace(/\s+/g, " ").toLowerCase();
7662
7955
  if (usedLabels.has(label)) fail("duplicate chapter label; combine its batch assignments");
7663
7956
  usedLabels.add(label);
7664
- if (Object.keys(item).some((key) => !["topicId", "title", "description", "sourceEventIds", "overview"].includes(key))) fail("unexpected topic field");
7957
+ if (Object.keys(item).some((key) => !["topicId", "title", "description", "sourceEventIds", "overview", "sections"].includes(key))) fail("unexpected topic field");
7665
7958
  const topicId = item.topicId === void 0 ? void 0 : boundedText(item.topicId, "topicId", 200);
7959
+ if (context.sectionBackfillTopicId && (rawTopics.length !== 1 || topicId !== context.sectionBackfillTopicId)) {
7960
+ fail("section backfill must update only its existing chapter");
7961
+ }
7666
7962
  if (topicId !== void 0 && (!candidates.has(topicId) || usedTopicIds.has(topicId))) fail("unknown or repeated topicId");
7667
7963
  if (topicId !== void 0) usedTopicIds.add(topicId);
7668
7964
  const existing = topicId === void 0 ? void 0 : candidates.get(topicId);
@@ -7670,7 +7966,24 @@ function parseTopicProjection(value, context) {
7670
7966
  const sources2 = sourceIds(item.sourceEventIds, "topic.sourceEventIds", allowedIds);
7671
7967
  if (!sources2.some((id) => eventIds.has(id))) fail("every returned topic must cover a supplied batch Event");
7672
7968
  for (const id of sources2) if (eventIds.has(id)) covered.add(id);
7969
+ if (!Array.isArray(item.sections) || item.sections.length === 0) fail("sections must assign batch Events independently of overview");
7970
+ const sectionLabels = /* @__PURE__ */ new Set();
7971
+ const sections = item.sections.map((candidateSection) => {
7972
+ const section = object(candidateSection);
7973
+ if (Object.keys(section).some((key2) => !["title", "sourceEventIds"].includes(key2))) fail("unexpected section field");
7974
+ const title2 = boundedText(section.title, "section.title", 80);
7975
+ const key = title2.normalize("NFKC").replace(/\s+/g, " ").toLowerCase();
7976
+ if (sectionLabels.has(key)) fail("duplicate section title");
7977
+ sectionLabels.add(key);
7978
+ return { title: title2, sourceEventIds: sourceIds(section.sourceEventIds, "section.sourceEventIds", new Set(sources2.filter((id) => eventIds.has(id)))) };
7979
+ });
7980
+ const assigned = new Set(sections.flatMap((section) => section.sourceEventIds));
7981
+ if (sources2.some((id) => eventIds.has(id) && !assigned.has(id))) fail("every topic batch Event must be assigned to a section");
7673
7982
  if (!Array.isArray(item.overview) || item.overview.length > 8) fail("overview must contain 0-8 source-grounded entries");
7983
+ const overviewTitles = [
7984
+ ...sections.map((section) => section.title),
7985
+ ...existing?.sectionTitles ?? memoryTopicMembershipSections(existing ?? { overview: [] }).map((section) => section.title)
7986
+ ];
7674
7987
  const overview2 = item.overview.map((candidateEntry) => {
7675
7988
  const entry = object(candidateEntry);
7676
7989
  if (Object.keys(entry).some((key) => !["kind", "title", "text", "sourceEventIds"].includes(key))) fail("unexpected overview field");
@@ -7679,22 +7992,27 @@ function parseTopicProjection(value, context) {
7679
7992
  const paragraph = boundedText(entry.text, "overview.text", 600);
7680
7993
  const paragraphSources = sourceIds(entry.sourceEventIds, "overview.sourceEventIds", new Set(sources2), 12);
7681
7994
  if (entry.kind !== "scope" && paragraphSources.some((id) => truncatedIds.has(id))) fail("truncated Event evidence may only support scope entries");
7682
- if (paragraphSources.some((id) => !eventIds.has(id)) && !existing?.overview.some((old) => old.kind === entry.kind && old.title === sectionTitle && old.text === paragraph && old.sourceEventIds.length === paragraphSources.length && old.sourceEventIds.every((id) => paragraphSources.includes(id)))) {
7995
+ const preserved = existing?.overview.some((old) => old.kind === entry.kind && old.title === sectionTitle && old.text === paragraph && old.sourceEventIds.length === paragraphSources.length && old.sourceEventIds.every((id) => paragraphSources.includes(id)));
7996
+ if (paragraphSources.some((id) => !eventIds.has(id)) && !preserved) {
7683
7997
  fail("old overview evidence may only be preserved verbatim; rewritten entries must cite batch Events only");
7684
7998
  }
7685
- return { kind: entry.kind, ...sectionTitle === void 0 ? {} : { title: sectionTitle }, text: paragraph, sourceEventIds: paragraphSources };
7999
+ const part = { kind: entry.kind, ...sectionTitle === void 0 ? {} : { title: sectionTitle }, text: paragraph, sourceEventIds: paragraphSources };
8000
+ if (!preserved && !memoryTopicOverviewMatchesSection(part, overviewTitles)) fail("new overview must match a declared or inherited section");
8001
+ return part;
7686
8002
  });
7687
8003
  return {
7688
8004
  ...topicId === void 0 ? {} : { topicId },
7689
8005
  title,
7690
8006
  description: boundedText(item.description, "topic.description", 400),
7691
8007
  sourceEventIds: sources2,
8008
+ sections,
7692
8009
  overview: overview2
7693
8010
  };
7694
8011
  });
7695
8012
  if ([...eventIds].some((id) => !covered.has(id))) fail("every supplied batch Event must be assigned to a topic");
7696
8013
  return { topics };
7697
8014
  }
8015
+ var TOPIC_OUTPUT_TOKEN_BUDGET = 32768;
7698
8016
  var DshModelBridge = class {
7699
8017
  constructor(ctx, config, liveStructuredReasoningEffort) {
7700
8018
  this.ctx = ctx;
@@ -7703,12 +8021,14 @@ var DshModelBridge = class {
7703
8021
  this.structuredReasoningEffort = config.structuredReasoningEffort ?? "auto";
7704
8022
  this.ctx.on?.("llm/adapters-updated", () => {
7705
8023
  this.offCapabilities.clear();
8024
+ this.contextWindows.clear();
7706
8025
  for (const listener of this.adaptersUpdatedListeners) listener();
7707
8026
  });
7708
8027
  }
7709
8028
  ctx;
7710
8029
  config;
7711
8030
  liveStructuredReasoningEffort;
8031
+ contextWindows = /* @__PURE__ */ new Map();
7712
8032
  sessions = new AsyncLocalStorage();
7713
8033
  successfulResponses = [];
7714
8034
  offCapabilities = /* @__PURE__ */ new Map();
@@ -7938,7 +8258,7 @@ Use project scope for repository decisions, user scope for stable preferences/id
7938
8258
  return { reason: text2(raw.reason, "Projected Event evidence into the Knowledge Graph."), nodes, edges };
7939
8259
  };
7940
8260
  topicProjector = async (context) => {
7941
- const { payload, shownContext } = topicProjectionPayload(context, this.config.maxOutputTokens);
8261
+ const { payload, shownContext } = topicProjectionPayload(context, TOPIC_OUTPUT_TOKEN_BUDGET);
7942
8262
  try {
7943
8263
  const raw = await this.callStructured(
7944
8264
  "topicProjector",
@@ -7946,13 +8266,16 @@ Use project scope for repository decisions, user scope for stable preferences/id
7946
8266
  payload,
7947
8267
  (value) => {
7948
8268
  parseTopicProjection(value, shownContext);
7949
- }
8269
+ },
8270
+ { eventCount: shownContext.events.length, candidateTopicCount: shownContext.existingTopics.length }
7950
8271
  );
7951
8272
  return parseTopicProjection(raw, shownContext);
7952
8273
  } catch (error) {
7953
- if (error instanceof ModelJsonResponseError) throw new Error(error.message.split("\nRaw response preview")[0]);
7954
- if (error instanceof Error && /^StrataGate structured model task timed out after \d+ms$/.test(error.message)) throw new Error(error.message);
7955
- 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");
7956
8279
  }
7957
8280
  };
7958
8281
  externalMemoryDecider = async (context) => {
@@ -8004,17 +8327,25 @@ Use project scope for repository decisions, user scope for stable preferences/id
8004
8327
  }
8005
8328
  return proposed;
8006
8329
  }
8007
- async callStructured(kind, system, payload, validate) {
8330
+ async callStructured(kind, system, payload, validate, topicMetadata) {
8008
8331
  const execution = this.sessions.getStore();
8009
8332
  if (!execution) throw new Error("StrataGate model callback ran without an execution context");
8010
8333
  const baseRoute = this.resolveRoute(execution.session);
8011
8334
  const routeKey = `${baseRoute.provider}\0${baseRoute.model}`;
8012
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);
8013
8343
  let lastError;
8014
8344
  let lastResponse = "";
8015
8345
  let attemptsUsed = 0;
8016
8346
  let noAdapterRetried = false;
8017
8347
  let graphRetryFeedback = "";
8348
+ let offFallback = false;
8018
8349
  for (let attempt = 1; attempt <= JSON_RESPONSE_ATTEMPTS; attempt += 1) {
8019
8350
  attemptsUsed = attempt;
8020
8351
  const message = createUserMessage({
@@ -8038,10 +8369,33 @@ ${JSON_RETRY_INSTRUCTION}${graphRetryFeedback}`,
8038
8369
  type: "function",
8039
8370
  function: { name: STRUCTURED_TOOLS[kind].name }
8040
8371
  },
8041
- 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,
8042
8373
  sessionId: execution.sessionId,
8043
8374
  purpose: "compaction"
8044
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
+ }
8045
8399
  try {
8046
8400
  await this.consumeStructuredStream(request, assembler);
8047
8401
  } catch (error) {
@@ -8053,13 +8407,19 @@ ${JSON_RETRY_INSTRUCTION}${graphRetryFeedback}`,
8053
8407
  if (useOff && isOffRejection(error)) {
8054
8408
  this.offCapabilities.set(routeKey, "unsupported");
8055
8409
  useOff = false;
8410
+ offFallback = true;
8056
8411
  this.warnOffFallbackOnce(routeKey, `${baseRoute.provider}/${baseRoute.model} rejected reasoningEffort=off; retrying once without it`);
8057
8412
  attempt -= 1;
8058
8413
  continue;
8059
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
+ }
8060
8419
  throw error;
8061
8420
  }
8062
8421
  const finish = assembler.finish;
8422
+ if (isTopic) topicDiagnostics.finishReason = finish.kind;
8063
8423
  if (finish.kind === "error" || finish.kind === "aborted") {
8064
8424
  const failure = new Error(`StrataGate model call failed [${finish.failure.code}]: ${finish.failure.message}`);
8065
8425
  if (!noAdapterRetried && isNoAdapter(finish.failure)) {
@@ -8070,26 +8430,34 @@ ${JSON_RETRY_INSTRUCTION}${graphRetryFeedback}`,
8070
8430
  if (useOff && isOffRejection(finish.failure)) {
8071
8431
  this.offCapabilities.set(routeKey, "unsupported");
8072
8432
  useOff = false;
8433
+ offFallback = true;
8073
8434
  this.warnOffFallbackOnce(routeKey, `${baseRoute.provider}/${baseRoute.model} rejected reasoningEffort=off; retrying once without it`);
8074
8435
  attempt -= 1;
8075
8436
  continue;
8076
8437
  }
8438
+ if (isTopic) throw topicError("provider-failed", "provider or route error");
8077
8439
  throw failure;
8078
8440
  }
8079
8441
  if (useOff) this.offCapabilities.set(routeKey, "supported");
8080
8442
  const blocks = assembler.blocks();
8081
8443
  const calls = blocks.filter((block) => block.type === "tool-call");
8082
- 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)}
8083
8450
  [finish=${finish.kind}; toolCalls=${calls.length}]`;
8084
8451
  lastResponse = responseForError;
8085
8452
  try {
8086
8453
  const expectedTool = STRUCTURED_TOOLS[kind].name;
8087
8454
  let parsed;
8088
8455
  if (calls.length !== 1 || calls[0]?.name !== expectedTool) {
8089
- 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");
8090
8457
  try {
8091
8458
  parsed = parseJsonResponse(textFallback, STRUCTURED_FIELDS[kind]);
8092
8459
  } catch {
8460
+ if (isTopic) throw topicError("schema-invalid", "expected exactly one structured topic call");
8093
8461
  throw new ModelJsonResponseError(
8094
8462
  `StrataGate model response did not call ${expectedTool} exactly once`,
8095
8463
  { response: responseForError }
@@ -8099,6 +8467,7 @@ ${JSON_RETRY_INSTRUCTION}${graphRetryFeedback}`,
8099
8467
  try {
8100
8468
  parsed = JSON.parse(calls[0].arguments);
8101
8469
  } catch {
8470
+ if (isTopic) throw topicError("schema-invalid", "tool arguments are not valid JSON");
8102
8471
  throw new ModelJsonResponseError(
8103
8472
  `StrataGate ${expectedTool} arguments were not valid JSON`,
8104
8473
  { response: responseForError }
@@ -8108,13 +8477,14 @@ ${JSON_RETRY_INSTRUCTION}${graphRetryFeedback}`,
8108
8477
  if (kind === "graphProjector") parsed = normalizeGraphNameProvenance(parsed);
8109
8478
  const violations = validateArgs(STRUCTURED_TOOLS[kind].parameters, parsed);
8110
8479
  if (violations.length > 0) {
8480
+ if (isTopic) throw topicError("schema-invalid", "structured topic schema mismatch");
8111
8481
  if (kind === "graphProjector") {
8112
8482
  const paths = violations.flatMap((violation) => violation.match(/nodes\[\d+\]\.metadataProvenance\.name(?:\[\d+\])?/g) ?? []);
8113
8483
  graphRetryFeedback = paths.length > 0 ? `
8114
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.` : "";
8115
8485
  }
8116
8486
  throw new ModelJsonResponseError(
8117
- `StrataGate ${expectedTool} arguments were invalid: ${kind === "topicProjector" ? "structured topic schema mismatch" : violations.join("; ")}`,
8487
+ `StrataGate ${expectedTool} arguments were invalid: ${violations.join("; ")}`,
8118
8488
  { response: responseForError }
8119
8489
  );
8120
8490
  }
@@ -8130,6 +8500,7 @@ Correct these canonical-name provenance fields: ${[...new Set(paths)].slice(0, 2
8130
8500
  try {
8131
8501
  validate?.(parsed);
8132
8502
  } catch (error) {
8503
+ if (isTopic) throw topicError("validation-failed", error instanceof TopicProjectionError ? error.diagnostics.reason : "topic semantic validation failed");
8133
8504
  throw new ModelJsonResponseError(
8134
8505
  `StrataGate ${expectedTool} arguments were invalid: ${error instanceof Error ? error.message : String(error)}`,
8135
8506
  { cause: error, response: responseForError }
@@ -8146,7 +8517,7 @@ Correct these canonical-name provenance fields: ${[...new Set(paths)].slice(0, 2
8146
8517
  }
8147
8518
  return parsed;
8148
8519
  } catch (error) {
8149
- if (!(error instanceof ModelJsonResponseError)) throw error;
8520
+ if (!(error instanceof ModelJsonResponseError) && !(isTopic && error instanceof TopicProjectionError)) throw error;
8150
8521
  lastError = finish.kind === "max-tokens" ? new ModelJsonResponseError(
8151
8522
  `StrataGate ${STRUCTURED_TOOLS[kind].name} call was truncated before valid arguments`,
8152
8523
  { cause: error, response: responseForError }
@@ -8157,6 +8528,7 @@ Correct these canonical-name provenance fields: ${[...new Set(paths)].slice(0, 2
8157
8528
  }
8158
8529
  }
8159
8530
  }
8531
+ if (isTopic && lastError instanceof TopicProjectionError) throw lastError;
8160
8532
  const validationDetail = lastError?.message ? `: ${lastError.message}` : "";
8161
8533
  throw new ModelJsonResponseError(
8162
8534
  `StrataGate model did not produce a valid ${STRUCTURED_TOOLS[kind].name} call after ${attemptsUsed} attempt${attemptsUsed === 1 ? "" : "s"}${validationDetail}`,
@@ -8193,6 +8565,7 @@ Correct these canonical-name provenance fields: ${[...new Set(paths)].slice(0, 2
8193
8565
  timer.unref?.();
8194
8566
  })
8195
8567
  ]);
8568
+ if (Number.isFinite(info.context?.contextWindow) && info.context.contextWindow > 0) this.contextWindows.set(key, info.context.contextWindow);
8196
8569
  if (!info.reasoning) return this.currentStructuredReasoningEffort() === "force-off";
8197
8570
  const supported = info.reasoning.efforts.some(({ id }) => String(id) === "off");
8198
8571
  this.offCapabilities.set(key, supported ? "supported" : "unsupported");
@@ -8562,6 +8935,12 @@ function memoryTopicSections(topic) {
8562
8935
  if (!memberPositions.has(id)) memberPositions.set(id, index);
8563
8936
  });
8564
8937
  const sections = /* @__PURE__ */ new Map();
8938
+ for (const membership of memoryTopicMembershipSections(topic)) {
8939
+ const key = memoryTopicSectionKey(membership.title);
8940
+ const section = sections.get(key) ?? { key, title: membership.title, paragraphs: [], sourceEventIds: [] };
8941
+ section.sourceEventIds.push(...membership.sourceEventIds.filter((id) => memberPositions.has(id)));
8942
+ sections.set(key, section);
8943
+ }
8565
8944
  for (const part of topic.overview) {
8566
8945
  const title = memoryTopicSectionTitle(part);
8567
8946
  const identity = memoryTopicSectionKey(title);
@@ -8571,7 +8950,6 @@ function memoryTopicSections(topic) {
8571
8950
  sections.set(identity, section);
8572
8951
  }
8573
8952
  section.paragraphs.push(part);
8574
- for (const id of part.sourceEventIds) if (memberPositions.has(id)) section.sourceEventIds.push(id);
8575
8953
  }
8576
8954
  return [...sections.values()].map((section) => ({
8577
8955
  section: {
@@ -8639,11 +9017,11 @@ function topicNavigation(topics, events) {
8639
9017
  coverage: topic.coverage,
8640
9018
  isFallback: topic.isFallback === true
8641
9019
  }));
8642
- const categories = CATEGORIES.map((category) => ({
9020
+ const categories2 = CATEGORIES.map((category) => ({
8643
9021
  ...category,
8644
9022
  count: entries.filter((entry) => entry.category === category.id).length
8645
9023
  })).filter(({ count }) => count > 0);
8646
- return { entries, categories };
9024
+ return { entries, categories: categories2 };
8647
9025
  }
8648
9026
  function topicPage(topics, events, options = {}) {
8649
9027
  const navigation = topicNavigation(topics, events);
@@ -8667,18 +9045,18 @@ function lineText(value, maximum) {
8667
9045
  return value.replace(/\s+/g, " ").trim().slice(0, maximum);
8668
9046
  }
8669
9047
  function renderMemoryDirectory(topics, events) {
8670
- const { entries, categories } = topicNavigation(topics, events);
9048
+ const { entries, categories: categories2 } = topicNavigation(topics, events);
8671
9049
  if (entries.length === 0) return "";
8672
9050
  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";
8673
9051
  const lines = entries.map((entry) => `- ${entry.id}\uFF1A${lineText(entry.title, 64)}${entry.description ? `\uFF1B${lineText(entry.description, 48)}` : ""}`);
8674
9052
  const complete = `${header}
8675
9053
  ${lines.join("\n")}`;
8676
9054
  if (estimateTokens(complete) <= MEMORY_DIRECTORY_TOKEN_BUDGET) return complete;
8677
- const branches = categories.map(({ id, label, count }) => `${id}\uFF08${label} ${count}\uFF09`).join("\uFF1B");
9055
+ const branches = categories2.map(({ id, label, count }) => `${id}\uFF08${label} ${count}\uFF09`).join("\uFF1B");
8678
9056
  const navigation = `${header}
8679
9057
  \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`;
8680
9058
  const selected = [];
8681
- for (const category of categories) {
9059
+ for (const category of categories2) {
8682
9060
  const entry = entries.find((item) => item.category === category.id);
8683
9061
  const line = `- ${entry.id}\uFF1A${lineText(entry.title, 40)}`;
8684
9062
  if (estimateTokens(`${navigation}
@@ -8690,16 +9068,26 @@ ${[...selected.map(({ line: line2 }) => line2), line].join("\n")}`) <= MEMORY_DI
8690
9068
  ${selected.map(({ line }) => line).join("\n")}`.trim();
8691
9069
  }
8692
9070
  function boundedTopic(topic, envelope = {}) {
9071
+ const { sections: _sections, ...summaryTopic } = topic;
9072
+ const sectionIndex = memoryTopicSections(topic).map(({ title, sourceEventIds: sourceEventIds2 }) => ({ title, sourceEventCount: sourceEventIds2.length }));
9073
+ const sections = [];
8693
9074
  const overview2 = [];
8694
9075
  const sourceEventIds = topic.sourceEventIds.slice(0, 12);
8695
- const result = (parts) => ({
8696
- ...topic,
9076
+ const result = (parts, selectedSections = sections) => ({
9077
+ ...summaryTopic,
8697
9078
  sourceEventIds,
8698
9079
  overview: parts,
9080
+ sections: selectedSections,
9081
+ totalSections: sectionIndex.length,
9082
+ omittedSections: sectionIndex.length - selectedSections.length,
8699
9083
  totalSourceEvents: topic.sourceEventIds.length,
8700
9084
  omittedSourceEvents: topic.sourceEventIds.length - sourceEventIds.length,
8701
9085
  omittedOverviewParagraphs: topic.overview.length - parts.length
8702
9086
  });
9087
+ for (const section of sectionIndex) {
9088
+ if (estimateTokens(JSON.stringify({ ...envelope, topic: result(overview2, [...sections, section]) })) > TOPIC_OVERVIEW_TOKEN_BUDGET) break;
9089
+ sections.push(section);
9090
+ }
8703
9091
  for (const paragraph of topic.overview) {
8704
9092
  if (estimateTokens(JSON.stringify({ ...envelope, topic: result([...overview2, paragraph]) })) <= TOPIC_OVERVIEW_TOKEN_BUDGET) overview2.push(paragraph);
8705
9093
  }
@@ -10306,6 +10694,7 @@ var StrataGateRuntime = class {
10306
10694
  const jobId = id.trim();
10307
10695
  if (!key) throw new TypeError("StrataGate admin namespace must not be empty");
10308
10696
  if (!jobId) throw new TypeError("StrataGate job id must not be empty");
10697
+ if (kind === "topic-projection") return this.adminRetryTopicProjection(key, jobId);
10309
10698
  const retryKey = `${key}\0${kind}\0${jobId}`;
10310
10699
  const existing = this.adminJobRetryRuns.get(retryKey);
10311
10700
  if (existing) return existing;
@@ -11496,7 +11885,7 @@ function currentPluginVersion() {
11496
11885
  return "unknown";
11497
11886
  }
11498
11887
  var STRATAGATE_DSH_VERSION = currentPluginVersion();
11499
- var TOPIC_DIRECTORY_VIEW_VERSION = 4;
11888
+ var TOPIC_DIRECTORY_VIEW_VERSION = 5;
11500
11889
  function graphProjectionIsProcessing(job) {
11501
11890
  return derivationJobState(job, false) === "processing" || derivationJobState(job, false) === "retryable";
11502
11891
  }
@@ -11802,26 +12191,44 @@ function topicDirectoryProjection(snapshot, agentMemoryWeight) {
11802
12191
  const pending = visibleEvents.filter(({ id }) => pendingIds.has(id)).sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
11803
12192
  const versions = new Map(visibleEvents.map((event) => [event.id, memoryTopicEventFingerprint(event)]));
11804
12193
  const frozen = state.bootstrap?.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION ? state.bootstrap : null;
12194
+ const relationKey = (topicId, id, version) => JSON.stringify([topicId ?? null, id, version]);
12195
+ const backfillRelations = Object.entries(state.sectionBackfill ?? {}).flatMap(([topicId, pending2]) => Object.entries(pending2).filter(([id, version]) => versions.get(id) === version).map(([id, version]) => ({ topicId, id, version })));
11805
12196
  const budgetedVersions = { ...frozen?.sourceVersions ?? {}, ...state.rebuildVersions };
11806
12197
  const history = frozen ? Object.entries(budgetedVersions).filter(([id, version]) => versions.get(id) === version) : [];
11807
- const outstanding = new Set(history.filter(([id, version]) => state.projectedVersions[id] !== version).map(([id]) => id));
12198
+ const ordinaryOutstanding = history.filter(([id, version]) => state.projectedVersions[id] !== version);
12199
+ const outstanding = /* @__PURE__ */ new Set([...ordinaryOutstanding.map(([id]) => id), ...backfillRelations.map(({ id }) => id)]);
12200
+ const outstandingWork = /* @__PURE__ */ new Set([
12201
+ ...ordinaryOutstanding.map(([id, version]) => relationKey(void 0, id, version)),
12202
+ ...backfillRelations.map(({ topicId, id, version }) => relationKey(topicId, id, version))
12203
+ ]);
12204
+ const failedWork = /* @__PURE__ */ new Set();
11808
12205
  const failures = state.jobs.flatMap((job) => {
11809
12206
  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 [];
11810
- const ids = job.sourceEventIds.filter((id) => outstanding.has(id) && budgetedVersions[id] === job.sourceVersions[id]);
12207
+ const diagnostics = safeTopicDiagnostics(job.diagnostics);
12208
+ const ids = job.sourceEventIds.filter((id) => {
12209
+ const key = relationKey(job.sectionBackfillTopicId, id, job.sourceVersions[id]);
12210
+ if (!outstandingWork.has(key)) return false;
12211
+ failedWork.add(key);
12212
+ return true;
12213
+ });
11811
12214
  return ids.length > 0 ? [{
11812
12215
  jobId: job.id,
11813
12216
  eventIds: ids,
11814
12217
  attempts: job.attempts,
11815
12218
  // Topic failures store reason codes, never raw model output.
11816
- lastError: ["timeout", "source-changed", "invalid-output", "worker-failed"].includes(job.lastError ?? "") ? job.lastError : "worker-failed",
12219
+ diagnostics,
12220
+ lastError: diagnostics ? `${diagnostics.category}: ${diagnostics.reason}` : ["timeout", "source-changed", "invalid-output", "worker-failed"].includes(job.lastError ?? "") ? job.lastError : "worker-failed",
11817
12221
  updatedAt: job.updatedAt
11818
12222
  }] : [];
11819
12223
  });
12224
+ const total = (/* @__PURE__ */ new Set([...history.map(([id]) => id), ...backfillRelations.map(({ id }) => id)])).size;
12225
+ const failedRelations = backfillRelations.filter(({ topicId, id, version }) => failedWork.has(relationKey(topicId, id, version))).length;
11820
12226
  const bootstrap = frozen ? {
11821
- status: Object.keys(state.rebuildVersions ?? {}).length > 0 ? outstanding.size === new Set(failures.flatMap(({ eventIds }) => eventIds)).size ? "completed" : "running" : frozen.status,
11822
- total: history.length,
11823
- completed: history.length - outstanding.size,
12227
+ status: backfillRelations.length > 0 && outstandingWork.size === failedWork.size ? "failed" : Object.keys(state.rebuildVersions ?? {}).length > 0 || backfillRelations.length > 0 ? outstandingWork.size === failedWork.size ? "completed" : "running" : frozen.status,
12228
+ total,
12229
+ completed: total - outstanding.size,
11824
12230
  failedEvents: new Set(failures.flatMap(({ eventIds }) => eventIds)).size,
12231
+ sectionBackfill: { pendingRelations: backfillRelations.length, failedRelations },
11825
12232
  failures
11826
12233
  } : null;
11827
12234
  const revision = createHash4("sha256").update(JSON.stringify({
@@ -11847,7 +12254,10 @@ function topicDirectoryProjection(snapshot, agentMemoryWeight) {
11847
12254
  description: topic.description,
11848
12255
  createdAt: topic.createdAt,
11849
12256
  updatedAt: topic.updatedAt,
11850
- coverage: { ...topic.coverage },
12257
+ coverage: { ...topic.coverage, unassignedEvents: (() => {
12258
+ const assigned = new Set(memoryTopicSections(topic).flatMap((section) => section.sourceEventIds));
12259
+ return topic.sourceEventIds.filter((id) => !assigned.has(id)).length;
12260
+ })() },
11851
12261
  sections: memoryTopicSectionNavigation(topic),
11852
12262
  overview: topic.overview.map((part) => ({
11853
12263
  kind: part.kind,
@@ -11924,7 +12334,7 @@ async function topicEvents(runtime, url) {
11924
12334
  if (!topic) throw new AdminHttpError(404, "Unknown or unavailable memory topic");
11925
12335
  if (sectionKey === null) ids = topic.sourceEventIds;
11926
12336
  else if (sectionKey === "uncovered") {
11927
- const covered = new Set(topic.overview.flatMap((part) => part.sourceEventIds));
12337
+ const covered = new Set(memoryTopicSections(topic).flatMap((section) => section.sourceEventIds));
11928
12338
  ids = topic.sourceEventIds.filter((id) => !covered.has(id));
11929
12339
  } else {
11930
12340
  const section = memoryTopicSections(topic).find((candidate) => candidate.key === sectionKey);
@@ -12023,17 +12433,41 @@ async function overview(runtime, cachedEntries) {
12023
12433
  sourceEventIds: job.sourceEventIds
12024
12434
  };
12025
12435
  };
12436
+ const topicJobs = (snapshot.memoryTopicState?.jobs ?? []).filter((job) => !job.superseded);
12437
+ const describeTopicJob = (job) => {
12438
+ const diagnostics = safeTopicDiagnostics(job.diagnostics);
12439
+ const lastError = diagnostics ? `${diagnostics.category}: ${diagnostics.reason}` : ["timeout", "source-changed", "invalid-output", "worker-failed"].includes(job.lastError ?? "") ? job.lastError : "worker-failed";
12440
+ return {
12441
+ id: job.id,
12442
+ kind: "topic-projection",
12443
+ state: derivationJobState(job, job.status === "completed"),
12444
+ status: job.status,
12445
+ attempts: job.attempts,
12446
+ nextRetryAt: job.nextRetryAt,
12447
+ updatedAt: job.updatedAt,
12448
+ lastError,
12449
+ lastErrorFull: lastError,
12450
+ diagnostics,
12451
+ eventCount: job.sourceEventIds.length,
12452
+ blockIds: [],
12453
+ threadIds: [],
12454
+ blockDetails: []
12455
+ };
12456
+ };
12457
+ const topicStatus = summarizeDerivationJobs(topicJobs, (job) => job.status === "completed");
12026
12458
  const summaryStatus = summarizeDerivationJobs(snapshot.summaryJobs, (job) => job.status === "succeeded");
12027
12459
  const extractionStatus = summarizeDerivationJobs(snapshot.extractionJobs, (job) => job.status === "succeeded" || job.status === "skipped");
12028
12460
  const graphStatus = summarizeDerivationJobs(snapshot.graphProjectionJobs, (job) => job.status === "completed");
12029
- const failedJobs = summaryStatus.terminalFailed + extractionStatus.terminalFailed + graphStatus.terminalFailed;
12030
- const processingJobs = summaryStatus.processing + extractionStatus.processing + graphStatus.processing;
12461
+ const failedJobs = summaryStatus.terminalFailed + extractionStatus.terminalFailed + graphStatus.terminalFailed + topicStatus.terminalFailed;
12462
+ const processingJobs = summaryStatus.processing + extractionStatus.processing + graphStatus.processing + topicStatus.processing;
12031
12463
  const failedJobDetails = [
12464
+ ...topicJobs.filter((job) => job.status === "failed").map(describeTopicJob),
12032
12465
  ...snapshot.summaryJobs.filter(({ status }) => status === "failed").map((job) => describeBlockJob("block-summary", job)),
12033
12466
  ...snapshot.extractionJobs.filter(({ status }) => status === "failed").map((job) => describeBlockJob("event-extraction", job)),
12034
12467
  ...snapshot.graphProjectionJobs.filter(({ status }) => status === "failed").map(describeGraphJob)
12035
12468
  ];
12036
12469
  const processingJobDetails = [
12470
+ ...topicJobs.filter((job) => ["processing", "retryable"].includes(derivationJobState(job, job.status === "completed"))).map(describeTopicJob),
12037
12471
  ...snapshot.summaryJobs.filter((job) => derivationJobState(job, job.status === "succeeded") === "processing" || derivationJobState(job, job.status === "succeeded") === "retryable").map((job) => describeBlockJob("block-summary", job)),
12038
12472
  ...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)),
12039
12473
  ...snapshot.graphProjectionJobs.filter(graphProjectionIsProcessing).map(describeGraphJob)
@@ -12063,7 +12497,8 @@ async function overview(runtime, cachedEntries) {
12063
12497
  taskStatus: {
12064
12498
  blockSummary: summaryStatus,
12065
12499
  eventExtraction: extractionStatus,
12066
- graphProjection: graphStatus
12500
+ graphProjection: graphStatus,
12501
+ topicProjection: topicStatus
12067
12502
  },
12068
12503
  graphMigration: (() => {
12069
12504
  const projected = new Set(snapshot.graphProjectionJobs.filter(({ status, projectorVersion }) => status === "completed" && projectorVersion === KNOWLEDGE_GRAPH_PROJECTOR_VERSION).flatMap(({ sourceEventIds }) => sourceEventIds)).size;
@@ -12704,13 +13139,13 @@ async function retryJob(runtime, url) {
12704
13139
  const kind = url.searchParams.get("kind")?.trim() ?? "";
12705
13140
  const jobId = url.searchParams.get("jobId")?.trim() ?? "";
12706
13141
  if (!namespace) throw new AdminHttpError(400, "namespace is required");
12707
- if (!["block-summary", "event-extraction", "graph-projection"].includes(kind)) {
12708
- throw new AdminHttpError(400, "kind must be block-summary, event-extraction, or graph-projection");
13142
+ if (!["block-summary", "event-extraction", "graph-projection", "topic-projection"].includes(kind)) {
13143
+ throw new AdminHttpError(400, "kind must be block-summary, event-extraction, graph-projection, or topic-projection");
12709
13144
  }
12710
13145
  if (!jobId) throw new AdminHttpError(400, "jobId is required");
12711
13146
  if (jobId.startsWith("virtual:")) throw new AdminHttpError(409, "Recovered legacy fragments cannot be retried");
12712
13147
  const snapshot = await requiredSnapshot(runtime, namespace);
12713
- 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);
13148
+ 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);
12714
13149
  if (!job) throw new AdminHttpError(404, `Unknown ${kind} job: ${jobId}`);
12715
13150
  if (job.status !== "failed") throw new AdminHttpError(409, `${kind} job is ${job.status}, not failed`);
12716
13151
  try {
@@ -12720,6 +13155,7 @@ async function retryJob(runtime, url) {
12720
13155
  jobId
12721
13156
  );
12722
13157
  } catch (error) {
13158
+ 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");
12723
13159
  throw new AdminHttpError(422, error instanceof Error ? error.message : String(error));
12724
13160
  }
12725
13161
  }
@@ -12813,7 +13249,7 @@ async function dashboard(runtime, url, ifNoneMatch) {
12813
13249
  namespace: selected.namespace,
12814
13250
  revision: selected.revision,
12815
13251
  overview: overviewValue,
12816
- processing: Number(selectedOverview?.processingJobs ?? 0) > 0 || topicDirectoryData.bootstrap !== null && topicDirectoryData.bootstrap.total > 0 && topicDirectoryData.bootstrap.status !== "completed",
13252
+ processing: Number(selectedOverview?.processingJobs ?? 0) > 0 || topicDirectoryData.bootstrap !== null && topicDirectoryData.bootstrap.total > 0 && (topicDirectoryData.bootstrap.status === "pending" || topicDirectoryData.bootstrap.status === "running"),
12817
13253
  data: {
12818
13254
  events: eventResult.items ?? [],
12819
13255
  graph: graphResult,