@sjawhar/pi-legion-envoy 1.27.2 → 1.28.1

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/dist/envoy.js CHANGED
@@ -29746,7 +29746,7 @@ var MessageEventPayloadSchema = object({
29746
29746
  });
29747
29747
  var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
29748
29748
  id: string2(),
29749
- issue_key: string2(),
29749
+ issue_key: string2().nullable(),
29750
29750
  author: object({ kind: string2(), id: string2() }),
29751
29751
  body: string2(),
29752
29752
  target: string2(),
@@ -29759,6 +29759,7 @@ var MessageDeliveryEventPayloadSchema = object({
29759
29759
  attempt: number2().int().positive().optional(),
29760
29760
  delivery: _enum2(["btw", "aside", "steer"]).optional(),
29761
29761
  session_id: string2().optional(),
29762
+ target: string2().optional(),
29762
29763
  title: string2().optional(),
29763
29764
  state: _enum2(["sent", "failed"]).optional(),
29764
29765
  error: string2().optional()
@@ -31095,7 +31096,7 @@ var DispatchTargetedFrameSchema = object({
31095
31096
  }).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
31096
31097
  var RecoverableDispatchDeliveryFailureSchema = object({
31097
31098
  event: object({
31098
- issue_key: string2(),
31099
+ issue_key: string2().nullable(),
31099
31100
  payload: object({ id: string2(), body: string2().optional() }).passthrough()
31100
31101
  }).passthrough(),
31101
31102
  delivery: DispatchDeliveryRequestSchema
@@ -31298,7 +31299,7 @@ function renderInbound(raw, sessionID, subject) {
31298
31299
  askQuestion = dispatchAskQuestion(frame.event);
31299
31300
  messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31300
31301
  dispatchReply = dispatchCommentReplyWith(frame.event, subject ?? envelope.topic);
31301
- if (frame.event.type === "message.created" && frame.event.issue_key !== null) {
31302
+ if (frame.event.type === "message.created") {
31302
31303
  const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
31303
31304
  const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
31304
31305
  if (message.success && requested.success && message.data.id !== undefined) {
@@ -31309,7 +31310,9 @@ function renderInbound(raw, sessionID, subject) {
31309
31310
  issueKey: frame.event.issue_key,
31310
31311
  body: message.data.body ?? envelope.payload_summary ?? ""
31311
31312
  };
31312
- dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31313
+ if (frame.event.issue_key !== null) {
31314
+ dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31315
+ }
31313
31316
  }
31314
31317
  }
31315
31318
  dispatchEvent = {
@@ -32614,17 +32617,18 @@ async function executeDispatchTool(input) {
32614
32617
  }
32615
32618
  case "dispatch_message": {
32616
32619
  const inReplyTo = messageInReplyTo(args);
32617
- const message = await client.message(issue(), {
32620
+ const issueKey = issue();
32621
+ const message = await client.message(issueKey, {
32618
32622
  body: stringArg(args, "body"),
32619
32623
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
32620
32624
  actor
32621
32625
  });
32622
- const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
32626
+ const messageRef = `dispatch://${issueKey}/message/${message.id}`;
32623
32627
  return {
32624
32628
  text: `Posted message ${message.id} (${messageRef})`,
32625
32629
  details: {
32626
- issue: message.issue_key,
32627
- topic: dispatchIssueSubject(message.issue_key, ">"),
32630
+ issue: issueKey,
32631
+ topic: dispatchIssueSubject(issueKey, ">"),
32628
32632
  message: message.id
32629
32633
  }
32630
32634
  };
@@ -33396,6 +33400,13 @@ function envoyExtension(pi) {
33396
33400
  };
33397
33401
  const deliver = async (subject, raw, reply) => {
33398
33402
  const rendered = renderInbound(raw, sessionID, subject);
33403
+ if (reply !== "" && subject === agentSubject(sessionID)) {
33404
+ try {
33405
+ (await ensureConnection()).publish(reply);
33406
+ } catch (error) {
33407
+ console.warn(`[envoy] failed to acknowledge envelope ${rendered.envelope?.event_id ?? "unknown"}`, error);
33408
+ }
33409
+ }
33399
33410
  for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
33400
33411
  closeIntentionally(topic);
33401
33412
  const dedupeKey = rendered.envelope?.dedupe_key;
@@ -33452,9 +33463,6 @@ function envoyExtension(pi) {
33452
33463
  }
33453
33464
  }
33454
33465
  }
33455
- if (reply !== "" && subject === agentSubject(sessionID)) {
33456
- (await ensureConnection()).publish(reply);
33457
- }
33458
33466
  };
33459
33467
  const RESUBSCRIBE_DELAY_MS = Number(process.env.ENVOY_RESUBSCRIBE_DELAY_MS ?? "") || 5000;
33460
33468
  let shuttingDown = false;
package/dist/legion.js CHANGED
@@ -29746,7 +29746,7 @@ var MessageEventPayloadSchema = object({
29746
29746
  });
29747
29747
  var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
29748
29748
  id: string2(),
29749
- issue_key: string2(),
29749
+ issue_key: string2().nullable(),
29750
29750
  author: object({ kind: string2(), id: string2() }),
29751
29751
  body: string2(),
29752
29752
  target: string2(),
@@ -29759,6 +29759,7 @@ var MessageDeliveryEventPayloadSchema = object({
29759
29759
  attempt: number2().int().positive().optional(),
29760
29760
  delivery: _enum2(["btw", "aside", "steer"]).optional(),
29761
29761
  session_id: string2().optional(),
29762
+ target: string2().optional(),
29762
29763
  title: string2().optional(),
29763
29764
  state: _enum2(["sent", "failed"]).optional(),
29764
29765
  error: string2().optional()
@@ -31548,7 +31549,7 @@ var DispatchTargetedFrameSchema = object({
31548
31549
  }).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
31549
31550
  var RecoverableDispatchDeliveryFailureSchema = object({
31550
31551
  event: object({
31551
- issue_key: string2(),
31552
+ issue_key: string2().nullable(),
31552
31553
  payload: object({ id: string2(), body: string2().optional() }).passthrough()
31553
31554
  }).passthrough(),
31554
31555
  delivery: DispatchDeliveryRequestSchema
@@ -31751,7 +31752,7 @@ function renderInbound(raw, sessionID, subject) {
31751
31752
  askQuestion = dispatchAskQuestion(frame.event);
31752
31753
  messageReplyPreview = dispatchMessageReplyPreview(frame.event);
31753
31754
  dispatchReply = dispatchCommentReplyWith(frame.event, subject ?? envelope.topic);
31754
- if (frame.event.type === "message.created" && frame.event.issue_key !== null) {
31755
+ if (frame.event.type === "message.created") {
31755
31756
  const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
31756
31757
  const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
31757
31758
  if (message.success && requested.success && message.data.id !== undefined) {
@@ -31762,7 +31763,9 @@ function renderInbound(raw, sessionID, subject) {
31762
31763
  issueKey: frame.event.issue_key,
31763
31764
  body: message.data.body ?? envelope.payload_summary ?? ""
31764
31765
  };
31765
- dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31766
+ if (frame.event.issue_key !== null) {
31767
+ dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
31768
+ }
31766
31769
  }
31767
31770
  }
31768
31771
  dispatchEvent = {
@@ -33045,17 +33048,18 @@ async function executeDispatchTool(input) {
33045
33048
  }
33046
33049
  case "dispatch_message": {
33047
33050
  const inReplyTo = messageInReplyTo(args);
33048
- const message = await client.message(issue(), {
33051
+ const issueKey = issue();
33052
+ const message = await client.message(issueKey, {
33049
33053
  body: stringArg(args, "body"),
33050
33054
  ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
33051
33055
  actor
33052
33056
  });
33053
- const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
33057
+ const messageRef = `dispatch://${issueKey}/message/${message.id}`;
33054
33058
  return {
33055
33059
  text: `Posted message ${message.id} (${messageRef})`,
33056
33060
  details: {
33057
- issue: message.issue_key,
33058
- topic: dispatchIssueSubject(message.issue_key, ">"),
33061
+ issue: issueKey,
33062
+ topic: dispatchIssueSubject(issueKey, ">"),
33059
33063
  message: message.id
33060
33064
  }
33061
33065
  };
@@ -33819,6 +33823,13 @@ function envoyExtension(pi) {
33819
33823
  };
33820
33824
  const deliver = async (subject, raw, reply) => {
33821
33825
  const rendered = renderInbound(raw, sessionID, subject);
33826
+ if (reply !== "" && subject === agentSubject(sessionID)) {
33827
+ try {
33828
+ (await ensureConnection()).publish(reply);
33829
+ } catch (error) {
33830
+ console.warn(`[envoy] failed to acknowledge envelope ${rendered.envelope?.event_id ?? "unknown"}`, error);
33831
+ }
33832
+ }
33822
33833
  for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
33823
33834
  closeIntentionally(topic);
33824
33835
  const dedupeKey = rendered.envelope?.dedupe_key;
@@ -33875,9 +33886,6 @@ function envoyExtension(pi) {
33875
33886
  }
33876
33887
  }
33877
33888
  }
33878
- if (reply !== "" && subject === agentSubject(sessionID)) {
33879
- (await ensureConnection()).publish(reply);
33880
- }
33881
33889
  };
33882
33890
  const RESUBSCRIBE_DELAY_MS = Number(process.env.ENVOY_RESUBSCRIBE_DELAY_MS ?? "") || 5000;
33883
33891
  let shuttingDown = false;
@@ -148,11 +148,16 @@ not construct a token from a partial issue reference.
148
148
 
149
149
  ## Legion exception lane
150
150
 
151
- `no_holder` and `delivery_failed` for a Legion role are daemon liveness signals, not reasons to
152
- add a second subscriber or manually retry. For example, treat a `delivery_failed` wake as the
153
- daemon's responsibility to revive or recreate the backing worker and re-deliver; otherwise it
154
- resurrects the root process with derived catch-up and the workspace handoffs. Raw delivery failures
155
- stay out of architect context.
151
+ `no_holder`, `delivery_failed`, and `receipt_timeout` for a Legion role are daemon liveness
152
+ signals, not reasons to add a second subscriber or manually retry. `no_holder`: no live session
153
+ holds the role. `delivery_failed`: the message was not forwarded at all — the holder lookup
154
+ failed, the holder was stale, or the publish errored. `receipt_timeout`: the listener forwarded
155
+ the message to a registered, live holder and no receipt arrived inside the window; its payload
156
+ keeps `original_topic`, `event_id`, `payload`, and the original `dedupe_key`, so the daemon can
157
+ re-send the same message and the receiver's dedupe drops a copy that did arrive. Treat any of
158
+ these wakes as the daemon's responsibility to revive or recreate the backing worker and
159
+ re-deliver; otherwise it resurrects the root process with derived catch-up and the workspace
160
+ handoffs. Raw delivery failures stay out of architect context.
156
161
 
157
162
  ## Slack
158
163
 
@@ -45,7 +45,7 @@ before step 3. The design gate is not a substitute for review and retro.
45
45
 
46
46
  1. Re-read the issue, its acceptance criteria, the PR, test evidence, and review evidence.
47
47
  Confirm the PR carries both proofs: the implementer's own `E2E (implementer)` line and the tester's `E2E (tester)` line,
48
- each naming a production-like surface (a scratch daemon, a smoke rig, a sandbox repository, a devN
48
+ each naming a production-like surface (the daemon's test harness (`packages/daemon/src/daemon/__tests__/`) and real-process fixtures; a live check at the operator's next daemon restart, recorded on the PR; a sandbox repository, a devN
49
49
  stack, staging, or a local stack with real migrations), a command or run id, an observation, a head
50
50
  SHA, and a negative control. If either is missing, or links only a unit suite, the retro's first
51
51
  durable learning is that gap and the issue goes back — to the implementer for its own proof, to the
@@ -251,7 +251,7 @@ PR opens, and every later phase keeps it current rather than replacing it:
251
251
  ```
252
252
  ## Verification
253
253
 
254
- **CI:** `Tests` run <run-id> — jobs lint, pr-title, typecheck, test all success at <head-sha>.
254
+ **CI:** `Tests` run <run-id> — jobs lint, typecheck, test all success at <head-sha>; `PR Title` run <run-id> — job pr-title success at <head-sha>.
255
255
 
256
256
  **Threads:** <n> resolved, 0 unresolved. Each disposed individually, never in bulk:
257
257
  - Thread <id>: fixed in <commit-sha> — <one line>.
@@ -276,6 +276,7 @@ Verified the implementer's proof by <re-running its command | driving the same s
276
276
  **Fast-follow:** <one named cleanup item and where it will land>, or "none".
277
277
 
278
278
  **Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
279
+ **Retarget:** Retargeting a pull request to a new base does not re-run Tests; after a retarget, rebase onto the new base and push — the new head runs Tests against the new merge result — and cite that run in the PR body.
279
280
  ```
280
281
 
281
282
  - **Threads are dispositioned individually, never resolved in bulk.** Every open review
@@ -301,11 +302,11 @@ Verified the implementer's proof by <re-running its command | driving the same s
301
302
  changes behavior, hides an error, or breaks a gate is fixed here — never deferred.
302
303
  Findings about naming, duplication, or wording are batched into the single `Fast-follow`
303
304
  line instead of iterating per push.
304
- - **Rebase only on a real conflict.** Sami, 2026-09-11, verbatim:
305
+ - **Rebase only on a real conflict, except after a base retarget.** Sami, 2026-09-11, verbatim:
305
306
  "Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
306
- The implementer rebases the issue branch only when GitHub reports it `CONFLICTING` or the
307
- controller asks because of a conflict — never to pick up `main` or to refresh CI. A single
308
- failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
307
+ The implementer rebases the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
308
+ because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never rebase to
309
+ pick up `main` or refresh CI. A single failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
309
310
  pushing a new commit. A conflict-forced rebase that leaves the branch's diff unchanged is a
310
311
  confirmation, not a new round (see *The unchanged-diff check* below). Before rebasing, record
311
312
  the fingerprint at the current tip; after pushing the rebased branch, record it at the new
@@ -317,9 +318,10 @@ Verified the implementer's proof by <re-running its command | driving the same s
317
318
  field names naming, duplication, or wording cleanup only; anything that changes behaviour,
318
319
  hides an error, or breaks a gate lands in this PR.
319
320
  - **The implementer proves the change before its phase completes, and writes the `E2E (implementer)` line when the pull request opens.**
320
- The proof is the changed behaviour exercised on the surface a user reaches it through — a
321
- scratch daemon, a smoke rig, a sandbox repository, a real browser, a devN stack, a local stack
322
- with real migrations — with the exact command or run id, what was observed, the head SHA, and
321
+ The proof is the changed behaviour exercised on the surface a user reaches it through — the daemon's
322
+ test harness (`packages/daemon/src/daemon/__tests__/`) and real-process fixtures; a live check at the
323
+ operator's next daemon restart, recorded on the PR; a sandbox repository, a real browser, a devN stack,
324
+ or a local stack with real migrations — with the exact command or run id, what was observed, the head SHA,
323
325
  one negative control. The same proof goes into `.legion/implement.json` as its required `proof`
324
326
  array (`legion handoff write --phase implement` refuses a payload without one and names the
325
327
  field), and into the PR body, because the reviewer and the merger verify facts on GitHub and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "1.27.2",
3
+ "version": "1.28.1",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [