@sjawhar/claude-envoy 1.14.1 → 1.16.0

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.
@@ -4,7 +4,23 @@ import { hasErrnoCode, messageFor } from "@legion/envoy-client/errors"
4
4
  import { z } from "zod"
5
5
  import { pruneStateDirectory, writeAtomicStateFile, writtenLongAgo } from "./session-state"
6
6
 
7
- const PersistedRole = z.object({ session_id: z.string().min(1), role: z.string().min(1) })
7
+ /**
8
+ * The role record on disk: the session the role was claimed for, the role, and the process that
9
+ * wrote the record.
10
+ *
11
+ * `client_instance_id` is the writer's client instance — one Claude Code channel-server process,
12
+ * the same id its registrations and its claims carry. One state directory is read by two
13
+ * processes whenever a second `claude` resumes a live session's id, and the record is keyed by
14
+ * session id alone, so without the writer a process cannot tell its own record from the other's.
15
+ * It is optional because a record written before the field existed carries none: such a record
16
+ * loads and is read as written by no known process (`restoreRole` in the channel server presses
17
+ * it as it always did).
18
+ */
19
+ const PersistedRole = z.object({
20
+ session_id: z.string().min(1),
21
+ role: z.string().min(1),
22
+ client_instance_id: z.string().min(1).optional(),
23
+ })
8
24
 
9
25
  /**
10
26
  * The session id the channel server acts as, held once and passed by reference.
@@ -72,9 +88,10 @@ export class UnparseableRoleRecordError extends Error {
72
88
  }
73
89
 
74
90
  /**
75
- * The role a session held, from its role file; undefined when it has none. Bytes that do not
76
- * parse throw `UnparseableRoleRecordError`; every other failure to read them (`EACCES`, `EISDIR`,
77
- * any I/O error) propagates unchanged.
91
+ * The role a session held, from its role file; undefined when it has none. A record written
92
+ * before the writer was named carries no `client_instance_id` and loads unchanged. Bytes that do
93
+ * not parse throw `UnparseableRoleRecordError`; every other failure to read them (`EACCES`,
94
+ * `EISDIR`, any I/O error) propagates unchanged.
78
95
  */
79
96
  export async function readPersistedRole(
80
97
  roleFile: string,
@@ -137,14 +154,17 @@ export async function pruneStaleSessionHandoffs(stateDirectory: string): Promise
137
154
  * title records follow, except the record of `currentSessionId` — the session running the sweep.
138
155
  *
139
156
  * A record's mtime is when its session was last seen holding the role: every heartbeat on which
140
- * the listener still names the session as holder touches the record (`refreshRoleRecord` in the
141
- * channel server), so this age reaches only a record whose session has not confirmed its role for
142
- * that long — in practice one that stopped running. Only one path removes a role file by hand —
143
- * a soft `setRole` another live session holds off (`claimed: false`, the listener's 409), never
144
- * any answer of the publish route — so a session that holds its role to the end leaves its file
145
- * behind, and the handoff sweep cannot reach it: that one walks `sessions/<pid>/` and asks
146
- * the operating system about a pid, while a role file is keyed by session id and has no process
147
- * left to ask about.
157
+ * the listener still names the session as holder touches the record, and so does every heartbeat
158
+ * a refused process spends pressing its claim again (`refreshRoleRecord` in the channel server),
159
+ * so this age reaches only a record whose session has not confirmed its role for that long — in
160
+ * practice one that stopped running. Only one path removes a role file by hand — a soft
161
+ * `setRole` refused (`claimed: false`, the listener's 409) naming a session other than the one
162
+ * the record is for, which says that session demonstrably no longer holds the role — never any
163
+ * answer of the publish route, and never a refusal naming the record's own session, which is the
164
+ * role held under that id by another process. So a session that holds its role to the end leaves
165
+ * its file behind, and the handoff sweep cannot reach it: that one walks `sessions/<pid>/` and
166
+ * asks the operating system about a pid, while a role file is keyed by session id and has no
167
+ * process left to ask about.
148
168
  *
149
169
  * The sweep runs at startup, before any heartbeat has touched anything, and a session whose claim
150
170
  * the listener refused there has not touched its own record either: age alone would expire the