@phyzess/pappra 0.1.0-beta.13 → 0.1.0-beta.18

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 (357) hide show
  1. package/README.md +134 -15
  2. package/assets/integrations/skills/pappra/SKILL.md +28 -7
  3. package/assets/integrations/skills/pappra/references/collaboration.md +633 -0
  4. package/assets/integrations/skills/pappra/references/environment.md +360 -0
  5. package/assets/integrations/skills/pappra/references/hosts.md +20 -0
  6. package/assets/integrations/skills/pappra/references/operations.md +36 -5
  7. package/dist/adapters/pi/recovery-extension.d.ts +65 -0
  8. package/dist/adapters/pi/recovery-extension.js +238 -0
  9. package/dist/adapters/pi/session-transition-extension.d.ts +30 -0
  10. package/dist/adapters/pi/session-transition-extension.js +265 -0
  11. package/dist/app/automatic-handoff-events.d.ts +26 -0
  12. package/dist/app/automatic-handoff-events.js +134 -0
  13. package/dist/app/bounded-file.d.ts +2 -0
  14. package/dist/app/bounded-file.js +2 -0
  15. package/dist/app/capability-evidence/claude.d.ts +37 -0
  16. package/dist/app/capability-evidence/claude.js +206 -0
  17. package/dist/app/capability-evidence/codex.d.ts +35 -0
  18. package/dist/app/capability-evidence/codex.js +193 -0
  19. package/dist/app/capability-evidence/index.d.ts +33 -0
  20. package/dist/app/capability-evidence/index.js +117 -0
  21. package/dist/app/capability-evidence/pi.d.ts +73 -0
  22. package/dist/app/capability-evidence/pi.js +669 -0
  23. package/dist/app/capability-evidence/shared.d.ts +69 -0
  24. package/dist/app/capability-evidence/shared.js +195 -0
  25. package/dist/app/collab-auto-handoff.d.ts +55 -0
  26. package/dist/app/collab-auto-handoff.js +384 -0
  27. package/dist/app/collab-bindings.d.ts +57 -0
  28. package/dist/app/collab-bindings.js +1006 -0
  29. package/dist/app/collab-bootstrap-transport.d.ts +163 -0
  30. package/dist/app/collab-bootstrap-transport.js +769 -0
  31. package/dist/app/collab-bootstrap.d.ts +137 -0
  32. package/dist/app/collab-bootstrap.js +3455 -0
  33. package/dist/app/collab-handoff.d.ts +12 -0
  34. package/dist/app/collab-handoff.js +511 -0
  35. package/dist/app/collab-messages.d.ts +2 -0
  36. package/dist/app/collab-messages.js +2 -0
  37. package/dist/app/collab-notify.d.ts +156 -0
  38. package/dist/app/collab-notify.js +1208 -0
  39. package/dist/app/collab-preflight.d.ts +34 -0
  40. package/dist/app/collab-preflight.js +96 -0
  41. package/dist/app/collab-recovery-executor.d.ts +161 -0
  42. package/dist/app/collab-recovery-executor.js +1153 -0
  43. package/dist/app/collab-recovery-projection.d.ts +103 -0
  44. package/dist/app/collab-recovery-projection.js +659 -0
  45. package/dist/app/collab-recovery.d.ts +25 -0
  46. package/dist/app/collab-recovery.js +429 -0
  47. package/dist/app/collab-resources.d.ts +16 -0
  48. package/dist/app/collab-resources.js +278 -0
  49. package/dist/app/collab-role-events.d.ts +22 -0
  50. package/dist/app/collab-role-events.js +329 -0
  51. package/dist/app/collab-role-orchestration.d.ts +53 -0
  52. package/dist/app/collab-role-orchestration.js +601 -0
  53. package/dist/app/collab-role-plan.d.ts +81 -0
  54. package/dist/app/collab-role-plan.js +504 -0
  55. package/dist/app/collab-role-repair.d.ts +109 -0
  56. package/dist/app/collab-role-repair.js +821 -0
  57. package/dist/app/collab-rollover.d.ts +263 -0
  58. package/dist/app/collab-rollover.js +1112 -0
  59. package/dist/app/collab-route.d.ts +64 -0
  60. package/dist/app/collab-route.js +240 -0
  61. package/dist/app/collab-session-transition.d.ts +221 -0
  62. package/dist/app/collab-session-transition.js +3334 -0
  63. package/dist/app/collab-state.d.ts +7 -0
  64. package/dist/app/collab-state.js +162 -0
  65. package/dist/app/collab-task-store.d.ts +22 -0
  66. package/dist/app/collab-task-store.js +204 -0
  67. package/dist/app/collab-work-execution.d.ts +127 -0
  68. package/dist/app/collab-work-execution.js +1027 -0
  69. package/dist/app/collab-work-plan.d.ts +87 -0
  70. package/dist/app/collab-work-plan.js +437 -0
  71. package/dist/app/collaboration.d.ts +8 -2
  72. package/dist/app/collaboration.js +101 -12
  73. package/dist/app/doctor.js +108 -5
  74. package/dist/app/host-mcp.js +34 -8
  75. package/dist/app/host-probe.js +4 -4
  76. package/dist/app/init.d.ts +58 -4
  77. package/dist/app/init.js +768 -155
  78. package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
  79. package/dist/app/managed-pi-bootstrap-admission.js +104 -0
  80. package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
  81. package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
  82. package/dist/app/managed-pi-delivery.d.ts +8 -0
  83. package/dist/app/managed-pi-delivery.js +110 -0
  84. package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
  85. package/dist/app/managed-pi-handoff-receiver.js +263 -0
  86. package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
  87. package/dist/app/managed-pi-handoff-reference.js +60 -0
  88. package/dist/app/managed-pi-herdr.d.ts +21 -0
  89. package/dist/app/managed-pi-herdr.js +185 -0
  90. package/dist/app/managed-pi-initial-admission.d.ts +28 -0
  91. package/dist/app/managed-pi-initial-admission.js +416 -0
  92. package/dist/app/managed-pi-initial-launch.d.ts +5 -0
  93. package/dist/app/managed-pi-initial-launch.js +61 -0
  94. package/dist/app/managed-pi-native-observation.d.ts +36 -0
  95. package/dist/app/managed-pi-native-observation.js +191 -0
  96. package/dist/app/managed-pi-readiness.d.ts +29 -0
  97. package/dist/app/managed-pi-readiness.js +288 -0
  98. package/dist/app/managed-pi-repair-stop.d.ts +21 -0
  99. package/dist/app/managed-pi-repair-stop.js +371 -0
  100. package/dist/app/managed-pi-resume-admission.d.ts +32 -0
  101. package/dist/app/managed-pi-resume-admission.js +618 -0
  102. package/dist/app/managed-pi-retirement.d.ts +28 -0
  103. package/dist/app/managed-pi-retirement.js +112 -0
  104. package/dist/app/managed-pi-sdk-control.d.ts +7 -0
  105. package/dist/app/managed-pi-sdk-control.js +169 -0
  106. package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
  107. package/dist/app/managed-pi-sdk-runtime.js +483 -0
  108. package/dist/app/managed-pi-source-admission.d.ts +14 -0
  109. package/dist/app/managed-pi-source-admission.js +323 -0
  110. package/dist/app/managed-pi-transition-host.d.ts +36 -0
  111. package/dist/app/managed-pi-transition-host.js +292 -0
  112. package/dist/app/managed-pi.d.ts +54 -0
  113. package/dist/app/managed-pi.js +248 -0
  114. package/dist/app/shell-readiness.d.ts +66 -0
  115. package/dist/app/shell-readiness.js +368 -0
  116. package/dist/app/task-pin.d.ts +2 -23
  117. package/dist/app/task-pin.js +2 -113
  118. package/dist/cli/commands/collaboration-binding.d.ts +13 -0
  119. package/dist/cli/commands/collaboration-binding.js +118 -0
  120. package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
  121. package/dist/cli/commands/collaboration-bootstrap.js +84 -0
  122. package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
  123. package/dist/cli/commands/collaboration-capabilities.js +127 -0
  124. package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
  125. package/dist/cli/commands/collaboration-handoff.js +102 -0
  126. package/dist/cli/commands/collaboration-message.d.ts +17 -0
  127. package/dist/cli/commands/collaboration-message.js +177 -0
  128. package/dist/cli/commands/collaboration-plan.d.ts +8 -0
  129. package/dist/cli/commands/collaboration-plan.js +108 -0
  130. package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
  131. package/dist/cli/commands/collaboration-preflight.js +167 -0
  132. package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
  133. package/dist/cli/commands/collaboration-recovery-run.js +224 -0
  134. package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
  135. package/dist/cli/commands/collaboration-recovery.js +104 -0
  136. package/dist/cli/commands/collaboration-resources.d.ts +11 -0
  137. package/dist/cli/commands/collaboration-resources.js +96 -0
  138. package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
  139. package/dist/cli/commands/collaboration-role-plan.js +69 -0
  140. package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
  141. package/dist/cli/commands/collaboration-role-run.js +128 -0
  142. package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
  143. package/dist/cli/commands/collaboration-rollover.js +103 -0
  144. package/dist/cli/commands/collaboration-state.d.ts +10 -0
  145. package/dist/cli/commands/collaboration-state.js +96 -0
  146. package/dist/cli/commands/collaboration-transition.d.ts +10 -0
  147. package/dist/cli/commands/collaboration-transition.js +75 -0
  148. package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
  149. package/dist/cli/commands/collaboration-work-run.js +178 -0
  150. package/dist/cli/commands/collaboration.d.ts +4 -0
  151. package/dist/cli/commands/collaboration.js +39 -4
  152. package/dist/cli/commands/continuity.js +2 -6
  153. package/dist/cli/commands/docs.d.ts +2 -0
  154. package/dist/cli/commands/docs.js +89 -15
  155. package/dist/cli/commands/host.d.ts +7 -1
  156. package/dist/cli/commands/host.js +122 -7
  157. package/dist/cli/commands/init.d.ts +20 -3
  158. package/dist/cli/commands/init.js +897 -232
  159. package/dist/cli/commands/mcp.d.ts +1 -0
  160. package/dist/cli/commands/mcp.js +11 -5
  161. package/dist/cli/commands/repair.d.ts +12 -0
  162. package/dist/cli/commands/repair.js +41 -0
  163. package/dist/cli/commands/runtime.d.ts +1 -1
  164. package/dist/cli/commands/runtime.js +139 -38
  165. package/dist/cli/commands/session.d.ts +7 -0
  166. package/dist/cli/commands/session.js +82 -0
  167. package/dist/cli/commands/task.js +19 -5
  168. package/dist/cli/run.js +866 -24
  169. package/dist/code/identity.d.ts +7 -0
  170. package/dist/code/identity.js +7 -3
  171. package/dist/code/registration.js +7 -0
  172. package/dist/code/service.d.ts +9 -0
  173. package/dist/code/service.js +16 -3
  174. package/dist/code/text.js +19 -7
  175. package/dist/collab/admission.d.ts +91 -66
  176. package/dist/collab/admission.js +21 -6
  177. package/dist/collab/canonical.d.ts +1 -1
  178. package/dist/collab/canonical.js +44 -3
  179. package/dist/collab/envelope.d.ts +2 -2
  180. package/dist/collab/envelope.js +12 -12
  181. package/dist/collab/first-work.d.ts +3 -0
  182. package/dist/collab/first-work.js +15 -0
  183. package/dist/collab/handoff.d.ts +16 -0
  184. package/dist/collab/handoff.js +39 -5
  185. package/dist/collab/index.d.ts +97 -1
  186. package/dist/collab/index.js +9 -1
  187. package/dist/collab/messages.js +126 -3
  188. package/dist/collab/persisted-messages.d.ts +123 -0
  189. package/dist/collab/persisted-messages.js +496 -0
  190. package/dist/collab/preflight.d.ts +1 -1
  191. package/dist/collab/resources.d.ts +13 -3
  192. package/dist/collab/resources.js +37 -6
  193. package/dist/collab/rollover.d.ts +311 -0
  194. package/dist/collab/rollover.js +1024 -0
  195. package/dist/collab/store.d.ts +1 -0
  196. package/dist/collab/store.js +1 -4
  197. package/dist/collab/types.d.ts +2 -354
  198. package/dist/collab/validation.d.ts +2 -10
  199. package/dist/collab/validation.js +2 -303
  200. package/dist/collab/work-plan.d.ts +105 -0
  201. package/dist/collab/work-plan.js +843 -0
  202. package/dist/continuity/context.d.ts +11 -2
  203. package/dist/continuity/context.js +100 -13
  204. package/dist/continuity/handoff-bundle.d.ts +296 -0
  205. package/dist/continuity/handoff-bundle.js +477 -0
  206. package/dist/continuity/index.d.ts +2 -0
  207. package/dist/continuity/index.js +1 -0
  208. package/dist/continuity/paths.js +10 -2
  209. package/dist/continuity/service.js +91 -15
  210. package/dist/continuity/task-pin.d.ts +24 -0
  211. package/dist/continuity/task-pin.js +590 -0
  212. package/dist/doctor.d.ts +1 -0
  213. package/dist/doctor.js +6 -0
  214. package/dist/experimental/index.d.ts +13 -0
  215. package/dist/experimental/index.js +25 -0
  216. package/dist/experimental/pi.d.ts +4 -0
  217. package/dist/experimental/pi.js +12 -0
  218. package/dist/hosts/claude/native.js +1 -1
  219. package/dist/hosts/codex/native.js +1 -1
  220. package/dist/hosts/codex/protocol.js +1 -1
  221. package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
  222. package/dist/hosts/integration/bootstrap-config.js +254 -0
  223. package/dist/hosts/integration/bundle-export.d.ts +44 -0
  224. package/dist/hosts/integration/bundle-export.js +366 -3
  225. package/dist/hosts/integration/bundle.js +3 -0
  226. package/dist/hosts/integration/hook.js +36 -9
  227. package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
  228. package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
  229. package/dist/hosts/integration/pappra-hook.mjs +61 -24749
  230. package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
  231. package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
  232. package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
  233. package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
  234. package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
  235. package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
  236. package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
  237. package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
  238. package/dist/hosts/integration/pi-native-events.d.ts +112 -0
  239. package/dist/hosts/integration/pi-native-events.js +348 -0
  240. package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
  241. package/dist/hosts/integration/pi-rollover-extension.js +431 -0
  242. package/dist/hosts/integration/recovery-config.d.ts +48 -0
  243. package/dist/hosts/integration/recovery-config.js +117 -0
  244. package/dist/hosts/integration/service.d.ts +6 -1
  245. package/dist/hosts/integration/service.js +485 -35
  246. package/dist/hosts/integration/support.d.ts +7 -0
  247. package/dist/hosts/integration/support.js +9 -1
  248. package/dist/hosts/integration/templates.d.ts +3 -0
  249. package/dist/hosts/integration/templates.js +26 -3
  250. package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
  251. package/dist/hosts/pi/managed-host-identity.js +40 -0
  252. package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
  253. package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
  254. package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
  255. package/dist/hosts/pi/managed-sdk-session.js +1639 -0
  256. package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
  257. package/dist/hosts/pi/managed-session-extension.js +84 -0
  258. package/dist/hosts/pi/managed-session.d.ts +177 -0
  259. package/dist/hosts/pi/managed-session.js +1557 -0
  260. package/dist/hosts/pi/protocol.js +1 -1
  261. package/dist/hosts/pi/query-result.js +1 -1
  262. package/dist/hosts/pi/resume-capability.d.ts +41 -0
  263. package/dist/hosts/pi/resume-capability.js +33 -0
  264. package/dist/hosts/pi/rpc.d.ts +182 -134
  265. package/dist/hosts/pi/rpc.js +230 -37
  266. package/dist/hosts/pilot.d.ts +97 -74
  267. package/dist/hosts/pilot.js +5 -1
  268. package/dist/hosts/version.d.ts +3 -0
  269. package/dist/hosts/version.js +18 -0
  270. package/dist/index.d.ts +0 -1
  271. package/dist/index.js +0 -1
  272. package/dist/infra/bounded-file.d.ts +19 -0
  273. package/dist/infra/bounded-file.js +66 -0
  274. package/dist/infra/fs.js +10 -2
  275. package/dist/infra/git.d.ts +29 -0
  276. package/dist/infra/git.js +45 -9
  277. package/dist/infra/json-property.d.ts +2 -0
  278. package/dist/infra/json-property.js +69 -0
  279. package/dist/infra/json.d.ts +4 -0
  280. package/dist/infra/json.js +24 -0
  281. package/dist/infra/owned-process-group.js +1 -1
  282. package/dist/infra/private-lock.d.ts +16 -0
  283. package/dist/infra/private-lock.js +148 -0
  284. package/dist/infra/private-state.d.ts +7 -0
  285. package/dist/infra/private-state.js +146 -0
  286. package/dist/infra/process-identity.d.ts +12 -0
  287. package/dist/infra/process-identity.js +142 -0
  288. package/dist/infra/validation.d.ts +5 -0
  289. package/dist/infra/validation.js +15 -0
  290. package/dist/init.d.ts +1 -0
  291. package/dist/init.js +6 -0
  292. package/dist/install/apply.d.ts +9 -1
  293. package/dist/install/apply.js +176 -12
  294. package/dist/install/dependencies.d.ts +38 -0
  295. package/dist/install/dependencies.js +282 -0
  296. package/dist/install/ownership.js +30 -1
  297. package/dist/install/references.d.ts +31 -0
  298. package/dist/install/references.js +208 -0
  299. package/dist/install/repair.d.ts +37 -0
  300. package/dist/install/repair.js +798 -0
  301. package/dist/install/reset-local.d.ts +16 -0
  302. package/dist/install/reset-local.js +304 -0
  303. package/dist/install/rollback.d.ts +3 -0
  304. package/dist/install/rollback.js +9 -3
  305. package/dist/install/runtime.d.ts +10 -6
  306. package/dist/install/runtime.js +299 -59
  307. package/dist/install/source-fingerprint.d.ts +2 -0
  308. package/dist/install/source-fingerprint.js +103 -0
  309. package/dist/knowledge/catalog.d.ts +1 -0
  310. package/dist/knowledge/catalog.js +143 -10
  311. package/dist/knowledge/journal.js +17 -7
  312. package/dist/knowledge/paths.js +2 -1
  313. package/dist/knowledge/refs.js +4 -0
  314. package/dist/knowledge/register-batch.d.ts +4 -0
  315. package/dist/knowledge/register-batch.js +225 -0
  316. package/dist/knowledge/relocate.d.ts +5 -0
  317. package/dist/knowledge/relocate.js +92 -0
  318. package/dist/knowledge/scan.js +103 -47
  319. package/dist/knowledge/service.d.ts +3 -0
  320. package/dist/knowledge/service.js +33 -1
  321. package/dist/knowledge/write.d.ts +4 -1
  322. package/dist/knowledge/write.js +1 -1
  323. package/dist/mcp/server.d.ts +4 -2
  324. package/dist/mcp/server.js +87 -136
  325. package/dist/schemas/collaboration-state-validation.d.ts +6 -0
  326. package/dist/schemas/collaboration-state-validation.js +342 -0
  327. package/dist/schemas/collaboration-state.d.ts +436 -0
  328. package/dist/schemas/collaboration-state.js +1 -0
  329. package/dist/schemas/collaboration.d.ts +335 -0
  330. package/dist/schemas/collaboration.js +272 -0
  331. package/dist/schemas/continuity.d.ts +8 -0
  332. package/dist/schemas/continuity.js +13 -3
  333. package/dist/schemas/doctor.d.ts +1 -0
  334. package/dist/schemas/doctor.js +1 -0
  335. package/dist/schemas/document.d.ts +61 -0
  336. package/dist/schemas/document.js +9 -0
  337. package/dist/schemas/errors.d.ts +1 -0
  338. package/dist/schemas/errors.js +1 -0
  339. package/dist/schemas/host.d.ts +24 -0
  340. package/dist/schemas/host.js +19 -0
  341. package/dist/schemas/install.d.ts +8 -2
  342. package/dist/schemas/install.js +11 -1
  343. package/dist/schemas/managed-pi-execution.d.ts +16 -0
  344. package/dist/schemas/managed-pi-execution.js +2 -0
  345. package/dist/schemas/outcome.d.ts +9 -5
  346. package/dist/workspace/config-file.js +1 -3
  347. package/dist/workspace/documents.js +12 -2
  348. package/dist/workspace/repositories.d.ts +7 -0
  349. package/dist/workspace/repositories.js +14 -5
  350. package/package.json +35 -35
  351. package/skills/pappra/SKILL.md +28 -7
  352. package/skills/pappra/references/collaboration.md +633 -0
  353. package/skills/pappra/references/environment.md +360 -0
  354. package/skills/pappra/references/hosts.md +20 -0
  355. package/skills/pappra/references/operations.md +36 -5
  356. package/dist/app/collab-from.d.ts +0 -35
  357. package/dist/app/collab-from.js +0 -84
