@co0ontty/wand 4.93.0 → 4.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/README.md +25 -30
  2. package/dist/ai-text-runner.d.ts +2 -10
  3. package/dist/ai-text-runner.js +13 -74
  4. package/dist/build-info.json +3 -3
  5. package/dist/cli.js +47 -9
  6. package/dist/config.d.ts +2 -3
  7. package/dist/config.js +26 -5
  8. package/dist/core-status-cli.d.ts +1 -1
  9. package/dist/core-status-cli.js +3 -1
  10. package/dist/daemon-connection.d.ts +10 -0
  11. package/dist/daemon-connection.js +28 -0
  12. package/dist/diagnostic-logs.d.ts +10 -0
  13. package/dist/diagnostic-logs.js +121 -0
  14. package/dist/employee-text.js +1 -3
  15. package/dist/git-quick-commit.d.ts +1 -1
  16. package/dist/git-quick-commit.js +2 -9
  17. package/dist/layout-tree.d.ts +3 -1
  18. package/dist/layout-tree.js +8 -0
  19. package/dist/local-model-process.js +20 -0
  20. package/dist/local-model-setup.js +5 -3
  21. package/dist/managed-temp.d.ts +32 -0
  22. package/dist/managed-temp.js +176 -0
  23. package/dist/middleware/api-scope.js +1 -0
  24. package/dist/models/shared.d.ts +0 -6
  25. package/dist/models/shared.js +0 -12
  26. package/dist/models/thinking.d.ts +0 -2
  27. package/dist/models/thinking.js +0 -2
  28. package/dist/models.d.ts +0 -1
  29. package/dist/models.js +3 -11
  30. package/dist/native-session-title.d.ts +1 -1
  31. package/dist/native-session-title.js +1 -1
  32. package/dist/pi-model-discovery.js +5 -17
  33. package/dist/pi-resource-recommendation.d.ts +1 -1
  34. package/dist/pi-resource-recommendation.js +129 -42
  35. package/dist/process-manager-native-history.js +1 -6
  36. package/dist/process-manager.d.ts +0 -1
  37. package/dist/process-manager.js +5 -18
  38. package/dist/provider-catalog.d.ts +1 -2
  39. package/dist/provider-catalog.js +1 -6
  40. package/dist/provider-cli-updater.js +1 -9
  41. package/dist/provider-history-scanner.d.ts +0 -3
  42. package/dist/provider-history-scanner.js +0 -26
  43. package/dist/render-daemon-client.js +6 -29
  44. package/dist/render-structured-client.js +2 -7
  45. package/dist/retention.js +1 -6
  46. package/dist/run-command.d.ts +5 -0
  47. package/dist/run-command.js +14 -1
  48. package/dist/server-session-lifecycle-routes.js +1 -1
  49. package/dist/server-storage-cleanup-routes.d.ts +3 -0
  50. package/dist/server-storage-cleanup-routes.js +19 -0
  51. package/dist/server-workspace-routes.d.ts +4 -1
  52. package/dist/server-workspace-routes.js +75 -9
  53. package/dist/server.js +21 -16
  54. package/dist/service-command-privilege.d.ts +17 -0
  55. package/dist/service-command-privilege.js +55 -0
  56. package/dist/service-manager/index.d.ts +3 -0
  57. package/dist/service-manager/index.js +3 -0
  58. package/dist/service-manager/launchd.d.ts +1 -0
  59. package/dist/service-manager/launchd.js +15 -0
  60. package/dist/service-manager/systemd.d.ts +1 -0
  61. package/dist/service-manager/systemd.js +11 -0
  62. package/dist/session-ai-context.js +17 -6
  63. package/dist/session-completion-state.d.ts +2 -0
  64. package/dist/session-completion-state.js +6 -2
  65. package/dist/session-logger.js +7 -1
  66. package/dist/session-model.d.ts +4 -0
  67. package/dist/session-model.js +13 -4
  68. package/dist/session-registry.d.ts +1 -1
  69. package/dist/session-registry.js +0 -3
  70. package/dist/silicon-employee-draft.js +0 -1
  71. package/dist/speech-service.js +18 -8
  72. package/dist/storage/mappers.js +6 -10
  73. package/dist/storage/migrations.js +14 -0
  74. package/dist/storage/uploads-repository.d.ts +36 -0
  75. package/dist/storage/uploads-repository.js +98 -0
  76. package/dist/storage-cleanup.d.ts +50 -0
  77. package/dist/storage-cleanup.js +344 -0
  78. package/dist/storage.d.ts +3 -1
  79. package/dist/storage.js +4 -0
  80. package/dist/structured-failure.d.ts +2 -0
  81. package/dist/structured-failure.js +9 -0
  82. package/dist/structured-grok-adapter.js +4 -4
  83. package/dist/structured-opencode-adapter.js +4 -9
  84. package/dist/structured-provider-common.d.ts +1 -2
  85. package/dist/structured-provider-common.js +3 -6
  86. package/dist/structured-session-manager-replay.js +2 -6
  87. package/dist/structured-session-manager.d.ts +1 -2
  88. package/dist/structured-session-manager.js +32 -25
  89. package/dist/task-organize-contract.d.ts +41 -0
  90. package/dist/task-organize-contract.js +4 -0
  91. package/dist/task-title-organize-service.d.ts +21 -0
  92. package/dist/task-title-organize-service.js +168 -0
  93. package/dist/task-title-organize.d.ts +77 -0
  94. package/dist/task-title-organize.js +346 -0
  95. package/dist/terminal-daemon-client.js +10 -48
  96. package/dist/time-utils.d.ts +2 -0
  97. package/dist/time-utils.js +7 -0
  98. package/dist/tui/attach.js +6 -5
  99. package/dist/tui/commands.d.ts +4 -1
  100. package/dist/tui/commands.js +36 -25
  101. package/dist/tui/index.js +2 -2
  102. package/dist/tui/service-actions.js +5 -4
  103. package/dist/tui/service-command.d.ts +6 -0
  104. package/dist/tui/service-command.js +40 -0
  105. package/dist/tui/service-panel.js +9 -8
  106. package/dist/types.d.ts +4 -4
  107. package/dist/upload-reference-guard.d.ts +4 -0
  108. package/dist/upload-reference-guard.js +29 -0
  109. package/dist/upload-routes.d.ts +4 -0
  110. package/dist/upload-routes.js +121 -21
  111. package/dist/user-profile.d.ts +1 -1
  112. package/dist/user-profile.js +8 -3
  113. package/dist/web-ui/content/ai-teams.js +9 -9
  114. package/dist/web-ui/content/module-manifest.json +1 -1
  115. package/dist/web-ui/content/modules/{2548dbe0d0783577/chat-render-EODYMQF6.js → 0051bd3d55f10ab7/chat-render-RY2I2CF3.js} +1 -1
  116. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-ZTEQSXPA.js → 0051bd3d55f10ab7/chunk-2F6J2MKX.js} +1 -1
  117. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-A33YPCY6.js → 0051bd3d55f10ab7/chunk-7AFM245W.js} +1 -1
  118. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-QNUWUMKW.js → 0051bd3d55f10ab7/chunk-MHX2QXIS.js} +1 -1
  119. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-WGADUEIT.js → 0051bd3d55f10ab7/chunk-RAAPETNH.js} +1 -1
  120. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-I3UYDQLS.js → 0051bd3d55f10ab7/chunk-RXKBHYUR.js} +2 -2
  121. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-3TCUG766.js → 0051bd3d55f10ab7/chunk-U4EDP4TI.js} +1 -1
  122. package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-HK7RBU3M.js → 0051bd3d55f10ab7/chunk-WHXQR7JT.js} +1 -1
  123. package/dist/web-ui/content/modules/0051bd3d55f10ab7/employee-card-NV34TZIH.js +1 -0
  124. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-2PU6TV2I.js → 0051bd3d55f10ab7/host-2IVIZ2YC.js} +1 -1
  125. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-BPYNMKFC.js → 0051bd3d55f10ab7/host-AZXNFCRW.js} +1 -1
  126. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-HZIKPWYY.js → 0051bd3d55f10ab7/host-EPEQX22R.js} +1 -1
  127. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-YELXZID4.js → 0051bd3d55f10ab7/host-FZYIMOLC.js} +1 -1
  128. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-OGD6AWHL.js → 0051bd3d55f10ab7/host-G6GL4SNL.js} +1 -1
  129. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-E3SCPOIU.js → 0051bd3d55f10ab7/host-PEILC5NU.js} +1 -1
  130. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-OWD32FZ3.js → 0051bd3d55f10ab7/host-PHHNGVNL.js} +2 -2
  131. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-CXDAFMXX.js → 0051bd3d55f10ab7/host-Q524TMSR.js} +1 -1
  132. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-RO5MB6RB.js +69 -0
  133. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-J5XRAQFP.js → 0051bd3d55f10ab7/host-WOQRK6NG.js} +1 -1
  134. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-HKXE2DOT.js → 0051bd3d55f10ab7/host-XNIR3NTJ.js} +1 -1
  135. package/dist/web-ui/content/modules/{2548dbe0d0783577/host-4E4H4JZJ.js → 0051bd3d55f10ab7/host-XRVOIEP6.js} +1 -1
  136. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-ZDKOQEMS.js +1 -0
  137. package/dist/web-ui/content/modules/0051bd3d55f10ab7/host-bridge-PMQZRG5G.js +1 -0
  138. package/dist/web-ui/content/modules/{2548dbe0d0783577/library-bridge-Y3Y24CPQ.js → 0051bd3d55f10ab7/library-bridge-DTMQIHVJ.js} +1 -1
  139. package/dist/web-ui/content/modules/{2548dbe0d0783577/page-GQBUU7T7.js → 0051bd3d55f10ab7/page-PY4JUL4W.js} +1 -1
  140. package/dist/web-ui/content/modules/{2548dbe0d0783577/panel-PG662OC5.js → 0051bd3d55f10ab7/panel-BBQFC55Q.js} +1 -1
  141. package/dist/web-ui/content/modules/0051bd3d55f10ab7/runtime.js +974 -0
  142. package/dist/web-ui/content/modules/0051bd3d55f10ab7/task-board-host-PXZSWBBG.js +2 -0
  143. package/dist/web-ui/content/modules/0051bd3d55f10ab7/workspace-agent-picker-DJSECSY6.js +1 -0
  144. package/dist/web-ui/content/modules/{2548dbe0d0783577/workspace-window-HV4T3WKR.js → 0051bd3d55f10ab7/workspace-window-MNMKSOBG.js} +1 -1
  145. package/dist/web-ui/content/styles.css +1 -1
  146. package/dist/web-ui/embedded-assets.d.ts +27 -27
  147. package/dist/web-ui/embedded-assets.js +4 -4
  148. package/dist/web-ui/provider-identity.d.ts +4 -5
  149. package/dist/web-ui/provider-identity.js +4 -10
  150. package/dist/web-ui/react/workspaces/sidebar-manage.d.ts +39 -0
  151. package/dist/web-ui/react/workspaces/sidebar-manage.js +149 -0
  152. package/dist/web-ui/react/workspaces/types.d.ts +408 -0
  153. package/dist/web-ui/react/workspaces/types.js +16 -0
  154. package/dist/web-ui/session-activity.js +1 -1
  155. package/package.json +2 -2
  156. package/dist/structured-gemini-adapter.d.ts +0 -40
  157. package/dist/structured-gemini-adapter.js +0 -208
  158. package/dist/web-ui/content/modules/2548dbe0d0783577/chunk-XGVT4ZWT.js +0 -1
  159. package/dist/web-ui/content/modules/2548dbe0d0783577/employee-card-LJ3TX72B.js +0 -1
  160. package/dist/web-ui/content/modules/2548dbe0d0783577/host-ITV2QZ4A.js +0 -1
  161. package/dist/web-ui/content/modules/2548dbe0d0783577/host-XNWC7KPR.js +0 -69
  162. package/dist/web-ui/content/modules/2548dbe0d0783577/host-bridge-G5HEVADT.js +0 -1
  163. package/dist/web-ui/content/modules/2548dbe0d0783577/runtime.js +0 -946
  164. package/dist/web-ui/content/modules/2548dbe0d0783577/task-board-host-RAN2Z2KR.js +0 -2
  165. package/dist/web-ui/content/modules/2548dbe0d0783577/workspace-agent-picker-FXLH6N5K.js +0 -1
  166. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-YX475LXO.js → 0051bd3d55f10ab7/chunk-6C6A2RHG.js} +0 -0
  167. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-NP35GDQ3.js → 0051bd3d55f10ab7/chunk-ECWYA3U5.js} +0 -0
  168. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-47EVOSJS.js → 0051bd3d55f10ab7/chunk-T35D57FW.js} +0 -0
  169. /package/dist/web-ui/content/modules/{2548dbe0d0783577 → 0051bd3d55f10ab7}/chunk-YOXXNXOX.js +0 -0
  170. /package/dist/web-ui/content/modules/{2548dbe0d0783577/chunk-C3K34WJD.js → 0051bd3d55f10ab7/chunk-YQJ6XYV7.js} +0 -0
  171. /package/dist/web-ui/content/modules/{2548dbe0d0783577/host-J6HBDK4A.js → 0051bd3d55f10ab7/host-2AQAIUNO.js} +0 -0
  172. /package/dist/web-ui/content/modules/{2548dbe0d0783577 → 0051bd3d55f10ab7}/sha256-ANXV72UC.js +0 -0
