@neuralnomads/codenomad-dev 0.20.0-dev-20260911-ea37f97b → 0.20.0-dev-20260917-aa51cbb9

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 (191) hide show
  1. package/README.md +2 -2
  2. package/dist/api-types.js.map +1 -1
  3. package/dist/opencode/automation-plugin.test.js +32 -38
  4. package/dist/opencode/automation-plugin.test.js.map +1 -1
  5. package/dist/opencode/compatibility/contract-fixture.js +15 -0
  6. package/dist/opencode/compatibility/contract-fixture.js.map +1 -0
  7. package/dist/opencode/compatibility/events.js +9 -0
  8. package/dist/opencode/compatibility/events.js.map +1 -0
  9. package/dist/opencode/compatibility/location.js +89 -0
  10. package/dist/opencode/compatibility/location.js.map +1 -0
  11. package/dist/opencode/compatibility/location.test.js +92 -0
  12. package/dist/opencode/compatibility/location.test.js.map +1 -0
  13. package/dist/opencode/compatibility/negotiate.js +73 -0
  14. package/dist/opencode/compatibility/negotiate.js.map +1 -0
  15. package/dist/opencode/compatibility/negotiate.test.js +61 -0
  16. package/dist/opencode/compatibility/negotiate.test.js.map +1 -0
  17. package/dist/opencode/compatibility/proxy-locations.js +137 -0
  18. package/dist/opencode/compatibility/proxy-locations.js.map +1 -0
  19. package/dist/opencode/compatibility/proxy-locations.test.js +42 -0
  20. package/dist/opencode/compatibility/proxy-locations.test.js.map +1 -0
  21. package/dist/opencode/compatibility/proxy.js +60 -0
  22. package/dist/opencode/compatibility/proxy.js.map +1 -0
  23. package/dist/opencode/compatibility/proxy.test.js +47 -0
  24. package/dist/opencode/compatibility/proxy.test.js.map +1 -0
  25. package/dist/opencode/compatibility/requests.js +54 -0
  26. package/dist/opencode/compatibility/requests.js.map +1 -0
  27. package/dist/opencode/compatibility/runtime.js +26 -0
  28. package/dist/opencode/compatibility/runtime.js.map +1 -0
  29. package/dist/opencode/compatibility/transport.js +112 -0
  30. package/dist/opencode/compatibility/transport.js.map +1 -0
  31. package/dist/opencode/compatibility/transport.test.js +83 -0
  32. package/dist/opencode/compatibility/transport.test.js.map +1 -0
  33. package/dist/opencode/session-pruning/location.js +16 -0
  34. package/dist/opencode/session-pruning/location.js.map +1 -0
  35. package/dist/opencode/session-pruning/package.test.js +37 -0
  36. package/dist/opencode/session-pruning/package.test.js.map +1 -0
  37. package/dist/opencode/session-pruning/plugin.js +5 -2
  38. package/dist/opencode/session-pruning/plugin.js.map +1 -1
  39. package/dist/opencode/session-pruning/preview-store.js +3 -2
  40. package/dist/opencode/session-pruning/preview-store.js.map +1 -1
  41. package/dist/opencode/session-pruning/pruning.test.js +7 -5
  42. package/dist/opencode/session-pruning/pruning.test.js.map +1 -1
  43. package/dist/opencode/session-pruning/service.js +5 -4
  44. package/dist/opencode/session-pruning/service.js.map +1 -1
  45. package/dist/opencode/session-pruning/service.test.js +65 -53
  46. package/dist/opencode/session-pruning/service.test.js.map +1 -1
  47. package/dist/permissions/opencode-replier.js +2 -2
  48. package/dist/permissions/opencode-replier.js.map +1 -1
  49. package/dist/permissions/opencode-replier.test.js +1 -1
  50. package/dist/permissions/opencode-replier.test.js.map +1 -1
  51. package/dist/permissions/opencode-yolo-metadata.js.map +1 -1
  52. package/dist/permissions/opencode-yolo-metadata.test.js.map +1 -1
  53. package/dist/plugins/session-pruning/plugin.mjs +422 -271
  54. package/dist/server/__tests__/instance-proxy-legacy.test.js +111 -0
  55. package/dist/server/__tests__/instance-proxy-legacy.test.js.map +1 -0
  56. package/dist/server/__tests__/instance-proxy.test.js +95 -34
  57. package/dist/server/__tests__/instance-proxy.test.js.map +1 -1
  58. package/dist/server/http-server.js +147 -237
  59. package/dist/server/http-server.js.map +1 -1
  60. package/dist/server/routes/session-pruning.js +4 -2
  61. package/dist/server/routes/session-pruning.js.map +1 -1
  62. package/dist/server/routes/session-pruning.test.js +16 -2
  63. package/dist/server/routes/session-pruning.test.js.map +1 -1
  64. package/dist/server/routes/worktrees.js +1 -0
  65. package/dist/server/routes/worktrees.js.map +1 -1
  66. package/dist/server/routes/worktrees.test.js +54 -1
  67. package/dist/server/routes/worktrees.test.js.map +1 -1
  68. package/dist/workspaces/host-opencode-service.test.js +56 -7
  69. package/dist/workspaces/host-opencode-service.test.js.map +1 -1
  70. package/dist/workspaces/instance-client.js.map +1 -1
  71. package/dist/workspaces/instance-events.js +71 -57
  72. package/dist/workspaces/instance-events.js.map +1 -1
  73. package/dist/workspaces/instance-events.test.js +122 -9
  74. package/dist/workspaces/instance-events.test.js.map +1 -1
  75. package/dist/workspaces/manager.js +24 -39
  76. package/dist/workspaces/manager.js.map +1 -1
  77. package/dist/workspaces/manager.test.js +43 -7
  78. package/dist/workspaces/manager.test.js.map +1 -1
  79. package/dist/workspaces/opencode-cli-service.js +56 -29
  80. package/dist/workspaces/opencode-cli-service.js.map +1 -1
  81. package/dist/workspaces/opencode-connection-generation.test.js +57 -0
  82. package/dist/workspaces/opencode-connection-generation.test.js.map +1 -0
  83. package/dist/workspaces/opencode-service.js +70 -16
  84. package/dist/workspaces/opencode-service.js.map +1 -1
  85. package/dist/workspaces/opencode-service.test.js +3 -4
  86. package/dist/workspaces/opencode-service.test.js.map +1 -1
  87. package/dist/workspaces/worktree-session-evacuation.js +31 -11
  88. package/dist/workspaces/worktree-session-evacuation.js.map +1 -1
  89. package/dist/workspaces/worktree-session-evacuation.test.js +211 -0
  90. package/dist/workspaces/worktree-session-evacuation.test.js.map +1 -1
  91. package/dist/workspaces/wsl-opencode-service.test.js +64 -24
  92. package/dist/workspaces/wsl-opencode-service.test.js.map +1 -1
  93. package/package.json +3 -3
  94. package/public/assets/App-BROhwPo9.js +54 -0
  95. package/public/assets/App-BROhwPo9.js.map +1 -0
  96. package/public/assets/{FilesTab-CRpGDs1B.js → FilesTab-DY3Vr-3r.js} +3 -3
  97. package/public/assets/{FilesTab-CRpGDs1B.js.map → FilesTab-DY3Vr-3r.js.map} +1 -1
  98. package/public/assets/{GitChangesTab-DxFebTiq.js → GitChangesTab-BD6CJngE.js} +3 -3
  99. package/public/assets/{GitChangesTab-DxFebTiq.js.map → GitChangesTab-BD6CJngE.js.map} +1 -1
  100. package/public/assets/{SplitFilePanel-DgTMrrO-.js → SplitFilePanel-BdY6d_6m.js} +2 -2
  101. package/public/assets/{SplitFilePanel-DgTMrrO-.js.map → SplitFilePanel-BdY6d_6m.js.map} +1 -1
  102. package/public/assets/StatusTab-C-_2yaPa.js +2 -0
  103. package/public/assets/StatusTab-C-_2yaPa.js.map +1 -0
  104. package/public/assets/{align-justify-v_yPXSys.js → align-justify-DI8VTZ4G.js} +2 -2
  105. package/public/assets/{align-justify-v_yPXSys.js.map → align-justify-DI8VTZ4G.js.map} +1 -1
  106. package/public/assets/{bundle-full-HzL_Jtqk.js → bundle-full-D_M65Eb_.js} +2 -2
  107. package/public/assets/{bundle-full-HzL_Jtqk.js.map → bundle-full-D_M65Eb_.js.map} +1 -1
  108. package/public/assets/{client-state-CF-PJ0GG.js → client-state-CWCzCOz6.js} +2 -2
  109. package/public/assets/{client-state-CF-PJ0GG.js.map → client-state-CWCzCOz6.js.map} +1 -1
  110. package/public/assets/{diff-viewer-BFDQBDVy.js → diff-viewer-CGPsYEr3.js} +2 -2
  111. package/public/assets/{diff-viewer-BFDQBDVy.js.map → diff-viewer-CGPsYEr3.js.map} +1 -1
  112. package/public/assets/index-B8-lAqjr.js +2 -0
  113. package/public/assets/index-B8-lAqjr.js.map +1 -0
  114. package/public/assets/index-BeUewQym.js +2 -0
  115. package/public/assets/index-BeUewQym.js.map +1 -0
  116. package/public/assets/index-BxRFHpJd.js +2 -0
  117. package/public/assets/{index-D2UHSOG5.js.map → index-BxRFHpJd.js.map} +1 -1
  118. package/public/assets/index-Bzv1kU-S.js +2 -0
  119. package/public/assets/{index-CTKYD_VJ.js.map → index-Bzv1kU-S.js.map} +1 -1
  120. package/public/assets/index-C9tI4CRG.js +2 -0
  121. package/public/assets/index-C9tI4CRG.js.map +1 -0
  122. package/public/assets/index-CGYHFZ6F.js +2 -0
  123. package/public/assets/index-CGYHFZ6F.js.map +1 -0
  124. package/public/assets/{index-CHYmKl10.js → index-CMEAjBFp.js} +2 -2
  125. package/public/assets/index-CMEAjBFp.js.map +1 -0
  126. package/public/assets/index-Cazypvyc.js +2 -0
  127. package/public/assets/index-Cazypvyc.js.map +1 -0
  128. package/public/assets/index-CwqzofWP.js +2 -0
  129. package/public/assets/index-CwqzofWP.js.map +1 -0
  130. package/public/assets/index-Da9yOa6q.js +2 -0
  131. package/public/assets/index-Da9yOa6q.js.map +1 -0
  132. package/public/assets/{index-RK9gf0uk.js → index-Ddjkirek.js} +2 -2
  133. package/public/assets/{index-RK9gf0uk.js.map → index-Ddjkirek.js.map} +1 -1
  134. package/public/assets/index-HHgvEOAH.js +3 -0
  135. package/public/assets/index-HHgvEOAH.js.map +1 -0
  136. package/public/assets/index-MeFffHqb.css +1 -0
  137. package/public/assets/loading-CdhHiSED.css +1 -0
  138. package/public/assets/{loading-BSB82snY.js → loading-Gqh6NPUr.js} +2 -2
  139. package/public/assets/{loading-BSB82snY.js.map → loading-Gqh6NPUr.js.map} +1 -1
  140. package/public/assets/main-Cbx7KdTr.js +38 -0
  141. package/public/assets/main-Cbx7KdTr.js.map +1 -0
  142. package/public/assets/{markdown-DtidoL2Z.js → markdown-BWnjyvnX.js} +4 -4
  143. package/public/assets/{markdown-DtidoL2Z.js.map → markdown-BWnjyvnX.js.map} +1 -1
  144. package/public/assets/monaco-viewer-BEJIiZWL.js +27 -0
  145. package/public/assets/monaco-viewer-BEJIiZWL.js.map +1 -0
  146. package/public/assets/{startup-state-settings-card-BY741Rt0.js → startup-state-settings-card-C4E2Itop.js} +2 -2
  147. package/public/assets/{startup-state-settings-card-BY741Rt0.js.map → startup-state-settings-card-C4E2Itop.js.map} +1 -1
  148. package/public/assets/{terminal-square-BdC_jwm3.js → terminal-square-C3wutdV0.js} +2 -2
  149. package/public/assets/{terminal-square-BdC_jwm3.js.map → terminal-square-C3wutdV0.js.map} +1 -1
  150. package/public/assets/tool-call-DNsAQ_m4.js +60 -0
  151. package/public/assets/tool-call-DNsAQ_m4.js.map +1 -0
  152. package/public/assets/{unified-picker-C0eo1chJ.js → unified-picker-tB7y17Qz.js} +2 -2
  153. package/public/assets/unified-picker-tB7y17Qz.js.map +1 -0
  154. package/public/assets/{wrap-text-BuMtdvMx.js → wrap-text-CkBxzt04.js} +2 -2
  155. package/public/assets/{wrap-text-BuMtdvMx.js.map → wrap-text-CkBxzt04.js.map} +1 -1
  156. package/public/index.html +4 -4
  157. package/public/loading.html +5 -5
  158. package/public/sw.js +1 -1
  159. package/public/sw.js.map +1 -1
  160. package/public/assets/App-0-FcHzPS.js +0 -44
  161. package/public/assets/App-0-FcHzPS.js.map +0 -1
  162. package/public/assets/StatusTab-LKUA9cbH.js +0 -2
  163. package/public/assets/StatusTab-LKUA9cbH.js.map +0 -1
  164. package/public/assets/index-8iPlm2r0.js +0 -2
  165. package/public/assets/index-8iPlm2r0.js.map +0 -1
  166. package/public/assets/index-B185mC7m.js +0 -2
  167. package/public/assets/index-B185mC7m.js.map +0 -1
  168. package/public/assets/index-Bbj1xF8i.js +0 -2
  169. package/public/assets/index-Bbj1xF8i.js.map +0 -1
  170. package/public/assets/index-BsmFN3k1.js +0 -2
  171. package/public/assets/index-BsmFN3k1.js.map +0 -1
  172. package/public/assets/index-Bz_OQJYA.css +0 -1
  173. package/public/assets/index-C52CfqK3.js +0 -2
  174. package/public/assets/index-C52CfqK3.js.map +0 -1
  175. package/public/assets/index-CHYmKl10.js.map +0 -1
  176. package/public/assets/index-CTKYD_VJ.js +0 -2
  177. package/public/assets/index-D2UHSOG5.js +0 -2
  178. package/public/assets/index-DDoB_Z36.js +0 -2
  179. package/public/assets/index-DDoB_Z36.js.map +0 -1
  180. package/public/assets/index-DcwAtZw-.js +0 -3
  181. package/public/assets/index-DcwAtZw-.js.map +0 -1
  182. package/public/assets/index-DxUD3RYr.js +0 -2
  183. package/public/assets/index-DxUD3RYr.js.map +0 -1
  184. package/public/assets/loading-CmEVQgyj.css +0 -1
  185. package/public/assets/main-CZztwrfv.js +0 -48
  186. package/public/assets/main-CZztwrfv.js.map +0 -1
  187. package/public/assets/monaco-viewer-CpxDfB-6.js +0 -27
  188. package/public/assets/monaco-viewer-CpxDfB-6.js.map +0 -1
  189. package/public/assets/tool-call-DbtCsJ4x.js +0 -60
  190. package/public/assets/tool-call-DbtCsJ4x.js.map +0 -1
  191. package/public/assets/unified-picker-C0eo1chJ.js.map +0 -1
