@coreplane/switchboard 1.236.0 → 1.237.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.
Files changed (121) hide show
  1. package/dist/assets/deploy/cloudflare-resident/worker.ts +175 -62
  2. package/dist/assets/deploy/cloudflare-sandbox/worker.ts +36 -10
  3. package/dist/assets/package-lock.json +3 -3
  4. package/dist/assets/package.json +1 -1
  5. package/dist/assets/source.json +3 -3
  6. package/dist/assets/src/core/authz/authorize.ts +25 -6
  7. package/dist/assets/src/core/authz/policy.ts +9 -0
  8. package/dist/assets/src/core/authz/types.ts +15 -1
  9. package/dist/assets/src/core/coordinator/driver.ts +5 -1
  10. package/dist/assets/src/core/runEvents.ts +10 -6
  11. package/dist/assets/src/core/ship/coordinator.ts +23 -1
  12. package/dist/assets/src/execution/residentDepsStore.ts +14 -2
  13. package/dist/assets/src/execution/residentDiskBudget.ts +45 -11
  14. package/dist/assets/src/execution/residentHead.ts +34 -8
  15. package/dist/assets/src/execution/residentRebind.ts +88 -31
  16. package/dist/assets/src/execution/sandboxLifecycle.ts +33 -0
  17. package/dist/assets/web/dist/.vite/manifest.json +408 -408
  18. package/dist/assets/web/dist/assets/AppShell-MYHNxJks.js +1 -0
  19. package/dist/assets/web/dist/assets/{CostsPage-BfJsol9y.js → CostsPage-DKk9OjB0.js} +1 -1
  20. package/dist/assets/web/dist/assets/{DeliveryPage-CvInfu7y.js → DeliveryPage-Kz6js49i.js} +1 -1
  21. package/dist/assets/web/dist/assets/{NotFoundPage-d37EP8hS.js → NotFoundPage-BXLrQQzy.js} +1 -1
  22. package/dist/assets/web/dist/assets/{ResidentDetailPage-DQSD0SZD.js → ResidentDetailPage-Dc23v_8v.js} +1 -1
  23. package/dist/assets/web/dist/assets/ResidentsIndexPage-TR5D1TRR.js +1 -0
  24. package/dist/assets/web/dist/assets/{RunFoldRow-Bngn4GjF.js → RunFoldRow-mkUj7WEN.js} +1 -1
  25. package/dist/assets/web/dist/assets/{RunRoutePage-a-tMrirB.js → RunRoutePage-CQI17__g.js} +3 -3
  26. package/dist/assets/web/dist/assets/RunsIndexPage-yIP4PlKP.js +1 -0
  27. package/dist/assets/web/dist/assets/{RunsTabs-CyCdOhJ2.js → RunsTabs-XpCqeWu4.js} +1 -1
  28. package/dist/assets/web/dist/assets/{ScheduledPage-ByCmyrDs.js → ScheduledPage-CdCVyNbh.js} +1 -1
  29. package/dist/assets/web/dist/assets/SettingsPage-DuA8JpLU.js +1 -0
  30. package/dist/assets/web/dist/assets/{StatusDot-D-BPSPpw.js → StatusDot-DmNHX6am.js} +1 -1
  31. package/dist/assets/web/dist/assets/{Tooltip-DPDx5tXT.js → Tooltip-qVnJTgSt.js} +1 -1
  32. package/dist/assets/web/dist/assets/{UnitRoutePage-0eiekTHQ.js → UnitRoutePage-BcTeH7m2.js} +1 -1
  33. package/dist/assets/web/dist/assets/{angular-html-Ni1Xil2G.js → angular-html-DYtV9uIN.js} +1 -1
  34. package/dist/assets/web/dist/assets/{angular-ts-oCgo6p05.js → angular-ts-DF2P25Zp.js} +1 -1
  35. package/dist/assets/web/dist/assets/{apl-CiWYl0gh.js → apl-Ff96wxko.js} +1 -1
  36. package/dist/assets/web/dist/assets/{astro-B-VeKZ_J.js → astro-LbBnZna-.js} +1 -1
  37. package/dist/assets/web/dist/assets/{blade-BIWEISKd.js → blade-DltuxeBW.js} +1 -1
  38. package/dist/assets/web/dist/assets/{c-DHxA5Byp.js → c-DBg7yuPx.js} +1 -1
  39. package/dist/assets/web/dist/assets/{chapel-D4qGLw-Q.js → chapel-C83hUAds.js} +1 -1
  40. package/dist/assets/web/dist/assets/{cobol-BznX5Q-u.js → cobol-DB0yV_2D.js} +1 -1
  41. package/dist/assets/web/dist/assets/{coffee-CVr8Idlj.js → coffee-Bpih6Kzl.js} +1 -1
  42. package/dist/assets/web/dist/assets/{cpp-wrk6NfW4.js → cpp-CU00ZeRn.js} +1 -1
  43. package/dist/assets/web/dist/assets/{crystal-CGYWMeH1.js → crystal-f8ImJkux.js} +1 -1
  44. package/dist/assets/web/dist/assets/{css-BgGytMsy.js → css-4VrC4ISo.js} +1 -1
  45. package/dist/assets/web/dist/assets/{dist-DSkVB16z.js → dist-TAGawIfM.js} +2 -2
  46. package/dist/assets/web/dist/assets/{durationTone-DYogEPEk.js → durationTone-CYMux25J.js} +1 -1
  47. package/dist/assets/web/dist/assets/{edge-CdR6AU9v.js → edge-BwFDzfS_.js} +1 -1
  48. package/dist/assets/web/dist/assets/{elixir-CQRb3PZx.js → elixir-wwIYIpJm.js} +1 -1
  49. package/dist/assets/web/dist/assets/{elm-CpuA6W8B.js → elm-DyDuM3v3.js} +1 -1
  50. package/dist/assets/web/dist/assets/{erb-CvlrblqH.js → erb-DFJBEVrh.js} +1 -1
  51. package/dist/assets/web/dist/assets/{git-rebase-COnovCVc.js → git-rebase-CuDv-44X.js} +1 -1
  52. package/dist/assets/web/dist/assets/{glimmer-js-C-DS4g_J.js → glimmer-js-ZFnrm4J3.js} +1 -1
  53. package/dist/assets/web/dist/assets/{glimmer-ts-DTkorWRI.js → glimmer-ts-XY4Yqshp.js} +1 -1
  54. package/dist/assets/web/dist/assets/{glsl-CH51-JW7.js → glsl-xnR8efx0.js} +1 -1
  55. package/dist/assets/web/dist/assets/{graphql-BO-XymNv.js → graphql-D-gO5tnP.js} +1 -1
  56. package/dist/assets/web/dist/assets/{hack-C-N0oicf.js → hack-CSxbW4ax.js} +1 -1
  57. package/dist/assets/web/dist/assets/{haml-BfmdtWoP.js → haml-D4ar42QT.js} +1 -1
  58. package/dist/assets/web/dist/assets/{handlebars-BqhO_te-.js → handlebars-CMLbDE7o.js} +1 -1
  59. package/dist/assets/web/dist/assets/{html-DUEZlTKV.js → html-CVekU4Fs.js} +1 -1
  60. package/dist/assets/web/dist/assets/{html-derivative-DOUJTS8A.js → html-derivative-St3HHw_N.js} +1 -1
  61. package/dist/assets/web/dist/assets/{http-j7UH0Cqv.js → http-Mon0DWHS.js} +1 -1
  62. package/dist/assets/web/dist/assets/{hurl-D6ZaBLJk.js → hurl-Cbl5IvBs.js} +1 -1
  63. package/dist/assets/web/dist/assets/{indexRow-BjXBzI5Z.js → indexRow-FLJu5mR9.js} +1 -1
  64. package/dist/assets/web/dist/assets/{java-dugWx3wi.js → java-Bprck1l-.js} +1 -1
  65. package/dist/assets/web/dist/assets/{javascript-CK4GC4JO.js → javascript-C73G1-UR.js} +1 -1
  66. package/dist/assets/web/dist/assets/{jinja-DrLKIyKG.js → jinja-C4DuRxYJ.js} +1 -1
  67. package/dist/assets/web/dist/assets/{jison-CS1Th-TO.js → jison-B2U03GEC.js} +1 -1
  68. package/dist/assets/web/dist/assets/{json-CkBqdVys.js → json-CvTZUDzx.js} +1 -1
  69. package/dist/assets/web/dist/assets/{jsx-CCxdg7n3.js → jsx-DMWKCzwo.js} +1 -1
  70. package/dist/assets/web/dist/assets/{julia-BalKW2hA.js → julia-Czn7bV_3.js} +1 -1
  71. package/dist/assets/web/dist/assets/{just-BKw1G40D.js → just-BBxpNfvC.js} +1 -1
  72. package/dist/assets/web/dist/assets/{latex-Dfad6ZN-.js → latex-Bw9yg5mr.js} +1 -1
  73. package/dist/assets/web/dist/assets/{liquid-f_CMGBvd.js → liquid-BRCjaydE.js} +1 -1
  74. package/dist/assets/web/dist/assets/{lua-BcMK_uKM.js → lua-DUu2KIC0.js} +1 -1
  75. package/dist/assets/web/dist/assets/main-csihqEj5.css +1 -0
  76. package/dist/assets/web/dist/assets/{main-DR8iKpNt.js → main-mUd_TJjH.js} +2 -2
  77. package/dist/assets/web/dist/assets/{marko-B0oNLx2i.js → marko-CZJZIlSQ.js} +1 -1
  78. package/dist/assets/web/dist/assets/{mdc-Bc_MmIyq.js → mdc-BmClq6fd.js} +1 -1
  79. package/dist/assets/web/dist/assets/{nginx-Cmxv8RIt.js → nginx-Bmt3xSEm.js} +1 -1
  80. package/dist/assets/web/dist/assets/{nim-BtgRfaY0.js → nim-Da-i0utK.js} +1 -1
  81. package/dist/assets/web/dist/assets/{org-Dl9EzlHh.js → org-BvAre2qP.js} +1 -1
  82. package/dist/assets/web/dist/assets/{perl-BmF7x7BB.js → perl-v9WwFFtB.js} +1 -1
  83. package/dist/assets/web/dist/assets/{php-rH6CVptQ.js → php-DTU4Zyze.js} +1 -1
  84. package/dist/assets/web/dist/assets/{pug-CIN5Ccck.js → pug-DhTwEqTO.js} +1 -1
  85. package/dist/assets/web/dist/assets/{qml-BpLS3RFr.js → qml-BUD9AC_c.js} +1 -1
  86. package/dist/assets/web/dist/assets/{r-BUMF3B-H.js → r-CAnu56n7.js} +1 -1
  87. package/dist/assets/web/dist/assets/{razor-B7sTBPdT.js → razor-Dn0P4gAq.js} +1 -1
  88. package/dist/assets/web/dist/assets/{regexp-h2SvwrsJ.js → regexp-CZyBRaVW.js} +1 -1
  89. package/dist/assets/web/dist/assets/residentsModel-By1fThvk.js +1 -0
  90. package/dist/assets/web/dist/assets/{rst-DLOWEsD2.js → rst-Ckyd5UsY.js} +1 -1
  91. package/dist/assets/web/dist/assets/{ruby-Cl_-I4k3.js → ruby-BBQ62fsx.js} +1 -1
  92. package/dist/assets/web/dist/assets/{sas-Cm4M-BvV.js → sas-CW35hMxG.js} +1 -1
  93. package/dist/assets/web/dist/assets/{scss-CEFCEQUF.js → scss-CpV8pF4J.js} +1 -1
  94. package/dist/assets/web/dist/assets/{shellscript-CE6zb5eS.js → shellscript-CRJvsmE1.js} +1 -1
  95. package/dist/assets/web/dist/assets/{shellsession-B4fDVzrM.js → shellsession-CyriUZxf.js} +1 -1
  96. package/dist/assets/web/dist/assets/{soy-DwXCMpjE.js → soy-D5QzYUeg.js} +1 -1
  97. package/dist/assets/web/dist/assets/{sql-DrnAKnyD.js → sql-CUBf_Mse.js} +1 -1
  98. package/dist/assets/web/dist/assets/{stata-BzlwPWD6.js → stata-B-4n4ITz.js} +1 -1
  99. package/dist/assets/web/dist/assets/{surrealql-CeUieZXd.js → surrealql-RuWZBLtr.js} +1 -1
  100. package/dist/assets/web/dist/assets/{svelte-BERIpIKR.js → svelte-CO5YzCsK.js} +1 -1
  101. package/dist/assets/web/dist/assets/{templ-6FSuUDwg.js → templ-DbkWVaBF.js} +1 -1
  102. package/dist/assets/web/dist/assets/{tex-SWCyMwNX.js → tex-IuxoEcId.js} +1 -1
  103. package/dist/assets/web/dist/assets/{ts-tags-VWj7gWgY.js → ts-tags-g6dGQPjd.js} +1 -1
  104. package/dist/assets/web/dist/assets/{tsx-B6dzfL8L.js → tsx-WnXwiyH2.js} +1 -1
  105. package/dist/assets/web/dist/assets/{twig-BDXsr482.js → twig-Lx1G7PnY.js} +1 -1
  106. package/dist/assets/web/dist/assets/{typescript-s100Dt8y.js → typescript-BQd7biLF.js} +1 -1
  107. package/dist/assets/web/dist/assets/{typst-WfyEAUdt.js → typst-D2lXPHK7.js} +1 -1
  108. package/dist/assets/web/dist/assets/{vue-CkXOOJ-6.js → vue-4wVQD9y9.js} +1 -1
  109. package/dist/assets/web/dist/assets/{vue-html-CEpzdcau.js → vue-html-DZPaRte4.js} +1 -1
  110. package/dist/assets/web/dist/assets/{vue-vine-D9dsfiFA.js → vue-vine-DbmZCmu-.js} +1 -1
  111. package/dist/assets/web/dist/assets/{xml-iBDofbJq.js → xml-DV16szyo.js} +1 -1
  112. package/dist/assets/web/dist/assets/{xsl-CqbJF4hD.js → xsl-eo5-1sk0.js} +1 -1
  113. package/dist/assets/web/dist/assets/{yaml-D9i6OxYe.js → yaml-BXdX8HZf.js} +1 -1
  114. package/dist/cli.js +960 -520
  115. package/package.json +1 -1
  116. package/dist/assets/web/dist/assets/AppShell-DZdz_sl7.js +0 -1
  117. package/dist/assets/web/dist/assets/ResidentsIndexPage-CZUGLen6.js +0 -1
  118. package/dist/assets/web/dist/assets/RunsIndexPage-s0VKmRda.js +0 -1
  119. package/dist/assets/web/dist/assets/SettingsPage-DhKEokGK.js +0 -1
  120. package/dist/assets/web/dist/assets/main-B77vAZVg.css +0 -1
  121. package/dist/assets/web/dist/assets/residentsModel-VSJEepL1.js +0 -1
package/dist/cli.js CHANGED
@@ -697,7 +697,7 @@ function validatePolicy(rules) {
697
697
  }
698
698
  }
699
699
  }
