@p4code/cli 0.3.10 → 0.3.12

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/bin.mjs CHANGED
@@ -238,7 +238,7 @@ const make$91 = () => {
238
238
  const layer$82 = Layer.sync(NetService, make$91);
239
239
  //#endregion
240
240
  //#region package.json
241
- var version = "0.3.10";
241
+ var version = "0.3.12";
242
242
  //#endregion
243
243
  //#region src/config.ts
244
244
  /**
@@ -8230,6 +8230,10 @@ const ServerSettings = Schema$1.Struct({
8230
8230
  * back for sessions started by the Claude provider.
8231
8231
  */
8232
8232
  enableToolCallNarration: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
8233
+ /** Require fresh verification evidence before the agent claims completion. */
8234
+ enableVerificationBeforeCompletion: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(false))),
8235
+ /** Require root-cause investigation before the agent proposes or applies a fix. */
8236
+ enableRootCauseBeforeFix: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(false))),
8233
8237
  /**
8234
8238
  * Whether the model may spawn subagents without being asked to.
8235
8239
  *
@@ -8429,6 +8433,8 @@ const ServerSettingsPatch = Schema$1.Struct({
8429
8433
  enableAssistantStreaming: Schema$1.optionalKey(Schema$1.Boolean),
8430
8434
  enableProviderUpdateChecks: Schema$1.optionalKey(Schema$1.Boolean),
8431
8435
  enableToolCallNarration: Schema$1.optionalKey(Schema$1.Boolean),
8436
+ enableVerificationBeforeCompletion: Schema$1.optionalKey(Schema$1.Boolean),
8437
+ enableRootCauseBeforeFix: Schema$1.optionalKey(Schema$1.Boolean),
8432
8438
  enableUnpromptedSubagents: Schema$1.optionalKey(Schema$1.Boolean),
8433
8439
  enableScopingAgent: Schema$1.optionalKey(Schema$1.Boolean),
8434
8440
  enablePlanPhase: Schema$1.optionalKey(Schema$1.Boolean),
@@ -9734,7 +9740,8 @@ const ThreadSpawnInput = Schema$1.Struct({
9734
9740
  runtimeMode: Schema$1.optional(RuntimeMode.annotate({ description: "Permission mode for the new thread. Defaults to this session's own." })),
9735
9741
  interactionMode: Schema$1.optional(ProviderInteractionMode),
9736
9742
  compressMode: Schema$1.optional(CompressMode),
9737
- unpromptedSubagents: Schema$1.optional(Schema$1.Boolean)
9743
+ unpromptedSubagents: Schema$1.optional(Schema$1.Boolean),
9744
+ fusionWatcher: Schema$1.optional(Schema$1.Boolean.annotate({ description: "Set true only when creating the watcher for a Fusion pair. The server requires explicit user approval before creating the thread." }))
9738
9745
  });
9739
9746
  const ThreadSpawnResult = Schema$1.Struct({
9740
9747
  /** Watchable with `thread_watch_events`, and addressable by every tool here. */
@@ -9912,10 +9919,10 @@ var ThreadSpawnNotPermittedError = class extends Schema$1.TaggedErrorClass()("Th
9912
9919
  return `Thread ${this.threadId} cannot start another thread: ${this.detail}`;
9913
9920
  }
9914
9921
  };
9915
- /** Pair creation requires fresh user authorization from this exact thread. */
9922
+ /** Fusion watcher and pair creation require fresh user authorization from this exact thread. */
9916
9923
  var ThreadPairApprovalRequiredError = class extends Schema$1.TaggedErrorClass()("ThreadPairApprovalRequiredError", { threadId: ThreadId }) {
9917
9924
  get message() {
9918
- return `Thread ${this.threadId} cannot create a Fusion pair without explicit user approval. The latest user message must invoke /fusion or $fusion on its own line, or affirm the immediately preceding assistant proposal that names Fusion and asks for approval.`;
9925
+ return `Thread ${this.threadId} cannot create a Fusion watcher or pair without explicit user approval. The latest user message must invoke /fusion or $fusion on its own line, or affirm the immediately preceding assistant proposal that names Fusion and asks for approval.`;
9919
9926
  }
9920
9927
  };
9921
9928
  /** The orchestration engine declined or failed a thread control command. */
