@hayasaka7/haya-pet 0.3.8 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -2
- package/apps/cli/src/haya-pet.js +11 -0
- package/docs/known-issues.md +28 -32
- package/package.json +1 -1
- package/packages/cli-core/src/codex-guardian-watcher.js +35 -2
- package/packages/cli-core/src/codex-transcript-watcher.js +25 -1
- package/packages/cli-core/test/codex-guardian-watcher.test.mjs +48 -0
- package/packages/cli-core/test/codex-transcript-watcher.test.mjs +50 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ All notable changes to HAYA Pet are documented here. This project adheres to
|
|
|
7
7
|
> 0.2.0 npm publish; they are listed under 0.2.1, which is the first version that
|
|
8
8
|
> ships them.
|
|
9
9
|
|
|
10
|
+
## [0.3.9]
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **The cross-session contamination fix now covers Codex too.** Codex had the same
|
|
14
|
+
flaw fixed for Claude in 0.3.8: its transcript watcher chose the rollout by
|
|
15
|
+
newest mtime + cwd, and the guardian-review watcher derived the main thread id
|
|
16
|
+
from the newest main rollout — so two Codex sessions in the same folder could
|
|
17
|
+
cross-report each other's `turn_aborted` (interrupt) or tool activity, with the
|
|
18
|
+
idle session showing the busy one's state. Codex's command-hook payload also
|
|
19
|
+
carries `transcript_path`, so the `haya-pet state` reporter's per-session
|
|
20
|
+
`session → transcript` link (already written for every client) now pins the Codex
|
|
21
|
+
transcript watcher to its own rollout, and the guardian watcher binds the main
|
|
22
|
+
thread id from the linked rollout's `payload.id` (and only follows a trunk whose
|
|
23
|
+
`parent_thread_id` matches it). Both fall back to the previous mtime+cwd heuristic
|
|
24
|
+
when no link is available (e.g. `transcript_path` null early), so there is no
|
|
25
|
+
regression. No timer involved.
|
|
26
|
+
|
|
10
27
|
## [0.3.8]
|
|
11
28
|
|
|
12
29
|
### Fixed
|
|
@@ -19,8 +36,8 @@ All notable changes to HAYA Pet are documented here. This project adheres to
|
|
|
19
36
|
session's watcher now pins to its own transcript via the `transcript_path` Claude
|
|
20
37
|
includes in every hook payload (recorded as a per-session link by the `haya-pet
|
|
21
38
|
state` reporter) instead of guessing; until that link exists it idles rather than
|
|
22
|
-
locking onto another session's file. Codex
|
|
23
|
-
|
|
39
|
+
locking onto another session's file. (Codex had the same discovery shape — fixed
|
|
40
|
+
in 0.3.9.)
|
|
24
41
|
- **The pet no longer disappears when the display layout changes.** The overlay
|
|
25
42
|
window's bounds were set once at creation to span one display's work area and
|
|
26
43
|
never re-homed, so unplugging a monitor, changing resolution/DPI, docking or
|
package/apps/cli/src/haya-pet.js
CHANGED
|
@@ -434,12 +434,20 @@ async function runRunCommand(parsed, dependencies) {
|
|
|
434
434
|
};
|
|
435
435
|
cleanup = injected.cleanup;
|
|
436
436
|
|
|
437
|
+
// Pin both Codex watchers to THIS session's rollout via the
|
|
438
|
+
// session->transcript link the `haya-pet state` reporter records from the
|
|
439
|
+
// hook payload's transcript_path, instead of guessing newest-by-mtime (which
|
|
440
|
+
// leaks a concurrent same-cwd session's activity/interrupts).
|
|
441
|
+
const sessionDir = resolveSessionDir(dependencies, env);
|
|
442
|
+
|
|
437
443
|
const activeToolCalls = new Set();
|
|
438
444
|
const watcher = watchCodexTranscript({
|
|
439
445
|
homeDir: dependencies.homeDir,
|
|
440
446
|
sessionsRoot: dependencies.codexSessionsRoot,
|
|
441
447
|
cwd,
|
|
442
448
|
startedAt: now(),
|
|
449
|
+
sessionId,
|
|
450
|
+
sessionDir,
|
|
443
451
|
onToolEvent: (event) => {
|
|
444
452
|
hookDebugLog(env, now, {
|
|
445
453
|
source: "codex_transcript",
|
|
@@ -522,6 +530,8 @@ async function runRunCommand(parsed, dependencies) {
|
|
|
522
530
|
sessionsRoot: dependencies.codexSessionsRoot,
|
|
523
531
|
cwd,
|
|
524
532
|
startedAt: now(),
|
|
533
|
+
sessionId,
|
|
534
|
+
sessionDir,
|
|
525
535
|
onReviewEvent: (event) => {
|
|
526
536
|
hookDebugLog(env, now, {
|
|
527
537
|
source: "codex_guardian",
|
|
@@ -550,6 +560,7 @@ async function runRunCommand(parsed, dependencies) {
|
|
|
550
560
|
stopWatcher = () => {
|
|
551
561
|
guardianWatcher.stop();
|
|
552
562
|
stopWithoutGuardian();
|
|
563
|
+
removeSessionTranscriptLink({ sessionDir, sessionId });
|
|
553
564
|
};
|
|
554
565
|
}
|
|
555
566
|
}
|
package/docs/known-issues.md
CHANGED
|
@@ -2,38 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
Issues found in live use, with their current status.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- **Symptom (same class as the
|
|
8
|
-
|
|
9
|
-
*interrupted* (and more generally mirror another session's tool/working
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- **Root cause:** `discoverCodexTranscript` (`codex-transcript-watcher.js`)
|
|
13
|
-
the rollout by **newest `.jsonl` by mtime**, filtered only by
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
the thread id
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
available. No timer, consistent with the rest of the status model.
|
|
34
|
-
- **Status:** unfixed. The binding fix shipped this session is **Claude-only** and
|
|
35
|
-
does not touch the Codex watcher or the guardian-review watcher (which shares the
|
|
36
|
-
same discovery shape and should be checked alongside it).
|
|
5
|
+
## ✅ Resolved: cross-session status contamination on Codex
|
|
6
|
+
|
|
7
|
+
- **Symptom (same class as the Claude entry below):** interrupting one Codex
|
|
8
|
+
session could flip a **different, concurrent** Codex session's pet to
|
|
9
|
+
*interrupted* (and more generally mirror another session's tool/working states).
|
|
10
|
+
Most likely when two Codex sessions ran in the **same folder** and one was busy
|
|
11
|
+
while the other was idle.
|
|
12
|
+
- **Root cause:** `discoverCodexTranscript` (`codex-transcript-watcher.js`) picked
|
|
13
|
+
the rollout by **newest `.jsonl` by mtime**, filtered only by `session_meta.cwd`
|
|
14
|
+
/ freshness — it did **not** bind to a specific session, so an idle session's
|
|
15
|
+
watcher could lock onto a busy session's rollout and read that session's
|
|
16
|
+
`turn_aborted` (Codex's interrupt signal) as its own. The `isFreshSession` branch
|
|
17
|
+
even admitted recently-started rollouts from **other cwds**, so the exposure was
|
|
18
|
+
slightly *wider* than Claude's (scoped to one project dir). The guardian-review
|
|
19
|
+
watcher had the same flaw: it derived the main thread id from the newest main
|
|
20
|
+
rollout by mtime, so a concurrent session's review status could be misattributed.
|
|
21
|
+
- **Fix:** the same per-session binding used for Claude. Verified against the
|
|
22
|
+
OpenAI Codex docs that the command-hook stdin payload carries **`transcript_path`**
|
|
23
|
+
(and `session_id`, the conversation/rollout id — which I also confirmed on disk
|
|
24
|
+
equals `session_meta.payload.id` and the rollout filename uuid). The
|
|
25
|
+
`haya-pet state` reporter already records a per-session `session→transcript` link
|
|
26
|
+
from that `transcript_path` (the capture is client-agnostic), so the Codex
|
|
27
|
+
transcript watcher now pins to its own rollout via the link
|
|
28
|
+
(`session-transcript-link.js`) instead of guessing newest-by-mtime, and the
|
|
29
|
+
guardian watcher derives the main thread id from the **linked** rollout's
|
|
30
|
+
`payload.id` (and only considers a trunk whose `parent_thread_id` matches it).
|
|
31
|
+
Both fall back to the old heuristic when no link is available (e.g. `transcript_path`
|
|
32
|
+
null early), so there is no regression. No timer involved.
|
|
37
33
|
|
|
38
34
|
## ✅ Resolved: Claude interrupt/denial leaked into a concurrent idle session
|
|
39
35
|
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
parseGuardianTranscriptLines
|
|
15
15
|
} from "../../adapters/src/codex-guardian.js";
|
|
16
16
|
import { listJsonlFiles, readFirstLine, readRange, safeMtime, safeSize } from "./codex-rollout-fs.js";
|
|
17
|
+
import { readSessionTranscriptLink } from "./session-transcript-link.js";
|
|
17
18
|
|
|
18
19
|
const DEFAULT_POLL_MS = 700;
|
|
19
20
|
const MTIME_SKEW_MS = 2000;
|
|
@@ -26,6 +27,8 @@ export function watchCodexGuardianReviews(options = {}) {
|
|
|
26
27
|
onReviewEvent = () => {},
|
|
27
28
|
pollIntervalMs = DEFAULT_POLL_MS,
|
|
28
29
|
sessionsRoot,
|
|
30
|
+
sessionId,
|
|
31
|
+
sessionDir,
|
|
29
32
|
setInterval: setIntervalFn = setInterval,
|
|
30
33
|
clearInterval: clearIntervalFn = clearInterval
|
|
31
34
|
} = options;
|
|
@@ -66,7 +69,33 @@ export function watchCodexGuardianReviews(options = {}) {
|
|
|
66
69
|
return meta;
|
|
67
70
|
};
|
|
68
71
|
|
|
72
|
+
// Authoritative main thread id from the session->transcript link (the linked
|
|
73
|
+
// rollout's session_meta.payload.id). Lets the guardian bind to OUR main thread
|
|
74
|
+
// even when another session's main rollout has a newer mtime.
|
|
75
|
+
const resolveLinkedMainThreadId = () => {
|
|
76
|
+
if (!sessionId || !sessionDir) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const linked = readSessionTranscriptLink({ sessionDir, sessionId });
|
|
80
|
+
if (!linked) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const firstLine = readFirstLine(linked);
|
|
84
|
+
if (firstLine === undefined) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const meta = classifyCodexSessionMeta(firstLine);
|
|
88
|
+
return meta?.kind === "main" ? meta.threadId : undefined;
|
|
89
|
+
};
|
|
90
|
+
|
|
69
91
|
const discoverTrunk = () => {
|
|
92
|
+
// Prefer the linked main thread id; fall back to the newest main rollout by
|
|
93
|
+
// mtime only when no link is available (older behavior, unsafe across
|
|
94
|
+
// concurrent same-cwd sessions).
|
|
95
|
+
if (!mainThreadId) {
|
|
96
|
+
mainThreadId = resolveLinkedMainThreadId();
|
|
97
|
+
}
|
|
98
|
+
|
|
70
99
|
let newestMain;
|
|
71
100
|
let newestTrunk;
|
|
72
101
|
|
|
@@ -83,8 +112,12 @@ export function watchCodexGuardianReviews(options = {}) {
|
|
|
83
112
|
if (meta.kind === "main" && meta.threadId && (!newestMain || mtime > newestMain.mtime)) {
|
|
84
113
|
newestMain = { threadId: meta.threadId, mtime };
|
|
85
114
|
}
|
|
86
|
-
if (meta.kind === "guardian" && (!newestTrunk || mtime > newestTrunk.mtime)) {
|
|
87
|
-
|
|
115
|
+
if (meta.kind === "guardian" && meta.parentThreadId && (!newestTrunk || mtime > newestTrunk.mtime)) {
|
|
116
|
+
// Once our main thread id is known, only consider OUR trunk so a
|
|
117
|
+
// concurrent session's (or collab subagent's) newer trunk cannot win.
|
|
118
|
+
if (!mainThreadId || meta.parentThreadId === mainThreadId) {
|
|
119
|
+
newestTrunk = { file, parentThreadId: meta.parentThreadId, mtime };
|
|
120
|
+
}
|
|
88
121
|
}
|
|
89
122
|
}
|
|
90
123
|
|
|
@@ -5,6 +5,7 @@ import { existsSync } from "node:fs";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { parseCodexTranscriptLines } from "../../adapters/src/codex-transcript.js";
|
|
7
7
|
import { listJsonlFiles, readFirstLine, readRange, safeMtime, safeSize } from "./codex-rollout-fs.js";
|
|
8
|
+
import { readSessionTranscriptLink } from "./session-transcript-link.js";
|
|
8
9
|
|
|
9
10
|
const DEFAULT_POLL_MS = 700;
|
|
10
11
|
const MTIME_SKEW_MS = 2000;
|
|
@@ -17,6 +18,8 @@ export function watchCodexTranscript(options = {}) {
|
|
|
17
18
|
onToolEvent = () => {},
|
|
18
19
|
pollIntervalMs = DEFAULT_POLL_MS,
|
|
19
20
|
sessionsRoot,
|
|
21
|
+
sessionId,
|
|
22
|
+
sessionDir,
|
|
20
23
|
transcriptPath: fixedPath,
|
|
21
24
|
setInterval: setIntervalFn = setInterval,
|
|
22
25
|
clearInterval: clearIntervalFn = clearInterval
|
|
@@ -25,6 +28,13 @@ export function watchCodexTranscript(options = {}) {
|
|
|
25
28
|
const root = sessionsRoot ?? (homeDir ? join(homeDir, ".codex", "sessions") : undefined);
|
|
26
29
|
const minMtime = startedAt > 0 ? startedAt - MTIME_SKEW_MS : 0;
|
|
27
30
|
|
|
31
|
+
// Preferred resolution: pin to the exact rollout this session's hook reported
|
|
32
|
+
// (Codex puts `transcript_path` in every hook payload; the `haya-pet state`
|
|
33
|
+
// reporter records it as a session->transcript link). Without that link (e.g.
|
|
34
|
+
// the path was null early), fall back to the newest-by-mtime heuristic — unsafe
|
|
35
|
+
// with concurrent same-cwd sessions, which is the bug the link avoids.
|
|
36
|
+
const useLink = Boolean(sessionId && sessionDir);
|
|
37
|
+
|
|
28
38
|
let transcriptPath = fixedPath;
|
|
29
39
|
let offset = 0;
|
|
30
40
|
let carry = "";
|
|
@@ -32,7 +42,9 @@ export function watchCodexTranscript(options = {}) {
|
|
|
32
42
|
const tick = () => {
|
|
33
43
|
try {
|
|
34
44
|
if (!transcriptPath) {
|
|
35
|
-
transcriptPath =
|
|
45
|
+
transcriptPath = useLink
|
|
46
|
+
? resolveLinkedRollout(sessionDir, sessionId)
|
|
47
|
+
: discoverCodexTranscript(root, minMtime, { cwd });
|
|
36
48
|
if (!transcriptPath) {
|
|
37
49
|
return;
|
|
38
50
|
}
|
|
@@ -79,6 +91,18 @@ export function watchCodexTranscript(options = {}) {
|
|
|
79
91
|
};
|
|
80
92
|
}
|
|
81
93
|
|
|
94
|
+
// Resolve the rollout this session's hook bound itself to (via the
|
|
95
|
+
// session->transcript link). Returns undefined until the link exists and points
|
|
96
|
+
// at a real file, so before the first hook the watcher idles rather than guessing
|
|
97
|
+
// a rollout that may belong to another concurrent session.
|
|
98
|
+
function resolveLinkedRollout(sessionDir, sessionId) {
|
|
99
|
+
const linked = readSessionTranscriptLink({ sessionDir, sessionId });
|
|
100
|
+
if (!linked || !existsSync(linked)) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return linked;
|
|
104
|
+
}
|
|
105
|
+
|
|
82
106
|
export function discoverCodexTranscript(root, minMtime = 0, options = {}) {
|
|
83
107
|
if (!root || !existsSync(root)) {
|
|
84
108
|
return undefined;
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { test } from "../../../test/harness.mjs";
|
|
6
6
|
import { watchCodexGuardianReviews } from "../src/codex-guardian-watcher.js";
|
|
7
|
+
import { writeSessionTranscriptLink } from "../src/session-transcript-link.js";
|
|
7
8
|
|
|
8
9
|
const noopTimers = { setInterval: () => ({}), clearInterval: () => {} };
|
|
9
10
|
|
|
@@ -262,6 +263,53 @@ test("watchCodexGuardianReviews ignores guardian trunks of other parents", () =>
|
|
|
262
263
|
watcher.stop();
|
|
263
264
|
});
|
|
264
265
|
|
|
266
|
+
test("watchCodexGuardianReviews binds to the LINKED main thread, not the newest main", () => {
|
|
267
|
+
const { root, dir } = makeSessionsRoot();
|
|
268
|
+
const sessionDir = mkdtempSync(join(tmpdir(), "sess-"));
|
|
269
|
+
|
|
270
|
+
// Our main rollout, and a DIFFERENT concurrent session's main rollout written
|
|
271
|
+
// afterwards (so it has the newer mtime — what the old heuristic would pick).
|
|
272
|
+
const ourMain = join(dir, "rollout-main-ours.jsonl");
|
|
273
|
+
writeFileSync(ourMain, metaLine({ id: "main-ours", parent_thread_id: null, source: "cli", thread_source: "user" }));
|
|
274
|
+
writeFileSync(
|
|
275
|
+
join(dir, "rollout-main-other.jsonl"),
|
|
276
|
+
metaLine({ id: "main-other", parent_thread_id: null, source: "cli", thread_source: "user" })
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// A guardian trunk for OUR main, plus a decoy trunk for the OTHER main that is
|
|
280
|
+
// newer and already has a review turn.
|
|
281
|
+
const ourTrunk = join(dir, "rollout-guardian-ours.jsonl");
|
|
282
|
+
writeFileSync(
|
|
283
|
+
ourTrunk,
|
|
284
|
+
metaLine({ id: "g-ours", parent_thread_id: "main-ours", source: { subagent: { other: "guardian" } } })
|
|
285
|
+
);
|
|
286
|
+
writeFileSync(
|
|
287
|
+
join(dir, "rollout-guardian-other.jsonl"),
|
|
288
|
+
metaLine({ id: "g-other", parent_thread_id: "main-other", source: { subagent: { other: "guardian" } } }) +
|
|
289
|
+
reviewStarted("decoy")
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
writeSessionTranscriptLink({ sessionDir, sessionId: "sess_a", transcriptPath: ourMain });
|
|
293
|
+
|
|
294
|
+
const events = [];
|
|
295
|
+
const watcher = watchCodexGuardianReviews({
|
|
296
|
+
sessionsRoot: root,
|
|
297
|
+
sessionId: "sess_a",
|
|
298
|
+
sessionDir,
|
|
299
|
+
onReviewEvent: (e) => events.push(e),
|
|
300
|
+
...noopTimers
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
watcher._tick();
|
|
304
|
+
assert.deepEqual(events, [], "the newer decoy trunk (another session) is ignored");
|
|
305
|
+
|
|
306
|
+
appendFileSync(ourTrunk, reviewStarted());
|
|
307
|
+
watcher._tick();
|
|
308
|
+
assert.deepEqual(events, [{ type: "review_started" }], "our trunk is the one tailed");
|
|
309
|
+
|
|
310
|
+
watcher.stop();
|
|
311
|
+
});
|
|
312
|
+
|
|
265
313
|
test("watchCodexGuardianReviews picks up a trunk created after watching began", () => {
|
|
266
314
|
const { root, dir } = makeSessionsRoot();
|
|
267
315
|
writeFileSync(
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { test } from "../../../test/harness.mjs";
|
|
6
6
|
import { discoverCodexTranscript, watchCodexTranscript } from "../src/codex-transcript-watcher.js";
|
|
7
|
+
import { writeSessionTranscriptLink } from "../src/session-transcript-link.js";
|
|
7
8
|
|
|
8
9
|
const noopTimers = { setInterval: () => ({}), clearInterval: () => {} };
|
|
9
10
|
|
|
@@ -60,6 +61,55 @@ test("discoverCodexTranscript skips files older than session start", () => {
|
|
|
60
61
|
assert.equal(discoverCodexTranscript(root, Date.now() - 1000), undefined);
|
|
61
62
|
});
|
|
62
63
|
|
|
64
|
+
test("watchCodexTranscript pins to the session's linked rollout, not newest-by-mtime", () => {
|
|
65
|
+
// Two concurrent Codex sessions, each with its own rollout and its own link.
|
|
66
|
+
const root = mkdtempSync(join(tmpdir(), "codex-sessions-"));
|
|
67
|
+
const dir = join(root, "2026", "06", "12");
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
const sessionDir = mkdtempSync(join(tmpdir(), "sess-"));
|
|
70
|
+
|
|
71
|
+
const fileA = join(dir, "rollout-a.jsonl");
|
|
72
|
+
const fileB = join(dir, "rollout-b.jsonl");
|
|
73
|
+
writeFileSync(fileA, sessionMeta("2026-06-12T01:00:00.000Z", "thread-a"));
|
|
74
|
+
writeFileSync(fileB, sessionMeta("2026-06-12T01:00:00.000Z", "thread-b"));
|
|
75
|
+
writeSessionTranscriptLink({ sessionDir, sessionId: "sess_a", transcriptPath: fileA });
|
|
76
|
+
writeSessionTranscriptLink({ sessionDir, sessionId: "sess_b", transcriptPath: fileB });
|
|
77
|
+
|
|
78
|
+
const eventsA = [];
|
|
79
|
+
const eventsB = [];
|
|
80
|
+
const watcherA = watchCodexTranscript({
|
|
81
|
+
sessionId: "sess_a",
|
|
82
|
+
sessionDir,
|
|
83
|
+
onToolEvent: (e) => eventsA.push(e),
|
|
84
|
+
...noopTimers
|
|
85
|
+
});
|
|
86
|
+
const watcherB = watchCodexTranscript({
|
|
87
|
+
sessionId: "sess_b",
|
|
88
|
+
sessionDir,
|
|
89
|
+
onToolEvent: (e) => eventsB.push(e),
|
|
90
|
+
...noopTimers
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Both pin to their own rollout and consume the session_meta line.
|
|
94
|
+
watcherA._tick();
|
|
95
|
+
watcherB._tick();
|
|
96
|
+
|
|
97
|
+
// Session A is interrupted.
|
|
98
|
+
appendFileSync(fileA, turnAborted());
|
|
99
|
+
watcherA._tick();
|
|
100
|
+
watcherB._tick();
|
|
101
|
+
|
|
102
|
+
assert.deepEqual(
|
|
103
|
+
eventsA,
|
|
104
|
+
[{ type: "turn_aborted", reason: "interrupted" }],
|
|
105
|
+
"session A sees its own interrupt"
|
|
106
|
+
);
|
|
107
|
+
assert.deepEqual(eventsB, [], "session B is NOT contaminated by session A's interrupt");
|
|
108
|
+
|
|
109
|
+
watcherA.stop();
|
|
110
|
+
watcherB.stop();
|
|
111
|
+
});
|
|
112
|
+
|
|
63
113
|
test("watchCodexTranscript reports appended tool events", () => {
|
|
64
114
|
const dir = mkdtempSync(join(tmpdir(), "codex-transcript-"));
|
|
65
115
|
const path = join(dir, "session.jsonl");
|