@sjawhar/pi-legion-envoy 6.0.1 → 6.0.3

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
@@ -30360,13 +30360,13 @@ var dispatchToolSpecs = [
30360
30360
  },
30361
30361
  {
30362
30362
  name: "dispatch_request_approval",
30363
- example: { issue: "DSP-1", summary: "Proposes a live sync in place of the nightly export." },
30364
- description: "Ask a human to approve a document at its current version. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. A later version makes an approval stale, " + "and writing it retracts an open request for an older version; request again for the new " + "one. A repeat at the version an open request names returns that request unchanged. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
30363
+ example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
30364
+ description: "Ask a human to approve a document at its current version. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. A later version carries the same open " + "request forward and leaves it waiting on you; once the revision is complete and the human " + "has agreed to every point in it, call this again to hand that request back. The request " + "carries nothing new. A call while it already waits on the human hands nothing back: the " + "same summary changes nothing, and a different one is refused, since it would rewrite the " + "card the human is reading. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
30365
30365
  arguments: (z2) => ({
30366
30366
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30367
30367
  project: z2.string().describe("Project key owning the document.").optional(),
30368
30368
  artifact: z2.string().describe("Project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
30369
- summary: z2.string({ min: 1 }).describe("The proposals in this version the human hasn't already agreed to, in one to three sentences.")
30369
+ summary: z2.string({ min: 1 }).describe("What the human is approving, in one to three sentences, and nothing else: no commentary on itself or the conversation, and no question. Request approval only once the human has agreed to every point in the document.")
30370
30370
  }),
30371
30371
  validation: documentOwnerValidation(true)
30372
30372
  },
@@ -31819,6 +31819,7 @@ var DISPATCH_PAYLOAD_SCHEMAS = {
31819
31819
  "ask.opened": AskEventPayloadSchema,
31820
31820
  "ask.anchor_refreshed": AskEventPayloadSchema,
31821
31821
  "ask.edited": AskEditedEventPayloadSchema,
31822
+ "ask.handed_back": AskEventPayloadSchema,
31822
31823
  "ask.answered": AskEventPayloadSchema,
31823
31824
  "ask.resolved": AskEventPayloadSchema,
31824
31825
  "comment.created": CommentEventPayloadSchema,
@@ -32701,7 +32702,8 @@ class DispatchClient {
32701
32702
  return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
32702
32703
  }
32703
32704
  async requestApproval(artifactID, input) {
32704
- return this.#json("POST", ["api", "v1", "artifacts", artifactID, "approval-requests"], input);
32705
+ const answer = await this.#jsonAnswer("POST", ["api", "v1", "artifacts", artifactID, "approval-requests"], input);
32706
+ return { ...answer.payload, recorded: answer.status === 201 };
32705
32707
  }
32706
32708
  async editAsk(id, input) {
32707
32709
  return this.#json("PATCH", ["api", "v1", "asks", id], input);
@@ -32846,6 +32848,9 @@ class DispatchClient {
32846
32848
  }
32847
32849
  }
32848
32850
  async#json(method, path2, body, query) {
32851
+ return (await this.#jsonAnswer(method, path2, body, query)).payload;
32852
+ }
32853
+ async#jsonAnswer(method, path2, body, query) {
32849
32854
  const headers = {
32850
32855
  Accept: "application/json",
32851
32856
  Authorization: `Bearer ${this.token}`
@@ -32859,7 +32864,7 @@ class DispatchClient {
32859
32864
  signal: this.#signal,
32860
32865
  ...body === undefined ? {} : { body: JSON.stringify(body) }
32861
32866
  });
32862
- return this.#response(method, url2, response);
32867
+ return { status: response.status, payload: await this.#response(method, url2, response) };
32863
32868
  }
32864
32869
  async#form(method, path2, body) {
32865
32870
  const url2 = this.#url(path2);
@@ -33704,8 +33709,10 @@ function approvalLine(artifact) {
33704
33709
  if (approval === undefined || approval.state === "draft")
33705
33710
  return;
33706
33711
  switch (approval.state) {
33707
- case "awaiting":
33708
- return `Approval: awaiting (requested by ${approval.requested_by?.id ?? "unknown"}, ask ${approval.ask_id ?? "?"})`;
33712
+ case "awaiting": {
33713
+ const turn = approval.waiting_on === undefined ? "" : `, waiting on ${approval.waiting_on}`;
33714
+ return `Approval: awaiting${turn} (requested by ${approval.requested_by?.id ?? "unknown"}, ask ${approval.ask_id ?? "?"})`;
33715
+ }
33709
33716
  case "approved":
33710
33717
  return `Approval: approved v${approval.version} by ${approval.by?.id ?? "unknown"}`;
33711
33718
  case "stale":
@@ -33845,6 +33852,7 @@ function eventHead(event) {
33845
33852
  case "ask.opened":
33846
33853
  case "ask.anchor_refreshed":
33847
33854
  case "ask.edited":
33855
+ case "ask.handed_back":
33848
33856
  case "ask.resolved":
33849
33857
  return textHead(event.payload.question);
33850
33858
  case "ask.answered":
@@ -34592,7 +34600,7 @@ ${followsAsk(askOwner)}`,
34592
34600
  replyToOwnAsk: replyToAsk !== undefined && (comment.advice?.your_open_asks?.some((ask) => ask.id === replyToAsk) ?? false)
34593
34601
  });
34594
34602
  if (replyToAsk !== undefined) {
34595
- const askState = comment.turn === null ? "" : `; ask now waiting on ${comment.turn}`;
34603
+ const askState = comment.ask_waiting_on === undefined ? "" : `; ask now waiting on ${comment.ask_waiting_on}`;
34596
34604
  return {
34597
34605
  text: [
34598
34606
  `Replied on ask ${replyToAsk} (comment ${comment.id}${askState}). ${followsAsk(commentOwner2)}`,
@@ -34603,7 +34611,7 @@ ${followsAsk(askOwner)}`,
34603
34611
  ...commentDetails,
34604
34612
  ask: replyToAsk,
34605
34613
  follows: { ask: replyToAsk },
34606
- ...comment.turn === null ? {} : { ask_waiting_on: comment.turn },
34614
+ ...comment.ask_waiting_on === undefined ? {} : { ask_waiting_on: comment.ask_waiting_on },
34607
34615
  ...comment.advice === undefined ? {} : { advice: comment.advice }
34608
34616
  }
34609
34617
  };
@@ -34776,8 +34784,9 @@ ${trailer.join(`
34776
34784
  };
34777
34785
  }
34778
34786
  const details = await followedAskDetails(client, result.ask, resolved.artifact);
34787
+ const outcome = result.recorded ? `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}).` : `The approval request for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}) already waits on the human, so this call changed nothing: nothing since it last reached the human (a newer version, a human's reply in its thread, or your progress note) left it waiting on you.`;
34779
34788
  return {
34780
- text: `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}). The human's Inbox asks: ${JSON.stringify(result.ask.question)}. The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
34789
+ text: `${outcome} The human's Inbox asks: ${JSON.stringify(result.ask.question)}. The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
34781
34790
  details: { ...details, artifact: resolved.artifact.id, version: result.version }
34782
34791
  };
34783
34792
  }
@@ -35454,7 +35463,7 @@ function sourceSession(sessionID) {
35454
35463
  }
35455
35464
 
35456
35465
  // extensions/envoy.ts
35457
- import { logger } from "@oh-my-pi/pi-utils";
35466
+ import { logger as logger2 } from "@oh-my-pi/pi-utils";
35458
35467
  var import_nats2 = __toESM(require_mod4(), 1);
35459
35468
 
35460
35469
  // src/agent-stream.ts
@@ -35932,33 +35941,56 @@ function sideTurn(pi, context) {
35932
35941
  import fs from "fs";
35933
35942
  import path3 from "path";
35934
35943
  import * as host from "@oh-my-pi/pi-coding-agent";
35944
+ import { logger } from "@oh-my-pi/pi-utils";
35935
35945
  var LEGION_BOOTSTRAPPED_SESSION = Symbol.for("legion.pi-envoy.bootstrapped-session");
35936
35946
  var bootstrappedSessionStore = globalThis;
35937
35947
  function recordBootstrappedSession(sessionFile) {
35938
35948
  bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION] = sessionFile;
35939
35949
  }
35940
35950
  async function isSubagentSession(context) {
35941
- await context.sessionManager.ensureOnDisk();
35951
+ const registered = registeredSubagent(context);
35952
+ if (registered !== undefined)
35953
+ return { subagent: registered, settled: true };
35954
+ try {
35955
+ await context.sessionManager.ensureOnDisk();
35956
+ } catch (error48) {
35957
+ const subagent = transcriptSaysSubagent(context);
35958
+ logger.warn("subagent check: publishing the transcript failed; answered from the transcript on disk, and the next call asks again", { sessionFile: context.sessionManager.getSessionFile(), subagent, error: messageFor(error48) });
35959
+ return { subagent, settled: false };
35960
+ }
35961
+ return { subagent: transcriptSaysSubagent(context), settled: true };
35962
+ }
35963
+ function transcriptSaysSubagent(context) {
35942
35964
  const sessionFile = context.sessionManager.getSessionFile();
35943
- if (sessionFile !== undefined && fs.existsSync(sessionFile)) {
35944
- return fs.existsSync(`${path3.dirname(sessionFile)}.jsonl`);
35965
+ if (sessionFile !== undefined) {
35966
+ if (fs.existsSync(`${path3.dirname(sessionFile)}.jsonl`))
35967
+ return true;
35968
+ if (fs.existsSync(sessionFile))
35969
+ return false;
35945
35970
  }
35946
35971
  const bootstrapped = bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION];
35947
35972
  return bootstrapped !== undefined && bootstrapped !== sessionFile;
35948
35973
  }
35949
- function isRegisteredSubagent(context) {
35974
+ function registeredSubagent(context) {
35950
35975
  const registry2 = host.AgentRegistry?.global();
35951
35976
  if (registry2 === undefined)
35952
- return false;
35977
+ return;
35953
35978
  const sessionID = context.sessionManager.getSessionId();
35954
35979
  const sessionFile = context.sessionManager.getSessionFile();
35955
- return registry2.list().some((ref) => ref.kind !== "main" && (ref.session === null ? sessionFile !== undefined && ref.sessionFile === sessionFile : ref.session.sessionManager.getSessionId() === sessionID));
35980
+ const own = registry2.list().filter((ref) => ref.session === null ? sessionFile !== undefined && ref.sessionFile === sessionFile : ref.session.sessionManager.getSessionId() === sessionID);
35981
+ if (own.length === 0)
35982
+ return;
35983
+ return own.some((ref) => ref.kind !== "main");
35956
35984
  }
35957
35985
  function subagentSessionCheck() {
35958
- let subagentSession;
35959
- return (context) => {
35960
- subagentSession ??= isSubagentSession(context);
35961
- return subagentSession;
35986
+ let settled;
35987
+ return async (context) => {
35988
+ if (settled !== undefined)
35989
+ return settled;
35990
+ const answer = await isSubagentSession(context);
35991
+ if (answer.settled)
35992
+ settled = answer.subagent;
35993
+ return answer.subagent;
35962
35994
  };
35963
35995
  }
35964
35996
 
@@ -36118,7 +36150,7 @@ function resolveSkillsDirectory() {
36118
36150
  var SKILLS_DIRECTORY = resolveSkillsDirectory();
36119
36151
  var DISPATCH_FIRST_CONTEXT = readDispatchFirstContext(dispatchFirstSkillFile(SKILLS_DIRECTORY));
36120
36152
  function envoyExtension(pi) {
36121
- logger.debug("extension instance loaded", { extension: import.meta.url });
36153
+ logger2.debug("extension instance loaded", { extension: import.meta.url });
36122
36154
  const defaults = envoyDefaultsFromEnvironment(process.env);
36123
36155
  const dispatchConfig = resolveDispatchConfig(process.env, { cwd: process.cwd() });
36124
36156
  const currentDispatchConfig = () => {
@@ -36181,7 +36213,7 @@ function envoyExtension(pi) {
36181
36213
  if (selfCheckWarningSessionIDs.has(failedSessionID))
36182
36214
  return;
36183
36215
  selfCheckWarningSessionIDs.add(failedSessionID);
36184
- logger.warn("envoy: run-end waiting self-check failed; no reminder was sent", {
36216
+ logger2.warn("envoy: run-end waiting self-check failed; no reminder was sent", {
36185
36217
  sessionID: failedSessionID,
36186
36218
  error: messageFor(error48)
36187
36219
  });
@@ -36249,7 +36281,7 @@ function envoyExtension(pi) {
36249
36281
  const { delivery } = rendered;
36250
36282
  const key = handledAttemptKey(delivery.id, delivery.attempt);
36251
36283
  if (handledDispatchAttempts.has(key)) {
36252
- logger.warn("envoy: a frame names a Dispatch attempt this session already delivered; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id });
36284
+ logger2.warn("envoy: a frame names a Dispatch attempt this session already delivered; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id });
36253
36285
  return;
36254
36286
  }
36255
36287
  handledDispatchAttempts.add(key);
@@ -36263,11 +36295,11 @@ function envoyExtension(pi) {
36263
36295
  });
36264
36296
  const turn = turnFromAccept(accepted);
36265
36297
  if (turn === undefined) {
36266
- logger.warn("envoy: Dispatch accepted a direct message without the stored body and mode to inject; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id });
36298
+ logger2.warn("envoy: Dispatch accepted a direct message without the stored body and mode to inject; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id });
36267
36299
  }
36268
36300
  return turn;
36269
36301
  } catch (error48) {
36270
- logger.warn("envoy: Dispatch did not accept a direct message as this session's turn; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id, error: messageFor(error48) });
36302
+ logger2.warn("envoy: Dispatch did not accept a direct message as this session's turn; it arrives as a card", { attempt: delivery.attempt, messageID: delivery.id, error: messageFor(error48) });
36271
36303
  return;
36272
36304
  }
36273
36305
  };
@@ -36426,7 +36458,7 @@ function envoyExtension(pi) {
36426
36458
  continue;
36427
36459
  connection?.publish(reply, codec2.encode(answer));
36428
36460
  } catch (error48) {
36429
- logger.warn("envoy: agent stream control message failed", {
36461
+ logger2.warn("envoy: agent stream control message failed", {
36430
36462
  error: messageFor(error48),
36431
36463
  sessionID: session
36432
36464
  });
@@ -36539,7 +36571,7 @@ function envoyExtension(pi) {
36539
36571
  }
36540
36572
  if (!result.claimed) {
36541
36573
  endRole();
36542
- logger.warn("envoy: role re-assertion refused; held by another live session", {
36574
+ logger2.warn("envoy: role re-assertion refused; held by another live session", {
36543
36575
  role,
36544
36576
  sessionID: id,
36545
36577
  holder: result.holder
@@ -36547,7 +36579,7 @@ function envoyExtension(pi) {
36547
36579
  context.ui.notify(`envoy: role ${role} is now held by session ${result.holder}; this session no longer holds it`, "warning");
36548
36580
  return;
36549
36581
  }
36550
- logger.warn("envoy: role re-asserted after the listener lost the claim", {
36582
+ logger2.warn("envoy: role re-asserted after the listener lost the claim", {
36551
36583
  role,
36552
36584
  sessionID: id,
36553
36585
  previousHolder: holder ?? null
@@ -36558,7 +36590,7 @@ function envoyExtension(pi) {
36558
36590
  if (regained === undefined)
36559
36591
  return;
36560
36592
  Promise.resolve().then(() => regained(role, reason)).catch((error48) => {
36561
- logger.warn("envoy: role regain hook failed", { role, reason, error: messageFor(error48) });
36593
+ logger2.warn("envoy: role regain hook failed", { role, reason, error: messageFor(error48) });
36562
36594
  });
36563
36595
  };
36564
36596
  const ensureHeartbeat = (context) => {
@@ -36615,7 +36647,7 @@ function envoyExtension(pi) {
36615
36647
  previousSessionID: options.previousSessionID
36616
36648
  });
36617
36649
  if (!result.claimed) {
36618
- logger.warn("envoy: role held by another live session; not reclaimed", {
36650
+ logger2.warn("envoy: role held by another live session; not reclaimed", {
36619
36651
  role,
36620
36652
  sessionID,
36621
36653
  holder: result.holder
@@ -36670,7 +36702,7 @@ function envoyExtension(pi) {
36670
36702
  return;
36671
36703
  const continued = carryPreviousSessionRole && previousSessionID !== "" && previousSessionID !== sessionID ? previousSessionID : undefined;
36672
36704
  await setEnvoyRole(role, { soft: true, previousSessionID: continued }).catch((error48) => {
36673
- logger.warn("envoy: role reclaim failed", { role, sessionID, error: messageFor(error48) });
36705
+ logger2.warn("envoy: role reclaim failed", { role, sessionID, error: messageFor(error48) });
36674
36706
  });
36675
36707
  };
36676
36708
  let sessionEstablishment = Promise.resolve();
@@ -36746,12 +36778,11 @@ function envoyExtension(pi) {
36746
36778
  sessionID: () => activeSessionContext?.sessionManager.getSessionId() ?? sessionID
36747
36779
  };
36748
36780
  bridge.instances.push(claimInstance);
36749
- const isSubagentTranscript = subagentSessionCheck();
36750
- const isSubagent = async (context) => await isSubagentTranscript(context) || isRegisteredSubagent(context);
36751
- const replyAddress = async (context, liveSessionID = "") => {
36781
+ const isSubagent = subagentSessionCheck();
36782
+ const replyAddress = async (context, liveSessionID = "", subagent) => {
36752
36783
  if (sessionID !== "")
36753
36784
  return liveSessionID || sessionID;
36754
- if (!await isSubagent(context))
36785
+ if (!(subagent ?? await isSubagent(context)))
36755
36786
  return liveSessionID;
36756
36787
  return resolveEnvoySession(context.sessionManager.getSessionFile());
36757
36788
  };
@@ -37116,14 +37147,15 @@ Warnings: ${warnings.join("; ")}`, {
37116
37147
  }
37117
37148
  case EnvoyToolOperation.publish: {
37118
37149
  const topic = stringFor(parameters, "topic");
37119
- const source = await replyAddress(context);
37150
+ const inSubagent = await isSubagent(context);
37151
+ const source = await replyAddress(context, "", inSubagent);
37120
37152
  const result = await client.publish({
37121
37153
  sourceSessionID: source,
37122
37154
  topic,
37123
37155
  message: stringFor(parameters, "message"),
37124
37156
  ...toMessageMetadata(parameters)
37125
37157
  });
37126
- const undelivered = source !== "" && result.holder === source && await isSubagent(context);
37158
+ const undelivered = source !== "" && result.holder === source && inSubagent;
37127
37159
  const published = result.holder === undefined ? `published ${result.envelope.event_id}` : `published ${result.envelope.event_id}; holder ${result.holder}`;
37128
37160
  return toolSuccess(undelivered ? `${published}
37129
37161
  Not delivered: that holder is the session this subagent sends as, and the listener drops a message whose source session is its recipient, so the agent that spawned you did not receive it. Reach it over hub instead.` : published, {
@@ -37144,7 +37176,7 @@ Not delivered: that holder is the session this subagent sends as, and the listen
37144
37176
  }
37145
37177
  case EnvoyToolOperation.whoami: {
37146
37178
  const inSubagent = await isSubagent(context);
37147
- const address = await replyAddress(context);
37179
+ const address = await replyAddress(context, "", inSubagent);
37148
37180
  const subagent = inSubagent ? {
37149
37181
  session_id: context.sessionManager.getSessionId(),
37150
37182
  note: address === "" ? "This session is a task subagent and registers no Envoy session of its own, and this process records no top-level session its transcript traces back to, so it has no reply address at all: say who you are in the message body, and reach the agent that spawned you over hub." : "This session is a task subagent and registers no Envoy session of its own; a reply to session_id reaches the agent that spawned it, which relays over hub. Your own envoy_publish never reaches that agent \u2014 use hub for that hop."
package/dist/legion.js CHANGED
@@ -14447,13 +14447,13 @@ var dispatchToolSpecs = [
14447
14447
  },
14448
14448
  {
14449
14449
  name: "dispatch_request_approval",
14450
- example: { issue: "DSP-1", summary: "Proposes a live sync in place of the nightly export." },
14451
- description: "Ask a human to approve a document at its current version. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. A later version makes an approval stale, " + "and writing it retracts an open request for an older version; request again for the new " + "one. A repeat at the version an open request names returns that request unchanged. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
14450
+ example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
14451
+ description: "Ask a human to approve a document at its current version. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. A later version carries the same open " + "request forward and leaves it waiting on you; once the revision is complete and the human " + "has agreed to every point in it, call this again to hand that request back. The request " + "carries nothing new. A call while it already waits on the human hands nothing back: the " + "same summary changes nothing, and a different one is refused, since it would rewrite the " + "card the human is reading. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
14452
14452
  arguments: (z2) => ({
14453
14453
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14454
14454
  project: z2.string().describe("Project key owning the document.").optional(),
14455
14455
  artifact: z2.string().describe("Project document artifact id, slug, or filename; primary document by default for an issue.").optional(),
14456
- summary: z2.string({ min: 1 }).describe("The proposals in this version the human hasn't already agreed to, in one to three sentences.")
14456
+ summary: z2.string({ min: 1 }).describe("What the human is approving, in one to three sentences, and nothing else: no commentary on itself or the conversation, and no question. Request approval only once the human has agreed to every point in the document.")
14457
14457
  }),
14458
14458
  validation: documentOwnerValidation(true)
14459
14459
  },
@@ -15464,7 +15464,8 @@ class DispatchClient {
15464
15464
  return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
15465
15465
  }
15466
15466
  async requestApproval(artifactID, input) {
15467
- return this.#json("POST", ["api", "v1", "artifacts", artifactID, "approval-requests"], input);
15467
+ const answer = await this.#jsonAnswer("POST", ["api", "v1", "artifacts", artifactID, "approval-requests"], input);
15468
+ return { ...answer.payload, recorded: answer.status === 201 };
15468
15469
  }
15469
15470
  async editAsk(id, input) {
15470
15471
  return this.#json("PATCH", ["api", "v1", "asks", id], input);
@@ -15609,6 +15610,9 @@ class DispatchClient {
15609
15610
  }
15610
15611
  }
15611
15612
  async#json(method, path2, body, query) {
15613
+ return (await this.#jsonAnswer(method, path2, body, query)).payload;
15614
+ }
15615
+ async#jsonAnswer(method, path2, body, query) {
15612
15616
  const headers = {
15613
15617
  Accept: "application/json",
15614
15618
  Authorization: `Bearer ${this.token}`
@@ -15622,7 +15626,7 @@ class DispatchClient {
15622
15626
  signal: this.#signal,
15623
15627
  ...body === undefined ? {} : { body: JSON.stringify(body) }
15624
15628
  });
15625
- return this.#response(method, url2, response);
15629
+ return { status: response.status, payload: await this.#response(method, url2, response) };
15626
15630
  }
15627
15631
  async#form(method, path2, body) {
15628
15632
  const url2 = this.#url(path2);
@@ -16490,8 +16494,10 @@ function approvalLine(artifact) {
16490
16494
  if (approval === undefined || approval.state === "draft")
16491
16495
  return;
16492
16496
  switch (approval.state) {
16493
- case "awaiting":
16494
- return `Approval: awaiting (requested by ${approval.requested_by?.id ?? "unknown"}, ask ${approval.ask_id ?? "?"})`;
16497
+ case "awaiting": {
16498
+ const turn = approval.waiting_on === undefined ? "" : `, waiting on ${approval.waiting_on}`;
16499
+ return `Approval: awaiting${turn} (requested by ${approval.requested_by?.id ?? "unknown"}, ask ${approval.ask_id ?? "?"})`;
16500
+ }
16495
16501
  case "approved":
16496
16502
  return `Approval: approved v${approval.version} by ${approval.by?.id ?? "unknown"}`;
16497
16503
  case "stale":
@@ -16631,6 +16637,7 @@ function eventHead(event) {
16631
16637
  case "ask.opened":
16632
16638
  case "ask.anchor_refreshed":
16633
16639
  case "ask.edited":
16640
+ case "ask.handed_back":
16634
16641
  case "ask.resolved":
16635
16642
  return textHead(event.payload.question);
16636
16643
  case "ask.answered":
@@ -17378,7 +17385,7 @@ ${followsAsk(askOwner)}`,
17378
17385
  replyToOwnAsk: replyToAsk !== undefined && (comment.advice?.your_open_asks?.some((ask) => ask.id === replyToAsk) ?? false)
17379
17386
  });
17380
17387
  if (replyToAsk !== undefined) {
17381
- const askState = comment.turn === null ? "" : `; ask now waiting on ${comment.turn}`;
17388
+ const askState = comment.ask_waiting_on === undefined ? "" : `; ask now waiting on ${comment.ask_waiting_on}`;
17382
17389
  return {
17383
17390
  text: [
17384
17391
  `Replied on ask ${replyToAsk} (comment ${comment.id}${askState}). ${followsAsk(commentOwner2)}`,
@@ -17389,7 +17396,7 @@ ${followsAsk(askOwner)}`,
17389
17396
  ...commentDetails,
17390
17397
  ask: replyToAsk,
17391
17398
  follows: { ask: replyToAsk },
17392
- ...comment.turn === null ? {} : { ask_waiting_on: comment.turn },
17399
+ ...comment.ask_waiting_on === undefined ? {} : { ask_waiting_on: comment.ask_waiting_on },
17393
17400
  ...comment.advice === undefined ? {} : { advice: comment.advice }
17394
17401
  }
17395
17402
  };
@@ -17562,8 +17569,9 @@ ${trailer.join(`
17562
17569
  };
17563
17570
  }
17564
17571
  const details = await followedAskDetails(client, result.ask, resolved.artifact);
17572
+ const outcome = result.recorded ? `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}).` : `The approval request for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}) already waits on the human, so this call changed nothing: nothing since it last reached the human (a newer version, a human's reply in its thread, or your progress note) left it waiting on you.`;
17565
17573
  return {
17566
- text: `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}). The human's Inbox asks: ${JSON.stringify(result.ask.question)}. The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
17574
+ text: `${outcome} The human's Inbox asks: ${JSON.stringify(result.ask.question)}. The answer arrives as artifact.approved or artifact.changes_requested; an edit after approval makes it stale, so request again for the new version.`,
17567
17575
  details: { ...details, artifact: resolved.artifact.id, version: result.version }
17568
17576
  };
17569
17577
  }
@@ -17736,7 +17744,7 @@ function asObject(value) {
17736
17744
  }
17737
17745
 
17738
17746
  // extensions/legion.ts
17739
- import { logger } from "@oh-my-pi/pi-utils";
17747
+ import { logger as logger2 } from "@oh-my-pi/pi-utils";
17740
17748
 
17741
17749
  // src/dispatch-user-turn.ts
17742
17750
  function isUserTurnCandidate(rendered) {
@@ -17835,7 +17843,7 @@ function endInjectedUserTurns(sessionID) {
17835
17843
  // package.json
17836
17844
  var package_default = {
17837
17845
  name: "@sjawhar/pi-legion-envoy",
17838
- version: "6.0.1",
17846
+ version: "6.0.3",
17839
17847
  type: "module",
17840
17848
  omp: {
17841
17849
  extensions: [
@@ -17883,33 +17891,56 @@ var package_default = {
17883
17891
  import fs from "fs";
17884
17892
  import path2 from "path";
17885
17893
  import * as host from "@oh-my-pi/pi-coding-agent";
17894
+ import { logger } from "@oh-my-pi/pi-utils";
17886
17895
  var LEGION_BOOTSTRAPPED_SESSION = Symbol.for("legion.pi-envoy.bootstrapped-session");
17887
17896
  var bootstrappedSessionStore = globalThis;
17888
17897
  function recordBootstrappedSession(sessionFile) {
17889
17898
  bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION] = sessionFile;
17890
17899
  }
17891
17900
  async function isSubagentSession(context) {
17892
- await context.sessionManager.ensureOnDisk();
17901
+ const registered = registeredSubagent(context);
17902
+ if (registered !== undefined)
17903
+ return { subagent: registered, settled: true };
17904
+ try {
17905
+ await context.sessionManager.ensureOnDisk();
17906
+ } catch (error48) {
17907
+ const subagent = transcriptSaysSubagent(context);
17908
+ logger.warn("subagent check: publishing the transcript failed; answered from the transcript on disk, and the next call asks again", { sessionFile: context.sessionManager.getSessionFile(), subagent, error: messageFor(error48) });
17909
+ return { subagent, settled: false };
17910
+ }
17911
+ return { subagent: transcriptSaysSubagent(context), settled: true };
17912
+ }
17913
+ function transcriptSaysSubagent(context) {
17893
17914
  const sessionFile = context.sessionManager.getSessionFile();
17894
- if (sessionFile !== undefined && fs.existsSync(sessionFile)) {
17895
- return fs.existsSync(`${path2.dirname(sessionFile)}.jsonl`);
17915
+ if (sessionFile !== undefined) {
17916
+ if (fs.existsSync(`${path2.dirname(sessionFile)}.jsonl`))
17917
+ return true;
17918
+ if (fs.existsSync(sessionFile))
17919
+ return false;
17896
17920
  }
17897
17921
  const bootstrapped = bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION];
17898
17922
  return bootstrapped !== undefined && bootstrapped !== sessionFile;
17899
17923
  }
17900
- function isRegisteredSubagent(context) {
17924
+ function registeredSubagent(context) {
17901
17925
  const registry2 = host.AgentRegistry?.global();
17902
17926
  if (registry2 === undefined)
17903
- return false;
17927
+ return;
17904
17928
  const sessionID = context.sessionManager.getSessionId();
17905
17929
  const sessionFile = context.sessionManager.getSessionFile();
17906
- return registry2.list().some((ref) => ref.kind !== "main" && (ref.session === null ? sessionFile !== undefined && ref.sessionFile === sessionFile : ref.session.sessionManager.getSessionId() === sessionID));
17930
+ const own = registry2.list().filter((ref) => ref.session === null ? sessionFile !== undefined && ref.sessionFile === sessionFile : ref.session.sessionManager.getSessionId() === sessionID);
17931
+ if (own.length === 0)
17932
+ return;
17933
+ return own.some((ref) => ref.kind !== "main");
17907
17934
  }
17908
17935
  function subagentSessionCheck() {
17909
- let subagentSession;
17910
- return (context) => {
17911
- subagentSession ??= isSubagentSession(context);
17912
- return subagentSession;
17936
+ let settled;
17937
+ return async (context) => {
17938
+ if (settled !== undefined)
17939
+ return settled;
17940
+ const answer = await isSubagentSession(context);
17941
+ if (answer.settled)
17942
+ settled = answer.subagent;
17943
+ return answer.subagent;
17913
17944
  };
17914
17945
  }
17915
17946
 
@@ -18496,7 +18527,7 @@ function createClaimSession(deps) {
18496
18527
 
18497
18528
  // src/legion/controller-session.ts
18498
18529
  function createControllerSession(deps) {
18499
- const { daemon, persistedTranscript, checkSubagentSession } = deps;
18530
+ const { daemon, persistedTranscript } = deps;
18500
18531
  let controllerSessionID;
18501
18532
  let controllerCapability;
18502
18533
  let mintControllerGrant;
@@ -18547,8 +18578,6 @@ function createControllerSession(deps) {
18547
18578
  }
18548
18579
  };
18549
18580
  const reclaimAfterSessionChange = async (context) => {
18550
- if (await checkSubagentSession(context))
18551
- return;
18552
18581
  if (classifySession(process.env).kind !== "controller")
18553
18582
  return;
18554
18583
  if (context.sessionManager.getSessionId() === controllerSessionID && context.sessionManager.getSessionFile() === controllerTranscript) {
@@ -19217,7 +19246,7 @@ function isToolDeviceInvocation(toolCall) {
19217
19246
  }
19218
19247
  function legionExtension(pi) {
19219
19248
  const instance = randomUUID2().slice(0, 8);
19220
- logger.debug("extension instance loaded", { extension: import.meta.url, instance });
19249
+ logger2.debug("extension instance loaded", { extension: import.meta.url, instance });
19221
19250
  globalThis[LEGION_LOADED_MARKER] = import.meta.url;
19222
19251
  const checkSubagentSession = subagentSessionCheck();
19223
19252
  let paneRules;
@@ -19246,8 +19275,7 @@ function legionExtension(pi) {
19246
19275
  });
19247
19276
  const controllerSession = createControllerSession({
19248
19277
  daemon: roleDaemon,
19249
- persistedTranscript,
19250
- checkSubagentSession
19278
+ persistedTranscript
19251
19279
  });
19252
19280
  const titleSession = async (context) => {
19253
19281
  const title = legionSessionTitle(classifySession(process.env), process.env.LEGION_PROJECT);
@@ -19271,16 +19299,17 @@ function legionExtension(pi) {
19271
19299
  await activateLegionTool();
19272
19300
  });
19273
19301
  const afterSessionChange = async (context) => {
19274
- if (classifySession(process.env).kind === "controller" && !await checkSubagentSession(context)) {
19302
+ if (await checkSubagentSession(context))
19303
+ return;
19304
+ if (classifySession(process.env).kind === "controller")
19275
19305
  await titleSession(context);
19276
- }
19277
19306
  await controllerSession.reclaimAfterSessionChange(context);
19278
19307
  };
19279
19308
  pi.on("session_switch", (_event, context) => afterSessionChange(context));
19280
19309
  pi.on("session_branch", (_event, context) => afterSessionChange(context));
19281
19310
  pi.on("session_tree", (_event, context) => afterSessionChange(context));
19282
19311
  pi.on("tool_call", async (toolCall, context) => {
19283
- logger.debug("legion tool_call hook", {
19312
+ logger2.debug("legion tool_call hook", {
19284
19313
  instance,
19285
19314
  toolCallId: toolCall.toolCallId,
19286
19315
  toolName: toolCall.toolName
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "6.0.1",
3
+ "version": "6.0.3",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [