A session is never told its session id by any tool, only by a hook's stdin, so it cannot name itself to claude_code_wait. The plugin's SessionStart hook (claude-code session-start <waits-dir>) prints the id and how to give it, plus the session's still-open waits after a /clear, reading the waits file claude_code_wait mirrors directly rather than calling back into this module. Carries the Stop hook's command and the GuardFiles.WaitsDir plumbing MarketplaceWith needs for both (issue 1743 wires the Stop hook's dispatch next).
69 lines
2.5 KiB
Go
69 lines
2.5 KiB
Go
package main
|
|
|
|
// The plugin's SessionStart hook (novox/hq ADR 0489 §1, issue 1744): `claude-code session-start <waits-dir>`,
|
|
// run by the agent's own account on every start, resume, /clear and /compact. The tool runner never tells a
|
|
// session its own id (only a hook's stdin carries session_id), so a session cannot name itself to
|
|
// claude_code_wait until this hook has told it. It also lists the session's still-open waits, so a session
|
|
// that /clear-ed sees what it was watching before.
|
|
//
|
|
// It reads the session's mirrored waits file directly — the same local read the Stop hook makes
|
|
// (readWaitsFile, waits.go) — never calling back into this module: this hook runs on every start, and must
|
|
// not wait on the bus to do it. A missing or malformed file is not an error here: there is simply nothing
|
|
// open to report.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// SessionStartInput is what the hook is given on stdin.
|
|
type SessionStartInput struct {
|
|
SessionID string `json:"session_id"`
|
|
}
|
|
|
|
// SessionStartMessage is the line the hook prints: the session's id and how to give it to claude_code_wait,
|
|
// then each still-open wait from before (1744/AC1, AC2). Pure of IO beyond the one file read.
|
|
func SessionStartMessage(sessionID, waitsDir string, now time.Time) string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "This session's id is %s; give it as `session` to claude_code_wait.\n", sessionID)
|
|
f, err := readWaitsFile(waitsFileIn(waitsDir, sessionID))
|
|
|
|
if err != nil || len(f.Waits) == 0 {
|
|
return b.String()
|
|
}
|
|
b.WriteString("Waits still open from before this session's last start, resume, /clear or /compact:\n")
|
|
for _, w := range f.Waits {
|
|
age := now.Sub(w.At).Round(time.Second)
|
|
state := "no watcher yet"
|
|
if w.Watcher != "" {
|
|
state = "watched"
|
|
}
|
|
if w.PastBound(now) {
|
|
state += ", past its bound"
|
|
if w.Said {
|
|
state += ", said"
|
|
}
|
|
}
|
|
fmt.Fprintf(&b, " %s — %s (%s ago, %s)\n", w.ID, w.What, age, state)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// runSessionStart is the hook: the session's id and its open waits out, exit 0 always (1744/AC3).
|
|
func runSessionStart(waitsDir string, stdin io.Reader, stdout io.Writer) (code int) {
|
|
defer func() {
|
|
if recover() != nil {
|
|
code = 0 // never fails the session's start
|
|
}
|
|
}()
|
|
var in SessionStartInput
|
|
if err := json.NewDecoder(stdin).Decode(&in); err != nil || strings.TrimSpace(in.SessionID) == "" {
|
|
return 0
|
|
}
|
|
fmt.Fprint(stdout, SessionStartMessage(in.SessionID, waitsDir, time.Now()))
|
|
return 0
|
|
}
|