mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-17 10:22:34 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8cba9a76b |
@@ -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
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
} from './embedding-context.ts';
|
||||
import { loadSearchModeConfig, resolveSearchMode } from './search/mode.ts';
|
||||
import { normalizeAliasList } from './search/alias-normalize.ts';
|
||||
import { isUndefinedTableError, warnOncePerProcess, validateSlug } from './utils.ts';
|
||||
import { isUndefinedTableError, warnOncePerProcess } from './utils.ts';
|
||||
import { computeCorpusGeneration } from './contextual-retrieval-service.ts';
|
||||
import { runGuardrails } from './guardrails.ts';
|
||||
|
||||
@@ -295,12 +295,6 @@ export async function importFromContent(
|
||||
remote?: boolean;
|
||||
} = {},
|
||||
): Promise<ImportResult> {
|
||||
// Normalize BEFORE any tx write: putPage lowercases via validateSlug but
|
||||
// upsertChunks used to query by the caller's raw slug, so a mixed-case slug
|
||||
// created the page row then failed the chunk upsert with "Page not found",
|
||||
// rolling back the whole import (#430).
|
||||
slug = validateSlug(slug);
|
||||
|
||||
// v0.18.0+ multi-source: when caller is syncing under a non-default source,
|
||||
// every per-page tx call must carry `sourceId` so writes target the right
|
||||
// (source_id, slug) row. Pre-fix, putPage relied on the schema DEFAULT and
|
||||
|
||||
@@ -2235,9 +2235,6 @@ export class PGLiteEngine implements BrainEngine {
|
||||
}
|
||||
|
||||
private async _upsertChunksOnce(slug: string, chunks: ChunkInput[], opts?: { sourceId?: string }): Promise<void> {
|
||||
// Normalize the same way putPage does — pages.slug is stored lowercased,
|
||||
// so a raw mixed-case slug here would miss the row it just wrote (#430).
|
||||
slug = validateSlug(slug);
|
||||
const sourceId = opts?.sourceId ?? 'default';
|
||||
|
||||
// Source-scope the page-id lookup so duplicate slugs in different sources
|
||||
|
||||
@@ -2385,9 +2385,6 @@ export class PostgresEngine implements BrainEngine {
|
||||
}
|
||||
|
||||
private async _upsertChunksOnce(slug: string, chunks: ChunkInput[], opts?: { sourceId?: string }): Promise<void> {
|
||||
// Normalize the same way putPage does — pages.slug is stored lowercased,
|
||||
// so a raw mixed-case slug here would miss the row it just wrote (#430).
|
||||
slug = validateSlug(slug);
|
||||
const sql = this.sql;
|
||||
const sourceId = opts?.sourceId ?? 'default';
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test';
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { importFromContent } from '../src/core/import-file.ts';
|
||||
import type { BrainEngine } from '../src/core/engine.ts';
|
||||
import type { PageInput, ChunkInput } from '../src/core/types.ts';
|
||||
|
||||
@@ -182,24 +181,6 @@ describe('PGLiteEngine: Pages', () => {
|
||||
const page = await engine.putPage('Test/UPPER', testPage);
|
||||
expect(page.slug).toBe('test/upper');
|
||||
});
|
||||
|
||||
test('importFromContent normalizes mixed-case slugs before all tx writes (#430)', async () => {
|
||||
const result = await importFromContent(
|
||||
engine,
|
||||
'TestNamespace/Page-Name',
|
||||
'---\ntype: note\ntitle: Mixed Case\n---\n\nbody text',
|
||||
{ noEmbed: true },
|
||||
);
|
||||
expect(result.status).toBe('imported');
|
||||
expect(result.slug).toBe('testnamespace/page-name');
|
||||
|
||||
const page = await engine.getPage('testnamespace/page-name');
|
||||
expect(page).not.toBeNull();
|
||||
expect(page!.title).toBe('Mixed Case');
|
||||
|
||||
const chunks = await engine.getChunks('testnamespace/page-name');
|
||||
expect(chunks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
@@ -383,17 +364,6 @@ describe('PGLiteEngine: Chunks', () => {
|
||||
expect(chunks[1].chunk_text).toBe('Chunk one');
|
||||
});
|
||||
|
||||
test('upsertChunks normalizes mixed-case slugs like putPage (#430)', async () => {
|
||||
await engine.putPage('Test/ChunkCase', testPage);
|
||||
await engine.upsertChunks('Test/ChunkCase', [
|
||||
{ chunk_index: 0, chunk_text: 'Mixed-case chunk', chunk_source: 'compiled_truth' },
|
||||
]);
|
||||
|
||||
const chunks = await engine.getChunks('test/chunkcase');
|
||||
expect(chunks.length).toBe(1);
|
||||
expect(chunks[0].chunk_text).toBe('Mixed-case chunk');
|
||||
});
|
||||
|
||||
test('upsertChunks removes orphan chunks', async () => {
|
||||
await engine.putPage('test/orphan', testPage);
|
||||
await engine.upsertChunks('test/orphan', [
|
||||
|
||||
Reference in New Issue
Block a user