@code-yeongyu/senpi 2026.8.7 → 2026.8.9-2

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 (199) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/core/agent-session.d.ts +4 -0
  3. package/dist/core/agent-session.d.ts.map +1 -1
  4. package/dist/core/agent-session.js +35 -12
  5. package/dist/core/agent-session.js.map +1 -1
  6. package/dist/core/extensions/builtin/cache-keepalive/index.d.ts +30 -0
  7. package/dist/core/extensions/builtin/cache-keepalive/index.d.ts.map +1 -0
  8. package/dist/core/extensions/builtin/cache-keepalive/index.js +281 -0
  9. package/dist/core/extensions/builtin/cache-keepalive/index.js.map +1 -0
  10. package/dist/core/extensions/builtin/compaction/index.js +9 -9
  11. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  12. package/dist/core/extensions/builtin/goal/cache-warm-renderer.d.ts.map +1 -1
  13. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js +21 -8
  14. package/dist/core/extensions/builtin/goal/cache-warm-renderer.js.map +1 -1
  15. package/dist/core/extensions/builtin/goal/cache-warm.d.ts +11 -0
  16. package/dist/core/extensions/builtin/goal/cache-warm.d.ts.map +1 -1
  17. package/dist/core/extensions/builtin/goal/cache-warm.js +16 -0
  18. package/dist/core/extensions/builtin/goal/cache-warm.js.map +1 -1
  19. package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
  20. package/dist/core/extensions/builtin/goal/index.js +31 -1
  21. package/dist/core/extensions/builtin/goal/index.js.map +1 -1
  22. package/dist/core/extensions/builtin/goal/monitor-continuation-types.d.ts +1 -0
  23. package/dist/core/extensions/builtin/goal/monitor-continuation-types.d.ts.map +1 -1
  24. package/dist/core/extensions/builtin/goal/monitor-continuation-types.js.map +1 -1
  25. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts +2 -1
  26. package/dist/core/extensions/builtin/goal/monitor-continuation.d.ts.map +1 -1
  27. package/dist/core/extensions/builtin/goal/monitor-continuation.js +153 -51
  28. package/dist/core/extensions/builtin/goal/monitor-continuation.js.map +1 -1
  29. package/dist/core/extensions/builtin/goal/prompt.d.ts +1 -1
  30. package/dist/core/extensions/builtin/goal/prompt.d.ts.map +1 -1
  31. package/dist/core/extensions/builtin/goal/prompt.js +21 -7
  32. package/dist/core/extensions/builtin/goal/prompt.js.map +1 -1
  33. package/dist/core/extensions/builtin/goal/store-changed-event.d.ts +8 -0
  34. package/dist/core/extensions/builtin/goal/store-changed-event.d.ts.map +1 -0
  35. package/dist/core/extensions/builtin/goal/store-changed-event.js +9 -0
  36. package/dist/core/extensions/builtin/goal/store-changed-event.js.map +1 -0
  37. package/dist/core/extensions/builtin/goal/wait-progress.d.ts +3 -1
  38. package/dist/core/extensions/builtin/goal/wait-progress.d.ts.map +1 -1
  39. package/dist/core/extensions/builtin/goal/wait-progress.js +27 -3
  40. package/dist/core/extensions/builtin/goal/wait-progress.js.map +1 -1
  41. package/dist/core/extensions/builtin/goal/wait-ticker.d.ts +4 -3
  42. package/dist/core/extensions/builtin/goal/wait-ticker.d.ts.map +1 -1
  43. package/dist/core/extensions/builtin/goal/wait-ticker.js +6 -6
  44. package/dist/core/extensions/builtin/goal/wait-ticker.js.map +1 -1
  45. package/dist/core/extensions/builtin/import-repro.d.ts.map +1 -1
  46. package/dist/core/extensions/builtin/import-repro.js +9 -0
  47. package/dist/core/extensions/builtin/import-repro.js.map +1 -1
  48. package/dist/core/extensions/builtin/index.d.ts.map +1 -1
  49. package/dist/core/extensions/builtin/index.js +2 -0
  50. package/dist/core/extensions/builtin/index.js.map +1 -1
  51. package/dist/core/extensions/builtin/model-fallback/settings.d.ts.map +1 -1
  52. package/dist/core/extensions/builtin/model-fallback/settings.js +21 -1
  53. package/dist/core/extensions/builtin/model-fallback/settings.js.map +1 -1
  54. package/dist/core/extensions/builtin/monitor-state-event.d.ts +16 -0
  55. package/dist/core/extensions/builtin/monitor-state-event.d.ts.map +1 -1
  56. package/dist/core/extensions/builtin/monitor-state-event.js +15 -0
  57. package/dist/core/extensions/builtin/monitor-state-event.js.map +1 -1
  58. package/dist/core/extensions/builtin/terminal/extension.d.ts.map +1 -1
  59. package/dist/core/extensions/builtin/terminal/extension.js +20 -1
  60. package/dist/core/extensions/builtin/terminal/extension.js.map +1 -1
  61. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts +5 -1
  62. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts.map +1 -1
  63. package/dist/core/extensions/builtin/terminal/session-bundle.js +19 -2
  64. package/dist/core/extensions/builtin/terminal/session-bundle.js.map +1 -1
  65. package/dist/core/extensions/builtin/terminal/tools/bash.d.ts.map +1 -1
  66. package/dist/core/extensions/builtin/terminal/tools/bash.js +2 -0
  67. package/dist/core/extensions/builtin/terminal/tools/bash.js.map +1 -1
  68. package/dist/core/extensions/builtin/terminal/tools/context.d.ts +2 -0
  69. package/dist/core/extensions/builtin/terminal/tools/context.d.ts.map +1 -1
  70. package/dist/core/extensions/builtin/terminal/tools/context.js.map +1 -1
  71. package/dist/core/extensions/runner.d.ts +2 -0
  72. package/dist/core/extensions/runner.d.ts.map +1 -1
  73. package/dist/core/extensions/runner.js +15 -2
  74. package/dist/core/extensions/runner.js.map +1 -1
  75. package/dist/core/extensions/types.d.ts +16 -0
  76. package/dist/core/extensions/types.d.ts.map +1 -1
  77. package/dist/core/extensions/types.js.map +1 -1
  78. package/dist/core/resource-loader.d.ts +4 -0
  79. package/dist/core/resource-loader.d.ts.map +1 -1
  80. package/dist/core/resource-loader.js +6 -0
  81. package/dist/core/resource-loader.js.map +1 -1
  82. package/dist/core/retry-fallback/chains.d.ts +13 -1
  83. package/dist/core/retry-fallback/chains.d.ts.map +1 -1
  84. package/dist/core/retry-fallback/chains.js +80 -8
  85. package/dist/core/retry-fallback/chains.js.map +1 -1
  86. package/dist/core/retry-fallback/controller.d.ts +2 -0
  87. package/dist/core/retry-fallback/controller.d.ts.map +1 -1
  88. package/dist/core/retry-fallback/controller.js.map +1 -1
  89. package/dist/core/retry-fallback/expansion.d.ts +46 -0
  90. package/dist/core/retry-fallback/expansion.d.ts.map +1 -0
  91. package/dist/core/retry-fallback/expansion.js +113 -0
  92. package/dist/core/retry-fallback/expansion.js.map +1 -0
  93. package/dist/core/retry-fallback/settings.d.ts +6 -0
  94. package/dist/core/retry-fallback/settings.d.ts.map +1 -1
  95. package/dist/core/retry-fallback/settings.js +12 -10
  96. package/dist/core/retry-fallback/settings.js.map +1 -1
  97. package/dist/core/retry-fallback/validate.d.ts.map +1 -1
  98. package/dist/core/retry-fallback/validate.js +14 -1
  99. package/dist/core/retry-fallback/validate.js.map +1 -1
  100. package/dist/core/session-manager.d.ts +1 -1
  101. package/dist/core/session-manager.d.ts.map +1 -1
  102. package/dist/core/session-manager.js.map +1 -1
  103. package/dist/core/settings-manager.d.ts +10 -0
  104. package/dist/core/settings-manager.d.ts.map +1 -1
  105. package/dist/core/settings-manager.js +14 -0
  106. package/dist/core/settings-manager.js.map +1 -1
  107. package/dist/main.d.ts.map +1 -1
  108. package/dist/main.js +18 -3
  109. package/dist/main.js.map +1 -1
  110. package/dist/modes/app-server/threads/goal-handlers.d.ts.map +1 -1
  111. package/dist/modes/app-server/threads/goal-handlers.js +7 -0
  112. package/dist/modes/app-server/threads/goal-handlers.js.map +1 -1
  113. package/dist/modes/interactive/components/favorite-models-selector.d.ts +4 -0
  114. package/dist/modes/interactive/components/favorite-models-selector.d.ts.map +1 -1
  115. package/dist/modes/interactive/components/favorite-models-selector.js +23 -7
  116. package/dist/modes/interactive/components/favorite-models-selector.js.map +1 -1
  117. package/dist/modes/interactive/components/model-selector.d.ts +1 -0
  118. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  119. package/dist/modes/interactive/components/model-selector.js +11 -7
  120. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  121. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  122. package/dist/modes/interactive/interactive-mode.js +26 -6
  123. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  124. package/dist/modes/interactive/model-search-rank.d.ts +13 -0
  125. package/dist/modes/interactive/model-search-rank.d.ts.map +1 -0
  126. package/dist/modes/interactive/model-search-rank.js +200 -0
  127. package/dist/modes/interactive/model-search-rank.js.map +1 -0
  128. package/dist/modes/interactive/model-search.d.ts +0 -5
  129. package/dist/modes/interactive/model-search.d.ts.map +1 -1
  130. package/dist/modes/interactive/model-search.js +0 -9
  131. package/dist/modes/interactive/model-search.js.map +1 -1
  132. package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
  133. package/dist/modes/rpc/session-command-router.js +3 -2
  134. package/dist/modes/rpc/session-command-router.js.map +1 -1
  135. package/dist/modes/rpc/session-registry.d.ts +1 -1
  136. package/dist/modes/rpc/session-registry.d.ts.map +1 -1
  137. package/dist/modes/rpc/session-registry.js +3 -3
  138. package/dist/modes/rpc/session-registry.js.map +1 -1
  139. package/docs/providers.md +23 -3
  140. package/docs/settings.md +1 -1
  141. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +33 -0
  142. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  143. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/wake-source-state.ts +29 -0
  144. package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +10 -0
  145. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/detached-cell-manager.ts +27 -1
  146. package/node_modules/@earendil-works/pi-agent-core/dist/empty-assistant-recovery.d.ts.map +1 -1
  147. package/node_modules/@earendil-works/pi-agent-core/dist/empty-assistant-recovery.js +4 -7
  148. package/node_modules/@earendil-works/pi-agent-core/dist/empty-assistant-recovery.js.map +1 -1
  149. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  150. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts +3 -1
  151. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  152. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +13 -0
  153. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  154. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  155. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +6 -3
  156. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js.map +1 -1
  157. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
  158. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +6 -2
  159. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  160. package/node_modules/@earendil-works/pi-ai/dist/api/warm-prompt-cache.d.ts +17 -0
  161. package/node_modules/@earendil-works/pi-ai/dist/api/warm-prompt-cache.d.ts.map +1 -0
  162. package/node_modules/@earendil-works/pi-ai/dist/api/warm-prompt-cache.js +52 -0
  163. package/node_modules/@earendil-works/pi-ai/dist/api/warm-prompt-cache.js.map +1 -0
  164. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +3 -1
  165. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
  166. package/node_modules/@earendil-works/pi-ai/dist/index.js +3 -1
  167. package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
  168. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  169. package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
  170. package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai-cn.json +1 -1
  171. package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai.json +1 -1
  172. package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
  173. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  174. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  176. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  177. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/antml/recovery-stream.d.ts.map +1 -1
  178. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/antml/recovery-stream.js +120 -1
  179. package/node_modules/@earendil-works/pi-ai/dist/tool-call-middleware/protocols/antml/recovery-stream.js.map +1 -1
  180. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +2 -0
  181. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  182. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  183. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.d.ts +3 -1
  184. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.d.ts.map +1 -1
  185. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.js +17 -3
  186. package/node_modules/@earendil-works/pi-ai/dist/utils/prompt-cache-ttl.js.map +1 -1
  187. package/node_modules/@earendil-works/pi-ai/dist/utils/visible-text.d.ts +4 -0
  188. package/node_modules/@earendil-works/pi-ai/dist/utils/visible-text.d.ts.map +1 -0
  189. package/node_modules/@earendil-works/pi-ai/dist/utils/visible-text.js +8 -0
  190. package/node_modules/@earendil-works/pi-ai/dist/utils/visible-text.js.map +1 -0
  191. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  192. package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.d.ts +12 -0
  193. package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.d.ts.map +1 -1
  194. package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.js +34 -2
  195. package/node_modules/@earendil-works/pi-pty/dist/pipe-fallback.js.map +1 -1
  196. package/node_modules/@earendil-works/pi-pty/native/prebuilds/darwin-arm64/senpi_pty.darwin-arm64.node +0 -0
  197. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  198. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  199. package/package.json +6 -6
