@p4code/cli 0.3.11 → 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 +891 -523
- package/dist/client/assets/{DiffPanel-BGO6aQhc.js → DiffPanel-ZuSaky6q.js} +2 -2
- package/dist/client/assets/{FilePreviewPanel-BfIDlE3r.js → FilePreviewPanel-BbiaXjTf.js} +2 -2
- package/dist/client/assets/{PreviewPanel-BRzuNjEM.js → PreviewPanel-CJ4cZJE4.js} +2 -2
- package/dist/client/assets/{PullRequestCodeTab-DRM7f4r-.js → PullRequestCodeTab-DNe02oza.js} +2 -2
- package/dist/client/assets/{fileCommentAnnotations-Bm2-xw0A.js → fileCommentAnnotations-bCFXVUao.js} +2 -2
- package/dist/client/assets/{index-C7Nfsa1N.css → index-D20qAWML.css} +1 -1
- package/dist/client/assets/{index-BMfIngdZ.js → index-DGL0rWox.js} +244 -236
- package/dist/client/assets/{previewAssetResource-CXEgFBIp.js → previewAssetResource-DAaQ0AtU.js} +3 -3
- package/dist/client/assets/{renderFileChildren-CQePFzqT.js → renderFileChildren-I0AJzaf5.js} +2 -2
- package/dist/client/assets/{toggle-group-sN-3vxTh.js → toggle-group-8iCHoIcR.js} +2 -2
- package/dist/client/index.html +3 -3
- package/package.json +1 -1
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.
|
|
241
|
+
var version = "0.3.12";
|
|
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
|
-
*
|
|
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.
|
|
22022
|
-
*
|
|
22023
|
-
*
|
|
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 = ["
|
|
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
|
-
|
|
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
|
-
|
|
22151
|
-
|
|
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*
|
|
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) =>
|
|
24353
|
-
|
|
24354
|
-
|
|
24355
|
-
|
|
24356
|
-
|
|
24357
|
-
|
|
24358
|
-
|
|
24359
|
-
|
|
24360
|
-
|
|
24361
|
-
|
|
24362
|
-
|
|
24363
|
-
|
|
24364
|
-
|
|
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:
|
|
29584
|
+
activityId: activity.id,
|
|
28853
29585
|
threadId: event.payload.threadId,
|
|
28854
|
-
turnId:
|
|
28855
|
-
tone:
|
|
28856
|
-
kind:
|
|
28857
|
-
summary:
|
|
28858
|
-
payload:
|
|
28859
|
-
...
|
|
28860
|
-
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$
|
|
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$
|
|
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$
|
|
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)
|
|
90620
|
-
|
|
90621
|
-
|
|
90622
|
-
|
|
90623
|
-
|
|
90624
|
-
|
|
90625
|
-
|
|
90626
|
-
|
|
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
|
-
|
|
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
|