@schoolai/shipyard 1.0.0 → 1.1.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/README.md +75 -0
- package/dist/{chunk-FNYWUFGS.js → chunk-4IVLSSKL.js} +195 -5
- package/dist/chunk-4IVLSSKL.js.map +1 -0
- package/dist/{chunk-ERDY7OVK.js → chunk-FY3DRRGT.js} +1 -1
- package/dist/chunk-FY3DRRGT.js.map +1 -0
- package/dist/{chunk-EB3RPXYR.js → chunk-K3GFMEBF.js} +2 -2
- package/dist/index.js +1946 -348
- package/dist/index.js.map +1 -1
- package/dist/{login-5RRT37UW.js → login-BS6C2BRS.js} +4 -4
- package/dist/{logout-NWYPFICH.js → logout-XX5ULFHB.js} +3 -3
- package/package.json +3 -2
- package/dist/chunk-ERDY7OVK.js.map +0 -1
- package/dist/chunk-FNYWUFGS.js.map +0 -1
- /package/dist/{chunk-EB3RPXYR.js.map → chunk-K3GFMEBF.js.map} +0 -0
- /package/dist/{login-5RRT37UW.js.map → login-BS6C2BRS.js.map} +0 -0
- /package/dist/{logout-NWYPFICH.js.map → logout-XX5ULFHB.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -6,27 +6,31 @@ import {
|
|
|
6
6
|
BRANCH_NAME_PATTERN,
|
|
7
7
|
CollabCreateRequestSchema,
|
|
8
8
|
CollabCreateResponseSchema,
|
|
9
|
+
CollabRoomConnection,
|
|
9
10
|
ErrorResponseSchema,
|
|
10
11
|
HealthResponseSchema,
|
|
11
12
|
PermissionModeSchema,
|
|
12
13
|
PersonalRoomConnection,
|
|
13
14
|
ROUTES,
|
|
14
15
|
ValidationErrorResponseSchema
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-4IVLSSKL.js";
|
|
16
17
|
import {
|
|
17
18
|
createChildLogger,
|
|
18
19
|
logger
|
|
19
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-FY3DRRGT.js";
|
|
20
21
|
import {
|
|
21
22
|
__export,
|
|
23
|
+
external_exports,
|
|
22
24
|
getShipyardHome,
|
|
23
25
|
validateEnv
|
|
24
26
|
} from "./chunk-HS57GMAL.js";
|
|
25
27
|
|
|
26
28
|
// src/index.ts
|
|
29
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
27
30
|
import { mkdir as mkdir4 } from "fs/promises";
|
|
28
31
|
import { homedir as homedir3 } from "os";
|
|
29
32
|
import { resolve as resolve3 } from "path";
|
|
33
|
+
import { fileURLToPath } from "url";
|
|
30
34
|
import { parseArgs } from "util";
|
|
31
35
|
|
|
32
36
|
// ../../node_modules/.pnpm/@loro-extended+change@6.0.0-beta.0_loro-crdt@1.10.6/node_modules/@loro-extended/change/dist/index.js
|
|
@@ -11759,8 +11763,15 @@ function nanoid(size2 = 21) {
|
|
|
11759
11763
|
}
|
|
11760
11764
|
|
|
11761
11765
|
// ../../packages/loro-schema/dist/index.js
|
|
11762
|
-
var DEFAULT_EPOCH =
|
|
11763
|
-
var DOCUMENT_PREFIXES = [
|
|
11766
|
+
var DEFAULT_EPOCH = 5;
|
|
11767
|
+
var DOCUMENT_PREFIXES = [
|
|
11768
|
+
"task-meta",
|
|
11769
|
+
"task-conv",
|
|
11770
|
+
"task-review",
|
|
11771
|
+
"room",
|
|
11772
|
+
"epoch",
|
|
11773
|
+
"user-prefs"
|
|
11774
|
+
];
|
|
11764
11775
|
function buildDocumentId(prefix, key, epoch) {
|
|
11765
11776
|
if (!prefix || !key) {
|
|
11766
11777
|
throw new Error(`Document ID prefix and key must be non-empty: prefix="${prefix}", key="${key}"`);
|
|
@@ -11782,6 +11793,9 @@ function buildTaskConvDocId(taskId, epoch) {
|
|
|
11782
11793
|
function buildTaskReviewDocId(taskId, epoch) {
|
|
11783
11794
|
return buildDocumentId("task-review", taskId, epoch);
|
|
11784
11795
|
}
|
|
11796
|
+
function buildRoomDocId(userId, epoch) {
|
|
11797
|
+
return buildDocumentId("room", userId, epoch);
|
|
11798
|
+
}
|
|
11785
11799
|
var DOCUMENT_PREFIX_SET = new Set(DOCUMENT_PREFIXES);
|
|
11786
11800
|
function isDocumentPrefix(value) {
|
|
11787
11801
|
return DOCUMENT_PREFIX_SET.has(value);
|
|
@@ -11802,7 +11816,6 @@ function parseDocumentId(id) {
|
|
|
11802
11816
|
return null;
|
|
11803
11817
|
return { prefix, key, epoch };
|
|
11804
11818
|
}
|
|
11805
|
-
var LOCAL_USER_ID = "local-user";
|
|
11806
11819
|
function generateTaskId() {
|
|
11807
11820
|
return nanoid();
|
|
11808
11821
|
}
|
|
@@ -11821,6 +11834,8 @@ function prefixMutability(prefix, role) {
|
|
|
11821
11834
|
return false;
|
|
11822
11835
|
case "epoch":
|
|
11823
11836
|
return false;
|
|
11837
|
+
case "user-prefs":
|
|
11838
|
+
return false;
|
|
11824
11839
|
default:
|
|
11825
11840
|
return assertNever(prefix);
|
|
11826
11841
|
}
|
|
@@ -11956,6 +11971,66 @@ var DiffFileShape = Shape.plain.struct({
|
|
|
11956
11971
|
path: Shape.plain.string(),
|
|
11957
11972
|
status: Shape.plain.string()
|
|
11958
11973
|
});
|
|
11974
|
+
var PR_STATES = ["open", "closed", "merged", "draft"];
|
|
11975
|
+
var PR_REVIEW_STATES = [
|
|
11976
|
+
"pending",
|
|
11977
|
+
"approved",
|
|
11978
|
+
"changes_requested",
|
|
11979
|
+
"commented",
|
|
11980
|
+
"dismissed"
|
|
11981
|
+
];
|
|
11982
|
+
var CI_CHECK_STATUSES = [
|
|
11983
|
+
"success",
|
|
11984
|
+
"failure",
|
|
11985
|
+
"pending",
|
|
11986
|
+
"neutral",
|
|
11987
|
+
"skipped",
|
|
11988
|
+
"cancelled"
|
|
11989
|
+
];
|
|
11990
|
+
var CICheckShape = Shape.plain.struct({
|
|
11991
|
+
name: Shape.plain.string(),
|
|
11992
|
+
status: Shape.plain.string(...CI_CHECK_STATUSES),
|
|
11993
|
+
conclusion: Shape.plain.string().nullable(),
|
|
11994
|
+
url: Shape.plain.string().nullable(),
|
|
11995
|
+
startedAt: Shape.plain.number().nullable(),
|
|
11996
|
+
completedAt: Shape.plain.number().nullable()
|
|
11997
|
+
});
|
|
11998
|
+
var PRReviewerShape = Shape.plain.struct({
|
|
11999
|
+
login: Shape.plain.string(),
|
|
12000
|
+
state: Shape.plain.string(...PR_REVIEW_STATES),
|
|
12001
|
+
avatarUrl: Shape.plain.string().nullable(),
|
|
12002
|
+
submittedAt: Shape.plain.number().nullable()
|
|
12003
|
+
});
|
|
12004
|
+
var PRCommentShape = Shape.plain.struct({
|
|
12005
|
+
id: Shape.plain.string(),
|
|
12006
|
+
author: Shape.plain.string(),
|
|
12007
|
+
body: Shape.plain.string(),
|
|
12008
|
+
createdAt: Shape.plain.number(),
|
|
12009
|
+
updatedAt: Shape.plain.number().nullable(),
|
|
12010
|
+
path: Shape.plain.string().nullable(),
|
|
12011
|
+
line: Shape.plain.number().nullable(),
|
|
12012
|
+
side: Shape.plain.string().nullable(),
|
|
12013
|
+
isReviewComment: Shape.plain.boolean()
|
|
12014
|
+
});
|
|
12015
|
+
var PRDataShape = Shape.plain.struct({
|
|
12016
|
+
number: Shape.plain.number(),
|
|
12017
|
+
title: Shape.plain.string(),
|
|
12018
|
+
state: Shape.plain.string(...PR_STATES),
|
|
12019
|
+
author: Shape.plain.string(),
|
|
12020
|
+
url: Shape.plain.string(),
|
|
12021
|
+
baseRef: Shape.plain.string(),
|
|
12022
|
+
headRef: Shape.plain.string(),
|
|
12023
|
+
body: Shape.plain.string(),
|
|
12024
|
+
isDraft: Shape.plain.boolean(),
|
|
12025
|
+
additions: Shape.plain.number(),
|
|
12026
|
+
deletions: Shape.plain.number(),
|
|
12027
|
+
changedFiles: Shape.plain.number(),
|
|
12028
|
+
createdAt: Shape.plain.number(),
|
|
12029
|
+
updatedAt: Shape.plain.number(),
|
|
12030
|
+
checks: Shape.plain.array(CICheckShape),
|
|
12031
|
+
reviewers: Shape.plain.array(PRReviewerShape),
|
|
12032
|
+
comments: Shape.plain.array(PRCommentShape)
|
|
12033
|
+
});
|
|
11959
12034
|
var DiffStateShape = Shape.struct({
|
|
11960
12035
|
unstaged: Shape.plain.string(),
|
|
11961
12036
|
staged: Shape.plain.string(),
|
|
@@ -11967,7 +12042,13 @@ var DiffStateShape = Shape.struct({
|
|
|
11967
12042
|
branchUpdatedAt: Shape.plain.number(),
|
|
11968
12043
|
lastTurnDiff: Shape.plain.string(),
|
|
11969
12044
|
lastTurnFiles: Shape.list(DiffFileShape),
|
|
11970
|
-
lastTurnUpdatedAt: Shape.plain.number()
|
|
12045
|
+
lastTurnUpdatedAt: Shape.plain.number(),
|
|
12046
|
+
currentBranchPRNumber: Shape.plain.number(),
|
|
12047
|
+
prData: Shape.plain.array(PRDataShape),
|
|
12048
|
+
prDiff: Shape.plain.string(),
|
|
12049
|
+
prFiles: Shape.list(DiffFileShape),
|
|
12050
|
+
prUpdatedAt: Shape.plain.number(),
|
|
12051
|
+
prAvailable: Shape.plain.boolean()
|
|
11971
12052
|
});
|
|
11972
12053
|
var SessionEntryShape = Shape.plain.struct({
|
|
11973
12054
|
sessionId: Shape.plain.string(),
|
|
@@ -12004,7 +12085,7 @@ var PlanVersionShape = Shape.plain.struct({
|
|
|
12004
12085
|
...ATTRIBUTION_FIELDS
|
|
12005
12086
|
});
|
|
12006
12087
|
var DIFF_COMMENT_SIDES = ["old", "new"];
|
|
12007
|
-
var DIFF_COMMENT_SCOPES = ["working-tree", "last-turn"];
|
|
12088
|
+
var DIFF_COMMENT_SCOPES = ["working-tree", "last-turn", "pr"];
|
|
12008
12089
|
var DiffCommentShape = Shape.plain.struct({
|
|
12009
12090
|
commentId: Shape.plain.string(),
|
|
12010
12091
|
filePath: Shape.plain.string(),
|
|
@@ -12072,6 +12153,16 @@ var TaskReviewDocumentSchema = Shape.doc({
|
|
|
12072
12153
|
deliveredCommentIds: Shape.list(Shape.plain.string()),
|
|
12073
12154
|
todoItems: Shape.list(TodoItemShape)
|
|
12074
12155
|
}, { mergeable: true });
|
|
12156
|
+
var PinnedEntryShape = Shape.plain.struct({
|
|
12157
|
+
pinnedAt: Shape.plain.number()
|
|
12158
|
+
});
|
|
12159
|
+
var AcknowledgedEntryShape = Shape.plain.struct({
|
|
12160
|
+
at: Shape.plain.number()
|
|
12161
|
+
});
|
|
12162
|
+
var UserPreferencesDocumentSchema = Shape.doc({
|
|
12163
|
+
pinnedTaskIds: Shape.record(PinnedEntryShape),
|
|
12164
|
+
acknowledgedTasks: Shape.record(AcknowledgedEntryShape)
|
|
12165
|
+
}, { mergeable: true });
|
|
12075
12166
|
var TERMINAL_TASK_STATES = ["completed", "failed", "canceled"];
|
|
12076
12167
|
var TOOL_RISK_LEVELS = ["low", "medium", "high"];
|
|
12077
12168
|
var PERMISSION_DECISIONS = ["approved", "denied"];
|
|
@@ -12117,6 +12208,7 @@ var TaskIndexEntryShape = Shape.struct({
|
|
|
12117
12208
|
todoTotal: Shape.plain.number(),
|
|
12118
12209
|
currentActivity: Shape.plain.string().nullable()
|
|
12119
12210
|
});
|
|
12211
|
+
var KEEP_AWAKE_GRACE_PERIOD_MS = 15 * 60 * 1e3;
|
|
12120
12212
|
var WORKTREE_SETUP_STATUSES = ["running", "done", "failed"];
|
|
12121
12213
|
var WorktreeSetupStatusShape = Shape.plain.struct({
|
|
12122
12214
|
status: Shape.plain.string(...WORKTREE_SETUP_STATUSES),
|
|
@@ -12212,6 +12304,18 @@ var AnthropicLoginResponseEphemeral = Shape.plain.struct({
|
|
|
12212
12304
|
loginUrl: Shape.plain.string().nullable(),
|
|
12213
12305
|
error: Shape.plain.string().nullable()
|
|
12214
12306
|
});
|
|
12307
|
+
var TERMINAL_SESSION_STATUSES = ["running", "exited"];
|
|
12308
|
+
var TERMINAL_CONTROL_PREFIX = "\0\0";
|
|
12309
|
+
var FILE_IO_CONTROL_PREFIX = "\0\0";
|
|
12310
|
+
var TerminalSessionEphemeral = Shape.plain.struct({
|
|
12311
|
+
terminalId: Shape.plain.string(),
|
|
12312
|
+
taskId: Shape.plain.string(),
|
|
12313
|
+
machineId: Shape.plain.string(),
|
|
12314
|
+
cwd: Shape.plain.string(),
|
|
12315
|
+
status: Shape.plain.string(...TERMINAL_SESSION_STATUSES),
|
|
12316
|
+
exitCode: Shape.plain.number().nullable(),
|
|
12317
|
+
createdAt: Shape.plain.number()
|
|
12318
|
+
});
|
|
12215
12319
|
var ROOM_EPHEMERAL_DECLARATIONS = {
|
|
12216
12320
|
capabilities: MachineCapabilitiesEphemeral,
|
|
12217
12321
|
enhancePromptReqs: EnhancePromptRequestEphemeral,
|
|
@@ -12220,7 +12324,8 @@ var ROOM_EPHEMERAL_DECLARATIONS = {
|
|
|
12220
12324
|
worktreeCreateResps: WorktreeCreateResponseEphemeral,
|
|
12221
12325
|
worktreeSetupResps: WorktreeSetupResultEphemeral,
|
|
12222
12326
|
anthropicLoginReqs: AnthropicLoginRequestEphemeral,
|
|
12223
|
-
anthropicLoginResps: AnthropicLoginResponseEphemeral
|
|
12327
|
+
anthropicLoginResps: AnthropicLoginResponseEphemeral,
|
|
12328
|
+
terminalSessions: TerminalSessionEphemeral
|
|
12224
12329
|
};
|
|
12225
12330
|
var TASK_CONV_EPHEMERAL_DECLARATIONS = {
|
|
12226
12331
|
permReqs: PermissionRequestEphemeral,
|
|
@@ -12297,8 +12402,8 @@ var FileStorageAdapter = class extends StorageAdapter {
|
|
|
12297
12402
|
return join(this.#dataDir, ...sanitized);
|
|
12298
12403
|
}
|
|
12299
12404
|
#pathToKey(filePath) {
|
|
12300
|
-
const
|
|
12301
|
-
return
|
|
12405
|
+
const relative2 = filePath.slice(this.#dataDir.length + 1);
|
|
12406
|
+
return relative2.split(sep).map((part) => decodeURIComponent(part));
|
|
12302
12407
|
}
|
|
12303
12408
|
#isPrefix(prefix, key) {
|
|
12304
12409
|
if (prefix.length > key.length) return false;
|
|
@@ -12358,24 +12463,11 @@ var LifecycleManager = class {
|
|
|
12358
12463
|
{ signal: "SIGINT", handler: intHandler }
|
|
12359
12464
|
];
|
|
12360
12465
|
const exceptionHandler = (error2) => {
|
|
12361
|
-
|
|
12362
|
-
logger.error({ error: error2 }, "Uncaught exception \u2014 initiating shutdown");
|
|
12363
|
-
} catch {
|
|
12364
|
-
}
|
|
12466
|
+
logger.error({ err: error2 }, "Uncaught exception \u2014 initiating shutdown");
|
|
12365
12467
|
void this.#shutdown("uncaughtException");
|
|
12366
12468
|
};
|
|
12367
12469
|
const rejectionHandler = (reason) => {
|
|
12368
|
-
|
|
12369
|
-
let jsonStr;
|
|
12370
|
-
try {
|
|
12371
|
-
jsonStr = JSON.stringify(reason);
|
|
12372
|
-
} catch {
|
|
12373
|
-
jsonStr = "(not serializable)";
|
|
12374
|
-
}
|
|
12375
|
-
const detail = reason instanceof Error ? { message: reason.message, stack: reason.stack } : { inspected: `${String(reason)} ${jsonStr}` };
|
|
12376
|
-
logger.error(detail, "Unhandled rejection (non-fatal)");
|
|
12377
|
-
} catch {
|
|
12378
|
-
}
|
|
12470
|
+
logger.error({ err: reason }, "Unhandled rejection (non-fatal)");
|
|
12379
12471
|
};
|
|
12380
12472
|
process.on("uncaughtException", exceptionHandler);
|
|
12381
12473
|
process.on("unhandledRejection", rejectionHandler);
|
|
@@ -12453,7 +12545,7 @@ var LifecycleManager = class {
|
|
|
12453
12545
|
async #shutdown(signal) {
|
|
12454
12546
|
if (this.#isShuttingDown) return;
|
|
12455
12547
|
this.#isShuttingDown = true;
|
|
12456
|
-
logger.info({ signal }, "Shutdown signal received");
|
|
12548
|
+
logger.info({ signal, pid: process.pid, ppid: process.ppid }, "Shutdown signal received");
|
|
12457
12549
|
const HARD_KILL_MS = 15e3;
|
|
12458
12550
|
const forceExit = setTimeout(() => {
|
|
12459
12551
|
logger.error("Graceful shutdown timed out, forcing exit");
|
|
@@ -12478,7 +12570,7 @@ var LifecycleManager = class {
|
|
|
12478
12570
|
})
|
|
12479
12571
|
]).finally(() => clearTimeout(timeoutId));
|
|
12480
12572
|
} catch (error2) {
|
|
12481
|
-
logger.error({
|
|
12573
|
+
logger.error({ err: error2 }, "Error during shutdown callback");
|
|
12482
12574
|
}
|
|
12483
12575
|
}
|
|
12484
12576
|
await this.#removePidFile();
|
|
@@ -12491,9 +12583,10 @@ var LifecycleManager = class {
|
|
|
12491
12583
|
|
|
12492
12584
|
// src/serve.ts
|
|
12493
12585
|
import { spawn as spawn4 } from "child_process";
|
|
12494
|
-
import {
|
|
12586
|
+
import { existsSync, statSync } from "fs";
|
|
12587
|
+
import { mkdir as mkdir3, readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
12495
12588
|
import { homedir as homedir2, hostname as hostname2 } from "os";
|
|
12496
|
-
import { resolve as resolve2 } from "path";
|
|
12589
|
+
import { isAbsolute as isAbsolute3, relative, resolve as resolve2 } from "path";
|
|
12497
12590
|
|
|
12498
12591
|
// ../../node_modules/.pnpm/@levischuck+tiny-cbor@0.3.2/node_modules/@levischuck/tiny-cbor/esm/cbor/cbor_internal.js
|
|
12499
12592
|
function decodeLength(data, argument, index) {
|
|
@@ -14328,7 +14421,7 @@ function runWithTimeout(command2, args, cwd, timeoutMs) {
|
|
|
14328
14421
|
execFile(
|
|
14329
14422
|
command2,
|
|
14330
14423
|
args,
|
|
14331
|
-
{ timeout: timeoutMs, cwd, maxBuffer:
|
|
14424
|
+
{ timeout: timeoutMs, cwd, maxBuffer: 10 * 1024 * 1024 },
|
|
14332
14425
|
(error2, stdout) => {
|
|
14333
14426
|
if (error2) {
|
|
14334
14427
|
reject(error2);
|
|
@@ -14341,6 +14434,19 @@ function runWithTimeout(command2, args, cwd, timeoutMs) {
|
|
|
14341
14434
|
}
|
|
14342
14435
|
var DIFF_TIMEOUT_MS = 15e3;
|
|
14343
14436
|
var MAX_DIFF_SIZE = 2e5;
|
|
14437
|
+
var gitRepoCache = /* @__PURE__ */ new Map();
|
|
14438
|
+
async function isGitRepo(cwd) {
|
|
14439
|
+
const cached = gitRepoCache.get(cwd);
|
|
14440
|
+
if (cached !== void 0) return cached;
|
|
14441
|
+
try {
|
|
14442
|
+
await runWithTimeout("git", ["rev-parse", "--git-dir"], cwd, TIMEOUT_MS);
|
|
14443
|
+
gitRepoCache.set(cwd, true);
|
|
14444
|
+
return true;
|
|
14445
|
+
} catch {
|
|
14446
|
+
gitRepoCache.set(cwd, false);
|
|
14447
|
+
return false;
|
|
14448
|
+
}
|
|
14449
|
+
}
|
|
14344
14450
|
async function withIntentToAdd(cwd, fn) {
|
|
14345
14451
|
let added = false;
|
|
14346
14452
|
try {
|
|
@@ -14362,29 +14468,46 @@ async function withIntentToAdd(cwd, fn) {
|
|
|
14362
14468
|
}
|
|
14363
14469
|
}
|
|
14364
14470
|
}
|
|
14365
|
-
|
|
14366
|
-
|
|
14367
|
-
|
|
14368
|
-
|
|
14369
|
-
|
|
14471
|
+
function isMaxBufferError(err) {
|
|
14472
|
+
return err instanceof Error && err.message.includes("maxBuffer");
|
|
14473
|
+
}
|
|
14474
|
+
var BUFFER_OVERFLOW_MSG = "... diff too large (exceeded buffer limit) ...\n";
|
|
14475
|
+
function truncateDiff(result) {
|
|
14370
14476
|
return result.length > MAX_DIFF_SIZE ? `${result.slice(0, MAX_DIFF_SIZE)}
|
|
14371
14477
|
|
|
14372
|
-
... diff truncated (exceeds
|
|
14478
|
+
... diff truncated (exceeds 200KB) ...
|
|
14373
14479
|
` : result;
|
|
14374
14480
|
}
|
|
14481
|
+
async function getUnstagedDiff(cwd) {
|
|
14482
|
+
if (!await isGitRepo(cwd)) return "";
|
|
14483
|
+
try {
|
|
14484
|
+
const result = await withIntentToAdd(
|
|
14485
|
+
cwd,
|
|
14486
|
+
() => runWithTimeout("git", ["diff", "--no-color"], cwd, DIFF_TIMEOUT_MS)
|
|
14487
|
+
);
|
|
14488
|
+
return truncateDiff(result);
|
|
14489
|
+
} catch (err) {
|
|
14490
|
+
if (isMaxBufferError(err)) return BUFFER_OVERFLOW_MSG;
|
|
14491
|
+
throw err;
|
|
14492
|
+
}
|
|
14493
|
+
}
|
|
14375
14494
|
async function getStagedDiff(cwd) {
|
|
14376
|
-
|
|
14377
|
-
|
|
14378
|
-
|
|
14379
|
-
|
|
14380
|
-
|
|
14381
|
-
|
|
14382
|
-
|
|
14383
|
-
|
|
14384
|
-
|
|
14385
|
-
|
|
14495
|
+
if (!await isGitRepo(cwd)) return "";
|
|
14496
|
+
try {
|
|
14497
|
+
const result = await runWithTimeout(
|
|
14498
|
+
"git",
|
|
14499
|
+
["diff", "--cached", "--no-color"],
|
|
14500
|
+
cwd,
|
|
14501
|
+
DIFF_TIMEOUT_MS
|
|
14502
|
+
);
|
|
14503
|
+
return truncateDiff(result);
|
|
14504
|
+
} catch (err) {
|
|
14505
|
+
if (isMaxBufferError(err)) return BUFFER_OVERFLOW_MSG;
|
|
14506
|
+
throw err;
|
|
14507
|
+
}
|
|
14386
14508
|
}
|
|
14387
14509
|
async function getChangedFiles(cwd) {
|
|
14510
|
+
if (!await isGitRepo(cwd)) return [];
|
|
14388
14511
|
const out = await runWithTimeout("git", ["status", "--porcelain"], cwd, DIFF_TIMEOUT_MS);
|
|
14389
14512
|
if (!out) return [];
|
|
14390
14513
|
return out.split("\n").filter(Boolean).map((line) => ({
|
|
@@ -14430,11 +14553,9 @@ async function getBranchDiff(cwd, baseBranch) {
|
|
|
14430
14553
|
cwd,
|
|
14431
14554
|
BRANCH_DIFF_TIMEOUT_MS
|
|
14432
14555
|
);
|
|
14433
|
-
return result
|
|
14434
|
-
|
|
14435
|
-
|
|
14436
|
-
` : result;
|
|
14437
|
-
} catch {
|
|
14556
|
+
return truncateDiff(result);
|
|
14557
|
+
} catch (err) {
|
|
14558
|
+
if (isMaxBufferError(err)) return BUFFER_OVERFLOW_MSG;
|
|
14438
14559
|
return "";
|
|
14439
14560
|
}
|
|
14440
14561
|
}
|
|
@@ -14477,11 +14598,9 @@ async function getSnapshotDiff(cwd, fromRef, toRef) {
|
|
|
14477
14598
|
cwd,
|
|
14478
14599
|
DIFF_TIMEOUT_MS
|
|
14479
14600
|
);
|
|
14480
|
-
return result
|
|
14481
|
-
|
|
14482
|
-
|
|
14483
|
-
` : result;
|
|
14484
|
-
} catch {
|
|
14601
|
+
return truncateDiff(result);
|
|
14602
|
+
} catch (err) {
|
|
14603
|
+
if (isMaxBufferError(err)) return BUFFER_OVERFLOW_MSG;
|
|
14485
14604
|
return "";
|
|
14486
14605
|
}
|
|
14487
14606
|
}
|
|
@@ -14625,6 +14744,265 @@ async function getRepoMetadata(repoPath) {
|
|
|
14625
14744
|
return null;
|
|
14626
14745
|
}
|
|
14627
14746
|
}
|
|
14747
|
+
var GH_PR_JSON_FIELDS = "number,title,state,author,url,baseRefName,headRefName,body,isDraft,additions,deletions,changedFiles,createdAt,updatedAt,statusCheckRollup,reviews,reviewRequests,comments";
|
|
14748
|
+
var GH_STATE_MAP = {
|
|
14749
|
+
OPEN: "open",
|
|
14750
|
+
CLOSED: "closed",
|
|
14751
|
+
MERGED: "merged"
|
|
14752
|
+
};
|
|
14753
|
+
var GhPRResponseSchema = external_exports.object({
|
|
14754
|
+
number: external_exports.number().optional(),
|
|
14755
|
+
title: external_exports.string().optional(),
|
|
14756
|
+
state: external_exports.string().optional(),
|
|
14757
|
+
author: external_exports.object({ login: external_exports.string() }).passthrough().optional(),
|
|
14758
|
+
url: external_exports.string().optional(),
|
|
14759
|
+
baseRefName: external_exports.string().optional(),
|
|
14760
|
+
headRefName: external_exports.string().optional(),
|
|
14761
|
+
body: external_exports.string().optional(),
|
|
14762
|
+
isDraft: external_exports.boolean().optional(),
|
|
14763
|
+
additions: external_exports.number().optional(),
|
|
14764
|
+
deletions: external_exports.number().optional(),
|
|
14765
|
+
changedFiles: external_exports.number().optional(),
|
|
14766
|
+
createdAt: external_exports.string().optional(),
|
|
14767
|
+
updatedAt: external_exports.string().optional(),
|
|
14768
|
+
statusCheckRollup: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())).optional(),
|
|
14769
|
+
reviews: external_exports.union([
|
|
14770
|
+
external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())),
|
|
14771
|
+
external_exports.object({ nodes: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())) }).passthrough()
|
|
14772
|
+
]).optional(),
|
|
14773
|
+
comments: external_exports.union([
|
|
14774
|
+
external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())),
|
|
14775
|
+
external_exports.object({ nodes: external_exports.array(external_exports.record(external_exports.string(), external_exports.unknown())) }).passthrough()
|
|
14776
|
+
]).optional()
|
|
14777
|
+
}).passthrough();
|
|
14778
|
+
var GhPRListResponseSchema = external_exports.array(GhPRResponseSchema);
|
|
14779
|
+
function parseTimestamp(value) {
|
|
14780
|
+
if (typeof value === "string") {
|
|
14781
|
+
const parsed = new Date(value).getTime();
|
|
14782
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
14783
|
+
}
|
|
14784
|
+
if (typeof value === "number") return value;
|
|
14785
|
+
return 0;
|
|
14786
|
+
}
|
|
14787
|
+
function field(obj, key) {
|
|
14788
|
+
if (typeof obj === "object" && obj !== null && key in obj) {
|
|
14789
|
+
return obj[key];
|
|
14790
|
+
}
|
|
14791
|
+
return void 0;
|
|
14792
|
+
}
|
|
14793
|
+
function toRecord(value) {
|
|
14794
|
+
if (typeof value === "object" && value !== null) return value;
|
|
14795
|
+
return {};
|
|
14796
|
+
}
|
|
14797
|
+
function extractLogin(obj) {
|
|
14798
|
+
const login = field(obj, "login");
|
|
14799
|
+
return typeof login === "string" ? login : "";
|
|
14800
|
+
}
|
|
14801
|
+
function extractStringField(obj, f) {
|
|
14802
|
+
const val = field(obj, f);
|
|
14803
|
+
return typeof val === "string" ? val : null;
|
|
14804
|
+
}
|
|
14805
|
+
function extractArrayOrNodes(value) {
|
|
14806
|
+
if (Array.isArray(value)) return value;
|
|
14807
|
+
const nodes = field(value, "nodes");
|
|
14808
|
+
if (Array.isArray(nodes)) return nodes;
|
|
14809
|
+
return [];
|
|
14810
|
+
}
|
|
14811
|
+
function mapCheckStatus(check) {
|
|
14812
|
+
const state = typeof check.state === "string" ? check.state.toLowerCase() : "";
|
|
14813
|
+
const conclusion = typeof check.conclusion === "string" ? check.conclusion.toLowerCase() : "";
|
|
14814
|
+
if (conclusion === "success") return "success";
|
|
14815
|
+
if (conclusion === "failure" || conclusion === "timed_out" || conclusion === "action_required")
|
|
14816
|
+
return "failure";
|
|
14817
|
+
if (conclusion === "neutral") return "neutral";
|
|
14818
|
+
if (conclusion === "skipped") return "skipped";
|
|
14819
|
+
if (conclusion === "cancelled") return "cancelled";
|
|
14820
|
+
if (state === "success") return "success";
|
|
14821
|
+
if (state === "failure" || state === "error") return "failure";
|
|
14822
|
+
if (state === "pending" || state === "queued" || state === "in_progress") return "pending";
|
|
14823
|
+
return "pending";
|
|
14824
|
+
}
|
|
14825
|
+
function mapReviewState(value) {
|
|
14826
|
+
if (typeof value !== "string") return "pending";
|
|
14827
|
+
const upper = value.toUpperCase();
|
|
14828
|
+
if (upper === "APPROVED") return "approved";
|
|
14829
|
+
if (upper === "CHANGES_REQUESTED") return "changes_requested";
|
|
14830
|
+
if (upper === "COMMENTED") return "commented";
|
|
14831
|
+
if (upper === "DISMISSED") return "dismissed";
|
|
14832
|
+
return "pending";
|
|
14833
|
+
}
|
|
14834
|
+
function mapGhCheck(c) {
|
|
14835
|
+
const name = typeof c.name === "string" ? c.name : typeof c.context === "string" ? c.context : "";
|
|
14836
|
+
const url = typeof c.targetUrl === "string" ? c.targetUrl : typeof c.detailsUrl === "string" ? c.detailsUrl : null;
|
|
14837
|
+
return {
|
|
14838
|
+
name,
|
|
14839
|
+
status: mapCheckStatus(c),
|
|
14840
|
+
conclusion: typeof c.conclusion === "string" ? c.conclusion.toLowerCase() : null,
|
|
14841
|
+
url,
|
|
14842
|
+
startedAt: parseTimestamp(c.startedAt),
|
|
14843
|
+
completedAt: parseTimestamp(c.completedAt)
|
|
14844
|
+
};
|
|
14845
|
+
}
|
|
14846
|
+
function mapGhReviewer(r) {
|
|
14847
|
+
return {
|
|
14848
|
+
login: extractLogin(r.author),
|
|
14849
|
+
state: mapReviewState(r.state),
|
|
14850
|
+
avatarUrl: extractStringField(r.author, "avatarUrl"),
|
|
14851
|
+
submittedAt: parseTimestamp(r.submittedAt)
|
|
14852
|
+
};
|
|
14853
|
+
}
|
|
14854
|
+
function mapGhComment(c) {
|
|
14855
|
+
return {
|
|
14856
|
+
id: typeof c.id === "string" ? c.id : String(c.id ?? ""),
|
|
14857
|
+
author: extractLogin(c.author),
|
|
14858
|
+
body: typeof c.body === "string" ? c.body : "",
|
|
14859
|
+
createdAt: parseTimestamp(c.createdAt),
|
|
14860
|
+
updatedAt: typeof c.updatedAt === "string" ? parseTimestamp(c.updatedAt) : null,
|
|
14861
|
+
path: typeof c.path === "string" ? c.path : null,
|
|
14862
|
+
line: typeof c.line === "number" ? c.line : null,
|
|
14863
|
+
side: typeof c.side === "string" ? c.side : null,
|
|
14864
|
+
isReviewComment: c.pullRequestReview !== void 0 && c.pullRequestReview !== null
|
|
14865
|
+
};
|
|
14866
|
+
}
|
|
14867
|
+
function mapGhPRToPRData(raw) {
|
|
14868
|
+
const isDraft2 = raw.isDraft === true;
|
|
14869
|
+
const ghState = typeof raw.state === "string" ? raw.state.toUpperCase() : "";
|
|
14870
|
+
const state = isDraft2 ? "draft" : GH_STATE_MAP[ghState] ?? "open";
|
|
14871
|
+
const rawChecks = Array.isArray(raw.statusCheckRollup) ? raw.statusCheckRollup : [];
|
|
14872
|
+
const checks = rawChecks.map(mapGhCheck);
|
|
14873
|
+
const reviewers = extractArrayOrNodes(raw.reviews).map(
|
|
14874
|
+
(r) => mapGhReviewer(toRecord(r))
|
|
14875
|
+
);
|
|
14876
|
+
const comments = extractArrayOrNodes(raw.comments).map(
|
|
14877
|
+
(c) => mapGhComment(toRecord(c))
|
|
14878
|
+
);
|
|
14879
|
+
return {
|
|
14880
|
+
number: typeof raw.number === "number" ? raw.number : 0,
|
|
14881
|
+
title: typeof raw.title === "string" ? raw.title : "",
|
|
14882
|
+
state,
|
|
14883
|
+
author: extractLogin(raw.author),
|
|
14884
|
+
url: typeof raw.url === "string" ? raw.url : "",
|
|
14885
|
+
baseRef: typeof raw.baseRefName === "string" ? raw.baseRefName : "",
|
|
14886
|
+
headRef: typeof raw.headRefName === "string" ? raw.headRefName : "",
|
|
14887
|
+
body: typeof raw.body === "string" ? raw.body : "",
|
|
14888
|
+
isDraft: isDraft2,
|
|
14889
|
+
additions: typeof raw.additions === "number" ? raw.additions : 0,
|
|
14890
|
+
deletions: typeof raw.deletions === "number" ? raw.deletions : 0,
|
|
14891
|
+
changedFiles: typeof raw.changedFiles === "number" ? raw.changedFiles : 0,
|
|
14892
|
+
createdAt: parseTimestamp(raw.createdAt),
|
|
14893
|
+
updatedAt: parseTimestamp(raw.updatedAt),
|
|
14894
|
+
checks,
|
|
14895
|
+
reviewers,
|
|
14896
|
+
comments
|
|
14897
|
+
};
|
|
14898
|
+
}
|
|
14899
|
+
async function isGhAvailable() {
|
|
14900
|
+
try {
|
|
14901
|
+
await run("which", ["gh"]);
|
|
14902
|
+
return true;
|
|
14903
|
+
} catch {
|
|
14904
|
+
return false;
|
|
14905
|
+
}
|
|
14906
|
+
}
|
|
14907
|
+
async function getPRForCurrentBranch(cwd) {
|
|
14908
|
+
try {
|
|
14909
|
+
const stdout = await runWithTimeout(
|
|
14910
|
+
"gh",
|
|
14911
|
+
["pr", "view", "--json", GH_PR_JSON_FIELDS],
|
|
14912
|
+
cwd,
|
|
14913
|
+
15e3
|
|
14914
|
+
);
|
|
14915
|
+
const parseResult = GhPRResponseSchema.safeParse(JSON.parse(stdout));
|
|
14916
|
+
if (!parseResult.success) {
|
|
14917
|
+
return null;
|
|
14918
|
+
}
|
|
14919
|
+
return mapGhPRToPRData(parseResult.data);
|
|
14920
|
+
} catch {
|
|
14921
|
+
return null;
|
|
14922
|
+
}
|
|
14923
|
+
}
|
|
14924
|
+
async function getPRDiff(cwd, prNumber) {
|
|
14925
|
+
try {
|
|
14926
|
+
const result = await runWithTimeout(
|
|
14927
|
+
"gh",
|
|
14928
|
+
["pr", "diff", String(prNumber), "--color=never"],
|
|
14929
|
+
cwd,
|
|
14930
|
+
15e3
|
|
14931
|
+
);
|
|
14932
|
+
return truncateDiff(result);
|
|
14933
|
+
} catch (err) {
|
|
14934
|
+
if (isMaxBufferError(err)) return BUFFER_OVERFLOW_MSG;
|
|
14935
|
+
return "";
|
|
14936
|
+
}
|
|
14937
|
+
}
|
|
14938
|
+
async function getPRFiles(cwd, prNumber) {
|
|
14939
|
+
try {
|
|
14940
|
+
const stdout = await runWithTimeout(
|
|
14941
|
+
"gh",
|
|
14942
|
+
["pr", "view", String(prNumber), "--json", "files"],
|
|
14943
|
+
cwd,
|
|
14944
|
+
15e3
|
|
14945
|
+
);
|
|
14946
|
+
const parsed = JSON.parse(stdout);
|
|
14947
|
+
if (!Array.isArray(parsed.files)) return [];
|
|
14948
|
+
return parsed.files.map((f) => ({
|
|
14949
|
+
path: typeof f.path === "string" ? f.path : "",
|
|
14950
|
+
status: f.additions !== void 0 || f.deletions !== void 0 ? "M" : "U"
|
|
14951
|
+
}));
|
|
14952
|
+
} catch {
|
|
14953
|
+
return [];
|
|
14954
|
+
}
|
|
14955
|
+
}
|
|
14956
|
+
async function getAssignedReviews(cwd) {
|
|
14957
|
+
try {
|
|
14958
|
+
const stdout = await runWithTimeout(
|
|
14959
|
+
"gh",
|
|
14960
|
+
[
|
|
14961
|
+
"pr",
|
|
14962
|
+
"list",
|
|
14963
|
+
"--search",
|
|
14964
|
+
"is:open review-requested:@me",
|
|
14965
|
+
"--json",
|
|
14966
|
+
GH_PR_JSON_FIELDS,
|
|
14967
|
+
"--limit",
|
|
14968
|
+
"10"
|
|
14969
|
+
],
|
|
14970
|
+
cwd,
|
|
14971
|
+
15e3
|
|
14972
|
+
);
|
|
14973
|
+
const parseResult = GhPRListResponseSchema.safeParse(JSON.parse(stdout));
|
|
14974
|
+
if (!parseResult.success) return [];
|
|
14975
|
+
return parseResult.data.map(mapGhPRToPRData);
|
|
14976
|
+
} catch {
|
|
14977
|
+
return [];
|
|
14978
|
+
}
|
|
14979
|
+
}
|
|
14980
|
+
async function getUserPRs(cwd) {
|
|
14981
|
+
try {
|
|
14982
|
+
const stdout = await runWithTimeout(
|
|
14983
|
+
"gh",
|
|
14984
|
+
[
|
|
14985
|
+
"pr",
|
|
14986
|
+
"list",
|
|
14987
|
+
"--author",
|
|
14988
|
+
"@me",
|
|
14989
|
+
"--state",
|
|
14990
|
+
"open",
|
|
14991
|
+
"--json",
|
|
14992
|
+
GH_PR_JSON_FIELDS,
|
|
14993
|
+
"--limit",
|
|
14994
|
+
"10"
|
|
14995
|
+
],
|
|
14996
|
+
cwd,
|
|
14997
|
+
15e3
|
|
14998
|
+
);
|
|
14999
|
+
const parseResult = GhPRListResponseSchema.safeParse(JSON.parse(stdout));
|
|
15000
|
+
if (!parseResult.success) return [];
|
|
15001
|
+
return parseResult.data.map(mapGhPRToPRData);
|
|
15002
|
+
} catch {
|
|
15003
|
+
return [];
|
|
15004
|
+
}
|
|
15005
|
+
}
|
|
14628
15006
|
async function detectEnvironments() {
|
|
14629
15007
|
const repoPaths = await findGitRepos(homedir());
|
|
14630
15008
|
const repoInfos = await Promise.all(repoPaths.map(getRepoMetadata));
|
|
@@ -14783,6 +15161,664 @@ function createBranchWatcher(options) {
|
|
|
14783
15161
|
};
|
|
14784
15162
|
}
|
|
14785
15163
|
|
|
15164
|
+
// src/backpressure-send.ts
|
|
15165
|
+
var DEFAULT_BACKPRESSURE_THRESHOLD = 64 * 1024;
|
|
15166
|
+
var DEFAULT_MAX_QUEUE_BYTES = 1024 * 1024;
|
|
15167
|
+
var BUFFERED_AMOUNT_LOW_EVENT = "bufferedamountlow";
|
|
15168
|
+
function byteLength(data) {
|
|
15169
|
+
if (typeof data === "string") {
|
|
15170
|
+
return Buffer.byteLength(data);
|
|
15171
|
+
}
|
|
15172
|
+
return data.byteLength;
|
|
15173
|
+
}
|
|
15174
|
+
function createBackpressureSender(channel, options = {}) {
|
|
15175
|
+
const threshold = options.threshold ?? DEFAULT_BACKPRESSURE_THRESHOLD;
|
|
15176
|
+
const maxQueueBytes = options.maxQueueBytes ?? DEFAULT_MAX_QUEUE_BYTES;
|
|
15177
|
+
const dropOldest = options.dropOldest ?? false;
|
|
15178
|
+
const queue = [];
|
|
15179
|
+
let totalQueuedBytes = 0;
|
|
15180
|
+
let isPaused = false;
|
|
15181
|
+
function trySend(data) {
|
|
15182
|
+
try {
|
|
15183
|
+
channel.send(data);
|
|
15184
|
+
} catch {
|
|
15185
|
+
}
|
|
15186
|
+
}
|
|
15187
|
+
function drain() {
|
|
15188
|
+
while (queue.length > 0) {
|
|
15189
|
+
const buffered = channel.bufferedAmount;
|
|
15190
|
+
if (buffered !== void 0 && buffered > threshold) {
|
|
15191
|
+
return;
|
|
15192
|
+
}
|
|
15193
|
+
const entry = queue.shift();
|
|
15194
|
+
if (!entry) break;
|
|
15195
|
+
totalQueuedBytes -= entry.bytes;
|
|
15196
|
+
trySend(entry.data);
|
|
15197
|
+
}
|
|
15198
|
+
if (queue.length === 0) {
|
|
15199
|
+
isPaused = false;
|
|
15200
|
+
channel.removeEventListener?.(BUFFERED_AMOUNT_LOW_EVENT, drain);
|
|
15201
|
+
}
|
|
15202
|
+
}
|
|
15203
|
+
function evictOldest(requiredBytes) {
|
|
15204
|
+
let droppedBytes = 0;
|
|
15205
|
+
while (queue.length > 0 && totalQueuedBytes + requiredBytes > maxQueueBytes) {
|
|
15206
|
+
const oldest = queue.shift();
|
|
15207
|
+
if (!oldest) break;
|
|
15208
|
+
totalQueuedBytes -= oldest.bytes;
|
|
15209
|
+
droppedBytes += oldest.bytes;
|
|
15210
|
+
}
|
|
15211
|
+
if (droppedBytes > 0) {
|
|
15212
|
+
options.onDrop?.(droppedBytes);
|
|
15213
|
+
}
|
|
15214
|
+
}
|
|
15215
|
+
function enqueue(data) {
|
|
15216
|
+
const bytes = byteLength(data);
|
|
15217
|
+
if (totalQueuedBytes + bytes > maxQueueBytes) {
|
|
15218
|
+
if (dropOldest) {
|
|
15219
|
+
evictOldest(bytes);
|
|
15220
|
+
} else {
|
|
15221
|
+
options.onOverflow?.();
|
|
15222
|
+
return;
|
|
15223
|
+
}
|
|
15224
|
+
}
|
|
15225
|
+
queue.push({ data, bytes });
|
|
15226
|
+
totalQueuedBytes += bytes;
|
|
15227
|
+
}
|
|
15228
|
+
function startPause() {
|
|
15229
|
+
if (isPaused) return;
|
|
15230
|
+
isPaused = true;
|
|
15231
|
+
if (channel.addEventListener) {
|
|
15232
|
+
if (channel.bufferedAmountLowThreshold !== void 0) {
|
|
15233
|
+
channel.bufferedAmountLowThreshold = threshold;
|
|
15234
|
+
}
|
|
15235
|
+
channel.addEventListener(BUFFERED_AMOUNT_LOW_EVENT, drain);
|
|
15236
|
+
}
|
|
15237
|
+
}
|
|
15238
|
+
return {
|
|
15239
|
+
send(data) {
|
|
15240
|
+
if (channel.bufferedAmount === void 0) {
|
|
15241
|
+
trySend(data);
|
|
15242
|
+
return true;
|
|
15243
|
+
}
|
|
15244
|
+
if (!isPaused && channel.bufferedAmount <= threshold) {
|
|
15245
|
+
trySend(data);
|
|
15246
|
+
return true;
|
|
15247
|
+
}
|
|
15248
|
+
startPause();
|
|
15249
|
+
enqueue(data);
|
|
15250
|
+
return !isPaused || queue.length > 0;
|
|
15251
|
+
},
|
|
15252
|
+
dispose() {
|
|
15253
|
+
channel.removeEventListener?.(BUFFERED_AMOUNT_LOW_EVENT, drain);
|
|
15254
|
+
queue.length = 0;
|
|
15255
|
+
totalQueuedBytes = 0;
|
|
15256
|
+
isPaused = false;
|
|
15257
|
+
},
|
|
15258
|
+
get paused() {
|
|
15259
|
+
return isPaused;
|
|
15260
|
+
},
|
|
15261
|
+
get queuedBytes() {
|
|
15262
|
+
return totalQueuedBytes;
|
|
15263
|
+
}
|
|
15264
|
+
};
|
|
15265
|
+
}
|
|
15266
|
+
|
|
15267
|
+
// src/channel-buffer.ts
|
|
15268
|
+
function createChannelBuffer(channel, options) {
|
|
15269
|
+
const log = createChildLogger({ mode: options.logPrefix ?? "channel-buffer" });
|
|
15270
|
+
let open = channel.readyState === "open";
|
|
15271
|
+
const pending = [];
|
|
15272
|
+
let pendingBytes = 0;
|
|
15273
|
+
let sender = null;
|
|
15274
|
+
function getSender() {
|
|
15275
|
+
if (!sender) {
|
|
15276
|
+
sender = createBackpressureSender(channel, {
|
|
15277
|
+
threshold: options.backpressureThreshold,
|
|
15278
|
+
maxQueueBytes: options.maxBytes,
|
|
15279
|
+
dropOldest: options.dropOldest,
|
|
15280
|
+
onOverflow: options.onOverflow,
|
|
15281
|
+
onDrop: (bytes) => {
|
|
15282
|
+
log.debug({ droppedBytes: bytes }, "Backpressure dropped oldest data");
|
|
15283
|
+
}
|
|
15284
|
+
});
|
|
15285
|
+
}
|
|
15286
|
+
return sender;
|
|
15287
|
+
}
|
|
15288
|
+
return {
|
|
15289
|
+
get isOpen() {
|
|
15290
|
+
return open;
|
|
15291
|
+
},
|
|
15292
|
+
markOpen() {
|
|
15293
|
+
open = true;
|
|
15294
|
+
},
|
|
15295
|
+
sendOrBuffer(data) {
|
|
15296
|
+
if (open) {
|
|
15297
|
+
return getSender().send(data);
|
|
15298
|
+
}
|
|
15299
|
+
const byteLen = Buffer.byteLength(data);
|
|
15300
|
+
if (pendingBytes + byteLen > options.maxBytes) {
|
|
15301
|
+
log.warn({ pendingBytes, newBytes: byteLen, max: options.maxBytes }, "Buffer overflow");
|
|
15302
|
+
options.onOverflow?.();
|
|
15303
|
+
return false;
|
|
15304
|
+
}
|
|
15305
|
+
pending.push(data);
|
|
15306
|
+
pendingBytes += byteLen;
|
|
15307
|
+
return true;
|
|
15308
|
+
},
|
|
15309
|
+
flush() {
|
|
15310
|
+
const bp = getSender();
|
|
15311
|
+
for (const chunk of pending) {
|
|
15312
|
+
bp.send(chunk);
|
|
15313
|
+
}
|
|
15314
|
+
pending.length = 0;
|
|
15315
|
+
pendingBytes = 0;
|
|
15316
|
+
},
|
|
15317
|
+
reset() {
|
|
15318
|
+
pending.length = 0;
|
|
15319
|
+
pendingBytes = 0;
|
|
15320
|
+
open = false;
|
|
15321
|
+
sender?.dispose();
|
|
15322
|
+
sender = null;
|
|
15323
|
+
},
|
|
15324
|
+
dispose() {
|
|
15325
|
+
sender?.dispose();
|
|
15326
|
+
sender = null;
|
|
15327
|
+
}
|
|
15328
|
+
};
|
|
15329
|
+
}
|
|
15330
|
+
|
|
15331
|
+
// src/peer-manager.ts
|
|
15332
|
+
var ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
15333
|
+
function machineIdToPeerId(machineId) {
|
|
15334
|
+
return machineId;
|
|
15335
|
+
}
|
|
15336
|
+
async function loadDefaultFactory() {
|
|
15337
|
+
const { RTCPeerConnection } = await import("node-datachannel/polyfill");
|
|
15338
|
+
return () => {
|
|
15339
|
+
const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
|
|
15340
|
+
return pc;
|
|
15341
|
+
};
|
|
15342
|
+
}
|
|
15343
|
+
function parseTerminalChannelLabel(label) {
|
|
15344
|
+
if (!label.startsWith("terminal-io:")) return null;
|
|
15345
|
+
const suffix = label.slice("terminal-io:".length);
|
|
15346
|
+
const colonIdx = suffix.indexOf(":");
|
|
15347
|
+
const taskId = colonIdx === -1 ? suffix : suffix.slice(0, colonIdx);
|
|
15348
|
+
if (!taskId) return null;
|
|
15349
|
+
const terminalId = colonIdx === -1 || suffix.slice(colonIdx + 1) === "" ? crypto.randomUUID() : suffix.slice(colonIdx + 1);
|
|
15350
|
+
return { taskId, terminalId };
|
|
15351
|
+
}
|
|
15352
|
+
function guardLoroChannelSend(dc) {
|
|
15353
|
+
if (!dc.send) return;
|
|
15354
|
+
const originalSend = dc.send.bind(dc);
|
|
15355
|
+
dc.send = (data) => {
|
|
15356
|
+
try {
|
|
15357
|
+
originalSend(data);
|
|
15358
|
+
} catch {
|
|
15359
|
+
}
|
|
15360
|
+
};
|
|
15361
|
+
}
|
|
15362
|
+
function parseFileChannelLabel(label) {
|
|
15363
|
+
if (!label.startsWith("file-io:")) return null;
|
|
15364
|
+
const channelId = label.slice("file-io:".length);
|
|
15365
|
+
return channelId || null;
|
|
15366
|
+
}
|
|
15367
|
+
function createPeerManager(config2) {
|
|
15368
|
+
const peers = /* @__PURE__ */ new Map();
|
|
15369
|
+
const pendingCreates = /* @__PURE__ */ new Map();
|
|
15370
|
+
let factoryPromise = null;
|
|
15371
|
+
async function getFactory() {
|
|
15372
|
+
if (config2.createPeerConnection) {
|
|
15373
|
+
return config2.createPeerConnection;
|
|
15374
|
+
}
|
|
15375
|
+
if (!factoryPromise) {
|
|
15376
|
+
factoryPromise = loadDefaultFactory();
|
|
15377
|
+
}
|
|
15378
|
+
return factoryPromise;
|
|
15379
|
+
}
|
|
15380
|
+
function setupPeerHandlers(machineId, pc) {
|
|
15381
|
+
pc.onicecandidate = (event) => {
|
|
15382
|
+
if (event.candidate) {
|
|
15383
|
+
config2.onIceCandidate(machineId, {
|
|
15384
|
+
candidate: event.candidate.candidate,
|
|
15385
|
+
sdpMid: event.candidate.sdpMid,
|
|
15386
|
+
sdpMLineIndex: event.candidate.sdpMLineIndex
|
|
15387
|
+
});
|
|
15388
|
+
}
|
|
15389
|
+
};
|
|
15390
|
+
pc.ondatachannel = (event) => {
|
|
15391
|
+
const channel = event.channel;
|
|
15392
|
+
const label = channel.label ?? "";
|
|
15393
|
+
const terminalParsed = parseTerminalChannelLabel(label);
|
|
15394
|
+
if (terminalParsed) {
|
|
15395
|
+
logger.debug(
|
|
15396
|
+
{ machineId, taskId: terminalParsed.taskId, terminalId: terminalParsed.terminalId },
|
|
15397
|
+
"Terminal data channel received"
|
|
15398
|
+
);
|
|
15399
|
+
config2.onTerminalChannel?.(
|
|
15400
|
+
machineId,
|
|
15401
|
+
event.channel,
|
|
15402
|
+
terminalParsed.taskId,
|
|
15403
|
+
terminalParsed.terminalId
|
|
15404
|
+
);
|
|
15405
|
+
return;
|
|
15406
|
+
}
|
|
15407
|
+
if (label.startsWith("terminal-io:")) {
|
|
15408
|
+
logger.warn({ machineId, label }, "Terminal channel with empty taskId");
|
|
15409
|
+
return;
|
|
15410
|
+
}
|
|
15411
|
+
const fileChannelId = parseFileChannelLabel(label);
|
|
15412
|
+
if (fileChannelId) {
|
|
15413
|
+
logger.info({ machineId, channelId: fileChannelId }, "File I/O data channel received");
|
|
15414
|
+
config2.onFileChannel?.(machineId, event.channel, fileChannelId);
|
|
15415
|
+
return;
|
|
15416
|
+
}
|
|
15417
|
+
if (label.startsWith("file-io:")) {
|
|
15418
|
+
logger.warn({ machineId }, "File channel with empty id, ignoring");
|
|
15419
|
+
return;
|
|
15420
|
+
}
|
|
15421
|
+
logger.info({ machineId }, "Data channel received from browser");
|
|
15422
|
+
const rawChannel = event.channel;
|
|
15423
|
+
guardLoroChannelSend(rawChannel);
|
|
15424
|
+
config2.webrtcAdapter.attachDataChannel(machineIdToPeerId(machineId), event.channel);
|
|
15425
|
+
logger.info({ machineId }, "Data channel attached to Loro adapter");
|
|
15426
|
+
};
|
|
15427
|
+
pc.onconnectionstatechange = () => {
|
|
15428
|
+
const state = pc.connectionState;
|
|
15429
|
+
logger.info({ machineId, state }, "Peer connection state changed");
|
|
15430
|
+
if (state === "failed" || state === "closed") {
|
|
15431
|
+
config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(machineId));
|
|
15432
|
+
peers.delete(machineId);
|
|
15433
|
+
pc.close();
|
|
15434
|
+
}
|
|
15435
|
+
};
|
|
15436
|
+
pc.onsignalingstatechange = () => {
|
|
15437
|
+
const sigState = pc.signalingState;
|
|
15438
|
+
logger.info({ machineId, signalingState: sigState }, "Signaling state changed");
|
|
15439
|
+
};
|
|
15440
|
+
pc.onicegatheringstatechange = () => {
|
|
15441
|
+
const iceState = pc.iceGatheringState;
|
|
15442
|
+
logger.info({ machineId, iceGatheringState: iceState }, "ICE gathering state changed");
|
|
15443
|
+
};
|
|
15444
|
+
}
|
|
15445
|
+
return {
|
|
15446
|
+
async handleOffer(fromMachineId, offer) {
|
|
15447
|
+
logger.info({ fromMachineId }, "Handling WebRTC offer");
|
|
15448
|
+
const existing = peers.get(fromMachineId);
|
|
15449
|
+
if (existing) {
|
|
15450
|
+
logger.debug({ fromMachineId }, "Closing existing peer connection");
|
|
15451
|
+
existing.close();
|
|
15452
|
+
peers.delete(fromMachineId);
|
|
15453
|
+
}
|
|
15454
|
+
const promise = (async () => {
|
|
15455
|
+
const factory = await getFactory();
|
|
15456
|
+
const pc = factory();
|
|
15457
|
+
logger.debug({ fromMachineId }, "Created peer connection");
|
|
15458
|
+
setupPeerHandlers(fromMachineId, pc);
|
|
15459
|
+
logger.debug({ fromMachineId }, "Setting remote description (offer)");
|
|
15460
|
+
await pc.setRemoteDescription(offer);
|
|
15461
|
+
logger.debug({ fromMachineId }, "Creating answer");
|
|
15462
|
+
const answer = await pc.createAnswer();
|
|
15463
|
+
logger.debug(
|
|
15464
|
+
{ fromMachineId, hasAnswerSdp: !!answer.sdp },
|
|
15465
|
+
"Setting local description (answer)"
|
|
15466
|
+
);
|
|
15467
|
+
await pc.setLocalDescription(answer);
|
|
15468
|
+
peers.set(fromMachineId, pc);
|
|
15469
|
+
pendingCreates.delete(fromMachineId);
|
|
15470
|
+
logger.info({ fromMachineId }, "Sending WebRTC answer");
|
|
15471
|
+
config2.onAnswer(fromMachineId, { type: "answer", sdp: answer.sdp });
|
|
15472
|
+
return pc;
|
|
15473
|
+
})();
|
|
15474
|
+
pendingCreates.set(fromMachineId, promise);
|
|
15475
|
+
const HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
15476
|
+
setTimeout(() => {
|
|
15477
|
+
if (pendingCreates.get(fromMachineId) === promise) {
|
|
15478
|
+
pendingCreates.delete(fromMachineId);
|
|
15479
|
+
logger.warn({ fromMachineId }, "WebRTC handshake timed out");
|
|
15480
|
+
}
|
|
15481
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
15482
|
+
await promise;
|
|
15483
|
+
},
|
|
15484
|
+
async initiateOffer(targetMachineId) {
|
|
15485
|
+
logger.info({ targetMachineId }, "Initiating WebRTC offer");
|
|
15486
|
+
const existing = peers.get(targetMachineId);
|
|
15487
|
+
if (existing) {
|
|
15488
|
+
logger.debug({ targetMachineId }, "Closing existing peer connection");
|
|
15489
|
+
existing.close();
|
|
15490
|
+
peers.delete(targetMachineId);
|
|
15491
|
+
}
|
|
15492
|
+
const promise = (async () => {
|
|
15493
|
+
const factory = await getFactory();
|
|
15494
|
+
const pc = factory();
|
|
15495
|
+
logger.debug({ targetMachineId }, "Created peer connection");
|
|
15496
|
+
setupPeerHandlers(targetMachineId, pc);
|
|
15497
|
+
if (!pc.createDataChannel) {
|
|
15498
|
+
throw new Error("PeerConnection does not support createDataChannel");
|
|
15499
|
+
}
|
|
15500
|
+
if (!pc.createOffer) {
|
|
15501
|
+
throw new Error("PeerConnection does not support createOffer");
|
|
15502
|
+
}
|
|
15503
|
+
logger.debug({ targetMachineId }, "Creating loro-sync data channel");
|
|
15504
|
+
const channel = pc.createDataChannel("loro-sync", { ordered: true });
|
|
15505
|
+
const rawChannel = channel;
|
|
15506
|
+
guardLoroChannelSend(rawChannel);
|
|
15507
|
+
rawChannel.onopen = () => {
|
|
15508
|
+
config2.webrtcAdapter.attachDataChannel(
|
|
15509
|
+
machineIdToPeerId(targetMachineId),
|
|
15510
|
+
channel
|
|
15511
|
+
);
|
|
15512
|
+
logger.debug({ targetMachineId }, "Data channel attached to Loro adapter");
|
|
15513
|
+
};
|
|
15514
|
+
logger.debug({ targetMachineId }, "Creating offer");
|
|
15515
|
+
const offer = await pc.createOffer();
|
|
15516
|
+
logger.debug(
|
|
15517
|
+
{ targetMachineId, hasOfferSdp: !!offer.sdp },
|
|
15518
|
+
"Setting local description (offer)"
|
|
15519
|
+
);
|
|
15520
|
+
await pc.setLocalDescription(offer);
|
|
15521
|
+
peers.set(targetMachineId, pc);
|
|
15522
|
+
pendingCreates.delete(targetMachineId);
|
|
15523
|
+
logger.info({ targetMachineId }, "Sending WebRTC offer");
|
|
15524
|
+
config2.onOffer?.(targetMachineId, { type: "offer", sdp: offer.sdp });
|
|
15525
|
+
return pc;
|
|
15526
|
+
})();
|
|
15527
|
+
pendingCreates.set(targetMachineId, promise);
|
|
15528
|
+
const HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
15529
|
+
setTimeout(() => {
|
|
15530
|
+
if (pendingCreates.get(targetMachineId) === promise) {
|
|
15531
|
+
pendingCreates.delete(targetMachineId);
|
|
15532
|
+
logger.warn({ targetMachineId }, "WebRTC handshake timed out (initiator)");
|
|
15533
|
+
}
|
|
15534
|
+
}, HANDSHAKE_TIMEOUT_MS);
|
|
15535
|
+
await promise;
|
|
15536
|
+
},
|
|
15537
|
+
async handleAnswer(fromMachineId, answer) {
|
|
15538
|
+
logger.debug({ fromMachineId }, "Handling WebRTC answer");
|
|
15539
|
+
let pc = peers.get(fromMachineId);
|
|
15540
|
+
if (!pc) {
|
|
15541
|
+
const pending = pendingCreates.get(fromMachineId);
|
|
15542
|
+
if (pending) {
|
|
15543
|
+
pc = await pending;
|
|
15544
|
+
} else {
|
|
15545
|
+
logger.warn({ fromMachineId }, "Received answer for unknown peer");
|
|
15546
|
+
return;
|
|
15547
|
+
}
|
|
15548
|
+
}
|
|
15549
|
+
await pc.setRemoteDescription(answer);
|
|
15550
|
+
logger.debug({ fromMachineId }, "Remote description (answer) set");
|
|
15551
|
+
},
|
|
15552
|
+
async handleIce(fromMachineId, candidate) {
|
|
15553
|
+
logger.debug({ fromMachineId }, "Handling WebRTC ICE candidate");
|
|
15554
|
+
let pc = peers.get(fromMachineId);
|
|
15555
|
+
if (!pc) {
|
|
15556
|
+
const pending = pendingCreates.get(fromMachineId);
|
|
15557
|
+
if (pending) {
|
|
15558
|
+
pc = await pending;
|
|
15559
|
+
} else {
|
|
15560
|
+
logger.debug({ fromMachineId }, "Received ICE candidate for unknown peer");
|
|
15561
|
+
return;
|
|
15562
|
+
}
|
|
15563
|
+
}
|
|
15564
|
+
await pc.addIceCandidate(candidate);
|
|
15565
|
+
logger.debug({ fromMachineId }, "ICE candidate added");
|
|
15566
|
+
},
|
|
15567
|
+
closePeer(targetId) {
|
|
15568
|
+
const pc = peers.get(targetId);
|
|
15569
|
+
if (pc) {
|
|
15570
|
+
config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(targetId));
|
|
15571
|
+
pc.close();
|
|
15572
|
+
peers.delete(targetId);
|
|
15573
|
+
}
|
|
15574
|
+
},
|
|
15575
|
+
destroy() {
|
|
15576
|
+
for (const [machineId, pc] of peers) {
|
|
15577
|
+
config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(machineId));
|
|
15578
|
+
pc.close();
|
|
15579
|
+
}
|
|
15580
|
+
peers.clear();
|
|
15581
|
+
pendingCreates.clear();
|
|
15582
|
+
}
|
|
15583
|
+
};
|
|
15584
|
+
}
|
|
15585
|
+
|
|
15586
|
+
// src/collab-room-manager.ts
|
|
15587
|
+
function assertNever2(x) {
|
|
15588
|
+
throw new Error(`Unhandled message type: ${JSON.stringify(x)}`);
|
|
15589
|
+
}
|
|
15590
|
+
function namespacePeerId(roomId, remoteUserId) {
|
|
15591
|
+
return `collab:${roomId}:${remoteUserId}`;
|
|
15592
|
+
}
|
|
15593
|
+
function stripPeerIdNamespace(roomId, namespacedId) {
|
|
15594
|
+
const prefix = `collab:${roomId}:`;
|
|
15595
|
+
if (!namespacedId.startsWith(prefix)) {
|
|
15596
|
+
throw new Error(`Expected namespaced peer ID with prefix "${prefix}", got "${namespacedId}"`);
|
|
15597
|
+
}
|
|
15598
|
+
return namespacedId.slice(prefix.length);
|
|
15599
|
+
}
|
|
15600
|
+
function handleAuthenticated(room, msg) {
|
|
15601
|
+
room.myUserId = msg.userId;
|
|
15602
|
+
room.log.info({ roomId: room.roomId, userId: msg.userId }, "Authenticated in collab room");
|
|
15603
|
+
}
|
|
15604
|
+
function maybeInitiateOffer(room, remoteUserId, context) {
|
|
15605
|
+
if (!room.myUserId) return;
|
|
15606
|
+
room.knownPeers.add(remoteUserId);
|
|
15607
|
+
const shouldInitiate = room.myUserId < remoteUserId;
|
|
15608
|
+
if (!shouldInitiate) return;
|
|
15609
|
+
room.log.info({ roomId: room.roomId, targetUserId: remoteUserId }, context);
|
|
15610
|
+
const namespacedPeerId = namespacePeerId(room.roomId, remoteUserId);
|
|
15611
|
+
room.peerManager.initiateOffer(namespacedPeerId).catch((err) => {
|
|
15612
|
+
room.log.error(
|
|
15613
|
+
{ roomId: room.roomId, targetUserId: remoteUserId, err },
|
|
15614
|
+
"Failed to initiate offer"
|
|
15615
|
+
);
|
|
15616
|
+
});
|
|
15617
|
+
}
|
|
15618
|
+
function handleParticipantsList(room, msg) {
|
|
15619
|
+
if (!room.myUserId) {
|
|
15620
|
+
room.log.warn({ roomId: room.roomId }, "Received participants-list before authentication");
|
|
15621
|
+
return;
|
|
15622
|
+
}
|
|
15623
|
+
for (const participant of msg.participants) {
|
|
15624
|
+
if (participant.userId === room.myUserId) continue;
|
|
15625
|
+
if (room.knownPeers.has(participant.userId)) continue;
|
|
15626
|
+
maybeInitiateOffer(room, participant.userId, "Initiating WebRTC offer to existing participant");
|
|
15627
|
+
}
|
|
15628
|
+
}
|
|
15629
|
+
function handleParticipantJoined(room, msg) {
|
|
15630
|
+
if (!room.myUserId) return;
|
|
15631
|
+
if (msg.participant.userId === room.myUserId) return;
|
|
15632
|
+
maybeInitiateOffer(room, msg.participant.userId, "Initiating WebRTC offer to new participant");
|
|
15633
|
+
}
|
|
15634
|
+
function handleWebrtcOffer(room, msg) {
|
|
15635
|
+
room.log.info({ roomId: room.roomId, fromUserId: msg.targetUserId }, "Received WebRTC offer");
|
|
15636
|
+
const offer = msg.offer;
|
|
15637
|
+
const namespacedPeerId = namespacePeerId(room.roomId, msg.targetUserId);
|
|
15638
|
+
room.peerManager.handleOffer(namespacedPeerId, offer).catch((err) => {
|
|
15639
|
+
room.log.error(
|
|
15640
|
+
{ roomId: room.roomId, fromUserId: msg.targetUserId, err },
|
|
15641
|
+
"Failed to handle WebRTC offer"
|
|
15642
|
+
);
|
|
15643
|
+
});
|
|
15644
|
+
}
|
|
15645
|
+
function handleWebrtcAnswer(room, msg) {
|
|
15646
|
+
room.log.info({ roomId: room.roomId, fromUserId: msg.targetUserId }, "Received WebRTC answer");
|
|
15647
|
+
const answer = msg.answer;
|
|
15648
|
+
const namespacedPeerId = namespacePeerId(room.roomId, msg.targetUserId);
|
|
15649
|
+
room.peerManager.handleAnswer(namespacedPeerId, answer).catch((err) => {
|
|
15650
|
+
room.log.error(
|
|
15651
|
+
{ roomId: room.roomId, fromUserId: msg.targetUserId, err },
|
|
15652
|
+
"Failed to handle WebRTC answer"
|
|
15653
|
+
);
|
|
15654
|
+
});
|
|
15655
|
+
}
|
|
15656
|
+
function handleWebrtcIce(room, msg) {
|
|
15657
|
+
room.log.debug({ roomId: room.roomId, fromUserId: msg.targetUserId }, "Received ICE candidate");
|
|
15658
|
+
const candidate = msg.candidate;
|
|
15659
|
+
const namespacedPeerId = namespacePeerId(room.roomId, msg.targetUserId);
|
|
15660
|
+
room.peerManager.handleIce(namespacedPeerId, candidate).catch((err) => {
|
|
15661
|
+
room.log.error(
|
|
15662
|
+
{ roomId: room.roomId, fromUserId: msg.targetUserId, err },
|
|
15663
|
+
"Failed to handle ICE candidate"
|
|
15664
|
+
);
|
|
15665
|
+
});
|
|
15666
|
+
}
|
|
15667
|
+
function handleCollabRoomMessage(room, msg) {
|
|
15668
|
+
switch (msg.type) {
|
|
15669
|
+
case "authenticated":
|
|
15670
|
+
handleAuthenticated(room, msg);
|
|
15671
|
+
break;
|
|
15672
|
+
case "participants-list":
|
|
15673
|
+
handleParticipantsList(room, msg);
|
|
15674
|
+
break;
|
|
15675
|
+
case "participant-joined":
|
|
15676
|
+
handleParticipantJoined(room, msg);
|
|
15677
|
+
break;
|
|
15678
|
+
case "participant-left":
|
|
15679
|
+
room.knownPeers.delete(msg.userId);
|
|
15680
|
+
room.peerManager.closePeer(namespacePeerId(room.roomId, msg.userId));
|
|
15681
|
+
room.log.info({ roomId: room.roomId, userId: msg.userId }, "Participant left collab room");
|
|
15682
|
+
break;
|
|
15683
|
+
case "webrtc-offer":
|
|
15684
|
+
handleWebrtcOffer(room, msg);
|
|
15685
|
+
break;
|
|
15686
|
+
case "webrtc-answer":
|
|
15687
|
+
handleWebrtcAnswer(room, msg);
|
|
15688
|
+
break;
|
|
15689
|
+
case "webrtc-ice":
|
|
15690
|
+
handleWebrtcIce(room, msg);
|
|
15691
|
+
break;
|
|
15692
|
+
case "error":
|
|
15693
|
+
room.log.error({ roomId: room.roomId, message: msg.message }, "Collab room error");
|
|
15694
|
+
break;
|
|
15695
|
+
default:
|
|
15696
|
+
assertNever2(msg);
|
|
15697
|
+
}
|
|
15698
|
+
}
|
|
15699
|
+
function createRoomPeerManager(roomId, connection, webrtcAdapter) {
|
|
15700
|
+
return createPeerManager({
|
|
15701
|
+
webrtcAdapter,
|
|
15702
|
+
onAnswer(namespacedId, answer) {
|
|
15703
|
+
const targetUserId = stripPeerIdNamespace(roomId, namespacedId);
|
|
15704
|
+
connection.send({
|
|
15705
|
+
type: "webrtc-answer",
|
|
15706
|
+
targetUserId,
|
|
15707
|
+
// eslint-disable-next-line no-restricted-syntax -- SDP is opaque over signaling
|
|
15708
|
+
answer
|
|
15709
|
+
});
|
|
15710
|
+
},
|
|
15711
|
+
onOffer(namespacedId, offer) {
|
|
15712
|
+
const targetUserId = stripPeerIdNamespace(roomId, namespacedId);
|
|
15713
|
+
connection.send({
|
|
15714
|
+
type: "webrtc-offer",
|
|
15715
|
+
targetUserId,
|
|
15716
|
+
// eslint-disable-next-line no-restricted-syntax -- SDP is opaque over signaling
|
|
15717
|
+
offer
|
|
15718
|
+
});
|
|
15719
|
+
},
|
|
15720
|
+
onIceCandidate(namespacedId, candidate) {
|
|
15721
|
+
const targetUserId = stripPeerIdNamespace(roomId, namespacedId);
|
|
15722
|
+
connection.send({
|
|
15723
|
+
type: "webrtc-ice",
|
|
15724
|
+
targetUserId,
|
|
15725
|
+
// eslint-disable-next-line no-restricted-syntax -- ICE candidate is opaque over signaling
|
|
15726
|
+
candidate
|
|
15727
|
+
});
|
|
15728
|
+
}
|
|
15729
|
+
});
|
|
15730
|
+
}
|
|
15731
|
+
function createCollabRoomManager() {
|
|
15732
|
+
const rooms = /* @__PURE__ */ new Map();
|
|
15733
|
+
function leaveRoom(roomId) {
|
|
15734
|
+
const room = rooms.get(roomId);
|
|
15735
|
+
if (!room) return;
|
|
15736
|
+
room.log.info({ roomId: room.roomId }, "Leaving collab room");
|
|
15737
|
+
clearTimeout(room.expiryTimer);
|
|
15738
|
+
room.unsubMessage();
|
|
15739
|
+
room.unsubState();
|
|
15740
|
+
room.peerManager.destroy();
|
|
15741
|
+
room.connection.disconnect();
|
|
15742
|
+
rooms.delete(roomId);
|
|
15743
|
+
}
|
|
15744
|
+
return {
|
|
15745
|
+
join(config2) {
|
|
15746
|
+
const { roomId, taskId, token, expiresAt, signalingBaseUrl, userToken, machineId, log } = config2;
|
|
15747
|
+
if (rooms.has(roomId)) {
|
|
15748
|
+
log.info({ roomId }, "Already joined collab room, replacing connection");
|
|
15749
|
+
leaveRoom(roomId);
|
|
15750
|
+
}
|
|
15751
|
+
const wsUrl = new URL(signalingBaseUrl);
|
|
15752
|
+
wsUrl.pathname = ROUTES.WS_COLLAB.replace(":roomId", roomId);
|
|
15753
|
+
wsUrl.searchParams.set("token", token);
|
|
15754
|
+
wsUrl.searchParams.set("userToken", userToken);
|
|
15755
|
+
wsUrl.searchParams.set("clientType", "agent");
|
|
15756
|
+
wsUrl.searchParams.set("machineId", machineId);
|
|
15757
|
+
const connection = new CollabRoomConnection({
|
|
15758
|
+
url: wsUrl.toString(),
|
|
15759
|
+
maxRetries: -1,
|
|
15760
|
+
initialDelayMs: 1e3,
|
|
15761
|
+
maxDelayMs: 3e4,
|
|
15762
|
+
backoffMultiplier: 2
|
|
15763
|
+
});
|
|
15764
|
+
const peerManager = createRoomPeerManager(roomId, connection, config2.webrtcAdapter);
|
|
15765
|
+
const handle = {
|
|
15766
|
+
roomId,
|
|
15767
|
+
taskId,
|
|
15768
|
+
destroy() {
|
|
15769
|
+
leaveRoom(roomId);
|
|
15770
|
+
}
|
|
15771
|
+
};
|
|
15772
|
+
const delayMs = expiresAt - Date.now();
|
|
15773
|
+
if (delayMs <= 0 || !Number.isFinite(delayMs)) {
|
|
15774
|
+
log.warn({ expiresAt }, "Collab room already expired or invalid expiresAt");
|
|
15775
|
+
return handle;
|
|
15776
|
+
}
|
|
15777
|
+
const room = {
|
|
15778
|
+
roomId,
|
|
15779
|
+
taskId,
|
|
15780
|
+
connection,
|
|
15781
|
+
peerManager,
|
|
15782
|
+
myUserId: null,
|
|
15783
|
+
knownPeers: /* @__PURE__ */ new Set(),
|
|
15784
|
+
expiryTimer: setTimeout(() => {
|
|
15785
|
+
log.info({ roomId }, "Collab room token expired, leaving");
|
|
15786
|
+
leaveRoom(roomId);
|
|
15787
|
+
}, delayMs),
|
|
15788
|
+
unsubMessage: () => {
|
|
15789
|
+
},
|
|
15790
|
+
unsubState: () => {
|
|
15791
|
+
},
|
|
15792
|
+
log
|
|
15793
|
+
};
|
|
15794
|
+
rooms.set(roomId, room);
|
|
15795
|
+
room.unsubMessage = connection.onMessage((msg) => {
|
|
15796
|
+
handleCollabRoomMessage(room, msg);
|
|
15797
|
+
});
|
|
15798
|
+
room.unsubState = connection.onStateChange((state) => {
|
|
15799
|
+
log.info({ roomId, state }, "Collab room connection state changed");
|
|
15800
|
+
if (state === "disconnected" || state === "error") {
|
|
15801
|
+
room.peerManager.destroy();
|
|
15802
|
+
room.peerManager = createRoomPeerManager(roomId, connection, config2.webrtcAdapter);
|
|
15803
|
+
room.myUserId = null;
|
|
15804
|
+
room.knownPeers.clear();
|
|
15805
|
+
}
|
|
15806
|
+
});
|
|
15807
|
+
connection.connect();
|
|
15808
|
+
log.info({ roomId, taskId, url: `${wsUrl.origin}${wsUrl.pathname}` }, "Joining collab room");
|
|
15809
|
+
return handle;
|
|
15810
|
+
},
|
|
15811
|
+
leave(roomId) {
|
|
15812
|
+
leaveRoom(roomId);
|
|
15813
|
+
},
|
|
15814
|
+
destroy() {
|
|
15815
|
+
for (const roomId of [...rooms.keys()]) {
|
|
15816
|
+
leaveRoom(roomId);
|
|
15817
|
+
}
|
|
15818
|
+
}
|
|
15819
|
+
};
|
|
15820
|
+
}
|
|
15821
|
+
|
|
14786
15822
|
// src/crash-recovery.ts
|
|
14787
15823
|
function recoverOrphanedTask(taskDocs, log) {
|
|
14788
15824
|
const metaJson = taskDocs.meta.toJSON();
|
|
@@ -14811,10 +15847,10 @@ function recoverOrphanedTask(taskDocs, log) {
|
|
|
14811
15847
|
});
|
|
14812
15848
|
}
|
|
14813
15849
|
change(taskDocs.meta, (draft) => {
|
|
14814
|
-
draft.meta.status.set("
|
|
15850
|
+
draft.meta.status.set("input-required");
|
|
14815
15851
|
draft.meta.updatedAt.set(Date.now());
|
|
14816
15852
|
});
|
|
14817
|
-
log.info({ previousStatus: status }, "Recovered orphaned task after daemon crash");
|
|
15853
|
+
log.info({ previousStatus: status }, "Recovered orphaned task after daemon crash (resumable)");
|
|
14818
15854
|
return true;
|
|
14819
15855
|
}
|
|
14820
15856
|
|
|
@@ -14910,15 +15946,52 @@ async function runEnhanceQuery(prompt, systemPrompt, abortController, callbacks,
|
|
|
14910
15946
|
);
|
|
14911
15947
|
}
|
|
14912
15948
|
|
|
15949
|
+
// src/file-lister.ts
|
|
15950
|
+
import { execFile as execFile2 } from "child_process";
|
|
15951
|
+
var TIMEOUT_MS2 = 1e4;
|
|
15952
|
+
var MAX_BUFFER = 5 * 1024 * 1024;
|
|
15953
|
+
function listWorkspaceFiles(repoPath) {
|
|
15954
|
+
return new Promise((resolve4, reject) => {
|
|
15955
|
+
execFile2(
|
|
15956
|
+
"git",
|
|
15957
|
+
["ls-files", "--cached", "--others", "--exclude-standard"],
|
|
15958
|
+
{ timeout: TIMEOUT_MS2, maxBuffer: MAX_BUFFER, cwd: repoPath },
|
|
15959
|
+
(error2, stdout) => {
|
|
15960
|
+
if (error2) {
|
|
15961
|
+
reject(error2);
|
|
15962
|
+
return;
|
|
15963
|
+
}
|
|
15964
|
+
const lines = stdout.split("\n").filter(Boolean);
|
|
15965
|
+
if (lines.length === 0) {
|
|
15966
|
+
resolve4([]);
|
|
15967
|
+
return;
|
|
15968
|
+
}
|
|
15969
|
+
const dirSet = /* @__PURE__ */ new Set();
|
|
15970
|
+
const files = [];
|
|
15971
|
+
for (const line of lines) {
|
|
15972
|
+
files.push({ path: line, isDirectory: false });
|
|
15973
|
+
const segments = line.split("/");
|
|
15974
|
+
for (let i = 1; i < segments.length; i++) {
|
|
15975
|
+
dirSet.add(segments.slice(0, i).join("/"));
|
|
15976
|
+
}
|
|
15977
|
+
}
|
|
15978
|
+
const dirs = [...dirSet].sort((a, b) => a.localeCompare(b)).map((p) => ({ path: p, isDirectory: true }));
|
|
15979
|
+
const sortedFiles = files.sort((a, b) => a.path.localeCompare(b.path));
|
|
15980
|
+
resolve4([...dirs, ...sortedFiles]);
|
|
15981
|
+
}
|
|
15982
|
+
);
|
|
15983
|
+
});
|
|
15984
|
+
}
|
|
15985
|
+
|
|
14913
15986
|
// src/keep-awake.ts
|
|
14914
15987
|
import { spawn } from "child_process";
|
|
14915
15988
|
var darwinStrategy = {
|
|
14916
|
-
spawn: () => spawn("caffeinate", ["-
|
|
15989
|
+
spawn: () => spawn("caffeinate", ["-di"], { stdio: ["ignore", "ignore", "ignore"] })
|
|
14917
15990
|
};
|
|
14918
15991
|
var linuxStrategy = {
|
|
14919
15992
|
spawn: () => spawn(
|
|
14920
15993
|
"systemd-inhibit",
|
|
14921
|
-
["--what=idle", "--why=Shipyard agent tasks running", "sleep", "infinity"],
|
|
15994
|
+
["--what=idle:sleep", "--why=Shipyard agent tasks running", "sleep", "infinity"],
|
|
14922
15995
|
{ stdio: ["ignore", "ignore", "ignore"] }
|
|
14923
15996
|
)
|
|
14924
15997
|
};
|
|
@@ -14928,7 +16001,7 @@ var win32Strategy = {
|
|
|
14928
16001
|
[
|
|
14929
16002
|
"-NoProfile",
|
|
14930
16003
|
"-Command",
|
|
14931
|
-
"[System.Runtime.InteropServices.Marshal]::SetThreadExecutionState(
|
|
16004
|
+
"[System.Runtime.InteropServices.Marshal]::SetThreadExecutionState(0x80000003); Start-Sleep -Seconds 2147483"
|
|
14932
16005
|
],
|
|
14933
16006
|
{ stdio: ["ignore", "ignore", "ignore"] }
|
|
14934
16007
|
)
|
|
@@ -14953,9 +16026,12 @@ var KeepAwakeManager = class {
|
|
|
14953
16026
|
#shouldBeRunning = false;
|
|
14954
16027
|
#restartAttempts = 0;
|
|
14955
16028
|
#log;
|
|
14956
|
-
|
|
16029
|
+
#graceTimer = null;
|
|
16030
|
+
#gracePeriodMs;
|
|
16031
|
+
constructor(log, gracePeriodMs = KEEP_AWAKE_GRACE_PERIOD_MS) {
|
|
14957
16032
|
this.#strategy = createKeepAwakeStrategy();
|
|
14958
16033
|
this.#log = log;
|
|
16034
|
+
this.#gracePeriodMs = gracePeriodMs;
|
|
14959
16035
|
if (!this.#strategy) {
|
|
14960
16036
|
log.info({ platform: process.platform }, "Keep-awake not supported on this platform");
|
|
14961
16037
|
}
|
|
@@ -14963,20 +16039,50 @@ var KeepAwakeManager = class {
|
|
|
14963
16039
|
get running() {
|
|
14964
16040
|
return this.#child !== null;
|
|
14965
16041
|
}
|
|
16042
|
+
get graceActive() {
|
|
16043
|
+
return this.#graceTimer !== null;
|
|
16044
|
+
}
|
|
14966
16045
|
update(enabled, hasActiveTasks) {
|
|
14967
16046
|
const shouldRun = enabled && hasActiveTasks;
|
|
14968
|
-
this.#shouldBeRunning = shouldRun;
|
|
14969
16047
|
this.#restartAttempts = 0;
|
|
14970
|
-
if (shouldRun
|
|
14971
|
-
this.#
|
|
14972
|
-
|
|
16048
|
+
if (shouldRun) {
|
|
16049
|
+
this.#cancelGrace();
|
|
16050
|
+
this.#shouldBeRunning = true;
|
|
16051
|
+
if (!this.#child) {
|
|
16052
|
+
this.#start();
|
|
16053
|
+
}
|
|
16054
|
+
} else if (this.#graceTimer && enabled && !hasActiveTasks) {
|
|
16055
|
+
return;
|
|
16056
|
+
} else if (this.#shouldBeRunning && enabled && !hasActiveTasks) {
|
|
16057
|
+
this.#shouldBeRunning = false;
|
|
16058
|
+
this.#startGrace();
|
|
16059
|
+
} else if (!shouldRun) {
|
|
16060
|
+
this.#shouldBeRunning = false;
|
|
16061
|
+
this.#cancelGrace();
|
|
14973
16062
|
this.#stop();
|
|
14974
16063
|
}
|
|
14975
16064
|
}
|
|
14976
16065
|
shutdown() {
|
|
14977
16066
|
this.#shouldBeRunning = false;
|
|
16067
|
+
this.#cancelGrace();
|
|
14978
16068
|
this.#stop();
|
|
14979
16069
|
}
|
|
16070
|
+
#startGrace() {
|
|
16071
|
+
if (this.#graceTimer) return;
|
|
16072
|
+
this.#log.info({ gracePeriodMs: this.#gracePeriodMs }, "Keep-awake grace period started");
|
|
16073
|
+
this.#graceTimer = setTimeout(() => {
|
|
16074
|
+
this.#graceTimer = null;
|
|
16075
|
+
this.#log.info("Keep-awake grace period expired");
|
|
16076
|
+
this.#stop();
|
|
16077
|
+
}, this.#gracePeriodMs);
|
|
16078
|
+
this.#graceTimer.unref();
|
|
16079
|
+
}
|
|
16080
|
+
#cancelGrace() {
|
|
16081
|
+
if (!this.#graceTimer) return;
|
|
16082
|
+
clearTimeout(this.#graceTimer);
|
|
16083
|
+
this.#graceTimer = null;
|
|
16084
|
+
this.#log.info("Keep-awake grace period cancelled (new task started)");
|
|
16085
|
+
}
|
|
14980
16086
|
#start() {
|
|
14981
16087
|
if (!this.#strategy || this.#child) return;
|
|
14982
16088
|
try {
|
|
@@ -15003,7 +16109,7 @@ var KeepAwakeManager = class {
|
|
|
15003
16109
|
}
|
|
15004
16110
|
}
|
|
15005
16111
|
#scheduleRestart() {
|
|
15006
|
-
if (!this.#shouldBeRunning) return;
|
|
16112
|
+
if (!this.#shouldBeRunning && !this.#graceTimer) return;
|
|
15007
16113
|
this.#restartAttempts++;
|
|
15008
16114
|
if (this.#restartAttempts > MAX_RESTART_ATTEMPTS) {
|
|
15009
16115
|
this.#log.warn(
|
|
@@ -15018,7 +16124,7 @@ var KeepAwakeManager = class {
|
|
|
15018
16124
|
"Scheduling keep-awake restart"
|
|
15019
16125
|
);
|
|
15020
16126
|
const timer = setTimeout(() => {
|
|
15021
|
-
if (this.#shouldBeRunning && !this.#child) {
|
|
16127
|
+
if ((this.#shouldBeRunning || this.#graceTimer) && !this.#child) {
|
|
15022
16128
|
this.#start();
|
|
15023
16129
|
}
|
|
15024
16130
|
}, delay);
|
|
@@ -15036,158 +16142,6 @@ var KeepAwakeManager = class {
|
|
|
15036
16142
|
}
|
|
15037
16143
|
};
|
|
15038
16144
|
|
|
15039
|
-
// src/peer-manager.ts
|
|
15040
|
-
var ICE_SERVERS = [{ urls: "stun:stun.l.google.com:19302" }];
|
|
15041
|
-
function machineIdToPeerId(machineId) {
|
|
15042
|
-
return machineId;
|
|
15043
|
-
}
|
|
15044
|
-
async function loadDefaultFactory() {
|
|
15045
|
-
const { RTCPeerConnection } = await import("node-datachannel/polyfill");
|
|
15046
|
-
return () => {
|
|
15047
|
-
const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
|
|
15048
|
-
return pc;
|
|
15049
|
-
};
|
|
15050
|
-
}
|
|
15051
|
-
function createPeerManager(config2) {
|
|
15052
|
-
const peers = /* @__PURE__ */ new Map();
|
|
15053
|
-
const pendingCreates = /* @__PURE__ */ new Map();
|
|
15054
|
-
let factoryPromise = null;
|
|
15055
|
-
async function getFactory() {
|
|
15056
|
-
if (config2.createPeerConnection) {
|
|
15057
|
-
return config2.createPeerConnection;
|
|
15058
|
-
}
|
|
15059
|
-
if (!factoryPromise) {
|
|
15060
|
-
factoryPromise = loadDefaultFactory();
|
|
15061
|
-
}
|
|
15062
|
-
return factoryPromise;
|
|
15063
|
-
}
|
|
15064
|
-
function setupPeerHandlers(machineId, pc) {
|
|
15065
|
-
pc.onicecandidate = (event) => {
|
|
15066
|
-
if (event.candidate) {
|
|
15067
|
-
config2.onIceCandidate(machineId, {
|
|
15068
|
-
candidate: event.candidate.candidate,
|
|
15069
|
-
sdpMid: event.candidate.sdpMid,
|
|
15070
|
-
sdpMLineIndex: event.candidate.sdpMLineIndex
|
|
15071
|
-
});
|
|
15072
|
-
}
|
|
15073
|
-
};
|
|
15074
|
-
pc.ondatachannel = (event) => {
|
|
15075
|
-
const channel = event.channel;
|
|
15076
|
-
if (channel.label?.startsWith("terminal-io:")) {
|
|
15077
|
-
const taskId = channel.label.slice("terminal-io:".length);
|
|
15078
|
-
if (!taskId) {
|
|
15079
|
-
logger.warn({ machineId }, "Terminal channel with empty taskId, ignoring");
|
|
15080
|
-
return;
|
|
15081
|
-
}
|
|
15082
|
-
logger.info({ machineId, taskId }, "Terminal data channel received");
|
|
15083
|
-
config2.onTerminalChannel?.(machineId, event.channel, taskId);
|
|
15084
|
-
} else {
|
|
15085
|
-
logger.info({ machineId }, "Data channel received from browser");
|
|
15086
|
-
config2.webrtcAdapter.attachDataChannel(
|
|
15087
|
-
machineIdToPeerId(machineId),
|
|
15088
|
-
event.channel
|
|
15089
|
-
);
|
|
15090
|
-
logger.info({ machineId }, "Data channel attached to Loro adapter");
|
|
15091
|
-
}
|
|
15092
|
-
};
|
|
15093
|
-
pc.onconnectionstatechange = () => {
|
|
15094
|
-
const state = pc.connectionState;
|
|
15095
|
-
logger.info({ machineId, state }, "Peer connection state changed");
|
|
15096
|
-
if (state === "failed" || state === "closed") {
|
|
15097
|
-
config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(machineId));
|
|
15098
|
-
peers.delete(machineId);
|
|
15099
|
-
pc.close();
|
|
15100
|
-
}
|
|
15101
|
-
};
|
|
15102
|
-
pc.onsignalingstatechange = () => {
|
|
15103
|
-
const sigState = pc.signalingState;
|
|
15104
|
-
logger.info({ machineId, signalingState: sigState }, "Signaling state changed");
|
|
15105
|
-
};
|
|
15106
|
-
pc.onicegatheringstatechange = () => {
|
|
15107
|
-
const iceState = pc.iceGatheringState;
|
|
15108
|
-
logger.info({ machineId, iceGatheringState: iceState }, "ICE gathering state changed");
|
|
15109
|
-
};
|
|
15110
|
-
}
|
|
15111
|
-
return {
|
|
15112
|
-
async handleOffer(fromMachineId, offer) {
|
|
15113
|
-
logger.info({ fromMachineId }, "Handling WebRTC offer");
|
|
15114
|
-
const existing = peers.get(fromMachineId);
|
|
15115
|
-
if (existing) {
|
|
15116
|
-
logger.debug({ fromMachineId }, "Closing existing peer connection");
|
|
15117
|
-
existing.close();
|
|
15118
|
-
peers.delete(fromMachineId);
|
|
15119
|
-
}
|
|
15120
|
-
const promise = (async () => {
|
|
15121
|
-
const factory = await getFactory();
|
|
15122
|
-
const pc = factory();
|
|
15123
|
-
logger.debug({ fromMachineId }, "Created peer connection");
|
|
15124
|
-
setupPeerHandlers(fromMachineId, pc);
|
|
15125
|
-
logger.debug({ fromMachineId }, "Setting remote description (offer)");
|
|
15126
|
-
await pc.setRemoteDescription(offer);
|
|
15127
|
-
logger.debug({ fromMachineId }, "Creating answer");
|
|
15128
|
-
const answer = await pc.createAnswer();
|
|
15129
|
-
logger.debug(
|
|
15130
|
-
{ fromMachineId, hasAnswerSdp: !!answer.sdp },
|
|
15131
|
-
"Setting local description (answer)"
|
|
15132
|
-
);
|
|
15133
|
-
await pc.setLocalDescription(answer);
|
|
15134
|
-
peers.set(fromMachineId, pc);
|
|
15135
|
-
pendingCreates.delete(fromMachineId);
|
|
15136
|
-
logger.info({ fromMachineId }, "Sending WebRTC answer");
|
|
15137
|
-
config2.onAnswer(fromMachineId, { type: "answer", sdp: answer.sdp });
|
|
15138
|
-
return pc;
|
|
15139
|
-
})();
|
|
15140
|
-
pendingCreates.set(fromMachineId, promise);
|
|
15141
|
-
const HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
15142
|
-
setTimeout(() => {
|
|
15143
|
-
if (pendingCreates.get(fromMachineId) === promise) {
|
|
15144
|
-
pendingCreates.delete(fromMachineId);
|
|
15145
|
-
logger.warn({ fromMachineId }, "WebRTC handshake timed out");
|
|
15146
|
-
}
|
|
15147
|
-
}, HANDSHAKE_TIMEOUT_MS);
|
|
15148
|
-
await promise;
|
|
15149
|
-
},
|
|
15150
|
-
async handleAnswer(fromMachineId, answer) {
|
|
15151
|
-
logger.debug({ fromMachineId }, "Handling WebRTC answer");
|
|
15152
|
-
let pc = peers.get(fromMachineId);
|
|
15153
|
-
if (!pc) {
|
|
15154
|
-
const pending = pendingCreates.get(fromMachineId);
|
|
15155
|
-
if (pending) {
|
|
15156
|
-
pc = await pending;
|
|
15157
|
-
} else {
|
|
15158
|
-
logger.warn({ fromMachineId }, "Received answer for unknown peer");
|
|
15159
|
-
return;
|
|
15160
|
-
}
|
|
15161
|
-
}
|
|
15162
|
-
await pc.setRemoteDescription(answer);
|
|
15163
|
-
logger.debug({ fromMachineId }, "Remote description (answer) set");
|
|
15164
|
-
},
|
|
15165
|
-
async handleIce(fromMachineId, candidate) {
|
|
15166
|
-
logger.debug({ fromMachineId }, "Handling WebRTC ICE candidate");
|
|
15167
|
-
let pc = peers.get(fromMachineId);
|
|
15168
|
-
if (!pc) {
|
|
15169
|
-
const pending = pendingCreates.get(fromMachineId);
|
|
15170
|
-
if (pending) {
|
|
15171
|
-
pc = await pending;
|
|
15172
|
-
} else {
|
|
15173
|
-
logger.warn({ fromMachineId }, "Received ICE candidate for unknown peer");
|
|
15174
|
-
return;
|
|
15175
|
-
}
|
|
15176
|
-
}
|
|
15177
|
-
await pc.addIceCandidate(candidate);
|
|
15178
|
-
logger.debug({ fromMachineId }, "ICE candidate added");
|
|
15179
|
-
},
|
|
15180
|
-
destroy() {
|
|
15181
|
-
for (const [machineId, pc] of peers) {
|
|
15182
|
-
config2.webrtcAdapter.detachDataChannel(machineIdToPeerId(machineId));
|
|
15183
|
-
pc.close();
|
|
15184
|
-
}
|
|
15185
|
-
peers.clear();
|
|
15186
|
-
pendingCreates.clear();
|
|
15187
|
-
}
|
|
15188
|
-
};
|
|
15189
|
-
}
|
|
15190
|
-
|
|
15191
16145
|
// src/plan-editor/format-diff-feedback.ts
|
|
15192
16146
|
function formatDiffFeedbackForClaudeCode(comments, generalFeedback) {
|
|
15193
16147
|
const sections = [];
|
|
@@ -21261,8 +22215,8 @@ var EditorState = class _EditorState {
|
|
|
21261
22215
|
throw new RangeError("Applying a mismatched transaction");
|
|
21262
22216
|
let newInstance = new _EditorState(this.config), fields = this.config.fields;
|
|
21263
22217
|
for (let i = 0; i < fields.length; i++) {
|
|
21264
|
-
let
|
|
21265
|
-
newInstance[
|
|
22218
|
+
let field2 = fields[i];
|
|
22219
|
+
newInstance[field2.name] = field2.apply(tr2, this[field2.name], this, newInstance);
|
|
21266
22220
|
}
|
|
21267
22221
|
return newInstance;
|
|
21268
22222
|
}
|
|
@@ -21334,24 +22288,24 @@ var EditorState = class _EditorState {
|
|
|
21334
22288
|
throw new RangeError("Required config field 'schema' missing");
|
|
21335
22289
|
let $config = new Configuration(config2.schema, config2.plugins);
|
|
21336
22290
|
let instance = new _EditorState($config);
|
|
21337
|
-
$config.fields.forEach((
|
|
21338
|
-
if (
|
|
22291
|
+
$config.fields.forEach((field2) => {
|
|
22292
|
+
if (field2.name == "doc") {
|
|
21339
22293
|
instance.doc = Node.fromJSON(config2.schema, json.doc);
|
|
21340
|
-
} else if (
|
|
22294
|
+
} else if (field2.name == "selection") {
|
|
21341
22295
|
instance.selection = Selection.fromJSON(instance.doc, json.selection);
|
|
21342
|
-
} else if (
|
|
22296
|
+
} else if (field2.name == "storedMarks") {
|
|
21343
22297
|
if (json.storedMarks)
|
|
21344
22298
|
instance.storedMarks = json.storedMarks.map(config2.schema.markFromJSON);
|
|
21345
22299
|
} else {
|
|
21346
22300
|
if (pluginFields)
|
|
21347
22301
|
for (let prop in pluginFields) {
|
|
21348
22302
|
let plugin = pluginFields[prop], state = plugin.spec.state;
|
|
21349
|
-
if (plugin.key ==
|
|
21350
|
-
instance[
|
|
22303
|
+
if (plugin.key == field2.name && state && state.fromJSON && Object.prototype.hasOwnProperty.call(json, prop)) {
|
|
22304
|
+
instance[field2.name] = state.fromJSON.call(plugin, config2, json[prop], instance);
|
|
21351
22305
|
return;
|
|
21352
22306
|
}
|
|
21353
22307
|
}
|
|
21354
|
-
instance[
|
|
22308
|
+
instance[field2.name] = field2.init(config2, instance);
|
|
21355
22309
|
}
|
|
21356
22310
|
});
|
|
21357
22311
|
return instance;
|
|
@@ -29057,18 +30011,18 @@ function findParentNodeClosestToPos($pos, predicate) {
|
|
|
29057
30011
|
function findParentNode(predicate) {
|
|
29058
30012
|
return (selection) => findParentNodeClosestToPos(selection.$from, predicate);
|
|
29059
30013
|
}
|
|
29060
|
-
function getExtensionField(extension,
|
|
29061
|
-
if (extension.config[
|
|
29062
|
-
return getExtensionField(extension.parent,
|
|
30014
|
+
function getExtensionField(extension, field2, context) {
|
|
30015
|
+
if (extension.config[field2] === void 0 && extension.parent) {
|
|
30016
|
+
return getExtensionField(extension.parent, field2, context);
|
|
29063
30017
|
}
|
|
29064
|
-
if (typeof extension.config[
|
|
29065
|
-
const value = extension.config[
|
|
30018
|
+
if (typeof extension.config[field2] === "function") {
|
|
30019
|
+
const value = extension.config[field2].bind({
|
|
29066
30020
|
...context,
|
|
29067
|
-
parent: extension.parent ? getExtensionField(extension.parent,
|
|
30021
|
+
parent: extension.parent ? getExtensionField(extension.parent, field2, context) : null
|
|
29068
30022
|
});
|
|
29069
30023
|
return value;
|
|
29070
30024
|
}
|
|
29071
|
-
return extension.config[
|
|
30025
|
+
return extension.config[field2];
|
|
29072
30026
|
}
|
|
29073
30027
|
function flattenExtensions(extensions) {
|
|
29074
30028
|
return extensions.map((extension) => {
|
|
@@ -43619,7 +44573,7 @@ function initPlanEditorDoc(loroDoc, planId, markdown) {
|
|
|
43619
44573
|
loroDoc.commit();
|
|
43620
44574
|
return true;
|
|
43621
44575
|
} catch (error2) {
|
|
43622
|
-
logger.warn({ planId,
|
|
44576
|
+
logger.warn({ planId, err: error2 }, "initPlanEditorDoc failed");
|
|
43623
44577
|
return false;
|
|
43624
44578
|
}
|
|
43625
44579
|
}
|
|
@@ -43681,6 +44635,7 @@ ensureSpawnHelperExecutable();
|
|
|
43681
44635
|
var KILL_TIMEOUT_MS = 5e3;
|
|
43682
44636
|
var DEFAULT_COLS = 80;
|
|
43683
44637
|
var DEFAULT_ROWS = 24;
|
|
44638
|
+
var SCROLLBACK_MAX_BYTES = 5e4;
|
|
43684
44639
|
function createPtyManager() {
|
|
43685
44640
|
const log = createChildLogger({ mode: "pty" });
|
|
43686
44641
|
let process2 = null;
|
|
@@ -43688,6 +44643,10 @@ function createPtyManager() {
|
|
|
43688
44643
|
let killTimer = null;
|
|
43689
44644
|
const dataCallbacks = [];
|
|
43690
44645
|
const exitCallbacks = [];
|
|
44646
|
+
let dataSink = null;
|
|
44647
|
+
let exitSink = null;
|
|
44648
|
+
const scrollbackChunks = [];
|
|
44649
|
+
let scrollbackBytes = 0;
|
|
43691
44650
|
function getDefaultShell() {
|
|
43692
44651
|
return globalThis.process.env.SHELL ?? "/bin/zsh";
|
|
43693
44652
|
}
|
|
@@ -43718,6 +44677,8 @@ function createPtyManager() {
|
|
|
43718
44677
|
}
|
|
43719
44678
|
isAlive = true;
|
|
43720
44679
|
process2.onData((data) => {
|
|
44680
|
+
appendScrollback(data);
|
|
44681
|
+
if (dataSink) dataSink(data);
|
|
43721
44682
|
for (const cb of dataCallbacks) {
|
|
43722
44683
|
cb(data);
|
|
43723
44684
|
}
|
|
@@ -43726,6 +44687,7 @@ function createPtyManager() {
|
|
|
43726
44687
|
log.info({ exitCode: exitCode3, signal, pid: process2?.pid }, "PTY exited");
|
|
43727
44688
|
isAlive = false;
|
|
43728
44689
|
clearKillTimer();
|
|
44690
|
+
if (exitSink) exitSink(exitCode3, signal);
|
|
43729
44691
|
for (const cb of exitCallbacks) {
|
|
43730
44692
|
cb(exitCode3, signal);
|
|
43731
44693
|
}
|
|
@@ -43769,10 +44731,33 @@ function createPtyManager() {
|
|
|
43769
44731
|
}
|
|
43770
44732
|
}, KILL_TIMEOUT_MS);
|
|
43771
44733
|
}
|
|
44734
|
+
function appendScrollback(data) {
|
|
44735
|
+
const byteLen = Buffer.byteLength(data);
|
|
44736
|
+
scrollbackChunks.push(data);
|
|
44737
|
+
scrollbackBytes += byteLen;
|
|
44738
|
+
while (scrollbackBytes > SCROLLBACK_MAX_BYTES && scrollbackChunks.length > 1) {
|
|
44739
|
+
const removed = scrollbackChunks.shift();
|
|
44740
|
+
if (!removed) break;
|
|
44741
|
+
scrollbackBytes -= Buffer.byteLength(removed);
|
|
44742
|
+
}
|
|
44743
|
+
}
|
|
44744
|
+
function setDataSink(callback) {
|
|
44745
|
+
dataSink = callback;
|
|
44746
|
+
}
|
|
44747
|
+
function setExitSink(callback) {
|
|
44748
|
+
exitSink = callback;
|
|
44749
|
+
}
|
|
44750
|
+
function getScrollback() {
|
|
44751
|
+
return scrollbackChunks.join("");
|
|
44752
|
+
}
|
|
43772
44753
|
function dispose() {
|
|
43773
44754
|
kill();
|
|
43774
44755
|
dataCallbacks.length = 0;
|
|
43775
44756
|
exitCallbacks.length = 0;
|
|
44757
|
+
dataSink = null;
|
|
44758
|
+
exitSink = null;
|
|
44759
|
+
scrollbackChunks.length = 0;
|
|
44760
|
+
scrollbackBytes = 0;
|
|
43776
44761
|
process2 = null;
|
|
43777
44762
|
isAlive = false;
|
|
43778
44763
|
}
|
|
@@ -43788,6 +44773,9 @@ function createPtyManager() {
|
|
|
43788
44773
|
resize,
|
|
43789
44774
|
onData,
|
|
43790
44775
|
onExit,
|
|
44776
|
+
setDataSink,
|
|
44777
|
+
setExitSink,
|
|
44778
|
+
getScrollback,
|
|
43791
44779
|
kill,
|
|
43792
44780
|
dispose
|
|
43793
44781
|
};
|
|
@@ -43918,7 +44906,7 @@ function toSdkContent(blocks) {
|
|
|
43918
44906
|
}
|
|
43919
44907
|
return result;
|
|
43920
44908
|
}
|
|
43921
|
-
function
|
|
44909
|
+
function extractStringField2(message, key, fallback) {
|
|
43922
44910
|
const value = message[key];
|
|
43923
44911
|
return typeof value === "string" ? value : fallback;
|
|
43924
44912
|
}
|
|
@@ -44024,6 +45012,7 @@ var SessionManager = class {
|
|
|
44024
45012
|
#reviewDoc;
|
|
44025
45013
|
#onStatusChange;
|
|
44026
45014
|
#onBackgroundAgent;
|
|
45015
|
+
#onTodoProgress;
|
|
44027
45016
|
#userId;
|
|
44028
45017
|
#userName;
|
|
44029
45018
|
#currentModel = null;
|
|
@@ -44032,7 +45021,7 @@ var SessionManager = class {
|
|
|
44032
45021
|
#inputController = null;
|
|
44033
45022
|
#activeQuery = null;
|
|
44034
45023
|
#knownSkills = /* @__PURE__ */ new Set();
|
|
44035
|
-
constructor(taskDocs, userId, userName, onStatusChange, onBackgroundAgent) {
|
|
45024
|
+
constructor(taskDocs, userId, userName, onStatusChange, onBackgroundAgent, onTodoProgress) {
|
|
44036
45025
|
this.#metaDoc = taskDocs.meta;
|
|
44037
45026
|
this.#convDoc = taskDocs.conv;
|
|
44038
45027
|
this.#reviewDoc = taskDocs.review;
|
|
@@ -44040,6 +45029,7 @@ var SessionManager = class {
|
|
|
44040
45029
|
this.#userName = userName;
|
|
44041
45030
|
this.#onStatusChange = onStatusChange;
|
|
44042
45031
|
this.#onBackgroundAgent = onBackgroundAgent;
|
|
45032
|
+
this.#onTodoProgress = onTodoProgress;
|
|
44043
45033
|
}
|
|
44044
45034
|
#buildAttribution() {
|
|
44045
45035
|
return {
|
|
@@ -44055,6 +45045,18 @@ var SessionManager = class {
|
|
|
44055
45045
|
#notifyStatusChange(status) {
|
|
44056
45046
|
this.#onStatusChange?.(status);
|
|
44057
45047
|
}
|
|
45048
|
+
#notifyTodoProgress(items) {
|
|
45049
|
+
if (!this.#onTodoProgress) return;
|
|
45050
|
+
let completed = 0;
|
|
45051
|
+
let currentActivity = null;
|
|
45052
|
+
for (const item of items) {
|
|
45053
|
+
if (item.status === "completed") completed++;
|
|
45054
|
+
if (item.status === "in_progress" && !currentActivity) {
|
|
45055
|
+
currentActivity = item.activeForm ?? null;
|
|
45056
|
+
}
|
|
45057
|
+
}
|
|
45058
|
+
this.#onTodoProgress({ todoCompleted: completed, todoTotal: items.length, currentActivity });
|
|
45059
|
+
}
|
|
44058
45060
|
/**
|
|
44059
45061
|
* Extract the latest user message text from the conversation.
|
|
44060
45062
|
* Walks backwards from the end to find the most recent user turn,
|
|
@@ -44206,17 +45208,19 @@ var SessionManager = class {
|
|
|
44206
45208
|
this.#activeQuery = null;
|
|
44207
45209
|
}
|
|
44208
45210
|
async #fetchKnownSkills(response) {
|
|
44209
|
-
const
|
|
45211
|
+
const TIMEOUT_MS3 = 1e4;
|
|
44210
45212
|
try {
|
|
44211
45213
|
const commands = await Promise.race([
|
|
44212
45214
|
response.supportedCommands(),
|
|
44213
45215
|
new Promise(
|
|
44214
|
-
(_, reject) => setTimeout(() => reject(new Error("supportedCommands timed out")),
|
|
45216
|
+
(_, reject) => setTimeout(() => reject(new Error("supportedCommands timed out")), TIMEOUT_MS3)
|
|
44215
45217
|
)
|
|
44216
45218
|
]);
|
|
44217
45219
|
return new Set(commands.map((c) => c.name));
|
|
44218
45220
|
} catch {
|
|
44219
|
-
logger.
|
|
45221
|
+
logger.debug(
|
|
45222
|
+
"Failed to fetch supportedCommands, all slash-prefixed messages will be escaped"
|
|
45223
|
+
);
|
|
44220
45224
|
return /* @__PURE__ */ new Set();
|
|
44221
45225
|
}
|
|
44222
45226
|
}
|
|
@@ -44434,9 +45438,9 @@ var SessionManager = class {
|
|
|
44434
45438
|
}
|
|
44435
45439
|
#handleTaskStarted(message) {
|
|
44436
45440
|
const msg = message;
|
|
44437
|
-
const taskId =
|
|
44438
|
-
const toolUseId =
|
|
44439
|
-
const description =
|
|
45441
|
+
const taskId = extractStringField2(msg, "task_id", "");
|
|
45442
|
+
const toolUseId = extractStringField2(msg, "tool_use_id", "") || taskId;
|
|
45443
|
+
const description = extractStringField2(msg, "description", "");
|
|
44440
45444
|
if (!toolUseId) {
|
|
44441
45445
|
logger.warn("Received task_started with no tool_use_id or task_id, skipping");
|
|
44442
45446
|
return;
|
|
@@ -44461,11 +45465,11 @@ var SessionManager = class {
|
|
|
44461
45465
|
}
|
|
44462
45466
|
#handleTaskNotification(message) {
|
|
44463
45467
|
const msg = message;
|
|
44464
|
-
const taskId =
|
|
44465
|
-
const toolUseId =
|
|
44466
|
-
const msgStatus =
|
|
44467
|
-
const summary =
|
|
44468
|
-
const outputFile =
|
|
45468
|
+
const taskId = extractStringField2(msg, "task_id", "unknown");
|
|
45469
|
+
const toolUseId = extractStringField2(msg, "tool_use_id", "") || taskId;
|
|
45470
|
+
const msgStatus = extractStringField2(msg, "status", "unknown");
|
|
45471
|
+
const summary = extractStringField2(msg, "summary", "");
|
|
45472
|
+
const outputFile = extractStringField2(msg, "output_file", "");
|
|
44469
45473
|
logger.info(
|
|
44470
45474
|
{ taskId, toolUseId, status: msgStatus, summary },
|
|
44471
45475
|
"Received task_notification from subagent"
|
|
@@ -44673,6 +45677,7 @@ var SessionManager = class {
|
|
|
44673
45677
|
{ toolUseId: block2.toolUseId, count: enrichedItems.length },
|
|
44674
45678
|
"Updated todoItems from TodoWrite tool call"
|
|
44675
45679
|
);
|
|
45680
|
+
this.#notifyTodoProgress(enrichedItems);
|
|
44676
45681
|
}
|
|
44677
45682
|
}
|
|
44678
45683
|
#handleResult(message, sessionId, agentSessionId) {
|
|
@@ -44711,6 +45716,7 @@ var SessionManager = class {
|
|
|
44711
45716
|
}
|
|
44712
45717
|
#handleProcessError(error2, sessionId, agentSessionId, idleTimedOut, abortController) {
|
|
44713
45718
|
if (idleTimedOut) {
|
|
45719
|
+
logger.info({ sessionId, cause: "idle-timeout" }, "Session error: idle timeout");
|
|
44714
45720
|
this.#markInterrupted(sessionId);
|
|
44715
45721
|
return {
|
|
44716
45722
|
sessionId,
|
|
@@ -44720,10 +45726,15 @@ var SessionManager = class {
|
|
|
44720
45726
|
};
|
|
44721
45727
|
}
|
|
44722
45728
|
if (abortController?.signal.aborted) {
|
|
45729
|
+
logger.info({ sessionId, cause: "abort-signal" }, "Session error: abort signal");
|
|
44723
45730
|
this.#markInterrupted(sessionId);
|
|
44724
45731
|
return { sessionId, agentSessionId, status: "interrupted" };
|
|
44725
45732
|
}
|
|
44726
45733
|
const errorMsg = error2 instanceof Error ? error2.message : String(error2);
|
|
45734
|
+
logger.info(
|
|
45735
|
+
{ sessionId, cause: "process-error", error: errorMsg },
|
|
45736
|
+
"Session error: process threw"
|
|
45737
|
+
);
|
|
44727
45738
|
this.#markFailed(sessionId, errorMsg);
|
|
44728
45739
|
return { sessionId, agentSessionId, status: "failed", error: errorMsg };
|
|
44729
45740
|
}
|
|
@@ -44769,11 +45780,13 @@ var SessionManager = class {
|
|
|
44769
45780
|
};
|
|
44770
45781
|
|
|
44771
45782
|
// src/signaling-setup.ts
|
|
45783
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
44772
45784
|
import { hostname } from "os";
|
|
45785
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
44773
45786
|
|
|
44774
45787
|
// src/signaling.ts
|
|
44775
45788
|
function createDaemonSignaling(config2) {
|
|
44776
|
-
const agentId = nanoid();
|
|
45789
|
+
const agentId = config2.agentId ?? nanoid();
|
|
44777
45790
|
function send(msg) {
|
|
44778
45791
|
config2.connection.send(msg);
|
|
44779
45792
|
}
|
|
@@ -44807,7 +45820,22 @@ function createDaemonSignaling(config2) {
|
|
|
44807
45820
|
}
|
|
44808
45821
|
|
|
44809
45822
|
// src/signaling-setup.ts
|
|
44810
|
-
|
|
45823
|
+
function getOrCreateAgentId(shipyardHome) {
|
|
45824
|
+
const filePath = join6(shipyardHome, "agent-id");
|
|
45825
|
+
try {
|
|
45826
|
+
const existing = readFileSync(filePath, "utf-8").trim();
|
|
45827
|
+
if (existing) return existing;
|
|
45828
|
+
} catch {
|
|
45829
|
+
}
|
|
45830
|
+
const id = crypto.randomUUID();
|
|
45831
|
+
try {
|
|
45832
|
+
mkdirSync(dirname3(filePath), { recursive: true });
|
|
45833
|
+
writeFileSync(filePath, id, { mode: 384 });
|
|
45834
|
+
} catch {
|
|
45835
|
+
}
|
|
45836
|
+
return id;
|
|
45837
|
+
}
|
|
45838
|
+
async function createSignalingHandle(env, log, shipyardHome) {
|
|
44811
45839
|
if (!env.SHIPYARD_SIGNALING_URL) {
|
|
44812
45840
|
return null;
|
|
44813
45841
|
}
|
|
@@ -44840,11 +45868,14 @@ async function createSignalingHandle(env, log) {
|
|
|
44840
45868
|
maxDelayMs: 3e4,
|
|
44841
45869
|
backoffMultiplier: 2
|
|
44842
45870
|
});
|
|
45871
|
+
const agentId = getOrCreateAgentId(shipyardHome);
|
|
45872
|
+
log.info({ agentId }, "Using persisted agent ID");
|
|
44843
45873
|
const signaling = createDaemonSignaling({
|
|
44844
45874
|
connection,
|
|
44845
45875
|
machineId,
|
|
44846
45876
|
machineName,
|
|
44847
|
-
agentType: "daemon"
|
|
45877
|
+
agentType: "daemon",
|
|
45878
|
+
agentId
|
|
44848
45879
|
});
|
|
44849
45880
|
connection.onStateChange((state) => {
|
|
44850
45881
|
if (state === "connected") {
|
|
@@ -44924,21 +45955,21 @@ function cleanupStaleSetupEntries(roomDoc, localMachineId, log) {
|
|
|
44924
45955
|
}
|
|
44925
45956
|
|
|
44926
45957
|
// src/worktree-command.ts
|
|
44927
|
-
import { execFile as
|
|
45958
|
+
import { execFile as execFile3, spawn as spawn3 } from "child_process";
|
|
44928
45959
|
import { closeSync, openSync } from "fs";
|
|
44929
45960
|
import { access, chmod, constants as constants2, copyFile, mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
44930
|
-
import { dirname as
|
|
45961
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7 } from "path";
|
|
44931
45962
|
var GIT_TIMEOUT_MS = 3e4;
|
|
44932
45963
|
function isErrnoException(err) {
|
|
44933
45964
|
return err instanceof Error && "code" in err;
|
|
44934
45965
|
}
|
|
44935
|
-
var
|
|
45966
|
+
var MAX_BUFFER2 = 10 * 1024 * 1024;
|
|
44936
45967
|
function runGit(args, cwd) {
|
|
44937
45968
|
return new Promise((resolve4, reject) => {
|
|
44938
|
-
|
|
45969
|
+
execFile3(
|
|
44939
45970
|
"git",
|
|
44940
45971
|
args,
|
|
44941
|
-
{ timeout: GIT_TIMEOUT_MS, cwd, maxBuffer:
|
|
45972
|
+
{ timeout: GIT_TIMEOUT_MS, cwd, maxBuffer: MAX_BUFFER2 },
|
|
44942
45973
|
(error2, stdout) => {
|
|
44943
45974
|
if (error2) reject(error2);
|
|
44944
45975
|
else resolve4(stdout.trim());
|
|
@@ -45017,9 +46048,9 @@ async function copyIgnoredFiles(sourceRepoPath, worktreePath) {
|
|
|
45017
46048
|
const files = ignoredOutput.split("\n").filter((f) => f && !shouldExclude(f));
|
|
45018
46049
|
for (const file of files) {
|
|
45019
46050
|
try {
|
|
45020
|
-
const destPath =
|
|
45021
|
-
await mkdir2(
|
|
45022
|
-
await copyFile(
|
|
46051
|
+
const destPath = join7(worktreePath, file);
|
|
46052
|
+
await mkdir2(dirname4(destPath), { recursive: true });
|
|
46053
|
+
await copyFile(join7(sourceRepoPath, file), destPath);
|
|
45023
46054
|
} catch (err) {
|
|
45024
46055
|
const msg = err instanceof Error ? err.message : String(err);
|
|
45025
46056
|
warnings.push(`Failed to copy ${file}: ${msg}`);
|
|
@@ -45029,9 +46060,9 @@ async function copyIgnoredFiles(sourceRepoPath, worktreePath) {
|
|
|
45029
46060
|
}
|
|
45030
46061
|
async function launchSetupScript(worktreePath, setupScript) {
|
|
45031
46062
|
if (!setupScript) return null;
|
|
45032
|
-
const shipyardDir =
|
|
45033
|
-
const scriptPath =
|
|
45034
|
-
const logPath =
|
|
46063
|
+
const shipyardDir = join7(worktreePath, ".shipyard");
|
|
46064
|
+
const scriptPath = join7(shipyardDir, "worktree-setup.sh");
|
|
46065
|
+
const logPath = join7(shipyardDir, "worktree-setup.log");
|
|
45035
46066
|
let fullScript;
|
|
45036
46067
|
if (setupScript.startsWith("#!")) {
|
|
45037
46068
|
const firstNewline = setupScript.indexOf("\n");
|
|
@@ -45063,7 +46094,7 @@ async function createWorktree(opts) {
|
|
|
45063
46094
|
const { sourceRepoPath, branchName, baseRef, setupScript, onProgress } = opts;
|
|
45064
46095
|
validateWorktreeInputs(sourceRepoPath, branchName, baseRef);
|
|
45065
46096
|
const worktreeParent = `${sourceRepoPath}-wt`;
|
|
45066
|
-
const worktreePath =
|
|
46097
|
+
const worktreePath = join7(worktreeParent, branchName);
|
|
45067
46098
|
onProgress("creating-worktree", `Creating worktree at ${worktreePath}`);
|
|
45068
46099
|
await mkdir2(worktreeParent, { recursive: true });
|
|
45069
46100
|
await assertWorktreeNotExists(worktreePath);
|
|
@@ -45119,7 +46150,7 @@ function validateWorktreeInputs(sourceRepoPath, branchName, baseRef) {
|
|
|
45119
46150
|
}
|
|
45120
46151
|
|
|
45121
46152
|
// src/serve.ts
|
|
45122
|
-
function
|
|
46153
|
+
function assertNever3(x) {
|
|
45123
46154
|
throw new Error(`Unhandled message type: ${JSON.stringify(x)}`);
|
|
45124
46155
|
}
|
|
45125
46156
|
var processedRequestIds = /* @__PURE__ */ new Set();
|
|
@@ -45131,10 +46162,71 @@ function scheduleEphemeralCleanup(fn, delayMs) {
|
|
|
45131
46162
|
}, delayMs);
|
|
45132
46163
|
pendingCleanupTimers.add(timer);
|
|
45133
46164
|
}
|
|
45134
|
-
|
|
46165
|
+
function parseFileChannelMessage(raw) {
|
|
46166
|
+
if (!raw.startsWith(FILE_IO_CONTROL_PREFIX)) return null;
|
|
46167
|
+
try {
|
|
46168
|
+
const msg = JSON.parse(raw.slice(FILE_IO_CONTROL_PREFIX.length));
|
|
46169
|
+
if (typeof msg.type !== "string" || typeof msg.requestId !== "string") {
|
|
46170
|
+
return null;
|
|
46171
|
+
}
|
|
46172
|
+
if (msg.type === "read") {
|
|
46173
|
+
if (typeof msg.path !== "string") return null;
|
|
46174
|
+
return {
|
|
46175
|
+
type: "read",
|
|
46176
|
+
requestId: msg.requestId,
|
|
46177
|
+
path: msg.path,
|
|
46178
|
+
envPath: typeof msg.envPath === "string" ? msg.envPath : null
|
|
46179
|
+
};
|
|
46180
|
+
}
|
|
46181
|
+
if (msg.type === "list-dir") {
|
|
46182
|
+
return {
|
|
46183
|
+
type: "list-dir",
|
|
46184
|
+
requestId: msg.requestId,
|
|
46185
|
+
envPath: typeof msg.envPath === "string" ? msg.envPath : null
|
|
46186
|
+
};
|
|
46187
|
+
}
|
|
46188
|
+
return null;
|
|
46189
|
+
} catch {
|
|
46190
|
+
return null;
|
|
46191
|
+
}
|
|
46192
|
+
}
|
|
45135
46193
|
var TERMINAL_BUFFER_MAX_BYTES = 1048576;
|
|
45136
46194
|
var TERMINAL_OPEN_TIMEOUT_MS = 1e4;
|
|
45137
46195
|
var TERMINAL_CWD_TIMEOUT_MS = 5e3;
|
|
46196
|
+
var TERMINAL_MAX_PTYS = 20;
|
|
46197
|
+
function handleTerminalInput(event, ptyMgr, termLog) {
|
|
46198
|
+
const raw = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
|
|
46199
|
+
if (raw.startsWith(TERMINAL_CONTROL_PREFIX)) {
|
|
46200
|
+
try {
|
|
46201
|
+
const ctrl = JSON.parse(raw.slice(TERMINAL_CONTROL_PREFIX.length));
|
|
46202
|
+
if (ctrl.type === "resize" && typeof ctrl.cols === "number" && typeof ctrl.rows === "number") {
|
|
46203
|
+
ptyMgr.resize(ctrl.cols, ctrl.rows);
|
|
46204
|
+
}
|
|
46205
|
+
} catch {
|
|
46206
|
+
termLog.warn("Invalid control message");
|
|
46207
|
+
}
|
|
46208
|
+
} else {
|
|
46209
|
+
try {
|
|
46210
|
+
ptyMgr.write(raw);
|
|
46211
|
+
} catch {
|
|
46212
|
+
}
|
|
46213
|
+
}
|
|
46214
|
+
}
|
|
46215
|
+
function sweepDeadPtys(ptys, roomHandle) {
|
|
46216
|
+
let cleaned = 0;
|
|
46217
|
+
for (const [key, pty2] of ptys) {
|
|
46218
|
+
if (!pty2.alive) {
|
|
46219
|
+
pty2.dispose();
|
|
46220
|
+
ptys.delete(key);
|
|
46221
|
+
try {
|
|
46222
|
+
sync(roomHandle).terminalSessions.delete(key);
|
|
46223
|
+
} catch {
|
|
46224
|
+
}
|
|
46225
|
+
cleaned++;
|
|
46226
|
+
}
|
|
46227
|
+
}
|
|
46228
|
+
return cleaned;
|
|
46229
|
+
}
|
|
45138
46230
|
var TERMINAL_STATUSES = new Set(TERMINAL_TASK_STATES);
|
|
45139
46231
|
async function rehydrateTaskDocuments(roomHandle, roomDoc, repo, log) {
|
|
45140
46232
|
try {
|
|
@@ -45168,7 +46260,7 @@ async function rehydrateTaskDocuments(roomHandle, roomDoc, repo, log) {
|
|
|
45168
46260
|
review: reviewHandle
|
|
45169
46261
|
};
|
|
45170
46262
|
if (recoverOrphanedTask(taskDocs, createChildLogger({ mode: "rehydrate", taskId }))) {
|
|
45171
|
-
updateTaskInIndex(roomDoc, taskId, { status: "
|
|
46263
|
+
updateTaskInIndex(roomDoc, taskId, { status: "input-required", updatedAt: Date.now() });
|
|
45172
46264
|
}
|
|
45173
46265
|
log.debug({ taskId }, "Task documents rehydrated");
|
|
45174
46266
|
} catch (err) {
|
|
@@ -45182,6 +46274,9 @@ async function serve(env) {
|
|
|
45182
46274
|
logger.error("SHIPYARD_SIGNALING_URL is required for serve mode");
|
|
45183
46275
|
process.exit(1);
|
|
45184
46276
|
}
|
|
46277
|
+
if (!env.SHIPYARD_USER_ID) {
|
|
46278
|
+
throw new Error("SHIPYARD_USER_ID is required. Run `shipyard login` first.");
|
|
46279
|
+
}
|
|
45185
46280
|
const log = createChildLogger({ mode: "serve" });
|
|
45186
46281
|
if (env.SHIPYARD_USER_TOKEN && env.SHIPYARD_SIGNALING_URL) {
|
|
45187
46282
|
const client = new SignalingClient(env.SHIPYARD_SIGNALING_URL);
|
|
@@ -45200,7 +46295,7 @@ async function serve(env) {
|
|
|
45200
46295
|
}
|
|
45201
46296
|
const lifecycle = new LifecycleManager();
|
|
45202
46297
|
await lifecycle.acquirePidFile(getShipyardHome());
|
|
45203
|
-
const handle = await createSignalingHandle(env, log);
|
|
46298
|
+
const handle = await createSignalingHandle(env, log, getShipyardHome());
|
|
45204
46299
|
if (!handle) {
|
|
45205
46300
|
logger.error("SHIPYARD_SIGNALING_URL is required for serve mode");
|
|
45206
46301
|
process.exit(1);
|
|
@@ -45223,6 +46318,8 @@ async function serve(env) {
|
|
|
45223
46318
|
});
|
|
45224
46319
|
const keepAwakeManager = new KeepAwakeManager(log);
|
|
45225
46320
|
const terminalPtys = /* @__PURE__ */ new Map();
|
|
46321
|
+
const FILE_MAX_SIZE = 2e5;
|
|
46322
|
+
const collabRoomManager = createCollabRoomManager();
|
|
45226
46323
|
const peerManager = createPeerManager({
|
|
45227
46324
|
webrtcAdapter,
|
|
45228
46325
|
onAnswer(targetMachineId, answer) {
|
|
@@ -45239,22 +46336,144 @@ async function serve(env) {
|
|
|
45239
46336
|
candidate
|
|
45240
46337
|
});
|
|
45241
46338
|
},
|
|
45242
|
-
onTerminalChannel(fromMachineId, rawChannel, taskId) {
|
|
46339
|
+
onTerminalChannel(fromMachineId, rawChannel, taskId, terminalId) {
|
|
45243
46340
|
const channel = rawChannel;
|
|
45244
|
-
const terminalKey = `${fromMachineId}:${taskId}`;
|
|
45245
|
-
const termLog = createChildLogger({
|
|
46341
|
+
const terminalKey = `${fromMachineId}:${taskId}:${terminalId}`;
|
|
46342
|
+
const termLog = createChildLogger({
|
|
46343
|
+
mode: `terminal:${fromMachineId}:${taskId}:${terminalId}`
|
|
46344
|
+
});
|
|
46345
|
+
function publishTerminalSession(cwd, status, exitCode3) {
|
|
46346
|
+
try {
|
|
46347
|
+
sync(roomHandle).terminalSessions.set(terminalKey, {
|
|
46348
|
+
terminalId,
|
|
46349
|
+
taskId,
|
|
46350
|
+
machineId,
|
|
46351
|
+
cwd,
|
|
46352
|
+
status,
|
|
46353
|
+
exitCode: exitCode3,
|
|
46354
|
+
createdAt: Date.now()
|
|
46355
|
+
});
|
|
46356
|
+
} catch {
|
|
46357
|
+
termLog.warn("Failed to publish terminal session to ephemeral");
|
|
46358
|
+
}
|
|
46359
|
+
}
|
|
46360
|
+
function deleteTerminalSession() {
|
|
46361
|
+
try {
|
|
46362
|
+
sync(roomHandle).terminalSessions.delete(terminalKey);
|
|
46363
|
+
} catch {
|
|
46364
|
+
}
|
|
46365
|
+
}
|
|
45246
46366
|
const existingPty = terminalPtys.get(terminalKey);
|
|
45247
|
-
if (existingPty) {
|
|
45248
|
-
termLog.info("
|
|
46367
|
+
if (existingPty?.alive) {
|
|
46368
|
+
termLog.info({ pid: existingPty.pid }, "Reattaching channel to existing PTY");
|
|
46369
|
+
const buf2 = createChannelBuffer(channel, {
|
|
46370
|
+
maxBytes: TERMINAL_BUFFER_MAX_BYTES,
|
|
46371
|
+
logPrefix: `terminal-reattach:${taskId}`
|
|
46372
|
+
});
|
|
46373
|
+
const openTimeout2 = setTimeout(() => {
|
|
46374
|
+
if (!buf2.isOpen) {
|
|
46375
|
+
termLog.warn("Reattach channel did not open within timeout");
|
|
46376
|
+
existingPty.setDataSink(null);
|
|
46377
|
+
existingPty.setExitSink(null);
|
|
46378
|
+
if (channel.readyState !== "closed") channel.close();
|
|
46379
|
+
}
|
|
46380
|
+
}, TERMINAL_OPEN_TIMEOUT_MS);
|
|
46381
|
+
const scrollbackSnapshot = existingPty.getScrollback();
|
|
46382
|
+
const dcAsEventTarget2 = rawChannel;
|
|
46383
|
+
dcAsEventTarget2.addEventListener("open", () => {
|
|
46384
|
+
termLog.info("Reattach channel open, replaying scrollback");
|
|
46385
|
+
buf2.markOpen();
|
|
46386
|
+
clearTimeout(openTimeout2);
|
|
46387
|
+
if (scrollbackSnapshot) {
|
|
46388
|
+
buf2.sendOrBuffer(scrollbackSnapshot);
|
|
46389
|
+
}
|
|
46390
|
+
buf2.flush();
|
|
46391
|
+
});
|
|
46392
|
+
existingPty.setDataSink((data) => buf2.sendOrBuffer(data));
|
|
46393
|
+
const existingCwd = (() => {
|
|
46394
|
+
try {
|
|
46395
|
+
const all = sync(roomHandle).terminalSessions.getAll();
|
|
46396
|
+
return all.get(terminalKey)?.cwd ?? "";
|
|
46397
|
+
} catch {
|
|
46398
|
+
return "";
|
|
46399
|
+
}
|
|
46400
|
+
})();
|
|
46401
|
+
existingPty.setExitSink((exitCode3, signal) => {
|
|
46402
|
+
termLog.info({ exitCode: exitCode3, signal }, "PTY exited (reattached)");
|
|
46403
|
+
publishTerminalSession(existingCwd, "exited", exitCode3);
|
|
46404
|
+
if (buf2.isOpen) {
|
|
46405
|
+
buf2.sendOrBuffer(
|
|
46406
|
+
TERMINAL_CONTROL_PREFIX + JSON.stringify({ type: "exited", exitCode: exitCode3, signal })
|
|
46407
|
+
);
|
|
46408
|
+
}
|
|
46409
|
+
});
|
|
46410
|
+
channel.onmessage = (event) => {
|
|
46411
|
+
handleTerminalInput(event, existingPty, termLog);
|
|
46412
|
+
};
|
|
46413
|
+
channel.onclose = () => {
|
|
46414
|
+
termLog.info("Reattached channel closed, detaching sink (PTY persists)");
|
|
46415
|
+
buf2.reset();
|
|
46416
|
+
clearTimeout(openTimeout2);
|
|
46417
|
+
existingPty.setDataSink(null);
|
|
46418
|
+
existingPty.setExitSink((exitCode3, signal) => {
|
|
46419
|
+
termLog.info({ exitCode: exitCode3, signal }, "PTY exited while detached (reattach)");
|
|
46420
|
+
publishTerminalSession(existingCwd, "exited", exitCode3);
|
|
46421
|
+
});
|
|
46422
|
+
};
|
|
46423
|
+
return;
|
|
46424
|
+
}
|
|
46425
|
+
if (existingPty && !existingPty.alive) {
|
|
46426
|
+
termLog.info("Cleaning up exited PTY for key");
|
|
45249
46427
|
existingPty.dispose();
|
|
45250
46428
|
terminalPtys.delete(terminalKey);
|
|
46429
|
+
deleteTerminalSession();
|
|
46430
|
+
}
|
|
46431
|
+
if (terminalPtys.size >= TERMINAL_MAX_PTYS) {
|
|
46432
|
+
sweepDeadPtys(terminalPtys, roomHandle);
|
|
46433
|
+
}
|
|
46434
|
+
if (terminalPtys.size >= TERMINAL_MAX_PTYS) {
|
|
46435
|
+
termLog.info(
|
|
46436
|
+
{ limit: TERMINAL_MAX_PTYS, current: terminalPtys.size },
|
|
46437
|
+
"PTY limit reached, rejecting channel"
|
|
46438
|
+
);
|
|
46439
|
+
const reject = () => {
|
|
46440
|
+
try {
|
|
46441
|
+
channel.send(
|
|
46442
|
+
TERMINAL_CONTROL_PREFIX + JSON.stringify({ type: "error", reason: "pty_limit_reached" })
|
|
46443
|
+
);
|
|
46444
|
+
} catch {
|
|
46445
|
+
}
|
|
46446
|
+
channel.close();
|
|
46447
|
+
};
|
|
46448
|
+
if (channel.readyState === "open") {
|
|
46449
|
+
reject();
|
|
46450
|
+
} else {
|
|
46451
|
+
const dcTarget = rawChannel;
|
|
46452
|
+
const rejectTimer = setTimeout(() => {
|
|
46453
|
+
if (channel.readyState !== "open" && channel.readyState !== "closed") {
|
|
46454
|
+
channel.close();
|
|
46455
|
+
}
|
|
46456
|
+
}, TERMINAL_OPEN_TIMEOUT_MS);
|
|
46457
|
+
dcTarget.addEventListener(
|
|
46458
|
+
"open",
|
|
46459
|
+
() => {
|
|
46460
|
+
clearTimeout(rejectTimer);
|
|
46461
|
+
reject();
|
|
46462
|
+
},
|
|
46463
|
+
{ once: true }
|
|
46464
|
+
);
|
|
46465
|
+
}
|
|
46466
|
+
return;
|
|
45251
46467
|
}
|
|
45252
46468
|
const ptyManager = createPtyManager();
|
|
45253
46469
|
terminalPtys.set(terminalKey, ptyManager);
|
|
45254
46470
|
let ptySpawned = false;
|
|
45255
|
-
let
|
|
45256
|
-
const
|
|
45257
|
-
|
|
46471
|
+
let terminalCwd = "";
|
|
46472
|
+
const buf = createChannelBuffer(channel, {
|
|
46473
|
+
maxBytes: TERMINAL_BUFFER_MAX_BYTES,
|
|
46474
|
+
logPrefix: `terminal-new:${taskId}`,
|
|
46475
|
+
onOverflow: () => disposeAndClose("Pending buffer exceeded max size")
|
|
46476
|
+
});
|
|
45258
46477
|
const preSpawnInputBuffer = [];
|
|
45259
46478
|
function disposeAndClose(reason) {
|
|
45260
46479
|
termLog.warn({ reason }, "Disposing terminal");
|
|
@@ -45262,51 +46481,36 @@ async function serve(env) {
|
|
|
45262
46481
|
clearTimeout(cwdTimeout);
|
|
45263
46482
|
ptyManager.dispose();
|
|
45264
46483
|
terminalPtys.delete(terminalKey);
|
|
46484
|
+
deleteTerminalSession();
|
|
45265
46485
|
if (channel.readyState === "open") {
|
|
45266
46486
|
channel.close();
|
|
45267
46487
|
}
|
|
45268
46488
|
}
|
|
45269
|
-
function flushPendingBuffer() {
|
|
45270
|
-
for (const chunk of pendingBuffer) {
|
|
45271
|
-
try {
|
|
45272
|
-
channel.send(chunk);
|
|
45273
|
-
} catch {
|
|
45274
|
-
}
|
|
45275
|
-
}
|
|
45276
|
-
pendingBuffer.length = 0;
|
|
45277
|
-
pendingBufferBytes = 0;
|
|
45278
|
-
}
|
|
45279
|
-
function sendOrBuffer(data) {
|
|
45280
|
-
if (channelOpen) {
|
|
45281
|
-
try {
|
|
45282
|
-
channel.send(data);
|
|
45283
|
-
} catch {
|
|
45284
|
-
}
|
|
45285
|
-
} else {
|
|
45286
|
-
const byteLen = Buffer.byteLength(data);
|
|
45287
|
-
if (pendingBufferBytes + byteLen > TERMINAL_BUFFER_MAX_BYTES) {
|
|
45288
|
-
disposeAndClose("Pending buffer exceeded max size");
|
|
45289
|
-
return;
|
|
45290
|
-
}
|
|
45291
|
-
pendingBuffer.push(data);
|
|
45292
|
-
pendingBufferBytes += byteLen;
|
|
45293
|
-
}
|
|
45294
|
-
}
|
|
45295
46489
|
const openTimeout = setTimeout(() => {
|
|
45296
|
-
if (!
|
|
46490
|
+
if (!buf.isOpen) {
|
|
45297
46491
|
disposeAndClose("Data channel did not open within timeout");
|
|
45298
46492
|
}
|
|
45299
46493
|
}, TERMINAL_OPEN_TIMEOUT_MS);
|
|
45300
46494
|
const dcAsEventTarget = rawChannel;
|
|
45301
46495
|
dcAsEventTarget.addEventListener("open", () => {
|
|
45302
46496
|
termLog.info("Terminal data channel now open, flushing buffered output");
|
|
45303
|
-
|
|
46497
|
+
buf.markOpen();
|
|
45304
46498
|
clearTimeout(openTimeout);
|
|
45305
|
-
|
|
46499
|
+
buf.flush();
|
|
45306
46500
|
});
|
|
46501
|
+
function validateCwd(path) {
|
|
46502
|
+
try {
|
|
46503
|
+
if (!existsSync(path)) return null;
|
|
46504
|
+
if (!statSync(path).isDirectory()) return null;
|
|
46505
|
+
return path;
|
|
46506
|
+
} catch {
|
|
46507
|
+
return null;
|
|
46508
|
+
}
|
|
46509
|
+
}
|
|
45307
46510
|
function spawnPty(cwd) {
|
|
45308
46511
|
if (ptySpawned) return;
|
|
45309
46512
|
ptySpawned = true;
|
|
46513
|
+
terminalCwd = cwd;
|
|
45310
46514
|
clearTimeout(cwdTimeout);
|
|
45311
46515
|
try {
|
|
45312
46516
|
ptyManager.spawn({ cwd });
|
|
@@ -45316,15 +46520,16 @@ async function serve(env) {
|
|
|
45316
46520
|
terminalPtys.delete(terminalKey);
|
|
45317
46521
|
return;
|
|
45318
46522
|
}
|
|
45319
|
-
|
|
45320
|
-
|
|
45321
|
-
|
|
45322
|
-
ptyManager.onExit((exitCode3, signal) => {
|
|
46523
|
+
publishTerminalSession(cwd, "running", null);
|
|
46524
|
+
ptyManager.setDataSink((data) => buf.sendOrBuffer(data));
|
|
46525
|
+
ptyManager.setExitSink((exitCode3, signal) => {
|
|
45323
46526
|
termLog.info({ exitCode: exitCode3, signal }, "Terminal PTY exited");
|
|
45324
|
-
|
|
45325
|
-
|
|
46527
|
+
publishTerminalSession(cwd, "exited", exitCode3);
|
|
46528
|
+
if (buf.isOpen) {
|
|
46529
|
+
buf.sendOrBuffer(
|
|
46530
|
+
TERMINAL_CONTROL_PREFIX + JSON.stringify({ type: "exited", exitCode: exitCode3, signal })
|
|
46531
|
+
);
|
|
45326
46532
|
}
|
|
45327
|
-
terminalPtys.delete(terminalKey);
|
|
45328
46533
|
});
|
|
45329
46534
|
for (const input of preSpawnInputBuffer) {
|
|
45330
46535
|
try {
|
|
@@ -45334,7 +46539,7 @@ async function serve(env) {
|
|
|
45334
46539
|
}
|
|
45335
46540
|
preSpawnInputBuffer.length = 0;
|
|
45336
46541
|
termLog.info(
|
|
45337
|
-
{ cwd, pid: ptyManager.pid, channelReady:
|
|
46542
|
+
{ cwd, pid: ptyManager.pid, channelReady: buf.isOpen },
|
|
45338
46543
|
"Terminal PTY wired to data channel"
|
|
45339
46544
|
);
|
|
45340
46545
|
}
|
|
@@ -45349,7 +46554,13 @@ async function serve(env) {
|
|
|
45349
46554
|
try {
|
|
45350
46555
|
const ctrl = JSON.parse(payload);
|
|
45351
46556
|
if (ctrl.type === "cwd" && typeof ctrl.path === "string") {
|
|
45352
|
-
|
|
46557
|
+
const validCwd = validateCwd(ctrl.path);
|
|
46558
|
+
if (validCwd) {
|
|
46559
|
+
spawnPty(validCwd);
|
|
46560
|
+
} else {
|
|
46561
|
+
termLog.warn({ path: ctrl.path }, "Invalid cwd path, falling back to $HOME");
|
|
46562
|
+
spawnPty(process.env.HOME ?? process.cwd());
|
|
46563
|
+
}
|
|
45353
46564
|
} else if (ctrl.type === "resize" && typeof ctrl.cols === "number" && typeof ctrl.rows === "number" && ptySpawned) {
|
|
45354
46565
|
ptyManager.resize(ctrl.cols, ctrl.rows);
|
|
45355
46566
|
}
|
|
@@ -45369,25 +46580,73 @@ async function serve(env) {
|
|
|
45369
46580
|
}
|
|
45370
46581
|
channel.onmessage = (event) => {
|
|
45371
46582
|
const raw = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
|
|
45372
|
-
if (raw.startsWith(
|
|
45373
|
-
handleControlMessage(raw.slice(
|
|
46583
|
+
if (raw.startsWith(TERMINAL_CONTROL_PREFIX)) {
|
|
46584
|
+
handleControlMessage(raw.slice(TERMINAL_CONTROL_PREFIX.length));
|
|
45374
46585
|
} else {
|
|
45375
46586
|
handleDataInput(raw);
|
|
45376
46587
|
}
|
|
45377
46588
|
};
|
|
45378
46589
|
channel.onclose = () => {
|
|
45379
|
-
termLog.info("Terminal data channel closed");
|
|
45380
|
-
|
|
46590
|
+
termLog.info("Terminal data channel closed, detaching sink (PTY persists)");
|
|
46591
|
+
buf.reset();
|
|
45381
46592
|
clearTimeout(openTimeout);
|
|
45382
46593
|
clearTimeout(cwdTimeout);
|
|
45383
|
-
ptyManager.
|
|
45384
|
-
|
|
45385
|
-
|
|
46594
|
+
ptyManager.setDataSink(null);
|
|
46595
|
+
ptyManager.setExitSink((exitCode3, signal) => {
|
|
46596
|
+
termLog.info({ exitCode: exitCode3, signal }, "PTY exited while detached");
|
|
46597
|
+
publishTerminalSession(terminalCwd, "exited", exitCode3);
|
|
46598
|
+
});
|
|
46599
|
+
};
|
|
46600
|
+
},
|
|
46601
|
+
onFileChannel(_fromMachineId, rawChannel, _channelId) {
|
|
46602
|
+
const channel = rawChannel;
|
|
46603
|
+
const fileLog = createChildLogger({ mode: "file-channel" });
|
|
46604
|
+
const FILE_CHANNEL_BUFFER_MAX_BYTES = 5 * 1024 * 1024;
|
|
46605
|
+
const buf = createChannelBuffer(channel, {
|
|
46606
|
+
maxBytes: FILE_CHANNEL_BUFFER_MAX_BYTES,
|
|
46607
|
+
logPrefix: "file-channel"
|
|
46608
|
+
});
|
|
46609
|
+
if (channel.readyState === "open") {
|
|
46610
|
+
buf.markOpen();
|
|
46611
|
+
} else {
|
|
46612
|
+
const dcAsEventTarget = rawChannel;
|
|
46613
|
+
dcAsEventTarget.addEventListener("open", () => {
|
|
46614
|
+
fileLog.info("File I/O data channel now open");
|
|
46615
|
+
buf.markOpen();
|
|
46616
|
+
buf.flush();
|
|
46617
|
+
});
|
|
46618
|
+
}
|
|
46619
|
+
const bufferedChannel = {
|
|
46620
|
+
send(data) {
|
|
46621
|
+
buf.sendOrBuffer(data);
|
|
46622
|
+
}
|
|
46623
|
+
};
|
|
46624
|
+
channel.onclose = () => {
|
|
46625
|
+
fileLog.info("File I/O channel closed");
|
|
46626
|
+
};
|
|
46627
|
+
channel.onmessage = (event) => {
|
|
46628
|
+
const raw = typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data);
|
|
46629
|
+
try {
|
|
46630
|
+
const msg = parseFileChannelMessage(raw);
|
|
46631
|
+
if (!msg) return;
|
|
46632
|
+
if (msg.type === "read") {
|
|
46633
|
+
handleFileRead(msg.requestId, msg.path, msg.envPath, bufferedChannel).catch((err) => {
|
|
46634
|
+
fileLog.warn({ err }, "Unhandled error in handleFileRead");
|
|
46635
|
+
});
|
|
46636
|
+
} else if (msg.type === "list-dir") {
|
|
46637
|
+
handleListDir(msg.requestId, msg.envPath, bufferedChannel).catch((err) => {
|
|
46638
|
+
fileLog.warn({ err }, "Unhandled error in handleListDir");
|
|
46639
|
+
});
|
|
46640
|
+
} else {
|
|
46641
|
+
assertNever3(msg);
|
|
46642
|
+
}
|
|
46643
|
+
} catch (err) {
|
|
46644
|
+
fileLog.warn({ err }, "File channel message handling failed");
|
|
45386
46645
|
}
|
|
45387
46646
|
};
|
|
45388
46647
|
}
|
|
45389
46648
|
});
|
|
45390
|
-
const roomDocId =
|
|
46649
|
+
const roomDocId = buildRoomDocId(env.SHIPYARD_USER_ID, DEFAULT_EPOCH);
|
|
45391
46650
|
const roomHandle = repo.get(roomDocId, TaskIndexDocumentSchema, ROOM_EPHEMERAL_DECLARATIONS);
|
|
45392
46651
|
function publishCapabilities(caps) {
|
|
45393
46652
|
const value = {
|
|
@@ -45410,6 +46669,128 @@ async function serve(env) {
|
|
|
45410
46669
|
sync(roomHandle).capabilities.set(machineId, value);
|
|
45411
46670
|
log.info({ machineId }, "Published capabilities to room ephemeral");
|
|
45412
46671
|
}
|
|
46672
|
+
async function handleFileRead(requestId, filePath, envPath, channel) {
|
|
46673
|
+
function sendResponse(payload) {
|
|
46674
|
+
try {
|
|
46675
|
+
channel.send(FILE_IO_CONTROL_PREFIX + JSON.stringify(payload));
|
|
46676
|
+
} catch (err) {
|
|
46677
|
+
log.warn(
|
|
46678
|
+
{ err, payload: { type: payload.type, requestId: payload.requestId } },
|
|
46679
|
+
"Failed to send file-io response"
|
|
46680
|
+
);
|
|
46681
|
+
}
|
|
46682
|
+
}
|
|
46683
|
+
const workspaceRoot = envPath ?? capabilities.environments[0]?.path ?? process.cwd();
|
|
46684
|
+
const absolutePath = resolve2(workspaceRoot, filePath);
|
|
46685
|
+
const relativePath = relative(workspaceRoot, absolutePath);
|
|
46686
|
+
if (relativePath.startsWith("..") || isAbsolute3(relativePath)) {
|
|
46687
|
+
sendResponse({ type: "error", requestId, path: filePath, error: "Path traversal denied" });
|
|
46688
|
+
return;
|
|
46689
|
+
}
|
|
46690
|
+
try {
|
|
46691
|
+
const fileStat = await stat2(absolutePath);
|
|
46692
|
+
if (fileStat.size > FILE_MAX_SIZE) {
|
|
46693
|
+
sendResponse({
|
|
46694
|
+
type: "error",
|
|
46695
|
+
requestId,
|
|
46696
|
+
path: filePath,
|
|
46697
|
+
error: `File too large (${(fileStat.size / 1024).toFixed(0)}KB, limit 200KB)`
|
|
46698
|
+
});
|
|
46699
|
+
return;
|
|
46700
|
+
}
|
|
46701
|
+
const buffer = await readFile5(absolutePath);
|
|
46702
|
+
if (buffer.subarray(0, 8192).includes(0)) {
|
|
46703
|
+
sendResponse({
|
|
46704
|
+
type: "error",
|
|
46705
|
+
requestId,
|
|
46706
|
+
path: filePath,
|
|
46707
|
+
error: "Binary file \u2014 cannot display"
|
|
46708
|
+
});
|
|
46709
|
+
return;
|
|
46710
|
+
}
|
|
46711
|
+
sendResponse({
|
|
46712
|
+
type: "content",
|
|
46713
|
+
requestId,
|
|
46714
|
+
path: filePath,
|
|
46715
|
+
content: buffer.toString("utf-8")
|
|
46716
|
+
});
|
|
46717
|
+
} catch (err) {
|
|
46718
|
+
sendResponse({
|
|
46719
|
+
type: "error",
|
|
46720
|
+
requestId,
|
|
46721
|
+
path: filePath,
|
|
46722
|
+
error: `Read failed: ${err instanceof Error ? err.message : String(err)}`
|
|
46723
|
+
});
|
|
46724
|
+
}
|
|
46725
|
+
}
|
|
46726
|
+
async function handleListDir(requestId, envPath, channel) {
|
|
46727
|
+
function sendResponse(payload) {
|
|
46728
|
+
try {
|
|
46729
|
+
channel.send(FILE_IO_CONTROL_PREFIX + JSON.stringify(payload));
|
|
46730
|
+
} catch (err) {
|
|
46731
|
+
log.warn(
|
|
46732
|
+
{ err, payload: { type: payload.type, requestId: payload.requestId } },
|
|
46733
|
+
"Failed to send file-io response"
|
|
46734
|
+
);
|
|
46735
|
+
}
|
|
46736
|
+
}
|
|
46737
|
+
const resolvedPath = envPath ?? capabilities.environments[0]?.path;
|
|
46738
|
+
if (!resolvedPath) {
|
|
46739
|
+
sendResponse({ type: "error", requestId, error: "No environment path" });
|
|
46740
|
+
return;
|
|
46741
|
+
}
|
|
46742
|
+
const validPaths = new Set(capabilities.environments.map((e) => e.path));
|
|
46743
|
+
if (!validPaths.has(resolvedPath)) {
|
|
46744
|
+
sendResponse({ type: "error", requestId, error: "Invalid environment path" });
|
|
46745
|
+
return;
|
|
46746
|
+
}
|
|
46747
|
+
try {
|
|
46748
|
+
const files = await listWorkspaceFiles(resolvedPath);
|
|
46749
|
+
sendDirContentsResponse(requestId, files, channel);
|
|
46750
|
+
} catch (err) {
|
|
46751
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
46752
|
+
sendResponse({ type: "error", requestId, error: msg });
|
|
46753
|
+
}
|
|
46754
|
+
}
|
|
46755
|
+
const MAX_CHUNK_FILES = 500;
|
|
46756
|
+
function sendDirContentsResponse(requestId, files, channel) {
|
|
46757
|
+
if (files.length <= MAX_CHUNK_FILES) {
|
|
46758
|
+
const serialized = JSON.stringify({ type: "dir-contents", requestId, files });
|
|
46759
|
+
if (serialized.length > 2e5) {
|
|
46760
|
+
log.debug({ requestId, size: serialized.length }, "dir-contents response exceeds 200KB");
|
|
46761
|
+
}
|
|
46762
|
+
try {
|
|
46763
|
+
channel.send(FILE_IO_CONTROL_PREFIX + serialized);
|
|
46764
|
+
} catch (err) {
|
|
46765
|
+
log.warn(
|
|
46766
|
+
{ err, payload: { type: "dir-contents", requestId } },
|
|
46767
|
+
"Failed to send file-io response"
|
|
46768
|
+
);
|
|
46769
|
+
}
|
|
46770
|
+
return;
|
|
46771
|
+
}
|
|
46772
|
+
const totalChunks = Math.ceil(files.length / MAX_CHUNK_FILES);
|
|
46773
|
+
log.debug(
|
|
46774
|
+
{ requestId, fileCount: files.length, totalChunks },
|
|
46775
|
+
"Chunking dir-contents response"
|
|
46776
|
+
);
|
|
46777
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
46778
|
+
const chunk = files.slice(i * MAX_CHUNK_FILES, (i + 1) * MAX_CHUNK_FILES);
|
|
46779
|
+
const serialized = JSON.stringify({
|
|
46780
|
+
type: "dir-contents-chunk",
|
|
46781
|
+
requestId,
|
|
46782
|
+
files: chunk,
|
|
46783
|
+
chunkIndex: i,
|
|
46784
|
+
totalChunks
|
|
46785
|
+
});
|
|
46786
|
+
try {
|
|
46787
|
+
channel.send(FILE_IO_CONTROL_PREFIX + serialized);
|
|
46788
|
+
} catch (err) {
|
|
46789
|
+
log.warn({ requestId, chunkIndex: i, err }, "Failed to send dir-contents chunk");
|
|
46790
|
+
break;
|
|
46791
|
+
}
|
|
46792
|
+
}
|
|
46793
|
+
}
|
|
45413
46794
|
publishCapabilities(capabilities);
|
|
45414
46795
|
const branchWatcher = createBranchWatcher({
|
|
45415
46796
|
environments: capabilities.environments,
|
|
@@ -45431,6 +46812,7 @@ async function serve(env) {
|
|
|
45431
46812
|
keepAwakeManager.update(keepAwakeEnabled, activeTasks.size > 0);
|
|
45432
46813
|
}
|
|
45433
46814
|
});
|
|
46815
|
+
keepAwakeManager.update(keepAwakeEnabled, activeTasks.size > 0);
|
|
45434
46816
|
sync(typedRoomHandle).enhancePromptReqs.subscribe(({ key: requestId, value, source }) => {
|
|
45435
46817
|
if (source !== "remote") return;
|
|
45436
46818
|
if (!value) return;
|
|
@@ -45588,7 +46970,10 @@ async function serve(env) {
|
|
|
45588
46970
|
});
|
|
45589
46971
|
});
|
|
45590
46972
|
connection.onStateChange((state) => {
|
|
45591
|
-
log.info(
|
|
46973
|
+
log.info(
|
|
46974
|
+
{ state, activeTaskCount: activeTasks.size, activeTasks: [...activeTasks.keys()] },
|
|
46975
|
+
"Signaling connection state changed"
|
|
46976
|
+
);
|
|
45592
46977
|
});
|
|
45593
46978
|
const dispatchingTasks = /* @__PURE__ */ new Set();
|
|
45594
46979
|
connection.onMessage((msg) => {
|
|
@@ -45612,10 +46997,19 @@ async function serve(env) {
|
|
|
45612
46997
|
publishCapabilities,
|
|
45613
46998
|
branchWatcher,
|
|
45614
46999
|
keepAwakeManager,
|
|
45615
|
-
getKeepAwakeEnabled: () => keepAwakeEnabled
|
|
47000
|
+
getKeepAwakeEnabled: () => keepAwakeEnabled,
|
|
47001
|
+
collabRoomManager,
|
|
47002
|
+
webrtcAdapter
|
|
45616
47003
|
});
|
|
45617
47004
|
});
|
|
45618
47005
|
connection.connect();
|
|
47006
|
+
const DEAD_PTY_SWEEP_MS = 3e4;
|
|
47007
|
+
const deadPtySweep = setInterval(() => {
|
|
47008
|
+
const cleaned = sweepDeadPtys(terminalPtys, roomHandle);
|
|
47009
|
+
if (cleaned > 0) {
|
|
47010
|
+
log.info({ cleaned, remaining: terminalPtys.size }, "Dead PTY sweep completed");
|
|
47011
|
+
}
|
|
47012
|
+
}, DEAD_PTY_SWEEP_MS);
|
|
45619
47013
|
log.info("Daemon running in serve mode, waiting for tasks...");
|
|
45620
47014
|
lifecycle.onShutdown(async () => {
|
|
45621
47015
|
log.info("Shutting down serve mode...");
|
|
@@ -45628,14 +47022,8 @@ async function serve(env) {
|
|
|
45628
47022
|
}
|
|
45629
47023
|
activeTasks.clear();
|
|
45630
47024
|
dispatchingTasks.clear();
|
|
45631
|
-
|
|
45632
|
-
|
|
45633
|
-
}
|
|
45634
|
-
diffDebounceTimers.clear();
|
|
45635
|
-
for (const timer of branchDiffTimers.values()) {
|
|
45636
|
-
clearTimeout(timer);
|
|
45637
|
-
}
|
|
45638
|
-
branchDiffTimers.clear();
|
|
47025
|
+
clearAllTimers(diffDebounceTimers, branchDiffTimers, prPollTimers);
|
|
47026
|
+
prPollLastActivity.clear();
|
|
45639
47027
|
for (const unsub of watchedTasks.values()) {
|
|
45640
47028
|
unsub();
|
|
45641
47029
|
}
|
|
@@ -45644,10 +47032,16 @@ async function serve(env) {
|
|
|
45644
47032
|
lastProcessedConvLen.clear();
|
|
45645
47033
|
for (const timer of pendingCleanupTimers) clearTimeout(timer);
|
|
45646
47034
|
pendingCleanupTimers.clear();
|
|
47035
|
+
clearInterval(deadPtySweep);
|
|
45647
47036
|
for (const [id, ptyMgr] of terminalPtys) {
|
|
45648
47037
|
ptyMgr.dispose();
|
|
45649
|
-
|
|
47038
|
+
try {
|
|
47039
|
+
sync(roomHandle).terminalSessions.delete(id);
|
|
47040
|
+
} catch {
|
|
47041
|
+
}
|
|
45650
47042
|
}
|
|
47043
|
+
terminalPtys.clear();
|
|
47044
|
+
collabRoomManager.destroy();
|
|
45651
47045
|
peerManager.destroy();
|
|
45652
47046
|
signaling.unregister();
|
|
45653
47047
|
await new Promise((resolve4) => setTimeout(resolve4, 200));
|
|
@@ -45660,8 +47054,12 @@ async function serve(env) {
|
|
|
45660
47054
|
}
|
|
45661
47055
|
var DIFF_DEBOUNCE_MS = 2e3;
|
|
45662
47056
|
var BRANCH_DIFF_DEBOUNCE_MS = 1e4;
|
|
47057
|
+
var PR_POLL_INTERVAL_MS = 3e4;
|
|
47058
|
+
var PR_POLL_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
45663
47059
|
var diffDebounceTimers = /* @__PURE__ */ new Map();
|
|
45664
47060
|
var branchDiffTimers = /* @__PURE__ */ new Map();
|
|
47061
|
+
var prPollTimers = /* @__PURE__ */ new Map();
|
|
47062
|
+
var prPollLastActivity = /* @__PURE__ */ new Map();
|
|
45665
47063
|
function debouncedDiffCapture(taskId, cwd, taskHandle, log) {
|
|
45666
47064
|
const existing = diffDebounceTimers.get(taskId);
|
|
45667
47065
|
if (existing) clearTimeout(existing);
|
|
@@ -45732,6 +47130,100 @@ async function captureBranchDiffState(cwd, taskHandle, log) {
|
|
|
45732
47130
|
});
|
|
45733
47131
|
log.debug({ baseBranch, fileCount: branchFiles.length }, "Branch diff state captured");
|
|
45734
47132
|
}
|
|
47133
|
+
async function capturePRState(cwd, taskHandle, log) {
|
|
47134
|
+
const available = await isGhAvailable();
|
|
47135
|
+
change(taskHandle.conv, (draft) => {
|
|
47136
|
+
draft.diffState.prAvailable.set(available);
|
|
47137
|
+
});
|
|
47138
|
+
if (!available) return;
|
|
47139
|
+
const [currentPR, assignedReviews, userPRs] = await Promise.allSettled([
|
|
47140
|
+
getPRForCurrentBranch(cwd),
|
|
47141
|
+
getAssignedReviews(cwd),
|
|
47142
|
+
getUserPRs(cwd)
|
|
47143
|
+
]);
|
|
47144
|
+
const allPRs = [];
|
|
47145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
47146
|
+
const currentBranchPR = currentPR.status === "fulfilled" ? currentPR.value : null;
|
|
47147
|
+
if (currentBranchPR) {
|
|
47148
|
+
allPRs.push(currentBranchPR);
|
|
47149
|
+
seen.add(currentBranchPR.number);
|
|
47150
|
+
}
|
|
47151
|
+
for (const pr of assignedReviews.status === "fulfilled" ? assignedReviews.value : []) {
|
|
47152
|
+
if (!seen.has(pr.number)) {
|
|
47153
|
+
allPRs.push(pr);
|
|
47154
|
+
seen.add(pr.number);
|
|
47155
|
+
}
|
|
47156
|
+
}
|
|
47157
|
+
for (const pr of userPRs.status === "fulfilled" ? userPRs.value : []) {
|
|
47158
|
+
if (!seen.has(pr.number)) {
|
|
47159
|
+
allPRs.push(pr);
|
|
47160
|
+
seen.add(pr.number);
|
|
47161
|
+
}
|
|
47162
|
+
}
|
|
47163
|
+
let prDiff = "";
|
|
47164
|
+
let prFiles = [];
|
|
47165
|
+
if (currentBranchPR) {
|
|
47166
|
+
[prDiff, prFiles] = await Promise.all([
|
|
47167
|
+
getPRDiff(cwd, currentBranchPR.number),
|
|
47168
|
+
getPRFiles(cwd, currentBranchPR.number)
|
|
47169
|
+
]);
|
|
47170
|
+
}
|
|
47171
|
+
change(taskHandle.conv, (draft) => {
|
|
47172
|
+
draft.diffState.currentBranchPRNumber.set(currentBranchPR?.number ?? 0);
|
|
47173
|
+
draft.diffState.prData.set(allPRs);
|
|
47174
|
+
draft.diffState.prDiff.set(prDiff);
|
|
47175
|
+
const fileList = draft.diffState.prFiles;
|
|
47176
|
+
if (fileList.length > 0) {
|
|
47177
|
+
fileList.delete(0, fileList.length);
|
|
47178
|
+
}
|
|
47179
|
+
for (const file of prFiles) {
|
|
47180
|
+
fileList.push(file);
|
|
47181
|
+
}
|
|
47182
|
+
draft.diffState.prUpdatedAt.set(Date.now());
|
|
47183
|
+
});
|
|
47184
|
+
log.debug({ prCount: allPRs.length, hasBranchPR: !!currentBranchPR }, "PR state captured");
|
|
47185
|
+
}
|
|
47186
|
+
function getLatestCwd(taskHandle) {
|
|
47187
|
+
const convJson = taskHandle.conv.toJSON();
|
|
47188
|
+
const lastUserMsg = [...convJson.conversation].reverse().find((m) => m.role === "user");
|
|
47189
|
+
return lastUserMsg?.cwd ?? process.cwd();
|
|
47190
|
+
}
|
|
47191
|
+
function refreshPRPoll(taskId, taskHandle, taskLog) {
|
|
47192
|
+
const cwd = getLatestCwd(taskHandle);
|
|
47193
|
+
prPollLastActivity.set(taskId, Date.now());
|
|
47194
|
+
capturePRState(cwd, taskHandle, taskLog).catch((err) => {
|
|
47195
|
+
taskLog.warn({ err }, "PR refresh on re-notify failed");
|
|
47196
|
+
});
|
|
47197
|
+
if (!prPollTimers.has(taskId)) {
|
|
47198
|
+
const convHandle = taskHandle.conv;
|
|
47199
|
+
const initialCwd = cwd;
|
|
47200
|
+
prPollTimers.set(
|
|
47201
|
+
taskId,
|
|
47202
|
+
setInterval(() => {
|
|
47203
|
+
try {
|
|
47204
|
+
const lastActivity = prPollLastActivity.get(taskId) ?? 0;
|
|
47205
|
+
if (Date.now() - lastActivity > PR_POLL_IDLE_TIMEOUT_MS) {
|
|
47206
|
+
const timer = prPollTimers.get(taskId);
|
|
47207
|
+
if (timer) clearInterval(timer);
|
|
47208
|
+
prPollTimers.delete(taskId);
|
|
47209
|
+
prPollLastActivity.delete(taskId);
|
|
47210
|
+
taskLog.info("PR poll stopped after idle timeout");
|
|
47211
|
+
return;
|
|
47212
|
+
}
|
|
47213
|
+
const convJson = convHandle.toJSON();
|
|
47214
|
+
const latestUserMsg = [...convJson.conversation].reverse().find((m) => m.role === "user");
|
|
47215
|
+
const pollCwd = latestUserMsg?.cwd ?? initialCwd;
|
|
47216
|
+
capturePRState(pollCwd, taskHandle, taskLog).catch((err) => {
|
|
47217
|
+
taskLog.warn({ err }, "PR poll failed");
|
|
47218
|
+
});
|
|
47219
|
+
} catch (err) {
|
|
47220
|
+
taskLog.warn({ err }, "PR poll tick failed");
|
|
47221
|
+
}
|
|
47222
|
+
}, PR_POLL_INTERVAL_MS)
|
|
47223
|
+
);
|
|
47224
|
+
taskLog.info("PR poll timer restarted after idle");
|
|
47225
|
+
}
|
|
47226
|
+
}
|
|
45735
47227
|
async function captureTurnDiff(cwd, turnStartRef, taskHandle, log) {
|
|
45736
47228
|
const turnEndRef = await captureTreeSnapshot(cwd);
|
|
45737
47229
|
if (!turnStartRef || !turnEndRef) {
|
|
@@ -45817,21 +47309,55 @@ function handleMessage(msg, ctx) {
|
|
|
45817
47309
|
ctx.log.debug({ type: msg.type }, "Worktree create echo");
|
|
45818
47310
|
break;
|
|
45819
47311
|
case "cancel-task":
|
|
47312
|
+
ctx.log.info(
|
|
47313
|
+
{ taskId: msg.taskId, requestId: msg.requestId, machineId: msg.machineId },
|
|
47314
|
+
"Received cancel-task from browser"
|
|
47315
|
+
);
|
|
45820
47316
|
handleCancelTask(msg, ctx);
|
|
45821
47317
|
break;
|
|
45822
47318
|
case "control-ack":
|
|
45823
47319
|
ctx.log.debug({ type: msg.type }, "Control ack echo");
|
|
45824
47320
|
break;
|
|
45825
47321
|
case "authenticated":
|
|
47322
|
+
ctx.log.info({ type: msg.type }, "Server notification: authenticated");
|
|
47323
|
+
break;
|
|
45826
47324
|
case "agent-joined":
|
|
45827
47325
|
case "agent-left":
|
|
45828
47326
|
case "agent-status-changed":
|
|
47327
|
+
ctx.log.info({ type: msg.type }, "Server notification: agent change");
|
|
47328
|
+
break;
|
|
45829
47329
|
case "error":
|
|
45830
|
-
ctx.log.
|
|
47330
|
+
ctx.log.warn({ type: msg.type, code: msg.code, message: msg.message }, "Server error");
|
|
47331
|
+
break;
|
|
47332
|
+
case "notify-collab-room":
|
|
47333
|
+
handleNotifyCollabRoom(msg, ctx);
|
|
45831
47334
|
break;
|
|
45832
47335
|
default:
|
|
45833
|
-
|
|
47336
|
+
assertNever3(msg);
|
|
47337
|
+
}
|
|
47338
|
+
}
|
|
47339
|
+
function handleNotifyCollabRoom(msg, ctx) {
|
|
47340
|
+
const collabLog = createChildLogger({ mode: `collab:${msg.roomId}`, taskId: msg.taskId });
|
|
47341
|
+
if (!ctx.env.SHIPYARD_SIGNALING_URL) {
|
|
47342
|
+
collabLog.warn("No signaling URL configured, cannot join collab room");
|
|
47343
|
+
return;
|
|
45834
47344
|
}
|
|
47345
|
+
if (!ctx.env.SHIPYARD_USER_TOKEN) {
|
|
47346
|
+
collabLog.warn("No user token configured, cannot join collab room");
|
|
47347
|
+
return;
|
|
47348
|
+
}
|
|
47349
|
+
collabLog.info("Joining collab room");
|
|
47350
|
+
ctx.collabRoomManager.join({
|
|
47351
|
+
roomId: msg.roomId,
|
|
47352
|
+
taskId: msg.taskId,
|
|
47353
|
+
token: msg.token,
|
|
47354
|
+
expiresAt: msg.expiresAt,
|
|
47355
|
+
signalingBaseUrl: ctx.env.SHIPYARD_SIGNALING_URL,
|
|
47356
|
+
userToken: ctx.env.SHIPYARD_USER_TOKEN,
|
|
47357
|
+
machineId: ctx.machineId,
|
|
47358
|
+
webrtcAdapter: ctx.webrtcAdapter,
|
|
47359
|
+
log: collabLog
|
|
47360
|
+
});
|
|
45835
47361
|
}
|
|
45836
47362
|
function handleNotifyTask(msg, ctx) {
|
|
45837
47363
|
const { taskId, requestId } = msg;
|
|
@@ -45858,6 +47384,7 @@ function handleNotifyTask(msg, ctx) {
|
|
|
45858
47384
|
const taskHandle = ctx.taskHandles.get(taskId);
|
|
45859
47385
|
if (taskHandle) {
|
|
45860
47386
|
onTaskDocChanged(taskId, taskHandle, taskLog, ctx);
|
|
47387
|
+
refreshPRPoll(taskId, taskHandle, taskLog);
|
|
45861
47388
|
}
|
|
45862
47389
|
return;
|
|
45863
47390
|
}
|
|
@@ -45889,7 +47416,7 @@ function handleCancelTask(msg, ctx) {
|
|
|
45889
47416
|
});
|
|
45890
47417
|
return;
|
|
45891
47418
|
}
|
|
45892
|
-
taskLog.info("Canceling active task");
|
|
47419
|
+
taskLog.info({ requestId, machineId: msg.machineId }, "Canceling active task");
|
|
45893
47420
|
activeTask.abortController.abort();
|
|
45894
47421
|
ctx.connection.send({
|
|
45895
47422
|
type: "control-ack",
|
|
@@ -46384,7 +47911,7 @@ async function watchTaskDocument(taskId, taskLog, ctx) {
|
|
|
46384
47911
|
review: reviewHandle
|
|
46385
47912
|
};
|
|
46386
47913
|
if (recoverOrphanedTask(taskDocs, taskLog)) {
|
|
46387
|
-
updateTaskInIndex(ctx.roomDoc, taskId, { status: "
|
|
47914
|
+
updateTaskInIndex(ctx.roomDoc, taskId, { status: "input-required", updatedAt: Date.now() });
|
|
46388
47915
|
}
|
|
46389
47916
|
const convJson = convHandle.toJSON();
|
|
46390
47917
|
const lastUserMsg = [...convJson.conversation].reverse().find((m) => m.role === "user");
|
|
@@ -46392,6 +47919,36 @@ async function watchTaskDocument(taskId, taskLog, ctx) {
|
|
|
46392
47919
|
captureBranchDiffState(initialCwd, taskHandle, taskLog).catch((err) => {
|
|
46393
47920
|
taskLog.warn({ err }, "Failed to capture initial branch diff");
|
|
46394
47921
|
});
|
|
47922
|
+
capturePRState(initialCwd, taskHandle, taskLog).catch((err) => {
|
|
47923
|
+
taskLog.warn({ err }, "Initial PR capture failed");
|
|
47924
|
+
});
|
|
47925
|
+
const existingPrTimer = prPollTimers.get(taskId);
|
|
47926
|
+
if (existingPrTimer) clearInterval(existingPrTimer);
|
|
47927
|
+
prPollLastActivity.set(taskId, Date.now());
|
|
47928
|
+
prPollTimers.set(
|
|
47929
|
+
taskId,
|
|
47930
|
+
setInterval(() => {
|
|
47931
|
+
try {
|
|
47932
|
+
const lastActivity = prPollLastActivity.get(taskId) ?? 0;
|
|
47933
|
+
if (Date.now() - lastActivity > PR_POLL_IDLE_TIMEOUT_MS) {
|
|
47934
|
+
const timer = prPollTimers.get(taskId);
|
|
47935
|
+
if (timer) clearInterval(timer);
|
|
47936
|
+
prPollTimers.delete(taskId);
|
|
47937
|
+
prPollLastActivity.delete(taskId);
|
|
47938
|
+
taskLog.info("PR poll stopped after idle timeout");
|
|
47939
|
+
return;
|
|
47940
|
+
}
|
|
47941
|
+
const convJson2 = convHandle.toJSON();
|
|
47942
|
+
const latestUserMsg = [...convJson2.conversation].reverse().find((m) => m.role === "user");
|
|
47943
|
+
const cwd = latestUserMsg?.cwd ?? initialCwd;
|
|
47944
|
+
capturePRState(cwd, taskHandle, taskLog).catch((err) => {
|
|
47945
|
+
taskLog.warn({ err }, "PR poll failed");
|
|
47946
|
+
});
|
|
47947
|
+
} catch (err) {
|
|
47948
|
+
taskLog.warn({ err }, "PR poll tick failed");
|
|
47949
|
+
}
|
|
47950
|
+
}, PR_POLL_INTERVAL_MS)
|
|
47951
|
+
);
|
|
46395
47952
|
const opCountBefore = loro(convHandle).opCount();
|
|
46396
47953
|
taskLog.info({ convDocId, opCount: opCountBefore }, "Doc state before subscribe");
|
|
46397
47954
|
const unsubscribe = subscribe(convHandle, (event) => {
|
|
@@ -46582,7 +48139,17 @@ function onTaskDocChanged(taskId, taskHandle, taskLog, ctx) {
|
|
|
46582
48139
|
};
|
|
46583
48140
|
const userId = ctx.env.SHIPYARD_USER_ID ?? null;
|
|
46584
48141
|
const userName = ctx.env.SHIPYARD_USER_DISPLAY_NAME ?? null;
|
|
46585
|
-
const
|
|
48142
|
+
const onTodoProgress = (progress) => {
|
|
48143
|
+
updateTaskInIndex(ctx.roomDoc, taskId, { ...progress, updatedAt: Date.now() });
|
|
48144
|
+
};
|
|
48145
|
+
const manager = new SessionManager(
|
|
48146
|
+
taskDocs,
|
|
48147
|
+
userId,
|
|
48148
|
+
userName,
|
|
48149
|
+
onStatusChange,
|
|
48150
|
+
onBackgroundAgent,
|
|
48151
|
+
onTodoProgress
|
|
48152
|
+
);
|
|
46586
48153
|
const activeTask = {
|
|
46587
48154
|
taskId,
|
|
46588
48155
|
abortController,
|
|
@@ -46644,6 +48211,7 @@ async function cleanupTaskRun(opts) {
|
|
|
46644
48211
|
abortController.abort();
|
|
46645
48212
|
clearDebouncedTimer(diffDebounceTimers, taskId);
|
|
46646
48213
|
clearDebouncedTimer(branchDiffTimers, taskId);
|
|
48214
|
+
prPollLastActivity.set(taskId, Date.now());
|
|
46647
48215
|
try {
|
|
46648
48216
|
await captureDiffState(cwd, taskHandle, taskLog);
|
|
46649
48217
|
} catch (err) {
|
|
@@ -46654,6 +48222,11 @@ async function cleanupTaskRun(opts) {
|
|
|
46654
48222
|
} catch (err) {
|
|
46655
48223
|
taskLog.warn({ err }, "Failed to capture final branch diff state");
|
|
46656
48224
|
}
|
|
48225
|
+
try {
|
|
48226
|
+
await capturePRState(cwd, taskHandle, taskLog);
|
|
48227
|
+
} catch (err) {
|
|
48228
|
+
taskLog.warn({ err }, "Failed to capture final PR state");
|
|
48229
|
+
}
|
|
46657
48230
|
try {
|
|
46658
48231
|
const turnStartRef = await turnStartRefPromise;
|
|
46659
48232
|
await captureTurnDiff(cwd, turnStartRef, taskHandle, taskLog);
|
|
@@ -46681,6 +48254,14 @@ function clearDebouncedTimer(timers, taskId) {
|
|
|
46681
48254
|
timers.delete(taskId);
|
|
46682
48255
|
}
|
|
46683
48256
|
}
|
|
48257
|
+
function clearAllTimers(...timerMaps) {
|
|
48258
|
+
for (const timers of timerMaps) {
|
|
48259
|
+
for (const timer of timers.values()) {
|
|
48260
|
+
clearTimeout(timer);
|
|
48261
|
+
}
|
|
48262
|
+
timers.clear();
|
|
48263
|
+
}
|
|
48264
|
+
}
|
|
46684
48265
|
function mapPermissionMode(mode) {
|
|
46685
48266
|
switch (mode) {
|
|
46686
48267
|
case "accept-edits":
|
|
@@ -47011,6 +48592,16 @@ async function runTask(opts) {
|
|
|
47011
48592
|
}
|
|
47012
48593
|
|
|
47013
48594
|
// src/index.ts
|
|
48595
|
+
function getVersion() {
|
|
48596
|
+
try {
|
|
48597
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
48598
|
+
const pkgPath = resolve3(thisFile, "../../package.json");
|
|
48599
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
48600
|
+
return pkg.version ?? "unknown";
|
|
48601
|
+
} catch {
|
|
48602
|
+
return "unknown";
|
|
48603
|
+
}
|
|
48604
|
+
}
|
|
47014
48605
|
function parseCliArgs() {
|
|
47015
48606
|
const { values } = parseArgs({
|
|
47016
48607
|
options: {
|
|
@@ -47021,23 +48612,29 @@ function parseCliArgs() {
|
|
|
47021
48612
|
cwd: { type: "string" },
|
|
47022
48613
|
model: { type: "string", short: "m" },
|
|
47023
48614
|
serve: { type: "boolean", short: "s" },
|
|
48615
|
+
version: { type: "boolean", short: "v" },
|
|
47024
48616
|
help: { type: "boolean", short: "h" }
|
|
47025
48617
|
},
|
|
47026
48618
|
strict: true
|
|
47027
48619
|
});
|
|
48620
|
+
if (values.version) {
|
|
48621
|
+
process.stdout.write(`${getVersion()}
|
|
48622
|
+
`);
|
|
48623
|
+
process.exit(0);
|
|
48624
|
+
}
|
|
47028
48625
|
if (values.help) {
|
|
47029
48626
|
logger.info(
|
|
47030
48627
|
[
|
|
47031
|
-
|
|
48628
|
+
`shipyard v${getVersion()} - Agent management hub for human-agent collaboration`,
|
|
47032
48629
|
"",
|
|
47033
48630
|
"Usage:",
|
|
47034
48631
|
" shipyard login Authenticate with Shipyard",
|
|
47035
48632
|
" shipyard login --check Check current auth status",
|
|
47036
48633
|
" shipyard logout Clear stored credentials",
|
|
47037
48634
|
"",
|
|
47038
|
-
' shipyard
|
|
47039
|
-
' shipyard
|
|
47040
|
-
" shipyard
|
|
48635
|
+
' shipyard --prompt "Fix the bug in auth.ts" [options]',
|
|
48636
|
+
' shipyard --resume <session-id> --task-id <id> [--prompt "Continue"]',
|
|
48637
|
+
" shipyard --serve",
|
|
47041
48638
|
"",
|
|
47042
48639
|
"Options:",
|
|
47043
48640
|
" -p, --prompt <text> Prompt for the agent",
|
|
@@ -47047,6 +48644,7 @@ function parseCliArgs() {
|
|
|
47047
48644
|
" --cwd <path> Working directory for agent",
|
|
47048
48645
|
" -m, --model <name> Model to use",
|
|
47049
48646
|
" -s, --serve Run in serve mode (signaling + spawn-agent)",
|
|
48647
|
+
" -v, --version Show version",
|
|
47050
48648
|
" -h, --help Show this help",
|
|
47051
48649
|
"",
|
|
47052
48650
|
"Authentication:",
|
|
@@ -47077,8 +48675,8 @@ function parseCliArgs() {
|
|
|
47077
48675
|
serve: values.serve
|
|
47078
48676
|
};
|
|
47079
48677
|
}
|
|
47080
|
-
async function setupSignaling(env, log) {
|
|
47081
|
-
const handle = await createSignalingHandle(env, log);
|
|
48678
|
+
async function setupSignaling(env, log, shipyardHome) {
|
|
48679
|
+
const handle = await createSignalingHandle(env, log, shipyardHome);
|
|
47082
48680
|
if (handle) {
|
|
47083
48681
|
handle.connection.connect();
|
|
47084
48682
|
}
|
|
@@ -47171,13 +48769,13 @@ function handleResult(log, result, startTime) {
|
|
|
47171
48769
|
async function handleSubcommand() {
|
|
47172
48770
|
const subcommand = process.argv[2];
|
|
47173
48771
|
if (subcommand === "login") {
|
|
47174
|
-
const { loginCommand } = await import("./login-
|
|
48772
|
+
const { loginCommand } = await import("./login-BS6C2BRS.js");
|
|
47175
48773
|
const hasCheck = process.argv.includes("--check");
|
|
47176
48774
|
await loginCommand({ check: hasCheck });
|
|
47177
48775
|
return true;
|
|
47178
48776
|
}
|
|
47179
48777
|
if (subcommand === "logout") {
|
|
47180
|
-
const { logoutCommand } = await import("./logout-
|
|
48778
|
+
const { logoutCommand } = await import("./logout-XX5ULFHB.js");
|
|
47181
48779
|
await logoutCommand();
|
|
47182
48780
|
return true;
|
|
47183
48781
|
}
|
|
@@ -47241,7 +48839,7 @@ async function main() {
|
|
|
47241
48839
|
const repo = await setupRepo(dataDir);
|
|
47242
48840
|
const lifecycle = new LifecycleManager();
|
|
47243
48841
|
await lifecycle.acquirePidFile(getShipyardHome());
|
|
47244
|
-
const signalingHandle = await setupSignaling(env, log);
|
|
48842
|
+
const signalingHandle = await setupSignaling(env, log, getShipyardHome());
|
|
47245
48843
|
const cleanup = createCleanup(signalingHandle, lifecycle, repo);
|
|
47246
48844
|
lifecycle.onShutdown(async () => {
|
|
47247
48845
|
log.info("Cleaning up...");
|