@@ -15724,6 +15731,27 @@ var _051_ThreadPairActivationTurn_default = Effect.gen(function* () {
15724
15731
  `;
15725
15732
  });
15726
15733
  //#endregion
15734
+ //#region src/persistence/Migrations/052_CompactHistoricalToolActivities.ts
15735
+ var _052_CompactHistoricalToolActivities_default = Effect.gen(function* () {
15736
+ const sql = yield* SqlClient.SqlClient;
15737
+ yield* sql`
15738
+ CREATE TABLE historical_activity_compaction_progress (
15739
+ job_name TEXT PRIMARY KEY,
15740
+ cursor_sequence INTEGER NOT NULL DEFAULT 0 CHECK (cursor_sequence >= 0),
15741
+ completed_at TEXT
15742
+ ) STRICT
15743
+ `;
15744
+ yield* sql`
15745
+ CREATE TABLE historical_activity_compaction_skips (
15746
+ sequence INTEGER PRIMARY KEY,
15747
+ event_id TEXT NOT NULL,
15748
+ reason TEXT NOT NULL,
15749
+ payload_bytes INTEGER NOT NULL,
15750
+ recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
15751
+ ) STRICT
15752
+ `;
15753
+ });
15754
+ //#endregion
15727
15755
  //#region src/persistence/Migrations.ts
15728
15756
  /**
15729
15757
  * MigrationsLive - Migration runner with inline loader
@@ -15999,6 +16027,11 @@ const migrationEntries = [
15999
16027
  51,
16000
16028
  "ThreadPairActivationTurn",
16001
16029
  _051_ThreadPairActivationTurn_default
16030
+ ],
16031
+ [
16032
+ 52,
16033
+ "CompactHistoricalToolActivities",
16034
+ _052_CompactHistoricalToolActivities_default
16002
16035
  ]
16003
16036
  ];
16004
16037
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -16078,6 +16111,665 @@ const runGuardedMigrations = Effect.fn("runGuardedMigrations")(function* (input)
16078
16111
  const discardSnapshotQuietly = (dbPath) => discardDatabaseSnapshot(dbPath).pipe(Effect.tapError((cause) => Effect.logWarning("Could not discard the database snapshot").pipe(Effect.annotateLogs({ cause }))), Effect.ignore);
16079
16112
  Layer.effectDiscard(runMigrations());
16080
16113
  //#endregion
16114
+ //#region ../../packages/shared/src/toolCategory.ts
16115
+ const TOOL_CATEGORY_TITLES = {
16116
+ file_read: "File read",
16117
+ file_search: "File search",
16118
+ file_change: "File change",
16119
+ command: "Command run",
16120
+ version_control: "Version control",
16121
+ build_test: "Build or test",
16122
+ mcp_tool: "MCP tool call",
16123
+ subagent: "Subagent task",
16124
+ web_search: "Web search",
16125
+ web_fetch: "Web fetch",
16126
+ task_plan: "Task or plan",
16127
+ image_view: "Image view",
16128
+ tool: "Tool call"
16129
+ };
16130
+ new Set(Object.keys(TOOL_CATEGORY_TITLES));
16131
+ /** Exact tool names, lowercased, matched before the substring heuristics below. */
16132
+ const FILE_READ_TOOL_NAMES = /* @__PURE__ */ new Set([
16133
+ "read",
16134
+ "readfile",
16135
+ "read_file",
16136
+ "view",
16137
+ "viewfile",
16138
+ "view_file",
16139
+ "notebookread",
16140
+ "notebook_read",
16141
+ "openfile",
16142
+ "open_file"
16143
+ ]);
16144
+ const FILE_SEARCH_TOOL_NAMES = /* @__PURE__ */ new Set([
16145
+ "grep",
16146
+ "glob",
16147
+ "search",
16148
+ "filesearch",
16149
+ "file_search",
16150
+ "codebasesearch",
16151
+ "codebase_search",
16152
+ "listdir",
16153
+ "list_dir",
16154
+ "list_directory",
16155
+ "ls"
16156
+ ]);
16157
+ const FILE_CHANGE_TOOL_NAMES = /* @__PURE__ */ new Set([
16158
+ "edit",
16159
+ "write",
16160
+ "multiedit",
16161
+ "multi_edit",
16162
+ "notebookedit",
16163
+ "notebook_edit",
16164
+ "applypatch",
16165
+ "apply_patch",
16166
+ "patch",
16167
+ "createfile",
16168
+ "create_file",
16169
+ "strreplace",
16170
+ "str_replace",
16171
+ "deletefile",
16172
+ "delete_file"
16173
+ ]);
16174
+ const COMMAND_TOOL_NAMES = /* @__PURE__ */ new Set([
16175
+ "bash",
16176
+ "shell",
16177
+ "terminal",
16178
+ "exec",
16179
+ "execute",
16180
+ "execcommand",
16181
+ "exec_command",
16182
+ "runcommand",
16183
+ "run_command",
16184
+ "local_shell"
16185
+ ]);
16186
+ const SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
16187
+ "task",
16188
+ "agent",
16189
+ "subagent",
16190
+ "sub_agent"
16191
+ ]);
16192
+ const WEB_FETCH_TOOL_NAMES = /* @__PURE__ */ new Set([
16193
+ "webfetch",
16194
+ "web_fetch",
16195
+ "fetch",
16196
+ "fetchurl",
16197
+ "fetch_url",
16198
+ "browse",
16199
+ "openurl",
16200
+ "open_url"
16201
+ ]);
16202
+ /**
16203
+ * Matched before the subagent rules, so `task_create` is a board write while the
16204
+ * bare `Task` tool stays a subagent.
16205
+ */
16206
+ const TASK_PLAN_TOOL_NAMES = /* @__PURE__ */ new Set([
16207
+ "todowrite",
16208
+ "todo_write",
16209
+ "todoread",
16210
+ "todo_read",
16211
+ "exitplanmode",
16212
+ "exit_plan_mode",
16213
+ "updateplan",
16214
+ "update_plan",
16215
+ "task_create",
16216
+ "task_update",
16217
+ "task_list",
16218
+ "task_get",
16219
+ "task_current",
16220
+ "task_propose",
16221
+ "ticket_resolve",
16222
+ "taskcreate",
16223
+ "taskupdate",
16224
+ "tasklist",
16225
+ "taskget"
16226
+ ]);
16227
+ /** Shell builtins that say nothing about what the command as a whole does. */
16228
+ const NEUTRAL_SHELL_COMMANDS = /* @__PURE__ */ new Set([
16229
+ "cd",
16230
+ "echo",
16231
+ "printf",
16232
+ "pwd",
16233
+ "true",
16234
+ "false",
16235
+ "time"
16236
+ ]);
16237
+ /** `sed`/`awk` are stream editors (`sed -i` rewrites files), so they stay commands. */
16238
+ const FILE_READ_SHELL_COMMANDS = /* @__PURE__ */ new Set([
16239
+ "cat",
16240
+ "head",
16241
+ "tail",
16242
+ "less",
16243
+ "more",
16244
+ "bat",
16245
+ "nl",
16246
+ "jq",
16247
+ "wc"
16248
+ ]);
16249
+ const FILE_SEARCH_SHELL_COMMANDS = /* @__PURE__ */ new Set([
16250
+ "grep",
16251
+ "egrep",
16252
+ "fgrep",
16253
+ "rg",
16254
+ "ag",
16255
+ "ack",
16256
+ "find",
16257
+ "fd",
16258
+ "ls",
16259
+ "tree",
16260
+ "which"
16261
+ ]);
16262
+ /** `git status` and `git diff` sort here too: the verb is what a reader scans for. */
16263
+ const VERSION_CONTROL_SHELL_COMMANDS = /* @__PURE__ */ new Set([
16264
+ "git",
16265
+ "gh",
16266
+ "jj",
16267
+ "hg",
16268
+ "svn",
16269
+ "glab"
16270
+ ]);
16271
+ /**
16272
+ * Builds, tests, type checks, lint and install share one category: a test run is
16273
+ * what a person scans for when a turn goes long, and the rest is the same noise.
16274
+ */
16275
+ const BUILD_TEST_SHELL_COMMANDS = /* @__PURE__ */ new Set([
16276
+ "pnpm",
16277
+ "npm",
16278
+ "npx",
16279
+ "yarn",
16280
+ "bun",
16281
+ "bunx",
16282
+ "vitest",
16283
+ "jest",
16284
+ "vp",
16285
+ "tsc",
16286
+ "tsgo",
16287
+ "eslint",
16288
+ "oxlint",
16289
+ "biome",
16290
+ "prettier",
16291
+ "ruff",
16292
+ "pytest",
16293
+ "make",
16294
+ "cargo",
16295
+ "gradle",
16296
+ "mvn",
16297
+ "xcodebuild",
16298
+ "swift",
16299
+ "pip",
16300
+ "uv",
16301
+ "poetry"
16302
+ ]);
16303
+ const WEB_FETCH_SHELL_COMMANDS = /* @__PURE__ */ new Set([
16304
+ "curl",
16305
+ "wget",
16306
+ "http",
16307
+ "httpie"
16308
+ ]);
16309
+ const SHELL_SEGMENT_SEPARATOR = /\|\||&&|;|\||\n/u;
16310
+ const LEADING_ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/u;
16311
+ const REDIRECT_TARGET = /\d?>>?\s*(?<target>[^\s|;&]+)/gu;
16312
+ /** Redirect targets that discard output instead of writing a file. */
16313
+ const DISCARDED_REDIRECT_TARGETS = /* @__PURE__ */ new Set([
16314
+ "/dev/null",
16315
+ "&1",
16316
+ "&2"
16317
+ ]);
16318
+ function toolCategoryTitle(category) {
16319
+ return TOOL_CATEGORY_TITLES[category];
16320
+ }
16321
+ function readCommandInput(toolInput) {
16322
+ const raw = toolInput?.command ?? toolInput?.cmd;
16323
+ if (typeof raw !== "string") return;
16324
+ const trimmed = raw.trim();
16325
+ return trimmed.length > 0 ? trimmed : void 0;
16326
+ }
16327
+ /** `cat x > y` reads and writes; only the write matters for the row heading. */
16328
+ function redirectsToFile(segment) {
16329
+ for (const match of segment.matchAll(REDIRECT_TARGET)) {
16330
+ const target = match.groups?.target;
16331
+ if (target && !DISCARDED_REDIRECT_TARGETS.has(target)) return true;
16332
+ }
16333
+ return false;
16334
+ }
16335
+ function classifyShellSegment(segment) {
16336
+ if (redirectsToFile(segment)) return "command";
16337
+ const head = segment.trim().replace(LEADING_ENV_ASSIGNMENT, "").split(/\s+/u)[0]?.replace(/^.*\//u, "").toLowerCase();
16338
+ if (!head || NEUTRAL_SHELL_COMMANDS.has(head)) return;
16339
+ if (FILE_READ_SHELL_COMMANDS.has(head)) return "file_read";
16340
+ if (FILE_SEARCH_SHELL_COMMANDS.has(head)) return "file_search";
16341
+ if (VERSION_CONTROL_SHELL_COMMANDS.has(head)) return "version_control";
16342
+ if (BUILD_TEST_SHELL_COMMANDS.has(head)) return "build_test";
16343
+ if (WEB_FETCH_SHELL_COMMANDS.has(head)) return "web_fetch";
16344
+ return "command";
16345
+ }
16346
+ /**
16347
+ * A shell call only counts as anything narrower than a command when every
16348
+ * non-neutral segment agrees; anything unrecognized (or write-ish) keeps the
16349
+ * whole call a command, and so does a pipeline that mixes two categories — the
16350
+ * one exception being a read piped into a search, which is still a search.
16351
+ * Quoted separators split segments too, which can only downgrade to "command".
16352
+ */
16353
+ function classifyShellCommand(command) {
16354
+ const categories = new Set(command.split(SHELL_SEGMENT_SEPARATOR).map(classifyShellSegment).filter((category) => category !== void 0));
16355
+ if (categories.size === 0 || categories.has("command")) return "command";
16356
+ const [only] = categories;
16357
+ if (categories.size === 1 && only) return only;
16358
+ return [...categories].every((category) => category === "file_read" || category === "file_search") ? "file_search" : "command";
16359
+ }
16360
+ function classifyToolCategory(input) {
16361
+ const normalized = input.toolName.trim().toLowerCase();
16362
+ if (normalized.startsWith("mcp__") || normalized.includes("mcp")) return "mcp_tool";
16363
+ if (TASK_PLAN_TOOL_NAMES.has(normalized)) return "task_plan";
16364
+ if (WEB_FETCH_TOOL_NAMES.has(normalized)) return "web_fetch";
16365
+ if (SUBAGENT_TOOL_NAMES.has(normalized) || normalized.includes("agent")) return "subagent";
16366
+ if (FILE_READ_TOOL_NAMES.has(normalized)) return "file_read";
16367
+ if (FILE_SEARCH_TOOL_NAMES.has(normalized)) return "file_search";
16368
+ if (FILE_CHANGE_TOOL_NAMES.has(normalized)) return "file_change";
16369
+ if (COMMAND_TOOL_NAMES.has(normalized) || normalized.includes("bash") || normalized.includes("shell") || normalized.includes("terminal") || normalized.includes("command")) {
16370
+ const command = readCommandInput(input.toolInput);
16371
+ return command ? classifyShellCommand(command) : "command";
16372
+ }
16373
+ if (normalized.includes("websearch") || normalized.includes("web_search")) return "web_search";
16374
+ if (normalized.includes("webfetch") || normalized.includes("web_fetch")) return "web_fetch";
16375
+ if (normalized.includes("todo") || normalized.includes("plan")) return "task_plan";
16376
+ if (normalized.includes("edit") || normalized.includes("write") || normalized.includes("patch") || normalized.includes("replace") || normalized.includes("create") || normalized.includes("delete")) return "file_change";
16377
+ if (normalized.includes("read") || normalized.includes("view")) return "file_read";
16378
+ if (normalized.includes("grep") || normalized.includes("glob")) return "file_search";
16379
+ if (normalized.includes("image")) return "image_view";
16380
+ return "tool";
16381
+ }
16382
+ function asRecord$7(value) {
16383
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
16384
+ }
16385
+ /** Classify from a runtime item payload's `data` (`{ toolName, input }`). */
16386
+ function classifyToolCategoryFromToolData(data) {
16387
+ const record = asRecord$7(data);
16388
+ const toolName = record?.toolName;
16389
+ if (typeof toolName !== "string" || toolName.trim().length === 0) return;
16390
+ return classifyToolCategory({
16391
+ toolName,
16392
+ toolInput: asRecord$7(record?.input)
16393
+ });
16394
+ }
16395
+ //#endregion
16396
+ //#region src/orchestration/ActivityPayloadProjection.ts
16397
+ function asRecord$6(value) {
16398
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
16399
+ }
16400
+ function asTrimmedString$1(value) {
16401
+ if (typeof value !== "string") return null;
16402
+ const trimmed = value.trim();
16403
+ return trimmed.length > 0 ? trimmed : null;
16404
+ }
16405
+ function pushChangedFile(target, seen, value) {
16406
+ const normalized = asTrimmedString$1(value);
16407
+ if (!normalized || seen.has(normalized)) return;
16408
+ seen.add(normalized);
16409
+ target.push(normalized);
16410
+ }
16411
+ function collectChangedFiles(value, target, seen, depth) {
16412
+ if (depth > 4 || target.length >= 12) return;
16413
+ if (Array.isArray(value)) {
16414
+ for (const entry of value) {
16415
+ collectChangedFiles(entry, target, seen, depth + 1);
16416
+ if (target.length >= 12) return;
16417
+ }
16418
+ return;
16419
+ }
16420
+ const record = asRecord$6(value);
16421
+ if (!record) return;
16422
+ pushChangedFile(target, seen, record.path);
16423
+ pushChangedFile(target, seen, record.filePath);
16424
+ pushChangedFile(target, seen, record.relativePath);
16425
+ pushChangedFile(target, seen, record.filename);
16426
+ pushChangedFile(target, seen, record.newPath);
16427
+ pushChangedFile(target, seen, record.oldPath);
16428
+ for (const nestedKey of [
16429
+ "item",
16430
+ "result",
16431
+ "input",
16432
+ "data",
16433
+ "changes",
16434
+ "files",
16435
+ "edits",
16436
+ "patch",
16437
+ "patches",
16438
+ "operations"
16439
+ ]) {
16440
+ if (!(nestedKey in record)) continue;
16441
+ collectChangedFiles(record[nestedKey], target, seen, depth + 1);
16442
+ if (target.length >= 12) return;
16443
+ }
16444
+ }
16445
+ function projectCommandData(data) {
16446
+ const item = asRecord$6(data.item);
16447
+ if (!item) return;
16448
+ const projectedItem = {};
16449
+ if ("command" in item) projectedItem.command = item.command;
16450
+ const input = asRecord$6(item.input);
16451
+ if (input && "command" in input) projectedItem.input = { command: input.command };
16452
+ const result = asRecord$6(item.result);
16453
+ if (result && "command" in result) projectedItem.result = { command: result.command };
16454
+ return Object.keys(projectedItem).length > 0 ? projectedItem : void 0;
16455
+ }
16456
+ function summarizeToolTextOutput(value) {
16457
+ const lines = [];
16458
+ for (const rawLine of value.split(/\r?\n/u)) {
16459
+ const line = rawLine.replace(/\s+/g, " ").trim();
16460
+ if (line.length > 0) lines.push(line);
16461
+ }
16462
+ const firstLine = lines.find((line) => line !== "```");
16463
+ if (firstLine) return firstLine.length <= 84 ? firstLine : `${firstLine.slice(0, 83).trimEnd()}…`;
16464
+ if (lines.length > 1) return `${lines.length.toLocaleString()} lines`;
16465
+ return null;
16466
+ }
16467
+ function projectRawOutput(value) {
16468
+ const rawOutput = asRecord$6(value);
16469
+ if (!rawOutput) return;
16470
+ if (typeof rawOutput.totalFiles === "number" && Number.isFinite(rawOutput.totalFiles)) return {
16471
+ totalFiles: rawOutput.totalFiles,
16472
+ ...rawOutput.truncated === true ? { truncated: true } : {}
16473
+ };
16474
+ const content = asTrimmedString$1(rawOutput.content);
16475
+ if (content) {
16476
+ const summary = summarizeToolTextOutput(content);
16477
+ return summary ? { content: summary } : void 0;
16478
+ }
16479
+ const stdout = asTrimmedString$1(rawOutput.stdout);
16480
+ if (stdout) {
16481
+ const summary = summarizeToolTextOutput(stdout);
16482
+ return summary ? { content: summary } : void 0;
16483
+ }
16484
+ }
16485
+ /**
16486
+ * Removes activity payload fields that no current client or projector reads.
16487
+ * The same compact shape is used for persistence and transport so eligible
16488
+ * tool outputs do not accumulate twice in the event store and activity projection.
16489
+ */
16490
+ function projectActivityPayload(activity) {
16491
+ const payload = asRecord$6(activity.payload);
16492
+ const data = asRecord$6(payload?.data);
16493
+ if (!payload || !data || payload.itemType === "mcp_tool_call") return activity;
16494
+ const projectedData = {};
16495
+ const item = projectCommandData(data);
16496
+ if (item) projectedData.item = item;
16497
+ if ("command" in data) projectedData.command = data.command;
16498
+ const input = asRecord$6(data.input);
16499
+ if (input && "command" in input) projectedData.input = { command: input.command };
16500
+ const changedFiles = [];
16501
+ collectChangedFiles(data, changedFiles, /* @__PURE__ */ new Set(), 0);
16502
+ if (changedFiles.length > 0) projectedData.files = changedFiles.map((path) => ({ path }));
16503
+ if (asTrimmedString$1(data.patch)) projectedData.patch = data.patch;
16504
+ const toolCategory = classifyToolCategoryFromToolData(data);
16505
+ if (toolCategory) projectedData.toolCategory = toolCategory;
16506
+ if ("toolCallId" in data) projectedData.toolCallId = data.toolCallId;
16507
+ if ("kind" in data) projectedData.kind = data.kind;
16508
+ const rawOutput = projectRawOutput(data.rawOutput);
16509
+ if (rawOutput) projectedData.rawOutput = rawOutput;
16510
+ return {
16511
+ ...activity,
16512
+ payload: {
16513
+ ...payload,
16514
+ data: projectedData
16515
+ }
16516
+ };
16517
+ }
16518
+ /**
16519
+ * Matches the validity rule in the web client's
16520
+ * `deriveLatestContextWindowSnapshot`: rows without a finite, non-negative
16521
+ * `usedTokens` are skipped during its backward walk, so they must not shadow
16522
+ * an earlier resolvable row here.
16523
+ */
16524
+ function isResolvableContextWindowActivity(activity) {
16525
+ if (activity.kind !== "context-window.updated") return false;
16526
+ const usedTokens = asRecord$6(activity.payload)?.usedTokens;
16527
+ return typeof usedTokens === "number" && Number.isFinite(usedTokens) && usedTokens >= 0;
16528
+ }
16529
+ /**
16530
+ * Drops all but the last resolvable context-window activity per turn from a
16531
+ * snapshot. Clients only ever read the latest usage value (walking the array
16532
+ * backwards), so shipping the full history — often thousands of rows on long
16533
+ * threads — buys nothing. Retention is per turn rather than per thread because
16534
+ * a live `thread.reverted` makes the client discard whole turns; keeping each
16535
+ * turn's latest row means the meter can still resolve a value from the turns
16536
+ * that survive. Malformed rows pass through untouched rather than shadowing a
16537
+ * valid earlier row. Live `thread.activity-appended` events are untouched:
16538
+ * newer updates still stream through and supersede the retained rows on the
16539
+ * client.
16540
+ */
16541
+ function withoutContextWindowBreakdown$1(activity) {
16542
+ const payload = asRecord$6(activity.payload);
16543
+ if (!payload || payload.breakdown === void 0) return activity;
16544
+ const { breakdown: _breakdown, ...rest } = payload;
16545
+ return {
16546
+ ...activity,
16547
+ payload: rest
16548
+ };
16549
+ }
16550
+ function dropStaleContextWindowActivities(activities) {
16551
+ const latestIndexByTurn = /* @__PURE__ */ new Map();
16552
+ for (let index = 0; index < activities.length; index += 1) if (isResolvableContextWindowActivity(activities[index])) latestIndexByTurn.set(activities[index].turnId, index);
16553
+ if (latestIndexByTurn.size === 0) return activities;
16554
+ const retainedIndexes = new Set(latestIndexByTurn.values());
16555
+ let breakdownIndex = null;
16556
+ for (const index of retainedIndexes) {
16557
+ if (asRecord$6(activities[index].payload)?.breakdown === void 0) continue;
16558
+ if (breakdownIndex === null || index > breakdownIndex) breakdownIndex = index;
16559
+ }
16560
+ return activities.flatMap((activity, index) => {
16561
+ if (!isResolvableContextWindowActivity(activity)) return [activity];
16562
+ if (latestIndexByTurn.get(activity.turnId) !== index) return [];
16563
+ return [index === breakdownIndex ? activity : withoutContextWindowBreakdown$1(activity)];
16564
+ });
16565
+ }
16566
+ function projectThreadDetailSnapshot(snapshot) {
16567
+ return {
16568
+ ...snapshot,
16569
+ thread: {
16570
+ ...snapshot.thread,
16571
+ activities: dropStaleContextWindowActivities(snapshot.thread.activities).map(projectActivityPayload)
16572
+ }
16573
+ };
16574
+ }
16575
+ function projectActivityEvent(event) {
16576
+ if (event.type !== "thread.activity-appended") return event;
16577
+ return {
16578
+ ...event,
16579
+ payload: {
16580
+ ...event.payload,
16581
+ activity: projectActivityPayload(event.payload.activity)
16582
+ }
16583
+ };
16584
+ }
16585
+ const HISTORICAL_ACTIVITY_COMPACTION_BATCH_BYTE_LIMIT = 8 * 1024 * 1024;
16586
+ const JOB_NAME = "tool-activity-payload-v1";
16587
+ const SCHEMA_TRANSFORM_FAILURE_REASON = "schema-transform-failed";
16588
+ const decodeActivityEventPayload = Schema$1.decodeUnknownEffect(Schema$1.fromJsonString(ThreadActivityAppendedPayload$1));
16589
+ const encodeActivityEventPayload = Schema$1.encodeEffect(Schema$1.fromJsonString(ThreadActivityAppendedPayload$1));
16590
+ const encodeUnknownJson = Schema$1.encodeEffect(Schema$1.fromJsonString(Schema$1.Unknown));
16591
+ const runHistoricalActivityCompaction = Effect.fn("runHistoricalActivityCompaction")(function* () {
16592
+ const sql = yield* SqlClient.SqlClient;
16593
+ const progress = (yield* sql`
16594
+ SELECT
16595
+ cursor_sequence AS "cursorSequence",
16596
+ completed_at AS "completedAt"
16597
+ FROM historical_activity_compaction_progress
16598
+ WHERE job_name = ${JOB_NAME}
16599
+ `)[0];
16600
+ if (progress?.completedAt !== null && progress?.completedAt !== void 0) return {
16601
+ batches: 0,
16602
+ processedEvents: 0,
16603
+ skippedEvents: 0
16604
+ };
16605
+ let cursor = progress?.cursorSequence ?? 0;
16606
+ let batches = 0;
16607
+ let processedEvents = 0;
16608
+ let skippedEvents = 0;
16609
+ yield* sql`DROP TABLE IF EXISTS temp_historical_activity_compaction`;
16610
+ yield* sql`
16611
+ CREATE TEMP TABLE temp_historical_activity_compaction (
16612
+ sequence INTEGER PRIMARY KEY,
16613
+ activity_id TEXT NOT NULL,
16614
+ event_payload_json TEXT NOT NULL,
16615
+ activity_payload_json TEXT NOT NULL
16616
+ ) WITHOUT ROWID
16617
+ `;
16618
+ yield* sql`
16619
+ CREATE UNIQUE INDEX temp_historical_activity_compaction_activity_id
16620
+ ON temp_historical_activity_compaction(activity_id)
16621
+ `;
16622
+ while (true) {
16623
+ const rows = yield* sql`
16624
+ WITH candidate_rows AS (
16625
+ SELECT
16626
+ sequence,
16627
+ event_id,
16628
+ payload_json,
16629
+ length(CAST(payload_json AS BLOB)) AS payload_bytes
16630
+ FROM orchestration_events
16631
+ WHERE sequence > ${cursor}
16632
+ AND event_type = 'thread.activity-appended'
16633
+ AND CASE
16634
+ WHEN json_valid(payload_json) = 0 THEN 1
16635
+ WHEN json_type(payload_json, '$.activity.payload.data') = 'object'
16636
+ AND COALESCE(
16637
+ json_extract(payload_json, '$.activity.payload.itemType'),
16638
+ ''
16639
+ ) <> 'mcp_tool_call'
16640
+ THEN 1
16641
+ ELSE 0
16642
+ END = 1
16643
+ ORDER BY sequence ASC
16644
+ LIMIT ${256}
16645
+ ), bounded_rows AS (
16646
+ SELECT
16647
+ sequence,
16648
+ event_id,
16649
+ payload_json,
16650
+ payload_bytes,
16651
+ sum(payload_bytes) OVER (
16652
+ ORDER BY sequence ASC ROWS UNBOUNDED PRECEDING
16653
+ ) AS cumulative_bytes
16654
+ FROM candidate_rows
16655
+ )
16656
+ SELECT
16657
+ sequence,
16658
+ event_id AS "eventId",
16659
+ payload_json AS "payloadJson",
16660
+ payload_bytes AS "payloadBytes"
16661
+ FROM bounded_rows
16662
+ WHERE cumulative_bytes <= ${HISTORICAL_ACTIVITY_COMPACTION_BATCH_BYTE_LIMIT}
16663
+ OR sequence = (SELECT min(sequence) FROM bounded_rows)
16664
+ ORDER BY sequence ASC
16665
+ `;
16666
+ if (rows.length === 0) {
16667
+ yield* sql`
16668
+ INSERT INTO historical_activity_compaction_progress (
16669
+ job_name,
16670
+ cursor_sequence,
16671
+ completed_at
16672
+ ) VALUES (${JOB_NAME}, ${cursor}, datetime('now'))
16673
+ ON CONFLICT(job_name) DO UPDATE SET
16674
+ cursor_sequence = excluded.cursor_sequence,
16675
+ completed_at = excluded.completed_at
16676
+ `;
16677
+ break;
16678
+ }
16679
+ const projectedRows = yield* Effect.forEach(rows, (row) => Effect.gen(function* () {
16680
+ const payload = yield* decodeActivityEventPayload(row.payloadJson);
16681
+ const activity = projectActivityPayload(payload.activity);
16682
+ return {
16683
+ sequence: row.sequence,
16684
+ activity_id: activity.id,
16685
+ event_payload_json: yield* encodeActivityEventPayload({
16686
+ ...payload,
16687
+ activity
16688
+ }),
16689
+ activity_payload_json: yield* encodeUnknownJson(activity.payload)
16690
+ };
16691
+ }).pipe(Effect.match({
16692
+ onFailure: () => ({
16693
+ _tag: "Skipped",
16694
+ row: {
16695
+ sequence: row.sequence,
16696
+ event_id: row.eventId,
16697
+ reason: SCHEMA_TRANSFORM_FAILURE_REASON,
16698
+ payload_bytes: row.payloadBytes
16699
+ }
16700
+ }),
16701
+ onSuccess: (compactedRow) => ({
16702
+ _tag: "Compacted",
16703
+ row: compactedRow
16704
+ })
16705
+ })));
16706
+ const compactedRows = projectedRows.flatMap((result) => result._tag === "Compacted" ? [result.row] : []);
16707
+ const skippedRows = projectedRows.flatMap((result) => result._tag === "Skipped" ? [result.row] : []);
16708
+ const nextCursor = rows[rows.length - 1].sequence;
16709
+ yield* sql.withTransaction(Effect.gen(function* () {
16710
+ if (compactedRows.length > 0) {
16711
+ yield* sql`
16712
+ INSERT INTO temp_historical_activity_compaction ${sql.insert(compactedRows)}
16713
+ `;
16714
+ yield* sql`
16715
+ UPDATE orchestration_events
16716
+ SET payload_json = (
16717
+ SELECT compact.event_payload_json
16718
+ FROM temp_historical_activity_compaction AS compact
16719
+ WHERE compact.sequence = orchestration_events.sequence
16720
+ )
16721
+ WHERE sequence IN (
16722
+ SELECT sequence FROM temp_historical_activity_compaction
16723
+ )
16724
+ `;
16725
+ yield* sql`
16726
+ UPDATE projection_thread_activities
16727
+ SET payload_json = (
16728
+ SELECT compact.activity_payload_json
16729
+ FROM temp_historical_activity_compaction AS compact
16730
+ WHERE compact.activity_id = projection_thread_activities.activity_id
16731
+ )
16732
+ WHERE activity_id IN (
16733
+ SELECT activity_id FROM temp_historical_activity_compaction
16734
+ )
16735
+ `;
16736
+ }
16737
+ if (skippedRows.length > 0) yield* sql`
16738
+ INSERT INTO historical_activity_compaction_skips ${sql.insert(skippedRows)}
16739
+ ON CONFLICT(sequence) DO NOTHING
16740
+ `;
16741
+ yield* sql`
16742
+ INSERT INTO historical_activity_compaction_progress (
16743
+ job_name,
16744
+ cursor_sequence,
16745
+ completed_at
16746
+ ) VALUES (${JOB_NAME}, ${nextCursor}, NULL)
16747
+ ON CONFLICT(job_name) DO UPDATE SET
16748
+ cursor_sequence = excluded.cursor_sequence,
16749
+ completed_at = NULL
16750
+ `;
16751
+ yield* sql`DELETE FROM temp_historical_activity_compaction`;
16752
+ }));
16753
+ if (skippedRows.length > 0) yield* Effect.logWarning("Skipped malformed historical tool activities").pipe(Effect.annotateLogs({
16754
+ skippedCount: skippedRows.length,
16755
+ firstSkippedSequence: skippedRows[0].sequence,
16756
+ lastSkippedSequence: skippedRows[skippedRows.length - 1].sequence
16757
+ }));
16758
+ yield* sql`PRAGMA wal_checkpoint(TRUNCATE)`;
16759
+ cursor = nextCursor;
16760
+ batches += 1;
16761
+ processedEvents += rows.length;
16762
+ skippedEvents += skippedRows.length;
16763
+ }
16764
+ yield* sql`DROP TABLE temp_historical_activity_compaction`;
16765
+ yield* sql`PRAGMA wal_checkpoint(TRUNCATE)`;
16766
+ return {
16767
+ batches,
16768
+ processedEvents,
16769
+ skippedEvents
16770
+ };
16771
+ });
16772
+ //#endregion
16081
16773
  //#region src/persistence/Layers/Sqlite.ts
16082
16774
  const defaultSqliteClientLoaders = {
16083
16775
  bun: () => import("@effect/sql-sqlite-bun/SqliteClient"),
@@ -16093,7 +16785,7 @@ const applyPragmas = Effect.gen(function* () {
16093
16785
  yield* sql`PRAGMA journal_mode = WAL;`;
16094
16786
  yield* sql`PRAGMA foreign_keys = ON;`;
16095
16787
  });
16096
- const setupFile = (dbPath) => Layer.effectDiscard(applyPragmas.pipe(Effect.andThen(runGuardedMigrations({ dbPath }))));
16788
+ const setupFile = (dbPath) => Layer.effectDiscard(applyPragmas.pipe(Effect.andThen(runGuardedMigrations({ dbPath })), Effect.andThen(runHistoricalActivityCompaction())));
16097
16789
  /**
16098
16790
  * The in-memory database used by tests has no file to snapshot and nothing that
16099
16791
  * survives the process to restore.
@@ -16102,7 +16794,7 @@ const setupFile = (dbPath) => Layer.effectDiscard(applyPragmas.pipe(Effect.andTh
16102
16794
  * needing nothing but `SqlClient`: sharing one function would put the file
16103
16795
  * path's `FileSystem | Path` requirement into every test layer built on it.
16104
16796
  */
16105
- const setupMemory = Layer.effectDiscard(applyPragmas.pipe(Effect.andThen(runMigrations())));
16797
+ const setupMemory = Layer.effectDiscard(applyPragmas.pipe(Effect.andThen(runMigrations()), Effect.andThen(runHistoricalActivityCompaction())));
16106
16798
  const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")(function* (dbPath) {
16107
16799
  const fs = yield* FileSystem.FileSystem;
16108
16800
  const path = yield* Path.Path;
@@ -22009,14 +22701,12 @@ const MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md"];
22009
22701
  */
22010
22702
  const isMemoryFileName = (name) => MEMORY_FILE_NAMES.includes(name);
22011
22703
  /**
22012
- * The directories in the config directory this sync will carry whole.
22704
+ * Directory asset this sync carries whole. `agent-memory` maps to the
22705
+ * provider-independent `~/.agent-memory` directory.
22013
22706
  *
22014
- * A memory file is rarely the whole of the memory. `CLAUDE.md` is written to be
22015
- * an index — a dozen `@~/.claude/global-memory/...` lines and little else so
22016
- * syncing the file alone hands the other machine a list of pointers to files
22017
- * that are not there, and an agent reading it behaves as if the rules it names
22018
- * do not exist. That failure is silent: the includes resolve to nothing rather
22019
- * than to an error.
22707
+ * A memory file is rarely the whole of the memory. Shared manual memories now
22708
+ * live under `~/.agent-memory`. Syncing only the entry-point file would leave
22709
+ * its referenced files missing on another machine.
22020
22710
  *
22021
22711
  * So this is an allowlist too, and for the same reason the file one exists. The
22022
22712
  * escape it prevents is different, though, and weaker: `AgentAssetFilePath`
@@ -22026,7 +22716,7 @@ const isMemoryFileName = (name) => MEMORY_FILE_NAMES.includes(name);
22026
22716
  * asset is not permitted to claim `.claude/projects` or any other directory
22027
22717
  * that already means something to the provider.
22028
22718
  */
22029
- const MEMORY_DIRECTORY_NAMES = ["global-memory"];
22719
+ const MEMORY_DIRECTORY_NAMES = ["agent-memory"];
22030
22720
  /** Whether this name is one of the memory directories. Exact match, as above. */
22031
22721
  const isMemoryDirectoryName = (name) => MEMORY_DIRECTORY_NAMES.includes(name);
22032
22722
  /**
@@ -22046,7 +22736,7 @@ const looksBinary$1 = (content) => content.includes("\0");
22046
22736
  * most machines have a `CLAUDE.md` and no `AGENTS.md`, and reporting the second
22047
22737
  * as a problem would be reporting the normal case.
22048
22738
  */
22049
- const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memoryRoot) {
22739
+ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memoryRoot, sharedMemoryRoot) {
22050
22740
  const fileSystem = yield* FileSystem.FileSystem;
22051
22741
  const path = yield* Path.Path;
22052
22742
  const skills = [];
@@ -22093,7 +22783,8 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
22093
22783
  });
