@piagent/platform 1.7.1 → 1.9.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 (227) hide show
  1. package/AGENTS.md +7 -0
  2. package/CHANGELOG.md +113 -1
  3. package/README.md +45 -70
  4. package/README.vi.md +34 -61
  5. package/architecture/layers.json +16 -3
  6. package/catalog/capabilities.json +14 -7
  7. package/catalog/claude-model-additions.json +42 -0
  8. package/docs/auto-delegation-policy.md +2 -0
  9. package/docs/command-reference-vietnamese.md +69 -91
  10. package/docs/context-window-policy.md +5 -5
  11. package/docs/distribution-standard.md +16 -19
  12. package/docs/en/architecture.md +9 -1
  13. package/docs/herdr-workflow.md +2 -3
  14. package/docs/languages.json +4 -4
  15. package/docs/managed-local-acceptance.md +403 -0
  16. package/docs/mcp-and-tools.md +5 -7
  17. package/docs/memory-policy.md +3 -3
  18. package/docs/model-options.md +22 -17
  19. package/docs/oauth-providers.md +1 -1
  20. package/docs/operator-manual-vietnamese.md +85 -130
  21. package/docs/package-architecture-notes.md +4 -4
  22. package/docs/piagent-core-improvement-plan.md +3 -5
  23. package/docs/piagent-core-plan-checkpoint-2026-09-06.json +2 -13
  24. package/docs/piagent-runtime-benchmark-execution-plan.md +10 -8
  25. package/docs/project-adapters.md +3 -3
  26. package/docs/project-onboarding.md +15 -16
  27. package/docs/publishing-for-teams.md +3 -1
  28. package/docs/quality-benchmark.md +2 -0
  29. package/docs/quickstart-vietnamese.md +24 -25
  30. package/docs/release-install-policy.md +16 -11
  31. package/docs/research/harness-next-evidence-review.md +7 -7
  32. package/docs/runtime-harness-standard.md +2 -0
  33. package/docs/runtime-policy-design.md +2 -0
  34. package/docs/runtime-quality-baseline.md +2 -0
  35. package/docs/subagent-orchestration-capabilities.md +1 -1
  36. package/docs/subagents-and-multiagent.md +10 -8
  37. package/docs/task-implementation-contract.md +2 -0
  38. package/docs/task-lifecycle-vietnamese.md +2 -0
  39. package/docs/team-onboarding.md +33 -64
  40. package/docs/usage-observability.md +2 -2
  41. package/docs/vi/architecture.md +9 -1
  42. package/docs/workflow-recipes.md +6 -10
  43. package/docs-site/architecture.html +4 -3
  44. package/docs-site/assets/docs.css +80 -0
  45. package/docs-site/benchmark.html +7 -1
  46. package/docs-site/commands.html +37 -23
  47. package/docs-site/content/architecture.html +2 -2
  48. package/docs-site/content/benchmark.html +5 -0
  49. package/docs-site/content/commands.html +35 -22
  50. package/docs-site/content/ecosystem.html +144 -0
  51. package/docs-site/content/en/architecture.html +1 -1
  52. package/docs-site/content/en/benchmark.html +5 -0
  53. package/docs-site/content/en/commands.html +30 -13
  54. package/docs-site/content/en/ecosystem.html +144 -0
  55. package/docs-site/content/en/index.html +24 -9
  56. package/docs-site/content/en/quickstart.html +10 -12
  57. package/docs-site/content/en/runtime.html +1 -1
  58. package/docs-site/content/en/security.html +1 -1
  59. package/docs-site/content/en/team.html +2 -2
  60. package/docs-site/content/en/whats-new.html +89 -1
  61. package/docs-site/content/en/workflows.html +29 -31
  62. package/docs-site/content/index.html +24 -9
  63. package/docs-site/content/quickstart.html +10 -12
  64. package/docs-site/content/runtime.html +2 -2
  65. package/docs-site/content/security.html +2 -2
  66. package/docs-site/content/team.html +2 -2
  67. package/docs-site/content/whats-new.html +89 -1
  68. package/docs-site/content/workflows.html +36 -38
  69. package/docs-site/context-engine.html +2 -1
  70. package/docs-site/ecosystem.html +283 -0
  71. package/docs-site/en/architecture.html +3 -2
  72. package/docs-site/en/benchmark.html +7 -1
  73. package/docs-site/en/commands.html +32 -14
  74. package/docs-site/en/context-engine.html +2 -1
  75. package/docs-site/en/ecosystem.html +283 -0
  76. package/docs-site/en/index.html +30 -14
  77. package/docs-site/en/mcp-approval.html +2 -1
  78. package/docs-site/en/mcp-auth.html +2 -1
  79. package/docs-site/en/mcp-servers.html +2 -1
  80. package/docs-site/en/mcp.html +2 -1
  81. package/docs-site/en/permissions.html +2 -1
  82. package/docs-site/en/profiles.html +2 -1
  83. package/docs-site/en/quickstart.html +17 -18
  84. package/docs-site/en/runtime.html +3 -2
  85. package/docs-site/en/scope.html +2 -1
  86. package/docs-site/en/security.html +6 -5
  87. package/docs-site/en/team.html +4 -3
  88. package/docs-site/en/whats-new.html +97 -8
  89. package/docs-site/en/workflows.html +34 -35
  90. package/docs-site/index.html +30 -14
  91. package/docs-site/mcp-approval.html +2 -1
  92. package/docs-site/mcp-auth.html +2 -1
  93. package/docs-site/mcp-servers.html +2 -1
  94. package/docs-site/mcp.html +2 -1
  95. package/docs-site/permissions.html +2 -1
  96. package/docs-site/profiles.html +2 -1
  97. package/docs-site/quickstart.html +17 -18
  98. package/docs-site/runtime.html +4 -3
  99. package/docs-site/scope.html +2 -1
  100. package/docs-site/security.html +7 -6
  101. package/docs-site/team.html +4 -3
  102. package/docs-site/whats-new.html +97 -8
  103. package/docs-site/workflows.html +41 -42
  104. package/evals/fs5-causal-arm.v1.json +2 -2
  105. package/evals/private-holdout-v1/CURRENT_BOUNDARY.md +17 -0
  106. package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
  107. package/package.json +10 -9
  108. package/packages/piagent-core/README.md +15 -23
  109. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +2 -0
  110. package/packages/piagent-core/extensions/experiment-loop-policy.ts +75 -0
  111. package/packages/piagent-core/extensions/local-state-path.js +15 -0
  112. package/packages/piagent-core/extensions/piagent-guard.ts +108 -960
  113. package/packages/piagent-core/extensions/policy-core.js +2 -2
  114. package/packages/piagent-core/extensions/source-checkout-paths.ts +54 -0
  115. package/packages/piagent-core/managed/broker-client.mjs +49 -0
  116. package/packages/piagent-core/managed/capabilities.mjs +28 -0
  117. package/packages/piagent-core/managed/compaction.mjs +63 -0
  118. package/packages/piagent-core/managed/doctor.mjs +107 -0
  119. package/packages/piagent-core/managed/native-catalog.mjs +42 -0
  120. package/packages/piagent-core/managed/patch-snapshot.mjs +48 -0
  121. package/packages/piagent-core/managed/repository-operation.mjs +84 -0
  122. package/packages/piagent-core/managed/request-stream.mjs +129 -0
  123. package/packages/piagent-core/managed/resource-loader.mjs +42 -0
  124. package/packages/piagent-core/managed/review-snapshot-worker.mjs +27 -0
  125. package/packages/piagent-core/managed/sandbox-diagnostic.mjs +12 -0
  126. package/packages/piagent-core/managed/search-fallback.mjs +93 -0
  127. package/packages/piagent-core/managed/session.mjs +472 -0
  128. package/packages/piagent-core/managed/store.d.mts +3 -0
  129. package/packages/piagent-core/managed/store.mjs +64 -0
  130. package/packages/piagent-core/managed/tool-boundary.mjs +243 -0
  131. package/packages/piagent-core/managed/tool-worker.mjs +36 -0
  132. package/packages/piagent-core/managed/toolchain.mjs +120 -0
  133. package/packages/piagent-core/managed/web-fetch.mjs +105 -0
  134. package/packages/piagent-core/managed/web-search.mjs +96 -0
  135. package/packages/piagent-core/managed/workflow.mjs +299 -0
  136. package/packages/piagent-core/package.json +3 -6
  137. package/packages/piagent-core/policies/base-policy.json +80 -26
  138. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +3 -3
  139. package/packages/piagent-core/runtime/hooks/input-hook.ts +35 -196
  140. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +2 -0
  141. package/packages/piagent-core/runtime/inspection/approval-broker.ts +9 -1
  142. package/packages/piagent-core/runtime/inspection/same-process-bridge-proof.ts +1 -1
  143. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +1 -1
  144. package/packages/piagent-core/runtime/managed-failure.d.mts +10 -0
  145. package/packages/piagent-core/runtime/managed-failure.mjs +164 -0
  146. package/packages/piagent-core/runtime/model/model-route-policy.ts +33 -13
  147. package/packages/piagent-core/runtime/model/model-route-types.ts +10 -6
  148. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +8 -29
  149. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +1 -1
  150. package/packages/piagent-core/runtime/policy/block-remedy.ts +5 -3
  151. package/packages/piagent-core/runtime/registration/context-commands.ts +1 -1
  152. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +7 -7
  153. package/packages/piagent-core/runtime/registration/policy-tools.ts +2 -2
  154. package/packages/piagent-core/runtime/registration/session-commands.ts +11 -26
  155. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +1 -1
  156. package/packages/piagent-core/runtime/session/system-prompt.ts +6 -7
  157. package/packages/piagent-core/runtime/workflows/webui-runtime-command.ts +6 -6
  158. package/packages/piagent-core/runtime/workflows/webui-workflow.ts +6 -11
  159. package/packages/piagent-core/security/sensitive-text.js +9 -0
  160. package/packages/piagent-core/skills/piagent-ops/SKILL.md +8 -10
  161. package/packages/piagent-core/skills/test-audit/SKILL.md +101 -0
  162. package/packages/piagent-core/subagents/piagent-planner.md +1 -1
  163. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-DGk34zJT.js → MarkdownMessage-Dd9wPk-v.js} +1 -1
  164. package/packages/piagent-webui/dist/client/assets/index-7I-50Q2U.js +228 -0
  165. package/packages/piagent-webui/dist/client/assets/{index-DVahUVWy.css → index-DgSmcnP8.css} +1 -1
  166. package/packages/piagent-webui/dist/client/index.html +2 -2
  167. package/packages/piagent-webui/extension/piagent-webui.ts +11 -11
  168. package/packages/piagent-webui/extension/session-stream-adapter.ts +3 -1
  169. package/packages/piagent-webui/extension/terminal-session-adapter.ts +6 -1
  170. package/packages/piagent-webui/gateway/company-relay.ts +308 -0
  171. package/packages/piagent-webui/gateway/control-socket.ts +39 -12
  172. package/packages/piagent-webui/gateway/gateway-service.ts +109 -28
  173. package/packages/piagent-webui/gateway/gateway-session-stream.ts +58 -2
  174. package/packages/piagent-webui/gateway/managed-gateway.mjs +41 -0
  175. package/packages/piagent-webui/gateway/managed-launch.ts +145 -0
  176. package/packages/piagent-webui/gateway/managed-projection.ts +20 -0
  177. package/packages/piagent-webui/gateway/pi-host.ts +22 -4
  178. package/packages/piagent-webui/gateway/runtime-session-controls.ts +5 -2
  179. package/packages/piagent-webui/gateway/session-approval-authority.ts +28 -0
  180. package/packages/piagent-webui/gateway/session-catalog.ts +11 -3
  181. package/packages/piagent-webui/gateway/session-command-controller.ts +8 -2
  182. package/packages/piagent-webui/gateway/session-file-facts.ts +25 -0
  183. package/packages/piagent-webui/gateway/session-inspection-registry.ts +42 -12
  184. package/packages/piagent-webui/gateway/session-list-cache.ts +68 -0
  185. package/packages/piagent-webui/gateway/session-runtime-ownership.ts +8 -1
  186. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +20 -27
  187. package/packages/piagent-webui/ownership/managed-terminal-lease.ts +27 -0
  188. package/packages/piagent-webui/ownership/profile-state.ts +2 -0
  189. package/packages/piagent-webui/ownership/session-lease-store.ts +22 -1
  190. package/packages/piagent-webui/server/approval-projection.ts +9 -0
  191. package/packages/piagent-webui/server/core-inspection-provider.ts +4 -9
  192. package/packages/piagent-webui/server/loopback-server.ts +37 -1
  193. package/packages/piagent-webui/server/message-stop-reason.ts +11 -0
  194. package/packages/piagent-webui/server/tool-call-summary.ts +185 -0
  195. package/packages/piagent-webui/server/transcript-projection.ts +86 -3
  196. package/packs/engineering-base/pack.json +4 -0
  197. package/schemas/piagent-webui/runtime-command-v1.schema.json +1 -2
  198. package/schemas/piagent-webui/session-catalog-v1.schema.json +8 -0
  199. package/schemas/piagent-webui/transcript-v1.schema.json +94 -1
  200. package/scripts/build-docs-site.mjs +23 -12
  201. package/scripts/check-public-wording.mjs +5 -6
  202. package/scripts/check-runtime-advisories.mjs +23 -6
  203. package/scripts/claude-model-additions.mjs +33 -0
  204. package/scripts/configure-model-scope.sh +36 -8
  205. package/scripts/configure-subagents.sh +1 -1
  206. package/scripts/explain-command.mjs +5 -6
  207. package/scripts/install-global.sh +2 -2
  208. package/scripts/model-route-evaluation-core.ts +1 -0
  209. package/scripts/model-scope-prune.mjs +75 -0
  210. package/scripts/phase-tools-evaluation.mjs +9 -4
  211. package/scripts/pi-model-route.mjs +1 -1
  212. package/scripts/piagent-cli.mjs +51 -2
  213. package/scripts/piagent-dashboard.mjs +27 -4
  214. package/scripts/piagent-installations.mjs +26 -0
  215. package/scripts/piagent-studio.mjs +155 -0
  216. package/scripts/runtime-advisory-exceptions.json +29 -0
  217. package/scripts/setup.sh +2 -2
  218. package/scripts/team-doctor.sh +20 -1
  219. package/scripts/typescript-loader.mjs +6 -0
  220. package/scripts/verify-local.sh +5 -5
  221. package/scripts/verify-release-identity.mjs +1 -1
  222. package/templates/global/settings.json +8 -2
  223. package/templates/project/.pi/project-context.md +3 -3
  224. package/templates/project/AGENTS.md +4 -5
  225. package/docs/piagent-core-reasoning-research-2026-09-06.md +0 -225
  226. package/docs/piagent-core-research-sources-2026-09-06.json +0 -416
  227. package/packages/piagent-webui/dist/client/assets/index-DiKRmCEP.js +0 -176
