@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)
@@ -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>`, reported
263
- to the daemon over the IPC pipe. `SubagentStop` is intentionally ignored because
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hayasaka7/haya-pet",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "description": "Generic AI CLI pet runtime foundation.",
6
6
  "keywords": [
@@ -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
  }