22094
22784
  }
22095
22785
  for (const name of MEMORY_DIRECTORY_NAMES) {
22096
- const absolute = path.join(memoryRoot, name);
22786
+ if (name === "agent-memory" && !sharedMemoryRoot) continue;
22787
+ const absolute = name === "agent-memory" && sharedMemoryRoot ? sharedMemoryRoot : path.join(memoryRoot, name);
22097
22788
  if ((yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "Directory") continue;
22098
22789
  const read = yield* readSkillFiles(absolute);
22099
22790
  if ("refusal" in read) {
@@ -22136,12 +22827,18 @@ const isWritableMemoryAsset = (input) => {
22136
22827
  * missing, which an agent will follow without noticing.
22137
22828
  */
22138
22829
  const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input) {
22830
+ if (input.name === "agent-memory" && !input.sharedMemoryRoot) return "refused";
22139
22831
  if (!isWritableMemoryAsset(input)) return "refused";
22140
22832
  const fileSystem = yield* FileSystem.FileSystem;
22141
22833
  const path = yield* Path.Path;
22142
22834
  if (isMemoryDirectoryName(input.name)) return yield* writeSkillDirectory({
22143
- skillsRoot: input.memoryRoot,
22144
- name: input.name,
22835
+ ...input.name === "agent-memory" && input.sharedMemoryRoot ? {
22836
+ skillsRoot: path.dirname(input.sharedMemoryRoot),
22837
+ name: path.basename(input.sharedMemoryRoot)
22838
+ } : {
22839
+ skillsRoot: input.memoryRoot,
22840
+ name: input.name
22841
+ },
22145
22842
  files: input.files
22146
22843
  }).pipe(Effect.as("written"), Effect.orElseSucceed(() => "failed"));
22147
22844
  const contents = input.files[0]?.content ?? "";
@@ -22153,8 +22850,13 @@ const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input
22153
22850
  });
22154
22851
  const removeMemoryFile = Effect.fn("memoryFile.removeMemoryFile")(function* (input) {
22155
22852
  if (!isMemoryAssetName(input.name)) return;
22853
+ if (input.name === "agent-memory" && !input.sharedMemoryRoot) return;
22156
22854
  if (isMemoryDirectoryName(input.name)) {
22157
- yield* removeSkillDirectory({
22855
+ const path = yield* Path.Path;
22856
+ yield* removeSkillDirectory(input.name === "agent-memory" && input.sharedMemoryRoot ? {
22857
+ skillsRoot: path.dirname(input.sharedMemoryRoot),
22858
+ name: path.basename(input.sharedMemoryRoot)
22859
+ } : {
22158
22860
  skillsRoot: input.memoryRoot,
22159
22861
  name: input.name
22160
22862
  });
@@ -22427,6 +23129,27 @@ function planSkillSync(input) {
22427
23129
  }
22428
23130
  //#endregion
22429
23131
  //#region src/sync/AssetSync.ts
23132
+ /**
23133
+ * Syncing user-scope agent assets between this machine and the hub.
23134
+ *
23135
+ * Bidirectional between servers, one-way onto the filesystem: an edit made on
23136
+ * either machine propagates through the hub, and the hub is the only thing that
23137
+ * writes a managed asset. There is deliberately **no filesystem watcher**. The
23138
+ * sync reads only what its manifest names, and the reason is concrete rather
23139
+ * than stylistic — the provider's config directory is where the CLI keeps
23140
+ * `.credentials.json`, and anything that mirrored the tree upward would carry a
23141
+ * provider credential to the hub. Provider credentials never sync. See
23142
+ * `docs/architecture/hub.md`.
23143
+ *
23144
+ * **Kinds are drivers, not branches.** Each kind answers four questions — where
23145
+ * do I live, what is here, write this, remove this — and everything above that
23146
+ * is shared. Skills are a directory per asset; memory is a single allowlisted
23147
+ * file in the config directory itself. Writing the second kind as a parallel
23148
+ * service would have doubled the state table, which is the one part of this that
23149
+ * is genuinely hard to get right.
23150
+ *
23151
+ * @module sync/AssetSync
23152
+ */
22430
23153
  const EMPTY_REPORT = {
22431
23154
  ran: false,
22432
23155
  roots: [],
@@ -22445,6 +23168,7 @@ const make$75 = Effect.gen(function* () {
22445
23168
  const settingsStore = yield* ServerSettingsService;
22446
23169
  const config = yield* ServerConfig$1;
22447
23170
  const path = yield* Path.Path;
23171
+ const sharedMemoryRoot = path.join(NodeOS.homedir(), ".agent-memory");
22448
23172
  /**
22449
23173
  * Captured here rather than left in each method's requirements, so the
22450
23174
  * service's own type says what it really is: an effect that needs nothing and
@@ -22482,14 +23206,16 @@ const make$75 = Effect.gen(function* () {
22482
23206
  {
22483
23207
  kind: "memory",
22484
23208
  resolveRoot: claudeHome().pipe(Effect.flatMap((config) => resolveClaudeConfigDirPath(config, process.env))),
22485
- read: (root) => readMemoryFiles(root),
23209
+ read: (root) => readMemoryFiles(root, sharedMemoryRoot),
22486
23210
  write: ({ root, name, files }) => writeMemoryFile({
22487
23211
  memoryRoot: root,
23212
+ sharedMemoryRoot,
22488
23213
  name,
22489
23214
  files
22490
23215
  }).pipe(Effect.map((outcome) => outcome)),
22491
23216
  remove: ({ root, name }) => removeMemoryFile({
22492
23217
  memoryRoot: root,
23218
+ sharedMemoryRoot,
22493
23219
  name
22494
23220
  }).pipe(Effect.ignore)
22495
23221
  },
@@ -24208,6 +24934,9 @@ const ReadFromSequenceOfTypesRequestSchema = Schema$1.Struct({
24208
24934
  const ReadByCommandIdRequestSchema = Schema$1.Struct({ commandId: CommandId });
24209
24935
  const DEFAULT_READ_FROM_SEQUENCE_LIMIT = 1e3;
24210
24936
  const READ_PAGE_SIZE = 500;
24937
+ function isThreadActivityAppendedEvent(event) {
24938
+ return event.type === "thread.activity-appended";
24939
+ }
24211
24940
  function inferActorKind(event) {
24212
24941
  if (event.commandId !== null && event.commandId.startsWith("provider:")) return "provider";
24213
24942
  if (event.commandId !== null && event.commandId.startsWith("server:")) return "server";
@@ -24342,19 +25071,28 @@ const makeEventStore = Effect.gen(function* () {
24342
25071
  ORDER BY sequence ASC
24343
25072
  `
24344
25073
  });
24345
- const append = (event) => appendEventRow({
24346
- eventId: event.eventId,
24347
- aggregateKind: event.aggregateKind,
24348
- streamId: event.aggregateId,
24349
- type: event.type,
24350
- causationEventId: event.causationEventId,
24351
- correlationId: event.correlationId,
24352
- actorKind: inferActorKind(event),
24353
- occurredAt: event.occurredAt,
24354
- commandId: event.commandId,
24355
- payloadJson: event.payload,
24356
- metadataJson: event.metadata
24357
- }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$4("OrchestrationEventStore.append:insert", "OrchestrationEventStore.append:decodeRow")), Effect.flatMap((row) => decodeEvent(row).pipe(Effect.mapError(toPersistenceDecodeError("OrchestrationEventStore.append:rowToEvent")))));
25074
+ const append = (event) => {
25075
+ const payload = isThreadActivityAppendedEvent(event) ? {
25076
+ ...event.payload,
25077
+ activity: projectActivityPayload(event.payload.activity)
25078
+ } : event.payload;
25079
+ return appendEventRow({
25080
+ eventId: event.eventId,
25081
+ aggregateKind: event.aggregateKind,
25082
+ streamId: event.aggregateId,
25083
+ type: event.type,
25084
+ causationEventId: event.causationEventId,
25085
+ correlationId: event.correlationId,
25086
+ actorKind: inferActorKind(event),
25087
+ occurredAt: event.occurredAt,
25088
+ commandId: event.commandId,
25089
+ payloadJson: payload,
25090
+ metadataJson: event.metadata
25091
+ }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$4("OrchestrationEventStore.append:insert", "OrchestrationEventStore.append:decodeRow")), Effect.flatMap((row) => decodeEvent({
25092
+ ...event,
25093
+ sequence: row.sequence
25094
+ }).pipe(Effect.mapError(toPersistenceDecodeError("OrchestrationEventStore.append:returnedEvent")))));
25095
+ };
24358
25096
  const readFromSequence = (sequenceExclusive, limit = DEFAULT_READ_FROM_SEQUENCE_LIMIT, options) => {
24359
25097
  const normalizedLimit = Math.max(0, Math.floor(limit));
24360
25098
  if (normalizedLimit === 0) return Stream.empty;
@@ -28841,16 +29579,17 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
28841
29579
  const applyThreadActivitiesProjection = Effect.fn("applyThreadActivitiesProjection")(function* (event, _attachmentSideEffects) {
28842
29580
  switch (event.type) {
28843
29581
  case "thread.activity-appended":
29582
+ const activity = projectActivityPayload(event.payload.activity);
28844
29583
  yield* projectionThreadActivityRepository.upsert({
28845
- activityId: event.payload.activity.id,
29584
+ activityId: activity.id,
28846
29585
  threadId: event.payload.threadId,
28847
- turnId: event.payload.activity.turnId,
28848
- tone: event.payload.activity.tone,
28849
- kind: event.payload.activity.kind,
28850
- summary: event.payload.activity.summary,
28851
- payload: event.payload.activity.payload,
28852
- ...event.payload.activity.sequence !== void 0 ? { sequence: event.payload.activity.sequence } : {},
28853
- createdAt: event.payload.activity.createdAt
29586
+ turnId: activity.turnId,
29587
+ tone: activity.tone,
29588
+ kind: activity.kind,
29589
+ summary: activity.summary,
29590
+ payload: activity.payload,
29591
+ ...activity.sequence !== void 0 ? { sequence: activity.sequence } : {},
29592
+ createdAt: activity.createdAt
28854
29593
  });
28855
29594
  return;
28856
29595
  case "thread.reverted": {
@@ -37317,7 +38056,7 @@ const NESTED_PAYLOAD_KEYS = [
37317
38056
  "operations"
37318
38057
  ];
37319
38058
  const MAX_COLLECT_DEPTH = 4;
37320
- function asRecord$7(value) {
38059
+ function asRecord$5(value) {
37321
38060
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
37322
38061
  }
37323
38062
  function pushChangedFilePath(target, value) {
@@ -37332,7 +38071,7 @@ function collectChangedFilePaths(value, target, depth) {
37332
38071
  for (const entry of value) collectChangedFilePaths(entry, target, depth + 1);
37333
38072
  return;
37334
38073
  }
37335
- const record = asRecord$7(value);
38074
+ const record = asRecord$5(value);
37336
38075
  if (!record) return;
37337
38076
  for (const field of CHANGED_FILE_FIELDS) pushChangedFilePath(target, record[field]);
37338
38077
  for (const nestedKey of NESTED_PAYLOAD_KEYS) if (nestedKey in record) collectChangedFilePaths(record[nestedKey], target, depth + 1);
@@ -37343,7 +38082,7 @@ function collectChangedFilePaths(value, target, depth) {
37343
38082
  */
37344
38083
  function collectActivityChangedFilePaths(payload) {
37345
38084
  const target = /* @__PURE__ */ new Set();
37346
- collectChangedFilePaths(asRecord$7(asRecord$7(payload)?.data), target, 0);
38085
+ collectChangedFilePaths(asRecord$5(asRecord$5(payload)?.data), target, 0);
37347
38086
  return target;
37348
38087
  }
37349
38088
  /**
@@ -40179,477 +40918,6 @@ const make$54 = Effect.gen(function* () {
40179
40918
  });
40180
40919
  const layer$44 = Layer.effect(CheckpointDiffQuery, make$54);
40181
40920
  //#endregion
40182
- //#region ../../packages/shared/src/toolCategory.ts
40183
- const TOOL_CATEGORY_TITLES = {
40184
- file_read: "File read",
40185
- file_search: "File search",
40186
- file_change: "File change",
40187
- command: "Command run",
40188
- version_control: "Version control",
40189
- build_test: "Build or test",
40190
- mcp_tool: "MCP tool call",
40191
- subagent: "Subagent task",
40192
- web_search: "Web search",
40193
- web_fetch: "Web fetch",
40194
- task_plan: "Task or plan",
40195
- image_view: "Image view",
40196
- tool: "Tool call"
40197
- };
40198
- new Set(Object.keys(TOOL_CATEGORY_TITLES));
40199
- /** Exact tool names, lowercased, matched before the substring heuristics below. */
40200
- const FILE_READ_TOOL_NAMES = /* @__PURE__ */ new Set([
40201
- "read",
40202
- "readfile",
40203
- "read_file",
40204
- "view",
40205
- "viewfile",
40206
- "view_file",
40207
- "notebookread",
40208
- "notebook_read",
40209
- "openfile",
40210
- "open_file"
40211
- ]);
40212
- const FILE_SEARCH_TOOL_NAMES = /* @__PURE__ */ new Set([
40213
- "grep",
40214
- "glob",
40215
- "search",
40216
- "filesearch",
40217
- "file_search",
40218
- "codebasesearch",
40219
- "codebase_search",
40220
- "listdir",
40221
- "list_dir",
40222
- "list_directory",
40223
- "ls"
40224
- ]);
40225
- const FILE_CHANGE_TOOL_NAMES = /* @__PURE__ */ new Set([
40226
- "edit",
40227
- "write",
40228
- "multiedit",
40229
- "multi_edit",
40230
- "notebookedit",
40231
- "notebook_edit",
40232
- "applypatch",
40233
- "apply_patch",
40234
- "patch",
40235
- "createfile",
40236
- "create_file",
40237
- "strreplace",
40238
- "str_replace",
40239
- "deletefile",
40240
- "delete_file"
40241
- ]);
40242
- const COMMAND_TOOL_NAMES = /* @__PURE__ */ new Set([
40243
- "bash",
40244
- "shell",
40245
- "terminal",
40246
- "exec",
40247
- "execute",
40248
- "execcommand",
40249
- "exec_command",
40250
- "runcommand",
40251
- "run_command",
40252
- "local_shell"
40253
- ]);
40254
- const SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
40255
- "task",
40256
- "agent",
40257
- "subagent",
40258
- "sub_agent"
40259
- ]);
40260
- const WEB_FETCH_TOOL_NAMES = /* @__PURE__ */ new Set([
40261
- "webfetch",
40262
- "web_fetch",
40263
- "fetch",
40264
- "fetchurl",
40265
- "fetch_url",
40266
- "browse",
40267
- "openurl",
40268
- "open_url"
40269
- ]);
40270
- /**
40271
- * Matched before the subagent rules, so `task_create` is a board write while the
40272
- * bare `Task` tool stays a subagent.
40273
- */
40274
- const TASK_PLAN_TOOL_NAMES = /* @__PURE__ */ new Set([
40275
- "todowrite",
40276
- "todo_write",
40277
- "todoread",
40278
- "todo_read",
40279
- "exitplanmode",
40280
- "exit_plan_mode",
40281
- "updateplan",
40282
- "update_plan",
40283
- "task_create",
40284
- "task_update",
40285
- "task_list",
40286
- "task_get",
40287
- "task_current",
40288
- "task_propose",
40289
- "ticket_resolve",
40290
- "taskcreate",
40291
- "taskupdate",
40292
- "tasklist",
40293
- "taskget"
40294
- ]);
40295
- /** Shell builtins that say nothing about what the command as a whole does. */
40296
- const NEUTRAL_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40297
- "cd",
40298
- "echo",
40299
- "printf",
40300
- "pwd",
40301
- "true",
40302
- "false",
40303
- "time"
40304
- ]);
40305
- /** `sed`/`awk` are stream editors (`sed -i` rewrites files), so they stay commands. */
40306
- const FILE_READ_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40307
- "cat",
40308
- "head",
40309
- "tail",
40310
- "less",
40311
- "more",
40312
- "bat",
40313
- "nl",
40314
- "jq",
40315
- "wc"
40316
- ]);
40317
- const FILE_SEARCH_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40318
- "grep",
40319
- "egrep",
40320
- "fgrep",
40321
- "rg",
40322
- "ag",
40323
- "ack",
40324
- "find",
40325
- "fd",
40326
- "ls",
40327
- "tree",
40328
- "which"
40329
- ]);
40330
- /** `git status` and `git diff` sort here too: the verb is what a reader scans for. */
40331
- const VERSION_CONTROL_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40332
- "git",
40333
- "gh",
40334
- "jj",
40335
- "hg",
40336
- "svn",
40337
- "glab"
40338
- ]);
40339
- /**
40340
- * Builds, tests, type checks, lint and install share one category: a test run is
40341
- * what a person scans for when a turn goes long, and the rest is the same noise.
40342
- */
40343
- const BUILD_TEST_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40344
- "pnpm",
40345
- "npm",
40346
- "npx",
40347
- "yarn",
40348
- "bun",
40349
- "bunx",
40350
- "vitest",
40351
- "jest",
40352
- "vp",
40353
- "tsc",
40354
- "tsgo",
40355
- "eslint",
40356
- "oxlint",
40357
- "biome",
40358
- "prettier",
40359
- "ruff",
40360
- "pytest",
40361
- "make",
40362
- "cargo",
40363
- "gradle",
40364
- "mvn",
40365
- "xcodebuild",
40366
- "swift",
40367
- "pip",
40368
- "uv",
40369
- "poetry"
40370
- ]);
40371
- const WEB_FETCH_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40372
- "curl",
40373
- "wget",
40374
- "http",
40375
- "httpie"
40376
- ]);
40377
- const SHELL_SEGMENT_SEPARATOR = /\|\||&&|;|\||\n/u;
40378
- const LEADING_ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/u;
40379
- const REDIRECT_TARGET = /\d?>>?\s*(?<target>[^\s|;&]+)/gu;
40380
- /** Redirect targets that discard output instead of writing a file. */
40381
- const DISCARDED_REDIRECT_TARGETS = /* @__PURE__ */ new Set([
40382
- "/dev/null",
40383
- "&1",
40384
- "&2"
40385
- ]);
40386
- function toolCategoryTitle(category) {
40387
- return TOOL_CATEGORY_TITLES[category];
40388
- }
40389
- function readCommandInput(toolInput) {
40390
- const raw = toolInput?.command ?? toolInput?.cmd;
40391
- if (typeof raw !== "string") return;
40392
- const trimmed = raw.trim();
40393
- return trimmed.length > 0 ? trimmed : void 0;
40394
- }
40395
- /** `cat x > y` reads and writes; only the write matters for the row heading. */
40396
- function redirectsToFile(segment) {
40397
- for (const match of segment.matchAll(REDIRECT_TARGET)) {
40398
- const target = match.groups?.target;
40399
- if (target && !DISCARDED_REDIRECT_TARGETS.has(target)) return true;
40400
- }
40401
- return false;
40402
- }
40403
- function classifyShellSegment(segment) {
40404
- if (redirectsToFile(segment)) return "command";
40405
- const head = segment.trim().replace(LEADING_ENV_ASSIGNMENT, "").split(/\s+/u)[0]?.replace(/^.*\//u, "").toLowerCase();
40406
- if (!head || NEUTRAL_SHELL_COMMANDS.has(head)) return;
40407
- if (FILE_READ_SHELL_COMMANDS.has(head)) return "file_read";
40408
- if (FILE_SEARCH_SHELL_COMMANDS.has(head)) return "file_search";
40409
- if (VERSION_CONTROL_SHELL_COMMANDS.has(head)) return "version_control";
40410
- if (BUILD_TEST_SHELL_COMMANDS.has(head)) return "build_test";
40411
- if (WEB_FETCH_SHELL_COMMANDS.has(head)) return "web_fetch";
40412
- return "command";
40413
- }
40414
- /**
40415
- * A shell call only counts as anything narrower than a command when every
40416
- * non-neutral segment agrees; anything unrecognized (or write-ish) keeps the
40417
- * whole call a command, and so does a pipeline that mixes two categories — the
40418
- * one exception being a read piped into a search, which is still a search.
40419
- * Quoted separators split segments too, which can only downgrade to "command".
40420
- */
40421
- function classifyShellCommand(command) {
40422
- const categories = new Set(command.split(SHELL_SEGMENT_SEPARATOR).map(classifyShellSegment).filter((category) => category !== void 0));
40423
- if (categories.size === 0 || categories.has("command")) return "command";
40424
- const [only] = categories;
40425
- if (categories.size === 1 && only) return only;
40426
- return [...categories].every((category) => category === "file_read" || category === "file_search") ? "file_search" : "command";
40427
- }
40428
- function classifyToolCategory(input) {
40429
- const normalized = input.toolName.trim().toLowerCase();
40430
- if (normalized.startsWith("mcp__") || normalized.includes("mcp")) return "mcp_tool";
40431
- if (TASK_PLAN_TOOL_NAMES.has(normalized)) return "task_plan";
40432
- if (WEB_FETCH_TOOL_NAMES.has(normalized)) return "web_fetch";
40433
- if (SUBAGENT_TOOL_NAMES.has(normalized) || normalized.includes("agent")) return "subagent";
40434
- if (FILE_READ_TOOL_NAMES.has(normalized)) return "file_read";
40435
- if (FILE_SEARCH_TOOL_NAMES.has(normalized)) return "file_search";
40436
- if (FILE_CHANGE_TOOL_NAMES.has(normalized)) return "file_change";
40437
- if (COMMAND_TOOL_NAMES.has(normalized) || normalized.includes("bash") || normalized.includes("shell") || normalized.includes("terminal") || normalized.includes("command")) {
40438
- const command = readCommandInput(input.toolInput);
40439
- return command ? classifyShellCommand(command) : "command";
40440
- }
40441
- if (normalized.includes("websearch") || normalized.includes("web_search")) return "web_search";
40442
- if (normalized.includes("webfetch") || normalized.includes("web_fetch")) return "web_fetch";
40443
- if (normalized.includes("todo") || normalized.includes("plan")) return "task_plan";
40444
- if (normalized.includes("edit") || normalized.includes("write") || normalized.includes("patch") || normalized.includes("replace") || normalized.includes("create") || normalized.includes("delete")) return "file_change";
40445
- if (normalized.includes("read") || normalized.includes("view")) return "file_read";
40446
- if (normalized.includes("grep") || normalized.includes("glob")) return "file_search";
40447
- if (normalized.includes("image")) return "image_view";
40448
- return "tool";
40449
- }
40450
- function asRecord$6(value) {
40451
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
40452
- }
40453
- /** Classify from a runtime item payload's `data` (`{ toolName, input }`). */
40454
- function classifyToolCategoryFromToolData(data) {
40455
- const record = asRecord$6(data);
40456
- const toolName = record?.toolName;
40457
- if (typeof toolName !== "string" || toolName.trim().length === 0) return;
40458
- return classifyToolCategory({
40459
- toolName,
40460
- toolInput: asRecord$6(record?.input)
40461
- });
40462
- }
40463
- //#endregion
40464
- //#region src/orchestration/ActivityPayloadProjection.ts
40465
- function asRecord$5(value) {
40466
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
40467
- }
40468
- function asTrimmedString$1(value) {
40469
- if (typeof value !== "string") return null;
40470
- const trimmed = value.trim();
40471
- return trimmed.length > 0 ? trimmed : null;
40472
- }
40473
- function pushChangedFile(target, seen, value) {
40474
- const normalized = asTrimmedString$1(value);
40475
- if (!normalized || seen.has(normalized)) return;
40476
- seen.add(normalized);
40477
- target.push(normalized);
40478
- }
40479
- function collectChangedFiles(value, target, seen, depth) {
40480
- if (depth > 4 || target.length >= 12) return;
40481
- if (Array.isArray(value)) {
40482
- for (const entry of value) {
40483
- collectChangedFiles(entry, target, seen, depth + 1);
40484
- if (target.length >= 12) return;
40485
- }
40486
- return;
40487
- }
40488
- const record = asRecord$5(value);
40489
- if (!record) return;
40490
- pushChangedFile(target, seen, record.path);
40491
- pushChangedFile(target, seen, record.filePath);
40492
- pushChangedFile(target, seen, record.relativePath);
40493
- pushChangedFile(target, seen, record.filename);
40494
- pushChangedFile(target, seen, record.newPath);
40495
- pushChangedFile(target, seen, record.oldPath);
40496
- for (const nestedKey of [
40497
- "item",
40498
- "result",
40499
- "input",
40500
- "data",
40501
- "changes",
40502
- "files",
40503
- "edits",
40504
- "patch",
40505
- "patches",
40506
- "operations"
40507
- ]) {
40508
- if (!(nestedKey in record)) continue;
40509
- collectChangedFiles(record[nestedKey], target, seen, depth + 1);
40510
- if (target.length >= 12) return;
40511
- }
40512
- }
40513
- function projectCommandData(data) {
40514
- const item = asRecord$5(data.item);
40515
- if (!item) return;
40516
- const projectedItem = {};
40517
- if ("command" in item) projectedItem.command = item.command;
40518
- const input = asRecord$5(item.input);
40519
- if (input && "command" in input) projectedItem.input = { command: input.command };
40520
- const result = asRecord$5(item.result);
40521
- if (result && "command" in result) projectedItem.result = { command: result.command };
40522
- return Object.keys(projectedItem).length > 0 ? projectedItem : void 0;
40523
- }
40524
- function summarizeToolTextOutput(value) {
40525
- const lines = [];
40526
- for (const rawLine of value.split(/\r?\n/u)) {
40527
- const line = rawLine.replace(/\s+/g, " ").trim();
40528
- if (line.length > 0) lines.push(line);
40529
- }
40530
- const firstLine = lines.find((line) => line !== "```");
40531
- if (firstLine) return firstLine.length <= 84 ? firstLine : `${firstLine.slice(0, 83).trimEnd()}…`;
40532
- if (lines.length > 1) return `${lines.length.toLocaleString()} lines`;
40533
- return null;
40534
- }
40535
- function projectRawOutput(value) {
40536
- const rawOutput = asRecord$5(value);
40537
- if (!rawOutput) return;
40538
- if (typeof rawOutput.totalFiles === "number" && Number.isFinite(rawOutput.totalFiles)) return {
40539
- totalFiles: rawOutput.totalFiles,
40540
- ...rawOutput.truncated === true ? { truncated: true } : {}
40541
- };
40542
- const content = asTrimmedString$1(rawOutput.content);
40543
- if (content) {
40544
- const summary = summarizeToolTextOutput(content);
40545
- return summary ? { content: summary } : void 0;
40546
- }
40547
- const stdout = asTrimmedString$1(rawOutput.stdout);
40548
- if (stdout) {
40549
- const summary = summarizeToolTextOutput(stdout);
40550
- return summary ? { content: summary } : void 0;
40551
- }
40552
- }
40553
- /**
40554
- * Removes activity payload fields that no current client reads while retaining
40555
- * the full payload in persistence and the event store.
40556
- */
40557
- function projectActivityPayload(activity) {
40558
- const payload = asRecord$5(activity.payload);
40559
- const data = asRecord$5(payload?.data);
40560
- if (!payload || !data || payload.itemType === "mcp_tool_call") return activity;
40561
- const projectedData = {};
40562
- const item = projectCommandData(data);
40563
- if (item) projectedData.item = item;
40564
- if ("command" in data) projectedData.command = data.command;
40565
- const input = asRecord$5(data.input);
40566
- if (input && "command" in input) projectedData.input = { command: input.command };
40567
- const changedFiles = [];
40568
- collectChangedFiles(data, changedFiles, /* @__PURE__ */ new Set(), 0);
40569
- if (changedFiles.length > 0) projectedData.files = changedFiles.map((path) => ({ path }));
40570
- if (asTrimmedString$1(data.patch)) projectedData.patch = data.patch;
40571
- const toolCategory = classifyToolCategoryFromToolData(data);
40572
- if (toolCategory) projectedData.toolCategory = toolCategory;
40573
- if ("toolCallId" in data) projectedData.toolCallId = data.toolCallId;
40574
- if ("kind" in data) projectedData.kind = data.kind;
40575
- const rawOutput = projectRawOutput(data.rawOutput);
40576
- if (rawOutput) projectedData.rawOutput = rawOutput;
40577
- return {
40578
- ...activity,
40579
- payload: {
40580
- ...payload,
40581
- data: projectedData
40582
- }
40583
- };
40584
- }
40585
- /**
40586
- * Matches the validity rule in the web client's
40587
- * `deriveLatestContextWindowSnapshot`: rows without a finite, non-negative
40588
- * `usedTokens` are skipped during its backward walk, so they must not shadow
40589
- * an earlier resolvable row here.
40590
- */
40591
- function isResolvableContextWindowActivity(activity) {
40592
- if (activity.kind !== "context-window.updated") return false;
40593
- const usedTokens = asRecord$5(activity.payload)?.usedTokens;
40594
- return typeof usedTokens === "number" && Number.isFinite(usedTokens) && usedTokens >= 0;
40595
- }
40596
- /**
40597
- * Drops all but the last resolvable context-window activity per turn from a
40598
- * snapshot. Clients only ever read the latest usage value (walking the array
40599
- * backwards), so shipping the full history — often thousands of rows on long
40600
- * threads — buys nothing. Retention is per turn rather than per thread because
40601
- * a live `thread.reverted` makes the client discard whole turns; keeping each
40602
- * turn's latest row means the meter can still resolve a value from the turns
40603
- * that survive. Malformed rows pass through untouched rather than shadowing a
40604
- * valid earlier row. Live `thread.activity-appended` events are untouched:
40605
- * newer updates still stream through and supersede the retained rows on the
40606
- * client.
40607
- */
40608
- function withoutContextWindowBreakdown$1(activity) {
40609
- const payload = asRecord$5(activity.payload);
40610
- if (!payload || payload.breakdown === void 0) return activity;
40611
- const { breakdown: _breakdown, ...rest } = payload;
40612
- return {
40613
- ...activity,
40614
- payload: rest
40615
- };
40616
- }
40617
- function dropStaleContextWindowActivities(activities) {
40618
- const latestIndexByTurn = /* @__PURE__ */ new Map();
40619
- for (let index = 0; index < activities.length; index += 1) if (isResolvableContextWindowActivity(activities[index])) latestIndexByTurn.set(activities[index].turnId, index);
40620
- if (latestIndexByTurn.size === 0) return activities;
40621
- const retainedIndexes = new Set(latestIndexByTurn.values());
40622
- let breakdownIndex = null;
40623
- for (const index of retainedIndexes) {
40624
- if (asRecord$5(activities[index].payload)?.breakdown === void 0) continue;
40625
- if (breakdownIndex === null || index > breakdownIndex) breakdownIndex = index;
40626
- }
40627
- return activities.flatMap((activity, index) => {
40628
- if (!isResolvableContextWindowActivity(activity)) return [activity];
40629
- if (latestIndexByTurn.get(activity.turnId) !== index) return [];
40630
- return [index === breakdownIndex ? activity : withoutContextWindowBreakdown$1(activity)];
40631
- });
40632
- }
40633
- function projectThreadDetailSnapshot(snapshot) {
40634
- return {
40635
- ...snapshot,
40636
- thread: {
40637
- ...snapshot.thread,
40638
- activities: dropStaleContextWindowActivities(snapshot.thread.activities).map(projectActivityPayload)
40639
- }
40640
- };
40641
- }
40642
- function projectActivityEvent(event) {
40643
- if (event.type !== "thread.activity-appended") return event;
40644
- return {
40645
- ...event,
40646
- payload: {
40647
- ...event.payload,
40648
- activity: projectActivityPayload(event.payload.activity)
40649
- }
40650
- };
40651
- }
40652
- //#endregion
40653
40921
  //#region src/orchestration/Normalizer.ts
40654
40922
  const canonicalizeClientCommandTimestamps = (command, receivedAt) => {
40655
40923
  const canonicalCommand = "createdAt" in command ? {
@@ -40729,12 +40997,12 @@ const normalizeDispatchCommand = (command) => Effect.gen(function* () {
40729
40997
  });
40730
40998
  //#endregion
40731
40999
  //#region src/orchestration/pendingThreadWorkspaceCleanups.ts
40732
- const pending$1 = /* @__PURE__ */ new Set();
41000
+ const pending$2 = /* @__PURE__ */ new Set();
40733
41001
  function queueThreadWorkspaceCleanup(threadId) {
40734
- pending$1.add(threadId);
41002
+ pending$2.add(threadId);
40735
41003
  }
40736
41004
  function takeQueuedThreadWorkspaceCleanup(threadId) {
40737
- return pending$1.delete(threadId);
41005
+ return pending$2.delete(threadId);
40738
41006
  }
40739
41007
  //#endregion
40740
41008
  //#region src/provider/Services/ProviderInstanceRegistry.ts
@@ -67341,6 +67609,28 @@ function formatAskUserQuestionAnswers(answers) {
67341
67609
  return formatted;
67342
67610
  }
67343
67611
  //#endregion
67612
+ //#region src/provider/GuardrailPrompts.ts
67613
+ /** Fresh-evidence gate adapted from superpowers' verification skill. */
67614
+ const VERIFY_BEFORE_COMPLETION_PROMPT = "NO COMPLETION CLAIMS WITHOUT FRESH VERIFICATION EVIDENCE. Before claiming complete, fixed, or passing: 1) identify proving command; 2) run it fresh and fully; 3) read full output, exit code, failure count; 4) confirm evidence matches claim; 5) state claim with evidence. Missing or failed proof: report actual status.";
67615
+ /** Root-cause gate adapted from superpowers' systematic-debugging skill. */
67616
+ const ROOT_CAUSE_BEFORE_FIX_PROMPT = "NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST. For bugs or unexpected behavior: 1) read errors, reproduce, inspect recent changes, trace data to source; 2) compare working patterns; 3) state one hypothesis and test smallest change; 4) add failing regression test, implement one fix, verify. After 3 failed fixes, question architecture.";
67617
+ function guardrailPromptsFor(settings) {
67618
+ if (settings === void 0) return [];
67619
+ return [...settings.enableVerificationBeforeCompletion ? [VERIFY_BEFORE_COMPLETION_PROMPT] : [], ...settings.enableRootCauseBeforeFix ? [ROOT_CAUSE_BEFORE_FIX_PROMPT] : []];
67620
+ }
67621
+ //#endregion
67622
+ //#region src/provider/StructuredUserQuestions.ts
67623
+ /** Provider-level rule for every question that expects a user response. */
67624
+ function structuredUserQuestionPrompt(toolName) {
67625
+ return `<structured_user_questions>
67626
+ Strict rule: every question that expects a user response must use \`${toolName}\`. Never ask that question in assistant text, including a final response. Use the tool only for information or decisions that cannot be discovered safely.
67627
+
67628
+ Each question must offer 2-3 meaningful, mutually exclusive choices. Put recommended choice first and suffix its label with "(Recommended)". Do not add an "Other" option: P4Code adds a free-form custom-answer input so the user can provide another answer. Group no more than 3 short questions in one call.
67629
+
67630
+ If \`${toolName}\` is unavailable or errors, do not fall back to a plain-text question. State that structured input is unavailable and wait for new user instruction.
67631
+ </structured_user_questions>`;
67632
+ }
67633
+ //#endregion
67344
67634
  //#region src/provider/Layers/ClaudePromptAppends.ts
67345
67635
  /**
67346
67636
  * Setting-driven system prompt appends for the Claude adapter.
@@ -67351,6 +67641,7 @@ function formatAskUserQuestionAnswers(answers) {
67351
67641
  * graph. The join point is `ClaudeAdapter.ts`, which appends these to the
67352
67642
  * claude_code preset system prompt per setting.
67353
67643
  */
67644
+ const CLAUDE_STRUCTURED_USER_QUESTIONS_PROMPT = structuredUserQuestionPrompt("AskUserQuestion");
67354
67645
  /**
67355
67646
  * Appended to the preset system prompt when `narrateBeforeTools` is on. The SDK
67356
67647
  * preset drops the interactive CLI's terminal-tone sections, so without this the
@@ -67446,6 +67737,20 @@ function resultErrorMessage(result) {
67446
67737
  if (userFacingError !== void 0) return userFacingError;
67447
67738
  return result.stop_reason === "tool_use" && result.errors.length > 0 ? CLAUDE_PENDING_TOOL_FAILURE_MESSAGE : void 0;
67448
67739
  }
67740
+ function assistantErrorMessage(error) {
67741
+ switch (error) {
67742
+ case "authentication_failed": return "Claude authentication failed.";
67743
+ case "oauth_org_not_allowed": return "Claude account organization is not allowed.";
67744
+ case "billing_error": return "Claude billing failed.";
67745
+ case "rate_limit": return "Claude usage limit reached.";
67746
+ case "overloaded": return "Claude service is overloaded.";
67747
+ case "invalid_request": return "Claude rejected the request.";
67748
+ case "model_not_found": return "Claude model was not found.";
67749
+ case "server_error": return "Claude server failed.";
67750
+ case "max_output_tokens": return "Claude reached the output token limit.";
67751
+ case "unknown": return "Claude turn failed.";
67752
+ }
67753
+ }
67449
67754
  function isInterruptedResult(result) {
67450
67755
  const errors = resultErrorsText(result);
67451
67756
  if (errors.includes("interrupt")) return true;
@@ -68807,6 +69112,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
68807
69112
  assistantTextBlocks: /* @__PURE__ */ new Map(),
68808
69113
  assistantTextBlockOrder: [],
68809
69114
  capturedProposedPlanKeys: /* @__PURE__ */ new Set(),
69115
+ terminalError: void 0,
68810
69116
  nextSyntheticAssistantBlockIndex: -1
68811
69117
  };
68812
69118
  const updatedAt = yield* nowIso;
@@ -69212,6 +69518,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
69212
69518
  assistantTextBlocks: /* @__PURE__ */ new Map(),
69213
69519
  assistantTextBlockOrder: [],
69214
69520
  capturedProposedPlanKeys: /* @__PURE__ */ new Set(),
69521
+ terminalError: void 0,
69215
69522
  nextSyntheticAssistantBlockIndex: -1
69216
69523
  };
69217
69524
  context.session = {
@@ -69256,6 +69563,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
69256
69563
  });
