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
11 changes: 10 additions & 1 deletion src/npx-cli/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1603,7 +1603,6 @@ async function runInstallCommandInner(options: InstallOptions, summary: InstallS
} finally {
stopHeartbeat();
}
writeInstallMarker(cacheDir, version, bunVersion, uvVersion);
}
return `Runtime ready (Bun ${bunVersion}, uv ${uvVersion}) ${pc.green('OK')}`;
},
Expand Down Expand Up @@ -1638,6 +1637,13 @@ async function runInstallCommandInner(options: InstallOptions, summary: InstallS
}

await runTasks(tasks);

if (installedBunVersion && installedUvVersion) {
writeInstallMarker(cacheDir, version, installedBunVersion, installedUvVersion);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Restore cacheDir scope
cacheDir is declared inside the runtime setup task at line 1597, so this post-runTasks marker write is outside its block scope and the TypeScript build fails with Cannot find name 'cacheDir'. Hoist const cacheDir = pluginCacheDirectory(version) before the tasks are created, or recompute it here before calling writeInstallMarker.

Artifacts

Repro: project typecheck attempt output showing missing type definitions before source checking

  • Keeps the command output available without making the summary code-heavy.

Repro: narrow TypeScript compile output showing Cannot find name cacheDir at install.ts line 1642

  • Keeps the command output available without making the summary code-heavy.

View artifacts

T-Rex Ran code and verified through T-Rex

if (needsMarketplace && existsSync(join(marketplaceDirectory(), 'plugin', 'package.json'))) {
writeInstallMarker(marketplaceDirectory(), version, installedBunVersion, installedUvVersion);
}
}
}

const failedIDEs = await setupIDEs(selectedIDEs, summary);
Expand Down Expand Up @@ -1908,6 +1914,9 @@ export async function runRepairCommand(): Promise<void> {
const { bunPath } = await ensureBun();
await installPluginDependencies(cacheDir, bunPath);
writeInstallMarker(cacheDir, version, bunVersion, uvVersion);
if (existsSync(join(marketplaceDirectory(), 'plugin', 'package.json'))) {
writeInstallMarker(marketplaceDirectory(), version, bunVersion, uvVersion);
}
return `Runtime ready (Bun ${bunVersion}, uv ${uvVersion}) ${pc.green('OK')}`;
},
},
Expand Down
13 changes: 11 additions & 2 deletions src/npx-cli/install/setup-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,15 @@ const LEGACY_VERSION_MARKER_RE =
/^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;

function markerPath(targetDir: string): string {
return join(targetDir, '.install-version');
return join(resolveInstallTargetDir(targetDir), '.install-version');
}

function resolveInstallTargetDir(targetDir: string): string {
const bundledPluginRoot = join(targetDir, 'plugin');
if (existsSync(join(bundledPluginRoot, 'package.json'))) {
return bundledPluginRoot;
}
return targetDir;
}