@@ -0,0 +1,47 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+ import { Readable } from "node:stream";
4
+ import { setImmediate } from "node:timers/promises";
5
+ import Fastify from "fastify";
6
+ import { OpenCodeSharedService } from "../../workspaces/opencode-service";
7
+ import { forwardRuntimeRequest } from "./proxy";
8
+ test("retiring the actual connection preserves complete and streamed native 401 envelopes", async () => {
9
+ for (const delayed of [false, true]) {
10
+ const upstream = Fastify();
11
+ const envelope = { _tag: "UnauthorizedError", message: "fixture" };
12
+ upstream.get("/api/denied", async (_request, reply) => {
13
+ reply.code(401);
14
+ if (!delayed)
15
+ return envelope;
16
+ return reply.type("application/json").send(Readable.from((async function* () {
17
+ yield '{"_tag":"UnauthorizedError",';
18
+ await setImmediate();
19
+ yield '"message":"fixture"}';
20
+ })()));
21
+ });
22
+ await upstream.listen({ host: "127.0.0.1", port: 0 });
23
+ const endpoint = { url: `http://127.0.0.1:${upstream.server.address().port}` };
24
+ const service = new OpenCodeSharedService();
25
+ await service.client({ kind: "lifecycle", identity: "fixture", lifecycle: { discover: async () => endpoint, ensure: async () => endpoint } });
26
+ const connection = await service.acquire();
27
+ const app = Fastify();
28
+ let releases = 0;
29
+ app.get("/fixture", (request, reply) => forwardRuntimeRequest({
30
+ request, reply, url: `${endpoint.url}/api/denied`, body: undefined, headers: {},
31
+ fetch: connection.fetch, invalidate: connection.invalidate, release: () => { releases++; },
32
+ }));
33
+ try {
34
+ const response = await app.inject("/fixture");
35
+ assert.equal(response.statusCode, 401);
36
+ assert.deepEqual(response.json(), envelope);
37
+ assert.equal(releases, 1);
38
+ assert.throws(connection.assertCurrent, /connection changed/);
39
+ }
40
+ finally {
41
+ await service.shutdown();
42
+ await app.close();
43
+ await upstream.close();
44
+ }
45
+ }
46
+ });
47
+ //# sourceMappingURL=proxy.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.test.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/proxy.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAA;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAA;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAA;AAE/C,IAAI,CAAC,qFAAqF,EAAE,KAAK,IAAI,EAAE;IACrG,KAAK,MAAM,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,QAAQ,GAAG,OAAO,EAAE,CAAA;QAC1B,MAAM,QAAQ,GAAG,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,SAAS,EAAE,CAAA;QAClE,QAAQ,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE;YACpD,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACf,IAAI,CAAC,OAAO;gBAAE,OAAO,QAAQ,CAAA;YAC7B,OAAO,KAAK,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,SAAS,CAAC;gBACvE,MAAM,8BAA8B,CAAA;gBACpC,MAAM,YAAY,EAAE,CAAA;gBACpB,MAAM,sBAAsB,CAAA;YAC9B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QACR,CAAC,CAAC,CAAA;QACF,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;QACrD,MAAM,QAAQ,GAAG,EAAE,GAAG,EAAE,oBAAqB,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAuB,CAAC,IAAI,EAAE,EAAE,CAAA;QACpG,MAAM,OAAO,GAAG,IAAI,qBAAqB,EAAE,CAAA;QAC3C,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAA;QAC7I,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;QAC1C,MAAM,GAAG,GAAG,OAAO,EAAE,CAAA;QACrB,IAAI,QAAQ,GAAG,CAAC,CAAA;QAChB,GAAG,CAAC,GAAG,CAAC,UAAU,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,qBAAqB,CAAC;YAC5D,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,QAAQ,CAAC,GAAG,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE;YAC/E,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,UAAU,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAA,CAAC,CAAC;SAC1F,CAAC,CAAC,CAAA;QACH,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;YAC7C,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,CAAA;YACtC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAA;YAC3C,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;YACzB,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,aAAa,EAAE,oBAAoB,CAAC,CAAA;QAC/D,CAAC;gBAAS,CAAC;YAAC,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC;YAAC,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;YAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAA;QAAC,CAAC;IACnF,CAAC;AACH,CAAC,CAAC,CAAA","sourcesContent":["import assert from \"node:assert/strict\"\nimport { test } from \"node:test\"\nimport { Readable } from \"node:stream\"\nimport { setImmediate } from \"node:timers/promises\"\nimport Fastify from \"fastify\"\nimport { OpenCodeSharedService } from \"../../workspaces/opencode-service\"\nimport { forwardRuntimeRequest } from \"./proxy\"\n\ntest(\"retiring the actual connection preserves complete and streamed native 401 envelopes\", async () => {\n for (const delayed of [false, true]) {\n const upstream = Fastify()\n const envelope = { _tag: \"UnauthorizedError\", message: \"fixture\" }\n upstream.get(\"/api/denied\", async (_request, reply) => {\n reply.code(401)\n if (!delayed) return envelope\n return reply.type(\"application/json\").send(Readable.from((async function* () {\n yield '{\"_tag\":\"UnauthorizedError\",'\n await setImmediate()\n yield '\"message\":\"fixture\"}'\n })()))\n })\n await upstream.listen({ host: \"127.0.0.1\", port: 0 })\n const endpoint = { url: `http://127.0.0.1:${(upstream.server.address() as { port: number }).port}` }\n const service = new OpenCodeSharedService()\n await service.client({ kind: \"lifecycle\", identity: \"fixture\", lifecycle: { discover: async () => endpoint, ensure: async () => endpoint } })\n const connection = await service.acquire()\n const app = Fastify()\n let releases = 0\n app.get(\"/fixture\", (request, reply) => forwardRuntimeRequest({\n request, reply, url: `${endpoint.url}/api/denied`, body: undefined, headers: {},\n fetch: connection.fetch, invalidate: connection.invalidate, release: () => { releases++ },\n }))\n try {\n const response = await app.inject(\"/fixture\")\n assert.equal(response.statusCode, 401)\n assert.deepEqual(response.json(), envelope)\n assert.equal(releases, 1)\n assert.throws(connection.assertCurrent, /connection changed/)\n } finally { await service.shutdown(); await app.close(); await upstream.close() }\n }\n})\n"]}
@@ -0,0 +1,54 @@
1
+ // Only translate consumed, reviewed operations. The external proxy allowlist is
2
+ // checked before reaching this module; translation never expands that allowlist.
3
+ export function legacyRequest(url, method, body) {
4
+ const original = url.pathname;
5
+ if (original === "/api/status" && method === "GET")
6
+ url.pathname = "/api/health";
7
+ if (/^\/api\/experimental\/session\/(?:stats|import|[^/]+\/(?:export|wait|instructions\/entries(?:\/[^/]+)?))$/.test(original)
8
+ || /^\/api\/experimental\/mcp\/[^/]+\/(?:connect|disconnect)$/.test(original)) {
9
+ url.pathname = original.replace("/experimental", "");
10
+ }
11
+ if (original === "/api/form" && method === "GET")
12
+ url.pathname = "/api/form/request";
13
+ if (/^\/api\/session\/[^/]+$/.test(original) && method === "PATCH") {
14
+ if (!body || typeof body.title !== "string" || Object.keys(body).some(key => key !== "title")) {
15
+ throw new Error("Earlier OpenCode V2 supports title-only session updates");
16
+ }
17
+ url.pathname += "/rename";
18
+ method = "POST";
19
+ }
20
+ if (/^\/api\/session\/[^/]+\/fork$/.test(original) && method === "POST") {
21
+ if (body?.before !== undefined && typeof body.before !== "string")
22
+ throw new Error("Invalid fork boundary");
23
+ body = { boundary: body?.before === undefined ? { type: "through" } : { type: "before", messageID: body.before } };
24
+ }
25
+ if (/^\/api\/session\/[^/]+\/command$/.test(original) && method === "POST" && body) {
26
+ const { name, ...rest } = body;
27
+ body = { ...rest, command: name };
28
+ }
29
+ if (/^\/api\/session\/[^/]+\/permission\/[^/]+\/reply$/.test(original) && method === "POST" && body) {
30
+ const { decision, ...rest } = body;
31
+ body = { ...rest, reply: decision };
32
+ }
33
+ if (/^\/api\/session\/[^/]+\/interrupt$/.test(original) && method === "POST" && url.searchParams.has("resume")) {
34
+ url.searchParams.set("continue", url.searchParams.get("resume"));
35
+ url.searchParams.delete("resume");
36
+ }
37
+ if (/^\/api\/session\/[^/]+\/inbox\/[^/]+$/.test(original) && method === "PATCH") {
38
+ if (body?.delivery !== "steer" && body?.delivery !== "queue")
39
+ throw new Error("Invalid inbox delivery");
40
+ url.pathname += `/${body.delivery}`;
41
+ method = "POST";
42
+ body = undefined;
43
+ }
44
+ if (/^\/api\/session\/[^/]+\/revert$/.test(original) && method === "DELETE") {
45
+ url.pathname += "/clear";
46
+ method = "POST";
47
+ }
48
+ if (/^\/api\/session\/[^/]+\/form\/[^/]+$/.test(original) && method === "DELETE") {
49
+ url.pathname += "/cancel";
50
+ method = "POST";
51
+ }
52
+ return { url, method, body };
53
+ }
54
+ //# sourceMappingURL=requests.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requests.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/requests.ts"],"names":[],"mappings":"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","sourcesContent":["// Only translate consumed, reviewed operations. The external proxy allowlist is\n// checked before reaching this module; translation never expands that allowlist.\nexport function legacyRequest(url: URL, method: string, body: Record<string, unknown> | undefined) {\n const original = url.pathname\n if (original === \"/api/status\" && method === \"GET\") url.pathname = \"/api/health\"\n if (/^\\/api\\/experimental\\/session\\/(?:stats|import|[^/]+\\/(?:export|wait|instructions\\/entries(?:\\/[^/]+)?))$/.test(original)\n || /^\\/api\\/experimental\\/mcp\\/[^/]+\\/(?:connect|disconnect)$/.test(original)) {\n url.pathname = original.replace(\"/experimental\", \"\")\n }\n if (original === \"/api/form\" && method === \"GET\") url.pathname = \"/api/form/request\"\n if (/^\\/api\\/session\\/[^/]+$/.test(original) && method === \"PATCH\") {\n if (!body || typeof body.title !== \"string\" || Object.keys(body).some(key => key !== \"title\")) {\n throw new Error(\"Earlier OpenCode V2 supports title-only session updates\")\n }\n url.pathname += \"/rename\"\n method = \"POST\"\n }\n if (/^\\/api\\/session\\/[^/]+\\/fork$/.test(original) && method === \"POST\") {\n if (body?.before !== undefined && typeof body.before !== \"string\") throw new Error(\"Invalid fork boundary\")\n body = { boundary: body?.before === undefined ? { type: \"through\" } : { type: \"before\", messageID: body.before } }\n }\n if (/^\\/api\\/session\\/[^/]+\\/command$/.test(original) && method === \"POST\" && body) {\n const { name, ...rest } = body\n body = { ...rest, command: name }\n }\n if (/^\\/api\\/session\\/[^/]+\\/permission\\/[^/]+\\/reply$/.test(original) && method === \"POST\" && body) {\n const { decision, ...rest } = body\n body = { ...rest, reply: decision }\n }\n if (/^\\/api\\/session\\/[^/]+\\/interrupt$/.test(original) && method === \"POST\" && url.searchParams.has(\"resume\")) {\n url.searchParams.set(\"continue\", url.searchParams.get(\"resume\")!)\n url.searchParams.delete(\"resume\")\n }\n if (/^\\/api\\/session\\/[^/]+\\/inbox\\/[^/]+$/.test(original) && method === \"PATCH\") {\n if (body?.delivery !== \"steer\" && body?.delivery !== \"queue\") throw new Error(\"Invalid inbox delivery\")\n url.pathname += `/${body.delivery}`\n method = \"POST\"\n body = undefined\n }\n if (/^\\/api\\/session\\/[^/]+\\/revert$/.test(original) && method === \"DELETE\") {\n url.pathname += \"/clear\"\n method = \"POST\"\n }\n if (/^\\/api\\/session\\/[^/]+\\/form\\/[^/]+$/.test(original) && method === \"DELETE\") {\n url.pathname += \"/cancel\"\n method = \"POST\"\n }\n return { url, method, body }\n}\n"]}
@@ -0,0 +1,26 @@
1
+ // Keep authenticated daemon metadata out of public endpoints and credentials.
2
+ const RUNTIME_IDENTITY = Symbol.for("codenomad.opencode.runtime");
3
+ export function rememberRuntime(endpoint, identity) {
4
+ Object.defineProperty(endpoint, RUNTIME_IDENTITY, { value: Object.freeze({ ...identity, contract: {} }) });
5
+ }
6
+ export function runtimeIdentity(endpoint) {
7
+ return endpoint[RUNTIME_IDENTITY];
8
+ }
9
+ export function contractProfile(identity) {
10
+ // Custom embedded/test lifecycles that don't negotiate use the pinned contract.
11
+ if (!identity)
12
+ return "modern";
13
+ if (identity.contract?.profile)
14
+ return identity.contract.profile;
15
+ if (/^2\.0\.[0-3]$/.test(identity.version))
16
+ return "legacy";
17
+ if (/^2\.0\.[45]$/.test(identity.version))
18
+ return "modern";
19
+ const beta = /^0\.0\.0-beta-(\d+)$/.exec(identity.version)?.[1];
20
+ const audited = new Set([18866, 18955, 18965, 18985, 18992, 18999, 19059, 19086,
21
+ 19124, 19129, 19133, 19135, 19151, 19157, 19187, 19192, 19215, 19213,
22
+ 19228, 19234, 19242, 19266, 19271, 19275, 19278, 19283, 19288, 19289,
23
+ 19296, 19365, 19378, 19381, 19398, 19419, 19422, 19425, 19500, 19507]);
24
+ return beta && audited.has(Number(beta)) ? "legacy" : "unknown";
25
+ }
26
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/runtime.ts"],"names":[],"mappings":"AAUA,8EAA8E;AAC9E,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAA;AACjE,MAAM,UAAU,eAAe,CAAC,QAAkB,EAAE,QAAyB;IAC3E,MAAM,CAAC,cAAc,CAAC,QAAQ,EAAE,gBAAgB,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,QAAQ,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,CAAA;AAC5G,CAAC;AACD,MAAM,UAAU,eAAe,CAAC,QAAkB;IAChD,OAAQ,QAAgE,CAAC,gBAAgB,CAAC,CAAA;AAC5F,CAAC;AAGD,MAAM,UAAU,eAAe,CAAC,QAAqC;IACnE,gFAAgF;IAChF,IAAI,CAAC,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAC9B,IAAI,QAAQ,CAAC,QAAQ,EAAE,OAAO;QAAE,OAAO,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAA;IAChE,IAAI,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,QAAQ,CAAA;IAC3D,IAAI,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,QAAQ,CAAA;IAC1D,MAAM,IAAI,GAAG,sBAAsB,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IAC/D,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK;QAC7E,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK;QACpE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK;QACpE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;IACxE,OAAO,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAA;AACjE,CAAC","sourcesContent":["import type { Endpoint } from \"@opencode/client/service\"\n\nexport interface RuntimeIdentity {\n version: string\n pid: number\n discovery: \"status\" | \"health\"\n /** Filled only by a successful, authenticated schema negotiation. */\n contract?: { profile?: Exclude<ContractProfile, \"unknown\"> }\n}\n\n// Keep authenticated daemon metadata out of public endpoints and credentials.\nconst RUNTIME_IDENTITY = Symbol.for(\"codenomad.opencode.runtime\")\nexport function rememberRuntime(endpoint: Endpoint, identity: RuntimeIdentity): void {\n Object.defineProperty(endpoint, RUNTIME_IDENTITY, { value: Object.freeze({ ...identity, contract: {} }) })\n}\nexport function runtimeIdentity(endpoint: Endpoint): RuntimeIdentity | undefined {\n return (endpoint as Endpoint & { [RUNTIME_IDENTITY]?: RuntimeIdentity })[RUNTIME_IDENTITY]\n}\n\nexport type ContractProfile = \"modern\" | \"legacy\" | \"unknown\"\nexport function contractProfile(identity: RuntimeIdentity | undefined): ContractProfile {\n // Custom embedded/test lifecycles that don't negotiate use the pinned contract.\n if (!identity) return \"modern\"\n if (identity.contract?.profile) return identity.contract.profile\n if (/^2\\.0\\.[0-3]$/.test(identity.version)) return \"legacy\"\n if (/^2\\.0\\.[45]$/.test(identity.version)) return \"modern\"\n const beta = /^0\\.0\\.0-beta-(\\d+)$/.exec(identity.version)?.[1]\n const audited = new Set([18866, 18955, 18965, 18985, 18992, 18999, 19059, 19086,\n 19124, 19129, 19133, 19135, 19151, 19157, 19187, 19192, 19215, 19213,\n 19228, 19234, 19242, 19266, 19271, 19275, 19278, 19283, 19288, 19289,\n 19296, 19365, 19378, 19381, 19398, 19419, 19422, 19425, 19500, 19507])\n return beta && audited.has(Number(beta)) ? \"legacy\" : \"unknown\"\n}\n"]}
@@ -0,0 +1,112 @@
1
+ import { Service } from "@opencode/client/service";
2
+ import { contractProfile, runtimeIdentity } from "./runtime";
3
+ import { legacyRequest } from "./requests";
4
+ import { negotiateRuntime } from "./negotiate";
5
+ import { applyLocationContext, LOCATION_CONTEXT_HEADER } from "./location";
6
+ function object(value) {
7
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
8
+ }
9
+ function normalizeInbox(value) {
10
+ if (!object(value))
11
+ throw new Error("Invalid native inbox item");
12
+ if (object(value.time) && typeof value.time.created === "number")
13
+ return value;
14
+ if (typeof value.timeCreated !== "number" || !Number.isFinite(value.timeCreated)) {
15
+ throw new Error("Invalid native inbox creation time");
16
+ }
17
+ const { timeCreated, ...rest } = value;
18
+ return { ...rest, time: { created: timeCreated } };
19
+ }
20
+ // Both the server's generated client and the authorized browser proxy use this
21
+ // transport. No error classification here retries a mutation.
22
+ export function createRuntimeFetch(endpoint, fetcher = globalThis.fetch) {
23
+ return createRuntimeTransport(endpoint, fetcher).fetch;
24
+ }
25
+ export function createRuntimeTransport(endpoint, fetcher = globalThis.fetch, lifetime = new AbortController().signal) {
26
+ const identity = runtimeIdentity(endpoint);
27
+ let profile = contractProfile(identity);
28
+ let negotiation;
29
+ const resolveProfile = async (signal) => {
30
+ signal?.throwIfAborted();
31
+ lifetime.throwIfAborted();
32
+ if (profile !== "unknown")
33
+ return profile;
34
+ negotiation ??= negotiateRuntime(endpoint, fetcher, lifetime).then(value => {
35
+ profile = value;
36
+ return value;
37
+ }).catch(error => { negotiation = undefined; throw error; });
38
+ return awaitLocal(negotiation, signal);
39
+ };
40
+ const adaptedFetch = async (input, init) => {
41
+ const request = new Request(input, init);
42
+ const url = new URL(request.url);
43
+ if (url.origin !== new URL(endpoint.url).origin)
44
+ throw new Error("OpenCode transport origin mismatch");
45
+ const profile = await resolveProfile(request.signal);
46
+ const headers = new Headers(request.headers);
47
+ for (const [name, value] of Object.entries(Service.headers(endpoint) ?? {}))
48
+ headers.set(name, value);
49
+ // Credentialed native requests must never follow a redirect to another host.
50
+ const options = { method: request.method, headers, signal: AbortSignal.any([request.signal, lifetime]), redirect: "error" };
51
+ const originalPath = url.pathname;
52
+ if (profile === "legacy" || headers.has(LOCATION_CONTEXT_HEADER)) {
53
+ const text = request.body ? await request.text() : undefined;
54
+ let body = text ? JSON.parse(text) : undefined;
55
+ body = applyLocationContext(url, request.method, body, headers, profile);
56
+ if (body !== undefined && !object(body))
57
+ throw new Error("Invalid OpenCode request body");
58
+ const translated = profile === "legacy" ? legacyRequest(url, request.method, body) : { method: request.method, body };
59
+ options.method = translated.method;
60
+ options.body = translated.body === undefined ? undefined : JSON.stringify(translated.body);
61
+ headers.delete("content-length");
62
+ if (options.body === undefined)
63
+ headers.delete("content-type");
64
+ }
65
+ else if (request.body) {
66
+ options.body = await request.arrayBuffer();
67
+ }
68
+ const response = await fetcher(url, options);
69
+ if (!response.ok && !response.headers.get("content-type")?.includes("application/json")) {
70
+ await response.body?.cancel();
71
+ return Response.json({
72
+ _tag: "OpenCodeContractError", message: `OpenCode ${identity?.version ?? profile}: ${request.method} ${originalPath} returned HTTP ${response.status}`,
73
+ status: response.status, profile, method: request.method, path: originalPath,
74
+ }, { status: response.status });
75
+ }
76
+ if (profile === "legacy" && response.ok && originalPath === "/api/status") {
77
+ const health = await response.json();
78
+ return Response.json({ version: health.version, pid: health.pid, urls: [endpoint.url] });
79
+ }
80
+ if (profile !== "legacy" || !response.ok || !/^\/api\/session\/[^/]+\/(?:inbox|prompt|synthetic|compact)$/.test(originalPath)
81
+ || response.status === 204)
82
+ return response;
83
+ const value = await response.json();
84
+ if (!object(value))
85
+ throw new Error("Invalid native inbox response");
86
+ const normalized = { ...value, data: Array.isArray(value.data) ? value.data.map(normalizeInbox) : normalizeInbox(value.data) };
87
+ const responseHeaders = new Headers(response.headers);
88
+ responseHeaders.delete("content-length");
89
+ responseHeaders.delete("content-encoding");
90
+ return new Response(JSON.stringify(normalized), { status: response.status, headers: responseHeaders });
91
+ };
92
+ return { fetch: adaptedFetch, profile: resolveProfile };
93
+ }
94
+ // A cancelled subscriber must not cancel the connection's negotiation or wait
95
+ // for another subscriber's request to finish before observing its own abort.
96
+ function awaitLocal(pending, signal) {
97
+ if (!signal)
98
+ return pending;
99
+ return new Promise((resolve, reject) => {
100
+ const abort = () => { signal.removeEventListener("abort", abort); reject(signal.reason); };
101
+ if (signal.aborted) {
102
+ reject(signal.reason);
103
+ return;
104
+ }
105
+ signal.addEventListener("abort", abort, { once: true });
106
+ pending.then(value => { signal.removeEventListener("abort", abort); resolve(value); }, error => {
107
+ signal.removeEventListener("abort", abort);
108
+ reject(error);
109
+ });
110
+ });
111
+ }
112
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/transport.ts"],"names":[],"mappings":"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","sourcesContent":["import { Service, type Endpoint } from \"@opencode/client/service\"\nimport { contractProfile, runtimeIdentity } from \"./runtime\"\nimport { legacyRequest } from \"./requests\"\nimport { negotiateRuntime } from \"./negotiate\"\nimport { applyLocationContext, LOCATION_CONTEXT_HEADER } from \"./location\"\n\nfunction object(value: unknown): value is Record<string, unknown> {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value)\n}\n\nfunction normalizeInbox(value: unknown): unknown {\n if (!object(value)) throw new Error(\"Invalid native inbox item\")\n if (object(value.time) && typeof value.time.created === \"number\") return value\n if (typeof value.timeCreated !== \"number\" || !Number.isFinite(value.timeCreated)) {\n throw new Error(\"Invalid native inbox creation time\")\n }\n const { timeCreated, ...rest } = value\n return { ...rest, time: { created: timeCreated } }\n}\n\n// Both the server's generated client and the authorized browser proxy use this\n// transport. No error classification here retries a mutation.\nexport function createRuntimeFetch(endpoint: Endpoint, fetcher: typeof fetch = globalThis.fetch): typeof fetch {\n return createRuntimeTransport(endpoint, fetcher).fetch\n}\n\nexport function createRuntimeTransport(endpoint: Endpoint, fetcher: typeof fetch = globalThis.fetch, lifetime = new AbortController().signal) {\n const identity = runtimeIdentity(endpoint)\n let profile = contractProfile(identity)\n let negotiation: Promise<\"modern\" | \"legacy\"> | undefined\n const resolveProfile = async (signal?: AbortSignal) => {\n signal?.throwIfAborted()\n lifetime.throwIfAborted()\n if (profile !== \"unknown\") return profile\n negotiation ??= negotiateRuntime(endpoint, fetcher, lifetime).then(value => {\n profile = value\n return value\n }).catch(error => { negotiation = undefined; throw error })\n return awaitLocal(negotiation, signal)\n }\n const adaptedFetch: typeof fetch = async (input, init) => {\n const request = new Request(input, init)\n const url = new URL(request.url)\n if (url.origin !== new URL(endpoint.url).origin) throw new Error(\"OpenCode transport origin mismatch\")\n const profile = await resolveProfile(request.signal)\n const headers = new Headers(request.headers)\n for (const [name, value] of Object.entries(Service.headers(endpoint) ?? {})) headers.set(name, value)\n // Credentialed native requests must never follow a redirect to another host.\n const options: RequestInit = { method: request.method, headers, signal: AbortSignal.any([request.signal, lifetime]), redirect: \"error\" }\n const originalPath = url.pathname\n if (profile === \"legacy\" || headers.has(LOCATION_CONTEXT_HEADER)) {\n const text = request.body ? await request.text() : undefined\n let body: unknown = text ? JSON.parse(text) : undefined\n body = applyLocationContext(url, request.method, body, headers, profile)\n if (body !== undefined && !object(body)) throw new Error(\"Invalid OpenCode request body\")\n const translated = profile === \"legacy\" ? legacyRequest(url, request.method, body) : { method: request.method, body }\n options.method = translated.method\n options.body = translated.body === undefined ? undefined : JSON.stringify(translated.body)\n headers.delete(\"content-length\")\n if (options.body === undefined) headers.delete(\"content-type\")\n } else if (request.body) {\n options.body = await request.arrayBuffer()\n }\n const response = await fetcher(url, options)\n if (!response.ok && !response.headers.get(\"content-type\")?.includes(\"application/json\")) {\n await response.body?.cancel()\n return Response.json({\n _tag: \"OpenCodeContractError\", message: `OpenCode ${identity?.version ?? profile}: ${request.method} ${originalPath} returned HTTP ${response.status}`,\n status: response.status, profile, method: request.method, path: originalPath,\n }, { status: response.status })\n }\n if (profile === \"legacy\" && response.ok && originalPath === \"/api/status\") {\n const health = await response.json() as { version: string; pid: number }\n return Response.json({ version: health.version, pid: health.pid, urls: [endpoint.url] })\n }\n if (profile !== \"legacy\" || !response.ok || !/^\\/api\\/session\\/[^/]+\\/(?:inbox|prompt|synthetic|compact)$/.test(originalPath)\n || response.status === 204) return response\n const value: unknown = await response.json()\n if (!object(value)) throw new Error(\"Invalid native inbox response\")\n const normalized = { ...value, data: Array.isArray(value.data) ? value.data.map(normalizeInbox) : normalizeInbox(value.data) }\n const responseHeaders = new Headers(response.headers)\n responseHeaders.delete(\"content-length\")\n responseHeaders.delete(\"content-encoding\")\n return new Response(JSON.stringify(normalized), { status: response.status, headers: responseHeaders })\n }\n return { fetch: adaptedFetch, profile: resolveProfile }\n}\n\n// A cancelled subscriber must not cancel the connection's negotiation or wait\n// for another subscriber's request to finish before observing its own abort.\nfunction awaitLocal<T>(pending: Promise<T>, signal?: AbortSignal): Promise<T> {\n if (!signal) return pending\n return new Promise<T>((resolve, reject) => {\n const abort = () => { signal.removeEventListener(\"abort\", abort); reject(signal.reason) }\n if (signal.aborted) { reject(signal.reason); return }\n signal.addEventListener(\"abort\", abort, { once: true })\n pending.then(value => { signal.removeEventListener(\"abort\", abort); resolve(value) }, error => {\n signal.removeEventListener(\"abort\", abort); reject(error)\n })\n })\n}\n"]}
@@ -0,0 +1,83 @@
1
+ import assert from "node:assert/strict";
2
+ import { createServer } from "node:http";
3
+ import { once } from "node:events";
4
+ import { test } from "node:test";
5
+ import { OpenCode } from "@opencode/client";
6
+ import { rememberRuntime } from "./runtime";
7
+ import { createRuntimeFetch } from "./transport";
8
+ test("the pinned client uses the earlier wire contract and retains admission/list timestamps", async () => {
9
+ const seen = [];
10
+ const item = { id: "inbox-fixture", sessionID: "s", type: "user", payload: { text: "fixture" }, delivery: "queue", timeCreated: 123 };
11
+ const server = createServer(async (req, res) => {
12
+ let raw = "";
13
+ for await (const data of req)
14
+ raw += data;
15
+ assert.equal(req.headers.authorization, `Basic ${Buffer.from("opencode:fixture").toString("base64")}`);
16
+ seen.push({ method: req.method, path: req.url, body: raw ? JSON.parse(raw) : undefined });
17
+ res.setHeader("content-type", "application/json");
18
+ if (req.url === "/api/session/s/prompt")
19
+ return res.end(JSON.stringify({ data: item }));
20
+ if (req.url === "/api/session/s/inbox")
21
+ return res.end(JSON.stringify({ data: [item] }));
22
+ if (req.url === "/api/session/s/fork")
23
+ return res.end(JSON.stringify({ data: { id: "fork" } }));
24
+ if (req.url?.startsWith("/api/session/s/interrupt"))
25
+ return res.end("{}");
26
+ res.writeHead(204).end();
27
+ });
28
+ server.listen(0, "127.0.0.1");
29
+ await once(server, "listening");
30
+ const endpoint = { url: `http://127.0.0.1:${server.address().port}`, auth: { type: "basic", username: "opencode", password: "fixture" } };
31
+ rememberRuntime(endpoint, { version: "2.0.3", pid: 1, discovery: "health" });
32
+ const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint) });
33
+ try {
34
+ assert.equal((await client.session.prompt({ sessionID: "s", text: "fixture" })).time.created, 123);
35
+ assert.equal((await client.session.inbox.list({ sessionID: "s" }))[0].time.created, 123);
36
+ await client.permission.reply({ sessionID: "s", requestID: "p", decision: "once" });
37
+ await client.session.command({ sessionID: "s", name: "test", text: "args", delivery: "queue" });
38
+ await client.session.fork({ sessionID: "s", before: "m" });
39
+ await client.session.interrupt({ sessionID: "s", resume: true });
40
+ await client.session.inbox.update({ sessionID: "s", inboxID: "i", delivery: "steer" });
41
+ await client.session.form.cancel({ sessionID: "s", formID: "f" });
42
+ await client.session.instructions.entry.remove({ sessionID: "s", key: "voice" });
43
+ assert.deepEqual(seen.slice(2), [
44
+ { method: "POST", path: "/api/session/s/permission/p/reply", body: { reply: "once" } },
45
+ { method: "POST", path: "/api/session/s/command", body: { command: "test", text: "args", delivery: "queue" } },
46
+ { method: "POST", path: "/api/session/s/fork", body: { boundary: { type: "before", messageID: "m" } } },
47
+ { method: "POST", path: "/api/session/s/interrupt?continue=true", body: undefined },
48
+ { method: "POST", path: "/api/session/s/inbox/i/steer", body: undefined },
49
+ { method: "POST", path: "/api/session/s/form/f/cancel", body: undefined },
50
+ { method: "DELETE", path: "/api/session/s/instructions/entries/voice", body: undefined },
51
+ ]);
52
+ }
53
+ finally {
54
+ server.closeAllConnections();
55
+ await new Promise(resolve => server.close(() => resolve()));
56
+ }
57
+ });
58
+ test("errors never retry a mutation, unsupported fields are rejected, and abort/auth are preserved", async () => {
59
+ const endpoint = { url: "http://127.0.0.1:4321" };
60
+ rememberRuntime(endpoint, { version: "2.0.3", pid: 2, discovery: "health" });
61
+ let calls = 0;
62
+ for (const status of [400, 401, 404, 500]) {
63
+ const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async () => { calls++; return new Response(null, { status }); }) });
64
+ await assert.rejects(client.session.update({ sessionID: "s", title: "title" }));
65
+ }
66
+ assert.equal(calls, 4);
67
+ const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async () => { calls++; return new Response(null, { status: 204 }); }) });
68
+ await assert.rejects(client.session.update({ sessionID: "s", title: "title", permissions: [] }), (error) => {
69
+ assert.match(String(error.cause), /title-only/);
70
+ return true;
71
+ });
72
+ assert.equal(calls, 4);
73
+ const controller = new AbortController();
74
+ controller.abort();
75
+ const transport = createRuntimeFetch(endpoint, async (_url, init) => {
76
+ assert.equal(init?.redirect, "error");
77
+ init?.signal?.throwIfAborted();
78
+ throw new Error("unexpected live request");
79
+ });
80
+ await assert.rejects(transport(new Request(`${endpoint.url}/api/session/s/wait`, { method: "POST", signal: controller.signal })), { name: "AbortError" });
81
+ await assert.rejects(transport("http://127.0.0.1:4322/api/session"), /origin mismatch/);
82
+ });
83
+ //# sourceMappingURL=transport.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.test.js","sourceRoot":"","sources":["../../../src/opencode/compatibility/transport.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { createServer } from \"node:http\"\nimport { once } from \"node:events\"\nimport { test } from \"node:test\"\nimport { OpenCode } from \"@opencode/client\"\nimport type { Endpoint } from \"@opencode/client/service\"\nimport { rememberRuntime } from \"./runtime\"\nimport { createRuntimeFetch } from \"./transport\"\n\ntest(\"the pinned client uses the earlier wire contract and retains admission/list timestamps\", async () => {\n const seen: Array<{ method: string; path: string; body: unknown }> = []\n const item = { id: \"inbox-fixture\", sessionID: \"s\", type: \"user\", payload: { text: \"fixture\" }, delivery: \"queue\", timeCreated: 123 }\n const server = createServer(async (req, res) => {\n let raw = \"\"\n for await (const data of req) raw += data\n assert.equal(req.headers.authorization, `Basic ${Buffer.from(\"opencode:fixture\").toString(\"base64\")}`)\n seen.push({ method: req.method!, path: req.url!, body: raw ? JSON.parse(raw) : undefined })\n res.setHeader(\"content-type\", \"application/json\")\n if (req.url === \"/api/session/s/prompt\") return res.end(JSON.stringify({ data: item }))\n if (req.url === \"/api/session/s/inbox\") return res.end(JSON.stringify({ data: [item] }))\n if (req.url === \"/api/session/s/fork\") return res.end(JSON.stringify({ data: { id: \"fork\" } }))\n if (req.url?.startsWith(\"/api/session/s/interrupt\")) return res.end(\"{}\")\n res.writeHead(204).end()\n })\n server.listen(0, \"127.0.0.1\")\n await once(server, \"listening\")\n const endpoint: Endpoint = { url: `http://127.0.0.1:${(server.address() as { port: number }).port}`, auth: { type: \"basic\", username: \"opencode\", password: \"fixture\" } }\n rememberRuntime(endpoint, { version: \"2.0.3\", pid: 1, discovery: \"health\" })\n const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint) })\n try {\n assert.equal((await client.session.prompt({ sessionID: \"s\", text: \"fixture\" })).time.created, 123)\n assert.equal((await client.session.inbox.list({ sessionID: \"s\" }))[0].time.created, 123)\n await client.permission.reply({ sessionID: \"s\", requestID: \"p\", decision: \"once\" })\n await client.session.command({ sessionID: \"s\", name: \"test\", text: \"args\", delivery: \"queue\" })\n await client.session.fork({ sessionID: \"s\", before: \"m\" })\n await client.session.interrupt({ sessionID: \"s\", resume: true })\n await client.session.inbox.update({ sessionID: \"s\", inboxID: \"i\", delivery: \"steer\" })\n await client.session.form.cancel({ sessionID: \"s\", formID: \"f\" })\n await client.session.instructions.entry.remove({ sessionID: \"s\", key: \"voice\" })\n assert.deepEqual(seen.slice(2), [\n { method: \"POST\", path: \"/api/session/s/permission/p/reply\", body: { reply: \"once\" } },\n { method: \"POST\", path: \"/api/session/s/command\", body: { command: \"test\", text: \"args\", delivery: \"queue\" } },\n { method: \"POST\", path: \"/api/session/s/fork\", body: { boundary: { type: \"before\", messageID: \"m\" } } },\n { method: \"POST\", path: \"/api/session/s/interrupt?continue=true\", body: undefined },\n { method: \"POST\", path: \"/api/session/s/inbox/i/steer\", body: undefined },\n { method: \"POST\", path: \"/api/session/s/form/f/cancel\", body: undefined },\n { method: \"DELETE\", path: \"/api/session/s/instructions/entries/voice\", body: undefined },\n ])\n } finally { server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve())) }\n})\n\ntest(\"errors never retry a mutation, unsupported fields are rejected, and abort/auth are preserved\", async () => {\n const endpoint: Endpoint = { url: \"http://127.0.0.1:4321\" }\n rememberRuntime(endpoint, { version: \"2.0.3\", pid: 2, discovery: \"health\" })\n let calls = 0\n for (const status of [400, 401, 404, 500]) {\n const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async () => { calls++; return new Response(null, { status }) }) })\n await assert.rejects(client.session.update({ sessionID: \"s\", title: \"title\" }))\n }\n assert.equal(calls, 4)\n const client = OpenCode.make({ baseUrl: endpoint.url, fetch: createRuntimeFetch(endpoint, async () => { calls++; return new Response(null, { status: 204 }) }) })\n await assert.rejects(client.session.update({ sessionID: \"s\", title: \"title\", permissions: [] }), (error: Error) => {\n assert.match(String((error as Error & { cause?: unknown }).cause), /title-only/)\n return true\n })\n assert.equal(calls, 4)\n const controller = new AbortController()\n controller.abort()\n const transport = createRuntimeFetch(endpoint, async (_url, init) => {\n assert.equal(init?.redirect, \"error\")\n init?.signal?.throwIfAborted()\n throw new Error(\"unexpected live request\")\n })\n await assert.rejects(transport(new Request(`${endpoint.url}/api/session/s/wait`, { method: \"POST\", signal: controller.signal })), { name: \"AbortError\" })\n await assert.rejects(transport(\"http://127.0.0.1:4322/api/session\"), /origin mismatch/)\n})\n"]}
@@ -0,0 +1,16 @@
1
+ // Keep identity primitives inside the standalone plugin's package boundary.
2
+ // The HTTP compatibility adapter reuses these without making the plugin depend on it.
3
+ export function readLocationRef(value) {
4
+ if (!value || typeof value !== "object" || Array.isArray(value))
5
+ throw new Error("Invalid location");
6
+ const input = value;
7
+ if (typeof input.directory !== "string" || !input.directory.trim() || input.directory.includes("\0")
8
+ || (input.workspaceID !== undefined && (typeof input.workspaceID !== "string"
9
+ || !input.workspaceID.trim() || input.workspaceID.includes("\0"))))
10
+ throw new Error("Invalid location");
11
+ return { directory: input.directory, ...(input.workspaceID === undefined ? {} : { workspaceID: input.workspaceID }) };
12
+ }
13
+ export function sameLocation(left, right) {
14
+ return left.directory === right.directory && left.workspaceID === right.workspaceID;
15
+ }
16
+ //# sourceMappingURL=location.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"location.js","sourceRoot":"","sources":["../../../src/opencode/session-pruning/location.ts"],"names":[],"mappings":"AAEA,4EAA4E;AAC5E,sFAAsF;AACtF,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAA;IACpG,MAAM,KAAK,GAAG,KAAgC,CAAA;IAC9C,IAAI,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC;WAC/F,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,IAAI,CAAC,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ;eACxE,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAA;IAC3G,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,EAAE,CAAA;AACvH,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAiB,EAAE,KAAkB;IAChE,OAAO,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,WAAW,KAAK,KAAK,CAAC,WAAW,CAAA;AACrF,CAAC","sourcesContent":["import type { LocationRef } from \"@opencode/client\"\n\n// Keep identity primitives inside the standalone plugin's package boundary.\n// The HTTP compatibility adapter reuses these without making the plugin depend on it.\nexport function readLocationRef(value: unknown): LocationRef {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"Invalid location\")\n const input = value as Record<string, unknown>\n if (typeof input.directory !== \"string\" || !input.directory.trim() || input.directory.includes(\"\\0\")\n || (input.workspaceID !== undefined && (typeof input.workspaceID !== \"string\"\n || !input.workspaceID.trim() || input.workspaceID.includes(\"\\0\")))) throw new Error(\"Invalid location\")\n return { directory: input.directory, ...(input.workspaceID === undefined ? {} : { workspaceID: input.workspaceID }) }\n}\n\nexport function sameLocation(left: LocationRef, right: LocationRef): boolean {\n return left.directory === right.directory && left.workspaceID === right.workspaceID\n}\n"]}
@@ -0,0 +1,37 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { copyFile, mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { test } from "node:test";
7
+ import { fileURLToPath } from "node:url";
8
+ import { build } from "esbuild";
9
+ test("every packed plugin entrypoint resolves without sibling checkout sources", async () => {
10
+ const source = fileURLToPath(new URL("./", import.meta.url));
11
+ const temporaryRoot = path.join(os.tmpdir(), "opencode");
12
+ await mkdir(temporaryRoot, { recursive: true });
13
+ const isolated = await mkdtemp(path.join(temporaryRoot, "pruning-package-"));
14
+ try {
15
+ const output = execFileSync(process.platform === "win32" ? "npm.cmd" : "npm", [
16
+ "pack", "--dry-run", "--json", "--ignore-scripts",
17
+ ], { cwd: source, encoding: "utf8", shell: process.platform === "win32" });
18
+ const [archive] = JSON.parse(output);
19
+ for (const file of archive.files) {
20
+ const destination = path.join(isolated, file.path);
21
+ await mkdir(path.dirname(destination), { recursive: true });
22
+ await copyFile(path.join(source, file.path), destination);
23
+ }
24
+ const manifest = JSON.parse(await readFile(path.join(isolated, "package.json"), "utf8"));
25
+ const result = await build({
26
+ absWorkingDir: isolated,
27
+ entryPoints: Object.values(manifest.exports),
28
+ outdir: "dist", bundle: true, write: false, platform: "node", format: "esm",
29
+ packages: "external", logLevel: "silent",
30
+ });
31
+ assert.equal(result.outputFiles.length, Object.keys(manifest.exports).length);
32
+ }
33
+ finally {
34
+ await rm(isolated, { recursive: true, force: true });
35
+ }
36
+ });
37
+ //# sourceMappingURL=package.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package.test.js","sourceRoot":"","sources":["../../../src/opencode/session-pruning/package.test.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from \"node:assert/strict\"\nimport { execFileSync } from \"node:child_process\"\nimport { copyFile, mkdir, mkdtemp, readFile, rm } from \"node:fs/promises\"\nimport os from \"node:os\"\nimport path from \"node:path\"\nimport { test } from \"node:test\"\nimport { fileURLToPath } from \"node:url\"\nimport { build } from \"esbuild\"\n\ntest(\"every packed plugin entrypoint resolves without sibling checkout sources\", async () => {\n const source = fileURLToPath(new URL(\"./\", import.meta.url))\n const temporaryRoot = path.join(os.tmpdir(), \"opencode\")\n await mkdir(temporaryRoot, { recursive: true })\n const isolated = await mkdtemp(path.join(temporaryRoot, \"pruning-package-\"))\n try {\n const output = execFileSync(process.platform === \"win32\" ? \"npm.cmd\" : \"npm\", [\n \"pack\", \"--dry-run\", \"--json\", \"--ignore-scripts\",\n ], { cwd: source, encoding: \"utf8\", shell: process.platform === \"win32\" })\n const [archive] = JSON.parse(output) as { files: { path: string }[] }[]\n for (const file of archive.files) {\n const destination = path.join(isolated, file.path)\n await mkdir(path.dirname(destination), { recursive: true })\n await copyFile(path.join(source, file.path), destination)\n }\n const manifest = JSON.parse(await readFile(path.join(isolated, \"package.json\"), \"utf8\"))\n const result = await build({\n absWorkingDir: isolated,\n entryPoints: Object.values(manifest.exports) as string[],\n outdir: \"dist\", bundle: true, write: false, platform: \"node\", format: \"esm\",\n packages: \"external\", logLevel: \"silent\",\n })\n assert.equal(result.outputFiles.length, Object.keys(manifest.exports).length)\n } finally {\n await rm(isolated, { recursive: true, force: true })\n }\n})\n"]}
@@ -5,6 +5,7 @@ import { previewContent } from "./planner";
5
5
  import { readPruningPreview } from "./preview-store";
6
6
  import { pruneBoundMessage } from "./service";
7
7
  import { pruningDatabasePath } from "./database-path";
8
+ import { readLocationRef, sameLocation } from "./location";
8
9
  export const SessionPruningRpc = Rpc.define(pruningRpcDefinition);
9
10
  // Native local-plugin entry point. Only a user's pruning RPC changes content.
10
11
  export default Plugin.define({
@@ -16,11 +17,13 @@ export default Plugin.define({
16
17
  const session = await ctx.session.get({ sessionID: target.sessionID });
17
18
  // Location membership must be checked again inside the plugin: clients
18
19
  // other than CodeNomad may invoke this RPC directly.
19
- if (session.location.directory !== ctx.location.directory)
20
+ const location = readLocationRef(session.location);
21
+ if (!sameLocation(location, readLocationRef(ctx.location)) || session.projectID !== ctx.location.project.id) {
20
22
  return { status: "blocked", reason: "not_deletable" };
23
+ }
21
24
  // Plugin Context does not expose session.message in beta-19398. Read
22
25
  // the explicit DB in query-only mode, including pre-compaction history.
23
- const data = await readPruningPreview(pruningDatabasePath(ctx.options.databasePath, ctx.app.channel), target, session.location.directory);
26
+ const data = await readPruningPreview(pruningDatabasePath(ctx.options.databasePath, ctx.app.channel), target, location, session.projectID);
24
27
  const preview = data ? previewContent(data) : { status: "blocked", reason: "unavailable" };
25
28
  return preview.status === "preview"
26
29
  ? { ...preview, liveMutation: true }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../../src/opencode/session-pruning/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,sBAAsB,CAAA;AAC1C,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AAC1F,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAA;AAC7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAA;AAEjE,8EAA8E;AAC9E,eAAe,MAAM,CAAC,MAAM,CAAC;IAC3B,EAAE,EAAE,2BAA2B;IAC/B,KAAK,CAAC,KAAK,CAAC,GAAG;QACb,MAAM,YAAY,GAAG,MAAM,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,iBAAiB,EAAE;YAC7D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBACvB,MAAM,MAAM,GAAG,mBAAmB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;gBAC/C,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAA;gBACtE,uEAAuE;gBACvE,qDAAqD;gBACrD,IAAI,OAAO,CAAC,QAAQ,CAAC,SAAS,KAAK,GAAG,CAAC,QAAQ,CAAC,SAAS;oBAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,EAAW,CAAA;gBACzH,qEAAqE;gBACrE,wEAAwE;gBACxE,MAAM,IAAI,GAAG,MAAM,kBAAkB,CAAC,mBAAmB,CAAC,GAAG,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAA;gBACzI,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAW,CAAA;gBACnG,OAAO,OAAO,CAAC,MAAM,KAAK,SAAS;oBACjC,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE;oBACpC,CAAC,CAAC,OAAO,CAAA;YACb,CAAC;YACD,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;gBAC3B,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;gBAC/B,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;gBAC/D,IAAI,MAAM,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;oBAC/B,sEAAsE;oBACtE,mEAAmE;oBACnE,MAAM,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE;wBACvC,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ;qBAClF,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;gBACpB,CAAC;gBACD,OAAO,MAAM,CAAA;YACf,CAAC;SACF,CAAC,CAAA;QACF,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,CAAA;IACrC,CAAC;CACF,CAAC,CAAA","sourcesContent":["import { Plugin } from \"@opencode/plugin\"\nimport { Rpc } from \"@opencode/plugin/rpc\"\nimport { messageTargetSchema, pruneRequestSchema, pruningRpcDefinition } from \"./contract\"\nimport { previewContent } from \"./planner\"\nimport { readPruningPreview } from \"./preview-store\"\nimport { pruneBoundMessage } from \"./service\"\nimport { pruningDatabasePath } from \"./database-path\"\n\nexport const SessionPruningRpc = Rpc.define(pruningRpcDefinition)\n\n// Native local-plugin entry point. Only a user's pruning RPC changes content.\nexport default Plugin.define({\n id: \"codenomad-session-pruning\",\n async setup(ctx) {\n const registration = await ctx.rpc.register(SessionPruningRpc, {\n preview: async (input) => {\n const target = messageTargetSchema.parse(input)\n const session = await ctx.session.get({ sessionID: target.sessionID })\n // Location membership must be checked again inside the plugin: clients\n // other than CodeNomad may invoke this RPC directly.\n if (session.location.directory !== ctx.location.directory) return { status: \"blocked\", reason: \"not_deletable\" } as const\n // Plugin Context does not expose session.message in beta-19398. Read\n // the explicit DB in query-only mode, including pre-compaction history.\n const data = await readPruningPreview(pruningDatabasePath(ctx.options.databasePath, ctx.app.channel), target, session.location.directory)\n const preview = data ? previewContent(data) : { status: \"blocked\", reason: \"unavailable\" } as const\n return preview.status === \"preview\"\n ? { ...preview, liveMutation: true }\n : preview\n },\n prune: async (input, call) => {\n pruneRequestSchema.parse(input)\n const result = await pruneBoundMessage(ctx, input, call.signal)\n if (result.status === \"pruned\") {\n // Retrying the same input returns its atomic receipt and re-emits the\n // notification. An emission failure cannot undo a committed write.\n await registration.events.emit(\"pruned\", {\n sessionID: input.sessionID, messageID: input.messageID, revision: result.revision,\n }).catch(() => {})\n }\n return result\n },\n })\n return () => registration.dispose()\n },\n})\n"]}
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../../src/opencode/session-pruning/plugin.ts"],"names":[],"mappings":"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","sourcesContent":["import { Plugin } from \"@opencode/plugin\"\nimport { Rpc } from \"@opencode/plugin/rpc\"\nimport { messageTargetSchema, pruneRequestSchema, pruningRpcDefinition } from \"./contract\"\nimport { previewContent } from \"./planner\"\nimport { readPruningPreview } from \"./preview-store\"\nimport { pruneBoundMessage } from \"./service\"\nimport { pruningDatabasePath } from \"./database-path\"\nimport { readLocationRef, sameLocation } from \"./location\"\n\nexport const SessionPruningRpc = Rpc.define(pruningRpcDefinition)\n\n// Native local-plugin entry point. Only a user's pruning RPC changes content.\nexport default Plugin.define({\n id: \"codenomad-session-pruning\",\n async setup(ctx) {\n const registration = await ctx.rpc.register(SessionPruningRpc, {\n preview: async (input) => {\n const target = messageTargetSchema.parse(input)\n const session = await ctx.session.get({ sessionID: target.sessionID })\n // Location membership must be checked again inside the plugin: clients\n // other than CodeNomad may invoke this RPC directly.\n const location = readLocationRef(session.location)\n if (!sameLocation(location, readLocationRef(ctx.location)) || session.projectID !== ctx.location.project.id) {\n return { status: \"blocked\", reason: \"not_deletable\" } as const\n }\n // Plugin Context does not expose session.message in beta-19398. Read\n // the explicit DB in query-only mode, including pre-compaction history.\n const data = await readPruningPreview(pruningDatabasePath(ctx.options.databasePath, ctx.app.channel), target, location, session.projectID)\n const preview = data ? previewContent(data) : { status: \"blocked\", reason: \"unavailable\" } as const\n return preview.status === \"preview\"\n ? { ...preview, liveMutation: true }\n : preview\n },\n prune: async (input, call) => {\n pruneRequestSchema.parse(input)\n const result = await pruneBoundMessage(ctx, input, call.signal)\n if (result.status === \"pruned\") {\n // Retrying the same input returns its atomic receipt and re-emits the\n // notification. An emission failure cannot undo a committed write.\n await registration.events.emit(\"pruned\", {\n sessionID: input.sessionID, messageID: input.messageID, revision: result.revision,\n }).catch(() => {})\n }\n return result\n },\n })\n return () => registration.dispose()\n },\n})\n"]}
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { storageDirectory } from "./storage-path";
3
- export async function readPruningPreview(filename, target, directory) {
3
+ export async function readPruningPreview(filename, target, location, projectID) {
4
4
  // Deliberately no default ~/.local/share path or UI-supplied override.
5
5
  if (typeof filename !== "string" || !path.isAbsolute(filename))
6
6
  return undefined;
@@ -11,7 +11,8 @@ export async function readPruningPreview(filename, target, directory) {
11
11
  const row = db.prepare(`SELECT m.data FROM session_message m
12
12
  JOIN session_v2 s ON s.id = m.session_id
13
13
  WHERE m.id = ? AND m.session_id = ? AND m.type = 'assistant' AND s.directory = ?
14
- AND length(CAST(m.data AS BLOB)) <= 16777216`).get(target.messageID, target.sessionID, storageDirectory(directory));
14
+ AND s.project_id = ? AND s.workspace_id IS ?
15
+ AND length(CAST(m.data AS BLOB)) <= 16777216`).get(target.messageID, target.sessionID, storageDirectory(location.directory), projectID, location.workspaceID ?? null);
15
16
  return typeof row?.data === "string" ? JSON.parse(row.data) : undefined;
16
17
  }
17
18
  finally {
@@ -1 +1 @@
1
- {"version":3,"file":"preview-store.js","sourceRoot":"","sources":["../../../src/opencode/session-pruning/preview-store.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAG5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAEjD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,QAAiB,EAAE,MAA2C,EAAE,SAAiB;IAEjF,uEAAuE;IACvE,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAA;IAChF,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAA;IACpD,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;IACxE,IAAI,CAAC;QACH,EAAE,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAA;QAC/B,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC;;;mDAGwB,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC,CAAA;QACrH,OAAO,OAAO,GAAG,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACzE,CAAC;YAAS,CAAC;QAAC,EAAE,CAAC,KAAK,EAAE,CAAA;IAAC,CAAC;AAC1B,CAAC","sourcesContent":["import path from \"node:path\"\nimport type { z } from \"zod\"\nimport type { messageTargetSchema } from \"./contract\"\nimport { storageDirectory } from \"./storage-path\"\n\nexport async function readPruningPreview(\n filename: unknown, target: z.infer<typeof messageTargetSchema>, directory: string,\n): Promise<unknown | undefined> {\n // Deliberately no default ~/.local/share path or UI-supplied override.\n if (typeof filename !== \"string\" || !path.isAbsolute(filename)) return undefined\n const { DatabaseSync } = await import(\"node:sqlite\")\n const db = new DatabaseSync(filename, { readOnly: true, timeout: 1000 })\n try {\n db.exec(\"PRAGMA query_only=ON\")\n const row = db.prepare(`SELECT m.data FROM session_message m\n JOIN session_v2 s ON s.id = m.session_id\n WHERE m.id = ? AND m.session_id = ? AND m.type = 'assistant' AND s.directory = ?\n AND length(CAST(m.data AS BLOB)) <= 16777216`).get(target.messageID, target.sessionID, storageDirectory(directory))\n return typeof row?.data === \"string\" ? JSON.parse(row.data) : undefined\n } finally { db.close() }\n}\n"]}
1
+ {"version":3,"file":"preview-store.js","sourceRoot":"","sources":["../../../src/opencode/session-pruning/preview-store.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAG5B,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAGjD,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,QAAiB,EAAE,MAA2C,EAAE,QAAqB,EAAE,SAAiB;IAExG,uEAAuE;IACvE,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,SAAS,CAAA;IAChF,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAA;IACpD,MAAM,EAAE,GAAG,IAAI,YAAY,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;IACxE,IAAI,CAAC;QACH,EAAE,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAA;QAC/B,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC;;;;oDAIyB,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,EACnF,gBAAgB,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,WAAW,IAAI,IAAI,CAAC,CAAA;QACnF,OAAO,OAAO,GAAG,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACzE,CAAC;YAAS,CAAC;QAAC,EAAE,CAAC,KAAK,EAAE,CAAA;IAAC,CAAC;AAC1B,CAAC","sourcesContent":["import path from \"node:path\"\nimport type { z } from \"zod\"\nimport type { messageTargetSchema } from \"./contract\"\nimport { storageDirectory } from \"./storage-path\"\nimport type { LocationRef } from \"@opencode/client\"\n\nexport async function readPruningPreview(\n filename: unknown, target: z.infer<typeof messageTargetSchema>, location: LocationRef, projectID: string,\n): Promise<unknown | undefined> {\n // Deliberately no default ~/.local/share path or UI-supplied override.\n if (typeof filename !== \"string\" || !path.isAbsolute(filename)) return undefined\n const { DatabaseSync } = await import(\"node:sqlite\")\n const db = new DatabaseSync(filename, { readOnly: true, timeout: 1000 })\n try {\n db.exec(\"PRAGMA query_only=ON\")\n const row = db.prepare(`SELECT m.data FROM session_message m\n JOIN session_v2 s ON s.id = m.session_id\n WHERE m.id = ? AND m.session_id = ? AND m.type = 'assistant' AND s.directory = ?\n AND s.project_id = ? AND s.workspace_id IS ?\n AND length(CAST(m.data AS BLOB)) <= 16777216`).get(target.messageID, target.sessionID,\n storageDirectory(location.directory), projectID, location.workspaceID ?? null)\n return typeof row?.data === \"string\" ? JSON.parse(row.data) : undefined\n } finally { db.close() }\n}\n"]}