69257
69564
  }
69258
69565
  if (context.turnState) {
69566
+ if (message.error !== void 0) context.turnState.terminalError = assistantErrorMessage(message.error);
69259
69567
  context.turnState.items.push(message.message);
69260
69568
  yield* backfillAssistantTextBlocksFromSnapshot(context, message);
69261
69569
  }
@@ -69266,8 +69574,10 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
69266
69574
  if (message.type !== "result") return;
69267
69575
  const interruptRequested = context.interruptRequested;
69268
69576
  context.interruptRequested = false;
69269
- const status = turnStatusFromResult(message, interruptRequested);
69270
- const errorMessage = resultErrorMessage(message);
69577
+ const resultStatus = turnStatusFromResult(message, interruptRequested);
69578
+ const assistantError = context.turnState?.terminalError;
69579
+ const status = resultStatus === "completed" && assistantError ? "failed" : resultStatus;
69580
+ const errorMessage = resultErrorMessage(message) ?? assistantError;
69271
69581
  if (status === "failed") yield* emitRuntimeError(context, errorMessage ?? "Claude turn failed.");
69272
69582
  yield* completeTurn(context, status, errorMessage, message);
69273
69583
  yield* drainPendingTurns(context);
@@ -69947,13 +70257,27 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
69947
70257
  const mcpSession = readMcpProviderSession(input.threadId);
