@p4code/cli 0.3.11 → 0.3.13

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.11";
241
+ var version = "0.3.13";
242
242
  //#endregion
243
243
  //#region src/config.ts
244
244
  /**
@@ -15731,6 +15731,27 @@ var _051_ThreadPairActivationTurn_default = Effect.gen(function* () {
15731
15731
  `;
15732
15732
  });
15733
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
15734
15755
  //#region src/persistence/Migrations.ts
15735
15756
  /**
15736
15757
  * MigrationsLive - Migration runner with inline loader
@@ -16006,6 +16027,11 @@ const migrationEntries = [
16006
16027
  51,
16007
16028
  "ThreadPairActivationTurn",
16008
16029
  _051_ThreadPairActivationTurn_default
16030
+ ],
16031
+ [
16032
+ 52,
16033
+ "CompactHistoricalToolActivities",
16034
+ _052_CompactHistoricalToolActivities_default
16009
16035
  ]
16010
16036
  ];
16011
16037
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -16085,6 +16111,665 @@ const runGuardedMigrations = Effect.fn("runGuardedMigrations")(function* (input)
16085
16111
  const discardSnapshotQuietly = (dbPath) => discardDatabaseSnapshot(dbPath).pipe(Effect.tapError((cause) => Effect.logWarning("Could not discard the database snapshot").pipe(Effect.annotateLogs({ cause }))), Effect.ignore);
16086
16112
  Layer.effectDiscard(runMigrations());
16087
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
16088
16773
  //#region src/persistence/Layers/Sqlite.ts
16089
16774
  const defaultSqliteClientLoaders = {
16090
16775
  bun: () => import("@effect/sql-sqlite-bun/SqliteClient"),
@@ -16100,7 +16785,7 @@ const applyPragmas = Effect.gen(function* () {
16100
16785
  yield* sql`PRAGMA journal_mode = WAL;`;
16101
16786
  yield* sql`PRAGMA foreign_keys = ON;`;
16102
16787
  });
16103
- 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())));
16104
16789
  /**
16105
16790
  * The in-memory database used by tests has no file to snapshot and nothing that
16106
16791
  * survives the process to restore.
@@ -16109,7 +16794,7 @@ const setupFile = (dbPath) => Layer.effectDiscard(applyPragmas.pipe(Effect.andTh
16109
16794
  * needing nothing but `SqlClient`: sharing one function would put the file
16110
16795
  * path's `FileSystem | Path` requirement into every test layer built on it.
16111
16796
  */
