@hayasaka7/haya-pet 0.3.4 → 0.3.5
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
CHANGED
|
@@ -7,6 +7,34 @@ 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.5]
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **The pet no longer gets stuck on "compacting" in Claude Code.** `PreCompact`
|
|
14
|
+
set the status to *compacting*, but nothing ever cleared it — Claude's `Stop`
|
|
15
|
+
does not fire for a `/compact`, so the pet sat on *compacting* until the next
|
|
16
|
+
prompt or the 30 s stale sweep. The Claude hook table now also subscribes
|
|
17
|
+
**`PostCompact`**, split by the documented `manual`/`auto` trigger matcher: a
|
|
18
|
+
**manual** `/compact` returns to *idle* (control is back at the prompt), while
|
|
19
|
+
an **auto** compaction (context filled mid-turn) resumes to *thinking* and the
|
|
20
|
+
next real event refines from there. Mirrors Codex, which already handled
|
|
21
|
+
`PostCompact`.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- **`HAYA_PET_DAEMON_DEBUG` diagnostic.** When set to a file path, the companion
|
|
25
|
+
appends one JSONL line per incoming non-heartbeat message in daemon **arrival
|
|
26
|
+
order** (with `updatedAt`), making out-of-order state delivery observable. Added
|
|
27
|
+
to investigate the Codex interrupt issue below.
|
|
28
|
+
|
|
29
|
+
### Known issues
|
|
30
|
+
- **Codex interrupt can still leave the pet "working".** On some interrupts the
|
|
31
|
+
pet keeps a working state instead of *interrupted*. The transcript watcher does
|
|
32
|
+
record `turn_aborted` (the "a late tool result resets it" theory was ruled out
|
|
33
|
+
across 257 real aborts), so the suspect is the daemon applying state by IPC
|
|
34
|
+
**arrival order**, letting a stale "working" message land after *interrupted*.
|
|
35
|
+
Instrumented via `HAYA_PET_DAEMON_DEBUG`; **fix to follow shortly.** See
|
|
36
|
+
`docs/known-issues.md`.
|
|
37
|
+
|
|
10
38
|
## [0.3.4]
|
|
11
39
|
|
|
12
40
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { app, BrowserWindow, ipcMain, Menu, nativeImage, screen, shell, Tray } from "electron";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { createDaemonRuntime } from "../../../../packages/daemon-core/src/daemon-runtime.js";
|
|
6
6
|
import { createIpcServer } from "../../../../packages/daemon-core/src/ipc-server.js";
|
|
@@ -30,6 +30,35 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
30
30
|
const TRAY_ICON_DATA_URL =
|
|
31
31
|
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAANElEQVR4nGNgoAXIW/HvPzZMtkaiDCJWM05DKDKAVM0YhowaQAUDBj4dUCUpE2MQQY3kAACyf/g8DHVl5wAAAABJRU5ErkJggg==";
|
|
32
32
|
|
|
33
|
+
// Best-effort daemon-side diagnostic, mirroring the wrapper's HAYA_PET_HOOK_DEBUG.
|
|
34
|
+
// When HAYA_PET_DAEMON_DEBUG points at a file, append one JSONL line per incoming
|
|
35
|
+
// non-heartbeat message in DAEMON ARRIVAL ORDER, with its updatedAt. This is the
|
|
36
|
+
// only place the true apply order is visible: the registry is last-writer-wins by
|
|
37
|
+
// arrival and ignores updatedAt, so a stale "working" message that arrives after
|
|
38
|
+
// "interrupted" would surface here as the clobber. Never throws.
|
|
39
|
+
function debugLogDaemonMessage(message) {
|
|
40
|
+
const target = process.env.HAYA_PET_DAEMON_DEBUG;
|
|
41
|
+
if (!target || !message || message.type === "heartbeat") {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
appendFileSync(
|
|
46
|
+
target,
|
|
47
|
+
`${JSON.stringify({
|
|
48
|
+
ts: Date.now(),
|
|
49
|
+
type: message.type,
|
|
50
|
+
sessionId: message.sessionId,
|
|
51
|
+
state: message.state,
|
|
52
|
+
source: message.source,
|
|
53
|
+
updatedAt: message.updatedAt,
|
|
54
|
+
summary: message.summary
|
|
55
|
+
})}\n`
|
|
56
|
+
);
|
|
57
|
+
} catch {
|
|
58
|
+
// diagnostics must never break the daemon
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
const paths = getDefaultPaths();
|
|
34
63
|
const capabilities = getPlatformCapabilities();
|
|
35
64
|
const stateFile = createStateFile({ statePath: paths.statePath });
|
|
@@ -113,6 +142,7 @@ async function bootstrap() {
|
|
|
113
142
|
app.quit();
|
|
114
143
|
return;
|
|
115
144
|
}
|
|
145
|
+
debugLogDaemonMessage(message);
|
|
116
146
|
return runtime.handleMessage(message);
|
|
117
147
|
},
|
|
118
148
|
onProtocolError: (error) => console.error("protocol error:", error.message)
|
package/docs/known-issues.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
Issues found in live use, with their current status.
|
|
4
4
|
|
|
5
|
+
## ⏳ In progress: Codex interrupt sometimes leaves the pet "working"
|
|
6
|
+
|
|
7
|
+
- **Symptom:** Pressing Esc to interrupt a Codex turn occasionally does **not**
|
|
8
|
+
flip the pet to *interrupted* — it keeps showing a working state (*thinking* /
|
|
9
|
+
*running*) even though the turn was cut short. Intermittent ("some occasions").
|
|
10
|
+
- **Investigation so far:** The "a late `function_call_output` resets the state to
|
|
11
|
+
*thinking*" theory was **ruled out** — across 257 real `turn_aborted` events in
|
|
12
|
+
live `~/.codex/sessions`, **zero** had a tool result after the abort. Codex
|
|
13
|
+
fires no hook on an abort, and the L3 transcript watcher does record
|
|
14
|
+
`turn_aborted` and emit `interrupted`.
|
|
15
|
+
- **Leading suspect:** The daemon registry applies session state **last-writer-
|
|
16
|
+
wins by IPC arrival order** (`registry.js` `applyState` ignores `updatedAt` and
|
|
17
|
+
`confidence`). Hooks (separate `haya-pet state` subprocesses) and the interrupt
|
|
18
|
+
watcher use different IPC connections, so a stale "working" message can arrive
|
|
19
|
+
*after* `interrupted` and clobber it. Discovery edges (a resumed session whose
|
|
20
|
+
`session_meta.timestamp` predates the wrapper launch is filtered out; concurrent
|
|
21
|
+
Codex sessions can attach the watcher to the wrong rollout) are secondary
|
|
22
|
+
candidates.
|
|
23
|
+
- **Status:** Instrumented with a daemon-side arrival trace (`HAYA_PET_DAEMON_DEBUG`,
|
|
24
|
+
in `apps/companion/src/main/index.js`) to confirm whether `interrupted` is never
|
|
25
|
+
emitted (watcher/discovery) vs. emitted-then-clobbered (arrival-order race).
|
|
26
|
+
**Fix to follow shortly** — likely an `updatedAt`-monotonic guard in `applyState`.
|
|
27
|
+
|
|
28
|
+
## ✅ Resolved: Claude pet stuck on "compacting" after a compaction
|
|
29
|
+
|
|
30
|
+
- **Symptom:** With Claude Code hooks enabled, the pet entered *compacting* on
|
|
31
|
+
`PreCompact` and never left — it sat there until the 30 s stale sweep or the
|
|
32
|
+
next prompt. Most visible after a manual `/compact`.
|
|
33
|
+
- **Root cause:** The Claude hook table subscribed `PreCompact` → *compacting* but
|
|
34
|
+
had **no completion event**. Claude's `Stop` does not fire for a compaction, so
|
|
35
|
+
nothing cleared the state. (Codex already subscribed `PostCompact`.)
|
|
36
|
+
- **Fix:** The Claude hook table now also subscribes **`PostCompact`**, split by
|
|
37
|
+
the documented `manual`/`auto` trigger matcher: a **manual** `/compact` returns
|
|
38
|
+
to *idle* (control is back at the prompt), while an **auto** compaction resumes
|
|
39
|
+
to *thinking* (the agent continues the turn) and the next real event refines it.
|
|
40
|
+
Verified live on the manual path; auto uses the identical matcher mechanism.
|
|
41
|
+
|
|
5
42
|
## ✅ Resolved: Claude Code subagent completion changed the main session status
|
|
6
43
|
|
|
7
44
|
- **Symptom:** In Claude Code multi-agent runs, the main agent could already be
|
|
@@ -259,8 +296,10 @@ observation (`--observe`) or L1 lifecycle as the fallback. Current state:
|
|
|
259
296
|
lifecycle status). Live in-session status is **opt-in** via `HAYA_PET_HOOKS=1`,
|
|
260
297
|
which injects a settings file (`claude --settings <stable-file>`, no change to
|
|
261
298
|
your global config) wiring Claude's `UserPromptSubmit`/`PreToolUse`/`PostToolUse`/
|
|
262
|
-
`Notification`/`PreCompact`/`Stop` events to `haya-pet state <state>`,
|
|
263
|
-
to the daemon over the IPC pipe. `
|
|
299
|
+
`Notification`/`PreCompact`/`PostCompact`/`Stop` events to `haya-pet state <state>`,
|
|
300
|
+
reported to the daemon over the IPC pipe. `PostCompact` is split by its
|
|
301
|
+
`manual`/`auto` trigger matcher (manual `/compact` → *idle*, auto compaction →
|
|
302
|
+
*thinking*) so the pet never sticks on *compacting*. `SubagentStop` is intentionally ignored because
|
|
264
303
|
it is not a main-turn idle signal. `PreToolUse` distinguishes
|
|
265
304
|
file-editing tools (`Edit`/`Write`/`MultiEdit`/`NotebookEdit` → *editing files*)
|
|
266
305
|
from other tools (→ *running tools*) via the hook `matcher`. **Why opt-in:**
|
package/package.json
CHANGED
|
@@ -33,6 +33,14 @@ const HOOK_TABLE = Object.freeze([
|
|
|
33
33
|
{ event: "Notification", matcher: "idle_prompt", state: "idle", summary: "idle" },
|
|
34
34
|
{ event: "PermissionDenied", state: "idle", summary: "denied" },
|
|
35
35
|
{ event: "PreCompact", state: "compacting" },
|
|
36
|
+
// PostCompact is the authoritative "compaction finished" signal (verified in
|
|
37
|
+
// the Claude hooks docs). Without it the pet stays stuck on `compacting` until
|
|
38
|
+
// the 30s stale sweep. The `trigger` matcher splits the two outcomes: a manual
|
|
39
|
+
// /compact hands control back to an idle prompt, while an AUTO compaction fires
|
|
40
|
+
// mid-turn and the agent immediately resumes working (thinking) — the next real
|
|
41
|
+
// event (PreToolUse/Stop) refines from there. Mirrors codex-hooks' PostCompact.
|
|
42
|
+
{ event: "PostCompact", matcher: "manual", state: "idle", summary: "compacted" },
|
|
43
|
+
{ event: "PostCompact", matcher: "auto", state: "thinking", summary: "compacted" },
|
|
36
44
|
{ event: "Stop", state: "idle" },
|
|
37
45
|
{ event: "StopFailure", state: "idle", summary: "stopped" }
|
|
38
46
|
]);
|
|
@@ -49,6 +57,11 @@ export function mapClaudeEventToState(event, detail) {
|
|
|
49
57
|
if (detail === "idle_prompt") return "idle";
|
|
50
58
|
return undefined;
|
|
51
59
|
}
|
|
60
|
+
if (event === "PostCompact") {
|
|
61
|
+
// `detail` is the compaction trigger ("manual" | "auto"). A manual /compact
|
|
62
|
+
// ends at an idle prompt; an auto compaction resumes the turn (thinking).
|
|
63
|
+
return detail === "manual" ? "idle" : "thinking";
|
|
64
|
+
}
|
|
52
65
|
const entry = HOOK_TABLE.find((row) => row.event === event && row.matcher === undefined);
|
|
53
66
|
return entry?.state;
|
|
54
67
|
}
|
|
@@ -31,6 +31,15 @@ test("mapClaudeEventToState branches Notification on type (approval vs idle)", (
|
|
|
31
31
|
assert.equal(mapClaudeEventToState("Notification", "auth_success"), undefined);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
+
test("mapClaudeEventToState branches PostCompact on compaction trigger", () => {
|
|
35
|
+
// Manual /compact returns to an idle prompt; auto compaction resumes the turn.
|
|
36
|
+
assert.equal(mapClaudeEventToState("PostCompact", "manual"), "idle");
|
|
37
|
+
assert.equal(mapClaudeEventToState("PostCompact", "auto"), "thinking");
|
|
38
|
+
// Unknown/missing trigger defaults to the "still working" assumption, which the
|
|
39
|
+
// next real event corrects — never leaving the pet stuck on `compacting`.
|
|
40
|
+
assert.equal(mapClaudeEventToState("PostCompact"), "thinking");
|
|
41
|
+
});
|
|
42
|
+
|
|
34
43
|
test("buildClaudeHookSettings bakes node + cli, no volatile session id", () => {
|
|
35
44
|
const settings = buildClaudeHookSettings({
|
|
36
45
|
nodePath: "/usr/bin/node",
|
|
@@ -62,6 +71,15 @@ test("buildClaudeHookSettings splits Notification into approval + idle matchers"
|
|
|
62
71
|
assert.match(idle.hooks[0].command, /state idle --summary idle$/);
|
|
63
72
|
});
|
|
64
73
|
|
|
74
|
+
test("buildClaudeHookSettings splits PostCompact into manual + auto triggers", () => {
|
|
75
|
+
const post = buildClaudeHookSettings({ nodePath: "n", cliPath: "c" }).hooks.PostCompact;
|
|
76
|
+
assert.equal(post.length, 2);
|
|
77
|
+
const manual = post.find((e) => e.matcher === "manual");
|
|
78
|
+
const auto = post.find((e) => e.matcher === "auto");
|
|
79
|
+
assert.match(manual.hooks[0].command, /state idle --summary compacted$/);
|
|
80
|
+
assert.match(auto.hooks[0].command, /state thinking --summary compacted$/);
|
|
81
|
+
});
|
|
82
|
+
|
|
65
83
|
test("buildClaudeHookSettings keeps two non-overlapping PreToolUse matchers", () => {
|
|
66
84
|
const pre = buildClaudeHookSettings({ nodePath: "n", cliPath: "c" }).hooks.PreToolUse;
|
|
67
85
|
assert.equal(pre.length, 2);
|
|
@@ -83,7 +101,7 @@ test("buildClaudeHookSettings includes all subscribed events", () => {
|
|
|
83
101
|
for (const event of [
|
|
84
102
|
"UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure",
|
|
85
103
|
"PermissionRequest", "Notification", "PermissionDenied", "PreCompact",
|
|
86
|
-
"Stop", "StopFailure"
|
|
104
|
+
"PostCompact", "Stop", "StopFailure"
|
|
87
105
|
]) {
|
|
88
106
|
assert.ok(settings.hooks[event], `missing hook event ${event}`);
|
|
89
107
|
}
|