Skip to content
Open
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
Original file line numberDiff line numberDiff line change
Expand Up@@ -364,6 +364,7 @@ function populateForVersion(db: DatabaseType, version: number, state: ReplayStat
case 76:
case 77:
case 78:
case 80:
if (!state.armed) throw new Error(`migration v${version} reached an unarmed store`);
populateModuleOwnedRows(db, version, state);
return;
Expand DownExpand Up@@ -419,7 +420,11 @@ test("every migration lands on populated rows and v72+ stores stay armed", () =>
installMigrationLedgerFromSource(db);

for (const [index, migration] of MIGRATIONS.entries()) {
expect(migration.version).toBe(index + 1);
const expectedVersion = index + 1;
// Temporary merge-order gap: PR #340 owns v79; remove this allowance
// once its migration lands ahead of this PR's v80 migration.
const awaitingPr340 = expectedVersion === 79 && migration.version === 80;
expect(migration.version === expectedVersion || awaitingPr340).toBe(true);
assertPopulatedRowsLanded(db, state);
applyExactlyOneMigration(db, migration);
populateForVersion(db, migration.version, state);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -36,7 +36,7 @@ describe("migration v74: detected context-limit provenance", () => {
runMigrations(db);

expect(columnNames(db, "session_meta")).toContain("detected_context_limit_provenance");
expect(LATEST_SUPPORTED_VERSION).toBe(78);
expect(LATEST_SUPPORTED_VERSION).toBe(80);
expect(LATEST_SUPPORTED_VERSION).toBe(LATEST_MIGRATION_VERSION);
} finally {
closeQuietly(db);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,7 +43,7 @@ describe("migration v76: retina condition compilation", () => {
"compile_status",
]),
);
expect(LATEST_SUPPORTED_VERSION).toBe(78);
expect(LATEST_SUPPORTED_VERSION).toBe(80);
expect(LATEST_SUPPORTED_VERSION).toBe(LATEST_MIGRATION_VERSION);
expect(() =>
db
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ describe("migration v77: durable candidate provenance", () => {

expect(columnNames(db, "user_memories")).toContain("source_candidate_provenance");
expect(columnNames(db, "primers")).toContain("source_candidate_provenance");
expect(LATEST_SUPPORTED_VERSION).toBe(78);
expect(LATEST_SUPPORTED_VERSION).toBe(80);
expect(LATEST_SUPPORTED_VERSION).toBe(LATEST_MIGRATION_VERSION);
} finally {
closeQuietly(db);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,7 +46,7 @@ describe("migration v78: migration_pending journal", () => {
"phase",
"created_at",
]);
expect(LATEST_SUPPORTED_VERSION).toBe(78);
expect(LATEST_SUPPORTED_VERSION).toBe(80);
expect(LATEST_SUPPORTED_VERSION).toBe(LATEST_MIGRATION_VERSION);
} finally {
closeQuietly(db);
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
/// <reference types="bun-types" />

import { describe, expect, test } from "bun:test";
import { Database } from "../../shared/sqlite";
import { closeQuietly } from "../../shared/sqlite-helpers";
import { LATEST_MIGRATION_VERSION, runMigrations } from "./migrations";
import { initializeDatabase, LATEST_SUPPORTED_VERSION } from "./storage-db";

function seedAppliedVersion(db: Database, version: number): void {
db.exec(`
CREATE TABLE schema_migrations (
version INTEGER PRIMARY KEY,
description TEXT NOT NULL,
applied_at INTEGER NOT NULL
);
`);
const insert = db.prepare(
"INSERT INTO schema_migrations (version, description, applied_at) VALUES (?, ?, ?)",
);
for (let current = 1; current <= version; current += 1) {
insert.run(current, `seed v${current}`, Date.now());
}
}

function columnNames(db: Database, table: string): string[] {
return (db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>).map(
(column) => column.name,
);
}

describe("migration v80: tokenless usage observation timestamp", () => {
test("fresh databases include the timestamp and align the schema fence", () => {
const db = new Database(":memory:");
try {
initializeDatabase(db);
runMigrations(db);

expect(columnNames(db, "session_meta")).toContain("last_usage_observed_at");
expect(LATEST_SUPPORTED_VERSION).toBe(80);
expect(LATEST_SUPPORTED_VERSION).toBe(LATEST_MIGRATION_VERSION);
} finally {
closeQuietly(db);
}
});

test("replaying from v79 preserves the observation time for legacy token usage", () => {
const db = new Database(":memory:");
try {
seedAppliedVersion(db, 79);
db.exec(`
CREATE TABLE session_meta (
session_id TEXT PRIMARY KEY,
last_context_percentage REAL DEFAULT 0,
last_input_tokens INTEGER DEFAULT 0,
last_response_time INTEGER
);
INSERT INTO session_meta (
session_id, last_context_percentage, last_input_tokens, last_response_time
) VALUES ('ses-legacy', 50, 50000, 123);
`);

runMigrations(db);
runMigrations(db);

expect(
db
.prepare("SELECT last_usage_observed_at FROM session_meta WHERE session_id = ?")
.get("ses-legacy"),
).toEqual({ last_usage_observed_at: 123 });
expect(
db
.prepare("SELECT COUNT(*) AS count FROM schema_migrations WHERE version = 80")
.get(),
).toEqual({ count: 1 });
} finally {
closeQuietly(db);
}
});
});
29 changes: 29 additions & 0 deletions packages/plugin/src/features/magic-context/migrations.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2818,6 +2818,35 @@ export const MIGRATIONS: Migration[] = [
`);
},
},
{
// Temporary merge-order reservation: PR #340 owns v79, so this PR must
// remain v80 even while v79 is absent from this worktree.
version: 80,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When this PR is applied before PR #340, v80 becomes the high-water mark and the later v79 migration is skipped permanently. Land v79 first, or change migration selection to support out-of-order pending versions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin/src/features/magic-context/migrations.ts, line 2824:
<comment>When this PR is applied before PR #340, v80 becomes the high-water mark and the later v79 migration is skipped permanently. Land v79 first, or change migration selection to support out-of-order pending versions.</comment>
<file context>
@@ -2818,6 +2818,21 @@ export const MIGRATIONS: Migration[] = [
+ {
+ // Temporary merge-order reservation: PR #340 owns v79, so this PR must
+ // remain v80 even while v79 is absent from this worktree.
+ version: 80,
+ description: "persist the original observation time for tokenless usage TTL",
+ up(db: Database): void {
</file context>

description: "persist the original observation time for tokenless usage TTL",
up(db: Database): void {
if (!tableExists(db, "session_meta")) return;
ensureColumn(
db,
"session_meta",
"last_usage_observed_at",
"INTEGER NOT NULL DEFAULT 0",
);
const columns = new Set(
(
db.prepare("PRAGMA table_info(session_meta)").all() as Array<{ name: string }>
).map((row) => row.name),
);
if (columns.has("last_input_tokens") && columns.has("last_response_time")) {
db.exec(`
UPDATE session_meta
SET last_usage_observed_at = last_response_time
WHERE last_usage_observed_at = 0
AND last_input_tokens > 0
AND last_response_time > 0;
`);
}
},
},
];

/**
Expand Down
40 changes: 23 additions & 17 deletions packages/plugin/src/features/magic-context/storage-db.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@ import {
__resetStoragePrivatePermissionEnforcementForTests,
setStoragePrivatePermissionEnforcement,
} from "../../shared/storage-permissions";
import { MIGRATIONS } from "./migrations";
import {
__resetRpcDiscoveryFsForTests,
__resetSchemaFenceStateForTests,
Expand All@@ -47,6 +48,11 @@ import { clearSession } from "./storage-meta-session";

const tempDirs: string[] = [];
const originalXdgDataHome = process.env.XDG_DATA_HOME;
const PREVIOUS_MIGRATION_VERSION = Math.max(
...MIGRATIONS.filter((migration) => migration.version < LATEST_SUPPORTED_VERSION).map(
(migration) => migration.version,
),
);

function makeTempDir(prefix: string): string {
const dir = mkdtempSync(join(tmpdir(), prefix));
Expand DownExpand Up@@ -492,7 +498,7 @@ describe("storage-db", () => {
expect(opened === null).toBe(scenario.blocksMigration);
expect(readPersistedVersion(dbPath)).toBe(
scenario.blocksMigration
? LATEST_SUPPORTED_VERSION - 1
? PREVIOUS_MIGRATION_VERSION
: LATEST_SUPPORTED_VERSION,
);
if (!scenario.blocksMigration && pid === process.pid) {
Expand DownExpand Up@@ -523,7 +529,7 @@ describe("storage-db", () => {

expect(openDatabase()).toBeNull();
expect(getMigrationOnOpenRefusal()).toEqual({
persistedVersion: LATEST_SUPPORTED_VERSION - 1,
persistedVersion: PREVIOUS_MIGRATION_VERSION,
supportedVersion: LATEST_SUPPORTED_VERSION,
serverPids: [41001],
blockingProcesses: [{ kind: "Pi", pid: 41001 }],
Expand All@@ -534,13 +540,13 @@ describe("storage-db", () => {
expect(
formatLiveProcessMigrationRefusal(
dbPath,
LATEST_SUPPORTED_VERSION - 1,
PREVIOUS_MIGRATION_VERSION,
LATEST_SUPPORTED_VERSION,
[],
[41001],
),
).toContain("confirmed Pi harness PID 41001");
expect(readPersistedVersion(dbPath)).toBe(LATEST_SUPPORTED_VERSION - 1);
expect(readPersistedVersion(dbPath)).toBe(PREVIOUS_MIGRATION_VERSION);
});

it("#when an unrelated Pi harness is live #then opens a fresh explicit-path database", () => {
Expand DownExpand Up@@ -647,14 +653,14 @@ describe("storage-db", () => {
// insurance for files left by older or interrupted installations.
expect(openDatabase()).toBeNull();
expect(getMigrationOnOpenRefusal()).toEqual({
persistedVersion: LATEST_SUPPORTED_VERSION - 1,
persistedVersion: PREVIOUS_MIGRATION_VERSION,
supportedVersion: LATEST_SUPPORTED_VERSION,
serverPids: [],
blockingProcesses: [],
unreadableFile: portFile,
unreadableArm: "parse",
});
expect(readPersistedVersion(dbPath)).toBe(LATEST_SUPPORTED_VERSION - 1);
expect(readPersistedVersion(dbPath)).toBe(PREVIOUS_MIGRATION_VERSION);
});

it("#when old malformed and pidless records are discovered #then deletes them and allows migration", () => {
Expand DownExpand Up@@ -705,7 +711,7 @@ describe("storage-db", () => {
);
expect(getMigrationOnOpenRefusal()?.unreadableArm).toBe("parse");
for (const file of junk) expect(existsSync(file)).toBe(true);
expect(readPersistedVersion(dbPath)).toBe(LATEST_SUPPORTED_VERSION - 1);
expect(readPersistedVersion(dbPath)).toBe(PREVIOUS_MIGRATION_VERSION);
});

it("#when a port path cannot be read as a file #then refuses migration and names the io arm", () => {
Expand All@@ -720,7 +726,7 @@ describe("storage-db", () => {
unreadableFile,
unreadableArm: "io",
});
expect(readPersistedVersion(dbPath)).toBe(LATEST_SUPPORTED_VERSION - 1);
expect(readPersistedVersion(dbPath)).toBe(PREVIOUS_MIGRATION_VERSION);
});

it("#when reading a port file returns EACCES #then refuses migration without deleting it", () => {
Expand All@@ -747,7 +753,7 @@ describe("storage-db", () => {
unreadableArm: "io",
});
expect(existsSync(unreadableFile)).toBe(true);
expect(readPersistedVersion(dbPath)).toBe(LATEST_SUPPORTED_VERSION - 1);
expect(readPersistedVersion(dbPath)).toBe(PREVIOUS_MIGRATION_VERSION);
});

it("#when stale junk cleanup returns EACCES #then refuses migration with the cleanup file named", () => {
Expand DownExpand Up@@ -776,7 +782,7 @@ describe("storage-db", () => {
unreadableArm: "io",
});
expect(existsSync(staleFile)).toBe(true);
expect(readPersistedVersion(dbPath)).toBe(LATEST_SUPPORTED_VERSION - 1);
expect(readPersistedVersion(dbPath)).toBe(PREVIOUS_MIGRATION_VERSION);
});

it("#when the RPC directory cannot be enumerated #then refuses migration", () => {
Expand All@@ -787,7 +793,7 @@ describe("storage-db", () => {

expect(openDatabase()).toBeNull();
expect(getMigrationOnOpenRefusal()?.unreadableFile).toBe(rpcPath);
expect(readPersistedVersion(dbPath)).toBe(LATEST_SUPPORTED_VERSION - 1);
expect(readPersistedVersion(dbPath)).toBe(PREVIOUS_MIGRATION_VERSION);
});

it("#when an alive PID is reused by a newer process #then removes the record and allows migration", () => {
Expand DownExpand Up@@ -822,7 +828,7 @@ describe("storage-db", () => {
const legacy = new Database(dbPath);
legacy.exec(`
CREATE TABLE schema_migrations (version INTEGER PRIMARY KEY);
INSERT INTO schema_migrations(version) VALUES (${LATEST_SUPPORTED_VERSION - 1});
INSERT INTO schema_migrations(version) VALUES (${PREVIOUS_MIGRATION_VERSION});
`);
legacy.close();

Expand DownExpand Up@@ -852,7 +858,7 @@ describe("storage-db", () => {
});
for (const junkFile of junkFiles) expect(existsSync(junkFile)).toBe(false);
expect(existsSync(livePortFile)).toBe(true);
expect(readPersistedVersion(dbPath)).toBe(LATEST_SUPPORTED_VERSION - 1);
expect(readPersistedVersion(dbPath)).toBe(PREVIOUS_MIGRATION_VERSION);
});

it("#when a discovery record provides a process kind #then it takes precedence over command probes", () => {
Expand All@@ -862,7 +868,7 @@ describe("storage-db", () => {
const legacy = new Database(dbPath);
legacy.exec(`
CREATE TABLE schema_migrations (version INTEGER PRIMARY KEY);
INSERT INTO schema_migrations(version) VALUES (${LATEST_SUPPORTED_VERSION - 1});
INSERT INTO schema_migrations(version) VALUES (${PREVIOUS_MIGRATION_VERSION});
INSERT INTO schema_migrations(version) VALUES (${FORK_MIGRATION_VERSION_FLOOR});
`);
legacy.close();
Expand DownExpand Up@@ -893,7 +899,7 @@ describe("storage-db", () => {
const legacy = new Database(dbPath);
legacy.exec(`
CREATE TABLE schema_migrations (version INTEGER PRIMARY KEY);
INSERT INTO schema_migrations(version) VALUES (${LATEST_SUPPORTED_VERSION - 1});
INSERT INTO schema_migrations(version) VALUES (${PREVIOUS_MIGRATION_VERSION});
INSERT INTO schema_migrations(version) VALUES (${FORK_MIGRATION_VERSION_FLOOR});
`);
legacy.close();
Expand DownExpand Up@@ -931,7 +937,7 @@ describe("storage-db", () => {
for (const junkFile of junkFiles) expect(existsSync(junkFile)).toBe(false);
expect(existsSync(livePortFile)).toBe(true);
expect(getMigrationOnOpenRefusal()).toEqual({
persistedVersion: LATEST_SUPPORTED_VERSION - 1,
persistedVersion: PREVIOUS_MIGRATION_VERSION,
supportedVersion: LATEST_SUPPORTED_VERSION,
serverPids: [process.pid],
blockingProcesses: [{ kind: "process", pid: process.pid }],
Expand All@@ -940,7 +946,7 @@ describe("storage-db", () => {
{ kind: "process", pid: process.pid },
]);
const unchanged = new Database(dbPath);
expect(getPersistedSchemaVersion(unchanged)).toBe(LATEST_SUPPORTED_VERSION - 1);
expect(getPersistedSchemaVersion(unchanged)).toBe(PREVIOUS_MIGRATION_VERSION);
expect(
unchanged
.prepare("SELECT 1 FROM schema_migrations WHERE version = ?")
Expand Down
4 changes: 3 additions & 1 deletion packages/plugin/src/features/magic-context/storage-db.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -93,7 +93,7 @@ export function __resetSchemaFenceStateForTests(): void {
lastMigrationOnOpenRefusal = null;
}

export const LATEST_SUPPORTED_VERSION = 78;
export const LATEST_SUPPORTED_VERSION = 80;

// chmod is meaningless on Windows (POSIX modes are not honored), so all
// permission tightening is skipped there. mkdir's `mode` is likewise ignored.
Expand DownExpand Up@@ -1475,6 +1475,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
cached_m1_bytes BLOB,
last_observed_model_key TEXT,
last_usage_context_limit INTEGER NOT NULL DEFAULT 0,
last_usage_observed_at INTEGER NOT NULL DEFAULT 0,
prior_boundary_ordinal INTEGER NOT NULL DEFAULT 1,
protected_tail_policy_version INTEGER NOT NULL DEFAULT 0,
protected_tail_drain_window_started_at INTEGER NOT NULL DEFAULT 0,
Expand DownExpand Up@@ -1880,6 +1881,7 @@ CREATE INDEX IF NOT EXISTS idx_dream_queue_pending ON dream_queue(started_at, en
ensureColumn(db, "session_meta", "cached_m1_bytes", "BLOB");
ensureColumn(db, "session_meta", "last_observed_model_key", "TEXT");
ensureColumn(db, "session_meta", "last_usage_context_limit", "INTEGER NOT NULL DEFAULT 0");
ensureColumn(db, "session_meta", "last_usage_observed_at", "INTEGER NOT NULL DEFAULT 0");
ensureColumn(db, "session_meta", "prior_boundary_ordinal", "INTEGER NOT NULL DEFAULT 1");
ensureColumn(db, "session_meta", "protected_tail_policy_version", "INTEGER NOT NULL DEFAULT 0");
ensureColumn(
Expand Down
Loading
Loading