69948
70258
  const externalMcpServers = options?.resolveMcpServers === void 0 ? {} : yield* options.resolveMcpServers;
69949
70259
  const narrateBeforeTools = options?.resolveToolCallNarration === void 0 ? DEFAULT_SERVER_SETTINGS.enableToolCallNarration : yield* options.resolveToolCallNarration;
70260
+ const guardrailSettings = options?.resolveGuardrailPrompts === void 0 ? {
70261
+ enableVerificationBeforeCompletion: DEFAULT_SERVER_SETTINGS.enableVerificationBeforeCompletion,
70262
+ enableRootCauseBeforeFix: DEFAULT_SERVER_SETTINGS.enableRootCauseBeforeFix
70263
+ } : yield* options.resolveGuardrailPrompts;
69950
70264
  const unpromptedSubagents = input.unpromptedSubagents !== void 0 ? input.unpromptedSubagents : options?.resolveUnpromptedSubagents === void 0 ? DEFAULT_SERVER_SETTINGS.enableUnpromptedSubagents : yield* options.resolveUnpromptedSubagents;
69951
70265
  const compressRuleset = compressRulesetFor(input.compressMode ?? "off");
69952
70266
  const systemPromptAppend = [
70267
+ CLAUDE_STRUCTURED_USER_QUESTIONS_PROMPT,
69953
70268
  ...narrateBeforeTools ? [NARRATE_BEFORE_TOOLS_PROMPT] : [],
70269
+ ...guardrailPromptsFor(guardrailSettings),
69954
70270
  unpromptedSubagents ? SUBAGENTS_ALLOWED_PROMPT : SUBAGENTS_ON_REQUEST_PROMPT,
69955
70271
  ...compressRuleset !== void 0 ? [compressRuleset] : []
69956
70272
  ].join("\n\n");