16112
- const setupMemory = Layer.effectDiscard(applyPragmas.pipe(Effect.andThen(runMigrations())));
16797
+ const setupMemory = Layer.effectDiscard(applyPragmas.pipe(Effect.andThen(runMigrations()), Effect.andThen(runHistoricalActivityCompaction())));
16113
16798
  const makeSqlitePersistenceLive = Effect.fn("makeSqlitePersistenceLive")(function* (dbPath) {
16114
16799
  const fs = yield* FileSystem.FileSystem;
16115
16800
  const path = yield* Path.Path;
@@ -22016,14 +22701,12 @@ const MEMORY_FILE_NAMES = ["CLAUDE.md", "AGENTS.md"];
22016
22701
  */
22017
22702
  const isMemoryFileName = (name) => MEMORY_FILE_NAMES.includes(name);
22018
22703
  /**
22019
- * 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.
22020
22706
  *
22021
- * A memory file is rarely the whole of the memory. `CLAUDE.md` is written to be
22022
- * an index — a dozen `@~/.claude/global-memory/...` lines and little else so
22023
- * syncing the file alone hands the other machine a list of pointers to files
22024
- * that are not there, and an agent reading it behaves as if the rules it names
22025
- * do not exist. That failure is silent: the includes resolve to nothing rather
22026
- * 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.
22027
22710
  *
22028
22711
  * So this is an allowlist too, and for the same reason the file one exists. The
22029
22712
  * escape it prevents is different, though, and weaker: `AgentAssetFilePath`
@@ -22033,7 +22716,7 @@ const isMemoryFileName = (name) => MEMORY_FILE_NAMES.includes(name);
22033
22716
  * asset is not permitted to claim `.claude/projects` or any other directory
22034
22717
  * that already means something to the provider.
22035
22718
  */
22036
- const MEMORY_DIRECTORY_NAMES = ["global-memory"];
22719
+ const MEMORY_DIRECTORY_NAMES = ["agent-memory"];
22037
22720
  /** Whether this name is one of the memory directories. Exact match, as above. */
22038
22721
  const isMemoryDirectoryName = (name) => MEMORY_DIRECTORY_NAMES.includes(name);
22039
22722
  /**
@@ -22053,7 +22736,7 @@ const looksBinary$1 = (content) => content.includes("\0");
22053
22736
  * most machines have a `CLAUDE.md` and no `AGENTS.md`, and reporting the second
22054
22737
  * as a problem would be reporting the normal case.
22055
22738
  */
22056
- const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memoryRoot) {
22739
+ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memoryRoot, sharedMemoryRoot) {
22057
22740
  const fileSystem = yield* FileSystem.FileSystem;
22058
22741
  const path = yield* Path.Path;
22059
22742
  const skills = [];
@@ -22100,7 +22783,8 @@ const readMemoryFiles = Effect.fn("memoryFile.readMemoryFiles")(function* (memor
22100
22783
  });
22101
22784
  }
22102
22785
  for (const name of MEMORY_DIRECTORY_NAMES) {
22103
- 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);
22104
22788
  if ((yield* fileSystem.stat(absolute).pipe(Effect.orElseSucceed(() => void 0)))?.type !== "Directory") continue;
22105
22789
  const read = yield* readSkillFiles(absolute);
22106
22790
  if ("refusal" in read) {
@@ -22143,12 +22827,18 @@ const isWritableMemoryAsset = (input) => {
22143
22827
  * missing, which an agent will follow without noticing.
22144
22828
  */
22145
22829
  const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input) {
22830
+ if (input.name === "agent-memory" && !input.sharedMemoryRoot) return "refused";
22146
22831
  if (!isWritableMemoryAsset(input)) return "refused";
22147
22832
  const fileSystem = yield* FileSystem.FileSystem;
22148
22833
  const path = yield* Path.Path;
22149
22834
  if (isMemoryDirectoryName(input.name)) return yield* writeSkillDirectory({
22150
- skillsRoot: input.memoryRoot,
22151
- 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
+ },
22152
22842
  files: input.files
22153
22843
  }).pipe(Effect.as("written"), Effect.orElseSucceed(() => "failed"));
22154
22844
  const contents = input.files[0]?.content ?? "";
@@ -22160,8 +22850,13 @@ const writeMemoryFile = Effect.fn("memoryFile.writeMemoryFile")(function* (input
22160
22850
  });
22161
22851
  const removeMemoryFile = Effect.fn("memoryFile.removeMemoryFile")(function* (input) {
22162
22852
  if (!isMemoryAssetName(input.name)) return;
22853
+ if (input.name === "agent-memory" && !input.sharedMemoryRoot) return;
22163
22854
  if (isMemoryDirectoryName(input.name)) {
22164
- 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
+ } : {
22165
22860
  skillsRoot: input.memoryRoot,
22166
22861
  name: input.name
22167
22862
  });
@@ -22434,6 +23129,27 @@ function planSkillSync(input) {
22434
23129
  }
22435
23130
  //#endregion
22436
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
+ */
22437
23153
  const EMPTY_REPORT = {
22438
23154
  ran: false,
22439
23155
  roots: [],
@@ -22452,6 +23168,7 @@ const make$75 = Effect.gen(function* () {
22452
23168
  const settingsStore = yield* ServerSettingsService;
22453
23169
  const config = yield* ServerConfig$1;
22454
23170
  const path = yield* Path.Path;
23171
+ const sharedMemoryRoot = path.join(NodeOS.homedir(), ".agent-memory");
22455
23172
  /**
22456
23173
  * Captured here rather than left in each method's requirements, so the
22457
23174
  * service's own type says what it really is: an effect that needs nothing and
@@ -22489,14 +23206,16 @@ const make$75 = Effect.gen(function* () {
22489
23206
  {
22490
23207
  kind: "memory",
22491
23208
  resolveRoot: claudeHome().pipe(Effect.flatMap((config) => resolveClaudeConfigDirPath(config, process.env))),
22492
- read: (root) => readMemoryFiles(root),
23209
+ read: (root) => readMemoryFiles(root, sharedMemoryRoot),
22493
23210
  write: ({ root, name, files }) => writeMemoryFile({
22494
23211
  memoryRoot: root,
23212
+ sharedMemoryRoot,
22495
23213
  name,
22496
23214
  files
22497
23215
  }).pipe(Effect.map((outcome) => outcome)),
22498
23216
  remove: ({ root, name }) => removeMemoryFile({
22499
23217
  memoryRoot: root,
23218
+ sharedMemoryRoot,
22500
23219
  name
22501
23220
  }).pipe(Effect.ignore)
22502
23221
  },
@@ -24215,6 +24934,9 @@ const ReadFromSequenceOfTypesRequestSchema = Schema$1.Struct({
24215
24934
  const ReadByCommandIdRequestSchema = Schema$1.Struct({ commandId: CommandId });
24216
24935
  const DEFAULT_READ_FROM_SEQUENCE_LIMIT = 1e3;
24217
24936
  const READ_PAGE_SIZE = 500;
24937
+ function isThreadActivityAppendedEvent(event) {
24938
+ return event.type === "thread.activity-appended";
24939
+ }
24218
24940
  function inferActorKind(event) {
24219
24941
  if (event.commandId !== null && event.commandId.startsWith("provider:")) return "provider";
24220
24942
  if (event.commandId !== null && event.commandId.startsWith("server:")) return "server";
@@ -24349,19 +25071,28 @@ const makeEventStore = Effect.gen(function* () {
24349
25071
  ORDER BY sequence ASC
24350
25072
  `
24351
25073
  });
24352
- const append = (event) => appendEventRow({
24353
- eventId: event.eventId,
24354
- aggregateKind: event.aggregateKind,
24355
- streamId: event.aggregateId,
24356
- type: event.type,
24357
- causationEventId: event.causationEventId,
24358
- correlationId: event.correlationId,
24359
- actorKind: inferActorKind(event),
24360
- occurredAt: event.occurredAt,
24361
- commandId: event.commandId,
24362
- payloadJson: event.payload,
24363
- metadataJson: event.metadata
24364
- }).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
+ };
24365
25096
  const readFromSequence = (sequenceExclusive, limit = DEFAULT_READ_FROM_SEQUENCE_LIMIT, options) => {
24366
25097
  const normalizedLimit = Math.max(0, Math.floor(limit));
24367
25098
  if (normalizedLimit === 0) return Stream.empty;
@@ -28848,16 +29579,17 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
28848
29579
  const applyThreadActivitiesProjection = Effect.fn("applyThreadActivitiesProjection")(function* (event, _attachmentSideEffects) {
28849
29580
  switch (event.type) {
28850
29581
  case "thread.activity-appended":
29582
+ const activity = projectActivityPayload(event.payload.activity);
28851
29583
  yield* projectionThreadActivityRepository.upsert({
28852
- activityId: event.payload.activity.id,
29584
+ activityId: activity.id,
28853
29585
  threadId: event.payload.threadId,
28854
- turnId: event.payload.activity.turnId,
28855
- tone: event.payload.activity.tone,
28856
- kind: event.payload.activity.kind,
28857
- summary: event.payload.activity.summary,
28858
- payload: event.payload.activity.payload,
28859
- ...event.payload.activity.sequence !== void 0 ? { sequence: event.payload.activity.sequence } : {},
28860
- 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
28861
29593
  });
28862
29594
  return;
28863
29595
  case "thread.reverted": {
@@ -37324,7 +38056,7 @@ const NESTED_PAYLOAD_KEYS = [
37324
38056
  "operations"
37325
38057
  ];
37326
38058
  const MAX_COLLECT_DEPTH = 4;
37327
- function asRecord$7(value) {
38059
+ function asRecord$5(value) {
37328
38060
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
37329
38061
  }
37330
38062
  function pushChangedFilePath(target, value) {
@@ -37339,7 +38071,7 @@ function collectChangedFilePaths(value, target, depth) {
37339
38071
  for (const entry of value) collectChangedFilePaths(entry, target, depth + 1);
37340
38072
  return;
37341
38073
  }
37342
- const record = asRecord$7(value);
38074
+ const record = asRecord$5(value);
37343
38075
  if (!record) return;
37344
38076
  for (const field of CHANGED_FILE_FIELDS) pushChangedFilePath(target, record[field]);
37345
38077
  for (const nestedKey of NESTED_PAYLOAD_KEYS) if (nestedKey in record) collectChangedFilePaths(record[nestedKey], target, depth + 1);
@@ -37350,7 +38082,7 @@ function collectChangedFilePaths(value, target, depth) {
37350
38082
  */
37351
38083
  function collectActivityChangedFilePaths(payload) {
37352
38084
  const target = /* @__PURE__ */ new Set();
37353
- collectChangedFilePaths(asRecord$7(asRecord$7(payload)?.data), target, 0);
38085
+ collectChangedFilePaths(asRecord$5(asRecord$5(payload)?.data), target, 0);
37354
38086
  return target;
37355
38087
  }
37356
38088
  /**
@@ -40186,477 +40918,6 @@ const make$54 = Effect.gen(function* () {
40186
40918
  });
40187
40919
  const layer$44 = Layer.effect(CheckpointDiffQuery, make$54);
40188
40920
  //#endregion
40189
- //#region ../../packages/shared/src/toolCategory.ts
40190
- const TOOL_CATEGORY_TITLES = {
40191
- file_read: "File read",
40192
- file_search: "File search",
40193
- file_change: "File change",
40194
- command: "Command run",
40195
- version_control: "Version control",
40196
- build_test: "Build or test",
40197
- mcp_tool: "MCP tool call",
40198
- subagent: "Subagent task",
40199
- web_search: "Web search",
40200
- web_fetch: "Web fetch",
40201
- task_plan: "Task or plan",
40202
- image_view: "Image view",
40203
- tool: "Tool call"
40204
- };
40205
- new Set(Object.keys(TOOL_CATEGORY_TITLES));
40206
- /** Exact tool names, lowercased, matched before the substring heuristics below. */
40207
- const FILE_READ_TOOL_NAMES = /* @__PURE__ */ new Set([
40208
- "read",
40209
- "readfile",
40210
- "read_file",
40211
- "view",
40212
- "viewfile",
40213
- "view_file",
40214
- "notebookread",
40215
- "notebook_read",
40216
- "openfile",
40217
- "open_file"
40218
- ]);
40219
- const FILE_SEARCH_TOOL_NAMES = /* @__PURE__ */ new Set([
40220
- "grep",
40221
- "glob",
40222
- "search",
40223
- "filesearch",
40224
- "file_search",
40225
- "codebasesearch",
40226
- "codebase_search",
40227
- "listdir",
40228
- "list_dir",
40229
- "list_directory",
40230
- "ls"
40231
- ]);
40232
- const FILE_CHANGE_TOOL_NAMES = /* @__PURE__ */ new Set([
40233
- "edit",
40234
- "write",
40235
- "multiedit",
40236
- "multi_edit",
40237
- "notebookedit",
40238
- "notebook_edit",
40239
- "applypatch",
40240
- "apply_patch",
40241
- "patch",
40242
- "createfile",
40243
- "create_file",
40244
- "strreplace",
40245
- "str_replace",
40246
- "deletefile",
40247
- "delete_file"
40248
- ]);
40249
- const COMMAND_TOOL_NAMES = /* @__PURE__ */ new Set([
40250
- "bash",
40251
- "shell",
40252
- "terminal",
40253
- "exec",
40254
- "execute",
40255
- "execcommand",
40256
- "exec_command",
40257
- "runcommand",
40258
- "run_command",
40259
- "local_shell"
40260
- ]);
40261
- const SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
40262
- "task",
40263
- "agent",
40264
- "subagent",
40265
- "sub_agent"
40266
- ]);
40267
- const WEB_FETCH_TOOL_NAMES = /* @__PURE__ */ new Set([
40268
- "webfetch",
40269
- "web_fetch",
40270
- "fetch",
40271
- "fetchurl",
40272
- "fetch_url",
40273
- "browse",
40274
- "openurl",
40275
- "open_url"
40276
- ]);
40277
- /**
40278
- * Matched before the subagent rules, so `task_create` is a board write while the
40279
- * bare `Task` tool stays a subagent.
40280
- */
40281
- const TASK_PLAN_TOOL_NAMES = /* @__PURE__ */ new Set([
40282
- "todowrite",
40283
- "todo_write",
40284
- "todoread",
40285
- "todo_read",
40286
- "exitplanmode",
40287
- "exit_plan_mode",
40288
- "updateplan",
40289
- "update_plan",
40290
- "task_create",
40291
- "task_update",
40292
- "task_list",
40293
- "task_get",
40294
- "task_current",
40295
- "task_propose",
40296
- "ticket_resolve",
40297
- "taskcreate",
40298
- "taskupdate",
40299
- "tasklist",
40300
- "taskget"
40301
- ]);
40302
- /** Shell builtins that say nothing about what the command as a whole does. */
40303
- const NEUTRAL_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40304
- "cd",
40305
- "echo",
40306
- "printf",
40307
- "pwd",
40308
- "true",
40309
- "false",
40310
- "time"
40311
- ]);
40312
- /** `sed`/`awk` are stream editors (`sed -i` rewrites files), so they stay commands. */
40313
- const FILE_READ_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40314
- "cat",
40315
- "head",
40316
- "tail",
40317
- "less",
40318
- "more",
40319
- "bat",
40320
- "nl",
40321
- "jq",
40322
- "wc"
40323
- ]);
40324
- const FILE_SEARCH_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40325
- "grep",
40326
- "egrep",
40327
- "fgrep",
40328
- "rg",
40329
- "ag",
40330
- "ack",
40331
- "find",
40332
- "fd",
40333
- "ls",
40334
- "tree",
40335
- "which"
40336
- ]);
40337
- /** `git status` and `git diff` sort here too: the verb is what a reader scans for. */
40338
- const VERSION_CONTROL_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40339
- "git",
40340
- "gh",
40341
- "jj",
40342
- "hg",
40343
- "svn",
40344
- "glab"
40345
- ]);
40346
- /**
40347
- * Builds, tests, type checks, lint and install share one category: a test run is
40348
- * what a person scans for when a turn goes long, and the rest is the same noise.
40349
- */
40350
- const BUILD_TEST_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40351
- "pnpm",
40352
- "npm",
40353
- "npx",
40354
- "yarn",
40355
- "bun",
40356
- "bunx",
40357
- "vitest",
40358
- "jest",
40359
- "vp",
40360
- "tsc",
40361
- "tsgo",
40362
- "eslint",
40363
- "oxlint",
40364
- "biome",
40365
- "prettier",
40366
- "ruff",
40367
- "pytest",
40368
- "make",
40369
- "cargo",
40370
- "gradle",
40371
- "mvn",
40372
- "xcodebuild",
40373
- "swift",
40374
- "pip",
40375
- "uv",
40376
- "poetry"
40377
- ]);
40378
- const WEB_FETCH_SHELL_COMMANDS = /* @__PURE__ */ new Set([
40379
- "curl",
40380
- "wget",
40381
- "http",
40382
- "httpie"
40383
- ]);
40384
- const SHELL_SEGMENT_SEPARATOR = /\|\||&&|;|\||\n/u;
40385
- const LEADING_ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/u;
40386
- const REDIRECT_TARGET = /\d?>>?\s*(?<target>[^\s|;&]+)/gu;
40387
- /** Redirect targets that discard output instead of writing a file. */
40388
- const DISCARDED_REDIRECT_TARGETS = /* @__PURE__ */ new Set([
40389
- "/dev/null",
40390
- "&1",
40391
- "&2"
40392
- ]);
40393
- function toolCategoryTitle(category) {
40394
- return TOOL_CATEGORY_TITLES[category];
40395
- }
40396
- function readCommandInput(toolInput) {
40397
- const raw = toolInput?.command ?? toolInput?.cmd;
40398
- if (typeof raw !== "string") return;
40399
- const trimmed = raw.trim();
40400
- return trimmed.length > 0 ? trimmed : void 0;
40401
- }
40402
- /** `cat x > y` reads and writes; only the write matters for the row heading. */
40403
- function redirectsToFile(segment) {
40404
- for (const match of segment.matchAll(REDIRECT_TARGET)) {
40405
- const target = match.groups?.target;
40406
- if (target && !DISCARDED_REDIRECT_TARGETS.has(target)) return true;
40407
- }
40408
- return false;
40409
- }
40410
- function classifyShellSegment(segment) {
40411
- if (redirectsToFile(segment)) return "command";
40412
- const head = segment.trim().replace(LEADING_ENV_ASSIGNMENT, "").split(/\s+/u)[0]?.replace(/^.*\//u, "").toLowerCase();
40413
- if (!head || NEUTRAL_SHELL_COMMANDS.has(head)) return;
40414
- if (FILE_READ_SHELL_COMMANDS.has(head)) return "file_read";
40415
- if (FILE_SEARCH_SHELL_COMMANDS.has(head)) return "file_search";
40416
- if (VERSION_CONTROL_SHELL_COMMANDS.has(head)) return "version_control";
40417
- if (BUILD_TEST_SHELL_COMMANDS.has(head)) return "build_test";
40418
- if (WEB_FETCH_SHELL_COMMANDS.has(head)) return "web_fetch";
40419
- return "command";
40420
- }
40421
- /**
40422
- * A shell call only counts as anything narrower than a command when every
40423
- * non-neutral segment agrees; anything unrecognized (or write-ish) keeps the
40424
- * whole call a command, and so does a pipeline that mixes two categories — the
40425
- * one exception being a read piped into a search, which is still a search.
40426
- * Quoted separators split segments too, which can only downgrade to "command".
40427
- */
40428
- function classifyShellCommand(command) {
40429
- const categories = new Set(command.split(SHELL_SEGMENT_SEPARATOR).map(classifyShellSegment).filter((category) => category !== void 0));
40430
- if (categories.size === 0 || categories.has("command")) return "command";
40431
- const [only] = categories;
40432
- if (categories.size === 1 && only) return only;
40433
- return [...categories].every((category) => category === "file_read" || category === "file_search") ? "file_search" : "command";
40434
- }
40435
- function classifyToolCategory(input) {
40436
- const normalized = input.toolName.trim().toLowerCase();
40437
- if (normalized.startsWith("mcp__") || normalized.includes("mcp")) return "mcp_tool";
40438
- if (TASK_PLAN_TOOL_NAMES.has(normalized)) return "task_plan";
40439
- if (WEB_FETCH_TOOL_NAMES.has(normalized)) return "web_fetch";
40440
- if (SUBAGENT_TOOL_NAMES.has(normalized) || normalized.includes("agent")) return "subagent";
40441
- if (FILE_READ_TOOL_NAMES.has(normalized)) return "file_read";
40442
- if (FILE_SEARCH_TOOL_NAMES.has(normalized)) return "file_search";
40443
- if (FILE_CHANGE_TOOL_NAMES.has(normalized)) return "file_change";
40444
- if (COMMAND_TOOL_NAMES.has(normalized) || normalized.includes("bash") || normalized.includes("shell") || normalized.includes("terminal") || normalized.includes("command")) {
40445
- const command = readCommandInput(input.toolInput);
40446
- return command ? classifyShellCommand(command) : "command";
40447
- }
40448
- if (normalized.includes("websearch") || normalized.includes("web_search")) return "web_search";
40449
- if (normalized.includes("webfetch") || normalized.includes("web_fetch")) return "web_fetch";
40450
- if (normalized.includes("todo") || normalized.includes("plan")) return "task_plan";
40451
- if (normalized.includes("edit") || normalized.includes("write") || normalized.includes("patch") || normalized.includes("replace") || normalized.includes("create") || normalized.includes("delete")) return "file_change";
40452
- if (normalized.includes("read") || normalized.includes("view")) return "file_read";
40453
- if (normalized.includes("grep") || normalized.includes("glob")) return "file_search";
40454
- if (normalized.includes("image")) return "image_view";
40455
- return "tool";
40456
- }
40457
- function asRecord$6(value) {
40458
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
40459
- }
40460
- /** Classify from a runtime item payload's `data` (`{ toolName, input }`). */
40461
- function classifyToolCategoryFromToolData(data) {
40462
- const record = asRecord$6(data);
40463
- const toolName = record?.toolName;
40464
- if (typeof toolName !== "string" || toolName.trim().length === 0) return;
40465
- return classifyToolCategory({
40466
- toolName,
40467
- toolInput: asRecord$6(record?.input)
40468
- });
40469
- }
40470
- //#endregion
40471
- //#region src/orchestration/ActivityPayloadProjection.ts
40472
- function asRecord$5(value) {
40473
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
40474
- }
40475
- function asTrimmedString$1(value) {
40476
- if (typeof value !== "string") return null;
40477
- const trimmed = value.trim();
40478
- return trimmed.length > 0 ? trimmed : null;
40479
- }
40480
- function pushChangedFile(target, seen, value) {
40481
- const normalized = asTrimmedString$1(value);
40482
- if (!normalized || seen.has(normalized)) return;
40483
- seen.add(normalized);
40484
- target.push(normalized);
40485
- }
40486
- function collectChangedFiles(value, target, seen, depth) {
40487
- if (depth > 4 || target.length >= 12) return;
40488
- if (Array.isArray(value)) {
40489
- for (const entry of value) {
40490
- collectChangedFiles(entry, target, seen, depth + 1);
40491
- if (target.length >= 12) return;
40492
- }
40493
- return;
40494
- }
40495
- const record = asRecord$5(value);
40496
- if (!record) return;
40497
- pushChangedFile(target, seen, record.path);
40498
- pushChangedFile(target, seen, record.filePath);
40499
- pushChangedFile(target, seen, record.relativePath);
40500
- pushChangedFile(target, seen, record.filename);
40501
- pushChangedFile(target, seen, record.newPath);
40502
- pushChangedFile(target, seen, record.oldPath);
40503
- for (const nestedKey of [
40504
- "item",
40505
- "result",
40506
- "input",
40507
- "data",
40508
- "changes",
40509
- "files",
40510
- "edits",
40511
- "patch",
40512
- "patches",
40513
- "operations"
40514
- ]) {
40515
- if (!(nestedKey in record)) continue;
40516
- collectChangedFiles(record[nestedKey], target, seen, depth + 1);
40517
- if (target.length >= 12) return;
40518
- }
40519
- }
40520
- function projectCommandData(data) {
40521
- const item = asRecord$5(data.item);
40522
- if (!item) return;
40523
- const projectedItem = {};
40524
- if ("command" in item) projectedItem.command = item.command;
40525
- const input = asRecord$5(item.input);
40526
- if (input && "command" in input) projectedItem.input = { command: input.command };
40527
- const result = asRecord$5(item.result);
40528
- if (result && "command" in result) projectedItem.result = { command: result.command };
40529
- return Object.keys(projectedItem).length > 0 ? projectedItem : void 0;
40530
- }
40531
- function summarizeToolTextOutput(value) {
40532
- const lines = [];
40533
- for (const rawLine of value.split(/\r?\n/u)) {
40534
- const line = rawLine.replace(/\s+/g, " ").trim();
40535
- if (line.length > 0) lines.push(line);
40536
- }
40537
- const firstLine = lines.find((line) => line !== "```");
40538
- if (firstLine) return firstLine.length <= 84 ? firstLine : `${firstLine.slice(0, 83).trimEnd()}…`;
40539
- if (lines.length > 1) return `${lines.length.toLocaleString()} lines`;
40540
- return null;
40541
- }
40542
- function projectRawOutput(value) {
40543
- const rawOutput = asRecord$5(value);
40544
- if (!rawOutput) return;
40545
- if (typeof rawOutput.totalFiles === "number" && Number.isFinite(rawOutput.totalFiles)) return {
40546
- totalFiles: rawOutput.totalFiles,
40547
- ...rawOutput.truncated === true ? { truncated: true } : {}
40548
- };
40549
- const content = asTrimmedString$1(rawOutput.content);
40550
- if (content) {
40551
- const summary = summarizeToolTextOutput(content);
40552
- return summary ? { content: summary } : void 0;
40553
- }
40554
- const stdout = asTrimmedString$1(rawOutput.stdout);
40555
- if (stdout) {
40556
- const summary = summarizeToolTextOutput(stdout);
40557
- return summary ? { content: summary } : void 0;
40558
- }
40559
- }
40560
- /**
40561
- * Removes activity payload fields that no current client reads while retaining
40562
- * the full payload in persistence and the event store.
40563
- */
40564
- function projectActivityPayload(activity) {
40565
- const payload = asRecord$5(activity.payload);
40566
- const data = asRecord$5(payload?.data);
40567
- if (!payload || !data || payload.itemType === "mcp_tool_call") return activity;
40568
- const projectedData = {};
40569
- const item = projectCommandData(data);
40570
- if (item) projectedData.item = item;
40571
- if ("command" in data) projectedData.command = data.command;
40572
- const input = asRecord$5(data.input);
40573
- if (input && "command" in input) projectedData.input = { command: input.command };
40574
- const changedFiles = [];
40575
- collectChangedFiles(data, changedFiles, /* @__PURE__ */ new Set(), 0);
40576
- if (changedFiles.length > 0) projectedData.files = changedFiles.map((path) => ({ path }));
40577
- if (asTrimmedString$1(data.patch)) projectedData.patch = data.patch;
40578
- const toolCategory = classifyToolCategoryFromToolData(data);
40579
- if (toolCategory) projectedData.toolCategory = toolCategory;
40580
- if ("toolCallId" in data) projectedData.toolCallId = data.toolCallId;
40581
- if ("kind" in data) projectedData.kind = data.kind;
40582
- const rawOutput = projectRawOutput(data.rawOutput);
40583
- if (rawOutput) projectedData.rawOutput = rawOutput;
40584
- return {
40585
- ...activity,
40586
- payload: {
40587
- ...payload,
40588
- data: projectedData
40589
- }
40590
- };
40591
- }
40592
- /**
40593
- * Matches the validity rule in the web client's
40594
- * `deriveLatestContextWindowSnapshot`: rows without a finite, non-negative
40595
- * `usedTokens` are skipped during its backward walk, so they must not shadow
40596
- * an earlier resolvable row here.
40597
- */
40598
- function isResolvableContextWindowActivity(activity) {
40599
- if (activity.kind !== "context-window.updated") return false;
40600
- const usedTokens = asRecord$5(activity.payload)?.usedTokens;
40601
- return typeof usedTokens === "number" && Number.isFinite(usedTokens) && usedTokens >= 0;
40602
- }
40603
- /**
40604
- * Drops all but the last resolvable context-window activity per turn from a
40605
- * snapshot. Clients only ever read the latest usage value (walking the array
40606
- * backwards), so shipping the full history — often thousands of rows on long
40607
- * threads — buys nothing. Retention is per turn rather than per thread because
40608
- * a live `thread.reverted` makes the client discard whole turns; keeping each
40609
- * turn's latest row means the meter can still resolve a value from the turns
40610
- * that survive. Malformed rows pass through untouched rather than shadowing a
40611
- * valid earlier row. Live `thread.activity-appended` events are untouched:
40612
- * newer updates still stream through and supersede the retained rows on the
40613
- * client.
40614
- */
40615
- function withoutContextWindowBreakdown$1(activity) {
40616
- const payload = asRecord$5(activity.payload);
40617
- if (!payload || payload.breakdown === void 0) return activity;
40618
- const { breakdown: _breakdown, ...rest } = payload;
40619
- return {
40620
- ...activity,
40621
- payload: rest
40622
- };
40623
- }
40624
- function dropStaleContextWindowActivities(activities) {
40625
- const latestIndexByTurn = /* @__PURE__ */ new Map();
40626
- for (let index = 0; index < activities.length; index += 1) if (isResolvableContextWindowActivity(activities[index])) latestIndexByTurn.set(activities[index].turnId, index);
40627
- if (latestIndexByTurn.size === 0) return activities;
40628
- const retainedIndexes = new Set(latestIndexByTurn.values());
40629
- let breakdownIndex = null;
40630
- for (const index of retainedIndexes) {
40631
- if (asRecord$5(activities[index].payload)?.breakdown === void 0) continue;
40632
- if (breakdownIndex === null || index > breakdownIndex) breakdownIndex = index;
40633
- }
40634
- return activities.flatMap((activity, index) => {
40635
- if (!isResolvableContextWindowActivity(activity)) return [activity];
40636
- if (latestIndexByTurn.get(activity.turnId) !== index) return [];
40637
- return [index === breakdownIndex ? activity : withoutContextWindowBreakdown$1(activity)];
40638
- });
40639
- }
40640
- function projectThreadDetailSnapshot(snapshot) {
40641
- return {
40642
- ...snapshot,
40643
- thread: {
40644
- ...snapshot.thread,
40645
- activities: dropStaleContextWindowActivities(snapshot.thread.activities).map(projectActivityPayload)
40646
- }
40647
- };
40648
- }
40649
- function projectActivityEvent(event) {
40650
- if (event.type !== "thread.activity-appended") return event;
40651
- return {
40652
- ...event,
40653
- payload: {
40654
- ...event.payload,
40655
- activity: projectActivityPayload(event.payload.activity)
40656
- }
40657
- };
40658
- }
40659
- //#endregion
40660
40921
  //#region src/orchestration/Normalizer.ts
40661
40922
  const canonicalizeClientCommandTimestamps = (command, receivedAt) => {
40662
40923
  const canonicalCommand = "createdAt" in command ? {
@@ -90075,6 +90336,32 @@ const makeCodexSessionRuntime = (options) => Effect.gen(function* () {
90075
90336
  return providerConversationId ? collabReceiverTurns.get(providerConversationId) : void 0;
90076
90337
  })();
90077
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
+ }
90078
90365
  if (childParentTurnId && shouldSuppressChildConversationNotification(notification.method)) {
90079
90366
  yield* Ref.set(collabReceiverTurnsRef, collabReceiverTurns);
90080
90367
  return;
@@ -90616,16 +90903,26 @@ function collabTaskEvents(event, canonicalThreadId, item) {
90616
90903
  const prompt = trimText$1(item.prompt);
90617
90904
  const model = trimText$1(item.model);
90618
90905
  const reasoningEffort = trimText$1(item.reasoningEffort);
90619
- if (item.tool === "spawnAgent") for (const receiverThreadId of item.receiverThreadIds) events.push({
90620
- ...base,
90621
- type: "task.started",
90622
- payload: {
90623
- taskId: RuntimeTaskId.make(receiverThreadId),
90624
- ...prompt ? { description: prompt } : {},
90625
- ...model ? { model } : {},
90626
- ...reasoningEffort ? { reasoningEffort } : {}
90627
- }
90628
- });
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
+ }
90629
90926
  for (const [agentThreadId, state] of Object.entries(item.agentsStates)) {
90630
90927
  const taskId = RuntimeTaskId.make(agentThreadId);
90631
90928
  const message = trimText$1(state.message);
@@ -109281,6 +109578,73 @@ const ObservabilityLive = Layer.unwrap(Effect.gen(function* () {
109281
109578
  return Layer.mergeAll(ServerLoggerLive, traceReferencesLayer, tracerLayer, metricsLayer);
109282
109579
  }));
109283
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
109284
109648
  //#region src/orchestration/http.ts
109285
109649
  const orchestrationHttpApiLayer = HttpApiBuilder.group(EnvironmentHttpApi, "orchestration", Effect.fnUntraced(function* (handlers) {
109286
109650
  const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
@@ -109640,7 +110004,11 @@ const makeServerLayer = Layer.unwrap(Effect.gen(function* () {
109640
110004
  cause,
109641
110005
  servePort: configured.servePort
109642
110006
  }))) : Effect.void)) : Layer.empty;
109643
- 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));
109644
110012
  }));
109645
110013
  const runServer = Layer.launch(makeServerLayer);
109646
110014
  //#endregion