@schoolai/shipyard 3.2.2 → 3.2.3-nightly.20260423.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{auth-5RJ4YKPC.js → auth-LS3NBD42.js} +3 -3
- package/dist/{chunk-NZFMJMJN.js → chunk-67VJDX7G.js} +3 -3
- package/dist/{chunk-HF5GUPSU.js → chunk-CCW5QAUH.js} +2 -2
- package/dist/{chunk-N4ZTO3K3.js → chunk-CUK27MYV.js} +12 -4
- package/dist/chunk-CUK27MYV.js.map +1 -0
- package/dist/{chunk-DB5R6SKT.js → chunk-DNIC3FOH.js} +2 -2
- package/dist/{chunk-5RH5JP3R.js → chunk-GLH3V7NG.js} +2 -2
- package/dist/{chunk-KWRAP2UK.js → chunk-GSTE3IXM.js} +5 -5
- package/dist/{chunk-KUV4J6NI.js → chunk-M5M6VC5F.js} +1 -2
- package/dist/{chunk-KUV4J6NI.js.map → chunk-M5M6VC5F.js.map} +1 -1
- package/dist/index.js +8 -8
- package/dist/login-O2AKBGIS.js +19 -0
- package/dist/{logout-432UPTCL.js → logout-M7F7HXUU.js} +5 -5
- package/dist/{mcp-servers-YT5BADYE.js → mcp-servers-MUVTAMDT.js} +4 -4
- package/dist/{roi-MVOOANFC.js → roi-ZCVNBSTO.js} +3 -3
- package/dist/{serve-O4AHOTL4.js → serve-IMSVRV3B.js} +1049 -333
- package/dist/{serve-O4AHOTL4.js.map → serve-IMSVRV3B.js.map} +1 -1
- package/dist/{start-G2VIRDQI.js → start-XGODMVQQ.js} +8 -8
- package/package.json +3 -3
- package/dist/chunk-N4ZTO3K3.js.map +0 -1
- package/dist/login-ZPYQLQ52.js +0 -19
- /package/dist/{auth-5RJ4YKPC.js.map → auth-LS3NBD42.js.map} +0 -0
- /package/dist/{chunk-NZFMJMJN.js.map → chunk-67VJDX7G.js.map} +0 -0
- /package/dist/{chunk-HF5GUPSU.js.map → chunk-CCW5QAUH.js.map} +0 -0
- /package/dist/{chunk-DB5R6SKT.js.map → chunk-DNIC3FOH.js.map} +0 -0
- /package/dist/{chunk-5RH5JP3R.js.map → chunk-GLH3V7NG.js.map} +0 -0
- /package/dist/{chunk-KWRAP2UK.js.map → chunk-GSTE3IXM.js.map} +0 -0
- /package/dist/{login-ZPYQLQ52.js.map → login-O2AKBGIS.js.map} +0 -0
- /package/dist/{logout-432UPTCL.js.map → logout-M7F7HXUU.js.map} +0 -0
- /package/dist/{mcp-servers-YT5BADYE.js.map → mcp-servers-MUVTAMDT.js.map} +0 -0
- /package/dist/{roi-MVOOANFC.js.map → roi-ZCVNBSTO.js.map} +0 -0
- /package/dist/{start-G2VIRDQI.js.map → start-XGODMVQQ.js.map} +0 -0
|
@@ -47,11 +47,11 @@ import {
|
|
|
47
47
|
VaultKeyPutRequestSchema,
|
|
48
48
|
VaultKeyPutResponseSchema,
|
|
49
49
|
classifyClaudeCodeCompatibility
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-CUK27MYV.js";
|
|
51
51
|
import "./chunk-EHQITHQX.js";
|
|
52
52
|
import {
|
|
53
53
|
loadAuthToken
|
|
54
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-GLH3V7NG.js";
|
|
55
55
|
import {
|
|
56
56
|
DiscoveryStateSchema,
|
|
57
57
|
detectMCPServers,
|
|
@@ -62,19 +62,19 @@ import {
|
|
|
62
62
|
redactEnv,
|
|
63
63
|
resolveEnabledMcpServers,
|
|
64
64
|
resolveStdioEnv
|
|
65
|
-
} from "./chunk-
|
|
65
|
+
} from "./chunk-67VJDX7G.js";
|
|
66
66
|
import {
|
|
67
67
|
createChildLogger,
|
|
68
68
|
flushLogger,
|
|
69
69
|
logger
|
|
70
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-DNIC3FOH.js";
|
|
71
71
|
import {
|
|
72
72
|
external_exports,
|
|
73
73
|
getShipyardHome,
|
|
74
74
|
isVanillaAgentMode,
|
|
75
75
|
toJSONSchema,
|
|
76
76
|
validateEnv
|
|
77
|
-
} from "./chunk-
|
|
77
|
+
} from "./chunk-M5M6VC5F.js";
|
|
78
78
|
import {
|
|
79
79
|
detectSkills
|
|
80
80
|
} from "./chunk-DPMRSLYJ.js";
|
|
@@ -83,8 +83,8 @@ import {
|
|
|
83
83
|
} from "./chunk-2H7UOFLK.js";
|
|
84
84
|
|
|
85
85
|
// src/services/serve.ts
|
|
86
|
-
import { mkdir as mkdir25, realpath as
|
|
87
|
-
import { join as
|
|
86
|
+
import { mkdir as mkdir25, realpath as realpath3 } from "fs/promises";
|
|
87
|
+
import { join as join57 } from "path";
|
|
88
88
|
import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
|
|
89
89
|
|
|
90
90
|
// ../../node_modules/.pnpm/@loro-extended+change@6.0.0-beta.0_loro-crdt@1.11.1/node_modules/@loro-extended/change/dist/index.js
|
|
@@ -27270,7 +27270,8 @@ var PlanDetectionSchema = external_exports.object({
|
|
|
27270
27270
|
filePath: external_exports.string(),
|
|
27271
27271
|
planDocId: external_exports.string(),
|
|
27272
27272
|
allowedPrompts: external_exports.array(AllowedPromptSchema).optional(),
|
|
27273
|
-
originalMarkdown: external_exports.string().optional()
|
|
27273
|
+
originalMarkdown: external_exports.string().optional(),
|
|
27274
|
+
approved: external_exports.boolean().optional()
|
|
27274
27275
|
});
|
|
27275
27276
|
var TaskRecordSchema = external_exports.object({
|
|
27276
27277
|
taskId: external_exports.string(),
|
|
@@ -27307,9 +27308,10 @@ var TaskRecordSchema = external_exports.object({
|
|
|
27307
27308
|
totalTurnCount: external_exports.number().int().nonnegative().default(0),
|
|
27308
27309
|
mergedAt: external_exports.number().int().positive().nullable().default(null),
|
|
27309
27310
|
attributedCommitShas: external_exports.array(external_exports.string()).max(50).default([]),
|
|
27310
|
-
lastCommitScanSha: external_exports.string().nullable().default(null)
|
|
27311
|
+
lastCommitScanSha: external_exports.string().nullable().default(null),
|
|
27312
|
+
originalCwd: external_exports.string().nullable().default(null)
|
|
27311
27313
|
});
|
|
27312
|
-
var TASK_STORE_VERSION =
|
|
27314
|
+
var TASK_STORE_VERSION = 12;
|
|
27313
27315
|
var TaskStoreSchema = external_exports.object({
|
|
27314
27316
|
schemaVersion: external_exports.number(),
|
|
27315
27317
|
tasks: external_exports.record(external_exports.string(), TaskRecordSchema)
|
|
@@ -28220,7 +28222,8 @@ var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("t
|
|
|
28220
28222
|
tool: external_exports.string(),
|
|
28221
28223
|
prompt: external_exports.string()
|
|
28222
28224
|
})).optional(),
|
|
28223
|
-
isRevision: external_exports.boolean().optional()
|
|
28225
|
+
isRevision: external_exports.boolean().optional(),
|
|
28226
|
+
approved: external_exports.boolean().optional()
|
|
28224
28227
|
}),
|
|
28225
28228
|
external_exports.object({
|
|
28226
28229
|
type: external_exports.literal("plan_continuation_timeout"),
|
|
@@ -28267,7 +28270,8 @@ var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("t
|
|
|
28267
28270
|
userId: external_exports.string(),
|
|
28268
28271
|
username: external_exports.string(),
|
|
28269
28272
|
avatarUrl: external_exports.string().nullable().optional(),
|
|
28270
|
-
role: CollabRoleSchema
|
|
28273
|
+
role: CollabRoleSchema,
|
|
28274
|
+
connectionId: external_exports.string()
|
|
28271
28275
|
})),
|
|
28272
28276
|
expiresAt: external_exports.number()
|
|
28273
28277
|
})
|
|
@@ -28633,7 +28637,12 @@ var HunkRangeSchema = external_exports.object({
|
|
|
28633
28637
|
modifiedEnd: external_exports.number()
|
|
28634
28638
|
});
|
|
28635
28639
|
var BrowserToFileIOMessageSchema = external_exports.discriminatedUnion("type", [
|
|
28636
|
-
external_exports.object({
|
|
28640
|
+
external_exports.object({
|
|
28641
|
+
type: external_exports.literal("read_file"),
|
|
28642
|
+
requestId: external_exports.string(),
|
|
28643
|
+
path: external_exports.string(),
|
|
28644
|
+
external: external_exports.boolean().optional()
|
|
28645
|
+
}),
|
|
28637
28646
|
external_exports.object({
|
|
28638
28647
|
type: external_exports.literal("write_file"),
|
|
28639
28648
|
requestId: external_exports.string(),
|
|
@@ -28641,7 +28650,12 @@ var BrowserToFileIOMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
28641
28650
|
content: external_exports.string()
|
|
28642
28651
|
}),
|
|
28643
28652
|
external_exports.object({ type: external_exports.literal("readdir"), requestId: external_exports.string(), path: external_exports.string() }),
|
|
28644
|
-
external_exports.object({
|
|
28653
|
+
external_exports.object({
|
|
28654
|
+
type: external_exports.literal("stat"),
|
|
28655
|
+
requestId: external_exports.string(),
|
|
28656
|
+
path: external_exports.string(),
|
|
28657
|
+
external: external_exports.boolean().optional()
|
|
28658
|
+
}),
|
|
28645
28659
|
external_exports.object({ type: external_exports.literal("git_status"), requestId: external_exports.string() }),
|
|
28646
28660
|
external_exports.object({
|
|
28647
28661
|
type: external_exports.literal("git_diff_file"),
|
|
@@ -31283,8 +31297,11 @@ function isProcessAlive(pid) {
|
|
|
31283
31297
|
try {
|
|
31284
31298
|
process.kill(pid, 0);
|
|
31285
31299
|
return true;
|
|
31286
|
-
} catch {
|
|
31287
|
-
|
|
31300
|
+
} catch (err) {
|
|
31301
|
+
if (err instanceof Error && "code" in err && err.code === "ESRCH") {
|
|
31302
|
+
return false;
|
|
31303
|
+
}
|
|
31304
|
+
throw err;
|
|
31288
31305
|
}
|
|
31289
31306
|
}
|
|
31290
31307
|
var LifecycleManager = class {
|
|
@@ -31362,15 +31379,36 @@ var LifecycleManager = class {
|
|
|
31362
31379
|
const pidFilePath = join9(shipyardHome, "daemon.pid");
|
|
31363
31380
|
try {
|
|
31364
31381
|
const existing = await readFile5(pidFilePath, "utf-8");
|
|
31365
|
-
const
|
|
31366
|
-
|
|
31367
|
-
|
|
31368
|
-
|
|
31369
|
-
|
|
31382
|
+
const raw = existing.trim();
|
|
31383
|
+
const pid = Number.parseInt(raw, 10);
|
|
31384
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
31385
|
+
this.#log.warn(
|
|
31386
|
+
{ event: "pid_file_corrupt_reclaimed", rawContent: raw, pidFile: pidFilePath },
|
|
31387
|
+
"PID file contains invalid content \u2014 treating as corrupt and reclaiming"
|
|
31388
|
+
);
|
|
31389
|
+
} else {
|
|
31390
|
+
let alive;
|
|
31391
|
+
try {
|
|
31392
|
+
alive = isProcessAlive(pid);
|
|
31393
|
+
} catch (killErr) {
|
|
31394
|
+
this.#log.error(
|
|
31395
|
+
{ event: "pid_kill_check_failed", err: killErr, pid, pidFile: pidFilePath },
|
|
31396
|
+
"Cannot determine if existing daemon is alive (EPERM) \u2014 refusing to overwrite PID file"
|
|
31397
|
+
);
|
|
31398
|
+
process.exit(1);
|
|
31399
|
+
}
|
|
31400
|
+
if (alive) {
|
|
31401
|
+
this.#log.error(
|
|
31402
|
+
{ pid, pidFile: pidFilePath },
|
|
31403
|
+
"Another daemon is already running. Stop it first or remove the stale PID file."
|
|
31404
|
+
);
|
|
31405
|
+
process.exit(1);
|
|
31406
|
+
}
|
|
31407
|
+
this.#log.info(
|
|
31408
|
+
{ event: "pid_reclaimed_stale", stalePid: pid, pidFile: pidFilePath },
|
|
31409
|
+
"Stale PID file found (process no longer running) \u2014 reclaiming"
|
|
31370
31410
|
);
|
|
31371
|
-
process.exit(1);
|
|
31372
31411
|
}
|
|
31373
|
-
this.#log.info({ stalePid: pid, pidFile: pidFilePath }, "Removing stale PID file");
|
|
31374
31412
|
} catch (err) {
|
|
31375
31413
|
if (!isEnoent(err)) throw err;
|
|
31376
31414
|
}
|
|
@@ -31690,7 +31728,7 @@ function nanoid(size2 = 21) {
|
|
|
31690
31728
|
}
|
|
31691
31729
|
|
|
31692
31730
|
// src/services/bootstrap/signaling.ts
|
|
31693
|
-
var DAEMON_NPM_VERSION = true ? "3.2.
|
|
31731
|
+
var DAEMON_NPM_VERSION = true ? "3.2.3" : "unknown";
|
|
31694
31732
|
function createDaemonSignaling(config2) {
|
|
31695
31733
|
const agentId = config2.agentId ?? nanoid();
|
|
31696
31734
|
function send(msg) {
|
|
@@ -34005,7 +34043,6 @@ async function deleteAdvertisement(shipyardHome) {
|
|
|
34005
34043
|
|
|
34006
34044
|
// src/services/local-direct/local-direct-wiring.ts
|
|
34007
34045
|
async function setupLocalDirect(deps) {
|
|
34008
|
-
if (!deps.env.SHIPYARD_LOCAL_DIRECT) return null;
|
|
34009
34046
|
const token = generateLocalDirectToken();
|
|
34010
34047
|
const server = await createLocalDirectServer({
|
|
34011
34048
|
token,
|
|
@@ -36984,36 +37021,7 @@ function createPRPoller(callbacks, log, resolveTopLevel = getGitTopLevel) {
|
|
|
36984
37021
|
resolveTopLevel(cwd).then((topLevel) => {
|
|
36985
37022
|
if (disposed) return;
|
|
36986
37023
|
removeFromPreviousRepo(taskId);
|
|
36987
|
-
|
|
36988
|
-
const existing = repos.get(topLevel);
|
|
36989
|
-
if (existing) {
|
|
36990
|
-
existing.subscribers.add(taskId);
|
|
36991
|
-
if (existing.lastPayload) {
|
|
36992
|
-
callbacks.onPRState({ ...existing.lastPayload, taskId });
|
|
36993
|
-
}
|
|
36994
|
-
log.info({ taskId, topLevel, cwd }, "PR polling joined existing repo");
|
|
36995
|
-
return;
|
|
36996
|
-
}
|
|
36997
|
-
const entry = {
|
|
36998
|
-
timer: null,
|
|
36999
|
-
burstTimers: [],
|
|
37000
|
-
lastContent: "",
|
|
37001
|
-
lastBranch: "",
|
|
37002
|
-
lastPayload: null,
|
|
37003
|
-
cwd,
|
|
37004
|
-
subscribers: /* @__PURE__ */ new Set([taskId]),
|
|
37005
|
-
isPolling: false
|
|
37006
|
-
};
|
|
37007
|
-
poll(topLevel, entry).catch((err) => {
|
|
37008
|
-
log.warn({ err, topLevel }, "Initial PR poll failed");
|
|
37009
|
-
});
|
|
37010
|
-
entry.timer = setInterval(() => {
|
|
37011
|
-
poll(topLevel, entry).catch((err) => {
|
|
37012
|
-
log.warn({ err, topLevel }, "PR poll tick failed");
|
|
37013
|
-
});
|
|
37014
|
-
}, PR_POLL_INTERVAL_MS);
|
|
37015
|
-
repos.set(topLevel, entry);
|
|
37016
|
-
log.info({ taskId, topLevel, cwd }, "PR polling started");
|
|
37024
|
+
subscribeToRepo(taskId, topLevel, cwd);
|
|
37017
37025
|
}).catch((err) => {
|
|
37018
37026
|
log.warn({ err, taskId, cwd }, "Failed to resolve git toplevel for PR polling");
|
|
37019
37027
|
});
|
|
@@ -37021,6 +37029,64 @@ function createPRPoller(callbacks, log, resolveTopLevel = getGitTopLevel) {
|
|
|
37021
37029
|
function getCwd(taskId) {
|
|
37022
37030
|
return taskToRepo.get(taskId)?.cwd ?? null;
|
|
37023
37031
|
}
|
|
37032
|
+
function subscribeToRepo(taskId, topLevel, cwd) {
|
|
37033
|
+
taskToRepo.set(taskId, { topLevel, cwd });
|
|
37034
|
+
const existing = repos.get(topLevel);
|
|
37035
|
+
if (existing) {
|
|
37036
|
+
existing.subscribers.add(taskId);
|
|
37037
|
+
if (existing.lastPayload) {
|
|
37038
|
+
callbacks.onPRState({ ...existing.lastPayload, taskId });
|
|
37039
|
+
}
|
|
37040
|
+
log.info({ taskId, topLevel, cwd }, "PR polling joined existing repo");
|
|
37041
|
+
return;
|
|
37042
|
+
}
|
|
37043
|
+
const repoEntry = {
|
|
37044
|
+
timer: null,
|
|
37045
|
+
burstTimers: [],
|
|
37046
|
+
lastContent: "",
|
|
37047
|
+
lastBranch: "",
|
|
37048
|
+
lastPayload: null,
|
|
37049
|
+
cwd,
|
|
37050
|
+
subscribers: /* @__PURE__ */ new Set([taskId]),
|
|
37051
|
+
isPolling: false
|
|
37052
|
+
};
|
|
37053
|
+
poll(topLevel, repoEntry).catch((err) => {
|
|
37054
|
+
log.warn({ err, topLevel }, "Initial PR poll failed");
|
|
37055
|
+
});
|
|
37056
|
+
repoEntry.timer = setInterval(() => {
|
|
37057
|
+
poll(topLevel, repoEntry).catch((err) => {
|
|
37058
|
+
log.warn({ err, topLevel }, "PR poll tick failed");
|
|
37059
|
+
});
|
|
37060
|
+
}, PR_POLL_INTERVAL_MS);
|
|
37061
|
+
repos.set(topLevel, repoEntry);
|
|
37062
|
+
log.info({ taskId, topLevel, cwd }, "PR polling started");
|
|
37063
|
+
}
|
|
37064
|
+
function applyResolvedCwd(taskId, newCwd, newTopLevel, currentTopLevel) {
|
|
37065
|
+
if (newTopLevel === currentTopLevel) {
|
|
37066
|
+
const repoEntry = repos.get(newTopLevel);
|
|
37067
|
+
if (repoEntry) repoEntry.cwd = newCwd;
|
|
37068
|
+
taskToRepo.set(taskId, { topLevel: newTopLevel, cwd: newCwd });
|
|
37069
|
+
log.info({ taskId, topLevel: newTopLevel, cwd: newCwd }, "PR polling cwd updated");
|
|
37070
|
+
return;
|
|
37071
|
+
}
|
|
37072
|
+
removeFromPreviousRepo(taskId);
|
|
37073
|
+
subscribeToRepo(taskId, newTopLevel, newCwd);
|
|
37074
|
+
log.info({ taskId, topLevel: newTopLevel, cwd: newCwd }, "PR polling migrated to new repo");
|
|
37075
|
+
}
|
|
37076
|
+
async function updateCwd(taskId, newCwd) {
|
|
37077
|
+
if (disposed) return;
|
|
37078
|
+
const entry = taskToRepo.get(taskId);
|
|
37079
|
+
if (!entry || entry.cwd === newCwd) return;
|
|
37080
|
+
try {
|
|
37081
|
+
const newTopLevel = await resolveTopLevel(newCwd);
|
|
37082
|
+
if (disposed) return;
|
|
37083
|
+
const currentEntry = taskToRepo.get(taskId);
|
|
37084
|
+
if (!currentEntry || currentEntry.cwd === newCwd) return;
|
|
37085
|
+
applyResolvedCwd(taskId, newCwd, newTopLevel, currentEntry.topLevel);
|
|
37086
|
+
} catch (err) {
|
|
37087
|
+
log.warn({ err, taskId, newCwd }, "Failed to resolve git toplevel for updateCwd");
|
|
37088
|
+
}
|
|
37089
|
+
}
|
|
37024
37090
|
function stopPolling(taskId) {
|
|
37025
37091
|
removeFromPreviousRepo(taskId);
|
|
37026
37092
|
}
|
|
@@ -37056,7 +37122,7 @@ function createPRPoller(callbacks, log, resolveTopLevel = getGitTopLevel) {
|
|
|
37056
37122
|
repos.clear();
|
|
37057
37123
|
taskToRepo.clear();
|
|
37058
37124
|
}
|
|
37059
|
-
return { startPolling, stopPolling, forceRefresh, getCwd, dispose };
|
|
37125
|
+
return { startPolling, stopPolling, forceRefresh, getCwd, updateCwd, dispose };
|
|
37060
37126
|
}
|
|
37061
37127
|
|
|
37062
37128
|
// src/services/serve-factory.ts
|
|
@@ -37367,7 +37433,8 @@ async function rehydrateFromPersistence(persistence, taskManager, log, taskState
|
|
|
37367
37433
|
mode: record?.mode,
|
|
37368
37434
|
initialRoiStartedEmitted: record?.roiStartedEmitted ?? false,
|
|
37369
37435
|
taskCreatedAt: record?.createdAt ?? Date.now(),
|
|
37370
|
-
initialTurnCount: record?.totalTurnCount ?? 0
|
|
37436
|
+
initialTurnCount: record?.totalTurnCount ?? 0,
|
|
37437
|
+
initialOriginalCwd: record?.originalCwd ?? null
|
|
37371
37438
|
});
|
|
37372
37439
|
log({ event: "task_restored", taskId: action.taskId, kind: "resumable" });
|
|
37373
37440
|
break;
|
|
@@ -66609,22 +66676,22 @@ function isTextSelectionAcrossCells({ $from, $to }) {
|
|
|
66609
66676
|
function normalizeSelection(state, tr2, allowTableNodeSelection) {
|
|
66610
66677
|
const sel = (tr2 || state).selection;
|
|
66611
66678
|
const doc3 = (tr2 || state).doc;
|
|
66612
|
-
let
|
|
66679
|
+
let normalize8;
|
|
66613
66680
|
let role;
|
|
66614
66681
|
if (sel instanceof NodeSelection && (role = sel.node.type.spec.tableRole)) {
|
|
66615
|
-
if (role == "cell" || role == "header_cell")
|
|
66682
|
+
if (role == "cell" || role == "header_cell") normalize8 = CellSelection.create(doc3, sel.from);
|
|
66616
66683
|
else if (role == "row") {
|
|
66617
66684
|
const $cell = doc3.resolve(sel.from + 1);
|
|
66618
|
-
|
|
66685
|
+
normalize8 = CellSelection.rowSelection($cell, $cell);
|
|
66619
66686
|
} else if (!allowTableNodeSelection) {
|
|
66620
66687
|
const map3 = TableMap.get(sel.node);
|
|
66621
66688
|
const start = sel.from + 1;
|
|
66622
66689
|
const lastCell = start + map3.map[map3.width * map3.height - 1];
|
|
66623
|
-
|
|
66690
|
+
normalize8 = CellSelection.create(doc3, start + 1, lastCell);
|
|
66624
66691
|
}
|
|
66625
|
-
} else if (sel instanceof TextSelection && isCellBoundarySelection(sel))
|
|
66626
|
-
else if (sel instanceof TextSelection && isTextSelectionAcrossCells(sel))
|
|
66627
|
-
if (
|
|
66692
|
+
} else if (sel instanceof TextSelection && isCellBoundarySelection(sel)) normalize8 = TextSelection.create(doc3, sel.from);
|
|
66693
|
+
else if (sel instanceof TextSelection && isTextSelectionAcrossCells(sel)) normalize8 = TextSelection.create(doc3, sel.$from.start(), sel.$from.end());
|
|
66694
|
+
if (normalize8) (tr2 || (tr2 = state.tr)).setSelection(normalize8);
|
|
66628
66695
|
return tr2;
|
|
66629
66696
|
}
|
|
66630
66697
|
var fixTablesKey = new PluginKey("fix-tables");
|
|
@@ -75659,6 +75726,14 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
|
|
|
75659
75726
|
#onEvent;
|
|
75660
75727
|
#spawnMcpServers;
|
|
75661
75728
|
#harnessTaskIdSetter = null;
|
|
75729
|
+
/**
|
|
75730
|
+
* Mutable slot for the CwdChanged handler. The SDK hook is registered
|
|
75731
|
+
* unconditionally at spawn time and reads through this slot on each
|
|
75732
|
+
* event, so pre-warm (no handler at spawn) → claim (handler installed
|
|
75733
|
+
* via `setOnCwdChanged`) works without re-spawning. Matches the
|
|
75734
|
+
* `#harnessTaskIdSetter` slot pattern.
|
|
75735
|
+
*/
|
|
75736
|
+
#onCwdChangedHandler = null;
|
|
75662
75737
|
/**
|
|
75663
75738
|
* Set to `true` when the message loop finishes (subprocess exited or errored).
|
|
75664
75739
|
* Guards all SDK write operations to prevent "ProcessTransport not ready" errors.
|
|
@@ -75667,13 +75742,28 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
|
|
|
75667
75742
|
#forceKilled = false;
|
|
75668
75743
|
#pidRef;
|
|
75669
75744
|
#exitRef;
|
|
75670
|
-
constructor(query3, controller, onEvent, spawnMcpServers, pidRef, exitRef) {
|
|
75745
|
+
constructor(query3, controller, onEvent, spawnMcpServers, pidRef, exitRef, initialOnCwdChanged) {
|
|
75671
75746
|
this.#query = query3;
|
|
75672
75747
|
this.#controller = controller;
|
|
75673
75748
|
this.#onEvent = onEvent;
|
|
75674
75749
|
this.#spawnMcpServers = spawnMcpServers;
|
|
75675
75750
|
this.#pidRef = pidRef;
|
|
75676
75751
|
this.#exitRef = exitRef;
|
|
75752
|
+
this.#onCwdChangedHandler = initialOnCwdChanged;
|
|
75753
|
+
}
|
|
75754
|
+
/**
|
|
75755
|
+
* Install or replace the CwdChanged handler. Pre-warm spawns register no
|
|
75756
|
+
* handler at startup (no Task yet); when the subprocess is claimed and
|
|
75757
|
+
* adopted by a Thread, the Task wires its `#applyCwdChange` funnel via
|
|
75758
|
+
* this method so subsequent SDK `CwdChanged` events (including
|
|
75759
|
+
* ExitWorktree, which the regex fallback misses) reach the funnel.
|
|
75760
|
+
*/
|
|
75761
|
+
setOnCwdChanged(handler) {
|
|
75762
|
+
this.#onCwdChangedHandler = handler;
|
|
75763
|
+
}
|
|
75764
|
+
/** Dispatch a CwdChanged event to the current handler. Invoked by the SDK hook. */
|
|
75765
|
+
dispatchCwdChanged(newCwd) {
|
|
75766
|
+
this.#onCwdChangedHandler?.(newCwd);
|
|
75677
75767
|
}
|
|
75678
75768
|
/** True once the subprocess has exited and SDK writes will throw. */
|
|
75679
75769
|
get isClosed() {
|
|
@@ -75729,6 +75819,7 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
|
|
|
75729
75819
|
exitRef.resolveReady();
|
|
75730
75820
|
}
|
|
75731
75821
|
const sdkEffort = resolveSupportedEffort(options.effort, log);
|
|
75822
|
+
const subprocessRef = { current: null };
|
|
75732
75823
|
const queryInstance = queryFn({
|
|
75733
75824
|
prompt: controller.iterable(),
|
|
75734
75825
|
options: {
|
|
@@ -75751,7 +75842,21 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
|
|
|
75751
75842
|
spawnClaudeCodeProcess,
|
|
75752
75843
|
resumeSessionAt: options.resumeSessionAt,
|
|
75753
75844
|
forkSession: options.forkSession,
|
|
75754
|
-
pathToClaudeCodeExecutable: options.pathToClaudeCodeExecutable
|
|
75845
|
+
pathToClaudeCodeExecutable: options.pathToClaudeCodeExecutable,
|
|
75846
|
+
hooks: {
|
|
75847
|
+
CwdChanged: [
|
|
75848
|
+
{
|
|
75849
|
+
hooks: [
|
|
75850
|
+
async (input) => {
|
|
75851
|
+
if (input.hook_event_name === "CwdChanged") {
|
|
75852
|
+
subprocessRef.current?.dispatchCwdChanged(input.new_cwd);
|
|
75853
|
+
}
|
|
75854
|
+
return {};
|
|
75855
|
+
}
|
|
75856
|
+
]
|
|
75857
|
+
}
|
|
75858
|
+
]
|
|
75859
|
+
}
|
|
75755
75860
|
}
|
|
75756
75861
|
});
|
|
75757
75862
|
const spawnMcpServers = options.mcpServers ?? {};
|
|
@@ -75761,8 +75866,10 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
|
|
|
75761
75866
|
onEvent,
|
|
75762
75867
|
spawnMcpServers,
|
|
75763
75868
|
pidRef,
|
|
75764
|
-
exitRef
|
|
75869
|
+
exitRef,
|
|
75870
|
+
options.onCwdChanged ?? null
|
|
75765
75871
|
);
|
|
75872
|
+
subprocessRef.current = subprocess;
|
|
75766
75873
|
if (initialContent.length > 0) {
|
|
75767
75874
|
controller.push(toSdkContent(initialContent));
|
|
75768
75875
|
}
|
|
@@ -76071,7 +76178,8 @@ function buildSpawnOptions(args) {
|
|
|
76071
76178
|
log,
|
|
76072
76179
|
metricsCollector,
|
|
76073
76180
|
claudeCodePath,
|
|
76074
|
-
disallowedTools
|
|
76181
|
+
disallowedTools,
|
|
76182
|
+
onCwdChanged
|
|
76075
76183
|
} = args;
|
|
76076
76184
|
const vanillaMode = isVanillaAgentMode();
|
|
76077
76185
|
const mcpServers = {
|
|
@@ -76128,7 +76236,8 @@ ${additionalSystemPrompt}` : basePrompt;
|
|
|
76128
76236
|
limitMB: sample.limitMB,
|
|
76129
76237
|
thresholdExceeded: sample.thresholdExceeded
|
|
76130
76238
|
});
|
|
76131
|
-
}
|
|
76239
|
+
},
|
|
76240
|
+
onCwdChanged
|
|
76132
76241
|
};
|
|
76133
76242
|
}
|
|
76134
76243
|
async function detectInitialCapabilities(tokenStore, settings) {
|
|
@@ -83101,6 +83210,9 @@ var DirectApiSubprocess = class _DirectApiSubprocess {
|
|
|
83101
83210
|
/** No-op in Phase 1 -- harness task context not used by direct API mode. */
|
|
83102
83211
|
setHarnessTaskId(_taskId) {
|
|
83103
83212
|
}
|
|
83213
|
+
/** No-op: direct API mode does not surface SDK CwdChanged events. */
|
|
83214
|
+
setOnCwdChanged(_handler) {
|
|
83215
|
+
}
|
|
83104
83216
|
async #stream() {
|
|
83105
83217
|
this.#abortController = new AbortController();
|
|
83106
83218
|
const startTime = Date.now();
|
|
@@ -84572,16 +84684,18 @@ async function buildRateLimitStore(dataDir, opts) {
|
|
|
84572
84684
|
}
|
|
84573
84685
|
};
|
|
84574
84686
|
}
|
|
84687
|
+
function deriveWindowFromEvent(incoming, eventTime) {
|
|
84688
|
+
if (!isWindowKey(incoming.rateLimitType)) return void 0;
|
|
84689
|
+
const utilization = typeof incoming.utilization === "number" ? incoming.utilization : incoming.status === "rejected" ? 1 : void 0;
|
|
84690
|
+
if (utilization === void 0) return void 0;
|
|
84691
|
+
return { utilization, resetsAt: incoming.resetsAt, updatedAt: eventTime };
|
|
84692
|
+
}
|
|
84575
84693
|
function mergeEvent(prev, incoming, eventTime) {
|
|
84576
84694
|
const prevByWindow = prev?.byWindow ?? {};
|
|
84577
84695
|
const byWindow = { ...prevByWindow };
|
|
84578
|
-
|
|
84579
|
-
|
|
84580
|
-
|
|
84581
|
-
resetsAt: incoming.resetsAt,
|
|
84582
|
-
updatedAt: eventTime
|
|
84583
|
-
};
|
|
84584
|
-
byWindow[incoming.rateLimitType] = window2;
|
|
84696
|
+
const derived = deriveWindowFromEvent(incoming, eventTime);
|
|
84697
|
+
if (derived && isWindowKey(incoming.rateLimitType)) {
|
|
84698
|
+
byWindow[incoming.rateLimitType] = derived;
|
|
84585
84699
|
}
|
|
84586
84700
|
const nowInOverage = incoming.isUsingOverage === true;
|
|
84587
84701
|
const wasInOverage = prev?.overageStartedAt !== void 0;
|
|
@@ -84635,12 +84749,15 @@ function migrateV1toV2(v1) {
|
|
|
84635
84749
|
for (const [accountKey, record] of Object.entries(v1.records)) {
|
|
84636
84750
|
const { info, updatedAt } = record;
|
|
84637
84751
|
const byWindow = {};
|
|
84638
|
-
if (isWindowKey(info.rateLimitType)
|
|
84639
|
-
|
|
84640
|
-
|
|
84641
|
-
|
|
84642
|
-
|
|
84643
|
-
|
|
84752
|
+
if (isWindowKey(info.rateLimitType)) {
|
|
84753
|
+
const utilization = typeof info.utilization === "number" ? info.utilization : info.status === "rejected" ? 1 : void 0;
|
|
84754
|
+
if (utilization !== void 0) {
|
|
84755
|
+
byWindow[info.rateLimitType] = {
|
|
84756
|
+
utilization,
|
|
84757
|
+
resetsAt: info.resetsAt,
|
|
84758
|
+
updatedAt
|
|
84759
|
+
};
|
|
84760
|
+
}
|
|
84644
84761
|
}
|
|
84645
84762
|
const nowInOverage = info.isUsingOverage === true;
|
|
84646
84763
|
records[accountKey] = {
|
|
@@ -86967,6 +87084,9 @@ var Thread = class {
|
|
|
86967
87084
|
this.#subprocess = config2.adoptedSubprocess.subprocess;
|
|
86968
87085
|
this.#effectiveSpawnMode = { kind: "fresh" };
|
|
86969
87086
|
config2.adoptedSubprocess.redirectEvents((event) => this.#enqueueSubprocessEvent(event));
|
|
87087
|
+
if (config2.onCwdChanged) {
|
|
87088
|
+
this.#subprocess.setOnCwdChanged(config2.onCwdChanged);
|
|
87089
|
+
}
|
|
86970
87090
|
}
|
|
86971
87091
|
const warmState = "warm_idle";
|
|
86972
87092
|
const initialSnapshot = config2.adoptedSubprocess ? { state: warmState, sessionId: null } : config2.initialState && config2.initialState !== "cold_idle" ? {
|
|
@@ -87129,6 +87249,7 @@ var Thread = class {
|
|
|
87129
87249
|
this.#stateChangeListeners.clear();
|
|
87130
87250
|
this.#manager.dispose();
|
|
87131
87251
|
await this.#asyncQueue;
|
|
87252
|
+
this.#subprocess?.setOnCwdChanged(null);
|
|
87132
87253
|
this.#subprocess?.close();
|
|
87133
87254
|
this.#subprocess = null;
|
|
87134
87255
|
}
|
|
@@ -87293,7 +87414,8 @@ ${conversationReplay}` : conversationReplay;
|
|
|
87293
87414
|
this.#config.threadId,
|
|
87294
87415
|
systemPrompt,
|
|
87295
87416
|
this.#config.disallowedTools,
|
|
87296
|
-
this.#config.mode
|
|
87417
|
+
this.#config.mode,
|
|
87418
|
+
this.#config.onCwdChanged
|
|
87297
87419
|
);
|
|
87298
87420
|
};
|
|
87299
87421
|
doSpawn().catch((err) => {
|
|
@@ -87369,6 +87491,7 @@ ${conversationReplay}` : conversationReplay;
|
|
|
87369
87491
|
});
|
|
87370
87492
|
}
|
|
87371
87493
|
#handleClose() {
|
|
87494
|
+
this.#subprocess?.setOnCwdChanged(null);
|
|
87372
87495
|
this.#subprocess?.close();
|
|
87373
87496
|
this.#subprocess = null;
|
|
87374
87497
|
}
|
|
@@ -87585,6 +87708,47 @@ ${conversationReplay}` : conversationReplay;
|
|
|
87585
87708
|
}
|
|
87586
87709
|
});
|
|
87587
87710
|
}
|
|
87711
|
+
/**
|
|
87712
|
+
* Deduped append for echoed user messages. Browser-originated writes carry
|
|
87713
|
+
* a correlationId so dedup catches the twin row via `corr:<id>`. Synthetic
|
|
87714
|
+
* pushes (plan injection, side threads, model-set) have no correlationId —
|
|
87715
|
+
* we pass `event.sdkUuid` so the `sdk:<uuid>` pass catches SDK replays
|
|
87716
|
+
* instead of falling back to the 60s fingerprint window, which would
|
|
87717
|
+
* wrongly collapse two distinct synthetic pushes with identical content.
|
|
87718
|
+
*/
|
|
87719
|
+
async #handleUserMessageEcho(event) {
|
|
87720
|
+
const echoMsgId = crypto.randomUUID();
|
|
87721
|
+
const meta = this.#callbacks.onBeforeStoreUserMessage?.(event);
|
|
87722
|
+
const result = await this.#config.store.appendMessageDeduped(
|
|
87723
|
+
{
|
|
87724
|
+
channelId: this.#config.channelId,
|
|
87725
|
+
messageId: echoMsgId,
|
|
87726
|
+
participantId: meta?.participantId ?? this.#config.humanParticipantId ?? PENDING_AGENT_PARTICIPANT_ID,
|
|
87727
|
+
senderKind: "human",
|
|
87728
|
+
content: meta?.content ?? event.content,
|
|
87729
|
+
timestamp: Date.now(),
|
|
87730
|
+
model: meta?.model ?? null,
|
|
87731
|
+
reasoningEffort: meta?.reasoningEffort ?? null,
|
|
87732
|
+
permissionMode: meta?.permissionMode ?? null,
|
|
87733
|
+
...meta?.isSynthetic && { isSynthetic: true },
|
|
87734
|
+
...meta?.correlationId && { correlationId: meta.correlationId },
|
|
87735
|
+
...event.sdkUuid && { sdkUuid: event.sdkUuid }
|
|
87736
|
+
},
|
|
87737
|
+
{}
|
|
87738
|
+
);
|
|
87739
|
+
if (result.isDuplicate) {
|
|
87740
|
+
this.#config.log({
|
|
87741
|
+
event: "thread_echo_dedup_hit",
|
|
87742
|
+
threadId: this.#config.threadId,
|
|
87743
|
+
dedupKey: result.dedupKey,
|
|
87744
|
+
seqNo: result.seqNo
|
|
87745
|
+
});
|
|
87746
|
+
}
|
|
87747
|
+
this.#callbacks.onMessageStored?.(result.seqNo, echoMsgId, "human", event.sdkUuid);
|
|
87748
|
+
if (meta?.correlationId) {
|
|
87749
|
+
this.#callbacks.onUserMessageConfirmed?.(meta.correlationId, event.sdkUuid);
|
|
87750
|
+
}
|
|
87751
|
+
}
|
|
87588
87752
|
async #handleSubprocessEvent(event) {
|
|
87589
87753
|
switch (event.type) {
|
|
87590
87754
|
case "init_received":
|
|
@@ -87639,25 +87803,7 @@ ${conversationReplay}` : conversationReplay;
|
|
|
87639
87803
|
case "rate_limit_error":
|
|
87640
87804
|
break;
|
|
87641
87805
|
case "user_message_echo": {
|
|
87642
|
-
|
|
87643
|
-
const meta = this.#callbacks.onBeforeStoreUserMessage?.(event);
|
|
87644
|
-
const echoSeqNo = await this.#config.store.appendMessage({
|
|
87645
|
-
channelId: this.#config.channelId,
|
|
87646
|
-
messageId: echoMsgId,
|
|
87647
|
-
participantId: meta?.participantId ?? this.#config.humanParticipantId ?? PENDING_AGENT_PARTICIPANT_ID,
|
|
87648
|
-
senderKind: "human",
|
|
87649
|
-
content: meta?.content ?? event.content,
|
|
87650
|
-
timestamp: Date.now(),
|
|
87651
|
-
model: meta?.model ?? null,
|
|
87652
|
-
reasoningEffort: meta?.reasoningEffort ?? null,
|
|
87653
|
-
permissionMode: meta?.permissionMode ?? null,
|
|
87654
|
-
...meta?.isSynthetic && { isSynthetic: true },
|
|
87655
|
-
...meta?.correlationId && { correlationId: meta.correlationId }
|
|
87656
|
-
});
|
|
87657
|
-
this.#callbacks.onMessageStored?.(echoSeqNo, echoMsgId, "human", event.sdkUuid);
|
|
87658
|
-
if (meta?.correlationId) {
|
|
87659
|
-
this.#callbacks.onUserMessageConfirmed?.(meta.correlationId, event.sdkUuid);
|
|
87660
|
-
}
|
|
87806
|
+
await this.#handleUserMessageEcho(event);
|
|
87661
87807
|
break;
|
|
87662
87808
|
}
|
|
87663
87809
|
case "assistant_message": {
|
|
@@ -87735,6 +87881,7 @@ function skipForMainChannel(content) {
|
|
|
87735
87881
|
|
|
87736
87882
|
// src/services/plan/plan-handler.ts
|
|
87737
87883
|
import { existsSync as existsSync7, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
87884
|
+
import { readFile as readFile30 } from "fs/promises";
|
|
87738
87885
|
import { homedir as homedir4 } from "os";
|
|
87739
87886
|
import { join as join39 } from "path";
|
|
87740
87887
|
|
|
@@ -87812,6 +87959,80 @@ var PlanFileBridge = class _PlanFileBridge {
|
|
|
87812
87959
|
});
|
|
87813
87960
|
}, PERIODIC_REREAD_MS);
|
|
87814
87961
|
}
|
|
87962
|
+
/**
|
|
87963
|
+
* Re-initialize the bridge for a plan file known from a prior daemon session.
|
|
87964
|
+
*
|
|
87965
|
+
* Differs from `attach()` in that the caller explicitly decides whether to
|
|
87966
|
+
* preserve CRDT content. This lets PlanHandler do a three-way compare
|
|
87967
|
+
* (file vs CRDT vs last stored PlanVersion) and tell the bridge which side
|
|
87968
|
+
* is authoritative:
|
|
87969
|
+
*
|
|
87970
|
+
* - `preserveCrdt: false` — file is truth (fresh daemon start, or Claude
|
|
87971
|
+
* rewrote the plan while the daemon was down). Overwrite CRDT with file
|
|
87972
|
+
* content, same as `attach()`.
|
|
87973
|
+
* - `preserveCrdt: true` — CRDT has edits not yet written to disk (browser
|
|
87974
|
+
* edited before the daemon's debounced write-back fired). Keep CRDT
|
|
87975
|
+
* content; the subscription below will flush it on the next loro change.
|
|
87976
|
+
*
|
|
87977
|
+
* Returns `{ fileContent }` so callers can emit catch-up versions, or
|
|
87978
|
+
* `null` if the file is unreadable.
|
|
87979
|
+
*/
|
|
87980
|
+
async reattach(filePath, options) {
|
|
87981
|
+
if (this.#disposed) return null;
|
|
87982
|
+
this.#dirWatcher?.close();
|
|
87983
|
+
this.#dirWatcher = null;
|
|
87984
|
+
this.#crdtUnsub?.();
|
|
87985
|
+
this.#crdtUnsub = null;
|
|
87986
|
+
if (this.#periodicRereadTimer) {
|
|
87987
|
+
clearInterval(this.#periodicRereadTimer);
|
|
87988
|
+
this.#periodicRereadTimer = null;
|
|
87989
|
+
}
|
|
87990
|
+
let fileContent;
|
|
87991
|
+
try {
|
|
87992
|
+
fileContent = await this.#readFileWithRetry(filePath);
|
|
87993
|
+
} catch (err) {
|
|
87994
|
+
this.#config.log({
|
|
87995
|
+
event: "plan_bridge_reattach_read_failed",
|
|
87996
|
+
taskId: this.#config.taskId,
|
|
87997
|
+
filePath,
|
|
87998
|
+
error: err instanceof Error ? err.message : String(err)
|
|
87999
|
+
});
|
|
88000
|
+
return null;
|
|
88001
|
+
}
|
|
88002
|
+
this.#filePath = filePath;
|
|
88003
|
+
this.#config.planRepo.setMetadata(this.#config.taskId, filePath);
|
|
88004
|
+
this.#lastWrittenHash = contentHash(fileContent);
|
|
88005
|
+
if (!options.preserveCrdt) {
|
|
88006
|
+
this.#config.planRepo.updateContent(this.#config.taskId, fileContent);
|
|
88007
|
+
}
|
|
88008
|
+
this.#hasAttachedContent = true;
|
|
88009
|
+
this.#config.log({
|
|
88010
|
+
event: "plan_bridge_reattached",
|
|
88011
|
+
taskId: this.#config.taskId,
|
|
88012
|
+
filePath,
|
|
88013
|
+
crdtPreserved: options.preserveCrdt,
|
|
88014
|
+
fileLength: fileContent.length
|
|
88015
|
+
});
|
|
88016
|
+
this.#startDirectoryWatcher(filePath);
|
|
88017
|
+
this.#startCrdtSubscription();
|
|
88018
|
+
this.#periodicRereadTimer = setInterval(() => {
|
|
88019
|
+
this.#handleFileChanged().catch(() => {
|
|
88020
|
+
});
|
|
88021
|
+
}, PERIODIC_REREAD_MS);
|
|
88022
|
+
if (options.preserveCrdt) {
|
|
88023
|
+
try {
|
|
88024
|
+
await this.flushWriteBack();
|
|
88025
|
+
} catch (err) {
|
|
88026
|
+
this.#config.log({
|
|
88027
|
+
event: "plan_bridge_reattach_flush_failed",
|
|
88028
|
+
taskId: this.#config.taskId,
|
|
88029
|
+
filePath,
|
|
88030
|
+
error: err instanceof Error ? err.message : String(err)
|
|
88031
|
+
});
|
|
88032
|
+
}
|
|
88033
|
+
}
|
|
88034
|
+
return { fileContent };
|
|
88035
|
+
}
|
|
87815
88036
|
/**
|
|
87816
88037
|
* Watch the plans DIRECTORY (not the file) for changes.
|
|
87817
88038
|
* This survives inode changes from atomic writes (tmp + rename).
|
|
@@ -88234,8 +88455,177 @@ var PlanHandler = class {
|
|
|
88234
88455
|
this.#lastPersistedPlanDetection = deps.initialPlanDetection;
|
|
88235
88456
|
this.#planPublished = true;
|
|
88236
88457
|
this.#processedPlanToolUseIds.add(deps.initialPlanDetection.toolUseId);
|
|
88458
|
+
const seedFilePath = deps.initialPlanDetection.filePath;
|
|
88459
|
+
const seedToolUseId = deps.initialPlanDetection.toolUseId;
|
|
88460
|
+
deps.enqueueAsync(() => this.#rehydratePlanFileBridge(seedFilePath, seedToolUseId));
|
|
88461
|
+
}
|
|
88462
|
+
}
|
|
88463
|
+
async #rehydratePlanFileBridge(filePath, toolUseId) {
|
|
88464
|
+
if (this.#deps.isDisposed()) return;
|
|
88465
|
+
if (!existsSync7(filePath)) {
|
|
88466
|
+
this.#deps.log({
|
|
88467
|
+
event: "plan_bridge_rehydrate_file_missing",
|
|
88468
|
+
taskId: this.#deps.taskId,
|
|
88469
|
+
filePath
|
|
88470
|
+
});
|
|
88471
|
+
return;
|
|
88472
|
+
}
|
|
88473
|
+
const fileContent = await this.#readFileForRehydrate(filePath);
|
|
88474
|
+
if (fileContent === null || this.#deps.isDisposed()) return;
|
|
88475
|
+
const versions = await this.#deps.annotationStore.getPlanVersions(this.#deps.taskId);
|
|
88476
|
+
if (this.#deps.isDisposed()) return;
|
|
88477
|
+
const lastVersionMarkdown = versions.at(-1)?.markdown ?? "";
|
|
88478
|
+
const preserveCrdt = this.#shouldPreserveCrdt(fileContent, lastVersionMarkdown);
|
|
88479
|
+
const result = await this.#ensureBridgeAttached(filePath, toolUseId, {
|
|
88480
|
+
mode: "rehydrate",
|
|
88481
|
+
preserveCrdt
|
|
88482
|
+
});
|
|
88483
|
+
if (!result || this.#deps.isDisposed()) return;
|
|
88484
|
+
await this.#maybeEmitRehydrateCatchup(lastVersionMarkdown, preserveCrdt, toolUseId);
|
|
88485
|
+
}
|
|
88486
|
+
/**
|
|
88487
|
+
* Three-way compare of {file on disk, CRDT content, last stored PlanVersion}:
|
|
88488
|
+
*
|
|
88489
|
+
* Case 1 (all equal) → preserveCrdt=false (no-op attach)
|
|
88490
|
+
* Case 2 (file drifted) → preserveCrdt=false (file wins)
|
|
88491
|
+
* Case 3 (browser-edited CRDT) → preserveCrdt=true (keep in-flight edits)
|
|
88492
|
+
* Case 4 (both drifted) → preserveCrdt=false (file wins)
|
|
88493
|
+
*
|
|
88494
|
+
* Case 3 also requires a non-empty lastVersionMarkdown — otherwise a
|
|
88495
|
+
* brand-new task with no prior stored versions would preserve whatever
|
|
88496
|
+
* happens to be in CRDT (e.g. a stale browser session's edits on a fresh
|
|
88497
|
+
* epoch) over an authoritative empty file.
|
|
88498
|
+
*/
|
|
88499
|
+
#shouldPreserveCrdt(fileContent, lastVersionMarkdown) {
|
|
88500
|
+
if (lastVersionMarkdown === "") return false;
|
|
88501
|
+
const crdtContent = this.#deps.planRepo.getContent(this.#deps.taskId);
|
|
88502
|
+
const fileMatchesLast = fileContent === lastVersionMarkdown;
|
|
88503
|
+
const crdtHasUnsavedEdits = crdtContent.length > 0 && crdtContent !== lastVersionMarkdown;
|
|
88504
|
+
return fileMatchesLast && crdtHasUnsavedEdits;
|
|
88505
|
+
}
|
|
88506
|
+
/**
|
|
88507
|
+
* Post-attach catch-up: read whatever CRDT now holds (file content or
|
|
88508
|
+
* preserved CRDT content) and emit a single version if it drifted from
|
|
88509
|
+
* `lastVersionMarkdown`. Covers file-drifted and browser-edited cases.
|
|
88510
|
+
*
|
|
88511
|
+
* Source labeling follows authoritative side: preserved CRDT = 'human',
|
|
88512
|
+
* file-overwritten CRDT = 'agent'. Mislabeling would corrupt
|
|
88513
|
+
* `findLastAgentVersion`'s baseline for plan-review diffs.
|
|
88514
|
+
*/
|
|
88515
|
+
async #maybeEmitRehydrateCatchup(lastVersionMarkdown, preserveCrdt, toolUseId) {
|
|
88516
|
+
const postAttachMarkdown = this.#deps.planRepo.getContent(this.#deps.taskId);
|
|
88517
|
+
if (postAttachMarkdown.length === 0 || postAttachMarkdown === lastVersionMarkdown) return;
|
|
88518
|
+
const source = preserveCrdt ? "human" : "agent";
|
|
88519
|
+
await this.#emitCatchupVersion(postAttachMarkdown, toolUseId, source);
|
|
88520
|
+
}
|
|
88521
|
+
async #readFileForRehydrate(filePath) {
|
|
88522
|
+
try {
|
|
88523
|
+
return await readFile30(filePath, "utf-8");
|
|
88524
|
+
} catch (err) {
|
|
88525
|
+
this.#deps.log({
|
|
88526
|
+
event: "plan_bridge_rehydrate_read_failed",
|
|
88527
|
+
taskId: this.#deps.taskId,
|
|
88528
|
+
filePath,
|
|
88529
|
+
error: err instanceof Error ? err.message : String(err)
|
|
88530
|
+
});
|
|
88531
|
+
return null;
|
|
88532
|
+
}
|
|
88533
|
+
}
|
|
88534
|
+
async #emitCatchupVersion(markdown, toolUseId, source) {
|
|
88535
|
+
const version = {
|
|
88536
|
+
markdown,
|
|
88537
|
+
timestamp: Date.now(),
|
|
88538
|
+
source,
|
|
88539
|
+
toolUseId
|
|
88540
|
+
};
|
|
88541
|
+
try {
|
|
88542
|
+
await this.#deps.annotationStore.addPlanVersion(this.#deps.taskId, version);
|
|
88543
|
+
const msg = {
|
|
88544
|
+
type: "annotation_version_added",
|
|
88545
|
+
taskId: this.#deps.taskId,
|
|
88546
|
+
version
|
|
88547
|
+
};
|
|
88548
|
+
const send = this.#deps.getSendControlMessage();
|
|
88549
|
+
if (send) {
|
|
88550
|
+
send(msg);
|
|
88551
|
+
} else {
|
|
88552
|
+
this.#deps.enqueueControlMessage(msg);
|
|
88553
|
+
}
|
|
88554
|
+
} catch (err) {
|
|
88555
|
+
this.#deps.log({
|
|
88556
|
+
event: "plan_version_add_failed",
|
|
88557
|
+
taskId: this.#deps.taskId,
|
|
88558
|
+
error: err instanceof Error ? err.message : String(err)
|
|
88559
|
+
});
|
|
88237
88560
|
}
|
|
88238
88561
|
}
|
|
88562
|
+
/**
|
|
88563
|
+
* Ensure a PlanFileBridge is attached for this task at the given file path.
|
|
88564
|
+
* Creates the bridge lazily and routes attach vs reattach based on options:
|
|
88565
|
+
*
|
|
88566
|
+
* - `mode: 'fresh'` — new ExitPlanMode cycle, no prior context. Uses
|
|
88567
|
+
* `attach()` which overwrites CRDT with file content.
|
|
88568
|
+
* - `mode: 'rehydrate'` — daemon restart. Uses `reattach()` with an
|
|
88569
|
+
* explicit `preserveCrdt` flag so the caller's three-way compare picks
|
|
88570
|
+
* the authoritative side.
|
|
88571
|
+
*
|
|
88572
|
+
* Noop if the bridge is already attached to the same filePath. Re-attaches
|
|
88573
|
+
* if the bridge points at a different filePath (Scenario A: a 2nd
|
|
88574
|
+
* ExitPlanMode cycle writes to a new UUID).
|
|
88575
|
+
*/
|
|
88576
|
+
async #ensureBridgeAttached(filePath, toolUseId, options) {
|
|
88577
|
+
if (this.#deps.isDisposed()) return null;
|
|
88578
|
+
const isFirstAttach = !this.#planFileBridge;
|
|
88579
|
+
if (!this.#planFileBridge) {
|
|
88580
|
+
this.#planFileBridge = this.#createPlanFileBridge(toolUseId);
|
|
88581
|
+
} else if (this.#planFileBridge.filePath === filePath) {
|
|
88582
|
+
return { isFirstAttach: false };
|
|
88583
|
+
}
|
|
88584
|
+
if (options.mode === "rehydrate") {
|
|
88585
|
+
const result = await this.#planFileBridge.reattach(filePath, {
|
|
88586
|
+
preserveCrdt: options.preserveCrdt
|
|
88587
|
+
});
|
|
88588
|
+
if (!result) return null;
|
|
88589
|
+
} else {
|
|
88590
|
+
await this.#planFileBridge.attach(filePath);
|
|
88591
|
+
}
|
|
88592
|
+
if (this.#deps.isDisposed()) return null;
|
|
88593
|
+
return { isFirstAttach };
|
|
88594
|
+
}
|
|
88595
|
+
#createPlanFileBridge(fallbackToolUseId) {
|
|
88596
|
+
return new PlanFileBridge({
|
|
88597
|
+
taskId: this.#deps.taskId,
|
|
88598
|
+
planRepo: this.#deps.planRepo,
|
|
88599
|
+
log: this.#deps.log,
|
|
88600
|
+
onBeforeContentUpdate: (currentMarkdown) => {
|
|
88601
|
+
const version = {
|
|
88602
|
+
markdown: currentMarkdown,
|
|
88603
|
+
timestamp: Date.now(),
|
|
88604
|
+
source: "agent",
|
|
88605
|
+
toolUseId: this.#lastPlanDetection?.toolUseId ?? fallbackToolUseId
|
|
88606
|
+
};
|
|
88607
|
+
this.#deps.annotationStore.addPlanVersion(this.#deps.taskId, version).then(() => {
|
|
88608
|
+
const msg = {
|
|
88609
|
+
type: "annotation_version_added",
|
|
88610
|
+
taskId: this.#deps.taskId,
|
|
88611
|
+
version
|
|
88612
|
+
};
|
|
88613
|
+
const send = this.#deps.getSendControlMessage();
|
|
88614
|
+
if (send) {
|
|
88615
|
+
send(msg);
|
|
88616
|
+
} else {
|
|
88617
|
+
this.#deps.enqueueControlMessage(msg);
|
|
88618
|
+
}
|
|
88619
|
+
}).catch((err) => {
|
|
88620
|
+
this.#deps.log({
|
|
88621
|
+
event: "plan_version_add_failed",
|
|
88622
|
+
taskId: this.#deps.taskId,
|
|
88623
|
+
error: err instanceof Error ? err.message : String(err)
|
|
88624
|
+
});
|
|
88625
|
+
});
|
|
88626
|
+
}
|
|
88627
|
+
});
|
|
88628
|
+
}
|
|
88239
88629
|
/**
|
|
88240
88630
|
* Persist a detection (or null clear) with shadow-pending semantics:
|
|
88241
88631
|
* optimistically update #lastPlanDetection so concurrent reads see the new
|
|
@@ -88292,14 +88682,17 @@ var PlanHandler = class {
|
|
|
88292
88682
|
});
|
|
88293
88683
|
if (this.#approvalReceived) {
|
|
88294
88684
|
this.#approvalReceived = false;
|
|
88295
|
-
this.#
|
|
88296
|
-
|
|
88297
|
-
|
|
88298
|
-
|
|
88299
|
-
|
|
88300
|
-
|
|
88685
|
+
const previous = this.#lastPlanDetection;
|
|
88686
|
+
if (previous) {
|
|
88687
|
+
this.#persistDetection({ ...previous, approved: true }).catch((err) => {
|
|
88688
|
+
this.#deps.log({
|
|
88689
|
+
event: "plan_persist_approve_failed",
|
|
88690
|
+
taskId: this.#deps.taskId,
|
|
88691
|
+
toolUseId,
|
|
88692
|
+
error: err instanceof Error ? err.message : String(err)
|
|
88693
|
+
});
|
|
88301
88694
|
});
|
|
88302
|
-
}
|
|
88695
|
+
}
|
|
88303
88696
|
this.#deps.log({
|
|
88304
88697
|
event: "exit_plan_mode_approved",
|
|
88305
88698
|
taskId: this.#deps.taskId,
|
|
@@ -88368,9 +88761,10 @@ var PlanHandler = class {
|
|
|
88368
88761
|
});
|
|
88369
88762
|
}
|
|
88370
88763
|
detectPlanEvents(content) {
|
|
88371
|
-
if (this.#approvalReceived
|
|
88764
|
+
if (this.#approvalReceived) return;
|
|
88372
88765
|
for (const block2 of content) {
|
|
88373
88766
|
if (block2.type !== "tool_use" || block2.toolName !== "ExitPlanMode") continue;
|
|
88767
|
+
if (this.#processedPlanToolUseIds.has(block2.toolUseId)) continue;
|
|
88374
88768
|
this.#deps.log({
|
|
88375
88769
|
event: "exit_plan_mode_detected",
|
|
88376
88770
|
taskId: this.#deps.taskId,
|
|
@@ -88402,7 +88796,7 @@ var PlanHandler = class {
|
|
|
88402
88796
|
*/
|
|
88403
88797
|
replayPlanDetection(send) {
|
|
88404
88798
|
if (!this.#lastPlanDetection) return;
|
|
88405
|
-
const { toolUseId, filePath, planDocId, allowedPrompts } = this.#lastPlanDetection;
|
|
88799
|
+
const { toolUseId, filePath, planDocId, allowedPrompts, approved } = this.#lastPlanDetection;
|
|
88406
88800
|
const markdown = this.#deps.planRepo.getContent(this.#deps.taskId);
|
|
88407
88801
|
send({
|
|
88408
88802
|
type: "plan_detected",
|
|
@@ -88411,7 +88805,8 @@ var PlanHandler = class {
|
|
|
88411
88805
|
filePath,
|
|
88412
88806
|
planDocId,
|
|
88413
88807
|
markdown,
|
|
88414
|
-
allowedPrompts
|
|
88808
|
+
allowedPrompts,
|
|
88809
|
+
approved
|
|
88415
88810
|
});
|
|
88416
88811
|
}
|
|
88417
88812
|
/**
|
|
@@ -88534,51 +88929,32 @@ var PlanHandler = class {
|
|
|
88534
88929
|
this.#pendingPlanAllowedPrompts.delete(toolUseId);
|
|
88535
88930
|
this.#deps.enqueueAsync(async () => {
|
|
88536
88931
|
if (this.#deps.isDisposed()) return;
|
|
88537
|
-
const
|
|
88538
|
-
|
|
88539
|
-
|
|
88540
|
-
|
|
88541
|
-
planRepo: this.#deps.planRepo,
|
|
88542
|
-
log: this.#deps.log,
|
|
88543
|
-
onBeforeContentUpdate: (currentMarkdown) => {
|
|
88544
|
-
const version = {
|
|
88545
|
-
markdown: currentMarkdown,
|
|
88546
|
-
timestamp: Date.now(),
|
|
88547
|
-
source: "agent",
|
|
88548
|
-
toolUseId: this.#lastPlanDetection?.toolUseId ?? toolUseId
|
|
88549
|
-
};
|
|
88550
|
-
this.#deps.annotationStore.addPlanVersion(this.#deps.taskId, version).then(() => {
|
|
88551
|
-
this.#deps.getSendControlMessage()?.({
|
|
88552
|
-
type: "annotation_version_added",
|
|
88553
|
-
taskId: this.#deps.taskId,
|
|
88554
|
-
version
|
|
88555
|
-
});
|
|
88556
|
-
}).catch((err) => {
|
|
88557
|
-
this.#deps.log({
|
|
88558
|
-
event: "plan_version_add_failed",
|
|
88559
|
-
taskId: this.#deps.taskId,
|
|
88560
|
-
error: err instanceof Error ? err.message : String(err)
|
|
88561
|
-
});
|
|
88562
|
-
});
|
|
88563
|
-
}
|
|
88564
|
-
});
|
|
88565
|
-
}
|
|
88566
|
-
await this.#planFileBridge.attach(filePath);
|
|
88932
|
+
const attachResult = await this.#ensureBridgeAttached(filePath, toolUseId, {
|
|
88933
|
+
mode: "fresh"
|
|
88934
|
+
});
|
|
88935
|
+
if (!attachResult) return;
|
|
88567
88936
|
const markdown = this.#deps.planRepo.getContent(this.#deps.taskId);
|
|
88568
|
-
if (
|
|
88569
|
-
const
|
|
88570
|
-
|
|
88571
|
-
|
|
88572
|
-
|
|
88573
|
-
|
|
88574
|
-
|
|
88575
|
-
|
|
88576
|
-
|
|
88577
|
-
|
|
88578
|
-
|
|
88579
|
-
|
|
88580
|
-
|
|
88937
|
+
if (markdown.length > 0) {
|
|
88938
|
+
const existingVersions = await this.#deps.annotationStore.getPlanVersions(
|
|
88939
|
+
this.#deps.taskId
|
|
88940
|
+
);
|
|
88941
|
+
const lastVersion = existingVersions.at(-1);
|
|
88942
|
+
if (lastVersion?.markdown !== markdown) {
|
|
88943
|
+
const initialVersion = {
|
|
88944
|
+
markdown,
|
|
88945
|
+
timestamp: Date.now(),
|
|
88946
|
+
source: "agent",
|
|
88947
|
+
toolUseId
|
|
88948
|
+
};
|
|
88949
|
+
await this.#deps.annotationStore.addPlanVersion(this.#deps.taskId, initialVersion);
|
|
88950
|
+
this.#deps.getSendControlMessage()?.({
|
|
88951
|
+
type: "annotation_version_added",
|
|
88952
|
+
taskId: this.#deps.taskId,
|
|
88953
|
+
version: initialVersion
|
|
88954
|
+
});
|
|
88955
|
+
}
|
|
88581
88956
|
}
|
|
88957
|
+
const isFirstAttach = attachResult.isFirstAttach;
|
|
88582
88958
|
const pendingDetection = {
|
|
88583
88959
|
toolUseId,
|
|
88584
88960
|
filePath,
|
|
@@ -89230,6 +89606,16 @@ var ResourcePushManager = class {
|
|
|
89230
89606
|
#batchTimer;
|
|
89231
89607
|
#pushCountThisTurn = 0;
|
|
89232
89608
|
#pushedThisTurn = /* @__PURE__ */ new Set();
|
|
89609
|
+
/**
|
|
89610
|
+
* Ref-counted session-lifetime claims. Each `markPushed` increments;
|
|
89611
|
+
* each `unmarkPushed` decrements. Entries are deleted when the count
|
|
89612
|
+
* hits zero. Used for "inject exactly once per subprocess session"
|
|
89613
|
+
* callers (the task-list prepend) where two concurrent paths (sync
|
|
89614
|
+
* prepend + async push pipeline) may both legitimately claim the URI.
|
|
89615
|
+
* A plain Set would let an optimistic claim's rollback erase another
|
|
89616
|
+
* caller's successful mark; the ref count prevents that.
|
|
89617
|
+
*/
|
|
89618
|
+
#pushedEverCount = /* @__PURE__ */ new Map();
|
|
89233
89619
|
#disposed = false;
|
|
89234
89620
|
#epoch = 0;
|
|
89235
89621
|
#flushInProgress = Promise.resolve();
|
|
@@ -89282,12 +89668,46 @@ var ResourcePushManager = class {
|
|
|
89282
89668
|
return this.#pushedThisTurn;
|
|
89283
89669
|
}
|
|
89284
89670
|
/**
|
|
89285
|
-
*
|
|
89286
|
-
*
|
|
89287
|
-
|
|
89671
|
+
* Whether a URI has been pushed at least once this session. For one-shot
|
|
89672
|
+
* callers (task-list prepend) to decide whether to inject again.
|
|
89673
|
+
*/
|
|
89674
|
+
isSessionPushed(uri) {
|
|
89675
|
+
return this.#pushedEverCount.has(uri);
|
|
89676
|
+
}
|
|
89677
|
+
/**
|
|
89678
|
+
* Seed the session-lifetime claim for URIs already pushed in a prior
|
|
89679
|
+
* daemon run (resumable_idle tasks whose JSONL already contains a
|
|
89680
|
+
* prepend synthetic). Without this, restart re-injects the task-list
|
|
89681
|
+
* because the fresh push manager has an empty count map.
|
|
89682
|
+
*/
|
|
89683
|
+
seedSessionPushed(uri) {
|
|
89684
|
+
if (!this.#pushedEverCount.has(uri)) this.#pushedEverCount.set(uri, 1);
|
|
89685
|
+
}
|
|
89686
|
+
/**
|
|
89687
|
+
* Mark a URI as pushed this turn AND record a session-lifetime claim.
|
|
89688
|
+
* Call from both the sync prepend (before awaiting resolve) and the
|
|
89689
|
+
* async push pipeline (after a successful write). Pair each call with
|
|
89690
|
+
* `unmarkPushed` only if the operation did NOT ultimately write.
|
|
89288
89691
|
*/
|
|
89289
89692
|
markPushed(uri) {
|
|
89290
89693
|
this.#pushedThisTurn.add(uri);
|
|
89694
|
+
this.#pushedEverCount.set(uri, (this.#pushedEverCount.get(uri) ?? 0) + 1);
|
|
89695
|
+
}
|
|
89696
|
+
/**
|
|
89697
|
+
* Release a `markPushed` claim. Decrements the session-lifetime count —
|
|
89698
|
+
* the entry is only removed when the count hits zero, so a concurrent
|
|
89699
|
+
* caller's successful write stays recorded even if this caller rolls
|
|
89700
|
+
* back. Also clears the per-turn set unconditionally (a successful
|
|
89701
|
+
* parallel caller re-adds it as needed before its own write).
|
|
89702
|
+
*/
|
|
89703
|
+
unmarkPushed(uri) {
|
|
89704
|
+
this.#pushedThisTurn.delete(uri);
|
|
89705
|
+
const current2 = this.#pushedEverCount.get(uri) ?? 0;
|
|
89706
|
+
if (current2 <= 1) {
|
|
89707
|
+
this.#pushedEverCount.delete(uri);
|
|
89708
|
+
} else {
|
|
89709
|
+
this.#pushedEverCount.set(uri, current2 - 1);
|
|
89710
|
+
}
|
|
89291
89711
|
}
|
|
89292
89712
|
/** Permanently shut down. Use reset() for clearSession. */
|
|
89293
89713
|
dispose() {
|
|
@@ -89309,6 +89729,7 @@ var ResourcePushManager = class {
|
|
|
89309
89729
|
this.#subscriptions.clear();
|
|
89310
89730
|
this.#dirtyUris.clear();
|
|
89311
89731
|
this.#pushedThisTurn.clear();
|
|
89732
|
+
this.#pushedEverCount.clear();
|
|
89312
89733
|
this.#pushCountThisTurn = 0;
|
|
89313
89734
|
}
|
|
89314
89735
|
#markDirty(uri) {
|
|
@@ -89419,35 +89840,11 @@ var ResourcePushManager = class {
|
|
|
89419
89840
|
this.#batchTimer.reset();
|
|
89420
89841
|
const batch = this.#drainDirtyBatch();
|
|
89421
89842
|
if (!batch) return;
|
|
89843
|
+
const markedUris = /* @__PURE__ */ new Set();
|
|
89422
89844
|
try {
|
|
89423
|
-
|
|
89424
|
-
if (this.#disposed || this.#epoch !== startEpoch) return;
|
|
89425
|
-
if (resolved.size === 0) {
|
|
89426
|
-
if (this.#dirtyUris.size > 0) this.#batchTimer.schedule();
|
|
89427
|
-
return;
|
|
89428
|
-
}
|
|
89429
|
-
const plan = await this.#planPush(resolved);
|
|
89430
|
-
if (this.#disposed || this.#epoch !== startEpoch || plan.syntheticMessages.length === 0)
|
|
89431
|
-
return;
|
|
89432
|
-
const allContent = await this.#writeSynthetics(plan.syntheticMessages);
|
|
89433
|
-
if (this.#disposed || this.#epoch !== startEpoch) return;
|
|
89434
|
-
this.#deliverToSubprocess(allContent, resolved.size);
|
|
89435
|
-
for (const uri of resolved.keys()) {
|
|
89436
|
-
this.#pushedThisTurn.add(uri);
|
|
89437
|
-
}
|
|
89438
|
-
this.#deps.log({
|
|
89439
|
-
event: "resource_push_delivered",
|
|
89440
|
-
taskId: this.#deps.taskId,
|
|
89441
|
-
uriCount: resolved.size
|
|
89442
|
-
});
|
|
89443
|
-
this.#deps.log({
|
|
89444
|
-
event: "resource_push_flushed",
|
|
89445
|
-
taskId: this.#deps.taskId,
|
|
89446
|
-
uriCount: batch.size,
|
|
89447
|
-
syntheticCount: plan.syntheticMessages.length,
|
|
89448
|
-
pushCountThisTurn: this.#pushCountThisTurn
|
|
89449
|
-
});
|
|
89845
|
+
await this.#executeFlush(batch, startEpoch, markedUris);
|
|
89450
89846
|
} catch (err) {
|
|
89847
|
+
for (const uri of markedUris) this.unmarkPushed(uri);
|
|
89451
89848
|
this.#requeueBatch(batch);
|
|
89452
89849
|
this.#deps.log({
|
|
89453
89850
|
event: "resource_push_flush_error",
|
|
@@ -89457,6 +89854,30 @@ var ResourcePushManager = class {
|
|
|
89457
89854
|
});
|
|
89458
89855
|
}
|
|
89459
89856
|
}
|
|
89857
|
+
async #executeFlush(batch, startEpoch, markedUris) {
|
|
89858
|
+
const resolved = await this.#resolveBatch(batch);
|
|
89859
|
+
if (this.#disposed || this.#epoch !== startEpoch) return;
|
|
89860
|
+
if (resolved.size === 0) {
|
|
89861
|
+
if (this.#dirtyUris.size > 0) this.#batchTimer.schedule();
|
|
89862
|
+
return;
|
|
89863
|
+
}
|
|
89864
|
+
const plan = await this.#planPush(resolved);
|
|
89865
|
+
if (this.#disposed || this.#epoch !== startEpoch || plan.syntheticMessages.length === 0) return;
|
|
89866
|
+
for (const uri of resolved.keys()) {
|
|
89867
|
+
this.markPushed(uri);
|
|
89868
|
+
markedUris.add(uri);
|
|
89869
|
+
}
|
|
89870
|
+
const allContent = await this.#writeSynthetics(plan.syntheticMessages);
|
|
89871
|
+
if (this.#disposed || this.#epoch !== startEpoch) return;
|
|
89872
|
+
this.#deliverToSubprocess(allContent, resolved.size);
|
|
89873
|
+
this.#deps.log({
|
|
89874
|
+
event: "resource_push_flushed",
|
|
89875
|
+
taskId: this.#deps.taskId,
|
|
89876
|
+
uriCount: batch.size,
|
|
89877
|
+
syntheticCount: plan.syntheticMessages.length,
|
|
89878
|
+
pushCountThisTurn: this.#pushCountThisTurn
|
|
89879
|
+
});
|
|
89880
|
+
}
|
|
89460
89881
|
};
|
|
89461
89882
|
|
|
89462
89883
|
// src/services/rewind.ts
|
|
@@ -89605,10 +90026,10 @@ var RewindCheckpointHandler = class {
|
|
|
89605
90026
|
this.#allCheckpointTurnNos = [...allTurnNos];
|
|
89606
90027
|
}
|
|
89607
90028
|
recordSeqNo(seqNo) {
|
|
89608
|
-
this.#latestSeqNo = seqNo;
|
|
90029
|
+
if (seqNo > this.#latestSeqNo) this.#latestSeqNo = seqNo;
|
|
89609
90030
|
}
|
|
89610
90031
|
recordSdkUuid(seqNo, sdkUuid) {
|
|
89611
|
-
this.#seqNoToSdkUuid.set(seqNo, sdkUuid);
|
|
90032
|
+
if (!this.#seqNoToSdkUuid.has(seqNo)) this.#seqNoToSdkUuid.set(seqNo, sdkUuid);
|
|
89612
90033
|
}
|
|
89613
90034
|
findSdkUuidForSeqNo(targetSeqNo) {
|
|
89614
90035
|
let bestUuid = null;
|
|
@@ -90021,7 +90442,7 @@ var RewindCheckpointHandler = class {
|
|
|
90021
90442
|
};
|
|
90022
90443
|
|
|
90023
90444
|
// src/services/task/side-thread-registry.ts
|
|
90024
|
-
import { mkdir as mkdir17, readFile as
|
|
90445
|
+
import { mkdir as mkdir17, readFile as readFile31, rename as rename17, writeFile as writeFile23 } from "fs/promises";
|
|
90025
90446
|
import { dirname as dirname15, join as join40 } from "path";
|
|
90026
90447
|
var ThreadFileSchema = external_exports.object({
|
|
90027
90448
|
threads: external_exports.record(external_exports.string(), ThreadMetadataSchema)
|
|
@@ -90355,7 +90776,7 @@ var SideThreadRegistry = class {
|
|
|
90355
90776
|
const filePath = this.#filePath();
|
|
90356
90777
|
let raw;
|
|
90357
90778
|
try {
|
|
90358
|
-
raw = await
|
|
90779
|
+
raw = await readFile31(filePath, "utf-8");
|
|
90359
90780
|
} catch (err) {
|
|
90360
90781
|
if (isEnoent(err)) return;
|
|
90361
90782
|
throw err;
|
|
@@ -90794,7 +91215,7 @@ async function resolveResources(ctx, content, history2, excludeUris) {
|
|
|
90794
91215
|
|
|
90795
91216
|
// src/services/task/cc-task-file-store.ts
|
|
90796
91217
|
import { watch as watch4 } from "fs";
|
|
90797
|
-
import { readdir as readdir9, readFile as
|
|
91218
|
+
import { readdir as readdir9, readFile as readFile32 } from "fs/promises";
|
|
90798
91219
|
import { homedir as homedir6 } from "os";
|
|
90799
91220
|
import { basename as basename4, dirname as dirname16, join as join41 } from "path";
|
|
90800
91221
|
var VALID_STATUSES2 = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
|
|
@@ -90830,7 +91251,7 @@ function createCCTaskFileWatcher(listId, log) {
|
|
|
90830
91251
|
async function readTask(taskId) {
|
|
90831
91252
|
const filePath = join41(dir, `${taskId}.json`);
|
|
90832
91253
|
try {
|
|
90833
|
-
const raw = await
|
|
91254
|
+
const raw = await readFile32(filePath, "utf-8");
|
|
90834
91255
|
const parsed = JSON.parse(raw);
|
|
90835
91256
|
if (isCCTaskFile(parsed)) return parsed;
|
|
90836
91257
|
log?.({
|
|
@@ -91117,11 +91538,24 @@ var StructuredTaskTracker = class {
|
|
|
91117
91538
|
#ccTaskWatcherDispose = null;
|
|
91118
91539
|
#suppressWriteThrough = false;
|
|
91119
91540
|
#ccTaskFileWriter;
|
|
91120
|
-
|
|
91541
|
+
/**
|
|
91542
|
+
* Gate the first flush until an overlay has either been applied or the
|
|
91543
|
+
* caller has explicitly signaled that none is coming. Both flags below
|
|
91544
|
+
* must be false for the flush to proceed during restore.
|
|
91545
|
+
*/
|
|
91546
|
+
#restoreNeedsOverlay = false;
|
|
91547
|
+
/**
|
|
91548
|
+
* Gate the first flush until the file watcher's initial disk reconcile
|
|
91549
|
+
* has fired. Only set during restore for tasks with a session id.
|
|
91550
|
+
*/
|
|
91551
|
+
#restoreNeedsDisk = false;
|
|
91121
91552
|
constructor(deps) {
|
|
91122
91553
|
this.#deps = deps;
|
|
91123
91554
|
this.#ccTaskFileWriter = createCCTaskFileWriter(null, deps.log);
|
|
91124
|
-
|
|
91555
|
+
if (deps.restoreInProgress) {
|
|
91556
|
+
this.#restoreNeedsOverlay = true;
|
|
91557
|
+
this.#restoreNeedsDisk = deps.restoreExpectDisk ?? false;
|
|
91558
|
+
}
|
|
91125
91559
|
}
|
|
91126
91560
|
get currentOverlay() {
|
|
91127
91561
|
return this.#currentOverlay;
|
|
@@ -91165,24 +91599,37 @@ var StructuredTaskTracker = class {
|
|
|
91165
91599
|
}
|
|
91166
91600
|
/**
|
|
91167
91601
|
* Apply an overlay on top of CC tasks. Stores the overlay and re-flushes.
|
|
91168
|
-
*
|
|
91169
|
-
*
|
|
91170
|
-
*
|
|
91602
|
+
* Clears the overlay-pending gate. The flush still waits if a disk reconcile
|
|
91603
|
+
* is expected (restoreExpectDisk=true at construction): without that second
|
|
91604
|
+
* gate, the applyOverlay-first restart ordering would emit an overlay-only
|
|
91605
|
+
* map to the store on every boot, bumping TaskRecord.lastActivityAt for
|
|
91606
|
+
* every restored task that has an overlay.
|
|
91171
91607
|
*/
|
|
91172
91608
|
applyOverlay(overlay) {
|
|
91173
91609
|
this.#currentOverlay = overlay;
|
|
91174
|
-
this.#
|
|
91610
|
+
this.#restoreNeedsOverlay = false;
|
|
91611
|
+
this.#flushStructuredTasks();
|
|
91612
|
+
}
|
|
91613
|
+
/**
|
|
91614
|
+
* Signal that no overlay will be applied during this restore. Clears the
|
|
91615
|
+
* overlay-pending gate but NOT the disk gate — if the restored task has a
|
|
91616
|
+
* session id, the file watcher's first reconcile remains the terminal
|
|
91617
|
+
* signal before the store receives a flush.
|
|
91618
|
+
*/
|
|
91619
|
+
markNoOverlay() {
|
|
91620
|
+
if (!this.#restoreNeedsOverlay) return;
|
|
91621
|
+
this.#restoreNeedsOverlay = false;
|
|
91175
91622
|
this.#flushStructuredTasks();
|
|
91176
91623
|
}
|
|
91177
91624
|
/**
|
|
91178
|
-
*
|
|
91179
|
-
*
|
|
91180
|
-
*
|
|
91181
|
-
* restoration caller whenever applyOverlay will not be invoked.
|
|
91625
|
+
* Clear BOTH restore gates. Used in error paths (hydration promise rejected)
|
|
91626
|
+
* and by restorers that know neither an overlay nor a disk reconcile will
|
|
91627
|
+
* arrive. If either signal fires after this, the natural flush handles it.
|
|
91182
91628
|
*/
|
|
91183
91629
|
markRestoreComplete() {
|
|
91184
|
-
if (!this.#
|
|
91185
|
-
this.#
|
|
91630
|
+
if (!this.#restoreNeedsOverlay && !this.#restoreNeedsDisk) return;
|
|
91631
|
+
this.#restoreNeedsOverlay = false;
|
|
91632
|
+
this.#restoreNeedsDisk = false;
|
|
91186
91633
|
this.#flushStructuredTasks();
|
|
91187
91634
|
}
|
|
91188
91635
|
processStructuredTaskEvents(content) {
|
|
@@ -91292,7 +91739,7 @@ var StructuredTaskTracker = class {
|
|
|
91292
91739
|
* 7. Push to updateStructuredTasks
|
|
91293
91740
|
*/
|
|
91294
91741
|
#flushStructuredTasks() {
|
|
91295
|
-
if (this.#
|
|
91742
|
+
if (this.#restoreNeedsOverlay || this.#restoreNeedsDisk) return;
|
|
91296
91743
|
const overlay = this.#currentOverlay ?? DEFAULT_TASK_OVERLAY;
|
|
91297
91744
|
const merged = applyOverlayToMap(this.#structuredTasks, overlay);
|
|
91298
91745
|
const todoProgress = computeTodoProgress(merged);
|
|
@@ -91314,6 +91761,7 @@ var StructuredTaskTracker = class {
|
|
|
91314
91761
|
* - Re-flush with overlay applied
|
|
91315
91762
|
*/
|
|
91316
91763
|
#reconcileFromDisk(ccTasks) {
|
|
91764
|
+
this.#restoreNeedsDisk = false;
|
|
91317
91765
|
const currentDiskIds = /* @__PURE__ */ new Set();
|
|
91318
91766
|
for (const file of ccTasks) {
|
|
91319
91767
|
currentDiskIds.add(file.id);
|
|
@@ -92013,31 +92461,20 @@ function trackWorktreeToolUse(content, pendingIds) {
|
|
|
92013
92461
|
}
|
|
92014
92462
|
}
|
|
92015
92463
|
function handleWorktreeToolResults(content, params) {
|
|
92016
|
-
let newCwd;
|
|
92017
|
-
let newOriginalCwd;
|
|
92018
|
-
let currentCwd = params.currentCwd;
|
|
92019
|
-
let originalCwd = params.originalCwd;
|
|
92020
92464
|
for (const block2 of content) {
|
|
92021
92465
|
if (block2.type !== "tool_result") continue;
|
|
92022
92466
|
if (!params.pendingIds.delete(block2.toolUseId)) continue;
|
|
92023
92467
|
if (block2.isError) continue;
|
|
92024
92468
|
const extracted = extractWorktreePath(block2.content);
|
|
92025
92469
|
if (!extracted) continue;
|
|
92026
|
-
|
|
92027
|
-
originalCwd = currentCwd;
|
|
92028
|
-
newOriginalCwd = currentCwd;
|
|
92029
|
-
}
|
|
92030
|
-
currentCwd = extracted;
|
|
92031
|
-
newCwd = extracted;
|
|
92470
|
+
params.onCwdChanged(extracted);
|
|
92032
92471
|
params.log({
|
|
92033
92472
|
event: "worktree_cwd_changed",
|
|
92034
92473
|
taskId: params.taskId,
|
|
92035
92474
|
newCwd: extracted,
|
|
92036
|
-
originalCwd
|
|
92475
|
+
originalCwd: params.getOriginalCwd() ?? null
|
|
92037
92476
|
});
|
|
92038
|
-
params.onCwdChanged(params.taskId, extracted);
|
|
92039
92477
|
}
|
|
92040
|
-
return { newCwd, newOriginalCwd };
|
|
92041
92478
|
}
|
|
92042
92479
|
function trackPlanFileFromToolUse(content, onPlanFile) {
|
|
92043
92480
|
for (const block2 of content) {
|
|
@@ -92412,6 +92849,12 @@ function classifyRoiTransition(nextStatus, roiEverRan) {
|
|
|
92412
92849
|
if (nextStatus === "in_progress") return "reset-cycle";
|
|
92413
92850
|
return "nothing";
|
|
92414
92851
|
}
|
|
92852
|
+
function isTaskListSynthetic(msg, taskUri) {
|
|
92853
|
+
if (!msg.isSynthetic || msg.content.length !== 1) return false;
|
|
92854
|
+
const block2 = msg.content[0];
|
|
92855
|
+
if (!block2 || block2.type !== "resource") return false;
|
|
92856
|
+
return "uri" in block2.resource && block2.resource.uri === taskUri;
|
|
92857
|
+
}
|
|
92415
92858
|
var Task = class {
|
|
92416
92859
|
#deps;
|
|
92417
92860
|
#mainThread;
|
|
@@ -92481,6 +92924,7 @@ var Task = class {
|
|
|
92481
92924
|
constructor(deps) {
|
|
92482
92925
|
this.#deps = deps;
|
|
92483
92926
|
this.#cwd = deps.cwd;
|
|
92927
|
+
this.#originalCwd = deps.initialOriginalCwd ?? void 0;
|
|
92484
92928
|
this.#broadcastToAllPeers = deps.sendControlMessage ?? null;
|
|
92485
92929
|
this.#costBaseline = deps.costBaseline;
|
|
92486
92930
|
this.#lastTurnStats = deps.initialTurnStats ?? null;
|
|
@@ -92497,7 +92941,12 @@ var Task = class {
|
|
|
92497
92941
|
taskId: deps.taskId,
|
|
92498
92942
|
log: deps.log,
|
|
92499
92943
|
updateStructuredTasks: deps.updateStructuredTasks,
|
|
92500
|
-
restoreInProgress: deps.restoreInProgress ?? false
|
|
92944
|
+
restoreInProgress: deps.restoreInProgress ?? false,
|
|
92945
|
+
/**
|
|
92946
|
+
* Only wait for a disk reconcile if a CC session exists — else the file
|
|
92947
|
+
* watcher never attaches and the gate would never clear.
|
|
92948
|
+
*/
|
|
92949
|
+
restoreExpectDisk: Boolean(deps.restoreInProgress && deps.existingSessionId)
|
|
92501
92950
|
});
|
|
92502
92951
|
this.#pushManager = new ResourcePushManager({
|
|
92503
92952
|
taskId: deps.taskId,
|
|
@@ -92542,6 +92991,8 @@ var Task = class {
|
|
|
92542
92991
|
error: err instanceof Error ? err.message : String(err)
|
|
92543
92992
|
});
|
|
92544
92993
|
});
|
|
92994
|
+
const seedPromise = this.#seedPushManagerFromHistory();
|
|
92995
|
+
this.#hydrationPromise = this.#hydrationPromise ? this.#hydrationPromise.then(() => seedPromise) : seedPromise;
|
|
92545
92996
|
}
|
|
92546
92997
|
this.#subagentManager = new SubagentManager({
|
|
92547
92998
|
taskId: deps.taskId,
|
|
@@ -92681,7 +93132,7 @@ var Task = class {
|
|
|
92681
93132
|
});
|
|
92682
93133
|
const spawnMode = deps.existingSessionId ? { kind: "resume", sessionId: deps.existingSessionId } : { kind: "fresh" };
|
|
92683
93134
|
const initialCwd = deps.cwd;
|
|
92684
|
-
const wrappedSpawn = (reason, content, onEvent, canUseTool, settings, _cwd, taskId, additionalPrompt, disallowedTools, mode) => {
|
|
93135
|
+
const wrappedSpawn = (reason, content, onEvent, canUseTool, settings, _cwd, taskId, additionalPrompt, disallowedTools, mode, onCwdChanged) => {
|
|
92685
93136
|
const effectiveCwd = _cwd && _cwd !== initialCwd ? _cwd : this.#cwd;
|
|
92686
93137
|
return deps.spawnSubprocess(
|
|
92687
93138
|
reason,
|
|
@@ -92693,7 +93144,8 @@ var Task = class {
|
|
|
92693
93144
|
taskId,
|
|
92694
93145
|
additionalPrompt,
|
|
92695
93146
|
disallowedTools,
|
|
92696
|
-
mode
|
|
93147
|
+
mode,
|
|
93148
|
+
onCwdChanged
|
|
92697
93149
|
);
|
|
92698
93150
|
};
|
|
92699
93151
|
this.#mainThread = new Thread(
|
|
@@ -92717,7 +93169,8 @@ var Task = class {
|
|
|
92717
93169
|
initialState: deps.initialState,
|
|
92718
93170
|
existingSessionId: deps.existingSessionId,
|
|
92719
93171
|
metricsCollector: deps.metricsCollector,
|
|
92720
|
-
adoptedSubprocess: deps.adoptedSubprocess
|
|
93172
|
+
adoptedSubprocess: deps.adoptedSubprocess,
|
|
93173
|
+
onCwdChanged: (newCwd) => this.#applyCwdChange(newCwd)
|
|
92721
93174
|
},
|
|
92722
93175
|
{
|
|
92723
93176
|
onStatusChange: (status) => this.#handleStatusChange(status),
|
|
@@ -92844,6 +93297,10 @@ var Task = class {
|
|
|
92844
93297
|
get cwd() {
|
|
92845
93298
|
return this.#cwd;
|
|
92846
93299
|
}
|
|
93300
|
+
/** The cwd at the time of the first directory switch; undefined if the agent never switched. */
|
|
93301
|
+
get originalCwd() {
|
|
93302
|
+
return this.#originalCwd;
|
|
93303
|
+
}
|
|
92847
93304
|
get latestSettings() {
|
|
92848
93305
|
return this.#latestSettings;
|
|
92849
93306
|
}
|
|
@@ -93119,6 +93576,16 @@ var Task = class {
|
|
|
93119
93576
|
setCwd(cwd) {
|
|
93120
93577
|
this.#cwd = cwd;
|
|
93121
93578
|
}
|
|
93579
|
+
/**
|
|
93580
|
+
* Public funnel entry for cwd changes originating outside the SDK/tool
|
|
93581
|
+
* signal path (browser `send_message.cwd`, schedule). Delegates to
|
|
93582
|
+
* `#applyCwdChange` so originalCwd capture, rewind-checkpoint reset,
|
|
93583
|
+
* prev-cwd commit scan, and downstream listener fan-out run once per
|
|
93584
|
+
* change regardless of source.
|
|
93585
|
+
*/
|
|
93586
|
+
applyCwdChange(cwd) {
|
|
93587
|
+
this.#applyCwdChange(cwd);
|
|
93588
|
+
}
|
|
93122
93589
|
setMode(mode) {
|
|
93123
93590
|
const previousMode = this.#deps.mode;
|
|
93124
93591
|
this.#deps.mode = mode;
|
|
@@ -93558,6 +94025,33 @@ var Task = class {
|
|
|
93558
94025
|
if (!arr || arr.length === 0) return void 0;
|
|
93559
94026
|
return arr.shift();
|
|
93560
94027
|
}
|
|
94028
|
+
/**
|
|
94029
|
+
* Resume path: if a prior daemon run already injected the task-list for
|
|
94030
|
+
* this channel, its synthetic row is in the JSONL. Seed the push manager
|
|
94031
|
+
* so `#resolveTaskListPrepend`'s `isSessionPushed` guard matches on the
|
|
94032
|
+
* first spawn — otherwise restart re-prepends, duplicating the chip and
|
|
94033
|
+
* dumping the full resource text back into the agent's context.
|
|
94034
|
+
*
|
|
94035
|
+
* Chained into `#hydrationPromise` (which `#awaitHydration` blocks on at
|
|
94036
|
+
* every spawn/flush entry point) so the seed completes before any
|
|
94037
|
+
* caller reaches `#resolveTaskListPrepend`. A fire-and-forget scan would
|
|
94038
|
+
* race against a fast-arriving first user message.
|
|
94039
|
+
*/
|
|
94040
|
+
async #seedPushManagerFromHistory() {
|
|
94041
|
+
const taskUri = buildTaskResourceUri(this.#deps.taskId);
|
|
94042
|
+
try {
|
|
94043
|
+
const msgs = await this.#deps.store.getMessages(this.#deps.channelId);
|
|
94044
|
+
if (msgs.some((msg) => isTaskListSynthetic(msg, taskUri))) {
|
|
94045
|
+
this.#pushManager.seedSessionPushed(taskUri);
|
|
94046
|
+
}
|
|
94047
|
+
} catch (err) {
|
|
94048
|
+
this.#deps.log({
|
|
94049
|
+
event: "push_manager_seed_failed",
|
|
94050
|
+
taskId: this.#deps.taskId,
|
|
94051
|
+
error: err instanceof Error ? err.message : String(err)
|
|
94052
|
+
});
|
|
94053
|
+
}
|
|
94054
|
+
}
|
|
93561
94055
|
async #awaitHydration() {
|
|
93562
94056
|
if (this.#hydrationPromise) {
|
|
93563
94057
|
await this.#hydrationPromise;
|
|
@@ -93678,10 +94172,12 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
93678
94172
|
const registry = this.#deps.resourceRegistry;
|
|
93679
94173
|
if (!registry) return null;
|
|
93680
94174
|
const taskUri = buildTaskResourceUri(this.#deps.taskId);
|
|
93681
|
-
if (this.#pushManager.
|
|
94175
|
+
if (this.#pushManager.isSessionPushed(taskUri)) return null;
|
|
94176
|
+
this.#pushManager.markPushed(taskUri);
|
|
94177
|
+
let injected = false;
|
|
93682
94178
|
try {
|
|
93683
94179
|
const taskResource = await registry.resolve(taskUri);
|
|
93684
|
-
if ("text" in taskResource && !taskResource.text.includes('task-count="0"')) {
|
|
94180
|
+
if ("text" in taskResource && !taskResource.text.includes('<task-list task-count="0"')) {
|
|
93685
94181
|
const rootMsg = buildRootMessage(
|
|
93686
94182
|
taskUri,
|
|
93687
94183
|
taskResource,
|
|
@@ -93696,10 +94192,12 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
93696
94192
|
},
|
|
93697
94193
|
[rootMsg]
|
|
93698
94194
|
);
|
|
93699
|
-
|
|
94195
|
+
injected = true;
|
|
93700
94196
|
return rootMsg.content;
|
|
93701
94197
|
}
|
|
93702
94198
|
} catch {
|
|
94199
|
+
} finally {
|
|
94200
|
+
if (!injected) this.#pushManager.unmarkPushed(taskUri);
|
|
93703
94201
|
}
|
|
93704
94202
|
return null;
|
|
93705
94203
|
}
|
|
@@ -94107,16 +94605,37 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
94107
94605
|
trackWorktreeToolUse(content, this.#pendingWorktreeToolUseIds);
|
|
94108
94606
|
}
|
|
94109
94607
|
#handleWorktreeToolResults(content) {
|
|
94110
|
-
|
|
94608
|
+
handleWorktreeToolResults(content, {
|
|
94111
94609
|
pendingIds: this.#pendingWorktreeToolUseIds,
|
|
94112
|
-
|
|
94113
|
-
originalCwd: this.#originalCwd,
|
|
94114
|
-
onCwdChanged: this.#deps.onCwdChanged,
|
|
94610
|
+
onCwdChanged: (newCwd) => this.#applyCwdChange(newCwd),
|
|
94115
94611
|
taskId: this.#deps.taskId,
|
|
94612
|
+
getOriginalCwd: () => this.#originalCwd,
|
|
94116
94613
|
log: this.#deps.log
|
|
94117
94614
|
});
|
|
94118
|
-
|
|
94119
|
-
|
|
94615
|
+
}
|
|
94616
|
+
/**
|
|
94617
|
+
* Unified funnel for cwd changes — both the SDK's native `CwdChanged`
|
|
94618
|
+
* hook and the regex-based EnterWorktree tool-result fallback converge
|
|
94619
|
+
* here. Dedups by value, captures `originalCwd` on first change, resets
|
|
94620
|
+
* rewind-checkpoint state, fires a one-shot commit scan against the
|
|
94621
|
+
* previous cwd to retain attribution, and notifies downstream listeners.
|
|
94622
|
+
*/
|
|
94623
|
+
#applyCwdChange(newCwd) {
|
|
94624
|
+
if (this.#disposed) return;
|
|
94625
|
+
if (newCwd === this.#cwd) return;
|
|
94626
|
+
const prevCwd = this.#cwd;
|
|
94627
|
+
if (!this.#originalCwd) this.#originalCwd = prevCwd;
|
|
94628
|
+
this.#cwd = newCwd;
|
|
94629
|
+
this.#rewindCheckpoint.resetForCwdChange();
|
|
94630
|
+
if (prevCwd) this.#triggerCommitScanForCwd(prevCwd);
|
|
94631
|
+
this.#deps.onCwdChanged(this.#deps.taskId, newCwd);
|
|
94632
|
+
this.#deps.log({
|
|
94633
|
+
event: "task_cwd_changed_via_apply",
|
|
94634
|
+
taskId: this.#deps.taskId,
|
|
94635
|
+
newCwd,
|
|
94636
|
+
prevCwd: prevCwd ?? null,
|
|
94637
|
+
originalCwd: this.#originalCwd ?? null
|
|
94638
|
+
});
|
|
94120
94639
|
}
|
|
94121
94640
|
#trackPlanFileFromToolUse(content) {
|
|
94122
94641
|
trackPlanFileFromToolUse(content, (path2) => this.#planHandler.trackCreatedFile(path2));
|
|
@@ -94145,12 +94664,12 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
94145
94664
|
this.#deps.persistAbandonedAt(Date.now());
|
|
94146
94665
|
}
|
|
94147
94666
|
}
|
|
94148
|
-
#
|
|
94149
|
-
const cwd = this.#cwd;
|
|
94150
|
-
if (!cwd) return;
|
|
94667
|
+
#chainCommitScan(resolveCwd, failEvent) {
|
|
94151
94668
|
this.#trailerInjectionChain = this.#trailerInjectionChain.then(async () => {
|
|
94152
94669
|
try {
|
|
94153
94670
|
const state = await this.#deps.getCommitScanState();
|
|
94671
|
+
const cwd = resolveCwd();
|
|
94672
|
+
if (!cwd) return;
|
|
94154
94673
|
await scanAndAttributeCommits(
|
|
94155
94674
|
{
|
|
94156
94675
|
cwd,
|
|
@@ -94177,7 +94696,7 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
94177
94696
|
);
|
|
94178
94697
|
} catch (err) {
|
|
94179
94698
|
this.#deps.log({
|
|
94180
|
-
event:
|
|
94699
|
+
event: failEvent,
|
|
94181
94700
|
taskId: this.#deps.taskId,
|
|
94182
94701
|
error: err instanceof Error ? err.message : String(err)
|
|
94183
94702
|
});
|
|
@@ -94185,6 +94704,17 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
94185
94704
|
}).catch(() => {
|
|
94186
94705
|
});
|
|
94187
94706
|
}
|
|
94707
|
+
#triggerCommitScan() {
|
|
94708
|
+
this.#chainCommitScan(() => this.#cwd, "roi_commit_scan_failed");
|
|
94709
|
+
}
|
|
94710
|
+
/**
|
|
94711
|
+
* Trigger a one-shot commit scan against an explicit cwd. Used at
|
|
94712
|
+
* cwd-switch time to retain attribution for commits made in the
|
|
94713
|
+
* outgoing directory before the agent's next Bash turn moves us on.
|
|
94714
|
+
*/
|
|
94715
|
+
#triggerCommitScanForCwd(cwd) {
|
|
94716
|
+
this.#chainCommitScan(() => cwd, "roi_commit_scan_for_cwd_failed");
|
|
94717
|
+
}
|
|
94188
94718
|
applyOverlay(overlay) {
|
|
94189
94719
|
this.#structuredTaskTracker.applyOverlay(overlay);
|
|
94190
94720
|
}
|
|
@@ -94192,6 +94722,10 @@ Use this context to maintain continuity. You have already done this work \u2014
|
|
|
94192
94722
|
markStructuredTaskRestoreComplete() {
|
|
94193
94723
|
this.#structuredTaskTracker.markRestoreComplete();
|
|
94194
94724
|
}
|
|
94725
|
+
/** See StructuredTaskTracker.markNoOverlay. */
|
|
94726
|
+
markStructuredTaskNoOverlay() {
|
|
94727
|
+
this.#structuredTaskTracker.markNoOverlay();
|
|
94728
|
+
}
|
|
94195
94729
|
/** ---------------------------------------------------------------- */
|
|
94196
94730
|
/** Resource resolution */
|
|
94197
94731
|
/** ---------------------------------------------------------------- */
|
|
@@ -94668,14 +95202,23 @@ function findSdkUuidForSeqNoInTask(tasks, taskId, seqNo) {
|
|
|
94668
95202
|
// src/services/task/manager/task-manager-template.ts
|
|
94669
95203
|
function buildInitialOverlayFromTemplate(template, now) {
|
|
94670
95204
|
if (!template || template.items.length === 0) return void 0;
|
|
94671
|
-
const
|
|
94672
|
-
|
|
95205
|
+
const idRemap = new Map(template.items.map((item2, index) => [item2.id, String(index + 1)]));
|
|
95206
|
+
const userTasks = template.items.map((item2, index) => ({
|
|
95207
|
+
id: String(index + 1),
|
|
94673
95208
|
subject: item2.content,
|
|
94674
95209
|
description: item2.description,
|
|
94675
95210
|
status: "pending",
|
|
94676
95211
|
owner: "user",
|
|
94677
95212
|
blocks: [],
|
|
94678
|
-
|
|
95213
|
+
/**
|
|
95214
|
+
* Drop deps pointing outside the template. Falling through the
|
|
95215
|
+
* original colon-bearing ID would leave a dangling `blockedBy` that
|
|
95216
|
+
* CC's TaskUpdate can't resolve — silently stuck in the UI.
|
|
95217
|
+
*/
|
|
95218
|
+
blockedBy: item2.deps.flatMap((dep) => {
|
|
95219
|
+
const mapped = idRemap.get(dep);
|
|
95220
|
+
return mapped ? [mapped] : [];
|
|
95221
|
+
}),
|
|
94679
95222
|
createdAt: now,
|
|
94680
95223
|
updatedAt: now
|
|
94681
95224
|
}));
|
|
@@ -94706,6 +95249,13 @@ var TaskManager = class {
|
|
|
94706
95249
|
#pendingStreamSubs = /* @__PURE__ */ new Map();
|
|
94707
95250
|
#onAuthNotLoggedIn = null;
|
|
94708
95251
|
#onCwdChanged = null;
|
|
95252
|
+
/**
|
|
95253
|
+
* Multi-listener fan-out for cwd changes. Populated by per-peer wirings
|
|
95254
|
+
* (e.g. PR poller) that need to react to cwd switches without clobbering
|
|
95255
|
+
* the primary daemon-level listener set via `setOnCwdChanged`. Each
|
|
95256
|
+
* registration returns an unsubscribe function.
|
|
95257
|
+
*/
|
|
95258
|
+
#cwdChangedListeners = /* @__PURE__ */ new Set();
|
|
94709
95259
|
#onCheckpointReady = null;
|
|
94710
95260
|
#onRateLimitEvent = null;
|
|
94711
95261
|
#onTurnCostDelta = null;
|
|
@@ -94779,6 +95329,17 @@ var TaskManager = class {
|
|
|
94779
95329
|
setOnCwdChanged(handler) {
|
|
94780
95330
|
this.#onCwdChanged = handler;
|
|
94781
95331
|
}
|
|
95332
|
+
/**
|
|
95333
|
+
* Register an additional cwd-change listener. Unlike `setOnCwdChanged`
|
|
95334
|
+
* this is multi-listener and intended for per-peer wirings (PR poller,
|
|
95335
|
+
* etc). Returns an unsubscribe function.
|
|
95336
|
+
*/
|
|
95337
|
+
addOnCwdChanged(handler) {
|
|
95338
|
+
this.#cwdChangedListeners.add(handler);
|
|
95339
|
+
return () => {
|
|
95340
|
+
this.#cwdChangedListeners.delete(handler);
|
|
95341
|
+
};
|
|
95342
|
+
}
|
|
94782
95343
|
setOnCheckpointReady(handler) {
|
|
94783
95344
|
this.#onCheckpointReady = handler;
|
|
94784
95345
|
}
|
|
@@ -94806,7 +95367,7 @@ var TaskManager = class {
|
|
|
94806
95367
|
}
|
|
94807
95368
|
if (task.cwd === cwd) return;
|
|
94808
95369
|
task.cwd = cwd;
|
|
94809
|
-
task.orchestrator.
|
|
95370
|
+
task.orchestrator.applyCwdChange(cwd);
|
|
94810
95371
|
this.#deps.taskStateStore.updateCwd(taskId, cwd).catch((err) => {
|
|
94811
95372
|
this.#deps.log({
|
|
94812
95373
|
event: "task_state_store_update_cwd_failed",
|
|
@@ -94814,8 +95375,37 @@ var TaskManager = class {
|
|
|
94814
95375
|
error: err instanceof Error ? err.message : String(err)
|
|
94815
95376
|
});
|
|
94816
95377
|
});
|
|
95378
|
+
const originalCwd = task.orchestrator.originalCwd;
|
|
95379
|
+
if (originalCwd) {
|
|
95380
|
+
this.#deps.taskStateStore.setOriginalCwd(taskId, originalCwd).catch((err) => {
|
|
95381
|
+
this.#deps.log({
|
|
95382
|
+
event: "task_state_store_set_original_cwd_failed",
|
|
95383
|
+
taskId,
|
|
95384
|
+
error: err instanceof Error ? err.message : String(err)
|
|
95385
|
+
});
|
|
95386
|
+
});
|
|
95387
|
+
}
|
|
94817
95388
|
this.#deps.log({ event: "task_cwd_updated", taskId, cwd });
|
|
94818
|
-
|
|
95389
|
+
try {
|
|
95390
|
+
this.#onCwdChanged?.(taskId, cwd);
|
|
95391
|
+
} catch (err) {
|
|
95392
|
+
this.#deps.log({
|
|
95393
|
+
event: "cwd_changed_primary_listener_error",
|
|
95394
|
+
taskId,
|
|
95395
|
+
error: err instanceof Error ? err.message : String(err)
|
|
95396
|
+
});
|
|
95397
|
+
}
|
|
95398
|
+
for (const listener of this.#cwdChangedListeners) {
|
|
95399
|
+
try {
|
|
95400
|
+
listener(taskId, cwd);
|
|
95401
|
+
} catch (err) {
|
|
95402
|
+
this.#deps.log({
|
|
95403
|
+
event: "cwd_changed_listener_error",
|
|
95404
|
+
taskId,
|
|
95405
|
+
error: err instanceof Error ? err.message : String(err)
|
|
95406
|
+
});
|
|
95407
|
+
}
|
|
95408
|
+
}
|
|
94819
95409
|
}
|
|
94820
95410
|
get taskStateStore() {
|
|
94821
95411
|
return this.#deps.taskStateStore;
|
|
@@ -95132,11 +95722,14 @@ var TaskManager = class {
|
|
|
95132
95722
|
const cwd = opts.cwd;
|
|
95133
95723
|
const mode = opts.mode ?? "task";
|
|
95134
95724
|
let orchestratorRef = null;
|
|
95725
|
+
const hasSession = Boolean(opts.existingSessionId);
|
|
95135
95726
|
const restoreHydration = async () => {
|
|
95136
95727
|
const record = await this.#deps.taskStateStore.getTask(taskId);
|
|
95137
95728
|
if (!orchestratorRef || !this.#tasks.has(taskId)) return;
|
|
95138
95729
|
if (record?.taskOverlay) {
|
|
95139
95730
|
orchestratorRef.applyOverlay(record.taskOverlay);
|
|
95731
|
+
} else if (hasSession) {
|
|
95732
|
+
orchestratorRef.markStructuredTaskNoOverlay();
|
|
95140
95733
|
} else {
|
|
95141
95734
|
orchestratorRef.markStructuredTaskRestoreComplete();
|
|
95142
95735
|
}
|
|
@@ -95165,6 +95758,7 @@ var TaskManager = class {
|
|
|
95165
95758
|
initialRoiStartedEmitted: opts.initialRoiStartedEmitted,
|
|
95166
95759
|
taskCreatedAt: opts.taskCreatedAt,
|
|
95167
95760
|
initialTurnCount: opts.initialTurnCount,
|
|
95761
|
+
initialOriginalCwd: opts.initialOriginalCwd,
|
|
95168
95762
|
restoreInProgress: true
|
|
95169
95763
|
});
|
|
95170
95764
|
orchestratorRef = orchestrator;
|
|
@@ -95224,7 +95818,8 @@ var TaskManager = class {
|
|
|
95224
95818
|
initialPlanDetection: record.lastPlanDetection,
|
|
95225
95819
|
initialRoiStartedEmitted: record.roiStartedEmitted,
|
|
95226
95820
|
taskCreatedAt: record.createdAt,
|
|
95227
|
-
initialTurnCount: record.totalTurnCount
|
|
95821
|
+
initialTurnCount: record.totalTurnCount,
|
|
95822
|
+
initialOriginalCwd: record.originalCwd ?? null
|
|
95228
95823
|
});
|
|
95229
95824
|
task = this.#tasks.get(taskId);
|
|
95230
95825
|
if (!task) {
|
|
@@ -95233,15 +95828,7 @@ var TaskManager = class {
|
|
|
95233
95828
|
this.#deps.log({ event: "task_lazy_restored", taskId, channelId: record.channelId });
|
|
95234
95829
|
}
|
|
95235
95830
|
if (cwd && cwd !== task.cwd) {
|
|
95236
|
-
|
|
95237
|
-
task.orchestrator.setCwd(cwd);
|
|
95238
|
-
this.#deps.taskStateStore.updateCwd(taskId, cwd).catch((err) => {
|
|
95239
|
-
this.#deps.log({
|
|
95240
|
-
event: "task_state_store_update_cwd_failed",
|
|
95241
|
-
taskId,
|
|
95242
|
-
error: err instanceof Error ? err.message : String(err)
|
|
95243
|
-
});
|
|
95244
|
-
});
|
|
95831
|
+
this.updateTaskCwd(taskId, cwd);
|
|
95245
95832
|
}
|
|
95246
95833
|
task.orchestrator.handleUserMessage(
|
|
95247
95834
|
content,
|
|
@@ -95661,7 +96248,8 @@ var TaskManager = class {
|
|
|
95661
96248
|
onForwardedAck: (correlationId) => {
|
|
95662
96249
|
this.#forwardedAckEmitters.get(taskId)?.(correlationId);
|
|
95663
96250
|
},
|
|
95664
|
-
restoreInProgress: opts?.restoreInProgress ?? false
|
|
96251
|
+
restoreInProgress: opts?.restoreInProgress ?? false,
|
|
96252
|
+
initialOriginalCwd: opts?.initialOriginalCwd
|
|
95665
96253
|
});
|
|
95666
96254
|
}
|
|
95667
96255
|
};
|
|
@@ -95803,6 +96391,7 @@ function buildTaskStateStore(dataDir) {
|
|
|
95803
96391
|
mergedAt: null,
|
|
95804
96392
|
attributedCommitShas: [],
|
|
95805
96393
|
lastCommitScanSha: null,
|
|
96394
|
+
originalCwd: null,
|
|
95806
96395
|
mode: mode ?? "task",
|
|
95807
96396
|
...scheduleId ? { scheduleId } : {},
|
|
95808
96397
|
...scheduleName ? { scheduleName } : {},
|
|
@@ -96022,6 +96611,13 @@ function buildTaskStateStore(dataDir) {
|
|
|
96022
96611
|
{ ...options, broadcast: false }
|
|
96023
96612
|
);
|
|
96024
96613
|
},
|
|
96614
|
+
async setOriginalCwd(taskId, originalCwd, options) {
|
|
96615
|
+
await safeUpdate(
|
|
96616
|
+
taskId,
|
|
96617
|
+
(task) => task.originalCwd != null ? task : { ...task, originalCwd },
|
|
96618
|
+
{ ...options, broadcast: false }
|
|
96619
|
+
);
|
|
96620
|
+
},
|
|
96025
96621
|
async sweepToInputRequired(taskId, options) {
|
|
96026
96622
|
await safeUpdate(
|
|
96027
96623
|
taskId,
|
|
@@ -97003,7 +97599,7 @@ function isPlainObject2(v2) {
|
|
|
97003
97599
|
}
|
|
97004
97600
|
|
|
97005
97601
|
// src/services/themes/theme-store.ts
|
|
97006
|
-
import { mkdir as mkdir19, readdir as readdir11, readFile as
|
|
97602
|
+
import { mkdir as mkdir19, readdir as readdir11, readFile as readFile33, rename as rename19, stat as stat8, unlink as unlink9, writeFile as writeFile26 } from "fs/promises";
|
|
97007
97603
|
import { join as join47 } from "path";
|
|
97008
97604
|
function planSeed(input) {
|
|
97009
97605
|
if (input.configExists) {
|
|
@@ -97083,7 +97679,7 @@ function buildThemeStore(dataDir) {
|
|
|
97083
97679
|
}
|
|
97084
97680
|
async function readConfigFile() {
|
|
97085
97681
|
try {
|
|
97086
|
-
const raw = await
|
|
97682
|
+
const raw = await readFile33(configPath, "utf-8");
|
|
97087
97683
|
const parsed = ThemeConfigSchema.safeParse(JSON.parse(raw));
|
|
97088
97684
|
if (!parsed.success) return null;
|
|
97089
97685
|
return parsed.data;
|
|
@@ -97102,7 +97698,7 @@ function buildThemeStore(dataDir) {
|
|
|
97102
97698
|
await atomicWrite3(themePath(id), serialized);
|
|
97103
97699
|
}
|
|
97104
97700
|
async function readThemeFile(id) {
|
|
97105
|
-
const raw = await
|
|
97701
|
+
const raw = await readFile33(themePath(id), "utf-8");
|
|
97106
97702
|
const parsed = VSCodeThemeSchema.safeParse(JSON.parse(raw));
|
|
97107
97703
|
if (!parsed.success) {
|
|
97108
97704
|
throw new Error(
|
|
@@ -97254,7 +97850,7 @@ function buildThemeStore(dataDir) {
|
|
|
97254
97850
|
}
|
|
97255
97851
|
|
|
97256
97852
|
// src/services/themes/vscode-scanner.ts
|
|
97257
|
-
import { readdir as readdir12, readFile as
|
|
97853
|
+
import { readdir as readdir12, readFile as readFile34, stat as stat9 } from "fs/promises";
|
|
97258
97854
|
import { homedir as homedir8 } from "os";
|
|
97259
97855
|
import { dirname as dirname17, join as join48, normalize as normalize5, resolve } from "path";
|
|
97260
97856
|
var VSCodeThemeEntrySchema2 = external_exports.object({
|
|
@@ -97313,7 +97909,7 @@ var PackageJsonContribSchema = external_exports.object({
|
|
|
97313
97909
|
async function tryReadPackageJson(extDir) {
|
|
97314
97910
|
let pkgRaw;
|
|
97315
97911
|
try {
|
|
97316
|
-
pkgRaw = await
|
|
97912
|
+
pkgRaw = await readFile34(join48(extDir, "package.json"), "utf-8");
|
|
97317
97913
|
} catch {
|
|
97318
97914
|
return null;
|
|
97319
97915
|
}
|
|
@@ -97383,7 +97979,7 @@ async function readVSCodeThemeWithIncludes(absolutePath) {
|
|
|
97383
97979
|
if (!validateScanPath(filePath)) {
|
|
97384
97980
|
throw new Error(`Include path outside allowed directories: ${filePath}`);
|
|
97385
97981
|
}
|
|
97386
|
-
const raw = await
|
|
97982
|
+
const raw = await readFile34(filePath, "utf-8");
|
|
97387
97983
|
const errors2 = [];
|
|
97388
97984
|
const parsed = parse3(raw, errors2, {
|
|
97389
97985
|
allowTrailingComma: true,
|
|
@@ -97784,7 +98380,7 @@ async function createDaemon(deps) {
|
|
|
97784
98380
|
previewLifecycleSubs.set(taskId, unsub);
|
|
97785
98381
|
reconcilePreviewRefs(taskId);
|
|
97786
98382
|
}
|
|
97787
|
-
const spawnSubprocessFn = (reason, initialContent, onEvent, canUseTool, settings, cwd, taskId, additionalSystemPrompt, disallowedTools, mode) => {
|
|
98383
|
+
const spawnSubprocessFn = (reason, initialContent, onEvent, canUseTool, settings, cwd, taskId, additionalSystemPrompt, disallowedTools, mode, onCwdChanged) => {
|
|
97788
98384
|
const resolvedTaskId = taskId ?? "unknown";
|
|
97789
98385
|
const { watcher: vizWatcher } = getOrCreateVizWatcher(resolvedTaskId);
|
|
97790
98386
|
deps.log({ event: "spawn_subprocess", mode, taskId: resolvedTaskId, hasMode: mode != null });
|
|
@@ -97821,7 +98417,8 @@ async function createDaemon(deps) {
|
|
|
97821
98417
|
log: deps.log,
|
|
97822
98418
|
metricsCollector: deps.metricsCollector,
|
|
97823
98419
|
claudeCodePath: deps.claudeCodePath,
|
|
97824
|
-
disallowedTools
|
|
98420
|
+
disallowedTools,
|
|
98421
|
+
onCwdChanged
|
|
97825
98422
|
});
|
|
97826
98423
|
const subprocess = AnthropicAgentSubprocess.spawn(
|
|
97827
98424
|
deps.queryFn,
|
|
@@ -99787,7 +100384,7 @@ function friendlyExecError(err) {
|
|
|
99787
100384
|
async function refreshPluginCapabilities(daemon, tokenStore) {
|
|
99788
100385
|
const [updatedMarketplace, updatedMcp, updatedSkills] = await Promise.all([
|
|
99789
100386
|
detectMarketplacePlugins(),
|
|
99790
|
-
import("./mcp-servers-
|
|
100387
|
+
import("./mcp-servers-MUVTAMDT.js").then(
|
|
99791
100388
|
(m2) => m2.detectMCPServers(daemon.capabilities.environments, tokenStore)
|
|
99792
100389
|
),
|
|
99793
100390
|
import("./skills-NCKYNLUS.js").then(
|
|
@@ -100456,6 +101053,15 @@ async function emitTaskCreatedAck(controlHandler, daemon, taskId, templateId, lo
|
|
|
100456
101053
|
});
|
|
100457
101054
|
}
|
|
100458
101055
|
}
|
|
101056
|
+
var controlChannelEpochs = /* @__PURE__ */ new Map();
|
|
101057
|
+
function isLiveControlChannelEpoch(controlPeerId, channelEpoch, registry = controlChannelEpochs) {
|
|
101058
|
+
return registry.get(controlPeerId) === channelEpoch;
|
|
101059
|
+
}
|
|
101060
|
+
function registerControlChannelEpoch(controlPeerId, registry = controlChannelEpochs) {
|
|
101061
|
+
const epoch = crypto.randomUUID();
|
|
101062
|
+
registry.set(controlPeerId, epoch);
|
|
101063
|
+
return epoch;
|
|
101064
|
+
}
|
|
100459
101065
|
function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
|
|
100460
101066
|
const dc = narrow(rawChannel);
|
|
100461
101067
|
const wsRoot = deps?.workspaceRoot ?? findProjectRoot(process.cwd());
|
|
@@ -100657,6 +101263,9 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
|
|
|
100657
101263
|
},
|
|
100658
101264
|
prPollerLog
|
|
100659
101265
|
);
|
|
101266
|
+
const unsubscribeCwdChanged = daemon.taskManager.addOnCwdChanged((taskId, newCwd) => {
|
|
101267
|
+
void prPoller.updateCwd(taskId, newCwd);
|
|
101268
|
+
});
|
|
100660
101269
|
function attributePrIfFirstDiscovery(payload) {
|
|
100661
101270
|
const { taskId } = payload;
|
|
100662
101271
|
const pr = payload.currentBranchPR;
|
|
@@ -101489,6 +102098,7 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
|
|
|
101489
102098
|
}
|
|
101490
102099
|
controlHandler = handler;
|
|
101491
102100
|
const controlPeerId = deps?.machineId ?? `_control_${crypto.randomUUID()}`;
|
|
102101
|
+
const channelEpoch = registerControlChannelEpoch(controlPeerId);
|
|
101492
102102
|
daemon.taskManager.registerControlChannel(controlPeerId, handler.sendControl);
|
|
101493
102103
|
daemon.taskManager.setOnAuthNotLoggedIn(() => {
|
|
101494
102104
|
const preferred = daemon.capabilities?.anthropicAuth?.method !== "none" ? daemon.capabilities?.anthropicAuth?.method : void 0;
|
|
@@ -101590,6 +102200,15 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
|
|
|
101590
102200
|
handler.onMessage(raw);
|
|
101591
102201
|
};
|
|
101592
102202
|
dc.onclose = () => {
|
|
102203
|
+
if (!isLiveControlChannelEpoch(controlPeerId, channelEpoch)) {
|
|
102204
|
+
logAdapter({
|
|
102205
|
+
event: "control_channel_onclose_stale",
|
|
102206
|
+
peerId: controlPeerId,
|
|
102207
|
+
staleEpoch: channelEpoch,
|
|
102208
|
+
currentEpoch: controlChannelEpochs.get(controlPeerId) ?? null
|
|
102209
|
+
});
|
|
102210
|
+
return;
|
|
102211
|
+
}
|
|
101593
102212
|
reassembler.clear();
|
|
101594
102213
|
if (loginChild) {
|
|
101595
102214
|
loginChild.kill();
|
|
@@ -101598,6 +102217,7 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
|
|
|
101598
102217
|
for (const unsub of resourceUnsubs.values()) unsub();
|
|
101599
102218
|
resourceUnsubs.clear();
|
|
101600
102219
|
userSettingsUnsub();
|
|
102220
|
+
unsubscribeCwdChanged();
|
|
101601
102221
|
prPoller.dispose();
|
|
101602
102222
|
handler.dispose();
|
|
101603
102223
|
guardedChannel.dispose();
|
|
@@ -101609,6 +102229,7 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
|
|
|
101609
102229
|
ref.pool = removePresence(ref.pool, controlPeerId);
|
|
101610
102230
|
daemon.taskManager.broadcastControl({ type: "presence_state", peers: ref.pool });
|
|
101611
102231
|
}
|
|
102232
|
+
controlChannelEpochs.delete(controlPeerId);
|
|
101612
102233
|
};
|
|
101613
102234
|
logAdapter({
|
|
101614
102235
|
event: "control_channel_wired",
|
|
@@ -101746,8 +102367,12 @@ function handleParticipantsList(room, participants, deps) {
|
|
|
101746
102367
|
userId: p2.userId,
|
|
101747
102368
|
username: p2.username,
|
|
101748
102369
|
avatarUrl: p2.avatarUrl,
|
|
101749
|
-
role: p2.role
|
|
102370
|
+
role: p2.role,
|
|
102371
|
+
connectionId: p2.connectionId
|
|
101750
102372
|
}));
|
|
102373
|
+
for (const p2 of participants) {
|
|
102374
|
+
room.connectionIdByUser.set(p2.userId, p2.connectionId);
|
|
102375
|
+
}
|
|
101751
102376
|
bumpAndNotify(room, deps);
|
|
101752
102377
|
for (const participant of participants) {
|
|
101753
102378
|
if (participant.userId === room.myUserId) continue;
|
|
@@ -101765,6 +102390,7 @@ function handleParticipantsList(room, participants, deps) {
|
|
|
101765
102390
|
function handleParticipantJoined(room, participant, deps) {
|
|
101766
102391
|
if (!room.myUserId) return;
|
|
101767
102392
|
if (participant.userId === room.myUserId) return;
|
|
102393
|
+
room.connectionIdByUser.set(participant.userId, participant.connectionId);
|
|
101768
102394
|
if (room.knownPeers.has(participant.userId)) return;
|
|
101769
102395
|
room.participants = [
|
|
101770
102396
|
...room.participants,
|
|
@@ -101772,31 +102398,38 @@ function handleParticipantJoined(room, participant, deps) {
|
|
|
101772
102398
|
userId: participant.userId,
|
|
101773
102399
|
username: participant.username,
|
|
101774
102400
|
avatarUrl: participant.avatarUrl,
|
|
101775
|
-
role: participant.role
|
|
102401
|
+
role: participant.role,
|
|
102402
|
+
connectionId: participant.connectionId
|
|
101776
102403
|
}
|
|
101777
102404
|
];
|
|
101778
102405
|
bumpAndNotify(room, deps);
|
|
101779
102406
|
registerCollabParticipant(room, participant.userId, participant.role, participant.username, deps);
|
|
101780
102407
|
maybeInitiateOffer(room, participant.userId, deps);
|
|
101781
102408
|
}
|
|
101782
|
-
function handleParticipantLeft(room,
|
|
102409
|
+
function handleParticipantLeft(room, msg, deps) {
|
|
102410
|
+
const { userId, connectionId } = msg;
|
|
102411
|
+
const tracked = room.connectionIdByUser.get(userId);
|
|
102412
|
+
if (tracked !== void 0 && tracked !== connectionId) {
|
|
102413
|
+
deps.log({
|
|
102414
|
+
event: "participant_left_stale_dropped",
|
|
102415
|
+
roomId: room.roomId,
|
|
102416
|
+
userId,
|
|
102417
|
+
incomingConnectionId: connectionId,
|
|
102418
|
+
trackedConnectionId: tracked
|
|
102419
|
+
});
|
|
102420
|
+
return;
|
|
102421
|
+
}
|
|
101783
102422
|
room.knownPeers.delete(userId);
|
|
101784
102423
|
room.participants = room.participants.filter((p2) => p2.userId !== userId);
|
|
102424
|
+
room.connectionIdByUser.delete(userId);
|
|
101785
102425
|
bumpAndNotify(room, deps);
|
|
101786
|
-
|
|
101787
|
-
|
|
102426
|
+
const peerId = namespacePeerId(room.roomId, userId);
|
|
102427
|
+
deps.peerRoleRegistry.unregisterPeer(peerId);
|
|
102428
|
+
room.peerManager.closePeer(peerId);
|
|
101788
102429
|
deps.log({ event: "collab_participant_left", roomId: room.roomId, userId });
|
|
101789
102430
|
}
|
|
101790
|
-
function handleWebrtcSignaling(room, type, targetUserId, payload, deps) {
|
|
102431
|
+
function handleWebrtcSignaling(room, type, targetUserId, generationId, payload, deps) {
|
|
101791
102432
|
const peerId = namespacePeerId(room.roomId, targetUserId);
|
|
101792
|
-
const generationId = `collab:${peerId}`;
|
|
101793
|
-
deps.log({
|
|
101794
|
-
event: "collab_generationid_placeholder",
|
|
101795
|
-
roomId: room.roomId,
|
|
101796
|
-
targetUserId,
|
|
101797
|
-
type,
|
|
101798
|
-
generationId
|
|
101799
|
-
});
|
|
101800
102433
|
const actions = {
|
|
101801
102434
|
// eslint-disable-next-line no-restricted-syntax -- SDP is opaque z.unknown() from signaling
|
|
101802
102435
|
offer: () => room.peerManager.handleOffer(peerId, payload, generationId),
|
|
@@ -101806,7 +102439,12 @@ function handleWebrtcSignaling(room, type, targetUserId, payload, deps) {
|
|
|
101806
102439
|
ice: () => room.peerManager.handleIce(peerId, payload, generationId)
|
|
101807
102440
|
};
|
|
101808
102441
|
actions[type]().catch((err) => {
|
|
101809
|
-
deps.log({
|
|
102442
|
+
deps.log({
|
|
102443
|
+
event: `collab_${type}_failed`,
|
|
102444
|
+
roomId: room.roomId,
|
|
102445
|
+
generationId,
|
|
102446
|
+
error: String(err)
|
|
102447
|
+
});
|
|
101810
102448
|
});
|
|
101811
102449
|
}
|
|
101812
102450
|
function handleCollabRoomMessage(room, msg, deps) {
|
|
@@ -101822,16 +102460,16 @@ function handleCollabRoomMessage(room, msg, deps) {
|
|
|
101822
102460
|
handleParticipantJoined(room, msg.participant, deps);
|
|
101823
102461
|
break;
|
|
101824
102462
|
case "participant-left":
|
|
101825
|
-
handleParticipantLeft(room, msg.userId, deps);
|
|
102463
|
+
handleParticipantLeft(room, { userId: msg.userId, connectionId: msg.connectionId }, deps);
|
|
101826
102464
|
break;
|
|
101827
102465
|
case "webrtc-offer":
|
|
101828
|
-
handleWebrtcSignaling(room, "offer", msg.targetUserId, msg.offer, deps);
|
|
102466
|
+
handleWebrtcSignaling(room, "offer", msg.targetUserId, msg.generationId, msg.offer, deps);
|
|
101829
102467
|
break;
|
|
101830
102468
|
case "webrtc-answer":
|
|
101831
|
-
handleWebrtcSignaling(room, "answer", msg.targetUserId, msg.answer, deps);
|
|
102469
|
+
handleWebrtcSignaling(room, "answer", msg.targetUserId, msg.generationId, msg.answer, deps);
|
|
101832
102470
|
break;
|
|
101833
102471
|
case "webrtc-ice":
|
|
101834
|
-
handleWebrtcSignaling(room, "ice", msg.targetUserId, msg.candidate, deps);
|
|
102472
|
+
handleWebrtcSignaling(room, "ice", msg.targetUserId, msg.generationId, msg.candidate, deps);
|
|
101835
102473
|
break;
|
|
101836
102474
|
case "ice-servers":
|
|
101837
102475
|
room.peerManager.updateIceServers(msg.iceServers);
|
|
@@ -101967,6 +102605,7 @@ function createCollabRoomManager(deps) {
|
|
|
101967
102605
|
myUserId: null,
|
|
101968
102606
|
knownPeers: /* @__PURE__ */ new Set(),
|
|
101969
102607
|
participants: [],
|
|
102608
|
+
connectionIdByUser: /* @__PURE__ */ new Map(),
|
|
101970
102609
|
expiresAt,
|
|
101971
102610
|
generation: 0,
|
|
101972
102611
|
expiryTimer: setTimeout(() => {
|
|
@@ -102000,6 +102639,7 @@ function createCollabRoomManager(deps) {
|
|
|
102000
102639
|
);
|
|
102001
102640
|
room.myUserId = null;
|
|
102002
102641
|
room.knownPeers.clear();
|
|
102642
|
+
room.connectionIdByUser.clear();
|
|
102003
102643
|
}
|
|
102004
102644
|
});
|
|
102005
102645
|
connection.connect();
|
|
@@ -102046,10 +102686,53 @@ function createCollabRoomManager(deps) {
|
|
|
102046
102686
|
|
|
102047
102687
|
// src/services/file-io-handler.ts
|
|
102048
102688
|
import { execFile as execFile10, spawn as spawn6 } from "child_process";
|
|
102049
|
-
import { readdir as readdir14, readFile as
|
|
102050
|
-
import { normalize as
|
|
102689
|
+
import { readdir as readdir14, readFile as readFile35, realpath as realpath2, stat as stat10, unlink as unlink10, writeFile as writeFile28 } from "fs/promises";
|
|
102690
|
+
import { normalize as normalize7, relative as relative3, resolve as resolve2 } from "path";
|
|
102051
102691
|
import { promisify as promisify7 } from "util";
|
|
102052
102692
|
|
|
102693
|
+
// src/shared/file-io-path-safety.ts
|
|
102694
|
+
import { realpath } from "fs/promises";
|
|
102695
|
+
import { basename as basename5, dirname as dirname19, join as join52, normalize as normalize6 } from "path";
|
|
102696
|
+
async function safeAbsolutePath(userPath, isHidden2, allowedHiddenNames) {
|
|
102697
|
+
const normalized = prepareAbsolutePath(userPath, isHidden2, allowedHiddenNames);
|
|
102698
|
+
if (normalized === null) return null;
|
|
102699
|
+
const canonical = await canonicalizeOrRecombine(normalized);
|
|
102700
|
+
if (canonical === null) return null;
|
|
102701
|
+
if (!checkSegmentsAllowed(canonical, isHidden2, allowedHiddenNames)) return null;
|
|
102702
|
+
return canonical;
|
|
102703
|
+
}
|
|
102704
|
+
function prepareAbsolutePath(userPath, isHidden2, allowedHiddenNames) {
|
|
102705
|
+
if (!userPath.startsWith("/")) return null;
|
|
102706
|
+
const normalized = normalize6(userPath);
|
|
102707
|
+
if (normalized.startsWith("..") || normalized.includes("/..") || normalized.includes("\\..")) {
|
|
102708
|
+
return null;
|
|
102709
|
+
}
|
|
102710
|
+
if (!checkSegmentsAllowed(normalized, isHidden2, allowedHiddenNames)) return null;
|
|
102711
|
+
return normalized;
|
|
102712
|
+
}
|
|
102713
|
+
async function canonicalizeOrRecombine(path2) {
|
|
102714
|
+
try {
|
|
102715
|
+
return await realpath(path2);
|
|
102716
|
+
} catch (err) {
|
|
102717
|
+
if (!isEnoent2(err)) return null;
|
|
102718
|
+
const parentCanonical = await realpath(dirname19(path2)).catch(() => null);
|
|
102719
|
+
if (parentCanonical === null) return null;
|
|
102720
|
+
return join52(parentCanonical, basename5(path2));
|
|
102721
|
+
}
|
|
102722
|
+
}
|
|
102723
|
+
function isEnoent2(err) {
|
|
102724
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
102725
|
+
}
|
|
102726
|
+
function checkSegmentsAllowed(path2, isHidden2, allowedHiddenNames) {
|
|
102727
|
+
const segments = path2.split(/[/\\]/).filter((s2) => s2.length > 0 && s2 !== ".");
|
|
102728
|
+
for (const seg of segments) {
|
|
102729
|
+
if (!isHidden2(seg)) continue;
|
|
102730
|
+
if (allowedHiddenNames?.has(seg)) continue;
|
|
102731
|
+
return false;
|
|
102732
|
+
}
|
|
102733
|
+
return true;
|
|
102734
|
+
}
|
|
102735
|
+
|
|
102053
102736
|
// src/services/channels/handlers/snapshot-diff-handler.ts
|
|
102054
102737
|
function planSnapshotDiff(input) {
|
|
102055
102738
|
const { fromRef, toRef, fromRefExists, toRefExists, autoFetch, parentIsMerged } = input;
|
|
@@ -102280,7 +102963,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
102280
102963
|
return safePathWithOverrides(userPath, /* @__PURE__ */ new Set(["node_modules"]));
|
|
102281
102964
|
}
|
|
102282
102965
|
function safePathWithOverrides(userPath, allowedHiddenNames) {
|
|
102283
|
-
const normalized =
|
|
102966
|
+
const normalized = normalize7(userPath);
|
|
102284
102967
|
if (normalized.startsWith("..") || normalized.includes("/..") || normalized.includes("\\..")) {
|
|
102285
102968
|
return null;
|
|
102286
102969
|
}
|
|
@@ -102295,6 +102978,9 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
102295
102978
|
if (rel.startsWith("..") || rel.startsWith("/")) return null;
|
|
102296
102979
|
return abs;
|
|
102297
102980
|
}
|
|
102981
|
+
function safeAbsolutePath2(userPath, allowedHiddenNames) {
|
|
102982
|
+
return safeAbsolutePath(userPath, isHidden, allowedHiddenNames);
|
|
102983
|
+
}
|
|
102298
102984
|
function onMessage(data) {
|
|
102299
102985
|
let parsed;
|
|
102300
102986
|
try {
|
|
@@ -102347,26 +103033,38 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
102347
103033
|
assertNever3(msg);
|
|
102348
103034
|
}
|
|
102349
103035
|
}
|
|
102350
|
-
function dispatchFileOp(msg) {
|
|
103036
|
+
async function dispatchFileOp(msg) {
|
|
102351
103037
|
const isReadOnlyOp = msg.type === "read_file" || msg.type === "stat";
|
|
102352
|
-
const
|
|
103038
|
+
const isExternal = isReadOnlyOp && msg.external === true;
|
|
103039
|
+
if (isExternal && !deps.allowExternalReads) {
|
|
103040
|
+
log({ event: "file_io_external_denied", path: msg.path });
|
|
103041
|
+
respondError(msg.requestId, `external_not_allowed:${msg.path}`);
|
|
103042
|
+
return;
|
|
103043
|
+
}
|
|
103044
|
+
let abs;
|
|
103045
|
+
if (isExternal) {
|
|
103046
|
+
abs = await safeAbsolutePath2(msg.path, /* @__PURE__ */ new Set(["node_modules"]));
|
|
103047
|
+
} else {
|
|
103048
|
+
abs = isReadOnlyOp ? safePathForRead(msg.path) : safePath(msg.path);
|
|
103049
|
+
}
|
|
102353
103050
|
if (!abs) {
|
|
102354
|
-
log({ event: "file_io_path_rejected", path: msg.path });
|
|
102355
|
-
|
|
103051
|
+
log({ event: "file_io_path_rejected", path: msg.path, external: isExternal });
|
|
103052
|
+
const code2 = isExternal ? "external_blocked" : "hidden_path";
|
|
103053
|
+
respondError(msg.requestId, `${code2}:${msg.path}`);
|
|
102356
103054
|
return;
|
|
102357
103055
|
}
|
|
102358
103056
|
switch (msg.type) {
|
|
102359
103057
|
case "read_file":
|
|
102360
|
-
handleReadFile(msg.requestId, abs);
|
|
103058
|
+
await handleReadFile(msg.requestId, abs);
|
|
102361
103059
|
break;
|
|
102362
103060
|
case "write_file":
|
|
102363
|
-
handleWriteFile(msg.requestId, abs, msg.content);
|
|
103061
|
+
await handleWriteFile(msg.requestId, abs, msg.content);
|
|
102364
103062
|
break;
|
|
102365
103063
|
case "readdir":
|
|
102366
|
-
handleReaddir(msg.requestId, abs);
|
|
103064
|
+
await handleReaddir(msg.requestId, abs);
|
|
102367
103065
|
break;
|
|
102368
103066
|
case "stat":
|
|
102369
|
-
handleStat(msg.requestId, abs);
|
|
103067
|
+
await handleStat(msg.requestId, abs);
|
|
102370
103068
|
break;
|
|
102371
103069
|
default:
|
|
102372
103070
|
assertNever3(msg);
|
|
@@ -102389,7 +103087,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
102389
103087
|
case "write_file":
|
|
102390
103088
|
case "readdir":
|
|
102391
103089
|
case "stat":
|
|
102392
|
-
dispatchFileOp(msg);
|
|
103090
|
+
void dispatchFileOp(msg);
|
|
102393
103091
|
break;
|
|
102394
103092
|
case "set_cwd":
|
|
102395
103093
|
handleSetCwd(msg.requestId, msg.path);
|
|
@@ -102456,7 +103154,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
102456
103154
|
respondError(requestId, "File too large (>10MB)");
|
|
102457
103155
|
return;
|
|
102458
103156
|
}
|
|
102459
|
-
const content = await
|
|
103157
|
+
const content = await readFile35(absPath, "utf-8");
|
|
102460
103158
|
respond({ type: "file_content", requestId, content });
|
|
102461
103159
|
} catch (err) {
|
|
102462
103160
|
respondError(requestId, formatError(err));
|
|
@@ -102505,7 +103203,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
102505
103203
|
async function handleSetCwd(requestId, newPath) {
|
|
102506
103204
|
try {
|
|
102507
103205
|
const resolved = resolve2(newPath);
|
|
102508
|
-
const canonical = await
|
|
103206
|
+
const canonical = await realpath2(resolved);
|
|
102509
103207
|
if (!deps.isAllowedCwd(canonical)) {
|
|
102510
103208
|
respondError(requestId, "cwd not allowed");
|
|
102511
103209
|
log({ event: "file_io_cwd_rejected", cwd: canonical });
|
|
@@ -102544,7 +103242,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
102544
103242
|
const { stdout } = await execFileAsync3(
|
|
102545
103243
|
"git",
|
|
102546
103244
|
["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
|
102547
|
-
{ cwd, maxBuffer:
|
|
103245
|
+
{ cwd, maxBuffer: 100 * 1024 * 1024 }
|
|
102548
103246
|
);
|
|
102549
103247
|
const files = stdout.split("\0").filter((f2) => f2.length > 0);
|
|
102550
103248
|
respond({ type: "git_ls_files_result", requestId, files });
|
|
@@ -102650,7 +103348,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
102650
103348
|
const originalContent = await readGitObject(`${mergeBase}:${filePath}`) ?? "";
|
|
102651
103349
|
let modifiedContent;
|
|
102652
103350
|
try {
|
|
102653
|
-
modifiedContent = await
|
|
103351
|
+
modifiedContent = await readFile35(resolve2(cwd, filePath), "utf-8");
|
|
102654
103352
|
} catch {
|
|
102655
103353
|
modifiedContent = "";
|
|
102656
103354
|
}
|
|
@@ -102707,7 +103405,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
|
|
|
102707
103405
|
const indexContent = await readGitObject(`:${safeRelPath}`);
|
|
102708
103406
|
originalContent = indexContent ?? await readGitObject(`HEAD:${safeRelPath}`) ?? "";
|
|
102709
103407
|
try {
|
|
102710
|
-
modifiedContent = await
|
|
103408
|
+
modifiedContent = await readFile35(absPath, "utf-8");
|
|
102711
103409
|
} catch {
|
|
102712
103410
|
modifiedContent = "";
|
|
102713
103411
|
}
|
|
@@ -103332,13 +104030,13 @@ function wireThreadErrorFallback(daemon, dc, taskId, threadId, channelId, log) {
|
|
|
103332
104030
|
// src/shared/pty-manager.ts
|
|
103333
104031
|
import { accessSync, chmodSync, constants as constants2 } from "fs";
|
|
103334
104032
|
import { createRequire as createRequire4 } from "module";
|
|
103335
|
-
import { dirname as
|
|
104033
|
+
import { dirname as dirname20, resolve as resolve3 } from "path";
|
|
103336
104034
|
import * as pty from "node-pty";
|
|
103337
104035
|
function ensureSpawnHelperExecutable() {
|
|
103338
104036
|
if (globalThis.process.platform === "win32") return;
|
|
103339
104037
|
try {
|
|
103340
104038
|
const req = createRequire4(import.meta.url);
|
|
103341
|
-
const nodePtyDir =
|
|
104039
|
+
const nodePtyDir = dirname20(req.resolve("node-pty/package.json"));
|
|
103342
104040
|
const spawnHelper = resolve3(
|
|
103343
104041
|
nodePtyDir,
|
|
103344
104042
|
"prebuilds",
|
|
@@ -103753,32 +104451,35 @@ function buildCollabRoomManager(deps) {
|
|
|
103753
104451
|
webrtcAdapter: collabWebrtcAdapter,
|
|
103754
104452
|
log: createChildLogger({ mode: `collab-peer:${roomId}` }),
|
|
103755
104453
|
logAdapter,
|
|
103756
|
-
onAnswer(namespacedId, answer) {
|
|
104454
|
+
onAnswer(namespacedId, answer, generationId) {
|
|
103757
104455
|
const prefix = `collab:${roomId}:`;
|
|
103758
104456
|
const targetUserId = namespacedId.startsWith(prefix) ? namespacedId.slice(prefix.length) : namespacedId;
|
|
103759
104457
|
connection.send({
|
|
103760
104458
|
type: "webrtc-answer",
|
|
103761
104459
|
targetUserId,
|
|
104460
|
+
generationId,
|
|
103762
104461
|
// eslint-disable-next-line no-restricted-syntax -- SDP is opaque over signaling
|
|
103763
104462
|
answer
|
|
103764
104463
|
});
|
|
103765
104464
|
},
|
|
103766
|
-
onOffer(namespacedId, offer) {
|
|
104465
|
+
onOffer(namespacedId, offer, generationId) {
|
|
103767
104466
|
const prefix = `collab:${roomId}:`;
|
|
103768
104467
|
const targetUserId = namespacedId.startsWith(prefix) ? namespacedId.slice(prefix.length) : namespacedId;
|
|
103769
104468
|
connection.send({
|
|
103770
104469
|
type: "webrtc-offer",
|
|
103771
104470
|
targetUserId,
|
|
104471
|
+
generationId,
|
|
103772
104472
|
// eslint-disable-next-line no-restricted-syntax -- SDP is opaque over signaling
|
|
103773
104473
|
offer
|
|
103774
104474
|
});
|
|
103775
104475
|
},
|
|
103776
|
-
onIceCandidate(namespacedId, candidate) {
|
|
104476
|
+
onIceCandidate(namespacedId, candidate, generationId) {
|
|
103777
104477
|
const prefix = `collab:${roomId}:`;
|
|
103778
104478
|
const targetUserId = namespacedId.startsWith(prefix) ? namespacedId.slice(prefix.length) : namespacedId;
|
|
103779
104479
|
connection.send({
|
|
103780
104480
|
type: "webrtc-ice",
|
|
103781
104481
|
targetUserId,
|
|
104482
|
+
generationId,
|
|
103782
104483
|
// eslint-disable-next-line no-restricted-syntax -- ICE candidate is opaque over signaling
|
|
103783
104484
|
candidate
|
|
103784
104485
|
});
|
|
@@ -103933,7 +104634,16 @@ function buildCollabRoomManager(deps) {
|
|
|
103933
104634
|
const taskCwd = daemon.taskManager.getTaskCwd(collabTaskId);
|
|
103934
104635
|
if (!taskCwd) return false;
|
|
103935
104636
|
return isUnderAllowedRoot2(abs, [taskCwd]);
|
|
103936
|
-
}
|
|
104637
|
+
},
|
|
104638
|
+
/**
|
|
104639
|
+
* Collab peers must NOT get external-path reads — a peer
|
|
104640
|
+
* could otherwise send `{external: true, path: '/etc/hosts'}`
|
|
104641
|
+
* or walk to another task's cwd via the abs-path flag, which
|
|
104642
|
+
* would defeat the entire task-isolation allowlist above.
|
|
104643
|
+
* The daemon rejects external:true with
|
|
104644
|
+
* `external_not_allowed:...` when this flag is false.
|
|
104645
|
+
*/
|
|
104646
|
+
allowExternalReads: false
|
|
103937
104647
|
}
|
|
103938
104648
|
});
|
|
103939
104649
|
log.info("Collab file I/O channel wired");
|
|
@@ -104065,13 +104775,13 @@ function buildCollabRoomManager(deps) {
|
|
|
104065
104775
|
import { execSync } from "child_process";
|
|
104066
104776
|
import { existsSync as existsSync8 } from "fs";
|
|
104067
104777
|
import { createRequire as createRequire5 } from "module";
|
|
104068
|
-
import { dirname as
|
|
104778
|
+
import { dirname as dirname21, join as join54 } from "path";
|
|
104069
104779
|
|
|
104070
104780
|
// src/services/bootstrap/self-update.ts
|
|
104071
104781
|
import { execFile as execFile11, spawn as spawn8 } from "child_process";
|
|
104072
104782
|
import { createHash as createHash6 } from "crypto";
|
|
104073
|
-
import { chmod as chmod3, mkdir as mkdir22, readFile as
|
|
104074
|
-
import { join as
|
|
104783
|
+
import { chmod as chmod3, mkdir as mkdir22, readFile as readFile36, rename as rename20, unlink as unlink11, writeFile as writeFile29 } from "fs/promises";
|
|
104784
|
+
import { join as join53 } from "path";
|
|
104075
104785
|
|
|
104076
104786
|
// src/services/bootstrap/self-update-installer-scripts.ts
|
|
104077
104787
|
function buildPosixInstallerScript(params) {
|
|
@@ -104473,7 +105183,7 @@ async function downloadTarball(url, destPath, fetchFn) {
|
|
|
104473
105183
|
throw new Error(`download failed: HTTP ${response.status} ${response.statusText}`);
|
|
104474
105184
|
}
|
|
104475
105185
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
104476
|
-
await mkdir22(
|
|
105186
|
+
await mkdir22(join53(destPath, ".."), { recursive: true });
|
|
104477
105187
|
try {
|
|
104478
105188
|
await writeFile29(tmpPath, bytes);
|
|
104479
105189
|
await rename20(tmpPath, destPath);
|
|
@@ -104492,7 +105202,7 @@ async function downloadTarball(url, destPath, fetchFn) {
|
|
|
104492
105202
|
}
|
|
104493
105203
|
async function verifyChecksum(path2, expectedHash) {
|
|
104494
105204
|
const algo = expectedHash.length === 40 ? "sha1" : "sha256";
|
|
104495
|
-
const raw = await
|
|
105205
|
+
const raw = await readFile36(path2);
|
|
104496
105206
|
const actual = createHash6(algo).update(raw).digest("hex");
|
|
104497
105207
|
if (actual !== expectedHash) {
|
|
104498
105208
|
try {
|
|
@@ -104503,7 +105213,7 @@ async function verifyChecksum(path2, expectedHash) {
|
|
|
104503
105213
|
}
|
|
104504
105214
|
}
|
|
104505
105215
|
async function stageInstallerScript(scriptPath, params) {
|
|
104506
|
-
await mkdir22(
|
|
105216
|
+
await mkdir22(join53(scriptPath, ".."), { recursive: true });
|
|
104507
105217
|
const body = process.platform === "win32" ? buildWindowsInstallerScript(params) : buildPosixInstallerScript(params);
|
|
104508
105218
|
await writeFile29(scriptPath, body);
|
|
104509
105219
|
await chmod3(scriptPath, 493);
|
|
@@ -104553,16 +105263,16 @@ function buildInstallerParams(state, deps) {
|
|
|
104553
105263
|
targetVersion: state.resolved.version,
|
|
104554
105264
|
previousVersion: deps.currentVersion,
|
|
104555
105265
|
parentPid: deps.pid,
|
|
104556
|
-
statusFilePath:
|
|
104557
|
-
pidFilePath:
|
|
104558
|
-
logPath:
|
|
104559
|
-
snapshotPath:
|
|
105266
|
+
statusFilePath: join53(deps.shipyardHome, "update-status.json"),
|
|
105267
|
+
pidFilePath: join53(deps.shipyardHome, "daemon.pid"),
|
|
105268
|
+
logPath: join53(deps.shipyardHome, "updates", `install-${deps.pid}.log`),
|
|
105269
|
+
snapshotPath: join53(deps.shipyardHome, "updates", `rollback-${deps.currentVersion}`),
|
|
104560
105270
|
npmBin: "npm"
|
|
104561
105271
|
};
|
|
104562
105272
|
}
|
|
104563
105273
|
function tarballPathFor(shipyardHome, version, shasum) {
|
|
104564
105274
|
const shaPrefix = shasum.slice(0, 12);
|
|
104565
|
-
return
|
|
105275
|
+
return join53(shipyardHome, "updates", `${version}-${shaPrefix}.tgz`);
|
|
104566
105276
|
}
|
|
104567
105277
|
async function runResolveStep(step, state, deps) {
|
|
104568
105278
|
const resolver = deps.resolveVersion ?? defaultResolveVersion;
|
|
@@ -104728,7 +105438,7 @@ function resolveClaudeCodePath(log) {
|
|
|
104728
105438
|
try {
|
|
104729
105439
|
const req = createRequire5(import.meta.url);
|
|
104730
105440
|
const sdkMain = req.resolve("@anthropic-ai/claude-agent-sdk");
|
|
104731
|
-
const p2 =
|
|
105441
|
+
const p2 = join54(dirname21(sdkMain), "cli.js");
|
|
104732
105442
|
if (existsSync8(p2)) return ok("sdk_bundled", p2);
|
|
104733
105443
|
} catch {
|
|
104734
105444
|
}
|
|
@@ -104738,7 +105448,7 @@ function resolveClaudeCodePath(log) {
|
|
|
104738
105448
|
} catch {
|
|
104739
105449
|
}
|
|
104740
105450
|
for (const c of [
|
|
104741
|
-
|
|
105451
|
+
join54(process.env.HOME ?? "", ".local", "bin", "claude"),
|
|
104742
105452
|
"/usr/local/bin/claude"
|
|
104743
105453
|
])
|
|
104744
105454
|
if (existsSync8(c)) return ok("well_known", c);
|
|
@@ -105069,7 +105779,14 @@ function buildSharedChannelCallbacks(deps) {
|
|
|
105069
105779
|
diffTurn: (taskId, turnIndex) => daemon.taskManager.diffTurn(taskId, turnIndex),
|
|
105070
105780
|
diffTurnFile: (taskId, turnIndex, path2) => daemon.taskManager.diffTurnFile(taskId, turnIndex, path2),
|
|
105071
105781
|
revertTurn: (taskId, turnIndex, mode, filePath) => daemon.taskManager.revertTurn(taskId, turnIndex, mode, filePath),
|
|
105072
|
-
isAllowedCwd: (abs) => isUnderAllowedRoot2(abs, daemon.taskManager.getAllowedCwds())
|
|
105782
|
+
isAllowedCwd: (abs) => isUnderAllowedRoot2(abs, daemon.taskManager.getAllowedCwds()),
|
|
105783
|
+
/**
|
|
105784
|
+
* Personal-mode: owner is operating on their own host, so reading
|
|
105785
|
+
* arbitrary files outside the workspace (subject to HIDDEN_PATTERNS)
|
|
105786
|
+
* is the feature that lets them open ~/.claude/plans/*.md and
|
|
105787
|
+
* similar. Collab wiring must pass `false` here.
|
|
105788
|
+
*/
|
|
105789
|
+
allowExternalReads: true
|
|
105073
105790
|
},
|
|
105074
105791
|
onCwdChange: (newCwd) => {
|
|
105075
105792
|
portDetectorRef.current?.setCwd(newCwd);
|
|
@@ -105236,8 +105953,8 @@ function buildLocalDirectChannelCallbacks(deps) {
|
|
|
105236
105953
|
|
|
105237
105954
|
// src/services/storage/daemon-settings-store.ts
|
|
105238
105955
|
import { createHash as createHash7 } from "crypto";
|
|
105239
|
-
import { mkdir as mkdir23, readFile as
|
|
105240
|
-
import { join as
|
|
105956
|
+
import { mkdir as mkdir23, readFile as readFile37, rename as rename21, writeFile as writeFile30 } from "fs/promises";
|
|
105957
|
+
import { join as join55 } from "path";
|
|
105241
105958
|
var ProjectSettingsSchema = external_exports.object({
|
|
105242
105959
|
disabledMcpServers: external_exports.array(external_exports.string()).optional()
|
|
105243
105960
|
});
|
|
@@ -105245,9 +105962,9 @@ function hashProjectPath(projectPath) {
|
|
|
105245
105962
|
return createHash7("sha256").update(projectPath).digest("hex").slice(0, 16);
|
|
105246
105963
|
}
|
|
105247
105964
|
function buildDaemonSettingsStore(dataDir) {
|
|
105248
|
-
const settingsDir =
|
|
105965
|
+
const settingsDir = join55(dataDir, "settings");
|
|
105249
105966
|
function settingsPath(projectPath) {
|
|
105250
|
-
return
|
|
105967
|
+
return join55(settingsDir, `${hashProjectPath(projectPath)}.json`);
|
|
105251
105968
|
}
|
|
105252
105969
|
async function ensureDir() {
|
|
105253
105970
|
await mkdir23(settingsDir, { recursive: true });
|
|
@@ -105255,7 +105972,7 @@ function buildDaemonSettingsStore(dataDir) {
|
|
|
105255
105972
|
return {
|
|
105256
105973
|
async load(projectPath) {
|
|
105257
105974
|
try {
|
|
105258
|
-
const raw = await
|
|
105975
|
+
const raw = await readFile37(settingsPath(projectPath), "utf-8");
|
|
105259
105976
|
return ProjectSettingsSchema.parse(JSON.parse(raw));
|
|
105260
105977
|
} catch (err) {
|
|
105261
105978
|
if (isEnoent(err)) return {};
|
|
@@ -105273,19 +105990,19 @@ function buildDaemonSettingsStore(dataDir) {
|
|
|
105273
105990
|
}
|
|
105274
105991
|
|
|
105275
105992
|
// src/services/storage/plugin-config-store.ts
|
|
105276
|
-
import { mkdir as mkdir24, readFile as
|
|
105277
|
-
import { join as
|
|
105993
|
+
import { mkdir as mkdir24, readFile as readFile38, rename as rename22, writeFile as writeFile31 } from "fs/promises";
|
|
105994
|
+
import { join as join56 } from "path";
|
|
105278
105995
|
function buildPluginConfigStore(pluginsDir) {
|
|
105279
105996
|
const cache2 = /* @__PURE__ */ new Map();
|
|
105280
105997
|
const writeQueues = /* @__PURE__ */ new Map();
|
|
105281
105998
|
function configPath(pluginId) {
|
|
105282
|
-
return
|
|
105999
|
+
return join56(pluginsDir, pluginId, "config.json");
|
|
105283
106000
|
}
|
|
105284
106001
|
async function ensureLoaded(pluginId) {
|
|
105285
106002
|
const cached2 = cache2.get(pluginId);
|
|
105286
106003
|
if (cached2) return cached2;
|
|
105287
106004
|
try {
|
|
105288
|
-
const raw = await
|
|
106005
|
+
const raw = await readFile38(configPath(pluginId), "utf-8");
|
|
105289
106006
|
const parsed = JSON.parse(raw);
|
|
105290
106007
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
105291
106008
|
const record = Object.fromEntries(Object.entries(parsed));
|
|
@@ -105310,7 +106027,7 @@ function buildPluginConfigStore(pluginsDir) {
|
|
|
105310
106027
|
const next = prev.then(async () => {
|
|
105311
106028
|
const filePath = configPath(pluginId);
|
|
105312
106029
|
const tmpPath = `${filePath}.tmp`;
|
|
105313
|
-
await mkdir24(
|
|
106030
|
+
await mkdir24(join56(pluginsDir, pluginId), { recursive: true });
|
|
105314
106031
|
await writeFile31(tmpPath, JSON.stringify(config2, null, 2), "utf-8");
|
|
105315
106032
|
await rename22(tmpPath, filePath);
|
|
105316
106033
|
});
|
|
@@ -105331,13 +106048,13 @@ function buildPluginConfigStore(pluginsDir) {
|
|
|
105331
106048
|
var LEGACY_EPOCH = 5;
|
|
105332
106049
|
async function serve(options = {}) {
|
|
105333
106050
|
const shipyardHome = options.shipyardHome ?? getShipyardHome();
|
|
105334
|
-
const dataDir =
|
|
106051
|
+
const dataDir = join57(shipyardHome, options.isDev ? "data-dev" : "data");
|
|
105335
106052
|
const log = createChildLogger({ mode: "serve" });
|
|
105336
|
-
const workspaceRoot = await
|
|
106053
|
+
const workspaceRoot = await realpath3(findProjectRoot(process.cwd())).catch(
|
|
105337
106054
|
() => findProjectRoot(process.cwd())
|
|
105338
106055
|
);
|
|
105339
106056
|
registerBuiltinPlugins();
|
|
105340
|
-
const pluginConfigStore = buildPluginConfigStore(
|
|
106057
|
+
const pluginConfigStore = buildPluginConfigStore(join57(dataDir, "plugins"));
|
|
105341
106058
|
await mkdir25(dataDir, { recursive: true });
|
|
105342
106059
|
log.info({ shipyardHome, dataDir, workspaceRoot }, "Starting daemon");
|
|
105343
106060
|
function logAdapter(entry) {
|
|
@@ -105363,8 +106080,8 @@ async function serve(options = {}) {
|
|
|
105363
106080
|
await lifecycle.acquirePidFile(shipyardHome);
|
|
105364
106081
|
const pidTracker = buildPidTracker(shipyardHome);
|
|
105365
106082
|
await pidTracker.sweepOrphans(logAdapter);
|
|
105366
|
-
await pruneOldEpochData(
|
|
105367
|
-
const storage = new FileStorageAdapter(
|
|
106083
|
+
await pruneOldEpochData(join57(dataDir, "loro"), LEGACY_EPOCH, logAdapter);
|
|
106084
|
+
const storage = new FileStorageAdapter(join57(dataDir, "loro"));
|
|
105368
106085
|
const personalWebrtcAdapter = new WebRtcDataChannelAdapter();
|
|
105369
106086
|
const peerRoleRegistry = createPeerRoleRegistry();
|
|
105370
106087
|
const presencePoolRef = { pool: {} };
|
|
@@ -105425,7 +106142,7 @@ async function serve(options = {}) {
|
|
|
105425
106142
|
previewProxy
|
|
105426
106143
|
});
|
|
105427
106144
|
daemon.healthMetrics.start();
|
|
105428
|
-
const pluginsDir =
|
|
106145
|
+
const pluginsDir = join57(shipyardHome, "plugins");
|
|
105429
106146
|
await mkdir25(pluginsDir, { recursive: true });
|
|
105430
106147
|
let loadedPlugins = [];
|
|
105431
106148
|
const loadedPluginsRef = {
|
|
@@ -105558,7 +106275,6 @@ async function serve(options = {}) {
|
|
|
105558
106275
|
const peerManager = peerSetupDeps ? buildPeerManager(peerSetupDeps) : null;
|
|
105559
106276
|
if (peerSetupDeps && !localDirectRef.current) {
|
|
105560
106277
|
const handle = await setupLocalDirect({
|
|
105561
|
-
env,
|
|
105562
106278
|
shipyardHome,
|
|
105563
106279
|
daemonId: daemonPeerId,
|
|
105564
106280
|
userId: auth3.userId,
|
|
@@ -105701,4 +106417,4 @@ export {
|
|
|
105701
106417
|
_testing,
|
|
105702
106418
|
serve
|
|
105703
106419
|
};
|
|
105704
|
-
//# sourceMappingURL=serve-
|
|
106420
|
+
//# sourceMappingURL=serve-IMSVRV3B.js.map
|