@sjawhar/pi-envoy 10.25.0 → 10.26.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/dist/envoy.js CHANGED
@@ -30013,6 +30013,7 @@ var SEARCH_QUERY_HINT = "search with a short phrase of a few words, not a passag
30013
30013
  var SEARCH_KIND_DEPTH = 100;
30014
30014
  var PROJECT_KEY_PATTERN = /^[A-Z][A-Z0-9]{1,9}$/;
30015
30015
  var PROJECT_KEY_RULE = "an uppercase letter, then 1 to 9 uppercase letters or digits, such as CORE";
30016
+ var ARCHITECTURE_SOURCE_REPO_PATTERN = /^[A-Za-z0-9-]+\/(?!\.\.?$)[A-Za-z0-9._-]+$/;
30016
30017
  var ISSUE_STATUSES = [
30017
30018
  "triage",
30018
30019
  "icebox",
@@ -30378,7 +30379,7 @@ var dispatchToolSpecs = [
30378
30379
  {
30379
30380
  name: "dispatch_issues",
30380
30381
  example: { project: "PROJ", limit: 250, offset: 250 },
30381
- description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase; dispatch search remains the keyword surface. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never.",
30382
+ description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, open-ask count, progress (its spec's task-list items and " + "its direct children, each done of total, as `tasks 3/7 \xB7 children 2/5`, each only when it has " + "any), and route with whether it reaches anyone, " + "so you can see backlog shape without opening every issue. Optionally filter by status, parent, " + "label, priority, route status, or how recently it changed; priority takes one or more of 0-3 " + "(P0-P3) and null for an issue with no priority, so an owner's P0/P1 audit is priority [0, 1]. " + 'route_status "no_holder" lists every open issue whose route names a role nobody holds or a ' + "session that is not running at the moment of the read, whatever its priority. A restarting " + "session is absent for minutes, so an issue is unowned only when a read ten minutes later agrees. " + "Do not use it to search by keyword or phrase; dispatch search remains the keyword surface. " + "Dispatch pages the list: limit sets the page size (default " + `${DEFAULT_ISSUE_PAGE_LIMIT}, max ${MAX_ISSUE_PAGE_LIMIT}) and offset selects where it starts ` + "(default 0), and the answer names how many issues match, so repeat with the next offset to " + "walk every matching issue. A walk is exact only while the list does not change: an issue " + "that enters or leaves what the filters match, or whose status or rank changes, between two " + "pages shifts rows across a page boundary, so one issue can come back twice and another never. " + "all takes the whole filtered list in one read instead, for a caller that must not miss or " + "repeat a row while other writers change the list, and especially for one whose own writes " + "move issues out of what it filtered on.",
30382
30383
  arguments: (z2) => ({
30383
30384
  project: z2.string().describe("Project key to list issues from."),
30384
30385
  status: z2.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
@@ -30388,7 +30389,8 @@ var dispatchToolSpecs = [
30388
30389
  updated_since: z2.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
30389
30390
  route_status: z2.enum(ISSUE_ROUTE_STATUSES).describe("Optional: only open issues whose route is in this state. live: a running session holds " + "the role or is the routed session. no_holder: nobody running holds the role, or the " + "session is not running, right now. unknown: the Envoy listener did not answer.").optional(),
30390
30391
  limit: z2.number({ int: true, min: 1, max: MAX_ISSUE_PAGE_LIMIT }).describe(`Maximum rows, 1-${MAX_ISSUE_PAGE_LIMIT}; default ${DEFAULT_ISSUE_PAGE_LIMIT}.`).optional(),
30391
- offset: z2.number({ int: true, min: 0 }).describe("Rows to skip before the page; nonnegative integer; default 0.").optional()
30392
+ offset: z2.number({ int: true, min: 0 }).describe("Rows to skip before the page; nonnegative integer; default 0.").optional(),
30393
+ all: z2.boolean().describe("Take every matching issue in one unpaged read, so no row is missed or repeated while " + "the list changes. Refused with limit or offset.").optional()
30392
30394
  })
30393
30395
  },
30394
30396
  {
@@ -30410,11 +30412,19 @@ var dispatchToolSpecs = [
30410
30412
  },
30411
30413
  {
30412
30414
  name: "dispatch_architecture_sync",
30413
- example: { project: "CORE" },
30414
- description: "Import a project's architecture model from its configured source repository now, instead of " + "waiting for the server's five-minute schedule. Returns the imported commit and component " + "count, or the recorded error when the model was rejected (the previous model stays up). " + "The source itself is configured by a human in Settings; 404 SOURCE_NOT_FOUND without one.",
30415
+ example: { project: "CORE", repo: "acme/infra" },
30416
+ description: "Import a project's architecture model from its source repositories now, instead of waiting " + "for the server's five-minute schedule. A project may have several sources, one per " + "repository, and its model is every source's components together. Without repo every " + "source imports and each answers on its own line; with repo only that source does. Each " + "line names the imported commit, or the recorded error when that source's model was " + "rejected (its previous components stay up, and every other source is untouched). Sources " + "are saved in Settings or with PUT /api/v1/projects/{key}/architecture-sources/{owner}/{repo}; " + "404 SOURCE_NOT_FOUND when the project has none, or none for that repository.",
30415
30417
  arguments: (z2) => ({
30416
- project: z2.string().describe("Project key whose architecture source to sync, such as CORE.")
30418
+ project: z2.string().describe("Project key whose architecture sources to sync, such as CORE."),
30419
+ repo: z2.string().describe("One source repository of the project to sync alone, owner/name; omit it to sync every source.").optional()
30417
30420
  }),
30421
+ validation: {
30422
+ check: (value) => {
30423
+ const { repo } = value;
30424
+ return typeof repo !== "string" || ARCHITECTURE_SOURCE_REPO_PATTERN.test(repo);
30425
+ },
30426
+ message: "repo is not a repository: give it as owner/name, such as acme/infra."
30427
+ },
30418
30428
  strict: true
30419
30429
  },
30420
30430
  {
@@ -32708,6 +32718,9 @@ function asErrorShape(value) {
32708
32718
  function isJson(response) {
32709
32719
  return response.headers.get("content-type")?.includes("application/json") ?? false;
32710
32720
  }
32721
+ function isNoRoute(error48) {
32722
+ return error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "NOT_FOUND" && error48.message.startsWith("no route for ");
32723
+ }
32711
32724
  var ISSUE_EVENTS_PAGE_MAX = 200;
32712
32725
  var DISPATCH_TOOL_DEADLINE_MS = 60000;
32713
32726
  function requestSignal(signal) {
@@ -32735,6 +32748,18 @@ function whyNotAPage(answer) {
32735
32748
  }
32736
32749
  return `${what}. Retrying will not help: the same request gets the same answer until that ` + "Dispatch is upgraded or fixed.";
32737
32750
  }
32751
+ function whyNotAnArray(answer) {
32752
+ if (typeof answer === "string")
32753
+ return `text that is not JSON, ${GATEWAY_PAGE}, so a retry may succeed`;
32754
+ if (answer === null)
32755
+ return "null";
32756
+ if (typeof answer !== "object")
32757
+ return `a ${typeof answer}`;
32758
+ if ("issues" in answer && Array.isArray(answer.issues)) {
32759
+ return "a page, so this Dispatch pages a request that carries no limit and no offset";
32760
+ }
32761
+ return "an object that is not the listing";
32762
+ }
32738
32763
  var GATEWAY_PAGE = "which looks like a proxy or gateway page rather than Dispatch's own answer";
32739
32764
  function gatewayAdvice(method, status) {
32740
32765
  if (!transientStatus(status)) {
@@ -32810,6 +32835,14 @@ class DispatchClient {
32810
32835
  return answer;
32811
32836
  throw new Error(`GET ${this.#url(path2, query)} asked for a page ({issues, total, limit, offset}) and got ` + refusal);
32812
32837
  }
32838
+ async listAllIssues(options) {
32839
+ const path2 = ["api", "v1", "issues"];
32840
+ const query = { ...options };
32841
+ const answer = await this.#json("GET", path2, undefined, query);
32842
+ if (Array.isArray(answer))
32843
+ return answer;
32844
+ throw new Error(`GET ${this.#url(path2, query)} asked for the unpaged listing and got ${whyNotAnArray(answer)}.`);
32845
+ }
32813
32846
  async listProjectArtifacts(project, unlinked = false) {
32814
32847
  return this.#json("GET", ["api", "v1", "projects", project, "artifacts"], undefined, unlinked ? { unlinked: "true" } : undefined);
32815
32848
  }
@@ -32898,19 +32931,69 @@ class DispatchClient {
32898
32931
  async listProjects() {
32899
32932
  return this.#json("GET", ["api", "v1", "projects"]);
32900
32933
  }
32901
- async syncArchitectureSource(project) {
32902
- return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
32903
- }
32904
- async getArchitectureSource(project) {
32934
+ async listArchitectureSources(project) {
32905
32935
  try {
32906
- return await this.#json("GET", ["api", "v1", "projects", project, "architecture-source"]);
32936
+ return await this.#json("GET", ["api", "v1", "projects", project, "architecture-sources"]);
32937
+ } catch (error48) {
32938
+ if (!isNoRoute(error48))
32939
+ throw error48;
32940
+ }
32941
+ try {
32942
+ const source = await this.#json("GET", [
32943
+ "api",
32944
+ "v1",
32945
+ "projects",
32946
+ project,
32947
+ "architecture-source"
32948
+ ]);
32949
+ return source === null ? [] : [source];
32907
32950
  } catch (error48) {
32908
32951
  if (error48 instanceof DispatchServiceError && error48.status === 404 && error48.code === "SOURCE_NOT_FOUND") {
32909
- return null;
32952
+ return [];
32910
32953
  }
32911
32954
  throw error48;
32912
32955
  }
32913
32956
  }
32957
+ async syncArchitectureSources(project) {
32958
+ try {
32959
+ return await this.#json("POST", [
32960
+ "api",
32961
+ "v1",
32962
+ "projects",
32963
+ project,
32964
+ "architecture-sources",
32965
+ "sync"
32966
+ ]);
32967
+ } catch (error48) {
32968
+ if (!isNoRoute(error48))
32969
+ throw error48;
32970
+ }
32971
+ const source = await this.#json("POST", [
32972
+ "api",
32973
+ "v1",
32974
+ "projects",
32975
+ project,
32976
+ "architecture-source",
32977
+ "sync"
32978
+ ]);
32979
+ return [source];
32980
+ }
32981
+ async syncArchitectureSource(project, repo2) {
32982
+ const [owner, name, ...rest] = repo2.split("/");
32983
+ if (!owner || !name || rest.length > 0) {
32984
+ throw new Error(`repository must be owner/name, got ${JSON.stringify(repo2)}`);
32985
+ }
32986
+ return this.#json("POST", [
32987
+ "api",
32988
+ "v1",
32989
+ "projects",
32990
+ project,
32991
+ "architecture-sources",
32992
+ owner,
32993
+ name,
32994
+ "sync"
32995
+ ]);
32996
+ }
32914
32997
  async resolveAsk(id, input) {
32915
32998
  return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
32916
32999
  }
@@ -34151,7 +34234,7 @@ var envoyToolSpecs = [
34151
34234
  },
34152
34235
  {
34153
34236
  name: "envoy_publish",
34154
- description: `Publish an Envoy message to a notifications. topic. A role or built-in topic is delivered as its guide says; any other notifications. topic you name (a team topic such as notifications.team.deploy-squad.all) is live only: it reaches every subscribed session that holds its own NATS connection, yours included, and is not retained. A session the listener delivers to over a serve port, such as an OpenCode agent, receives nothing on such a topic and is told nothing (dispatch://INFRA-122). ${DELIVERY_CONTRACT}`,
34237
+ description: `Publish an Envoy message to a notifications. topic. A role or built-in topic is delivered as its guide says; any other notifications. topic you name (a team topic such as notifications.team.deploy-squad.all) is live only: it reaches every session subscribed to it when you publish, and is not retained. A session that holds its own NATS connection receives it directly, yours included; a session the listener delivers to over a serve port, such as an OpenCode agent, receives it through its machine's listener when that listener subscribes to every topic (ENVOY_SUBSCRIBE_ALL_TOPICS=1; the shared hub's listener does not). ${DELIVERY_CONTRACT}`,
34155
34238
  arguments: (schema) => ({ topic: schema.string(), ...messageArguments(schema) }),
34156
34239
  operation: EnvoyToolOperation.publish
34157
34240
  },
@@ -34724,12 +34807,17 @@ var REPLAY_ARRAY_BYTES = 2;
34724
34807
  function jsonBytes(value) {
34725
34808
  return encoder.encode(JSON.stringify(value)).length;
34726
34809
  }
34727
- function readModel(message) {
34728
- if (typeof message.model !== "string" || message.model === "")
34810
+ var SEQ_PER_MS = 1000;
34811
+ function modelReference(provider, id) {
34812
+ if (typeof id !== "string" || id === "")
34729
34813
  return;
34730
- if (typeof message.provider !== "string" || message.provider === "")
34814
+ if (typeof provider !== "string" || provider === "")
34731
34815
  return;
34732
- return capAgentStreamText(`${message.provider}/${message.model}`, AGENT_STREAM_LIMITS.modelChars);
34816
+ return `${provider}/${id}`;
34817
+ }
34818
+ function readModel(message) {
34819
+ const model = modelReference(message.provider, message.model);
34820
+ return model === undefined ? undefined : capAgentStreamText(model, AGENT_STREAM_LIMITS.modelChars);
34733
34821
  }
34734
34822
  function readMessage(raw) {
34735
34823
  if (typeof raw !== "object" || raw === null)
@@ -34854,16 +34942,31 @@ class AgentStreamPublisher {
34854
34942
  #history = new Map;
34855
34943
  #settled = new Set;
34856
34944
  #publishedAt = new Map;
34857
- #seq = 0;
34945
+ #seq;
34858
34946
  #watchedUntil = 0;
34947
+ #listedCommands;
34859
34948
  #commands;
34860
34949
  #commandsSeq = 0;
34950
+ #settings;
34951
+ #settingsSeq = 0;
34952
+ #settingsJson;
34861
34953
  constructor(deps) {
34862
34954
  this.#deps = deps;
34955
+ this.#seq = Math.trunc(deps.now()) * SEQ_PER_MS;
34863
34956
  }
34864
34957
  noteViewer() {
34865
34958
  this.#watchedUntil = this.#deps.now() + AGENT_STREAM_WATCH_TTL_MS;
34866
34959
  }
34960
+ publishState(subject2) {
34961
+ if (!this.watched)
34962
+ return;
34963
+ if (this.#settings !== undefined) {
34964
+ this.#deps.publish(subject2, JSON.stringify(this.#settingsFrame(this.#settings)));
34965
+ }
34966
+ if (this.#commands !== undefined) {
34967
+ this.#deps.publish(subject2, JSON.stringify(this.#commandsFrame(this.#commands)));
34968
+ }
34969
+ }
34867
34970
  get watched() {
34868
34971
  return this.#deps.now() < this.#watchedUntil;
34869
34972
  }
@@ -34872,35 +34975,67 @@ class AgentStreamPublisher {
34872
34975
  this.#settled.clear();
34873
34976
  this.#publishedAt.clear();
34874
34977
  this.#watchedUntil = 0;
34978
+ this.#listedCommands = undefined;
34875
34979
  this.#commands = undefined;
34980
+ this.#settings = undefined;
34981
+ this.#settingsJson = undefined;
34876
34982
  }
34877
34983
  setCommands(subject2, commands) {
34878
- const limit = AGENT_STREAM_LIMITS.historyBytes - REPLAY_ARRAY_BYTES;
34879
- let bytes = jsonBytes(this.#commandsFrame([], this.#seq + 1));
34880
34984
  const seen = new Set;
34881
- const kept = [];
34985
+ const listed = [];
34882
34986
  for (const command of commands) {
34883
- if (kept.length >= AGENT_STREAM_LIMITS.commands)
34987
+ if (listed.length >= AGENT_STREAM_LIMITS.commands)
34884
34988
  break;
34885
34989
  const name = command.name;
34886
34990
  if (name.length > AGENT_STREAM_LIMITS.commandNameChars)
34887
34991
  continue;
34888
34992
  if (!isTypableCommandName(name) || seen.has(name))
34889
34993
  continue;
34890
- const listed = {
34994
+ seen.add(name);
34995
+ listed.push({
34891
34996
  name,
34892
34997
  source: command.source,
34893
34998
  ...command.description !== undefined && {
34894
34999
  description: capAgentStreamText(command.description, AGENT_STREAM_LIMITS.commandDescriptionChars)
34895
35000
  },
34896
35001
  ...command.terminalOnly === true && { terminalOnly: true }
34897
- };
34898
- const cost = jsonBytes(listed) + (kept.length === 0 ? 0 : 1);
35002
+ });
35003
+ }
35004
+ this.#listedCommands = listed;
35005
+ this.#fitCommands(subject2);
35006
+ }
35007
+ setSettings(subject2, settings) {
35008
+ const kept = settings.reported ? {
35009
+ ...settings.model !== undefined && { model: settings.model },
35010
+ models: [...settings.models],
35011
+ reported: true,
35012
+ thinking: settings.thinking === null ? null : { level: settings.thinking.level, levels: [...settings.thinking.levels] }
35013
+ } : { reported: false };
35014
+ const json2 = JSON.stringify(kept);
35015
+ if (json2 === this.#settingsJson)
35016
+ return;
35017
+ this.#settings = kept;
35018
+ this.#settingsJson = json2;
35019
+ this.#seq += 1;
35020
+ this.#settingsSeq = this.#seq;
35021
+ if (this.watched)
35022
+ this.#deps.publish(subject2, JSON.stringify(this.#settingsFrame(kept)));
35023
+ this.#fitCommands(subject2);
35024
+ }
35025
+ #fitCommands(subject2) {
35026
+ const listed = this.#listedCommands;
35027
+ if (listed === undefined)
35028
+ return;
35029
+ const settings = this.#settings === undefined ? 0 : jsonBytes(this.#settingsFrame(this.#settings)) + 1;
35030
+ const limit = AGENT_STREAM_LIMITS.historyBytes - REPLAY_ARRAY_BYTES - settings;
35031
+ let bytes = jsonBytes(this.#commandsFrame([], this.#seq + 1));
35032
+ const kept = [];
35033
+ for (const command of listed) {
35034
+ const cost = jsonBytes(command) + (kept.length === 0 ? 0 : 1);
34899
35035
  if (bytes + cost > limit)
34900
35036
  break;
34901
35037
  bytes += cost;
34902
- seen.add(name);
34903
- kept.push(listed);
35038
+ kept.push(command);
34904
35039
  }
34905
35040
  if (JSON.stringify(kept) === JSON.stringify(this.#commands))
34906
35041
  return;
@@ -34913,6 +35048,9 @@ class AgentStreamPublisher {
34913
35048
  #commandsFrame(commands, seq = this.#commandsSeq) {
34914
35049
  return { commands, kind: "commands", seq, v: AGENT_STREAM_PROTOCOL };
34915
35050
  }
35051
+ #settingsFrame(settings) {
35052
+ return { ...settings, kind: "settings", seq: this.#settingsSeq, v: AGENT_STREAM_PROTOCOL };
35053
+ }
34916
35054
  record(subject2, raw, streaming, dispatchMessageId) {
34917
35055
  const message = readMessage(raw);
34918
35056
  if (message === null)
@@ -34958,23 +35096,26 @@ class AgentStreamPublisher {
34958
35096
  if (!this.watched)
34959
35097
  return { frames: [], session_id: sessionID, v: AGENT_STREAM_PROTOCOL };
34960
35098
  const ordered = [...this.#history.values()].sort((left, right) => left.at === right.at ? left.seq - right.seq : left.at - right.at);
34961
- const commands = this.#commands === undefined ? undefined : this.#commandsFrame(this.#commands);
34962
- let used = REPLAY_ARRAY_BYTES + (commands === undefined ? 0 : jsonBytes(commands));
35099
+ const tail = [];
35100
+ if (this.#settings !== undefined)
35101
+ tail.push(this.#settingsFrame(this.#settings));
35102
+ if (this.#commands !== undefined)
35103
+ tail.push(this.#commandsFrame(this.#commands));
35104
+ let used = REPLAY_ARRAY_BYTES + tail.reduce((bytes, frame, index) => bytes + jsonBytes(frame) + (index === 0 ? 0 : 1), 0);
34963
35105
  const kept = [];
34964
35106
  for (let index = ordered.length - 1;index >= 0; index -= 1) {
34965
35107
  const entry = ordered[index];
34966
35108
  if (entry === undefined)
34967
35109
  continue;
34968
35110
  const frame = frameFor(entry);
34969
- const separated = kept.length > 0 || commands !== undefined;
35111
+ const separated = kept.length > 0 || tail.length > 0;
34970
35112
  const cost = jsonBytes(frame) + (separated ? 1 : 0);
34971
35113
  if (used + cost > AGENT_STREAM_LIMITS.historyBytes)
34972
35114
  break;
34973
35115
  used += cost;
34974
35116
  kept.unshift(frame);
34975
35117
  }
34976
- if (commands !== undefined)
34977
- kept.push(commands);
35118
+ kept.push(...tail);
34978
35119
  return { frames: kept, session_id: sessionID, v: AGENT_STREAM_PROTOCOL };
34979
35120
  }
34980
35121
  }
@@ -35334,6 +35475,25 @@ import("@oh-my-pi/pi-coding-agent/extensibility/extensions/send-user-input-handl
35334
35475
  }, () => {
35335
35476
  return;
35336
35477
  });
35478
+ function liveAgentSession(session) {
35479
+ return host2.AgentRegistry?.global().list().find((ref) => ref.session?.sessionManager.getSessionId() === session)?.session ?? undefined;
35480
+ }
35481
+ function liveSessionSettings(live) {
35482
+ if (live === undefined || !("model" in live) || typeof live.configuredThinkingLevel !== "function" || typeof live.getAvailableModels !== "function" || typeof live.getAvailableEffortSelectors !== "function") {
35483
+ return { reported: false };
35484
+ }
35485
+ const model = modelReference(live.model?.provider, live.model?.id);
35486
+ const levels = live.getAvailableEffortSelectors();
35487
+ return {
35488
+ ...model !== undefined && { model },
35489
+ models: live.getAvailableModels().flatMap((available) => {
35490
+ const listed = modelReference(available.provider, available.id);
35491
+ return listed === undefined ? [] : [listed];
35492
+ }),
35493
+ reported: true,
35494
+ thinking: levels.length === 0 ? null : { level: live.configuredThinkingLevel() ?? null, levels }
35495
+ };
35496
+ }
35337
35497
  var codec2 = import_nats2.StringCodec();
35338
35498
  var NATS_RETRY_INTERVAL_MS = 15000;
35339
35499
  var DELIVERY_PICTURES_TIMEOUT_MS = 20000;
@@ -35739,6 +35899,7 @@ function envoyExtension(pi) {
35739
35899
  typedPictures.withdraw(turn.messageId);
35740
35900
  return typedInputReply(turn.body, { error: messageFor(error48) });
35741
35901
  }).then(async (reply) => {
35902
+ refreshAgentStreamSettings(sessionID);
35742
35903
  if (reply !== undefined)
35743
35904
  await postDispatchReply(delivery, { body: reply }, replySessionID);
35744
35905
  }).catch((error48) => {
@@ -35960,7 +36121,7 @@ function envoyExtension(pi) {
35960
36121
  return;
35961
36122
  const builtins = hostBuiltinCommands?.() ?? [];
35962
36123
  const reserved = new Set(builtins.flatMap((builtin) => [builtin.name, ...builtin.aliases]));
35963
- const live = host2.AgentRegistry?.global().list().find((ref) => ref.session?.sessionManager.getSessionId() === session)?.session;
36124
+ const live = liveAgentSession(session);
35964
36125
  const commands = builtins.map((builtin) => ({
35965
36126
  description: builtin.description,
35966
36127
  name: builtin.name,
@@ -35986,6 +36147,18 @@ function envoyExtension(pi) {
35986
36147
  }
35987
36148
  agentStream.setCommands(agentStreamFramesSubject(session), commands);
35988
36149
  };
36150
+ const refreshAgentStreamSettings = (session) => {
36151
+ if (session === "")
36152
+ return;
36153
+ try {
36154
+ agentStream.setSettings(agentStreamFramesSubject(session), liveSessionSettings(liveAgentSession(session)));
36155
+ } catch (error48) {
36156
+ logger3.warn("envoy: could not refresh agent stream settings", {
36157
+ error: messageFor(error48),
36158
+ sessionID: session
36159
+ });
36160
+ }
36161
+ };
35989
36162
  const pumpAgentStreamControl = async (subscription, session) => {
35990
36163
  try {
35991
36164
  for await (const message of subscription) {
@@ -35996,6 +36169,7 @@ function envoyExtension(pi) {
35996
36169
  const request = parsed;
35997
36170
  if (request.type !== "replay" && request.type !== "watch")
35998
36171
  continue;
36172
+ refreshAgentStreamSettings(session);
35999
36173
  const attached = agentStreamViewerAttached;
36000
36174
  if (request.type === "replay" || !attached || Date.now() - agentStreamCommandsRefreshedOnWatchAt >= AGENT_STREAM_WATCH_INTERVAL_MS) {
36001
36175
  try {
@@ -36012,8 +36186,11 @@ function envoyExtension(pi) {
36012
36186
  }
36013
36187
  const reply = message.reply;
36014
36188
  const answer = request.type === "replay" && reply !== undefined ? JSON.stringify(agentStream.replay(session)) : undefined;
36189
+ const arming = !agentStream.watched;
36015
36190
  agentStream.noteViewer();
36016
36191
  agentStreamViewerAttached = true;
36192
+ if (arming)
36193
+ agentStream.publishState(agentStreamFramesSubject(session));
36017
36194
  if (answer === undefined || reply === undefined)
36018
36195
  continue;
36019
36196
  connection?.publish(reply, codec2.encode(answer));
@@ -36072,6 +36249,12 @@ function envoyExtension(pi) {
36072
36249
  pi.on("message_end", async (event) => {
36073
36250
  recordAgentStreamMessage(event.message, false);
36074
36251
  });
36252
+ pi.on("turn_start", async () => {
36253
+ refreshAgentStreamSettings(sessionID);
36254
+ });
36255
+ pi.on("retry_fallback_applied", async () => {
36256
+ refreshAgentStreamSettings(sessionID);
36257
+ });
36075
36258
  const endRole = () => {
36076
36259
  claimedRoleTopic = undefined;
36077
36260
  claimedRoleSessionID = undefined;
@@ -136,7 +136,7 @@ EOF
136
136
 
137
137
  - Attach every architectural issue to the components it changes. Attach the root before you decompose it: children inherit.
138
138
  - `--components-json '{"mode":"explicit","ids":["web"]}'`, `'{"mode":"none","reason":"hiring, not code"}'` (only for non-architectural work, with a concrete reason), or `'{"mode":"inherit"}'`.
139
- - Component ids: each non-external `components[].id` from `GET /api/v1/projects/{key}/architecture`, or the file names under `.dispatch/architecture/` in its source repository. Others are refused `COMPONENTS_INPUT`. A closed issue can be classified without reopening.
139
+ - Component ids: each non-external `components[].id` from `GET /api/v1/projects/{key}/architecture`, or the file names under `.dispatch/architecture/` in any of the project's source repositories. A project may import from several; ids are project-wide, so one issue can name components of different sources. Others are refused `COMPONENTS_INPUT`. A closed issue can be classified without reopening.
140
140
  - Change code and `.dispatch/architecture/<id>.md` in the same review. A stale model: `dispatch architecture-sync` ([issues.md](skill://dispatch/references/issues.md#syncing-a-projects-architecture-model)).
141
141
 
142
142
  ## Close what you opened
@@ -63,4 +63,4 @@ As a surface's owner, run `dispatch issues --project <KEY> --priority 0 --priori
63
63
 
64
64
  ## Syncing a project's architecture model
65
65
 
66
- `dispatch architecture-sync --project CORE` imports the model now. It prints the commit, or the error, and keeps the old model. No source configured answers `SOURCE_NOT_FOUND`.
66
+ A project imports its model from one or more source repositories, each on its own branch. `dispatch architecture-sync --project CORE` imports every source now and prints a line for each: `Synced CORE architecture from <repo>@<branch>: commit <sha> (<time>).`, or `Sync failed for CORE (<repo>@<branch>): <error>` followed by what of that source still shows: its commit, its component count when the source was re-saved on another branch and has not imported from it yet, or that none ever imported. A failed source keeps its previous components; every other source is untouched. `--repo <owner/name>` imports that one source alone. No source configured, or no source from that repository, answers `SOURCE_NOT_FOUND`. A credential or branch problem is that source's failed line, or with `--repo` an error, `SOURCE_ACCESS`.