harden(history): exclude automated transcripts by SDK shape, not by "not cli"

#215 filters non-interactive transcripts out of Past Sessions with
`entrypoint !== 'cli'`. That is an allowlist on a value, and the check
hides rows, so it fails CLOSED on anything Claude Code has not shipped
yet: the day it stamps a new interactive entrypoint (a rename, or a
second interactive host), no transcript matches 'cli' any more and the
entire Past Sessions list goes blank with nothing in the UI explaining
why.

Invert it to a blocklist on the SDK shape (`sdk`, `sdk-cli`, `sdk-py`).
An automated entrypoint we do not recognize yet now costs a few noisy
rows, which is the annoyance the filter set out to fix, rather than a
dead feature. Matches the fail-open reasoning #215 already applied to a
MISSING entrypoint field; only the unknown-VALUE case was inverted.

Test fails against the pre-fix line and passes after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-05 21:47:06 +02:00
parent c19d884a51
commit ecd3f3f32a
2 changed files with 48 additions and 1 deletions
+19 -1
View File
@@ -2478,6 +2478,24 @@ export function registerSessionRoutes(
return undefined;
}
/**
* Is this `entrypoint` value an automated/SDK-driven invocation?
*
* ⚠️ Deliberately a BLOCKLIST on the SDK shape, not an allowlist on `'cli'`.
* The exclusion below hides rows, so an allowlist fails CLOSED on any value
* Claude Code has not shipped yet: the day it stamps a new interactive
* entrypoint (a rename, or a second interactive host), every transcript stops
* matching `'cli'` and the whole Past Sessions list goes blank with nothing in
* the UI to explain it. A blocklist fails OPEN instead — an automated
* entrypoint we do not recognize yet costs a few noisy rows, which is the
* annoyance this filter set out to fix rather than a broken feature.
*
* Observed values: `cli` (interactive), `sdk-cli` / `sdk-py` (automated).
*/
function isAutomatedEntrypoint(entrypoint: string): boolean {
return /^sdk(-|$)/.test(entrypoint);
}
/**
* The `entrypoint` field Claude Code stamps on its own message records:
* 'cli' for a real interactive session, something else (e.g. 'sdk-py') for
@@ -2873,7 +2891,7 @@ export function registerSessionRoutes(
const tailEntrypoint = tail ? extractTranscriptEntrypoint(tail) : undefined;
const entrypoint =
headEntrypoint === 'cli' || tailEntrypoint === 'cli' ? 'cli' : (headEntrypoint ?? tailEntrypoint);
if (entrypoint && entrypoint !== 'cli') continue;
if (entrypoint && isAutomatedEntrypoint(entrypoint)) continue;
out.push({
sessionId,
+29
View File
@@ -1495,6 +1495,35 @@ describe('session-routes', () => {
expect(ids).not.toContain(sessionId);
});
it('keeps a session whose entrypoint is an unrecognized non-SDK value (fail open)', async () => {
// The exclusion is a blocklist on the SDK shape, NOT an allowlist on 'cli'.
// An allowlist fails CLOSED on any value Claude Code has not shipped yet:
// the day it stamps a new interactive entrypoint, nothing matches 'cli' and
// the entire Past Sessions list silently goes blank. Excluding only what we
// positively recognize as automated fails open instead — a few noisy rows,
// not a dead feature.
const home = process.env.HOME as string;
const projPath = join(home, '.claude', 'projects', 'proj-entrypoint-unknown-test');
await mkdir(projPath, { recursive: true });
const sessionId = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
const line =
JSON.stringify({
type: 'user',
entrypoint: 'cli-next',
message: { role: 'user', content: 'a question from a future interactive host' },
}) + '\n';
await writeFile(join(projPath, `${sessionId}.jsonl`), line + '#'.repeat(4200 - line.length));
const res = await harness.app.inject({
method: 'GET',
url: '/api/history/sessions?projectKey=proj-entrypoint-unknown-test',
});
expect(res.statusCode).toBe(200);
const ids = JSON.parse(res.body).data.sessions.map((s: { sessionId: string }) => s.sessionId);
expect(ids).toContain(sessionId);
});
it('finds the real first prompt past a large run of pre-message bookkeeping lines', async () => {
// A session restarted many times over a long conversation accumulates a batch
// of small bookkeeping lines (mode/permission-mode/last-prompt/queue-operation)