@threadbase-sh/streamer 1.69.5 → 1.70.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.
- package/dist/api/handlers/sessions.handlers.d.ts +28 -2
- package/dist/api/handlers/sessions.handlers.d.ts.map +1 -1
- package/dist/api/routes/misc.routes.d.ts.map +1 -1
- package/dist/api/routes/sessions.routes.d.ts.map +1 -1
- package/dist/api/types/api-deps.d.ts +1 -0
- package/dist/api/types/api-deps.d.ts.map +1 -1
- package/dist/cli.cjs +1298 -317
- package/dist/cli.cjs.map +1 -1
- package/dist/index.cjs +891 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +890 -49
- package/dist/index.js.map +1 -1
- package/dist/pty-host/protocol.d.ts +21 -4
- package/dist/pty-host/protocol.d.ts.map +1 -1
- package/dist/pty-host/remote-session-runner.d.ts +1 -0
- package/dist/pty-host/remote-session-runner.d.ts.map +1 -1
- package/dist/schemas/prompt.schema.d.ts +113 -0
- package/dist/schemas/prompt.schema.d.ts.map +1 -0
- package/dist/server-wiring.d.ts +22 -0
- package/dist/server-wiring.d.ts.map +1 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/services/prompts/promptRegistry.d.ts +99 -0
- package/dist/services/prompts/promptRegistry.d.ts.map +1 -0
- package/dist/services/prompts/ptyPromptAdapter.d.ts +6 -0
- package/dist/services/prompts/ptyPromptAdapter.d.ts.map +1 -0
- package/dist/types.d.ts +4 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,13 @@ __export(index_exports, {
|
|
|
34
34
|
CODEX_CLI_PROVIDER: () => CODEX_CLI_PROVIDER,
|
|
35
35
|
ConversationWatcher: () => ConversationWatcher,
|
|
36
36
|
LiveSessionManager: () => LiveSessionManager,
|
|
37
|
+
PROMPT_SCHEMA_VERSION: () => PROMPT_SCHEMA_VERSION,
|
|
37
38
|
PTYManager: () => PTYManager,
|
|
39
|
+
PromptAnswerSchema: () => PromptAnswerSchema,
|
|
40
|
+
PromptOptionSchema: () => PromptOptionSchema,
|
|
41
|
+
PromptQuestionSchema: () => PromptQuestionSchema,
|
|
42
|
+
PromptResponseSchema: () => PromptResponseSchema,
|
|
43
|
+
PromptSchema: () => PromptSchema,
|
|
38
44
|
SessionStore: () => SessionStore,
|
|
39
45
|
StreamerServer: () => StreamerServer,
|
|
40
46
|
WSHub: () => WSHub,
|
|
@@ -2222,7 +2228,7 @@ function toPublicSession(s) {
|
|
|
2222
2228
|
}
|
|
2223
2229
|
|
|
2224
2230
|
// src/pty-host/protocol.ts
|
|
2225
|
-
var PTY_HOST_PROTOCOL_VERSION =
|
|
2231
|
+
var PTY_HOST_PROTOCOL_VERSION = 4;
|
|
2226
2232
|
function isHostEvent(message) {
|
|
2227
2233
|
return "type" in message && message.type === "event";
|
|
2228
2234
|
}
|
|
@@ -2310,8 +2316,12 @@ var RemoteSessionRunner = class _RemoteSessionRunner {
|
|
|
2310
2316
|
}
|
|
2311
2317
|
throw new PtyHostProtocolMismatchError(status.protocolVersion, PTY_HOST_PROTOCOL_VERSION);
|
|
2312
2318
|
}
|
|
2313
|
-
await runner.request({ type: "subscribe" });
|
|
2319
|
+
const subscribed = await runner.request({ type: "subscribe" });
|
|
2314
2320
|
runner.refreshMirror(status);
|
|
2321
|
+
runner.restorePromptSnapshots({
|
|
2322
|
+
...status,
|
|
2323
|
+
promptSnapshots: subscribed.promptSnapshots ?? status.promptSnapshots
|
|
2324
|
+
});
|
|
2315
2325
|
return runner;
|
|
2316
2326
|
}
|
|
2317
2327
|
constructor(transport, options) {
|
|
@@ -2404,6 +2414,19 @@ var RemoteSessionRunner = class _RemoteSessionRunner {
|
|
|
2404
2414
|
this.pids.set(session.id, entry.pid);
|
|
2405
2415
|
}
|
|
2406
2416
|
}
|
|
2417
|
+
restorePromptSnapshots(status) {
|
|
2418
|
+
for (const snapshot of status.promptSnapshots ?? []) {
|
|
2419
|
+
if (snapshot.kind === "permission") {
|
|
2420
|
+
this.options.onPermissionChange?.(snapshot.sessionId, snapshot.gate, snapshot.occurrenceId);
|
|
2421
|
+
} else {
|
|
2422
|
+
this.options.onLiveQuestion?.(
|
|
2423
|
+
snapshot.sessionId,
|
|
2424
|
+
snapshot.questions,
|
|
2425
|
+
snapshot.occurrenceId
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2407
2430
|
async heartbeat(state, timeoutMs = HOST_HEARTBEAT_REQUEST_TIMEOUT_MS) {
|
|
2408
2431
|
await this.request({ type: "heartbeat", ...state }, timeoutMs);
|
|
2409
2432
|
}
|
|
@@ -2458,13 +2481,13 @@ var RemoteSessionRunner = class _RemoteSessionRunner {
|
|
|
2458
2481
|
break;
|
|
2459
2482
|
}
|
|
2460
2483
|
case "permission-change":
|
|
2461
|
-
this.options.onPermissionChange?.(event.sessionId, event.gate);
|
|
2484
|
+
this.options.onPermissionChange?.(event.sessionId, event.gate, event.occurrenceId);
|
|
2462
2485
|
break;
|
|
2463
2486
|
case "phase-change":
|
|
2464
2487
|
this.options.onPhaseChange?.(event.sessionId, event.phase);
|
|
2465
2488
|
break;
|
|
2466
2489
|
case "live-question":
|
|
2467
|
-
this.options.onLiveQuestion?.(event.sessionId, event.questions);
|
|
2490
|
+
this.options.onLiveQuestion?.(event.sessionId, event.questions, event.occurrenceId);
|
|
2468
2491
|
break;
|
|
2469
2492
|
case "live-question-gone":
|
|
2470
2493
|
this.options.onLiveQuestionGone?.(event.sessionId);
|
|
@@ -4139,6 +4162,130 @@ async function readGitBranch(dir) {
|
|
|
4139
4162
|
}
|
|
4140
4163
|
}
|
|
4141
4164
|
|
|
4165
|
+
// src/schemas/prompt.schema.ts
|
|
4166
|
+
var import_zod = require("zod");
|
|
4167
|
+
var PROMPT_SCHEMA_VERSION = 1;
|
|
4168
|
+
var OpaqueIdSchema = import_zod.z.string().trim().min(1).max(200);
|
|
4169
|
+
var MeaningfulStringSchema = import_zod.z.string().trim().min(1);
|
|
4170
|
+
var PromptOptionSchema = import_zod.z.object({
|
|
4171
|
+
optionId: OpaqueIdSchema,
|
|
4172
|
+
label: MeaningfulStringSchema,
|
|
4173
|
+
description: import_zod.z.string().optional(),
|
|
4174
|
+
preview: import_zod.z.string().optional()
|
|
4175
|
+
});
|
|
4176
|
+
var PromptQuestionSchema = import_zod.z.object({
|
|
4177
|
+
questionId: OpaqueIdSchema,
|
|
4178
|
+
text: MeaningfulStringSchema,
|
|
4179
|
+
header: import_zod.z.string().optional(),
|
|
4180
|
+
inputMode: import_zod.z.enum(["single", "multi", "text"]),
|
|
4181
|
+
options: import_zod.z.array(PromptOptionSchema),
|
|
4182
|
+
allowOther: import_zod.z.boolean(),
|
|
4183
|
+
secret: import_zod.z.union([import_zod.z.boolean(), import_zod.z.literal("unknown")])
|
|
4184
|
+
}).superRefine((question, ctx) => {
|
|
4185
|
+
const optionIds = question.options.map((option) => option.optionId);
|
|
4186
|
+
if (new Set(optionIds).size !== optionIds.length) {
|
|
4187
|
+
ctx.addIssue({
|
|
4188
|
+
code: "custom",
|
|
4189
|
+
message: "optionId values must be unique",
|
|
4190
|
+
path: ["options"]
|
|
4191
|
+
});
|
|
4192
|
+
}
|
|
4193
|
+
if (question.inputMode === "text" && question.options.length !== 0) {
|
|
4194
|
+
ctx.addIssue({
|
|
4195
|
+
code: "custom",
|
|
4196
|
+
message: "text questions cannot carry options",
|
|
4197
|
+
path: ["options"]
|
|
4198
|
+
});
|
|
4199
|
+
}
|
|
4200
|
+
if (question.inputMode !== "text" && question.options.length === 0) {
|
|
4201
|
+
ctx.addIssue({
|
|
4202
|
+
code: "custom",
|
|
4203
|
+
message: "select questions require options",
|
|
4204
|
+
path: ["options"]
|
|
4205
|
+
});
|
|
4206
|
+
}
|
|
4207
|
+
});
|
|
4208
|
+
var TERMINAL_PROMPT_STATES = /* @__PURE__ */ new Set(["resolved", "cancelled", "expired", "unavailable"]);
|
|
4209
|
+
var PromptSchema = import_zod.z.object({
|
|
4210
|
+
schemaVersion: import_zod.z.literal(PROMPT_SCHEMA_VERSION),
|
|
4211
|
+
sessionId: OpaqueIdSchema,
|
|
4212
|
+
promptId: OpaqueIdSchema,
|
|
4213
|
+
revision: import_zod.z.number().int().positive(),
|
|
4214
|
+
state: import_zod.z.enum(["open", "updated", "resolved", "cancelled", "expired", "unavailable"]),
|
|
4215
|
+
terminalReason: MeaningfulStringSchema.optional(),
|
|
4216
|
+
intent: import_zod.z.enum(["approval", "question"]),
|
|
4217
|
+
title: import_zod.z.string().optional(),
|
|
4218
|
+
message: import_zod.z.string().optional(),
|
|
4219
|
+
detail: import_zod.z.string().optional(),
|
|
4220
|
+
questions: import_zod.z.array(PromptQuestionSchema).min(1),
|
|
4221
|
+
answerRequirement: import_zod.z.enum(["blocking", "non_blocking", "unknown"]),
|
|
4222
|
+
expiresAt: import_zod.z.string().datetime({ offset: true }).nullable(),
|
|
4223
|
+
provenance: import_zod.z.object({
|
|
4224
|
+
source: import_zod.z.enum(["provider", "screen", "transcript", "synthetic"]),
|
|
4225
|
+
confidence: import_zod.z.enum(["authoritative", "inferred"])
|
|
4226
|
+
})
|
|
4227
|
+
}).superRefine((prompt, ctx) => {
|
|
4228
|
+
if (![prompt.title, prompt.message, prompt.detail].some((value) => value?.trim())) {
|
|
4229
|
+
ctx.addIssue({
|
|
4230
|
+
code: "custom",
|
|
4231
|
+
message: "prompt requires a meaningful title, message, or detail",
|
|
4232
|
+
path: ["message"]
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
const questionIds = prompt.questions.map((question) => question.questionId);
|
|
4236
|
+
if (new Set(questionIds).size !== questionIds.length) {
|
|
4237
|
+
ctx.addIssue({
|
|
4238
|
+
code: "custom",
|
|
4239
|
+
message: "questionId values must be unique",
|
|
4240
|
+
path: ["questions"]
|
|
4241
|
+
});
|
|
4242
|
+
}
|
|
4243
|
+
const optionIds = prompt.questions.flatMap(
|
|
4244
|
+
(question) => question.options.map((option) => option.optionId)
|
|
4245
|
+
);
|
|
4246
|
+
if (new Set(optionIds).size !== optionIds.length) {
|
|
4247
|
+
ctx.addIssue({
|
|
4248
|
+
code: "custom",
|
|
4249
|
+
message: "optionId values must be unique within a prompt",
|
|
4250
|
+
path: ["questions"]
|
|
4251
|
+
});
|
|
4252
|
+
}
|
|
4253
|
+
const terminal = TERMINAL_PROMPT_STATES.has(prompt.state);
|
|
4254
|
+
if (terminal !== (prompt.terminalReason !== void 0)) {
|
|
4255
|
+
ctx.addIssue({
|
|
4256
|
+
code: "custom",
|
|
4257
|
+
message: terminal ? "terminal prompts require terminalReason" : "actionable prompts cannot carry terminalReason",
|
|
4258
|
+
path: ["terminalReason"]
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
});
|
|
4262
|
+
var OptionResponseSchema = import_zod.z.object({
|
|
4263
|
+
questionId: OpaqueIdSchema,
|
|
4264
|
+
optionIds: import_zod.z.array(OpaqueIdSchema).min(1).refine((ids) => new Set(ids).size === ids.length, "optionIds must be unique"),
|
|
4265
|
+
text: import_zod.z.never().optional()
|
|
4266
|
+
});
|
|
4267
|
+
var TextResponseSchema = import_zod.z.object({
|
|
4268
|
+
questionId: OpaqueIdSchema,
|
|
4269
|
+
text: import_zod.z.string(),
|
|
4270
|
+
optionIds: import_zod.z.never().optional()
|
|
4271
|
+
});
|
|
4272
|
+
var PromptResponseSchema = import_zod.z.union([OptionResponseSchema, TextResponseSchema]);
|
|
4273
|
+
var PromptAnswerSchema = import_zod.z.object({
|
|
4274
|
+
promptId: OpaqueIdSchema,
|
|
4275
|
+
revision: import_zod.z.number().int().positive(),
|
|
4276
|
+
responses: import_zod.z.array(PromptResponseSchema).min(1),
|
|
4277
|
+
idempotencyKey: OpaqueIdSchema
|
|
4278
|
+
}).superRefine((answer, ctx) => {
|
|
4279
|
+
const questionIds = answer.responses.map((response) => response.questionId);
|
|
4280
|
+
if (new Set(questionIds).size !== questionIds.length) {
|
|
4281
|
+
ctx.addIssue({
|
|
4282
|
+
code: "custom",
|
|
4283
|
+
message: "each questionId can be answered only once",
|
|
4284
|
+
path: ["responses"]
|
|
4285
|
+
});
|
|
4286
|
+
}
|
|
4287
|
+
});
|
|
4288
|
+
|
|
4142
4289
|
// src/server.ts
|
|
4143
4290
|
var import_node_ws = require("@hono/node-ws");
|
|
4144
4291
|
var import_client = require("@temporalio/client");
|
|
@@ -4778,11 +4925,11 @@ var createBrowseRoutes = (deps) => {
|
|
|
4778
4925
|
var import_hono4 = require("hono");
|
|
4779
4926
|
|
|
4780
4927
|
// src/schemas/cacheAlert.schema.ts
|
|
4781
|
-
var
|
|
4782
|
-
var ResolveCacheAlertSchema =
|
|
4783
|
-
fingerprint:
|
|
4784
|
-
action:
|
|
4785
|
-
ids:
|
|
4928
|
+
var import_zod2 = require("zod");
|
|
4929
|
+
var ResolveCacheAlertSchema = import_zod2.z.object({
|
|
4930
|
+
fingerprint: import_zod2.z.string(),
|
|
4931
|
+
action: import_zod2.z.enum(["prune_all", "prune_selected", "ignore", "reset_rescan"]),
|
|
4932
|
+
ids: import_zod2.z.array(import_zod2.z.string()).optional()
|
|
4786
4933
|
}).refine((v) => v.action !== "prune_selected" || v.ids !== void 0 && v.ids.length > 0, {
|
|
4787
4934
|
message: "prune_selected requires a non-empty ids array",
|
|
4788
4935
|
path: ["ids"]
|
|
@@ -4838,12 +4985,12 @@ var createCacheAlertRoutes = (deps) => {
|
|
|
4838
4985
|
var import_hono5 = require("hono");
|
|
4839
4986
|
|
|
4840
4987
|
// src/schemas/claudeFlags.schema.ts
|
|
4841
|
-
var
|
|
4842
|
-
var ClaudeFlagsBodySchema =
|
|
4843
|
-
values:
|
|
4988
|
+
var import_zod3 = require("zod");
|
|
4989
|
+
var ClaudeFlagsBodySchema = import_zod3.z.object({
|
|
4990
|
+
values: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.union([import_zod3.z.string(), import_zod3.z.boolean(), import_zod3.z.array(import_zod3.z.string())])).default({}),
|
|
4844
4991
|
// A newline would corrupt the flat one-line-per-key server.yaml, so reject
|
|
4845
4992
|
// it here with a field error instead of silently stripping it.
|
|
4846
|
-
extraArgs:
|
|
4993
|
+
extraArgs: import_zod3.z.string().refine((v) => !/[\r\n]/.test(v), "extraArgs must not contain newlines").optional()
|
|
4847
4994
|
}).strict();
|
|
4848
4995
|
|
|
4849
4996
|
// src/api/routes/config.routes.ts
|
|
@@ -5377,15 +5524,15 @@ var import_node_path5 = require("path");
|
|
|
5377
5524
|
var import_yaml = require("yaml");
|
|
5378
5525
|
|
|
5379
5526
|
// src/schemas/updateConfig.schema.ts
|
|
5380
|
-
var
|
|
5381
|
-
var UpdateConfigSchema =
|
|
5382
|
-
auto_update:
|
|
5383
|
-
channel:
|
|
5384
|
-
allow:
|
|
5385
|
-
poll_interval_minutes:
|
|
5386
|
-
defer_if_active_sessions:
|
|
5387
|
-
github_repo:
|
|
5388
|
-
webhook_secret:
|
|
5527
|
+
var import_zod4 = require("zod");
|
|
5528
|
+
var UpdateConfigSchema = import_zod4.z.object({
|
|
5529
|
+
auto_update: import_zod4.z.boolean().default(false),
|
|
5530
|
+
channel: import_zod4.z.enum(["stable", "next"]).default("stable"),
|
|
5531
|
+
allow: import_zod4.z.array(import_zod4.z.enum(["patch", "minor", "major"])).default(["patch", "minor"]),
|
|
5532
|
+
poll_interval_minutes: import_zod4.z.number().int().min(0).default(1440),
|
|
5533
|
+
defer_if_active_sessions: import_zod4.z.boolean().default(true),
|
|
5534
|
+
github_repo: import_zod4.z.string().regex(/^[^/]+\/[^/]+$/, "github_repo must be 'owner/name'"),
|
|
5535
|
+
webhook_secret: import_zod4.z.string().min(1).nullable().default(null)
|
|
5389
5536
|
}).strict();
|
|
5390
5537
|
|
|
5391
5538
|
// src/config/update-config.ts
|
|
@@ -6010,7 +6157,13 @@ var createMiscRoutes = (deps) => {
|
|
|
6010
6157
|
// the box is starved. Additive capability flag only — live readings stay
|
|
6011
6158
|
// off this polled endpoint. Absent means an older server that never
|
|
6012
6159
|
// samples. Informational: pressure never holds, kills, or refuses sessions.
|
|
6013
|
-
hostPressure: true
|
|
6160
|
+
hostPressure: true,
|
|
6161
|
+
// Provider-neutral prompt contract: normalized prompt events, opaque ids
|
|
6162
|
+
// and the atomic /prompt/answer route. A prompt_snapshot on subscribe
|
|
6163
|
+
// carries RETAINED prompts, terminal ones included — render on `state`,
|
|
6164
|
+
// not on presence — and an answer retry after that retention window is
|
|
6165
|
+
// answered 404 prompt_not_found rather than the recorded outcome.
|
|
6166
|
+
promptContract: { schemaVersion: 1, atomicAnswer: true }
|
|
6014
6167
|
});
|
|
6015
6168
|
});
|
|
6016
6169
|
app.get("/api/profiles", (c) => c.json([]));
|
|
@@ -6379,6 +6532,10 @@ var createSessionRoutes = (deps) => {
|
|
|
6379
6532
|
await deps.handleSendAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6380
6533
|
return alreadyHandled6();
|
|
6381
6534
|
});
|
|
6535
|
+
app.post("/:id/prompt/answer", async (c) => {
|
|
6536
|
+
await deps.handlePromptAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6537
|
+
return alreadyHandled6();
|
|
6538
|
+
});
|
|
6382
6539
|
app.post("/:id/permission/answer", async (c) => {
|
|
6383
6540
|
await deps.handlePermissionAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6384
6541
|
return alreadyHandled6();
|
|
@@ -9584,6 +9741,396 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
9584
9741
|
);
|
|
9585
9742
|
}
|
|
9586
9743
|
|
|
9744
|
+
// src/services/prompts/promptRegistry.ts
|
|
9745
|
+
var import_node_crypto5 = require("crypto");
|
|
9746
|
+
var PROMPT_TERMINAL_RETENTION_MS = 10 * 60 * 1e3;
|
|
9747
|
+
var PROMPT_MAX_RECORDS_PER_SESSION = 200;
|
|
9748
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
9749
|
+
function copyPrompt(prompt) {
|
|
9750
|
+
return {
|
|
9751
|
+
...prompt,
|
|
9752
|
+
questions: prompt.questions.map((question) => ({
|
|
9753
|
+
...question,
|
|
9754
|
+
options: question.options.map((option) => ({ ...option }))
|
|
9755
|
+
})),
|
|
9756
|
+
provenance: { ...prompt.provenance }
|
|
9757
|
+
};
|
|
9758
|
+
}
|
|
9759
|
+
function terminalError(state) {
|
|
9760
|
+
switch (state) {
|
|
9761
|
+
case "resolved":
|
|
9762
|
+
return "already_resolved";
|
|
9763
|
+
case "expired":
|
|
9764
|
+
return "prompt_expired";
|
|
9765
|
+
case "cancelled":
|
|
9766
|
+
return "prompt_cancelled";
|
|
9767
|
+
case "unavailable":
|
|
9768
|
+
return "prompt_unavailable";
|
|
9769
|
+
}
|
|
9770
|
+
}
|
|
9771
|
+
var PromptRegistry = class {
|
|
9772
|
+
bySession = /* @__PURE__ */ new Map();
|
|
9773
|
+
byId = /* @__PURE__ */ new Map();
|
|
9774
|
+
sequences = /* @__PURE__ */ new Map();
|
|
9775
|
+
createId;
|
|
9776
|
+
emit;
|
|
9777
|
+
onExpire;
|
|
9778
|
+
now;
|
|
9779
|
+
terminalRetentionMs;
|
|
9780
|
+
maxRecordsPerSession;
|
|
9781
|
+
constructor(options = {}) {
|
|
9782
|
+
this.createId = options.createId ?? import_node_crypto5.randomUUID;
|
|
9783
|
+
this.emit = options.emit;
|
|
9784
|
+
this.onExpire = options.onExpire;
|
|
9785
|
+
this.now = options.now ?? Date.now;
|
|
9786
|
+
this.terminalRetentionMs = options.terminalRetentionMs ?? PROMPT_TERMINAL_RETENTION_MS;
|
|
9787
|
+
this.maxRecordsPerSession = options.maxRecordsPerSession ?? PROMPT_MAX_RECORDS_PER_SESSION;
|
|
9788
|
+
}
|
|
9789
|
+
open(draft, adapter, promptId = this.createId()) {
|
|
9790
|
+
this.prune(draft.sessionId);
|
|
9791
|
+
const held = this.byId.get(promptId);
|
|
9792
|
+
if (held && (held.prompt.state === "open" || held.prompt.state === "updated")) {
|
|
9793
|
+
throw new Error(`Prompt id already exists: ${promptId}`);
|
|
9794
|
+
}
|
|
9795
|
+
const id = held ? this.createId() : promptId;
|
|
9796
|
+
const prompt = PromptSchema.parse({
|
|
9797
|
+
...draft,
|
|
9798
|
+
schemaVersion: PROMPT_SCHEMA_VERSION,
|
|
9799
|
+
promptId: id,
|
|
9800
|
+
revision: 1,
|
|
9801
|
+
state: "open",
|
|
9802
|
+
questions: draft.questions.map((question) => ({
|
|
9803
|
+
...question,
|
|
9804
|
+
questionId: this.createId(),
|
|
9805
|
+
options: question.options.map((option) => ({ ...option, optionId: this.createId() }))
|
|
9806
|
+
})),
|
|
9807
|
+
provenance: { ...draft.provenance }
|
|
9808
|
+
});
|
|
9809
|
+
const entry = {
|
|
9810
|
+
prompt,
|
|
9811
|
+
adapter,
|
|
9812
|
+
queue: Promise.resolve(),
|
|
9813
|
+
inFlight: /* @__PURE__ */ new Map(),
|
|
9814
|
+
outcomes: /* @__PURE__ */ new Map()
|
|
9815
|
+
};
|
|
9816
|
+
const session = this.bySession.get(prompt.sessionId) ?? /* @__PURE__ */ new Map();
|
|
9817
|
+
session.set(prompt.promptId, entry);
|
|
9818
|
+
this.bySession.set(prompt.sessionId, session);
|
|
9819
|
+
this.byId.set(prompt.promptId, entry);
|
|
9820
|
+
this.publish(entry);
|
|
9821
|
+
this.scheduleExpiration(entry);
|
|
9822
|
+
this.enforceCap(prompt.sessionId);
|
|
9823
|
+
return copyPrompt(prompt);
|
|
9824
|
+
}
|
|
9825
|
+
update(promptId, draft, adapter) {
|
|
9826
|
+
const entry = this.requireEntry(promptId);
|
|
9827
|
+
this.expireIfDue(entry, this.now());
|
|
9828
|
+
if (entry.prompt.sessionId !== draft.sessionId) throw new Error("Prompt session cannot change");
|
|
9829
|
+
if (entry.prompt.state !== "open" && entry.prompt.state !== "updated") {
|
|
9830
|
+
throw new Error(`Cannot update terminal prompt ${promptId}`);
|
|
9831
|
+
}
|
|
9832
|
+
if (entry.prompt.questions.length !== draft.questions.length) {
|
|
9833
|
+
throw new Error("Prompt question cardinality cannot change during an update");
|
|
9834
|
+
}
|
|
9835
|
+
const questions = draft.questions.map((question, questionIndex) => {
|
|
9836
|
+
const prior = entry.prompt.questions[questionIndex];
|
|
9837
|
+
if (prior.options.length !== question.options.length) {
|
|
9838
|
+
throw new Error("Prompt option cardinality cannot change during an update");
|
|
9839
|
+
}
|
|
9840
|
+
return {
|
|
9841
|
+
...question,
|
|
9842
|
+
questionId: prior.questionId,
|
|
9843
|
+
options: question.options.map((option, optionIndex) => ({
|
|
9844
|
+
...option,
|
|
9845
|
+
optionId: prior.options[optionIndex].optionId
|
|
9846
|
+
}))
|
|
9847
|
+
};
|
|
9848
|
+
});
|
|
9849
|
+
const prompt = PromptSchema.parse({
|
|
9850
|
+
...draft,
|
|
9851
|
+
schemaVersion: PROMPT_SCHEMA_VERSION,
|
|
9852
|
+
promptId,
|
|
9853
|
+
revision: entry.prompt.revision + 1,
|
|
9854
|
+
state: "updated",
|
|
9855
|
+
questions,
|
|
9856
|
+
provenance: { ...draft.provenance }
|
|
9857
|
+
});
|
|
9858
|
+
entry.prompt = prompt;
|
|
9859
|
+
if (adapter) entry.adapter = adapter;
|
|
9860
|
+
this.publish(entry);
|
|
9861
|
+
this.scheduleExpiration(entry);
|
|
9862
|
+
return copyPrompt(entry.prompt);
|
|
9863
|
+
}
|
|
9864
|
+
transition(promptId, state, reason) {
|
|
9865
|
+
const entry = this.requireEntry(promptId);
|
|
9866
|
+
if (entry.prompt.state !== "open" && entry.prompt.state !== "updated") {
|
|
9867
|
+
throw new Error(`Cannot transition terminal prompt ${promptId}`);
|
|
9868
|
+
}
|
|
9869
|
+
entry.prompt = {
|
|
9870
|
+
...entry.prompt,
|
|
9871
|
+
revision: entry.prompt.revision + 1,
|
|
9872
|
+
state,
|
|
9873
|
+
terminalReason: reason
|
|
9874
|
+
};
|
|
9875
|
+
entry.terminalAt = this.now();
|
|
9876
|
+
this.clearExpiration(entry);
|
|
9877
|
+
this.publish(entry);
|
|
9878
|
+
return copyPrompt(entry.prompt);
|
|
9879
|
+
}
|
|
9880
|
+
invalidateSession(sessionId, reason = "session_ended") {
|
|
9881
|
+
const transitioned = [];
|
|
9882
|
+
for (const entry of this.bySession.get(sessionId)?.values() ?? []) {
|
|
9883
|
+
if (entry.prompt.state === "open" || entry.prompt.state === "updated") {
|
|
9884
|
+
transitioned.push(this.transition(entry.prompt.promptId, "unavailable", reason));
|
|
9885
|
+
}
|
|
9886
|
+
}
|
|
9887
|
+
return transitioned;
|
|
9888
|
+
}
|
|
9889
|
+
get(promptId) {
|
|
9890
|
+
const entry = this.byId.get(promptId);
|
|
9891
|
+
if (!entry) return null;
|
|
9892
|
+
this.prune(entry.prompt.sessionId);
|
|
9893
|
+
return this.byId.has(promptId) ? copyPrompt(entry.prompt) : null;
|
|
9894
|
+
}
|
|
9895
|
+
hasActionable(sessionId) {
|
|
9896
|
+
this.prune(sessionId);
|
|
9897
|
+
return [...this.bySession.get(sessionId)?.values() ?? []].some(
|
|
9898
|
+
(entry) => entry.prompt.state === "open" || entry.prompt.state === "updated"
|
|
9899
|
+
);
|
|
9900
|
+
}
|
|
9901
|
+
snapshot(sessionId) {
|
|
9902
|
+
this.prune(sessionId);
|
|
9903
|
+
return {
|
|
9904
|
+
type: "prompt_snapshot",
|
|
9905
|
+
schemaVersion: PROMPT_SCHEMA_VERSION,
|
|
9906
|
+
sessionId,
|
|
9907
|
+
sequence: this.sequences.get(sessionId) ?? 0,
|
|
9908
|
+
prompts: [...this.bySession.get(sessionId)?.values() ?? []].map(
|
|
9909
|
+
(entry) => copyPrompt(entry.prompt)
|
|
9910
|
+
)
|
|
9911
|
+
};
|
|
9912
|
+
}
|
|
9913
|
+
dispose() {
|
|
9914
|
+
for (const entry of this.byId.values()) this.clearExpiration(entry);
|
|
9915
|
+
}
|
|
9916
|
+
answer(sessionId, answer) {
|
|
9917
|
+
this.prune(sessionId);
|
|
9918
|
+
const entry = this.byId.get(answer.promptId);
|
|
9919
|
+
if (!entry || entry.prompt.sessionId !== sessionId) {
|
|
9920
|
+
return Promise.resolve({ ok: false, code: "prompt_not_found" });
|
|
9921
|
+
}
|
|
9922
|
+
this.pruneOutcomes(entry);
|
|
9923
|
+
const recorded = entry.outcomes.get(answer.idempotencyKey);
|
|
9924
|
+
if (recorded) return Promise.resolve(recorded.outcome);
|
|
9925
|
+
const pending = entry.inFlight.get(answer.idempotencyKey);
|
|
9926
|
+
if (pending) return pending;
|
|
9927
|
+
const task = entry.queue.then(() => this.performAnswer(entry, answer));
|
|
9928
|
+
entry.queue = task.then(
|
|
9929
|
+
() => void 0,
|
|
9930
|
+
() => void 0
|
|
9931
|
+
);
|
|
9932
|
+
entry.inFlight.set(answer.idempotencyKey, task);
|
|
9933
|
+
void task.then((outcome) => {
|
|
9934
|
+
entry.inFlight.delete(answer.idempotencyKey);
|
|
9935
|
+
entry.outcomes.set(answer.idempotencyKey, { at: this.now(), outcome });
|
|
9936
|
+
});
|
|
9937
|
+
return task;
|
|
9938
|
+
}
|
|
9939
|
+
async performAnswer(entry, answer) {
|
|
9940
|
+
const prompt = entry.prompt;
|
|
9941
|
+
if (prompt.state !== "open" && prompt.state !== "updated") {
|
|
9942
|
+
return { ok: false, code: terminalError(prompt.state) };
|
|
9943
|
+
}
|
|
9944
|
+
if (this.expireIfDue(entry, this.now())) {
|
|
9945
|
+
return { ok: false, code: "prompt_expired" };
|
|
9946
|
+
}
|
|
9947
|
+
if (prompt.revision !== answer.revision) {
|
|
9948
|
+
return {
|
|
9949
|
+
ok: false,
|
|
9950
|
+
code: "prompt_revision_mismatch",
|
|
9951
|
+
currentRevision: prompt.revision
|
|
9952
|
+
};
|
|
9953
|
+
}
|
|
9954
|
+
const responseError = this.validateResponses(prompt, answer);
|
|
9955
|
+
if (responseError) return { ok: false, code: responseError };
|
|
9956
|
+
if (!entry.adapter) return { ok: false, code: "prompt_unavailable" };
|
|
9957
|
+
let adapterResult;
|
|
9958
|
+
try {
|
|
9959
|
+
adapterResult = await entry.adapter({ prompt: copyPrompt(prompt), answer });
|
|
9960
|
+
} catch {
|
|
9961
|
+
return { ok: false, code: "provider_error" };
|
|
9962
|
+
}
|
|
9963
|
+
if (entry.prompt.state !== "open" && entry.prompt.state !== "updated") {
|
|
9964
|
+
return { ok: false, code: terminalError(entry.prompt.state) };
|
|
9965
|
+
}
|
|
9966
|
+
if (entry.prompt.revision !== answer.revision) {
|
|
9967
|
+
return {
|
|
9968
|
+
ok: false,
|
|
9969
|
+
code: "prompt_revision_mismatch",
|
|
9970
|
+
currentRevision: entry.prompt.revision
|
|
9971
|
+
};
|
|
9972
|
+
}
|
|
9973
|
+
if (!adapterResult.ok) {
|
|
9974
|
+
if (adapterResult.terminal) {
|
|
9975
|
+
this.transition(
|
|
9976
|
+
prompt.promptId,
|
|
9977
|
+
adapterResult.terminal.state,
|
|
9978
|
+
adapterResult.terminal.reason
|
|
9979
|
+
);
|
|
9980
|
+
}
|
|
9981
|
+
return { ok: false, code: adapterResult.code };
|
|
9982
|
+
}
|
|
9983
|
+
return { ok: true, prompt: this.transition(prompt.promptId, "resolved", "answered") };
|
|
9984
|
+
}
|
|
9985
|
+
validateResponses(prompt, answer) {
|
|
9986
|
+
const questions = new Map(prompt.questions.map((question) => [question.questionId, question]));
|
|
9987
|
+
for (const response of answer.responses) {
|
|
9988
|
+
if (!questions.has(response.questionId)) return "unknown_question";
|
|
9989
|
+
}
|
|
9990
|
+
if (answer.responses.length !== prompt.questions.length) return "incomplete_answer";
|
|
9991
|
+
const responses = new Map(answer.responses.map((response) => [response.questionId, response]));
|
|
9992
|
+
for (const question of prompt.questions) {
|
|
9993
|
+
const response = responses.get(question.questionId);
|
|
9994
|
+
if (!response) return "incomplete_answer";
|
|
9995
|
+
if (question.inputMode === "text") {
|
|
9996
|
+
if (typeof response.text !== "string") return "incomplete_answer";
|
|
9997
|
+
continue;
|
|
9998
|
+
}
|
|
9999
|
+
const optionIds = response.optionIds;
|
|
10000
|
+
if (!optionIds) return "incomplete_answer";
|
|
10001
|
+
if (question.inputMode === "single" && optionIds.length !== 1) {
|
|
10002
|
+
return "unsupported_prompt_shape";
|
|
10003
|
+
}
|
|
10004
|
+
const known = new Set(question.options.map((option) => option.optionId));
|
|
10005
|
+
if (optionIds.some((optionId) => !known.has(optionId))) return "unknown_option";
|
|
10006
|
+
}
|
|
10007
|
+
return null;
|
|
10008
|
+
}
|
|
10009
|
+
publish(entry) {
|
|
10010
|
+
const sessionId = entry.prompt.sessionId;
|
|
10011
|
+
const sequence = (this.sequences.get(sessionId) ?? 0) + 1;
|
|
10012
|
+
this.sequences.set(sessionId, sequence);
|
|
10013
|
+
this.emit?.({
|
|
10014
|
+
type: "prompt_event",
|
|
10015
|
+
sessionId,
|
|
10016
|
+
sequence,
|
|
10017
|
+
prompt: copyPrompt(entry.prompt)
|
|
10018
|
+
});
|
|
10019
|
+
}
|
|
10020
|
+
scheduleExpiration(entry) {
|
|
10021
|
+
this.clearExpiration(entry);
|
|
10022
|
+
const expiresAt = entry.prompt.expiresAt;
|
|
10023
|
+
if (expiresAt === null) return;
|
|
10024
|
+
const delay = Math.min(MAX_TIMEOUT_MS, Math.max(0, Date.parse(expiresAt) - this.now()));
|
|
10025
|
+
entry.expiryTimer = setTimeout(() => {
|
|
10026
|
+
entry.expiryTimer = void 0;
|
|
10027
|
+
if (!this.expireIfDue(entry, this.now())) this.scheduleExpiration(entry);
|
|
10028
|
+
}, delay);
|
|
10029
|
+
entry.expiryTimer.unref?.();
|
|
10030
|
+
}
|
|
10031
|
+
clearExpiration(entry) {
|
|
10032
|
+
if (entry.expiryTimer) clearTimeout(entry.expiryTimer);
|
|
10033
|
+
entry.expiryTimer = void 0;
|
|
10034
|
+
}
|
|
10035
|
+
expireIfDue(entry, now) {
|
|
10036
|
+
if (entry.prompt.state !== "open" && entry.prompt.state !== "updated") return false;
|
|
10037
|
+
if (entry.prompt.expiresAt === null || now < Date.parse(entry.prompt.expiresAt)) return false;
|
|
10038
|
+
const expired = this.transition(entry.prompt.promptId, "expired", "deadline_elapsed");
|
|
10039
|
+
this.onExpire?.(expired);
|
|
10040
|
+
return true;
|
|
10041
|
+
}
|
|
10042
|
+
requireEntry(promptId) {
|
|
10043
|
+
const entry = this.byId.get(promptId);
|
|
10044
|
+
if (!entry) throw new Error(`Unknown prompt: ${promptId}`);
|
|
10045
|
+
return entry;
|
|
10046
|
+
}
|
|
10047
|
+
prune(sessionId) {
|
|
10048
|
+
const now = this.now();
|
|
10049
|
+
const session = this.bySession.get(sessionId);
|
|
10050
|
+
if (!session) return;
|
|
10051
|
+
for (const [promptId, entry] of session) {
|
|
10052
|
+
this.expireIfDue(entry, now);
|
|
10053
|
+
if (entry.terminalAt !== void 0 && now - entry.terminalAt > this.terminalRetentionMs) {
|
|
10054
|
+
this.clearExpiration(entry);
|
|
10055
|
+
session.delete(promptId);
|
|
10056
|
+
this.byId.delete(promptId);
|
|
10057
|
+
}
|
|
10058
|
+
}
|
|
10059
|
+
if (session.size === 0) this.bySession.delete(sessionId);
|
|
10060
|
+
}
|
|
10061
|
+
enforceCap(sessionId) {
|
|
10062
|
+
const session = this.bySession.get(sessionId);
|
|
10063
|
+
if (!session || session.size <= this.maxRecordsPerSession) return;
|
|
10064
|
+
const terminal = [...session.entries()].filter(([, entry]) => entry.terminalAt !== void 0).sort((a, b) => (a[1].terminalAt ?? 0) - (b[1].terminalAt ?? 0));
|
|
10065
|
+
while (session.size > this.maxRecordsPerSession && terminal.length > 0) {
|
|
10066
|
+
const [promptId, entry] = terminal.shift();
|
|
10067
|
+
this.clearExpiration(entry);
|
|
10068
|
+
session.delete(promptId);
|
|
10069
|
+
this.byId.delete(promptId);
|
|
10070
|
+
}
|
|
10071
|
+
}
|
|
10072
|
+
pruneOutcomes(entry) {
|
|
10073
|
+
const now = this.now();
|
|
10074
|
+
for (const [key, recorded] of entry.outcomes) {
|
|
10075
|
+
if (now - recorded.at > this.terminalRetentionMs) entry.outcomes.delete(key);
|
|
10076
|
+
}
|
|
10077
|
+
}
|
|
10078
|
+
};
|
|
10079
|
+
|
|
10080
|
+
// src/services/prompts/ptyPromptAdapter.ts
|
|
10081
|
+
function permissionPromptDraft(sessionId, gate) {
|
|
10082
|
+
if (!gate) throw new Error("Cannot normalize an absent permission gate");
|
|
10083
|
+
const message = gate.prompt?.trim() || "Approval required";
|
|
10084
|
+
return {
|
|
10085
|
+
sessionId,
|
|
10086
|
+
intent: "approval",
|
|
10087
|
+
title: "Approval",
|
|
10088
|
+
message,
|
|
10089
|
+
...gate.detail?.trim() ? { detail: gate.detail } : {},
|
|
10090
|
+
questions: [
|
|
10091
|
+
{
|
|
10092
|
+
text: message,
|
|
10093
|
+
header: "Approval",
|
|
10094
|
+
inputMode: "single",
|
|
10095
|
+
options: gate.options.map((option) => ({ label: option.label })),
|
|
10096
|
+
allowOther: false,
|
|
10097
|
+
secret: "unknown"
|
|
10098
|
+
}
|
|
10099
|
+
],
|
|
10100
|
+
answerRequirement: "unknown",
|
|
10101
|
+
expiresAt: null,
|
|
10102
|
+
provenance: { source: "screen", confidence: "inferred" }
|
|
10103
|
+
};
|
|
10104
|
+
}
|
|
10105
|
+
function questionPromptDraft(sessionId, questions, source) {
|
|
10106
|
+
const first = questions[0];
|
|
10107
|
+
if (!first) throw new Error("Cannot normalize an empty question list");
|
|
10108
|
+
return {
|
|
10109
|
+
sessionId,
|
|
10110
|
+
intent: "question",
|
|
10111
|
+
...first.header.trim() ? { title: first.header } : {},
|
|
10112
|
+
message: first.question,
|
|
10113
|
+
questions: questions.map((question) => ({
|
|
10114
|
+
text: question.question,
|
|
10115
|
+
...question.header.trim() ? { header: question.header } : {},
|
|
10116
|
+
inputMode: question.multiSelect ? "multi" : "single",
|
|
10117
|
+
options: question.options.map((option) => ({
|
|
10118
|
+
label: option.label,
|
|
10119
|
+
...option.description ? { description: option.description } : {},
|
|
10120
|
+
...option.preview ? { preview: option.preview } : {}
|
|
10121
|
+
})),
|
|
10122
|
+
allowOther: false,
|
|
10123
|
+
secret: "unknown"
|
|
10124
|
+
})),
|
|
10125
|
+
answerRequirement: "unknown",
|
|
10126
|
+
expiresAt: null,
|
|
10127
|
+
provenance: {
|
|
10128
|
+
source,
|
|
10129
|
+
confidence: source === "transcript" ? "authoritative" : "inferred"
|
|
10130
|
+
}
|
|
10131
|
+
};
|
|
10132
|
+
}
|
|
10133
|
+
|
|
9587
10134
|
// src/services/questions/parseStatusLine.ts
|
|
9588
10135
|
var MODEL_RE = /(Opus|Sonnet|Haiku|Fable)\s+[\d.]+(?:\s*\([^)]*\))?/;
|
|
9589
10136
|
var EFFORT_RE = /●\s*([A-Za-z]+)\s*·\s*\/effort/;
|
|
@@ -9946,6 +10493,21 @@ function codexSessionActiveBody(outcome) {
|
|
|
9946
10493
|
...outcome.ownerSource != null && { ownerSource: outcome.ownerSource }
|
|
9947
10494
|
};
|
|
9948
10495
|
}
|
|
10496
|
+
function promptAnswerStatus(code) {
|
|
10497
|
+
switch (code) {
|
|
10498
|
+
case "prompt_not_found":
|
|
10499
|
+
return 404;
|
|
10500
|
+
case "provider_error":
|
|
10501
|
+
return 502;
|
|
10502
|
+
case "unknown_question":
|
|
10503
|
+
case "unknown_option":
|
|
10504
|
+
case "incomplete_answer":
|
|
10505
|
+
case "unsupported_prompt_shape":
|
|
10506
|
+
return 400;
|
|
10507
|
+
default:
|
|
10508
|
+
return 409;
|
|
10509
|
+
}
|
|
10510
|
+
}
|
|
9949
10511
|
var SessionHandlers = class {
|
|
9950
10512
|
constructor(deps) {
|
|
9951
10513
|
this.deps = deps;
|
|
@@ -9984,6 +10546,10 @@ var SessionHandlers = class {
|
|
|
9984
10546
|
get pendingQuestions() {
|
|
9985
10547
|
return this.deps.pendingQuestions;
|
|
9986
10548
|
}
|
|
10549
|
+
get promptRegistry() {
|
|
10550
|
+
if (!this.deps.promptRegistry) this.deps.promptRegistry = new PromptRegistry();
|
|
10551
|
+
return this.deps.promptRegistry;
|
|
10552
|
+
}
|
|
9987
10553
|
get pendingQuestionKey() {
|
|
9988
10554
|
return this.deps.pendingQuestionKey;
|
|
9989
10555
|
}
|
|
@@ -10002,6 +10568,13 @@ var SessionHandlers = class {
|
|
|
10002
10568
|
get sessionSubscribers() {
|
|
10003
10569
|
return this.deps.sessionSubscribers;
|
|
10004
10570
|
}
|
|
10571
|
+
// Prompt lifecycle events (question / permission and their cancellations)
|
|
10572
|
+
// carry prompt content and go ONLY to the session's subscribers — never to
|
|
10573
|
+
// every connected socket. A client that subscribes after a prompt opened
|
|
10574
|
+
// gets it from the subscribe replay (server-wiring), not from a broadcast.
|
|
10575
|
+
broadcastToSession(sessionId, message) {
|
|
10576
|
+
this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], message);
|
|
10577
|
+
}
|
|
10005
10578
|
get agentConfig() {
|
|
10006
10579
|
return this.deps.agentConfig;
|
|
10007
10580
|
}
|
|
@@ -10495,6 +11068,7 @@ var SessionHandlers = class {
|
|
|
10495
11068
|
json(res, 400, { error: "Missing input field" });
|
|
10496
11069
|
return;
|
|
10497
11070
|
}
|
|
11071
|
+
this.promptRegistry.hasActionable(sessionId);
|
|
10498
11072
|
const openPrompt = this.pendingPermission.has(sessionId) ? "permission" : this.pendingQuestions.has(sessionId) ? "question" : null;
|
|
10499
11073
|
if (openPrompt) {
|
|
10500
11074
|
this.log.info(`[input.prompt_pending] ${sessionId.slice(0, 8)} kind=${openPrompt}`, {
|
|
@@ -10550,37 +11124,106 @@ var SessionHandlers = class {
|
|
|
10550
11124
|
// the later JSONL flush of the same question is de-duped. We synthesize a
|
|
10551
11125
|
// screen-scoped toolUseId; the JSONL path overwrites pendingQuestions with the
|
|
10552
11126
|
// real toolUseId when it lands, so answering works once JSONL catches up.
|
|
10553
|
-
handleLiveQuestion(sessionId, questions) {
|
|
11127
|
+
handleLiveQuestion(sessionId, questions, occurrenceId) {
|
|
10554
11128
|
const key = questionContentKey(questions);
|
|
10555
11129
|
if (this.pendingQuestionKey.get(sessionId) === key) return;
|
|
10556
11130
|
const toolUseId = `screen:${sessionId}:${key.length}`;
|
|
10557
|
-
this.pendingQuestions.
|
|
11131
|
+
const prior = this.pendingQuestions.get(sessionId);
|
|
11132
|
+
const priorPrompt = prior ? this.promptRegistry.get(prior.promptId) : null;
|
|
11133
|
+
if (priorPrompt?.state === "open" || priorPrompt?.state === "updated") {
|
|
11134
|
+
this.promptRegistry.transition(priorPrompt.promptId, "cancelled", "replaced");
|
|
11135
|
+
}
|
|
11136
|
+
const prompt = this.promptRegistry.open(
|
|
11137
|
+
questionPromptDraft(sessionId, questions, "screen"),
|
|
11138
|
+
this.questionAnswerAdapter(sessionId),
|
|
11139
|
+
occurrenceId
|
|
11140
|
+
);
|
|
11141
|
+
this.pendingQuestions.set(sessionId, {
|
|
11142
|
+
toolUseId,
|
|
11143
|
+
questions,
|
|
11144
|
+
origin: "pty",
|
|
11145
|
+
promptId: prompt.promptId
|
|
11146
|
+
});
|
|
10558
11147
|
this.pendingQuestionKey.set(sessionId, key);
|
|
10559
|
-
this.
|
|
11148
|
+
this.broadcastToSession(sessionId, { type: "question", sessionId, toolUseId, questions });
|
|
11149
|
+
}
|
|
11150
|
+
handleJsonlQuestion(sessionId, toolUseId, questions, origin) {
|
|
11151
|
+
const prior = this.pendingQuestions.get(sessionId);
|
|
11152
|
+
const sameQuestion = prior !== void 0 && questionContentKey(prior.questions) === questionContentKey(questions);
|
|
11153
|
+
let prompt;
|
|
11154
|
+
if (sameQuestion) {
|
|
11155
|
+
const current = this.promptRegistry.get(prior.promptId);
|
|
11156
|
+
prompt = current?.provenance.source === "transcript" ? current : this.promptRegistry.update(
|
|
11157
|
+
prior.promptId,
|
|
11158
|
+
questionPromptDraft(sessionId, questions, "transcript"),
|
|
11159
|
+
this.questionAnswerAdapter(sessionId)
|
|
11160
|
+
);
|
|
11161
|
+
} else {
|
|
11162
|
+
const priorPrompt = prior ? this.promptRegistry.get(prior.promptId) : null;
|
|
11163
|
+
if (priorPrompt?.state === "open" || priorPrompt?.state === "updated") {
|
|
11164
|
+
this.promptRegistry.transition(priorPrompt.promptId, "cancelled", "replaced");
|
|
11165
|
+
}
|
|
11166
|
+
prompt = this.promptRegistry.open(
|
|
11167
|
+
questionPromptDraft(sessionId, questions, "transcript"),
|
|
11168
|
+
this.questionAnswerAdapter(sessionId)
|
|
11169
|
+
);
|
|
11170
|
+
}
|
|
11171
|
+
this.pendingQuestions.set(sessionId, {
|
|
11172
|
+
toolUseId,
|
|
11173
|
+
questions,
|
|
11174
|
+
origin,
|
|
11175
|
+
promptId: prompt.promptId
|
|
11176
|
+
});
|
|
10560
11177
|
}
|
|
10561
11178
|
// Permission gate opened/closed (OSC 777 + scraped options). Broadcasts the
|
|
10562
11179
|
// additive `permission` / `permission_cancelled` events. Mobile answers by
|
|
10563
11180
|
// sending the chosen option index via /input { keys } (e.g. "2\r").
|
|
10564
|
-
handlePermissionChange(sessionId, gate) {
|
|
11181
|
+
handlePermissionChange(sessionId, gate, occurrenceId) {
|
|
10565
11182
|
if (gate === null) {
|
|
10566
|
-
|
|
11183
|
+
const prior2 = this.pendingPermission.get(sessionId);
|
|
11184
|
+
if (!prior2) return;
|
|
11185
|
+
const prompt2 = prior2.promptId ? this.promptRegistry.get(prior2.promptId) : null;
|
|
11186
|
+
if (prompt2?.state === "open" || prompt2?.state === "updated") {
|
|
11187
|
+
this.promptRegistry.transition(prompt2.promptId, "cancelled", "provider_closed");
|
|
11188
|
+
}
|
|
10567
11189
|
this.pendingPermission.delete(sessionId);
|
|
10568
11190
|
this.pendingPermissionKey.delete(sessionId);
|
|
10569
|
-
this.
|
|
11191
|
+
this.broadcastToSession(sessionId, { type: "permission_cancelled", sessionId });
|
|
10570
11192
|
return;
|
|
10571
11193
|
}
|
|
10572
11194
|
const key = permissionContentKey(gate);
|
|
10573
|
-
if (this.pendingPermissionKey.get(sessionId) === key) return;
|
|
10574
11195
|
const prior = this.pendingPermission.get(sessionId);
|
|
10575
|
-
const
|
|
10576
|
-
this.
|
|
11196
|
+
const priorPromptId = prior?.promptId;
|
|
11197
|
+
if (this.pendingPermissionKey.get(sessionId) === key && (occurrenceId === void 0 || prior?.occurrenceId === occurrenceId)) {
|
|
11198
|
+
return;
|
|
11199
|
+
}
|
|
11200
|
+
const samePrompt = prior && priorPromptId !== void 0 && permissionGateKey(prior) === permissionGateKey(gate) && (occurrenceId === void 0 || prior.occurrenceId === occurrenceId);
|
|
11201
|
+
if (prior && !samePrompt) {
|
|
11202
|
+
const priorPrompt = prior.promptId ? this.promptRegistry.get(prior.promptId) : null;
|
|
11203
|
+
if (priorPrompt?.state === "open" || priorPrompt?.state === "updated") {
|
|
11204
|
+
this.promptRegistry.transition(priorPrompt.promptId, "cancelled", "replaced");
|
|
11205
|
+
}
|
|
11206
|
+
}
|
|
11207
|
+
const prompt = gate.options.length === 0 ? null : samePrompt ? this.promptRegistry.get(priorPromptId) : this.promptRegistry.open(
|
|
11208
|
+
permissionPromptDraft(sessionId, gate),
|
|
11209
|
+
this.permissionAnswerAdapter(sessionId),
|
|
11210
|
+
occurrenceId
|
|
11211
|
+
);
|
|
11212
|
+
if (samePrompt && !prompt) throw new Error("Pending permission prompt disappeared");
|
|
11213
|
+
const gateId = prompt?.promptId ?? occurrenceId ?? prior?.gateId ?? (0, import_crypto9.randomUUID)();
|
|
11214
|
+
this.pendingPermission.set(sessionId, {
|
|
11215
|
+
...gate,
|
|
11216
|
+
gateId,
|
|
11217
|
+
...prompt ? { promptId: prompt.promptId } : {},
|
|
11218
|
+
...occurrenceId !== void 0 ? { occurrenceId } : {}
|
|
11219
|
+
});
|
|
10577
11220
|
this.pendingPermissionKey.set(sessionId, key);
|
|
10578
11221
|
const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
|
|
10579
11222
|
this.log.info(
|
|
10580
11223
|
`[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
|
|
10581
11224
|
{ event: "ws.broadcast_permission", sessionId, subscriberCount }
|
|
10582
11225
|
);
|
|
10583
|
-
this.
|
|
11226
|
+
this.broadcastToSession(sessionId, {
|
|
10584
11227
|
type: "permission",
|
|
10585
11228
|
sessionId,
|
|
10586
11229
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
@@ -10591,6 +11234,107 @@ var SessionHandlers = class {
|
|
|
10591
11234
|
gateId
|
|
10592
11235
|
});
|
|
10593
11236
|
}
|
|
11237
|
+
permissionAnswerAdapter(sessionId) {
|
|
11238
|
+
return async ({ prompt, answer }) => {
|
|
11239
|
+
const gate = this.pendingPermission.get(sessionId);
|
|
11240
|
+
if (!gate || gate.promptId !== prompt.promptId) {
|
|
11241
|
+
return {
|
|
11242
|
+
ok: false,
|
|
11243
|
+
code: "prompt_unavailable",
|
|
11244
|
+
terminal: { state: "unavailable", reason: "provider_prompt_missing" }
|
|
11245
|
+
};
|
|
11246
|
+
}
|
|
11247
|
+
const response = answer.responses[0];
|
|
11248
|
+
const selectedId = response?.optionIds?.[0];
|
|
11249
|
+
const selectedIndex = prompt.questions[0]?.options.findIndex(
|
|
11250
|
+
(option2) => option2.optionId === selectedId
|
|
11251
|
+
);
|
|
11252
|
+
if (selectedIndex === void 0 || selectedIndex < 0) {
|
|
11253
|
+
return { ok: false, code: "unknown_option" };
|
|
11254
|
+
}
|
|
11255
|
+
const option = gate.options[selectedIndex];
|
|
11256
|
+
if (!option) return { ok: false, code: "unknown_option" };
|
|
11257
|
+
const provider = this.sessionStore.getManaged(sessionId)?.provider;
|
|
11258
|
+
if (provider !== CODEX_CLI_PROVIDER && !await this.permissionGateStillOpen(sessionId, permissionGateKey(gate))) {
|
|
11259
|
+
return {
|
|
11260
|
+
ok: false,
|
|
11261
|
+
code: "prompt_cancelled",
|
|
11262
|
+
terminal: { state: "cancelled", reason: "provider_closed" }
|
|
11263
|
+
};
|
|
11264
|
+
}
|
|
11265
|
+
if (this.pendingPermission.get(sessionId)?.promptId !== prompt.promptId) {
|
|
11266
|
+
return { ok: false, code: "prompt_cancelled" };
|
|
11267
|
+
}
|
|
11268
|
+
try {
|
|
11269
|
+
this.ptyManager.sendKeys(
|
|
11270
|
+
sessionId,
|
|
11271
|
+
option.answerKeys ?? permissionAnswerKeys(option.index)
|
|
11272
|
+
);
|
|
11273
|
+
} catch {
|
|
11274
|
+
return { ok: false, code: "provider_error" };
|
|
11275
|
+
}
|
|
11276
|
+
return { ok: true };
|
|
11277
|
+
};
|
|
11278
|
+
}
|
|
11279
|
+
questionAnswerAdapter(sessionId) {
|
|
11280
|
+
return async ({ prompt, answer }) => {
|
|
11281
|
+
const pending = this.pendingQuestions.get(sessionId);
|
|
11282
|
+
if (!pending || pending.promptId !== prompt.promptId) {
|
|
11283
|
+
return {
|
|
11284
|
+
ok: false,
|
|
11285
|
+
code: "prompt_unavailable",
|
|
11286
|
+
terminal: { state: "unavailable", reason: "provider_prompt_missing" }
|
|
11287
|
+
};
|
|
11288
|
+
}
|
|
11289
|
+
const answers = {};
|
|
11290
|
+
for (const question of prompt.questions) {
|
|
11291
|
+
const response = answer.responses.find((item) => item.questionId === question.questionId);
|
|
11292
|
+
if (!response?.optionIds) return { ok: false, code: "unsupported_prompt_shape" };
|
|
11293
|
+
answers[question.text] = response.optionIds.map((optionId) => {
|
|
11294
|
+
const option = question.options.find((item) => item.optionId === optionId);
|
|
11295
|
+
return option?.label ?? "";
|
|
11296
|
+
});
|
|
11297
|
+
}
|
|
11298
|
+
const resolution = resolveAnswer(pending, {
|
|
11299
|
+
toolUseId: pending.toolUseId,
|
|
11300
|
+
answers
|
|
11301
|
+
});
|
|
11302
|
+
if (!resolution.ok) {
|
|
11303
|
+
const code = resolution.reason === "unknown_option" || resolution.reason === "incomplete_answer" || resolution.reason === "unsupported_prompt_shape" ? resolution.reason : "prompt_unavailable";
|
|
11304
|
+
return { ok: false, code };
|
|
11305
|
+
}
|
|
11306
|
+
if (!await this.questionMenuStillOpen(sessionId)) {
|
|
11307
|
+
this.pendingQuestions.delete(sessionId);
|
|
11308
|
+
this.pendingQuestionKey.delete(sessionId);
|
|
11309
|
+
this.broadcastToSession(sessionId, {
|
|
11310
|
+
type: "question_cancelled",
|
|
11311
|
+
sessionId,
|
|
11312
|
+
toolUseId: pending.toolUseId
|
|
11313
|
+
});
|
|
11314
|
+
return {
|
|
11315
|
+
ok: false,
|
|
11316
|
+
code: "prompt_cancelled",
|
|
11317
|
+
terminal: { state: "cancelled", reason: "provider_closed" }
|
|
11318
|
+
};
|
|
11319
|
+
}
|
|
11320
|
+
if (this.pendingQuestions.get(sessionId)?.promptId !== prompt.promptId) {
|
|
11321
|
+
return { ok: false, code: "prompt_cancelled" };
|
|
11322
|
+
}
|
|
11323
|
+
try {
|
|
11324
|
+
this.ptyManager.sendKeys(sessionId, resolution.keys);
|
|
11325
|
+
} catch {
|
|
11326
|
+
return { ok: false, code: "provider_error" };
|
|
11327
|
+
}
|
|
11328
|
+
this.pendingQuestions.delete(sessionId);
|
|
11329
|
+
this.pendingQuestionKey.delete(sessionId);
|
|
11330
|
+
this.broadcastToSession(sessionId, {
|
|
11331
|
+
type: "question_cancelled",
|
|
11332
|
+
sessionId,
|
|
11333
|
+
toolUseId: pending.toolUseId
|
|
11334
|
+
});
|
|
11335
|
+
return { ok: true };
|
|
11336
|
+
};
|
|
11337
|
+
}
|
|
10594
11338
|
/**
|
|
10595
11339
|
* Answer a permission gate — the validated counterpart of POST /:id/input.
|
|
10596
11340
|
*
|
|
@@ -10626,9 +11370,14 @@ var SessionHandlers = class {
|
|
|
10626
11370
|
return;
|
|
10627
11371
|
}
|
|
10628
11372
|
const gateClosed = () => {
|
|
11373
|
+
const pending = this.pendingPermission.get(sessionId);
|
|
11374
|
+
const prompt = pending?.promptId ? this.promptRegistry.get(pending.promptId) : null;
|
|
11375
|
+
if (prompt?.state === "open" || prompt?.state === "updated") {
|
|
11376
|
+
this.promptRegistry.transition(prompt.promptId, "cancelled", "provider_closed");
|
|
11377
|
+
}
|
|
10629
11378
|
this.pendingPermission.delete(sessionId);
|
|
10630
11379
|
this.pendingPermissionKey.delete(sessionId);
|
|
10631
|
-
this.
|
|
11380
|
+
this.broadcastToSession(sessionId, { type: "permission_cancelled", sessionId });
|
|
10632
11381
|
json(res, 409, { ok: false, reason: "gate_closed" });
|
|
10633
11382
|
};
|
|
10634
11383
|
const gate = this.pendingPermission.get(sessionId);
|
|
@@ -10667,6 +11416,10 @@ var SessionHandlers = class {
|
|
|
10667
11416
|
json(res, 400, { ok: false, reason: message });
|
|
10668
11417
|
return;
|
|
10669
11418
|
}
|
|
11419
|
+
const normalized = gate.promptId ? this.promptRegistry.get(gate.promptId) : null;
|
|
11420
|
+
if (normalized?.state === "open" || normalized?.state === "updated") {
|
|
11421
|
+
this.promptRegistry.transition(normalized.promptId, "resolved", "answered_legacy");
|
|
11422
|
+
}
|
|
10670
11423
|
json(res, 200, { ok: true });
|
|
10671
11424
|
}
|
|
10672
11425
|
/**
|
|
@@ -10712,9 +11465,13 @@ var SessionHandlers = class {
|
|
|
10712
11465
|
}
|
|
10713
11466
|
const toolUseId = pending?.toolUseId ?? "";
|
|
10714
11467
|
if (!await this.questionMenuStillOpen(sessionId)) {
|
|
11468
|
+
const prompt = this.promptRegistry.get(pending?.promptId ?? "");
|
|
11469
|
+
if (prompt?.state === "open" || prompt?.state === "updated") {
|
|
11470
|
+
this.promptRegistry.transition(prompt.promptId, "cancelled", "provider_closed");
|
|
11471
|
+
}
|
|
10715
11472
|
this.pendingQuestions.delete(sessionId);
|
|
10716
11473
|
this.pendingQuestionKey.delete(sessionId);
|
|
10717
|
-
this.
|
|
11474
|
+
this.broadcastToSession(sessionId, { type: "question_cancelled", sessionId, toolUseId });
|
|
10718
11475
|
json(res, 409, { ok: false, reason: "question_gone" });
|
|
10719
11476
|
return;
|
|
10720
11477
|
}
|
|
@@ -10725,10 +11482,41 @@ var SessionHandlers = class {
|
|
|
10725
11482
|
json(res, 400, { ok: false, reason: message });
|
|
10726
11483
|
return;
|
|
10727
11484
|
}
|
|
11485
|
+
const normalized = this.promptRegistry.get(pending?.promptId ?? "");
|
|
11486
|
+
if (normalized?.state === "open" || normalized?.state === "updated") {
|
|
11487
|
+
this.promptRegistry.transition(normalized.promptId, "resolved", "answered_legacy");
|
|
11488
|
+
}
|
|
10728
11489
|
this.pendingQuestions.delete(sessionId);
|
|
10729
|
-
this.
|
|
11490
|
+
this.broadcastToSession(sessionId, { type: "question_cancelled", sessionId, toolUseId });
|
|
10730
11491
|
json(res, 200, { ok: true });
|
|
10731
11492
|
}
|
|
11493
|
+
/**
|
|
11494
|
+
* Answer a normalized prompt by its opaque ids.
|
|
11495
|
+
*
|
|
11496
|
+
* Refusals are keyed by `code` — the stable machine taxonomy of the prompt
|
|
11497
|
+
* contract. The released legacy routes (`/answer`, `/permission/answer`) key
|
|
11498
|
+
* theirs by `reason` and keep doing so; a client reads whichever key belongs
|
|
11499
|
+
* to the route it called, and the two vocabularies are not merged.
|
|
11500
|
+
*
|
|
11501
|
+
* Status follows the same split as the legacy routes: a malformed or
|
|
11502
|
+
* unanswerable *request* is 400, a prompt whose *state* refuses the answer is
|
|
11503
|
+
* 409. A retry after PROMPT_TERMINAL_RETENTION_MS answers 404
|
|
11504
|
+
* `prompt_not_found`, not the recorded outcome — the record it would replay
|
|
11505
|
+
* is gone by then.
|
|
11506
|
+
*/
|
|
11507
|
+
async handlePromptAnswer(sessionId, req, res) {
|
|
11508
|
+
const parsed = PromptAnswerSchema.safeParse(await readBody2(req));
|
|
11509
|
+
if (!parsed.success) {
|
|
11510
|
+
json(res, 400, { ok: false, code: "invalid_prompt_answer" });
|
|
11511
|
+
return;
|
|
11512
|
+
}
|
|
11513
|
+
const outcome = await this.promptRegistry.answer(sessionId, parsed.data);
|
|
11514
|
+
if (outcome.ok) {
|
|
11515
|
+
json(res, 200, outcome);
|
|
11516
|
+
return;
|
|
11517
|
+
}
|
|
11518
|
+
json(res, promptAnswerStatus(outcome.code), outcome);
|
|
11519
|
+
}
|
|
10732
11520
|
// Best-effort: a session we don't own a PTY for, or one that raced away
|
|
10733
11521
|
// mid-read, is not ours to veto — say yes and let the write decide.
|
|
10734
11522
|
async questionMenuStillOpen(sessionId) {
|
|
@@ -12872,6 +13660,27 @@ function handleListProjects(url, res) {
|
|
|
12872
13660
|
function wsAllows(principal, required) {
|
|
12873
13661
|
return principal === null || hasCapability(principal, required);
|
|
12874
13662
|
}
|
|
13663
|
+
function clearExpiredPendingPrompt(deps, prompt) {
|
|
13664
|
+
const permission = deps.pendingPermission.get(prompt.sessionId);
|
|
13665
|
+
if (permission?.promptId === prompt.promptId) {
|
|
13666
|
+
deps.pendingPermission.delete(prompt.sessionId);
|
|
13667
|
+
deps.pendingPermissionKey.delete(prompt.sessionId);
|
|
13668
|
+
deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(prompt.sessionId) ?? [], {
|
|
13669
|
+
type: "permission_cancelled",
|
|
13670
|
+
sessionId: prompt.sessionId
|
|
13671
|
+
});
|
|
13672
|
+
}
|
|
13673
|
+
const question = deps.pendingQuestions.get(prompt.sessionId);
|
|
13674
|
+
if (question?.promptId === prompt.promptId) {
|
|
13675
|
+
deps.pendingQuestions.delete(prompt.sessionId);
|
|
13676
|
+
deps.pendingQuestionKey.delete(prompt.sessionId);
|
|
13677
|
+
deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(prompt.sessionId) ?? [], {
|
|
13678
|
+
type: "question_cancelled",
|
|
13679
|
+
sessionId: prompt.sessionId,
|
|
13680
|
+
toolUseId: question.toolUseId
|
|
13681
|
+
});
|
|
13682
|
+
}
|
|
13683
|
+
}
|
|
12875
13684
|
function createConversationWatcherEvents(deps) {
|
|
12876
13685
|
return {
|
|
12877
13686
|
onNewLineSpans: (filePath, spans, readFrom, endOffset) => {
|
|
@@ -12994,11 +13803,11 @@ function createLiveSessionOptions(deps) {
|
|
|
12994
13803
|
ts
|
|
12995
13804
|
});
|
|
12996
13805
|
},
|
|
12997
|
-
onPermissionChange: (sessionId, gate) => {
|
|
12998
|
-
deps.sessionHandlers().handlePermissionChange(sessionId, gate);
|
|
13806
|
+
onPermissionChange: (sessionId, gate, occurrenceId) => {
|
|
13807
|
+
deps.sessionHandlers().handlePermissionChange(sessionId, gate, occurrenceId);
|
|
12999
13808
|
},
|
|
13000
|
-
onLiveQuestion: (sessionId, questions) => {
|
|
13001
|
-
deps.sessionHandlers().handleLiveQuestion(sessionId, questions);
|
|
13809
|
+
onLiveQuestion: (sessionId, questions, occurrenceId) => {
|
|
13810
|
+
deps.sessionHandlers().handleLiveQuestion(sessionId, questions, occurrenceId);
|
|
13002
13811
|
},
|
|
13003
13812
|
onLiveQuestionGone: (sessionId) => {
|
|
13004
13813
|
deps.pendingQuestionKey.delete(sessionId);
|
|
@@ -13079,10 +13888,11 @@ function createLiveSessionOptions(deps) {
|
|
|
13079
13888
|
if (filePath) {
|
|
13080
13889
|
deps.fileWatcher.unwatch(filePath);
|
|
13081
13890
|
deps.sessionFileMap.delete(session.id);
|
|
13082
|
-
deps.cancelPendingQuestion(session.id);
|
|
13083
13891
|
}
|
|
13892
|
+
deps.cancelPendingQuestion(session.id);
|
|
13084
13893
|
deps.pendingPermission.delete(session.id);
|
|
13085
13894
|
deps.pendingPermissionKey.delete(session.id);
|
|
13895
|
+
deps.promptRegistry.invalidateSession(session.id, "session_ended");
|
|
13086
13896
|
deps.contendedSessions.delete(session.id);
|
|
13087
13897
|
deps.rememberSelfPtyEnded(session.id);
|
|
13088
13898
|
}
|
|
@@ -13137,6 +13947,7 @@ function createApiDeps(deps) {
|
|
|
13137
13947
|
handleGetOutput: (id, res) => deps.sessionHandlers.handleGetOutput(id, res),
|
|
13138
13948
|
handleSendInput: (id, req, res) => deps.sessionHandlers.handleSendInput(id, req, res),
|
|
13139
13949
|
handleSendAnswer: (id, req, res) => deps.sessionHandlers.handleSendAnswer(id, req, res),
|
|
13950
|
+
handlePromptAnswer: (id, req, res) => deps.sessionHandlers.handlePromptAnswer(id, req, res),
|
|
13140
13951
|
handlePermissionAnswer: (id, req, res) => deps.sessionHandlers.handlePermissionAnswer(id, req, res),
|
|
13141
13952
|
handleCancel: (id, res) => deps.sessionHandlers.handleCancel(id, res),
|
|
13142
13953
|
handleStopSession: (id, res) => deps.sessionHandlers.handleStopSession(id, res),
|
|
@@ -13195,6 +14006,9 @@ function createApiDeps(deps) {
|
|
|
13195
14006
|
return;
|
|
13196
14007
|
}
|
|
13197
14008
|
deps.addSessionSubscriber(msg.sessionId, ws);
|
|
14009
|
+
if (deps.promptRegistry) {
|
|
14010
|
+
ws.send(JSON.stringify(deps.promptRegistry.snapshot(msg.sessionId)));
|
|
14011
|
+
}
|
|
13198
14012
|
if (deps.ptyManager.hasSession(msg.sessionId)) {
|
|
13199
14013
|
const lines = await deps.ptyManager.getOutputLines(msg.sessionId, REPLAY_MAX_LINES);
|
|
13200
14014
|
const userMessages = deps.ptyManager.getInputHistory(msg.sessionId);
|
|
@@ -15965,6 +16779,7 @@ var StreamerServer = class {
|
|
|
15965
16779
|
// repaint of the same gate doesn't re-broadcast on every tick. Cleared
|
|
15966
16780
|
// alongside pendingPermission.
|
|
15967
16781
|
pendingPermissionKey = /* @__PURE__ */ new Map();
|
|
16782
|
+
promptRegistry;
|
|
15968
16783
|
// Scanner lifecycle, freshness state and the cache↔disk reconcile.
|
|
15969
16784
|
scannerManager;
|
|
15970
16785
|
// Binds a live session to the JSONL/rollout its provider writes.
|
|
@@ -16217,6 +17032,20 @@ var StreamerServer = class {
|
|
|
16217
17032
|
this.browserCors = config.browserCors ?? loadBrowserCors();
|
|
16218
17033
|
this.sessionStore = new SessionStore();
|
|
16219
17034
|
this.wsHub = new WSHub();
|
|
17035
|
+
this.promptRegistry = new PromptRegistry({
|
|
17036
|
+
emit: (event) => this.wsHub.broadcastToClients(this.sessionSubscribers.get(event.sessionId) ?? [], event),
|
|
17037
|
+
onExpire: (prompt) => clearExpiredPendingPrompt(
|
|
17038
|
+
{
|
|
17039
|
+
pendingPermission: this.pendingPermission,
|
|
17040
|
+
pendingPermissionKey: this.pendingPermissionKey,
|
|
17041
|
+
pendingQuestions: this.pendingQuestions,
|
|
17042
|
+
pendingQuestionKey: this.pendingQuestionKey,
|
|
17043
|
+
sessionSubscribers: this.sessionSubscribers,
|
|
17044
|
+
wsHub: this.wsHub
|
|
17045
|
+
},
|
|
17046
|
+
prompt
|
|
17047
|
+
)
|
|
17048
|
+
});
|
|
16220
17049
|
this.fileWatcher = new ConversationWatcher(
|
|
16221
17050
|
createConversationWatcherEvents({
|
|
16222
17051
|
sessionFileMap: this.sessionFileMap,
|
|
@@ -16260,6 +17089,7 @@ var StreamerServer = class {
|
|
|
16260
17089
|
pendingQuestionKey: this.pendingQuestionKey,
|
|
16261
17090
|
pendingPermission: this.pendingPermission,
|
|
16262
17091
|
pendingPermissionKey: this.pendingPermissionKey,
|
|
17092
|
+
promptRegistry: this.promptRegistry,
|
|
16263
17093
|
contendedSessions: this.contendedSessions,
|
|
16264
17094
|
// Thunks, not values: sessionHandlers is constructed below, the
|
|
16265
17095
|
// registry repo and the push notifiers are bound during listen(), and
|
|
@@ -16350,6 +17180,7 @@ var StreamerServer = class {
|
|
|
16350
17180
|
sessionStatusBus: this.sessionStatusBus,
|
|
16351
17181
|
sessionFileMap: this.sessionFileMap,
|
|
16352
17182
|
pendingQuestions: this.pendingQuestions,
|
|
17183
|
+
promptRegistry: this.promptRegistry,
|
|
16353
17184
|
pendingQuestionKey: this.pendingQuestionKey,
|
|
16354
17185
|
pendingPermission: this.pendingPermission,
|
|
16355
17186
|
pendingPermissionKey: this.pendingPermissionKey,
|
|
@@ -16450,6 +17281,7 @@ var StreamerServer = class {
|
|
|
16450
17281
|
terminalSeq: this.terminalSeq,
|
|
16451
17282
|
pendingPermission: this.pendingPermission,
|
|
16452
17283
|
pendingQuestions: this.pendingQuestions,
|
|
17284
|
+
promptRegistry: this.promptRegistry,
|
|
16453
17285
|
agentClient,
|
|
16454
17286
|
conversationWriter,
|
|
16455
17287
|
agentConfig
|
|
@@ -17230,6 +18062,7 @@ var StreamerServer = class {
|
|
|
17230
18062
|
if (!this.ptyManager.isRemote()) this.ptyManager.dispose();
|
|
17231
18063
|
this.fileWatcher.dispose();
|
|
17232
18064
|
this.externalTails.clear();
|
|
18065
|
+
this.promptRegistry.dispose();
|
|
17233
18066
|
this.wsHub.dispose();
|
|
17234
18067
|
this.pairTokens.dispose();
|
|
17235
18068
|
this.liveActivityRenewal?.stop();
|
|
@@ -17818,7 +18651,7 @@ var StreamerServer = class {
|
|
|
17818
18651
|
for (const p of pending) {
|
|
17819
18652
|
if (contended || foreignVsPty(p.questions)) continue;
|
|
17820
18653
|
const origin = priorPtyKey !== null && questionContentKey(p.questions) === priorPtyKey ? "pty" : "jsonl";
|
|
17821
|
-
this.
|
|
18654
|
+
this.sessionHandlers.handleJsonlQuestion(sessionId, p.toolUseId, p.questions, origin);
|
|
17822
18655
|
const t = setTimeout(() => {
|
|
17823
18656
|
if (this.pendingQuestions.get(sessionId)?.toolUseId === p.toolUseId) {
|
|
17824
18657
|
this.cancelPendingQuestion(sessionId);
|
|
@@ -17836,7 +18669,7 @@ var StreamerServer = class {
|
|
|
17836
18669
|
priorToolUseId
|
|
17837
18670
|
});
|
|
17838
18671
|
this.pendingQuestionKey.set(sessionId, key);
|
|
17839
|
-
if (broadcast) this.wsHub.
|
|
18672
|
+
if (broadcast) this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], m);
|
|
17840
18673
|
}
|
|
17841
18674
|
}
|
|
17842
18675
|
cancelPendingQuestion(sessionId) {
|
|
@@ -17844,7 +18677,15 @@ var StreamerServer = class {
|
|
|
17844
18677
|
if (!pq) return;
|
|
17845
18678
|
this.pendingQuestions.delete(sessionId);
|
|
17846
18679
|
this.pendingQuestionKey.delete(sessionId);
|
|
17847
|
-
this.
|
|
18680
|
+
const prompt = this.promptRegistry.get(pq.promptId);
|
|
18681
|
+
if (prompt?.state === "open" || prompt?.state === "updated") {
|
|
18682
|
+
this.promptRegistry.transition(pq.promptId, "cancelled", "provider_closed");
|
|
18683
|
+
}
|
|
18684
|
+
this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
|
|
18685
|
+
type: "question_cancelled",
|
|
18686
|
+
sessionId,
|
|
18687
|
+
toolUseId: pq.toolUseId
|
|
18688
|
+
});
|
|
17848
18689
|
}
|
|
17849
18690
|
async handleBrowse(url, res) {
|
|
17850
18691
|
if (!this.browseRoot) {
|
|
@@ -17995,7 +18836,13 @@ function parseDirScanDebounceEnv(raw) {
|
|
|
17995
18836
|
CODEX_CLI_PROVIDER,
|
|
17996
18837
|
ConversationWatcher,
|
|
17997
18838
|
LiveSessionManager,
|
|
18839
|
+
PROMPT_SCHEMA_VERSION,
|
|
17998
18840
|
PTYManager,
|
|
18841
|
+
PromptAnswerSchema,
|
|
18842
|
+
PromptOptionSchema,
|
|
18843
|
+
PromptQuestionSchema,
|
|
18844
|
+
PromptResponseSchema,
|
|
18845
|
+
PromptSchema,
|
|
17999
18846
|
SessionStore,
|
|
18000
18847
|
StreamerServer,
|
|
18001
18848
|
WSHub,
|