stratagate-dsh 0.3.2 → 0.3.3

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
@@ -3762,8 +3762,25 @@ function memoryTopicSectionTitle(part) {
3762
3762
  return part.title?.trim() || SECTION_TITLES[part.kind] || "\u4E3B\u9898\u6982\u89C8";
3763
3763
  }
3764
3764
  var normalizedLabel = (value) => value.normalize("NFKC").trim().replace(/\s+/g, " ").toLowerCase();
3765
+ function memoryTopicOverviewMatchesSection(part, titles) {
3766
+ const display = normalizedLabel(memoryTopicSectionTitle(part));
3767
+ return titles.some((title) => normalizedLabel(title) === display);
3768
+ }
3769
+ function memoryTopicMembershipSections(topic) {
3770
+ if (topic.sections !== void 0) return structuredClone(topic.sections);
3771
+ const sections = /* @__PURE__ */ new Map();
3772
+ for (const part of topic.overview) {
3773
+ const title = memoryTopicSectionTitle(part);
3774
+ const key = normalizedLabel(title);
3775
+ const section = sections.get(key) ?? { title, sourceEventIds: [] };
3776
+ section.sourceEventIds = unique2([...section.sourceEventIds, ...part.sourceEventIds]);
3777
+ sections.set(key, section);
3778
+ }
3779
+ return [...sections.values()];
3780
+ }
3765
3781
  function sectionTitles(topic) {
3766
3782
  const titles = /* @__PURE__ */ new Map();
3783
+ for (const section of memoryTopicMembershipSections(topic)) titles.set(normalizedLabel(section.title), section.title);
3767
3784
  for (const part of topic.overview) {
3768
3785
  const title = memoryTopicSectionTitle(part);
3769
3786
  if (!titles.has(normalizedLabel(title))) titles.set(normalizedLabel(title), title);
@@ -3788,6 +3805,7 @@ function topicView(topic, events, versions) {
3788
3805
  description: valid ? topic.description : "\u6982\u89C8\u7B49\u5F85\u91CD\u65B0\u6574\u7406\uFF0C\u8BF7\u67E5\u770B\u5173\u8054\u4E8B\u4EF6\u3002",
3789
3806
  sourceEventIds: ids,
3790
3807
  overview: overview2,
3808
+ sections: valid ? memoryTopicMembershipSections(topic) : [],
3791
3809
  createdAt: topic.createdAt,
3792
3810
  updatedAt: topic.updatedAt,
3793
3811
  coverage: { totalEvents: ids.length, summarizedEvents: summarized.size, omittedEvents: ids.length - summarized.size },
@@ -3816,6 +3834,9 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3816
3834
  state = { topics: [], jobs: [], projectedVersions: {} };
3817
3835
  restore(state) {
3818
3836
  this.state = state ? structuredClone(state) : { topics: [], jobs: [], projectedVersions: {} };
3837
+ for (const topic of this.state.topics) if (topic.sections === void 0) {
3838
+ topic.sections = memoryTopicMembershipSections(topic);
3839
+ }
3819
3840
  if (state && state.rebuildVersions === void 0 && state.topics.some((topic) => topic.invalidated)) {
3820
3841
  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
3842
  }
@@ -3851,9 +3872,23 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3851
3872
  bootstrap() {
3852
3873
  return this.state.bootstrap ? structuredClone(this.state.bootstrap) : null;
3853
3874
  }
3875
+ needsInitialization() {
3876
+ return this.state.bootstrap?.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION || this.state.sectionMembershipVersion !== 1;
3877
+ }
3854
3878
  initializeBootstrap(events, now) {
3855
- if (this.state.bootstrap?.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION) return;
3856
3879
  this.synchronize(events, now);
3880
+ if (this.state.sectionMembershipVersion !== 1) {
3881
+ const versions = sourceVersions(new Map(events.filter(visible).map((event) => [event.id, event])));
3882
+ const missing = {};
3883
+ for (const topic of this.state.topics.filter((topic2) => !topic2.invalidated)) {
3884
+ const assigned = new Set(memoryTopicMembershipSections(topic).flatMap((section) => section.sourceEventIds));
3885
+ const ids = topic.sourceEventIds.filter((id) => !assigned.has(id) && versions.has(id) && this.state.projectedVersions[id] === versions.get(id));
3886
+ if (ids.length) missing[topic.id] = Object.fromEntries(ids.map((id) => [id, versions.get(id)]));
3887
+ }
3888
+ this.state.sectionBackfill = missing;
3889
+ this.state.sectionMembershipVersion = 1;
3890
+ }
3891
+ if (this.state.bootstrap?.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION) return;
3857
3892
  this.state.bootstrap = {
3858
3893
  projectorVersion: MEMORY_TOPIC_PROJECTOR_VERSION,
3859
3894
  sourceVersions: Object.fromEntries(sourceVersions(new Map(events.filter(visible).map((event) => [event.id, event])))),
@@ -3869,7 +3904,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3869
3904
  const versions = sourceVersions(sources2);
3870
3905
  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
3906
  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 })) : [];
3907
+ 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
3908
  this.state = { topics: chapters, jobs: [], projectedVersions: {} };
3874
3909
  }
3875
3910
  for (const topic of this.state.topics) {
@@ -3878,6 +3913,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3878
3913
  topic.title = "";
3879
3914
  topic.description = "";
3880
3915
  topic.overview = [];
3916
+ topic.sections = [];
3881
3917
  for (const id of topic.sourceEventIds) if (sources2.has(id)) {
3882
3918
  const version = versions.get(id);
3883
3919
  if (topic.sourceVersions[id] === version && this.state.projectedVersions[id] === version) {
@@ -3908,6 +3944,12 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3908
3944
  for (const [id, version] of Object.entries(this.state.rebuildVersions ?? {})) {
3909
3945
  if (versions.get(id) !== version || this.state.projectedVersions[id] === version) delete this.state.rebuildVersions[id];
3910
3946
  }
3947
+ for (const [topicId, pending] of Object.entries(this.state.sectionBackfill ?? {})) {
3948
+ const topic = this.state.topics.find((topic2) => topic2.id === topicId && !topic2.invalidated);
3949
+ const assigned = new Set(topic ? memoryTopicMembershipSections(topic).flatMap((section) => section.sourceEventIds) : []);
3950
+ for (const [id, version] of Object.entries(pending)) if (!topic || !topic.sourceEventIds.includes(id) || versions.get(id) !== version || assigned.has(id)) delete pending[id];
3951
+ if (!Object.keys(pending).length) delete this.state.sectionBackfill[topicId];
3952
+ }
3911
3953
  const bootstrap = this.state.bootstrap;
3912
3954
  if (bootstrap && bootstrap.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION) {
3913
3955
  const outstanding = Object.entries(bootstrap.sourceVersions).filter(([id, version]) => versions.get(id) === version && this.state.projectedVersions[id] !== version);
@@ -3928,29 +3970,36 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3928
3970
  job.updatedAt = now;
3929
3971
  }
3930
3972
  }
3931
- pendingEvents(events) {
3973
+ pendingWork(events) {
3932
3974
  const sources2 = new Map(events.filter(visible).map((event) => [event.id, event]));
3933
3975
  const versions = sourceVersions(sources2);
3934
3976
  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) => {
3977
+ 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)));
3978
+ return events.filter(visible).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id)).flatMap((event) => {
3936
3979
  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));
3980
+ const work = [];
3981
+ if ((this.state.projectedVersions[event.id] !== version || dirtyIds.has(event.id)) && !blocked(event.id, version)) {
3982
+ work.push({ event });
3983
+ }
3984
+ for (const [topicId, pending] of Object.entries(this.state.sectionBackfill ?? {})) {
3985
+ if (pending[event.id] === version && !blocked(event.id, version, topicId)) work.push({ event, sectionBackfillTopicId: topicId });
3986
+ }
3987
+ return work;
3988
+ });
3940
3989
  }
