From f3142fb8ece6e4a186f1589fd49864984f87fb0a Mon Sep 17 00:00:00 2001 From: Vikas Singhal Date: Fri, 31 Jul 2026 16:05:35 +0530 Subject: [PATCH] fix(dreaming): version the topic map so an extractor fix reaches existing workspaces (v0.281.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.281.1 fixed WHAT gets extracted as a topic, but `topics` is a cumulative map: counts compound across passes and decay only on a 21-day half-life. So on any workspace already running, the words the OLD extractor admitted keep their large counts and keep headlining the guidance line that rides in every agent's prompt. Live instapods: 300 stored topics led by drafts(61), sweep(59), automated(58) — all artefacts of one shouted prompt header, none of which the fix could remove. DreamState now carries `topicsVersion`. A state written by an older extractor has its map cleared and rebuilt from the current corpus. The reset runs BEFORE the no-activity early return, so a quiet workspace stops serving the stale line today rather than at its next busy pass. Audited `learning.topics.reset`. Verified against a copy of the live instapods state: 300 topics (version undefined) → 0 topics (version 2), guidance topic line drops until the map rebuilds. Every tenant self-heals on its next pass — no hand-edited databases. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/edge/dreaming.ts | 28 +++++++++++++++++++++++++++- 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index affa3cb..9ec60ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ new version heading in the same commit. ## [Unreleased] +## [0.281.2] — 2026-07-31 +### Fixed +- **A fixed topic extractor now actually reaches workspaces that have already been running.** `topics` is a + CUMULATIVE map — counts compound across passes and decay only on a 21-day half-life — so v0.281.1's + extractor fix changed nothing for an existing tenant: the words the old extractor admitted kept their + (large) counts and kept headlining the guidance line in every agent's prompt. Live instapods held 300 + topics led by `drafts(61)`, `sweep(59)`, `automated(58)`, all artefacts of one shouted prompt header. + `DreamState` now carries a `topicsVersion`; a state written by an older extractor has its map cleared and + rebuilt from the current corpus. The reset runs **before** the no-activity early return, so a quiet + workspace stops serving the stale line immediately rather than at its next busy pass. Audited + `learning.topics.reset`. Every tenant self-heals on its next pass — no hand-edited databases. + ## [0.281.1] — 2026-07-31 ### Fixed - **Self-learning topic extraction: the case signal is now read the way a writer meant it.** v0.280.0 diff --git a/package-lock.json b/package-lock.json index 52ff8c4..81c973a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agent-os", - "version": "0.281.1", + "version": "0.281.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agent-os", - "version": "0.281.1", + "version": "0.281.2", "license": "MIT", "bin": { "agent-os": "bin/agent-os" diff --git a/package.json b/package.json index 1f72225..3fb2120 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-os", - "version": "0.281.1", + "version": "0.281.2", "description": "A generic, governed operating system for running autonomous agents safely across brands. Ships with a local web console.", "license": "MIT", "type": "commonjs", diff --git a/src/edge/dreaming.ts b/src/edge/dreaming.ts index d56aaef..36bc254 100644 --- a/src/edge/dreaming.ts +++ b/src/edge/dreaming.ts @@ -40,6 +40,9 @@ interface DreamState { * fall into the gap between a pass's `until` and the next `since` and be silently skipped. Absent on * states written before this field existed → we fall back to the old marker (see `dream`). */ watermark?: number; + /** Which topic EXTRACTOR produced `topics`. Bumped when the extractor changes meaning; a state carrying + * an older version has its topic map reset on the next pass (see TOPICS_VERSION). */ + topicsVersion?: number; } interface EpisodeRow { content: string; created_at: number } @@ -62,6 +65,17 @@ const TOPIC_CAP = 300; // hard cap on stored topic keys // this many distinct episodes (topicCounts counts once per episode, so `count` = distinct-episode hits). // Stops a one-off word — or a handful of near-identical test runs — from headlining the guidance line. const MIN_TOPIC_COUNT = 3; +/** + * Version of the topic extractor. `topics` is a CUMULATIVE map — counts compound across passes and decay + * only on a 21-day half-life — so fixing the extractor does nothing for a workspace that has already been + * running: the words the old one admitted keep their (large) counts and keep headlining the guidance line + * for weeks. Live instapods held 300 topics led by `drafts(61)`, `sweep(59)`, `automated(58)` — all + * artefacts of one shouted prompt header, all of which v0.281.1 stopped extracting but none of which it + * removed. Bump this whenever the extractor's meaning changes; a state carrying an older version has its + * map cleared and rebuilt from the current corpus, so every tenant self-heals on its next pass instead of + * needing a hand-edited DB. + */ +const TOPICS_VERSION = 2; const STOP = new Set(['task', 'outcome', 'session', 'after', 'then', 'with', 'this', 'that', 'from', 'into', 'your', 'their', 'about', 'over', 'when', 'while', 'should', 'would', 'could', 'have', 'been', 'were', 'them', 'they', 'will', 'just', 'also', 'using', 'used', 'ran', 'run', 'done', 'made', 'make', 'need', 'needs', 'some', 'more', 'than', 'only', 'each', 'both', 'unknown', 'none', // Procedural / plumbing words — they describe HOW an agent worked, not WHAT the fleet works on, so they // drown the real topics ("slack, check, report, completed, summary" is a useless "frequently works on"). @@ -136,6 +150,17 @@ export class DreamingEngine { // H2: window on a durable **data watermark** — the newest activity ts we've already consumed — // NOT the run clock. Migration-safe: states written before `watermark` existed fall back to the old // `learning.dreamed` marker, then to a 7-day cold start. + // Retire a topic map built by an older extractor BEFORE anything reads it — including the + // no-activity early return below, so a quiet workspace still stops serving the stale line today + // rather than at its next busy pass. + if (prior && prior.topicsVersion !== TOPICS_VERSION) { + const dropped = Object.keys(prior.topics ?? {}).length; + prior.topics = {}; + prior.topicsVersion = TOPICS_VERSION; + this.os.settings.setDreamingState(prior as unknown as Record, by); + this.os.settings.setLearnedGuidance(deriveGuidance(prior), by); + this.os.audit.append({ ts: Date.now(), runId: '-', tenant: this.os.tenant, principal: by, type: 'learning.topics.reset', data: { dropped, version: TOPICS_VERSION } }); + } const lastMark = db.prepare("SELECT MAX(ts) AS t FROM audit_events WHERE type = 'learning.dreamed'").get<{ t: number | null }>(); const since = prior?.watermark ?? lastMark?.t ?? until - 7 * 24 * 3_600_000; const window = { since, until }; @@ -195,7 +220,8 @@ export class DreamingEngine { for (const f of frictionRows) if (f.ts > watermark) watermark = f.ts; // ── fold into the cumulative state ── - const state = prior ?? { firstPass: until, passes: 0, totals: zeroTally(), topics: {}, recent: [], watermark: since }; + const state = prior ?? { firstPass: until, passes: 0, totals: zeroTally(), topics: {}, recent: [], watermark: since, topicsVersion: TOPICS_VERSION }; + state.topicsVersion = TOPICS_VERSION; state.passes += 1; state.watermark = watermark; for (const k of Object.keys(win) as (keyof Tally)[]) state.totals[k] += win[k];