@jmfederico/pi-web 1.202607.2 → 1.202608.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 (103) hide show
  1. package/README.md +1 -1
  2. package/dist/client/assets/{CodeViewer-B4M13447.js → CodeViewer-D91Sp61M.js} +1 -1
  3. package/dist/client/assets/{TerminalPanel-Bmvj-Ecn.js → TerminalPanel-fYrKc3jh.js} +2 -2
  4. package/dist/client/assets/{UnifiedDiffViewer-C0hygwRo.js → UnifiedDiffViewer-BafLWGtF.js} +1 -1
  5. package/dist/client/assets/index-CA8q9_o7.js +4025 -0
  6. package/dist/client/index.html +1 -1
  7. package/dist/config.js +39 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/pi-web-plugins/info/infoInternals.js +208 -0
  10. package/dist/pi-web-plugins/info/pi-web-plugin.js +12 -15
  11. package/dist/pi-web-plugins/relays/markdownDocument.js +81 -0
  12. package/dist/pi-web-plugins/relays/package.json +9 -0
  13. package/dist/pi-web-plugins/relays/pi-web-plugin.js +44 -0
  14. package/dist/pi-web-plugins/relays/relayDiscovery.js +228 -0
  15. package/dist/pi-web-plugins/relays/relaysPanelElement.js +550 -0
  16. package/dist/pi-web-plugins/relays/vendor/README.md +24 -0
  17. package/dist/pi-web-plugins/relays/vendor/marked.esm.js +76 -0
  18. package/dist/plugin-api.d.ts +7 -1
  19. package/dist/server/app.js +0 -22
  20. package/dist/server/app.js.map +1 -1
  21. package/dist/server/browserMessageProjection.js +0 -2
  22. package/dist/server/browserMessageProjection.js.map +1 -1
  23. package/dist/server/configRoutes.js +13 -11
  24. package/dist/server/configRoutes.js.map +1 -1
  25. package/dist/server/machines/machineProxyRoutes.js +2 -13
  26. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  27. package/dist/server/piWebStatus.js +12 -38
  28. package/dist/server/piWebStatus.js.map +1 -1
  29. package/dist/server/sessiond/agentHttpDispatcher.js +108 -0
  30. package/dist/server/sessiond/agentHttpDispatcher.js.map +1 -0
  31. package/dist/server/sessiond/agentProcessEnvironment.js +62 -0
  32. package/dist/server/sessiond/agentProcessEnvironment.js.map +1 -0
  33. package/dist/server/sessiond/sessionServiceDependencies.js +33 -0
  34. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -0
  35. package/dist/server/sessiond.js +145 -116
  36. package/dist/server/sessiond.js.map +1 -1
  37. package/dist/server/sessions/askUserTool.js +102 -0
  38. package/dist/server/sessions/askUserTool.js.map +1 -0
  39. package/dist/server/sessions/authRoutes.js +0 -10
  40. package/dist/server/sessions/authRoutes.js.map +1 -1
  41. package/dist/server/sessions/authService.js +0 -24
  42. package/dist/server/sessions/authService.js.map +1 -1
  43. package/dist/server/sessions/extensionDialogWaiters.js +76 -0
  44. package/dist/server/sessions/extensionDialogWaiters.js.map +1 -0
  45. package/dist/server/sessions/globalProviderPolicy.js +79 -10
  46. package/dist/server/sessions/globalProviderPolicy.js.map +1 -1
  47. package/dist/server/sessions/messagePaging.js +2 -2
  48. package/dist/server/sessions/messagePaging.js.map +1 -1
  49. package/dist/server/sessions/modelCatalogRefresher.js +8 -0
  50. package/dist/server/sessions/modelCatalogRefresher.js.map +1 -1
  51. package/dist/server/sessions/oauthLoginFlowService.js +2 -5
  52. package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
  53. package/dist/server/sessions/parentSessionLocator.js +75 -0
  54. package/dist/server/sessions/parentSessionLocator.js.map +1 -0
  55. package/dist/server/sessions/pendingAskStore.js +275 -0
  56. package/dist/server/sessions/pendingAskStore.js.map +1 -0
  57. package/dist/server/sessions/pendingExtensionDialogStore.js +196 -0
  58. package/dist/server/sessions/pendingExtensionDialogStore.js.map +1 -0
  59. package/dist/server/sessions/piSessionManagerGateway.js +182 -2
  60. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
  61. package/dist/server/sessions/piSessionService.js +721 -203
  62. package/dist/server/sessions/piSessionService.js.map +1 -1
  63. package/dist/server/sessions/sessionFileHeader.js +117 -0
  64. package/dist/server/sessions/sessionFileHeader.js.map +1 -0
  65. package/dist/server/sessions/sessionRoutes.js +169 -54
  66. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  67. package/dist/server/sessions/sessionSummaryScanner.js +619 -0
  68. package/dist/server/sessions/sessionSummaryScanner.js.map +1 -0
  69. package/dist/server/sessions/spawnSessionTool.js +8 -1
  70. package/dist/server/sessions/spawnSessionTool.js.map +1 -1
  71. package/dist/server/sessions/spawnSubsessionTool.js +7 -1
  72. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  73. package/dist/server/sessions/spawnTargetResolver.js +3 -16
  74. package/dist/server/sessions/spawnTargetResolver.js.map +1 -1
  75. package/dist/server/workspaces/effectivePathAccess.js +0 -19
  76. package/dist/server/workspaces/effectivePathAccess.js.map +1 -1
  77. package/dist/server/workspaces/gitWorktreeDiscovery.js +9 -0
  78. package/dist/server/workspaces/gitWorktreeDiscovery.js.map +1 -1
  79. package/dist/server/workspaces/projectWorkspaceCwds.js +22 -0
  80. package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -0
  81. package/dist/server/workspaces/workspaceDeletionRoutes.js +30 -4
  82. package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
  83. package/dist/server/workspaces/workspaceService.js +15 -2
  84. package/dist/server/workspaces/workspaceService.js.map +1 -1
  85. package/dist/sessiond/config.js +2 -2
  86. package/dist/sessiond/config.js.map +1 -1
  87. package/dist/shared/activity.js +9 -1
  88. package/dist/shared/activity.js.map +1 -1
  89. package/dist/shared/apiTypes.d.ts +300 -24
  90. package/dist/shared/apiTypes.js +30 -15
  91. package/dist/shared/apiTypes.js.map +1 -1
  92. package/dist/shared/capabilities.js +10 -41
  93. package/dist/shared/capabilities.js.map +1 -1
  94. package/dist/shared/federatedRoutes.js +4 -3
  95. package/dist/shared/federatedRoutes.js.map +1 -1
  96. package/docs/config.md +115 -11
  97. package/docs/plugins.md +82 -14
  98. package/package.json +11 -9
  99. package/dist/client/assets/index-TvPsRyS1.js +0 -3545
  100. package/dist/server/sessiond/sessionDaemonStartup.js +0 -49
  101. package/dist/server/sessiond/sessionDaemonStartup.js.map +0 -1
  102. package/dist/server/sessions/sessionArchiveMigration.js +0 -590
  103. package/dist/server/sessions/sessionArchiveMigration.js.map +0 -1
