mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-16 09:52:22 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8cba9a76b |
@@ -1743,15 +1743,7 @@ async function extractStaleFromDB(
|
||||
// `page.updated_at.toISOString()` — the JS Date is ms-truncated, so the
|
||||
// µs-precision DB updated_at stayed strictly greater and the page never
|
||||
// cleared on Postgres. Stamping the exact value makes them equal.
|
||||
//
|
||||
// Version-arm floor: a page last edited BEFORE LINK_EXTRACTOR_VERSION_TS
|
||||
// would otherwise be stamped below the version watermark and stay
|
||||
// permanently stale (`links_extracted_at < versionTs` re-fires every run).
|
||||
// Stamp max(updated_at, versionTs) — versionTs is always a past release
|
||||
// date, so a concurrent edit's now() still exceeds the stamp and D4 holds.
|
||||
// Tie at ms precision picks updated_at_iso (its µs ≥ versionTs's .000000).
|
||||
const stampTs = new Date(page.updated_at_iso) >= new Date(versionTs) ? page.updated_at_iso : versionTs;
|
||||
processedRefs.push({ slug: page.slug, source_id: page.source_id, extractedAt: stampTs });
|
||||
processedRefs.push({ slug: page.slug, source_id: page.source_id, extractedAt: page.updated_at_iso });
|
||||
}
|
||||
|
||||
// Flush NON-swallowing (CDX-4): a throw here propagates out of the sweep so
|
||||
|
||||
@@ -453,7 +453,14 @@ function resolveActivity(
|
||||
* every `assemble()` call. 1 MB is generous for a human-edited task list. */
|
||||
const MAX_TASKS_MD_BYTES = 1_000_000;
|
||||
|
||||
/** Extract open tasks from ops/tasks.md "## Today" section. */
|
||||
/** Extract open tasks from ops/tasks.md Today section.
|
||||
*
|
||||
* The daily-task-manager skill's documented Output Format uses priority
|
||||
* headings (`## P1 — Today`) with plain `- [ ] task` lines; older fixtures
|
||||
* used a bare `## Today` heading with bold task names. Accept both so the
|
||||
* live-context reader matches the documented writer contract instead of
|
||||
* silently surfacing no tasks (#2186).
|
||||
*/
|
||||
function resolveTodayTasks(workspaceDir: string): string[] {
|
||||
try {
|
||||
const path = join(workspaceDir, 'ops', 'tasks.md');
|
||||
@@ -461,14 +468,18 @@ function resolveTodayTasks(workspaceDir: string): string[] {
|
||||
// statSync throws if the file doesn't exist; that lands in the outer catch.
|
||||
if (statSync(path).size > MAX_TASKS_MD_BYTES) return [];
|
||||
const raw = readFileSync(path, 'utf8');
|
||||
const todayMatch = raw.match(/## Today[\s\S]*?(?=\n## |$)/);
|
||||
const todayMatch = raw.match(/^##\s+(?:P\d\s*[—–-]\s*)?Today\b[\s\S]*?(?=\n##\s|$(?![\s\S]))/m);
|
||||
if (!todayMatch) return [];
|
||||
|
||||
const lines = todayMatch[0].split('\n');
|
||||
const open: string[] = [];
|
||||
for (const line of lines) {
|
||||
// Match unchecked task lines: - [ ] **task name** ...
|
||||
const m = line.match(/^\s*-\s*\[ \]\s*\*\*(.+?)\*\*/);
|
||||
// Match unchecked task lines. Legacy bold form first (extracts just
|
||||
// the task name, dropping trailing metadata), then the documented
|
||||
// plain form (whole line body is the task).
|
||||
const m =
|
||||
line.match(/^\s*-\s*\[ \]\s*\*\*(.+?)\*\*/) ??
|
||||
line.match(/^\s*-\s*\[ \]\s*(.+?)\s*$/);
|
||||
if (m) open.push(sanitizeForPrompt(m[1].trim()));
|
||||
}
|
||||
return open.slice(0, 5); // cap at 5 to keep prompt lean
|
||||
|
||||
@@ -28,7 +28,7 @@ import { ensureWellFormed } from './text-safe.ts';
|
||||
* OR updated_at > links_extracted_at`. It is an ISO-8601 string (NOT a number) —
|
||||
* the column is TIMESTAMPTZ and the predicate binds it as `::timestamptz`.
|
||||
*/
|
||||
export const LINK_EXTRACTOR_VERSION_TS = '2026-07-21T00:00:00Z';
|
||||
export const LINK_EXTRACTOR_VERSION_TS = '2026-05-31T00:00:00Z';
|
||||
|
||||
// ─── Entity references ──────────────────────────────────────────
|
||||
|
||||
@@ -80,10 +80,10 @@ export type LinkResolutionType = 'qualified' | 'unqualified';
|
||||
* Directory prefix whitelist. These are the top-level slug dirs the extractor
|
||||
* recognizes as entity references. Upstream canonical + our extensions:
|
||||
* - Gbrain canonical: people, companies, meetings, concepts, deal, civic, project, source, media, yc, projects
|
||||
* - Our domain extensions: tech, finance, personal, openclaw, ops (domain-organized wikis)
|
||||
* - Our domain extensions: tech, finance, personal, openclaw (domain-organized wikis)
|
||||
* - Our entity prefix: entities (we kept some legacy entities/projects/ pages)
|
||||
*/
|
||||
const DIR_PATTERN = '(?:people|companies|meetings|concepts|deal|civic|project|projects|source|media|yc|tech|finance|personal|openclaw|entities|ops)';
|
||||
const DIR_PATTERN = '(?:people|companies|meetings|concepts|deal|civic|project|projects|source|media|yc|tech|finance|personal|openclaw|entities)';
|
||||
|
||||
/**
|
||||
* Match `[Name](path)` markdown links pointing to entity directories.
|
||||
@@ -865,16 +865,7 @@ export function queryBasenameIndex(idx: Map<string, string[]>, name: string): st
|
||||
if (!name || typeof name !== 'string') return [];
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return [];
|
||||
let hit = idx.get(trimmed) ?? idx.get(trimmed.toLowerCase()) ?? idx.get(normalizeBasename(trimmed));
|
||||
// Issue #2576 bug 2: path-style refs (`runbooks/2026-05-01-x`) from dirs
|
||||
// outside DIR_PATTERN reach here, but normalizeBasename strips slashes
|
||||
// into a garbage key (`runbooks2026-05-01-x`) that can never hit the
|
||||
// tail-keyed index. Fall back to the path tail so qualified refs resolve
|
||||
// by basename like everything else.
|
||||
if (!hit && trimmed.includes('/')) {
|
||||
const tail = trimmed.slice(trimmed.lastIndexOf('/') + 1).trim();
|
||||
if (tail) hit = idx.get(tail) ?? idx.get(tail.toLowerCase()) ?? idx.get(normalizeBasename(tail));
|
||||
}
|
||||
const hit = idx.get(trimmed) ?? idx.get(trimmed.toLowerCase()) ?? idx.get(normalizeBasename(trimmed));
|
||||
return hit ? [...hit].sort(basenameSort) : [];
|
||||
}
|
||||
|
||||
|
||||
@@ -322,6 +322,28 @@ describe('gbrain-context engine', () => {
|
||||
expect(result.systemPromptAddition).not.toContain('Something later');
|
||||
});
|
||||
|
||||
it('injects documented "## P1 — Today" plain tasks from ops/tasks.md (#2186)', async () => {
|
||||
tmpDir = makeWorkspace({
|
||||
heartbeat: { garryAwake: true },
|
||||
tasks: `# Tasks\n\n## P0 — Urgent\n- [ ] **Escalate outage**\n\n## P1 — Today\n- [ ] Call Alice about launch plan\n- [ ] **Review Bob contract** — due Friday\n- [x] Completed item\n\n## P2 — This Week\n- [ ] Should not surface`,
|
||||
});
|
||||
const engine = createGBrainContextEngine({ workspaceDir: tmpDir });
|
||||
|
||||
const result = await engine.assemble({
|
||||
sessionId: 'test-session',
|
||||
messages: [],
|
||||
});
|
||||
|
||||
expect(result.systemPromptAddition).toContain('Open tasks');
|
||||
expect(result.systemPromptAddition).toContain('Call Alice about launch plan');
|
||||
// Bold form still extracts just the task name, not trailing metadata.
|
||||
expect(result.systemPromptAddition).toContain('Review Bob contract');
|
||||
expect(result.systemPromptAddition).not.toContain('due Friday');
|
||||
expect(result.systemPromptAddition).not.toContain('Escalate outage');
|
||||
expect(result.systemPromptAddition).not.toContain('Completed item');
|
||||
expect(result.systemPromptAddition).not.toContain('Should not surface');
|
||||
});
|
||||
|
||||
it('no activity section when calendar is empty and no tasks', async () => {
|
||||
tmpDir = makeWorkspace({
|
||||
heartbeat: { garryAwake: true },
|
||||
|
||||
@@ -209,24 +209,6 @@ describe('gbrain extract --stale', () => {
|
||||
expect(usRows[0]?.eq).toBe(true);
|
||||
});
|
||||
|
||||
test('version-arm floor: page edited BEFORE LINK_EXTRACTOR_VERSION_TS clears after --stale (issue #2576 bug 3)', async () => {
|
||||
// A page whose updated_at predates the version watermark used to be
|
||||
// stamped at its updated_at (< versionTs), so the version arm re-fired
|
||||
// every run — permanently stale. The sweep now floors the stamp at
|
||||
// versionTs. (The #1768 test above also covers this since the v0.42.x
|
||||
// VERSION_TS bump moved its date below the watermark, but this pins the
|
||||
// behavior explicitly so a date "repair" there can't drop coverage.)
|
||||
await engine.putPage('people/alice', personPage('Alice'));
|
||||
await engine.executeRaw(`UPDATE pages SET updated_at = '2000-01-01T00:00:00Z' WHERE slug = 'people/alice'`);
|
||||
expect(await engine.countStalePagesForExtraction({ versionTs: LINK_EXTRACTOR_VERSION_TS })).toBe(1);
|
||||
|
||||
await runExtract(engine, ['--stale']);
|
||||
// Pre-floor this stayed 1 forever (stamp < versionTs → version arm re-fires).
|
||||
expect(await engine.countStalePagesForExtraction({ versionTs: LINK_EXTRACTOR_VERSION_TS })).toBe(0);
|
||||
await runExtract(engine, ['--stale']);
|
||||
expect(await engine.countStalePagesForExtraction({ versionTs: LINK_EXTRACTOR_VERSION_TS })).toBe(0);
|
||||
});
|
||||
|
||||
test('CDX-4 (D2): a link-flush throw aborts the sweep and leaves pages UNSTAMPED', async () => {
|
||||
await engine.putPage('people/alice', personPage('Alice'));
|
||||
await engine.putPage('companies/acme', companyPage('Acme', '[Alice](people/alice) founded [Acme](companies/acme).'));
|
||||
|
||||
@@ -140,15 +140,6 @@ describe('extractEntityRefs', () => {
|
||||
expect(wikiRefs[0].needsResolution).toBe(true);
|
||||
});
|
||||
|
||||
test('recognizes ops/ qualified wikilinks (issue #2576 bug 2)', () => {
|
||||
// `ops` was missing from DIR_PATTERN, so [[ops/...]] fell through to
|
||||
// the generic 2c pass (needsResolution) instead of being a real ref.
|
||||
const refs = extractEntityRefs('Deployed via [[ops/services/pointer-agent]].');
|
||||
expect(refs.length).toBe(1);
|
||||
expect(refs[0].slug).toBe('ops/services/pointer-agent');
|
||||
expect(refs[0].needsResolution).toBeUndefined();
|
||||
});
|
||||
|
||||
test('skips qualified-syntax tokens (those belong to 2a)', () => {
|
||||
// [[wiki:topics/ai]] looks like 2a's qualified shape — even though
|
||||
// it wouldn't satisfy DIR_PATTERN, 2c must not claim it either
|
||||
@@ -1078,15 +1069,6 @@ describe('makeResolver — fallback chain', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('resolveBasenameMatches: path-style ref falls back to the tail (issue #2576 bug 2)', async () => {
|
||||
// normalizeBasename strips slashes, so `runbooks/2026-05-01-pointer-agent`
|
||||
// used to normalize to a garbage key that never hit the tail-keyed index.
|
||||
const engine = makeFakeEngineWithSlugs(['ops/changes/2026-05-01-pointer-agent']);
|
||||
const r = makeResolver(engine);
|
||||
expect(await r.resolveBasenameMatches!('runbooks/2026-05-01-pointer-agent'))
|
||||
.toEqual(['ops/changes/2026-05-01-pointer-agent']);
|
||||
});
|
||||
|
||||
test('resolveBasenameMatches: case-insensitive fallback', async () => {
|
||||
const engine = makeFakeEngineWithSlugs(['companies/fast-weigh']);
|
||||
const r = makeResolver(engine);
|
||||
|
||||
Reference in New Issue
Block a user