3941
- isBootstrap(id, version) {
3990
+ isBootstrap(id, version, sectionBackfillTopicId) {
3942
3991
  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;
3992
+ return sectionBackfillTopicId !== void 0 || this.state.rebuildVersions?.[id] === version || !bootstrap || bootstrap.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION || bootstrap.status !== "completed" && bootstrap.sourceVersions[id] === version;
3944
3993
  }
3945
- matchesMode(ids, versions, mode) {
3946
- return mode === "all" || ids.some((id) => this.isBootstrap(id, versions.get(id)) === (mode === "bootstrap"));
3994
+ matchesMode(ids, versions, mode, sectionBackfillTopicId) {
3995
+ return mode === "all" || ids.some((id) => this.isBootstrap(id, versions.get(id), sectionBackfillTopicId) === (mode === "bootstrap"));
3947
3996
  }
3948
3997
  hasPending(events, now, mode = "all") {
3949
3998
  const sources2 = new Map(events.filter(visible).map((event) => [event.id, event]));
3950
3999
  const versions = sourceVersions(sources2);
3951
4000
  const running = this.state.jobs.find((job) => job.status === "running");
3952
4001
  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));
4002
+ return this.pendingWork(events).some(({ event, sectionBackfillTopicId }) => this.matchesMode([event.id], versions, mode, sectionBackfillTopicId)) || this.state.jobs.some((job) => !job.superseded && job.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION && job.attempts < TOPIC_MAX_ATTEMPTS && (job.status === "pending" || job.status === "failed" && job.nextRetryAt !== null && Date.parse(job.nextRetryAt) <= now) && jobInputsCurrent(job, versions) && this.matchesMode(job.sourceEventIds, versions, mode, job.sectionBackfillTopicId));
3954
4003
  }
3955
4004
  claim(events, now, mode = "all") {
3956
4005
  if (this.state.bootstrap?.projectorVersion !== MEMORY_TOPIC_PROJECTOR_VERSION) {
@@ -3961,9 +4010,9 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3961
4010
  if (this.state.jobs.some((job2) => job2.status === "running")) return null;
3962
4011
  const sources2 = new Map(events.filter(visible).map((event) => [event.id, event]));
3963
4012
  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];
4013
+ const eligible = this.state.jobs.filter((job2) => !job2.superseded && job2.attempts < TOPIC_MAX_ATTEMPTS && (job2.status === "pending" || job2.status === "failed" && job2.nextRetryAt !== null && Date.parse(job2.nextRetryAt) <= Date.parse(now)) && jobInputsCurrent(job2, versions) && this.matchesMode(job2.sourceEventIds, versions, mode, job2.sectionBackfillTopicId)).sort((a, b) => Number(this.isBootstrap(a.sourceEventIds[0], versions.get(a.sourceEventIds[0]), a.sectionBackfillTopicId)) - Number(this.isBootstrap(b.sourceEventIds[0], versions.get(b.sourceEventIds[0]), b.sectionBackfillTopicId)))[0];
3965
4014
  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;
4015
+ 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
4016
  if (job?.status === "failed") {
3968
4017
  const previous = job;
3969
4018
  previous.nextRetryAt = null;
@@ -3983,9 +4032,10 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
3983
4032
  this.state.jobs.push(job);
3984
4033
  }
3985
4034
  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)));
