Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions devlog/_plan/260804_router_intelligence/001_pr_stack_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ other; closing one is a maintainer decision and neither is stale.

| RI | Branch | Base | Head SHA | PR | URL | Status |
|---|---|---|---|---|---|---|
| RI-01 | `feat/ri-01-route-decision-traces` | `e44d234f0` | pending | pending | pending | in progress |
| RI-02 | `feat/ri-02-request-history-index` | `feat/ri-01` head | pending | pending | pending | queued |
| RI-01 | `feat/ri-01-route-decision-traces` | `e44d234f0` | `b5a8e7c4c` | #1003 | https://github.com/lidge-jun/opencodex/pull/1003 | MERGED |
| RI-02 | `feat/ri-02-request-history-index` | `dev` (post-#1003 merge) | `03b0eafa7` | #1004 | https://github.com/lidge-jun/opencodex/pull/1004 | OPEN |
| RI-03 | `feat/ri-03-routing-analytics` | `feat/ri-02` head | pending | pending | pending | queued |
| RI-04 | `feat/ri-04-policy-profile-core` | `feat/ri-03` head | pending | pending | pending | queued |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| RI-05 | `feat/ri-05-capability-aware-routing` | `feat/ri-04` head | pending | pending | pending | queued |
Expand Down Expand Up @@ -90,4 +90,34 @@ other; closing one is a maintainer decision and neither is stale.

### RI-02..RI-10

Appended as each PR is implemented.
### RI-02 - feat/ri-02-request-history-index

- Base SHA: `34d21b1bc` (`dev` after #1003 merge; rebased from RI-01 head
`b5a8e7c4c` when #1003 landed: `7efb6e842` -> `03b0eafa7`)
- Reviewed commit: same as final (author self-review before push)
- Findings (self-review): 4 defects caught pre-push -
1. `destroyAndRecreate` never reassigned the fresh handle to module `db`
(first-open rebuild crashed);
2. bun:sqlite named-parameter objects silently failed to bind for
`LIMIT $x` and INSERT statements (datatype mismatch / silent no-op) -
query and insert paths switched to positional parameters;
3. Windows file locking: an unfinalized prepared statement kept the DB
locked after close (EBUSY in tests) - insert statement now finalizes;
a partially-opened handle on a corrupt file is closed before recreate;
4. duplicate-replay accounting counted ignored rows in `indexedRows` -
now counts real `INSERT` changes.
- Fixes: all four above; tests cover every one.
- PR: #1004 (OPEN) https://github.com/lidge-jun/opencodex/pull/1004
- Final commit: recorded after review round (rebase + CodeRabbit/simplify
fixes; new head pushes to #1004)
- Verification:
- `bun x tsc --noEmit`: PASSED (0 errors)
- `bun run test tests/request-history-index.test.ts`: 16/16 pass
(1574 assertions) covering the mandatory matrix: empty/missing/corrupt/
old-schema/partial-line/replacement/truncation/duplicate-replay/cursor
stability/invalid-cursor/page-bounds/rebuild-equivalence/filters/row-by-id
- Focused regression suites: 269/269 pass across 8 files (incl. RI-01
tests, request-log, usage-log, combos, combo-management-api,
codex-routing, codex-account-namespaces)
- `bun run privacy:scan`: passed
- Remaining Low findings: none
42 changes: 41 additions & 1 deletion src/cli/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
const USAGE = `Usage:
ocx observe logs [--provider <name>] [--model <id>] [--status <code>]
[--limit <n>] [--follow] [--json|--jsonl]
ocx logs rebuild-index
ocx logs index-status
ocx observe usage [--range <7d|30d|all>] [--surface <all|codex|claude|grok>] [--json]
ocx observe storage [--json]
ocx observe memory [--json]
Expand Down Expand Up @@ -79,6 +81,39 @@ async function logs(argv: string[], deps: RuntimeApiDeps): Promise<void> {
} while (true);
}

async function rebuildIndex(argv: string[], deps: RuntimeApiDeps): Promise<void> {
const args = [...argv];
const wantsJson = takeFlag(args, "--json");
rejectArgs(args, USAGE);
const { rebuildRequestHistoryIndex } = await import("../routing/history/indexer");
const meta = await rebuildRequestHistoryIndex();
if (wantsJson) printData(meta, true);
Comment thread
Wibias marked this conversation as resolved.
else {
console.log(`Request-history index rebuilt (${meta.dbPath})`);
console.log(` schema version: ${meta.schemaVersion}`);
console.log(` indexed rows: ${meta.indexedRows}`);
console.log(` source size: ${meta.sourceSize} bytes`);
console.log(` last error: ${meta.lastError ?? "none"}`);
}
}

async function indexStatus(argv: string[], deps: RuntimeApiDeps): Promise<void> {
const args = [...argv];
const wantsJson = takeFlag(args, "--json");
rejectArgs(args, USAGE);
const { requestHistoryIndexStatus } = await import("../routing/history/indexer");
const meta = await requestHistoryIndexStatus();
if (wantsJson) printData(meta, true);
else {
console.log(`Request-history index (${meta.dbPath})`);
console.log(` schema version: ${meta.schemaVersion}`);
console.log(` indexed rows: ${meta.indexedRows}`);
console.log(` source size: ${meta.sourceSize} bytes`);
console.log(` indexed offset: ${meta.indexedOffset} bytes`);
console.log(` last error: ${meta.lastError ?? "none"}`);
}
}

async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
const args = [...argv];
const wantsJson = takeFlag(args, "--json");
Expand All @@ -103,7 +138,12 @@ async function simple(path: string, argv: string[], deps: RuntimeApiDeps): Promi
export async function handleObserveCommand(argv: string[], deps: RuntimeApiDeps = {}): Promise<number> {
return runCliAction(async () => {
const [sub = "logs", ...rest] = argv;
if (sub === "logs") await logs(rest, deps);
if (sub === "logs") {
const action = rest[0];
if (action === "rebuild-index") await rebuildIndex(rest.slice(1), deps);
else if (action === "index-status") await indexStatus(rest.slice(1), deps);
else await logs(rest, deps);
}
else if (sub === "usage") await usage(rest, deps);
else if (sub === "storage") await simple("/api/storage", rest, deps);
else if (sub === "memory") await simple("/api/system/memory", rest, deps);
Expand Down
43 changes: 43 additions & 0 deletions src/routing/history/cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Opaque keyset cursor for request-history pagination (RI-02, ADR-9).
*
* Ordering is `timestamp DESC, request_id DESC`; the cursor encodes the last
* returned row's `(timestamp, requestId)` pair as base64url JSON. Cursors are
* opaque to clients: any decode failure or shape mismatch yields `null` and
* the API answers `400 invalid_cursor` instead of guessing.
*/

export interface HistoryCursor {
t: number;
i: string;
}

export class InvalidCursorError extends Error {
readonly code = "invalid_cursor" as const;

constructor() {
super("invalid_cursor");
this.name = "InvalidCursorError";
}
}

export function encodeHistoryCursor(cursor: HistoryCursor): string {
return Buffer.from(JSON.stringify(cursor)).toString("base64url");
}

export function decodeHistoryCursor(raw: string | null | undefined): HistoryCursor | null {
if (typeof raw !== "string" || raw.length === 0 || raw.length > 4096) return null;
let parsed: unknown;
try {
parsed = JSON.parse(Buffer.from(raw, "base64url").toString("utf-8"));
} catch {
return null;
}
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
const record = parsed as Record<string, unknown>;
const t = record.t;
const i = record.i;
if (typeof t !== "number" || !Number.isFinite(t)) return null;
if (typeof i !== "string" || i.length === 0 || i.length > 256) return null;
return { t, i };
}
Loading
Loading