@@ -0,0 +1,13 @@
1
+ import type { ModelSearchItem } from "./model-search.ts";
2
+ export interface RankModelSearchOptions<T> {
3
+ /** When true, favorite matches partition above non-favorites (relevance preserved within each partition). */
4
+ favoritesFirst: boolean;
5
+ /** Callers with a null favorite-ids sentinel return true for all items, making the partition a no-op. */
6
+ isFavorite: (item: T) => boolean;
7
+ }
8
+ /**
9
+ * Filter and rank items by search relevance, optionally partitioning favorites first.
10
+ * Empty trimmed query returns the input unchanged (caller base order is authoritative).
11
+ */
12
+ export declare function rankModelSearchItems<T>(items: T[], query: string, getModel: (item: T) => ModelSearchItem, options: RankModelSearchOptions<T>): T[];
13
+ //# sourceMappingURL=model-search-rank.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-search-rank.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/model-search-rank.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAkCzD,MAAM,WAAW,sBAAsB,CAAC,CAAC;IACxC,6GAA6G;IAC7G,cAAc,EAAE,OAAO,CAAC;IACxB,yGAAyG;IACzG,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC;CACjC;AAoID;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACrC,KAAK,EAAE,CAAC,EAAE,EACV,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,eAAe,EACtC,OAAO,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAChC,CAAC,EAAE,CAwDL"}
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Favorites-aware relevance ranking for model search.
3
+ *
4
+ * Implements .omo/drafts/model-selector-favorites-search-ux-ranking-spec.md:
5
+ * dual query token plans (compound keeps slashes, legacy splits them), a
6
+ * per-token tier ladder over independently matched lowercased fields,
7
+ * worst-tier-first aggregation, and a composite sort key whose canonical
8
+ * provider-path and favorites partitions precede all relevance costs.
9
+ */
10
+ import { fuzzyMatch } from "@earendil-works/pi-tui";
11
+ const FIELD_NAMES = ["id", "name", "provider", "providerId"];
12
+ /** Exact tier: provider/id < id < provider < name (direct provider/id beats proxy id). */
13
+ const EXACT_FIELD_WEIGHTS = { providerId: 0, id: 1, provider: 2, name: 3 };
14
+ /** Tiers 1-4: id < name < provider < provider/id. */
15
+ const RANKED_FIELD_WEIGHTS = { id: 0, name: 1, provider: 2, providerId: 3 };
16
+ function isAlphanumeric(char) {
17
+ if (!char) {
18
+ return false;
19
+ }
20
+ const code = char.charCodeAt(0);
21
+ return (code >= 97 && code <= 122) || (code >= 48 && code <= 57);
22
+ }
23
+ /** Best tier for a token inside one field; multiple occurrences pick best tier, then earliest. */
24
+ function matchField(token, field) {
25
+ if (field.length === 0) {
26
+ return null;
27
+ }
28
+ if (field === token) {
29
+ return { tier: 0, occurrenceStart: 0, fuzzyScore: 0 };
30
+ }
31
+ let best = null;
32
+ let index = field.indexOf(token);
33
+ while (index !== -1) {
34
+ const leftBoundary = index === 0 || !isAlphanumeric(field[index - 1]);
35
+ const end = index + token.length;
36
+ const rightBoundary = end === field.length || !isAlphanumeric(field[end]);
37
+ const tier = leftBoundary && rightBoundary ? 1 : leftBoundary || rightBoundary ? 2 : 3;
38
+ if (!best || tier < best.tier) {
39
+ best = { tier, occurrenceStart: index, fuzzyScore: 0 };
40
+ }
41
+ index = field.indexOf(token, index + 1);
42
+ }
43
+ if (best) {
44
+ return best;
45
+ }
46
+ const fuzzy = fuzzyMatch(token, field);
47
+ return fuzzy.matches ? { tier: 4, occurrenceStart: 0, fuzzyScore: fuzzy.score } : null;
48
+ }
49
+ function compareTokenMatches(a, b) {
50
+ return a.tier - b.tier || a.fieldWeight - b.fieldWeight || a.cost - b.cost;
51
+ }
52
+ /** Token's best field by [tier, fieldWeight, tier === 4 ? fuzzyScore : occurrenceStart]. */
53
+ function matchToken(token, fields) {
54
+ let best = null;
55
+ for (const fieldName of FIELD_NAMES) {
56
+ const match = matchField(token, fields[fieldName]);
57
+ if (!match) {
58
+ continue;
59
+ }
60
+ const fieldWeight = match.tier === 0 ? EXACT_FIELD_WEIGHTS[fieldName] : RANKED_FIELD_WEIGHTS[fieldName];
61
+ const candidate = {
62
+ tier: match.tier,
63
+ fieldWeight,
64
+ cost: match.tier === 4 ? match.fuzzyScore : match.occurrenceStart,
65
+ };
66
+ if (!best || compareTokenMatches(candidate, best) < 0) {
67
+ best = candidate;
68
+ }
69
+ }
70
+ return best;
71
+ }
72
+ /** Aggregate a plan's token matches worst-tier-first; null if any token matches no field. */
73
+ function scorePlan(tokens, fields) {
74
+ const score = {
75
+ fuzzyCount: 0,
76
+ substringCount: 0,
77
+ boundarySubstringCount: 0,
78
+ wholeTokenCount: 0,
79
+ fieldCost: 0,
80
+ fuzzyCost: 0,
81
+ positionCost: 0,
82
+ };
83
+ for (const token of tokens) {
84
+ const match = matchToken(token, fields);
85
+ if (!match) {
86
+ return null;
87
+ }
88
+ if (match.tier === 4) {
89
+ score.fuzzyCount += 1;
90
+ score.fuzzyCost += match.cost;
91
+ }
92
+ else if (match.tier === 3) {
93
+ score.substringCount += 1;
94
+ score.positionCost += match.cost;
95
+ }
96
+ else if (match.tier === 2) {
97
+ score.boundarySubstringCount += 1;
98
+ score.positionCost += match.cost;
99
+ }
100
+ else if (match.tier === 1) {
101
+ score.wholeTokenCount += 1;
102
+ score.positionCost += match.cost;
103
+ }
104
+ else {
105
+ score.positionCost += match.cost;
106
+ }
107
+ score.fieldCost += match.fieldWeight;
108
+ }
109
+ return score;
110
+ }
111
+ const PLAN_SCORE_KEYS = [
112
+ "fuzzyCount",
113
+ "substringCount",
114
+ "boundarySubstringCount",
115
+ "wholeTokenCount",
116
+ "fieldCost",
117
+ "fuzzyCost",
118
+ "positionCost",
119
+ ];
120
+ function comparePlanScores(a, b) {
121
+ for (const key of PLAN_SCORE_KEYS) {
122
+ const diff = a[key] - b[key];
123
+ if (diff !== 0) {
124
+ return diff;
125
+ }
126
+ }
127
+ return 0;
128
+ }
129
+ function compareKeys(a, b) {
130
+ for (let i = 0; i < a.length && i < b.length; i++) {
131
+ const diff = a[i] - b[i];
132
+ if (diff !== 0) {
133
+ return diff;
134
+ }
135
+ }
136
+ return a.length - b.length;
137
+ }
138
+ function sameTokens(a, b) {
139
+ return a.length === b.length && a.every((token, index) => token === b[index]);
140
+ }
141
+ /**
142
+ * Filter and rank items by search relevance, optionally partitioning favorites first.
143
+ * Empty trimmed query returns the input unchanged (caller base order is authoritative).
144
+ */
145
+ export function rankModelSearchItems(items, query, getModel, options) {
146
+ const normalizedQuery = query
147
+ .trim()
148
+ .toLowerCase()
149
+ .replace(/\s*\/\s*/g, "/");
150
+ if (!normalizedQuery) {
151
+ return items;
152
+ }
153
+ const compoundTokens = normalizedQuery.split(/\s+/).filter((token) => token.length > 0);
154
+ const legacyTokens = normalizedQuery.split(/[\s/]+/).filter((token) => token.length > 0);
155
+ const plans = sameTokens(compoundTokens, legacyTokens) ? [compoundTokens] : [compoundTokens, legacyTokens];
156
+ const ranked = [];
157
+ for (let index = 0; index < items.length; index++) {
158
+ const item = items[index];
159
+ const model = getModel(item);
160
+ const id = model.id.toLowerCase();
161
+ const provider = model.provider.toLowerCase();
162
+ const fields = {
163
+ id,
164
+ name: (model.name ?? "").toLowerCase(),
165
+ provider,
166
+ providerId: `${provider}/${id}`,
167
+ };
168
+ let bestPlan = null;
169
+ for (const plan of plans) {
170
+ const score = scorePlan(plan, fields);
171
+ if (score && (!bestPlan || comparePlanScores(score, bestPlan) < 0)) {
172
+ bestPlan = score;
173
+ }
174
+ }
175
+ if (!bestPlan) {
176
+ continue;
177
+ }
178
+ const canonicalProviderPathMiss = normalizedQuery === fields.providerId ? 0 : 1;
179
+ const favoritePartition = options.favoritesFirst ? (options.isFavorite(item) ? 0 : 1) : 0;
180
+ ranked.push({
181
+ item,
182
+ key: [
183
+ canonicalProviderPathMiss,
184
+ favoritePartition,
185
+ bestPlan.fuzzyCount,
186
+ bestPlan.substringCount,
187
+ bestPlan.boundarySubstringCount,
188
+ bestPlan.wholeTokenCount,
189
+ bestPlan.fieldCost,
190
+ bestPlan.fuzzyCost,
191
+ bestPlan.positionCost,
192
+ id.length,
193
+ index,
194
+ ],
195
+ });
196
+ }
197
+ ranked.sort((a, b) => compareKeys(a.key, b.key));
198
+ return ranked.map((entry) => entry.item);
199
+ }
200
+ //# sourceMappingURL=model-search-rank.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-search-rank.js","sourceRoot":"","sources":["../../../src/modes/interactive/model-search-rank.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Favorites-aware relevance ranking for model search.\n *\n * Implements .omo/drafts/model-selector-favorites-search-ux-ranking-spec.md:\n * dual query token plans (compound keeps slashes, legacy splits them), a\n * per-token tier ladder over independently matched lowercased fields,\n * worst-tier-first aggregation, and a composite sort key whose canonical\n * provider-path and favorites partitions precede all relevance costs.\n */\nimport { fuzzyMatch } from \"@earendil-works/pi-tui\";\nimport type { ModelSearchItem } from \"./model-search.ts\";\n\ntype FieldName = \"id\" | \"name\" | \"provider\" | \"providerId\";\n\nconst FIELD_NAMES: readonly FieldName[] = [\"id\", \"name\", \"provider\", \"providerId\"];\n\n/** Exact tier: provider/id < id < provider < name (direct provider/id beats proxy id). */\nconst EXACT_FIELD_WEIGHTS: Record<FieldName, number> = { providerId: 0, id: 1, provider: 2, name: 3 };\n/** Tiers 1-4: id < name < provider < provider/id. */\nconst RANKED_FIELD_WEIGHTS: Record<FieldName, number> = { id: 0, name: 1, provider: 2, providerId: 3 };\n\ninterface FieldMatch {\n\ttier: number;\n\toccurrenceStart: number;\n\tfuzzyScore: number;\n}\n\ninterface TokenMatch {\n\ttier: number;\n\tfieldWeight: number;\n\t/** fuzzyScore for tier 4, occurrenceStart otherwise. */\n\tcost: number;\n}\n\ninterface PlanScore {\n\tfuzzyCount: number;\n\tsubstringCount: number;\n\tboundarySubstringCount: number;\n\twholeTokenCount: number;\n\tfieldCost: number;\n\tfuzzyCost: number;\n\tpositionCost: number;\n}\n\nexport interface RankModelSearchOptions<T> {\n\t/** When true, favorite matches partition above non-favorites (relevance preserved within each partition). */\n\tfavoritesFirst: boolean;\n\t/** Callers with a null favorite-ids sentinel return true for all items, making the partition a no-op. */\n\tisFavorite: (item: T) => boolean;\n}\n\nfunction isAlphanumeric(char: string | undefined): boolean {\n\tif (!char) {\n\t\treturn false;\n\t}\n\tconst code = char.charCodeAt(0);\n\treturn (code >= 97 && code <= 122) || (code >= 48 && code <= 57);\n}\n\n/** Best tier for a token inside one field; multiple occurrences pick best tier, then earliest. */\nfunction matchField(token: string, field: string): FieldMatch | null {\n\tif (field.length === 0) {\n\t\treturn null;\n\t}\n\tif (field === token) {\n\t\treturn { tier: 0, occurrenceStart: 0, fuzzyScore: 0 };\n\t}\n\tlet best: FieldMatch | null = null;\n\tlet index = field.indexOf(token);\n\twhile (index !== -1) {\n\t\tconst leftBoundary = index === 0 || !isAlphanumeric(field[index - 1]);\n\t\tconst end = index + token.length;\n\t\tconst rightBoundary = end === field.length || !isAlphanumeric(field[end]);\n\t\tconst tier = leftBoundary && rightBoundary ? 1 : leftBoundary || rightBoundary ? 2 : 3;\n\t\tif (!best || tier < best.tier) {\n\t\t\tbest = { tier, occurrenceStart: index, fuzzyScore: 0 };\n\t\t}\n\t\tindex = field.indexOf(token, index + 1);\n\t}\n\tif (best) {\n\t\treturn best;\n\t}\n\tconst fuzzy = fuzzyMatch(token, field);\n\treturn fuzzy.matches ? { tier: 4, occurrenceStart: 0, fuzzyScore: fuzzy.score } : null;\n}\n\nfunction compareTokenMatches(a: TokenMatch, b: TokenMatch): number {\n\treturn a.tier - b.tier || a.fieldWeight - b.fieldWeight || a.cost - b.cost;\n}\n\n/** Token's best field by [tier, fieldWeight, tier === 4 ? fuzzyScore : occurrenceStart]. */\nfunction matchToken(token: string, fields: Record<FieldName, string>): TokenMatch | null {\n\tlet best: TokenMatch | null = null;\n\tfor (const fieldName of FIELD_NAMES) {\n\t\tconst match = matchField(token, fields[fieldName]);\n\t\tif (!match) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst fieldWeight = match.tier === 0 ? EXACT_FIELD_WEIGHTS[fieldName] : RANKED_FIELD_WEIGHTS[fieldName];\n\t\tconst candidate: TokenMatch = {\n\t\t\ttier: match.tier,\n\t\t\tfieldWeight,\n\t\t\tcost: match.tier === 4 ? match.fuzzyScore : match.occurrenceStart,\n\t\t};\n\t\tif (!best || compareTokenMatches(candidate, best) < 0) {\n\t\t\tbest = candidate;\n\t\t}\n\t}\n\treturn best;\n}\n\n/** Aggregate a plan's token matches worst-tier-first; null if any token matches no field. */\nfunction scorePlan(tokens: string[], fields: Record<FieldName, string>): PlanScore | null {\n\tconst score: PlanScore = {\n\t\tfuzzyCount: 0,\n\t\tsubstringCount: 0,\n\t\tboundarySubstringCount: 0,\n\t\twholeTokenCount: 0,\n\t\tfieldCost: 0,\n\t\tfuzzyCost: 0,\n\t\tpositionCost: 0,\n\t};\n\tfor (const token of tokens) {\n\t\tconst match = matchToken(token, fields);\n\t\tif (!match) {\n\t\t\treturn null;\n\t\t}\n\t\tif (match.tier === 4) {\n\t\t\tscore.fuzzyCount += 1;\n\t\t\tscore.fuzzyCost += match.cost;\n\t\t} else if (match.tier === 3) {\n\t\t\tscore.substringCount += 1;\n\t\t\tscore.positionCost += match.cost;\n\t\t} else if (match.tier === 2) {\n\t\t\tscore.boundarySubstringCount += 1;\n\t\t\tscore.positionCost += match.cost;\n\t\t} else if (match.tier === 1) {\n\t\t\tscore.wholeTokenCount += 1;\n\t\t\tscore.positionCost += match.cost;\n\t\t} else {\n\t\t\tscore.positionCost += match.cost;\n\t\t}\n\t\tscore.fieldCost += match.fieldWeight;\n\t}\n\treturn score;\n}\n\nconst PLAN_SCORE_KEYS = [\n\t\"fuzzyCount\",\n\t\"substringCount\",\n\t\"boundarySubstringCount\",\n\t\"wholeTokenCount\",\n\t\"fieldCost\",\n\t\"fuzzyCost\",\n\t\"positionCost\",\n] as const;\n\nfunction comparePlanScores(a: PlanScore, b: PlanScore): number {\n\tfor (const key of PLAN_SCORE_KEYS) {\n\t\tconst diff = a[key] - b[key];\n\t\tif (diff !== 0) {\n\t\t\treturn diff;\n\t\t}\n\t}\n\treturn 0;\n}\n\nfunction compareKeys(a: readonly number[], b: readonly number[]): number {\n\tfor (let i = 0; i < a.length && i < b.length; i++) {\n\t\tconst diff = a[i]! - b[i]!;\n\t\tif (diff !== 0) {\n\t\t\treturn diff;\n\t\t}\n\t}\n\treturn a.length - b.length;\n}\n\nfunction sameTokens(a: readonly string[], b: readonly string[]): boolean {\n\treturn a.length === b.length && a.every((token, index) => token === b[index]);\n}\n\n/**\n * Filter and rank items by search relevance, optionally partitioning favorites first.\n * Empty trimmed query returns the input unchanged (caller base order is authoritative).\n */\nexport function rankModelSearchItems<T>(\n\titems: T[],\n\tquery: string,\n\tgetModel: (item: T) => ModelSearchItem,\n\toptions: RankModelSearchOptions<T>,\n): T[] {\n\tconst normalizedQuery = query\n\t\t.trim()\n\t\t.toLowerCase()\n\t\t.replace(/\\s*\\/\\s*/g, \"/\");\n\tif (!normalizedQuery) {\n\t\treturn items;\n\t}\n\n\tconst compoundTokens = normalizedQuery.split(/\\s+/).filter((token) => token.length > 0);\n\tconst legacyTokens = normalizedQuery.split(/[\\s/]+/).filter((token) => token.length > 0);\n\tconst plans = sameTokens(compoundTokens, legacyTokens) ? [compoundTokens] : [compoundTokens, legacyTokens];\n\n\tconst ranked: { item: T; key: number[] }[] = [];\n\tfor (let index = 0; index < items.length; index++) {\n\t\tconst item = items[index]!;\n\t\tconst model = getModel(item);\n\t\tconst id = model.id.toLowerCase();\n\t\tconst provider = model.provider.toLowerCase();\n\t\tconst fields: Record<FieldName, string> = {\n\t\t\tid,\n\t\t\tname: (model.name ?? \"\").toLowerCase(),\n\t\t\tprovider,\n\t\t\tproviderId: `${provider}/${id}`,\n\t\t};\n\t\tlet bestPlan: PlanScore | null = null;\n\t\tfor (const plan of plans) {\n\t\t\tconst score = scorePlan(plan, fields);\n\t\t\tif (score && (!bestPlan || comparePlanScores(score, bestPlan) < 0)) {\n\t\t\t\tbestPlan = score;\n\t\t\t}\n\t\t}\n\t\tif (!bestPlan) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst canonicalProviderPathMiss = normalizedQuery === fields.providerId ? 0 : 1;\n\t\tconst favoritePartition = options.favoritesFirst ? (options.isFavorite(item) ? 0 : 1) : 0;\n\t\tranked.push({\n\t\t\titem,\n\t\t\tkey: [\n\t\t\t\tcanonicalProviderPathMiss,\n\t\t\t\tfavoritePartition,\n\t\t\t\tbestPlan.fuzzyCount,\n\t\t\t\tbestPlan.substringCount,\n\t\t\t\tbestPlan.boundarySubstringCount,\n\t\t\t\tbestPlan.wholeTokenCount,\n\t\t\t\tbestPlan.fieldCost,\n\t\t\t\tbestPlan.fuzzyCost,\n\t\t\t\tbestPlan.positionCost,\n\t\t\t\tid.length,\n\t\t\t\tindex,\n\t\t\t],\n\t\t});\n\t}\n\tranked.sort((a, b) => compareKeys(a.key, b.key));\n\treturn ranked.map((entry) => entry.item);\n}\n"]}
@@ -4,9 +4,4 @@ export interface ModelSearchItem {
4
4
  name?: string;
5
5
  }
