mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
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:
@@ -2478,6 +2478,24 @@ export function registerSessionRoutes(
|
|||||||
return undefined;
|
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:
|
* The `entrypoint` field Claude Code stamps on its own message records:
|
||||||
* 'cli' for a real interactive session, something else (e.g. 'sdk-py') for
|
* '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 tailEntrypoint = tail ? extractTranscriptEntrypoint(tail) : undefined;
|
||||||
const entrypoint =
|
const entrypoint =
|
||||||
headEntrypoint === 'cli' || tailEntrypoint === 'cli' ? 'cli' : (headEntrypoint ?? tailEntrypoint);
|
headEntrypoint === 'cli' || tailEntrypoint === 'cli' ? 'cli' : (headEntrypoint ?? tailEntrypoint);
|
||||||
if (entrypoint && entrypoint !== 'cli') continue;
|
if (entrypoint && isAutomatedEntrypoint(entrypoint)) continue;
|
||||||
|
|
||||||
out.push({
|
out.push({
|
||||||
sessionId,
|
sessionId,
|
||||||
|
|||||||
@@ -1495,6 +1495,35 @@ describe('session-routes', () => {
|
|||||||
expect(ids).not.toContain(sessionId);
|
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 () => {
|
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
|
// A session restarted many times over a long conversation accumulates a batch
|
||||||
// of small bookkeeping lines (mode/permission-mode/last-prompt/queue-operation)
|
// of small bookkeeping lines (mode/permission-mode/last-prompt/queue-operation)
|
||||||
|
|||||||
Reference in New Issue
Block a user