4035
+ 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
4036
  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);
4037
+ const backfillTopicId = first?.sectionBackfillTopicId;
4038
+ 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
4039
  const batch2 = [];
3990
4040
  let eventBudget = 16e3;
3991
4041
  for (const event of sameKind) {
@@ -4012,6 +4062,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4012
4062
  createdAt: now,
4013
4063
  updatedAt: now
4014
4064
  };
4065
+ if (backfillTopicId) job.sectionBackfillTopicId = backfillTopicId;
4015
4066
  this.state.jobs.push(job);
4016
4067
  }
4017
4068
  const batch = job.sourceEventIds.map((id) => sources2.get(id));
@@ -4030,21 +4081,30 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4030
4081
  }).join(" "), 4]
4031
4082
  ])).map(({ item, score }) => [item.id, score]));
4032
4083
  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);
4084
+ 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
4085
  const existingTopics = [];
4035
4086
  let remaining = 3e4 - JSON.stringify(batch.map(compactEvent)).length;
4036
4087
  for (const topic of candidates) {
4037
- const overview2 = topic.overview.slice(0, 4);
4088
+ const overview2 = job.sectionBackfillTopicId ? [] : topic.overview.slice(0, 4);
4038
4089
  const exposed = unique2([
4039
4090
  ...overview2.flatMap((part) => part.sourceEventIds),
4040
4091
  ...topic.sourceEventIds.filter((id) => batchIds.has(id))
4041
4092
  ]);
4093
+ const allTitles = sectionTitles(topic);
4094
+ let titleBudget = 4e3;
4095
+ const shownTitles = job.sectionBackfillTopicId ? allTitles.filter((title, index) => {
4096
+ const cost2 = JSON.stringify(title).length;
4097
+ if (index >= 120 || cost2 > titleBudget) return false;
4098
+ titleBudget -= cost2;
4099
+ return true;
4100
+ }) : allTitles;
4042
4101
  const candidate = {
4043
4102
  id: topic.id,
4044
4103
  title: topic.title,
4045
4104
  description: topic.description,
4046
4105
  overview: overview2,
4047
- sectionTitles: sectionTitles(topic),
4106
+ sectionTitles: shownTitles,
4107
+ ...shownTitles.length < allTitles.length ? { sectionTitlesOmitted: allTitles.length - shownTitles.length } : {},
4048
4108
  sourceEventIds: exposed,
4049
4109
  totalSourceEvents: topic.sourceEventIds.length
4050
4110
  };
@@ -4057,7 +4117,16 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4057
4117
  const compact = compactEvent(event);
4058
4118
  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
4119
  }).map((event) => event.id);
4060
- job.context = { jobId: job.id, events: batch.map(compactEvent), existingTopics, truncatedEventIds };
4120
+ if (job.sectionBackfillTopicId && !existingTopics.some((topic) => topic.id === job.sectionBackfillTopicId)) {
4121
+ throw new Error("Section backfill chapter cannot fit the projection input budget.");
4122
+ }
4123
+ job.context = {
4124
+ jobId: job.id,
4125
+ events: batch.map(compactEvent),
4126
+ existingTopics,
4127
+ truncatedEventIds,
4128
+ ...job.sectionBackfillTopicId ? { sectionBackfillTopicId: job.sectionBackfillTopicId } : {}
4129
+ };
4061
4130
  const storedById = new Map(this.state.topics.map((topic) => [topic.id, topic]));
4062
4131
  job.candidateVersions = Object.fromEntries(existingTopics.map((topic) => [topic.id, digest(storedById.get(topic.id))]));
