Skip to content

Commit 442edce

Browse files
authored
Merge pull request #16 from japer-technology/copilot/debug-issues
Fix local chat EOF handling and failure exit semantics
2 parents 396b865 + 9b884be commit 442edce

3 files changed

Lines changed: 54 additions & 18 deletions

File tree

.github-minimum-intelligence/docs/local-chat.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Invoke with `bun run chat` from the `.github-minimum-intelligence` directory.
2222
| Invocation | Description |
2323
|------------|-------------|
2424
| `bun run chat` | Interactive launcher (pick an existing thread or create one). |
25-
| `bun run chat --new [--name <alias>]` | Create a new thread and enter the REPL. |
25+
| `bun run chat --new [--name <alias>]` | Create a new thread and print its ID. |
2626
| `bun run chat --thread <id\|alias> [prompt...]` | Continue a thread; enter the REPL if no prompt is given, otherwise send the prompt one-shot. |
2727
| `bun run chat --list` | List all threads. |
2828
| `bun run chat --rm <id\|alias>` | Delete a thread mapping. |

.github-minimum-intelligence/lifecycle/local-chat.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* exit 2; environment problems exit 1.
1414
* 5. EOF safety: closed stdin (non-TTY / Ctrl-D) must never hang an
1515
* interactive prompt.
16+
* 6. A failed one-shot model turn must exit 1 rather than reporting success.
1617
*/
1718

1819
import { describe, expect, test } from "bun:test";
@@ -112,4 +113,25 @@ describe("local-chat regression tests", () => {
112113
expect(result.signal).toBeNull();
113114
expect(result.status).toBe(0);
114115
});
116+
117+
test("failed one-shot model turn exits 1", () => {
118+
const alias = `gmi-failed-turn-${Date.now()}-${process.pid}`;
119+
try {
120+
const created = runChat(["--new", "--name", alias], MI_DIR);
121+
expect(created.status).toBe(0);
122+
123+
const result = runChat(["--thread", alias, "hello"], MI_DIR, {
124+
LOCAL_PROVIDER: "not-a-real-provider",
125+
LOCAL_MODEL: "not-a-real-model",
126+
OPENAI_API_KEY: undefined,
127+
OPENAI_BASE_URL: undefined,
128+
LOCAL_LLM_BASE_URL: undefined,
129+
});
130+
expect(result.status).toBe(1);
131+
expect(result.stdout).toContain("Turn failed");
132+
} finally {
133+
const rm = runChat(["--rm", alias], MI_DIR);
134+
expect(rm.status).toBe(0);
135+
}
136+
});
115137
});

.github-minimum-intelligence/lifecycle/local-chat.ts

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ function printCliHelp(): void {
575575
576576
Usage:
577577
bun run chat Interactive launcher (pick or create).
578-
bun run chat --new [--name <alias>] Create a new thread and enter REPL.
578+
bun run chat --new [--name <alias>] Create a new thread; prints its ID.
579579
bun run chat --thread <id|alias> [prompt...] Continue a thread; REPL if no prompt.
580580
bun run chat --list List all threads.
581581
bun run chat --rm <id|alias> Delete a thread mapping.
@@ -686,7 +686,11 @@ async function interactiveStart(provider: string, model: string, thinking: strin
686686
console.log(" • Type q (or Ctrl-C) to quit.");
687687
console.log("");
688688

689-
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
689+
const rl = createInterface({
690+
input: process.stdin,
691+
output: process.stdout,
692+
terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY),
693+
});
690694
const ask = makeAsk(rl);
691695
try {
692696
while (true) {
@@ -1166,7 +1170,11 @@ async function repl(initial: Thread, rt: RuntimeState): Promise<void> {
11661170
console.log(" " + c.dim("─────────────────────────────────────────────────────────────────────"));
11671171
console.log("");
11681172

1169-
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
1173+
const rl = createInterface({
1174+
input: process.stdin,
1175+
output: process.stdout,
1176+
terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY),
1177+
});
11701178
// EOF-safe question wrapper: resolves null on Ctrl-D / closed stdin so the
11711179
// REPL exits cleanly instead of hanging on an unanswerable question.
11721180
const ask = makeAsk(rl);
@@ -1641,7 +1649,11 @@ type RuntimeCfg = { provider: string; model: string; thinking: string | undefine
16411649
* throwing, so callers can treat it as "user backed out".
16421650
*/
16431651
async function promptLine(question: string): Promise<string> {
1644-
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
1652+
const rl = createInterface({
1653+
input: process.stdin,
1654+
output: process.stdout,
1655+
terminal: Boolean(process.stdin.isTTY && process.stdout.isTTY),
1656+
});
16451657
try {
16461658
// makeAsk resolves null on EOF (Ctrl-D / closed stdin); map that to ""
16471659
// so callers can treat it as "user backed out".
@@ -1883,19 +1895,6 @@ async function main(): Promise<void> {
18831895
}
18841896
}
18851897

1886-
// ── Locate pi binary; guide the user if it's missing ────────────────────
1887-
let piBin: string;
1888-
try {
1889-
piBin = locatePiBin();
1890-
} catch (err) {
1891-
// Extract the candidate list out of the throw message for the guide.
1892-
const msg = (err as Error).message;
1893-
const m = msg.match(/not found in any of: ([^\n]+)/);
1894-
const candidates = m ? m[1].split(", ") : [];
1895-
guidePiNotInstalled(candidates);
1896-
return;
1897-
}
1898-
18991898
// ── Resolve / create the active thread ──────────────────────────────────
19001899
let activeThread: Thread | null = null;
19011900
if (args.newThread) {
@@ -1924,6 +1923,20 @@ async function main(): Promise<void> {
19241923
}
19251924
}
19261925

1926+
// ── Locate pi binary; guide the user if it's missing ────────────────────
1927+
// Allocation-only --new returns above and does not need runtime dependencies.
1928+
let piBin: string;
1929+
try {
1930+
piBin = locatePiBin();
1931+
} catch (err) {
1932+
// Extract the candidate list out of the throw message for the guide.
1933+
const msg = (err as Error).message;
1934+
const m = msg.match(/not found in any of: ([^\n]+)/);
1935+
const candidates = m ? m[1].split(", ") : [];
1936+
guidePiNotInstalled(candidates);
1937+
return;
1938+
}
1939+
19271940
// Auto-retry default: on for local providers (flaky/slow), off for cloud
19281941
// (failures are usually configuration errors, not transient).
19291942
const rt: RuntimeState = {
@@ -1947,6 +1960,7 @@ async function main(): Promise<void> {
19471960
console.log(renderMarkdown(reply || "(no text reply produced)"));
19481961
} catch (err) {
19491962
say.error("Turn failed", (err as Error).message);
1963+
process.exitCode = 1;
19501964
}
19511965
return;
19521966
}

0 commit comments

Comments
 (0)