@@ -0,0 +1,117 @@
1
+ import { open } from "node:fs/promises";
2
+ /** Chunk size for streaming a session file's header region. */
3
+ const HEADER_READ_CHUNK_BYTES = 8192;
4
+ /**
5
+ * How far to look for the first parseable line. Headers are single-line and
6
+ * written at creation; the bound keeps a pathological file with a huge
7
+ * non-header prefix from dragging the listing and open paths.
8
+ */
9
+ const HEADER_READ_CAP_BYTES = 262144;
10
+ const NEWLINE = 0x0a; // `\n`
11
+ const CARRIAGE_RETURN = 0x0d; // `\r`
12
+ /**
13
+ * Read a Pi session file's header without loading the whole transcript.
14
+ *
15
+ * Streams the file in chunks and stops at the first parseable line, mirroring
16
+ * the SDK rule the summary scanner follows: unparseable lines are skipped
17
+ * until one parses, and a parseable non-session entry disqualifies the file.
18
+ * The search is bounded to the header region; if no parseable line completes
19
+ * within the bound, the file is treated as headerless.
20
+ *
21
+ * Returns undefined for any unreadable file or file without a usable session
22
+ * header: callers use this to verify links between sessions, so an unusable
23
+ * header must behave the same as a missing one rather than throwing.
24
+ */
25
+ export async function readSessionHeaderSummary(sessionFile) {
26
+ let file;
27
+ try {
28
+ file = await open(sessionFile, "r");
29
+ return await streamHeaderSummary(file);
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ finally {
35
+ await file?.close().catch(() => undefined);
36
+ }
37
+ }
38
+ async function streamHeaderSummary(file) {
39
+ let pending = Buffer.alloc(0); // bytes of the current line, not yet newline-terminated
40
+ let position = 0;
41
+ for (;;) {
42
+ const chunk = Buffer.alloc(HEADER_READ_CHUNK_BYTES);
43
+ const { bytesRead } = await file.read(chunk, 0, chunk.length, position);
44
+ if (bytesRead === 0) {
45
+ // End of file: an unterminated tail is the final line (readline parity).
46
+ const outcome = classifyHeaderLine(pending);
47
+ return outcome === "skip" ? undefined : outcome;
48
+ }
49
+ position += bytesRead;
50
+ let chunkStart = 0;
51
+ for (;;) {
52
+ const newlineAt = chunk.indexOf(NEWLINE, chunkStart);
53
+ if (newlineAt === -1) {
54
+ pending = Buffer.concat([pending, chunk.subarray(chunkStart, bytesRead)]);
55
+ break;
56
+ }
57
+ const line = Buffer.concat([pending, chunk.subarray(chunkStart, newlineAt)]);
58
+ const outcome = classifyHeaderLine(line);
59
+ if (outcome !== "skip")
60
+ return outcome;
61
+ pending = Buffer.alloc(0);
62
+ chunkStart = newlineAt + 1;
63
+ }
64
+ // Bounded search: once the cap is consumed, leave the unterminated tail
65
+ // unclassified and report no header rather than reading without limit.
66
+ if (position >= HEADER_READ_CAP_BYTES)
67
+ return undefined;
68
+ }
69
+ }
70
+ /**
71
+ * Classify one candidate line, mirroring the SDK's first-parseable-entry rule
72
+ * that the summary scanner applies: unparseable lines are skipped until one
73
+ * parses, and the first parseable line decides the file's fate. Returns
74
+ * "skip" to keep looking, undefined to reject, or the parsed header.
75
+ */
76
+ function classifyHeaderLine(lineBytes) {
77
+ // Readline parity: a CRLF file yields lines without their trailing `\r`.
78
+ let end = lineBytes.length;
79
+ if (end > 0 && lineBytes[end - 1] === CARRIAGE_RETURN)
80
+ end -= 1;
81
+ const entry = tryParseEntry(lineBytes.toString("utf8", 0, end));
82
+ if (entry === undefined)
83
+ return "skip";
84
+ if (entry["type"] !== "session")
85
+ return undefined;
86
+ const id = entry["id"];
87
+ // The scanner skips headers without a usable id; the reader must agree so a
88
+ // link verified here can never point at an unusable session.
89
+ if (typeof id !== "string" || id === "")
90
+ return undefined;
91
+ const cwd = nonEmptyStringField(entry, "cwd");
92
+ const parentSession = nonEmptyStringField(entry, "parentSession");
93
+ return {
94
+ id,
95
+ ...(cwd === undefined ? {} : { cwd }),
96
+ ...(parentSession === undefined ? {} : { parentSession }),
97
+ };
98
+ }
99
+ function tryParseEntry(line) {
100
+ if (line.trim() === "")
101
+ return undefined;
102
+ try {
103
+ const parsed = JSON.parse(line);
104
+ return isRecord(parsed) ? parsed : undefined;
105
+ }
106
+ catch {
107
+ return undefined;
108
+ }
109
+ }
110
+ function nonEmptyStringField(header, key) {
111
+ const value = header[key];
112
+ return typeof value === "string" && value !== "" ? value : undefined;
113
+ }
114
+ function isRecord(value) {
115
+ return typeof value === "object" && value !== null;
116
+ }
117
+ //# sourceMappingURL=sessionFileHeader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionFileHeader.js","sourceRoot":"","sources":["../../../src/server/sessions/sessionFileHeader.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_CATALOG_ID_MAX_LENGTH, SESSION_UNREAD_CWD_MAX_LENGTH, SESSION_UNREAD_SESSION_ID_MAX_LENGTH } from "../../shared/apiTypes.js";
1
+ import { ASK_USER_ID_MAX_LENGTH, ASK_USER_OPTION_LIMIT, ASK_USER_OTHER_TEXT_MAX_LENGTH, ASK_USER_QUESTION_LIMIT, EXTENSION_DIALOG_ID_MAX_LENGTH, EXTENSION_DIALOG_INPUT_MAX_LENGTH, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_CATALOG_ID_MAX_LENGTH, SESSION_UNREAD_CWD_MAX_LENGTH, SESSION_UNREAD_SESSION_ID_MAX_LENGTH } from "../../shared/apiTypes.js";
2
2
  import { projectBrowserMessageResponse } from "../browserMessageProjection.js";
3
3
  import { normalizeRequestCwd } from "../workingDirectory.js";
4
4
  import { normalizeSessionCleanupRequest } from "./sessionCleanup.js";
@@ -20,7 +20,11 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
20
20
  app.post(`${prefix}/sessions`, async (request, reply) => {
21
21
  try {
22
22
  const body = requireRecord(request.body);
23
- return await sessions.start(normalizeRequestCwd(requireString(body, "cwd")));
23
+ // An opaque label the caller uses to recognise its own construction's
24
+ // startup reports. Optional: only a browser row waiting for a session id
25
+ // has anything to correlate.
26
+ const startupToken = body["startupToken"] === undefined ? undefined : requireNonEmptyString(body, "startupToken");
27
+ return await sessions.start(normalizeRequestCwd(requireString(body, "cwd")), optionalField("startupToken", startupToken));
24
28
  }
25
29
  catch (error) {
26
30
  return reply.code(400).send({ error: errorMessage(error) });
@@ -132,9 +136,12 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
132
136
  }
133
137
  });
134
138
  app.get(`${prefix}/sessions/:sessionId/messages`, async (request, reply) => {
139
+ const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
140
+ if (ref === undefined)
141
+ return reply;
135
142
  try {
136
143
  const page = { ...optionalField("before", optionalNumber(request.query.before)), ...optionalField("limit", optionalNumber(request.query.limit)) };
137
- const messages = await sessions.messages(sessionLookupFromQuery(request.params.sessionId, request.query), page);
144
+ const messages = await sessions.messages(ref, page);
138
145
  return projectBrowserMessageResponse(messages);
139
146
  }
140
147
  catch (error) {
@@ -142,24 +149,33 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
142
149
  }
143
150
  });
144
151
  app.get(`${prefix}/sessions/:sessionId/status`, async (request, reply) => {
152
+ const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
153
+ if (ref === undefined)
154
+ return reply;
145
155
  try {
146
- return await sessions.status(sessionLookupFromQuery(request.params.sessionId, request.query));
156
+ return await sessions.status(ref);
147
157
  }
148
158
  catch (error) {
149
159
  return reply.code(404).send({ error: errorMessage(error) });
150
160
  }
151
161
  });
152
162
  app.get(`${prefix}/sessions/:sessionId/stream-snapshot`, async (request, reply) => {
163
+ const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
164
+ if (ref === undefined)
165
+ return reply;
153
166
  try {
154
- return await sessions.streamSnapshot(sessionLookupFromQuery(request.params.sessionId, request.query));
167
+ return await sessions.streamSnapshot(ref);
155
168
  }
156
169
  catch (error) {
157
170
  return reply.code(404).send({ error: errorMessage(error) });
158
171
  }
159
172
  });
160
173
  app.get(`${prefix}/sessions/:sessionId/models`, async (request, reply) => {
174
+ const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
175
+ if (ref === undefined)
176
+ return reply;
161
177
  try {
162
- return { models: await sessions.availableModels(sessionLookupFromQuery(request.params.sessionId, request.query)) };
178
+ return { models: await sessions.availableModels(ref) };
163
179
  }
164
180
  catch (error) {
165
181
  return reply.code(404).send({ error: errorMessage(error) });
@@ -168,7 +184,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
168
184
  app.post(`${prefix}/sessions/:sessionId/model`, async (request, reply) => {
169
185
  try {
170
186
  const body = optionalRecord(request.body);
171
- return await sessions.setModel(sessionLookupFromBody(request.params.sessionId, body), requireString(body, "provider"), requireString(body, "modelId"));
187
+ return await sessions.setModel(sessionRefFromBody(request.params.sessionId, body), requireString(body, "provider"), requireString(body, "modelId"));
172
188
  }
173
189
  catch (error) {
174
190
  return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
@@ -180,15 +196,18 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
180
196
  const direction = body["direction"];
181
197
  if (direction !== undefined && direction !== "forward" && direction !== "backward")
182
198
  throw new Error("direction must be forward or backward");
183
- return await sessions.cycleModel(sessionLookupFromBody(request.params.sessionId, body), direction ?? "forward");
199
+ return await sessions.cycleModel(sessionRefFromBody(request.params.sessionId, body), direction ?? "forward");
184
200
  }
185
201
  catch (error) {
186
202
  return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
187
203
  }
188
204
  });
189
205
  app.get(`${prefix}/sessions/:sessionId/thinking-levels`, async (request, reply) => {
206
+ const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
207
+ if (ref === undefined)
208
+ return reply;
190
209
  try {
191
- return { levels: await sessions.availableThinkingLevels(sessionLookupFromQuery(request.params.sessionId, request.query)) };
210
+ return { levels: await sessions.availableThinkingLevels(ref) };
192
211
  }
193
212
  catch (error) {
194
213
  return reply.code(404).send({ error: errorMessage(error) });
@@ -199,7 +218,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
199
218
  const body = optionalRecord(request.body);
200
219
  // The level string is validated against the session's live available levels
201
220
  // in the service, so it stays correct if pi changes the set.
202
- return await sessions.setThinkingLevel(sessionLookupFromBody(request.params.sessionId, body), requireThinkingLevel(body["level"]));
221
+ return await sessions.setThinkingLevel(sessionRefFromBody(request.params.sessionId, body), requireThinkingLevel(body["level"]));
203
222
  }
204
223
  catch (error) {
205
224
  return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
@@ -208,15 +227,18 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
208
227
  app.post(`${prefix}/sessions/:sessionId/thinking-level/cycle`, async (request, reply) => {
209
228
  try {
210
229
  const body = optionalRecord(request.body);
211
- return await sessions.cycleThinkingLevel(sessionLookupFromBody(request.params.sessionId, body));
230
+ return await sessions.cycleThinkingLevel(sessionRefFromBody(request.params.sessionId, body));
212
231
  }
213
232
  catch (error) {
214
233
  return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
215
234
  }
216
235
  });
217
236
  app.get(`${prefix}/sessions/:sessionId/commands`, async (request, reply) => {
237
+ const ref = sessionRefFromQueryOr400(request.params.sessionId, request.query, reply);
238
+ if (ref === undefined)
239
+ return reply;
218
240
  try {
219
- return await sessions.commands(sessionLookupFromQuery(request.params.sessionId, request.query));
241
+ return await sessions.commands(ref);
220
242
  }
221
243
  catch (error) {
222
244
  return reply.code(404).send({ error: errorMessage(error) });
@@ -225,7 +247,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
225
247
  app.post(`${prefix}/sessions/:sessionId/prompt`, async (request, reply) => {
226
248
  try {
227
249
  const body = optionalRecord(request.body);
228
- await sessions.prompt(sessionLookupFromBody(request.params.sessionId, body), body["text"], body["streamingBehavior"], body["attachments"]);
250
+ await sessions.prompt(sessionRefFromBody(request.params.sessionId, body), body["text"], body["streamingBehavior"], body["attachments"]);
229
251
  return { accepted: true };
230
252
  }
231
253
  catch (error) {
@@ -234,7 +256,46 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
234
256
  });
235
257
  app.post(`${prefix}/sessions/:sessionId/queue/clear`, async (request, reply) => {
236
258
  try {
237
- return await sessions.clearQueue(sessionLookupFromBody(request.params.sessionId, optionalRecord(request.body)));
259
+ return await sessions.clearQueue(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
260
+ }
261
+ catch (error) {
262
+ return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
263
+ }
264
+ });
265
+ app.post(`${prefix}/sessions/:sessionId/ask/submit`, async (request, reply) => {
266
+ try {
267
+ const body = requireRecord(request.body);
268
+ const askId = requireBoundedId(body["askId"], "askId");
269
+ return await sessions.submitAsk(sessionRefFromBody(request.params.sessionId, body), askId, askUserSubmissionFromBody(body));
270
+ }
271
+ catch (error) {
272
+ return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
273
+ }
274
+ });
275
+ app.post(`${prefix}/sessions/:sessionId/ask/cancel`, async (request, reply) => {
276
+ try {
277
+ const body = requireRecord(request.body);
278
+ return await sessions.cancelAsk(sessionRefFromBody(request.params.sessionId, body), requireBoundedId(body["askId"], "askId"));
279
+ }
280
+ catch (error) {
281
+ return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
282
+ }
283
+ });
284
+ app.post(`${prefix}/sessions/:sessionId/dialogs/answer`, async (request, reply) => {
285
+ try {
286
+ const body = requireRecord(request.body);
287
+ const answer = extensionDialogAnswerFromBody(body);
288
+ return await sessions.answerDialog(sessionRefFromBody(request.params.sessionId, body), answer.dialogId, answer.value);
289
+ }
290
+ catch (error) {
291
+ return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
292
+ }
293
+ });
294
+ app.post(`${prefix}/sessions/:sessionId/dialogs/cancel`, async (request, reply) => {
295
+ try {
296
+ const body = requireRecord(request.body);
297
+ const cancel = extensionDialogCancelFromBody(body);
298
+ return await sessions.cancelDialog(sessionRefFromBody(request.params.sessionId, body), cancel.dialogId);
238
299
  }
239
300
  catch (error) {
240
301
  return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
@@ -243,7 +304,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
243
304
  app.post(`${prefix}/sessions/:sessionId/warnings/dismiss`, async (request, reply) => {
244
305
  try {
245
306
  const body = optionalRecord(request.body);
246
- return await sessions.dismissWarning(sessionLookupFromBody(request.params.sessionId, body), requireString(body, "dismissId"));
307
+ return await sessions.dismissWarning(sessionRefFromBody(request.params.sessionId, body), requireString(body, "dismissId"));
247
308
  }
248
309
  catch (error) {
249
310
  return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
@@ -255,7 +316,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
255
316
  const folder = body["folder"];
256
317
  if (folder !== undefined && typeof folder !== "string")
257
318
  throw new Error("folder field must be a string");
258
- const attachments = await sessions.saveAttachments(sessionLookupFromBody(request.params.sessionId, body), body["attachments"], folder);
319
+ const attachments = await sessions.saveAttachments(sessionRefFromBody(request.params.sessionId, body), body["attachments"], folder);
259
320
  return { attachments };
260
321
  }
261
322
  catch (error) {
@@ -265,7 +326,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
265
326
  app.post(`${prefix}/sessions/:sessionId/shell`, async (request, reply) => {
266
327
  try {
267
328
  const body = optionalRecord(request.body);
268
- await sessions.shell(sessionLookupFromBody(request.params.sessionId, body), requireString(body, "text"));
329
+ await sessions.shell(sessionRefFromBody(request.params.sessionId, body), requireString(body, "text"));
269
330
  return { accepted: true };
270
331
  }
271
332
  catch (error) {
@@ -275,7 +336,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
275
336
  app.post(`${prefix}/sessions/:sessionId/commands/run`, async (request, reply) => {
276
337
  try {
277
338
  const body = optionalRecord(request.body);
278
- return await sessions.runCommand(sessionLookupFromBody(request.params.sessionId, body), requireString(body, "text"));
339
+ return await sessions.runCommand(sessionRefFromBody(request.params.sessionId, body), requireString(body, "text"));
279
340
  }
280
341
  catch (error) {
281
342
  return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
@@ -284,7 +345,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
284
345
  app.post(`${prefix}/sessions/:sessionId/commands/respond`, async (request, reply) => {
285
346
  try {
286
347
  const body = optionalRecord(request.body);
287
- return await sessions.respondToCommand(sessionLookupFromBody(request.params.sessionId, body), requireString(body, "requestId"), requireString(body, "value"));
348
+ return await sessions.respondToCommand(sessionRefFromBody(request.params.sessionId, body), requireString(body, "requestId"), requireString(body, "value"));
288
349
  }
289
350
  catch (error) {
290
351
  return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
@@ -293,7 +354,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
293
354
  app.post(`${prefix}/sessions/:sessionId/tree/navigate`, async (request, reply) => {
294
355
  try {
295
356
  const body = requireRecord(request.body);
296
- return await sessions.navigateTree(sessionLookupFromBody(request.params.sessionId, body), sessionTreeNavigateRequestFromBody(body));
357
+ return await sessions.navigateTree(sessionRefFromBody(request.params.sessionId, body), sessionTreeNavigateRequestFromBody(body));
297
358
  }
298
359
  catch (error) {
299
360
  return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
@@ -301,7 +362,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
301
362
  });
302
363
  app.post(`${prefix}/sessions/:sessionId/abort`, async (request, reply) => {
303
364
  try {
304
- await sessions.abort(sessionLookupFromBody(request.params.sessionId, optionalRecord(request.body)));
365
+ await sessions.abort(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
305
366
  return { aborted: true };
306
367
  }
307
368
  catch (error) {
@@ -310,7 +371,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
310
371
  });
311
372
  app.post(`${prefix}/sessions/:sessionId/stop`, async (request, reply) => {
312
373
  try {
313
- await sessions.stop(sessionLookupFromBody(request.params.sessionId, optionalRecord(request.body)));
374
+ await sessions.stop(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
314
375
  return { stopped: true };
315
376
  }
316
377
  catch (error) {
@@ -319,7 +380,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
319
380
  });
320
381
  app.post(`${prefix}/sessions/:sessionId/archive`, async (request, reply) => {
321
382
  try {
322
- await sessions.archive(sessionLookupFromBody(request.params.sessionId, optionalRecord(request.body)));
383
+ await sessions.archive(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
323
384
  return { archived: true };
324
385
  }
325
386
  catch (error) {
@@ -328,7 +389,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
328
389
  });
329
390
  app.post(`${prefix}/sessions/:sessionId/archive-tree`, async (request, reply) => {
330
391
  try {
331
- return await sessions.archiveTree(sessionLookupFromBody(request.params.sessionId, optionalRecord(request.body)));
392
+ return await sessions.archiveTree(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
332
393
  }
333
394
  catch (error) {
334
395
  return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
@@ -336,25 +397,16 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
336
397
  });
337
398
  app.post(`${prefix}/sessions/:sessionId/restore`, async (request, reply) => {
338
399
  try {
339
- await sessions.restore(sessionLookupFromBody(request.params.sessionId, optionalRecord(request.body)));
400
+ await sessions.restore(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
340
401
  return { restored: true };
341
402
  }
342
403
  catch (error) {
343
404
  return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
344
405
  }
345
406
  });
346
- app.delete(`${prefix}/sessions/:sessionId`, async (request, reply) => {
347
- try {
348
- await sessions.deleteArchived(sessionLookupFromQuery(request.params.sessionId, request.query));
349
- return { deleted: true };
350
- }
351
- catch (error) {
352
- return reply.code(mutationErrorStatus(error)).send({ error: errorMessage(error) });
353
- }
354
- });
355
407
  app.post(`${prefix}/sessions/:sessionId/reload`, async (request, reply) => {
356
408
  try {
357
- await sessions.reload(sessionLookupFromBody(request.params.sessionId, optionalRecord(request.body)));
409
+ await sessions.reload(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
358
410
  return { reloaded: true };
359
411
  }
360
412
  catch (error) {
@@ -363,7 +415,7 @@ export function registerSessionRoutes(app, sessions, eventHub, prefix = "") {
363
415
  });
364
416
  app.post(`${prefix}/sessions/:sessionId/detach-parent`, async (request, reply) => {
365
417
  try {
366
- await sessions.detachParent(sessionLookupFromBody(request.params.sessionId, optionalRecord(request.body)));
418
+ await sessions.detachParent(sessionRefFromBody(request.params.sessionId, optionalRecord(request.body)));
367
419
  return { detached: true };
368
420
  }
369
421
  catch (error) {
@@ -394,12 +446,7 @@ function parseBulkMutationRef(value) {
394
446
  const id = requireString(record, "id").trim();
395
447
  if (id === "")
396
448
  throw new Error("id field must not be empty");
397
- const cwd = record["cwd"];
398
- if (cwd === undefined || cwd === "")
399
- return { id };
400
- if (typeof cwd !== "string")
401
- throw new Error("cwd field must be a string");
402
- return { id, cwd: normalizeRequestCwd(cwd) };
449
+ return { id, cwd: requireRefCwd(record["cwd"]) };
403
450
  }
404
451
  function notificationRefFromQuery(id, query) {
405
452
  const cwd = requireNonEmptyBoundedString(query.cwd, "cwd", MAX_NOTIFICATION_CWD_LENGTH);
@@ -415,21 +462,35 @@ function notificationRef(id, cwd) {
415
462
  cwd: normalizeRequestCwd(cwd),
416
463
  };
417
464
  }
418
- function sessionLookupFromQuery(id, query) {
419
- return sessionLookupFromCwd(id, query.cwd);
420
- }
421
- function sessionLookupFromBody(id, body) {
422
- const cwd = body["cwd"];
465
+ function sessionRefFromQuery(id, query) {
466
+ const cwd = query.cwd;
423
467
  if (cwd === undefined || cwd === "")
424
- return id;
425
- if (typeof cwd !== "string")
426
- throw new Error("cwd field must be a string");
468
+ throw new Error("cwd query parameter is required");
427
469
  return { id, cwd: normalizeRequestCwd(cwd) };
428
470
  }
429
- function sessionLookupFromCwd(id, cwd) {
430
- // Legacy id-only lookups (no cwd) remain supported; a supplied cwd is
431
- // normalized here so everything past the route layer sees canonical paths.
432
- return cwd === undefined || cwd === "" ? id : { id, cwd: normalizeRequestCwd(cwd) };
471
+ // Every read-only per-session GET route starts with this parse; a malformed
472
+ // query is a 400, sent here so the handlers stay flat.
473
+ function sessionRefFromQueryOr400(id, query, reply) {
474
+ try {
475
+ return sessionRefFromQuery(id, query);
476
+ }
477
+ catch (error) {
478
+ reply.code(400).send({ error: errorMessage(error) });
479
+ return undefined;
480
+ }
481
+ }
482
+ function sessionRefFromBody(id, body) {
483
+ return { id, cwd: requireRefCwd(body["cwd"]) };
484
+ }
485
+ // Every per-session ref is cwd-scoped: the workspace path picks the session
486
+ // store the id resolves in, and it is normalized here so everything past the
487
+ // route layer sees canonical paths.
488
+ function requireRefCwd(cwd) {
489
+ if (typeof cwd !== "string")
490
+ throw new Error("cwd field must be a string");
491
+ if (cwd === "")
492
+ throw new Error("cwd field must not be empty");
493
+ return normalizeRequestCwd(cwd);
433
494
  }
434
495
  function sessionTreeNavigateRequestFromBody(body) {
435
496
  const targetId = requireNonEmptyString(body, "targetId");
@@ -463,6 +524,60 @@ function requireExactFields(record, fields, label) {
463
524
  if (unexpected !== undefined)
464
525
  throw new Error(`${label} field contains unsupported property: ${unexpected}`);
465
526
  }
527
+ /**
528
+ * Shape-check one submitted answer set. Only transport-level checks belong here:
529
+ * whether the answers fit the questions that were actually asked is the pending
530
+ * ask store's job, since only it knows the open ask.
531
+ */
532
+ function askUserSubmissionFromBody(body) {
533
+ const answers = body["answers"];
534
+ if (!Array.isArray(answers))
535
+ throw new Error("answers field must be an array");
536
+ if (answers.length > ASK_USER_QUESTION_LIMIT)
537
+ throw new Error("answers field has too many entries");
538
+ return { answers: answers.map(askUserAnswerFromValue) };
539
+ }
540
+ function askUserAnswerFromValue(value) {
541
+ const record = requireRecord(value);
542
+ const values = record["values"];
543
+ if (!Array.isArray(values))
544
+ throw new Error("values field must be an array");
545
+ if (values.length > ASK_USER_OPTION_LIMIT)
546
+ throw new Error("values field has too many entries");
547
+ const otherText = record["otherText"];
548
+ if (otherText !== undefined && typeof otherText !== "string")
549
+ throw new Error("otherText field must be a string");
550
+ if (typeof otherText === "string" && otherText.length > ASK_USER_OTHER_TEXT_MAX_LENGTH)
551
+ throw new Error("otherText field is too long");
552
+ return {
553
+ id: requireBoundedId(record["id"], "id"),
554
+ values: values.map((entry) => requireBoundedId(entry, "values entry")),
555
+ ...(otherText === undefined ? {} : { otherText }),
556
+ };
557
+ }
558
+ function requireBoundedId(value, field) {
559
+ return requireNonEmptyBoundedString(value, field, ASK_USER_ID_MAX_LENGTH);
560
+ }
561
+ /**
562
+ * Shape-check one dialog answer. Only transport-level checks belong here:
563
+ * whether the value fits the answered dialog's kind is the pending dialog
564
+ * store's job, since only it knows the open dialog.
565
+ */
566
+ function extensionDialogAnswerFromBody(body) {
567
+ const dialogId = requireNonEmptyBoundedString(body["dialogId"], "dialogId", EXTENSION_DIALOG_ID_MAX_LENGTH);
568
+ const value = body["value"];
569
+ if (typeof value === "boolean")
570
+ return { dialogId, value };
571
+ if (typeof value === "string") {
572
+ if (value.length > EXTENSION_DIALOG_INPUT_MAX_LENGTH)
573
+ throw new Error("value field is too long");
574
+ return { dialogId, value };
575
+ }
576
+ throw new Error("value field must be a string or a boolean");
577
+ }
578
+ function extensionDialogCancelFromBody(body) {
579
+ return { dialogId: requireNonEmptyBoundedString(body["dialogId"], "dialogId", EXTENSION_DIALOG_ID_MAX_LENGTH) };
580
+ }
466
581
  function optionalRecord(value) {
467
582
  if (value === undefined || value === null)
468
583
  return {};