kith-coord 0.1.2 → 0.1.4
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/CHANGELOG.md +20 -0
- package/README.md +28 -2
- package/bin/kith-menubar +0 -0
- package/dist/cli/index.js +859 -123
- package/dist/cli/index.js.map +1 -1
- package/dist/command-worker.js +1 -1
- package/dist/command-worker.js.map +1 -1
- package/dist/index.d.ts +56 -15
- package/dist/index.js +110 -9
- package/dist/index.js.map +1 -1
- package/docs/client-validation.md +5 -0
- package/docs/integrations.md +16 -3
- package/docs/protocol/tools.v1.json +125 -1
- package/docs/protocol.md +10 -2
- package/docs/reliability.md +3 -0
- package/docs/roadmap.md +12 -33
- package/docs/usage.md +68 -0
- package/docs/validation/2026-10-22-product-claude.json +38 -0
- package/docs/validation/2026-10-22-product-codex.json +76 -0
- package/package.json +7 -4
- package/server.json +4 -4
package/dist/command-worker.js
CHANGED
|
@@ -13,7 +13,7 @@ process.once("message", (message) => {
|
|
|
13
13
|
if (started) return;
|
|
14
14
|
started = true;
|
|
15
15
|
let finished = false;
|
|
16
|
-
const env = { ...process.env, ...message.mutexName === "package-install" ? { KITH_PACKAGE_INSTALL_GUARDED: "1" } : {} };
|
|
16
|
+
const env = { ...process.env, ...message.env, ...message.mutexName === "package-install" ? { KITH_PACKAGE_INSTALL_GUARDED: "1" } : {} };
|
|
17
17
|
const child = Array.isArray(message.command) ? spawn(message.command[0], message.command.slice(1), { cwd: message.cwd, env, stdio: ["ignore", "inherit", "inherit"] }) : spawn(message.command, { cwd: message.cwd, env, stdio: ["ignore", "inherit", "inherit"], shell: true });
|
|
18
18
|
child.once("error", (err) => {
|
|
19
19
|
if (finished) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/command-worker.ts"],"sourcesContent":["// A detached process-group leader. The command cannot start until its supervisor\n// has durably recorded this group and sent the start message over private IPC.\nimport os from 'node:os';\nimport { spawn } from 'node:child_process';\n\nlet started = false;\nprocess.on('disconnect', () => {\n try { process.kill(-process.pid, 'SIGKILL'); } catch { process.exit(1); }\n});\nprocess.once('message', (message: { command: string | string[]; cwd: string; mutexName: string }) => {\n if (started) return;\n started = true;\n let finished = false;\n const env = { ...process.env, ...(message.mutexName === 'package-install' ? { KITH_PACKAGE_INSTALL_GUARDED: '1' } : {}) };\n const child = Array.isArray(message.command)\n ? spawn(message.command[0], message.command.slice(1), { cwd: message.cwd, env, stdio: ['ignore', 'inherit', 'inherit'] })\n : spawn(message.command, { cwd: message.cwd, env, stdio: ['ignore', 'inherit', 'inherit'], shell: true });\n child.once('error', (err) => {\n if (finished) return;\n finished = true;\n process.send?.({ error: err.message }, () => process.exit(1));\n });\n child.once('close', (code, signal) => {\n if (finished) return;\n finished = true;\n if (process.connected) process.send?.({ exitCode: code ?? (signal ? 128 + (os.constants.signals[signal] ?? 1) : 1), signal }, () => process.exit(0));\n });\n});\nprocess.send?.({ ready: true });\n"],"mappings":";AAEA,OAAO,QAAQ;AACf,SAAS,aAAa;AAEtB,IAAI,UAAU;AACd,QAAQ,GAAG,cAAc,MAAM;AAC7B,MAAI;AAAE,YAAQ,KAAK,CAAC,QAAQ,KAAK,SAAS;AAAA,EAAG,QAAQ;AAAE,YAAQ,KAAK,CAAC;AAAA,EAAG;AAC1E,CAAC;AACD,QAAQ,KAAK,WAAW,CAAC,
|
|
1
|
+
{"version":3,"sources":["../src/core/command-worker.ts"],"sourcesContent":["// A detached process-group leader. The command cannot start until its supervisor\n// has durably recorded this group and sent the start message over private IPC.\nimport os from 'node:os';\nimport { spawn } from 'node:child_process';\n\nlet started = false;\nprocess.on('disconnect', () => {\n try { process.kill(-process.pid, 'SIGKILL'); } catch { process.exit(1); }\n});\nprocess.once('message', (message: { command: string | string[]; cwd: string; mutexName: string; env?: Record<string, string> }) => {\n if (started) return;\n started = true;\n let finished = false;\n const env = { ...process.env, ...message.env, ...(message.mutexName === 'package-install' ? { KITH_PACKAGE_INSTALL_GUARDED: '1' } : {}) };\n const child = Array.isArray(message.command)\n ? spawn(message.command[0], message.command.slice(1), { cwd: message.cwd, env, stdio: ['ignore', 'inherit', 'inherit'] })\n : spawn(message.command, { cwd: message.cwd, env, stdio: ['ignore', 'inherit', 'inherit'], shell: true });\n child.once('error', (err) => {\n if (finished) return;\n finished = true;\n process.send?.({ error: err.message }, () => process.exit(1));\n });\n child.once('close', (code, signal) => {\n if (finished) return;\n finished = true;\n if (process.connected) process.send?.({ exitCode: code ?? (signal ? 128 + (os.constants.signals[signal] ?? 1) : 1), signal }, () => process.exit(0));\n });\n});\nprocess.send?.({ ready: true });\n"],"mappings":";AAEA,OAAO,QAAQ;AACf,SAAS,aAAa;AAEtB,IAAI,UAAU;AACd,QAAQ,GAAG,cAAc,MAAM;AAC7B,MAAI;AAAE,YAAQ,KAAK,CAAC,QAAQ,KAAK,SAAS;AAAA,EAAG,QAAQ;AAAE,YAAQ,KAAK,CAAC;AAAA,EAAG;AAC1E,CAAC;AACD,QAAQ,KAAK,WAAW,CAAC,YAA0G;AACjI,MAAI,QAAS;AACb,YAAU;AACV,MAAI,WAAW;AACf,QAAM,MAAM,EAAE,GAAG,QAAQ,KAAK,GAAG,QAAQ,KAAK,GAAI,QAAQ,cAAc,oBAAoB,EAAE,8BAA8B,IAAI,IAAI,CAAC,EAAG;AACxI,QAAM,QAAQ,MAAM,QAAQ,QAAQ,OAAO,IACvC,MAAM,QAAQ,QAAQ,CAAC,GAAG,QAAQ,QAAQ,MAAM,CAAC,GAAG,EAAE,KAAK,QAAQ,KAAK,KAAK,OAAO,CAAC,UAAU,WAAW,SAAS,EAAE,CAAC,IACtH,MAAM,QAAQ,SAAS,EAAE,KAAK,QAAQ,KAAK,KAAK,OAAO,CAAC,UAAU,WAAW,SAAS,GAAG,OAAO,KAAK,CAAC;AAC1G,QAAM,KAAK,SAAS,CAAC,QAAQ;AAC3B,QAAI,SAAU;AACd,eAAW;AACX,YAAQ,OAAO,EAAE,OAAO,IAAI,QAAQ,GAAG,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA,EAC9D,CAAC;AACD,QAAM,KAAK,SAAS,CAAC,MAAM,WAAW;AACpC,QAAI,SAAU;AACd,eAAW;AACX,QAAI,QAAQ,UAAW,SAAQ,OAAO,EAAE,UAAU,SAAS,SAAS,OAAO,GAAG,UAAU,QAAQ,MAAM,KAAK,KAAK,IAAI,OAAO,GAAG,MAAM,QAAQ,KAAK,CAAC,CAAC;AAAA,EACrJ,CAAC;AACH,CAAC;AACD,QAAQ,OAAO,EAAE,OAAO,KAAK,CAAC;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ declare class FencingAndAgentManager {
|
|
|
74
74
|
static readonly DEFAULT_LEASE_MS: number;
|
|
75
75
|
private readonly capabilities;
|
|
76
76
|
private static ownWindowsStart;
|
|
77
|
+
private static inspectionFailure;
|
|
77
78
|
constructor(coordDb: CoordDatabase);
|
|
78
79
|
static validateLease(duration: number): number;
|
|
79
80
|
/** A real kernel process identity; tests must not supply liveness sentinels. */
|
|
@@ -542,6 +543,19 @@ interface ResourceRecord {
|
|
|
542
543
|
acquired_at: number;
|
|
543
544
|
expires_at: number;
|
|
544
545
|
}
|
|
546
|
+
interface SupervisedCommandOptions {
|
|
547
|
+
agentId: string;
|
|
548
|
+
command: string | string[];
|
|
549
|
+
cwd?: string;
|
|
550
|
+
timeoutMs?: number;
|
|
551
|
+
commandTimeoutMs?: number;
|
|
552
|
+
leaseDurationMs?: number;
|
|
553
|
+
killGraceMs?: number;
|
|
554
|
+
signal?: AbortSignal;
|
|
555
|
+
maxOutputBytes?: number;
|
|
556
|
+
onStdout?: (chunk: Buffer) => void;
|
|
557
|
+
onStderr?: (chunk: Buffer) => void;
|
|
558
|
+
}
|
|
545
559
|
declare class ResourceBroker {
|
|
546
560
|
private readonly coordDb;
|
|
547
561
|
private readonly fencing;
|
|
@@ -586,26 +600,29 @@ declare class ResourceBroker {
|
|
|
586
600
|
mutexName: string;
|
|
587
601
|
fencingToken: number;
|
|
588
602
|
}): boolean;
|
|
603
|
+
/** Launch on the allocated port, without interpreting arguments as shell code. */
|
|
604
|
+
executeWithPort(params: Omit<SupervisedCommandOptions, 'command'> & {
|
|
605
|
+
command: string[];
|
|
606
|
+
serviceName: string;
|
|
607
|
+
basePort?: number;
|
|
608
|
+
onPort?: (port: number) => void;
|
|
609
|
+
}): Promise<{
|
|
610
|
+
port: number;
|
|
611
|
+
stdout: string;
|
|
612
|
+
stderr: string;
|
|
613
|
+
exitCode: number;
|
|
614
|
+
waitedMs: number;
|
|
615
|
+
}>;
|
|
589
616
|
/** Renew leases while a detached, recorded process group runs; stop it before releasing. */
|
|
590
|
-
executeWithMutex(params: {
|
|
591
|
-
agentId: string;
|
|
617
|
+
executeWithMutex(params: SupervisedCommandOptions & {
|
|
592
618
|
mutexName: string;
|
|
593
|
-
command: string | string[];
|
|
594
|
-
cwd?: string;
|
|
595
|
-
timeoutMs?: number;
|
|
596
|
-
commandTimeoutMs?: number;
|
|
597
|
-
leaseDurationMs?: number;
|
|
598
|
-
killGraceMs?: number;
|
|
599
|
-
signal?: AbortSignal;
|
|
600
|
-
maxOutputBytes?: number;
|
|
601
|
-
onStdout?: (chunk: Buffer) => void;
|
|
602
|
-
onStderr?: (chunk: Buffer) => void;
|
|
603
619
|
}): Promise<{
|
|
604
620
|
stdout: string;
|
|
605
621
|
stderr: string;
|
|
606
622
|
exitCode: number;
|
|
607
623
|
waitedMs: number;
|
|
608
624
|
}>;
|
|
625
|
+
private executeGuarded;
|
|
609
626
|
private getRun;
|
|
610
627
|
private runIsAlive;
|
|
611
628
|
private clearRun;
|
|
@@ -698,6 +715,7 @@ interface PostFileEditHookResult {
|
|
|
698
715
|
warnings: string[];
|
|
699
716
|
}
|
|
700
717
|
declare class KithEngine {
|
|
718
|
+
private activeClaimWaits;
|
|
701
719
|
readonly coordDb: CoordDatabase;
|
|
702
720
|
readonly fencing: FencingAndAgentManager;
|
|
703
721
|
readonly locks: LockEngine;
|
|
@@ -785,6 +803,29 @@ declare class KithEngine {
|
|
|
785
803
|
lockConflicts: LockConflict[];
|
|
786
804
|
activeSiblingIntents: TaskRecord[];
|
|
787
805
|
};
|
|
806
|
+
beginWorkWhenAvailable(params: Parameters<KithEngine['beginWork']>[0] & {
|
|
807
|
+
waitTimeoutMs: number;
|
|
808
|
+
signal?: AbortSignal;
|
|
809
|
+
}): Promise<{
|
|
810
|
+
worktree: SpawnedWorktree | null;
|
|
811
|
+
ok: boolean;
|
|
812
|
+
task: TaskRecord | null;
|
|
813
|
+
fencingToken: number;
|
|
814
|
+
locks: LockRecord[];
|
|
815
|
+
lockConflicts: LockConflict[];
|
|
816
|
+
activeSiblingIntents: TaskRecord[];
|
|
817
|
+
} & {
|
|
818
|
+
waitedMs: number;
|
|
819
|
+
waitTimedOut: boolean;
|
|
820
|
+
}>;
|
|
821
|
+
acquireLocksWhenAvailable(params: Parameters<LockEngine['acquireLocks']>[0] & {
|
|
822
|
+
waitTimeoutMs: number;
|
|
823
|
+
signal?: AbortSignal;
|
|
824
|
+
}): Promise<AcquireLockResult & {
|
|
825
|
+
waitedMs: number;
|
|
826
|
+
waitTimedOut: boolean;
|
|
827
|
+
}>;
|
|
828
|
+
private waitForOwnedClaim;
|
|
788
829
|
completeWork(params: {
|
|
789
830
|
agentId: string;
|
|
790
831
|
taskId?: string;
|
|
@@ -978,7 +1019,7 @@ declare function recoveryReport(directory?: string): {
|
|
|
978
1019
|
error: string | undefined;
|
|
979
1020
|
};
|
|
980
1021
|
|
|
981
|
-
declare const KITH_VERSION = "0.1.
|
|
982
|
-
declare const KITH_PROTOCOL_VERSION = "1.
|
|
1022
|
+
declare const KITH_VERSION = "0.1.4";
|
|
1023
|
+
declare const KITH_PROTOCOL_VERSION = "1.3.0";
|
|
983
1024
|
|
|
984
|
-
export { type AcquireLockResult, type AgentRecord, type AgentUnreadDelta, AstAnalyzer, type BashGuardPostReport, BashMerkleGuard, type BlastRadiusAlert, CoWWorktreeManager, type ConflictRecord, type ContractRecord, CoordDatabase, type CoordEvent, type DeclareIntentResult, DependencyAndContractEngine, type DirtyMergeProbeResult, type ExtractedImport, type ExtractedSymbol, FencingAndAgentManager, type FileAstAnalysis, type FileModificationReport, type FileSnapshotEntry, IntentAndTaskEngine, KITH_PROTOCOL_VERSION, KITH_VERSION, KithEngine, type LockConflict, LockEngine, type LockRecord, type MergeTrainEntry, type PostFileEditHookResult, type QuarantineRecord, type RepositoryLayout, ResourceBroker, type ResourceRecord, SessionOwnershipError, type SessionRegistration, ShadowGitEngine, type SpawnedWorktree, type SpeculativeMergeResult, SpeculativeMergeTrain, StaleFencingTokenError, type SymbolKind, type TaskRecord, type WorkspaceObservation, type WorktreeChange, caseSensitivePaths, discoverRepository, isWithin, observeWorkspaces, physicalPath, previewIntegration, readCoordinationState, recoveryReport, repositoryFile, worktreeChanges };
|
|
1025
|
+
export { type AcquireLockResult, type AgentRecord, type AgentUnreadDelta, AstAnalyzer, type BashGuardPostReport, BashMerkleGuard, type BlastRadiusAlert, CoWWorktreeManager, type ConflictRecord, type ContractRecord, CoordDatabase, type CoordEvent, type DeclareIntentResult, DependencyAndContractEngine, type DirtyMergeProbeResult, type ExtractedImport, type ExtractedSymbol, FencingAndAgentManager, type FileAstAnalysis, type FileModificationReport, type FileSnapshotEntry, IntentAndTaskEngine, KITH_PROTOCOL_VERSION, KITH_VERSION, KithEngine, type LockConflict, LockEngine, type LockRecord, type MergeTrainEntry, type PostFileEditHookResult, type QuarantineRecord, type RepositoryLayout, ResourceBroker, type ResourceRecord, SessionOwnershipError, type SessionRegistration, ShadowGitEngine, type SpawnedWorktree, type SpeculativeMergeResult, SpeculativeMergeTrain, StaleFencingTokenError, type SupervisedCommandOptions, type SymbolKind, type TaskRecord, type WorkspaceObservation, type WorktreeChange, caseSensitivePaths, discoverRepository, isWithin, observeWorkspaces, physicalPath, previewIntegration, readCoordinationState, recoveryReport, repositoryFile, worktreeChanges };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
|
+
// src/core/claim-wait.ts
|
|
2
|
+
async function waitForClaim(params) {
|
|
3
|
+
const { waitTimeoutMs, signal } = params;
|
|
4
|
+
if (!Number.isSafeInteger(waitTimeoutMs) || waitTimeoutMs < 0 || waitTimeoutMs > 6e5) {
|
|
5
|
+
throw new Error("waitTimeoutMs must be an integer between 0 and 600000");
|
|
6
|
+
}
|
|
7
|
+
const started = performance.now();
|
|
8
|
+
let previous;
|
|
9
|
+
for (; ; ) {
|
|
10
|
+
signal?.throwIfAborted();
|
|
11
|
+
const elapsed = Math.round(performance.now() - started);
|
|
12
|
+
if (previous && elapsed >= waitTimeoutMs) return { ...previous, waitedMs: elapsed, waitTimedOut: true };
|
|
13
|
+
params.heartbeat();
|
|
14
|
+
const result = params.attempt();
|
|
15
|
+
const waitedMs = Math.round(performance.now() - started);
|
|
16
|
+
if (result.ok || waitedMs >= waitTimeoutMs) return { ...result, waitedMs, waitTimedOut: !result.ok };
|
|
17
|
+
previous = result;
|
|
18
|
+
await new Promise((resolve, reject) => {
|
|
19
|
+
const abort = () => {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
reject(signal.reason);
|
|
22
|
+
};
|
|
23
|
+
const timer = setTimeout(() => {
|
|
24
|
+
signal?.removeEventListener("abort", abort);
|
|
25
|
+
resolve();
|
|
26
|
+
}, Math.min(100, waitTimeoutMs - waitedMs));
|
|
27
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
1
32
|
// src/version.ts
|
|
2
|
-
var KITH_VERSION = "0.1.
|
|
3
|
-
var KITH_PROTOCOL_VERSION = "1.
|
|
33
|
+
var KITH_VERSION = "0.1.4";
|
|
34
|
+
var KITH_PROTOCOL_VERSION = "1.3.0";
|
|
4
35
|
|
|
5
36
|
// src/core/workspace-observation.ts
|
|
6
37
|
import { execFileSync as execFileSync2, spawnSync } from "child_process";
|
|
@@ -51,6 +82,9 @@ function repositoryFile(root, file) {
|
|
|
51
82
|
}
|
|
52
83
|
function discoverRepository(input) {
|
|
53
84
|
const directory = fs.realpathSync.native(input);
|
|
85
|
+
if (directory === path.parse(directory).root) {
|
|
86
|
+
throw new Error("Cannot coordinate a filesystem root; select an explicit project with --repo.");
|
|
87
|
+
}
|
|
54
88
|
if (!fs.statSync(directory).isDirectory()) throw new Error(`Not a directory: ${input}`);
|
|
55
89
|
const git2 = (...args) => execFileSync("git", ["rev-parse", ...args], {
|
|
56
90
|
cwd: directory,
|
|
@@ -615,6 +649,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
|
|
|
615
649
|
static DEFAULT_LEASE_MS = 5 * 60 * 1e3;
|
|
616
650
|
capabilities = /* @__PURE__ */ new Map();
|
|
617
651
|
static ownWindowsStart = null;
|
|
652
|
+
static inspectionFailure = null;
|
|
618
653
|
static validateLease(duration) {
|
|
619
654
|
if (!Number.isSafeInteger(duration) || duration < 100 || duration > 864e5) {
|
|
620
655
|
throw new Error("Lease duration must be an integer between 100 and 86400000 milliseconds");
|
|
@@ -623,6 +658,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
|
|
|
623
658
|
}
|
|
624
659
|
/** A real kernel process identity; tests must not supply liveness sentinels. */
|
|
625
660
|
static getProcessStartTime(pid) {
|
|
661
|
+
this.inspectionFailure = null;
|
|
626
662
|
if (!Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
627
663
|
try {
|
|
628
664
|
if (process.platform === "linux") {
|
|
@@ -638,11 +674,14 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
|
|
|
638
674
|
"-NoProfile",
|
|
639
675
|
"-NonInteractive",
|
|
640
676
|
"-Command",
|
|
641
|
-
`(
|
|
677
|
+
`([System.Diagnostics.Process]::GetProcessById(${pid})).StartTime.ToUniversalTime().Ticks.ToString([System.Globalization.CultureInfo]::InvariantCulture)`
|
|
642
678
|
],
|
|
643
|
-
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout:
|
|
679
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 15e3, windowsHide: true }
|
|
644
680
|
).trim();
|
|
645
|
-
if (!/^\d+$/.test(ticks))
|
|
681
|
+
if (!/^\d+$/.test(ticks)) {
|
|
682
|
+
this.inspectionFailure = "invalid kernel creation-time response";
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
646
685
|
const identity = `windows:${ticks}`;
|
|
647
686
|
if (pid === process.pid) this.ownWindowsStart = identity;
|
|
648
687
|
return identity;
|
|
@@ -654,7 +693,9 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
|
|
|
654
693
|
env: { ...process.env, LC_ALL: "C" }
|
|
655
694
|
}).trim();
|
|
656
695
|
return out || null;
|
|
657
|
-
} catch {
|
|
696
|
+
} catch (error) {
|
|
697
|
+
const failure = error;
|
|
698
|
+
this.inspectionFailure = [failure.code, failure.status === void 0 ? null : `exit ${failure.status}`, failure.signal].filter(Boolean).join(", ") || "inspection unavailable";
|
|
658
699
|
return null;
|
|
659
700
|
}
|
|
660
701
|
}
|
|
@@ -684,7 +725,7 @@ var FencingAndAgentManager = class _FencingAndAgentManager {
|
|
|
684
725
|
if (params.sessionToken !== void 0) throw new SessionOwnershipError(params.id);
|
|
685
726
|
const pid = params.pid ?? process.pid;
|
|
686
727
|
const start = _FencingAndAgentManager.getProcessStartTime(pid);
|
|
687
|
-
if (!start) throw new Error(`Cannot determine start time of live session owner PID ${pid}`);
|
|
728
|
+
if (!start) throw new Error(`Cannot determine start time of live session owner PID ${pid} (${_FencingAndAgentManager.inspectionFailure ?? "inspection unavailable"})`);
|
|
688
729
|
const now = Date.now();
|
|
689
730
|
const epoch = this.coordDb.nextFencingToken();
|
|
690
731
|
const token = crypto2.randomBytes(32).toString("base64url");
|
|
@@ -1376,6 +1417,7 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
1376
1417
|
const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
|
|
1377
1418
|
const now = Date.now();
|
|
1378
1419
|
const expiresAt = now + leaseMs;
|
|
1420
|
+
if (!Number.isInteger(basePort) || basePort < 1 || basePort > 65535) throw new Error("basePort must be an integer between 1 and 65535");
|
|
1379
1421
|
const resourceKey = `${params.agentId}:${params.serviceName}`;
|
|
1380
1422
|
const existing = this.getResource("port", resourceKey);
|
|
1381
1423
|
if (existing && existing.expires_at > now) {
|
|
@@ -1510,8 +1552,44 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
1510
1552
|
return true;
|
|
1511
1553
|
});
|
|
1512
1554
|
}
|
|
1555
|
+
/** Launch on the allocated port, without interpreting arguments as shell code. */
|
|
1556
|
+
async executeWithPort(params) {
|
|
1557
|
+
if (!params.command.length || !params.command[0]) throw new Error("A command is required");
|
|
1558
|
+
if (!params.serviceName || params.serviceName.length > 128) throw new Error("serviceName must contain 1 to 128 characters");
|
|
1559
|
+
const mutexName = `dev-port:${params.agentId}:${params.serviceName}`;
|
|
1560
|
+
let allocation;
|
|
1561
|
+
const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
|
|
1562
|
+
const result = await this.executeGuarded({ ...params, mutexName }, {
|
|
1563
|
+
prepare: async () => {
|
|
1564
|
+
allocation = await this.acquirePort({ ...params, basePort: params.basePort ?? 3e3 });
|
|
1565
|
+
params.onPort?.(allocation.port);
|
|
1566
|
+
return {
|
|
1567
|
+
command: params.command.map((arg) => arg.replaceAll("{port}", String(allocation.port))),
|
|
1568
|
+
env: { PORT: String(allocation.port), KITH_PORT: String(allocation.port) }
|
|
1569
|
+
};
|
|
1570
|
+
},
|
|
1571
|
+
renew: () => {
|
|
1572
|
+
if (!allocation) return;
|
|
1573
|
+
this.coordDb.withTransaction(() => {
|
|
1574
|
+
this.fencing.assertSession(params.agentId);
|
|
1575
|
+
const resource = this.getResource("port", `${params.agentId}:${params.serviceName}`);
|
|
1576
|
+
if (!resource || resource.fencing_token !== allocation.fencingToken || resource.expires_at <= Date.now()) {
|
|
1577
|
+
throw new Error("Dev server port lease lost");
|
|
1578
|
+
}
|
|
1579
|
+
this.coordDb.db.prepare("UPDATE resources SET expires_at=? WHERE id=?").run(Date.now() + leaseMs, resource.id);
|
|
1580
|
+
});
|
|
1581
|
+
},
|
|
1582
|
+
release: () => {
|
|
1583
|
+
if (allocation) this.coordDb.db.prepare("DELETE FROM resources WHERE type='port' AND name=? AND fencing_token=?").run(`${params.agentId}:${params.serviceName}`, allocation.fencingToken);
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
return { ...result, port: allocation.port };
|
|
1587
|
+
}
|
|
1513
1588
|
/** Renew leases while a detached, recorded process group runs; stop it before releasing. */
|
|
1514
|
-
|
|
1589
|
+
executeWithMutex(params) {
|
|
1590
|
+
return this.executeGuarded(params);
|
|
1591
|
+
}
|
|
1592
|
+
async executeGuarded(params, lifecycle) {
|
|
1515
1593
|
if (process.platform === "win32") throw new Error("Supervised mutex commands currently require macOS or Linux process groups");
|
|
1516
1594
|
if (!params.command.length) throw new Error("A command is required");
|
|
1517
1595
|
const leaseMs = FencingAndAgentManager.validateLease(params.leaseDurationMs ?? FencingAndAgentManager.DEFAULT_LEASE_MS);
|
|
@@ -1556,6 +1634,8 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
1556
1634
|
await delay(Math.min(50, Math.max(10, leaseMs / 4)));
|
|
1557
1635
|
}
|
|
1558
1636
|
const waitedMs = Date.now() - started;
|
|
1637
|
+
const prepared = await lifecycle?.prepare();
|
|
1638
|
+
if (params.signal?.aborted) throw abortError();
|
|
1559
1639
|
const candidates = ["./command-worker.js", "../command-worker.js", "../../dist/command-worker.js"].map((name) => fileURLToPath(new URL(name, import.meta.url)));
|
|
1560
1640
|
const script = candidates.find((name) => fs6.existsSync(name));
|
|
1561
1641
|
if (!script) throw new Error("Missing command worker. Build Kith before running supervised commands.");
|
|
@@ -1598,6 +1678,7 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
1598
1678
|
try {
|
|
1599
1679
|
this.fencing.heartbeat(params.agentId);
|
|
1600
1680
|
this.renewMutex({ ...params, fencingToken: run.fencing_token });
|
|
1681
|
+
lifecycle?.renew();
|
|
1601
1682
|
} catch (err) {
|
|
1602
1683
|
stop(err instanceof Error ? err : new Error(String(err)));
|
|
1603
1684
|
}
|
|
@@ -1625,8 +1706,9 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
1625
1706
|
run.worker_start = identity;
|
|
1626
1707
|
workerIdentity = identity;
|
|
1627
1708
|
this.renewMutex({ ...params, fencingToken: run.fencing_token });
|
|
1709
|
+
lifecycle?.renew();
|
|
1628
1710
|
});
|
|
1629
|
-
worker.send({ command: params.command, cwd: params.cwd ?? this.coordDb.repoRoot, mutexName: params.mutexName });
|
|
1711
|
+
worker.send({ command: prepared?.command ?? params.command, env: prepared?.env, cwd: params.cwd ?? this.coordDb.repoRoot, mutexName: params.mutexName });
|
|
1630
1712
|
} catch (err) {
|
|
1631
1713
|
stop(err instanceof Error ? err : new Error(String(err)));
|
|
1632
1714
|
}
|
|
@@ -1674,6 +1756,7 @@ var ResourceBroker = class _ResourceBroker {
|
|
|
1674
1756
|
this.coordDb.db.prepare("DELETE FROM mutex_waiters WHERE mutex_name=? AND request_id=? AND session_epoch=?").run(params.mutexName, requestId, epoch);
|
|
1675
1757
|
if (run && !this.runIsAlive(run)) this.clearRun(run);
|
|
1676
1758
|
if (run && run.worker_pid === null) this.clearRun(run);
|
|
1759
|
+
if (!this.getRun(params.mutexName)) lifecycle?.release();
|
|
1677
1760
|
});
|
|
1678
1761
|
} finally {
|
|
1679
1762
|
this.activeExecutions--;
|
|
@@ -3415,6 +3498,7 @@ var CoWWorktreeManager = class _CoWWorktreeManager {
|
|
|
3415
3498
|
|
|
3416
3499
|
// src/core/engine.ts
|
|
3417
3500
|
var KithEngine = class {
|
|
3501
|
+
activeClaimWaits = 0;
|
|
3418
3502
|
coordDb;
|
|
3419
3503
|
fencing;
|
|
3420
3504
|
locks;
|
|
@@ -3567,6 +3651,22 @@ ${lines.join("\n")}` : "";
|
|
|
3567
3651
|
const worktree = intent.ok && params.isolateWorktree ? this.worktrees.spawnWorktree({ agentId: params.agentId }) : null;
|
|
3568
3652
|
return { ...intent, worktree };
|
|
3569
3653
|
}
|
|
3654
|
+
async beginWorkWhenAvailable(params) {
|
|
3655
|
+
return this.waitForOwnedClaim(params, () => this.beginWork(params));
|
|
3656
|
+
}
|
|
3657
|
+
async acquireLocksWhenAvailable(params) {
|
|
3658
|
+
return this.waitForOwnedClaim(params, () => this.locks.acquireLocks(params));
|
|
3659
|
+
}
|
|
3660
|
+
async waitForOwnedClaim(params, attempt) {
|
|
3661
|
+
this.activeClaimWaits++;
|
|
3662
|
+
try {
|
|
3663
|
+
return await waitForClaim({ ...params, attempt, heartbeat: () => {
|
|
3664
|
+
this.fencing.heartbeat(params.agentId);
|
|
3665
|
+
} });
|
|
3666
|
+
} finally {
|
|
3667
|
+
this.activeClaimWaits--;
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3570
3670
|
completeWork(params) {
|
|
3571
3671
|
this.fencing.assertSession(params.agentId);
|
|
3572
3672
|
const agent = this.fencing.getAgent(params.agentId);
|
|
@@ -3587,6 +3687,7 @@ ${lines.join("\n")}` : "";
|
|
|
3587
3687
|
return { ok: true, completedTasks, mergeResult };
|
|
3588
3688
|
}
|
|
3589
3689
|
close() {
|
|
3690
|
+
if (this.activeClaimWaits) throw new Error("Await or cancel active file claim waits before closing Kith");
|
|
3590
3691
|
if (this.broker.hasActiveCommands()) throw new Error("Await or cancel active mutex commands before closing Kith");
|
|
3591
3692
|
this.coordDb.close();
|
|
3592
3693
|
}
|