Skip to content

Commit 42238ad

Browse files
committed
fix(core): preserve compound command output
1 parent 9077d34 commit 42238ad

8 files changed

Lines changed: 163 additions & 17 deletions

File tree

src/cli/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,7 @@ export function decorateWrapInlineText(result: WrapResult["result"], raw: boolea
723723
"",
724724
"---",
725725
WRAP_AUTHORITATIVE_FOOTER,
726+
...(result.rawRef?.id ? [`Raw output is available locally: tokenjuice cat ${result.rawRef.id}`] : []),
726727
].join("\n");
727728
return `${result.inlineText}${footer}`;
728729
}

src/core/command-match.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { CommandMatchSource, ToolExecutionInput } from "../types.js";
22

33
import {
44
ENV_ASSIGNMENT_PATTERN,
5+
hasSequentialShellCommands,
56
isCompoundShellCommand,
67
splitTopLevelCommandChain,
78
stripLeadingEnvAssignmentsFromCommand,
@@ -523,6 +524,40 @@ export function resolveEffectiveCommand(input: Pick<ToolExecutionInput, "argv" |
523524
return null;
524525
}
525526

527+
export function hasMultipleSubstantiveShellCommands(input: Pick<ToolExecutionInput, "argv" | "command">): boolean {
528+
const shellBody = unwrapShellRunner(input);
529+
const command = (shellBody ?? input.command ?? "").trim();
530+
if (!command || !hasSequentialShellCommands(command)) {
531+
return false;
532+
}
533+
534+
let effectiveCommand = command;
535+
for (let iteration = 0; iteration < 16; iteration += 1) {
536+
const setupIfTail = stripLeadingSetupIfBlock(effectiveCommand);
537+
const setupSegmentTail = setupIfTail ?? stripLeadingSetupSegment(effectiveCommand);
538+
if (!setupSegmentTail) {
539+
break;
540+
}
541+
effectiveCommand = setupSegmentTail;
542+
}
543+
544+
let substantiveCount = 0;
545+
for (const sequenceSegment of splitTopLevelCommandChain(effectiveCommand)) {
546+
for (const segment of splitTopLevelOrChain(sequenceSegment)) {
547+
const candidate = buildEffectiveCandidate(tokenizeCommand(segment), true, segment);
548+
if (!candidate) {
549+
continue;
550+
}
551+
substantiveCount += 1;
552+
if (substantiveCount > 1) {
553+
return true;
554+
}
555+
}
556+
}
557+
558+
return false;
559+
}
560+
526561
export function getEffectiveCommandArgv(input: Pick<ToolExecutionInput, "argv" | "command">): string[] {
527562
return resolveEffectiveCommand(input)?.argv ?? getCandidateArgv(input);
528563
}

src/core/command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type { CommandMatchCandidate } from "./command-match.js";
1010
export {
1111
deriveCommandMatchCandidates,
1212
getEffectiveCommandArgv,
13+
hasMultipleSubstantiveShellCommands,
1314
isSetupWrapperSegment,
1415
resolveEffectiveCommand,
1516
stripLeadingEnvAssignments,

src/core/compaction-metadata.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const NO_COMPACTION_METADATA: CompactionMetadata = {
2323
kinds: [],
2424
};
2525

26-
export const WRAP_AUTHORITATIVE_FOOTER = "[tokenjuice] This is the complete, authoritative output for this command. It was deterministically compacted to remove low-signal noise; the omitted content is not retrievable. Do not re-run the command, vary flags, or switch tools to try to recover it. Proceed with the task using this output.";
26+
export const WRAP_AUTHORITATIVE_FOOTER = "[tokenjuice] This is the complete, authoritative output for this command. It was deterministically compacted to remove low-signal noise. Do not re-run the command, vary flags, or switch tools to try to recover omitted content; use a raw-artifact recovery command below when one is provided. Proceed with the task using this output.";
2727

2828
function buildCompactionMetadata(authoritative: boolean, ...kinds: CompactionKind[]): CompactionMetadata {
2929
if (kinds.length === 0) {

src/core/reduce.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { loadRules } from "./rules.js";
2+
import { hasMultipleSubstantiveShellCommands } from "./command-match.js";
23
import { classifyExecution, resolveRuleMatch } from "./classify.js";
34
import { isFileContentInspectionCommand } from "./command-identity.js";
45
import { normalizeExecutionInput } from "./execution-input.js";
@@ -323,12 +324,21 @@ export async function reduceExecutionWithRules(
323324
reducedChars,
324325
ratio: measuredRawChars === 0 ? 1 : reducedChars / measuredRawChars,
325326
});
326-
const resolvedMatch = opts.classifier
327+
const multipleSubstantiveCommands = !opts.classifier && hasMultipleSubstantiveShellCommands(input);
328+
const resolvedMatch = opts.classifier || multipleSubstantiveCommands
327329
? undefined
328330
: resolveRuleMatch(input, rules);
329-
const classification = resolvedMatch?.classification
330-
?? classifyExecution(input, rules, opts.classifier);
331-
const reducerInput = resolvedMatch?.candidateInput ?? normalizedInput;
331+
const classification = multipleSubstantiveCommands
332+
? {
333+
family: "generic",
334+
confidence: 0.2,
335+
matchedReducer: "generic/fallback",
336+
}
337+
: resolvedMatch?.classification
338+
?? classifyExecution(input, rules, opts.classifier);
339+
const reducerInput = multipleSubstantiveCommands
340+
? normalizedInput
341+
: resolvedMatch?.candidateInput ?? normalizedInput;
332342
const trace = opts.trace
333343
? {
334344
...(normalizedInput.command ? { normalizedCommand: normalizedInput.command } : {}),
@@ -390,7 +400,9 @@ export async function reduceExecutionWithRules(
390400
};
391401
}
392402

393-
const inspectionSummary = buildInspectionSummary(normalizedInput, rawText, opts.noOmit);
403+
const inspectionSummary = multipleSubstantiveCommands
404+
? null
405+
: buildInspectionSummary(normalizedInput, rawText, opts.noOmit);
394406
if (inspectionSummary) {
395407
const summaryText = inspectionSummary.lines.join("\n").trim();
396408
const selectedText = clampTextMiddleWithMetadata(summaryText, maxInlineChars, opts.noOmit);

test/cli/main.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,33 @@ describe("decorateWrapInlineText", () => {
5151
expect(decorateWrapInlineText(result, false)).toContain(WRAP_AUTHORITATIVE_FOOTER);
5252
});
5353

54+
it("provides the raw-artifact recovery command when output is stored", () => {
55+
const result: CompactResult = {
56+
inlineText: "summary",
57+
compaction: {
58+
authoritative: true,
59+
kinds: ["head-tail-omission"],
60+
},
61+
rawRef: {
62+
id: "tj_0123456789ab",
63+
path: "/tmp/tokenjuice/raw.txt",
64+
metadataPath: "/tmp/tokenjuice/meta.json",
65+
},
66+
stats: {
67+
rawChars: 4_000,
68+
reducedChars: 40,
69+
ratio: 0.01,
70+
},
71+
classification: {
72+
family: "generic",
73+
confidence: 0.9,
74+
matchedReducer: "generic/fallback",
75+
},
76+
};
77+
78+
expect(decorateWrapInlineText(result, false)).toContain("tokenjuice cat tj_0123456789ab");
79+
});
80+
5481
it("suppresses the authoritative footer for lossless rewrites", () => {
5582
const result: CompactResult = {
5683
inlineText: "summary",

test/core/command.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
33
import {
44
deriveCommandMatchCandidates,
55
getGitSubcommand,
6+
hasMultipleSubstantiveShellCommands,
67
hasSequentialShellCommands,
78
isFileContentInspectionCommand,
89
isRepositoryInspectionCommand,
@@ -371,6 +372,26 @@ describe("hasSequentialShellCommands", () => {
371372
});
372373
});
373374

375+
describe("hasMultipleSubstantiveShellCommands", () => {
376+
it.each([
377+
"grep -i github /etc/hosts; echo '---dig:'; dig +short api.github.com @1.1.1.1; scutil --dns",
378+
"cd repo && swift test && rg -n failure src",
379+
"command -v rg || cargo install ripgrep; rg --files src",
380+
"bash -lc 'grep -i github /etc/hosts; dig +short api.github.com @1.1.1.1'",
381+
])("detects `%s` as multiple substantive commands", (command) => {
382+
expect(hasMultipleSubstantiveShellCommands({ command })).toBe(true);
383+
});
384+
385+
it.each([
386+
"cd repo && pnpm test",
387+
"source .env && cargo test",
388+
"if command -v tt >/dev/null 2>&1; then tt title 'tests'; else tmux select-pane -T 'tests' 2>/dev/null || true; fi; pnpm test",
389+
"bash -lc 'cd repo && pnpm test'",
390+
])("keeps setup-wrapped `%s` as one substantive command", (command) => {
391+
expect(hasMultipleSubstantiveShellCommands({ command })).toBe(false);
392+
});
393+
});
394+
374395
describe("getGitSubcommand", () => {
375396
it.each([
376397
{ command: "git ls-files src", subcommand: "ls-files" },

test/core/reduce.test.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -710,7 +710,7 @@ describe("reduceExecution", () => {
710710
}
711711
});
712712

713-
it("matches wrapped rg search commands and prefers the first substantive command in a chain", async () => {
713+
it("matches wrapped rg search commands after setup commands", async () => {
714714
const searchResult = await reduceExecution({
715715
toolName: "exec",
716716
command: "pwd && rg -n AssertionError src",
@@ -721,20 +721,69 @@ describe("reduceExecution", () => {
721721
expect(searchResult.classification.matchedReducer).toBe("search/rg");
722722
expect(searchResult.classification.matchedVia).toBe("effective");
723723
expect(searchResult.classification.matchedCommand).toBe("rg -n AssertionError src");
724+
});
725+
726+
it("preserves all short output from a multi-command sequence", async () => {
727+
const rawText = [
728+
"127.0.0.1 github.com",
729+
"---dig:",
730+
"140.82.121.4",
731+
"---scutil:",
732+
"DNS configuration",
733+
].join("\n");
724734

725-
const firstCommandWins = await reduceExecution({
735+
const result = await reduceExecution({
726736
toolName: "exec",
727-
command: "cd repo && swift test && rg -n failure src",
728-
combinedText: [
729-
"Test Case 'FooTests.testExample' failed (0.12 seconds).",
730-
"Executed 1 test, with 1 failure (0 unexpected) in 0.12 (0.12) seconds",
731-
].join("\n"),
732-
exitCode: 1,
737+
command: "grep -i github /etc/hosts; echo '---dig:'; dig +short api.github.com @1.1.1.1; echo '---scutil:'; scutil --dns | head -1",
738+
combinedText: rawText,
739+
exitCode: 0,
733740
});
734741

735-
expect(firstCommandWins.classification.matchedReducer).toBe("tests/swift-test");
736-
expect(firstCommandWins.classification.matchedVia).toBe("effective");
737-
expect(firstCommandWins.classification.matchedCommand).toBe("swift test");
742+
expect(result.classification.matchedReducer).toBe("generic/fallback");
743+
expect(result.inlineText).toBe(rawText);
744+
expect(result.stats.ratio).toBe(1);
745+
});
746+
747+
it("uses authoritative generic compaction for large multi-command output", async () => {
748+
const rawText = Array.from({ length: 80 }, (_, index) => `output ${index + 1} ${"x".repeat(48)}`).join("\n");
749+
const result = await reduceExecution({
750+
toolName: "exec",
751+
command: "grep -i github /etc/hosts; dig +short api.github.com @1.1.1.1",
752+
combinedText: rawText,
753+
exitCode: 0,
754+
}, {
755+
maxInlineChars: 240,
756+
});
757+
758+
expect(result.classification.matchedReducer).toBe("generic/fallback");
759+
expect(result.inlineText).toContain("output 1");
760+
expect(result.inlineText).toContain("output 80");
761+
expect(result.inlineText).not.toContain("output 40");
762+
expect(result.compaction).toEqual({
763+
authoritative: true,
764+
kinds: expect.arrayContaining(["head-tail-omission"]),
765+
});
766+
});
767+
768+
it("does not summarize file inspection output from a multi-command sequence", async () => {
769+
const rawText = [
770+
"{",
771+
" \"name\": \"example\",",
772+
" \"lockfileVersion\": 3,",
773+
" \"packages\": {}",
774+
"}",
775+
"DONE",
776+
].join("\n");
777+
const result = await reduceExecution({
778+
toolName: "exec",
779+
command: "cat package-lock.json; echo DONE",
780+
combinedText: rawText,
781+
exitCode: 0,
782+
});
783+
784+
expect(result.classification.matchedReducer).toBe("generic/fallback");
785+
expect(result.inlineText).toBe(rawText);
786+
expect(result.stats.ratio).toBe(1);
738787
});
739788

740789
it("keeps wrapped file inspection output verbatim under generic fallback", async () => {

0 commit comments

Comments
 (0)