Skip to content

Commit 0efadb8

Browse files
authored
Merge branch 'main' into docs/ai-chat-powershell
2 parents 3ccdd2f + 774cd25 commit 0efadb8

3 files changed

Lines changed: 34 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Docs: https://docs.openclaw.ai
8080
- **Control UI New Session reconnects:** rediscover agents, nodes, repository branches, and folder-browser state, refresh derived workspaces, gate unvalidated devices, and block ambiguous retries after Gateway client replacement while preserving the typed task and explicit choices. Fixes #106372.
8181
- **macOS remote node readiness:** take the main-session key from the node hello snapshot instead of opening an operator connection during node admission, preventing remote tunnel recovery from leaving Computer Use and node exec stuck in lifecycle transition.
8282
- **Claude CLI context budgets:** honor Anthropic model and per-agent `contextTokens` limits by passing the effective limit to Claude Code's native auto-compactor and persisting the same prepared budget in OpenClaw session state. Fixes #80933. (#93198) Thanks @mushuiyu886.
83+
- **Transcript read failures:** propagate permission and I/O failures from streaming JSONL session reads instead of treating unreadable transcripts as empty. (#106412) Thanks @zenglingbiao.
8384
- **Native app connection and relay reliability:** keep Android disconnects stopped across Activity recreation, fail remote camera commands without opening permission prompts, refresh mobile node registration after capability changes, surface iOS onboarding connection failures, cancel stale Talk owners on session switches, reject invalid Watch acknowledgments, preserve Watch events received during startup, and prevent older agent overview requests from replacing newer gateway state.
8485
- **Gateway source watch:** hand the configured port off from the installed service before starting the tmux watcher, preserve failed panes for attach/capture, and keep explicit alternate-port watches side by side with the managed Gateway.
8586
- **Claude CLI max-turn diagnostics:** preserve terminal max-turn results with OpenClaw and Claude session context, warn when tool actions may already have run, and stop unsafe auth-profile or model replay for potentially side-effecting turns. (#94130) Thanks @zhangguiping-xydt.

src/config/sessions/transcript-stream.test.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import fs from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
5-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
5+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
66
import {
77
streamSessionTranscriptLines,
88
streamSessionTranscriptLinesReverse,
@@ -23,6 +23,7 @@ beforeEach(() => {
2323
});
2424

2525
afterEach(() => {
26+
vi.restoreAllMocks();
2627
fs.rmSync(tempDir, { recursive: true, force: true });
2728
});
2829

@@ -58,6 +59,15 @@ describe("streamSessionTranscriptLines", () => {
5859
expect(lines).toEqual([]);
5960
});
6061

62+
it("propagates stat failures other than ENOENT", async () => {
63+
const error = Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" });
64+
vi.spyOn(fs.promises, "stat").mockRejectedValueOnce(error);
65+
66+
await expect(
67+
collect(streamSessionTranscriptLines("/some/protected/transcript.jsonl")),
68+
).rejects.toBe(error);
69+
});
70+
6171
it("forwards malformed JSON lines as raw text so callers can choose to skip them", async () => {
6272
fs.writeFileSync(
6373
transcriptPath,
@@ -106,14 +116,23 @@ describe("streamSessionTranscriptLinesReverse", () => {
106116
expect(lines).toEqual(["third", "second", "first"]);
107117
});
108118

109-
it("returns an empty iterator when the file cannot be opened", async () => {
119+
it("returns an empty iterator when the file does not exist", async () => {
110120
const lines = await collect(
111121
streamSessionTranscriptLinesReverse(path.join(tempDir, "missing.jsonl")),
112122
);
113123

114124
expect(lines).toEqual([]);
115125
});
116126

127+
it("propagates open failures other than ENOENT", async () => {
128+
const error = Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" });
129+
vi.spyOn(fs.promises, "open").mockRejectedValueOnce(error);
130+
131+
await expect(
132+
collect(streamSessionTranscriptLinesReverse("/some/protected/transcript.jsonl")),
133+
).rejects.toBe(error);
134+
});
135+
117136
it("preserves complete lines across chunk boundaries", async () => {
118137
const longLine = "x".repeat(2048);
119138
fs.writeFileSync(transcriptPath, `${longLine}\nbeta\ngamma\n`, "utf-8");

src/config/sessions/transcript-stream.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Transcript streaming reads large JSONL files forward or backward without whole-file buffering.
22
import fs from "node:fs";
33
import readline from "node:readline";
4+
import { hasErrnoCode } from "../../infra/errors.js";
45
import { readFileRangeAsync } from "./file-range.js";
56

67
// Shared streaming helpers for JSONL session transcripts.
@@ -40,8 +41,11 @@ export async function* streamSessionTranscriptLines(
4041
let stat: fs.Stats;
4142
try {
4243
stat = await fs.promises.stat(filePath);
43-
} catch {
44-
return;
44+
} catch (error) {
45+
if (hasErrnoCode(error, "ENOENT")) {
46+
return;
47+
}
48+
throw error;
4549
}
4650
if (!stat.isFile() || stat.size <= 0) {
4751
return;
@@ -72,7 +76,7 @@ export async function* streamSessionTranscriptLines(
7276
* Stream the non-empty, trimmed JSONL lines of a transcript file in reverse
7377
* (newest-first) order.
7478
*
75-
* Returns an empty async iterator if the file cannot be opened, is empty, or is
79+
* Returns an empty async iterator if the file does not exist, is empty, or is
7680
* not a regular file. The implementation splits on newline bytes before UTF-8
7781
* decoding so multibyte characters survive arbitrary chunk boundaries.
7882
*/
@@ -88,8 +92,11 @@ export async function* streamSessionTranscriptLinesReverse(
8892
let fileHandle: Awaited<ReturnType<typeof fs.promises.open>>;
8993
try {
9094
fileHandle = await fs.promises.open(filePath, "r");
91-
} catch {
92-
return;
95+
} catch (error) {
96+
if (hasErrnoCode(error, "ENOENT")) {
97+
return;
98+
}
99+
throw error;
93100
}
94101
try {
95102
const stat = await fileHandle.stat();

0 commit comments

Comments
 (0)