@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.
- package/.claude-plugin/plugin.json +1 -1
- package/.omp-plugin/plugin.json +1 -1
- package/AGENTS.md +42 -9
- package/README.md +24 -8
- package/dist/dispatch.js +38 -8
- package/dist/envoy-channel.js +137 -46
- package/dist/session-hook.js +21 -6
- package/legion/agents/design-outsider.md +44 -0
- package/legion/skills/legion-architect/SKILL.md +2 -1
- package/legion/skills/legion-architect/references/wake-routing.md +1 -1
- package/legion/skills/legion-worker/references/findings.md +3 -3
- package/legion/skills/legion-worker/references/merge-gate.md +1 -1
- package/package.json +1 -1
- package/scripts/build.ts +4 -1
- package/scripts/smoke-clear-rebind.sh +9 -4
- package/skills/envoy/SKILL.md +4 -4
- package/skills/envoy/references/deliveries.md +3 -3
- package/src/envoy-channel-server.ts +151 -49
- package/src/legion-claim.ts +103 -4
- package/src/session-identity.ts +32 -12
package/src/session-identity.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
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
|