@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.js
CHANGED
|
@@ -2169,7 +2169,7 @@ function toPublicSession(s) {
|
|
|
2169
2169
|
}
|
|
2170
2170
|
|
|
2171
2171
|
// src/pty-host/protocol.ts
|
|
2172
|
-
var PTY_HOST_PROTOCOL_VERSION =
|
|
2172
|
+
var PTY_HOST_PROTOCOL_VERSION = 4;
|
|
2173
2173
|
function isHostEvent(message) {
|
|
2174
2174
|
return "type" in message && message.type === "event";
|
|
2175
2175
|
}
|
|
@@ -2257,8 +2257,12 @@ var RemoteSessionRunner = class _RemoteSessionRunner {
|
|
|
2257
2257
|
}
|
|
2258
2258
|
throw new PtyHostProtocolMismatchError(status.protocolVersion, PTY_HOST_PROTOCOL_VERSION);
|
|
2259
2259
|
}
|
|
2260
|
-
await runner.request({ type: "subscribe" });
|
|
2260
|
+
const subscribed = await runner.request({ type: "subscribe" });
|
|
2261
2261
|
runner.refreshMirror(status);
|
|
2262
|
+
runner.restorePromptSnapshots({
|
|
2263
|
+
...status,
|
|
2264
|
+
promptSnapshots: subscribed.promptSnapshots ?? status.promptSnapshots
|
|
2265
|
+
});
|
|
2262
2266
|
return runner;
|
|
2263
2267
|
}
|
|
2264
2268
|
constructor(transport, options) {
|
|
@@ -2351,6 +2355,19 @@ var RemoteSessionRunner = class _RemoteSessionRunner {
|
|
|
2351
2355
|
this.pids.set(session.id, entry.pid);
|
|
2352
2356
|
}
|
|
2353
2357
|
}
|
|
2358
|
+
restorePromptSnapshots(status) {
|
|
2359
|
+
for (const snapshot of status.promptSnapshots ?? []) {
|
|
2360
|
+
if (snapshot.kind === "permission") {
|
|
2361
|
+
this.options.onPermissionChange?.(snapshot.sessionId, snapshot.gate, snapshot.occurrenceId);
|
|
2362
|
+
} else {
|
|
2363
|
+
this.options.onLiveQuestion?.(
|
|
2364
|
+
snapshot.sessionId,
|
|
2365
|
+
snapshot.questions,
|
|
2366
|
+
snapshot.occurrenceId
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2354
2371
|
async heartbeat(state, timeoutMs = HOST_HEARTBEAT_REQUEST_TIMEOUT_MS) {
|
|
2355
2372
|
await this.request({ type: "heartbeat", ...state }, timeoutMs);
|
|
2356
2373
|
}
|
|
@@ -2405,13 +2422,13 @@ var RemoteSessionRunner = class _RemoteSessionRunner {
|
|
|
2405
2422
|
break;
|
|
2406
2423
|
}
|
|
2407
2424
|
case "permission-change":
|
|
2408
|
-
this.options.onPermissionChange?.(event.sessionId, event.gate);
|
|
2425
|
+
this.options.onPermissionChange?.(event.sessionId, event.gate, event.occurrenceId);
|
|
2409
2426
|
break;
|
|
2410
2427
|
case "phase-change":
|
|
2411
2428
|
this.options.onPhaseChange?.(event.sessionId, event.phase);
|
|
2412
2429
|
break;
|
|
2413
2430
|
case "live-question":
|
|
2414
|
-
this.options.onLiveQuestion?.(event.sessionId, event.questions);
|
|
2431
|
+
this.options.onLiveQuestion?.(event.sessionId, event.questions, event.occurrenceId);
|
|
2415
2432
|
break;
|
|
2416
2433
|
case "live-question-gone":
|
|
2417
2434
|
this.options.onLiveQuestionGone?.(event.sessionId);
|
|
@@ -4086,10 +4103,134 @@ async function readGitBranch(dir) {
|
|
|
4086
4103
|
}
|
|
4087
4104
|
}
|
|
4088
4105
|
|
|
4106
|
+
// src/schemas/prompt.schema.ts
|
|
4107
|
+
import { z } from "zod";
|
|
4108
|
+
var PROMPT_SCHEMA_VERSION = 1;
|
|
4109
|
+
var OpaqueIdSchema = z.string().trim().min(1).max(200);
|
|
4110
|
+
var MeaningfulStringSchema = z.string().trim().min(1);
|
|
4111
|
+
var PromptOptionSchema = z.object({
|
|
4112
|
+
optionId: OpaqueIdSchema,
|
|
4113
|
+
label: MeaningfulStringSchema,
|
|
4114
|
+
description: z.string().optional(),
|
|
4115
|
+
preview: z.string().optional()
|
|
4116
|
+
});
|
|
4117
|
+
var PromptQuestionSchema = z.object({
|
|
4118
|
+
questionId: OpaqueIdSchema,
|
|
4119
|
+
text: MeaningfulStringSchema,
|
|
4120
|
+
header: z.string().optional(),
|
|
4121
|
+
inputMode: z.enum(["single", "multi", "text"]),
|
|
4122
|
+
options: z.array(PromptOptionSchema),
|
|
4123
|
+
allowOther: z.boolean(),
|
|
4124
|
+
secret: z.union([z.boolean(), z.literal("unknown")])
|
|
4125
|
+
}).superRefine((question, ctx) => {
|
|
4126
|
+
const optionIds = question.options.map((option) => option.optionId);
|
|
4127
|
+
if (new Set(optionIds).size !== optionIds.length) {
|
|
4128
|
+
ctx.addIssue({
|
|
4129
|
+
code: "custom",
|
|
4130
|
+
message: "optionId values must be unique",
|
|
4131
|
+
path: ["options"]
|
|
4132
|
+
});
|
|
4133
|
+
}
|
|
4134
|
+
if (question.inputMode === "text" && question.options.length !== 0) {
|
|
4135
|
+
ctx.addIssue({
|
|
4136
|
+
code: "custom",
|
|
4137
|
+
message: "text questions cannot carry options",
|
|
4138
|
+
path: ["options"]
|
|
4139
|
+
});
|
|
4140
|
+
}
|
|
4141
|
+
if (question.inputMode !== "text" && question.options.length === 0) {
|
|
4142
|
+
ctx.addIssue({
|
|
4143
|
+
code: "custom",
|
|
4144
|
+
message: "select questions require options",
|
|
4145
|
+
path: ["options"]
|
|
4146
|
+
});
|
|
4147
|
+
}
|
|
4148
|
+
});
|
|
4149
|
+
var TERMINAL_PROMPT_STATES = /* @__PURE__ */ new Set(["resolved", "cancelled", "expired", "unavailable"]);
|
|
4150
|
+
var PromptSchema = z.object({
|
|
4151
|
+
schemaVersion: z.literal(PROMPT_SCHEMA_VERSION),
|
|
4152
|
+
sessionId: OpaqueIdSchema,
|
|
4153
|
+
promptId: OpaqueIdSchema,
|
|
4154
|
+
revision: z.number().int().positive(),
|
|
4155
|
+
state: z.enum(["open", "updated", "resolved", "cancelled", "expired", "unavailable"]),
|
|
4156
|
+
terminalReason: MeaningfulStringSchema.optional(),
|
|
4157
|
+
intent: z.enum(["approval", "question"]),
|
|
4158
|
+
title: z.string().optional(),
|
|
4159
|
+
message: z.string().optional(),
|
|
4160
|
+
detail: z.string().optional(),
|
|
4161
|
+
questions: z.array(PromptQuestionSchema).min(1),
|
|
4162
|
+
answerRequirement: z.enum(["blocking", "non_blocking", "unknown"]),
|
|
4163
|
+
expiresAt: z.string().datetime({ offset: true }).nullable(),
|
|
4164
|
+
provenance: z.object({
|
|
4165
|
+
source: z.enum(["provider", "screen", "transcript", "synthetic"]),
|
|
4166
|
+
confidence: z.enum(["authoritative", "inferred"])
|
|
4167
|
+
})
|
|
4168
|
+
}).superRefine((prompt, ctx) => {
|
|
4169
|
+
if (![prompt.title, prompt.message, prompt.detail].some((value) => value?.trim())) {
|
|
4170
|
+
ctx.addIssue({
|
|
4171
|
+
code: "custom",
|
|
4172
|
+
message: "prompt requires a meaningful title, message, or detail",
|
|
4173
|
+
path: ["message"]
|
|
4174
|
+
});
|
|
4175
|
+
}
|
|
4176
|
+
const questionIds = prompt.questions.map((question) => question.questionId);
|
|
4177
|
+
if (new Set(questionIds).size !== questionIds.length) {
|
|
4178
|
+
ctx.addIssue({
|
|
4179
|
+
code: "custom",
|
|
4180
|
+
message: "questionId values must be unique",
|
|
4181
|
+
path: ["questions"]
|
|
4182
|
+
});
|
|
4183
|
+
}
|
|
4184
|
+
const optionIds = prompt.questions.flatMap(
|
|
4185
|
+
(question) => question.options.map((option) => option.optionId)
|
|
4186
|
+
);
|
|
4187
|
+
if (new Set(optionIds).size !== optionIds.length) {
|
|
4188
|
+
ctx.addIssue({
|
|
4189
|
+
code: "custom",
|
|
4190
|
+
message: "optionId values must be unique within a prompt",
|
|
4191
|
+
path: ["questions"]
|
|
4192
|
+
});
|
|
4193
|
+
}
|
|
4194
|
+
const terminal = TERMINAL_PROMPT_STATES.has(prompt.state);
|
|
4195
|
+
if (terminal !== (prompt.terminalReason !== void 0)) {
|
|
4196
|
+
ctx.addIssue({
|
|
4197
|
+
code: "custom",
|
|
4198
|
+
message: terminal ? "terminal prompts require terminalReason" : "actionable prompts cannot carry terminalReason",
|
|
4199
|
+
path: ["terminalReason"]
|
|
4200
|
+
});
|
|
4201
|
+
}
|
|
4202
|
+
});
|
|
4203
|
+
var OptionResponseSchema = z.object({
|
|
4204
|
+
questionId: OpaqueIdSchema,
|
|
4205
|
+
optionIds: z.array(OpaqueIdSchema).min(1).refine((ids) => new Set(ids).size === ids.length, "optionIds must be unique"),
|
|
4206
|
+
text: z.never().optional()
|
|
4207
|
+
});
|
|
4208
|
+
var TextResponseSchema = z.object({
|
|
4209
|
+
questionId: OpaqueIdSchema,
|
|
4210
|
+
text: z.string(),
|
|
4211
|
+
optionIds: z.never().optional()
|
|
4212
|
+
});
|
|
4213
|
+
var PromptResponseSchema = z.union([OptionResponseSchema, TextResponseSchema]);
|
|
4214
|
+
var PromptAnswerSchema = z.object({
|
|
4215
|
+
promptId: OpaqueIdSchema,
|
|
4216
|
+
revision: z.number().int().positive(),
|
|
4217
|
+
responses: z.array(PromptResponseSchema).min(1),
|
|
4218
|
+
idempotencyKey: OpaqueIdSchema
|
|
4219
|
+
}).superRefine((answer, ctx) => {
|
|
4220
|
+
const questionIds = answer.responses.map((response) => response.questionId);
|
|
4221
|
+
if (new Set(questionIds).size !== questionIds.length) {
|
|
4222
|
+
ctx.addIssue({
|
|
4223
|
+
code: "custom",
|
|
4224
|
+
message: "each questionId can be answered only once",
|
|
4225
|
+
path: ["responses"]
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
4228
|
+
});
|
|
4229
|
+
|
|
4089
4230
|
// src/server.ts
|
|
4090
4231
|
import { createNodeWebSocket } from "@hono/node-ws";
|
|
4091
4232
|
import { Connection, Client as TemporalClient } from "@temporalio/client";
|
|
4092
|
-
import { randomUUID as
|
|
4233
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
4093
4234
|
import { EventEmitter } from "events";
|
|
4094
4235
|
import { existsSync as existsSync16 } from "fs";
|
|
4095
4236
|
import { realpath as realpath2 } from "fs/promises";
|
|
@@ -4725,11 +4866,11 @@ var createBrowseRoutes = (deps) => {
|
|
|
4725
4866
|
import { Hono as Hono4 } from "hono";
|
|
4726
4867
|
|
|
4727
4868
|
// src/schemas/cacheAlert.schema.ts
|
|
4728
|
-
import { z } from "zod";
|
|
4729
|
-
var ResolveCacheAlertSchema =
|
|
4730
|
-
fingerprint:
|
|
4731
|
-
action:
|
|
4732
|
-
ids:
|
|
4869
|
+
import { z as z2 } from "zod";
|
|
4870
|
+
var ResolveCacheAlertSchema = z2.object({
|
|
4871
|
+
fingerprint: z2.string(),
|
|
4872
|
+
action: z2.enum(["prune_all", "prune_selected", "ignore", "reset_rescan"]),
|
|
4873
|
+
ids: z2.array(z2.string()).optional()
|
|
4733
4874
|
}).refine((v) => v.action !== "prune_selected" || v.ids !== void 0 && v.ids.length > 0, {
|
|
4734
4875
|
message: "prune_selected requires a non-empty ids array",
|
|
4735
4876
|
path: ["ids"]
|
|
@@ -4785,12 +4926,12 @@ var createCacheAlertRoutes = (deps) => {
|
|
|
4785
4926
|
import { Hono as Hono5 } from "hono";
|
|
4786
4927
|
|
|
4787
4928
|
// src/schemas/claudeFlags.schema.ts
|
|
4788
|
-
import { z as
|
|
4789
|
-
var ClaudeFlagsBodySchema =
|
|
4790
|
-
values:
|
|
4929
|
+
import { z as z3 } from "zod";
|
|
4930
|
+
var ClaudeFlagsBodySchema = z3.object({
|
|
4931
|
+
values: z3.record(z3.string(), z3.union([z3.string(), z3.boolean(), z3.array(z3.string())])).default({}),
|
|
4791
4932
|
// A newline would corrupt the flat one-line-per-key server.yaml, so reject
|
|
4792
4933
|
// it here with a field error instead of silently stripping it.
|
|
4793
|
-
extraArgs:
|
|
4934
|
+
extraArgs: z3.string().refine((v) => !/[\r\n]/.test(v), "extraArgs must not contain newlines").optional()
|
|
4794
4935
|
}).strict();
|
|
4795
4936
|
|
|
4796
4937
|
// src/api/routes/config.routes.ts
|
|
@@ -5324,15 +5465,15 @@ import { join as join9 } from "path";
|
|
|
5324
5465
|
import { parse as parseYaml } from "yaml";
|
|
5325
5466
|
|
|
5326
5467
|
// src/schemas/updateConfig.schema.ts
|
|
5327
|
-
import { z as
|
|
5328
|
-
var UpdateConfigSchema =
|
|
5329
|
-
auto_update:
|
|
5330
|
-
channel:
|
|
5331
|
-
allow:
|
|
5332
|
-
poll_interval_minutes:
|
|
5333
|
-
defer_if_active_sessions:
|
|
5334
|
-
github_repo:
|
|
5335
|
-
webhook_secret:
|
|
5468
|
+
import { z as z4 } from "zod";
|
|
5469
|
+
var UpdateConfigSchema = z4.object({
|
|
5470
|
+
auto_update: z4.boolean().default(false),
|
|
5471
|
+
channel: z4.enum(["stable", "next"]).default("stable"),
|
|
5472
|
+
allow: z4.array(z4.enum(["patch", "minor", "major"])).default(["patch", "minor"]),
|
|
5473
|
+
poll_interval_minutes: z4.number().int().min(0).default(1440),
|
|
5474
|
+
defer_if_active_sessions: z4.boolean().default(true),
|
|
5475
|
+
github_repo: z4.string().regex(/^[^/]+\/[^/]+$/, "github_repo must be 'owner/name'"),
|
|
5476
|
+
webhook_secret: z4.string().min(1).nullable().default(null)
|
|
5336
5477
|
}).strict();
|
|
5337
5478
|
|
|
5338
5479
|
// src/config/update-config.ts
|
|
@@ -5962,7 +6103,13 @@ var createMiscRoutes = (deps) => {
|
|
|
5962
6103
|
// the box is starved. Additive capability flag only — live readings stay
|
|
5963
6104
|
// off this polled endpoint. Absent means an older server that never
|
|
5964
6105
|
// samples. Informational: pressure never holds, kills, or refuses sessions.
|
|
5965
|
-
hostPressure: true
|
|
6106
|
+
hostPressure: true,
|
|
6107
|
+
// Provider-neutral prompt contract: normalized prompt events, opaque ids
|
|
6108
|
+
// and the atomic /prompt/answer route. A prompt_snapshot on subscribe
|
|
6109
|
+
// carries RETAINED prompts, terminal ones included — render on `state`,
|
|
6110
|
+
// not on presence — and an answer retry after that retention window is
|
|
6111
|
+
// answered 404 prompt_not_found rather than the recorded outcome.
|
|
6112
|
+
promptContract: { schemaVersion: 1, atomicAnswer: true }
|
|
5966
6113
|
});
|
|
5967
6114
|
});
|
|
5968
6115
|
app.get("/api/profiles", (c) => c.json([]));
|
|
@@ -6331,6 +6478,10 @@ var createSessionRoutes = (deps) => {
|
|
|
6331
6478
|
await deps.handleSendAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6332
6479
|
return alreadyHandled6();
|
|
6333
6480
|
});
|
|
6481
|
+
app.post("/:id/prompt/answer", async (c) => {
|
|
6482
|
+
await deps.handlePromptAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6483
|
+
return alreadyHandled6();
|
|
6484
|
+
});
|
|
6334
6485
|
app.post("/:id/permission/answer", async (c) => {
|
|
6335
6486
|
await deps.handlePermissionAnswer(c.req.param("id"), c.env.incoming, c.env.outgoing);
|
|
6336
6487
|
return alreadyHandled6();
|
|
@@ -9211,7 +9362,7 @@ var ConversationHandlers = class {
|
|
|
9211
9362
|
};
|
|
9212
9363
|
|
|
9213
9364
|
// src/api/handlers/sessions.handlers.ts
|
|
9214
|
-
import { randomUUID as
|
|
9365
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
9215
9366
|
import { existsSync as existsSync10 } from "fs";
|
|
9216
9367
|
import { basename as basename6, dirname as dirname9, join as join16 } from "path";
|
|
9217
9368
|
|
|
@@ -9544,6 +9695,396 @@ async function recordUpload(pool2, instanceId, row) {
|
|
|
9544
9695
|
);
|
|
9545
9696
|
}
|
|
9546
9697
|
|
|
9698
|
+
// src/services/prompts/promptRegistry.ts
|
|
9699
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
9700
|
+
var PROMPT_TERMINAL_RETENTION_MS = 10 * 60 * 1e3;
|
|
9701
|
+
var PROMPT_MAX_RECORDS_PER_SESSION = 200;
|
|
9702
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
9703
|
+
function copyPrompt(prompt) {
|
|
9704
|
+
return {
|
|
9705
|
+
...prompt,
|
|
9706
|
+
questions: prompt.questions.map((question) => ({
|
|
9707
|
+
...question,
|
|
9708
|
+
options: question.options.map((option) => ({ ...option }))
|
|
9709
|
+
})),
|
|
9710
|
+
provenance: { ...prompt.provenance }
|
|
9711
|
+
};
|
|
9712
|
+
}
|
|
9713
|
+
function terminalError(state) {
|
|
9714
|
+
switch (state) {
|
|
9715
|
+
case "resolved":
|
|
9716
|
+
return "already_resolved";
|
|
9717
|
+
case "expired":
|
|
9718
|
+
return "prompt_expired";
|
|
9719
|
+
case "cancelled":
|
|
9720
|
+
return "prompt_cancelled";
|
|
9721
|
+
case "unavailable":
|
|
9722
|
+
return "prompt_unavailable";
|
|
9723
|
+
}
|
|
9724
|
+
}
|
|
9725
|
+
var PromptRegistry = class {
|
|
9726
|
+
bySession = /* @__PURE__ */ new Map();
|
|
9727
|
+
byId = /* @__PURE__ */ new Map();
|
|
9728
|
+
sequences = /* @__PURE__ */ new Map();
|
|
9729
|
+
createId;
|
|
9730
|
+
emit;
|
|
9731
|
+
onExpire;
|
|
9732
|
+
now;
|
|
9733
|
+
terminalRetentionMs;
|
|
9734
|
+
maxRecordsPerSession;
|
|
9735
|
+
constructor(options = {}) {
|
|
9736
|
+
this.createId = options.createId ?? randomUUID4;
|
|
9737
|
+
this.emit = options.emit;
|
|
9738
|
+
this.onExpire = options.onExpire;
|
|
9739
|
+
this.now = options.now ?? Date.now;
|
|
9740
|
+
this.terminalRetentionMs = options.terminalRetentionMs ?? PROMPT_TERMINAL_RETENTION_MS;
|
|
9741
|
+
this.maxRecordsPerSession = options.maxRecordsPerSession ?? PROMPT_MAX_RECORDS_PER_SESSION;
|
|
9742
|
+
}
|
|
9743
|
+
open(draft, adapter, promptId = this.createId()) {
|
|
9744
|
+
this.prune(draft.sessionId);
|
|
9745
|
+
const held = this.byId.get(promptId);
|
|
9746
|
+
if (held && (held.prompt.state === "open" || held.prompt.state === "updated")) {
|
|
9747
|
+
throw new Error(`Prompt id already exists: ${promptId}`);
|
|
9748
|
+
}
|
|
9749
|
+
const id = held ? this.createId() : promptId;
|
|
9750
|
+
const prompt = PromptSchema.parse({
|
|
9751
|
+
...draft,
|
|
9752
|
+
schemaVersion: PROMPT_SCHEMA_VERSION,
|
|
9753
|
+
promptId: id,
|
|
9754
|
+
revision: 1,
|
|
9755
|
+
state: "open",
|
|
9756
|
+
questions: draft.questions.map((question) => ({
|
|
9757
|
+
...question,
|
|
9758
|
+
questionId: this.createId(),
|
|
9759
|
+
options: question.options.map((option) => ({ ...option, optionId: this.createId() }))
|
|
9760
|
+
})),
|
|
9761
|
+
provenance: { ...draft.provenance }
|
|
9762
|
+
});
|
|
9763
|
+
const entry = {
|
|
9764
|
+
prompt,
|
|
9765
|
+
adapter,
|
|
9766
|
+
queue: Promise.resolve(),
|
|
9767
|
+
inFlight: /* @__PURE__ */ new Map(),
|
|
9768
|
+
outcomes: /* @__PURE__ */ new Map()
|
|
9769
|
+
};
|
|
9770
|
+
const session = this.bySession.get(prompt.sessionId) ?? /* @__PURE__ */ new Map();
|
|
9771
|
+
session.set(prompt.promptId, entry);
|
|
9772
|
+
this.bySession.set(prompt.sessionId, session);
|
|
9773
|
+
this.byId.set(prompt.promptId, entry);
|
|
9774
|
+
this.publish(entry);
|
|
9775
|
+
this.scheduleExpiration(entry);
|
|
9776
|
+
this.enforceCap(prompt.sessionId);
|
|
9777
|
+
return copyPrompt(prompt);
|
|
9778
|
+
}
|
|
9779
|
+
update(promptId, draft, adapter) {
|
|
9780
|
+
const entry = this.requireEntry(promptId);
|
|
9781
|
+
this.expireIfDue(entry, this.now());
|
|
9782
|
+
if (entry.prompt.sessionId !== draft.sessionId) throw new Error("Prompt session cannot change");
|
|
9783
|
+
if (entry.prompt.state !== "open" && entry.prompt.state !== "updated") {
|
|
9784
|
+
throw new Error(`Cannot update terminal prompt ${promptId}`);
|
|
9785
|
+
}
|
|
9786
|
+
if (entry.prompt.questions.length !== draft.questions.length) {
|
|
9787
|
+
throw new Error("Prompt question cardinality cannot change during an update");
|
|
9788
|
+
}
|
|
9789
|
+
const questions = draft.questions.map((question, questionIndex) => {
|
|
9790
|
+
const prior = entry.prompt.questions[questionIndex];
|
|
9791
|
+
if (prior.options.length !== question.options.length) {
|
|
9792
|
+
throw new Error("Prompt option cardinality cannot change during an update");
|
|
9793
|
+
}
|
|
9794
|
+
return {
|
|
9795
|
+
...question,
|
|
9796
|
+
questionId: prior.questionId,
|
|
9797
|
+
options: question.options.map((option, optionIndex) => ({
|
|
9798
|
+
...option,
|
|
9799
|
+
optionId: prior.options[optionIndex].optionId
|
|
9800
|
+
}))
|
|
9801
|
+
};
|
|
9802
|
+
});
|
|
9803
|
+
const prompt = PromptSchema.parse({
|
|
9804
|
+
...draft,
|
|
9805
|
+
schemaVersion: PROMPT_SCHEMA_VERSION,
|
|
9806
|
+
promptId,
|
|
9807
|
+
revision: entry.prompt.revision + 1,
|
|
9808
|
+
state: "updated",
|
|
9809
|
+
questions,
|
|
9810
|
+
provenance: { ...draft.provenance }
|
|
9811
|
+
});
|
|
9812
|
+
entry.prompt = prompt;
|
|
9813
|
+
if (adapter) entry.adapter = adapter;
|
|
9814
|
+
this.publish(entry);
|
|
9815
|
+
this.scheduleExpiration(entry);
|
|
9816
|
+
return copyPrompt(entry.prompt);
|
|
9817
|
+
}
|
|
9818
|
+
transition(promptId, state, reason) {
|
|
9819
|
+
const entry = this.requireEntry(promptId);
|
|
9820
|
+
if (entry.prompt.state !== "open" && entry.prompt.state !== "updated") {
|
|
9821
|
+
throw new Error(`Cannot transition terminal prompt ${promptId}`);
|
|
9822
|
+
}
|
|
9823
|
+
entry.prompt = {
|
|
9824
|
+
...entry.prompt,
|
|
9825
|
+
revision: entry.prompt.revision + 1,
|
|
9826
|
+
state,
|
|
9827
|
+
terminalReason: reason
|
|
9828
|
+
};
|
|
9829
|
+
entry.terminalAt = this.now();
|
|
9830
|
+
this.clearExpiration(entry);
|
|
9831
|
+
this.publish(entry);
|
|
9832
|
+
return copyPrompt(entry.prompt);
|
|
9833
|
+
}
|
|
9834
|
+
invalidateSession(sessionId, reason = "session_ended") {
|
|
9835
|
+
const transitioned = [];
|
|
9836
|
+
for (const entry of this.bySession.get(sessionId)?.values() ?? []) {
|
|
9837
|
+
if (entry.prompt.state === "open" || entry.prompt.state === "updated") {
|
|
9838
|
+
transitioned.push(this.transition(entry.prompt.promptId, "unavailable", reason));
|
|
9839
|
+
}
|
|
9840
|
+
}
|
|
9841
|
+
return transitioned;
|
|
9842
|
+
}
|
|
9843
|
+
get(promptId) {
|
|
9844
|
+
const entry = this.byId.get(promptId);
|
|
9845
|
+
if (!entry) return null;
|
|
9846
|
+
this.prune(entry.prompt.sessionId);
|
|
9847
|
+
return this.byId.has(promptId) ? copyPrompt(entry.prompt) : null;
|
|
9848
|
+
}
|
|
9849
|
+
hasActionable(sessionId) {
|
|
9850
|
+
this.prune(sessionId);
|
|
9851
|
+
return [...this.bySession.get(sessionId)?.values() ?? []].some(
|
|
9852
|
+
(entry) => entry.prompt.state === "open" || entry.prompt.state === "updated"
|
|
9853
|
+
);
|
|
9854
|
+
}
|
|
9855
|
+
snapshot(sessionId) {
|
|
9856
|
+
this.prune(sessionId);
|
|
9857
|
+
return {
|
|
9858
|
+
type: "prompt_snapshot",
|
|
9859
|
+
schemaVersion: PROMPT_SCHEMA_VERSION,
|
|
9860
|
+
sessionId,
|
|
9861
|
+
sequence: this.sequences.get(sessionId) ?? 0,
|
|
9862
|
+
prompts: [...this.bySession.get(sessionId)?.values() ?? []].map(
|
|
9863
|
+
(entry) => copyPrompt(entry.prompt)
|
|
9864
|
+
)
|
|
9865
|
+
};
|
|
9866
|
+
}
|
|
9867
|
+
dispose() {
|
|
9868
|
+
for (const entry of this.byId.values()) this.clearExpiration(entry);
|
|
9869
|
+
}
|
|
9870
|
+
answer(sessionId, answer) {
|
|
9871
|
+
this.prune(sessionId);
|
|
9872
|
+
const entry = this.byId.get(answer.promptId);
|
|
9873
|
+
if (!entry || entry.prompt.sessionId !== sessionId) {
|
|
9874
|
+
return Promise.resolve({ ok: false, code: "prompt_not_found" });
|
|
9875
|
+
}
|
|
9876
|
+
this.pruneOutcomes(entry);
|
|
9877
|
+
const recorded = entry.outcomes.get(answer.idempotencyKey);
|
|
9878
|
+
if (recorded) return Promise.resolve(recorded.outcome);
|
|
9879
|
+
const pending = entry.inFlight.get(answer.idempotencyKey);
|
|
9880
|
+
if (pending) return pending;
|
|
9881
|
+
const task = entry.queue.then(() => this.performAnswer(entry, answer));
|
|
9882
|
+
entry.queue = task.then(
|
|
9883
|
+
() => void 0,
|
|
9884
|
+
() => void 0
|
|
9885
|
+
);
|
|
9886
|
+
entry.inFlight.set(answer.idempotencyKey, task);
|
|
9887
|
+
void task.then((outcome) => {
|
|
9888
|
+
entry.inFlight.delete(answer.idempotencyKey);
|
|
9889
|
+
entry.outcomes.set(answer.idempotencyKey, { at: this.now(), outcome });
|
|
9890
|
+
});
|
|
9891
|
+
return task;
|
|
9892
|
+
}
|
|
9893
|
+
async performAnswer(entry, answer) {
|
|
9894
|
+
const prompt = entry.prompt;
|
|
9895
|
+
if (prompt.state !== "open" && prompt.state !== "updated") {
|
|
9896
|
+
return { ok: false, code: terminalError(prompt.state) };
|
|
9897
|
+
}
|
|
9898
|
+
if (this.expireIfDue(entry, this.now())) {
|
|
9899
|
+
return { ok: false, code: "prompt_expired" };
|
|
9900
|
+
}
|
|
9901
|
+
if (prompt.revision !== answer.revision) {
|
|
9902
|
+
return {
|
|
9903
|
+
ok: false,
|
|
9904
|
+
code: "prompt_revision_mismatch",
|
|
9905
|
+
currentRevision: prompt.revision
|
|
9906
|
+
};
|
|
9907
|
+
}
|
|
9908
|
+
const responseError = this.validateResponses(prompt, answer);
|
|
9909
|
+
if (responseError) return { ok: false, code: responseError };
|
|
9910
|
+
if (!entry.adapter) return { ok: false, code: "prompt_unavailable" };
|
|
9911
|
+
let adapterResult;
|
|
9912
|
+
try {
|
|
9913
|
+
adapterResult = await entry.adapter({ prompt: copyPrompt(prompt), answer });
|
|
9914
|
+
} catch {
|
|
9915
|
+
return { ok: false, code: "provider_error" };
|
|
9916
|
+
}
|
|
9917
|
+
if (entry.prompt.state !== "open" && entry.prompt.state !== "updated") {
|
|
9918
|
+
return { ok: false, code: terminalError(entry.prompt.state) };
|
|
9919
|
+
}
|
|
9920
|
+
if (entry.prompt.revision !== answer.revision) {
|
|
9921
|
+
return {
|
|
9922
|
+
ok: false,
|
|
9923
|
+
code: "prompt_revision_mismatch",
|
|
9924
|
+
currentRevision: entry.prompt.revision
|
|
9925
|
+
};
|
|
9926
|
+
}
|
|
9927
|
+
if (!adapterResult.ok) {
|
|
9928
|
+
if (adapterResult.terminal) {
|
|
9929
|
+
this.transition(
|
|
9930
|
+
prompt.promptId,
|
|
9931
|
+
adapterResult.terminal.state,
|
|
9932
|
+
adapterResult.terminal.reason
|
|
9933
|
+
);
|
|
9934
|
+
}
|
|
9935
|
+
return { ok: false, code: adapterResult.code };
|
|
9936
|
+
}
|
|
9937
|
+
return { ok: true, prompt: this.transition(prompt.promptId, "resolved", "answered") };
|
|
9938
|
+
}
|
|
9939
|
+
validateResponses(prompt, answer) {
|
|
9940
|
+
const questions = new Map(prompt.questions.map((question) => [question.questionId, question]));
|
|
9941
|
+
for (const response of answer.responses) {
|
|
9942
|
+
if (!questions.has(response.questionId)) return "unknown_question";
|
|
9943
|
+
}
|
|
9944
|
+
if (answer.responses.length !== prompt.questions.length) return "incomplete_answer";
|
|
9945
|
+
const responses = new Map(answer.responses.map((response) => [response.questionId, response]));
|
|
9946
|
+
for (const question of prompt.questions) {
|
|
9947
|
+
const response = responses.get(question.questionId);
|
|
9948
|
+
if (!response) return "incomplete_answer";
|
|
9949
|
+
if (question.inputMode === "text") {
|
|
9950
|
+
if (typeof response.text !== "string") return "incomplete_answer";
|
|
9951
|
+
continue;
|
|
9952
|
+
}
|
|
9953
|
+
const optionIds = response.optionIds;
|
|
9954
|
+
if (!optionIds) return "incomplete_answer";
|
|
9955
|
+
if (question.inputMode === "single" && optionIds.length !== 1) {
|
|
9956
|
+
return "unsupported_prompt_shape";
|
|
9957
|
+
}
|
|
9958
|
+
const known = new Set(question.options.map((option) => option.optionId));
|
|
9959
|
+
if (optionIds.some((optionId) => !known.has(optionId))) return "unknown_option";
|
|
9960
|
+
}
|
|
9961
|
+
return null;
|
|
9962
|
+
}
|
|
9963
|
+
publish(entry) {
|
|
9964
|
+
const sessionId = entry.prompt.sessionId;
|
|
9965
|
+
const sequence = (this.sequences.get(sessionId) ?? 0) + 1;
|
|
9966
|
+
this.sequences.set(sessionId, sequence);
|
|
9967
|
+
this.emit?.({
|
|
9968
|
+
type: "prompt_event",
|
|
9969
|
+
sessionId,
|
|
9970
|
+
sequence,
|
|
9971
|
+
prompt: copyPrompt(entry.prompt)
|
|
9972
|
+
});
|
|
9973
|
+
}
|
|
9974
|
+
scheduleExpiration(entry) {
|
|
9975
|
+
this.clearExpiration(entry);
|
|
9976
|
+
const expiresAt = entry.prompt.expiresAt;
|
|
9977
|
+
if (expiresAt === null) return;
|
|
9978
|
+
const delay = Math.min(MAX_TIMEOUT_MS, Math.max(0, Date.parse(expiresAt) - this.now()));
|
|
9979
|
+
entry.expiryTimer = setTimeout(() => {
|
|
9980
|
+
entry.expiryTimer = void 0;
|
|
9981
|
+
if (!this.expireIfDue(entry, this.now())) this.scheduleExpiration(entry);
|
|
9982
|
+
}, delay);
|
|
9983
|
+
entry.expiryTimer.unref?.();
|
|
9984
|
+
}
|
|
9985
|
+
clearExpiration(entry) {
|
|
9986
|
+
if (entry.expiryTimer) clearTimeout(entry.expiryTimer);
|
|
9987
|
+
entry.expiryTimer = void 0;
|
|
9988
|
+
}
|
|
9989
|
+
expireIfDue(entry, now) {
|
|
9990
|
+
if (entry.prompt.state !== "open" && entry.prompt.state !== "updated") return false;
|
|
9991
|
+
if (entry.prompt.expiresAt === null || now < Date.parse(entry.prompt.expiresAt)) return false;
|
|
9992
|
+
const expired = this.transition(entry.prompt.promptId, "expired", "deadline_elapsed");
|
|
9993
|
+
this.onExpire?.(expired);
|
|
9994
|
+
return true;
|
|
9995
|
+
}
|
|
9996
|
+
requireEntry(promptId) {
|
|
9997
|
+
const entry = this.byId.get(promptId);
|
|
9998
|
+
if (!entry) throw new Error(`Unknown prompt: ${promptId}`);
|
|
9999
|
+
return entry;
|
|
10000
|
+
}
|
|
10001
|
+
prune(sessionId) {
|
|
10002
|
+
const now = this.now();
|
|
10003
|
+
const session = this.bySession.get(sessionId);
|
|
10004
|
+
if (!session) return;
|
|
10005
|
+
for (const [promptId, entry] of session) {
|
|
10006
|
+
this.expireIfDue(entry, now);
|
|
10007
|
+
if (entry.terminalAt !== void 0 && now - entry.terminalAt > this.terminalRetentionMs) {
|
|
10008
|
+
this.clearExpiration(entry);
|
|
10009
|
+
session.delete(promptId);
|
|
10010
|
+
this.byId.delete(promptId);
|
|
10011
|
+
}
|
|
10012
|
+
}
|
|
10013
|
+
if (session.size === 0) this.bySession.delete(sessionId);
|
|
10014
|
+
}
|
|
10015
|
+
enforceCap(sessionId) {
|
|
10016
|
+
const session = this.bySession.get(sessionId);
|
|
10017
|
+
if (!session || session.size <= this.maxRecordsPerSession) return;
|
|
10018
|
+
const terminal = [...session.entries()].filter(([, entry]) => entry.terminalAt !== void 0).sort((a, b) => (a[1].terminalAt ?? 0) - (b[1].terminalAt ?? 0));
|
|
10019
|
+
while (session.size > this.maxRecordsPerSession && terminal.length > 0) {
|
|
10020
|
+
const [promptId, entry] = terminal.shift();
|
|
10021
|
+
this.clearExpiration(entry);
|
|
10022
|
+
session.delete(promptId);
|
|
10023
|
+
this.byId.delete(promptId);
|
|
10024
|
+
}
|
|
10025
|
+
}
|
|
10026
|
+
pruneOutcomes(entry) {
|
|
10027
|
+
const now = this.now();
|
|
10028
|
+
for (const [key, recorded] of entry.outcomes) {
|
|
10029
|
+
if (now - recorded.at > this.terminalRetentionMs) entry.outcomes.delete(key);
|
|
10030
|
+
}
|
|
10031
|
+
}
|
|
10032
|
+
};
|
|
10033
|
+
|
|
10034
|
+
// src/services/prompts/ptyPromptAdapter.ts
|
|
10035
|
+
function permissionPromptDraft(sessionId, gate) {
|
|
10036
|
+
if (!gate) throw new Error("Cannot normalize an absent permission gate");
|
|
10037
|
+
const message = gate.prompt?.trim() || "Approval required";
|
|
10038
|
+
return {
|
|
10039
|
+
sessionId,
|
|
10040
|
+
intent: "approval",
|
|
10041
|
+
title: "Approval",
|
|
10042
|
+
message,
|
|
10043
|
+
...gate.detail?.trim() ? { detail: gate.detail } : {},
|
|
10044
|
+
questions: [
|
|
10045
|
+
{
|
|
10046
|
+
text: message,
|
|
10047
|
+
header: "Approval",
|
|
10048
|
+
inputMode: "single",
|
|
10049
|
+
options: gate.options.map((option) => ({ label: option.label })),
|
|
10050
|
+
allowOther: false,
|
|
10051
|
+
secret: "unknown"
|
|
10052
|
+
}
|
|
10053
|
+
],
|
|
10054
|
+
answerRequirement: "unknown",
|
|
10055
|
+
expiresAt: null,
|
|
10056
|
+
provenance: { source: "screen", confidence: "inferred" }
|
|
10057
|
+
};
|
|
10058
|
+
}
|
|
10059
|
+
function questionPromptDraft(sessionId, questions, source) {
|
|
10060
|
+
const first = questions[0];
|
|
10061
|
+
if (!first) throw new Error("Cannot normalize an empty question list");
|
|
10062
|
+
return {
|
|
10063
|
+
sessionId,
|
|
10064
|
+
intent: "question",
|
|
10065
|
+
...first.header.trim() ? { title: first.header } : {},
|
|
10066
|
+
message: first.question,
|
|
10067
|
+
questions: questions.map((question) => ({
|
|
10068
|
+
text: question.question,
|
|
10069
|
+
...question.header.trim() ? { header: question.header } : {},
|
|
10070
|
+
inputMode: question.multiSelect ? "multi" : "single",
|
|
10071
|
+
options: question.options.map((option) => ({
|
|
10072
|
+
label: option.label,
|
|
10073
|
+
...option.description ? { description: option.description } : {},
|
|
10074
|
+
...option.preview ? { preview: option.preview } : {}
|
|
10075
|
+
})),
|
|
10076
|
+
allowOther: false,
|
|
10077
|
+
secret: "unknown"
|
|
10078
|
+
})),
|
|
10079
|
+
answerRequirement: "unknown",
|
|
10080
|
+
expiresAt: null,
|
|
10081
|
+
provenance: {
|
|
10082
|
+
source,
|
|
10083
|
+
confidence: source === "transcript" ? "authoritative" : "inferred"
|
|
10084
|
+
}
|
|
10085
|
+
};
|
|
10086
|
+
}
|
|
10087
|
+
|
|
9547
10088
|
// src/services/questions/parseStatusLine.ts
|
|
9548
10089
|
var MODEL_RE = /(Opus|Sonnet|Haiku|Fable)\s+[\d.]+(?:\s*\([^)]*\))?/;
|
|
9549
10090
|
var EFFORT_RE = /●\s*([A-Za-z]+)\s*·\s*\/effort/;
|
|
@@ -9906,6 +10447,21 @@ function codexSessionActiveBody(outcome) {
|
|
|
9906
10447
|
...outcome.ownerSource != null && { ownerSource: outcome.ownerSource }
|
|
9907
10448
|
};
|
|
9908
10449
|
}
|
|
10450
|
+
function promptAnswerStatus(code) {
|
|
10451
|
+
switch (code) {
|
|
10452
|
+
case "prompt_not_found":
|
|
10453
|
+
return 404;
|
|
10454
|
+
case "provider_error":
|
|
10455
|
+
return 502;
|
|
10456
|
+
case "unknown_question":
|
|
10457
|
+
case "unknown_option":
|
|
10458
|
+
case "incomplete_answer":
|
|
10459
|
+
case "unsupported_prompt_shape":
|
|
10460
|
+
return 400;
|
|
10461
|
+
default:
|
|
10462
|
+
return 409;
|
|
10463
|
+
}
|
|
10464
|
+
}
|
|
9909
10465
|
var SessionHandlers = class {
|
|
9910
10466
|
constructor(deps) {
|
|
9911
10467
|
this.deps = deps;
|
|
@@ -9944,6 +10500,10 @@ var SessionHandlers = class {
|
|
|
9944
10500
|
get pendingQuestions() {
|
|
9945
10501
|
return this.deps.pendingQuestions;
|
|
9946
10502
|
}
|
|
10503
|
+
get promptRegistry() {
|
|
10504
|
+
if (!this.deps.promptRegistry) this.deps.promptRegistry = new PromptRegistry();
|
|
10505
|
+
return this.deps.promptRegistry;
|
|
10506
|
+
}
|
|
9947
10507
|
get pendingQuestionKey() {
|
|
9948
10508
|
return this.deps.pendingQuestionKey;
|
|
9949
10509
|
}
|
|
@@ -9962,6 +10522,13 @@ var SessionHandlers = class {
|
|
|
9962
10522
|
get sessionSubscribers() {
|
|
9963
10523
|
return this.deps.sessionSubscribers;
|
|
9964
10524
|
}
|
|
10525
|
+
// Prompt lifecycle events (question / permission and their cancellations)
|
|
10526
|
+
// carry prompt content and go ONLY to the session's subscribers — never to
|
|
10527
|
+
// every connected socket. A client that subscribes after a prompt opened
|
|
10528
|
+
// gets it from the subscribe replay (server-wiring), not from a broadcast.
|
|
10529
|
+
broadcastToSession(sessionId, message) {
|
|
10530
|
+
this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], message);
|
|
10531
|
+
}
|
|
9965
10532
|
get agentConfig() {
|
|
9966
10533
|
return this.deps.agentConfig;
|
|
9967
10534
|
}
|
|
@@ -10455,6 +11022,7 @@ var SessionHandlers = class {
|
|
|
10455
11022
|
json(res, 400, { error: "Missing input field" });
|
|
10456
11023
|
return;
|
|
10457
11024
|
}
|
|
11025
|
+
this.promptRegistry.hasActionable(sessionId);
|
|
10458
11026
|
const openPrompt = this.pendingPermission.has(sessionId) ? "permission" : this.pendingQuestions.has(sessionId) ? "question" : null;
|
|
10459
11027
|
if (openPrompt) {
|
|
10460
11028
|
this.log.info(`[input.prompt_pending] ${sessionId.slice(0, 8)} kind=${openPrompt}`, {
|
|
@@ -10510,37 +11078,106 @@ var SessionHandlers = class {
|
|
|
10510
11078
|
// the later JSONL flush of the same question is de-duped. We synthesize a
|
|
10511
11079
|
// screen-scoped toolUseId; the JSONL path overwrites pendingQuestions with the
|
|
10512
11080
|
// real toolUseId when it lands, so answering works once JSONL catches up.
|
|
10513
|
-
handleLiveQuestion(sessionId, questions) {
|
|
11081
|
+
handleLiveQuestion(sessionId, questions, occurrenceId) {
|
|
10514
11082
|
const key = questionContentKey(questions);
|
|
10515
11083
|
if (this.pendingQuestionKey.get(sessionId) === key) return;
|
|
10516
11084
|
const toolUseId = `screen:${sessionId}:${key.length}`;
|
|
10517
|
-
this.pendingQuestions.
|
|
11085
|
+
const prior = this.pendingQuestions.get(sessionId);
|
|
11086
|
+
const priorPrompt = prior ? this.promptRegistry.get(prior.promptId) : null;
|
|
11087
|
+
if (priorPrompt?.state === "open" || priorPrompt?.state === "updated") {
|
|
11088
|
+
this.promptRegistry.transition(priorPrompt.promptId, "cancelled", "replaced");
|
|
11089
|
+
}
|
|
11090
|
+
const prompt = this.promptRegistry.open(
|
|
11091
|
+
questionPromptDraft(sessionId, questions, "screen"),
|
|
11092
|
+
this.questionAnswerAdapter(sessionId),
|
|
11093
|
+
occurrenceId
|
|
11094
|
+
);
|
|
11095
|
+
this.pendingQuestions.set(sessionId, {
|
|
11096
|
+
toolUseId,
|
|
11097
|
+
questions,
|
|
11098
|
+
origin: "pty",
|
|
11099
|
+
promptId: prompt.promptId
|
|
11100
|
+
});
|
|
10518
11101
|
this.pendingQuestionKey.set(sessionId, key);
|
|
10519
|
-
this.
|
|
11102
|
+
this.broadcastToSession(sessionId, { type: "question", sessionId, toolUseId, questions });
|
|
11103
|
+
}
|
|
11104
|
+
handleJsonlQuestion(sessionId, toolUseId, questions, origin) {
|
|
11105
|
+
const prior = this.pendingQuestions.get(sessionId);
|
|
11106
|
+
const sameQuestion = prior !== void 0 && questionContentKey(prior.questions) === questionContentKey(questions);
|
|
11107
|
+
let prompt;
|
|
11108
|
+
if (sameQuestion) {
|
|
11109
|
+
const current = this.promptRegistry.get(prior.promptId);
|
|
11110
|
+
prompt = current?.provenance.source === "transcript" ? current : this.promptRegistry.update(
|
|
11111
|
+
prior.promptId,
|
|
11112
|
+
questionPromptDraft(sessionId, questions, "transcript"),
|
|
11113
|
+
this.questionAnswerAdapter(sessionId)
|
|
11114
|
+
);
|
|
11115
|
+
} else {
|
|
11116
|
+
const priorPrompt = prior ? this.promptRegistry.get(prior.promptId) : null;
|
|
11117
|
+
if (priorPrompt?.state === "open" || priorPrompt?.state === "updated") {
|
|
11118
|
+
this.promptRegistry.transition(priorPrompt.promptId, "cancelled", "replaced");
|
|
11119
|
+
}
|
|
11120
|
+
prompt = this.promptRegistry.open(
|
|
11121
|
+
questionPromptDraft(sessionId, questions, "transcript"),
|
|
11122
|
+
this.questionAnswerAdapter(sessionId)
|
|
11123
|
+
);
|
|
11124
|
+
}
|
|
11125
|
+
this.pendingQuestions.set(sessionId, {
|
|
11126
|
+
toolUseId,
|
|
11127
|
+
questions,
|
|
11128
|
+
origin,
|
|
11129
|
+
promptId: prompt.promptId
|
|
11130
|
+
});
|
|
10520
11131
|
}
|
|
10521
11132
|
// Permission gate opened/closed (OSC 777 + scraped options). Broadcasts the
|
|
10522
11133
|
// additive `permission` / `permission_cancelled` events. Mobile answers by
|
|
10523
11134
|
// sending the chosen option index via /input { keys } (e.g. "2\r").
|
|
10524
|
-
handlePermissionChange(sessionId, gate) {
|
|
11135
|
+
handlePermissionChange(sessionId, gate, occurrenceId) {
|
|
10525
11136
|
if (gate === null) {
|
|
10526
|
-
|
|
11137
|
+
const prior2 = this.pendingPermission.get(sessionId);
|
|
11138
|
+
if (!prior2) return;
|
|
11139
|
+
const prompt2 = prior2.promptId ? this.promptRegistry.get(prior2.promptId) : null;
|
|
11140
|
+
if (prompt2?.state === "open" || prompt2?.state === "updated") {
|
|
11141
|
+
this.promptRegistry.transition(prompt2.promptId, "cancelled", "provider_closed");
|
|
11142
|
+
}
|
|
10527
11143
|
this.pendingPermission.delete(sessionId);
|
|
10528
11144
|
this.pendingPermissionKey.delete(sessionId);
|
|
10529
|
-
this.
|
|
11145
|
+
this.broadcastToSession(sessionId, { type: "permission_cancelled", sessionId });
|
|
10530
11146
|
return;
|
|
10531
11147
|
}
|
|
10532
11148
|
const key = permissionContentKey(gate);
|
|
10533
|
-
if (this.pendingPermissionKey.get(sessionId) === key) return;
|
|
10534
11149
|
const prior = this.pendingPermission.get(sessionId);
|
|
10535
|
-
const
|
|
10536
|
-
this.
|
|
11150
|
+
const priorPromptId = prior?.promptId;
|
|
11151
|
+
if (this.pendingPermissionKey.get(sessionId) === key && (occurrenceId === void 0 || prior?.occurrenceId === occurrenceId)) {
|
|
11152
|
+
return;
|
|
11153
|
+
}
|
|
11154
|
+
const samePrompt = prior && priorPromptId !== void 0 && permissionGateKey(prior) === permissionGateKey(gate) && (occurrenceId === void 0 || prior.occurrenceId === occurrenceId);
|
|
11155
|
+
if (prior && !samePrompt) {
|
|
11156
|
+
const priorPrompt = prior.promptId ? this.promptRegistry.get(prior.promptId) : null;
|
|
11157
|
+
if (priorPrompt?.state === "open" || priorPrompt?.state === "updated") {
|
|
11158
|
+
this.promptRegistry.transition(priorPrompt.promptId, "cancelled", "replaced");
|
|
11159
|
+
}
|
|
11160
|
+
}
|
|
11161
|
+
const prompt = gate.options.length === 0 ? null : samePrompt ? this.promptRegistry.get(priorPromptId) : this.promptRegistry.open(
|
|
11162
|
+
permissionPromptDraft(sessionId, gate),
|
|
11163
|
+
this.permissionAnswerAdapter(sessionId),
|
|
11164
|
+
occurrenceId
|
|
11165
|
+
);
|
|
11166
|
+
if (samePrompt && !prompt) throw new Error("Pending permission prompt disappeared");
|
|
11167
|
+
const gateId = prompt?.promptId ?? occurrenceId ?? prior?.gateId ?? randomUUID5();
|
|
11168
|
+
this.pendingPermission.set(sessionId, {
|
|
11169
|
+
...gate,
|
|
11170
|
+
gateId,
|
|
11171
|
+
...prompt ? { promptId: prompt.promptId } : {},
|
|
11172
|
+
...occurrenceId !== void 0 ? { occurrenceId } : {}
|
|
11173
|
+
});
|
|
10537
11174
|
this.pendingPermissionKey.set(sessionId, key);
|
|
10538
11175
|
const subscriberCount = this.sessionSubscribers.get(sessionId)?.size ?? 0;
|
|
10539
11176
|
this.log.info(
|
|
10540
11177
|
`[ws.broadcast_permission] ${sessionId.slice(0, 8)} subscribers=${subscriberCount}`,
|
|
10541
11178
|
{ event: "ws.broadcast_permission", sessionId, subscriberCount }
|
|
10542
11179
|
);
|
|
10543
|
-
this.
|
|
11180
|
+
this.broadcastToSession(sessionId, {
|
|
10544
11181
|
type: "permission",
|
|
10545
11182
|
sessionId,
|
|
10546
11183
|
...gate.prompt ? { prompt: gate.prompt } : {},
|
|
@@ -10551,6 +11188,107 @@ var SessionHandlers = class {
|
|
|
10551
11188
|
gateId
|
|
10552
11189
|
});
|
|
10553
11190
|
}
|
|
11191
|
+
permissionAnswerAdapter(sessionId) {
|
|
11192
|
+
return async ({ prompt, answer }) => {
|
|
11193
|
+
const gate = this.pendingPermission.get(sessionId);
|
|
11194
|
+
if (!gate || gate.promptId !== prompt.promptId) {
|
|
11195
|
+
return {
|
|
11196
|
+
ok: false,
|
|
11197
|
+
code: "prompt_unavailable",
|
|
11198
|
+
terminal: { state: "unavailable", reason: "provider_prompt_missing" }
|
|
11199
|
+
};
|
|
11200
|
+
}
|
|
11201
|
+
const response = answer.responses[0];
|
|
11202
|
+
const selectedId = response?.optionIds?.[0];
|
|
11203
|
+
const selectedIndex = prompt.questions[0]?.options.findIndex(
|
|
11204
|
+
(option2) => option2.optionId === selectedId
|
|
11205
|
+
);
|
|
11206
|
+
if (selectedIndex === void 0 || selectedIndex < 0) {
|
|
11207
|
+
return { ok: false, code: "unknown_option" };
|
|
11208
|
+
}
|
|
11209
|
+
const option = gate.options[selectedIndex];
|
|
11210
|
+
if (!option) return { ok: false, code: "unknown_option" };
|
|
11211
|
+
const provider = this.sessionStore.getManaged(sessionId)?.provider;
|
|
11212
|
+
if (provider !== CODEX_CLI_PROVIDER && !await this.permissionGateStillOpen(sessionId, permissionGateKey(gate))) {
|
|
11213
|
+
return {
|
|
11214
|
+
ok: false,
|
|
11215
|
+
code: "prompt_cancelled",
|
|
11216
|
+
terminal: { state: "cancelled", reason: "provider_closed" }
|
|
11217
|
+
};
|
|
11218
|
+
}
|
|
11219
|
+
if (this.pendingPermission.get(sessionId)?.promptId !== prompt.promptId) {
|
|
11220
|
+
return { ok: false, code: "prompt_cancelled" };
|
|
11221
|
+
}
|
|
11222
|
+
try {
|
|
11223
|
+
this.ptyManager.sendKeys(
|
|
11224
|
+
sessionId,
|
|
11225
|
+
option.answerKeys ?? permissionAnswerKeys(option.index)
|
|
11226
|
+
);
|
|
11227
|
+
} catch {
|
|
11228
|
+
return { ok: false, code: "provider_error" };
|
|
11229
|
+
}
|
|
11230
|
+
return { ok: true };
|
|
11231
|
+
};
|
|
11232
|
+
}
|
|
11233
|
+
questionAnswerAdapter(sessionId) {
|
|
11234
|
+
return async ({ prompt, answer }) => {
|
|
11235
|
+
const pending = this.pendingQuestions.get(sessionId);
|
|
11236
|
+
if (!pending || pending.promptId !== prompt.promptId) {
|
|
11237
|
+
return {
|
|
11238
|
+
ok: false,
|
|
11239
|
+
code: "prompt_unavailable",
|
|
11240
|
+
terminal: { state: "unavailable", reason: "provider_prompt_missing" }
|
|
11241
|
+
};
|
|
11242
|
+
}
|
|
11243
|
+
const answers = {};
|
|
11244
|
+
for (const question of prompt.questions) {
|
|
11245
|
+
const response = answer.responses.find((item) => item.questionId === question.questionId);
|
|
11246
|
+
if (!response?.optionIds) return { ok: false, code: "unsupported_prompt_shape" };
|
|
11247
|
+
answers[question.text] = response.optionIds.map((optionId) => {
|
|
11248
|
+
const option = question.options.find((item) => item.optionId === optionId);
|
|
11249
|
+
return option?.label ?? "";
|
|
11250
|
+
});
|
|
11251
|
+
}
|
|
11252
|
+
const resolution = resolveAnswer(pending, {
|
|
11253
|
+
toolUseId: pending.toolUseId,
|
|
11254
|
+
answers
|
|
11255
|
+
});
|
|
11256
|
+
if (!resolution.ok) {
|
|
11257
|
+
const code = resolution.reason === "unknown_option" || resolution.reason === "incomplete_answer" || resolution.reason === "unsupported_prompt_shape" ? resolution.reason : "prompt_unavailable";
|
|
11258
|
+
return { ok: false, code };
|
|
11259
|
+
}
|
|
11260
|
+
if (!await this.questionMenuStillOpen(sessionId)) {
|
|
11261
|
+
this.pendingQuestions.delete(sessionId);
|
|
11262
|
+
this.pendingQuestionKey.delete(sessionId);
|
|
11263
|
+
this.broadcastToSession(sessionId, {
|
|
11264
|
+
type: "question_cancelled",
|
|
11265
|
+
sessionId,
|
|
11266
|
+
toolUseId: pending.toolUseId
|
|
11267
|
+
});
|
|
11268
|
+
return {
|
|
11269
|
+
ok: false,
|
|
11270
|
+
code: "prompt_cancelled",
|
|
11271
|
+
terminal: { state: "cancelled", reason: "provider_closed" }
|
|
11272
|
+
};
|
|
11273
|
+
}
|
|
11274
|
+
if (this.pendingQuestions.get(sessionId)?.promptId !== prompt.promptId) {
|
|
11275
|
+
return { ok: false, code: "prompt_cancelled" };
|
|
11276
|
+
}
|
|
11277
|
+
try {
|
|
11278
|
+
this.ptyManager.sendKeys(sessionId, resolution.keys);
|
|
11279
|
+
} catch {
|
|
11280
|
+
return { ok: false, code: "provider_error" };
|
|
11281
|
+
}
|
|
11282
|
+
this.pendingQuestions.delete(sessionId);
|
|
11283
|
+
this.pendingQuestionKey.delete(sessionId);
|
|
11284
|
+
this.broadcastToSession(sessionId, {
|
|
11285
|
+
type: "question_cancelled",
|
|
11286
|
+
sessionId,
|
|
11287
|
+
toolUseId: pending.toolUseId
|
|
11288
|
+
});
|
|
11289
|
+
return { ok: true };
|
|
11290
|
+
};
|
|
11291
|
+
}
|
|
10554
11292
|
/**
|
|
10555
11293
|
* Answer a permission gate — the validated counterpart of POST /:id/input.
|
|
10556
11294
|
*
|
|
@@ -10586,9 +11324,14 @@ var SessionHandlers = class {
|
|
|
10586
11324
|
return;
|
|
10587
11325
|
}
|
|
10588
11326
|
const gateClosed = () => {
|
|
11327
|
+
const pending = this.pendingPermission.get(sessionId);
|
|
11328
|
+
const prompt = pending?.promptId ? this.promptRegistry.get(pending.promptId) : null;
|
|
11329
|
+
if (prompt?.state === "open" || prompt?.state === "updated") {
|
|
11330
|
+
this.promptRegistry.transition(prompt.promptId, "cancelled", "provider_closed");
|
|
11331
|
+
}
|
|
10589
11332
|
this.pendingPermission.delete(sessionId);
|
|
10590
11333
|
this.pendingPermissionKey.delete(sessionId);
|
|
10591
|
-
this.
|
|
11334
|
+
this.broadcastToSession(sessionId, { type: "permission_cancelled", sessionId });
|
|
10592
11335
|
json(res, 409, { ok: false, reason: "gate_closed" });
|
|
10593
11336
|
};
|
|
10594
11337
|
const gate = this.pendingPermission.get(sessionId);
|
|
@@ -10627,6 +11370,10 @@ var SessionHandlers = class {
|
|
|
10627
11370
|
json(res, 400, { ok: false, reason: message });
|
|
10628
11371
|
return;
|
|
10629
11372
|
}
|
|
11373
|
+
const normalized = gate.promptId ? this.promptRegistry.get(gate.promptId) : null;
|
|
11374
|
+
if (normalized?.state === "open" || normalized?.state === "updated") {
|
|
11375
|
+
this.promptRegistry.transition(normalized.promptId, "resolved", "answered_legacy");
|
|
11376
|
+
}
|
|
10630
11377
|
json(res, 200, { ok: true });
|
|
10631
11378
|
}
|
|
10632
11379
|
/**
|
|
@@ -10672,9 +11419,13 @@ var SessionHandlers = class {
|
|
|
10672
11419
|
}
|
|
10673
11420
|
const toolUseId = pending?.toolUseId ?? "";
|
|
10674
11421
|
if (!await this.questionMenuStillOpen(sessionId)) {
|
|
11422
|
+
const prompt = this.promptRegistry.get(pending?.promptId ?? "");
|
|
11423
|
+
if (prompt?.state === "open" || prompt?.state === "updated") {
|
|
11424
|
+
this.promptRegistry.transition(prompt.promptId, "cancelled", "provider_closed");
|
|
11425
|
+
}
|
|
10675
11426
|
this.pendingQuestions.delete(sessionId);
|
|
10676
11427
|
this.pendingQuestionKey.delete(sessionId);
|
|
10677
|
-
this.
|
|
11428
|
+
this.broadcastToSession(sessionId, { type: "question_cancelled", sessionId, toolUseId });
|
|
10678
11429
|
json(res, 409, { ok: false, reason: "question_gone" });
|
|
10679
11430
|
return;
|
|
10680
11431
|
}
|
|
@@ -10685,10 +11436,41 @@ var SessionHandlers = class {
|
|
|
10685
11436
|
json(res, 400, { ok: false, reason: message });
|
|
10686
11437
|
return;
|
|
10687
11438
|
}
|
|
11439
|
+
const normalized = this.promptRegistry.get(pending?.promptId ?? "");
|
|
11440
|
+
if (normalized?.state === "open" || normalized?.state === "updated") {
|
|
11441
|
+
this.promptRegistry.transition(normalized.promptId, "resolved", "answered_legacy");
|
|
11442
|
+
}
|
|
10688
11443
|
this.pendingQuestions.delete(sessionId);
|
|
10689
|
-
this.
|
|
11444
|
+
this.broadcastToSession(sessionId, { type: "question_cancelled", sessionId, toolUseId });
|
|
10690
11445
|
json(res, 200, { ok: true });
|
|
10691
11446
|
}
|
|
11447
|
+
/**
|
|
11448
|
+
* Answer a normalized prompt by its opaque ids.
|
|
11449
|
+
*
|
|
11450
|
+
* Refusals are keyed by `code` — the stable machine taxonomy of the prompt
|
|
11451
|
+
* contract. The released legacy routes (`/answer`, `/permission/answer`) key
|
|
11452
|
+
* theirs by `reason` and keep doing so; a client reads whichever key belongs
|
|
11453
|
+
* to the route it called, and the two vocabularies are not merged.
|
|
11454
|
+
*
|
|
11455
|
+
* Status follows the same split as the legacy routes: a malformed or
|
|
11456
|
+
* unanswerable *request* is 400, a prompt whose *state* refuses the answer is
|
|
11457
|
+
* 409. A retry after PROMPT_TERMINAL_RETENTION_MS answers 404
|
|
11458
|
+
* `prompt_not_found`, not the recorded outcome — the record it would replay
|
|
11459
|
+
* is gone by then.
|
|
11460
|
+
*/
|
|
11461
|
+
async handlePromptAnswer(sessionId, req, res) {
|
|
11462
|
+
const parsed = PromptAnswerSchema.safeParse(await readBody2(req));
|
|
11463
|
+
if (!parsed.success) {
|
|
11464
|
+
json(res, 400, { ok: false, code: "invalid_prompt_answer" });
|
|
11465
|
+
return;
|
|
11466
|
+
}
|
|
11467
|
+
const outcome = await this.promptRegistry.answer(sessionId, parsed.data);
|
|
11468
|
+
if (outcome.ok) {
|
|
11469
|
+
json(res, 200, outcome);
|
|
11470
|
+
return;
|
|
11471
|
+
}
|
|
11472
|
+
json(res, promptAnswerStatus(outcome.code), outcome);
|
|
11473
|
+
}
|
|
10692
11474
|
// Best-effort: a session we don't own a PTY for, or one that raced away
|
|
10693
11475
|
// mid-read, is not ours to veto — say yes and let the write decide.
|
|
10694
11476
|
async questionMenuStillOpen(sessionId) {
|
|
@@ -11331,7 +12113,7 @@ var ManagedSessionsRepository = class {
|
|
|
11331
12113
|
};
|
|
11332
12114
|
|
|
11333
12115
|
// src/db/repositories/projects.repository.ts
|
|
11334
|
-
import { randomUUID as
|
|
12116
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
11335
12117
|
function rowToProject(row) {
|
|
11336
12118
|
return {
|
|
11337
12119
|
id: row.id,
|
|
@@ -11416,7 +12198,7 @@ var ProjectsRepository = class {
|
|
|
11416
12198
|
});
|
|
11417
12199
|
return rowToProject(this.getById.get(existing.id));
|
|
11418
12200
|
}
|
|
11419
|
-
const id =
|
|
12201
|
+
const id = randomUUID6();
|
|
11420
12202
|
this.insert.run({
|
|
11421
12203
|
id,
|
|
11422
12204
|
path,
|
|
@@ -12843,6 +13625,27 @@ function handleListProjects(url, res) {
|
|
|
12843
13625
|
function wsAllows(principal, required) {
|
|
12844
13626
|
return principal === null || hasCapability(principal, required);
|
|
12845
13627
|
}
|
|
13628
|
+
function clearExpiredPendingPrompt(deps, prompt) {
|
|
13629
|
+
const permission = deps.pendingPermission.get(prompt.sessionId);
|
|
13630
|
+
if (permission?.promptId === prompt.promptId) {
|
|
13631
|
+
deps.pendingPermission.delete(prompt.sessionId);
|
|
13632
|
+
deps.pendingPermissionKey.delete(prompt.sessionId);
|
|
13633
|
+
deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(prompt.sessionId) ?? [], {
|
|
13634
|
+
type: "permission_cancelled",
|
|
13635
|
+
sessionId: prompt.sessionId
|
|
13636
|
+
});
|
|
13637
|
+
}
|
|
13638
|
+
const question = deps.pendingQuestions.get(prompt.sessionId);
|
|
13639
|
+
if (question?.promptId === prompt.promptId) {
|
|
13640
|
+
deps.pendingQuestions.delete(prompt.sessionId);
|
|
13641
|
+
deps.pendingQuestionKey.delete(prompt.sessionId);
|
|
13642
|
+
deps.wsHub.broadcastToClients(deps.sessionSubscribers.get(prompt.sessionId) ?? [], {
|
|
13643
|
+
type: "question_cancelled",
|
|
13644
|
+
sessionId: prompt.sessionId,
|
|
13645
|
+
toolUseId: question.toolUseId
|
|
13646
|
+
});
|
|
13647
|
+
}
|
|
13648
|
+
}
|
|
12846
13649
|
function createConversationWatcherEvents(deps) {
|
|
12847
13650
|
return {
|
|
12848
13651
|
onNewLineSpans: (filePath, spans, readFrom, endOffset) => {
|
|
@@ -12965,11 +13768,11 @@ function createLiveSessionOptions(deps) {
|
|
|
12965
13768
|
ts
|
|
12966
13769
|
});
|
|
12967
13770
|
},
|
|
12968
|
-
onPermissionChange: (sessionId, gate) => {
|
|
12969
|
-
deps.sessionHandlers().handlePermissionChange(sessionId, gate);
|
|
13771
|
+
onPermissionChange: (sessionId, gate, occurrenceId) => {
|
|
13772
|
+
deps.sessionHandlers().handlePermissionChange(sessionId, gate, occurrenceId);
|
|
12970
13773
|
},
|
|
12971
|
-
onLiveQuestion: (sessionId, questions) => {
|
|
12972
|
-
deps.sessionHandlers().handleLiveQuestion(sessionId, questions);
|
|
13774
|
+
onLiveQuestion: (sessionId, questions, occurrenceId) => {
|
|
13775
|
+
deps.sessionHandlers().handleLiveQuestion(sessionId, questions, occurrenceId);
|
|
12973
13776
|
},
|
|
12974
13777
|
onLiveQuestionGone: (sessionId) => {
|
|
12975
13778
|
deps.pendingQuestionKey.delete(sessionId);
|
|
@@ -13050,10 +13853,11 @@ function createLiveSessionOptions(deps) {
|
|
|
13050
13853
|
if (filePath) {
|
|
13051
13854
|
deps.fileWatcher.unwatch(filePath);
|
|
13052
13855
|
deps.sessionFileMap.delete(session.id);
|
|
13053
|
-
deps.cancelPendingQuestion(session.id);
|
|
13054
13856
|
}
|
|
13857
|
+
deps.cancelPendingQuestion(session.id);
|
|
13055
13858
|
deps.pendingPermission.delete(session.id);
|
|
13056
13859
|
deps.pendingPermissionKey.delete(session.id);
|
|
13860
|
+
deps.promptRegistry.invalidateSession(session.id, "session_ended");
|
|
13057
13861
|
deps.contendedSessions.delete(session.id);
|
|
13058
13862
|
deps.rememberSelfPtyEnded(session.id);
|
|
13059
13863
|
}
|
|
@@ -13108,6 +13912,7 @@ function createApiDeps(deps) {
|
|
|
13108
13912
|
handleGetOutput: (id, res) => deps.sessionHandlers.handleGetOutput(id, res),
|
|
13109
13913
|
handleSendInput: (id, req, res) => deps.sessionHandlers.handleSendInput(id, req, res),
|
|
13110
13914
|
handleSendAnswer: (id, req, res) => deps.sessionHandlers.handleSendAnswer(id, req, res),
|
|
13915
|
+
handlePromptAnswer: (id, req, res) => deps.sessionHandlers.handlePromptAnswer(id, req, res),
|
|
13111
13916
|
handlePermissionAnswer: (id, req, res) => deps.sessionHandlers.handlePermissionAnswer(id, req, res),
|
|
13112
13917
|
handleCancel: (id, res) => deps.sessionHandlers.handleCancel(id, res),
|
|
13113
13918
|
handleStopSession: (id, res) => deps.sessionHandlers.handleStopSession(id, res),
|
|
@@ -13166,6 +13971,9 @@ function createApiDeps(deps) {
|
|
|
13166
13971
|
return;
|
|
13167
13972
|
}
|
|
13168
13973
|
deps.addSessionSubscriber(msg.sessionId, ws);
|
|
13974
|
+
if (deps.promptRegistry) {
|
|
13975
|
+
ws.send(JSON.stringify(deps.promptRegistry.snapshot(msg.sessionId)));
|
|
13976
|
+
}
|
|
13169
13977
|
if (deps.ptyManager.hasSession(msg.sessionId)) {
|
|
13170
13978
|
const lines = await deps.ptyManager.getOutputLines(msg.sessionId, REPLAY_MAX_LINES);
|
|
13171
13979
|
const userMessages = deps.ptyManager.getInputHistory(msg.sessionId);
|
|
@@ -15936,6 +16744,7 @@ var StreamerServer = class {
|
|
|
15936
16744
|
// repaint of the same gate doesn't re-broadcast on every tick. Cleared
|
|
15937
16745
|
// alongside pendingPermission.
|
|
15938
16746
|
pendingPermissionKey = /* @__PURE__ */ new Map();
|
|
16747
|
+
promptRegistry;
|
|
15939
16748
|
// Scanner lifecycle, freshness state and the cache↔disk reconcile.
|
|
15940
16749
|
scannerManager;
|
|
15941
16750
|
// Binds a live session to the JSONL/rollout its provider writes.
|
|
@@ -16052,7 +16861,7 @@ var StreamerServer = class {
|
|
|
16052
16861
|
runtimeStore = null;
|
|
16053
16862
|
// Identifies this streamer run. A registry row carrying a different id is a
|
|
16054
16863
|
// session that outlived the process that started it.
|
|
16055
|
-
streamerInstanceId =
|
|
16864
|
+
streamerInstanceId = randomUUID7();
|
|
16056
16865
|
cacheMetadataRepo = null;
|
|
16057
16866
|
// Push registration + delivery state (C7). Null when the cache DB failed to
|
|
16058
16867
|
// open — registration then degrades to a no-op rather than 500ing.
|
|
@@ -16188,6 +16997,20 @@ var StreamerServer = class {
|
|
|
16188
16997
|
this.browserCors = config.browserCors ?? loadBrowserCors();
|
|
16189
16998
|
this.sessionStore = new SessionStore();
|
|
16190
16999
|
this.wsHub = new WSHub();
|
|
17000
|
+
this.promptRegistry = new PromptRegistry({
|
|
17001
|
+
emit: (event) => this.wsHub.broadcastToClients(this.sessionSubscribers.get(event.sessionId) ?? [], event),
|
|
17002
|
+
onExpire: (prompt) => clearExpiredPendingPrompt(
|
|
17003
|
+
{
|
|
17004
|
+
pendingPermission: this.pendingPermission,
|
|
17005
|
+
pendingPermissionKey: this.pendingPermissionKey,
|
|
17006
|
+
pendingQuestions: this.pendingQuestions,
|
|
17007
|
+
pendingQuestionKey: this.pendingQuestionKey,
|
|
17008
|
+
sessionSubscribers: this.sessionSubscribers,
|
|
17009
|
+
wsHub: this.wsHub
|
|
17010
|
+
},
|
|
17011
|
+
prompt
|
|
17012
|
+
)
|
|
17013
|
+
});
|
|
16191
17014
|
this.fileWatcher = new ConversationWatcher(
|
|
16192
17015
|
createConversationWatcherEvents({
|
|
16193
17016
|
sessionFileMap: this.sessionFileMap,
|
|
@@ -16231,6 +17054,7 @@ var StreamerServer = class {
|
|
|
16231
17054
|
pendingQuestionKey: this.pendingQuestionKey,
|
|
16232
17055
|
pendingPermission: this.pendingPermission,
|
|
16233
17056
|
pendingPermissionKey: this.pendingPermissionKey,
|
|
17057
|
+
promptRegistry: this.promptRegistry,
|
|
16234
17058
|
contendedSessions: this.contendedSessions,
|
|
16235
17059
|
// Thunks, not values: sessionHandlers is constructed below, the
|
|
16236
17060
|
// registry repo and the push notifiers are bound during listen(), and
|
|
@@ -16321,6 +17145,7 @@ var StreamerServer = class {
|
|
|
16321
17145
|
sessionStatusBus: this.sessionStatusBus,
|
|
16322
17146
|
sessionFileMap: this.sessionFileMap,
|
|
16323
17147
|
pendingQuestions: this.pendingQuestions,
|
|
17148
|
+
promptRegistry: this.promptRegistry,
|
|
16324
17149
|
pendingQuestionKey: this.pendingQuestionKey,
|
|
16325
17150
|
pendingPermission: this.pendingPermission,
|
|
16326
17151
|
pendingPermissionKey: this.pendingPermissionKey,
|
|
@@ -16421,6 +17246,7 @@ var StreamerServer = class {
|
|
|
16421
17246
|
terminalSeq: this.terminalSeq,
|
|
16422
17247
|
pendingPermission: this.pendingPermission,
|
|
16423
17248
|
pendingQuestions: this.pendingQuestions,
|
|
17249
|
+
promptRegistry: this.promptRegistry,
|
|
16424
17250
|
agentClient,
|
|
16425
17251
|
conversationWriter,
|
|
16426
17252
|
agentConfig
|
|
@@ -17201,6 +18027,7 @@ var StreamerServer = class {
|
|
|
17201
18027
|
if (!this.ptyManager.isRemote()) this.ptyManager.dispose();
|
|
17202
18028
|
this.fileWatcher.dispose();
|
|
17203
18029
|
this.externalTails.clear();
|
|
18030
|
+
this.promptRegistry.dispose();
|
|
17204
18031
|
this.wsHub.dispose();
|
|
17205
18032
|
this.pairTokens.dispose();
|
|
17206
18033
|
this.liveActivityRenewal?.stop();
|
|
@@ -17789,7 +18616,7 @@ var StreamerServer = class {
|
|
|
17789
18616
|
for (const p of pending) {
|
|
17790
18617
|
if (contended || foreignVsPty(p.questions)) continue;
|
|
17791
18618
|
const origin = priorPtyKey !== null && questionContentKey(p.questions) === priorPtyKey ? "pty" : "jsonl";
|
|
17792
|
-
this.
|
|
18619
|
+
this.sessionHandlers.handleJsonlQuestion(sessionId, p.toolUseId, p.questions, origin);
|
|
17793
18620
|
const t = setTimeout(() => {
|
|
17794
18621
|
if (this.pendingQuestions.get(sessionId)?.toolUseId === p.toolUseId) {
|
|
17795
18622
|
this.cancelPendingQuestion(sessionId);
|
|
@@ -17807,7 +18634,7 @@ var StreamerServer = class {
|
|
|
17807
18634
|
priorToolUseId
|
|
17808
18635
|
});
|
|
17809
18636
|
this.pendingQuestionKey.set(sessionId, key);
|
|
17810
|
-
if (broadcast) this.wsHub.
|
|
18637
|
+
if (broadcast) this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], m);
|
|
17811
18638
|
}
|
|
17812
18639
|
}
|
|
17813
18640
|
cancelPendingQuestion(sessionId) {
|
|
@@ -17815,7 +18642,15 @@ var StreamerServer = class {
|
|
|
17815
18642
|
if (!pq) return;
|
|
17816
18643
|
this.pendingQuestions.delete(sessionId);
|
|
17817
18644
|
this.pendingQuestionKey.delete(sessionId);
|
|
17818
|
-
this.
|
|
18645
|
+
const prompt = this.promptRegistry.get(pq.promptId);
|
|
18646
|
+
if (prompt?.state === "open" || prompt?.state === "updated") {
|
|
18647
|
+
this.promptRegistry.transition(pq.promptId, "cancelled", "provider_closed");
|
|
18648
|
+
}
|
|
18649
|
+
this.wsHub.broadcastToClients(this.sessionSubscribers.get(sessionId) ?? [], {
|
|
18650
|
+
type: "question_cancelled",
|
|
18651
|
+
sessionId,
|
|
18652
|
+
toolUseId: pq.toolUseId
|
|
18653
|
+
});
|
|
17819
18654
|
}
|
|
17820
18655
|
async handleBrowse(url, res) {
|
|
17821
18656
|
if (!this.browseRoot) {
|
|
@@ -17965,7 +18800,13 @@ export {
|
|
|
17965
18800
|
CODEX_CLI_PROVIDER,
|
|
17966
18801
|
ConversationWatcher,
|
|
17967
18802
|
LiveSessionManager,
|
|
18803
|
+
PROMPT_SCHEMA_VERSION,
|
|
17968
18804
|
PTYManager,
|
|
18805
|
+
PromptAnswerSchema,
|
|
18806
|
+
PromptOptionSchema,
|
|
18807
|
+
PromptQuestionSchema,
|
|
18808
|
+
PromptResponseSchema,
|
|
18809
|
+
PromptSchema,
|
|
17969
18810
|
SessionStore,
|
|
17970
18811
|
StreamerServer,
|
|
17971
18812
|
WSHub,
|