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/CHANGELOG.md +12 -0
- package/dist/client.js +3 -3
- package/dist/plugin.js +528 -92
- package/dist/plugin.js.map +1 -1
- package/docs/DSH.md +6 -0
- package/docs/DSH.zh-CN.md +6 -0
- package/docs/MEMORY_TOPICS.zh-CN.md +5 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4128
|
-
if (!["history", "decision", "change", "open-question", "scope"].includes(part.kind) || typeof part.text !== "string" || !part.text.trim() || [...part.text].length > 600) throw new
|
|
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
|
|
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
|
|
4134
|
-
if (part.sourceEventIds.some((eventId) => !batchIds.has(eventId)) && !candidates.get(topicId)?.overview.some((old) => digest(old) === digest(part))) throw new
|
|
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
|
-
|
|
4139
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
7954
|
-
if (error instanceof Error && /^StrataGate structured model task timed out after \d+ms$/.test(error.message))
|
|
7955
|
-
|
|
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
|
-
|
|
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: ${
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
...
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
11822
|
-
total
|
|
11823
|
-
completed:
|
|
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.
|
|
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
|
|
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
|
|
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,
|