700
- var grant, MEMBER_OF, IS_SELF, OWNER_OF, ALL_CHANNELS, POLICY, ACTOR_KINDS, CONDITION_KINDS, REQUIRED_ATTRIBUTE, PLACEHOLDER, PolicyError;
700
+ var grant, MEMBER_OF, IS_SELF, ACTS_AS_PERSON, OWNER_OF, ALL_CHANNELS, POLICY, ACTOR_KINDS, CONDITION_KINDS, REQUIRED_ATTRIBUTE, PLACEHOLDER, PolicyError;
701
701
  var init_policy = __esm({
702
702
  "../../src/core/authz/policy.ts"() {
703
703
  "use strict";
@@ -705,6 +705,7 @@ var init_policy = __esm({
705
705
  grant = (g) => ({ kind: "has-grant", grant: g });
706
706
  MEMBER_OF = { kind: "member-of" };
707
707
  IS_SELF = { kind: "is-self" };
708
+ ACTS_AS_PERSON = { kind: "acts-as-person" };
708
709
  OWNER_OF = { kind: "owner-of" };
709
710
  ALL_CHANNELS = { kind: "all-channels" };
710
711
  POLICY = [
@@ -799,6 +800,13 @@ var init_policy = __esm({
799
800
  // ── mcp (external MCP servers live in the three config tiers) ────────────
800
801
  { action: "mcp:read", resource: "command", when: [grant("mcp:read")] },
801
802
  { action: "mcp:write", resource: "command", when: [grant("mcp:write")] },
803
+ // A person always has their own tier to write (`mcp add|connect|remove`
804
+ // for themselves), as `config set me` already is theirs: the row admits an
805
+ // actor whose `self` names a chat identity — every chat user (who holds
806
+ // `mcp:write` by baseline anyway) and a dashboard session linked to its
807
+ // person (record 0042) — never an unlinked browser session or a credential.
808
+ // Which tier the write reaches stays the handler's question below.
809
+ { action: "mcp:write", resource: "command", actorKinds: ["user"], when: [ACTS_AS_PERSON] },
802
810
  // A CHANNEL's servers: the channel-config right for a person; a credential
803
811
  // an admin minted with `mcp:write` manages any tier it can name.
804
812
  { action: "mcp:write", resource: "config-scope", resourceKind: "channel", when: [grant("config:write")] },
@@ -864,6 +872,7 @@ var init_policy = __esm({
864
872
  "has-grant",
865
873
  "member-of",
866
874
  "is-self",
875
+ "acts-as-person",
867
876
  "owner-of",
868
877
  "all-channels"
869
878
  ];
@@ -918,13 +927,20 @@ function principalOf(actor) {
918
927
  while (current.onBehalfOf) current = current.onBehalfOf;
919
928
  return current;
920
929
  }
930
+ function selfIdsOf(actor) {
931
+ const principal = principalOf(actor);
932
+ return principal.self && principal.self.length > 0 ? principal.self : [principal.id];
933
+ }
934
+ function actsAsPerson(selfIds) {
935
+ return selfIds.some((id) => id.startsWith("slack:"));
936
+ }
921
937
  function deny(reason2) {
922
938
  return { allow: false, reason: reason2 };
923
939
  }
924
940
  function isKnownActorKind(kind) {
925
941
  return ACTOR_KINDS.includes(kind);
926
942
  }
927
- function evaluateCondition(condition, grants, selfId, attributes) {
943
+ function evaluateCondition(condition, grants, selfIds, attributes) {
928
944
  switch (condition.kind) {
929
945
  case "has-grant": {
930
946
  const grant2 = resolveGrant(condition.grant, attributes);
@@ -933,7 +949,9 @@ function evaluateCondition(condition, grants, selfId, attributes) {
933
949
  case "member-of":
934
950
  return attributes.channelId !== void 0 && holds(grants.channels, attributes.channelId) || attributes.channelVisibility === "public";
935
951
  case "is-self":
936
- return attributes.userId !== void 0 && attributes.userId === selfId;
952
+ return attributes.userId !== void 0 && selfIds.includes(attributes.userId);
953
+ case "acts-as-person":
954
+ return actsAsPerson(selfIds);
937
955
  case "owner-of":
938
956
  return attributes.repo !== void 0 && holds(grants.repos, attributes.repo);
939
957
  case "all-channels":
@@ -954,10 +972,10 @@ function authorizeWith(rules, actor, action, resource) {
954
972
  );
955
973
  if (forOrigin.length === 0) return deny("origin-visibility");
956
974
  const grants = effectiveGrants(actor);
957
- const selfId = principalOf(actor).id;
975
+ const selfIds = selfIdsOf(actor);
958
976
  let reason2;
959
977
  for (const rule of forOrigin) {
960
- const failed = rule.when.find((condition) => !evaluateCondition(condition, grants, selfId, attributes));
978
+ const failed = rule.when.find((condition) => !evaluateCondition(condition, grants, selfIds, attributes));
961
979
  if (!failed) return ALLOW;
962
980
  reason2 ??= FAILURE_REASON[failed.kind];
963
981
  }
@@ -975,6 +993,7 @@ var init_authorize = __esm({
975
993
  ALLOW = Object.freeze({ allow: true });
976
994
  FAILURE_REASON = {
977
995
  "has-grant": "missing-grant",
996
+ "acts-as-person": "not-person",
978
997
  "member-of": "not-member",
979
998
  "is-self": "not-self",
980
999
  "owner-of": "not-owner",
@@ -2540,6 +2559,9 @@ function unitSlug(unit) {
2540
2559
  function unitBranch(planId, slug) {
2541
2560
  return `plan/${planId}/${slug}`;
2542
2561
  }
2562
+ function isUnitBranch(ref) {
2563
+ return /^plan\/[^/]+\/[^/]+$/.test(ref);
2564
+ }
2543
2565
  function parsePlanBranch(branch2) {
2544
2566
  const m = PLAN_BRANCH.exec(branch2);
2545
2567
  return m ? { planId: m[1], unitSlug: m[2] } : void 0;
@@ -10044,10 +10066,15 @@ function wakePause(ms, signal2) {
10044
10066
  });
10045
10067
  }
10046
10068
  function reboundOf(value) {
10069
+ const move = returnedOf(value);
10070
+ return move !== void 0 && move.pr !== void 0 ? { from: move.from, to: move.to, pr: move.pr } : void 0;
10071
+ }
10072
+ function returnedOf(value) {
10047
10073
  if (typeof value !== "object" || value === null) return void 0;
10048
10074
  const v = value;
10049
- if (typeof v.from !== "string" || typeof v.to !== "string" || typeof v.pr !== "number") return void 0;
10050
- if (!v.from || !v.to || !Number.isSafeInteger(v.pr) || v.pr <= 0) return void 0;
10075
+ if (typeof v.from !== "string" || typeof v.to !== "string" || !v.from || !v.to) return void 0;
10076
+ if (v.pr === void 0) return { from: v.from, to: v.to };
10077
+ if (typeof v.pr !== "number" || !Number.isSafeInteger(v.pr) || v.pr <= 0) return void 0;
10051
10078
  return { from: v.from, to: v.to, pr: v.pr };
10052
10079
  }
10053
10080
  function rebindRefusedOf(value) {
@@ -10288,7 +10315,7 @@ var init_resident = __esm({
10288
10315
  const trace = sanitizeGraftedSteps(data.trace);
10289
10316
  const rebound = reboundOf(data.rebound);
10290
10317
  const rebindRefused = rebindRefusedOf(data.rebindRefused);
10291
- const returned = reboundOf(data.returned);
10318
+ const returned = returnedOf(data.returned);
10292
10319
  this.lastBinding = {
10293
10320
  ref: data.ref,
10294
10321
  sha: data.sha,
@@ -10858,7 +10885,7 @@ function rebindLabel(binding) {
10858
10885
  if (binding.rebound) return ` \xB7 rebound to ${binding.rebound.to} (this thread's PR #${binding.rebound.pr})`;
10859
10886
  if (binding.returned) {
10860
10887
  const r = binding.returned;
10861
- return ` \xB7 returned to ${r.to} (the branch of this thread's PR #${r.pr} is gone)`;
10888
+ return r.pr !== void 0 ? ` \xB7 returned to ${r.to} (the branch of this thread's PR #${r.pr} is gone)` : ` \xB7 returned to ${r.to} (this thread's branch ${r.from} is gone)`;
10862
10889
  }
10863
10890
  if (binding.rebindRefused) {
10864
10891
  const r = binding.rebindRefused;
@@ -15232,6 +15259,7 @@ var init_commandRegistry = __esm({
15232
15259
  commandId: cmd.id,
15233
15260
  callerKind: caller.kind,
15234
15261
  callerId: caller.id,
15262
+ ...caller.actor.asUser ? { asUser: caller.actor.asUser.id } : {},
15235
15263
  effect: cmd.effect,
15236
15264
  outcome: res.ok ? "ok" : res.error,
15237
15265
  ...reason2 === void 0 ? {} : { reason: reason2 },
@@ -15416,8 +15444,14 @@ function assertMayEditChannel(caller, channel) {
15416
15444
  if (!mayEditChannel(caller, channel))
15417
15445
  throw new CommandError("unauthorized", "Channel config changes are restricted.");
15418
15446
  }
15419
- function assertMeWritable(caller) {
15420
- if (caller.kind === "access") throw new CommandError("unauthorized", ME_ON_ACCESS_MESSAGE);
15447
+ function meIdOf(caller) {
15448
+ if (caller.kind !== "access") return caller.id;
15449
+ return caller.actor.self?.find((id) => id.startsWith("slack:"));
15450
+ }
15451
+ function meIdOrRefuse(caller) {
15452
+ const id = meIdOf(caller);
15453
+ if (id === void 0) throw new CommandError("unauthorized", ME_ON_ACCESS_MESSAGE);
15454
+ return id;
15421
15455
  }
15422
15456
  async function mayReadChannel(caller, channel, deps) {
15423
15457
  const origin = caller.origin?.channelId;
@@ -15477,7 +15511,7 @@ var init_config4 = __esm({
15477
15511
  const channel = targetChannel(caller, options.channel);
15478
15512
  await assertMayReadChannel(caller, channel, deps);
15479
15513
  const description = {
15480
- ...await deps.config.describeConfig(channel, caller.id),
15514
+ ...await deps.config.describeConfig(channel, meIdOf(caller) ?? caller.id),
15481
15515
  channelConfigRestricted: !mayEditChannel(caller, channel)
15482
15516
  };
15483
15517
  return description;
@@ -15570,8 +15604,7 @@ var init_config4 = __esm({
15570
15604
  assertMayEditChannel(caller, channel);
15571
15605
  effective = await deps.config.setChannelOverride(channel, patch);
15572
15606
  } else {
15573
- assertMeWritable(caller);
15574
- effective = await deps.config.setUserOverride(caller.id, patch);
15607
+ effective = await deps.config.setUserOverride(meIdOrRefuse(caller), patch);
15575
15608
  }
15576
15609
  return { scope: args.scope, effective: summarizeScope(effective) };
15577
15610
  }
@@ -15590,8 +15623,7 @@ var init_config4 = __esm({
15590
15623
  assertMayEditChannel(caller, channel);
15591
15624
  await deps.config.clearChannelOverride(channel);
15592
15625
  } else {
15593
- assertMeWritable(caller);
15594
- await deps.config.clearUserOverride(caller.id);
15626
+ await deps.config.clearUserOverride(meIdOrRefuse(caller));
15595
15627
  }
15596
15628
  return { scope: args.scope, cleared: true };
15597
15629
  }
@@ -15633,7 +15665,7 @@ ${quote(current ?? "")}`;
15633
15665
  handler: async ({ args, options, caller, deps }) => {
15634
15666
  const channel = args.scope === "channel" ? targetChannel(caller, options.channel) : void 0;
15635
15667
  if (args.text === void 0 && channel !== void 0) await assertMayReadChannel(caller, channel, deps);
15636
- const scopes = await deps.config.scopes(channel ?? caller.origin?.channelId ?? "", caller.id);
15668
+ const scopes = await deps.config.scopes(channel ?? caller.origin?.channelId ?? "", meIdOf(caller) ?? caller.id);
15637
15669
  const current = (args.scope === "channel" ? scopes.channel : scopes.user).instructions?.trim();
15638
15670
  if (args.text === void 0)
15639
15671
  return { scope: args.scope, action: "show", ...current ? { instructions: current } : {} };
@@ -15650,8 +15682,7 @@ ${quote(current ?? "")}`;
15650
15682
  assertMayEditChannel(caller, channel);
15651
15683
  effective = await deps.config.setChannelOverride(channel, patch);
15652
15684
  } else {
15653
- assertMeWritable(caller);
15654
- effective = await deps.config.setUserOverride(caller.id, patch);
15685
+ effective = await deps.config.setUserOverride(meIdOrRefuse(caller), patch);
15655
15686
  }
15656
15687
  if (text.length === 0) {
15657
15688
  const fromStatic = effective.instructions?.trim();
@@ -15782,7 +15813,25 @@ function anyOf(alternatives) {
15782
15813
  if (flat.length === 1) return flat[0];
15783
15814
  return { kind: "or", of: flat };
15784
15815
  }
15785
- function compileCondition(condition, grants, selfId) {
15816
+ function allOf(parts) {
15817
+ const flat = [];
15818
+ for (const p of parts) {
15819
+ if (p.kind === "none") return NONE;
15820
+ if (p.kind === "all") continue;
15821
+ if (p.kind === "and") flat.push(...p.of);
15822
+ else flat.push(p);
15823
+ }
15824
+ if (flat.length === 0) return ALL;
15825
+ if (flat.length === 1) return flat[0];
15826
+ return { kind: "and", of: flat };
15827
+ }
15828
+ function selfPredicate(selfIds) {
15829
+ return anyOf(selfIds.map((userId) => ({ kind: "user-is", userId })));
15830
+ }
15831
+ function ownedBy(actor) {
15832
+ return selfPredicate(selfIdsOf(actor));
15833
+ }
15834
+ function compileCondition(condition, grants, selfIds) {
15786
15835
  switch (condition.kind) {
15787
15836
  case "has-grant": {
15788
15837
  if (grants.actions === "all") return ALL;
@@ -15793,7 +15842,9 @@ function compileCondition(condition, grants, selfId) {
15793
15842
  if (grants.channels === "all") return ALL;
15794
15843
  return anyOf([grants.channels.size === 0 ? NONE : { kind: "channels-in", channelIds: grants.channels }, PUBLIC]);
15795
15844
  case "is-self":
15796
- return { kind: "user-is", userId: selfId };
15845
+ return selfPredicate(selfIds);
15846
+ case "acts-as-person":
15847
+ return actsAsPerson(selfIds) ? ALL : NONE;
15797
15848
  case "owner-of":
15798
15849
  if (grants.repos === "all") return ALL;
15799
15850
  return grants.repos.size === 0 ? NONE : { kind: "repos-in", repos: grants.repos };
@@ -15802,17 +15853,9 @@ function compileCondition(condition, grants, selfId) {
15802
15853
  }
15803
15854
  return NONE;
15804
15855
  }
15805
- function compileRule(rule, grants, selfId) {
15856
+ function compileRule(rule, grants, selfIds) {
15806
15857
  if (rule.originVisibility) return NONE;
15807
- const parts = [];
15808
- for (const condition of rule.when) {
15809
- const part = compileCondition(condition, grants, selfId);
15810
- if (part.kind === "none") return NONE;
15811
- if (part.kind !== "all") parts.push(part);
15812
- }
15813
- if (parts.length === 0) return ALL;
15814
- if (parts.length === 1) return parts[0];
15815
- return { kind: "and", of: parts };
15858
+ return allOf(rule.when.map((condition) => compileCondition(condition, grants, selfIds)));
15816
15859
  }
15817
15860
  function predicateWith(rules, actor, action, resourceType, kind) {
15818
15861
  if (RESOURCE_KINDS[resourceType] && kind === void 0) {
@@ -15822,12 +15865,12 @@ function predicateWith(rules, actor, action, resourceType, kind) {
15822
15865
  if (!target) throw new TypeError(`authz predicate: no target for ${resourceType}${kind ? `/${kind}` : ""}`);
15823
15866
  if (!isKnownActorKind(actor.kind)) return NONE;
15824
15867
  const grants = effectiveGrants(actor);
15825
- const selfId = principalOf(actor).id;
15868
+ const selfIds = selfIdsOf(actor);
15826
15869
  const alternatives = [];
15827
15870
  for (const rule of rules) {
15828
15871
  if (rule.action !== action || ruleTarget(rule) !== target) continue;
15829
15872
  if (rule.actorKinds && !rule.actorKinds.includes(actor.kind)) continue;
15830
- alternatives.push(compileRule(rule, grants, selfId));
15873
+ alternatives.push(compileRule(rule, grants, selfIds));
15831
15874
  }
15832
15875
  return anyOf(alternatives);
15833
15876
  }
@@ -19576,7 +19619,9 @@ async function serviceOf(deps) {
19576
19619
  }
19577
19620
  function actorOf(caller, channel) {
19578
19621
  return {
19579
- id: caller.id,
19622
+ // The person, for a dashboard session linked to one (record 0042): its
19623
+ // own tier, `addedBy` and the connect ticket's email are the person's.
19624
+ id: meIdOf(caller) ?? caller.id,
19580
19625
  orgAdmin: authorize(caller.actor, "mcp:write", { type: "config-scope", kind: "org" }).allow,
19581
19626
  channelAdmin: authorize(caller.actor, "mcp:write", { type: "config-scope", kind: "channel", id: channel ?? "" }).allow
19582
19627
  };
@@ -19585,7 +19630,7 @@ function channelOf2(caller, option) {
19585
19630
  return option ?? caller.origin?.channelId;
19586
19631
  }
19587
19632
  async function writeTarget(svc, caller, actor, word, channel) {
19588
- if ((word ?? "me") === "me" && caller.kind === "access")
19633
+ if ((word ?? "me") === "me" && meIdOf(caller) === void 0)
19589
19634
  throw new CommandError("unauthorized", MCP_ME_ON_ACCESS_MESSAGE);
19590
19635
  return via(() => svc.target(actor, word ?? "me", channel));
19591
19636
  }
@@ -19674,6 +19719,7 @@ var init_mcp = __esm({
19674
19719
  init_config3();
19675
19720
  init_service();
19676
19721
  init_authorize();
19722
+ init_config4();
19677
19723
  init_commandRegistry();
19678
19724
  defineCommand10 = commandDefiner();
19679
19725
  serverName = z13.string().refine((s) => MCP_SERVER_NAME_PATTERN.test(s), "expected a slug: lowercase letters, digits, dashes (\u2264 32 chars)");
@@ -22669,14 +22715,16 @@ var init_runs = __esm({
22669
22715
  sinceMs: z18.coerce.number().int().nonnegative().optional().describe("only runs started at or after this epoch ms"),
22670
22716
  limit: positiveInt3.max(RUN_LIST_MAX_LIMIT).optional().describe(`page size (max ${RUN_LIST_MAX_LIMIT})`),
22671
22717
  before: z18.coerce.number().int().nonnegative().optional().describe("page cursor: runs finished before this epoch ms"),
22672
- beforeId: runId2.optional().describe("page cursor tie-breaker: the last id of the previous page")
22718
+ beforeId: runId2.optional().describe("page cursor tie-breaker: the last id of the previous page"),
22719
+ mine: flag.optional().describe("only the runs you requested \u2014 for a dashboard session, the Slack user its email names (record 0042)")
22673
22720
  }),
22674
22721
  action: "runs:read",
22675
22722
  effect: "read",
22676
22723
  describe: "List runs (live and persisted, newest first) \u2014 metadata only, never message text.",
22677
22724
  handler: async ({ options, caller, deps }) => {
22678
- const visibleTo = predicateFor(caller.actor, "runs:read", "run");
22679
- const { thread, parent, ...rest } = options;
22725
+ const { thread, parent, mine, ...rest } = options;
22726
+ const readable = predicateFor(caller.actor, "runs:read", "run");
22727
+ const visibleTo = mine ? allOf([readable, ownedBy(caller.actor)]) : readable;
22680
22728
  return asJson3(
22681
22729
  await (await deps.runs()).listRuns({
22682
22730
  ...rest,
@@ -25768,7 +25816,13 @@ async function fetchPullRequestFacts(pr) {
25768
25816
  ...typeof data.title === "string" && data.title ? { title: data.title } : {},
25769
25817
  ..."auto_merge" in data ? { autoMergeEnabled: data.auto_merge !== null } : {},
25770
25818
  ..."mergeable" in data ? { mergeable: data.mergeable === null ? null : data.mergeable === true } : {},
25771
- ...typeof data.mergeable_state === "string" && data.mergeable_state ? { mergeableState: data.mergeable_state } : {}
25819
+ ...typeof data.mergeable_state === "string" && data.mergeable_state ? { mergeableState: data.mergeable_state } : {},
25820
+ // The merge commit rides only a merged row: on an open pull request GitHub
25821
+ // reports a test-merge sha under `merge_commit_sha`.
25822
+ ...typeof data.merged_at === "string" && data.merged_at ? {
25823
+ mergedAt: data.merged_at,
25824
+ ...typeof data.merge_commit_sha === "string" && /^[0-9a-f]{40}$/.test(data.merge_commit_sha) ? { mergeCommitSha: data.merge_commit_sha } : {}
25825
+ } : {}
25772
25826
  };
25773
25827
  }
25774
25828
  async function mergePullRequest(pr, opts) {
@@ -32512,6 +32566,14 @@ async function attachRound(deps, ctx) {
32512
32566
  return attached;
32513
32567
  });
32514
32568
  }
32569
+ async function reattachWorkspace2(deps, ctx) {
32570
+ try {
32571
+ return { kind: "attached", round: await attachRound(deps, ctx) };
32572
+ } catch (err2) {
32573
+ if (err2 instanceof WorkspaceReattachRefusedError) return { kind: "reattach_refused", why: err2.why };
32574
+ throw err2;
32575
+ }
32576
+ }
32515
32577
  async function attachWorkspace(deps, ctx) {
32516
32578
  const { msg, io, refuse: refuse3, card, shell, closeLines, clock: clock4, agent, profile, repoCtx, root, reattach } = ctx;
32517
32579
  let round2;
@@ -32908,7 +32970,7 @@ function openThroughSeam(harness, deps, run2) {
32908
32970
  }
32909
32971
  return harness.open(deps, run2);
32910
32972
  }
32911
- var isRecord3, relaunchesOf, isPiFacts, HarnessInterruptedError, HarnessMismatchError;
32973
+ var isRecord3, relaunchesOf, isPiFacts, HarnessInterruptedError, HarnessMismatchError, RELAUNCH_CEILING, HarnessContainerReplacedError;
32912
32974
  var init_contract4 = __esm({
32913
32975
  "../../src/core/harness/contract.ts"() {
32914
32976
  "use strict";
@@ -32939,6 +33001,114 @@ var init_contract4 = __esm({
32939
33001
  expected;
32940
33002
  found;
32941
33003
  };
33004
+ RELAUNCH_CEILING = 2;
33005
+ HarnessContainerReplacedError = class extends HarnessInterruptedError {
33006
+ constructor(message3, said2, was, now, record2) {
33007
+ super(message3, "container replaced under the run", "container_replaced");
33008
+ this.said = said2;
33009
+ this.was = was;
33010
+ this.now = now;
33011
+ this.record = record2;
33012
+ this.name = "HarnessContainerReplacedError";
33013
+ }
33014
+ said;
33015
+ was;
33016
+ now;
33017
+ record;
33018
+ };
33019
+ }
33020
+ });
33021
+
33022
+ // ../../src/core/dispatch/relaunch.ts
33023
+ async function prepareRelaunch(deps, ctx) {
33024
+ const { replaced, facts: facts2, harness } = ctx;
33025
+ const refuse3 = (why, reason2, refusal = "container_replaced") => ({
33026
+ kind: "refused",
33027
+ interruption: new RelaunchRefusedError(why, reason2, refusal)
33028
+ });
33029
+ if (harness.history !== "authored-session")
33030
+ return refuse3(
33031
+ `the ${harness.name} harness keeps its own store, which went with the container, so nothing here can rebuild its process; the run restarts from its request`,
33032
+ "container replaced under the run; the harness keeps its own store; restarting from the request"
33033
+ );
33034
+ if (facts2 === void 0)
33035
+ return refuse3(
33036
+ "the row carries no harness facts to count a relaunch on; the run restarts from its request",
33037
+ "container replaced under the run before its process left facts; restarting from the request"
33038
+ );
33039
+ const relaunches = facts2.relaunches;
33040
+ if (relaunches >= RELAUNCH_CEILING)
33041
+ return refuse3(
33042
+ `the container was replaced under the run again and the relaunch ceiling is ${RELAUNCH_CEILING} (${relaunches} relaunches already), so pi is not started a ${ordinal(relaunches + 2)} time; the run restarts from its request`,
33043
+ `relaunch ceiling: ${relaunches} relaunches already; restarting from the request`
33044
+ );
33045
+ let round2;
33046
+ if (ctx.binding !== void 0) {
33047
+ const reattached = await reattachWorkspace2(deps, {
33048
+ threadKey: ctx.threadKey,
33049
+ agent: ctx.agent,
33050
+ profile: ctx.profile,
33051
+ repoCtx: ctx.repoCtx,
33052
+ root: ctx.root,
33053
+ clock: ctx.clock,
33054
+ reattach: ctx.binding
33055
+ });
33056
+ if (reattached.kind === "reattach_refused")
33057
+ return refuse3(
33058
+ `the run's workspace could not be re-attached in the replacement container (${reattached.why}); the run restarts from its request as a new run in this thread`,
33059
+ "workspace lost with the replaced container; restarting from the request",
33060
+ "workspace_lost"
33061
+ );
33062
+ round2 = reattached.round;
33063
+ }
33064
+ const counted = { ...facts2, relaunches: relaunches + 1 };
33065
+ let written = counted;
33066
+ let bearer;
33067
+ if (deps.runBearers) {
33068
+ const rotated = deps.runBearers.rotate(ctx.runId, (secretHash) => {
33069
+ written = { ...counted, bearerHash: secretHash };
33070
+ ctx.saveFacts(written);
33071
+ });
33072
+ if (!rotated.ok)
33073
+ return refuse3(
33074
+ `the run's bearer could not be rotated for the relaunch (${rotated.reason}); the run restarts from its request`,
33075
+ `the run's bearer could not be rotated (${rotated.reason}); restarting from the request`
33076
+ );
33077
+ bearer = rotated.token;
33078
+ } else {
33079
+ ctx.saveFacts(written);
33080
+ }
33081
+ const { deadline, ...record2 } = replaced.record;
33082
+ return {
33083
+ kind: "relaunch",
33084
+ resume: {
33085
+ ...record2,
33086
+ remainingMs: Math.max(0, deadline - ctx.clock()),
33087
+ facts: written,
33088
+ relaunch: {
33089
+ ...replaced.was !== void 0 ? { from: replaced.was } : {},
33090
+ ...replaced.now !== void 0 ? { to: replaced.now } : {}
33091
+ }
33092
+ },
33093
+ ...bearer !== void 0 ? { bearer } : {},
33094
+ ...round2 !== void 0 ? { round: round2 } : {}
33095
+ };
33096
+ }
33097
+ function ordinal(n2) {
33098
+ return n2 === 2 ? "second" : n2 === 3 ? "third" : n2 === 4 ? "fourth" : `${n2}th`;
33099
+ }
33100
+ var RelaunchRefusedError;
33101
+ var init_relaunch = __esm({
33102
+ "../../src/core/dispatch/relaunch.ts"() {
33103
+ "use strict";
33104
+ init_contract4();
33105
+ init_provision();
33106
+ RelaunchRefusedError = class extends HarnessInterruptedError {
33107
+ constructor(message3, reason2, refusal) {
33108
+ super(message3, reason2, refusal);
33109
+ this.name = "RelaunchRefusedError";
33110
+ }
33111
+ };
32942
33112
  }
32943
33113
  });
32944
33114
 
@@ -32980,7 +33150,7 @@ function startScript(start3) {
32980
33150
  `mkfifo -m 600 ${shellQuote(paths.fifo)}`,
32981
33151
  keepLog ? `: >> ${shellQuote(paths.log)}` : `: > ${shellQuote(paths.log)}`,
32982
33152
  keepLog ? `: >> ${shellQuote(paths.errLog)}` : `: > ${shellQuote(paths.errLog)}`,
32983
- `setsid -f sh -c ${shellQuote(inner)}`,
33153
+ `setsid -f sh -c ${shellQuote(inner)} ${DETACHED_STDIO}`,
32984
33154
  `sleep 0.3`,
32985
33155
  `cat ${shellQuote(paths.pidFile)}`
32986
33156
  ].join(" && ");
@@ -33087,7 +33257,7 @@ function stdoutOf(operation, out) {
33087
33257
  const stdout = cut >= 0 ? out.slice(0, cut) : out;
33088
33258
  return stdout === "(no output)" ? "" : stdout;
33089
33259
  }
33090
- var PORT_ARG, HARNESS_PORT_ENV, HarnessContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, COMMAND_WORD, FREE_PORT_JS, IDENTITY_WORD, METHOD_WORD, PATH_WORD, HEADER_NAME, OP_TIMEOUT_MS, CURL_MAX_TIME_S, secretHeaderEnv, STDERR_MARK, RUNTIME_WORD, HarnessContainerRuntimeReplacedError, ExecHarnessContainer;
33260
+ var PORT_ARG, HARNESS_PORT_ENV, HarnessContainerError, WRITE_CHUNK_CHARS, INLINE_LINE_CHARS, LOG_READ_BYTES, COMMAND_WORD, FREE_PORT_JS, DETACHED_STDIO, IDENTITY_WORD, METHOD_WORD, PATH_WORD, HEADER_NAME, OP_TIMEOUT_MS, CURL_MAX_TIME_S, secretHeaderEnv, STDERR_MARK, RUNTIME_WORD, HarnessContainerRuntimeReplacedError, ExecHarnessContainer;
33091
33261
  var init_container = __esm({
33092
33262
  "../../src/core/harness/container.ts"() {
33093
33263
  "use strict";
@@ -33109,6 +33279,7 @@ var init_container = __esm({
33109
33279
  LOG_READ_BYTES = 48 * 1024;
33110
33280
  COMMAND_WORD = /^[A-Za-z0-9_./-]+$/;
33111
33281
  FREE_PORT_JS = 'const s=require("net").createServer();s.listen(0,"127.0.0.1",()=>{console.log(s.address().port);s.close()})';
33282
+ DETACHED_STDIO = "</dev/null >/dev/null 2>&1";
33112
33283
  IDENTITY_WORD = /^[A-Za-z0-9-]{1,64}$/;
33113
33284
  METHOD_WORD = /^[A-Z]+$/;
33114
33285
  PATH_WORD = /^\/\S*$/;
@@ -34251,12 +34422,32 @@ var init_mirror = __esm({
34251
34422
  seedEchoPending = false;
34252
34423
  mirroredTail;
34253
34424
  iteration = 0;
34425
+ /** The rows written since the mirror started, and the compaction entries
34426
+ * among them by position — kept whether or not the ledger is wired, so
34427
+ * the harness holds this generation's copy of the record it wrote
34428
+ * (`written`), what a relaunch under a living bot rebuilds from. Bounded
34429
+ * by the run as the ledger's transcript and pi's own session file are — one
34430
+ * row per turn under the run's turn guard, the same bytes both already
34431
+ * hold — and never trimmed: a rebuilt session renders every row, with each
34432
+ * compaction where it sat, so a row dropped here would be a row the record
34433
+ * clause lost. */
34434
+ rows = [];
34435
+ compactionRows = [];
34254
34436
  /** The highest ledger inbox seq folded in so far (run-history item 40). */
34255
34437
  inboxConsumedSeq = 0;
34256
34438
  /** Whether a report is owed: the mirror is wired and something happened. */
34257
34439
  get wired() {
34258
34440
  return this.deps.onStep !== void 0;
34259
34441
  }
34442
+ /** The transcript rows this mirror has written since it started (the rows
34443
+ * after the seed or the resumed transcript it was constructed on), and the
34444
+ * compaction entries among them, each `before` counted from the first of
34445
+ * these rows: what a relaunch under a living bot rebuilds from
34446
+ * (harness-pi item 8), this generation's copy of the ledger's rows. A row
34447
+ * met again on a re-attach (`alreadyHeld`) was the caller's and is not here. */
34448
+ get written() {
34449
+ return { messages: [...this.rows], compactions: [...this.compactionRows] };
34450
+ }
34260
34451
  /** The row (the run's own index) the calls under way ride in — the last
34261
34452
  * assistant turn the ledger holds — for the `tool_call` stamp (run-history
34262
34453
  * item 53); undefined before any assistant turn, or when the transcript's
@@ -34277,6 +34468,19 @@ var init_mirror = __esm({
34277
34468
  expectSeedEcho() {
34278
34469
  this.seedEchoPending = true;
34279
34470
  }
34471
+ /** The settlement turn a rebuilt process's session starts on (harness-pi
34472
+ * item 8) — one tool result per call in flight at the death: the relay's
34473
+ * answer, the still-running note or the restart note — primed as the user
34474
+ * content pending for the next step, the write-through's own convention
34475
+ * (the results since the last assistant turn are the one user turn the next
34476
+ * step carries). The ledger's next rows are then the session's,
34477
+ * `[user(settlements + the continue echo), assistant]` from the seed index,
34478
+ * so a rebuild from the ledger at the next death hands the model a
34479
+ * `tool_result` for every `tool_use` and the two records agree on what the
34480
+ * model saw. Never on a re-attach, where pi's own log carries the results. */
34481
+ prime(parts) {
34482
+ this.pendingUser.push(...parts);
34483
+ }
34280
34484
  /** The row about to be written, against the transcript's tail — compared
34281
34485
  * once, with the first row the mirror meets: equal means the ledger holds
34282
34486
  * it already (read again, harness-pi item 8), together with the results
@@ -34295,7 +34499,6 @@ var init_mirror = __esm({
34295
34499
  * there — so the harness can move the row's offset past it. A result or a
34296
34500
  * steer's text waits in memory for the next step, and the answer is no. */
34297
34501
  async onMessage(message3, turn) {
34298
- if (!this.deps.onStep) return false;
34299
34502
  const chat = chatMessageOf(message3);
34300
34503
  if (!chat) return false;
34301
34504
  if (chat.content.length === 0) return false;
@@ -34307,7 +34510,7 @@ var init_mirror = __esm({
34307
34510
  this.pendingUser.push(...chat.content);
34308
34511
  return false;
34309
34512
  }
34310
- if (this.alreadyHeld({ turn: chat })) return true;
34513
+ if (this.alreadyHeld({ turn: chat })) return this.wired;
34311
34514
  const turns = [];
34312
34515
  if (this.pendingUser.length > 0) turns.push({ role: "user", content: this.pendingUser });
34313
34516
  this.pendingUser = [];
@@ -34324,6 +34527,8 @@ var init_mirror = __esm({
34324
34527
  };
34325
34528
  this.idx += turns.length;
34326
34529
  this.lastAssistantIdx = this.idx - 1;
34530
+ this.rows.push(...turns);
34531
+ if (!this.deps.onStep) return false;
34327
34532
  await this.deps.onStep(report2);
34328
34533
  return true;
34329
34534
  }
@@ -34333,8 +34538,7 @@ var init_mirror = __esm({
34333
34538
  * the summary where pi wrote it and the index moves past it. Answers as
34334
34539
  * `onMessage` does: the ledger holds everything up to the entry. */
34335
34540
  async onCompaction(entry, turn) {
34336
- if (!this.deps.onStep) return false;
34337
- if (this.alreadyHeld({ compaction: entry })) return true;
34541
+ if (this.alreadyHeld({ compaction: entry })) return this.wired;
34338
34542
  const turns = [];
34339
34543
  if (this.pendingUser.length > 0) turns.push({ role: "user", content: this.pendingUser });
34340
34544
  this.pendingUser = [];
@@ -34349,6 +34553,9 @@ var init_mirror = __esm({
34349
34553
  inboxConsumedSeq: this.inboxConsumedSeq
34350
34554
  };
34351
34555
  this.idx += turns.length + 1;
34556
+ this.rows.push(...turns);
34557
+ this.compactionRows.push({ before: this.rows.length, entry });
34558
+ if (!this.deps.onStep) return false;
34352
34559
  await this.deps.onStep(report2);
34353
34560
  return true;
34354
34561
  }
@@ -34507,6 +34714,426 @@ var init_process = __esm({
34507
34714
  }
34508
34715
  });
34509
34716
 
34717
+ // ../../src/execution/tracingExecutor.ts
34718
+ var TracingExecutor;
34719
+ var init_tracingExecutor = __esm({
34720
+ "../../src/execution/tracingExecutor.ts"() {
34721
+ "use strict";
34722
+ TracingExecutor = class {
34723
+ constructor(inner, span, backend) {
34724
+ this.inner = inner;
34725
+ this.span = span;
34726
+ this.backend = backend;
34727
+ const innerRelease = inner.release?.bind(inner);
34728
+ if (innerRelease) this.release = (mode) => this.timed("exec.release", (s) => innerRelease(mode, { span: s }));
34729
+ const innerMoveTo = inner.moveTo?.bind(inner);
34730
+ if (innerMoveTo) this.moveTo = (sha) => this.timed("exec.move_to", (s) => innerMoveTo(sha, { span: s }));
34731
+ const innerReadBytes = inner.readBytes?.bind(inner);
34732
+ if (innerReadBytes)
34733
+ this.readBytes = (path) => this.timed("exec.read_bytes", (s) => innerReadBytes(path, { span: s }));
34734
+ }
34735
+ inner;
34736
+ span;
34737
+ backend;
34738
+ release;
34739
+ moveTo;
34740
+ readBytes;
34741
+ /** Each op under its own `exec.*` span, handed to the inner executor as
34742
+ * `opts.span` so its HTTP calls become `http.client` children (item 21). */
34743
+ timed(name, fn, extra = {}) {
34744
+ return this.span.span(name, (s) => fn(s), {
34745
+ attrs: {
34746
+ ...this.backend !== void 0 ? { backend: this.backend } : {},
34747
+ ...extra.timeoutMs !== void 0 ? { timeoutMs: extra.timeoutMs } : {}
34748
+ }
34749
+ });
34750
+ }
34751
+ exec(command2, opts) {
34752
+ return this.timed("exec.exec", (s) => this.inner.exec(command2, { ...opts, span: s }), {
34753
+ timeoutMs: opts?.timeoutMs
34754
+ });
34755
+ }
34756
+ readFile(path) {
34757
+ return this.timed("exec.read_file", (s) => this.inner.readFile(path, { span: s }));
34758
+ }
34759
+ writeFile(path, content) {
34760
+ return this.timed("exec.write_file", (s) => this.inner.writeFile(path, content, { span: s }));
34761
+ }
34762
+ };
34763
+ }
34764
+ });
34765
+
34766
+ // ../../src/core/chatMessage.ts
34767
+ function capToolResultContent(content, cap = MAX_TOOL_RESULT_CHARS) {
34768
+ const capText = (text) => text.length > cap ? `${text.slice(0, cap)}
34769
+ \u2026[tool result truncated: ${text.length - cap} of ${text.length} characters cut \u2014 ask for a narrower slice]` : text;
34770
+ if (typeof content === "string") return capText(content);
34771
+ return content.map((p) => p.type === "text" ? { ...p, text: capText(p.text) } : p);
34772
+ }
34773
+ var MAX_TOOL_RESULT_CHARS;
34774
+ var init_chatMessage = __esm({
34775
+ "../../src/core/chatMessage.ts"() {
34776
+ "use strict";
34777
+ MAX_TOOL_RESULT_CHARS = 12e4;
34778
+ }
34779
+ });
34780
+
34781
+ // ../../src/core/harness/pi/toolRules.ts
34782
+ import { isAbsolute as isAbsolute5, relative, resolve as resolve10 } from "node:path";
34783
+ function reachFor(identity2) {
34784
+ if (identity2 === "write") return CODING_REACH;
34785
+ if (identity2 === "read") return READ_REACH;
34786
+ return NONE_REACH;
34787
+ }
34788
+ function judgeToolCall(tool, input, ctx) {
34789
+ const bundle = PI_TOOL_BUNDLES[tool];
34790
+ if (bundle === void 0) return outside(`${tool} is not in any bundle the ${ctx.identity} identity reaches`);
34791
+ if (!reachFor(ctx.identity).has(bundle)) {
34792
+ return outside(
34793
+ ctx.identity === "none" ? `${tool} is the \`${bundle}\` bundle: a run without a workspace (identity none) has none of pi's own tools` : `${tool} is the \`${bundle}\` bundle, outside the ${ctx.identity} identity's reach`
34794
+ );
34795
+ }
34796
+ const args = isRecord6(input) ? input : {};
34797
+ if (tool === "bash") return judgeBash(args.command, ctx);
34798
+ if (bundle === "files" || bundle === "write-files") return judgePath(args.path, ctx);
34799
+ return allowed;
34800
+ }
34801
+ function judgeBash(command2, ctx) {
34802
+ if (typeof command2 !== "string") return refused3("malformed \u2014 bash without a string command");
34803
+ if (CREDENTIAL_FILE.test(command2)) return refused3("credential \u2014 reads the executor's credential store");
34804
+ if (ENV_DUMP.test(command2)) return refused3("credential \u2014 dumps the process environment");
34805
+ if (CREDENTIAL_VAR.test(command2)) return refused3("credential \u2014 expands a credential variable");
34806
+ if (MERGE_OR_APPROVE.test(command2)) {
34807
+ return refused3("merge/approve \u2014 a coding run never merges or approves a pull request");
34808
+ }
34809
+ if (ctx.identity !== "write") {
34810
+ if (ANY_PUSH.test(command2)) return refused3("read-only \u2014 a read-identity run never pushes");
34811
+ if (GH_WRITE_VERB.test(command2) || GH_API_WRITE.test(command2) || CURL_GITHUB_WRITE.test(command2)) {
34812
+ return refused3("read-only \u2014 a read-identity run never writes to GitHub");
34813
+ }
34814
+ return allowed;
34815
+ }
34816
+ for (const match of command2.matchAll(GIT_PUSH)) {
34817
+ const verdict = judgePush(match[1], ctx);
34818
+ if (verdict.verdict !== "allowed") return verdict;
34819
+ }
34820
+ return allowed;
34821
+ }
34822
+ function judgePush(tail, ctx) {
34823
+ const words = tail.split(/\s+/).filter((w) => w.length > 0 && !w.startsWith("-"));
34824
+ const [remote, refspec] = words;
34825
+ if (remote !== void 0 && remote !== "origin") {
34826
+ return refused3(`repo:use \u2014 push to remote \`${remote}\`, not the run's repository (origin)`);
34827
+ }
34828
+ if (refspec === void 0) return allowed;
34829
+ const destination = refspec.replace(/^\+/, "").split(":").pop() ?? refspec;
34830
+ if (ctx.branch !== void 0) {
34831
+ const branch3 = destination === "HEAD" ? ctx.branch : destination.replace(/^refs\/heads\//, "");
34832
+ if (branch3 !== ctx.branch) return refused3(`repo:use \u2014 push to \`${branch3}\`, not the run's branch ${ctx.branch}`);
34833
+ return allowed;
34834
+ }
34835
+ const branch2 = destination.replace(/^refs\/heads\//, "");
34836
+ if (branch2 !== "HEAD" && (ctx.protectedBranches ?? []).includes(branch2)) {
34837
+ return refused3(
34838
+ `repo:use \u2014 push to \`${branch2}\`, the branch this run's pull request targets; push your own branch`
34839
+ );
34840
+ }
34841
+ return allowed;
34842
+ }
34843
+ function judgePath(path, ctx) {
34844
+ if (path === void 0) return allowed;
34845
+ if (typeof path !== "string") return refused3("malformed \u2014 a path tool without a string path");
34846
+ if (CREDENTIAL_FILE.test(path)) return refused3("credential \u2014 reads the executor's credential store");
34847
+ const rel = relative(ctx.checkout, resolve10(ctx.checkout, path));
34848
+ if (rel.startsWith("..") || isAbsolute5(rel)) return refused3(`path \u2014 \`${path}\` resolves outside the checkout`);
34849
+ return allowed;
34850
+ }
34851
+ var PI_TOOL_BUNDLES, CODING_REACH, READ_REACH, NONE_REACH, allowed, refused3, outside, isRecord6, CREDENTIAL_FILE, ENV_DUMP, CREDENTIAL_VAR, MERGE_OR_APPROVE, GIT_THEN_PUSH, GIT_PUSH, ANY_PUSH, GH_WRITE_VERB, WRITE_METHOD, GH_API_WRITE, CURL_GITHUB_WRITE;
34852
+ var init_toolRules = __esm({
34853
+ "../../src/core/harness/pi/toolRules.ts"() {
34854
+ "use strict";
34855
+ PI_TOOL_BUNDLES = {
34856
+ bash: "shell",
34857
+ read: "files",
34858
+ grep: "files",
34859
+ find: "files",
34860
+ ls: "files",
34861
+ write: "write-files",
34862
+ edit: "write-files",
34863
+ submit_pr_description: "pr",
34864
+ submit_verdict: "verdict"
34865
+ };
34866
+ CODING_REACH = /* @__PURE__ */ new Set([
34867
+ "shell",
34868
+ "files",
34869
+ "write-files",
34870
+ "web",
34871
+ "github-read",
34872
+ "github-write",
34873
+ "pr"
34874
+ ]);
34875
+ READ_REACH = /* @__PURE__ */ new Set(["shell", "files", "web", "search", "github-read", "verdict"]);
34876
+ NONE_REACH = /* @__PURE__ */ new Set();
34877
+ allowed = { verdict: "allowed" };
34878
+ refused3 = (reason2) => ({ verdict: "refused", reason: reason2 });
34879
+ outside = (reason2) => ({ verdict: "outside-profile", reason: reason2 });
34880
+ isRecord6 = (v) => typeof v === "object" && v !== null;
34881
+ CREDENTIAL_FILE = /(^|[\s/'"`])(\.git\/)?(github-credentials|\.git-credentials)($|[\s'"`|;&])/;
34882
+ ENV_DUMP = /(^|[;&|(]\s*)((printenv|env)(\s+-[A-Za-z0-9-]+)*|export\s+-p|declare\s+-[a-zA-Z]*[px][a-zA-Z]*|set)\s*($|[|;&>)])|\/proc\/(self|\d+)\/environ|process\.env(?![.[\w])/;
34883
+ CREDENTIAL_VAR = /\$\{?[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b|printenv\s+[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b/;
34884
+ MERGE_OR_APPROVE = /\bgh\s+pr\s+(merge|review)\b|\/pulls\/\d+\/(merge|reviews)\b/;
34885
+ GIT_THEN_PUSH = String.raw`\bgit(?:\s+(?:-C\s+\S+|--git-dir=\S+|--work-tree=\S+|-c\s+\S+|--no-pager))*\s+push\b`;
34886
+ GIT_PUSH = new RegExp(`${GIT_THEN_PUSH}([^;&|]*)`, "g");
34887
+ ANY_PUSH = new RegExp(GIT_THEN_PUSH);
34888
+ GH_WRITE_VERB = /\bgh\s+(pr|issue)\s+(comment|create|edit|close|reopen|ready|lock|unlock|review|merge|delete|transfer|pin|unpin)\b/;
34889
+ WRITE_METHOD = "(POST|post|PATCH|patch|PUT|put|DELETE|delete)";
34890
+ GH_API_WRITE = new RegExp(
34891
+ `\\bgh\\s+api\\b(?=[^;&|]*(?:\\s(?:-X|--method)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-f|-F|--field|--raw-field|--input)[\\s=]))`
34892
+ );
34893
+ CURL_GITHUB_WRITE = new RegExp(
34894
+ `\\bcurl\\b(?=[^;&|]*api\\.github\\.com)(?=[^;&|]*(?:\\s(?:-X|--request)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-d|--data(?:-\\w+)?|--json|-T|--upload-file)[\\s=]))`
34895
+ );
34896
+ }
34897
+ });
34898
+
34899
+ // ../../src/core/harness/pi/relay.ts
34900
+ function stillRunningNote(tool) {
34901
+ return `This ${tool} call is still running in the bot: the process that made it was relaunched while the call was in flight, and the call keeps running there rather than being re-run \u2014 do not run it again; its result is kept on the relay under the same call id.`;
34902
+ }
34903
+ async function relayToolCall(harness, calls, ask, opts = {}) {
34904
+ const answer = calls.join(ask.toolCallId, () => runRelayedTool(harness, ask, { signal: calls.signal }));
34905
+ const window = new AbortController();
34906
+ const sleep2 = opts.sleep ?? sleepUnlessAborted;
34907
+ const progress = await Promise.race([
34908
+ answer.then((a) => ({ done: true, answer: a })),
34909
+ sleep2(opts.windowMs ?? RELAY_POLL_WINDOW_MS, window.signal).then(() => ({ done: false }))
34910
+ ]);
34911
+ window.abort();
34912
+ return progress;
34913
+ }
34914
+ function relayedToolDefinitions(harness) {
34915
+ return harness.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
34916
+ }
34917
+ function authorizeToolCall(harness, ask) {
34918
+ harness.gateSaw(ask.toolCallId);
34919
+ const blocked = harness.toolsBlocked();
34920
+ const refuse3 = (reason2) => {
34921
+ harness.emit({
34922
+ type: "run_note",
34923
+ kind: "tool_refused",
34924
+ summary: redactAndCap(`${ask.tool} refused: ${reason2}`, 300)
34925
+ });
34926
+ return { allow: false, reason: reason2 };
34927
+ };
34928
+ if (blocked !== void 0) return refuse3(blocked);
34929
+ if (harness.tools.some((t) => t.name === ask.tool)) return { allow: true };
34930
+ const verdict = judgeToolCall(ask.tool, ask.input, harness.rules);
34931
+ if (verdict.verdict === "allowed") return { allow: true };
34932
+ return refuse3(verdict.reason);
34933
+ }
34934
+ async function runRelayedTool(harness, ask, opts = {}) {
34935
+ const tool = harness.tools.find((t) => t.name === ask.tool);
34936
+ if (!tool) return { content: [{ type: "text", text: `Unknown tool: ${ask.tool}` }], isError: true };
34937
+ const span = harness.toolSpan(ask.toolCallId);
34938
+ const base = harness.toolContext;
34939
+ const readConversation = base.conversation;
34940
+ const ctx = {
34941
+ ...span ? {
34942
+ ...base,
34943
+ span,
34944
+ executor: new TracingExecutor(base.executor, span, harness.backend),
34945
+ publish: (e) => harness.emit(withSpanId(e, span.id)),
34946
+ ...base.github?.api.withSpan ? { github: { ...base.github, api: base.github.api.withSpan(span) } } : {}
34947
+ } : { ...base, publish: (e) => harness.emit(e) },
34948
+ ...opts.signal && !base.signal ? { signal: opts.signal } : {},
34949
+ ...readConversation && harness.callSeen ? { conversation: () => harness.callSeen(ask.toolCallId).then(() => readConversation()) } : {},
34950
+ // After the spreads, so the call's own id can never be shadowed by one on the base context.
34951
+ callId: ask.toolCallId
34952
+ };
34953
+ try {
34954
+ const input = typeof ask.input === "object" && ask.input !== null ? ask.input : {};
34955
+ const output = await tool.run(input, ctx);
34956
+ return { content: piContentOf(capToolResultContent(output)), isError: false };
34957
+ } catch (err2) {
34958
+ return errorAnswerOf(err2);
34959
+ }
34960
+ }
34961
+ function errorAnswerOf(err2) {
34962
+ const message3 = err2 instanceof Error ? err2.message : String(err2);
34963
+ return { content: piContentOf(capToolResultContent(`Error: ${message3}`)), isError: true };
34964
+ }
34965
+ function piContentOf(content) {
34966
+ if (typeof content === "string") return [{ type: "text", text: content }];
34967
+ return content.map(
34968
+ (p) => p.type === "text" ? { type: "text", text: p.text } : p.type === "image" ? { type: "image", data: p.data, mimeType: p.mediaType } : { type: "text", text: `[document ${p.name ?? "document"} (${p.mediaType})]` }
34969
+ );
34970
+ }
34971
+ function withSpanId(e, spanId) {
34972
+ switch (e.type) {
34973
+ case "tool_call":
34974
+ case "tool_result":
34975
+ case "run_note":
34976
+ case "assistant":
34977
+ case "skill_use":
34978
+ return { ...e, spanId };
34979
+ default:
34980
+ return e;
34981
+ }
34982
+ }
34983
+ var HarnessRegistry, RELAY_POLL_WINDOW_MS, RelayedCalls;
34984
+ var init_relay = __esm({
34985
+ "../../src/core/harness/pi/relay.ts"() {
34986
+ "use strict";
34987
+ init_tracingExecutor();
34988
+ init_chatMessage();
34989
+ init_awaitChildren();
34990
+ init_runEvents();
34991
+ init_toolRules();
34992
+ HarnessRegistry = class {
34993
+ live = /* @__PURE__ */ new Map();
34994
+ /** The run drives a pi from here, its relayed calls kept beside it; the
34995
+ * returned function forgets it and ends whatever call still runs. */
34996
+ register(harness) {
34997
+ return this.hold({ harness, calls: new RelayedCalls() });
34998
+ }
34999
+ /** A process relaunched under the same run — in a replacement container,
35000
+ * with a rotated bearer (harness-pi item 8) — takes the run's registration
35001
+ * over, and the run's relayed calls stay: a call in flight keeps running
35002
+ * under the same end signal, and an ask by its id through the new
35003
+ * registration joins it and reads its answer. The earlier registration's
35004
+ * forget is a no-op from here; the returned one forgets the run and ends
35005
+ * the calls. Refuses by name a run that is not registered. */
35006
+ replace(harness) {
35007
+ const previous = this.live.get(harness.runId);
35008
+ if (!previous) throw new Error(`run ${harness.runId} is not registered on the relay: nothing to replace`);
35009
+ return this.hold({ harness, calls: previous.calls });
35010
+ }
35011
+ /** The run forgotten whichever registration holds it, its calls ended: the
35012
+ * run loop's, for a run whose process was found gone with its container
35013
+ * and is not relaunched — the harness left the registration standing so a
35014
+ * relaunch could take it over (`replace`) with the calls kept, and a
35015
+ * refusal has to end them. A run not registered is nothing to forget. */
35016
+ forget(runId3) {
35017
+ const entry = this.live.get(runId3);
35018
+ if (!entry) return;
35019
+ this.live.delete(runId3);
35020
+ entry.calls.end();
35021
+ }
35022
+ hold(entry) {
35023
+ this.live.set(entry.harness.runId, entry);
35024
+ return () => {
35025
+ if (this.live.get(entry.harness.runId) !== entry) return;
35026
+ this.live.delete(entry.harness.runId);
35027
+ entry.calls.end();
35028
+ };
35029
+ }
35030
+ get(runId3) {
35031
+ return this.live.get(runId3)?.harness;
35032
+ }
35033
+ /** The run's relayed calls, for as long as it is registered. */
35034
+ calls(runId3) {
35035
+ return this.live.get(runId3)?.calls;
35036
+ }
35037
+ size() {
35038
+ return this.live.size;
35039
+ }
35040
+ };
35041
+ RELAY_POLL_WINDOW_MS = 3e4;
35042
+ RelayedCalls = class {
35043
+ calls = /* @__PURE__ */ new Map();
35044
+ /** The ids whose call is still running: joined by `join`, forgotten as each answers. */
35045
+ running = /* @__PURE__ */ new Set();
35046
+ /** The ids answered from the record (`settle`), never run here. */
35047
+ settledIds = /* @__PURE__ */ new Set();
35048
+ ending = new AbortController();
35049
+ get size() {
35050
+ return this.calls.size;
35051
+ }
35052
+ /** Aborted when the run ends: the signal every call here runs under. */
35053
+ get signal() {
35054
+ return this.ending.signal;
35055
+ }
35056
+ /** The call's answer as a promise: started by `start` on the first ask, the same promise after. */
35057
+ join(callId, start3) {
35058
+ let answer = this.calls.get(callId);
35059
+ if (!answer) {
35060
+ answer = start3();
35061
+ this.calls.set(callId, answer);
35062
+ this.running.add(callId);
35063
+ const done = () => void this.running.delete(callId);
35064
+ answer.then(done, done);
35065
+ }
35066
+ return answer;
35067
+ }
35068
+ /** The ids of the calls still running, in the order they were started. */
35069
+ inFlight() {
35070
+ return [...this.running];
35071
+ }
35072
+ /** A call asked here whose answer already landed: the answer, or nothing
35073
+ * for a call never asked, one still running (`awaitInFlight` reads those)
35074
+ * or one settled from the record (`settle`: the record's answer, not the
35075
+ * bot's). What a relaunch reads for a call the record names beside the
35076
+ * calls it awaits (harness-pi item 8): an answer that landed after the
35077
+ * process died and before it could ask again is carried into the rebuilt
35078
+ * session too, never lost to a restart note. A `start` that rejected is
35079
+ * the error answer `runRelayedTool` would have given. */
35080
+ async answered(callId) {
35081
+ const answer = this.calls.get(callId);
35082
+ if (!answer || this.running.has(callId) || this.settledIds.has(callId)) return void 0;
35083
+ return answer.then(
35084
+ (a) => a,
35085
+ (err2) => errorAnswerOf(err2)
35086
+ );
35087
+ }
35088
+ /** Every call still running, awaited together up to one window — the relay
35089
+ * window a request waits, by default — for a relaunch that rebuilds the
35090
+ * process's session (harness-pi item 8): a call that answers inside it is
35091
+ * `done` with its answer, the result the rebuilt session carries; one still
35092
+ * running after it is not, keeps running here for an ask by the same id,
35093
+ * and the rebuilt session carries the still-running note in its place
35094
+ * (`stillRunningNote`). A call whose `start` rejected is `done` with an
35095
+ * error answer, as `runRelayedTool` answers its own throws, so one
35096
+ * straggler's exception never loses the others' answers. Nothing is ended
35097
+ * or re-run. Answers at once with nothing in flight. */
35098
+ async awaitInFlight(opts = {}) {
35099
+ const ids = this.inFlight();
35100
+ if (ids.length === 0) return [];
35101
+ const window = new AbortController();
35102
+ const sleep2 = opts.sleep ?? sleepUnlessAborted;
35103
+ const closed = sleep2(opts.windowMs ?? RELAY_POLL_WINDOW_MS, window.signal).then(() => ({
35104
+ done: false
35105
+ }));
35106
+ const progress = await Promise.all(
35107
+ ids.map(async (callId) => {
35108
+ const answered = this.calls.get(callId).then(
35109
+ (answer) => ({ done: true, answer }),
35110
+ (err2) => ({ done: true, answer: errorAnswerOf(err2) })
35111
+ );
35112
+ return { callId, ...await Promise.race([answered, closed]) };
35113
+ })
35114
+ );
35115
+ window.abort();
35116
+ return progress;
35117
+ }
35118
+ /** The call's answer is known before it is asked (harness-pi item 8): the
35119
+ * record's settlement for a call in flight when the previous generation
35120
+ * died, whose extension asks again with the same id. Every ask reads it and
35121
+ * the tool never runs; a call already joined keeps the answer it has. */
35122
+ settle(callId, answer) {
35123
+ if (this.calls.has(callId)) return;
35124
+ this.calls.set(callId, Promise.resolve(answer));
35125
+ this.settledIds.add(callId);
35126
+ }
35127
+ end() {
35128
+ this.ending.abort();
35129
+ this.calls.clear();
35130
+ this.running.clear();
35131
+ this.settledIds.clear();
35132
+ }
35133
+ };
35134
+ }
35135
+ });
35136
+
34510
35137
  // ../../src/core/harness/pi/transport.ts
34511
35138
  var NEWLINE, PiRpcTransport;
34512
35139
  var init_transport = __esm({
@@ -34642,16 +35269,25 @@ function promptOf(seed) {
34642
35269
  function settlementText(s) {
34643
35270
  return s.action === "synthetic" ? s.text : `The bot restarted while this ${s.toolUse.name} call was in flight; its result was lost \u2014 re-check its effects before re-running it.`;
34644
35271
  }
34645
- function settlementResults(settlements) {
35272
+ function settlementResults(settlements, settled = /* @__PURE__ */ new Map()) {
34646
35273
  if (settlements.length === 0) return void 0;
34647
35274
  return {
34648
35275
  role: "user",
34649
- content: settlements.map((s) => ({
34650
- type: "tool_result",
34651
- toolUseId: s.toolUse.id,
34652
- content: settlementText(s),
34653
- isError: true
34654
- }))
35276
+ content: settlements.map((s) => {
35277
+ const relay = settled.get(s.toolUse.id);
35278
+ return {
35279
+ type: "tool_result",
35280
+ toolUseId: s.toolUse.id,
35281
+ content: relay?.text ?? settlementText(s),
35282
+ ...relay?.isError ?? true ? { isError: true } : {}
35283
+ };
35284
+ })
35285
+ };
35286
+ }
35287
+ function relaySettlementOf(answer) {
35288
+ return {
35289
+ text: answer.content.map((c) => c.type === "text" ? c.text : "[image]").join("\n"),
35290
+ isError: answer.isError
34655
35291
  };
34656
35292
  }
34657
35293
  function ledgerTailOf(resume) {
@@ -34782,12 +35418,36 @@ async function runPiHarnessOpen(deps, run2) {
34782
35418
  await deps.sleep(seenTick);
34783
35419
  }
34784
35420
  };
34785
- const forget = deps.registry.register(live);
35421
+ const relaunch = run2.resume?.relaunch;
35422
+ const forget = relaunch !== void 0 && deps.registry.get(run2.runId) !== void 0 ? deps.registry.replace(live) : deps.registry.register(live);
34786
35423
  const calls = deps.registry.calls(run2.runId);
34787
35424
  for (const s of run2.resume?.settlements ?? []) calls?.settle(s.toolUse.id, settlementAnswer(s));
34788
35425
  let pid;
34789
35426
  let transport;
34790
35427
  let facts2;
35428
+ const recordBase = {
35429
+ messages: run2.resume?.messages ?? run2.messages,
35430
+ compactions: run2.resume?.compactions ?? []
35431
+ };
35432
+ const relaySettled = /* @__PURE__ */ new Map();
35433
+ let stillRunning = 0;
35434
+ const recordNow = () => {
35435
+ const written = mirror.written;
35436
+ const messages = [...recordBase.messages, ...written.messages];
35437
+ const last = messages.at(-1);
35438
+ const calls2 = last?.role === "assistant" ? last.content.filter((p) => p.type === "tool_use") : [];
35439
+ return {
35440
+ messages,
35441
+ compactions: [
35442
+ ...recordBase.compactions,
35443
+ ...written.compactions.map((c) => ({ ...c, before: recordBase.messages.length + c.before }))
35444
+ ],
35445
+ settlements: calls2.map((toolUse) => ({ toolUse, action: "synthetic", text: replacedCallNote(toolUse.name) })),
35446
+ turn: bridge.turns,
35447
+ inboxConsumedSeq: mirror.inboxConsumedSeq,
35448
+ deadline
35449
+ };
35450
+ };
34791
35451
  let mirrored = 0;
34792
35452
  const save = () => {
34793
35453
  if (facts2) {
@@ -34814,8 +35474,8 @@ async function runPiHarnessOpen(deps, run2) {
34814
35474
  hardStopped ? "the run was hard-stopped" : bypass ? "the run was stopped: a tool call bypassed the gate" : "the run ended"
34815
35475
  );
34816
35476
  save();
34817
- forget();
34818
35477
  if (replaced !== void 0) return;
35478
+ forget();
34819
35479
  if (pid !== void 0) await container.kill(pid).catch(() => {
34820
35480
  });
34821
35481
  if (paths !== void 0) await container.remove(paths).catch(() => {
@@ -34827,7 +35487,7 @@ async function runPiHarnessOpen(deps, run2) {
34827
35487
  let elsewhere;
34828
35488
  const here = await container.identity();
34829
35489
  const honoured = (hash) => hash !== void 0 && (deps.bearers === void 0 || deps.bearers.adopt(run2.runId, hash));
34830
- if (recorded !== void 0) {
35490
+ if (recorded !== void 0 && relaunch === void 0) {
34831
35491
  const located2 = await locatePi(recorded, container, here);
34832
35492
  if (located2 === "another-container") {
34833
35493
  elsewhere = `pi is elsewhere: the row's pi (pid ${recorded.pid}) ran in container ${recorded.container}, not the one this run was handed (${here}), so it was neither probed nor ended here`;
@@ -34862,7 +35522,7 @@ async function runPiHarnessOpen(deps, run2) {
34862
35522
  );
34863
35523
  } else {
34864
35524
  paths = piRunPathsAt(await container.makeRoot(piRunPaths(run2.runId).dir));
34865
- if (recorded?.root !== void 0 && recorded.root !== paths.dir && elsewhere === void 0)
35525
+ if (recorded?.root !== void 0 && recorded.root !== paths.dir && elsewhere === void 0 && relaunch === void 0)
34866
35526
  await container.remove(piRunPathsAt(recorded.root)).catch(() => {
34867
35527
  });
34868
35528
  const spec = {
@@ -34878,12 +35538,33 @@ async function runPiHarnessOpen(deps, run2) {
34878
35538
  };
34879
35539
  let session;
34880
35540
  if (run2.resume) {
34881
- const settled2 = settlementResults(run2.resume.settlements);
35541
+ const wanted = new Map(run2.resume.settlements.map((s) => [s.toolUse.id, s.toolUse.name]));
35542
+ const running = calls?.inFlight().filter((id) => wanted.has(id)) ?? [];
35543
+ if (running.length > 0)
35544
+ run2.onProgress?.(
35545
+ `waiting up to ${Math.round(RELAY_POLL_WINDOW_MS / 1e3)} s for ${running.length} relayed call(s) still running in the bot before pi restarts`
35546
+ );
35547
+ for (const p of await calls?.awaitInFlight({ sleep: (ms) => deps.sleep(ms) }) ?? []) {
35548
+ const tool = wanted.get(p.callId);
35549
+ if (tool === void 0) continue;
35550
+ relaySettled.set(
35551
+ p.callId,
35552
+ p.done ? relaySettlementOf(p.answer) : { text: stillRunningNote(tool), isError: false }
35553
+ );
35554
+ if (!p.done) stillRunning++;
35555
+ }
35556
+ for (const [callId] of wanted) {
35557
+ if (relaySettled.has(callId)) continue;
35558
+ const answer2 = await calls?.answered(callId);
35559
+ if (answer2 !== void 0) relaySettled.set(callId, relaySettlementOf(answer2));
35560
+ }
35561
+ const settled2 = settlementResults(run2.resume.settlements, relaySettled);
34882
35562
  session = {
34883
35563
  stem: "resumed",
34884
35564
  messages: settled2 ? [...run2.resume.messages, settled2] : run2.resume.messages,
34885
35565
  compactions: run2.resume.compactions ?? []
34886
35566
  };
35567
+ if (settled2) mirror.prime(settled2.content);
34887
35568
  } else {
34888
35569
  const earlier2 = splitSeed(run2.messages).session;
34889
35570
  if (earlier2.length > 0) session = { stem: "seed", messages: earlier2, compactions: [] };
@@ -34908,7 +35589,20 @@ async function runPiHarnessOpen(deps, run2) {
34908
35589
  )
34909
35590
  );
34910
35591
  }
34911
- if (run2.resume) {
35592
+ if (run2.resume && relaunch !== void 0) {
35593
+ const inFlight = run2.resume.settlements.length;
35594
+ const answered = relaySettled.size - stillRunning;
35595
+ const lost = inFlight - relaySettled.size;
35596
+ const settledHow = inFlight === 0 ? "nothing was in flight" : `${inFlight} call(s) were in flight: ` + [
35597
+ ...answered > 0 ? [`${answered} answered on the relay`] : [],
35598
+ ...stillRunning > 0 ? [`${stillRunning} still running there`] : [],
35599
+ ...lost > 0 ? [`${lost} lost with the container, each answered with a restart note`] : []
35600
+ ].join(", ");
35601
+ note(
35602
+ "resumed",
35603
+ `relaunched after the container was replaced (${relaunch.from ?? "unknown"} \u2192 ${relaunch.to ?? "unknown"}): the row's pi (pid ${recorded?.pid ?? "unknown"}) went with the old container and was neither probed nor ended here; pi restarted in the container the run holds on the mirrored transcript \u2014 ${settledHow}; ${Math.round(remainingMs / 6e4)} min of budget left`
35604
+ );
35605
+ } else if (run2.resume) {
34912
35606
  const lost = run2.resume.settlements.length;
34913
35607
  const how = elsewhere !== void 0 ? `${elsewhere}, and pi restarted` : ended !== void 0 && recorded !== void 0 ? `the row ${ended} (pid ${recorded.pid}), so it was ended and pi restarted` : "pi restarted";
34914
35608
  note(
@@ -35066,7 +35760,8 @@ ${stagedLine}` : prompt;
35066
35760
  return new PiContainerReplacedError(
35067
35761
  said2.message,
35068
35762
  facts2?.container,
35069
- await container.identity().catch(() => void 0)
35763
+ await container.identity().catch(() => void 0),
35764
+ recordNow()
35070
35765
  );
35071
35766
  };
35072
35767
  check();
@@ -35336,6 +36031,7 @@ var init_harness = __esm({
35336
36031
  init_mirror();
35337
36032
  init_process();
35338
36033
  init_protocol();
36034
+ init_relay();
35339
36035
  init_transport();
35340
36036
  FINALE_TIMEOUT_MS = 3 * 6e4;
35341
36037
  CALL_SEEN_WAIT_MS = 3e3;
@@ -35355,21 +36051,17 @@ var init_harness = __esm({
35355
36051
  this.name = "GateBypassed";
35356
36052
  }
35357
36053
  };
35358
- PiContainerReplacedError = class extends HarnessInterruptedError {
35359
- constructor(said2, was, now) {
36054
+ PiContainerReplacedError = class extends HarnessContainerReplacedError {
36055
+ constructor(said2, was, now, record2) {
35360
36056
  super(
35361
- `the container running pi was replaced (${was ?? "unknown"} \u2192 ${now ?? "unknown"}; the executor said: ${redactAndCap(said2.replace(/\s+/g, " ").trim(), 240)}); the run restarts from its request`,
35362
- "container replaced under the run; restarting from the request",
35363
- "container_replaced"
36057
+ `the container running pi was replaced (${was ?? "unknown"} \u2192 ${now ?? "unknown"}; the executor said: ${redactAndCap(said2.replace(/\s+/g, " ").trim(), 240)})`,
36058
+ said2,
36059
+ was,
36060
+ now,
36061
+ record2
35364
36062
  );
35365
- this.said = said2;
35366
- this.was = was;
35367
- this.now = now;
35368
36063
  this.name = "PiContainerReplacedError";
35369
36064
  }
35370
- said;
35371
- was;
35372
- now;
35373
36065
  };
35374
36066
  POLICY_REFUSAL_REPLY = "the model refused this request under its usage policy \u2014 rephrase it and the thread continues";
35375
36067
  ModelPolicyRefusedError = class extends Error {
@@ -36199,7 +36891,6 @@ async function runLoop(deps, ctx) {
36199
36891
  mcpForRun,
36200
36892
  run: run2,
36201
36893
  registry,
36202
- round: round2,
36203
36894
  admitted,
36204
36895
  ledgerRun,
36205
36896
  resume,
@@ -36226,7 +36917,9 @@ async function runLoop(deps, ctx) {
36226
36917
  seed
36227
36918
  } = ctx;
36228
36919
  const agent = budgetedAgent(ctx.agent, profile);
36229
- const { executor, binding } = round2.selection;
36920
+ const { round: round2 } = ctx;
36921
+ let { executor } = round2.selection;
36922
+ const { binding } = round2.selection;
36230
36923
  const coordinatorBase = (async () => {
36231
36924
  if (coordinator === void 0) return void 0;
36232
36925
  if (coordinator.base !== void 0 || repoCtx.baseRef !== void 0) return coordinator.base;
@@ -36420,6 +37113,12 @@ async function runLoop(deps, ctx) {
36420
37113
  const postedBefore = resume ? reviewPostedBefore(resume.events) : void 0;
36421
37114
  if (postedBefore) reviewHead = postedBefore.head;
36422
37115
  const facts2 = resume ? harnessFactsOf(resume.row.state.harness) : void 0;
37116
+ let lastFacts = facts2;
37117
+ const saveFacts = (h) => {
37118
+ lastFacts = h;
37119
+ ledgerRun?.setState({ harness: h });
37120
+ };
37121
+ let workspaceBinding = workspaceBindingFor(round2.selection, profile.machine);
36423
37122
  try {
36424
37123
  if (finish2) {
36425
37124
  onEvent({
@@ -36490,11 +37189,21 @@ async function runLoop(deps, ctx) {
36490
37189
  )
36491
37190
  )
36492
37191
  ];
36493
- harnessSession = await openThroughSeam(
37192
+ let bearer = ctx.bearer;
37193
+ let harnessResume = reentry ? {
37194
+ messages: reentry.messages,
37195
+ compactions: reentry.compactions,
37196
+ settlements: reentry.settlements,
37197
+ remainingMs: reentry.remainingMs,
37198
+ turn: reentry.turn,
37199
+ inboxConsumedSeq: reentry.inboxConsumedSeq,
37200
+ ...facts2 ? { facts: facts2 } : {}
37201
+ } : void 0;
37202
+ const openRun = () => openThroughSeam(
36494
37203
  harnessDeps.harness,
36495
37204
  {
36496
37205
  container: harnessDeps.containerFor?.(executor, profile.machine) ?? harnessContainerFor(executor, profile.machine),
36497
- bearer: ctx.bearer,
37206
+ bearer,
36498
37207
  harnessUrl,
36499
37208
  registry: harnessDeps.registry,
36500
37209
  ...deps.runBearers ? { bearers: deps.runBearers } : {},
@@ -36525,24 +37234,55 @@ async function runLoop(deps, ctx) {
36525
37234
  ...stageFollowUps ? { stageFollowUps } : {},
36526
37235
  onEvent,
36527
37236
  onProgress,
36528
- ...ledgerRun ? {
36529
- onStep: ledgerRun.step.bind(ledgerRun),
36530
- logIndexOf: ledgerRun.logIndexOf.bind(ledgerRun),
36531
- saveFacts: (h) => ledgerRun.setState({ harness: h })
36532
- } : {},
36533
- ...reentry ? {
36534
- resume: {
36535
- messages: reentry.messages,
36536
- compactions: reentry.compactions,
36537
- settlements: reentry.settlements,
36538
- remainingMs: reentry.remainingMs,
36539
- turn: reentry.turn,
36540
- inboxConsumedSeq: reentry.inboxConsumedSeq,
36541
- ...facts2 ? { facts: facts2 } : {}
36542
- }
36543
- } : {}
37237
+ ...ledgerRun ? { onStep: ledgerRun.step.bind(ledgerRun), logIndexOf: ledgerRun.logIndexOf.bind(ledgerRun) } : {},
37238
+ saveFacts,
37239
+ ...harnessResume ? { resume: harnessResume } : {}
36544
37240
  }
36545
37241
  );
37242
+ for (; ; ) {
37243
+ try {
37244
+ harnessSession = await openRun();
37245
+ break;
37246
+ } catch (err2) {
37247
+ if (!(err2 instanceof HarnessContainerReplacedError)) throw err2;
37248
+ let decision;
37249
+ try {
37250
+ decision = await prepareRelaunch(deps, {
37251
+ runId: run2.id,
37252
+ threadKey: msg.threadKey,
37253
+ agent: ctx.agent,
37254
+ profile,
37255
+ repoCtx,
37256
+ root,
37257
+ clock: clock4,
37258
+ harness: harnessDeps.harness,
37259
+ replaced: err2,
37260
+ facts: lastFacts,
37261
+ binding: workspaceBinding,
37262
+ saveFacts
37263
+ });
37264
+ } catch (failed) {
37265
+ harnessDeps.registry.forget(run2.id);
37266
+ throw failed;
37267
+ }
37268
+ if (decision.kind === "refused") {
37269
+ harnessDeps.registry.forget(run2.id);
37270
+ onEvent({ type: "run_note", kind: "sandbox_restarted", summary: decision.interruption.message });
37271
+ throw decision.interruption;
37272
+ }
37273
+ if (decision.round !== void 0) {
37274
+ executor = decision.round.selection.executor;
37275
+ toolContext.executor = executor;
37276
+ const rebound = workspaceBindingFor(decision.round.selection, profile.machine);
37277
+ if (rebound !== void 0) {
37278
+ workspaceBinding = rebound;
37279
+ ledgerRun?.setState({ binding: rebound });
37280
+ }
37281
+ }
37282
+ if (decision.bearer !== void 0) bearer = decision.bearer;
37283
+ harnessResume = decision.resume;
37284
+ }
37285
+ }
36546
37286
  answer = harnessSession.answer;
36547
37287
  }
36548
37288
  if (isPrReview && repoCtx.repo && repoCtx.pr !== void 0 && run2.control.requested !== "hard" && postedBefore === void 0) {
@@ -36851,7 +37591,9 @@ var init_runLoop = __esm({
36851
37591
  init_provider();
36852
37592
  init_toolsets();
36853
37593
  init_contract4();
37594
+ init_relaunch();
36854
37595
  init_botHostContainer();
37596
+ init_factory();
36855
37597
  init_container();
36856
37598
  init_harness();
36857
37599
  init_windDown();
@@ -37004,7 +37746,7 @@ async function shipPreflight(input) {
37004
37746
  `\u{1F6AB} Nothing to ship: give ship a task (\`agent:ship in ${repo}: <task>\`), or name an open ship PR by URL to resume its review loop.`
37005
37747
  );
37006
37748
  }
37007
- const ref = repoCtx.ref && !repoCtx.refFromPr && repoCtx.ref.toLowerCase() !== repo.toLowerCase() ? repoCtx.ref : void 0;
37749
+ const ref = repoCtx.ref && !repoCtx.refFromPr && !isUnitBranch(repoCtx.ref) && repoCtx.ref.toLowerCase() !== repo.toLowerCase() ? repoCtx.ref : void 0;
37008
37750
  return {
37009
37751
  ok: true,
37010
37752
  entry: { repo, base: resolveBaseRef([ref], info?.defaultBranch) }
@@ -37137,7 +37879,7 @@ function planWhere(p, units, mergedBefore) {
37137
37879
  async function handOffToCoordinator(deps, input) {
37138
37880
  const log = deps.log ?? console.log;
37139
37881
  const planned = await plan(deps, input);
37140
- if (!planned.ok) return refused3(planned.reply);
37882
+ if (!planned.ok) return refused4(planned.reply);
37141
37883
  const p = planned.planned;
37142
37884
  const planOf = () => ({
37143
37885
  id: p.planId,
@@ -37167,15 +37909,15 @@ async function handOffToCoordinator(deps, input) {
37167
37909
  const status3 = await deps.status(latest.id);
37168
37910
  const where = `plan \`${p.planId}\``;
37169
37911
  if (status3.kind === "unanswered")
37170
- return refused3(
37912
+ return refused4(
37171
37913
  `\u26A0\uFE0F The plan runner could not tell whether \`${latest.id}\` still runs: ${status3.reason}. Nothing ran; re-issue the request to try again.`
37172
37914
  );
37173
37915
  if (status3.kind === "status" && RUNNING.has(status3.status))
37174
- return refused3(
37916
+ return refused4(
37175
37917
  `\u{1F6AB} A runner for ${where} is still running (\`${latest.id}\`, status: ${status3.status}): wait for it to end \u2014 or terminate it in the Workflows dashboard \u2014 before re-issuing.`
37176
37918
  );
37177
37919
  if (status3.kind === "status" && !ENDED.has(status3.status))
37178
- return refused3(
37920
+ return refused4(
37179
37921
  `\u{1F6AB} A runner for ${where} (\`${latest.id}\`) is in a state the bot does not read as ended (${status3.status}) \u2014 a person decides.`
37180
37922
  );
37181
37923
  const merged = /* @__PURE__ */ new Set();
@@ -37187,7 +37929,7 @@ async function handOffToCoordinator(deps, input) {
37187
37929
  }
37188
37930
  const remaining = p.selected.filter((u) => !merged.has(u));
37189
37931
  if (remaining.length === 0)
37190
- return refused3(
37932
+ return refused4(
37191
37933
  `\u{1F6AB} Every unit of ${where} this request names is merged already (${p.selected.join(", ")}) \u2014 nothing left to run.`
37192
37934
  );
37193
37935
  const mergedBefore = p.selected.filter((u) => merged.has(u));
@@ -37222,16 +37964,16 @@ async function start(deps, input, instance, units, where, write) {
37222
37964
  const put = write === "replace" ? await deps.instances.replace(instance) : await deps.instances.put(instance);
37223
37965
  if (!put.ok) {
37224
37966
  if (put.reason === "unavailable")
37225
- return refused3(
37967
+ return refused4(
37226
37968
  "\u26A0\uFE0F The plan runner needs run history on the state Worker (`runHistory.worker`): the instance record could not be written, so nothing ran."
37227
37969
  );
37228
- return refused3(
37970
+ return refused4(
37229
37971
  `\u{1F6AB} A runner for \`${instance.id}\` was just recorded by another request \u2014 re-issue in a minute if it did not start.`
37230
37972
  );
37231
37973
  }
37232
37974
  const rows = await deps.instances.putUnits(units);
37233
37975
  if (!rows.ok)
37234
- return refused3(
37976
+ return refused4(
37235
37977
  "\u26A0\uFE0F The plan runner needs run history on the state Worker: the unit rows could not be written, so nothing ran."
37236
37978
  );
37237
37979
  let answer;
@@ -37248,26 +37990,26 @@ async function start(deps, input, instance, units, where, write) {
37248
37990
  }
37249
37991
  case "duplicate": {
37250
37992
  const status3 = answer.status !== void 0 ? `, status: ${answer.status}` : "";
37251
- return refused3(
37993
+ return refused4(
37252
37994
  `\u{1F6AB} A Workflow instance \`${instance.id}\` already exists on the platform${status3} but the state Worker knew nothing of it \u2014 a person decides; re-issuing will not resume it.`
37253
37995
  );
37254
37996
  }
37255
37997
  case "failed":
37256
37998
  case "unanswered":
37257
37999
  log(`[ship] ${input.msg.threadKey}: the plan runner ${instance.id} could not be started \u2014 ${answer.reason}`);
37258
- return refused3(
38000
+ return refused4(
37259
38001
  `\u26A0\uFE0F The plan runner could not be started: ${answer.reason}. Nothing ran; re-issue the request to try again.`
37260
38002
  );
37261
38003
  }
37262
38004
  }
37263
- var refused3, describe6, RUNNING, ENDED;
38005
+ var refused4, describe6, RUNNING, ENDED;
37264
38006
  var init_handOff = __esm({
37265
38007
  "../../src/core/coordinator/handOff.ts"() {
37266
38008
  "use strict";
37267
38009
  init_contract();
37268
38010
  init_preflight();
37269
38011
  init_coordinator();
37270
- refused3 = (reply) => ({ status: "aborted", reply });
38012
+ refused4 = (reply) => ({ status: "aborted", reply });
37271
38013
  describe6 = (err2) => err2 instanceof Error ? err2.message : String(err2);
37272
38014
  RUNNING = /* @__PURE__ */ new Set(["queued", "running", "paused", "waiting", "waitingForPause"]);
37273
38015
  ENDED = /* @__PURE__ */ new Set(["complete", "errored", "terminated"]);
@@ -37867,12 +38609,12 @@ function continuable(run2) {
37867
38609
  return run2.finished && run2.session !== void 0 && run2.agent !== void 0;
37868
38610
  }
37869
38611
  function stickyAgentOf(runs) {
37870
- const newest = runs[0];
38612
+ const newest = runs.find(addressed);
37871
38613
  if (newest === void 0 || !continuable(newest)) return void 0;
37872
38614
  return newest.agent;
37873
38615
  }
37874
38616
  function threadRouteOf(runs, agent) {
37875
- const newest = runs[0];
38617
+ const newest = runs.find(addressed);
37876
38618
  if (newest === void 0 || !continuable(newest) || newest.agent !== agent || newest.route === void 0)
37877
38619
  return void 0;
37878
38620
  const { preset, reason: reason2, model } = newest.route;
@@ -37896,11 +38638,12 @@ function threadPrOf(runs) {
37896
38638
  if (run2 === void 0) return void 0;
37897
38639
  return { repo: run2.repo, number: run2.pr.number, at: run2.finishedAt ?? run2.startedAt };
37898
38640
  }
37899
- var THREAD_READ_LIMIT;
38641
+ var THREAD_READ_LIMIT, addressed;
37900
38642
  var init_thread = __esm({
37901
38643
  "../../src/core/dispatch/thread.ts"() {
37902
38644
  "use strict";
37903
38645
  THREAD_READ_LIMIT = 8;
38646
+ addressed = (run2) => run2.parentInstanceId === void 0;
37904
38647
  }
37905
38648
  });
37906
38649
 
@@ -39988,391 +40731,6 @@ var init_piHarness = __esm({
39988
40731
  }
39989
40732
  });
39990
40733
 
39991
- // ../../src/execution/tracingExecutor.ts
39992
- var TracingExecutor;
39993
- var init_tracingExecutor = __esm({
39994
- "../../src/execution/tracingExecutor.ts"() {
39995
- "use strict";
39996
- TracingExecutor = class {
39997
- constructor(inner, span, backend) {
39998
- this.inner = inner;
39999
- this.span = span;
40000
- this.backend = backend;
40001
- const innerRelease = inner.release?.bind(inner);
40002
- if (innerRelease) this.release = (mode) => this.timed("exec.release", (s) => innerRelease(mode, { span: s }));
40003
- const innerMoveTo = inner.moveTo?.bind(inner);
40004
- if (innerMoveTo) this.moveTo = (sha) => this.timed("exec.move_to", (s) => innerMoveTo(sha, { span: s }));
40005
- const innerReadBytes = inner.readBytes?.bind(inner);
40006
- if (innerReadBytes)
40007
- this.readBytes = (path) => this.timed("exec.read_bytes", (s) => innerReadBytes(path, { span: s }));
40008
- }
40009
- inner;
40010
- span;
40011
- backend;
40012
- release;
40013
- moveTo;
40014
- readBytes;
40015
- /** Each op under its own `exec.*` span, handed to the inner executor as
40016
- * `opts.span` so its HTTP calls become `http.client` children (item 21). */
40017
- timed(name, fn, extra = {}) {
40018
- return this.span.span(name, (s) => fn(s), {
40019
- attrs: {
40020
- ...this.backend !== void 0 ? { backend: this.backend } : {},
40021
- ...extra.timeoutMs !== void 0 ? { timeoutMs: extra.timeoutMs } : {}
40022
- }
40023
- });
40024
- }
40025
- exec(command2, opts) {
40026
- return this.timed("exec.exec", (s) => this.inner.exec(command2, { ...opts, span: s }), {
40027
- timeoutMs: opts?.timeoutMs
40028
- });
40029
- }
40030
- readFile(path) {
40031
- return this.timed("exec.read_file", (s) => this.inner.readFile(path, { span: s }));
40032
- }
40033
- writeFile(path, content) {
40034
- return this.timed("exec.write_file", (s) => this.inner.writeFile(path, content, { span: s }));
40035
- }
40036
- };
40037
- }
40038
- });
40039
-
40040
- // ../../src/core/chatMessage.ts
40041
- function capToolResultContent(content, cap = MAX_TOOL_RESULT_CHARS) {
40042
- const capText = (text) => text.length > cap ? `${text.slice(0, cap)}
40043
- \u2026[tool result truncated: ${text.length - cap} of ${text.length} characters cut \u2014 ask for a narrower slice]` : text;
40044
- if (typeof content === "string") return capText(content);
40045
- return content.map((p) => p.type === "text" ? { ...p, text: capText(p.text) } : p);
40046
- }
40047
- var MAX_TOOL_RESULT_CHARS;
40048
- var init_chatMessage = __esm({
40049
- "../../src/core/chatMessage.ts"() {
40050
- "use strict";
40051
- MAX_TOOL_RESULT_CHARS = 12e4;
40052
- }
40053
- });
40054
-
40055
- // ../../src/core/harness/pi/toolRules.ts
40056
- import { isAbsolute as isAbsolute5, relative, resolve as resolve10 } from "node:path";
40057
- function reachFor(identity2) {
40058
- if (identity2 === "write") return CODING_REACH;
40059
- if (identity2 === "read") return READ_REACH;
40060
- return NONE_REACH;
40061
- }
40062
- function judgeToolCall(tool, input, ctx) {
40063
- const bundle = PI_TOOL_BUNDLES[tool];
40064
- if (bundle === void 0) return outside(`${tool} is not in any bundle the ${ctx.identity} identity reaches`);
40065
- if (!reachFor(ctx.identity).has(bundle)) {
40066
- return outside(
40067
- ctx.identity === "none" ? `${tool} is the \`${bundle}\` bundle: a run without a workspace (identity none) has none of pi's own tools` : `${tool} is the \`${bundle}\` bundle, outside the ${ctx.identity} identity's reach`
40068
- );
40069
- }
40070
- const args = isRecord6(input) ? input : {};
40071
- if (tool === "bash") return judgeBash(args.command, ctx);
40072
- if (bundle === "files" || bundle === "write-files") return judgePath(args.path, ctx);
40073
- return allowed;
40074
- }
40075
- function judgeBash(command2, ctx) {
40076
- if (typeof command2 !== "string") return refused4("malformed \u2014 bash without a string command");
40077
- if (CREDENTIAL_FILE.test(command2)) return refused4("credential \u2014 reads the executor's credential store");
40078
- if (ENV_DUMP.test(command2)) return refused4("credential \u2014 dumps the process environment");
40079
- if (CREDENTIAL_VAR.test(command2)) return refused4("credential \u2014 expands a credential variable");
40080
- if (MERGE_OR_APPROVE.test(command2)) {
40081
- return refused4("merge/approve \u2014 a coding run never merges or approves a pull request");
40082
- }
40083
- if (ctx.identity !== "write") {
40084
- if (ANY_PUSH.test(command2)) return refused4("read-only \u2014 a read-identity run never pushes");
40085
- if (GH_WRITE_VERB.test(command2) || GH_API_WRITE.test(command2) || CURL_GITHUB_WRITE.test(command2)) {
40086
- return refused4("read-only \u2014 a read-identity run never writes to GitHub");
40087
- }
40088
- return allowed;
40089
- }
40090
- for (const match of command2.matchAll(GIT_PUSH)) {
40091
- const verdict = judgePush(match[1], ctx);
40092
- if (verdict.verdict !== "allowed") return verdict;
40093
- }
40094
- return allowed;
40095
- }
40096
- function judgePush(tail, ctx) {
40097
- const words = tail.split(/\s+/).filter((w) => w.length > 0 && !w.startsWith("-"));
40098
- const [remote, refspec] = words;
40099
- if (remote !== void 0 && remote !== "origin") {
40100
- return refused4(`repo:use \u2014 push to remote \`${remote}\`, not the run's repository (origin)`);
40101
- }
40102
- if (refspec === void 0) return allowed;
40103
- const destination = refspec.replace(/^\+/, "").split(":").pop() ?? refspec;
40104
- if (ctx.branch !== void 0) {
40105
- const branch3 = destination === "HEAD" ? ctx.branch : destination.replace(/^refs\/heads\//, "");
40106
- if (branch3 !== ctx.branch) return refused4(`repo:use \u2014 push to \`${branch3}\`, not the run's branch ${ctx.branch}`);
40107
- return allowed;
40108
- }
40109
- const branch2 = destination.replace(/^refs\/heads\//, "");
40110
- if (branch2 !== "HEAD" && (ctx.protectedBranches ?? []).includes(branch2)) {
40111
- return refused4(
40112
- `repo:use \u2014 push to \`${branch2}\`, the branch this run's pull request targets; push your own branch`
40113
- );
40114
- }
40115
- return allowed;
40116
- }
40117
- function judgePath(path, ctx) {
40118
- if (path === void 0) return allowed;
40119
- if (typeof path !== "string") return refused4("malformed \u2014 a path tool without a string path");
40120
- if (CREDENTIAL_FILE.test(path)) return refused4("credential \u2014 reads the executor's credential store");
40121
- const rel = relative(ctx.checkout, resolve10(ctx.checkout, path));
40122
- if (rel.startsWith("..") || isAbsolute5(rel)) return refused4(`path \u2014 \`${path}\` resolves outside the checkout`);
40123
- return allowed;
40124
- }
40125
- var PI_TOOL_BUNDLES, CODING_REACH, READ_REACH, NONE_REACH, allowed, refused4, outside, isRecord6, CREDENTIAL_FILE, ENV_DUMP, CREDENTIAL_VAR, MERGE_OR_APPROVE, GIT_THEN_PUSH, GIT_PUSH, ANY_PUSH, GH_WRITE_VERB, WRITE_METHOD, GH_API_WRITE, CURL_GITHUB_WRITE;
40126
- var init_toolRules = __esm({
40127
- "../../src/core/harness/pi/toolRules.ts"() {
40128
- "use strict";
40129
- PI_TOOL_BUNDLES = {
40130
- bash: "shell",
40131
- read: "files",
40132
- grep: "files",
40133
- find: "files",
40134
- ls: "files",
40135
- write: "write-files",
40136
- edit: "write-files",
40137
- submit_pr_description: "pr",
40138
- submit_verdict: "verdict"
40139
- };
40140
- CODING_REACH = /* @__PURE__ */ new Set([
40141
- "shell",
40142
- "files",
40143
- "write-files",
40144
- "web",
40145
- "github-read",
40146
- "github-write",
40147
- "pr"
40148
- ]);
40149
- READ_REACH = /* @__PURE__ */ new Set(["shell", "files", "web", "search", "github-read", "verdict"]);
40150
- NONE_REACH = /* @__PURE__ */ new Set();
40151
- allowed = { verdict: "allowed" };
40152
- refused4 = (reason2) => ({ verdict: "refused", reason: reason2 });
40153
- outside = (reason2) => ({ verdict: "outside-profile", reason: reason2 });
40154
- isRecord6 = (v) => typeof v === "object" && v !== null;
40155
- CREDENTIAL_FILE = /(^|[\s/'"`])(\.git\/)?(github-credentials|\.git-credentials)($|[\s'"`|;&])/;
40156
- ENV_DUMP = /(^|[;&|(]\s*)((printenv|env)(\s+-[A-Za-z0-9-]+)*|export\s+-p|declare\s+-[a-zA-Z]*[px][a-zA-Z]*|set)\s*($|[|;&>)])|\/proc\/(self|\d+)\/environ|process\.env(?![.[\w])/;
40157
- CREDENTIAL_VAR = /\$\{?[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b|printenv\s+[A-Z][A-Z0-9_]*(_API_KEY|_TOKEN|_SECRET|_BEARER|_PASSWORD)\b/;
40158
- MERGE_OR_APPROVE = /\bgh\s+pr\s+(merge|review)\b|\/pulls\/\d+\/(merge|reviews)\b/;
40159
- GIT_THEN_PUSH = String.raw`\bgit(?:\s+(?:-C\s+\S+|--git-dir=\S+|--work-tree=\S+|-c\s+\S+|--no-pager))*\s+push\b`;
40160
- GIT_PUSH = new RegExp(`${GIT_THEN_PUSH}([^;&|]*)`, "g");
40161
- ANY_PUSH = new RegExp(GIT_THEN_PUSH);
40162
- GH_WRITE_VERB = /\bgh\s+(pr|issue)\s+(comment|create|edit|close|reopen|ready|lock|unlock|review|merge|delete|transfer|pin|unpin)\b/;
40163
- WRITE_METHOD = "(POST|post|PATCH|patch|PUT|put|DELETE|delete)";
40164
- GH_API_WRITE = new RegExp(
40165
- `\\bgh\\s+api\\b(?=[^;&|]*(?:\\s(?:-X|--method)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-f|-F|--field|--raw-field|--input)[\\s=]))`
40166
- );
40167
- CURL_GITHUB_WRITE = new RegExp(
40168
- `\\bcurl\\b(?=[^;&|]*api\\.github\\.com)(?=[^;&|]*(?:\\s(?:-X|--request)[\\s=]*${WRITE_METHOD}\\b|\\s(?:-d|--data(?:-\\w+)?|--json|-T|--upload-file)[\\s=]))`
40169
- );
40170
- }
40171
- });
40172
-
40173
- // ../../src/core/harness/pi/relay.ts
40174
- async function relayToolCall(harness, calls, ask, opts = {}) {
40175
- const answer = calls.join(ask.toolCallId, () => runRelayedTool(harness, ask, { signal: calls.signal }));
40176
- const window = new AbortController();
40177
- const sleep2 = opts.sleep ?? sleepUnlessAborted;
40178
- const progress = await Promise.race([
40179
- answer.then((a) => ({ done: true, answer: a })),
40180
- sleep2(opts.windowMs ?? RELAY_POLL_WINDOW_MS, window.signal).then(() => ({ done: false }))
40181
- ]);
40182
- window.abort();
40183
- return progress;
40184
- }
40185
- function relayedToolDefinitions(harness) {
40186
- return harness.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
40187
- }
40188
- function authorizeToolCall(harness, ask) {
40189
- harness.gateSaw(ask.toolCallId);
40190
- const blocked = harness.toolsBlocked();
40191
- const refuse3 = (reason2) => {
40192
- harness.emit({
40193
- type: "run_note",
40194
- kind: "tool_refused",
40195
- summary: redactAndCap(`${ask.tool} refused: ${reason2}`, 300)
40196
- });
40197
- return { allow: false, reason: reason2 };
40198
- };
40199
- if (blocked !== void 0) return refuse3(blocked);
40200
- if (harness.tools.some((t) => t.name === ask.tool)) return { allow: true };
40201
- const verdict = judgeToolCall(ask.tool, ask.input, harness.rules);
40202
- if (verdict.verdict === "allowed") return { allow: true };
40203
- return refuse3(verdict.reason);
40204
- }
40205
- async function runRelayedTool(harness, ask, opts = {}) {
40206
- const tool = harness.tools.find((t) => t.name === ask.tool);
40207
- if (!tool) return { content: [{ type: "text", text: `Unknown tool: ${ask.tool}` }], isError: true };
40208
- const span = harness.toolSpan(ask.toolCallId);
40209
- const base = harness.toolContext;
40210
- const readConversation = base.conversation;
40211
- const ctx = {
40212
- ...span ? {
40213
- ...base,
40214
- span,
40215
- executor: new TracingExecutor(base.executor, span, harness.backend),
40216
- publish: (e) => harness.emit(withSpanId(e, span.id)),
40217
- ...base.github?.api.withSpan ? { github: { ...base.github, api: base.github.api.withSpan(span) } } : {}
40218
- } : { ...base, publish: (e) => harness.emit(e) },
40219
- ...opts.signal && !base.signal ? { signal: opts.signal } : {},
40220
- ...readConversation && harness.callSeen ? { conversation: () => harness.callSeen(ask.toolCallId).then(() => readConversation()) } : {},
40221
- // After the spreads, so the call's own id can never be shadowed by one on the base context.
40222
- callId: ask.toolCallId
40223
- };
40224
- try {
40225
- const input = typeof ask.input === "object" && ask.input !== null ? ask.input : {};
40226
- const output = await tool.run(input, ctx);
40227
- return { content: piContentOf(capToolResultContent(output)), isError: false };
40228
- } catch (err2) {
40229
- return errorAnswerOf(err2);
40230
- }
40231
- }
40232
- function errorAnswerOf(err2) {
40233
- const message3 = err2 instanceof Error ? err2.message : String(err2);
40234
- return { content: piContentOf(capToolResultContent(`Error: ${message3}`)), isError: true };
40235
- }
40236
- function piContentOf(content) {
40237
- if (typeof content === "string") return [{ type: "text", text: content }];
40238
- return content.map(
40239
- (p) => p.type === "text" ? { type: "text", text: p.text } : p.type === "image" ? { type: "image", data: p.data, mimeType: p.mediaType } : { type: "text", text: `[document ${p.name ?? "document"} (${p.mediaType})]` }
40240
- );
40241
- }
40242
- function withSpanId(e, spanId) {
40243
- switch (e.type) {
40244
- case "tool_call":
40245
- case "tool_result":
40246
- case "run_note":
40247
- case "assistant":
40248
- case "skill_use":
40249
- return { ...e, spanId };
40250
- default:
40251
- return e;
40252
- }
40253
- }
40254
- var HarnessRegistry, RELAY_POLL_WINDOW_MS, RelayedCalls;
40255
- var init_relay = __esm({
40256
- "../../src/core/harness/pi/relay.ts"() {
40257
- "use strict";
40258
- init_tracingExecutor();
40259
- init_chatMessage();
40260
- init_awaitChildren();
40261
- init_runEvents();
40262
- init_toolRules();
40263
- HarnessRegistry = class {
40264
- live = /* @__PURE__ */ new Map();
40265
- /** The run drives a pi from here, its relayed calls kept beside it; the
40266
- * returned function forgets it and ends whatever call still runs. */
40267
- register(harness) {
40268
- return this.hold({ harness, calls: new RelayedCalls() });
40269
- }
40270
- /** A process relaunched under the same run — in a replacement container,
40271
- * with a rotated bearer (harness-pi item 8) — takes the run's registration
40272
- * over, and the run's relayed calls stay: a call in flight keeps running
40273
- * under the same end signal, and an ask by its id through the new
40274
- * registration joins it and reads its answer. The earlier registration's
40275
- * forget is a no-op from here; the returned one forgets the run and ends
40276
- * the calls. Refuses by name a run that is not registered. */
40277
- replace(harness) {
40278
- const previous = this.live.get(harness.runId);
40279
- if (!previous) throw new Error(`run ${harness.runId} is not registered on the relay: nothing to replace`);
40280
- return this.hold({ harness, calls: previous.calls });
40281
- }
40282
- hold(entry) {
40283
- this.live.set(entry.harness.runId, entry);
40284
- return () => {
40285
- if (this.live.get(entry.harness.runId) !== entry) return;
40286
- this.live.delete(entry.harness.runId);
40287
- entry.calls.end();
40288
- };
40289
- }
40290
- get(runId3) {
40291
- return this.live.get(runId3)?.harness;
40292
- }
40293
- /** The run's relayed calls, for as long as it is registered. */
40294
- calls(runId3) {
40295
- return this.live.get(runId3)?.calls;
40296
- }
40297
- size() {
40298
- return this.live.size;
40299
- }
40300
- };
40301
- RELAY_POLL_WINDOW_MS = 3e4;
40302
- RelayedCalls = class {
40303
- calls = /* @__PURE__ */ new Map();
40304
- /** The ids whose call is still running: joined by `join`, forgotten as each answers. */
40305
- running = /* @__PURE__ */ new Set();
40306
- ending = new AbortController();
40307
- get size() {
40308
- return this.calls.size;
40309
- }
40310
- /** Aborted when the run ends: the signal every call here runs under. */
40311
- get signal() {
40312
- return this.ending.signal;
40313
- }
40314
- /** The call's answer as a promise: started by `start` on the first ask, the same promise after. */
40315
- join(callId, start3) {
40316
- let answer = this.calls.get(callId);
40317
- if (!answer) {
40318
- answer = start3();
40319
- this.calls.set(callId, answer);
40320
- this.running.add(callId);
40321
- const done = () => void this.running.delete(callId);
40322
- answer.then(done, done);
40323
- }
40324
- return answer;
40325
- }
40326
- /** The ids of the calls still running, in the order they were started. */
40327
- inFlight() {
40328
- return [...this.running];
40329
- }
40330
- /** Every call still running, awaited together up to one window — the relay
40331
- * window a request waits, by default — for a relaunch that rebuilds the
40332
- * process's session (harness-pi item 8): a call that answers inside it is
40333
- * `done` with its answer, the result the rebuilt session carries; one still
40334
- * running after it is not, keeps running here for an ask by the same id,
40335
- * and the rebuilt session carries the still-running note in its place
40336
- * (`stillRunningNote`). A call whose `start` rejected is `done` with an
40337
- * error answer, as `runRelayedTool` answers its own throws, so one
40338
- * straggler's exception never loses the others' answers. Nothing is ended
40339
- * or re-run. Answers at once with nothing in flight. */
40340
- async awaitInFlight(opts = {}) {
40341
- const ids = this.inFlight();
40342
- if (ids.length === 0) return [];
40343
- const window = new AbortController();
40344
- const sleep2 = opts.sleep ?? sleepUnlessAborted;
40345
- const closed = sleep2(opts.windowMs ?? RELAY_POLL_WINDOW_MS, window.signal).then(() => ({
40346
- done: false
40347
- }));
40348
- const progress = await Promise.all(
40349
- ids.map(async (callId) => {
40350
- const answered = this.calls.get(callId).then(
40351
- (answer) => ({ done: true, answer }),
40352
- (err2) => ({ done: true, answer: errorAnswerOf(err2) })
40353
- );
40354
- return { callId, ...await Promise.race([answered, closed]) };
40355
- })
40356
- );
40357
- window.abort();
40358
- return progress;
40359
- }
40360
- /** The call's answer is known before it is asked (harness-pi item 8): the
40361
- * record's settlement for a call in flight when the previous generation
40362
- * died, whose extension asks again with the same id. Every ask reads it and
40363
- * the tool never runs; a call already joined keeps the answer it has. */
40364
- settle(callId, answer) {
40365
- if (!this.calls.has(callId)) this.calls.set(callId, Promise.resolve(answer));
40366
- }
40367
- end() {
40368
- this.ending.abort();
40369
- this.calls.clear();
40370
- this.running.clear();
40371
- }
40372
- };
40373
- }
40374
- });
40375
-
40376
40734
  // ../../src/core/runLedger/takeover.ts
40377
40735
  var LedgerTakeover;
40378
40736
  var init_takeover = __esm({
@@ -42285,6 +42643,45 @@ var init_dedupe = __esm({
42285
42643
  });
42286
42644
 
42287
42645
  // ../../src/channels/slack/lookups.ts
42646
+ function resolvePersonByEmail(client, email, opts = {}) {
42647
+ const key = email.trim().toLowerCase();
42648
+ if (!key.includes("@") || typeof client.users.lookupByEmail !== "function") return Promise.resolve(void 0);
42649
+ const now = opts.now ?? Date.now;
42650
+ const cached = personByEmailCache.get(key);
42651
+ if (cached && cached.expiresAt > now()) return Promise.resolve(cached.person);
42652
+ const inFlight = personByEmailInFlight.get(key);
42653
+ if (inFlight) return inFlight;
42654
+ const lookup2 = (async () => {
42655
+ let timer;
42656
+ try {
42657
+ const call2 = client.users.lookupByEmail({ email: key }).then(
42658
+ (r) => r.user,
42659
+ () => void 0
42660
+ );
42661
+ const timedOut = /* @__PURE__ */ Symbol("person lookup timed out");
42662
+ const answer = await Promise.race([
42663
+ call2,
42664
+ new Promise((resolve12) => {
42665
+ timer = setTimeout(() => resolve12(timedOut), opts.timeoutMs ?? PERSON_LINK_TIMEOUT_MS);
42666
+ })
42667
+ ]);
42668
+ if (answer === timedOut) return void 0;
42669
+ const u = answer;
42670
+ const person = u?.id && !u.deleted && !u.is_bot && !u.is_app_user ? { id: `slack:${u.id}`, ...displayNameOf2(u) ? { name: displayNameOf2(u) } : {} } : void 0;
42671
+ personByEmailCache.set(key, { person, expiresAt: now() + PERSON_LINK_TTL_MS });
42672
+ if (personByEmailCache.size > NAME_CACHE_MAX) {
42673
+ const oldest = personByEmailCache.keys().next().value;
42674
+ if (oldest !== void 0) personByEmailCache.delete(oldest);
42675
+ }
42676
+ return person;
42677
+ } finally {
42678
+ if (timer) clearTimeout(timer);
42679
+ personByEmailInFlight.delete(key);
42680
+ }
42681
+ })();
42682
+ personByEmailInFlight.set(key, lookup2);
42683
+ return lookup2;
42684
+ }
42288
42685
  function cachePut(cache2, key, value) {
42289
42686
  cache2.set(key, value);
42290
42687
  if (cache2.size > NAME_CACHE_MAX) {
@@ -42338,10 +42735,15 @@ async function resolveTeamUrl(client) {
42338
42735
  }
42339
42736
  return teamUrl;
42340
42737
  }
42341
- var NAME_CACHE_MAX, channelNameCache, userNameCache, teamUrl;
42738
+ var PERSON_LINK_TTL_MS, PERSON_LINK_TIMEOUT_MS, personByEmailCache, personByEmailInFlight, displayNameOf2, NAME_CACHE_MAX, channelNameCache, userNameCache, teamUrl;
42342
42739
  var init_lookups = __esm({
42343
42740
  "../../src/channels/slack/lookups.ts"() {
42344
42741
  "use strict";
42742
+ PERSON_LINK_TTL_MS = 10 * 6e4;
42743
+ PERSON_LINK_TIMEOUT_MS = 1500;
42744
+ personByEmailCache = /* @__PURE__ */ new Map();
42745
+ personByEmailInFlight = /* @__PURE__ */ new Map();
42746
+ displayNameOf2 = (u) => u?.profile?.display_name || u?.profile?.real_name || u?.real_name || u?.name || void 0;
42345
42747
  NAME_CACHE_MAX = 1e3;
42346
42748
  channelNameCache = /* @__PURE__ */ new Map();
42347
42749
  userNameCache = /* @__PURE__ */ new Map();
@@ -44000,8 +44402,8 @@ function parseIndexCursor(params) {
44000
44402
  if (!params.has("before") || !Number.isFinite(before) || before <= 0 || beforeId === "") return void 0;
44001
44403
  return { before, beforeId };
44002
44404
  }
44003
- function olderRunsHref(cursor) {
44004
- return `/runs?all=1&before=${cursor.finishedAt}&beforeId=${encodeURIComponent(cursor.id)}`;
44405
+ function olderRunsHref(cursor, opts = {}) {
44406
+ return `/runs?all=1${opts.mine ? "&mine=1" : ""}&before=${cursor.finishedAt}&beforeId=${encodeURIComponent(cursor.id)}`;
44005
44407
  }
44006
44408
  function readDecision(actor, view) {
44007
44409
  const admitted = authorize(actor, "runs:read", RUNS_GET);
@@ -44099,7 +44501,7 @@ function createLiveViewHandler(deps) {
44099
44501
  })
44100
44502
  );
44101
44503
  };
44102
- const mergedRows = async (live, visibleTo, cursor) => {
44504
+ const mergedRows = async (live, visibleTo, cursor, view = {}) => {
44103
44505
  const tokens = new Map(live.map((s) => [s.id, s.token]));
44104
44506
  const { runs, nextBefore, storeUnavailable } = await service.listRuns({
44105
44507
  status: "all",
@@ -44111,7 +44513,7 @@ function createLiveViewHandler(deps) {
44111
44513
  return {
44112
44514
  rows,
44113
44515
  ...storeUnavailable ? { storeUnavailable: true } : {},
44114
- ...nextBefore ? { olderHref: olderRunsHref(nextBefore) } : {},
44516
+ ...nextBefore ? { olderHref: olderRunsHref(nextBefore, view) } : {},
44115
44517
  ...cursor ? { olderThan: cursor.before } : {}
44116
44518
  };
44117
44519
  };
@@ -44128,7 +44530,9 @@ function createLiveViewHandler(deps) {
44128
44530
  }
44129
44531
  if (route3.kind === "index") {
44130
44532
  const all = url.searchParams.get("all") === "1";
44131
- const visibleTo = readableRuns(ctx.actor);
44533
+ const mine = url.searchParams.get("mine") === "1";
44534
+ const readable2 = readableRuns(ctx.actor);
44535
+ const visibleTo = mine ? allOf([readable2, ownedBy(ctx.actor)]) : readable2;
44132
44536
  if (url.searchParams.get("stream") === "1") {
44133
44537
  serveIndexEvents(
44134
44538
  visibleIndexFeed((onEvent) => index.subscribeIndex(onEvent), visibleTo),
@@ -44144,6 +44548,8 @@ function createLiveViewHandler(deps) {
44144
44548
  const seed = {
44145
44549
  page: "runs",
44146
44550
  all,
44551
+ mine,
44552
+ ...ctx.actor.asUser ? { asUser: ctx.actor.asUser } : {},
44147
44553
  retentionDays: deps.retention ? deps.retention.retentionDays : null,
44148
44554
  now: now(),
44149
44555
  rows: [...page2.rows],
@@ -44161,7 +44567,7 @@ function createLiveViewHandler(deps) {
44161
44567
  );
44162
44568
  return true;
44163
44569
  }
44164
- run2(res, async () => render3(await mergedRows(live, visibleTo, parseIndexCursor(url.searchParams))));
44570
+ run2(res, async () => render3(await mergedRows(live, visibleTo, parseIndexCursor(url.searchParams), { mine })));
44165
44571
  return true;
44166
44572
  }
44167
44573
  if (route3.kind === "scheduled") {
@@ -44976,9 +45382,7 @@ function createSettingsViewHandler(deps, shell) {
44976
45382
  plain4(res, 405, "method not allowed", { allow: "GET" });
44977
45383
  return true;
44978
45384
  }
44979
- const caller = deps.callerFor(ctx.identity);
44980
45385
  const tab = route3.tab === "home" ? homeTab(deps.capabilities) : route3.tab;
44981
- const base = { page: "settings", tab, viewer: caller.id, vocabulary: deps.vocabulary };
44982
45386
  const failed = (err2) => {
44983
45387
  const reason2 = (err2 instanceof Error ? err2.message : String(err2)).slice(0, UPSTREAM_REASON_MAX4);
44984
45388
  if (res.headersSent) {
@@ -44993,20 +45397,27 @@ function createSettingsViewHandler(deps, shell) {
44993
45397
  res.end(shell("Settings", seed));
44994
45398
  };
44995
45399
  const channel = "channel" in route3 ? route3.channel : void 0;
44996
- if (tab === "installation") {
44997
- if (!authorize(caller.actor, "config:read", { type: "command", id: "config.show" }).allow) {
44998
- plain4(res, 403, "forbidden");
44999
- return true;
45400
+ deps.callerFor(ctx.identity).then(async (caller) => {
45401
+ const base = {
45402
+ page: "settings",
45403
+ tab,
45404
+ viewer: caller.id,
45405
+ ...caller.actor.asUser ? { asUser: caller.actor.asUser } : {},
45406
+ vocabulary: deps.vocabulary
45407
+ };
45408
+ if (tab === "installation") {
45409
+ if (!authorize(caller.actor, "config:read", { type: "command", id: "config.show" }).allow) {
45410
+ plain4(res, 403, "forbidden");
45411
+ return;
45412
+ }
45413
+ render3({ ...base, installation: deps.installation() });
45414
+ return;
45000
45415
  }
45001
- render3({ ...base, installation: deps.installation() });
45002
- return true;
45003
- }
45004
- const build = tab === "mcps" ? mcpsSeed(caller, channel) : channelsSeed(caller, channel);
45005
- build.then(
45006
- (part) => render3(
45416
+ const part = tab === "mcps" ? await mcpsSeed(caller, channel) : await channelsSeed(caller, channel);
45417
+ render3(
45007
45418
  tab === "mcps" ? { ...base, mcps: part } : { ...base, channels: part }
45008
- )
45009
- ).catch(failed);
45419
+ );
45420
+ }).catch(failed);
45010
45421
  return true;
45011
45422
  };
45012
45423
  }
@@ -47459,7 +47870,18 @@ async function merge(body, deps, subject2) {
47459
47870
  return json(502, { ok: false, error: "github_unavailable", message: `${where} could not be read`, at });
47460
47871
  if (isReleasePullRequest(facts2))
47461
47872
  return refused6(`${where} is the release pull request \u2014 always a person's merge, never the runner's`);
47462
- if (facts2.state !== "open") return refused6(`${where} is ${facts2.state}`);
47873
+ if (facts2.state !== "open") {
47874
+ if (facts2.mergedAt !== void 0 && facts2.mergeCommitSha !== void 0)
47875
+ return json(200, {
47876
+ ok: true,
47877
+ outcome: "merged",
47878
+ by: "other",
47879
+ sha: facts2.mergeCommitSha,
47880
+ mergedAt: facts2.mergedAt,
47881
+ at
47882
+ });
47883
+ return refused6(`${where} is ${facts2.state}`);
47884
+ }
47463
47885
  if (facts2.headRef !== row2.branch)
47464
47886
  return refused6(`${where} heads \`${facts2.headRef ?? "?"}\`, not the unit's branch \`${row2.branch}\``);
47465
47887
  if (facts2.headSha === void 0 || !sameCommit(facts2.headSha, headSha))
@@ -47857,8 +48279,19 @@ function accessActor(identity2, grantsFor) {
47857
48279
  function serviceTokenAllowed(pathname, identity2) {
47858
48280
  return !isServiceToken(identity2) || isCommandPath(pathname);
47859
48281
  }
47860
- function callerFor(identity2, opts) {
47861
- return { kind: "access", id: callerIdFor(identity2), actor: accessActor(identity2, opts.grantsFor) };
48282
+ async function resolveAccessActor(identity2, opts) {
48283
+ const actor = accessActor(identity2, opts.grantsFor);
48284
+ if (isServiceToken(identity2) || !identity2.email || !opts.personByEmail) return actor;
48285
+ const person = await opts.personByEmail(identity2.email).catch(() => void 0);
48286
+ if (!person || !person.id.startsWith("slack:")) return actor;
48287
+ return {
48288
+ ...actor,
48289
+ self: [actor.id, person.id],
48290
+ asUser: { id: person.id, ...person.name ? { name: person.name } : {} }
48291
+ };
48292
+ }
48293
+ async function callerFor(identity2, opts) {
48294
+ return { kind: "access", id: callerIdFor(identity2), actor: await resolveAccessActor(identity2, opts) };
47862
48295
  }
47863
48296
  function hostOf2(url) {
47864
48297
  if (!url) return void 0;
@@ -47925,7 +48358,7 @@ function createCommandHttpHandler(commands, opts) {
47925
48358
  refuse3(res, 403, "forbidden_origin", "cross-origin writes are refused");
47926
48359
  return;
47927
48360
  }
47928
- const caller = callerFor(identity2, opts);
48361
+ const caller = await callerFor(identity2, opts);
47929
48362
  if (CommandRegistry.refuses(cmd, caller)) {
47930
48363
  refuse3(res, ERROR_STATUS.unauthorized, "unauthorized", `${caller.id} is not allowed to run ${cmd.id}`);
47931
48364
  return;
@@ -48013,6 +48446,8 @@ async function runBot() {
48013
48446
  const completions = new PiAiProviders(config.config.providers);
48014
48447
  const skills = new BundledSkillStore(DEFAULT_SKILLS_DIR);
48015
48448
  let slackEmailLookup;
48449
+ let slackPersonByEmail;
48450
+ const personByEmail = (email) => slackPersonByEmail ? slackPersonByEmail(email) : Promise.resolve(void 0);
48016
48451
  const mcpWiring = buildMcp(config, processSecrets, {
48017
48452
  publicBaseUrl: process.env.PUBLIC_BASE_URL,
48018
48453
  resolveEmail: (userId) => slackEmailLookup ? slackEmailLookup(userId) : Promise.resolve(void 0),
@@ -48211,6 +48646,7 @@ async function runBot() {
48211
48646
  const conversationReader = new SlackConversationReader(app.client);
48212
48647
  deps.conversationReaders = [conversationReader];
48213
48648
  slackEmailLookup = (userId) => userId.startsWith("slack:") ? resolveUserEmail(app.client, userId.slice("slack:".length)) : Promise.resolve(void 0);
48649
+ slackPersonByEmail = (email) => resolvePersonByEmail(app.client, email);
48214
48650
  const pendingHistoryWrites = () => runHistoryWriter.pending();
48215
48651
  const inFlight = () => activeRunCount() + pendingReflectionCount() + pendingHistoryWrites();
48216
48652
  let draining = false;
@@ -48287,7 +48723,7 @@ async function runBot() {
48287
48723
  const settingsView = createSettingsViewHandler(
48288
48724
  {
48289
48725
  commands,
48290
- callerFor: (identity2) => callerFor(identity2, { grantsFor: (id) => config.grantsFor(id) }),
48726
+ callerFor: (identity2) => callerFor(identity2, { grantsFor: (id) => config.grantsFor(id), personByEmail }),
48291
48727
  installation: () => installationSettings(config.config, capabilities),
48292
48728
  vocabulary: {
48293
48729
  agents: Object.keys(AGENTS),
@@ -48338,6 +48774,7 @@ async function runBot() {
48338
48774
  const accessState = `dashboard auth: ${dashboardAuth.describe()}`;
48339
48775
  const commandHttp = createCommandHttpHandler(commands, {
48340
48776
  grantsFor: (id) => config.grantsFor(id),
48777
+ personByEmail,
48341
48778
  publicBaseUrl
48342
48779
  });
48343
48780
  const commandHttpState = `GET|POST /api/<group>.<verb> (${commands.list().length} commands)`;
@@ -48401,7 +48838,7 @@ async function runBot() {
48401
48838
  }
48402
48839
  if (webAssets.serve(req, res)) return;
48403
48840
  if (isCommandPath(path) || isConnectPath(path) || path === "/runs" || path.startsWith("/runs/") || path === "/residents" || path.startsWith("/residents/") || path === "/costs" || path === "/costs.json" || path.startsWith("/costs/") || path === "/delivery" || path === "/delivery.json" || path.startsWith("/delivery/") || path === "/settings" || path.startsWith("/settings/")) {
48404
- dashboardAuth.verify(req).then((gate) => {
48841
+ dashboardAuth.verify(req).then(async (gate) => {
48405
48842
  if (!gate.ok) {
48406
48843
  res.writeHead(gate.status, { "content-type": "text/plain; charset=utf-8" });
48407
48844
  res.end(gate.body);
@@ -48417,7 +48854,10 @@ async function runBot() {
48417
48854
  return;
48418
48855
  }
48419
48856
  if (isCommandPath(path)) return commandHttp(req, res, gate.identity);
48420
- const actor = accessActor(gate.identity, (id) => config.grantsFor(id));
48857
+ const actor = await resolveAccessActor(gate.identity, {
48858
+ grantsFor: (id) => config.grantsFor(id),
48859
+ personByEmail
48860
+ });
48421
48861
  if (liveView2(req, res, { actor })) return;
48422
48862
  if (mcpConnectView(req, res, gate.identity)) return;
48423
48863
  if (residentsView(req, res, { actor })) return;