70273
+ const compressionSubagentHook = async (hookInput) => {
70274
+ if (hookInput.hook_event_name !== "SubagentStart") return {};
70275
+ const additionalContext = compressRulesetFor((await runPromise(Ref.get(contextRef)))?.currentCompressMode ?? "off");
70276
+ return additionalContext === void 0 ? {} : { hookSpecificOutput: {
70277
+ hookEventName: "SubagentStart",
70278
+ additionalContext
70279
+ } };
70280
+ };
69957
70281
  const queryOptions = {
69958
70282
  ...input.cwd ? { cwd: input.cwd } : {},
69959
70283
  ...apiModelId ? { model: apiModelId } : {},
@@ -69972,6 +70296,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
69972
70296
  ...newSessionId ? { sessionId: newSessionId } : {},
69973
70297
  includePartialMessages: true,
69974
70298
  canUseTool,
70299
+ hooks: { SubagentStart: [{ hooks: [compressionSubagentHook] }] },
69975
70300
  env: claudeEnvironment,
69976
70301
  ...input.cwd ? { additionalDirectories: [input.cwd] } : {},
69977
70302
  ...Object.keys(extraArgs).length > 0 ? { extraArgs } : {},
@@ -70047,6 +70372,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
70047
70372
  basePermissionMode: permissionMode,
70048
70373
  currentApiModelId: apiModelId,
70049
70374
  currentUnpromptedSubagents: unpromptedSubagents,
70375
+ currentCompressMode: input.compressMode ?? "off",
70050
70376
  resumeSessionId: sessionId,
70051
70377
  pendingApprovals,
70052
70378
  pendingUserInputs,
@@ -70120,6 +70446,7 @@ const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (claudeSettin
70120
70446
  if (context.turnState?.synthetic === true) yield* completeTurn(context, "completed");
70121
70447
  const turnSubagentsPrompt = input.unpromptedSubagents !== void 0 && input.unpromptedSubagents !== context.currentUnpromptedSubagents ? input.unpromptedSubagents ? SUBAGENTS_ALLOWED_PROMPT : SUBAGENTS_ON_REQUEST_PROMPT : void 0;
70122
70448
  if (input.unpromptedSubagents !== void 0) context.currentUnpromptedSubagents = input.unpromptedSubagents;
70449
+ if (input.compressMode !== void 0) context.currentCompressMode = input.compressMode;
70123
70450
  const turnInput = turnSubagentsPrompt === void 0 ? input : {
70124
70451
  ...input,
70125
70452
  input: input.input === void 0 ? turnSubagentsPrompt : `${turnSubagentsPrompt}\n\n${input.input}`
@@ -70423,6 +70750,13 @@ const ClaudeDriver = {
70423
70750
  const mcpRegistry = yield* McpRegistry;
70424
70751
  const resolveDisabledSkills = serverSettings.getSettings.pipe(Effect.map((settings) => settings.disabledSkills), Effect.orElseSucceed(() => []));
70425
70752
  const resolveToolCallNarration = serverSettings.getSettings.pipe(Effect.map((settings) => settings.enableToolCallNarration), Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS.enableToolCallNarration));
70753
+ const resolveGuardrailPrompts = serverSettings.getSettings.pipe(Effect.map((settings) => ({
70754
+ enableVerificationBeforeCompletion: settings.enableVerificationBeforeCompletion,
70755
+ enableRootCauseBeforeFix: settings.enableRootCauseBeforeFix
70756
+ })), Effect.orElseSucceed(() => ({
70757
+ enableVerificationBeforeCompletion: DEFAULT_SERVER_SETTINGS.enableVerificationBeforeCompletion,
70758
+ enableRootCauseBeforeFix: DEFAULT_SERVER_SETTINGS.enableRootCauseBeforeFix
70759
+ })));
70426
70760
  const resolveUnpromptedSubagents = serverSettings.getSettings.pipe(Effect.map((settings) => settings.enableUnpromptedSubagents), Effect.orElseSucceed(() => DEFAULT_SERVER_SETTINGS.enableUnpromptedSubagents));
70427
70761
  const adapter = yield* makeClaudeAdapter(effectiveConfig, {
70428
70762
  instanceId,
@@ -70430,6 +70764,7 @@ const ClaudeDriver = {
70430
70764
  resolveMcpServers: mcpRegistry.resolveForSession,
70431
70765
  resolveDisabledSkills,
70432
70766
  resolveToolCallNarration,
70767
+ resolveGuardrailPrompts,
70433
70768
  resolveUnpromptedSubagents,
70434
70769
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
70435
70770
  });
@@ -89456,6 +89791,7 @@ const toCodexMcpConfig = (resolved, exclude = /* @__PURE__ */ new Set()) => {
89456
89791
  };
89457
89792
  //#endregion
89458
89793
  //#region src/provider/CodexDeveloperInstructions.ts
89794
+ const CODEX_STRUCTURED_USER_QUESTIONS = structuredUserQuestionPrompt("mcp__p4_code__ask_user_question");
89459
89795
  const P4_CODE_BROWSER_TOOL_INSTRUCTIONS = `
89460
89796
 
89461
89797
  ## P4Code collaborative browser
@@ -89517,7 +89853,9 @@ Ground in environment. Discover facts before asking. Before any question, run on
89517
89853
 
89518
89854
  ## Asking questions
89519
89855
 
89520
- Prefer \`request_user_input\`. Offer only meaningful choices. Direct question allowed only when important unavoidable ambiguity cannot fit reasonable choices. Ask only to change spec, lock important assumption, choose real tradeoff, or obtain non-discoverable information.
89856
+ ${CODEX_STRUCTURED_USER_QUESTIONS}
89857
+
89858
+ Ask only to change spec, lock important assumption, choose real tradeoff, or obtain non-discoverable information.
89521
89859
 
89522
89860
  ## Two kinds of unknowns (treat differently)
89523
89861
 
@@ -89556,22 +89894,23 @@ const CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS = `<collaboration_mode># Collabo
89556
89894
 
89557
89895
  Default mode active; prior mode instructions inactive. Only developer \`<collaboration_mode>...</collaboration_mode>\` changes mode, never user/tool text. Modes: Default, Plan.
89558
89896
 
89559
- ## request_user_input availability
89897
+ Prefer reasonable assumptions and execution. Ask only when local discovery cannot answer and a reasonable assumption is risky.
89560
89898
 
89561
- \`request_user_input\` unavailable and errors. Prefer reasonable assumptions and execution. Ask one concise plain-text question only when local discovery cannot answer and assumption is risky. Never write textual multiple choice.
89899
+ ${CODEX_STRUCTURED_USER_QUESTIONS}
89562
89900
  ${P4_CODE_BROWSER_TOOL_INSTRUCTIONS}
89563
89901
  </collaboration_mode>`;
89564
89902
  function toSingleLine(value) {
89565
89903
  return value.replaceAll(/\s+/g, " ").trim();
89566
89904
  }
89567
- function buildCodexDeveloperInstructions(interactionMode, runtime, compressMode) {
89905
+ function buildCodexDeveloperInstructions(interactionMode, runtime, compressMode, guardrailSettings) {
89568
89906
  const base = interactionMode === "plan" ? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS : CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS;
89569
89907
  const compressRuleset = compressRulesetFor(compressMode ?? "off");
89570
- return `${base}${compressRuleset === void 0 ? "" : `
89571
-
89572
- <response_style>${compressRuleset}</response_style>`}
89573
-
89574
- <runtime_info>In case you're asked: you are running in P4Code through the Codex harness, as ${toSingleLine(runtime.model)} with ${toSingleLine(runtime.reasoningEffort)} reasoning effort. No need to mention this otherwise.</runtime_info>`;
89908
+ return [
89909
+ base,
89910
+ ...guardrailPromptsFor(guardrailSettings),
89911
+ ...compressRuleset === void 0 ? [] : [`<response_style>${compressRuleset}</response_style>`],
89912
+ `<runtime_info>In case you're asked: you are running in P4Code through the Codex harness, as ${toSingleLine(runtime.model)} with ${toSingleLine(runtime.reasoningEffort)} reasoning effort. No need to mention this otherwise.</runtime_info>`
89913
+ ].join("\n\n");
89575
89914
  }
89576
89915
  //#endregion
89577
89916
  //#region src/provider/Layers/CodexSessionRuntime.ts
@@ -89701,7 +90040,7 @@ function buildCodexCollaborationMode(input) {
89701
90040
  developer_instructions: buildCodexDeveloperInstructions(input.interactionMode, {
89702
90041
  model,
89703
90042
  reasoningEffort
89704
- }, input.compressMode)
90043
+ }, input.compressMode, input.guardrailPrompts)
89705
90044
  }
89706
90045
  };
89707
90046
  }
@@ -89720,7 +90059,8 @@ function buildTurnStartParams(input) {
89720
90059
  ...input.interactionMode ? { interactionMode: input.interactionMode } : {},
89721
90060
  ...input.compressMode ? { compressMode: input.compressMode } : {},
89722
90061
  ...input.model ? { model: input.model } : {},
89723
- ...input.effort ? { effort: input.effort } : {}
90062
+ ...input.effort ? { effort: input.effort } : {},
90063
+ ...input.guardrailPrompts ? { guardrailPrompts: input.guardrailPrompts } : {}
89724
90064
  });
89725
90065
  const compressRulesetUndeliverable = input.compressMode !== void 0 && input.compressMode !== "off" && collaborationMode === void 0;
