diff --git a/apps/desktop/src/main/e2e-fixture.ts b/apps/desktop/src/main/e2e-fixture.ts index 0952245094a..cef2fb0fa53 100644 --- a/apps/desktop/src/main/e2e-fixture.ts +++ b/apps/desktop/src/main/e2e-fixture.ts @@ -1,12 +1,7 @@ import { mkdir, rm } from 'node:fs/promises'; import { join } from 'node:path'; import type { E2eFixtureScenario, E2eFixtureState, UiLocale } from '@maka/core'; -import { - backfillSessionProjects, - createFileCredentialStore, - createProjectCatalog, - createSessionStore, -} from '@maka/storage'; +import { createFileCredentialStore } from '@maka/storage'; import type { CredentialStore } from '@maka/storage'; import { resolveStorageRoot } from '@maka/storage/root-authority'; import { @@ -228,16 +223,4 @@ export async function seedE2eFixture(input: { await writeSession(input.workspaceRoot, seed.header, seed.messages); } } - await seedSessionProjects(input.workspaceRoot); -} - -async function seedSessionProjects(workspaceRoot: string): Promise { - const sessions = createSessionStore(workspaceRoot); - const catalog = createProjectCatalog(workspaceRoot); - try { - await backfillSessionProjects({ sessions, catalog }); - } finally { - await sessions.close?.(); - catalog.close(); - } } diff --git a/packages/cli/src/__tests__/pi-transcript.test.ts b/packages/cli/src/__tests__/pi-transcript.test.ts index a2e3189e886..eb82f860572 100644 --- a/packages/cli/src/__tests__/pi-transcript.test.ts +++ b/packages/cli/src/__tests__/pi-transcript.test.ts @@ -3986,44 +3986,6 @@ describe('Maka Pi TUI transcript', () => { }); describe('transcript entry render memoization', () => { - test('re-renders live progress after the bounded buffer reaches a stable length', () => { - const state = createMakaPiTranscriptState(); - applyMakaSessionEventToTranscript( - state, - event({ - type: 'tool_start', - toolUseId: 'progress-cache', - toolName: 'Workflow', - args: {}, - }), - ); - for (let i = 0; i < 512; i += 1) { - applyMakaSessionEventToTranscript( - state, - event({ - type: 'tool_progress', - toolUseId: 'progress-cache', - chunk: `progress-${i}\n`, - }), - ); - } - assert.equal(toggleAllToolExpansion(state), true); - const before = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); - assert.match(before, /progress-511\b/); - - applyMakaSessionEventToTranscript( - state, - event({ - type: 'tool_progress', - toolUseId: 'progress-cache', - chunk: 'progress-512\n', - }), - ); - const after = renderMakaPiTranscript(state, meta(), 100).map(stripAnsi).join('\n'); - assert.match(after, /progress-512\b/); - assert.doesNotMatch(after, /progress-0\b/); - }); - test('re-renders thinking when a same-length final replaces the streamed text', () => { const state = createMakaPiTranscriptState(); applyMakaSessionEventToTranscript( diff --git a/packages/runtime-host/src/__tests__/agent-graph-coordinator.test.ts b/packages/runtime-host/src/__tests__/agent-graph-coordinator.test.ts index b57598780a8..0676f36304a 100644 --- a/packages/runtime-host/src/__tests__/agent-graph-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/agent-graph-coordinator.test.ts @@ -13,12 +13,10 @@ import { import { AGENT_GRAPH_RESULT_MAX_BYTES, decodeAgentGraphClientSnapshot, - decodeAgentGraphOperatorInspection, } from '../protocol/index.js'; import { HostAgentGraphCoordinator, projectAgentGraphClientSnapshot, - projectAgentGraphOperatorInspection, } from '../server/agent-graph-coordinator.js'; import type { ConnectionContext } from '../server/operation-dispatcher.js'; @@ -210,51 +208,6 @@ describe('Host Agent Graph coordinator', () => { assert.equal(cursor.recordId, projected.terminalHistory.records.at(-1)?.recordId); }); - test('bounds a large operator inspection without changing its identity', () => { - const snapshot = graphSnapshot(); - const source = operatorInspection(snapshot); - source.inboundEdges = Array.from({ length: 100 }, (_, index) => ({ - edgeId: `edge-in-${index}`, - fromOperatorId: `upstream-${index}`, - toOperatorId: 'operator-1', - })); - source.outboundEdges = Array.from({ length: 100 }, (_, index) => ({ - edgeId: `edge-out-${index}`, - fromOperatorId: 'operator-1', - toOperatorId: `downstream-${index}`, - })); - source.recentRecords = Array.from({ length: 128 }, (_, index) => activity(index)); - - const projected = projectAgentGraphOperatorInspection(source); - assert.ok(Buffer.byteLength(JSON.stringify(projected), 'utf8') <= AGENT_GRAPH_RESULT_MAX_BYTES); - assert.equal(projected.operator.operatorId, 'operator-1'); - assert.equal( - projected.inboundEdges.length + projected.omitted.inboundEdges, - source.inboundEdges.length, - ); - assert.equal( - projected.outboundEdges.length + projected.omitted.outboundEdges, - source.outboundEdges.length, - ); - assert.equal( - projected.recentRecords.length + projected.omitted.records, - source.recentRecords.length, - ); - assert.deepEqual( - projected.inboundEdges.map((edge) => edge.edgeId), - source.inboundEdges - .slice(source.inboundEdges.length - projected.inboundEdges.length) - .map((edge) => edge.edgeId), - ); - assert.deepEqual( - projected.recentRecords.map((record) => record.recordId), - source.recentRecords - .slice(source.recentRecords.length - projected.recentRecords.length) - .map((record) => record.recordId), - ); - assert.doesNotThrow(() => decodeAgentGraphOperatorInspection(projected)); - }); - test('projects only allowlisted Runtime fields onto the wire', () => { const source = graphSnapshot(); Object.assign(source.operators[0]!, { privatePrompt: 'operator-secret' }); diff --git a/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts b/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts index 6f2cf3cb14d..5921285d503 100644 --- a/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts +++ b/packages/runtime/src/__tests__/history-compact-checkpoint.test.ts @@ -14,63 +14,26 @@ import { estimateRuntimeEventsTokens } from '../context-budget.js'; import { applyRuntimeEventHistoryCompact } from '../history-compact.js'; describe('history compact checkpoint', () => { - test('keeps 10K-event coverage bounded and validates the exact ordered prefix', () => { - const events = Array.from({ length: 10_000 }, (_, index) => textEvent(index)); + test('validates the exact ordered source prefix', () => { + const events = Array.from({ length: 4 }, (_, index) => textEvent(index)); const checkpoint = buildHistoryCompactCheckpoint({ sessionId: 'session-1', coveredRuntimeEvents: events, - summary: 'A bounded continuation summary.', + summary: 'Continuation summary.', now: 1_800_000_010_000, }); assert.equal(validateHistoryCompactCheckpointShape(checkpoint, 'session-1'), true); - assert.ok(Buffer.byteLength(JSON.stringify(checkpoint), 'utf8') < 64 * 1024); - assert.equal(checkpoint.coverage.eventCount, 10_000); - assert.equal(checkpoint.coverage.turnCount, 5_000); - assert.equal(checkpoint.coverage.through.runtimeEventId, 'event-9999'); - assert.equal(checkpoint.source?.policyVersion, 'maka.compactable_runtime_event_projection.v1'); - assert.deepEqual(checkpoint.source?.coverage, { - lowWater: { - ledger: 'runtime_event_projection', - streamId: 'session-1', - sequence: 0, - eventId: 'event-0', - }, - highWater: { - ledger: 'runtime_event_projection', - streamId: 'session-1', - sequence: 9_999, - eventId: 'event-9999', - }, - eventCount: 10_000, - }); - const prefixMatch = matchHistoryCompactCheckpointPrefix(checkpoint, [ - ...events, - textEvent(10_000), - ]); - assert.equal(prefixMatch.coveredEventCount, 10_000); + const prefixMatch = matchHistoryCompactCheckpointPrefix(checkpoint, [...events, textEvent(4)]); + assert.equal(prefixMatch.coveredEventCount, 4); assert.deepEqual( prefixMatch.successorRuntimeEvents.map((event) => event.id), - ['event-10000'], + ['event-4'], ); - const replay = applyRuntimeEventHistoryCompact( - [...events, textEvent(10_000)], - { - enabled: true, - mode: 'read_write', - checkpoint, - highWaterRatio: 0.000001, - tailEstimatedTokens: 1, - }, - 1, - 1_000_000, - ); - assert.equal(replay.checkpoint?.checkpointId, checkpoint.checkpointId); - assert.ok(Buffer.byteLength(JSON.stringify(replay.diagnosticPatch), 'utf8') < 16 * 1024); const changed = [...events]; - changed[4_999] = { - ...changed[4_999]!, + changed[1] = { + ...changed[1]!, content: { kind: 'text', text: 'changed source fact' }, }; assert.equal( diff --git a/packages/runtime/src/__tests__/shell-run-manager.test.ts b/packages/runtime/src/__tests__/shell-run-manager.test.ts index bf4fdb0768e..320070f828f 100644 --- a/packages/runtime/src/__tests__/shell-run-manager.test.ts +++ b/packages/runtime/src/__tests__/shell-run-manager.test.ts @@ -1559,34 +1559,6 @@ describe('ShellRunProcessManager', () => { await manager.stopBackgroundTask('session-1', run.ref, NO_ABORT); }); - test('coalesces high-volume PTY renderer deltas without dropping bytes', async () => { - const cwd = await workspace(); - const events: ShellRunPtyDataEvent[] = []; - const manager = createManager(createSqliteShellRunStore(cwd), undefined, { - onPtyData: (event) => events.push(event), - }); - const run = await manager.runBackgroundBash( - shellInput({ - cwd, - command: `node -e "process.stdout.write('x'.repeat(1024 * 1024))"`, - pty: true, - timeoutMs: 5_000, - }), - ); - assert.equal(run.kind, 'shell_run'); - await waitForTerminalShellRun(manager, run.ref); - - const bytes = events.reduce((total, event) => total + Buffer.byteLength(event.data, 'utf8'), 0); - assert.equal(bytes, 1024 * 1024); - assert.equal( - events.every((event) => Buffer.byteLength(JSON.stringify(event.data), 'utf8') <= 40 * 1024), - true, - ); - for (let index = 1; index < events.length; index += 1) { - assert.ok(events[index]!.sequence > events[index - 1]!.sequence); - } - }); - test('keeps concurrent PTY control and Read persistence in parser-cut order', async () => { const updates: ShellRunUpdate[] = []; const store = createSqliteShellRunStore(await workspace()); diff --git a/packages/storage/src/__tests__/project-session-backfill.test.ts b/packages/storage/src/__tests__/project-session-backfill.test.ts deleted file mode 100644 index e2c7e071e33..00000000000 --- a/packages/storage/src/__tests__/project-session-backfill.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -import assert from 'node:assert/strict'; -import { execFile } from 'node:child_process'; -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { test } from 'node:test'; -import { promisify } from 'node:util'; -import { createProjectCatalog } from '../project-catalog.js'; -import { backfillSessionProjects } from '../project-session-backfill.js'; -import { createSessionStore } from '../session-store.js'; - -const execFileAsync = promisify(execFile); - -function sessionInput(cwd: string) { - return { - cwd, - backend: 'fake' as const, - llmConnectionSlug: 'fixture', - model: 'fixture-model', - permissionMode: 'execute' as const, - }; -} - -async function withWorkspace( - run: (context: { - sessions: ReturnType; - catalog: ReturnType; - projectPath: string; - base: string; - }) => Promise, -): Promise { - const root = await mkdtemp(join(tmpdir(), 'maka-project-backfill-')); - const projectPath = await mkdtemp(join(tmpdir(), 'maka-project-backfill-cwd-')); - const workspace = join(root, 'workspace'); - const sessions = createSessionStore(workspace); - const catalog = createProjectCatalog(workspace); - try { - await run({ sessions, catalog, projectPath, base: root }); - } finally { - await sessions.close?.(); - catalog.close(); - await rm(root, { recursive: true, force: true }); - await rm(projectPath, { recursive: true, force: true }); - } -} - -test('a session that never resolved a project is grouped by its working directory', async () => { - await withWorkspace(async ({ sessions, catalog, projectPath }) => { - const session = await sessions.create({ - ...sessionInput(projectPath), - name: 'session without a project', - }); - assert.equal( - (await sessions.readHeaderSnapshot(session.id)).projectId, - undefined, - 'precondition: the session starts with no resolved project', - ); - - const result = await backfillSessionProjects({ sessions, catalog }); - - assert.deepEqual(result, { resolved: 1, failures: [] }); - const projects = await catalog.list(); - assert.equal(projects.length, 1); - assert.equal( - (await sessions.readHeaderSnapshot(session.id)).projectId, - projects[0]!.id, - 'the session must join the project derived from its cwd', - ); - }); -}); - -test('a session detached from every project keeps that choice', async () => { - await withWorkspace(async ({ sessions, catalog, projectPath }) => { - const session = await sessions.create({ - ...sessionInput(projectPath), - name: 'session with no project by choice', - projectId: null, - }); - - const result = await backfillSessionProjects({ sessions, catalog }); - - assert.deepEqual(result, { resolved: 0, failures: [] }); - assert.equal( - (await sessions.readHeaderSnapshot(session.id)).projectId, - null, - 'an explicit "no project" is a user decision, not missing data', - ); - assert.deepEqual(await catalog.list(), []); - }); -}); - -test('a deleted directory resolves to the same project it had while it existed', async () => { - await withWorkspace(async ({ sessions, catalog, projectPath }) => { - const before = await catalog.register(projectPath); - const session = await sessions.create({ - ...sessionInput(projectPath), - name: 'session in a deleted folder', - }); - await rm(projectPath, { recursive: true, force: true }); - - const result = await backfillSessionProjects({ sessions, catalog }); - - assert.deepEqual(result, { resolved: 1, failures: [] }); - const projects = await catalog.list(); - // On macOS the temp root is a symlink, so a path canonicalized without - // realpath would mint a second project for the very same directory. - assert.equal(projects.length, 1, 'the vanished directory must not mint a second project'); - assert.equal(projects[0]!.id, before.id); - assert.equal(projects[0]!.available, false, 'the project exists but its directory does not'); - assert.equal((await sessions.readHeaderSnapshot(session.id)).projectId, before.id); - }); -}); - -test('backfill is idempotent and leaves resolved sessions untouched', async () => { - await withWorkspace(async ({ sessions, catalog, projectPath }) => { - await sessions.create({ ...sessionInput(projectPath), name: 'session' }); - - const first = await backfillSessionProjects({ sessions, catalog }); - const second = await backfillSessionProjects({ sessions, catalog }); - - assert.deepEqual(first, { resolved: 1, failures: [] }); - assert.deepEqual(second, { resolved: 0, failures: [] }, 'a second start finds nothing to do'); - assert.equal((await catalog.list()).length, 1, 'no duplicate project is created'); - }); -}); - -test('projects are ordered by when their sessions were last active, not by upgrade order', async () => { - await withWorkspace(async ({ sessions, catalog, base }) => { - const older = join(base, 'older-project'); - const newer = join(base, 'newer-project'); - await mkdir(older); - await mkdir(newer); - // Names chosen so session-id order cannot accidentally produce the - // expected result: recency has to come from the sessions' own timestamps. - const olderSession = await sessions.create({ ...sessionInput(older), name: 'older' }); - const newerSession = await sessions.create({ ...sessionInput(newer), name: 'newer' }); - await sessions.updateHeader(olderSession.id, { lastMessageAt: 1_000 }); - await sessions.updateHeader(newerSession.id, { lastMessageAt: 9_000 }); - - await backfillSessionProjects({ sessions, catalog }); - - assert.deepEqual( - (await catalog.list()).map((project) => project.name), - ['newer-project', 'older-project'], - 'an upgrade must rebuild the real recency order, not flatten it to "now"', - ); - }); -}); - -test('a subagent worktree never becomes one of the user project locations', async () => { - await withWorkspace(async ({ sessions, catalog, base }) => { - const repository = join(base, 'repository'); - const worktree = join(base, 'subagent-worktree'); - await mkdir(repository); - await execFileAsync('git', ['init', '--quiet', '-b', 'main'], { cwd: repository }); - await execFileAsync('git', ['commit', '--quiet', '--allow-empty', '-m', 'root'], { - cwd: repository, - env: { - ...process.env, - GIT_AUTHOR_NAME: 'maka', - GIT_AUTHOR_EMAIL: 'maka@example.com', - GIT_COMMITTER_NAME: 'maka', - GIT_COMMITTER_EMAIL: 'maka@example.com', - }, - }); - await execFileAsync('git', ['worktree', 'add', '--quiet', worktree, '-b', 'child'], { - cwd: repository, - }); - const parent = await sessions.create({ ...sessionInput(repository), name: 'parent' }); - await sessions.create({ - ...sessionInput(worktree), - name: 'subagent', - subagentParent: { - kind: 'subagent' as const, - lifecycle: 'foreground' as const, - parentSessionId: parent.id, - spawnedBy: { parentRunId: 'run-1', parentTurnId: 'turn-1', toolCallId: 'call-1' }, - }, - }); - - const result = await backfillSessionProjects({ sessions, catalog }); - - // The child inherits its parent's project when spawned; its scratch - // worktree is disposable and must never outrank the real checkout. - assert.deepEqual(result, { resolved: 1, failures: [] }); - const projects = await catalog.list(); - assert.equal(projects.length, 1); - assert.deepEqual( - projects[0]!.locations.map((location) => location.path), - [await realpath(repository)], - 'a subagent scratch worktree is not a place the user works', - ); - assert.equal(projects[0]!.preferredPath, await realpath(repository)); - }); -}); - -test('a session detached while resolution is running keeps the user decision', async () => { - await withWorkspace(async ({ sessions, catalog, projectPath }) => { - const session = await sessions.create({ ...sessionInput(projectPath), name: 'session' }); - - const result = await backfillSessionProjects({ - sessions, - catalog: { - // Resolution is where the real work — a git probe — happens, so this is - // exactly the window in which a user can still act on the session. - resolveHistoricalPath: async (path, usedAt) => { - await sessions.updateHeader(session.id, { projectId: null }); - return catalog.resolveHistoricalPath(path, usedAt); - }, - }, - }); - - assert.deepEqual(result, { resolved: 0, failures: [] }); - assert.equal( - (await sessions.readHeaderSnapshot(session.id)).projectId, - null, - 'a detach made after the plan was formed must still win', - ); - }); -}); - -test('a working directory reachable through a replaced ancestor still resolves', async () => { - await withWorkspace(async ({ sessions, catalog, base }) => { - const ancestor = join(base, 'ancestor'); - const cwd = join(ancestor, 'project'); - await mkdir(cwd, { recursive: true }); - const before = await catalog.register(cwd); - const session = await sessions.create({ ...sessionInput(cwd), name: 'session' }); - // The directory is gone and something else now occupies its parent, so - // walking up reports ENOTDIR rather than ENOENT. - await rm(ancestor, { recursive: true, force: true }); - await writeFile(ancestor, 'not a directory', 'utf8'); - - const result = await backfillSessionProjects({ sessions, catalog }); - - assert.deepEqual(result, { resolved: 1, failures: [] }); - assert.equal( - (await sessions.readHeaderSnapshot(session.id)).projectId, - before.id, - 'a replaced ancestor must not strand the session outside its project', - ); - }); -}); - -test('a working directory that cannot be resolved is reported instead of guessed', async () => { - await withWorkspace(async ({ sessions, projectPath }) => { - const session = await sessions.create({ ...sessionInput(projectPath), name: 'session' }); - - const result = await backfillSessionProjects({ - sessions, - catalog: { - resolveHistoricalPath: () => Promise.reject(new Error('git exploded')), - }, - }); - - assert.deepEqual(result, { - resolved: 0, - failures: [{ cwd: projectPath, reason: 'git exploded' }], - }); - assert.equal( - (await sessions.readHeaderSnapshot(session.id)).projectId, - undefined, - 'an unresolved session stays unresolved so the next start can retry', - ); - }); -}); diff --git a/packages/storage/src/execution-stores.ts b/packages/storage/src/execution-stores.ts index 32f366a97b8..b170ac5f7a3 100644 --- a/packages/storage/src/execution-stores.ts +++ b/packages/storage/src/execution-stores.ts @@ -348,8 +348,6 @@ async function createExecutionStoresForWrite run(() => sessionStore.listCatalogPage(filter, cursor, limit, expectedRevision)), listHeaders: () => run(() => sessionStore.listHeaders()), - listSessionsWithUnresolvedProject: () => - run(() => sessionStore.listSessionsWithUnresolvedProject()), listForRecovery: () => run(() => sessionStore.listForRecovery()), readHeaderSnapshot: (sessionId) => run(() => sessionStore.readHeaderSnapshot(sessionId)), readHeaderRecordSnapshot: (sessionId) => diff --git a/packages/storage/src/index.ts b/packages/storage/src/index.ts index 63f62660e10..f55c4cfae9e 100644 --- a/packages/storage/src/index.ts +++ b/packages/storage/src/index.ts @@ -126,7 +126,6 @@ export * from './memory-bundle-store.js'; export * from './long-term-memory-store.js'; export * from './project-catalog.js'; export * from './project-catalog-authority.js'; -export * from './project-session-backfill.js'; export * from './git-worktree-child-executor.js'; export * from './managed-workspace-owner.js'; export * from './pet-pack-store.js'; diff --git a/packages/storage/src/project-session-backfill.ts b/packages/storage/src/project-session-backfill.ts deleted file mode 100644 index 62aad4ab4d2..00000000000 --- a/packages/storage/src/project-session-backfill.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { ProjectCatalog } from './project-catalog.js'; -import type { SessionAuthorityStore } from './session-store.js'; -import { SessionMetadataVersionConflictError } from './sqlite-session-metadata-store.js'; - -export interface ProjectSessionBackfillResult { - resolved: number; - failures: Array<{ cwd: string; reason: string }>; -} - -/** - * Give every session that never had its project resolved a membership. - * - * `projectId` is three-valued — an id, an explicit `null` for "no project", or - * absent for "never decided" — and only the last state is backfilled here, so - * a user who deliberately detached a session keeps that choice. - * - * Sessions are grouped by working directory before resolution: an upgrade - * typically holds many sessions per project, and resolution costs a `git` - * subprocess plus a catalog write each time. Each directory carries the latest - * activity of the sessions that share it, so the rebuilt catalog keeps its real - * recency order rather than collapsing every project to the upgrade's timestamp. - * - * Each assignment is fenced by the metadata revision the session was listed at. - * This runs during startup while a window is already open, so a user detaching - * a session mid-resolution would otherwise be overwritten by a plan made before - * they decided; the fenced write simply loses instead. - */ -export async function backfillSessionProjects(input: { - sessions: Pick< - SessionAuthorityStore, - 'listSessionsWithUnresolvedProject' | 'updateHeaderVersioned' - >; - catalog: Pick; -}): Promise { - const pending = await input.sessions.listSessionsWithUnresolvedProject(); - const byDirectory = new Map(); - for (const session of pending) { - const group = byDirectory.get(session.cwd); - if (group) { - group.usedAt = Math.max(group.usedAt, session.usedAt); - group.sessions.push(session); - } else { - byDirectory.set(session.cwd, { usedAt: session.usedAt, sessions: [session] }); - } - } - - let resolved = 0; - const failures: Array<{ cwd: string; reason: string }> = []; - - for (const [cwd, group] of byDirectory) { - let projectId: string; - try { - projectId = (await input.catalog.resolveHistoricalPath(cwd, group.usedAt)).id; - } catch (error) { - failures.push({ cwd, reason: error instanceof Error ? error.message : String(error) }); - continue; - } - for (const session of group.sessions) { - try { - await input.sessions.updateHeaderVersioned(session.id, { projectId }, session.revision); - resolved += 1; - } catch (error) { - // Someone changed the session first. Whatever they decided outranks a - // plan formed before it, and a still-unresolved session is retried on - // the next start, so this is not a failure to report. - if (error instanceof SessionMetadataVersionConflictError) continue; - failures.push({ cwd, reason: error instanceof Error ? error.message : String(error) }); - } - } - } - - return { resolved, failures }; -} - -type UnresolvedSession = Awaited< - ReturnType ->[number]; diff --git a/packages/storage/src/session-store.ts b/packages/storage/src/session-store.ts index 150d7d3b59c..e9bd991f65c 100644 --- a/packages/storage/src/session-store.ts +++ b/packages/storage/src/session-store.ts @@ -9,7 +9,6 @@ import { SessionMetadataVersionConflictError, type SqliteSessionMetadataStore, type StableSessionCreateProbe, - type UnresolvedProjectSession, type VersionedSessionIdentity, } from './sqlite-session-metadata-store.js'; import { isDiscardableConversationCopy } from './session-conversation-copy.js'; @@ -153,8 +152,6 @@ export interface SessionStore { list(filter?: SessionListFilter): Promise; /** Enumerate durable metadata without reading transcript bodies. */ listHeaders(): Promise; - /** Sessions whose project membership was never decided, newest activity last. */ - listSessionsWithUnresolvedProject(): Promise; listForRecovery(): Promise; /** Read only the durable header without triggering connection-lock self-healing. */ readHeaderSnapshot(sessionId: string): Promise; @@ -577,11 +574,6 @@ class SqliteSessionStore implements SessionAuthorityStore { .sort((a, b) => a.id.localeCompare(b.id)); } - async listSessionsWithUnresolvedProject(): Promise { - await this.ensureReady(); - return this.metadata.listSessionsWithUnresolvedProject(); - } - async readHeaderSnapshot(sessionId: string): Promise { return (await this.readHeaderRecordSnapshot(sessionId)).header; } diff --git a/packages/storage/src/sqlite-session-metadata-store.ts b/packages/storage/src/sqlite-session-metadata-store.ts index 1dbc3cd0b1c..4bcb7e31ba7 100644 --- a/packages/storage/src/sqlite-session-metadata-store.ts +++ b/packages/storage/src/sqlite-session-metadata-store.ts @@ -162,22 +162,6 @@ export interface VersionedSessionIdentity { readonly expectedVersion: number; } -/** - * A session whose project membership was never decided. - * - * `usedAt` is the moment it was last active, so resolving it later rebuilds the - * catalog's real recency order instead of collapsing every project to "now". - * `revision` is the metadata version this row was read at, so the write that - * assigns a project can fence itself against anything that touched the session - * in between — including a user detaching it while resolution is still running. - */ -export interface UnresolvedProjectSession { - readonly id: string; - readonly cwd: string; - readonly usedAt: number; - readonly revision: number; -} - export type SessionRemovalProbe = | { readonly kind: 'present'; readonly record: SessionMetadataRecord } | { readonly kind: 'removed' } @@ -1216,61 +1200,6 @@ export class SqliteSessionMetadataStore { return rows.map(decodeRecord); } - /** - * Sessions whose project membership was never resolved. - * - * `projectId` is deliberately three-valued: a project id means resolved, - * `null` means the user chose no project, and an absent key means nobody has - * decided yet. Only the third state may be backfilled, and SQL can tell them - * apart through `json_type` — `null` reports `'null'` while an absent key - * reports SQL NULL. Scoping the query this way keeps startup proportional to - * the sessions that still need work rather than to the whole catalog. - */ - async listSessionsWithUnresolvedProject(): Promise { - this.assertOpen(); - // `json_type` distinguishes an absent `projectId` (never decided) from an - // explicit JSON `null` (detached on purpose); only the former is pending. - // Subagent sessions are excluded: they inherit their parent's project when - // spawned, and their working directory is often a throwaway worktree that - // must never become one of the user's project locations. - const rows = this.db - .prepare( - ` - SELECT - session_id AS id, - json_extract(payload_json, '$.cwd') AS cwd, - COALESCE(last_message_at, last_used_at) AS used_at, - metadata_version AS revision - FROM session_metadata - WHERE json_type(payload_json, '$.projectId') IS NULL - AND subagent_parent_session_id IS NULL - ORDER BY used_at, session_id - `, - ) - .all() as Array<{ - id?: unknown; - cwd?: unknown; - used_at?: unknown; - revision?: unknown; - }>; - return rows.flatMap((row) => - typeof row.id === 'string' && - typeof row.cwd === 'string' && - row.cwd.length > 0 && - typeof row.used_at === 'number' && - typeof row.revision === 'number' - ? [ - { - id: row.id, - cwd: row.cwd, - usedAt: row.used_at, - revision: row.revision, - }, - ] - : [], - ); - } - async listCatalogPage( filter: SessionListFilter, cursor: SessionMetadataCatalogCursor | undefined,