@@ -1,3 +1,4 @@
1
+ import { managedProjection } from './managed-projection.ts';
1
2
  import { randomBytes } from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
@@ -16,11 +17,14 @@ import {
16
17
  writeGatewayDescriptor,
17
18
  type GatewayDescriptor
18
19
  } from "./profile-state.ts";
19
- import { buildSessionCatalog } from "./session-catalog.ts";
20
+ import { buildSessionCatalog, projectRefForCwd } from "./session-catalog.ts";
21
+ import { sessionRefForPath } from "../ownership/session-refs.ts";
20
22
  import { SessionMetadataStore } from "./session-metadata-store.ts";
21
23
  import { GatewayProtocolService } from "./gateway-protocol-service.ts";
22
24
  import { SessionLeaseStore } from "./session-lease-store.ts";
23
25
  import { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
26
+ import { cachedSessionLister } from "./session-list-cache.ts";
27
+ import { cachedSessionFacts } from "./session-file-facts.ts";
24
28
  import { buildSessionLiveState } from "./session-live-state.ts";
25
29
  import { SessionCommandStore } from "./session-command-store.ts";
26
30
  import { SessionCommandController } from "./session-command-controller.ts";
@@ -30,9 +34,10 @@ import { SessionInspectionRegistry } from "./session-inspection-registry.ts";
30
34
  import { SessionAttachmentRegistry } from "./session-attachment-registry.ts";
31
35
  import { ProjectRegistry } from "./project-registry.ts";
32
36
  import { pickNativeProjectFolders } from "./native-project-picker.ts";
37
+ import { CompanyRelay, type CompanyConnector } from "./company-relay.ts";
33
38
  import { ProviderAuthBroker } from "./provider-auth-broker.ts";
34
39
  import { McpAuthBroker } from "./mcp-auth-broker.ts";
35
- import type { ScopedBrokerRouter } from "./session-runtime-factory.ts";
40
+ import type { ScopedBrokerRouter, RuntimeFactory } from "./session-runtime-factory.ts";
36
41
 
37
42
  function unavailable(reasonCode: string) {
38
43
  return { status: "unavailable" as const, version: null, reasonCode };
@@ -44,7 +49,7 @@ function safeModelLabel(value: unknown): string {
44
49
  return (redactSensitiveText(String(value ?? "Model")).text.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim() || "Model").slice(0, 120);
45
50
  }
46
51
 
47
- function capabilities(gatewayInstanceRef: string, runtimeAvailable = false): PiagentGatewayCapabilityHandshakeV1 {
52
+ function capabilities(gatewayInstanceRef: string, runtimeAvailable = false, managed = false): PiagentGatewayCapabilityHandshakeV1 {
48
53
  return {
49
54
  schemaVersion: 1,
50
55
  version: "piagent-gateway-capabilities-v1",
@@ -61,9 +66,9 @@ function capabilities(gatewayInstanceRef: string, runtimeAvailable = false): Pia
61
66
  create: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
62
67
  send: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
63
68
  abort: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
64
- setModel: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
69
+ setModel: runtimeAvailable && !managed ? available() : unavailable("model-managed-by-studio"),
65
70
  setThinking: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
66
- setPermission: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
71
+ setPermission: runtimeAvailable && !managed ? available() : unavailable("permissions-managed-by-studio"),
67
72
  rename: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
68
73
  pin: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
69
74
  archive: runtimeAvailable ? available() : unavailable("session-runtime-not-enabled"),
@@ -77,12 +82,20 @@ function capabilities(gatewayInstanceRef: string, runtimeAvailable = false): Pia
77
82
  };
78
83
  }
79
84
 
85
+ function packageVersion(root: string): string | undefined {
86
+ try { return String(JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version); } catch { return undefined; }
87
+ }
88
+
80
89
  export async function startPiagentGateway(options: {
81
90
  packageRoot: string;
82
91
  expectedPiVersion: string;
83
92
  agentDir?: string;
84
93
  staticRoot?: string;
85
94
  scopedBrokerRouter?: ScopedBrokerRouter;
95
+ managed?: { host: any; models: any; runtimeFactory: RuntimeFactory; project: string | null };
96
+ // Company sessions relayed into this dashboard (the dashboard entry passes
97
+ // the Agent Watch binding; omitted: none).
98
+ company?: CompanyConnector;
86
99
  }): Promise<{ descriptor: GatewayDescriptor; wait(): Promise<void>; close(): Promise<void> }> {
87
100
  const state = gatewayProfileState(options.agentDir);
88
101
  process.env.PI_CODING_AGENT_DIR = state.agentDir;
@@ -99,6 +112,7 @@ export async function startPiagentGateway(options: {
99
112
  let runtimes: SessionRuntimeSupervisor | null = null;
100
113
  let mcpAuth: McpAuthBroker | null = null;
101
114
  let attachments: SessionAttachmentRegistry | null = null;
115
+ let relay: CompanyRelay | null = null;
102
116
  let closing: Promise<void> | null = null;
103
117
  let settleWait: (() => void) | null = null;
104
118
  const waited = new Promise<void>((resolve) => { settleWait = resolve; });
@@ -107,6 +121,7 @@ export async function startPiagentGateway(options: {
107
121
  if (closing) return await closing;
108
122
  closing = (async () => {
109
123
  removeGatewayDescriptor(state, gatewayInstanceRef);
124
+ relay?.close();
110
125
  await loopback?.close().catch(() => undefined);
111
126
  // Staged bytes are private temp files. Closing deletes them rather than
112
127
  // leaving a directory per session behind for the TTL sweep that will never
@@ -129,14 +144,26 @@ export async function startPiagentGateway(options: {
129
144
  if (!loopback) return { ok: false, error: "gateway-starting" };
130
145
  return reply({ launchUrl: loopback.issueLaunchUrl(), gatewayInstanceRef });
131
146
  }
147
+ if (request.action === "project.register" || request.action === "project.paths") {
148
+ if (!options.managed || !runtimes) return { ok: false, error: "project-control-unavailable" };
149
+ if (request.action === "project.register") {
150
+ try { return reply(projects.register(request.cwd)); } catch { return { ok: false, error: "project-import-folder-invalid" }; }
151
+ }
152
+ // Registered folders and the folders of existing sessions.
153
+ return runtimes.listSessions().then((sessions) => {
154
+ const paths = new Map(projects.list().map((item) => [item.projectRef, projects.resolve(item.projectRef)]));
155
+ for (const info of sessions) if (typeof info.cwd === "string") paths.set(projectRefForCwd(key, info.cwd), info.cwd);
156
+ return reply([...paths].filter(([, cwd]) => cwd).map(([projectRef, cwd]) => ({ projectRef, cwd })));
157
+ }, () => ({ ok: false as const, error: "project-control-unavailable" }));
158
+ }
132
159
  setImmediate(() => { void close(); });
133
160
  return reply({ stopping: true, gatewayInstanceRef });
134
161
  }
135
162
  });
136
163
 
137
164
  try {
138
- const host = await loadPinnedPiHost(options.expectedPiVersion);
139
- const inspectionModels = await host.ModelRuntime.create({
165
+ const host = options.managed?.host ?? await loadPinnedPiHost(options.expectedPiVersion);
166
+ const inspectionModels = options.managed?.models ?? await host.ModelRuntime.create({
140
167
  authPath: path.join(state.agentDir, "auth.json"),
141
168
  modelsPath: path.join(state.agentDir, "models.json"),
142
169
  allowModelNetwork: false
@@ -145,12 +172,21 @@ export async function startPiagentGateway(options: {
145
172
  mcpAuth = new McpAuthBroker(state.agentDir);
146
173
  const leases = new SessionLeaseStore(state.root, key);
147
174
  const events = new GatewayEventStore();
175
+ if (options.managed?.project) projects.register(options.managed.project);
148
176
  runtimes = new SessionRuntimeSupervisor({
149
- gatewayInstanceRef, key, leases, listSessions: () => host.SessionManager.listAll(),
177
+ gatewayInstanceRef, key, leases, listSessions: cachedSessionLister((dir) => host.SessionManager.listAll(dir), path.join(state.agentDir, 'sessions'), Boolean(options.managed)),
150
178
  host, agentDir: state.agentDir, packageRoot: options.packageRoot, modelRuntime: inspectionModels, events,
179
+ runtimeFactory: options.managed?.runtimeFactory,
180
+ sessionDirectory: options.managed ? path.join(state.agentDir, 'sessions') : undefined,
151
181
  scopedBrokerRouter: options.scopedBrokerRouter,
152
182
  resolveProject: (projectRef) => projects.resolve(projectRef)
153
183
  });
184
+ const sessionFacts = (manager: { buildSessionContext(): any; getBranch(): any[] }) => {
185
+ const context = manager.buildSessionContext();
186
+ return { model: context.model ? { provider: String(context.model.provider), modelId: String(context.model.modelId) } : null,
187
+ thinkingLevel: context.thinkingLevel, managed: managedProjection(context, manager.getBranch()) };
188
+ };
189
+ const fileFacts = cachedSessionFacts((file) => sessionFacts(host.SessionManager.open(file)));
154
190
  const readCatalog = () => buildSessionCatalog({
155
191
  gatewayInstanceRef,
156
192
  key,
@@ -159,12 +195,15 @@ export async function startPiagentGateway(options: {
159
195
  readOwnership: (sessionRef) => runtimes!.ownership(sessionRef),
160
196
  readSessionOptions: (info) => {
161
197
  try {
162
- const context = host.SessionManager.open(info.path).buildSessionContext();
163
- const model = context.model ? inspectionModels.getModel(context.model.provider, context.model.modelId) : null;
164
- const thinking = ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(String(context.thinkingLevel))
165
- ? context.thinkingLevel : "unknown";
166
- return { modelLabel: model ? safeModelLabel(model.name ?? model.id ?? context.model?.modelId) : null,
167
- thinkingLevel: thinking };
198
+ // A session running here may not have flushed its model and thinking
199
+ // entries yet; its live manager has them, the file does not.
200
+ const live = runtimes!.liveSessionManager(sessionRefForPath(key, info.path));
201
+ const facts = live ? sessionFacts(live) : fileFacts(info.path);
202
+ const model = facts.model ? inspectionModels.getModel(facts.model.provider, facts.model.modelId) : null;
203
+ const thinking = ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(String(facts.thinkingLevel))
204
+ ? facts.thinkingLevel : "unknown";
205
+ return { modelLabel: model ? safeModelLabel(model.name ?? model.id ?? facts.model?.modelId) : null,
206
+ thinkingLevel: thinking, ...facts.managed };
168
207
  } catch { return { modelLabel: null, thinkingLevel: "unknown" }; }
169
208
  }
170
209
  });
@@ -173,7 +212,16 @@ export async function startPiagentGateway(options: {
173
212
  if (catalog.state !== "ready" || !session) throw new Error("session-projection-unavailable");
174
213
  return { sessionRevision: session.sessionRevision, liveState: session.liveState };
175
214
  });
176
- const commands = new SessionCommandController({ catalog: readCatalog, runtimes, metadata,
215
+ relay = options.managed || !options.company ? null : new CompanyRelay({
216
+ connector: options.company,
217
+ events, hubProject: (cwd) => projectRefForCwd(key, cwd),
218
+ folder: async (projectRef) => projects.resolve(projectRef)
219
+ ?? (await runtimes!.listSessions()).find((info) => projectRefForCwd(key, info.cwd) === projectRef)?.cwd ?? null });
220
+ // Every revision the browser sees and sends back is the merged one.
221
+ const hubCatalog = relay ? async () => relay!.merge(await readCatalog()) : readCatalog;
222
+ const company = (sessionRef: unknown) => !!relay?.owns(sessionRef);
223
+ const relayPost = (path: string, body: unknown) => relay!.json("POST", path, body);
224
+ const commands = new SessionCommandController({ catalog: hubCatalog, runtimes, metadata,
177
225
  store: new SessionCommandStore(state.root, key), events,
178
226
  // Resolved at call time: the attachment registry is built after this
179
227
  // controller, because it reads sessions through the inspection registry.
@@ -182,8 +230,9 @@ export async function startPiagentGateway(options: {
182
230
  return attachments.reserveForPrompt(sessionRef, refs, messageRequestId, text);
183
231
  } });
184
232
  const runtimeCommands = new RuntimeCommandController({ catalog: readCatalog, runtimes, events });
185
- const protocol = new GatewayProtocolService({ capabilities: () => capabilities(gatewayInstanceRef, true), catalog: readCatalog,
186
- events, command: commands });
233
+ const protocol = new GatewayProtocolService({ capabilities: () => capabilities(gatewayInstanceRef, true, !!options.managed), catalog: hubCatalog,
234
+ events, command: relay ? { execute: (command) => relay!.handles(command)
235
+ ? relay!.command(command as Record<string, unknown>, async () => (await hubCatalog()).catalogRevision) : commands.execute(command) } : commands });
187
236
  const inspections = new SessionInspectionRegistry({ gatewayInstanceRef, host, key, packageRoot: options.packageRoot, agentDir: state.agentDir,
188
237
  models: inspectionModels, projects, mcpAuth, listSessions: () => runtimes!.listSessions(),
189
238
  openLiveSession: (sessionRef) => runtimes!.liveSessionManager(sessionRef),
@@ -196,22 +245,51 @@ export async function startPiagentGateway(options: {
196
245
  loopback = await startLoopbackServer({
197
246
  staticRoot,
198
247
  mode: "gateway",
199
- readCapabilities: () => capabilities(gatewayInstanceRef, true),
200
- readSessionCatalog: readCatalog,
201
- readSessionLiveState: () => buildSessionLiveState({ gatewayInstanceRef, eventSequence: events.stateVersion,
202
- operations: runtimes!.currentOperations(), settlements: events.recentOperationSettlements() }),
203
- readSessionCreationOptions: () => inspections.creationOptions(),
248
+ readCapabilities: () => capabilities(gatewayInstanceRef, true, !!options.managed),
249
+ readSessionCatalog: hubCatalog,
250
+ // Company settlements arrive as re-published events; running company
251
+ // operations are read from its Gateway.
252
+ readSessionLiveState: async () => {
253
+ const companyOperations = relay ? await relay.operations() : [];
254
+ const live = buildSessionLiveState({ gatewayInstanceRef, eventSequence: events.stateVersion,
255
+ operations: runtimes!.currentOperations(), settlements: events.recentOperationSettlements() });
256
+ return companyOperations.length ? { ...live, operations: [...live.operations, ...companyOperations] } : live;
257
+ },
258
+ readSessionCreationOptions: async () => {
259
+ if (!relay) return inspections.creationOptions();
260
+ // A folder with company conversations is a dashboard project too, so
261
+ // a new chat there (company or personal) can be started from here.
262
+ for (const folder of await relay.folders()) { try { projects.register(folder); } catch { /* moved or unreadable */ } }
263
+ const options = await inspections.creationOptions() as { models: unknown[] };
264
+ return { ...options, models: [...await relay.models(), ...options.models] };
265
+ },
266
+ relaySessionRead: relay ? async (sessionRef, path) => company(sessionRef)
267
+ ? await relay!.http("GET", `/api/v1/sessions/${encodeURIComponent(sessionRef)}/inspection${path}`) : null : undefined,
204
268
  readSessionModel: (sessionRef) => inspections.provider(sessionRef),
205
269
  readSessionConnections: (sessionRef) => inspections.connections(sessionRef),
206
- executeSessionConnection: (command) => inspections.executeConnectionCommand(command),
207
- executeRuntimeCommand: (command) => runtimeCommands.execute(command),
208
- executeSessionAttachment: (sessionRef, command) => attachments.execute(sessionRef, command),
209
- executeApproval: (approvalRef, decision) => runtimes!.decideApproval(approvalRef, decision),
270
+ executeSessionConnection: (command) => {
271
+ if (options.managed) throw new Error('connections-managed-by-studio');
272
+ return company((command as { sessionRef?: unknown } | null)?.sessionRef) ? relayPost("/api/v1/session-connections", command)
273
+ : inspections.executeConnectionCommand(command);
274
+ },
275
+ executeRuntimeCommand: (command) => company((command as { sessionRef?: unknown } | null)?.sessionRef)
276
+ ? relayPost("/api/v1/runtime-commands", command) : runtimeCommands.execute(command),
277
+ executeSessionAttachment: (sessionRef, command) => company(sessionRef)
278
+ ? relayPost(`/api/v1/sessions/${encodeURIComponent(sessionRef)}/attachments`, command) : attachments.execute(sessionRef, command),
279
+ // Approval refs carry no session; a company approval is not pending here.
280
+ executeApproval: async (approvalRef, decision) => {
281
+ try { return await runtimes!.decideApproval(approvalRef, decision); }
282
+ catch (error) {
283
+ if (!relay || (error as Error).message !== "approval-not-pending") throw error;
284
+ return await relayPost(`/api/v1/approvals/${encodeURIComponent(approvalRef)}/decision`, decision);
285
+ }
286
+ },
210
287
  readMcpAuthJob: (jobRef) => mcpAuth!.read(jobRef),
211
288
  cancelMcpAuthJob: (jobRef) => mcpAuth!.cancel(jobRef),
212
- readProviderAuthCatalog: () => providerAuth.catalog(),
289
+ readProviderAuthCatalog: () => options.managed ? { ...providerAuth.catalog(), providers: [], reasonCode: 'personal-oauth-requires-personal-session' } : providerAuth.catalog(),
213
290
  readProviderAuthJob: (jobRef) => providerAuth.read(jobRef),
214
291
  executeProviderAuth: (command) => {
292
+ if (options.managed) throw new Error('personal-oauth-requires-personal-session');
215
293
  const action = command && typeof command === "object" && !Array.isArray(command) ? (command as Record<string, unknown>).action : null;
216
294
  if (action === "provider-auth.start") return providerAuth.start(command);
217
295
  if (action === "provider-auth.respond") return providerAuth.respond(command);
@@ -225,6 +303,7 @@ export async function startPiagentGateway(options: {
225
303
  events.publish("catalog.changed", { reasonCode: "project-imported" });
226
304
  return { schemaVersion: 1, version: "piagent-project-import-result-v1", importedAt: new Date().toISOString(), project, projects: registered };
227
305
  },
306
+ ...(relay ? { readCompanyStatus: () => relay!.status(), executeCompanyConnect: async () => { await relay!.connect(true); return relay!.status(); } } : {}),
228
307
  gatewayProtocol: protocol
229
308
  });
230
309
  descriptor = {
@@ -234,9 +313,11 @@ export async function startPiagentGateway(options: {
234
313
  startedAt: new Date().toISOString(),
235
314
  origin: loopback.origin,
236
315
  controlSocket: state.controlSocket,
237
- profileRef: profileRef(state, key)
316
+ profileRef: profileRef(state, key),
317
+ packageVersion: packageVersion(options.packageRoot)
238
318
  };
239
319
  writeGatewayDescriptor(state, descriptor);
320
+ relay?.start();
240
321
  } catch (error) {
241
322
  await close();
242
323
  throw error;
@@ -3,6 +3,7 @@ import { randomBytes } from "node:crypto";
3
3
  import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
4
4
  import { hasVisibleText } from "../shared/text-visibility.ts";
5
5
  import type { TerminalDeliveryReceipt } from "../server/terminal-delivery-receipt.ts";
6
+ import { effectiveStopReason } from "../server/message-stop-reason.ts";
6
7
  import { GatewayEventStore, type GatewayTaskStatus } from "./gateway-events.ts";
7
8
  import { SessionOperationLifecycle, type SessionOperationObservation,
8
9
  type SessionOperationPhase, type SessionOperationRetryPolicyOptions } from "./session-operation-lifecycle.ts";
@@ -103,7 +104,7 @@ function messageText(message: any): string {
103
104
  .map((item: any) => item.text).join("\n");
104
105
  }
105
106
  function assistantSettlement(message: any): Settlement {
106
- const stopReason = String(message?.stopReason ?? "");
107
+ const stopReason = effectiveStopReason(message);
107
108
  if (stopReason === "aborted") return { outcome: "aborted", reasonCode: "operation-aborted" };
108
109
  if (stopReason === "error") return { outcome: "error", reasonCode: "assistant-response-failed" };
109
110
  if (stopReason === "length") return { outcome: "error", reasonCode: "assistant-output-incomplete" };
@@ -159,6 +160,8 @@ export class GatewaySessionStream {
159
160
  readonly #commitTerminalDeliveryReceipt: ((message: unknown) => TerminalDeliveryReceipt | undefined) | undefined;
160
161
  #terminalDeliveryMessage: unknown;
161
162
  #receiptOnly = true;
163
+ #compactionRef: string | null = null;
164
+ readonly #capacityRefs = new Map<string, string>();
162
165
 
163
166
  get runtimeRestartRequired(): boolean { return this.#runtimeRestartRequired; }
164
167
  get lifecycleTerminationReasonCode(): string | null { return this.#lifecycleTerminationReasonCode; }
@@ -204,7 +207,17 @@ export class GatewaySessionStream {
204
207
  }
205
208
 
206
209
  observe(event: any): SessionOperationObservation {
210
+ // A company subagent started or ended while the main agent was quiet: its
211
+ // count in the catalog row ("Subagent 1/2") changed, and no agent event says so.
212
+ if (event?.type === "managed_helpers") this.#events.publish("catalog.changed", { reasonCode: "managed-helpers-changed" });
207
213
  const wasStarted = this.#lifecycle.started, wasSettled = this.#lifecycle.hostSettled;
214
+ // Summarising the older part of a long conversation can take a minute,
215
+ // right before the model is asked or after it answered: shown as a step
216
+ // of the running turn, not as silence.
217
+ if (event?.type === "compaction_start" || event?.type === "compaction_end") this.#compaction(event);
218
+ // A company request waiting in line for a free model account: a step of
219
+ // the running turn until Studio admits it, it gives up or Stop ends it.
220
+ if (event?.type === "managed_capacity_wait") this.#capacityWait(event);
208
221
  const observation = this.#lifecycle.observe(event);
209
222
  if (!observation.accepted) return observation;
210
223
  if (event?.type === "agent_start" || event?.message?.role === "assistant" || event?.assistantMessageEvent
@@ -255,6 +268,38 @@ export class GatewaySessionStream {
255
268
  return observation;
256
269
  }
257
270
 
271
+ #compaction(event: any): void {
272
+ if (event.type === "compaction_start") {
273
+ if (this.#compactionRef) return;
274
+ this.#compactionRef = ref("tool");
275
+ this.#events.publish("tool.started", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(),
276
+ toolCallRef: this.#compactionRef, toolLabel: "compaction", fileLabel: null, isError: null, reasonCode: null });
277
+ return;
278
+ }
279
+ if (!this.#compactionRef) return;
280
+ const failed = typeof event.errorMessage === "string" && event.errorMessage.length > 0;
281
+ this.#events.publish("tool.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(),
282
+ toolCallRef: this.#compactionRef, toolLabel: "compaction", fileLabel: null, isError: failed, reasonCode: failed ? "compaction-failed" : null });
283
+ this.#compactionRef = null;
284
+ }
285
+
286
+ #capacityWait(event: any): void {
287
+ const id = typeof event.id === "string" ? event.id : null;
288
+ if (!id) return;
289
+ if (event.state === "start" && !this.#capacityRefs.has(id)) {
290
+ const toolCallRef = ref("tool"); this.#capacityRefs.set(id, toolCallRef);
291
+ this.#events.publish("tool.started", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(),
292
+ toolCallRef, toolLabel: "capacity-wait", fileLabel: null, isError: null, reasonCode: null });
293
+ return;
294
+ }
295
+ const toolCallRef = this.#capacityRefs.get(id);
296
+ if (event.state !== "end" || !toolCallRef) return;
297
+ this.#capacityRefs.delete(id);
298
+ const failed = event.outcome === "gave-up";
299
+ this.#events.publish("tool.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(),
300
+ toolCallRef, toolLabel: "capacity-wait", fileLabel: null, isError: failed, reasonCode: failed ? "capacity-wait-expired" : null });
301
+ }
302
+
258
303
  #settleActiveTools(reasonCode: string): void {
259
304
  for (const [toolCallId, toolCallRef] of this.#toolRefs) {
260
305
  this.#events.publish("tool.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(), toolCallRef,
@@ -262,6 +307,11 @@ export class GatewaySessionStream {
262
307
  isError: true, reasonCode });
263
308
  }
264
309
  this.#toolRefs.clear(); this.#toolLabels.clear(); this.#toolFileLabels.clear();
310
+ for (const toolCallRef of this.#capacityRefs.values()) {
311
+ this.#events.publish("tool.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef, ...this.#correlation(), toolCallRef,
312
+ toolLabel: "capacity-wait", fileLabel: null, isError: false, reasonCode: null });
313
+ }
314
+ this.#capacityRefs.clear();
265
315
  }
266
316
 
267
317
  #flush(final: boolean): void {
@@ -270,7 +320,13 @@ export class GatewaySessionStream {
270
320
  return;
271
321
  }
272
322
  let take = final ? this.#buffer.length : this.#buffer.lastIndexOf("\n") + 1;
273
- if (take <= 0 && this.#buffer.length >= 1_024) take = this.#buffer.length;
323
+ // A long line goes out in pieces cut after a space. Credentials hold no
324
+ // space, so one still arriving is never sent half-formed (redaction knows
325
+ // a token only by its whole shape); a blob without any space is sent at 16 KiB.
326
+ if (take <= 0 && this.#buffer.length >= 1_024) {
327
+ const space = this.#buffer.search(/\s\S*$/);
328
+ take = space >= 0 ? space + 1 : this.#buffer.length >= 16_384 ? this.#buffer.length : 0;
329
+ }
274
330
  if (take <= 0) return;
275
331
  const candidate = this.#buffer.slice(0, take), privateStart = candidate.search(PRIVATE_BEGIN);
276
332
  if (privateStart >= 0 && !PRIVATE_END.test(candidate.slice(privateStart))) {
@@ -0,0 +1,41 @@
1
+ import { managedThinkingLevels } from '../../piagent-core/managed/capabilities.mjs';
2
+ import { ManagedBrokerClient } from '../../piagent-core/managed/broker-client.mjs';
3
+ import { ManagedSession } from '../../piagent-core/managed/session.mjs';
4
+ import fs from 'node:fs';
5
+
6
+ // Only called by the pinned launcher with Watch's non-secret import manifest.
7
+ // Each web session owns its own broker; no personal auth/model/resource loader.
8
+ export async function startManagedGateway({ config, configPath, cwd, agentDir, packageRoot, registerProject = true }) {
9
+ // Agent Watch may hold another key of this member since the Gateway
10
+ // started: a new conversation, or one that enrolls again, uses that key.
11
+ const slot=()=>{try{const now=JSON.parse(fs.readFileSync(configPath,'utf8'));return /^[a-f0-9]{64}$/.test(now.profile_id)&&now.broker===config.broker?now.profile_id:config.profile_id;}catch{return config.profile_id;}};
12
+ const newBroker=()=>new ManagedBrokerClient({executable:config.broker,profileID:slot()});
13
+ // Agent Watch rewrites the binding's revision when the key or its harness
14
+ // changes; a warm conversation then enrolls again before its next run.
15
+ const bindingRevision=()=>{try{return String(JSON.parse(fs.readFileSync(configPath,'utf8')).configuration_revision??'')||null;}catch{return null;}};
16
+ const probeBroker = newBroker();
17
+ let probe;
18
+ try {
19
+ probe = await ManagedSession.create({sdkRoot:config.sdk_root,cwd,origin:config.origin,broker:probeBroker,agentDir,protectedRoots:[configPath,agentDir]});
20
+ const { startPiagentGateway } = await import('./gateway-service.ts');
21
+ const { rpcUiContext } = await import('./rpc-ui-context.ts');
22
+ const { webUiModelRef } = await import('../../piagent-core/runtime/inspection/webui-snapshot.ts');
23
+ const host=probe.api,models=probe.modelRuntime;
24
+ const snapshot=models.getAvailableSnapshot.bind(models);
25
+ models.getAvailableSnapshot=()=>snapshot().map(model=>({...model,managedThinkingLevels:managedThinkingLevels(probe.manifest,models,model)}));
26
+ const factory=async(info,_instance,manager,initial)=>{
27
+ if(initial?.modelRef && initial.modelRef!==webUiModelRef('agent_watch_managed','agent-watch-auto')) throw Error('managed-personal-switch-requires-new-session');
28
+ const broker=newBroker();
29
+ let runtime;
30
+ try {
31
+ runtime=await ManagedSession.create({sdkRoot:config.sdk_root,cwd:info.cwd,origin:config.origin,broker,agentDir,
32
+ sessionManager:manager??host.SessionManager.open(info.path),protectedRoots:[configPath,agentDir],renewBroker:newBroker,bindingRevision});
33
+ if(initial) runtime.session.setThinkingLevel(initial.thinkingLevel);
34
+ await runtime.session.bindExtensions({mode:'rpc',uiContext:rpcUiContext()});
35
+ return {session:runtime.session,dispose:()=>runtime.dispose()};
36
+ } catch(error) { if(runtime) await runtime.dispose(); else await broker.dispose(); throw error; }
37
+ };
38
+ const gateway=await startPiagentGateway({packageRoot,agentDir,expectedPiVersion:'0.87.1',managed:{host,models,runtimeFactory:factory,project:registerProject?cwd:null}});
39
+ return gateway;
40
+ } finally { if(probe) await probe.dispose(); else await probeBroker.dispose(); }
41
+ }
@@ -0,0 +1,145 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { requestGatewayControl } from "./control-socket.ts";
8
+ import { gatewayProfileState } from "./profile-state.ts";
9
+ import { storedSlot } from "../../piagent-core/managed/store.mjs";
10
+
11
+ // The personal dashboard shows company sessions but never runs them: it starts
12
+ // the pinned managed entrypoint (its own process and Gateway, `--serve`) the
13
+ // way Agent Watch's launcher does, then relays to it. Grants stay inside that
14
+ // process; this process only reads the public binding.
15
+ export type ManagedLaunchStatus = { schemaVersion: 1; version: "piagent-managed-launch-status-v1"; available: boolean; model: "agent-watch-auto" };
16
+ type Binding = { file: string; profile: string; node: string; entrypoint: string; broker: string; brokerSha256: string };
17
+
18
+ const CLEAN_ENV = { PATH: "/usr/bin:/bin", LANG: "en_US.UTF-8", TERM: "xterm-256color" };
19
+ const READY = /^Agent Watch Auto: ready$/m;
20
+ const sha256 = (file: string) => createHash("sha256").update(fs.readFileSync(file)).digest("hex");
21
+ const HEX64 = /^[a-f0-9]{64}$/;
22
+
23
+ const DEFAULT_AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
24
+
25
+ // The relay and the company Gateway speak one protocol, so they must be the
26
+ // same Piagent release: Agent Watch may still point at another install (nvm,
27
+ // Homebrew…) and a company Gateway started before an update keeps running.
28
+ const versionAt = (file: string): string | null => { try { return String(JSON.parse(fs.readFileSync(file, "utf8")).version); } catch { return null; } };
29
+ const OWN_VERSION = versionAt(fileURLToPath(new URL("../../../package.json", import.meta.url)));
30
+ const sameRelease = (pinned: Binding) => OWN_VERSION === null || versionAt(path.join(path.dirname(path.dirname(pinned.entrypoint)), "package.json")) === OWN_VERSION;
31
+ const current = (health: { packageVersion?: unknown }) => OWN_VERSION === null || health.packageVersion === OWN_VERSION;
32
+
33
+ // Agent Watch writes the binding into the personal Pi folder it imported into.
34
+ function binding(agentDir: string): Binding | null {
35
+ const file = path.join(agentDir, "agent-watch-managed.json");
36
+ try {
37
+ const stat = fs.lstatSync(file);
38
+ if (!stat.isFile() || stat.size > 16_384) return null;
39
+ const value = JSON.parse(fs.readFileSync(file, "utf8"));
40
+ const absolute = (entry: unknown) => typeof entry === "string" && path.isAbsolute(entry);
41
+ if (value?.schema_version !== 1 || value.model !== "agent-watch-auto" || !HEX64.test(String(value.profile_id))
42
+ || !absolute(value.node) || !absolute(value.entrypoint) || !absolute(value.broker) || !HEX64.test(String(value.broker_sha256))) return null;
43
+ return { file, profile: value.profile_id, node: value.node, entrypoint: value.entrypoint, broker: value.broker, brokerSha256: value.broker_sha256 };
44
+ } catch { return null; }
45
+ }
46
+
47
+ export function managedLaunchStatus(agentDir = DEFAULT_AGENT_DIR): ManagedLaunchStatus {
48
+ return { schemaVersion: 1, version: "piagent-managed-launch-status-v1", available: binding(agentDir) !== null, model: "agent-watch-auto" };
49
+ }
50
+
51
+ // A key's slot points at its member's conversation store once it has been
52
+ // launched (store.mjs); until then the slot's own folder is asked.
53
+ function controlSocket(home: string, profile: string): string {
54
+ const root = path.join(home, "Library/Application Support/AgentWatch/ManagedSessions");
55
+ return gatewayProfileState(path.join(root, storedSlot(root, profile) ?? profile)).controlSocket;
56
+ }
57
+
58
+ // "busy": the Gateway took the connection but has not answered yet. It is
59
+ // alive; starting another beside it would leave two on the same store.
60
+ async function running(socket: string): Promise<{ packageVersion?: unknown } | "busy" | null> {
61
+ try {
62
+ const reply = await requestGatewayControl(socket, { action: "health" });
63
+ return reply.ok ? (reply.value && typeof reply.value === "object" ? reply.value : {}) : null;
64
+ } catch (error) { return error instanceof Error && error.message === "gateway-control-timeout" ? "busy" : null; }
65
+ }
66
+
67
+ // Same foreground step as Agent Watch: macOS may ask once to let this build of
68
+ // the broker read its Keychain item. Nothing secret comes back. Exit 67 means
69
+ // the imported key is no longer connected in Agent Watch.
70
+ async function authorize(pinned: Binding): Promise<void> {
71
+ if (fs.realpathSync(pinned.broker) !== pinned.broker || sha256(pinned.broker) !== pinned.brokerSha256) throw new Error("managed-launch-binding-changed");
72
+ const code = await new Promise<number | null>((resolve) => {
73
+ const child = spawn(pinned.broker, ["managed-authorize", "--profile", pinned.profile], { cwd: "/", stdio: "ignore", env: CLEAN_ENV, timeout: 180_000, killSignal: "SIGKILL" });
74
+ child.once("error", () => resolve(null));
75
+ child.once("exit", (exit) => resolve(exit));
76
+ });
77
+ if (code === 67) throw new Error("managed-profile-disconnected");
78
+ if (code !== 0) throw new Error("managed-keychain-approval-required");
79
+ }
80
+
81
+ // The control socket of a running company Gateway, without launching one.
82
+ export async function attachCompanyGateway(agentDir = DEFAULT_AGENT_DIR, home = os.homedir()): Promise<string | null> {
83
+ const pinned = binding(agentDir);
84
+ if (!pinned || !sameRelease(pinned)) return null;
85
+ const socket = controlSocket(home, pinned.profile);
86
+ const health = await running(socket);
87
+ return health && health !== "busy" && current(health) ? socket : null;
88
+ }
89
+
90
+ let inFlight: Promise<string> | null = null;
91
+
92
+ // Returns the company Gateway's control socket, starting it when needed.
93
+ export function ensureCompanyGateway(agentDir = DEFAULT_AGENT_DIR, home = os.homedir(), timeoutMs = 45_000): Promise<string> {
94
+ inFlight ??= start(agentDir, home, timeoutMs).finally(() => { inFlight = null; });
95
+ return inFlight;
96
+ }
97
+
98
+ async function start(agentDir: string, home: string, timeoutMs: number): Promise<string> {
99
+ const pinned = binding(agentDir);
100
+ if (!pinned) throw new Error("managed-config-missing");
101
+ if (!sameRelease(pinned)) throw new Error("managed-launch-version-mismatch");
102
+ const socket = controlSocket(home, pinned.profile);
103
+ let health = await running(socket);
104
+ for (const started = Date.now(); health === "busy"; health = await running(socket)) {
105
+ if (Date.now() - started > timeoutMs) throw new Error("managed-launch-busy");
106
+ }
107
+ if (health && current(health)) return socket;
108
+ if (health) {
109
+ // A company Gateway from before the update: stop it (its transcripts stay
110
+ // on disk) and start this release.
111
+ try { await requestGatewayControl(socket, { action: "stop" }); } catch { /* already stopping */ }
112
+ for (const started = Date.now(); await running(socket);) {
113
+ if (Date.now() - started > 15_000) throw new Error("managed-launch-stale-runtime");
114
+ await new Promise((resolve) => setTimeout(resolve, 250));
115
+ }
116
+ }
117
+ await authorize(pinned);
118
+ // Startup failures are reported on stderr as `Agent Watch: managed-…`; the
119
+ // log is private and removed once the process is ready or has exited.
120
+ const directory = path.join(home, "Library/Application Support/AgentWatch/ManagedLaunch");
121
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); fs.chmodSync(directory, 0o700);
122
+ const log = path.join(directory, `launch-${process.pid}-${Date.now()}.log`);
123
+ const fd = fs.openSync(log, "wx", 0o600);
124
+ let exited = false;
125
+ try {
126
+ const child = spawn(pinned.node, [pinned.entrypoint, "--config", pinned.file, "--serve"],
127
+ { cwd: "/", detached: true, stdio: ["ignore", fd, fd], env: CLEAN_ENV });
128
+ child.once("exit", () => { exited = true; }); child.once("error", () => { exited = true; }); child.unref();
129
+ } finally { fs.closeSync(fd); }
130
+ try {
131
+ for (const started = Date.now(); Date.now() - started < timeoutMs;) {
132
+ const text = fs.readFileSync(log, "utf8");
133
+ // The launch may just have pointed this slot at the member's store.
134
+ if (READY.test(text)) return controlSocket(home, pinned.profile);
135
+ if (exited) throw new Error(text.match(/Agent Watch: (managed-[a-z:_-]+)\./)?.[1] ?? "managed-launch-failed");
136
+ await new Promise((resolve) => setTimeout(resolve, 250));
137
+ }
138
+ throw new Error("managed-launch-timeout");
139
+ } finally { fs.rmSync(log, { force: true }); }
140
+ }
141
+
142
+ // The personal dashboard's connector for company sessions (CompanyRelay).
143
+ export function agentWatchCompanyConnector(agentDir = DEFAULT_AGENT_DIR, home = os.homedir()) {
144
+ return { configured: () => binding(agentDir) !== null, ensure: () => ensureCompanyGateway(agentDir, home), attach: () => attachCompanyGateway(agentDir, home) };
145
+ }
@@ -0,0 +1,20 @@
1
+ // Public managed state contains no provider/model route or credential.
2
+ const STATUS = ['pending', 'in_progress', 'completed'];
3
+ const OUTCOMES = ['no_change', 'interrupted', 'blocking_open', 'unverified', 'review_unavailable', 'unreviewed', 'clean'];
4
+ export function managedProjection(context: any, entries: any[]) {
5
+ if (context?.model?.provider !== 'agent_watch_managed') return {};
6
+ const last = (type: string) => entries.filter(e => e.type === 'custom' && e.customType === type).at(-1)?.data;
7
+ const counter = last('agent-watch-helpers');
8
+ const capabilities = last('agent-watch-model')?.managedThinkingLevels;
9
+ const levels = ['off','minimal','low','medium','high','xhigh','max'];
10
+ // The agent's checklist (update_plan) and how its last code-changing turn ended.
11
+ const plan = last('agent-watch-plan')?.plan;
12
+ const steps = Array.isArray(plan) && plan.length >= 1 && plan.length <= 30 && plan.every((p: any) => typeof p?.step === 'string' && p.step.trim() && STATUS.includes(p.status))
13
+ ? plan.map((p: any) => ({ step: p.step.trim().slice(0, 200), status: p.status })) : null;
14
+ const final = entries.filter(e => e.type === 'custom_message' && e.customType === 'agent-watch-process' && e.details?.phase === 'final').at(-1)?.details;
15
+ const process = final && OUTCOMES.includes(final.outcome) ? { outcome: final.outcome, verified: final.verified === true, reviewed: final.reviewed === true,
16
+ blockingOpen: Number.isInteger(final.blockingOpen) && final.blockingOpen >= 0 ? Math.min(final.blockingOpen, 1000) : 0, ...(final.planSkipped === true ? { planSkipped: true as const } : {}) } : null;
17
+ return {modelLabel:'agent-watch-auto', managedHelpers:{active:Number.isInteger(counter?.active) && counter.active>=0 && counter.active<=2 ? counter.active : 0, maximum:2 as const},
18
+ ...(Array.isArray(capabilities)?{managedThinkingLevels:levels.filter(level=>capabilities.includes(level))}:{}),
19
+ ...(steps ? {managedPlan:{steps}} : {}), ...(process ? {managedProcess:process} : {})};
20
+ }
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
+ import os from "node:os";
3
4
  import path from "node:path";
4
5
  import { createRequire } from "node:module";
5
6
  import { pathToFileURL } from "node:url";
@@ -27,10 +28,27 @@ export function installedPiHostRoot(): string {
27
28
  } catch {
28
29
  // The operator installation is normally global.
29
30
  }
30
- const executable = execFileSync("which", ["pi"], { encoding: "utf8", timeout: 2_000 }).trim();
31
- const found = packageRootFrom(fs.realpathSync(executable));
32
- if (!found) throw new Error("pi-host-unavailable");
33
- return found;
31
+ try {
32
+ const executable = execFileSync("which", ["pi"], { encoding: "utf8", timeout: 2_000, stdio: ["ignore", "pipe", "ignore"] }).trim();
33
+ const found = packageRootFrom(fs.realpathSync(executable));
34
+ if (found) return found;
35
+ } catch {
36
+ // A launcher with a short PATH (Agent Watch, launchd) cannot see `pi`.
37
+ }
38
+ // Then the host the installed CLI recorded, and the usual global prefixes.
39
+ const home = os.homedir(), hostPackage = path.join("lib", "node_modules", "@earendil-works", "pi-coding-agent");
40
+ let recorded: string | null = null;
41
+ try {
42
+ const agentDir = process.env.PI_CODING_AGENT_DIR || path.join(home, ".pi", "agent");
43
+ recorded = JSON.parse(fs.readFileSync(path.join(agentDir, "piagent-runtime.json"), "utf8")).pi_sdk_root ?? null;
44
+ } catch { /* no record yet */ }
45
+ for (const candidate of [recorded, path.join(home, ".pi", "npm-global", hostPackage), path.join(home, ".local", hostPackage),
46
+ path.join("/opt/homebrew", hostPackage), path.join("/usr/local", hostPackage)]) {
47
+ if (!candidate) continue;
48
+ const found = packageRootFrom(path.join(candidate, "package.json"));
49
+ if (found) return found;
50
+ }
51
+ throw new Error("pi-host-unavailable");
34
52
  }
35
53
 
36
54
  export async function loadPinnedPiHost(expectedVersion: string): Promise<any> {