@@ -0,0 +1,1639 @@
1
+ import { object as isRecord } from '../../infra/validation.js';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { withManagedProviderDiagnostics, managedProviderResponseId, } from './managed-provider-diagnostics.js';
5
+ import { managedPiHostIdentity } from './managed-host-identity.js';
6
+ import { captureProcessIdentity } from '../../infra/process-identity.js';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+ import { canonicalJson } from '../../collab/envelope.js';
11
+ import { assertManagedPiSdkResumeAuthorization, } from './resume-capability.js';
12
+ import { createManagedPiJournalArtifacts, ManagedPiSessionError, } from './managed-session.js';
13
+ const SDK_VERSION = '1.4.0';
14
+ const SDK_PRIVATE_SOURCE_HASHES = {
15
+ 'dist/core/compaction/compaction.js': 'f51e78ef1da66b5803fc91e185f07b968f8f989ab9326b7744958416aad3f6f6',
16
+ 'dist/core/agent-session.js': '436e8be3bbab2db475ce30bb5d0bb30f0046b19a3476f8cd57b8133e68891162',
17
+ 'dist/modes/interactive/interactive-mode.js': '633000038a8a9c60111fc9b32481c4015285fe530685277068bc5ed74f77dafa',
18
+ };
19
+ export const MANAGED_PI_SDK_PRIVATE_SOURCE_HASHES = SDK_PRIVATE_SOURCE_HASHES;
20
+ const SESSION_SCHEMA = 'harness-collab/pi-managed-session/1';
21
+ const SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
22
+ const SDK_SESSION_SCHEMA = 'harness-collab/pi-managed-sdk-session/1';
23
+ /** Shared fence used by the SDK extension and the lifecycle controller. */
24
+ export class ManagedPiSdkIngressFence {
25
+ journal;
26
+ isFenced = false;
27
+ constructor(journal) {
28
+ this.journal = journal;
29
+ }
30
+ checkpointRevisionValue = 0;
31
+ get checkpointRevision() {
32
+ return this.checkpointRevisionValue;
33
+ }
34
+ invalidateResumeCheckpoint(reason) {
35
+ this.checkpointRevisionValue += 1;
36
+ try {
37
+ this.journal.append('sdk_resume_checkpoint_invalidated', { reason });
38
+ }
39
+ catch (error) {
40
+ this.fence('resume_checkpoint_invalidation_failed');
41
+ throw error;
42
+ }
43
+ }
44
+ get fenced() {
45
+ return this.isFenced;
46
+ }
47
+ fence(reason) {
48
+ if (this.isFenced)
49
+ return;
50
+ this.isFenced = true;
51
+ try {
52
+ this.journal.append('sdk_ingress_fenced', { reason });
53
+ }
54
+ catch {
55
+ // The in-memory gate remains closed when durable evidence fails.
56
+ }
57
+ }
58
+ }
59
+ function appendPiMetadata(metadataPath, record) {
60
+ let fd;
61
+ try {
62
+ fd = fs.openSync(metadataPath, 'a', 0o600);
63
+ const bytes = Buffer.from(`${JSON.stringify(record)}\n`, 'utf8');
64
+ let offset = 0;
65
+ while (offset < bytes.length)
66
+ offset += fs.writeSync(fd, bytes, offset, bytes.length - offset);
67
+ fs.fsyncSync(fd);
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ finally {
74
+ if (fd !== undefined)
75
+ fs.closeSync(fd);
76
+ }
77
+ }
78
+ function metadataRecord(type, context) {
79
+ return {
80
+ schema: SESSION_SCHEMA,
81
+ type,
82
+ session_id: context.sessionManager.getSessionId(),
83
+ cwd: context.cwd,
84
+ session_file: context.sessionManager.getSessionFile() ?? null,
85
+ at: new Date().toISOString(),
86
+ };
87
+ }
88
+ /** Inline Pi extension factory: journals semantic ingress and blocks new tools after fencing. */
89
+ export function createManagedPiSdkLifecycleExtension(fence, journal, metadataPath, canExecute = () => true) {
90
+ return (pi) => {
91
+ const api = pi;
92
+ api.on('session_start', (_event, context) => {
93
+ if (!appendPiMetadata(metadataPath, metadataRecord('session_start', context)))
94
+ fence.fence('session_start_metadata_write_failed');
95
+ });
96
+ api.on('agent_settled', (_event, context) => {
97
+ if (!appendPiMetadata(metadataPath, metadataRecord('agent_settled', context)))
98
+ fence.fence('agent_settled_metadata_write_failed');
99
+ });
100
+ api.on('input', (event) => {
101
+ if (fence.fenced || !canExecute())
102
+ return { action: 'handled' };
103
+ try {
104
+ journal.append('sdk_input_hook_observed', {
105
+ source: event.source ?? 'unknown',
106
+ streaming_behavior: event.streamingBehavior ?? null,
107
+ });
108
+ return { action: 'continue' };
109
+ }
110
+ catch {
111
+ fence.fence('semantic_input_journal_failed');
112
+ return { action: 'handled' };
113
+ }
114
+ });
115
+ api.on('tool_call', () => {
116
+ if (fence.fenced || !canExecute())
117
+ return {
118
+ block: true,
119
+ reason: 'managed Pi runtime is fenced',
120
+ terminate: true,
121
+ };
122
+ try {
123
+ fence.invalidateResumeCheckpoint('native_tool_call');
124
+ }
125
+ catch {
126
+ return {
127
+ block: true,
128
+ reason: 'managed Pi resume checkpoint journal failed',
129
+ terminate: true,
130
+ };
131
+ }
132
+ return undefined;
133
+ });
134
+ };
135
+ }
136
+ function loadSdkModule(sdkModulePath) {
137
+ if (!path.isAbsolute(sdkModulePath))
138
+ throw new ManagedPiSessionError('sdk_module_path_must_be_absolute');
139
+ return import(pathToFileURL(sdkModulePath).href);
140
+ }
141
+ function verifySdkPrivateSourceHashes(sdkModulePath) {
142
+ const packageRoot = path.dirname(path.dirname(sdkModulePath));
143
+ for (const [relative, expected] of Object.entries(SDK_PRIVATE_SOURCE_HASHES)) {
144
+ let actual;
145
+ try {
146
+ actual = createHash('sha256')
147
+ .update(fs.readFileSync(path.join(packageRoot, relative)))
148
+ .digest('hex');
149
+ }
150
+ catch {
151
+ throw new ManagedPiSessionError('sdk_private_source_unavailable', relative);
152
+ }
153
+ if (actual !== expected)
154
+ throw new ManagedPiSessionError('sdk_private_source_hash_mismatch', relative);
155
+ }
156
+ }
157
+ /** Read-only compatibility gate: never create a session or freeze a source. */
158
+ export function verifyManagedPiSdkInstallation(sdkModulePath) {
159
+ if (!path.isAbsolute(sdkModulePath))
160
+ throw new ManagedPiSessionError('sdk_module_path_must_be_absolute');
161
+ const packageFile = path.join(path.dirname(path.dirname(sdkModulePath)), 'package.json');
162
+ const stat = fs.lstatSync(packageFile);
163
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024)
164
+ throw new ManagedPiSessionError('sdk_package_invalid');
165
+ const manifest = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
166
+ if (!isRecord(manifest) ||
167
+ manifest['name'] !== '@earendil-works/pi-coding-agent' ||
168
+ manifest['version'] !== SDK_VERSION)
169
+ throw new ManagedPiSessionError('unsupported_sdk_version');
170
+ verifySdkPrivateSourceHashes(sdkModulePath);
171
+ }
172
+ function readArrayField(target, key) {
173
+ if (!Reflect.has(target, key))
174
+ return null;
175
+ const value = Reflect.get(target, key);
176
+ return Array.isArray(value) ? [...value] : null;
177
+ }
178
+ function readRecordArrayField(target, key) {
179
+ const value = readArrayField(target, key);
180
+ return value?.every(isRecord)
181
+ ? value
182
+ : null;
183
+ }
184
+ function readFunctionCount(target, key) {
185
+ const value = readArrayField(target, key);
186
+ return value?.every((item) => typeof item === 'function')
187
+ ? value.length
188
+ : null;
189
+ }
190
+ function capturePinnedPrivateQueues(session, mode) {
191
+ const sessionObject = session;
192
+ const modeObject = mode;
193
+ const pendingNextTurn = readRecordArrayField(sessionObject, '_pendingNextTurnMessages');
194
+ const pendingCustom = readRecordArrayField(sessionObject, '_pendingCustomMessages');
195
+ const pendingBash = readRecordArrayField(sessionObject, '_pendingBashMessages');
196
+ const deferredSettledActionCount = readFunctionCount(sessionObject, '_deferredSettledActions');
197
+ const rawInputs = readArrayField(modeObject, 'pendingUserInputs');
198
+ const pendingUserInputs = rawInputs?.every((item) => typeof item === 'string')
199
+ ? rawInputs
200
+ : null;
201
+ const rawCompactionMessages = readArrayField(modeObject, 'compactionQueuedMessages');
202
+ const compactionMessages = rawCompactionMessages?.every((message) => isRecord(message) &&
203
+ typeof message['text'] === 'string' &&
204
+ (message['mode'] === 'steer' || message['mode'] === 'followUp'))
205
+ ? rawCompactionMessages
206
+ : null;
207
+ const rawDialogs = readArrayField(modeObject, 'extensionDialogs');
208
+ const dialogsAreValid = rawDialogs?.every((dialog) => isRecord(dialog) &&
209
+ typeof dialog['blocked'] === 'boolean' &&
210
+ typeof dialog['component'] === 'object' &&
211
+ dialog['component'] !== null);
212
+ const hasSelectorField = Reflect.has(modeObject, 'activeSelectorToken');
213
+ const selectorToken = hasSelectorField
214
+ ? Reflect.get(modeObject, 'activeSelectorToken')
215
+ : undefined;
216
+ const activeSelector = hasSelectorField &&
217
+ (selectorToken === undefined ||
218
+ selectorToken === null ||
219
+ typeof selectorToken === 'string')
220
+ ? typeof selectorToken === 'string'
221
+ : null;
222
+ return {
223
+ complete: pendingNextTurn !== null &&
224
+ pendingCustom !== null &&
225
+ pendingBash !== null &&
226
+ deferredSettledActionCount !== null &&
227
+ pendingUserInputs !== null &&
228
+ compactionMessages !== null &&
229
+ rawDialogs !== null &&
230
+ dialogsAreValid === true &&
231
+ activeSelector !== null,
232
+ pendingNextTurn,
233
+ pendingCustom,
234
+ pendingBash,
235
+ deferredSettledActionCount,
236
+ pendingUserInputs,
237
+ compactionMessages,
238
+ extensionDialogCount: dialogsAreValid ? (rawDialogs?.length ?? null) : null,
239
+ activeSelector,
240
+ };
241
+ }
242
+ async function waitWithin(promise, milliseconds) {
243
+ return await new Promise((resolve) => {
244
+ const timer = setTimeout(() => resolve(false), milliseconds);
245
+ promise.then(() => {
246
+ clearTimeout(timer);
247
+ resolve(true);
248
+ }, () => {
249
+ clearTimeout(timer);
250
+ resolve(false);
251
+ });
252
+ });
253
+ }
254
+ function isCompleteIdleSnapshot(snapshot) {
255
+ const { queues, work, extensionDialogs, builtInUiApprovals } = snapshot.ownedRuntime;
256
+ return (queues.completeness === 'complete' &&
257
+ queues.pendingMessageCount === 0 &&
258
+ work.observation === 'complete' &&
259
+ work.isIdle &&
260
+ !work.isStreaming &&
261
+ !work.isCompacting &&
262
+ work.isBashRunning === false &&
263
+ work.hasPendingBashMessages === false &&
264
+ work.pendingToolCalls === 0 &&
265
+ extensionDialogs.completeness === 'complete' &&
266
+ extensionDialogs.pending.length === 0 &&
267
+ builtInUiApprovals.observation === 'complete' &&
268
+ builtInUiApprovals.uncoveredPaths.length === 0);
269
+ }
270
+ function hashSessionTranscript(sessionFile, sessionId, cwd) {
271
+ let fd;
272
+ try {
273
+ const absolute = path.resolve(sessionFile);
274
+ const stat = fs.lstatSync(absolute, { bigint: true });
275
+ if (!stat.isFile() || stat.isSymbolicLink())
276
+ return null;
277
+ const canonical = fs.realpathSync(absolute);
278
+ if (canonical !== absolute)
279
+ return null;
280
+ fd = fs.openSync(canonical, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
281
+ const opened = fs.fstatSync(fd, { bigint: true });
282
+ if (!opened.isFile() ||
283
+ opened.dev !== stat.dev ||
284
+ opened.ino !== stat.ino ||
285
+ opened.size !== stat.size)
286
+ return null;
287
+ const headerBuffer = Buffer.alloc(64 * 1024);
288
+ const headerBytes = fs.readSync(fd, headerBuffer, 0, headerBuffer.length, 0);
289
+ const firstLine = headerBuffer
290
+ .subarray(0, headerBytes)
291
+ .toString('utf8')
292
+ .split('\n', 1)[0];
293
+ if (!firstLine)
294
+ return null;
295
+ const header = JSON.parse(firstLine);
296
+ if (!isRecord(header) ||
297
+ header['type'] !== 'session' ||
298
+ header['id'] !== sessionId ||
299
+ typeof header['cwd'] !== 'string' ||
300
+ (header['cwd'] !== cwd &&
301
+ path.resolve(header['cwd']) !== path.resolve(cwd)))
302
+ return null;
303
+ const hash = createHash('sha256');
304
+ const buffer = Buffer.allocUnsafe(1024 * 1024);
305
+ let offset = 0;
306
+ for (;;) {
307
+ const count = fs.readSync(fd, buffer, 0, buffer.length, offset);
308
+ if (count === 0)
309
+ break;
310
+ hash.update(buffer.subarray(0, count));
311
+ offset += count;
312
+ }
313
+ const after = fs.fstatSync(fd, { bigint: true });
314
+ if (after.dev !== opened.dev ||
315
+ after.ino !== opened.ino ||
316
+ after.size !== opened.size ||
317
+ after.mtimeNs !== opened.mtimeNs ||
318
+ BigInt(offset) !== after.size)
319
+ return null;
320
+ return {
321
+ sessionFile: canonical,
322
+ transcriptDigest: `sha256:${hash.digest('hex')}`,
323
+ };
324
+ }
325
+ catch {
326
+ return null;
327
+ }
328
+ finally {
329
+ if (fd !== undefined)
330
+ fs.closeSync(fd);
331
+ }
332
+ }
333
+ /** Wrap the public ExtensionUIContext passed through AgentSession.bindExtensions. */
334
+ function wrapExtensionUiDialogs(context, onDialogStart, onDialogEnd) {
335
+ if (!isRecord(context))
336
+ throw new ManagedPiSessionError('sdk_extension_ui_context_invalid');
337
+ const wrapped = { ...context };
338
+ for (const kind of ['confirm', 'select', 'input']) {
339
+ const original = context[kind];
340
+ if (typeof original !== 'function')
341
+ throw new ManagedPiSessionError(`sdk_interactive_${kind}_handler_unavailable`);
342
+ wrapped[kind] = async function (...dialogArgs) {
343
+ const dialog = {
344
+ id: randomUUID(),
345
+ kind,
346
+ startedAt: new Date().toISOString(),
347
+ };
348
+ onDialogStart(dialog);
349
+ let result;
350
+ let failed = false;
351
+ try {
352
+ result = await Reflect.apply(original, context, dialogArgs);
353
+ return result;
354
+ }
355
+ catch (error) {
356
+ failed = true;
357
+ throw error;
358
+ }
359
+ finally {
360
+ onDialogEnd(dialog.id, result, failed);
361
+ }
362
+ };
363
+ }
364
+ return wrapped;
365
+ }
366
+ /**
367
+ * Lifecycle wrapper around the controller-owned SDK AgentSession and native InteractiveMode.
368
+ * The public start function builds both from the explicitly selected SDK module.
369
+ */
370
+ export class ManagedPiSdkSessionController {
371
+ input;
372
+ disposed = false;
373
+ summaryProviderRun = new AsyncLocalStorage();
374
+ originalStreams = new WeakMap();
375
+ unsubscribers = new WeakMap();
376
+ pendingDialogs = new Map();
377
+ lifecycleListeners = new Set();
378
+ terminalStopped = false;
379
+ originalSessionMethods = new WeakMap();
380
+ activeBashPromises = new WeakMap();
381
+ interactiveRun;
382
+ session;
383
+ nativeIdentity;
384
+ initialized = false;
385
+ resourceFingerprint = null;
386
+ resourceUpdatePending = false;
387
+ submittedInputs = new Map();
388
+ resumeCheckpointScope;
389
+ lastAgentEndCheckpointSafe = false;
390
+ providerCalls = new WeakMap();
391
+ constructor(input) {
392
+ this.input = input;
393
+ this.session = input.runtime.session;
394
+ this.nativeIdentity = input.nativeIdentity;
395
+ this.resumeCheckpointScope =
396
+ input.resumeAuthorization?.scope ?? input.executionScope;
397
+ for (const registered of input.resumeAuthorization?.registeredInputs ??
398
+ []) {
399
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(registered.requestId) ||
400
+ !/^[a-f0-9]{64}$/.test(registered.inputDigest) ||
401
+ this.submittedInputs.size >= 256 ||
402
+ this.submittedInputs.has(registered.requestId))
403
+ throw new ManagedPiSessionError('resume_registered_input_invalid');
404
+ this.submittedInputs.set(registered.requestId, registered.inputDigest);
405
+ }
406
+ this.bindSession(this.session);
407
+ }
408
+ onDialogStart(dialog) {
409
+ this.pendingDialogs.set(dialog.id, dialog);
410
+ try {
411
+ this.input.journal.append('sdk_extension_dialog_opened', {
412
+ dialog_id: dialog.id,
413
+ kind: dialog.kind,
414
+ started_at: dialog.startedAt,
415
+ });
416
+ }
417
+ catch {
418
+ this.input.fence.fence('extension_dialog_journal_failed');
419
+ }
420
+ }
421
+ onDialogEnd(id, result, failed) {
422
+ const dialog = this.pendingDialogs.get(id);
423
+ if (!dialog)
424
+ return;
425
+ this.pendingDialogs.delete(id);
426
+ const disposition = failed
427
+ ? 'failed'
428
+ : dialog.kind === 'confirm'
429
+ ? result === true
430
+ ? 'confirmed'
431
+ : 'declined'
432
+ : result === undefined
433
+ ? 'dismissed'
434
+ : 'accepted';
435
+ try {
436
+ this.input.journal.append('sdk_extension_dialog_closed', {
437
+ dialog_id: id,
438
+ kind: dialog.kind,
439
+ disposition,
440
+ });
441
+ }
442
+ catch {
443
+ this.input.fence.fence('extension_dialog_journal_failed');
444
+ }
445
+ }
446
+ wrapSessionExtensionBindings(session) {
447
+ const target = session;
448
+ const original = target['bindExtensions'];
449
+ if (typeof original !== 'function')
450
+ throw new ManagedPiSessionError('sdk_session_bind_extensions_unavailable');
451
+ this.originalSessionMethods
452
+ .get(session)
453
+ ?.set('bindExtensions', original);
454
+ target['bindExtensions'] = (bindings) => {
455
+ if (!isRecord(bindings))
456
+ throw new ManagedPiSessionError('sdk_extension_bindings_invalid');
457
+ if (!('uiContext' in bindings) || bindings['uiContext'] === undefined)
458
+ return Reflect.apply(original, session, [bindings]);
459
+ const uiContext = wrapExtensionUiDialogs(bindings['uiContext'], (dialog) => this.onDialogStart(dialog), (id, result, failed) => this.onDialogEnd(id, result, failed));
460
+ return Reflect.apply(original, session, [{ ...bindings, uiContext }]);
461
+ };
462
+ }
463
+ bindSession(session) {
464
+ this.session = session;
465
+ this.wrapSessionOperations(session);
466
+ this.wrapSessionExtensionBindings(session);
467
+ const target = session;
468
+ // MCP discovery and explicit tool selection can finish after TUI startup.
469
+ // These methods belong to the source-hash-pinned Pi 1.4 session contract.
470
+ for (const name of ['_refreshToolRegistry', 'setActiveToolsByName']) {
471
+ const original = target[name];
472
+ if (typeof original !== 'function')
473
+ continue;
474
+ this.originalSessionMethods
475
+ .get(session)
476
+ ?.set(name, original);
477
+ target[name] = (...args) => {
478
+ const result = Reflect.apply(original, session, args);
479
+ this.scheduleResourceObservation();
480
+ return result;
481
+ };
482
+ }
483
+ const agent = session.agent;
484
+ if (!this.originalStreams.has(agent)) {
485
+ const original = agent.streamFunction;
486
+ this.originalStreams.set(agent, original);
487
+ agent.streamFunction = (...args) => {
488
+ if ((this.input.fence.fenced || this.input.canExecute?.() === false) &&
489
+ !this.summaryProviderRun.getStore())
490
+ throw new Error('managed Pi provider ingress is fenced');
491
+ const providerCallId = randomUUID();
492
+ try {
493
+ this.input.fence.invalidateResumeCheckpoint('provider_run');
494
+ this.input.journal.append('sdk_provider_run_admitted', {
495
+ session_id: session.sessionId,
496
+ provider_call_id: providerCallId,
497
+ purpose: this.summaryProviderRun.getStore()
498
+ ? 'native_summary_compaction'
499
+ : 'task_work',
500
+ });
501
+ }
502
+ catch (error) {
503
+ this.input.fence.fence('provider_run_journal_failed');
504
+ throw error;
505
+ }
506
+ this.providerCalls.set(session, {
507
+ id: providerCallId,
508
+ started: performance.now(),
509
+ });
510
+ const observedArgs = [...args];
511
+ observedArgs[2] = withManagedProviderDiagnostics(args[2], args[0], providerCallId, (kind, fields) => {
512
+ try {
513
+ this.input.journal.append(kind, {
514
+ session_id: session.sessionId,
515
+ ...fields,
516
+ });
517
+ }
518
+ catch {
519
+ this.input.fence.fence('provider_diagnostics_journal_failed');
520
+ }
521
+ });
522
+ return Reflect.apply(original, agent, observedArgs);
523
+ };
524
+ }
525
+ this.unsubscribers.get(session)?.();
526
+ this.unsubscribers.set(session, session.subscribe((event) => this.observeSessionEvent(session, event)));
527
+ }
528
+ scheduleResourceObservation() {
529
+ if (this.resourceUpdatePending || this.disposed)
530
+ return;
531
+ this.resourceUpdatePending = true;
532
+ setImmediate(() => {
533
+ this.resourceUpdatePending = false;
534
+ if (this.disposed)
535
+ return;
536
+ try {
537
+ if (!this.publishResourcesReady())
538
+ this.notifyLifecycleListeners({
539
+ type: 'native_resources_changed',
540
+ published: false,
541
+ });
542
+ }
543
+ catch {
544
+ this.input.fence.fence('native_resource_observation_failed');
545
+ }
546
+ }).unref();
547
+ }
548
+ wrapSessionOperations(session) {
549
+ const target = session;
550
+ const originals = new Map();
551
+ const bashPromises = new Set();
552
+ this.originalSessionMethods.set(session, originals);
553
+ this.activeBashPromises.set(session, bashPromises);
554
+ for (const name of [
555
+ 'prompt',
556
+ 'steer',
557
+ 'followUp',
558
+ 'sendUserMessage',
559
+ 'sendCustomMessage',
560
+ 'executeBash',
561
+ ]) {
562
+ const original = target[name];
563
+ if (typeof original !== 'function')
564
+ throw new ManagedPiSessionError('sdk_session_ingress_method_unavailable', name);
565
+ const originalMethod = original;
566
+ originals.set(name, originalMethod);
567
+ target[name] = (...args) => {
568
+ if (this.input.fence.fenced || this.input.canExecute?.() === false)
569
+ return Promise.reject(new ManagedPiSessionError('sdk_runtime_ingress_fenced'));
570
+ try {
571
+ this.input.fence.invalidateResumeCheckpoint(`semantic_${name}`);
572
+ const serializedArguments = JSON.stringify(args);
573
+ this.input.journal.append(name === 'executeBash'
574
+ ? 'sdk_user_bash_admitted'
575
+ : 'sdk_semantic_input_admitted', {
576
+ session_id: session.sessionId,
577
+ channel: name,
578
+ argument_count: args.length,
579
+ arguments_digest: `sha256:${createHash('sha256').update(serializedArguments).digest('hex')}`,
580
+ arguments_byte_length: Buffer.byteLength(serializedArguments),
581
+ });
582
+ }
583
+ catch (error) {
584
+ this.input.fence.fence('semantic_input_journal_failed');
585
+ return Promise.reject(error);
586
+ }
587
+ const result = Reflect.apply(originalMethod, session, args);
588
+ if (name !== 'executeBash')
589
+ return result;
590
+ const running = Promise.resolve(result);
591
+ bashPromises.add(running);
592
+ void running.then(() => bashPromises.delete(running), () => bashPromises.delete(running));
593
+ return running;
594
+ };
595
+ }
596
+ }
597
+ subscribe(listener) {
598
+ this.lifecycleListeners.add(listener);
599
+ return () => this.lifecycleListeners.delete(listener);
600
+ }
601
+ notifyLifecycleListeners(event) {
602
+ const snapshot = this.snapshot();
603
+ for (const listener of this.lifecycleListeners) {
604
+ try {
605
+ listener(event, snapshot);
606
+ }
607
+ catch {
608
+ // Observer failures do not mutate Pi's native event dispatch.
609
+ }
610
+ }
611
+ }
612
+ observeSessionEvent(session, event) {
613
+ if (!isRecord(event) || typeof event['type'] !== 'string')
614
+ return;
615
+ const type = event['type'];
616
+ if (type === 'agent_start')
617
+ this.lastAgentEndCheckpointSafe = false;
618
+ if (type === 'agent_end') {
619
+ const messages = Array.isArray(event['messages'])
620
+ ? event['messages']
621
+ : [];
622
+ const lastAssistant = [...messages]
623
+ .reverse()
624
+ .find((message) => isRecord(message) && message['role'] === 'assistant');
625
+ this.lastAgentEndCheckpointSafe =
626
+ event['willRetry'] === false &&
627
+ isRecord(lastAssistant) &&
628
+ lastAssistant['stopReason'] === 'stop' &&
629
+ typeof lastAssistant['errorMessage'] !== 'string';
630
+ }
631
+ const invalidatesCheckpoint = type === 'queue_update' ||
632
+ type === 'tool_execution_start' ||
633
+ type === 'tool_execution_update' ||
634
+ type === 'tool_execution_end' ||
635
+ type === 'agent_start' ||
636
+ type === 'agent_end' ||
637
+ type === 'agent_settled' ||
638
+ type === 'message_end' ||
639
+ type === 'entry_appended' ||
640
+ type === 'session_info_changed' ||
641
+ type === 'thinking_level_changed' ||
642
+ type === 'compaction_start' ||
643
+ type === 'compaction_end';
644
+ try {
645
+ if (invalidatesCheckpoint)
646
+ this.input.fence.invalidateResumeCheckpoint(`native_${type}`);
647
+ if (type === 'message_end' &&
648
+ isRecord(event['message']) &&
649
+ event['message']['role'] === 'assistant') {
650
+ const message = event['message'];
651
+ const call = this.providerCalls.get(session);
652
+ const reason = message['stopReason'];
653
+ this.input.journal.append('sdk_provider_message_observed', {
654
+ session_id: session.sessionId,
655
+ provider_call_id: call?.id ?? null,
656
+ response_id: managedProviderResponseId(message['responseId']),
657
+ stop_reason: typeof reason === 'string' &&
658
+ ['stop', 'length', 'error', 'aborted', 'toolUse'].includes(reason)
659
+ ? reason
660
+ : 'unknown',
661
+ elapsed_ms: call
662
+ ? Math.round(Math.max(0, performance.now() - call.started) * 1000) /
663
+ 1000
664
+ : null,
665
+ });
666
+ }
667
+ if (type === 'queue_update') {
668
+ const queueDigests = (value) => Array.isArray(value)
669
+ ? value.map((item) => `sha256:${createHash('sha256')
670
+ .update(JSON.stringify(item) ?? 'undefined')
671
+ .digest('hex')}`)
672
+ : null;
673
+ this.input.journal.append('sdk_queue_observed', {
674
+ session_id: session.sessionId,
675
+ steering: queueDigests(event['steering']),
676
+ follow_up: queueDigests(event['followUp']),
677
+ });
678
+ }
679
+ else if (type === 'tool_execution_start' ||
680
+ type === 'tool_execution_end' ||
681
+ type === 'agent_settled' ||
682
+ type === 'agent_start' ||
683
+ type === 'agent_end') {
684
+ const lifecycle = {
685
+ session_id: session.sessionId,
686
+ };
687
+ if (type === 'agent_settled')
688
+ lifecycle['aborted'] = event['aborted'] === true;
689
+ if (type === 'agent_end') {
690
+ const messages = Array.isArray(event['messages'])
691
+ ? event['messages']
692
+ : [];
693
+ const lastAssistant = [...messages]
694
+ .reverse()
695
+ .find((message) => isRecord(message) && message['role'] === 'assistant');
696
+ lifecycle['will_retry'] = event['willRetry'] === true;
697
+ lifecycle['stop_reason'] = isRecord(lastAssistant)
698
+ ? (lastAssistant['stopReason'] ?? null)
699
+ : null;
700
+ lifecycle['error_message_present'] =
701
+ isRecord(lastAssistant) &&
702
+ typeof lastAssistant['errorMessage'] === 'string';
703
+ }
704
+ this.input.journal.append(`sdk_${type}`, lifecycle);
705
+ }
706
+ }
707
+ catch {
708
+ this.input.fence.fence('sdk_lifecycle_journal_failed');
709
+ }
710
+ if (type === 'queue_update' ||
711
+ type === 'tool_execution_start' ||
712
+ type === 'tool_execution_end' ||
713
+ type === 'agent_settled' ||
714
+ type === 'agent_start' ||
715
+ type === 'agent_end')
716
+ this.notifyLifecycleListeners(event);
717
+ if (type === 'agent_settled') {
718
+ const settledSafely = event['aborted'] === false && this.lastAgentEndCheckpointSafe;
719
+ this.lastAgentEndCheckpointSafe = false;
720
+ try {
721
+ this.publishResourcesReady(true);
722
+ }
723
+ catch {
724
+ this.input.fence.fence('resources_ready_refresh_failed');
725
+ }
726
+ if (settledSafely) {
727
+ try {
728
+ this.appendResumeCheckpoint();
729
+ }
730
+ catch {
731
+ this.input.fence.fence('resume_checkpoint_write_failed');
732
+ }
733
+ }
734
+ }
735
+ }
736
+ /** Called by AgentSessionRuntime's native replacement callback wrapper. */
737
+ replaceSession(session) {
738
+ const previousSessionId = this.session.sessionId;
739
+ this.input.fence.invalidateResumeCheckpoint('session_replaced');
740
+ this.bindSession(session);
741
+ try {
742
+ this.input.journal.append('sdk_session_replaced', {
743
+ previous_session_id: previousSessionId,
744
+ session_id: session.sessionId,
745
+ session_file: session.sessionFile ?? null,
746
+ });
747
+ }
748
+ catch {
749
+ this.input.fence.fence('session_replacement_journal_failed');
750
+ }
751
+ this.notifyLifecycleListeners({
752
+ type: 'session_replaced',
753
+ previousSessionId,
754
+ sessionId: session.sessionId,
755
+ });
756
+ }
757
+ fenceIngress(reason = 'managed_sdk_ingress_fenced') {
758
+ this.input.fence.fence(reason);
759
+ }
760
+ snapshot() {
761
+ const { fence, journal, interactiveMode } = this.input;
762
+ const session = this.session;
763
+ const privateQueues = capturePinnedPrivateQueues(session, interactiveMode);
764
+ const steering = [...session.getSteeringMessages()];
765
+ const followUp = [...session.getFollowUpMessages()];
766
+ const pendingToolCalls = session.state.pendingToolCalls;
767
+ const pendingToolCallCount = pendingToolCalls instanceof Set ? pendingToolCalls.size : null;
768
+ const isBashRunning = session.isBashRunning ?? null;
769
+ const hasPendingBashMessages = session.hasPendingBashMessages ?? null;
770
+ const pendingDialogs = [...this.pendingDialogs.values()];
771
+ const unclassifiedDialogs = privateQueues.extensionDialogCount === null
772
+ ? null
773
+ : Math.max(0, privateQueues.extensionDialogCount - pendingDialogs.length);
774
+ const internalQueueCount = [
775
+ privateQueues.pendingNextTurn?.length,
776
+ privateQueues.pendingCustom?.length,
777
+ privateQueues.pendingBash?.length,
778
+ privateQueues.deferredSettledActionCount,
779
+ privateQueues.pendingUserInputs?.length,
780
+ privateQueues.compactionMessages?.length,
781
+ ].every((count) => typeof count === 'number')
782
+ ? (privateQueues.pendingNextTurn?.length ?? 0) +
783
+ (privateQueues.pendingCustom?.length ?? 0) +
784
+ (privateQueues.pendingBash?.length ?? 0) +
785
+ (privateQueues.deferredSettledActionCount ?? 0) +
786
+ (privateQueues.pendingUserInputs?.length ?? 0) +
787
+ (privateQueues.compactionMessages?.length ?? 0)
788
+ : null;
789
+ const workComplete = pendingToolCallCount !== null &&
790
+ isBashRunning !== null &&
791
+ hasPendingBashMessages !== null;
792
+ const inventoryComplete = privateQueues.complete && workComplete;
793
+ const workIsIdle = inventoryComplete &&
794
+ session.isIdle &&
795
+ !session.isStreaming &&
796
+ !session.isCompacting &&
797
+ isBashRunning === false &&
798
+ hasPendingBashMessages === false &&
799
+ pendingToolCallCount === 0;
800
+ const hasDialog = pendingDialogs.length > 0 ||
801
+ (privateQueues.extensionDialogCount ?? 0) > 0 ||
802
+ privateQueues.activeSelector === true;
803
+ const blockers = [
804
+ ...(!privateQueues.complete
805
+ ? ['owned_private_queue_inventory_unknown']
806
+ : []),
807
+ ...(!workComplete ? ['owned_work_inventory_unknown'] : []),
808
+ ...(steering.length + followUp.length > 0 || (internalQueueCount ?? 0) > 0
809
+ ? ['owned_runtime_messages_pending']
810
+ : []),
811
+ ...(!workIsIdle ? ['owned_agent_session_work_active'] : []),
812
+ ...(hasDialog ? ['owned_interactive_dialog_pending'] : []),
813
+ ];
814
+ const usage = session.getContextUsage();
815
+ const totalPendingMessages = internalQueueCount === null
816
+ ? session.pendingMessageCount
817
+ : session.pendingMessageCount + internalQueueCount;
818
+ return {
819
+ schema: SDK_SESSION_SCHEMA,
820
+ process: { pid: process.pid, exit: 'not_owned' },
821
+ session: {
822
+ sessionId: session.sessionId,
823
+ sessionFile: session.sessionFile ?? null,
824
+ },
825
+ contextUsage: usage ? { ...usage } : null,
826
+ ingress: {
827
+ state: fence.fenced ? 'fenced' : 'open',
828
+ journalPath: journal.path,
829
+ sequence: journal.currentSequence,
830
+ headDigest: journal.currentDigest,
831
+ healthy: journal.healthy,
832
+ },
833
+ ownedRuntime: {
834
+ scope: 'pi_agent_session',
835
+ queues: {
836
+ observedScope: 'pinned_pi_1_4_owned_runtime',
837
+ completeness: inventoryComplete ? 'complete' : 'unknown',
838
+ steering,
839
+ followUp,
840
+ pendingMessageCount: totalPendingMessages,
841
+ internal: {
842
+ pendingNextTurn: privateQueues.pendingNextTurn,
843
+ pendingCustom: privateQueues.pendingCustom,
844
+ pendingBash: privateQueues.pendingBash,
845
+ deferredSettledActionCount: privateQueues.deferredSettledActionCount,
846
+ pendingUserInputs: privateQueues.pendingUserInputs,
847
+ compactionMessages: privateQueues.compactionMessages,
848
+ extensionDialogCount: privateQueues.extensionDialogCount,
849
+ activeSelector: privateQueues.activeSelector,
850
+ },
851
+ },
852
+ work: {
853
+ observation: workComplete ? 'complete' : 'unknown',
854
+ isIdle: session.isIdle,
855
+ isStreaming: session.isStreaming,
856
+ isCompacting: session.isCompacting,
857
+ isBashRunning,
858
+ hasPendingBashMessages,
859
+ pendingToolCalls: pendingToolCallCount,
860
+ },
861
+ extensionDialogs: {
862
+ observedScope: 'InteractiveMode_extension_confirm_select_input',
863
+ completeness: privateQueues.complete && unclassifiedDialogs !== null
864
+ ? 'complete'
865
+ : 'unknown',
866
+ pending: unclassifiedDialogs === null
867
+ ? pendingDialogs
868
+ : [
869
+ ...pendingDialogs,
870
+ ...Array.from({ length: unclassifiedDialogs }, (_, index) => ({
871
+ id: `unclassified-native-dialog-${index + 1}`,
872
+ kind: 'unclassified',
873
+ startedAt: 'unknown',
874
+ })),
875
+ ],
876
+ },
877
+ builtInUiApprovals: {
878
+ observation: inventoryComplete ? 'complete' : 'unknown',
879
+ uncoveredPaths: inventoryComplete
880
+ ? []
881
+ : ['Pi 1.4 pinned native queue/UI fields failed validation'],
882
+ },
883
+ },
884
+ externalWriters: 'outside_runtime_scope',
885
+ descendants: 'not_owned_by_sdk_runtime',
886
+ scopedQuiescent: fence.fenced &&
887
+ workIsIdle &&
888
+ steering.length === 0 &&
889
+ followUp.length === 0 &&
890
+ internalQueueCount === 0 &&
891
+ !hasDialog,
892
+ blockers,
893
+ };
894
+ }
895
+ getContextUsage() {
896
+ const usage = this.session.getContextUsage();
897
+ return usage ? { ...usage } : null;
898
+ }
899
+ observe() {
900
+ const session = this.session;
901
+ const model = session.model;
902
+ const identity = this.nativeIdentity;
903
+ const usage = session.getContextUsage();
904
+ return {
905
+ native: {
906
+ harness: 'pi',
907
+ sessionId: session.sessionId,
908
+ agentId: identity?.agentId ?? null,
909
+ paneId: identity?.paneId ?? null,
910
+ surface: identity?.surface ?? 'pi-sdk-tui',
911
+ configurationId: identity?.configurationId ?? null,
912
+ modelObservation: {
913
+ model: model?.id ?? this.input.model,
914
+ provider: model?.provider ?? 'openrouter',
915
+ reasoning: session.thinkingLevel ?? null,
916
+ },
917
+ },
918
+ cwd: this.input.runtime.cwd,
919
+ contextPercent: usage?.percent ?? null,
920
+ };
921
+ }
922
+ async inventory() {
923
+ const snapshot = this.snapshot();
924
+ const { queues, work, extensionDialogs } = snapshot.ownedRuntime;
925
+ const pendingMessages = [
926
+ ...queues.steering.map((text) => ({ kind: 'steering', text })),
927
+ ...queues.followUp.map((text) => ({ kind: 'follow_up', text })),
928
+ ];
929
+ const internal = queues.internal;
930
+ for (const [kind, items] of [
931
+ ['next_turn', internal.pendingNextTurn],
932
+ ['custom', internal.pendingCustom],
933
+ ['bash', internal.pendingBash],
934
+ ['pending_user_input', internal.pendingUserInputs],
935
+ ['compaction', internal.compactionMessages],
936
+ ]) {
937
+ if (items)
938
+ pendingMessages.push(...items.map((item) => ({ kind, item })));
939
+ }
940
+ const pendingApprovals = extensionDialogs.pending.filter((dialog) => dialog.kind === 'confirm' || dialog.kind === 'unclassified');
941
+ const inflight = [];
942
+ if (!work.isIdle || work.isStreaming || work.isCompacting)
943
+ inflight.push({
944
+ kind: 'agent_session',
945
+ isIdle: work.isIdle,
946
+ isStreaming: work.isStreaming,
947
+ });
948
+ if (work.pendingToolCalls !== null && work.pendingToolCalls > 0)
949
+ inflight.push({ kind: 'tool_calls', count: work.pendingToolCalls });
950
+ if (work.isBashRunning)
951
+ inflight.push({ kind: 'bash_execution', active: true });
952
+ if (work.hasPendingBashMessages)
953
+ inflight.push({ kind: 'pending_bash_messages', active: true });
954
+ if (internal.deferredSettledActionCount !== null &&
955
+ internal.deferredSettledActionCount > 0)
956
+ inflight.push({
957
+ kind: 'deferred_settled_actions',
958
+ count: internal.deferredSettledActionCount,
959
+ });
960
+ if (internal.activeSelector)
961
+ pendingApprovals.push({
962
+ id: 'active-native-selector',
963
+ kind: 'unclassified',
964
+ startedAt: 'unknown',
965
+ });
966
+ return {
967
+ scope: 'pinned_pi_1_4_owned_runtime',
968
+ completeness: queues.completeness === 'complete' &&
969
+ work.observation === 'complete' &&
970
+ extensionDialogs.completeness === 'complete'
971
+ ? 'complete'
972
+ : 'unknown',
973
+ inflight,
974
+ pending_approvals: pendingApprovals,
975
+ pending_messages: pendingMessages,
976
+ outside_scope: [
977
+ 'arbitrary_external_writers',
978
+ 'unowned_processes_and_descendants',
979
+ ],
980
+ };
981
+ }
982
+ canCaptureSummary() {
983
+ const { queues, work, extensionDialogs, builtInUiApprovals } = this.snapshot().ownedRuntime;
984
+ if (this.disposed ||
985
+ queues.completeness !== 'complete' ||
986
+ queues.pendingMessageCount !== 0 ||
987
+ work.observation !== 'complete' ||
988
+ !work.isIdle ||
989
+ work.isStreaming ||
990
+ work.isCompacting ||
991
+ work.isBashRunning ||
992
+ work.pendingToolCalls !== 0 ||
993
+ extensionDialogs.completeness !== 'complete' ||
994
+ extensionDialogs.pending.length ||
995
+ builtInUiApprovals.observation !== 'complete' ||
996
+ builtInUiApprovals.uncoveredPaths.length)
997
+ return false;
998
+ try {
999
+ return this.input.canCompact?.() === true;
1000
+ }
1001
+ catch {
1002
+ return false;
1003
+ }
1004
+ }
1005
+ async captureSummary(causeId) {
1006
+ if (!causeId.trim())
1007
+ throw new ManagedPiSessionError('summary_cause_id_required');
1008
+ if (this.disposed)
1009
+ throw new ManagedPiSessionError('sdk_runtime_already_disposed');
1010
+ const quiescence = await this.quiesce('native_summary_capture');
1011
+ if (!quiescence.scopedQuiescent)
1012
+ throw new ManagedPiSessionError('sdk_summary_source_not_quiescent');
1013
+ return this.summaryProviderRun.run(causeId, async () => {
1014
+ const result = await this.session.compact();
1015
+ if (!isRecord(result) || typeof result['summary'] !== 'string')
1016
+ throw new ManagedPiSessionError('sdk_native_summary_missing');
1017
+ const summary = result['summary'];
1018
+ if (!this.session.sessionFile)
1019
+ throw new ManagedPiSessionError('sdk_session_file_unavailable');
1020
+ const sessionFile = this.session.sessionFile;
1021
+ this.input.journal.append('sdk_native_summary_captured', {
1022
+ cause_id: causeId,
1023
+ session_id: this.session.sessionId,
1024
+ session_file: sessionFile,
1025
+ summary_utf8_bytes: Buffer.byteLength(summary, 'utf8'),
1026
+ });
1027
+ return { summary, complete: false, sessionFile };
1028
+ });
1029
+ }
1030
+ stopTerminalIngress() {
1031
+ if (this.terminalStopped)
1032
+ return;
1033
+ this.input.interactiveMode.stop();
1034
+ this.terminalStopped = true;
1035
+ }
1036
+ async quiesce(reason = 'managed_sdk_quiesce') {
1037
+ this.stopTerminalIngress();
1038
+ this.input.fence.fence(reason);
1039
+ const deadline = Date.now() + this.input.quiesceTimeoutMs;
1040
+ const activeSession = this.session;
1041
+ if (this.session.isBashRunning) {
1042
+ const abortBash = activeSession['abortBash'];
1043
+ if (typeof abortBash === 'function')
1044
+ Reflect.apply(abortBash, this.session, []);
1045
+ }
1046
+ const abortDone = await waitWithin(this.session.abort().catch(() => undefined), Math.max(0, deadline - Date.now()));
1047
+ const bashPromises = [...(this.activeBashPromises.get(this.session) ?? [])];
1048
+ const bashDone = await waitWithin(Promise.allSettled(bashPromises), Math.max(0, deadline - Date.now()));
1049
+ const idleDone = await waitWithin(this.session.waitForIdle(), Math.max(0, deadline - Date.now()));
1050
+ const snapshot = this.snapshot();
1051
+ this.input.journal.append('sdk_quiesced', {
1052
+ session_id: snapshot.session.sessionId,
1053
+ scoped_quiescent: snapshot.scopedQuiescent,
1054
+ blockers: snapshot.blockers,
1055
+ abort_completed: abortDone,
1056
+ bash_completed: bashDone,
1057
+ idle_observed: idleDone,
1058
+ timeout_ms: this.input.quiesceTimeoutMs,
1059
+ });
1060
+ return snapshot;
1061
+ }
1062
+ async stopOwnedRuntime() {
1063
+ if (this.disposed)
1064
+ throw new ManagedPiSessionError('sdk_runtime_already_disposed');
1065
+ const finalSnapshot = await this.quiesce('managed_sdk_stop');
1066
+ if (!finalSnapshot.scopedQuiescent)
1067
+ throw new ManagedPiSessionError('sdk_runtime_not_quiescent');
1068
+ const disposed = await waitWithin(this.input.runtime.dispose(), this.input.quiesceTimeoutMs);
1069
+ if (!disposed)
1070
+ throw new ManagedPiSessionError('sdk_runtime_dispose_timeout');
1071
+ this.disposed = true;
1072
+ this.input.journal.append('sdk_runtime_disposed', {
1073
+ session_id: finalSnapshot.session.sessionId,
1074
+ process_pid: process.pid,
1075
+ process_identity: captureProcessIdentity(),
1076
+ process_exit: 'not_owned',
1077
+ blockers: finalSnapshot.blockers,
1078
+ });
1079
+ const receipt = {
1080
+ status: 'runtime_disposed',
1081
+ process: { pid: process.pid, exit: 'not_owned' },
1082
+ sessionId: finalSnapshot.session.sessionId,
1083
+ finalSnapshot,
1084
+ };
1085
+ this.closeObservers();
1086
+ this.input.journal.close();
1087
+ return receipt;
1088
+ }
1089
+ get isDisposed() {
1090
+ return this.disposed;
1091
+ }
1092
+ async cleanupAfterStartupFailure() {
1093
+ this.stopTerminalIngress();
1094
+ this.input.fence.fence('sdk_start_failed');
1095
+ const deadline = Date.now() + this.input.quiesceTimeoutMs;
1096
+ const activeSession = this.session;
1097
+ if (this.session.isBashRunning) {
1098
+ const abortBash = activeSession['abortBash'];
1099
+ if (typeof abortBash === 'function')
1100
+ Reflect.apply(abortBash, this.session, []);
1101
+ }
1102
+ await waitWithin(this.session.abort().catch(() => undefined), Math.max(0, deadline - Date.now()));
1103
+ await waitWithin(this.session.waitForIdle(), Math.max(0, deadline - Date.now()));
1104
+ const snapshot = this.snapshot();
1105
+ const disposed = await waitWithin(this.input.runtime.dispose(), Math.max(0, deadline - Date.now()));
1106
+ this.input.journal.append('sdk_start_failure_cleanup', {
1107
+ session_id: snapshot.session.sessionId,
1108
+ runtime_disposed: disposed,
1109
+ scoped_quiescent: snapshot.scopedQuiescent,
1110
+ blockers: snapshot.blockers,
1111
+ });
1112
+ if (disposed) {
1113
+ this.disposed = true;
1114
+ this.closeObservers();
1115
+ this.input.journal.close();
1116
+ }
1117
+ return disposed;
1118
+ }
1119
+ closeObservers() {
1120
+ this.unsubscribers.get(this.session)?.();
1121
+ // A disposed session may still be referenced by an extension. Keep the
1122
+ // ingress/provider gates latched; restoring originals would re-enable work.
1123
+ this.lifecycleListeners.clear();
1124
+ }
1125
+ bindResumeCheckpointScope(scope) {
1126
+ if (scope.session_id !== this.session.sessionId ||
1127
+ path.resolve(scope.cwd) !== path.resolve(this.input.runtime.cwd))
1128
+ throw new ManagedPiSessionError('sdk_resume_checkpoint_scope_mismatch');
1129
+ const authorizedScope = this.input.resumeAuthorization?.scope;
1130
+ if (authorizedScope &&
1131
+ canonicalJson(scope) !== canonicalJson(authorizedScope))
1132
+ throw new ManagedPiSessionError('sdk_resume_authorized_scope_mismatch');
1133
+ if (this.resumeCheckpointScope &&
1134
+ canonicalJson(scope) !== canonicalJson(this.resumeCheckpointScope))
1135
+ throw new ManagedPiSessionError('sdk_resume_checkpoint_scope_conflict');
1136
+ this.resumeCheckpointScope = scope;
1137
+ }
1138
+ recordInitialResumeCheckpoint() {
1139
+ if (!this.initialized || this.disposed)
1140
+ return false;
1141
+ return this.appendResumeCheckpoint();
1142
+ }
1143
+ appendResumeCheckpoint() {
1144
+ if (this.disposed ||
1145
+ !this.input.journal.healthy ||
1146
+ !this.resumeCheckpointScope)
1147
+ return false;
1148
+ const hostIdentity = managedPiHostIdentity();
1149
+ if (!hostIdentity)
1150
+ return false;
1151
+ const revision = this.input.fence.checkpointRevision;
1152
+ const snapshot = this.snapshot();
1153
+ if (snapshot.ingress.state !== 'open' ||
1154
+ !snapshot.ingress.healthy ||
1155
+ !isCompleteIdleSnapshot(snapshot))
1156
+ return false;
1157
+ const file = this.session.sessionFile;
1158
+ if (!file)
1159
+ return false;
1160
+ const transcript = hashSessionTranscript(file, snapshot.session.sessionId, this.input.runtime.cwd);
1161
+ if (!transcript || revision !== this.input.fence.checkpointRevision)
1162
+ return false;
1163
+ const finalSnapshot = this.snapshot();
1164
+ if (revision !== this.input.fence.checkpointRevision ||
1165
+ finalSnapshot.session.sessionId !== snapshot.session.sessionId ||
1166
+ finalSnapshot.session.sessionFile !== snapshot.session.sessionFile ||
1167
+ !isCompleteIdleSnapshot(finalSnapshot) ||
1168
+ finalSnapshot.ingress.state !== 'open' ||
1169
+ !finalSnapshot.ingress.healthy)
1170
+ return false;
1171
+ const registeredInputs = [...this.submittedInputs]
1172
+ .sort(([left], [right]) => left.localeCompare(right))
1173
+ .map(([requestId, inputDigest]) => ({
1174
+ request_id: requestId,
1175
+ input_digest: inputDigest,
1176
+ }));
1177
+ this.input.journal.append('sdk_resume_checkpoint', {
1178
+ checkpoint_schema: 'pappra/managed-pi-resume-checkpoint/1',
1179
+ session_id: finalSnapshot.session.sessionId,
1180
+ session_file: transcript.sessionFile,
1181
+ cwd: this.input.runtime.cwd,
1182
+ model: this.observe().native.modelObservation.model,
1183
+ process_pid: process.pid,
1184
+ host_identity: hostIdentity,
1185
+ execution_scope: this.resumeCheckpointScope,
1186
+ transcript_digest: transcript.transcriptDigest,
1187
+ snapshot: finalSnapshot,
1188
+ registered_inputs: registeredInputs,
1189
+ });
1190
+ return true;
1191
+ }
1192
+ async initializeInteractiveMode() {
1193
+ await this.input.interactiveMode.init();
1194
+ this.initialized = true;
1195
+ this.publishResourcesReady();
1196
+ this.interactiveRun = this.input.interactiveMode.run().then(() => {
1197
+ this.input.journal.append('sdk_interactive_mode_exited', {
1198
+ session_id: this.session.sessionId,
1199
+ });
1200
+ });
1201
+ // A caller can await runPromise; this handler prevents an early unhandled rejection.
1202
+ void this.interactiveRun.catch(() => undefined);
1203
+ }
1204
+ get runPromise() {
1205
+ return this.interactiveRun;
1206
+ }
1207
+ /** Admits one durable semantic input, without waiting for or claiming model success. */
1208
+ submitInput(requestId, text) {
1209
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(requestId) ||
1210
+ !text ||
1211
+ text.length > 128 * 1024)
1212
+ throw new ManagedPiSessionError('sdk_input_invalid');
1213
+ if (this.disposed ||
1214
+ this.input.fence.fenced ||
1215
+ this.input.canExecute?.() === false)
1216
+ throw new ManagedPiSessionError('sdk_runtime_ingress_fenced');
1217
+ const inputDigest = createHash('sha256').update(text).digest('hex');
1218
+ const prior = this.submittedInputs.get(requestId);
1219
+ if (prior && prior !== inputDigest)
1220
+ throw new ManagedPiSessionError('sdk_input_id_conflict');
1221
+ if (prior)
1222
+ return {
1223
+ status: 'submitted',
1224
+ request_id: requestId,
1225
+ duplicate: true,
1226
+ session_id: this.session.sessionId,
1227
+ };
1228
+ if (this.submittedInputs.size >= 256)
1229
+ throw new ManagedPiSessionError('sdk_input_limit');
1230
+ this.input.fence.invalidateResumeCheckpoint('sdk_control_input_reserved');
1231
+ this.input.journal.append('sdk_control_input_reserved', {
1232
+ request_id: requestId,
1233
+ input_digest: inputDigest,
1234
+ session_id: this.session.sessionId,
1235
+ });
1236
+ this.submittedInputs.set(requestId, inputDigest);
1237
+ const target = this.session;
1238
+ const prompt = target['prompt'];
1239
+ if (typeof prompt !== 'function')
1240
+ throw new ManagedPiSessionError('sdk_input_api_unavailable');
1241
+ void Promise.resolve(Reflect.apply(prompt, this.session, [
1242
+ text,
1243
+ { streamingBehavior: 'followUp' },
1244
+ ])).catch(() => {
1245
+ try {
1246
+ this.input.journal.append('sdk_control_input_outcome_unknown', {
1247
+ request_id: requestId,
1248
+ });
1249
+ }
1250
+ catch {
1251
+ this.input.fence.fence('input_outcome_journal_failed');
1252
+ }
1253
+ });
1254
+ return {
1255
+ status: 'submitted',
1256
+ request_id: requestId,
1257
+ duplicate: false,
1258
+ session_id: this.session.sessionId,
1259
+ };
1260
+ }
1261
+ async importHandoffContext(content) {
1262
+ if (!content ||
1263
+ content.length > 1024 * 1024 ||
1264
+ this.disposed ||
1265
+ this.input.fence.fenced)
1266
+ throw new ManagedPiSessionError('sdk_handoff_context_invalid');
1267
+ const target = this.session;
1268
+ const send = target['sendCustomMessage'];
1269
+ if (typeof send !== 'function')
1270
+ throw new ManagedPiSessionError('sdk_custom_context_unavailable');
1271
+ await Reflect.apply(send, this.session, [
1272
+ { customType: 'pappra.handoff-context.v1', content, display: true },
1273
+ { triggerTurn: false },
1274
+ ]);
1275
+ }
1276
+ /** The app binds logical assignment identity; native UUID/cwd/model remain SDK observations. */
1277
+ bindNativeIdentity(identity) {
1278
+ if (this.nativeIdentity?.agentId &&
1279
+ this.nativeIdentity.agentId !== identity.agentId)
1280
+ throw new ManagedPiSessionError('sdk_native_identity_conflict');
1281
+ this.nativeIdentity = identity;
1282
+ this.publishResourcesReady();
1283
+ }
1284
+ /** Resource availability after real initialization, without a fabricated model turn. */
1285
+ get metadataFile() {
1286
+ return this.input.metadataPath ?? null;
1287
+ }
1288
+ publishResourcesReady(force = false) {
1289
+ if (!this.initialized ||
1290
+ this.disposed ||
1291
+ !this.input.metadataPath ||
1292
+ !this.nativeIdentity?.agentId ||
1293
+ !this.nativeIdentity.configurationId)
1294
+ return false;
1295
+ const native = this.session;
1296
+ const services = this.input.resources?.();
1297
+ if (!native.getAllTools ||
1298
+ !native.getActiveToolNames ||
1299
+ !native.sessionManager ||
1300
+ !services?.resourceLoader ||
1301
+ !services.settingsManager)
1302
+ return false;
1303
+ const skills = services.resourceLoader.getSkills();
1304
+ const all = native.getAllTools().map((tool) => tool.name);
1305
+ const active = [...native.getActiveToolNames()];
1306
+ const skillRows = skills.skills.map((entry) => {
1307
+ if (!isRecord(entry) ||
1308
+ typeof entry['name'] !== 'string' ||
1309
+ typeof entry['filePath'] !== 'string')
1310
+ throw new ManagedPiSessionError('sdk_skill_inventory_invalid');
1311
+ return {
1312
+ name: entry['name'],
1313
+ entry: entry['filePath'],
1314
+ availability: 'available',
1315
+ body_loaded: 'not_observed',
1316
+ };
1317
+ });
1318
+ if (all.length > 2048 ||
1319
+ active.length > 2048 ||
1320
+ skillRows.length > 1024 ||
1321
+ all.some((name) => typeof name !== 'string' || !name || name.length > 256) ||
1322
+ new Set(all).size !== all.length ||
1323
+ new Set(active).size !== active.length ||
1324
+ active.some((name) => !all.includes(name)))
1325
+ throw new ManagedPiSessionError('sdk_resource_inventory_invalid');
1326
+ const snapshot = this.snapshot();
1327
+ const { queues, work, extensionDialogs, builtInUiApprovals } = snapshot.ownedRuntime;
1328
+ const observed = this.observe();
1329
+ const payload = {
1330
+ schema: SESSION_SCHEMA,
1331
+ type: 'resources_ready',
1332
+ at: new Date().toISOString(),
1333
+ session_id: this.session.sessionId,
1334
+ cwd: this.input.runtime.cwd,
1335
+ native_session_file: this.session.sessionFile ?? null,
1336
+ native_header: native.sessionManager.getHeader(),
1337
+ phase: 'interactive_mode_init_complete',
1338
+ sdk: { package: '@earendil-works/pi-coding-agent', version: SDK_VERSION },
1339
+ native_identity: {
1340
+ agent_id: this.nativeIdentity.agentId,
1341
+ harness: 'pi',
1342
+ surface: this.nativeIdentity.surface,
1343
+ configuration_id: this.nativeIdentity.configurationId,
1344
+ },
1345
+ model: {
1346
+ provider: observed.native.modelObservation.provider,
1347
+ id: observed.native.modelObservation.model,
1348
+ thinking_level: observed.native.modelObservation.reasoning,
1349
+ },
1350
+ configuration: {
1351
+ configuration_id: this.nativeIdentity.configurationId,
1352
+ project_trusted: services.settingsManager.isProjectTrusted(),
1353
+ },
1354
+ resource_inventory: {
1355
+ completeness: skills.diagnostics.some((value) => isRecord(value) && value['type'] === 'error')
1356
+ ? 'unknown'
1357
+ : 'complete',
1358
+ source: 'pi_1_4_runtime_apis',
1359
+ tools: { all_names: all, active_names: active },
1360
+ skills: skillRows,
1361
+ },
1362
+ owned_runtime: {
1363
+ scope: 'pinned_pi_1_4_owned_runtime',
1364
+ queues_completeness: queues.completeness,
1365
+ pending_message_count: queues.pendingMessageCount,
1366
+ work_observation: work.observation,
1367
+ is_idle: work.isIdle,
1368
+ is_streaming: work.isStreaming,
1369
+ is_compacting: work.isCompacting,
1370
+ is_bash_running: work.isBashRunning,
1371
+ has_pending_bash_messages: work.hasPendingBashMessages,
1372
+ pending_tool_calls: work.pendingToolCalls,
1373
+ extension_dialogs_completeness: extensionDialogs.completeness,
1374
+ pending_extension_dialogs: extensionDialogs.pending.length,
1375
+ built_in_ui_approvals_observation: builtInUiApprovals.observation,
1376
+ uncovered_approval_paths: builtInUiApprovals.uncoveredPaths,
1377
+ },
1378
+ };
1379
+ const fingerprint = createHash('sha256')
1380
+ .update(JSON.stringify({ ...payload, at: null }))
1381
+ .digest('hex');
1382
+ if (!force && fingerprint === this.resourceFingerprint)
1383
+ return true;
1384
+ native.sessionManager.appendCustomEntry('pappra.resources_ready.v1', payload);
1385
+ if (!appendPiMetadata(this.input.metadataPath, payload)) {
1386
+ this.input.fence.fence('resources_ready_metadata_write_failed');
1387
+ throw new ManagedPiSessionError('sdk_resources_ready_publication_failed');
1388
+ }
1389
+ this.resourceFingerprint = fingerprint;
1390
+ this.input.journal.append('sdk_resources_ready', {
1391
+ session_id: this.session.sessionId,
1392
+ fingerprint,
1393
+ });
1394
+ this.notifyLifecycleListeners({ type: 'resources_ready' });
1395
+ return true;
1396
+ }
1397
+ }
1398
+ /**
1399
+ * Start Pi 1.4.0's controller-owned SDK runtime and the native interactive TUI.
1400
+ * The module path is explicit; this function never falls back to a global/installed SDK.
1401
+ */
1402
+ export async function startManagedPiSdkSession(options) {
1403
+ if (!path.isAbsolute(options.cwd))
1404
+ throw new ManagedPiSessionError('cwd_must_be_absolute');
1405
+ if (options.sessionId !== undefined &&
1406
+ !SESSION_ID_PATTERN.test(options.sessionId))
1407
+ throw new ManagedPiSessionError('session_id_invalid');
1408
+ const resumeAuthorization = options.resumeAuthorization;
1409
+ if (resumeAuthorization) {
1410
+ if (!options.sessionId ||
1411
+ options.sessionId !== resumeAuthorization.sessionId ||
1412
+ path.resolve(options.cwd) !== path.resolve(resumeAuthorization.cwd) ||
1413
+ !options.executionScope ||
1414
+ canonicalJson(options.executionScope) !==
1415
+ canonicalJson(resumeAuthorization.scope) ||
1416
+ options.bootstrapExtensionPath)
1417
+ throw new ManagedPiSessionError('sdk_resume_authorization_mismatch');
1418
+ assertManagedPiSdkResumeAuthorization(resumeAuthorization, {
1419
+ sessionId: options.sessionId,
1420
+ cwd: options.cwd,
1421
+ });
1422
+ }
1423
+ if (!options.model.trim())
1424
+ throw new ManagedPiSessionError('model_required');
1425
+ const quiesceTimeoutMs = options.quiesceTimeoutMs ?? 5_000;
1426
+ if (!Number.isSafeInteger(quiesceTimeoutMs) ||
1427
+ quiesceTimeoutMs <= 0 ||
1428
+ quiesceTimeoutMs > 30_000)
1429
+ throw new ManagedPiSessionError('quiesce_timeout_invalid');
1430
+ const sdk = await loadSdkModule(options.sdkModulePath);
1431
+ if (options.bootstrapExtensionPath &&
1432
+ !path.isAbsolute(options.bootstrapExtensionPath))
1433
+ throw new ManagedPiSessionError('bootstrap_extension_path_must_be_absolute');
1434
+ if (sdk.VERSION !== SDK_VERSION)
1435
+ throw new ManagedPiSessionError('unsupported_sdk_version', `Expected @earendil-works/pi-coding-agent ${SDK_VERSION}; received ${String(sdk.VERSION)}`);
1436
+ verifySdkPrivateSourceHashes(options.sdkModulePath);
1437
+ const compaction = (await import(pathToFileURL(path.join(path.dirname(options.sdkModulePath), 'core', 'compaction', 'compaction.js')).href));
1438
+ if (typeof compaction.prepareCompaction !== 'function')
1439
+ throw new ManagedPiSessionError('sdk_compaction_preparation_unavailable');
1440
+ if (options.sessionId && !resumeAuthorization) {
1441
+ const existingSessions = await sdk.SessionManager.list(options.cwd);
1442
+ if (existingSessions.some((session) => session.id === options.sessionId))
1443
+ throw new ManagedPiSessionError('session_id_already_exists');
1444
+ }
1445
+ for (const name of [
1446
+ 'createAgentSessionServices',
1447
+ 'createAgentSessionFromServices',
1448
+ 'createAgentSessionRuntime',
1449
+ 'createCodemodeExtension',
1450
+ 'createToolSearchExtension',
1451
+ 'createMcpExtension',
1452
+ 'InteractiveMode',
1453
+ ]) {
1454
+ if (!(name in sdk))
1455
+ throw new ManagedPiSessionError('sdk_required_export_missing', name);
1456
+ }
1457
+ const artifacts = createManagedPiJournalArtifacts(options.journalPath);
1458
+ const { journal, metadataPath } = artifacts;
1459
+ const fence = new ManagedPiSdkIngressFence(journal);
1460
+ let controller;
1461
+ let runtime;
1462
+ let interactiveMode;
1463
+ let currentServices;
1464
+ try {
1465
+ journal.append('sdk_session_start_requested', {
1466
+ model: options.model,
1467
+ cwd: options.cwd,
1468
+ sdk_version: sdk.VERSION,
1469
+ });
1470
+ const agentDir = sdk.getAgentDir();
1471
+ let initialSessionManager;
1472
+ if (resumeAuthorization) {
1473
+ if (typeof sdk.SessionManager.open !== 'function')
1474
+ throw new ManagedPiSessionError('sdk_session_resume_api_unavailable');
1475
+ assertManagedPiSdkResumeAuthorization(resumeAuthorization, {
1476
+ sessionId: options.sessionId,
1477
+ cwd: options.cwd,
1478
+ });
1479
+ const pinnedTranscript = hashSessionTranscript(resumeAuthorization.sessionFile, resumeAuthorization.sessionId, resumeAuthorization.cwd);
1480
+ if (!pinnedTranscript ||
1481
+ pinnedTranscript.sessionFile !==
1482
+ path.resolve(resumeAuthorization.sessionFile) ||
1483
+ pinnedTranscript.transcriptDigest !==
1484
+ resumeAuthorization.transcriptDigest)
1485
+ throw new ManagedPiSessionError('sdk_resume_transcript_changed');
1486
+ initialSessionManager = sdk.SessionManager.open(resumeAuthorization.sessionFile);
1487
+ }
1488
+ else {
1489
+ initialSessionManager = sdk.SessionManager.create(options.cwd, undefined, options.sessionId ? { id: options.sessionId } : undefined);
1490
+ }
1491
+ const createRuntime = async (runtimeOptions) => {
1492
+ const services = await sdk.createAgentSessionServices({
1493
+ cwd: runtimeOptions.cwd,
1494
+ agentDir: runtimeOptions.agentDir,
1495
+ resourceLoaderOptions: {
1496
+ ...(options.bootstrapExtensionPath
1497
+ ? { additionalExtensionPaths: [options.bootstrapExtensionPath] }
1498
+ : {}),
1499
+ extensionFactories: [
1500
+ sdk.createCodemodeExtension(),
1501
+ sdk.createToolSearchExtension(),
1502
+ sdk.createMcpExtension(),
1503
+ createManagedPiSdkLifecycleExtension(fence, journal, metadataPath, options.canExecute),
1504
+ ],
1505
+ },
1506
+ });
1507
+ currentServices = services;
1508
+ if (options.bootstrapExtensionPath) {
1509
+ const loaded = services.resourceLoader?.getExtensions();
1510
+ if (!loaded ||
1511
+ !loaded.extensions.some((entry) => path.resolve(entry.path) ===
1512
+ path.resolve(options.bootstrapExtensionPath)) ||
1513
+ loaded.errors.length > 0)
1514
+ throw new ManagedPiSessionError('bootstrap_extension_not_loaded');
1515
+ }
1516
+ const model = services.modelRuntime.getModel('openrouter', options.model);
1517
+ if (!model)
1518
+ throw new ManagedPiSessionError('openrouter_model_unavailable', options.model);
1519
+ const created = await sdk.createAgentSessionFromServices({
1520
+ services,
1521
+ sessionManager: runtimeOptions.sessionManager,
1522
+ model,
1523
+ ...(options.thinkingLevel
1524
+ ? { thinkingLevel: options.thinkingLevel }
1525
+ : {}),
1526
+ sessionStartEvent: runtimeOptions.sessionStartEvent,
1527
+ });
1528
+ return {
1529
+ ...created,
1530
+ services,
1531
+ diagnostics: [...services.diagnostics],
1532
+ };
1533
+ };
1534
+ const createdRuntime = await sdk.createAgentSessionRuntime(createRuntime, {
1535
+ cwd: options.cwd,
1536
+ agentDir,
1537
+ sessionManager: initialSessionManager,
1538
+ });
1539
+ runtime = createdRuntime;
1540
+ if (resumeAuthorization) {
1541
+ assertManagedPiSdkResumeAuthorization(resumeAuthorization, {
1542
+ sessionId: options.sessionId,
1543
+ cwd: options.cwd,
1544
+ });
1545
+ const reopenedSession = createdRuntime.session;
1546
+ const reopenedFile = reopenedSession.sessionFile;
1547
+ const reopenedTranscript = reopenedFile
1548
+ ? hashSessionTranscript(reopenedFile, reopenedSession.sessionId, createdRuntime.cwd)
1549
+ : null;
1550
+ if (reopenedSession.sessionId !== resumeAuthorization.sessionId ||
1551
+ path.resolve(createdRuntime.cwd) !==
1552
+ path.resolve(resumeAuthorization.cwd) ||
1553
+ !reopenedFile ||
1554
+ path.resolve(reopenedFile) !==
1555
+ path.resolve(resumeAuthorization.sessionFile) ||
1556
+ !reopenedTranscript ||
1557
+ reopenedTranscript.transcriptDigest !==
1558
+ resumeAuthorization.transcriptDigest)
1559
+ throw new ManagedPiSessionError('sdk_resume_opened_session_mismatch');
1560
+ }
1561
+ const originalSetRebindSession = createdRuntime.setRebindSession.bind(createdRuntime);
1562
+ createdRuntime.setRebindSession = (callback) => originalSetRebindSession(callback
1563
+ ? async (session) => {
1564
+ controller?.replaceSession(session);
1565
+ await callback(session);
1566
+ }
1567
+ : undefined);
1568
+ interactiveMode = new sdk.InteractiveMode(createdRuntime);
1569
+ controller = new ManagedPiSdkSessionController({
1570
+ ...(options.canExecute ? { canExecute: options.canExecute } : {}),
1571
+ ...(resumeAuthorization ? { resumeAuthorization } : {}),
1572
+ runtime: createdRuntime,
1573
+ interactiveMode,
1574
+ fence,
1575
+ journal,
1576
+ model: options.model,
1577
+ nativeIdentity: options.nativeIdentity,
1578
+ quiesceTimeoutMs: quiesceTimeoutMs,
1579
+ ...(options.executionScope
1580
+ ? {
1581
+ executionScope: resumeAuthorization?.scope ?? options.executionScope,
1582
+ }
1583
+ : {}),
1584
+ metadataPath,
1585
+ resources: () => currentServices,
1586
+ canCompact: () => {
1587
+ const session = createdRuntime.session;
1588
+ const getSettings = currentServices?.settingsManager?.getCompactionSettings;
1589
+ if (!session.sessionManager || !getSettings)
1590
+ return false;
1591
+ return (compaction.prepareCompaction(session.sessionManager.getBranch(), getSettings.call(currentServices.settingsManager, session.model)) !== undefined);
1592
+ },
1593
+ });
1594
+ await options.beforeInteractiveStart?.(controller);
1595
+ await controller.initializeInteractiveMode();
1596
+ journal.append('sdk_session_ready', {
1597
+ session_id: createdRuntime.session.sessionId,
1598
+ session_file: createdRuntime.session.sessionFile ?? null,
1599
+ cwd: options.cwd,
1600
+ sdk_version: sdk.VERSION,
1601
+ terminal_ui_render: 'not_observed',
1602
+ });
1603
+ return controller;
1604
+ }
1605
+ catch (error) {
1606
+ fence.fence('sdk_start_failed');
1607
+ if (controller) {
1608
+ try {
1609
+ await controller.cleanupAfterStartupFailure();
1610
+ }
1611
+ catch {
1612
+ // The original startup error remains primary; do not claim cleanup succeeded.
1613
+ }
1614
+ }
1615
+ else {
1616
+ try {
1617
+ interactiveMode?.stop();
1618
+ }
1619
+ catch {
1620
+ // Best effort when InteractiveMode construction was partial.
1621
+ }
1622
+ let disposed = runtime === undefined;
1623
+ if (runtime)
1624
+ disposed = await waitWithin(runtime.dispose(), quiesceTimeoutMs);
1625
+ try {
1626
+ journal.append('sdk_start_failure_cleanup', {
1627
+ runtime_created: runtime !== undefined,
1628
+ runtime_disposed: disposed,
1629
+ });
1630
+ if (disposed)
1631
+ journal.close();
1632
+ }
1633
+ catch {
1634
+ // Preserve the original startup error and leave any uncertain journal open.
1635
+ }
1636
+ }
1637
+ throw error;
1638
+ }
1639
+ }