function getBunPath(): string | null {
Expand Down Expand Up @@ -467,7 +475,8 @@ export function writeInstallMarker(
}

export function isInstallCurrent(targetDir: string, expectedVersion: string): boolean {
if (!existsSync(join(targetDir, 'node_modules'))) return false;
const installDir = resolveInstallTargetDir(targetDir);
if (!existsSync(join(installDir, 'node_modules'))) return false;
const marker = readInstallMarker(targetDir);
if (!marker) return false;
if (marker.version !== expectedVersion) return false;
Expand Down
9 changes: 7 additions & 2 deletions src/services/sqlite/SessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,8 +460,6 @@ export class SessionStore {
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_project ON sdk_sessions(project);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_status ON sdk_sessions(status);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_started ON sdk_sessions(started_at_epoch DESC);
CREATE INDEX IF NOT EXISTS idx_sdk_sessions_platform_source ON sdk_sessions(platform_source);
CREATE UNIQUE INDEX IF NOT EXISTS ux_sdk_sessions_platform_content ON sdk_sessions(platform_source, content_session_id);

CREATE TABLE IF NOT EXISTS observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
Expand Down Expand Up @@ -501,6 +499,13 @@ export class SessionStore {
CREATE INDEX IF NOT EXISTS idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`);

const sdkSessionColumns = this.db.query('PRAGMA table_info(sdk_sessions)').all() as TableColumnInfo[];
const hasPlatformSource = sdkSessionColumns.some(col => col.name === 'platform_source');
if (hasPlatformSource) {
this.db.run('CREATE INDEX IF NOT EXISTS idx_sdk_sessions_platform_source ON sdk_sessions(platform_source)');
this.db.run('CREATE UNIQUE INDEX IF NOT EXISTS ux_sdk_sessions_platform_content ON sdk_sessions(platform_source, content_session_id)');
}

this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(4, new Date().toISOString());
}

Expand Down
12 changes: 12 additions & 0 deletions tests/install-non-tty.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ describe('Install Non-TTY Support', () => {
expect(installSource).toContain('installCodexCli(marketplaceDirectory())');
});

it('writes the install marker into the durable marketplace plugin root for Codex hook launches', () => {
expect(installSource).toContain('writeInstallMarker(marketplaceDirectory(), version, bunVersion, uvVersion)');
});

it('repairs the durable marketplace plugin root marker as well as the cache copy', () => {
const repairRegion = installSource.slice(
installSource.indexOf('export async function runRepairCommand'),
installSource.indexOf('if (isInteractive) {\n p.outro'),
);
expect(repairRegion).toContain('writeInstallMarker(marketplaceDirectory(), version, bunVersion, uvVersion)');
});

it('refreshes Codex marketplace cache after registration', () => {
const installRegion = codexInstallerSource.slice(
codexInstallerSource.indexOf('export async function installCodexCli'),
Expand Down
28 changes: 28 additions & 0 deletions tests/setup-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ describe('setup-runtime install marker', () => {
const marker = readInstallMarker(tempDir);
expect(marker).toEqual({ version: '12.4.4' });
});

it('reads the marker from plugin/.install-version when given a marketplace root', () => {
mkdirSync(join(tempDir, 'plugin'), { recursive: true });
writeFileSync(join(tempDir, 'plugin', 'package.json'), JSON.stringify({ name: 'claude-mem' }));
writeFileSync(
join(tempDir, 'plugin', '.install-version'),
JSON.stringify({ version: '13.9.2', bun: '1.3.11', uv: '0.11.14', installedAt: '2026-07-01T00:00:00.000Z' }),
);

const marker = readInstallMarker(tempDir);

expect(marker).toEqual({
version: '13.9.2',
bun: '1.3.11',
uv: '0.11.14',
installedAt: '2026-07-01T00:00:00.000Z',
});
});
});

describe('writeInstallMarker', () => {
Expand All @@ -94,6 +112,16 @@ describe('setup-runtime install marker', () => {
const parsed = JSON.parse(readFileSync(join(tempDir, '.install-version'), 'utf-8'));
expect(Object.keys(parsed).sort()).toEqual(['bun', 'installedAt', 'uv', 'version'].sort());
});

it('writes the marker into plugin/.install-version when targetDir is a marketplace root', () => {
mkdirSync(join(tempDir, 'plugin'), { recursive: true });
writeFileSync(join(tempDir, 'plugin', 'package.json'), JSON.stringify({ name: 'claude-mem' }));

writeInstallMarker(tempDir, '13.9.2', '1.3.11', '0.11.14');

expect(existsSync(join(tempDir, 'plugin', '.install-version'))).toBe(true);
expect(existsSync(join(tempDir, '.install-version'))).toBe(false);
});
});

describe('isInstallCurrent', () => {
Expand Down
159 changes: 159 additions & 0 deletions tests/sqlite/session-store-migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,132 @@ function seedLegacyGlobalContentIdentityScenario(db: Database): void {
`).run(101, 'shared-raw-id', 'tool-1', epoch + 4);
}

function seedPrePlatformSourceSdkSessionsScenario(db: Database): void {
const now = new Date().toISOString();
const epoch = Date.now();

db.run(`
CREATE TABLE schema_versions (
id INTEGER PRIMARY KEY,
version INTEGER UNIQUE NOT NULL,
applied_at TEXT NOT NULL
)
`);

db.run(`
CREATE TABLE sdk_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content_session_id TEXT UNIQUE NOT NULL,
memory_session_id TEXT UNIQUE,
project TEXT NOT NULL,
user_prompt TEXT,
started_at TEXT NOT NULL,
started_at_epoch INTEGER NOT NULL,
completed_at TEXT,
completed_at_epoch INTEGER,
status TEXT CHECK(status IN ('active', 'completed', 'failed')) NOT NULL DEFAULT 'active',
worker_port INTEGER,
prompt_counter INTEGER DEFAULT 0,
custom_title TEXT
)
`);

db.run(`
CREATE TABLE observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memory_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT,
type TEXT NOT NULL,
title TEXT,
subtitle TEXT,
facts TEXT,
narrative TEXT,
concepts TEXT,
files_read TEXT,
files_modified TEXT,
prompt_number INTEGER,
discovery_tokens INTEGER DEFAULT 0,
content_hash TEXT,
agent_type TEXT,
agent_id TEXT,
merged_into_project TEXT,
generated_by_model TEXT,
metadata TEXT,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE ON UPDATE CASCADE
)
`);

db.run(`
CREATE TABLE session_summaries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memory_session_id TEXT NOT NULL,
project TEXT NOT NULL,
request TEXT,
investigated TEXT,
learned TEXT,
completed TEXT,
next_steps TEXT,
files_read TEXT,
files_edited TEXT,
notes TEXT,
prompt_number INTEGER,
discovery_tokens INTEGER DEFAULT 0,
merged_into_project TEXT,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE ON UPDATE CASCADE
)
`);

db.run(`
CREATE TABLE user_prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content_session_id TEXT NOT NULL,
prompt_number INTEGER NOT NULL,
prompt_text TEXT NOT NULL,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(content_session_id) REFERENCES sdk_sessions(content_session_id) ON DELETE CASCADE
)
`);

db.run(`
CREATE TABLE pending_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_db_id INTEGER NOT NULL,
content_session_id TEXT NOT NULL,
tool_use_id TEXT,
message_type TEXT NOT NULL CHECK(message_type IN ('observation', 'summarize')),
tool_name TEXT,
tool_input TEXT,
tool_response TEXT,
cwd TEXT,
last_user_message TEXT,
last_assistant_message TEXT,
prompt_number INTEGER,
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'processing')),
created_at_epoch INTEGER NOT NULL,
agent_type TEXT,
agent_id TEXT,
FOREIGN KEY (session_db_id) REFERENCES sdk_sessions(id) ON DELETE CASCADE
)
`);

for (let version = 4; version <= 23; version++) {
db.prepare('INSERT INTO schema_versions (version, applied_at) VALUES (?, ?)').run(version, now);
}

db.prepare(`
INSERT INTO sdk_sessions (
content_session_id, memory_session_id, project, user_prompt,
started_at, started_at_epoch, status, worker_port, prompt_counter, custom_title
) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, ?)
`).run('legacy-content-id', 'legacy-memory-id', 'legacy-project', 'legacy prompt', now, epoch, 37777, 1, 'Legacy Title');
}

describe('SessionStore migrations', () => {
let store: SessionStore | undefined;

Expand Down Expand Up @@ -395,6 +521,39 @@ describe('SessionStore migrations', () => {
}
});

it('bootstraps a pre-platform_source sdk_sessions table without crashing initializeSchema', () => {
const db = new Database(':memory:');
try {
seedPrePlatformSourceSdkSessionsScenario(db);

expect(() => new SessionStore(db)).not.toThrow();

const cols = new Set((db.query('PRAGMA table_info(sdk_sessions)').all() as Array<{ name: string }>).map(c => c.name));
expect(cols.has('platform_source')).toBe(true);
expect(hasUniqueIndexOnColumns(db, 'sdk_sessions', ['platform_source', 'content_session_id'])).toBe(true);

const row = db.prepare(`
SELECT content_session_id, memory_session_id, project, platform_source, worker_port, prompt_counter, custom_title
FROM sdk_sessions
WHERE content_session_id = 'legacy-content-id'
`).get() as {
content_session_id: string;
memory_session_id: string;
project: string;
platform_source: string;
worker_port: number;
prompt_counter: number;
custom_title: string | null;
};
expect(row.platform_source).toBe('claude');
expect(row.worker_port).toBe(37777);
expect(row.prompt_counter).toBe(1);
expect(row.custom_title).toBe('Legacy Title');
} finally {
db.close();
}
});

it('drops the dead pending_messages columns (retry_count / failed_at_epoch / completed_at_epoch / worker_pid) on a legacy db', () => {
const db = new Database(':memory:');
try {
Expand Down