@@ -5,7 +5,6 @@ import { isRecord } from "./object-utils.js";
5
5
  const MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
6
6
  const SUPPORTED_APIS = new Set([
7
7
  "anthropic-messages",
8
- "google-generative-ai",
9
8
  "openai-completions",
10
9
  "openai-responses",
11
10
  ]);
@@ -61,9 +60,6 @@ function endpointUrl(baseUrl, api) {
61
60
  if (api === "anthropic-messages") {
62
61
  suffix = /\/v\d+(?:beta)?$/i.test(basePath) ? "models" : "v1/models";
63
62
  }
64
- else if (api === "google-generative-ai") {
65
- suffix = /\/v\d+(?:beta)?$/i.test(basePath) ? "models" : "v1beta/models";
66
- }
67
63
  else {
68
64
  suffix = "models";
69
65
  }
@@ -135,8 +131,6 @@ function asModelId(value, api) {
135
131
  if (typeof value !== "string")
136
132
  return "";
137
133
  const trimmed = value.trim();
138
- if (api === "google-generative-ai")
139
- return trimmed.replace(/^models\//, "");
140
134
  return trimmed;
141
135
  }
142
136
  function parseEndpointPage(api, body, provider) {
@@ -149,23 +143,20 @@ function parseEndpointPage(api, body, provider) {
149
143
  for (const row of rows) {
150
144
  if (!isRecord(row))
151
145
  continue;
152
- const id = asModelId(api === "google-generative-ai" ? row.name : row.id, api);
146
+ const id = asModelId(row.id, api);
153
147
  const qualifiedId = `${provider}/${id}`;
154
148
  if (!id || id.length > 128 || !MODEL_ID_PATTERN.test(id) || !MODEL_ID_PATTERN.test(qualifiedId))
155
149
  continue;
156
150
  const displayName = typeof row.display_name === "string" ? row.display_name.trim()
157
151
  : typeof row.displayName === "string" ? row.displayName.trim()
158
- : typeof row.name === "string" && api !== "google-generative-ai" ? row.name.trim() : "";
152
+ : typeof row.name === "string" ? row.name.trim() : "";
159
153
  models.push({
160
154
  id: qualifiedId,
161
155
  label: displayName && displayName !== id ? `${displayName} · ${qualifiedId}` : qualifiedId,
162
156
  });
163
157
  }
164
158
  let nextCursor = "";
165
- if (api === "google-generative-ai") {
166
- nextCursor = typeof body.nextPageToken === "string" ? body.nextPageToken : "";
167
- }
168
- else if (body.has_more === true) {
159
+ if (body.has_more === true) {
169
160
  const lastRow = rows.at(-1);
170
161
  const lastRowId = isRecord(lastRow) && typeof lastRow.id === "string" ? lastRow.id : "";
171
162
  nextCursor = typeof body.last_id === "string" ? body.last_id : lastRowId;
@@ -177,8 +168,7 @@ function pageUrl(endpoint, cursor) {
177
168
  if (!url)
178
169
  return null;
179
170
  if (cursor) {
180
- const key = endpoint.api === "google-generative-ai" ? "pageToken"
181
- : endpoint.api === "anthropic-messages" ? "after_id" : "after";
171
+ const key = endpoint.api === "anthropic-messages" ? "after_id" : "after";
182
172
  url.searchParams.set(key, cursor);
183
173
  }
184
174
  return url;
@@ -214,9 +204,7 @@ async function fetchEndpointModels(endpoint, fetchImpl) {
214
204
  break;
215
205
  const headers = { ...endpoint.headers };
216
206
  if (endpoint.apiKey) {
217
- if (endpoint.api === "google-generative-ai")
218
- headers["x-goog-api-key"] = endpoint.apiKey;
219
- else if (endpoint.api === "anthropic-messages") {
207
+ if (endpoint.api === "anthropic-messages") {
220
208
  headers["x-api-key"] = endpoint.apiKey;
221
209
  headers["anthropic-version"] = "2023-06-01";
222
210
  }
@@ -18,7 +18,7 @@ export declare function decidePiCodemode(input: {
18
18
  caller: string;
19
19
  signal?: AbortSignal;
20
20
  }): Promise<PiAutomaticCodemodeChoice>;
21
- /** Read-only matching. Results never modify settings, connect MCP, or authorize an operation. */
21
+ /** Read-only matching. Bounded system-role decisions rank the candidate set; they never connect MCP or grant authority. */
22
22
  export declare function recommendPiResources(input: {
23
23
  prompt: string;
24
24
  candidates: PiResourceSelection;
@@ -48,14 +48,6 @@ function resourceTask(prompt, catalog) {
48
48
  const task = clauses.filter((clause) => !switchOnly.test(clause.trim()));
49
49
  return task.length === clauses.length ? prompt : task.filter((clause) => clause.trim()).join(",").trim();
50
50
  }
51
- const MATCH_INSTRUCTIONS = "Which skill fits the task?";
52
- /** Keep the task separate from skill marketing text; the small model overweights long descriptions. */
53
- function shortlistQuestions(name) {
54
- return {
55
- forward: { type: "choice", instructions: MATCH_INSTRUCTIONS, criteria: { match: name, none: "None" } },
56
- reverse: { type: "choice", instructions: MATCH_INSTRUCTIONS, criteria: { none: "None", match: name } },
57
- };
58
- }
59
51
  function matchProbability(result, id) {
60
52
  const answer = result.answers[id];
61
53
  const probabilities = answer?.probabilities;
@@ -105,9 +97,89 @@ export async function decidePiCodemode(input) {
105
97
  return { override: "off", source: "local" };
106
98
  return { source: "uncertain" };
107
99
  }
108
- /** Read-only matching. Results never modify settings, connect MCP, or authorize an operation. */
100
+ const SKILL_GROUP_SIZE = 4;
101
+ const SKILL_GROUPS_PER_REQUEST = 4;
102
+ const SKILL_DESCRIPTION_CHARS = 48;
103
+ const NO_SKILL = "none";
104
+ function compactSkillDescription(item) {
105
+ const description = item.description.replace(/\s+/g, " ").trim();
106
+ return `${item.name}: ${Array.from(description).slice(0, SKILL_DESCRIPTION_CHARS).join("")}`;
107
+ }
108
+ function skillDecisionGroup(items, index) {
109
+ return { candidates: items.map((item, option) => ({ item, option: `s${option}` })),
110
+ forwardId: `g${index}f`, reverseId: `g${index}r` };
111
+ }
112
+ function skillDecisionCriteria(group, reverse) {
113
+ const entries = group.candidates.map((candidate) => [candidate.option, compactSkillDescription(candidate.item)]);
114
+ entries.push([NO_SKILL, "No installed skill is relevant"]);
115
+ if (reverse)
116
+ entries.reverse();
117
+ return Object.fromEntries(entries);
118
+ }
119
+ function skillDecisionQuestions(groups) {
120
+ const questions = {};
121
+ for (const group of groups) {
122
+ questions[group.forwardId] = { type: "choice", instructions: "Which installed skill best fits the task?",
123
+ criteria: skillDecisionCriteria(group, false) };
124
+ questions[group.reverseId] = { type: "choice", instructions: "Which installed skill best fits the task?",
125
+ criteria: skillDecisionCriteria(group, true) };
126
+ }
127
+ return questions;
128
+ }
129
+ function readSkillChoice(result, id, labels) {
130
+ const answer = result.answers[id];
131
+ const probabilities = answer?.probabilities;
132
+ const values = labels.map((label) => probabilities?.[label]);
133
+ if (answer?.type !== "choice" || typeof answer.choice !== "string" || !labels.includes(answer.choice)
134
+ || !probabilities || Object.keys(probabilities).length !== labels.length
135
+ || values.some((value) => typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1)
136
+ || Math.abs(values.reduce((sum, value) => sum + value, 0) - 1) > 0.002
137
+ || probabilities[answer.choice] < Math.max(...values)) {
138
+ throw new DecisionError("INVALID_RESULT", "决策员工返回无效的 Skills 候选判断,原选择未改变。", 502);
139
+ }
140
+ return { choice: answer.choice, probabilities: probabilities };
141
+ }
142
+ function readSkillPair(result, group) {
143
+ const labels = [...group.candidates.map((candidate) => candidate.option), NO_SKILL];
144
+ return { group, forward: readSkillChoice(result, group.forwardId, labels),
145
+ reverse: readSkillChoice(result, group.reverseId, labels) };
146
+ }
147
+ function averageSkillScore(pair, option) {
148
+ return (pair.forward.probabilities[option] + pair.reverse.probabilities[option]) / 2;
149
+ }
150
+ function strongestSkill(pair) {
151
+ let winner = pair.group.candidates[0];
152
+ let score = -1;
153
+ for (const candidate of pair.group.candidates) {
154
+ const next = averageSkillScore(pair, candidate.option);
155
+ if (next > score) {
156
+ winner = candidate;
157
+ score = next;
158
+ }
159
+ }
160
+ return winner.item;
161
+ }
162
+ /** Average the two reversed option orders; an exact aggregate tie is not actionable. */
163
+ function strongestSkillOption(pair) {
164
+ let winner = null;
165
+ let score = -1;
166
+ let tied = false;
167
+ for (const option of [...pair.group.candidates.map((candidate) => candidate.option), NO_SKILL]) {
168
+ const next = averageSkillScore(pair, option);
169
+ if (next > score + Number.EPSILON) {
170
+ winner = option;
171
+ score = next;
172
+ tied = false;
173
+ }
174
+ else if (Math.abs(next - score) <= Number.EPSILON)
175
+ tied = true;
176
+ }
177
+ return tied ? null : winner;
178
+ }
179
+ /** Read-only matching. Bounded system-role decisions rank the candidate set; they never connect MCP or grant authority. */
109
180
  export async function recommendPiResources(input) {
110
- const resources = [];
181
+ const statuses = new Map();
182
+ const semanticSkills = [];
111
183
  const task = resourceTask(input.prompt, input.catalog);
112
184
  let calls = 0;
113
185
  for (const kind of ["skills", "mcpServers"]) {
@@ -119,48 +191,63 @@ export async function recommendPiResources(input) {
119
191
  throw new DecisionError("RESOURCE_MISSING", "候选资源已移除,请重新打开设置面板。");
120
192
  const mention = explicitMention(input.prompt, item.name);
121
193
  if (mention) {
122
- resources.push({ id, kind, status: mention === "included" ? "recommended" : "unmatched", source: "explicit" });
123
- continue;
194
+ statuses.set(id, { id, kind, status: mention === "included" ? "recommended" : "unmatched", source: "explicit" });
124
195
  }
125
- // Transport type is not a description of a server's capabilities. Never connect for discovery.
126
- if (!task || kind === "mcpServers" || item.description.length > 1000) {
127
- resources.push({ id, kind, status: "unassessed", source: "local" });
128
- continue;
196
+ else if (!task || kind === "mcpServers" || item.description.length > 1000) {
197
+ // Transport type is not a capability description. Never connect MCP for discovery.
198
+ statuses.set(id, { id, kind, status: "unassessed", source: "local" });
129
199
  }
130
- let result;
131
- try {
132
- calls++;
133
- const shortlist = await input.evaluate({ state: task, questions: shortlistQuestions(item.name) }, input.caller, input.signal);
134
- const forward = matchProbability(shortlist, "forward");
135
- const reverse = matchProbability(shortlist, "reverse");
136
- if (forward < 0.8 || reverse < 0.8) {
137
- resources.push({ id, kind, status: forward <= 0.2 && reverse <= 0.2 ? "unmatched" : "uncertain", source: "local" });
138
- continue;
139
- }
200
+ else {
201
+ semanticSkills.push(item);
202
+ }
203
+ }
204
+ }
205
+ if (semanticSkills.length) {
206
+ try {
207
+ const groups = Array.from({ length: Math.ceil(semanticSkills.length / SKILL_GROUP_SIZE) }, (_, index) => skillDecisionGroup(semanticSkills.slice(index * SKILL_GROUP_SIZE, (index + 1) * SKILL_GROUP_SIZE), index));
208
+ const pairs = [];
209
+ for (let index = 0; index < groups.length; index += SKILL_GROUPS_PER_REQUEST) {
140
210
  if (input.signal?.aborted)
141
211
  throw new DecisionError("CANCELLED", "推荐已取消。", 499);
142
- // A name match alone cannot load a skill. Confirm against its complete description.
212
+ const batch = groups.slice(index, index + SKILL_GROUPS_PER_REQUEST);
143
213
  calls++;
144
- result = await input.evaluate({
145
- state: { task, resource: { name: item.name, description: item.description } },
146
- questions: { relevant: { type: "noul", instructions: "Does this skill match the task?" } },
147
- }, input.caller, input.signal);
214
+ const result = await input.evaluate({ state: task, questions: skillDecisionQuestions(batch) }, input.caller, input.signal);
215
+ if (input.signal?.aborted)
216
+ throw new DecisionError("CANCELLED", "推荐已取消。", 499);
217
+ pairs.push(...batch.map((group) => readSkillPair(result, group)));
148
218
  }
149
- catch (error) {
150
- if (error instanceof DecisionError && ["CONTEXT_LIMIT", "QUESTION_LIMIT", "OPTIONS_COLLAPSED"].includes(error.code)) {
151
- resources.push({ id, kind, status: "unassessed", source: "local" });
152
- continue;
153
- }
154
- throw error;
219
+ let finalPair;
220
+ if (pairs.length === 1) {
221
+ finalPair = pairs[0];
155
222
  }
156
- const relevant = result.answers.relevant?.noul;
157
- if (typeof relevant !== "number" || !Number.isFinite(relevant) || relevant < 0 || relevant > 1) {
158
- throw new DecisionError("INVALID_RESULT", "本地模型返回无效判断,原选择未改变。", 502);
223
+ else {
224
+ const finalists = pairs.map(strongestSkill);
225
+ const finalGroup = skillDecisionGroup(finalists, groups.length);
226
+ calls++;
227
+ const result = await input.evaluate({ state: task, questions: skillDecisionQuestions([finalGroup]) }, input.caller, input.signal);
228
+ if (input.signal?.aborted)
229
+ throw new DecisionError("CANCELLED", "推荐已取消。", 499);
230
+ finalPair = readSkillPair(result, finalGroup);
159
231
  }
160
- const status = relevant >= 0.8 ? "recommended" : relevant <= 0.2 ? "unmatched" : "uncertain";
161
- resources.push({ id, kind, status, source: "local" });
232
+ const winner = strongestSkillOption(finalPair);
233
+ const selected = winner && winner !== NO_SKILL
234
+ ? finalPair.group.candidates.find((candidate) => candidate.option === winner)?.item.id : undefined;
235
+ for (const item of semanticSkills)
236
+ statuses.set(item.id, { id: item.id, kind: "skills",
237
+ status: item.id === selected ? "recommended" : winner === NO_SKILL ? "unmatched" : "uncertain", source: "local" });
238
+ }
239
+ catch (error) {
240
+ if (!(error instanceof DecisionError) || !["CONTEXT_LIMIT", "QUESTION_LIMIT", "OPTIONS_COLLAPSED"].includes(error.code))
241
+ throw error;
242
+ for (const item of semanticSkills)
243
+ statuses.set(item.id, { id: item.id, kind: "skills", status: "unassessed", source: "local" });
162
244
  }
163
245
  }
246
+ const resources = [];
247
+ for (const kind of ["skills", "mcpServers"]) {
248
+ for (const id of input.candidates[kind])
249
+ resources.push(statuses.get(id));
250
+ }
164
251
  const selection = { skills: [], mcpServers: [] };
165
252
  for (const resource of resources)
166
253
  if (resource.status === "recommended")
@@ -4,6 +4,7 @@ import process from "node:process";
4
4
  import os from "node:os";
5
5
  import { DatabaseSync } from "node:sqlite";
6
6
  import { getProviderResumeCommandSessionId } from "./resume-policy.js";
7
+ import { parseTimestampMs as parseTimeMs } from "./time-utils.js";
7
8
  const REAL_CONVERSATION_MIN_LINES = 2;
8
9
  const DISCOVERY_RECENT_WINDOW_MS = 10 * 60 * 1000;
9
10
  const START_TIME_SKEW_MS = 30 * 1000;
@@ -173,12 +174,6 @@ function isSameResolvedPath(left, right) {
173
174
  function isUsableCodexHistorySession(session) {
174
175
  return session.hasUser || session.hasConversation;
175
176
  }
176
- function parseTimeMs(value) {
177
- if (!value)
178
- return null;
179
- const parsed = Date.parse(value);
180
- return Number.isFinite(parsed) ? parsed : null;
181
- }
182
177
  function selectCodexSessionForRecord(record, sessions) {
183
178
  const knownMtimes = record.knownCodexSessionMtimes ?? new Map();
184
179
  const candidates = sessions
@@ -243,7 +243,6 @@ export declare class ProcessManager extends EventEmitter {
243
243
  deleteQoderHistoryFiles(sessionIds: string[]): number;
244
244
  deletePiHistoryFiles(sessionIds: string[]): number;
245
245
  deleteGrokHistoryFiles(sessionIds: string[]): number;
246
- deleteGeminiHistoryFiles(sessionIds: string[]): number;
247
246
  private captureCodexSessionId;
248
247
  private captureOpenCodeSessionId;
249
248
  private captureClaudeSessionId;
@@ -674,6 +674,9 @@ export class ProcessManager extends EventEmitter {
674
674
  async start(command, cwd, mode, initialInput, opts) {
675
675
  if (this.disposed)
676
676
  throw new Error("ProcessManager has been disposed.");
677
+ if (opts?.provider != null && !isSessionProvider(opts.provider)) {
678
+ throw new Error(`不支持的终端 provider:${String(opts.provider)}`);
679
+ }
677
680
  const id = opts?.reuseId || randomUUID();
678
681
  // Host operations are keyed by session id, not by the manager record. A
679
682
  // second handshake must not adopt/forget a cancelled first incarnation.
@@ -798,10 +801,10 @@ export class ProcessManager extends EventEmitter {
798
801
  ? getProviderCommandSessionId(provider, processedCommand)
799
802
  ?? getProviderCommandSessionId(provider, command)
800
803
  : null;
801
- // Grok / Qoder / Gemini accept caller-selected IDs for new conversations. Assigning
804
+ // Grok / Qoder accept caller-selected IDs for new conversations. Assigning
802
805
  // one up front avoids depending on provider-specific TUI rendering to learn
803
806
  // the durable ID later.
804
- const assignedProviderSessionId = !existingProviderSessionId && (provider === "grok" || provider === "qoder" || provider === "gemini")
807
+ const assignedProviderSessionId = !existingProviderSessionId && (provider === "grok" || provider === "qoder")
805
808
  ? randomUUID()
806
809
  : null;
807
810
  if (assignedProviderSessionId) {
@@ -1129,9 +1132,6 @@ export class ProcessManager extends EventEmitter {
1129
1132
  deleteGrokHistoryFiles(sessionIds) {
1130
1133
  return this.providerHistory.deleteGrokHistoryFiles(sessionIds);
1131
1134
  }
1132
- deleteGeminiHistoryFiles(sessionIds) {
1133
- return this.providerHistory.deleteGeminiHistoryFiles(sessionIds);
1134
- }
1135
1135
  captureCodexSessionId(record, options) {
1136
1136
  if (record.provider !== "codex" || record.claudeSessionId) {
1137
1137
  return false;
@@ -2263,19 +2263,6 @@ export class ProcessManager extends EventEmitter {
2263
2263
  result += ` --thinking '${level}'`;
2264
2264
  return result;
2265
2265
  }
2266
- if (provider === "gemini") {
2267
- let result = command;
2268
- const trimmedModel = model?.trim();
2269
- if (trimmedModel && trimmedModel !== "default" && !/--model(?:\s|=)/.test(result) && !/(?:^|\s)-m(?:\s|$)/.test(result)) {
2270
- result += ` --model '${trimmedModel.replace(/'/g, "'\\''")}'`;
2271
- }
2272
- // Gemini CLI 没有 thinking 档位开关。approval mode 与其它 provider 的
2273
- // auto-approve 语义对齐,default 下让 TUI 自己弹确认。
2274
- if ((mode === "managed" || mode === "full-access" || mode === "auto-edit") && !/--approval-mode(?:\s|=)/.test(result) && !/(?:^|\s)-y(?:\s|$)/.test(result)) {
2275
- result += " --approval-mode yolo";
2276
- }
2277
- return result;
2278
- }
2279
2266
  const isClaudeCmd = /^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/.test(command);
2280
2267
  if (!isClaudeCmd)
2281
2268
  return command;
@@ -6,11 +6,10 @@
6
6
  * 都直接复用它,避免同一个 provider 概念散落在两处各写一份。
7
7
  */
8
8
  /** 全部受支持的会话 provider;顺序即 UI / 派发选择器里的展示顺序。 */
9
- export declare const SESSION_PROVIDERS: readonly ["claude", "codex", "opencode", "grok", "qoder", "pi", "gemini"];
9
+ export declare const SESSION_PROVIDERS: readonly ["claude", "codex", "opencode", "grok", "qoder", "pi"];
10
10
  export type SessionProvider = (typeof SESSION_PROVIDERS)[number];
11
11
  /**
12
12
  * 拥有原生思考档位(`provider:level`)的 provider。
13
- * Gemini CLI 没有档位开关,所以不出现在这里:`gemini:max` 这种值不应该被当成合法档位保留。
14
13
  */
15
14
  export declare const NATIVE_THINKING_EFFORT_PROVIDERS: readonly ["claude", "codex", "opencode", "grok", "qoder", "pi"];
16
15
  /** 原生思考档位的 `provider:level` 形状;各端必须共用同一张 provider 名单。 */
@@ -6,10 +6,9 @@
6
6
  * 都直接复用它,避免同一个 provider 概念散落在两处各写一份。
7
7
  */
8
8
  /** 全部受支持的会话 provider;顺序即 UI / 派发选择器里的展示顺序。 */
9
- export const SESSION_PROVIDERS = ["claude", "codex", "opencode", "grok", "qoder", "pi", "gemini"];
9
+ export const SESSION_PROVIDERS = ["claude", "codex", "opencode", "grok", "qoder", "pi"];
10
10
  /**
11
11
  * 拥有原生思考档位(`provider:level`)的 provider。
12
- * Gemini CLI 没有档位开关,所以不出现在这里:`gemini:max` 这种值不应该被当成合法档位保留。
13
12
  */
14
13
  export const NATIVE_THINKING_EFFORT_PROVIDERS = ["claude", "codex", "opencode", "grok", "qoder", "pi"];
15
14
  /** 原生思考档位的 `provider:level` 形状;各端必须共用同一张 provider 名单。 */
@@ -41,7 +40,6 @@ export const PROVIDER_LABELS = {
41
40
  grok: "Grok",
42
41
  qoder: "Qoder",
43
42
  pi: "Pi",
44
- gemini: "Gemini",
45
43
  };
46
44
  /**
47
45
  * Wand 自带 Agent(进程内 `core` harness / SDK)的展示名。
@@ -79,9 +77,6 @@ export const PROVIDER_ALIASES = {
79
77
  pi: "pi",
80
78
  "pi-cli": "pi",
81
79
  "pi-cli-json": "pi",
82
- gemini: "gemini",
83
- "gemini-cli": "gemini",
84
- "gemini-cli-json": "gemini",
85
80
  };
86
81
  /** 归一化一个已持久化的 provider id 或 runner/别名;无法识别时返回 null。 */
87
82
  export function normalizeProviderId(value) {
@@ -63,14 +63,6 @@ const PROVIDER_CLI_SPECS = [
63
63
  versionArgs: ["--version"],
64
64
  updateArgs: ["update", "self"],
65
65
  },
66
- {
67
- id: "gemini",
68
- label: "Gemini CLI",
69
- command: "gemini",
70
- npmPackage: "@google/gemini-cli",
71
- versionArgs: ["--version"],
72
- // 没有 `gemini update` 子命令,只能走 npm 发布渠道。
73
- },
74
66
  ];
75
67
  async function runCommand(command, args, timeout, options) {
76
68
  const { stdout, stderr } = await execFileAsync(command, args, {
@@ -351,7 +343,7 @@ export async function updateProviderClis(statuses, ids, options = {}) {
351
343
  output = await runCommand(executable, spec.updateArgs, options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options);
352
344
  }
353
345
  else {
354
- // 没有自更新子命令的 CLI(Gemini)走它的 npm 发布渠道。
346
+ // 没有自更新子命令的 CLI 走它的 npm 发布渠道。
355
347
  output = await runCommand(npmBin(options), ["install", "-g", `${spec.npmPackage}@latest`], options.updateTimeoutMs ?? UPDATE_TIMEOUT_MS, options);
356
348
  }
357
349
  if (spec.id === "codex") {
@@ -50,7 +50,6 @@ export interface ProviderHistoryScannerOptions {
50
50
  qoderProjectsDirs?: string[];
51
51
  piSessionsDir?: string;
52
52
  grokSessionsDir?: string;
53
- geminiHome?: string;
54
53
  }
55
54
  /**
56
55
  * Incremental provider-history index. Directory entries are refreshed on each
@@ -64,7 +63,6 @@ export declare class ProviderHistoryScanner {
64
63
  private readonly qoderProjectsDirs;
65
64
  private readonly piSessionsDir;
66
65
  private readonly grokSessionsDir;
67
- private readonly geminiHome;
68
66
  private readonly claudeIndex;
69
67
  private readonly codexIndex;
70
68
  private readonly qoderIndex;
@@ -96,5 +94,4 @@ export declare class ProviderHistoryScanner {
96
94
  deleteQoderHistoryFiles(sessionIds: string[]): number;
97
95
  deletePiHistoryFiles(sessionIds: string[]): number;
98
96
  deleteGrokHistoryFiles(sessionIds: string[]): number;
99
- deleteGeminiHistoryFiles(sessionIds: string[]): number;
100
97
  }
@@ -266,7 +266,6 @@ export class ProviderHistoryScanner {
266
266
  qoderProjectsDirs;
267
267
  piSessionsDir;
268
268
  grokSessionsDir;
269
- geminiHome;
270
269
  claudeIndex = new Map();
271
270
  codexIndex = new Map();
272
271
  qoderIndex = new Map();
@@ -285,7 +284,6 @@ export class ProviderHistoryScanner {
285
284
  ];
286
285
  this.piSessionsDir = options.piSessionsDir ?? path.join(os.homedir(), ".pi", "agent", "sessions");
287
286
  this.grokSessionsDir = options.grokSessionsDir ?? path.join(os.homedir(), ".grok", "sessions");
288
- this.geminiHome = options.geminiHome ?? path.join(os.homedir(), ".gemini");
289
287
  }
290
288
  getDiagnostics() {
291
289
  return {
@@ -729,28 +727,4 @@ export class ProviderHistoryScanner {
729
727
  }
730
728
  return deleted;
731
729
  }
732
- deleteGeminiHistoryFiles(sessionIds) {
733
- const ids = new Set(sessionIds.filter((id) => PROVIDER_SESSION_ID_PATTERN.test(id)));
734
- if (ids.size === 0 || !existsSync(this.geminiHome))
735
- return 0;
736
- let deleted = 0;
737
- for (const id of ids) {
738
- const dbPath = path.join(this.geminiHome, "antigravity-cli", "conversations", `${id}.db`);
739
- if (existsSync(dbPath)) {
740
- try {
741
- unlinkSync(dbPath);
742
- deleted += 1;
743
- }
744
- catch { /* already absent */ }
745
- }
746
- const lockPath = path.join(this.geminiHome, "antigravity-cli", "presence", `${id}.lock`);
747
- if (existsSync(lockPath)) {
748
- try {
749
- unlinkSync(lockPath);
750
- }
751
- catch { /* best effort */ }
752
- }
753
- }
754
- return deleted;
755
- }
756
730
  }
@@ -1,7 +1,7 @@
1
1
  import { lstatSync, readFileSync } from "node:fs";
2
2
  import net from "node:net";
3
3
  import process from "node:process";
4
- import { rejectPendingRequests, startDaemonHeartbeat, waitForDaemonSocket } from "./daemon-connection.js";
4
+ import { emitToSubscribedListeners, rejectPendingRequests, startDaemonHeartbeat, subscribeWithBufferedEvents, subscribeWithPendingEvent, waitForDaemonSocket } from "./daemon-connection.js";
5
5
  import { getErrorMessage } from "./error-utils.js";
6
6
  import { appendWindow, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
7
7
  import { MAX_FRAME_BYTES, RENDER_PROTOCOL_VERSION, encodeRenderFrame, renderPaths, } from "./render-protocol.js";
@@ -71,30 +71,13 @@ class RemoteRenderProcess {
71
71
  this.client.fireOperation(this.sessionId, "kill", { sessionId: this.sessionId, signal });
72
72
  }
73
73
  onData(listener) {
74
- this.dataListeners.add(listener);
75
- if (this.undeliveredEvents.length > 0) {
76
- const pending = this.undeliveredEvents;
74
+ const pending = this.undeliveredEvents;
75
+ if (pending.length > 0)
77
76
  this.undeliveredEvents = [];
78
- queueMicrotask(() => {
79
- if (!this.dataListeners.has(listener))
80
- return;
81
- for (const event of pending)
82
- listener(event);
83
- });
84
- }
85
- return { dispose: () => this.dataListeners.delete(listener) };
77
+ return subscribeWithBufferedEvents(this.dataListeners, listener, pending);
86
78
  }
87
79
  onExit(listener) {
88
- this.exitListeners.add(listener);
89
- const pending = this.pendingExit;
90
- if (pending) {
91
- // 退出事件只发生一次,但可能早于 listener 到达;补发语义必须与 data 一致。
92
- queueMicrotask(() => {
93
- if (this.exitListeners.has(listener))
94
- listener(pending);
95
- });
96
- }
97
- return { dispose: () => this.exitListeners.delete(listener) };
80
+ return subscribeWithPendingEvent(this.exitListeners, listener, this.pendingExit);
98
81
  }
99
82
  onResync(listener) {
100
83
  this.resyncListeners.add(listener);
@@ -122,21 +105,15 @@ class RemoteRenderProcess {
122
105
  listener(state);
123
106
  }
124
107
  acceptData(event) {
125
- if (this.dataListeners.size === 0) {
108
+ if (!emitToSubscribedListeners(this.dataListeners, event)) {
126
109
  this.undeliveredEvents = appendTerminalChunkWindow(this.undeliveredEvents, event);
127
- return;
128
110
  }
129
- this.emitData(event);
130
111
  }
131
112
  acceptExit(event) {
132
113
  this.pendingExit = event;
133
114
  for (const listener of Array.from(this.exitListeners))
134
115
  listener(event);
135
116
  }
136
- emitData(event) {
137
- for (const listener of Array.from(this.dataListeners))
138
- listener(event);
139
- }
140
117
  }
141
118
  /**
142
119
  * Render(常驻 PTY 持有者)的 Node 客户端。
@@ -1,6 +1,6 @@
1
1
  import { lstatSync, readFileSync } from "node:fs";
2
2
  import net from "node:net";
3
- import { rejectPendingRequests, startDaemonHeartbeat, waitForDaemonSocket } from "./daemon-connection.js";
3
+ import { rejectPendingRequests, startDaemonHeartbeat, subscribeWithPendingEvent, waitForDaemonSocket } from "./daemon-connection.js";
4
4
  import { getErrorMessage } from "./error-utils.js";
5
5
  import { STRUCTURED_RENDER_PROTOCOL_VERSION, decodeRenderFrames, encodeRenderFrame, structuredRenderPaths, } from "./render-structured-protocol.js";
6
6
  import { logInfo } from "./runtime-log.js";
@@ -51,12 +51,7 @@ class RemoteRun {
51
51
  return { dispose: () => this.streamListeners.delete(listener) };
52
52
  }
53
53
  onExit(listener) {
54
- this.exitListeners.add(listener);
55
- const event = this.pendingExit;
56
- if (event)
57
- queueMicrotask(() => { if (this.exitListeners.has(listener))
58
- listener(event); });
59
- return { dispose: () => this.exitListeners.delete(listener) };
54
+ return subscribeWithPendingEvent(this.exitListeners, listener, this.pendingExit);
60
55
  }
61
56
  acceptStream(event) {
62
57
  const last = this.seq[event.stream];
package/dist/retention.js CHANGED
@@ -2,6 +2,7 @@ import { AI_TEAM_ACTIVE_RUN_STATUSES } from "./ai-team-types.js";
2
2
  import { isArchiveBlocked } from "./archive-policy.js";
3
3
  import { cleanupWorktreeSync } from "./git-worktree.js";
4
4
  import { defaultTaskRetention, taskRetentionWindows } from "./task-retention.js";
5
+ import { parseTimestampMs as timestamp } from "./time-utils.js";
5
6
  /** Backward-compatible exports for consumers of the historical default windows. */
6
7
  const DEFAULT_RETENTION_WINDOWS = taskRetentionWindows(defaultTaskRetention());
7
8
  export const RETENTION_IDLE_MS = DEFAULT_RETENTION_WINDOWS.idleMs;
@@ -17,12 +18,6 @@ function isIdleLivePty(session) {
17
18
  return session.status === "running" && !isRetentionBusy(session)
18
19
  && ((session.sessionKind ?? "pty") === "pty" || session.runner === "pty");
19
20
  }
20
- function timestamp(value) {
21
- if (!value)
22
- return null;
23
- const parsed = Date.parse(value);
24
- return Number.isFinite(parsed) ? parsed : null;
25
- }
26
21
  /**
27
22
  * 任务的「最后一次活动」:卡片自身的 updated_at、容器最后一次被打开的时间,
28
23
  * 以及它名下会话的结束/开始时间,取三者里最新的一个。
@@ -16,6 +16,11 @@ export interface RunCommandOptions {
16
16
  stdio?: SpawnSyncOptionsWithStringEncoding["stdio"];
17
17
  }
18
18
  export declare function runCommandSync(file: string, args: readonly string[], options?: RunCommandOptions): SpawnSyncReturns<string>;
19
+ /** Foreground operations such as sudo: the terminal owns input (including passwords).
20
+ * No capture, shell, or timeout: cancellation belongs to the user, and Core drain
21
+ * may legitimately wait longer than the bounded probe timeout.
22
+ */
23
+ export declare function runCommandInteractive(file: string, args: readonly string[]): Promise<number>;
19
24
  export interface RunCommandResult {
20
25
  ok: boolean;
21
26
  code: number | null;