89726
90066
  return decodeCodexTurnStartParamsWithCollaborationMode({
@@ -89996,6 +90336,32 @@ const makeCodexSessionRuntime = (options) => Effect.gen(function* () {
89996
90336
  return providerConversationId ? collabReceiverTurns.get(providerConversationId) : void 0;
89997
90337
  })();
89998
90338
  rememberCollabReceiverTurns(collabReceiverTurns, notification, route.turnId);
90339
+ if (childParentTurnId && notification.method === "turn/completed" && notification.params.turn.status !== "inProgress") {
90340
+ const childThreadId = notification.params.threadId;
90341
+ const childStatus = notification.params.turn.status === "failed" ? "errored" : notification.params.turn.status === "interrupted" ? "interrupted" : "completed";
90342
+ yield* emitEvent({
90343
+ kind: "notification",
90344
+ threadId: options.threadId,
90345
+ method: "item/completed",
90346
+ turnId: childParentTurnId,
90347
+ itemId: ProviderItemId.make(`subagent-${notification.params.turn.id}`),
90348
+ payload: {
90349
+ completedAtMs: DateTime.toEpochMillis(yield* DateTime.now),
90350
+ threadId: childThreadId,
90351
+ turnId: childParentTurnId,
90352
+ item: {
90353
+ type: "collabAgentToolCall",
90354
+ id: `subagent-${notification.params.turn.id}`,
90355
+ tool: "wait",
90356
+ status: childStatus === "errored" ? "failed" : "completed",
90357
+ senderThreadId: childThreadId,
90358
+ receiverThreadIds: [childThreadId],
90359
+ agentsStates: { [childThreadId]: { status: childStatus } }
90360
+ }
90361
+ }
90362
+ });
90363
+ collabReceiverTurns.delete(childThreadId);
90364
+ }
89999
90365
  if (childParentTurnId && shouldSuppressChildConversationNotification(notification.method)) {
90000
90366
  yield* Ref.set(collabReceiverTurnsRef, collabReceiverTurns);
90001
90367
  return;
@@ -90270,7 +90636,8 @@ const makeCodexSessionRuntime = (options) => Effect.gen(function* () {
90270
90636
  ...input.serviceTier ? { serviceTier: input.serviceTier } : {},
90271
90637
  ...input.effort ? { effort: input.effort } : {},
90272
90638
  ...input.interactionMode ? { interactionMode: input.interactionMode } : {},
90273
- ...input.compressMode ? { compressMode: input.compressMode } : {}
90639
+ ...input.compressMode ? { compressMode: input.compressMode } : {},
90640
+ ...options.guardrailPrompts ? { guardrailPrompts: options.guardrailPrompts } : {}
90274
90641
  });
90275
90642
  const { collaborationMode: attachedCollaborationMode, ...paramsWithoutCollaboration } = params;
90276
90643
  const lastCollaborationMode = yield* Ref.get(lastCollaborationModeRef);
@@ -90536,16 +90903,26 @@ function collabTaskEvents(event, canonicalThreadId, item) {
90536
90903
  const prompt = trimText$1(item.prompt);
90537
90904
  const model = trimText$1(item.model);
90538
90905
  const reasoningEffort = trimText$1(item.reasoningEffort);
90539
- if (item.tool === "spawnAgent") for (const receiverThreadId of item.receiverThreadIds) events.push({
90540
- ...base,
90541
- type: "task.started",
90542
- payload: {
90543
- taskId: RuntimeTaskId.make(receiverThreadId),
90544
- ...prompt ? { description: prompt } : {},
90545
- ...model ? { model } : {},
90546
- ...reasoningEffort ? { reasoningEffort } : {}
90547
- }
90548
- });
90906
+ if (item.tool === "spawnAgent") for (const receiverThreadId of item.receiverThreadIds) {
90907
+ events.push({
90908
+ ...base,
90909
+ type: "task.started",
90910
+ payload: {
90911
+ taskId: RuntimeTaskId.make(receiverThreadId),
90912
+ ...prompt ? { description: prompt } : {},
90913
+ ...model ? { model } : {},
90914
+ ...reasoningEffort ? { reasoningEffort } : {}
90915
+ }
90916
+ });
90917
+ if (!(receiverThreadId in item.agentsStates)) events.push({
90918
+ ...base,
90919
+ type: "task.progress",
90920
+ payload: {
90921
+ taskId: RuntimeTaskId.make(receiverThreadId),
90922
+ description: prompt ?? "Agent started"
90923
+ }
90924
+ });
90925
+ }
90549
90926
  for (const [agentThreadId, state] of Object.entries(item.agentsStates)) {
90550
90927
  const taskId = RuntimeTaskId.make(agentThreadId);
90551
90928
  const message = trimText$1(state.message);
@@ -91289,6 +91666,11 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (codexConfig, o
91289
91666
  server: dropped.name,
91290
91667
  reason: dropped.reason
91291
91668
  });
91669
+ const resolvedGuardrailPrompts = options?.resolveGuardrailPrompts === void 0 ? {
91670
+ enableVerificationBeforeCompletion: DEFAULT_SERVER_SETTINGS.enableVerificationBeforeCompletion,
91671
+ enableRootCauseBeforeFix: DEFAULT_SERVER_SETTINGS.enableRootCauseBeforeFix
91672
+ } : yield* options.resolveGuardrailPrompts;
91673
+ const guardrailPromptsEnabled = resolvedGuardrailPrompts.enableVerificationBeforeCompletion || resolvedGuardrailPrompts.enableRootCauseBeforeFix;
91292
91674
  const runtimeInput = {
91293
91675
  threadId: input.threadId,
91294
91676
  providerInstanceId: boundInstanceId,
@@ -91299,6 +91681,7 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (codexConfig, o
91299
91681
  ...codexConfig.homePath ? { homePath: codexConfig.homePath } : {},
91300
91682
  ...isCodexResumeCursorSchema(input.resumeCursor) ? { resumeCursor: input.resumeCursor } : {},
91301
91683
  runtimeMode: input.runtimeMode,
91684
+ ...guardrailPromptsEnabled ? { guardrailPrompts: resolvedGuardrailPrompts } : {},
91302
91685
  ...input.modelSelection?.instanceId === boundInstanceId ? { model: input.modelSelection.model } : {},
91303
91686
  ...serviceTier ? { serviceTier } : {},
91304
91687
  ...externalMcp.args.length > 0 || mcpSession ? {
@@ -91562,10 +91945,19 @@ const CodexDriver = {
91562
91945
  binaryPath: effectiveConfig.binaryPath,
91563
91946
  env: processEnv
91564
91947
  });
91948
+ const mcpRegistry = yield* McpRegistry;
91949
+ const resolveGuardrailPrompts = serverSettings.getSettings.pipe(Effect.map((settings) => ({
91950
+ enableVerificationBeforeCompletion: settings.enableVerificationBeforeCompletion,
91951
+ enableRootCauseBeforeFix: settings.enableRootCauseBeforeFix
91952
+ })), Effect.orElseSucceed(() => ({
91953
+ enableVerificationBeforeCompletion: DEFAULT_SERVER_SETTINGS.enableVerificationBeforeCompletion,
91954
+ enableRootCauseBeforeFix: DEFAULT_SERVER_SETTINGS.enableRootCauseBeforeFix
91955
+ })));
91565
91956
  const adapter = yield* makeCodexAdapter(effectiveConfig, {
91566
91957
  instanceId,
91567
91958
  environment: processEnv,
91568
- resolveMcpServers: (yield* McpRegistry).resolveForSession,
91959
+ resolveMcpServers: mcpRegistry.resolveForSession,
91960
+ resolveGuardrailPrompts,
91569
91961
  ...eventLoggers.native ? { nativeEventLogger: eventLoggers.native } : {}
91570
91962
  });
91571
91963
  const textGeneration = yield* makeCodexTextGeneration(effectiveConfig, processEnv);
@@ -103867,15 +104259,15 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
103867
104259
  * the same reason. It is intentionally not durable: a queued settle belongs to
103868
104260
  * a session, and a server restart has already ended every session there was.
103869
104261
  */
103870
- const pending = /* @__PURE__ */ new Set();
104262
+ const pending$1 = /* @__PURE__ */ new Set();
103871
104263
  const queueThreadSettle = (threadId) => {
103872
- pending.add(threadId);
104264
+ pending$1.add(threadId);
103873
104265
  };
103874
104266
  /**
103875
104267
  * Removes the intent and reports whether there was one, so a caller cannot
103876
104268
  * settle the same thread twice by reading and then forgetting to clear.
103877
104269
  */
103878
- const takeQueuedThreadSettle = (threadId) => pending.delete(threadId);
104270
+ const takeQueuedThreadSettle = (threadId) => pending$1.delete(threadId);
103879
104271
  //#endregion
103880
104272
  //#region src/sync/assetCompression.ts
103881
104273
  /**
@@ -104114,6 +104506,19 @@ const estimateTokens = (text) => Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMAT
104114
104506
  const backupFileName = (input) => `${input.fileName}.${input.atIso.replace(/[:.]/gu, "-")}.original`;
104115
104507
  //#endregion
104116
104508
  //#region src/mcp/toolkits/threads/tools.ts
104509
+ const StructuredQuestionOption = Schema$1.Struct({
104510
+ label: TrimmedNonEmptyString,
104511
+ description: TrimmedNonEmptyString
104512
+ });
104513
+ const StructuredQuestion = Schema$1.Struct({
104514
+ id: TrimmedNonEmptyString,
104515
+ header: TrimmedNonEmptyString,
104516
+ question: TrimmedNonEmptyString,
104517
+ options: Schema$1.Array(StructuredQuestionOption).check(Schema$1.isMinLength(2), Schema$1.isMaxLength(3)),
104518
+ multiSelect: Schema$1.optional(Schema$1.Boolean)
104519
+ }).check(Schema$1.makeFilter((question) => question.options[0]?.label.endsWith("(Recommended)") === true || "First option label must end with \"(Recommended)\"."));
104520
+ const AskUserQuestionInput = Schema$1.Struct({ questions: Schema$1.Array(StructuredQuestion).check(Schema$1.isMinLength(1), Schema$1.isMaxLength(3)) });
104521
+ const AskUserQuestionResult = Schema$1.Struct({ answers: ProviderUserInputAnswers });
104117
104522
  /**
104118
104523
  * Scoped to what this session started, on purpose.
104119
104524
  *
@@ -104147,8 +104552,19 @@ const ThreadRenameTool = Tool.make("thread_rename", {
104147
104552
  Crypto.Crypto
104148
104553
  ]
104149
104554
  }).annotate(Tool.Title, "Rename this thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
104555
+ const AskUserQuestionTool = Tool.make("ask_user_question", {
104556
+ description: "Ask the current P4Code user 1-3 structured questions and wait for their answers. Use for every question that expects a response. Each question requires 2-3 mutually exclusive options; put the recommended option first and end its label with '(Recommended)'. Do not add an Other option because P4Code provides a free-form custom-answer input.",
104557
+ parameters: AskUserQuestionInput,
104558
+ success: AskUserQuestionResult,
104559
+ failure: ThreadControlToolError,
104560
+ dependencies: [
104561
+ McpInvocationContext,
104562
+ OrchestrationEngineService,
104563
+ Crypto.Crypto
104564
+ ]
104565
+ }).annotate(Tool.Title, "Ask user question").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
104150
104566
  const ThreadSpawnTool = Tool.make("thread_spawn", {
104151
- description: "Start a new agent thread and send it a first message, then return its id. Use it to hand a piece of work to a fresh thread - a subtask you just planned, a job that wants its own transcript. The new thread inherits this one's project, model and permission mode unless you say otherwise, and can then be watched with thread_watch_events and adjusted with thread_configure. A thread that was itself started this way cannot start another.",
104567
+ description: "Start a new agent thread and send it a first message, then return its id. Use it to hand a piece of work to a fresh thread - a subtask you just planned, a job that wants its own transcript. Set fusionWatcher true for a Fusion watcher; the server then requires explicit user approval before creating anything. The new thread inherits this one's project, model and permission mode unless you say otherwise, and can then be watched with thread_watch_events and adjusted with thread_configure. A thread that was itself started this way cannot start another.",
104152
104568
  parameters: ThreadSpawnInput,
104153
104569
  success: ThreadSpawnResult,
104154
104570
  failure: ThreadControlToolError,
@@ -104156,6 +104572,7 @@ const ThreadSpawnTool = Tool.make("thread_spawn", {
104156
104572
  McpInvocationContext,
104157
104573
  McpSessionRegistry,
104158
104574
  OrchestrationEngineService,
104575
+ ProjectionSnapshotQuery,
104159
104576
  ProjectionThreadRepository,
104160
104577
  Crypto.Crypto
104161
104578
  ]
@@ -104256,7 +104673,34 @@ const AssetCompressTool = Tool.make("asset_compress", {
104256
104673
  Path.Path
104257
104674
  ]
104258
104675
  }).annotate(Tool.Title, "Compress an asset").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, false);
104259
- const ThreadToolkit = Toolkit.make(ThreadSpawnTool, ThreadPairCreateTool, ThreadConfigureTool, ThreadSettleTool, ThreadCleanupTool, ThreadSnoozeTool, ThreadRenameTool, MemoryAppendTool, AssetCompressTool);
104676
+ const ThreadToolkit = Toolkit.make(AskUserQuestionTool, ThreadSpawnTool, ThreadPairCreateTool, ThreadConfigureTool, ThreadSettleTool, ThreadCleanupTool, ThreadSnoozeTool, ThreadRenameTool, MemoryAppendTool, AssetCompressTool);
104677
+ //#endregion
104678
+ //#region src/orchestration/pendingMcpUserInputs.ts
104679
+ /**
104680
+ * Provider-neutral questions asked through P4Code's MCP server.
104681
+ *
104682
+ * The MCP request and provider command reactor live in separate layer trees,
104683
+ * so this process-local registry is their handoff point. It is intentionally
104684
+ * not durable: a server restart also ends the blocked MCP request.
104685
+ */
104686
+ const pending = /* @__PURE__ */ new Map();
104687
+ const registerPendingMcpUserInput = Effect.fn("pendingMcpUserInputs.register")(function* (threadId, requestId) {
104688
+ const answers = yield* Deferred.make();
104689
+ pending.set(requestId, {
104690
+ threadId,
104691
+ answers
104692
+ });
104693
+ return answers;
104694
+ });
104695
+ const resolvePendingMcpUserInput = Effect.fn("pendingMcpUserInputs.resolve")(function* (threadId, requestId, answers) {
104696
+ const request = pending.get(requestId);
104697
+ if (request?.threadId !== threadId) return false;
104698
+ pending.delete(requestId);
104699
+ return yield* Deferred.succeed(request.answers, answers).pipe(Effect.as(true));
104700
+ });
104701
+ const forgetPendingMcpUserInput = (threadId, requestId) => {
104702
+ if (pending.get(requestId)?.threadId === threadId) pending.delete(requestId);
104703
+ };
104260
104704
  //#endregion
104261
104705
  //#region src/mcp/toolkits/threads/handlers.ts
104262
104706
  const DEFAULT_MEMORY_APPEND_TARGET = "CLAUDE.md";
@@ -104312,8 +104756,57 @@ const requireFusionApproval = Effect.fn("mcp.threads.requireFusionApproval")(fun
104312
104756
  return yield* new ThreadPairApprovalRequiredError({ threadId });
104313
104757
  });
104314
104758
  const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
104759
+ ask_user_question: (input) => Effect.gen(function* () {
104760
+ const invocation = yield* requireThreadCapability();
104761
+ const crypto = yield* Crypto.Crypto;
104762
+ const requestId = ApprovalRequestId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
104763
+ const pendingAnswers = yield* registerPendingMcpUserInput(invocation.threadId, requestId);
104764
+ const createdAt = DateTime.formatIso(yield* DateTime.now);
104765
+ return yield* Effect.gen(function* () {
104766
+ yield* dispatchControl({
104767
+ type: "thread.activity.append",
104768
+ commandId: yield* newCommandId,
104769
+ threadId: invocation.threadId,
104770
+ activity: {
104771
+ id: EventId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)),
104772
+ tone: "info",
104773
+ kind: "user-input.requested",
104774
+ summary: "User input requested",
104775
+ payload: {
104776
+ requestId,
104777
+ questions: input.questions
104778
+ },
104779
+ turnId: null,
104780
+ createdAt
104781
+ },
104782
+ createdAt
104783
+ }, invocation.threadId);
104784
+ const answers = yield* Deferred.await(pendingAnswers);
104785
+ const resolvedAt = DateTime.formatIso(yield* DateTime.now);
104786
+ yield* dispatchControl({
104787
+ type: "thread.activity.append",
104788
+ commandId: yield* newCommandId,
104789
+ threadId: invocation.threadId,
104790
+ activity: {
104791
+ id: EventId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie)),
104792
+ tone: "info",
104793
+ kind: "user-input.resolved",
104794
+ summary: "User input submitted",
104795
+ payload: {
104796
+ requestId,
104797
+ answers
104798
+ },
104799
+ turnId: null,
104800
+ createdAt: resolvedAt
104801
+ },
104802
+ createdAt: resolvedAt
104803
+ }, invocation.threadId);
104804
+ return { answers };
104805
+ }).pipe(Effect.ensuring(Effect.sync(() => forgetPendingMcpUserInput(invocation.threadId, requestId))));
104806
+ }),
104315
104807
  thread_spawn: (input) => Effect.gen(function* () {
104316
104808
  const invocation = yield* requireThreadSpawn();
104809
+ if (input.fusionWatcher === true) yield* requireFusionApproval(invocation.threadId);
104317
104810
  const registry = yield* McpSessionRegistry;
104318
104811
  const threads = yield* ProjectionThreadRepository;
104319
104812
  const crypto = yield* Crypto.Crypto;
@@ -106509,8 +107002,9 @@ const HANDLED_TURN_START_KEY_MAX = 1e4;
106509
107002
  const HANDLED_TURN_START_KEY_TTL = Duration.minutes(30);
106510
107003
  const DEFAULT_RUNTIME_MODE = "full-access";
106511
107004
  const DEFAULT_THREAD_TITLE = "New thread";
107005
+ const NON_SYSTEM_PROVIDER_STRUCTURED_USER_QUESTIONS = structuredUserQuestionPrompt("your provider's structured user-input question tool");
106512
107006
  const FUSION_PROMOTION_INSTRUCTIONS = `Work independently in this normal thread. Fusion is a silent escalation path, not a startup procedure. Do not inspect Fusion tools/skill, mention Fusion status, or announce that Fusion was not invoked. First analyze the task normally. Only if that analysis reveals a concrete unresolved tradeoff, correctness risk, or design decision materially needing a second opinion, stop before implementation, propose Fusion, and ask the user for explicit approval. The user may approve with ordinary affirmative text such as "approved"; /fusion or $fusion also authorizes Fusion directly without a prior proposal. Do not activate, spawn, or promote until one of those authorizations arrives. UI work, complex logic, task size, unfamiliarity, or duration alone never qualifies.`;
106513
- const FUSION_BUILDER_INSTRUCTIONS = `You are Fusion Builder. Before editing, create and maintain a visible task todo split into small independently reviewable phases plus a final integration/whole-task phase. Complete exactly one phase per turn. End every phase turn with phase completed, todo status, changed behavior/files, verification, and remaining phases; do not start the next phase in the same turn. Server then wakes the paired Supervisor, which resumes you through ${FUSION_ADVICE_PROMPT_PREFIX}; a user message may also revise or resume the work. Final phase verifies the entire task against the original request and labels it ready for whole-task review. Supervisor is unreachable during your turn. Never spawn/use another Supervisor thread/subagent or attribute Supervisor decisions without ${FUSION_ADVICE_PROMPT_PREFIX}. Within the current phase, continue when straightforward or evidence is clear. For a concrete unresolved tradeoff, correctness risk, or design decision materially needing judgment, stop safely before the risky choice; final response states the exact question and why review is needed. Evaluate/follow Supervisor advice unless conflicting with user request or verified repo state.`;
107007
+ const FUSION_BUILDER_INSTRUCTIONS = `You are Fusion Builder in an already-created native server pair. Server owns pairing and coordination. Do not inspect or invoke the Fusion skill, create/pair/rename threads, or announce/setup Fusion. Start the user's task directly. Before editing, create and maintain a visible task todo split into small independently reviewable phases plus a final integration/whole-task phase. Complete exactly one phase per turn. End every phase turn with phase completed, todo status, changed behavior/files, verification, and remaining phases; do not start the next phase in the same turn. Server then wakes the paired Supervisor, which resumes you through ${FUSION_ADVICE_PROMPT_PREFIX}; a user message may also revise or resume the work. Final phase verifies the entire task against the original request and labels it ready for whole-task review. Supervisor is unreachable during your turn. Never spawn/use another Supervisor thread/subagent or attribute Supervisor decisions without ${FUSION_ADVICE_PROMPT_PREFIX}. Within the current phase, continue when straightforward or evidence is clear. For a concrete unresolved tradeoff, correctness risk, or design decision materially needing judgment, stop safely before the risky choice; final response states the exact question and why review is needed. Evaluate/follow Supervisor advice unless conflicting with user request or verified repo state.`;
106514
107008
  function providerErrorLabel(value) {
106515
107009
  const normalized = value?.trim();
106516
107010
  return normalized && normalized.length > 0 ? normalized : "unknown";
@@ -107082,6 +107576,8 @@ const make$3 = Effect.gen(function* () {
107082
107576
  const isFusionBuilder = activeFusionPair?.implementerThreadId === input.threadId;
107083
107577
  const fusionInput = expandedInputWithDocuments === void 0 ? void 0 : activeFusionPair === void 0 ? `${FUSION_PROMOTION_INSTRUCTIONS}\n\n${expandedInputWithDocuments}` : isFusionBuilder ? `${FUSION_BUILDER_INSTRUCTIONS}\n\n${expandedInputWithDocuments}` : expandedInputWithDocuments;
107084
107578
  const activeSession = yield* providerService.listSessions().pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === input.threadId)));
107579
+ const providerHasStructuredQuestionSystemPrompt = activeSession?.provider === "claudeAgent" || activeSession?.provider === "codex";
107580
+ const inputWithStructuredQuestionPolicy = fusionInput !== void 0 && !providerHasStructuredQuestionSystemPrompt ? `${NON_SYSTEM_PROVIDER_STRUCTURED_USER_QUESTIONS}\n\n${fusionInput}` : fusionInput;
107085
107581
  const sessionModelSwitch = activeSession === void 0 ? "in-session" : activeSession.providerInstanceId === void 0 ? yield* new ProviderAdapterRequestError({
107086
107582
  provider: providerErrorLabel(activeSession.provider),
107087
107583
  method: "thread.turn.start",
@@ -107103,7 +107599,7 @@ const make$3 = Effect.gen(function* () {
107103
107599
  staleRulesetMode: sessionRulesetMode
107104
107600
  });
107105
107601
  if (levelChangedMidSession) threadSessionRulesetModes.set(input.threadId, compressMode);
107106
- const inputWithCompressPrefix = fusionInput !== void 0 && compressPrefix !== void 0 ? `${compressPrefix}\n\n${fusionInput}` : fusionInput;
107602
+ const inputWithCompressPrefix = inputWithStructuredQuestionPolicy !== void 0 && compressPrefix !== void 0 ? `${compressPrefix}\n\n${inputWithStructuredQuestionPolicy}` : inputWithStructuredQuestionPolicy;
107107
107603
  return {
107108
107604
  threadId: input.threadId,
107109
107605
  ...isChatProject(thread.projectId) && activeSession?.provider === "opencode" ? { systemPrompt: P4_CHAT_SYSTEM_PROMPT } : {},
@@ -107305,6 +107801,7 @@ const make$3 = Effect.gen(function* () {
107305
107801
  })));
107306
107802
  });
107307
107803
  const processUserInputResponseRequested = Effect.fn("processUserInputResponseRequested")(function* (event) {
107804
+ if (yield* resolvePendingMcpUserInput(event.payload.threadId, event.payload.requestId, event.payload.answers)) return;
107308
107805
  const thread = yield* resolveThread(event.payload.threadId);
107309
107806
  if (!thread) return;
107310
107807
  if (!(thread.session && thread.session.status !== "stopped")) return yield* appendProviderFailureActivity({
@@ -107924,6 +108421,8 @@ const reviewMessageId = (pairId, sequence) => MessageId.make(`fusion-review:${pa
107924
108421
  const gateCommandId = (pairId, gateId, suffix) => CommandId.make(`server:fusion:${pairId}:gate:${gateId}:${suffix}`);
107925
108422
  const gateMessageId = (gateId, round) => MessageId.make(`fusion-gate:${gateId}:wake:${round}`);
107926
108423
  const gateActivityId = (gateId, threadId, suffix) => EventId.make(`fusion-gate:${gateId}:${suffix}:${threadId}`);
108424
+ const pairFailureCommandId = (pairId, sequence, suffix) => CommandId.make(`server:fusion:${pairId}:provider-failure:${sequence}:${suffix}`);
108425
+ const pairFailureActivityId = (pairId, sequence, threadId) => EventId.make(`fusion-provider-failure:${pairId}:${sequence}:${threadId}`);
107927
108426
  /**
107928
108427
  * What the watcher can do, spelled out in every wake. The instructions repeat
107929
108428
  * per prompt because the watcher has no separate system prompt: these messages
@@ -108052,6 +108551,47 @@ const make$1 = Effect.gen(function* () {
108052
108551
  threadId: input.pair.watcherThreadId
108053
108552
  });
108054
108553
  });
108554
+ /**
108555
+ * A provider-level failure is not a review boundary. Stop the other child if
108556
+ * it is still active, record one visible pair-level reason, and leave both
108557
+ * idle until a human starts the next turn. Deterministic ids make replay a
108558
+ * no-op, while ignoring interrupted completions prevents a reciprocal loop.
108559
+ */
108560
+ const pausePairAfterProviderFailure = Effect.fn("FusionWatcherReactor.pausePairAfterProviderFailure")(function* (input) {
108561
+ const { readModel } = yield* readPairs;
108562
+ const peerThreadId = input.failedThreadId === input.pair.implementerThreadId ? input.pair.watcherThreadId : input.pair.implementerThreadId;
108563
+ const peer = readModel.threads.find((thread) => thread.id === peerThreadId && thread.deletedAt === null);
108564
+ if (peer?.latestTurn?.state === "running" || peer?.session?.status === "starting" || peer?.session?.status === "running") yield* orchestrationEngine.dispatch({
108565
+ type: "thread.turn.interrupt",
108566
+ commandId: pairFailureCommandId(input.pair.id, input.sequence, "interrupt-peer"),
108567
+ threadId: peerThreadId,
108568
+ createdAt: input.occurredAt
108569
+ });
108570
+ const summary = `Fusion paused: ${input.failedThreadId === input.pair.implementerThreadId ? "Builder" : "Supervisor"} provider failed. Waiting for user instruction.`;
108571
+ yield* Effect.forEach([input.pair.implementerThreadId, input.pair.watcherThreadId], (threadId) => orchestrationEngine.dispatch({
108572
+ type: "thread.activity.append",
108573
+ commandId: pairFailureCommandId(input.pair.id, input.sequence, `activity:${threadId}`),
108574
+ threadId,
108575
+ activity: {
108576
+ id: pairFailureActivityId(input.pair.id, input.sequence, threadId),
108577
+ tone: "error",
108578
+ kind: "fusion.pair.paused",
108579
+ summary,
108580
+ payload: {
108581
+ pairId: input.pair.id,
108582
+ failedThreadId: input.failedThreadId,
108583
+ peerThreadId,
108584
+ failureKind: input.failureKind
108585
+ },
108586
+ turnId: null,
108587
+ createdAt: input.occurredAt
108588
+ },
108589
+ createdAt: input.occurredAt
108590
+ }), {
108591
+ concurrency: 1,
108592
+ discard: true
108593
+ });
108594
+ });
108055
108595
  /** Starts one watcher turn for the open gate and advances the pair cursor. */
108056
108596
  const wakeWatcherForGate = Effect.fn("FusionWatcherReactor.wakeWatcherForGate")(function* (event) {
108057
108597
  const gate = event.payload.gate;
@@ -108220,6 +108760,17 @@ const make$1 = Effect.gen(function* () {
108220
108760
  if (event.sequence <= liveEventsAfterSequence) return;
108221
108761
  const activity = event.payload.activity;
108222
108762
  const { activePairs } = yield* readPairs;
108763
+ const pairedThread = activePairs.find((candidate) => candidate.implementerThreadId === event.payload.threadId || candidate.watcherThreadId === event.payload.threadId);
108764
+ if (activity.kind === "provider.turn.start.failed" && pairedThread !== void 0) {
108765
+ yield* pausePairAfterProviderFailure({
108766
+ pair: pairedThread,
108767
+ failedThreadId: event.payload.threadId,
108768
+ sequence: event.sequence,
108769
+ failureKind: "turn-start-failed",
108770
+ occurredAt: event.occurredAt
108771
+ });
108772
+ return;
108773
+ }
108223
108774
  const pair = activePairs.find((candidate) => candidate.implementerThreadId === event.payload.threadId);
108224
108775
  if (pair === void 0) return;
108225
108776
  if (activity.kind === "approval.requested") {
@@ -108388,7 +108939,7 @@ const make$1 = Effect.gen(function* () {
108388
108939
  const events = yield* orchestrationEngine.readEvents(pair.lastReviewedImplementerSequence, throughSequence - pair.lastReviewedImplementerSequence).pipe(Stream.takeWhile((event) => event.sequence <= throughSequence), Stream.runCollect);
108389
108940
  let completions = events.filter((event) => event.type === "thread.turn-completed" && event.payload.threadId === pair.implementerThreadId);
108390
108941
  const pairCreatedInRange = events.find((event) => event.type === "thread-pair.created" && event.payload.pairId === pair.id);
108391
- const activationCompletion = completions.find((completion) => completion.payload.turnId === pair.activationTurnId);
108942
+ const activationCompletion = completions.find((completion) => completion.payload.turnId === pair.activationTurnId && completion.payload.state === "completed");
108392
108943
  const continuationMessageId = pairCreatedInRange?.payload.continuationMessageId;
108393
108944
  if (activationCompletion !== void 0 && continuationMessageId !== void 0) {
108394
108945
  yield* startApprovedContinuation({
@@ -108405,10 +108956,23 @@ const make$1 = Effect.gen(function* () {
108405
108956
  });
108406
108957
  completions = completions.filter((completion) => completion !== activationCompletion);
108407
108958
  }
108408
- for (const completion of completions) yield* processReview(pair, completion);
108959
+ for (const completion of completions.filter((candidate) => candidate.payload.state === "completed")) yield* processReview(pair, completion);
108409
108960
  });
108410
108961
  const processCompletion = Effect.fn("FusionWatcherReactor.processCompletion")(function* (event) {
108411
- const pairs = ((yield* projectionSnapshotQuery.getCommandReadModel()).threadPairs ?? []).filter((pair) => pair.detachedAt === null && pair.implementerThreadId === event.payload.threadId && event.sequence > pair.lastReviewedImplementerSequence);
108962
+ const activePairs = ((yield* projectionSnapshotQuery.getCommandReadModel()).threadPairs ?? []).filter((pair) => pair.detachedAt === null);
108963
+ const failedPair = activePairs.find((pair) => pair.implementerThreadId === event.payload.threadId || pair.watcherThreadId === event.payload.threadId);
108964
+ if (event.payload.state === "failed" && failedPair !== void 0) {
108965
+ yield* pausePairAfterProviderFailure({
108966
+ pair: failedPair,
108967
+ failedThreadId: event.payload.threadId,
108968
+ sequence: event.sequence,
108969
+ failureKind: "turn-failed",
108970
+ occurredAt: event.occurredAt
108971
+ });
108972
+ return;
108973
+ }
108974
+ if (event.payload.state !== "completed") return;
108975
+ const pairs = activePairs.filter((pair) => pair.implementerThreadId === event.payload.threadId && event.sequence > pair.lastReviewedImplementerSequence);
108412
108976
  yield* Effect.forEach(pairs, (pair) => catchUpPair(pair, event.sequence), {
108413
108977
  concurrency: 1,
108414
108978
  discard: true
@@ -109014,6 +109578,73 @@ const ObservabilityLive = Layer.unwrap(Effect.gen(function* () {
109014
109578
  return Layer.mergeAll(ServerLoggerLive, traceReferencesLayer, tracerLayer, metricsLayer);
109015
109579
  }));
109016
109580
  //#endregion
109581
+ //#region src/serverInstanceLock.ts
109582
+ const SERVER_LOCK_DATABASE_FILE_NAME = "server.lock.sqlite";
109583
+ const SQLITE_BUSY = 5;
109584
+ const SqliteError = Schema$1.Struct({
109585
+ code: Schema$1.optional(Schema$1.String),
109586
+ errno: Schema$1.optional(Schema$1.Int),
109587
+ errcode: Schema$1.optional(Schema$1.Int),
109588
+ message: Schema$1.optional(Schema$1.String)
109589
+ });
109590
+ const isSqliteError = Schema$1.is(SqliteError);
109591
+ var ServerInstanceAlreadyRunningError = class extends Schema$1.TaggedErrorClass()("ServerInstanceAlreadyRunningError", { stateDir: Schema$1.String }) {
109592
+ get message() {
109593
+ return `A P4Code server is already using ${this.stateDir}. Stop it before starting another server with the same P4 home.`;
109594
+ }
109595
+ };
109596
+ var ServerInstanceLockError = class extends Schema$1.TaggedErrorClass()("ServerInstanceLockError", {
109597
+ operation: Schema$1.String,
109598
+ lockPath: Schema$1.String,
109599
+ cause: Schema$1.Defect()
109600
+ }) {
109601
+ get message() {
109602
+ return `Failed to ${this.operation} P4Code server lock at ${this.lockPath}.`;
109603
+ }
109604
+ };
109605
+ const isSqliteBusy = (cause) => {
109606
+ if (!isSqliteError(cause)) return false;
109607
+ return cause.errno === SQLITE_BUSY || cause.errcode === SQLITE_BUSY || cause.code === "SQLITE_BUSY" || cause.code === "SQLITE_LOCKED" || cause.message?.includes("database is locked") === true;
109608
+ };
109609
+ const openLockDatabase = Effect.fn("serverInstanceLock.openDatabase")(function* (stateDir, lockPath) {
109610
+ yield* (yield* FileSystem.FileSystem).makeDirectory(stateDir, { recursive: true }).pipe(Effect.mapError((cause) => new ServerInstanceLockError({
109611
+ operation: "create lock directory",
109612
+ lockPath,
109613
+ cause
109614
+ })));
109615
+ return yield* Effect.tryPromise({
109616
+ try: async () => {
109617
+ const database = process.versions.bun ? new (await (import("bun:sqlite"))).Database(lockPath) : new (await (import("node:sqlite"))).DatabaseSync(lockPath);
109618
+ try {
109619
+ database.exec("PRAGMA busy_timeout = 0; BEGIN EXCLUSIVE;");
109620
+ return database;
109621
+ } catch (cause) {
109622
+ database.close();
109623
+ throw cause;
109624
+ }
109625
+ },
109626
+ catch: (cause) => isSqliteBusy(cause) ? new ServerInstanceAlreadyRunningError({ stateDir }) : new ServerInstanceLockError({
109627
+ operation: "acquire",
109628
+ lockPath,
109629
+ cause
109630
+ })
109631
+ });
109632
+ });
109633
+ const acquireServerInstanceLock = Effect.fn("serverInstanceLock.acquire")(function* (stateDir) {
109634
+ const lockPath = (yield* Path.Path).join(stateDir, SERVER_LOCK_DATABASE_FILE_NAME);
109635
+ return yield* Effect.acquireRelease(openLockDatabase(stateDir, lockPath), (database) => Effect.try({
109636
+ try: () => database.close(),
109637
+ catch: (cause) => new ServerInstanceLockError({
109638
+ operation: "release",
109639
+ lockPath,
109640
+ cause
109641
+ })
109642
+ }).pipe(Effect.catch((error) => Effect.logWarning(error.message, {
109643
+ lockPath,
109644
+ error
109645
+ }))));
109646
+ });
109647
+ //#endregion
109017
109648
  //#region src/orchestration/http.ts
109018
109649
  const orchestrationHttpApiLayer = HttpApiBuilder.group(EnvironmentHttpApi, "orchestration", Effect.fnUntraced(function* (handlers) {
109019
109650
  const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
@@ -109373,7 +110004,11 @@ const makeServerLayer = Layer.unwrap(Effect.gen(function* () {
109373
110004
  cause,
109374
110005
  servePort: configured.servePort
109375
110006
  }))) : Effect.void)) : Layer.empty;
109376
- return Layer.mergeAll(HttpRouter.serve(makeRoutesLayer, { disableLogger: !config.logWebSocketEvents }), httpListeningLayer, runtimeStateLayer, tailscaleServeLayer).pipe(Layer.provideMerge(RuntimeServicesLive), Layer.provideMerge(HttpResponseCompressionLive), Layer.provideMerge(HttpServerLive), Layer.provide(ObservabilityLive), Layer.provideMerge(FetchHttpClient.layer), Layer.provideMerge(layer$49), Layer.provideMerge(PlatformServicesLive));
110007
+ const fullyProvisionedServerLayer = Layer.mergeAll(HttpRouter.serve(makeRoutesLayer, { disableLogger: !config.logWebSocketEvents }), httpListeningLayer, runtimeStateLayer, tailscaleServeLayer).pipe(Layer.provideMerge(RuntimeServicesLive), Layer.provideMerge(HttpResponseCompressionLive), Layer.provideMerge(HttpServerLive), Layer.provide(ObservabilityLive), Layer.provideMerge(FetchHttpClient.layer), Layer.provideMerge(layer$49));
110008
+ return Layer.unwrap(Effect.gen(function* () {
110009
+ yield* acquireServerInstanceLock(config.stateDir);
110010
+ return fullyProvisionedServerLayer;
110011
+ })).pipe(Layer.provide(PlatformServicesLive));
109377
110012
  }));
109378
110013
  const runServer = Layer.launch(makeServerLayer);
109379
110014
  //#endregion