openvibers/openvibe.codes Commits
Recheck Codes knowledge against task transcripts nightly (#414)
Changes
nnew file mode 100644@@@ -0,0 +1,11 @@++-- phase: expand++-- The nightly dream pass rechecks auto-captured knowledge against its source task's saved transcript.++-- Unsupported rows remain visible to their owner but are omitted from the next agent's prompt.++ALTER TABLE codes_knowledge ADD COLUMN IF NOT EXISTS dream_status text COLLATE "C" NOT NULL DEFAULT 'active'++ CHECK (dream_status IN ('active', 'unsupported'));++++CREATE TABLE IF NOT EXISTS codes_knowledge_dream_state (++ name text COLLATE "C" PRIMARY KEY,++ last_at bigint NOT NULL,++ cursor text COLLATE "C" NOT NULL DEFAULT ''++);@@@ -153,6 +153,7 @@ function present(row, { subject } = {}) {source: row.source_task ? { task: row.source_task, job: row.source_job || null, agent: row.source_agent || null } : null,seen: Number(row.seen) || 1,redacted: !!row.redacted,++ dream_status: row.dream_status || 'active',created_at: iso(row.created_at),updated_at: iso(row.updated_at),last_seen_at: iso(row.last_seen_at),@@@ -269,7 +270,7 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log/** Add one row, or raise the one it repeats. → { row, created, redacted }. A fixed `key` names the row instead* of the text hash: the same key rewritten is the same row (the session summary), never a second one. */-- async function remember({ subject, kind = 'lesson', text, repo = null, paths = [], sourceTask = null, sourceJob = null, sourceAgent = null, teamId = null, pinned = false, visibility = null, key = null } = {}) {++ async function remember({ subject, kind = 'lesson', text, repo = null, paths = [], sourceTask = null, sourceJob = null, sourceAgent = null, teamId = null, pinned = false, visibility = null, key = null, replay = false } = {}) {const k = KINDS.includes(kind) ? kind : 'lesson';const fixed = key == null ? null : String(key).slice(0, 200).trim() || null;const cleaned = scrub(clean(text));@@@ -287,16 +288,20 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, logdedupe_key, seen, pinned, redacted, created_at, updated_at, last_seen_at)VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11, $12, 1, $13, $14, $15, $15, $15)ON CONFLICT (subject, kind, dedupe_key) WHERE dedupe_key IS NOT NULL-- DO UPDATE SET seen = codes_knowledge.seen + 1,-- last_seen_at = EXCLUDED.last_seen_at,-- updated_at = EXCLUDED.updated_at,-- source_task = COALESCE(EXCLUDED.source_task, codes_knowledge.source_task),-- source_job = COALESCE(EXCLUDED.source_job, codes_knowledge.source_job),-- source_agent = COALESCE(EXCLUDED.source_agent, codes_knowledge.source_agent)++ DO UPDATE SET seen = CASE WHEN $16::boolean THEN codes_knowledge.seen++ WHEN EXCLUDED.source_task IS NOT NULL AND codes_knowledge.source_task = EXCLUDED.source_task++ THEN codes_knowledge.seen ELSE codes_knowledge.seen + 1 END,++ last_seen_at = CASE WHEN $16::boolean THEN codes_knowledge.last_seen_at ELSE EXCLUDED.last_seen_at END,++ updated_at = CASE WHEN $16::boolean THEN codes_knowledge.updated_at ELSE EXCLUDED.updated_at END,++ dream_status = CASE WHEN $16::boolean AND codes_knowledge.source_task IS DISTINCT FROM EXCLUDED.source_task++ THEN codes_knowledge.dream_status ELSE 'active' END,++ source_task = CASE WHEN $16::boolean THEN codes_knowledge.source_task ELSE COALESCE(EXCLUDED.source_task, codes_knowledge.source_task) END,++ source_job = CASE WHEN $16::boolean THEN codes_knowledge.source_job ELSE COALESCE(EXCLUDED.source_job, codes_knowledge.source_job) END,++ source_agent = CASE WHEN $16::boolean THEN codes_knowledge.source_agent ELSE COALESCE(EXCLUDED.source_agent, codes_knowledge.source_agent) ENDRETURNING *, (xmax = 0) AS created`,[id, subject, share.team_id, share.visibility, k, cleaned.text, repository, JSON.stringify(allPaths),sourceTask ? String(sourceTask) : null, sourceJob ? String(sourceJob) : null, sourceAgent ? String(sourceAgent) : null,-- fixed || dedupeKey({ kind: k, text: cleaned.text, repo: repository }), !!pinned, cleaned.redacted, at],++ fixed || dedupeKey({ kind: k, text: cleaned.text, repo: repository }), !!pinned, cleaned.redacted, at, !!replay],);return { row, created: !!row.created, redacted: cleaned.redacted };}@@@ -313,6 +318,7 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, logconst repository = repoOf(row.repo);return await db.one(`UPDATE codes_knowledge SET text = $2, kind = $3, dedupe_key = $4, redacted = $5,++ source_task = NULL, source_job = NULL, dream_status = 'active',pinned = COALESCE($6::boolean, pinned), updated_at = $7WHERE id = $1 RETURNING *`,[row.id, cleaned.text, k, dedupeKey({ kind: k, text: cleaned.text, repo: repository }), cleaned.redacted,@@@ -347,7 +353,7 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log* What one finished run taught, captured idempotently: the agent's Learned: and Fact: lines, a failure with its* reason, the command that failed, and the commands the repository's runs use. Never the whole transcript.*/-- async function captureTask(row) {++ async function captureTask(row, { replay = false } = {}) {if (!row || !row.id || !row.subject) return [];const result = row.result && typeof row.result === 'object' ? row.result : {};const logLines = Array.isArray(result.log) ? result.log : [];@@@ -395,10 +401,11 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, logtry {const saved = await remember({subject: row.subject, kind: entry.kind, text: entry.text, repo, paths: pathsIn(entry.text),-- sourceTask: row.id, sourceJob: row.job_id || null, sourceAgent: agent,++ sourceTask: row.id, sourceJob: row.job_id || null, sourceAgent: agent, replay,});out.push(saved.row);} catch (e) {++ if (replay && (!e || e.code !== 'knowledge.empty')) throw e;if (!e || e.code !== 'knowledge.empty') log.warn(`[Codes] knowledge capture for ${row.id}: ${String((e && e.message) || e).slice(0, 160)}`);}}@@@ -426,6 +433,44 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log}}++ /** Recheck a bounded batch of auto-captured rows against the saved task transcript, once per day.++ * Re-capture restores missed observations through remember's exact dedupe key. A row the transcript no longer++ * supports stays visible to its owner but cannot steer a later run. Owner edits become manual rows and are never++ * touched by this pass. The cursor carries a large backlog across nights. */++ async function dream({ force = false, limit = 50 } = {}) {++ const at = now();++ const state = await db.maybe('SELECT last_at, cursor FROM codes_knowledge_dream_state WHERE name = $1', ['nightly']);++ if (!force && state && at - Number(state.last_at) < 86400e3) return { skipped: true, processed: 0, unsupported: 0 };++ const batch = Math.max(1, Math.min(100, Number(limit) || 50));++ const tasks = await db.many(`SELECT t.id, t.subject, t.repo, t.job_id, t.agent, t.state, t.error,++ t.result - 'patch' - 'files' AS result++ FROM run_tasks t++ WHERE t.id > $1 AND t.result IS NOT NULL AND t.state IN ('succeeded', 'failed', 'cancelled', 'expired')++ AND EXISTS (SELECT 1 FROM codes_knowledge k WHERE k.source_task = t.id AND k.subject = t.subject++ AND COALESCE(k.dedupe_key, '') NOT LIKE 'summary:%')++ ORDER BY t.id LIMIT $2`, [state && state.cursor || '', batch]);++ let unsupported = 0;++ for (const task of tasks) {++ const before = await db.many(`SELECT id FROM codes_knowledge++ WHERE source_task = $1 AND subject = $2 AND pinned = false AND visibility = 'private'++ AND updated_at = last_seen_at AND COALESCE(dedupe_key, '') NOT LIKE 'summary:%'`, [task.id, task.subject]);++ const supported = new Set((await captureTask(task, { replay: true })).map((row) => row.id));++ for (const row of before) {++ if (supported.has(row.id)) continue;++ await db.exec(`UPDATE codes_knowledge SET dream_status = 'unsupported'++ WHERE id = $1 AND source_task = $2 AND subject = $3 AND pinned = false AND visibility = 'private'++ AND updated_at = last_seen_at`,++ [row.id, task.id, task.subject]);++ unsupported++;++ }++ }++ const cursor = tasks.length === batch ? tasks[tasks.length - 1].id : '';++ await db.exec(`INSERT INTO codes_knowledge_dream_state (name, last_at, cursor) VALUES ($1, $2, $3)++ ON CONFLICT (name) DO UPDATE SET last_at = EXCLUDED.last_at, cursor = EXCLUDED.cursor`,++ ['nightly', at, cursor]);++ return { skipped: false, processed: tasks.length, unsupported, cursor };++ }++/** Rank the rows a next run should see: pinned first, then this repository, then the words it shares with the brief. */function rank(rows, { repo, prompt } = {}) {const words = new Set(String(prompt || '').toLowerCase().match(/[a-z0-9_./-]{4,}/g) || []);@@@ -457,13 +502,13 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log// run reads it first. It is keyed by repository, so a summary from another repository never leaks in.const key = summaryKey(repo);const previous = rows.find((r) => r.kind === 'fact' && r.dedupe_key === key) || null;-- const rest = previous ? rows.filter((r) => r !== previous) : rows;++ const rest = rows.filter((r) => r !== previous && (r.dream_status || 'active') === 'active');const picked = rank(rest, { repo: repoOf(repo), prompt }).slice(0, Math.max(1, Math.min(8, limit)));return { rows: picked, previous, previousText: previous ? previous.text : '', text: block(picked, { limit: picked.length }) };}return {-- projectsOf, teamIdsOf, list, get, remember, edit, pin, remove, share, unshare, captureTask, captureSummary,++ projectsOf, teamIdsOf, list, get, remember, edit, pin, remove, share, unshare, captureTask, captureSummary, dream,summaryKey, summaryText, recall, rank,block, present: (row, opts) => present(row, opts), scrub, repoOf, learnedLines, factLines, KINDS,};@@@ -120,9 +120,9 @@ ${row.visibility === 'team' ? `<form method="post" action="/knowledge/${esc(row.<details class="c4-more kn-delete"><summary>Delete</summary><div class="c4-more-body"><p class="kn-confirm">Delete this ${row.kind === 'fact' ? 'fact' : 'lesson'}? Runs stop recalling it, and it cannot be brought back.</p><form method="post" action="/knowledge/${esc(row.id)}/delete"><input type="hidden" name="tab" value="${esc(tab)}"><button class="c4-button c4-button-danger" type="submit">Delete it</button></form></div></details></div>` : `<p class="kn-team-note">${row.visibility === 'team' ? 'Shared with your team.' : ''} Only ${mine ? 'you' : 'its owner'} can edit it.</p>`;return `<article class="c4-card kn-row${row.pinned ? ' is-pinned' : ''}">--<div class="kn-row-head">${pill(row.kind === 'fact' ? 'Fact' : 'Lesson')}${row.pinned ? pill('Pinned', 'pinned') : ''}${shared}</div>++<div class="kn-row-head">${pill(row.kind === 'fact' ? 'Fact' : 'Lesson')}${row.pinned ? pill('Pinned', 'pinned') : ''}${row.dream_status === 'unsupported' ? pill('Needs review') : ''}${shared}</div><p class="kn-text">${esc(row.text)}</p>--<p class="kn-meta">${esc(dayOf(row.last_seen_at))}${row.seen > 1 ? ` · seen ${row.seen} times` : ''}${row.redacted ? ' · secrets removed' : ''}</p>++<p class="kn-meta">${esc(dayOf(row.last_seen_at))}${row.seen > 1 ? ` · seen ${row.seen} times` : ''}${row.redacted ? ' · secrets removed' : ''}${row.dream_status === 'unsupported' ? ' · not sent to the next run' : ''}</p>${source}${actions}</article>`;}@@@ -23,6 +23,7 @@ function createLifecycle({ server, ctx, exit, signals }) {() => { if (ctx.publisher) return ctx.publisher.stop(); return undefined; },() => { if (ctx.sessionRetention) ctx.sessionRetention.stop(); },() => { if (ctx.githubLearningJob) ctx.githubLearningJob(); },++ () => { if (ctx.knowledgeDreamJob) ctx.knowledgeDreamJob(); },() => ctx.keys.client.stop(),async () => { if (ctx.telemetryRetention) await ctx.telemetryRetention.stop(); await telemetry.stop(); },() => ctx.store.close(),@@@ -35,6 +36,8 @@ async function start() {const { config } = ctx;// GitHub research is due based: the first boot catches an overdue day, then checks once an hour.ctx.githubLearningJob = every('codes-github-learning', 60 * 60_000, () => ctx.learning.runOnce(), { initialDelayMs: 10_000, jitterMs: 30_000, log: console });++ if (!config.drill) ctx.knowledgeDreamJob = every('codes-knowledge-dream', 60 * 60_000,++ () => ctx.knowledge.dream(), { initialDelayMs: 30_000, jitterMs: 30_000, log: console });// Socket activation (2026-10-31): systemd's .socket unit holds the listener across restarts, so a deploy// queues requests in the kernel instead of answering 502. Without the socket unit, the usual listen.@@@ -266,6 +266,41 @@ function fakeHarness() {assert.strictEqual(other.previousText, '');});++ await check('the nightly dream rechecks captured rows against the saved transcript without repeating a lesson', async () => {++ const task = await startTask('Check the release tests.');++ await end(task.id, 'succeeded', { ok: true, summary: 'Done.\n\nLearned:\n- npm test covers the release.\n- The release uses CI.', agents: ['codex'], log: [] });++ const valid = (await knowledgeOf(ada.subject)).find((r) => r.source_task === task.id && /covers the release/.test(r.text));++ assert.ok(valid);++ const ghost = await t.ctx.knowledge.remember({ subject: ada.subject, kind: 'lesson',++ text: 'The moon is green.', repo: REPO, sourceTask: task.id });++ const pass = await t.ctx.knowledge.dream({ limit: 100 });++ assert.strictEqual(pass.skipped, false);++ assert.ok(pass.processed > 0);++ assert.strictEqual((await db.one('SELECT seen FROM codes_knowledge WHERE id = $1', [valid.id])).seen, valid.seen,++ 'a nightly replay of the same task is not a second sighting');++ const stale = await db.one('SELECT * FROM codes_knowledge WHERE id = $1', [ghost.row.id]);++ assert.strictEqual(stale.dream_status, 'unsupported', 'the saved task does not support this claim');++ const recalled = await t.ctx.knowledge.recall({ subject: ada.subject, repo: REPO, prompt: 'moon green' });++ assert.ok(!recalled.rows.some((r) => r.id === stale.id), 'unsupported text cannot steer another run');++ assert.strictEqual((await t.ctx.knowledge.list({ subject: ada.subject })).find((r) => r.id === stale.id).dream_status,++ 'unsupported', 'the owner can still inspect it');++ assert.match((await t.get('/knowledge', { as: ada })).text, /Needs review.*The moon is green\..*not sent to the next run/s,++ 'the owner sees why the row is no longer recalled');++ const manual = await t.ctx.knowledge.edit({ subject: ada.subject, id: stale.id, text: 'The moon is blue.' });++ assert.strictEqual(manual.source_task, null, 'an owner edit becomes manual knowledge');++ assert.strictEqual(manual.dream_status, 'active');++ assert.strictEqual((await t.ctx.knowledge.dream()).skipped, true, 'the due check runs once a day');++ const next = await startTask('Check the next release.');++ await end(next.id, 'succeeded', { ok: true, summary: 'Done.\n\nLearned:\n- npm test covers the release.', agents: ['codex'], log: [] });++ const repeated = await db.one('SELECT seen, source_task FROM codes_knowledge WHERE id = $1', [valid.id]);++ assert.strictEqual(repeated.source_task, next.id);++ assert.strictEqual(repeated.seen, valid.seen + 1, 'a new task is a second sighting');++ await t.ctx.knowledge.dream({ force: true, limit: 100 });++ const replayed = await db.one('SELECT seen, source_task FROM codes_knowledge WHERE id = $1', [valid.id]);++ assert.strictEqual(replayed.seen, repeated.seen, 'replaying an older task does not inflate sightings');++ assert.strictEqual(replayed.source_task, next.id, 'replay does not move attribution backwards');++ });++await check('the person’s knowledge is exported with the account and erased with it', async () => {const { TABLES } = require('../server/account-data');assert.ok(TABLES.some((x) => x.table === 'codes_knowledge' && x.subject === 'subject'), 'the table is covered');
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+-- phase: expand
+-- The nightly dream pass rechecks auto-captured knowledge against its source task's saved transcript.
+-- Unsupported rows remain visible to their owner but are omitted from the next agent's prompt.
+ALTER TABLE codes_knowledge ADD COLUMN IF NOT EXISTS dream_status text COLLATE "C" NOT NULL DEFAULT 'active'
+ CHECK (dream_status IN ('active', 'unsupported'));
+
+CREATE TABLE IF NOT EXISTS codes_knowledge_dream_state (
+ name text COLLATE "C" PRIMARY KEY,
+ last_at bigint NOT NULL,
+ cursor text COLLATE "C" NOT NULL DEFAULT ''
+);
@@ -153,6 +153,7 @@ function present(row, { subject } = {}) {
@@ -153,6 +153,7 @@ function present(row, { subject } = {}) {
source: row.source_task ? { task: row.source_task, job: row.source_job || null, agent: row.source_agent || null } : null,
source: row.source_task ? { task: row.source_task, job: row.source_job || null, agent: row.source_agent || null } : null,
seen: Number(row.seen) || 1,
seen: Number(row.seen) || 1,
redacted: !!row.redacted,
redacted: !!row.redacted,
+ dream_status: row.dream_status || 'active',
created_at: iso(row.created_at),
created_at: iso(row.created_at),
updated_at: iso(row.updated_at),
updated_at: iso(row.updated_at),
last_seen_at: iso(row.last_seen_at),
last_seen_at: iso(row.last_seen_at),
@@ -269,7 +270,7 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
@@ -269,7 +270,7 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
/** Add one row, or raise the one it repeats. → { row, created, redacted }. A fixed `key` names the row instead
/** Add one row, or raise the one it repeats. → { row, created, redacted }. A fixed `key` names the row instead
* of the text hash: the same key rewritten is the same row (the session summary), never a second one. */
* of the text hash: the same key rewritten is the same row (the session summary), never a second one. */
- async function remember({ subject, kind = 'lesson', text, repo = null, paths = [], sourceTask = null, sourceJob = null, sourceAgent = null, teamId = null, pinned = false, visibility = null, key = null } = {}) {
+ async function remember({ subject, kind = 'lesson', text, repo = null, paths = [], sourceTask = null, sourceJob = null, sourceAgent = null, teamId = null, pinned = false, visibility = null, key = null, replay = false } = {}) {
const k = KINDS.includes(kind) ? kind : 'lesson';
const k = KINDS.includes(kind) ? kind : 'lesson';
const fixed = key == null ? null : String(key).slice(0, 200).trim() || null;
const fixed = key == null ? null : String(key).slice(0, 200).trim() || null;
const cleaned = scrub(clean(text));
const cleaned = scrub(clean(text));
@@ -287,16 +288,20 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
@@ -287,16 +288,20 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
dedupe_key, seen, pinned, redacted, created_at, updated_at, last_seen_at)
dedupe_key, seen, pinned, redacted, created_at, updated_at, last_seen_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11, $12, 1, $13, $14, $15, $15, $15)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11, $12, 1, $13, $14, $15, $15, $15)
ON CONFLICT (subject, kind, dedupe_key) WHERE dedupe_key IS NOT NULL
ON CONFLICT (subject, kind, dedupe_key) WHERE dedupe_key IS NOT NULL
- DO UPDATE SET seen = codes_knowledge.seen + 1,
- last_seen_at = EXCLUDED.last_seen_at,
- updated_at = EXCLUDED.updated_at,
- source_task = COALESCE(EXCLUDED.source_task, codes_knowledge.source_task),
- source_job = COALESCE(EXCLUDED.source_job, codes_knowledge.source_job),
- source_agent = COALESCE(EXCLUDED.source_agent, codes_knowledge.source_agent)
+ DO UPDATE SET seen = CASE WHEN $16::boolean THEN codes_knowledge.seen
+ WHEN EXCLUDED.source_task IS NOT NULL AND codes_knowledge.source_task = EXCLUDED.source_task
+ THEN codes_knowledge.seen ELSE codes_knowledge.seen + 1 END,
+ last_seen_at = CASE WHEN $16::boolean THEN codes_knowledge.last_seen_at ELSE EXCLUDED.last_seen_at END,
+ updated_at = CASE WHEN $16::boolean THEN codes_knowledge.updated_at ELSE EXCLUDED.updated_at END,
+ dream_status = CASE WHEN $16::boolean AND codes_knowledge.source_task IS DISTINCT FROM EXCLUDED.source_task
+ THEN codes_knowledge.dream_status ELSE 'active' END,
+ source_task = CASE WHEN $16::boolean THEN codes_knowledge.source_task ELSE COALESCE(EXCLUDED.source_task, codes_knowledge.source_task) END,
+ source_job = CASE WHEN $16::boolean THEN codes_knowledge.source_job ELSE COALESCE(EXCLUDED.source_job, codes_knowledge.source_job) END,
+ source_agent = CASE WHEN $16::boolean THEN codes_knowledge.source_agent ELSE COALESCE(EXCLUDED.source_agent, codes_knowledge.source_agent) END
RETURNING *, (xmax = 0) AS created`,
RETURNING *, (xmax = 0) AS created`,
[id, subject, share.team_id, share.visibility, k, cleaned.text, repository, JSON.stringify(allPaths),
[id, subject, share.team_id, share.visibility, k, cleaned.text, repository, JSON.stringify(allPaths),
sourceTask ? String(sourceTask) : null, sourceJob ? String(sourceJob) : null, sourceAgent ? String(sourceAgent) : null,
sourceTask ? String(sourceTask) : null, sourceJob ? String(sourceJob) : null, sourceAgent ? String(sourceAgent) : null,
- fixed || dedupeKey({ kind: k, text: cleaned.text, repo: repository }), !!pinned, cleaned.redacted, at],
+ fixed || dedupeKey({ kind: k, text: cleaned.text, repo: repository }), !!pinned, cleaned.redacted, at, !!replay],
);
);
return { row, created: !!row.created, redacted: cleaned.redacted };
return { row, created: !!row.created, redacted: cleaned.redacted };
}
}
@@ -313,6 +318,7 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
@@ -313,6 +318,7 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
const repository = repoOf(row.repo);
const repository = repoOf(row.repo);
return await db.one(
return await db.one(
`UPDATE codes_knowledge SET text = $2, kind = $3, dedupe_key = $4, redacted = $5,
`UPDATE codes_knowledge SET text = $2, kind = $3, dedupe_key = $4, redacted = $5,
+ source_task = NULL, source_job = NULL, dream_status = 'active',
pinned = COALESCE($6::boolean, pinned), updated_at = $7
pinned = COALESCE($6::boolean, pinned), updated_at = $7
WHERE id = $1 RETURNING *`,
WHERE id = $1 RETURNING *`,
[row.id, cleaned.text, k, dedupeKey({ kind: k, text: cleaned.text, repo: repository }), cleaned.redacted,
[row.id, cleaned.text, k, dedupeKey({ kind: k, text: cleaned.text, repo: repository }), cleaned.redacted,
@@ -347,7 +353,7 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
@@ -347,7 +353,7 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
* What one finished run taught, captured idempotently: the agent's Learned: and Fact: lines, a failure with its
* What one finished run taught, captured idempotently: the agent's Learned: and Fact: lines, a failure with its
* reason, the command that failed, and the commands the repository's runs use. Never the whole transcript.
* reason, the command that failed, and the commands the repository's runs use. Never the whole transcript.
*/
*/
- async function captureTask(row) {
+ async function captureTask(row, { replay = false } = {}) {
if (!row || !row.id || !row.subject) return [];
if (!row || !row.id || !row.subject) return [];
const result = row.result && typeof row.result === 'object' ? row.result : {};
const result = row.result && typeof row.result === 'object' ? row.result : {};
const logLines = Array.isArray(result.log) ? result.log : [];
const logLines = Array.isArray(result.log) ? result.log : [];
@@ -395,10 +401,11 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
@@ -395,10 +401,11 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
try {
try {
const saved = await remember({
const saved = await remember({
subject: row.subject, kind: entry.kind, text: entry.text, repo, paths: pathsIn(entry.text),
subject: row.subject, kind: entry.kind, text: entry.text, repo, paths: pathsIn(entry.text),
- sourceTask: row.id, sourceJob: row.job_id || null, sourceAgent: agent,
+ sourceTask: row.id, sourceJob: row.job_id || null, sourceAgent: agent, replay,
});
});
out.push(saved.row);
out.push(saved.row);
} catch (e) {
} catch (e) {
+ if (replay && (!e || e.code !== 'knowledge.empty')) throw e;
if (!e || e.code !== 'knowledge.empty') log.warn(`[Codes] knowledge capture for ${row.id}: ${String((e && e.message) || e).slice(0, 160)}`);
if (!e || e.code !== 'knowledge.empty') log.warn(`[Codes] knowledge capture for ${row.id}: ${String((e && e.message) || e).slice(0, 160)}`);
}
}
}
}
@@ -426,6 +433,44 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
@@ -426,6 +433,44 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
}
}
}
}
+ /** Recheck a bounded batch of auto-captured rows against the saved task transcript, once per day.
+ * Re-capture restores missed observations through remember's exact dedupe key. A row the transcript no longer
+ * supports stays visible to its owner but cannot steer a later run. Owner edits become manual rows and are never
+ * touched by this pass. The cursor carries a large backlog across nights. */
+ async function dream({ force = false, limit = 50 } = {}) {
+ const at = now();
+ const state = await db.maybe('SELECT last_at, cursor FROM codes_knowledge_dream_state WHERE name = $1', ['nightly']);
+ if (!force && state && at - Number(state.last_at) < 86400e3) return { skipped: true, processed: 0, unsupported: 0 };
+ const batch = Math.max(1, Math.min(100, Number(limit) || 50));
+ const tasks = await db.many(`SELECT t.id, t.subject, t.repo, t.job_id, t.agent, t.state, t.error,
+ t.result - 'patch' - 'files' AS result
+ FROM run_tasks t
+ WHERE t.id > $1 AND t.result IS NOT NULL AND t.state IN ('succeeded', 'failed', 'cancelled', 'expired')
+ AND EXISTS (SELECT 1 FROM codes_knowledge k WHERE k.source_task = t.id AND k.subject = t.subject
+ AND COALESCE(k.dedupe_key, '') NOT LIKE 'summary:%')
+ ORDER BY t.id LIMIT $2`, [state && state.cursor || '', batch]);
+ let unsupported = 0;
+ for (const task of tasks) {
+ const before = await db.many(`SELECT id FROM codes_knowledge
+ WHERE source_task = $1 AND subject = $2 AND pinned = false AND visibility = 'private'
+ AND updated_at = last_seen_at AND COALESCE(dedupe_key, '') NOT LIKE 'summary:%'`, [task.id, task.subject]);
+ const supported = new Set((await captureTask(task, { replay: true })).map((row) => row.id));
+ for (const row of before) {
+ if (supported.has(row.id)) continue;
+ await db.exec(`UPDATE codes_knowledge SET dream_status = 'unsupported'
+ WHERE id = $1 AND source_task = $2 AND subject = $3 AND pinned = false AND visibility = 'private'
+ AND updated_at = last_seen_at`,
+ [row.id, task.id, task.subject]);
+ unsupported++;
+ }
+ }
+ const cursor = tasks.length === batch ? tasks[tasks.length - 1].id : '';
+ await db.exec(`INSERT INTO codes_knowledge_dream_state (name, last_at, cursor) VALUES ($1, $2, $3)
+ ON CONFLICT (name) DO UPDATE SET last_at = EXCLUDED.last_at, cursor = EXCLUDED.cursor`,
+ ['nightly', at, cursor]);
+ return { skipped: false, processed: tasks.length, unsupported, cursor };
+ }
+
/** Rank the rows a next run should see: pinned first, then this repository, then the words it shares with the brief. */
/** Rank the rows a next run should see: pinned first, then this repository, then the words it shares with the brief. */
function rank(rows, { repo, prompt } = {}) {
function rank(rows, { repo, prompt } = {}) {
const words = new Set(String(prompt || '').toLowerCase().match(/[a-z0-9_./-]{4,}/g) || []);
const words = new Set(String(prompt || '').toLowerCase().match(/[a-z0-9_./-]{4,}/g) || []);
@@ -457,13 +502,13 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
@@ -457,13 +502,13 @@ function createKnowledge({ store, config = {}, fetchImpl = globalThis.fetch, log
// run reads it first. It is keyed by repository, so a summary from another repository never leaks in.
// run reads it first. It is keyed by repository, so a summary from another repository never leaks in.
const key = summaryKey(repo);
const key = summaryKey(repo);
const previous = rows.find((r) => r.kind === 'fact' && r.dedupe_key === key) || null;
const previous = rows.find((r) => r.kind === 'fact' && r.dedupe_key === key) || null;
- const rest = previous ? rows.filter((r) => r !== previous) : rows;
+ const rest = rows.filter((r) => r !== previous && (r.dream_status || 'active') === 'active');
const picked = rank(rest, { repo: repoOf(repo), prompt }).slice(0, Math.max(1, Math.min(8, limit)));
const picked = rank(rest, { repo: repoOf(repo), prompt }).slice(0, Math.max(1, Math.min(8, limit)));
return { rows: picked, previous, previousText: previous ? previous.text : '', text: block(picked, { limit: picked.length }) };
return { rows: picked, previous, previousText: previous ? previous.text : '', text: block(picked, { limit: picked.length }) };
}
}
return {
return {
- projectsOf, teamIdsOf, list, get, remember, edit, pin, remove, share, unshare, captureTask, captureSummary,
+ projectsOf, teamIdsOf, list, get, remember, edit, pin, remove, share, unshare, captureTask, captureSummary, dream,
summaryKey, summaryText, recall, rank,
summaryKey, summaryText, recall, rank,
block, present: (row, opts) => present(row, opts), scrub, repoOf, learnedLines, factLines, KINDS,
block, present: (row, opts) => present(row, opts), scrub, repoOf, learnedLines, factLines, KINDS,
};
};
@@ -120,9 +120,9 @@ ${row.visibility === 'team' ? `<form method="post" action="/knowledge/${esc(row.
@@ -120,9 +120,9 @@ ${row.visibility === 'team' ? `<form method="post" action="/knowledge/${esc(row.
<details class="c4-more kn-delete"><summary>Delete</summary><div class="c4-more-body"><p class="kn-confirm">Delete this ${row.kind === 'fact' ? 'fact' : 'lesson'}? Runs stop recalling it, and it cannot be brought back.</p><form method="post" action="/knowledge/${esc(row.id)}/delete"><input type="hidden" name="tab" value="${esc(tab)}"><button class="c4-button c4-button-danger" type="submit">Delete it</button></form></div></details>
<details class="c4-more kn-delete"><summary>Delete</summary><div class="c4-more-body"><p class="kn-confirm">Delete this ${row.kind === 'fact' ? 'fact' : 'lesson'}? Runs stop recalling it, and it cannot be brought back.</p><form method="post" action="/knowledge/${esc(row.id)}/delete"><input type="hidden" name="tab" value="${esc(tab)}"><button class="c4-button c4-button-danger" type="submit">Delete it</button></form></div></details>
</div>` : `<p class="kn-team-note">${row.visibility === 'team' ? 'Shared with your team.' : ''} Only ${mine ? 'you' : 'its owner'} can edit it.</p>`;
</div>` : `<p class="kn-team-note">${row.visibility === 'team' ? 'Shared with your team.' : ''} Only ${mine ? 'you' : 'its owner'} can edit it.</p>`;
return `<article class="c4-card kn-row${row.pinned ? ' is-pinned' : ''}">
return `<article class="c4-card kn-row${row.pinned ? ' is-pinned' : ''}">
-<div class="kn-row-head">${pill(row.kind === 'fact' ? 'Fact' : 'Lesson')}${row.pinned ? pill('Pinned', 'pinned') : ''}${shared}</div>
+<div class="kn-row-head">${pill(row.kind === 'fact' ? 'Fact' : 'Lesson')}${row.pinned ? pill('Pinned', 'pinned') : ''}${row.dream_status === 'unsupported' ? pill('Needs review') : ''}${shared}</div>
<p class="kn-text">${esc(row.text)}</p>
<p class="kn-text">${esc(row.text)}</p>
-<p class="kn-meta">${esc(dayOf(row.last_seen_at))}${row.seen > 1 ? ` · seen ${row.seen} times` : ''}${row.redacted ? ' · secrets removed' : ''}</p>
+<p class="kn-meta">${esc(dayOf(row.last_seen_at))}${row.seen > 1 ? ` · seen ${row.seen} times` : ''}${row.redacted ? ' · secrets removed' : ''}${row.dream_status === 'unsupported' ? ' · not sent to the next run' : ''}</p>
${source}${actions}
${source}${actions}
</article>`;
</article>`;
}
}
@@ -23,6 +23,7 @@ function createLifecycle({ server, ctx, exit, signals }) {
@@ -23,6 +23,7 @@ function createLifecycle({ server, ctx, exit, signals }) {
() => { if (ctx.publisher) return ctx.publisher.stop(); return undefined; },
() => { if (ctx.publisher) return ctx.publisher.stop(); return undefined; },
() => { if (ctx.sessionRetention) ctx.sessionRetention.stop(); },
() => { if (ctx.sessionRetention) ctx.sessionRetention.stop(); },
() => { if (ctx.githubLearningJob) ctx.githubLearningJob(); },
() => { if (ctx.githubLearningJob) ctx.githubLearningJob(); },
+ () => { if (ctx.knowledgeDreamJob) ctx.knowledgeDreamJob(); },
() => ctx.keys.client.stop(),
() => ctx.keys.client.stop(),
async () => { if (ctx.telemetryRetention) await ctx.telemetryRetention.stop(); await telemetry.stop(); },
async () => { if (ctx.telemetryRetention) await ctx.telemetryRetention.stop(); await telemetry.stop(); },
() => ctx.store.close(),
() => ctx.store.close(),
@@ -35,6 +36,8 @@ async function start() {
@@ -35,6 +36,8 @@ async function start() {
const { config } = ctx;
const { config } = ctx;
// GitHub research is due based: the first boot catches an overdue day, then checks once an hour.
// GitHub research is due based: the first boot catches an overdue day, then checks once an hour.
ctx.githubLearningJob = every('codes-github-learning', 60 * 60_000, () => ctx.learning.runOnce(), { initialDelayMs: 10_000, jitterMs: 30_000, log: console });
ctx.githubLearningJob = every('codes-github-learning', 60 * 60_000, () => ctx.learning.runOnce(), { initialDelayMs: 10_000, jitterMs: 30_000, log: console });
+ if (!config.drill) ctx.knowledgeDreamJob = every('codes-knowledge-dream', 60 * 60_000,
+ () => ctx.knowledge.dream(), { initialDelayMs: 30_000, jitterMs: 30_000, log: console });
// Socket activation (2026-10-31): systemd's .socket unit holds the listener across restarts, so a deploy
// Socket activation (2026-10-31): systemd's .socket unit holds the listener across restarts, so a deploy
// queues requests in the kernel instead of answering 502. Without the socket unit, the usual listen.
// queues requests in the kernel instead of answering 502. Without the socket unit, the usual listen.
@@ -266,6 +266,41 @@ function fakeHarness() {
@@ -266,6 +266,41 @@ function fakeHarness() {
assert.strictEqual(other.previousText, '');
assert.strictEqual(other.previousText, '');
});
});
+ await check('the nightly dream rechecks captured rows against the saved transcript without repeating a lesson', async () => {
+ const task = await startTask('Check the release tests.');
+ await end(task.id, 'succeeded', { ok: true, summary: 'Done.\n\nLearned:\n- npm test covers the release.\n- The release uses CI.', agents: ['codex'], log: [] });
+ const valid = (await knowledgeOf(ada.subject)).find((r) => r.source_task === task.id && /covers the release/.test(r.text));
+ assert.ok(valid);
+ const ghost = await t.ctx.knowledge.remember({ subject: ada.subject, kind: 'lesson',
+ text: 'The moon is green.', repo: REPO, sourceTask: task.id });
+ const pass = await t.ctx.knowledge.dream({ limit: 100 });
+ assert.strictEqual(pass.skipped, false);
+ assert.ok(pass.processed > 0);
+ assert.strictEqual((await db.one('SELECT seen FROM codes_knowledge WHERE id = $1', [valid.id])).seen, valid.seen,
+ 'a nightly replay of the same task is not a second sighting');
+ const stale = await db.one('SELECT * FROM codes_knowledge WHERE id = $1', [ghost.row.id]);
+ assert.strictEqual(stale.dream_status, 'unsupported', 'the saved task does not support this claim');
+ const recalled = await t.ctx.knowledge.recall({ subject: ada.subject, repo: REPO, prompt: 'moon green' });
+ assert.ok(!recalled.rows.some((r) => r.id === stale.id), 'unsupported text cannot steer another run');
+ assert.strictEqual((await t.ctx.knowledge.list({ subject: ada.subject })).find((r) => r.id === stale.id).dream_status,
+ 'unsupported', 'the owner can still inspect it');
+ assert.match((await t.get('/knowledge', { as: ada })).text, /Needs review.*The moon is green\..*not sent to the next run/s,
+ 'the owner sees why the row is no longer recalled');
+ const manual = await t.ctx.knowledge.edit({ subject: ada.subject, id: stale.id, text: 'The moon is blue.' });
+ assert.strictEqual(manual.source_task, null, 'an owner edit becomes manual knowledge');
+ assert.strictEqual(manual.dream_status, 'active');
+ assert.strictEqual((await t.ctx.knowledge.dream()).skipped, true, 'the due check runs once a day');
+ const next = await startTask('Check the next release.');
+ await end(next.id, 'succeeded', { ok: true, summary: 'Done.\n\nLearned:\n- npm test covers the release.', agents: ['codex'], log: [] });
+ const repeated = await db.one('SELECT seen, source_task FROM codes_knowledge WHERE id = $1', [valid.id]);
+ assert.strictEqual(repeated.source_task, next.id);
+ assert.strictEqual(repeated.seen, valid.seen + 1, 'a new task is a second sighting');
+ await t.ctx.knowledge.dream({ force: true, limit: 100 });
+ const replayed = await db.one('SELECT seen, source_task FROM codes_knowledge WHERE id = $1', [valid.id]);
+ assert.strictEqual(replayed.seen, repeated.seen, 'replaying an older task does not inflate sightings');
+ assert.strictEqual(replayed.source_task, next.id, 'replay does not move attribution backwards');
+ });
+
await check('the person’s knowledge is exported with the account and erased with it', async () => {
await check('the person’s knowledge is exported with the account and erased with it', async () => {
const { TABLES } = require('../server/account-data');
const { TABLES } = require('../server/account-data');
assert.ok(TABLES.some((x) => x.table === 'codes_knowledge' && x.subject === 'subject'), 'the table is covered');
assert.ok(TABLES.some((x) => x.table === 'codes_knowledge' && x.subject === 'subject'), 'the table is covered');