Skip to content
Original file line number Diff line number Diff 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 Down Expand 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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
79 changes: 79 additions & 0 deletions packages/plugin/src/features/magic-context/migrations-v80.test.ts
Original file line number Diff line number Diff 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 number Diff line number Diff 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 number Diff line number Diff 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 Down Expand 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 Down Expand 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 Down Expand 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 Down Expand 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 Down Expand 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 Down Expand 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 Down Expand 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 Down Expand 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 Down Expand 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 number Diff line number Diff 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 Down Expand 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 Down Expand 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