4063
4132
  const dependencyIds = unique2([...job.sourceEventIds, ...existingTopics.flatMap((topic) => [
@@ -4070,7 +4139,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4070
4139
  job.lastError = null;
4071
4140
  job.nextRetryAt = null;
4072
4141
  job.updatedAt = now;
4073
- if (this.state.bootstrap.status === "pending" && this.matchesMode(job.sourceEventIds, versions, "bootstrap")) {
4142
+ if (this.state.bootstrap.status === "pending" && this.matchesMode(job.sourceEventIds, versions, "bootstrap", job.sectionBackfillTopicId)) {
4074
4143
  this.state.bootstrap.status = "running";
4075
4144
  }
4076
4145
  job.leaseUntil = new Date(Date.parse(now) + TOPIC_LEASE_MS).toISOString();
@@ -4087,6 +4156,9 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4087
4156
  throw new Error("Topic projection source version changed; stale result rejected.");
4088
4157
  }
4089
4158
  if (!Array.isArray(result?.topics) || result.topics.length < 1 || result.topics.length > 12) throw new Error("Topic projection must return 1-12 topics.");
4159
+ if (job.sectionBackfillTopicId && (result.topics.length !== 1 || result.topics[0].topicId !== job.sectionBackfillTopicId || result.topics[0].sections === void 0)) {
4160
+ throw new Error("Invalid section backfill: explicit sections must cover the existing chapter.");
4161
+ }
4090
4162
  const candidates = new Map(job.context.existingTopics.map((topic) => [topic.id, topic]));
4091
4163
  const allowed = /* @__PURE__ */ new Set([...job.sourceEventIds, ...job.context.existingTopics.flatMap((topic) => topic.sourceEventIds)]);
4092
4164
  const batchIds = new Set(job.sourceEventIds);
@@ -4133,16 +4205,44 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4133
4205
  if (part.sourceEventIds.length > 12 || part.sourceEventIds.some((eventId) => !ids.includes(eventId))) throw new Error("Overview references must belong to returned topic membership.");
4134
4206
  if (part.sourceEventIds.some((eventId) => !batchIds.has(eventId)) && !candidates.get(topicId)?.overview.some((old) => digest(old) === digest(part))) throw new Error("Old overview evidence must be preserved verbatim; new sections need batch events.");
4135
4207
  }
4208
+ if (proposal.sections !== void 0 && !Array.isArray(proposal.sections)) throw new Error("Invalid topic sections.");
4209
+ const assignments = memoryTopicMembershipSections(proposal);
4210
+ const sectionLabels = /* @__PURE__ */ new Set();
4211
+ for (const section of assignments) {
4212
+ if (!section || typeof section.title !== "string" || !section.title.trim() || [...section.title].length > 80 || sectionLabels.has(label(section.title))) throw new Error("Invalid or duplicate topic section title.");
4213
+ sectionLabels.add(label(section.title));
4214
+ checkIds(section.sourceEventIds, "section");
4215
+ if (section.sourceEventIds.some((eventId) => !ids.includes(eventId))) throw new Error("Section references must belong to returned topic membership.");
4216
+ if (section.sourceEventIds.some((eventId) => !batchIds.has(eventId)) && !memoryTopicMembershipSections(existing ?? { overview: [] }).some((old) => label(old.title) === label(section.title) && section.sourceEventIds.filter((eventId) => !batchIds.has(eventId)).every((eventId) => old.sourceEventIds.includes(eventId)))) throw new Error("Old section membership must be preserved; reassignment needs batch events.");
4217
+ }
4218
+ if (proposal.sections !== void 0 && ids.some((eventId) => batchIds.has(eventId) && !assignments.some((section) => section.sourceEventIds.includes(eventId)))) throw new Error("Topic sections omitted batch events.");
4136
4219
  const membership = unique2([...(existing?.sourceEventIds ?? []).filter((eventId) => sources2.has(eventId)), ...ids]);
4220
+ const assignedIds = new Set(proposal.sections === void 0 ? [] : assignments.flatMap((section) => section.sourceEventIds).filter((eventId) => batchIds.has(eventId)));
4221
+ const sections = existing && !existing.invalidated ? memoryTopicMembershipSections(existing) : [];
4222
+ const memberIds = new Set(membership);
4223
+ for (const section of sections) section.sourceEventIds = section.sourceEventIds.filter((eventId) => memberIds.has(eventId) && !assignedIds.has(eventId));
4224
+ for (const assignment of assignments) {
4225
+ const section = sections.find((current) => label(current.title) === label(assignment.title));
4226
+ if (section) section.sourceEventIds = unique2([...section.sourceEventIds, ...assignment.sourceEventIds]);
4227
+ else sections.push({ title: assignment.title.trim(), sourceEventIds: [...assignment.sourceEventIds] });
4228
+ }
4229
+ const finalSections = sections.filter((section) => section.sourceEventIds.length > 0);
4230
+ const titles = finalSections.map((section) => section.title);
4137
4231
  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));
4232
+ if (proposal.sections !== void 0) for (const part of proposal.overview) {
4233
+ if (!inherited.some((old) => digest(old) === digest(part)) && !memoryTopicOverviewMatchesSection(part, titles)) {
4234
+ throw new Error("New topic overview must match a final topic section.");
4235
+ }
4236
+ }
4237
+ const overview2 = unique2(proposal.overview.filter((part) => memoryTopicOverviewMatchesSection(part, titles)).map((part) => JSON.stringify(part))).map((part) => JSON.parse(part));
4238
+ for (const part of inherited) if (memoryTopicOverviewMatchesSection(part, titles) && !overview2.some((current) => digest(current) === digest(part))) overview2.push(structuredClone(part));
4140
4239
  proposals.push({
4141
4240
  id: topicId,
4142
- title: proposal.title.trim(),
4143
- description: proposal.description.trim(),
4241
+ title: job.sectionBackfillTopicId ? existing.title : proposal.title.trim(),
4242
+ description: job.sectionBackfillTopicId ? existing.description : proposal.description.trim(),
4144
4243
  sourceEventIds: membership,
4145
4244
  overview: overview2,
4245
+ sections: finalSections,
4146
4246
  sourceVersions: Object.fromEntries(membership.map((eventId) => [eventId, versions.get(eventId)])),
4147
4247
  dependencyVersions: { ...!existing?.invalidated ? existing?.dependencyVersions ?? {} : {}, ...job.dependencyVersions },
4148
4248
  projectorVersion: MEMORY_TOPIC_PROJECTOR_VERSION,
@@ -4156,6 +4256,11 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4156
4256
  const index = this.state.topics.findIndex((current) => current.id === topic.id);
4157
4257
  if (index < 0) this.state.topics.push(topic);
4158
4258
  else this.state.topics[index] = topic;
4259
+ const pending = this.state.sectionBackfill?.[topic.id];
4260
+ if (pending) {
4261
+ for (const eventId of memoryTopicMembershipSections(topic).flatMap((section) => section.sourceEventIds)) delete pending[eventId];
4262
+ if (!Object.keys(pending).length) delete this.state.sectionBackfill[topic.id];
4263
+ }
4159
4264
  }
4160
4265
  Object.assign(this.state.projectedVersions, job.sourceVersions);
4161
4266
  for (const [eventId, version] of Object.entries(job.sourceVersions)) {
@@ -4193,7 +4298,7 @@ var MemoryTopicDirectory = class _MemoryTopicDirectory {
4193
4298
  const previous = this.requireJob(id);
4194
4299
  const sources2 = new Map(events.filter(visible).map((event) => [event.id, event]));
4195
4300
  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))) {
4301
+ 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
4302
  throw new Error("Topic retry conflict: failure is no longer current or retryable.");
4198
4303
  }
4199
4304
  const job = {
@@ -4518,7 +4623,7 @@ var StrataGate = class _StrataGate {
4518
4623
  }
4519
4624
  async initializeTopicBootstrap() {
4520
4625
  for (let attempt = 0; attempt < 3; attempt += 1) {
4521
- if (this.topicDirectory.bootstrap()?.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION) return;
4626
+ if (!this.topicDirectory.needsInitialization()) return;
4522
4627
  try {
4523
4628
  await this.commitMutation(() => this.topicDirectory.initializeBootstrap(this.listAllEvents(), toUtc8Iso(this.now())));
4524
4629
  return;
@@ -7411,7 +7516,7 @@ var TOPIC_OVERVIEW = {
7411
7516
  additionalProperties: false,
7412
7517
  properties: {
7413
7518
  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." },
7519
+ 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
7520
  text: { type: "string", description: "Source-grounded scope, historical progress, decision, change, or unresolved question; preserve time, uncertainty, plans, cancellation, and disputes.", required: true },
7416
7521
  sourceEventIds: { ...STRING_ARRAY, required: true }
7417
7522
  }
@@ -7428,6 +7533,14 @@ var TOPIC_PROJECTOR_PARAMETERS = {
7428
7533
  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
7534
  description: { type: "string", description: "Describe which records this topic covers; navigation only, not a claim about current truth.", required: true },
7430
7535
  sourceEventIds: { ...STRING_ARRAY, description: "Exact supplied Event ids assigned to this topic; existing unseen topic members are retained by the store.", required: true },
7536
+ sections: { type: "array", required: true, items: {
7537
+ type: "object",
7538
+ additionalProperties: false,
7539
+ properties: {
7540
+ title: { type: "string", description: "Reuse a lasting section title from sectionTitles when applicable.", required: true },
7541
+ sourceEventIds: { ...STRING_ARRAY, description: "Batch Event ids routed to this section, regardless of overview citations.", required: true }
7542
+ }
7543
+ } },
7431
7544
  overview: { type: "array", items: TOPIC_OVERVIEW, required: true }
7432
7545
  }
7433
7546
  }
@@ -7441,7 +7554,7 @@ events.catalogHints \u4EC5\u662F\u53EF\u9009 routing hint\uFF08\u5206\u7C7B\u5BF
7441
7554
  \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
7555
  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
7556
 
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
7557
+ 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
7558
  \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
7559
 
7447
7560
  \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 +7562,10 @@ Topic \u662F\u957F\u671F\u7684\u5927\u7AE0\u8282\uFF0C\u6309\u9879\u76EE\u3001\u
7449
7562
 
7450
7563
  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
7564
 
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
7565
+ 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
7566
+ \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
7567
+
7568
+ 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
7569
  \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
7570
  var MAX_TOPIC_INPUT_TOKENS = 2e4;
7455
7571
  var EXTERNAL_MEMORY_DECIDER_PARAMETERS = {
@@ -7594,6 +7710,9 @@ function topicProjectionPayload(context, outputTokenBudget) {
7594
7710
  if (new Set(context.existingTopics.map(({ id }) => id)).size !== context.existingTopics.length) {
7595
7711
  throw new Error("Topic projection input contains duplicate candidate topic ids");
7596
7712
  }
7713
+ if (context.sectionBackfillTopicId && !context.existingTopics.some(({ id }) => id === context.sectionBackfillTopicId)) {
7714
+ throw new Error("Topic section backfill input must include its existing chapter");
7715
+ }
7597
7716
  const events = context.events.map((event) => ({
7598
7717
  id: event.id,
7599
7718
  title: event.title,
@@ -7608,6 +7727,7 @@ function topicProjectionPayload(context, outputTokenBudget) {
7608
7727
  for (; ; ) {
7609
7728
  const payload = {
7610
7729
  jobId: context.jobId,
7730
+ ...context.sectionBackfillTopicId ? { sectionBackfillTopicId: context.sectionBackfillTopicId } : {},
7611
7731
  evidenceCompleteness: "bounded-navigation-cards; retrieve Event and original-source evidence before relying on factual content",
7612
7732
  outputTokenBudget,
7613
7733
  events,
@@ -7619,7 +7739,8 @@ function topicProjectionPayload(context, outputTokenBudget) {
7619
7739
  overview: topic.overview,
7620
7740
  ...topic.sectionTitles ? { sectionTitles: topic.sectionTitles } : {},
7621
7741
  sourceEventIds: topic.sourceEventIds,
7622
- totalSourceEvents: topic.totalSourceEvents
7742
+ totalSourceEvents: topic.totalSourceEvents,
7743
+ ...topic.sectionTitlesOmitted ? { sectionTitlesOmitted: topic.sectionTitlesOmitted } : {}
7623
7744
  })),
7624
7745
  candidateTopicsOmitted: context.existingTopics.length - existingTopics.length
7625
7746
  };
@@ -7627,7 +7748,7 @@ function topicProjectionPayload(context, outputTokenBudget) {
7627
7748
 
7628
7749
  ${JSON_RETRY_INSTRUCTION}`, tool: toolSchema("topicProjector"), payload }));
7629
7750
  if (inputTokens <= MAX_TOPIC_INPUT_TOKENS) return { payload, shownContext: { ...context, existingTopics } };
7630
- if (existingTopics.length > 0) existingTopics.pop();
7751
+ if (existingTopics.length > 0 && existingTopics.at(-1).id !== context.sectionBackfillTopicId) existingTopics.pop();
7631
7752
  else throw new Error(`Topic projection input exceeds ${MAX_TOPIC_INPUT_TOKENS} estimated tokens; split the Event batch instead of truncating evidence`);
7632
7753
  }
7633
7754
  }
@@ -7655,14 +7776,18 @@ function parseTopicProjection(value, context) {
7655
7776
  }
7656
7777
  return value2;
7657
7778
  };
7658
- const topics = raw.topics.map((candidate) => {
7779
+ const rawTopics = raw.topics;
7780
+ const topics = rawTopics.map((candidate) => {
7659
7781
  const item = object(candidate);
7660
7782
  const title = boundedText(item.title, "topic.title", 120);
7661
7783
  const label = title.normalize("NFKC").replace(/\s+/g, " ").toLowerCase();
7662
7784
  if (usedLabels.has(label)) fail("duplicate chapter label; combine its batch assignments");
7663
7785
  usedLabels.add(label);
7664
- if (Object.keys(item).some((key) => !["topicId", "title", "description", "sourceEventIds", "overview"].includes(key))) fail("unexpected topic field");
7786
+ if (Object.keys(item).some((key) => !["topicId", "title", "description", "sourceEventIds", "overview", "sections"].includes(key))) fail("unexpected topic field");
7665
7787
  const topicId = item.topicId === void 0 ? void 0 : boundedText(item.topicId, "topicId", 200);
7788
+ if (context.sectionBackfillTopicId && (rawTopics.length !== 1 || topicId !== context.sectionBackfillTopicId)) {
7789
+ fail("section backfill must update only its existing chapter");
7790
+ }
7666
7791
  if (topicId !== void 0 && (!candidates.has(topicId) || usedTopicIds.has(topicId))) fail("unknown or repeated topicId");
7667
7792
  if (topicId !== void 0) usedTopicIds.add(topicId);
7668
7793
  const existing = topicId === void 0 ? void 0 : candidates.get(topicId);
@@ -7670,7 +7795,24 @@ function parseTopicProjection(value, context) {
7670
7795
  const sources2 = sourceIds(item.sourceEventIds, "topic.sourceEventIds", allowedIds);
7671
7796
  if (!sources2.some((id) => eventIds.has(id))) fail("every returned topic must cover a supplied batch Event");
7672
7797
  for (const id of sources2) if (eventIds.has(id)) covered.add(id);
7798
+ if (!Array.isArray(item.sections) || item.sections.length === 0) fail("sections must assign batch Events independently of overview");
7799
+ const sectionLabels = /* @__PURE__ */ new Set();
7800
+ const sections = item.sections.map((candidateSection) => {
7801
+ const section = object(candidateSection);
7802
+ if (Object.keys(section).some((key2) => !["title", "sourceEventIds"].includes(key2))) fail("unexpected section field");
7803
+ const title2 = boundedText(section.title, "section.title", 80);
7804
+ const key = title2.normalize("NFKC").replace(/\s+/g, " ").toLowerCase();
7805
+ if (sectionLabels.has(key)) fail("duplicate section title");
7806
+ sectionLabels.add(key);
7807
+ return { title: title2, sourceEventIds: sourceIds(section.sourceEventIds, "section.sourceEventIds", new Set(sources2.filter((id) => eventIds.has(id)))) };
7808
+ });
7809
+ const assigned = new Set(sections.flatMap((section) => section.sourceEventIds));
7810
+ if (sources2.some((id) => eventIds.has(id) && !assigned.has(id))) fail("every topic batch Event must be assigned to a section");
7673
7811
  if (!Array.isArray(item.overview) || item.overview.length > 8) fail("overview must contain 0-8 source-grounded entries");
7812
+ const overviewTitles = [
7813
+ ...sections.map((section) => section.title),
7814
+ ...existing?.sectionTitles ?? memoryTopicMembershipSections(existing ?? { overview: [] }).map((section) => section.title)
7815
+ ];
7674
7816
  const overview2 = item.overview.map((candidateEntry) => {
7675
7817
  const entry = object(candidateEntry);
7676
7818
  if (Object.keys(entry).some((key) => !["kind", "title", "text", "sourceEventIds"].includes(key))) fail("unexpected overview field");
@@ -7679,16 +7821,20 @@ function parseTopicProjection(value, context) {
7679
7821
  const paragraph = boundedText(entry.text, "overview.text", 600);
7680
7822
  const paragraphSources = sourceIds(entry.sourceEventIds, "overview.sourceEventIds", new Set(sources2), 12);
7681
7823
  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)))) {
7824
+ 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)));
7825
+ if (paragraphSources.some((id) => !eventIds.has(id)) && !preserved) {
7683
7826
  fail("old overview evidence may only be preserved verbatim; rewritten entries must cite batch Events only");
7684
7827
  }
7685
- return { kind: entry.kind, ...sectionTitle === void 0 ? {} : { title: sectionTitle }, text: paragraph, sourceEventIds: paragraphSources };
7828
+ const part = { kind: entry.kind, ...sectionTitle === void 0 ? {} : { title: sectionTitle }, text: paragraph, sourceEventIds: paragraphSources };
7829
+ if (!preserved && !memoryTopicOverviewMatchesSection(part, overviewTitles)) fail("new overview must match a declared or inherited section");
7830
+ return part;
7686
7831
  });
7687
7832
  return {
7688
7833
  ...topicId === void 0 ? {} : { topicId },
7689
7834
  title,
7690
7835
  description: boundedText(item.description, "topic.description", 400),
7691
7836
  sourceEventIds: sources2,
7837
+ sections,
7692
7838
  overview: overview2
7693
7839
  };
7694
7840
  });
@@ -8562,6 +8708,12 @@ function memoryTopicSections(topic) {
8562
8708
  if (!memberPositions.has(id)) memberPositions.set(id, index);
8563
8709
  });
8564
8710
  const sections = /* @__PURE__ */ new Map();
8711
+ for (const membership of memoryTopicMembershipSections(topic)) {
8712
+ const key = memoryTopicSectionKey(membership.title);
8713
+ const section = sections.get(key) ?? { key, title: membership.title, paragraphs: [], sourceEventIds: [] };
8714
+ section.sourceEventIds.push(...membership.sourceEventIds.filter((id) => memberPositions.has(id)));
8715
+ sections.set(key, section);
8716
+ }
8565
8717
  for (const part of topic.overview) {
8566
8718
  const title = memoryTopicSectionTitle(part);
8567
8719
  const identity = memoryTopicSectionKey(title);
@@ -8571,7 +8723,6 @@ function memoryTopicSections(topic) {
8571
8723
  sections.set(identity, section);
8572
8724
  }
8573
8725
  section.paragraphs.push(part);
8574
- for (const id of part.sourceEventIds) if (memberPositions.has(id)) section.sourceEventIds.push(id);
8575
8726
  }
8576
8727
  return [...sections.values()].map((section) => ({
8577
8728
  section: {
@@ -8690,16 +8841,26 @@ ${[...selected.map(({ line: line2 }) => line2), line].join("\n")}`) <= MEMORY_DI
8690
8841
  ${selected.map(({ line }) => line).join("\n")}`.trim();
8691
8842
  }
8692
8843
  function boundedTopic(topic, envelope = {}) {
8844
+ const { sections: _sections, ...summaryTopic } = topic;
8845
+ const sectionIndex = memoryTopicSections(topic).map(({ title, sourceEventIds: sourceEventIds2 }) => ({ title, sourceEventCount: sourceEventIds2.length }));
8846
+ const sections = [];
8693
8847
  const overview2 = [];
8694
8848
  const sourceEventIds = topic.sourceEventIds.slice(0, 12);
8695
- const result = (parts) => ({
8696
- ...topic,
8849
+ const result = (parts, selectedSections = sections) => ({
8850
+ ...summaryTopic,
8697
8851
  sourceEventIds,
8698
8852
  overview: parts,
8853
+ sections: selectedSections,
8854
+ totalSections: sectionIndex.length,
8855
+ omittedSections: sectionIndex.length - selectedSections.length,
8699
8856
  totalSourceEvents: topic.sourceEventIds.length,
8700
8857
  omittedSourceEvents: topic.sourceEventIds.length - sourceEventIds.length,
8701
8858
  omittedOverviewParagraphs: topic.overview.length - parts.length
8702
8859
  });
8860
+ for (const section of sectionIndex) {
8861
+ if (estimateTokens(JSON.stringify({ ...envelope, topic: result(overview2, [...sections, section]) })) > TOPIC_OVERVIEW_TOKEN_BUDGET) break;
8862
+ sections.push(section);
8863
+ }
8703
8864
  for (const paragraph of topic.overview) {
8704
8865
  if (estimateTokens(JSON.stringify({ ...envelope, topic: result([...overview2, paragraph]) })) <= TOPIC_OVERVIEW_TOKEN_BUDGET) overview2.push(paragraph);
8705
8866
  }
@@ -11496,7 +11657,7 @@ function currentPluginVersion() {
11496
11657
  return "unknown";
11497
11658
  }
11498
11659
  var STRATAGATE_DSH_VERSION = currentPluginVersion();
11499
- var TOPIC_DIRECTORY_VIEW_VERSION = 4;
11660
+ var TOPIC_DIRECTORY_VIEW_VERSION = 5;
11500
11661
  function graphProjectionIsProcessing(job) {
11501
11662
  return derivationJobState(job, false) === "processing" || derivationJobState(job, false) === "retryable";
11502
11663
  }
@@ -11802,12 +11963,25 @@ function topicDirectoryProjection(snapshot, agentMemoryWeight) {
11802
11963
  const pending = visibleEvents.filter(({ id }) => pendingIds.has(id)).sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
11803
11964
  const versions = new Map(visibleEvents.map((event) => [event.id, memoryTopicEventFingerprint(event)]));
11804
11965
  const frozen = state.bootstrap?.projectorVersion === MEMORY_TOPIC_PROJECTOR_VERSION ? state.bootstrap : null;
11966
+ const relationKey = (topicId, id, version) => JSON.stringify([topicId ?? null, id, version]);
11967
+ 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
11968
  const budgetedVersions = { ...frozen?.sourceVersions ?? {}, ...state.rebuildVersions };
11806
11969
  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));
11970
+ const ordinaryOutstanding = history.filter(([id, version]) => state.projectedVersions[id] !== version);
11971
+ const outstanding = /* @__PURE__ */ new Set([...ordinaryOutstanding.map(([id]) => id), ...backfillRelations.map(({ id }) => id)]);
11972
+ const outstandingWork = /* @__PURE__ */ new Set([
11973
+ ...ordinaryOutstanding.map(([id, version]) => relationKey(void 0, id, version)),
11974
+ ...backfillRelations.map(({ topicId, id, version }) => relationKey(topicId, id, version))
11975
+ ]);
11976
+ const failedWork = /* @__PURE__ */ new Set();
11808
11977
  const failures = state.jobs.flatMap((job) => {
11809
11978
  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]);
11979
+ const ids = job.sourceEventIds.filter((id) => {
11980
+ const key = relationKey(job.sectionBackfillTopicId, id, job.sourceVersions[id]);
11981
+ if (!outstandingWork.has(key)) return false;
11982
+ failedWork.add(key);
11983
+ return true;
11984
+ });
11811
11985
  return ids.length > 0 ? [{
11812
11986
  jobId: job.id,
11813
11987
  eventIds: ids,
@@ -11817,11 +11991,14 @@ function topicDirectoryProjection(snapshot, agentMemoryWeight) {
11817
11991
  updatedAt: job.updatedAt
11818
11992
  }] : [];
11819
11993
  });
11994
+ const total = (/* @__PURE__ */ new Set([...history.map(([id]) => id), ...backfillRelations.map(({ id }) => id)])).size;
11995
+ const failedRelations = backfillRelations.filter(({ topicId, id, version }) => failedWork.has(relationKey(topicId, id, version))).length;
11820
11996
  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,
11997
+ 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,
11998
+ total,
11999
+ completed: total - outstanding.size,
11824
12000
  failedEvents: new Set(failures.flatMap(({ eventIds }) => eventIds)).size,
12001
+ sectionBackfill: { pendingRelations: backfillRelations.length, failedRelations },
11825
12002
  failures
11826
12003
  } : null;
11827
12004
  const revision = createHash4("sha256").update(JSON.stringify({
@@ -11847,7 +12024,10 @@ function topicDirectoryProjection(snapshot, agentMemoryWeight) {
11847
12024
  description: topic.description,
11848
12025
  createdAt: topic.createdAt,
11849
12026
  updatedAt: topic.updatedAt,
11850
- coverage: { ...topic.coverage },
12027
+ coverage: { ...topic.coverage, unassignedEvents: (() => {
12028
+ const assigned = new Set(memoryTopicSections(topic).flatMap((section) => section.sourceEventIds));
12029
+ return topic.sourceEventIds.filter((id) => !assigned.has(id)).length;
12030
+ })() },
11851
12031
  sections: memoryTopicSectionNavigation(topic),
11852
12032
  overview: topic.overview.map((part) => ({
11853
12033
  kind: part.kind,
@@ -11924,7 +12104,7 @@ async function topicEvents(runtime, url) {
11924
12104
  if (!topic) throw new AdminHttpError(404, "Unknown or unavailable memory topic");
11925
12105
  if (sectionKey === null) ids = topic.sourceEventIds;
11926
12106
  else if (sectionKey === "uncovered") {
11927
- const covered = new Set(topic.overview.flatMap((part) => part.sourceEventIds));
12107
+ const covered = new Set(memoryTopicSections(topic).flatMap((section) => section.sourceEventIds));
11928
12108
  ids = topic.sourceEventIds.filter((id) => !covered.has(id));
11929
12109
  } else {
11930
12110
  const section = memoryTopicSections(topic).find((candidate) => candidate.key === sectionKey);
@@ -12813,7 +12993,7 @@ async function dashboard(runtime, url, ifNoneMatch) {
12813
12993
  namespace: selected.namespace,
12814
12994
  revision: selected.revision,
12815
12995
  overview: overviewValue,
12816
- processing: Number(selectedOverview?.processingJobs ?? 0) > 0 || topicDirectoryData.bootstrap !== null && topicDirectoryData.bootstrap.total > 0 && topicDirectoryData.bootstrap.status !== "completed",
12996
+ processing: Number(selectedOverview?.processingJobs ?? 0) > 0 || topicDirectoryData.bootstrap !== null && topicDirectoryData.bootstrap.total > 0 && (topicDirectoryData.bootstrap.status === "pending" || topicDirectoryData.bootstrap.status === "running"),
12817
12997
  data: {
12818
12998
  events: eventResult.items ?? [],
12819
12999
  graph: graphResult,