6
6
  export declare function getModelSearchText(item: ModelSearchItem): string;
7
- /**
8
- * The /model selector search should rank exact provider-prefixed queries before proxy-provider IDs
9
- * like openrouter/openai/gpt-5, so keep the bare model ID out of the leading position.
10
- */
11
- export declare function getModelSelectorSearchText(item: ModelSearchItem): string;
12
7
  //# sourceMappingURL=model-search.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-search.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/model-search.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAIhE;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAIxE"}
1
+ {"version":3,"file":"model-search.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/model-search.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,CAIhE"}
@@ -3,13 +3,4 @@ export function getModelSearchText(item) {
3
3
  const name = item.name ? ` ${item.name}` : "";
4
4
  return `${id} ${provider} ${provider}/${id} ${provider} ${id}${name}`;
5
5
  }
6
- /**
7
- * The /model selector search should rank exact provider-prefixed queries before proxy-provider IDs
8
- * like openrouter/openai/gpt-5, so keep the bare model ID out of the leading position.
9
- */
10
- export function getModelSelectorSearchText(item) {
11
- const { id, provider } = item;
12
- const name = item.name ? ` ${item.name}` : "";
13
- return `${provider} ${provider}/${id} ${provider} ${id}${name}`;
14
- }
15
6
  //# sourceMappingURL=model-search.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"model-search.js","sourceRoot":"","sources":["../../../src/modes/interactive/model-search.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,kBAAkB,CAAC,IAAqB;IACvD,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9C,OAAO,GAAG,EAAE,IAAI,QAAQ,IAAI,QAAQ,IAAI,EAAE,IAAI,QAAQ,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC;AACvE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,0BAA0B,CAAC,IAAqB;IAC/D,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9C,OAAO,GAAG,QAAQ,IAAI,QAAQ,IAAI,EAAE,IAAI,QAAQ,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC;AACjE,CAAC","sourcesContent":["export interface ModelSearchItem {\n\tid: string;\n\tprovider: string;\n\tname?: string;\n}\n\nexport function getModelSearchText(item: ModelSearchItem): string {\n\tconst { id, provider } = item;\n\tconst name = item.name ? ` ${item.name}` : \"\";\n\treturn `${id} ${provider} ${provider}/${id} ${provider} ${id}${name}`;\n}\n\n/**\n * The /model selector search should rank exact provider-prefixed queries before proxy-provider IDs\n * like openrouter/openai/gpt-5, so keep the bare model ID out of the leading position.\n */\nexport function getModelSelectorSearchText(item: ModelSearchItem): string {\n\tconst { id, provider } = item;\n\tconst name = item.name ? ` ${item.name}` : \"\";\n\treturn `${provider} ${provider}/${id} ${provider} ${id}${name}`;\n}\n"]}
