@sjawhar/opencode-legion-envoy 0.18.1 → 0.19.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.
@@ -14986,6 +14986,11 @@ var RoleWireSchema = object({
14986
14986
  holder: string2(),
14987
14987
  last_seen: number2().int()
14988
14988
  });
14989
+ var RoleHeldWireSchema = object({
14990
+ error: string2(),
14991
+ role: string2(),
14992
+ holder: string2()
14993
+ });
14989
14994
  var ErrorWireSchema = object({
14990
14995
  error: string2(),
14991
14996
  expected: array(string2()).optional()
@@ -15118,7 +15123,28 @@ function createEnvoyClient(config) {
15118
15123
  unregisterSession: async (sessionID) => {
15119
15124
  await request(`/v1/sessions/${encodeURIComponent(sessionID)}`, { method: "DELETE" });
15120
15125
  },
15121
- setRole: async (input) => InterestWireSchema.parse(JSON.parse(await post("/v1/roles/set", { session_id: input.sessionID, role: input.role }))),
15126
+ setRole: async (input) => {
15127
+ const soft = input.soft === true;
15128
+ const previous = soft ? input.previousSessionID ?? "" : "";
15129
+ const body = {
15130
+ session_id: input.sessionID,
15131
+ role: input.role,
15132
+ ...soft ? { soft: true } : {},
15133
+ ...previous === "" ? {} : { previous_session_id: previous }
15134
+ };
15135
+ let raw;
15136
+ try {
15137
+ raw = await post("/v1/roles/set", body);
15138
+ } catch (error) {
15139
+ if (error instanceof EnvoyApiError && error.details.status === 409) {
15140
+ const held = RoleHeldWireSchema.safeParse(JSON.parse(error.details.responseBody));
15141
+ if (held.success)
15142
+ return { claimed: false, holder: held.data.holder };
15143
+ }
15144
+ throw error;
15145
+ }
15146
+ return { claimed: true, interest: InterestWireSchema.parse(JSON.parse(raw)) };
15147
+ },
15122
15148
  getRole: async (role) => RoleWireSchema.parse(JSON.parse(await request(`/v1/roles/${encodeURIComponent(role)}`, { method: "GET" }))),
15123
15149
  listSessions: async (input = {}) => {
15124
15150
  const search = new URLSearchParams;
@@ -15499,7 +15525,13 @@ var server_default = async (input) => {
15499
15525
  args: roleSetSpec.arguments(zodSchemaApi(tool.schema)),
15500
15526
  async execute(args, ctx) {
15501
15527
  ctx.metadata({ title: "Set Envoy role" });
15502
- return JSON.stringify(await envoy.setRole({ sessionID: ctx.sessionID, role: args.role }));
15528
+ const result = await envoy.setRole({
15529
+ sessionID: ctx.sessionID,
15530
+ role: args.role
15531
+ });
15532
+ if (!result.claimed)
15533
+ throw new Error(`role ${args.role} is held by ${result.holder}`);
15534
+ return JSON.stringify(result.interest);
15503
15535
  }
15504
15536
  }),
15505
15537
  envoy_whoami: tool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -123,6 +123,21 @@ Publish to a role; do not subscribe as its holder. A successful `envoy_publish`
123
123
  its live `holder`; an unheld role returns an error. Use `envoy_role_get(role="reviewer")` to find
124
124
  the live holder first when you need one.
125
125
 
126
+ A claim belongs to the session, not the process. `omp --resume` re-claims the role recorded in
127
+ the session transcript, including after the listener's stale-interest reaper has removed the old
128
+ row. A `/fork`, `/branch`, `/handoff`, or other transcript-carrying switch checks the
129
+ previous id and moves any held role to the new id while establishing the new session. A `/new`
130
+ or `/resume` switch installs an unrelated transcript and carries nothing. A process that dies and
131
+ is never resumed does not stay a live holder; once liveness expires, role publishes return
132
+ `no holder` until that transcript resumes or another session claims the role.
133
+
134
+ Automatic reclaim never steals. It is a *soft* claim, which the listener grants only when the role
135
+ is unheld, held by a session that is no longer live, or held by the id this session continues (a
136
+ fork's parent). If a different live session holds it — the parent of a `/fork` whose role moved to
137
+ the child, or a second process on the same transcript — the listener refuses atomically and the
138
+ resumed session logs a warning and holds nothing. Only an explicit `envoy_role_set` is
139
+ last-claim-wins. Re-running `envoy_role_set` for a role this session already holds is harmless.
140
+
126
141
  ## Legion role claims
127
142
 
128
143
  Legion agents receive through a daemon-minted role token. Claim the assigned role with
package/src/server.ts CHANGED
@@ -371,9 +371,13 @@ export default async (input: { serverUrl: URL }) => {
371
371
  args: roleSetSpec.arguments(zodSchemaApi(tool.schema)) as never,
372
372
  async execute(args, ctx) {
373
373
  ctx.metadata({ title: "Set Envoy role" });
374
- return JSON.stringify(
375
- await envoy.setRole({ sessionID: ctx.sessionID, role: args.role as string })
376
- );
374
+ const result = await envoy.setRole({
375
+ sessionID: ctx.sessionID,
376
+ role: args.role as string,
377
+ });
378
+ // A hard claim always lands; the discriminant exists for soft claims.
379
+ if (!result.claimed) throw new Error(`role ${args.role} is held by ${result.holder}`);
380
+ return JSON.stringify(result.interest);
377
381
  },
378
382
  }),
379
383
  envoy_whoami: tool({