1
+ {"version":3,"file":"model-search.js","sourceRoot":"","sources":["../../../src/modes/interactive/model-search.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,kBAAkB,CAAC,IAAqB;IACvD,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9C,OAAO,GAAG,EAAE,IAAI,QAAQ,IAAI,QAAQ,IAAI,EAAE,IAAI,QAAQ,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC;AACvE,CAAC","sourcesContent":["export interface ModelSearchItem {\n\tid: string;\n\tprovider: string;\n\tname?: string;\n}\n\nexport function getModelSearchText(item: ModelSearchItem): string {\n\tconst { id, provider } = item;\n\tconst name = item.name ? ` ${item.name}` : \"\";\n\treturn `${id} ${provider} ${provider}/${id} ${provider} ${id}${name}`;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"session-command-router.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/session-command-router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAO9D,OAAO,EAAE,uBAAuB,EAA0B,MAAM,sBAAsB,CAAC;AACvF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AASzF,uFAAuF;AACvF,qBAAa,oBAAoB;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAGvB;IACF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAiC;IAE/D,YACC,QAAQ,EAAE,kBAAkB,EAC5B,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,IAAI,CAAC,uBAAuB,EAAE,KAAK,GAAG,kBAAkB,GAAG,eAAe,GAAG,sBAAsB,CAAC,EAC9G,aAAa,GAAE,OAAO,uBAAiD,EAMvE;IAEK,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA6BlE;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAiB7B;YAEa,IAAI;YA6DJ,KAAK;IAwBnB,OAAO,CAAC,IAAI;CASZ"}
1
+ {"version":3,"file":"session-command-router.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/session-command-router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAO9D,OAAO,EAAE,uBAAuB,EAA0B,MAAM,sBAAsB,CAAC;AACvF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AASzF,uFAAuF;AACvF,qBAAa,oBAAoB;IAChC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAwC;IACjE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAGvB;IACF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAiC;IAE/D,YACC,QAAQ,EAAE,kBAAkB,EAC5B,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,IAAI,CAAC,uBAAuB,EAAE,KAAK,GAAG,kBAAkB,GAAG,eAAe,GAAG,sBAAsB,CAAC,EAC9G,aAAa,GAAE,OAAO,uBAAiD,EAMvE;IAEK,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CA6BlE;IAEK,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAiB7B;YAEa,IAAI;YA6DJ,KAAK;IAwBnB,OAAO,CAAC,IAAI;CAWZ"}
@@ -146,8 +146,9 @@ export class SessionCommandRouter {
146
146
  }
147
147
  }
148
148
  code(cause) {
149
- if (cause instanceof RpcSessionRegistryError)
150
- return cause.code;
149
+ if (cause instanceof RpcSessionRegistryError) {
150
+ return cause.code === RPC_ERROR_OPEN_FAILED ? cause.message : cause.code;
151
+ }
151
152
  if (cause instanceof Error && [RPC_ERROR_UNKNOWN_SESSION, RPC_ERROR_SESSION_CLOSING].includes(cause.message)) {
152
153
  return cause.message;
153
154
  }
@@ -1 +1 @@
1
- {"version":3,"file":"session-command-router.js","sourceRoot":"","sources":["../../../src/modes/rpc/session-command-router.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RpcCommand, RpcResponse } from \"./rpc-types.ts\";\nimport {\n\tRPC_ERROR_MISSING_SESSION_ID,\n\tRPC_ERROR_OPEN_FAILED,\n\tRPC_ERROR_SESSION_CLOSING,\n\tRPC_ERROR_UNKNOWN_SESSION,\n} from \"./rpc-types.ts\";\nimport { createRpcSessionBinding, type RpcSessionBinding } from \"./session-binding.ts\";\nimport type { SessionEventWriter } from \"./session-event-writer.ts\";\nimport type { RpcSessionLaunchProfile, RpcSessionRegistry } from \"./session-registry.ts\";\nimport { RpcSessionRegistryError } from \"./session-registry.ts\";\n\nconst controls = new Set([\"get_protocol_info\", \"open_session\", \"close_session\", \"list_sessions\"]);\n\nfunction error(id: string | undefined, command: string, code: string): RpcResponse {\n\treturn { id, type: \"response\", command, success: false, error: code };\n}\n\n/** Routes control messages and enforces a routing handle for every session command. */\nexport class SessionCommandRouter {\n\tprivate readonly bindings = new Map<string, RpcSessionBinding>();\n\tprivate readonly registry: RpcSessionRegistry;\n\tprivate readonly writer: SessionEventWriter;\n\tprivate readonly defaults: Pick<\n\t\tRpcSessionLaunchProfile,\n\t\t\"cwd\" | \"permissionPreset\" | \"creationModel\" | \"initialThinkingLevel\"\n\t>;\n\tprivate readonly createBinding: typeof createRpcSessionBinding;\n\n\tconstructor(\n\t\tregistry: RpcSessionRegistry,\n\t\twriter: SessionEventWriter,\n\t\tdefaults: Pick<RpcSessionLaunchProfile, \"cwd\" | \"permissionPreset\" | \"creationModel\" | \"initialThinkingLevel\">,\n\t\tcreateBinding: typeof createRpcSessionBinding = createRpcSessionBinding,\n\t) {\n\t\tthis.registry = registry;\n\t\tthis.writer = writer;\n\t\tthis.defaults = defaults;\n\t\tthis.createBinding = createBinding;\n\t}\n\n\tasync handle(command: RpcCommand): Promise<RpcResponse | undefined> {\n\t\tif (command.type === \"get_protocol_info\") {\n\t\t\treturn {\n\t\t\t\tid: command.id,\n\t\t\t\ttype: \"response\",\n\t\t\t\tcommand: \"get_protocol_info\",\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: { protocolVersion: 1, capabilities: [\"multi_session\"], mode: \"multi\" },\n\t\t\t};\n\t\t}\n\t\tif (command.type === \"list_sessions\")\n\t\t\treturn {\n\t\t\t\tid: command.id,\n\t\t\t\ttype: \"response\",\n\t\t\t\tcommand: \"list_sessions\",\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: { sessions: this.registry.list() },\n\t\t\t};\n\t\tif (command.type === \"open_session\") return this.open(command);\n\t\tif (command.type === \"close_session\") return this.close(command);\n\t\tif (controls.has(command.type)) return undefined;\n\t\tif (!command.sessionId) return error(command.id, command.type, RPC_ERROR_MISSING_SESSION_ID);\n\t\ttry {\n\t\t\tthis.registry.getForCommand(command.sessionId, command.type);\n\t\t\tawait this.bindings.get(command.sessionId)?.handle(command);\n\t\t\treturn undefined;\n\t\t} catch (cause) {\n\t\t\treturn error(command.id, command.type, this.code(cause));\n\t\t}\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tawait Promise.all(\n\t\t\t[...this.bindings.entries()].map(async ([sessionId, binding]) => {\n\t\t\t\ttry {\n\t\t\t\t\tthis.registry.beginClose(sessionId);\n\t\t\t\t} catch {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tawait binding.dispose();\n\t\t\t\t} finally {\n\t\t\t\t\tthis.bindings.delete(sessionId);\n\t\t\t\t\tawait this.registry.closeMarked(sessionId);\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t\tthis.bindings.clear();\n\t}\n\n\tprivate async open(command: Extract<RpcCommand, { type: \"open_session\" }>): Promise<RpcResponse | undefined> {\n\t\tlet opened: { sessionId: string } | undefined;\n\t\ttry {\n\t\t\topened = await this.registry.openSession({\n\t\t\t\tcwd: command.cwd ?? this.defaults.cwd,\n\t\t\t\tsessionPath: command.sessionPath,\n\t\t\t\tpermissionPreset: command.permissionPreset ?? this.defaults.permissionPreset,\n\t\t\t\tcreationModel:\n\t\t\t\t\tcommand.provider && command.modelId\n\t\t\t\t\t\t? { provider: command.provider, modelId: command.modelId }\n\t\t\t\t\t\t: this.defaults.creationModel,\n\t\t\t\tinitialThinkingLevel: command.thinkingLevel ?? this.defaults.initialThinkingLevel,\n\t\t\t});\n\t\t\tconst openedSession = opened;\n\t\t\tconst entry = this.registry.getForCommand(openedSession.sessionId, \"open_session\");\n\t\t\tthis.bindings.set(\n\t\t\t\topenedSession.sessionId,\n\t\t\t\tawait this.createBinding(\n\t\t\t\t\topenedSession.sessionId,\n\t\t\t\t\tentry,\n\t\t\t\t\tthis.writer,\n\t\t\t\t\t() => void this.close({ type: \"close_session\", sessionId: openedSession.sessionId }),\n\t\t\t\t),\n\t\t\t);\n\t\t\tconst state = entry.runtime!.session;\n\t\t\tthis.writer.enqueue(opened.sessionId, {\n\t\t\t\tid: command.id,\n\t\t\t\ttype: \"response\",\n\t\t\t\tcommand: \"open_session\",\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: {\n\t\t\t\t\tsessionId: opened.sessionId,\n\t\t\t\t\tstate: {\n\t\t\t\t\t\tmodel: state.model,\n\t\t\t\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\t\t\t\tisStreaming: state.isStreaming,\n\t\t\t\t\t\tisCompacting: state.isCompacting,\n\t\t\t\t\t\tsteeringMode: state.steeringMode,\n\t\t\t\t\t\tfollowUpMode: state.followUpMode,\n\t\t\t\t\t\tsessionFile: state.sessionFile,\n\t\t\t\t\t\tsessionId: state.sessionId,\n\t\t\t\t\t\tsessionName: state.sessionName,\n\t\t\t\t\t\tautoCompactionEnabled: state.autoCompactionEnabled,\n\t\t\t\t\t\tmessageCount: state.messages.length,\n\t\t\t\t\t\tpendingMessageCount: state.pendingMessageCount,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn undefined;\n\t\t} catch (cause) {\n\t\t\tif (opened) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.registry.close(opened.sessionId);\n\t\t\t\t} catch {\n\t\t\t\t\t/* The open rollback has already removed the entry. */\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn error(command.id, \"open_session\", this.code(cause));\n\t\t}\n\t}\n\n\tprivate async close(command: Extract<RpcCommand, { type: \"close_session\" }>): Promise<RpcResponse | undefined> {\n\t\ttry {\n\t\t\t// This must be the first operation: binding.dispose() awaits teardown and\n\t\t\t// otherwise leaves a window where commands can enter the old handler.\n\t\t\tthis.registry.beginClose(command.sessionId);\n\t\t\ttry {\n\t\t\t\tawait this.bindings.get(command.sessionId)?.dispose();\n\t\t\t} finally {\n\t\t\t\tthis.bindings.delete(command.sessionId);\n\t\t\t\tawait this.registry.closeMarked(command.sessionId);\n\t\t\t}\n\t\t\tthis.writer.closeSession(command.sessionId, {\n\t\t\t\tid: command.id,\n\t\t\t\ttype: \"response\",\n\t\t\t\tcommand: \"close_session\",\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: {},\n\t\t\t});\n\t\t\treturn undefined;\n\t\t} catch (cause) {\n\t\t\treturn error(command.id, \"close_session\", this.code(cause));\n\t\t}\n\t}\n\n\tprivate code(cause: unknown): string {\n\t\tif (cause instanceof RpcSessionRegistryError) return cause.code;\n\t\tif (cause instanceof Error && [RPC_ERROR_UNKNOWN_SESSION, RPC_ERROR_SESSION_CLOSING].includes(cause.message)) {\n\t\t\treturn cause.message;\n\t\t}\n\t\treturn cause instanceof Error && cause.message\n\t\t\t? `${RPC_ERROR_OPEN_FAILED}: ${cause.message}`\n\t\t\t: RPC_ERROR_UNKNOWN_SESSION;\n\t}\n}\n"]}
1
+ {"version":3,"file":"session-command-router.js","sourceRoot":"","sources":["../../../src/modes/rpc/session-command-router.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RpcCommand, RpcResponse } from \"./rpc-types.ts\";\nimport {\n\tRPC_ERROR_MISSING_SESSION_ID,\n\tRPC_ERROR_OPEN_FAILED,\n\tRPC_ERROR_SESSION_CLOSING,\n\tRPC_ERROR_UNKNOWN_SESSION,\n} from \"./rpc-types.ts\";\nimport { createRpcSessionBinding, type RpcSessionBinding } from \"./session-binding.ts\";\nimport type { SessionEventWriter } from \"./session-event-writer.ts\";\nimport type { RpcSessionLaunchProfile, RpcSessionRegistry } from \"./session-registry.ts\";\nimport { RpcSessionRegistryError } from \"./session-registry.ts\";\n\nconst controls = new Set([\"get_protocol_info\", \"open_session\", \"close_session\", \"list_sessions\"]);\n\nfunction error(id: string | undefined, command: string, code: string): RpcResponse {\n\treturn { id, type: \"response\", command, success: false, error: code };\n}\n\n/** Routes control messages and enforces a routing handle for every session command. */\nexport class SessionCommandRouter {\n\tprivate readonly bindings = new Map<string, RpcSessionBinding>();\n\tprivate readonly registry: RpcSessionRegistry;\n\tprivate readonly writer: SessionEventWriter;\n\tprivate readonly defaults: Pick<\n\t\tRpcSessionLaunchProfile,\n\t\t\"cwd\" | \"permissionPreset\" | \"creationModel\" | \"initialThinkingLevel\"\n\t>;\n\tprivate readonly createBinding: typeof createRpcSessionBinding;\n\n\tconstructor(\n\t\tregistry: RpcSessionRegistry,\n\t\twriter: SessionEventWriter,\n\t\tdefaults: Pick<RpcSessionLaunchProfile, \"cwd\" | \"permissionPreset\" | \"creationModel\" | \"initialThinkingLevel\">,\n\t\tcreateBinding: typeof createRpcSessionBinding = createRpcSessionBinding,\n\t) {\n\t\tthis.registry = registry;\n\t\tthis.writer = writer;\n\t\tthis.defaults = defaults;\n\t\tthis.createBinding = createBinding;\n\t}\n\n\tasync handle(command: RpcCommand): Promise<RpcResponse | undefined> {\n\t\tif (command.type === \"get_protocol_info\") {\n\t\t\treturn {\n\t\t\t\tid: command.id,\n\t\t\t\ttype: \"response\",\n\t\t\t\tcommand: \"get_protocol_info\",\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: { protocolVersion: 1, capabilities: [\"multi_session\"], mode: \"multi\" },\n\t\t\t};\n\t\t}\n\t\tif (command.type === \"list_sessions\")\n\t\t\treturn {\n\t\t\t\tid: command.id,\n\t\t\t\ttype: \"response\",\n\t\t\t\tcommand: \"list_sessions\",\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: { sessions: this.registry.list() },\n\t\t\t};\n\t\tif (command.type === \"open_session\") return this.open(command);\n\t\tif (command.type === \"close_session\") return this.close(command);\n\t\tif (controls.has(command.type)) return undefined;\n\t\tif (!command.sessionId) return error(command.id, command.type, RPC_ERROR_MISSING_SESSION_ID);\n\t\ttry {\n\t\t\tthis.registry.getForCommand(command.sessionId, command.type);\n\t\t\tawait this.bindings.get(command.sessionId)?.handle(command);\n\t\t\treturn undefined;\n\t\t} catch (cause) {\n\t\t\treturn error(command.id, command.type, this.code(cause));\n\t\t}\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tawait Promise.all(\n\t\t\t[...this.bindings.entries()].map(async ([sessionId, binding]) => {\n\t\t\t\ttry {\n\t\t\t\t\tthis.registry.beginClose(sessionId);\n\t\t\t\t} catch {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tawait binding.dispose();\n\t\t\t\t} finally {\n\t\t\t\t\tthis.bindings.delete(sessionId);\n\t\t\t\t\tawait this.registry.closeMarked(sessionId);\n\t\t\t\t}\n\t\t\t}),\n\t\t);\n\t\tthis.bindings.clear();\n\t}\n\n\tprivate async open(command: Extract<RpcCommand, { type: \"open_session\" }>): Promise<RpcResponse | undefined> {\n\t\tlet opened: { sessionId: string } | undefined;\n\t\ttry {\n\t\t\topened = await this.registry.openSession({\n\t\t\t\tcwd: command.cwd ?? this.defaults.cwd,\n\t\t\t\tsessionPath: command.sessionPath,\n\t\t\t\tpermissionPreset: command.permissionPreset ?? this.defaults.permissionPreset,\n\t\t\t\tcreationModel:\n\t\t\t\t\tcommand.provider && command.modelId\n\t\t\t\t\t\t? { provider: command.provider, modelId: command.modelId }\n\t\t\t\t\t\t: this.defaults.creationModel,\n\t\t\t\tinitialThinkingLevel: command.thinkingLevel ?? this.defaults.initialThinkingLevel,\n\t\t\t});\n\t\t\tconst openedSession = opened;\n\t\t\tconst entry = this.registry.getForCommand(openedSession.sessionId, \"open_session\");\n\t\t\tthis.bindings.set(\n\t\t\t\topenedSession.sessionId,\n\t\t\t\tawait this.createBinding(\n\t\t\t\t\topenedSession.sessionId,\n\t\t\t\t\tentry,\n\t\t\t\t\tthis.writer,\n\t\t\t\t\t() => void this.close({ type: \"close_session\", sessionId: openedSession.sessionId }),\n\t\t\t\t),\n\t\t\t);\n\t\t\tconst state = entry.runtime!.session;\n\t\t\tthis.writer.enqueue(opened.sessionId, {\n\t\t\t\tid: command.id,\n\t\t\t\ttype: \"response\",\n\t\t\t\tcommand: \"open_session\",\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: {\n\t\t\t\t\tsessionId: opened.sessionId,\n\t\t\t\t\tstate: {\n\t\t\t\t\t\tmodel: state.model,\n\t\t\t\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\t\t\t\tisStreaming: state.isStreaming,\n\t\t\t\t\t\tisCompacting: state.isCompacting,\n\t\t\t\t\t\tsteeringMode: state.steeringMode,\n\t\t\t\t\t\tfollowUpMode: state.followUpMode,\n\t\t\t\t\t\tsessionFile: state.sessionFile,\n\t\t\t\t\t\tsessionId: state.sessionId,\n\t\t\t\t\t\tsessionName: state.sessionName,\n\t\t\t\t\t\tautoCompactionEnabled: state.autoCompactionEnabled,\n\t\t\t\t\t\tmessageCount: state.messages.length,\n\t\t\t\t\t\tpendingMessageCount: state.pendingMessageCount,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn undefined;\n\t\t} catch (cause) {\n\t\t\tif (opened) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.registry.close(opened.sessionId);\n\t\t\t\t} catch {\n\t\t\t\t\t/* The open rollback has already removed the entry. */\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn error(command.id, \"open_session\", this.code(cause));\n\t\t}\n\t}\n\n\tprivate async close(command: Extract<RpcCommand, { type: \"close_session\" }>): Promise<RpcResponse | undefined> {\n\t\ttry {\n\t\t\t// This must be the first operation: binding.dispose() awaits teardown and\n\t\t\t// otherwise leaves a window where commands can enter the old handler.\n\t\t\tthis.registry.beginClose(command.sessionId);\n\t\t\ttry {\n\t\t\t\tawait this.bindings.get(command.sessionId)?.dispose();\n\t\t\t} finally {\n\t\t\t\tthis.bindings.delete(command.sessionId);\n\t\t\t\tawait this.registry.closeMarked(command.sessionId);\n\t\t\t}\n\t\t\tthis.writer.closeSession(command.sessionId, {\n\t\t\t\tid: command.id,\n\t\t\t\ttype: \"response\",\n\t\t\t\tcommand: \"close_session\",\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: {},\n\t\t\t});\n\t\t\treturn undefined;\n\t\t} catch (cause) {\n\t\t\treturn error(command.id, \"close_session\", this.code(cause));\n\t\t}\n\t}\n\n\tprivate code(cause: unknown): string {\n\t\tif (cause instanceof RpcSessionRegistryError) {\n\t\t\treturn cause.code === RPC_ERROR_OPEN_FAILED ? cause.message : cause.code;\n\t\t}\n\t\tif (cause instanceof Error && [RPC_ERROR_UNKNOWN_SESSION, RPC_ERROR_SESSION_CLOSING].includes(cause.message)) {\n\t\t\treturn cause.message;\n\t\t}\n\t\treturn cause instanceof Error && cause.message\n\t\t\t? `${RPC_ERROR_OPEN_FAILED}: ${cause.message}`\n\t\t\t: RPC_ERROR_UNKNOWN_SESSION;\n\t}\n}\n"]}
@@ -17,7 +17,7 @@ export interface RpcSessionEntry {
17
17
  }
18
18
  export declare class RpcSessionRegistryError extends Error {
19
19
  readonly code: "unknown_session" | "session_closing" | "session_path_in_use" | "invalid_path" | "open_failed";
20
- constructor(code: RpcSessionRegistryError["code"]);
20
+ constructor(code: RpcSessionRegistryError["code"], reason?: string);
21
21
  }
22
22
  export interface RpcSessionRegistryOptions {
23
23
  agentDir: string;
@@ -1 +1 @@
1
- {"version":3,"file":"session-registry.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/session-registry.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAwB,MAAM,2CAA2C,CAAC;AAChG,OAAO,EACN,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,KAAK,gCAAgC,EAErC,MAAM,qCAAqC,CAAC;AAG7C,qEAAqE;AACrE,MAAM,WAAW,uBAAwB,SAAQ,yBAAyB;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC;AAC9E,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,QAAQ,CAAC,uBAAuB,CAAC,CAAC;IAC3C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,qBAAqB,GAAG,cAAc,GAAG,aAAa,CAAC;IAE9G,YAAY,IAAI,EAAE,uBAAuB,CAAC,MAAM,CAAC,EAIhD;CACD;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,gCAAgC,CAAC;CAChD;AAED,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAeD,oEAAoE;AACpE,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IAEpD,YAAY,OAAO,EAAE,yBAAyB,EAE7C;IAEK,WAAW,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,cAAc,CAAC,CA6D3E;IAED,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,CAQ9D;IAED;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAK1C;IAEK,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzC;IAED,iEAAiE;IAC3D,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB/C;IAED,IAAI,IAAI,KAAK,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,eAAe,CAAC;KACxB,CAAC,CASD;IAED,OAAO,CAAC,eAAe;CAKvB"}
1
+ {"version":3,"file":"session-registry.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/session-registry.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAwB,MAAM,2CAA2C,CAAC;AAChG,OAAO,EACN,KAAK,yBAAyB,EAC9B,KAAK,mBAAmB,EACxB,KAAK,gCAAgC,EAErC,MAAM,qCAAqC,CAAC;AAG7C,qEAAqE;AACrE,MAAM,WAAW,uBAAwB,SAAQ,yBAAyB;IACzE,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,mBAAmB,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC;AAC9E,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;AAExE,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,eAAe,CAAC;IACvB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,QAAQ,CAAC,uBAAuB,CAAC,CAAC;IAC3C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED,qBAAa,uBAAwB,SAAQ,KAAK;IACjD,QAAQ,CAAC,IAAI,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,qBAAqB,GAAG,cAAc,GAAG,aAAa,CAAC;IAE9G,YAAY,IAAI,EAAE,uBAAuB,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,EAIjE;CACD;AAED,MAAM,WAAW,yBAAyB;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,gCAAgC,CAAC;CAChD;AAED,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,gBAAgB,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAeD,oEAAoE;AACpE,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;IAC9D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IAEpD,YAAY,OAAO,EAAE,yBAAyB,EAE7C;IAEK,WAAW,CAAC,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,cAAc,CAAC,CA6D3E;IAED,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,eAAe,CAQ9D;IAED;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,CAK1C;IAEK,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAGzC;IAED,iEAAiE;IAC3D,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB/C;IAED,IAAI,IAAI,KAAK,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,eAAe,CAAC;KACxB,CAAC,CASD;IAED,OAAO,CAAC,eAAe;CAKvB"}
@@ -4,8 +4,8 @@ import { ProviderScope, runWithProviderScope } from "@earendil-works/pi-ai/node/
4
4
  import { createAgentSessionRuntime, } from "../../core/agent-session-runtime.js";
5
5
  import { SessionManager } from "../../core/session-manager.js";
6
6
  export class RpcSessionRegistryError extends Error {
7
- constructor(code) {
8
- super(code);
7
+ constructor(code, reason) {
8
+ super(code === "open_failed" && reason ? `${code}: ${reason}` : code);
9
9
  this.code = code;
10
10
  this.name = "RpcSessionRegistryError";
11
11
  }
@@ -93,7 +93,7 @@ export class RpcSessionRegistry {
93
93
  }
94
94
  if (error instanceof RpcSessionRegistryError)
95
95
  throw error;
96
- throw new RpcSessionRegistryError("open_failed");
96
+ throw new RpcSessionRegistryError("open_failed", error instanceof Error ? error.message : undefined);
97
97
  }
98
98
  }
99
99
  getForCommand(handle, command) {
@@ -1 +1 @@
1
- {"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../../../src/modes/rpc/session-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, realpathSync } from \"node:fs\";\nimport { basename, dirname, isAbsolute, resolve } from \"node:path\";\nimport { ProviderScope, runWithProviderScope } from \"@earendil-works/pi-ai/node/provider-scope\";\nimport {\n\ttype AgentSessionLaunchProfile,\n\ttype AgentSessionRuntime,\n\ttype CreateAgentSessionRuntimeFactory,\n\tcreateAgentSessionRuntime,\n} from \"../../core/agent-session-runtime.ts\";\nimport { SessionManager } from \"../../core/session-manager.ts\";\n\n/** The immutable flags selected when a routing session is opened. */\nexport interface RpcSessionLaunchProfile extends AgentSessionLaunchProfile {\n\tsessionPath?: string;\n}\n\nexport type SessionRuntime = Pick<AgentSessionRuntime, \"session\" | \"dispose\">;\nexport type RpcSessionState = \"opening\" | \"open\" | \"closing\" | \"closed\";\n\nexport interface RpcSessionEntry {\n\tstate: RpcSessionState;\n\truntime?: SessionRuntime;\n\tscope: ProviderScope;\n\tprofile: Readonly<RpcSessionLaunchProfile>;\n\tdurableSessionId?: string;\n\tsessionPath?: string;\n\tlifecycleMutex: Promise<void>;\n}\n\nexport class RpcSessionRegistryError extends Error {\n\treadonly code: \"unknown_session\" | \"session_closing\" | \"session_path_in_use\" | \"invalid_path\" | \"open_failed\";\n\n\tconstructor(code: RpcSessionRegistryError[\"code\"]) {\n\t\tsuper(code);\n\t\tthis.code = code;\n\t\tthis.name = \"RpcSessionRegistryError\";\n\t}\n}\n\nexport interface RpcSessionRegistryOptions {\n\tagentDir: string;\n\tcreateRuntime: CreateAgentSessionRuntimeFactory;\n}\n\nexport interface OpenRpcSession {\n\tsessionId: string;\n\tdurableSessionId: string;\n\tsessionPath?: string;\n}\n\nfunction canonicalPath(path: string): string {\n\tconst absolutePath = resolve(path);\n\tif (existsSync(absolutePath)) return realpathSync(absolutePath);\n\treturn `${realpathSync(dirname(absolutePath))}/${basename(absolutePath)}`;\n}\n\nfunction frozenProfile(profile: RpcSessionLaunchProfile): Readonly<RpcSessionLaunchProfile> {\n\treturn Object.freeze({\n\t\t...profile,\n\t\t...(profile.creationModel ? { creationModel: Object.freeze({ ...profile.creationModel }) } : {}),\n\t});\n}\n\n/** Process-local lifecycle owner for multi-session RPC runtimes. */\nexport class RpcSessionRegistry {\n\tprivate readonly entries = new Map<string, RpcSessionEntry>();\n\tprivate readonly reservations = new Set<string>();\n\tprivate nextHandle = 0;\n\tprivate readonly options: RpcSessionRegistryOptions;\n\n\tconstructor(options: RpcSessionRegistryOptions) {\n\t\tthis.options = options;\n\t}\n\n\tasync openSession(profile: RpcSessionLaunchProfile): Promise<OpenRpcSession> {\n\t\tthis.validateProfile(profile);\n\t\tconst sessionPath = profile.sessionPath ? canonicalPath(profile.sessionPath) : undefined;\n\t\tif (sessionPath && this.reservations.has(sessionPath)) throw new RpcSessionRegistryError(\"session_path_in_use\");\n\t\tif (sessionPath) this.reservations.add(sessionPath);\n\n\t\t// Resume vs create parity (D1 + omo SenpiSessionRuntime.ts:198-200):\n\t\t// Create-only launch semantics mirror classic startup flags. A resumed\n\t\t// session restores its persisted model and thinking level instead of being\n\t\t// overridden by the new open_session request.\n\t\tconst isResume = sessionPath !== undefined && existsSync(sessionPath);\n\t\tconst storedProfile = frozenProfile({ ...profile, ...(sessionPath ? { sessionPath } : {}) });\n\t\tconst runtimeProfile = isResume\n\t\t\t? frozenProfile({ ...storedProfile, creationModel: undefined, initialThinkingLevel: undefined })\n\t\t\t: storedProfile;\n\n\t\tconst handle = `rpc-${++this.nextHandle}`;\n\t\tconst entry: RpcSessionEntry = {\n\t\t\tstate: \"opening\",\n\t\t\tscope: new ProviderScope(),\n\t\t\tprofile: storedProfile,\n\t\t\tsessionPath,\n\t\t\tlifecycleMutex: Promise.resolve(),\n\t\t};\n\t\tthis.entries.set(handle, entry);\n\t\ttry {\n\t\t\tconst manager = sessionPath\n\t\t\t\t? SessionManager.open(sessionPath, undefined, storedProfile.cwd)\n\t\t\t\t: SessionManager.create(storedProfile.cwd);\n\t\t\tentry.runtime = await runWithProviderScope(entry.scope, () =>\n\t\t\t\tcreateAgentSessionRuntime(this.options.createRuntime, {\n\t\t\t\t\tcwd: manager.getCwd(),\n\t\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\t\tsessionManager: manager,\n\t\t\t\t\tlaunchProfile: runtimeProfile,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tentry.durableSessionId = manager.getSessionId();\n\t\t\tentry.sessionPath ??= manager.getSessionFile();\n\t\t\tentry.state = \"open\";\n\t\t\treturn { sessionId: handle, durableSessionId: entry.durableSessionId, sessionPath: entry.sessionPath };\n\t\t} catch (error) {\n\t\t\t// Runtime construction may have started extensions, watchers, and provider\n\t\t\t// registrations before it rejects. Keep the reservation and entry private\n\t\t\t// until all of those resources have been torn down, then release them as\n\t\t\t// one rollback so the path can immediately be opened again.\n\t\t\ttry {\n\t\t\t\tawait entry.runtime?.dispose();\n\t\t\t} catch {\n\t\t\t\t// The original construction error remains the externally visible cause.\n\t\t\t} finally {\n\t\t\t\ttry {\n\t\t\t\t\tawait entry.scope.close?.();\n\t\t\t\t} finally {\n\t\t\t\t\tthis.entries.delete(handle);\n\t\t\t\t\tif (sessionPath) this.reservations.delete(sessionPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (error instanceof RpcSessionRegistryError) throw error;\n\t\t\tthrow new RpcSessionRegistryError(\"open_failed\");\n\t\t}\n\t}\n\n\tgetForCommand(handle: string, command: string): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (!entry) throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tif (entry.state === \"closing\" && ![\"abort\", \"abort_bash\", \"extension_ui_response\"].includes(command)) {\n\t\t\tthrow new RpcSessionRegistryError(\"session_closing\");\n\t\t}\n\t\tif (entry.state !== \"open\" && entry.state !== \"closing\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\treturn entry;\n\t}\n\n\t/**\n\t * Starts a close synchronously. Call this before disposing any session-owned\n\t * binding so a concurrent command cannot reach a half-disposed handler.\n\t */\n\tbeginClose(handle: string): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (entry?.state !== \"open\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tentry.state = \"closing\";\n\t\treturn entry;\n\t}\n\n\tasync close(handle: string): Promise<void> {\n\t\tthis.beginClose(handle);\n\t\treturn this.closeMarked(handle);\n\t}\n\n\t/** Completes a close previously made visible by beginClose(). */\n\tasync closeMarked(handle: string): Promise<void> {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (entry?.state !== \"closing\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tentry.lifecycleMutex = (async () => {\n\t\t\ttry {\n\t\t\t\tawait entry.runtime?.session.abort();\n\t\t\t\tawait entry.runtime?.session.waitForIdle();\n\t\t\t\tawait entry.runtime?.dispose();\n\t\t\t\tawait entry.scope.close?.();\n\t\t\t} finally {\n\t\t\t\tentry.state = \"closed\";\n\t\t\t\tthis.entries.delete(handle);\n\t\t\t\tif (entry.sessionPath) this.reservations.delete(entry.sessionPath);\n\t\t\t}\n\t\t})();\n\t\treturn entry.lifecycleMutex;\n\t}\n\n\tlist(): Array<{\n\t\tsessionId: string;\n\t\tdurableSessionId?: string;\n\t\tsessionPath?: string;\n\t\tcwd: string;\n\t\tname?: string;\n\t\tstatus: RpcSessionState;\n\t}> {\n\t\treturn [...this.entries].map(([sessionId, entry]) => ({\n\t\t\tsessionId,\n\t\t\tdurableSessionId: entry.durableSessionId,\n\t\t\tsessionPath: entry.sessionPath,\n\t\t\tcwd: entry.profile.cwd,\n\t\t\tname: entry.runtime?.session.sessionManager.getSessionName(),\n\t\t\tstatus: entry.state,\n\t\t}));\n\t}\n\n\tprivate validateProfile(profile: RpcSessionLaunchProfile): void {\n\t\tif (!isAbsolute(profile.cwd) || (profile.sessionPath !== undefined && !isAbsolute(profile.sessionPath))) {\n\t\t\tthrow new RpcSessionRegistryError(\"invalid_path\");\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../../../src/modes/rpc/session-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, realpathSync } from \"node:fs\";\nimport { basename, dirname, isAbsolute, resolve } from \"node:path\";\nimport { ProviderScope, runWithProviderScope } from \"@earendil-works/pi-ai/node/provider-scope\";\nimport {\n\ttype AgentSessionLaunchProfile,\n\ttype AgentSessionRuntime,\n\ttype CreateAgentSessionRuntimeFactory,\n\tcreateAgentSessionRuntime,\n} from \"../../core/agent-session-runtime.ts\";\nimport { SessionManager } from \"../../core/session-manager.ts\";\n\n/** The immutable flags selected when a routing session is opened. */\nexport interface RpcSessionLaunchProfile extends AgentSessionLaunchProfile {\n\tsessionPath?: string;\n}\n\nexport type SessionRuntime = Pick<AgentSessionRuntime, \"session\" | \"dispose\">;\nexport type RpcSessionState = \"opening\" | \"open\" | \"closing\" | \"closed\";\n\nexport interface RpcSessionEntry {\n\tstate: RpcSessionState;\n\truntime?: SessionRuntime;\n\tscope: ProviderScope;\n\tprofile: Readonly<RpcSessionLaunchProfile>;\n\tdurableSessionId?: string;\n\tsessionPath?: string;\n\tlifecycleMutex: Promise<void>;\n}\n\nexport class RpcSessionRegistryError extends Error {\n\treadonly code: \"unknown_session\" | \"session_closing\" | \"session_path_in_use\" | \"invalid_path\" | \"open_failed\";\n\n\tconstructor(code: RpcSessionRegistryError[\"code\"], reason?: string) {\n\t\tsuper(code === \"open_failed\" && reason ? `${code}: ${reason}` : code);\n\t\tthis.code = code;\n\t\tthis.name = \"RpcSessionRegistryError\";\n\t}\n}\n\nexport interface RpcSessionRegistryOptions {\n\tagentDir: string;\n\tcreateRuntime: CreateAgentSessionRuntimeFactory;\n}\n\nexport interface OpenRpcSession {\n\tsessionId: string;\n\tdurableSessionId: string;\n\tsessionPath?: string;\n}\n\nfunction canonicalPath(path: string): string {\n\tconst absolutePath = resolve(path);\n\tif (existsSync(absolutePath)) return realpathSync(absolutePath);\n\treturn `${realpathSync(dirname(absolutePath))}/${basename(absolutePath)}`;\n}\n\nfunction frozenProfile(profile: RpcSessionLaunchProfile): Readonly<RpcSessionLaunchProfile> {\n\treturn Object.freeze({\n\t\t...profile,\n\t\t...(profile.creationModel ? { creationModel: Object.freeze({ ...profile.creationModel }) } : {}),\n\t});\n}\n\n/** Process-local lifecycle owner for multi-session RPC runtimes. */\nexport class RpcSessionRegistry {\n\tprivate readonly entries = new Map<string, RpcSessionEntry>();\n\tprivate readonly reservations = new Set<string>();\n\tprivate nextHandle = 0;\n\tprivate readonly options: RpcSessionRegistryOptions;\n\n\tconstructor(options: RpcSessionRegistryOptions) {\n\t\tthis.options = options;\n\t}\n\n\tasync openSession(profile: RpcSessionLaunchProfile): Promise<OpenRpcSession> {\n\t\tthis.validateProfile(profile);\n\t\tconst sessionPath = profile.sessionPath ? canonicalPath(profile.sessionPath) : undefined;\n\t\tif (sessionPath && this.reservations.has(sessionPath)) throw new RpcSessionRegistryError(\"session_path_in_use\");\n\t\tif (sessionPath) this.reservations.add(sessionPath);\n\n\t\t// Resume vs create parity (D1 + omo SenpiSessionRuntime.ts:198-200):\n\t\t// Create-only launch semantics mirror classic startup flags. A resumed\n\t\t// session restores its persisted model and thinking level instead of being\n\t\t// overridden by the new open_session request.\n\t\tconst isResume = sessionPath !== undefined && existsSync(sessionPath);\n\t\tconst storedProfile = frozenProfile({ ...profile, ...(sessionPath ? { sessionPath } : {}) });\n\t\tconst runtimeProfile = isResume\n\t\t\t? frozenProfile({ ...storedProfile, creationModel: undefined, initialThinkingLevel: undefined })\n\t\t\t: storedProfile;\n\n\t\tconst handle = `rpc-${++this.nextHandle}`;\n\t\tconst entry: RpcSessionEntry = {\n\t\t\tstate: \"opening\",\n\t\t\tscope: new ProviderScope(),\n\t\t\tprofile: storedProfile,\n\t\t\tsessionPath,\n\t\t\tlifecycleMutex: Promise.resolve(),\n\t\t};\n\t\tthis.entries.set(handle, entry);\n\t\ttry {\n\t\t\tconst manager = sessionPath\n\t\t\t\t? SessionManager.open(sessionPath, undefined, storedProfile.cwd)\n\t\t\t\t: SessionManager.create(storedProfile.cwd);\n\t\t\tentry.runtime = await runWithProviderScope(entry.scope, () =>\n\t\t\t\tcreateAgentSessionRuntime(this.options.createRuntime, {\n\t\t\t\t\tcwd: manager.getCwd(),\n\t\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\t\tsessionManager: manager,\n\t\t\t\t\tlaunchProfile: runtimeProfile,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tentry.durableSessionId = manager.getSessionId();\n\t\t\tentry.sessionPath ??= manager.getSessionFile();\n\t\t\tentry.state = \"open\";\n\t\t\treturn { sessionId: handle, durableSessionId: entry.durableSessionId, sessionPath: entry.sessionPath };\n\t\t} catch (error) {\n\t\t\t// Runtime construction may have started extensions, watchers, and provider\n\t\t\t// registrations before it rejects. Keep the reservation and entry private\n\t\t\t// until all of those resources have been torn down, then release them as\n\t\t\t// one rollback so the path can immediately be opened again.\n\t\t\ttry {\n\t\t\t\tawait entry.runtime?.dispose();\n\t\t\t} catch {\n\t\t\t\t// The original construction error remains the externally visible cause.\n\t\t\t} finally {\n\t\t\t\ttry {\n\t\t\t\t\tawait entry.scope.close?.();\n\t\t\t\t} finally {\n\t\t\t\t\tthis.entries.delete(handle);\n\t\t\t\t\tif (sessionPath) this.reservations.delete(sessionPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (error instanceof RpcSessionRegistryError) throw error;\n\t\t\tthrow new RpcSessionRegistryError(\"open_failed\", error instanceof Error ? error.message : undefined);\n\t\t}\n\t}\n\n\tgetForCommand(handle: string, command: string): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (!entry) throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tif (entry.state === \"closing\" && ![\"abort\", \"abort_bash\", \"extension_ui_response\"].includes(command)) {\n\t\t\tthrow new RpcSessionRegistryError(\"session_closing\");\n\t\t}\n\t\tif (entry.state !== \"open\" && entry.state !== \"closing\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\treturn entry;\n\t}\n\n\t/**\n\t * Starts a close synchronously. Call this before disposing any session-owned\n\t * binding so a concurrent command cannot reach a half-disposed handler.\n\t */\n\tbeginClose(handle: string): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (entry?.state !== \"open\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tentry.state = \"closing\";\n\t\treturn entry;\n\t}\n\n\tasync close(handle: string): Promise<void> {\n\t\tthis.beginClose(handle);\n\t\treturn this.closeMarked(handle);\n\t}\n\n\t/** Completes a close previously made visible by beginClose(). */\n\tasync closeMarked(handle: string): Promise<void> {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (entry?.state !== \"closing\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tentry.lifecycleMutex = (async () => {\n\t\t\ttry {\n\t\t\t\tawait entry.runtime?.session.abort();\n\t\t\t\tawait entry.runtime?.session.waitForIdle();\n\t\t\t\tawait entry.runtime?.dispose();\n\t\t\t\tawait entry.scope.close?.();\n\t\t\t} finally {\n\t\t\t\tentry.state = \"closed\";\n\t\t\t\tthis.entries.delete(handle);\n\t\t\t\tif (entry.sessionPath) this.reservations.delete(entry.sessionPath);\n\t\t\t}\n\t\t})();\n\t\treturn entry.lifecycleMutex;\n\t}\n\n\tlist(): Array<{\n\t\tsessionId: string;\n\t\tdurableSessionId?: string;\n\t\tsessionPath?: string;\n\t\tcwd: string;\n\t\tname?: string;\n\t\tstatus: RpcSessionState;\n\t}> {\n\t\treturn [...this.entries].map(([sessionId, entry]) => ({\n\t\t\tsessionId,\n\t\t\tdurableSessionId: entry.durableSessionId,\n\t\t\tsessionPath: entry.sessionPath,\n\t\t\tcwd: entry.profile.cwd,\n\t\t\tname: entry.runtime?.session.sessionManager.getSessionName(),\n\t\t\tstatus: entry.state,\n\t\t}));\n\t}\n\n\tprivate validateProfile(profile: RpcSessionLaunchProfile): void {\n\t\tif (!isAbsolute(profile.cwd) || (profile.sessionPath !== undefined && !isAbsolute(profile.sessionPath))) {\n\t\t\tthrow new RpcSessionRegistryError(\"invalid_path\");\n\t\t}\n\t}\n}\n"]}
package/docs/providers.md CHANGED
@@ -88,8 +88,18 @@ If your Claude Pro/Max subscription usage through `claude-sdk-oauth` feels unexp
88
88
  1. **Upgrade to v2026.8.3 or later.** Resume-first session continuity (#634-637) landed on 2026-08-03. On older builds, every turn after a divergence (compaction, abort, model switch, restart, failover) re-sends the entire conversation, which is the dominant token-burn mechanism.
89
89
  2. **Check which lane you are on.** The `ambient` lane (default) inherits the environment. `oauth-slots` and `config-dir` are managed lanes set via `SENPI_CLAUDE_SDK_OAUTH_TOKEN_INJECTION`. The `config-dir` lane keeps each account's credentials in its own `CLAUDE_CONFIG_DIR`; no official SDK API moves a transcript across roots, so account failover on that lane always flattens (re-sends the full history) — this is a declared residual, not a bug.
90
90
  3. **Read the continuity observations.** Tail the session log and filter for `flatten` — each `flatten` line means the lane re-sent the whole conversation and lost prompt-cache hits. A healthy conversation shows one `bootstrap` followed by `delta` lines. Common flatten reasons: `transcript_missing`, `registry_miss`, `resume_initialization_failed`, `cross_root_unsupported` (config-dir only).
91
- 4. **Prompt-cache retention.** On the direct Anthropic API path (`anthropic` provider with an OAuth token), cache retention defaults to `long` with a 1-hour TTL on the native endpoint. Set `PI_CACHE_RETENTION=long` to ensure 1h caching. The SDK OAuth lane manages its own caching internally and senpi cannot add `cache_control` breakpoints to it.
92
- 5. **Directive-block deduplication.** As of v2026.8.4, the flatten serialization collapses repeated `<ultrawork-mode>` directive blocks to a single copy, preventing the issue-#494 scenario where duplicated ~17KB directive blocks consumed up to 73% of the re-sent prompt. The continuity observation reports how many were collapsed and the payload size.
91
+ 4. **Prompt-cache retention.** Effective cache TTL depends on which lane you are on:
92
+
93
+ | Lane | Effective TTL | Who controls it | How to override |
94
+ | --- | --- | --- | --- |
95
+ | Claude SDK OAuth (subscription, `claude-sdk-oauth`) | 5 minutes | The Claude SDK owns `cache_control`; senpi cannot add breakpoints. senpi reports 300s for this lane so cache-aware budgets (tool waits, goal timing) size themselves correctly. | Not overridable |
96
+ | Direct Anthropic API (`api.anthropic.com`, API key or OAuth token) | 1 hour | senpi defaults retention to `long` on this lane. Note: Anthropic's own API default is 5 minutes; the 1h TTL is senpi's choice, and 1h cache writes cost 2x base input vs 1.25x for 5m ([Anthropic prompt caching](https://docs.claude.com/en/docs/build-with-claude/prompt-caching)). | `PI_CACHE_RETENTION`, `cacheRetention` |
97
+ | Anthropic-compatible providers (kimi-coding, fireworks, gateways) | 5 minutes | The 1h TTL is gated on the native `api.anthropic.com` base URL, so these lanes stay short. | `cacheRetention` |
98
+
99
+ Override precedence: `cacheRetention` in `models.json` / the model catalog wins over everything. `PI_CACHE_RETENTION=long` selects long; any other set value forces short; unset falls back to the lane default above.
100
+ 5. **Goal-monitor timing.** The goal monitor's continuation backstop is derived from the model's cache-safe wait (TTL minus `promptCache.safetyBufferSeconds`, default 30), capped by `promptCache.goalBackstopMaxSeconds` (default 3570), instead of a fixed 4 minutes. On 5m lanes that means wakes every ~4m30s; on the 1h direct-API lane, up to 59m30s. Cache-warm notices show which warm iteration you are on.
101
+ 6. **Wake sources that hold the goal backstop.** Anything that can wake a parked session publishes a `wake_source_state` event (`{source, activeCount}`): terminal monitors (`terminal-monitors`), background bash sessions including auto-detached and killed ones (`terminal-background-sessions`), detached `eval` cells (`senpi-codemode`), and omo-senpi background task children plus owned team members (`senpi-task`). The goal extension sums every source, so a goal waits inside the prompt-cache TTL while ANY of them is on duty instead of continuing immediately. The legacy `terminal_monitor_state` event is still emitted for external consumers and is folded onto the same `terminal-monitors` count.
102
+ 7. **Directive-block deduplication.** As of v2026.8.4, the flatten serialization collapses repeated `<ultrawork-mode>` directive blocks to a single copy, preventing the issue-#494 scenario where duplicated ~17KB directive blocks consumed up to 73% of the re-sent prompt. The continuity observation reports how many were collapsed and the payload size.
93
103
 
94
104
  ### GitHub Copilot
95
105
 
@@ -108,6 +118,16 @@ If your Claude Pro/Max subscription usage through `claude-sdk-oauth` feels unexp
108
118
  - On remote/headless machines (e.g. over SSH) the browser cannot reach the loopback callback; paste the final redirect URL (or the authorization code) into the login prompt instead
109
119
  - `OPENROUTER_API_KEY` remains available through **Use an API key**
110
120
 
121
+ #### OpenRouter prompt caching and sticky routing
122
+
123
+ senpi pins every OpenRouter request to the same upstream from the first call by sending the session id both as the `x-session-id` header and as the request-body `session_id` field. OpenRouter's precedence is body > header > `prompt_cache_key`, with a 256-character limit on the value; specifying a manual `provider.order` disables sticky routing entirely ([OpenRouter prompt caching](https://openrouter.ai/docs/guides/best-practices/prompt-caching)).
124
+
125
+ Upstreams that require explicit cache breakpoints get `cache_control` blocks through OpenRouter: model ids starting with `anthropic/`, `qwen/`, or `google/` (catalog ids carrying one leading `~` are matched after stripping it). Other upstreams (OpenAI, DeepSeek, Moonshot, Groq, Z.AI, Grok) cache automatically and need no markers.
126
+
127
+ #### Moonshot / Kimi prompt caching
128
+
129
+ senpi sends `prompt_cache_key` (set to the session id) on Moonshot requests. Kimi documents the field as required for the Kimi Code Plan and recommended for any multi-turn agent ([Kimi context caching](https://platform.kimi.ai/docs/guide/use-context-caching-feature-of-kimi-api)). Kimi reports cache hits as a flat `usage.cached_tokens` field, which senpi parses as cache-read tokens.
130
+
111
131
  ### Radius
112
132
 
113
133
  Radius is a dynamic `pi-messages` gateway. `/login radius` stores OAuth tokens in `auth.json`; the gateway catalog is refreshed independently and cached in `models-store.json`. Custom Radius gateways can be declared in `models.json` with `"oauth": "radius"` and a gateway `baseUrl`.
@@ -296,7 +316,7 @@ Also supports ECS task roles (`AWS_CONTAINER_CREDENTIALS_*`) and IRSA (`AWS_WEB_
296
316
  pi --provider amazon-bedrock --model us.anthropic.claude-sonnet-4-20250514-v1:0
297
317
  ```
298
318
 
299
- Prompt caching is enabled automatically for Claude models whose ID contains a recognizable model name (base models and system-defined inference profiles). For application inference profiles (whose ARNs don't contain the model name), set `AWS_BEDROCK_FORCE_CACHE=1` to enable cache points:
319
+ Prompt caching is enabled automatically for Claude models whose ID contains a recognizable model name (base models and system-defined inference profiles). With `PI_CACHE_RETENTION=long`, the 1-hour cache TTL is only requested for Claude Opus 4.5, Sonnet 4.5, and Haiku 4.5, the models AWS documents as supporting it ([Bedrock prompt caching](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html)); every other cacheable Bedrock Claude model uses 5 minutes on both the wire and the TTL estimate. For application inference profiles (whose ARNs don't contain the model name), set `AWS_BEDROCK_FORCE_CACHE=1` to enable cache points:
300
320
 
301
321
  ```bash
302
322
  export AWS_BEDROCK_FORCE_CACHE=1
package/docs/settings.md CHANGED
@@ -247,7 +247,7 @@ Keep `retry.provider.maxRetries` at `0` unless provider-level retries are explic
247
247
 
248
248
  #### Model fallback chains
249
249
 
250
- `retry.fallbackChains` maps one exact primary-model selector to an ordered list of fallback selectors. A selector is `provider/model` with an optional `:thinking-level` suffix. For example, this switches Fable 5 to Kimi K3 at `max` thinking when an eligible failure occurs:
250
+ `retry.fallbackChains` maps a primary-model selector to an ordered list of fallback selectors. A selector is `provider/model` with an optional `:thinking-level` suffix, or a bare `model` id that applies to every provider serving that model family. Bare selectors expand against the models you actually have: providers holding an OAuth credential are preferred, then a fixed precedence order, and OpenRouter is never chosen by expansion. Senpi ships a bare default chain for `claude-fable-5`, so Fable 5 keeps a fallback chain whichever provider serves it; set that key to `[]` to opt out entirely, or set one `provider/claude-fable-5` key to override just that provider. For example, this switches Fable 5 to Kimi K3 at `max` thinking when an eligible failure occurs:
251
251
 
252
252
  ```json
253
253
  {
@@ -12,6 +12,39 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.8.9-2] - 2026-08-09
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ - Detached eval cells now emit the shared `wake_source_state` event under source `senpi-codemode` when they detach, complete, stop, or are disposed. The optional host event passthrough remains guarded, synchronous cells emit no lifecycle transition, and per-cell snapshot metadata is preserved.
24
+
25
+ ### Fixed
26
+
27
+ ### Removed
28
+
29
+ ## [2026.8.9] - 2026-08-09
30
+
31
+ ### Breaking Changes
32
+
33
+ ### Added
34
+
35
+ - Detached eval cells now publish their liveness as a `resumption_channel_state` event (source `eval-detached`) on the
36
+ host event bus: a full per-source snapshot with `activeCount` and per-cell `id`/`description`/`startedAtMs` entries is
37
+ emitted whenever a cell detaches, settles, is stopped, or is disposed, and once on `session_start`. The goal builtin
38
+ consumes this to hold its hidden continuation while detached cells are still computing instead of nagging immediately
39
+ at turn end. Hosts without an event bus are unaffected (emission is a no-op), and the footer/status rendering is
40
+ unchanged.
41
+
42
+ ### Changed
43
+
44
+ ### Fixed
45
+
46
+ ### Removed
47
+
15
48
  ## [2026.8.7] - 2026-08-07
16
49
 
17
50
  ### Breaking Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.8.7",
3
+ "version": "2026.8.9-2",
4
4
  "description": "Source-only senpi extension package for codemode evaluation tools",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -30,14 +30,14 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@babel/parser": "8.0.4",
33
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.7",
33
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.9-2",
34
34
  "typebox": "1.3.8"
35
35
  },
36
36
  "peerDependencies": {
37
- "@code-yeongyu/senpi": "2026.8.7"
37
+ "@code-yeongyu/senpi": "2026.8.9-2"
38
38
  },
39
39
  "devDependencies": {
40
- "@code-yeongyu/senpi": "2026.8.7"
40
+ "@code-yeongyu/senpi": "2026.8.9-2"
41
41
  },
42
42
  "keywords": [
43
43
  "senpi",