@gajae-code/coding-agent 0.18.5 → 0.18.7
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 +140 -0
- package/dist/types/capability/types.d.ts +4 -0
- package/dist/types/cli-commands.d.ts +10 -0
- package/dist/types/cli-ordinary.d.ts +13 -0
- package/dist/types/cli.d.ts +12 -3
- package/dist/types/commands/contribution-prep.d.ts +0 -3
- package/dist/types/commands/sdk.d.ts +3 -0
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-profile-activation.d.ts +19 -0
- package/dist/types/config/settings.d.ts +4 -0
- package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
- package/dist/types/discovery/helpers.d.ts +2 -2
- package/dist/types/edit/modes/replace.d.ts +3 -3
- package/dist/types/edit/streaming.d.ts +2 -2
- package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
- package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
- package/dist/types/sdk/broker/discovery.d.ts +8 -0
- package/dist/types/sdk/broker/ensure.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
- package/dist/types/sdk/broker/runtime.d.ts +1 -1
- package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +4 -2
- package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
- package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +6 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/sdk/session.d.ts +13 -0
- package/dist/types/session/agent-session.d.ts +5 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
- package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
- package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
- package/dist/types/session/session-retirement.d.ts +47 -0
- package/dist/types/session/session-storage.d.ts +0 -46
- package/dist/types/session/session-transcript-header.d.ts +2 -0
- package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
- package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
- package/dist/types/session/task-artifact-owner.d.ts +10 -0
- package/dist/types/tools/ask-contract.d.ts +75 -254
- package/dist/types/tools/bash.d.ts +1 -1
- package/package.json +7 -7
- package/scripts/build-sdk-package-smoke.ts +5 -0
- package/scripts/resident-memory-bench.ts +174 -20
- package/src/capability/index.ts +1 -0
- package/src/capability/types.ts +4 -0
- package/src/cli/config-cli.ts +8 -18
- package/src/cli/public-command-entry.ts +5 -0
- package/src/cli/ssh-cli.ts +6 -3
- package/src/cli/web-search-cli.ts +9 -3
- package/src/cli-commands.ts +14 -0
- package/src/cli-main.ts +7 -8
- package/src/cli-ordinary.ts +197 -0
- package/src/cli.ts +25 -182
- package/src/commands/contribution-prep.ts +1 -3
- package/src/commands/sdk.ts +171 -21
- package/src/commands/update.ts +15 -1
- package/src/config/file-lock.ts +91 -2
- package/src/config/model-profile-activation.ts +70 -4
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +23 -31
- package/src/config/model-resolver.ts +9 -1
- package/src/config/settings.ts +44 -4
- package/src/coordinator-mcp/event-webhook.ts +19 -14
- package/src/coordinator-mcp/policy.ts +14 -4
- package/src/coordinator-mcp/server.ts +142 -7
- package/src/coordinator-mcp/session-reaper.ts +9 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/discovery/helpers.ts +9 -3
- package/src/discovery/ssh.ts +8 -3
- package/src/edit/modes/replace.ts +6 -6
- package/src/edit/streaming.ts +15 -15
- package/src/exec/bash-shell-supervisor.ts +60 -3
- package/src/gjc-runtime/gc-runtime.ts +2 -1
- package/src/hindsight/config.ts +9 -3
- package/src/internal-urls/docs-index.generated.ts +11 -10
- package/src/memories/index.ts +8 -2
- package/src/modes/acp/acp-agent.ts +724 -370
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/prompts/tools/ask.md +2 -0
- package/src/runtime-mcp/client.ts +13 -4
- package/src/runtime-mcp/elicitation.ts +142 -0
- package/src/sdk/broker/broker.ts +54 -1
- package/src/sdk/broker/daemon-entry.ts +67 -6
- package/src/sdk/broker/discovery.ts +8 -0
- package/src/sdk/broker/ensure.ts +156 -20
- package/src/sdk/broker/lifecycle-ledger.ts +110 -12
- package/src/sdk/broker/lifecycle.ts +334 -348
- package/src/sdk/broker/runtime.ts +5 -1
- package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
- package/src/sdk/broker/transport.ts +5 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
- package/src/sdk/bus/index.ts +3 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +15 -12
- package/src/sdk/bus/telegram-daemon.ts +38 -3
- package/src/sdk/cli/diagnostics-cli.ts +247 -0
- package/src/sdk/diagnostics/observe-broker.ts +654 -0
- package/src/sdk/host/control/dispatch.ts +52 -4
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +6 -0
- package/src/sdk/host/session-runtime.test.ts +83 -4
- package/src/sdk/host/session-runtime.ts +15 -7
- package/src/sdk/index.ts +1 -0
- package/src/sdk/lifecycle-session.ts +1 -0
- package/src/sdk/mcp/server.ts +15 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/router/session-router.ts +26 -3
- package/src/sdk/session.ts +51 -8
- package/src/session/agent-session.ts +169 -43
- package/src/session/blob-store.ts +78 -33
- package/src/session/internal/managed-session-storage.ts +203 -4
- package/src/session/internal/task-artifact-owner-access.ts +229 -0
- package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
- package/src/session/session-retirement.ts +155 -0
- package/src/session/session-storage.ts +1 -160
- package/src/session/session-transcript-header.ts +17 -0
- package/src/session/task-artifact-owner-codec.ts +578 -0
- package/src/session/task-artifact-owner-retirement.ts +475 -0
- package/src/session/task-artifact-owner.ts +289 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
- package/src/setup/hermes-setup.ts +4 -1
- package/src/slash-commands/builtin-registry.ts +8 -5
- package/src/tools/ask-contract.test.ts +76 -1
- package/src/tools/ask-contract.ts +57 -6
- package/src/tools/ask.ts +16 -7
- package/src/tools/bash.ts +38 -29
- package/src/tools/descriptors.test.ts +29 -0
- package/src/tools/tool-catalog.generated.ts +7 -4
- package/src/web/search/index.ts +2 -2
package/src/commands/sdk.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import * as nodeFs from "node:fs";
|
|
3
4
|
import * as fs from "node:fs/promises";
|
|
4
5
|
import * as os from "node:os";
|
|
5
6
|
import * as path from "node:path";
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
exactRemoveDirectoryTree,
|
|
9
|
+
exactUnlinkDirect,
|
|
10
|
+
type NativeDirectoryTreeSnapshot,
|
|
11
|
+
snapshotDirectoryTree,
|
|
12
|
+
} from "@gajae-code/natives";
|
|
7
13
|
import { logger, postmortem } from "@gajae-code/utils";
|
|
8
14
|
import { CliParseError, Command } from "@gajae-code/utils/cli";
|
|
9
15
|
import type { Args as ParsedArgs } from "../cli/args";
|
|
@@ -23,6 +29,7 @@ import {
|
|
|
23
29
|
writeBrokerStartupExitRecordBounded,
|
|
24
30
|
} from "../sdk/broker/broker-exit";
|
|
25
31
|
import { readBrokerDiscovery } from "../sdk/broker/discovery";
|
|
32
|
+
import { type EndpointFileRead, readEndpointFile } from "../sdk/broker/endpoint-authority";
|
|
26
33
|
import {
|
|
27
34
|
emitBrokerStartupTestSignal,
|
|
28
35
|
reconcileBrokerGenerationForStartup,
|
|
@@ -42,6 +49,7 @@ import {
|
|
|
42
49
|
writeSessionLifecycleReady,
|
|
43
50
|
} from "../sdk/broker/lifecycle";
|
|
44
51
|
import { processIncarnation } from "../sdk/broker/process-incarnation";
|
|
52
|
+
import { resolveSdkInternalSpawnCommand } from "../sdk/broker/runtime";
|
|
45
53
|
import { writeBrokerStartupFailureMarker } from "../sdk/broker/startup-failure";
|
|
46
54
|
import { runSdkStderrDrainer } from "../sdk/broker/stderr-drainer";
|
|
47
55
|
import { renderSdkSearchTable, runSdkSearch, runSdkSessionCli } from "../sdk/cli";
|
|
@@ -651,6 +659,7 @@ export async function runSessionHost(
|
|
|
651
659
|
let startupComplete = false;
|
|
652
660
|
let readinessPublicationCleanupComplete = true;
|
|
653
661
|
let revokePendingReadinessMarker: (() => Promise<boolean>) | undefined;
|
|
662
|
+
let revokePublishedReadinessMarker: (() => Promise<boolean>) | undefined;
|
|
654
663
|
let readinessRevocation: Promise<boolean> | undefined;
|
|
655
664
|
let startupInterruption: SdkStartupFailure | undefined;
|
|
656
665
|
const interrupted = Promise.withResolvers<SdkStartupFailure>();
|
|
@@ -1265,20 +1274,76 @@ export async function runSessionHost(
|
|
|
1265
1274
|
return failureRollback;
|
|
1266
1275
|
};
|
|
1267
1276
|
const sessionEndpointPath = path.join(request.stateRoot, "sdk", `${request.sessionId}.json`);
|
|
1268
|
-
const
|
|
1269
|
-
|
|
1277
|
+
const removeOwnedSessionEndpoint = async (): Promise<boolean> => {
|
|
1278
|
+
try {
|
|
1279
|
+
await fs.stat(sessionEndpointPath);
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
|
|
1282
|
+
return false;
|
|
1283
|
+
}
|
|
1284
|
+
const endpoint: EndpointFileRead | undefined = await readEndpointFile(sessionEndpointPath);
|
|
1285
|
+
if (!endpoint) return false;
|
|
1286
|
+
let parsed: { pid?: unknown; sessionId?: unknown };
|
|
1287
|
+
try {
|
|
1288
|
+
parsed = JSON.parse(endpoint.source) as { pid?: unknown; sessionId?: unknown };
|
|
1289
|
+
} catch {
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
if (parsed.pid !== process.pid || parsed.sessionId !== request.sessionId) {
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
const removal = (() => {
|
|
1296
|
+
try {
|
|
1297
|
+
return exactUnlinkDirect(sessionEndpointPath, {
|
|
1298
|
+
dev: endpoint.dev,
|
|
1299
|
+
ino: endpoint.ino,
|
|
1300
|
+
nlink: endpoint.nlink,
|
|
1301
|
+
size: endpoint.size,
|
|
1302
|
+
mtimeNs: endpoint.mtimeNs,
|
|
1303
|
+
quarantineName: `.gjc-delete-endpoint-${request.sessionId}-${randomUUID()}`,
|
|
1304
|
+
sha256: createHash("sha256").update(endpoint.source).digest("hex"),
|
|
1305
|
+
allowHardLink: true,
|
|
1306
|
+
});
|
|
1307
|
+
} catch (error) {
|
|
1308
|
+
logger.warn("SDK host endpoint cleanup failed", { error: String(error) });
|
|
1309
|
+
return undefined;
|
|
1310
|
+
}
|
|
1311
|
+
})();
|
|
1312
|
+
if (!removal) return false;
|
|
1313
|
+
return (
|
|
1314
|
+
removal.ok ||
|
|
1315
|
+
removal.code === "not_found" ||
|
|
1316
|
+
(removal.code === "cleanup_pending" &&
|
|
1317
|
+
removal.payloadDurable === true &&
|
|
1318
|
+
!removal.retainedSuccessorPath &&
|
|
1319
|
+
!removal.retainedUnknownPath)
|
|
1320
|
+
);
|
|
1321
|
+
};
|
|
1322
|
+
let unregisterHostPostmortem: (() => void) | undefined;
|
|
1323
|
+
let sessionDisposalExit: Promise<void> | undefined;
|
|
1324
|
+
const exitAfterSessionDisposal = async (reason?: "detached_idle", exitProcess = true): Promise<void> => {
|
|
1270
1325
|
let failure: SdkStartupFailure | undefined;
|
|
1326
|
+
let ownsEndpoint = false;
|
|
1327
|
+
const endpointCleanup = (async (): Promise<boolean> => {
|
|
1328
|
+
let complete = await removeOwnedSessionEndpoint();
|
|
1329
|
+
for (let attempt = 0; !complete && attempt < 20; attempt += 1) {
|
|
1330
|
+
await Bun.sleep(25);
|
|
1331
|
+
complete = await removeOwnedSessionEndpoint();
|
|
1332
|
+
}
|
|
1333
|
+
return complete;
|
|
1334
|
+
})();
|
|
1335
|
+
let [endpointCleanupComplete, readinessRevoked] = await Promise.all([
|
|
1336
|
+
endpointCleanup,
|
|
1337
|
+
revokePublishedReadinessMarker?.() ?? Promise.resolve(true),
|
|
1338
|
+
]);
|
|
1339
|
+
await disposeSession(reason);
|
|
1340
|
+
endpointCleanupComplete = await removeOwnedSessionEndpoint();
|
|
1271
1341
|
try {
|
|
1272
1342
|
const endpoint = JSON.parse(await fs.readFile(sessionEndpointPath, "utf8")) as {
|
|
1273
1343
|
pid?: unknown;
|
|
1274
1344
|
sessionId?: unknown;
|
|
1275
1345
|
};
|
|
1276
|
-
|
|
1277
|
-
failure = {
|
|
1278
|
-
phase: "startup",
|
|
1279
|
-
reason: "failed",
|
|
1280
|
-
message: `SDK host endpoint remained after graceful shutdown: ${request.sessionId}`,
|
|
1281
|
-
};
|
|
1346
|
+
ownsEndpoint = endpoint.pid === process.pid && endpoint.sessionId === request.sessionId;
|
|
1282
1347
|
} catch (error) {
|
|
1283
1348
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
1284
1349
|
failure = {
|
|
@@ -1286,27 +1351,57 @@ export async function runSessionHost(
|
|
|
1286
1351
|
reason: "failed",
|
|
1287
1352
|
message: `SDK host endpoint cleanup could not be verified: ${request.sessionId}`,
|
|
1288
1353
|
};
|
|
1289
|
-
}
|
|
1354
|
+
} else endpointCleanupComplete = true;
|
|
1290
1355
|
}
|
|
1356
|
+
if (!ownsEndpoint && !failure && !readinessRevoked && revokePublishedReadinessMarker) {
|
|
1357
|
+
failure = {
|
|
1358
|
+
phase: "startup",
|
|
1359
|
+
reason: "failed",
|
|
1360
|
+
message: `SDK host readiness marker cleanup could not be verified: ${request.sessionId}`,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
if (ownsEndpoint && !endpointCleanupComplete && !failure)
|
|
1364
|
+
failure = {
|
|
1365
|
+
phase: "startup",
|
|
1366
|
+
reason: "failed",
|
|
1367
|
+
message: `SDK host endpoint remained after graceful shutdown: ${request.sessionId}`,
|
|
1368
|
+
};
|
|
1291
1369
|
if (failure) {
|
|
1292
1370
|
process.exitCode = 1;
|
|
1293
1371
|
process.stderr.write(`${failure.message}\n`);
|
|
1294
1372
|
await writeFailure(failure, rollback.result).catch(() => {});
|
|
1295
1373
|
}
|
|
1296
|
-
|
|
1374
|
+
unregisterHostPostmortem?.();
|
|
1375
|
+
unregisterHostPostmortem = undefined;
|
|
1376
|
+
if (exitProcess) process.exit(process.exitCode ?? 0);
|
|
1297
1377
|
};
|
|
1298
1378
|
let stopping = false;
|
|
1379
|
+
const requestSessionStop = (reason?: "detached_idle", exitProcess = true): Promise<void> => {
|
|
1380
|
+
if (stopping) return sessionDisposalExit ?? Promise.resolve();
|
|
1381
|
+
stopping = true;
|
|
1382
|
+
sessionDisposalExit = exitAfterSessionDisposal(reason, exitProcess);
|
|
1383
|
+
return sessionDisposalExit;
|
|
1384
|
+
};
|
|
1299
1385
|
const stop = (reason?: "detached_idle") => {
|
|
1300
1386
|
if (startupComplete) {
|
|
1301
|
-
|
|
1302
|
-
stopping = true;
|
|
1303
|
-
void exitAfterSessionDisposal(reason);
|
|
1387
|
+
void requestSessionStop(reason);
|
|
1304
1388
|
return;
|
|
1305
1389
|
}
|
|
1306
1390
|
const failure = capability.normalizeFailure("startup", "failed", "SDK lifecycle host terminated.");
|
|
1307
1391
|
interruptStartup(failure);
|
|
1308
1392
|
};
|
|
1309
|
-
const
|
|
1393
|
+
const registerHostPostmortem = (): void => {
|
|
1394
|
+
if (unregisterHostPostmortem) return;
|
|
1395
|
+
unregisterHostPostmortem = postmortem.register("sdk-session-host:exit", async reason => {
|
|
1396
|
+
if (reason !== postmortem.Reason.SIGTERM && reason !== postmortem.Reason.SIGINT) return;
|
|
1397
|
+
// The postmortem signal authority defaults to 143/130; a session host
|
|
1398
|
+
// signal is graceful and must preserve its cleanup result instead.
|
|
1399
|
+
await requestSessionStop(undefined, false);
|
|
1400
|
+
process.exitCode = 0;
|
|
1401
|
+
process.exit(0);
|
|
1402
|
+
});
|
|
1403
|
+
};
|
|
1404
|
+
registerHostPostmortem();
|
|
1310
1405
|
|
|
1311
1406
|
try {
|
|
1312
1407
|
const startupThinkingLevel = request.modelId ? parseModelString(request.modelId)?.thinkingLevel : undefined;
|
|
@@ -1354,6 +1449,7 @@ export async function runSessionHost(
|
|
|
1354
1449
|
effectMarker,
|
|
1355
1450
|
() => startupInterruption === undefined && now() < request.semanticReadyDeadlineAt,
|
|
1356
1451
|
revoke => {
|
|
1452
|
+
revokePublishedReadinessMarker = revoke;
|
|
1357
1453
|
revokePendingReadinessMarker = revoke;
|
|
1358
1454
|
if (startupInterruption !== undefined) startReadinessRevocation(revoke);
|
|
1359
1455
|
else if (now() >= request.semanticReadyDeadlineAt) {
|
|
@@ -1388,8 +1484,6 @@ export async function runSessionHost(
|
|
|
1388
1484
|
revokePendingReadinessMarker = undefined;
|
|
1389
1485
|
readinessRevocation = undefined;
|
|
1390
1486
|
removeStartupSignalHandlers();
|
|
1391
|
-
process.once("SIGTERM", onReadySignal);
|
|
1392
|
-
process.once("SIGINT", onReadySignal);
|
|
1393
1487
|
} catch (error) {
|
|
1394
1488
|
if (error instanceof LifecycleReadinessCleanupError) constructionCleanupComplete = false;
|
|
1395
1489
|
const failure =
|
|
@@ -1402,8 +1496,6 @@ export async function runSessionHost(
|
|
|
1402
1496
|
await failAfterRollback(durableFailure);
|
|
1403
1497
|
} finally {
|
|
1404
1498
|
removeStartupSignalHandlers();
|
|
1405
|
-
process.removeListener("SIGTERM", onReadySignal);
|
|
1406
|
-
process.removeListener("SIGINT", onReadySignal);
|
|
1407
1499
|
}
|
|
1408
1500
|
throw error;
|
|
1409
1501
|
}
|
|
@@ -1441,6 +1533,7 @@ export async function runSessionHost(
|
|
|
1441
1533
|
|
|
1442
1534
|
export type SdkInternalArgv =
|
|
1443
1535
|
| { action: "broker-internal"; agentDir: string }
|
|
1536
|
+
| { action: "broker-trampoline-internal"; agentDir: string }
|
|
1444
1537
|
| { action: "session-host-internal" }
|
|
1445
1538
|
| { action: "stderr-drain-internal"; logPath: string; maxBytes: number };
|
|
1446
1539
|
|
|
@@ -1455,6 +1548,14 @@ export function parseSdkInternalArgv(argv: readonly string[]): SdkInternalArgv {
|
|
|
1455
1548
|
isSafeSdkInternalAgentDir(argv[2])
|
|
1456
1549
|
)
|
|
1457
1550
|
return { action: "broker-internal", agentDir: argv[2] };
|
|
1551
|
+
if (
|
|
1552
|
+
argv[0] === "broker-trampoline-internal" &&
|
|
1553
|
+
argv.length === 3 &&
|
|
1554
|
+
argv[1] === "--agent-dir" &&
|
|
1555
|
+
typeof argv[2] === "string" &&
|
|
1556
|
+
isSafeSdkInternalAgentDir(argv[2])
|
|
1557
|
+
)
|
|
1558
|
+
return { action: "broker-trampoline-internal", agentDir: argv[2] };
|
|
1458
1559
|
if (
|
|
1459
1560
|
argv[0] === "stderr-drain-internal" &&
|
|
1460
1561
|
argv.length === 5 &&
|
|
@@ -1494,6 +1595,7 @@ export default class Sdk extends Command {
|
|
|
1494
1595
|
async run(): Promise<void> {
|
|
1495
1596
|
if (
|
|
1496
1597
|
this.argv[0] !== "broker-internal" &&
|
|
1598
|
+
this.argv[0] !== "broker-trampoline-internal" &&
|
|
1497
1599
|
this.argv[0] !== "session-host-internal" &&
|
|
1498
1600
|
this.argv[0] !== "stderr-drain-internal"
|
|
1499
1601
|
) {
|
|
@@ -1596,7 +1698,55 @@ export default class Sdk extends Command {
|
|
|
1596
1698
|
}
|
|
1597
1699
|
|
|
1598
1700
|
const internal = parseSdkInternalArgv(this.argv);
|
|
1599
|
-
if (internal.action === "broker-internal")
|
|
1701
|
+
if (internal.action === "broker-trampoline-internal") {
|
|
1702
|
+
if (process.platform === "win32") throw new CliParseError("Broker trampoline is unavailable on Windows.");
|
|
1703
|
+
const command = resolveSdkInternalSpawnCommand("broker-internal");
|
|
1704
|
+
let child: ChildProcess;
|
|
1705
|
+
try {
|
|
1706
|
+
child = spawn(command.file, [...command.args, "--agent-dir", internal.agentDir], {
|
|
1707
|
+
detached: true,
|
|
1708
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
1709
|
+
env: command.env,
|
|
1710
|
+
...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
|
|
1711
|
+
});
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
throw new Error("Broker trampoline could not start broker.", { cause: error });
|
|
1714
|
+
}
|
|
1715
|
+
let spawnError: Error | undefined;
|
|
1716
|
+
child.once("error", error => {
|
|
1717
|
+
spawnError = error;
|
|
1718
|
+
});
|
|
1719
|
+
if (child.pid === undefined) throw new Error("Broker trampoline could not start broker.");
|
|
1720
|
+
const incarnation = processIncarnation(child.pid);
|
|
1721
|
+
if (!incarnation) {
|
|
1722
|
+
try {
|
|
1723
|
+
child.kill("SIGKILL");
|
|
1724
|
+
} catch {
|
|
1725
|
+
// The child may already have exited; either way identity was not established.
|
|
1726
|
+
}
|
|
1727
|
+
throw new Error("Broker trampoline could not observe broker identity.");
|
|
1728
|
+
}
|
|
1729
|
+
if (spawnError) throw new Error("Broker trampoline could not start broker.", { cause: spawnError });
|
|
1730
|
+
const stdoutWrite = Promise.withResolvers<void>();
|
|
1731
|
+
process.stdout.write(`${child.pid}\t${incarnation}\n`, error => {
|
|
1732
|
+
if (error) stdoutWrite.reject(error);
|
|
1733
|
+
else stdoutWrite.resolve();
|
|
1734
|
+
});
|
|
1735
|
+
try {
|
|
1736
|
+
await stdoutWrite.promise;
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
try {
|
|
1739
|
+
child.kill("SIGKILL");
|
|
1740
|
+
} catch {
|
|
1741
|
+
// The child may already have exited; reporting failure remains authoritative.
|
|
1742
|
+
}
|
|
1743
|
+
throw error;
|
|
1744
|
+
}
|
|
1745
|
+
child.unref();
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (internal.action === "broker-internal" || internal.action === "session-host-internal")
|
|
1749
|
+
usePostmortemSignalExitAuthority();
|
|
1600
1750
|
if (internal.action === "session-host-internal") {
|
|
1601
1751
|
await runSessionHost();
|
|
1602
1752
|
return;
|
package/src/commands/update.ts
CHANGED
|
@@ -13,6 +13,20 @@ import {
|
|
|
13
13
|
} from "../config/update-channel";
|
|
14
14
|
import { initTheme } from "../modes/theme/theme";
|
|
15
15
|
|
|
16
|
+
export async function runUpdateRecoveryCommand(
|
|
17
|
+
recover: () => Promise<void> = () => runManagedNotifyRecovery({}),
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
await recover();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
23
|
+
process.stderr.write(
|
|
24
|
+
`Post-update recovery failed: ${reason}. For each configured notification daemon (telegram, discord, or slack), manually run 'gjc daemon stop <kind> --force' and then 'gjc daemon reload <kind>', then run 'gjc notify recovery'.\n`,
|
|
25
|
+
);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
export default class Update extends Command {
|
|
17
31
|
static description = "Check for and install updates";
|
|
18
32
|
|
|
@@ -35,7 +49,7 @@ export default class Update extends Command {
|
|
|
35
49
|
async run(): Promise<void> {
|
|
36
50
|
const { args, flags } = await this.parse(Update);
|
|
37
51
|
if (args.action === "update-recovery") {
|
|
38
|
-
await
|
|
52
|
+
await runUpdateRecoveryCommand();
|
|
39
53
|
return;
|
|
40
54
|
}
|
|
41
55
|
let channel: UpdateChannel | undefined;
|
package/src/config/file-lock.ts
CHANGED
|
@@ -610,7 +610,7 @@ async function classifyFileLockRemovalTransition(
|
|
|
610
610
|
if (!observation) return "active";
|
|
611
611
|
const info = parseLockInfoBytes(observation.bytes);
|
|
612
612
|
if (info) {
|
|
613
|
-
const stale = await staleLockSnapshot(transitionPath, 0, ownerHostId, previousOwnerHostIds);
|
|
613
|
+
const stale = await staleLockSnapshot(transitionPath, 0, ownerHostId, previousOwnerHostIds, undefined, true);
|
|
614
614
|
return stale.stale ? "abandoned" : "active";
|
|
615
615
|
}
|
|
616
616
|
|
|
@@ -698,6 +698,74 @@ async function adoptOrphanedFileLockRemovalTransition(lockPath: string, orphanAg
|
|
|
698
698
|
return false;
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
+
async function adoptAbandonedFileLockRemovalTransition(
|
|
702
|
+
lockPath: string,
|
|
703
|
+
ownerHostId?: string,
|
|
704
|
+
previousOwnerHostIds: readonly string[] = [],
|
|
705
|
+
): Promise<boolean> {
|
|
706
|
+
const transitionPath = fileLockRemovalTransitionPath(lockPath);
|
|
707
|
+
const stale = await staleLockSnapshot(transitionPath, 0, ownerHostId, previousOwnerHostIds, undefined, true);
|
|
708
|
+
if (!stale.stale || !stale.identity) return false;
|
|
709
|
+
if (!(await isNativeExactRemovalUsable())) return false;
|
|
710
|
+
|
|
711
|
+
let captured: NativeDirectoryTreeResult;
|
|
712
|
+
try {
|
|
713
|
+
captured = nativeFileLockBindings().snapshotDirectoryTree(transitionPath);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (isTransientReleaseError(error)) return false;
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
if (!captured.ok || !captured.snapshot) return false;
|
|
719
|
+
const snapshot = captured.snapshot;
|
|
720
|
+
const root = snapshot.entries.find(entry => entry.relativePath === "");
|
|
721
|
+
const infoEntry = snapshot.entries.find(entry => entry.relativePath === "info");
|
|
722
|
+
if (
|
|
723
|
+
root?.kind !== "directory" ||
|
|
724
|
+
infoEntry?.kind !== "file" ||
|
|
725
|
+
snapshot.entries.some(
|
|
726
|
+
entry =>
|
|
727
|
+
entry.relativePath !== "" &&
|
|
728
|
+
entry.relativePath !== "info" &&
|
|
729
|
+
entry.kind !== "directory" &&
|
|
730
|
+
(entry.kind !== "file" || entry.size !== "0"),
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
return false;
|
|
734
|
+
const currentIdentity = await captureFileLockDirIdentity(transitionPath);
|
|
735
|
+
if (!currentIdentity || !sameStableFileLockIdentity(currentIdentity, stale.identity)) return false;
|
|
736
|
+
if (
|
|
737
|
+
!nativeFileLockInfoMatchesStableIdentity(snapshot.rootDev, snapshot.rootIno, infoEntry, stale.identity) ||
|
|
738
|
+
!nativeFileLockInfoMatchesContentEvidence(infoEntry, stale.identity)
|
|
739
|
+
)
|
|
740
|
+
return false;
|
|
741
|
+
|
|
742
|
+
let removal: NativeExactUnlinkResult;
|
|
743
|
+
try {
|
|
744
|
+
removal = nativeFileLockBindings().exactRemoveDirectoryTree(transitionPath, snapshot);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (isTransientReleaseError(error)) return false;
|
|
747
|
+
throw error;
|
|
748
|
+
}
|
|
749
|
+
if (removal.ok === true) return removal.code === undefined && Object.keys(removal).every(key => key === "ok");
|
|
750
|
+
if (
|
|
751
|
+
removal.ok === false &&
|
|
752
|
+
removal.code === "cleanup_pending" &&
|
|
753
|
+
removal.payloadDurable === true &&
|
|
754
|
+
removal.detachedPath !== undefined &&
|
|
755
|
+
path.resolve(removal.detachedPath) === path.resolve(transitionPath) &&
|
|
756
|
+
removal.retainedSuccessorPath === undefined &&
|
|
757
|
+
removal.retainedPlaceholderPath === undefined &&
|
|
758
|
+
removal.retainedUnknownPath === undefined &&
|
|
759
|
+
Object.keys(removal).every(key => ["ok", "code", "payloadDurable", "detachedPath"].includes(key))
|
|
760
|
+
)
|
|
761
|
+
return await removeDetachedLockQuarantineOnDisk(transitionPath, snapshot.rootDev, snapshot.rootIno);
|
|
762
|
+
return (
|
|
763
|
+
removal.ok === false &&
|
|
764
|
+
removal.code === "not_found" &&
|
|
765
|
+
Object.keys(removal).every(key => key === "ok" || key === "code")
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
701
769
|
function sameFileLockTreeAfterPublication(
|
|
702
770
|
staged: NativeDirectoryTreeSnapshot,
|
|
703
771
|
published: NativeDirectoryTreeSnapshot,
|
|
@@ -1488,6 +1556,14 @@ async function staleLockSnapshot(
|
|
|
1488
1556
|
ownerHostId?: string,
|
|
1489
1557
|
previousOwnerHostIds: readonly string[] = [],
|
|
1490
1558
|
startTimeCache?: Map<string, string | null>,
|
|
1559
|
+
// `.removing` transitions are internal cleanup state this codebase's own removal
|
|
1560
|
+
// machinery produced, never a live holder's public identity: a hostless transition
|
|
1561
|
+
// record predates host-id stamping and is reclaimable by local liveness proof alone,
|
|
1562
|
+
// regardless of the acquirer's own host id. Active `<file>.lock` holders keep the
|
|
1563
|
+
// stricter fail-closed rule (see `lockRecordIsForeignHost`) because that record can
|
|
1564
|
+
// belong to a genuinely foreign process; only the two transition-specific callers
|
|
1565
|
+
// below set this.
|
|
1566
|
+
hostlessTransitionIsLocal = false,
|
|
1491
1567
|
): Promise<LockStaleSnapshot> {
|
|
1492
1568
|
// Capture the root and info inode BEFORE asking whether the owner is stale. A later
|
|
1493
1569
|
// snapshot alone would let a copied successor inherit the stale verdict's authority.
|
|
@@ -1521,7 +1597,8 @@ async function staleLockSnapshot(
|
|
|
1521
1597
|
// A host-qualified lock may only be reclaimed after proving that its owner is
|
|
1522
1598
|
// local. Foreign and malformed host-qualified records fail closed: PID values
|
|
1523
1599
|
// and clocks are not meaningful across hosts.
|
|
1524
|
-
|
|
1600
|
+
const foreignCheckHostId = hostlessTransitionIsLocal && info.owner_host_id === undefined ? undefined : ownerHostId;
|
|
1601
|
+
if (lockRecordIsForeignHost(info, foreignCheckHostId, previousOwnerHostIds)) return { stale: false };
|
|
1525
1602
|
if (ownerIncarnationChanged(info, startTimeCache)) {
|
|
1526
1603
|
if (!judgedIdentity) return { stale: false };
|
|
1527
1604
|
let currentIdentity: GenericFileLockDirIdentity | null;
|
|
@@ -1752,6 +1829,18 @@ async function tryAcquireLock(
|
|
|
1752
1829
|
}
|
|
1753
1830
|
if (transitionState === "orphan_transition")
|
|
1754
1831
|
return { kind: "orphan_transition", path: fileLockRemovalTransitionPath(destinationPath) };
|
|
1832
|
+
if (transitionState === "abandoned") {
|
|
1833
|
+
if (await adoptAbandonedFileLockRemovalTransition(destinationPath, ownerHostId, previousOwnerHostIds)) {
|
|
1834
|
+
transitionState = await classifyFileLockRemovalTransition(
|
|
1835
|
+
destinationPath,
|
|
1836
|
+
orphanTransitionAgeMs,
|
|
1837
|
+
ownerHostId,
|
|
1838
|
+
previousOwnerHostIds,
|
|
1839
|
+
);
|
|
1840
|
+
} else {
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1755
1844
|
if (transitionState !== null) return null;
|
|
1756
1845
|
const staged = snapshotDirectoryTree(canonicalPendingPath);
|
|
1757
1846
|
if (!staged.ok || !staged.snapshot) {
|
|
@@ -115,6 +115,7 @@ export interface PrepareModelProfileActivationOptions {
|
|
|
115
115
|
>
|
|
116
116
|
> & {
|
|
117
117
|
getError?: ModelRegistry["getError"];
|
|
118
|
+
authStorage?: ModelRegistry["authStorage"];
|
|
118
119
|
};
|
|
119
120
|
settings: Pick<Settings, "get" | "getGlobal" | "getOverride">;
|
|
120
121
|
profileName: string;
|
|
@@ -475,6 +476,53 @@ export class ModelProfileCredentialError extends Error {
|
|
|
475
476
|
}
|
|
476
477
|
}
|
|
477
478
|
|
|
479
|
+
interface SessionPinRegistry {
|
|
480
|
+
authStorage?: {
|
|
481
|
+
hasRuntimeApiKey(provider: string): boolean;
|
|
482
|
+
hasLiteralConfigApiKey(provider: string, owner?: object): boolean;
|
|
483
|
+
hasSessionCredentialUnavailable(provider: string, scopeId?: string): boolean;
|
|
484
|
+
};
|
|
485
|
+
getAuthStorageOwner?(): object;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Whether an unavailable session credential pin blocks `provider`.
|
|
490
|
+
*
|
|
491
|
+
* Mirrors `AuthStorage.getApiKey` precedence: a runtime `--api-key` override or a literal
|
|
492
|
+
* `models.yml` `apiKey` registered for this registry's owner is resolved before the
|
|
493
|
+
* unavailable-pin marker, so either one keeps the provider usable. An `apiKeyEnv` key does
|
|
494
|
+
* not: `getApiKey` prefers another stored api_key account over it, which would silently
|
|
495
|
+
* retarget the unavailable pin.
|
|
496
|
+
*/
|
|
497
|
+
export function isSessionCredentialPinBlocking(
|
|
498
|
+
registry: SessionPinRegistry,
|
|
499
|
+
provider: string,
|
|
500
|
+
sessionId: string | undefined,
|
|
501
|
+
): boolean {
|
|
502
|
+
const authStorage = registry.authStorage;
|
|
503
|
+
if (!authStorage) return false;
|
|
504
|
+
if (authStorage.hasRuntimeApiKey(provider)) return false;
|
|
505
|
+
if (authStorage.hasLiteralConfigApiKey(provider, registry.getAuthStorageOwner?.())) return false;
|
|
506
|
+
return authStorage.hasSessionCredentialUnavailable(provider, sessionId);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function getProfileProviderApiKey(
|
|
510
|
+
registry: PrepareModelProfileActivationOptions["modelRegistry"],
|
|
511
|
+
provider: string,
|
|
512
|
+
sessionId: string,
|
|
513
|
+
profileLabel: string,
|
|
514
|
+
): Promise<string | undefined> {
|
|
515
|
+
const pinBlocking = () => isSessionCredentialPinBlocking(registry, provider, sessionId);
|
|
516
|
+
if (pinBlocking()) throw new ModelProfileCredentialError(profileLabel, [provider]);
|
|
517
|
+
try {
|
|
518
|
+
return await registry.getApiKeyForProvider(provider, sessionId);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
// OAuth selection can invalidate a pin while the credential probe is running.
|
|
521
|
+
if (pinBlocking()) throw new ModelProfileCredentialError(profileLabel, [provider]);
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
478
526
|
export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
|
|
479
527
|
return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
|
|
480
528
|
}
|
|
@@ -762,7 +810,12 @@ export async function resolveModelProfileDefaultChain(options: {
|
|
|
762
810
|
])) {
|
|
763
811
|
let apiKey: string | undefined;
|
|
764
812
|
try {
|
|
765
|
-
apiKey = await
|
|
813
|
+
apiKey = await getProfileProviderApiKey(
|
|
814
|
+
options.modelRegistry,
|
|
815
|
+
provider,
|
|
816
|
+
options.credentialSessionId,
|
|
817
|
+
profileLabel,
|
|
818
|
+
);
|
|
766
819
|
} catch (error) {
|
|
767
820
|
if (requiredProviderSet.has(provider) && !alternativeSet.has(provider)) throw error;
|
|
768
821
|
continue;
|
|
@@ -787,7 +840,12 @@ export async function resolveModelProfileDefaultChain(options: {
|
|
|
787
840
|
const proxyApiKey =
|
|
788
841
|
proxyProvider === undefined
|
|
789
842
|
? undefined
|
|
790
|
-
: await
|
|
843
|
+
: await getProfileProviderApiKey(
|
|
844
|
+
options.modelRegistry,
|
|
845
|
+
proxyProvider,
|
|
846
|
+
options.credentialSessionId,
|
|
847
|
+
profileLabel,
|
|
848
|
+
);
|
|
791
849
|
if (
|
|
792
850
|
proxyProvider !== undefined &&
|
|
793
851
|
!isModelProfileProxyConfigured(proxyProvider, configuredProviderIds, proxyApiKey === kNoAuth)
|
|
@@ -886,6 +944,8 @@ export async function resolveModelProfileDefaultChain(options: {
|
|
|
886
944
|
aliasIntent: "preset-equivalent",
|
|
887
945
|
canonicalSessionId: null,
|
|
888
946
|
credentialSessionId: options.credentialSessionId,
|
|
947
|
+
isCredentialUnavailable: provider =>
|
|
948
|
+
isSessionCredentialPinBlocking(options.modelRegistry, provider, options.credentialSessionId),
|
|
889
949
|
},
|
|
890
950
|
);
|
|
891
951
|
return { profileName, entries: defaultChain, ...resolution };
|
|
@@ -1083,6 +1143,8 @@ async function resolveAndClampSelectorValue(
|
|
|
1083
1143
|
aliasIntent: options.aliasIntent,
|
|
1084
1144
|
canonicalSessionId: options.sessionId,
|
|
1085
1145
|
credentialSessionId: options.credentialSessionId,
|
|
1146
|
+
isCredentialUnavailable: provider =>
|
|
1147
|
+
isSessionCredentialPinBlocking(options.modelRegistry, provider, options.credentialSessionId),
|
|
1086
1148
|
},
|
|
1087
1149
|
);
|
|
1088
1150
|
resolved = {
|
|
@@ -1213,6 +1275,8 @@ async function concretizeProfileSelectorValue(
|
|
|
1213
1275
|
aliasIntent: "preset-equivalent",
|
|
1214
1276
|
canonicalSessionId: prepared.session.sessionId,
|
|
1215
1277
|
credentialSessionId,
|
|
1278
|
+
isCredentialUnavailable: provider =>
|
|
1279
|
+
isSessionCredentialPinBlocking(prepared.modelRegistry, provider, credentialSessionId),
|
|
1216
1280
|
},
|
|
1217
1281
|
)
|
|
1218
1282
|
: resolveModelRoleValue(selector, candidates, {
|
|
@@ -1322,7 +1386,7 @@ export async function prepareModelProfileActivation(
|
|
|
1322
1386
|
for (const provider of authenticationProbeProviders) {
|
|
1323
1387
|
let apiKey: string | undefined;
|
|
1324
1388
|
try {
|
|
1325
|
-
apiKey = await options.modelRegistry
|
|
1389
|
+
apiKey = await getProfileProviderApiKey(options.modelRegistry, provider, credentialSessionId, profileLabel);
|
|
1326
1390
|
} catch (error) {
|
|
1327
1391
|
if (requiredProviderSet.has(provider) && !alternativeSet.has(provider)) throw error;
|
|
1328
1392
|
continue;
|
|
@@ -1357,7 +1421,7 @@ export async function prepareModelProfileActivation(
|
|
|
1357
1421
|
const proxyApiKey =
|
|
1358
1422
|
proxyProvider === undefined
|
|
1359
1423
|
? undefined
|
|
1360
|
-
: await options.modelRegistry
|
|
1424
|
+
: await getProfileProviderApiKey(options.modelRegistry, proxyProvider, credentialSessionId, profileLabel);
|
|
1361
1425
|
if (proxyProvider !== undefined) {
|
|
1362
1426
|
const configuredProxyProviders = options.modelRegistry.getConfiguredProviderIds?.();
|
|
1363
1427
|
if (!isModelProfileProxyConfigured(proxyProvider, configuredProxyProviders, proxyApiKey === kNoAuth)) {
|
|
@@ -1462,6 +1526,8 @@ export async function prepareModelProfileActivation(
|
|
|
1462
1526
|
aliasIntent: "preset-equivalent",
|
|
1463
1527
|
canonicalSessionId: options.session.sessionId,
|
|
1464
1528
|
credentialSessionId,
|
|
1529
|
+
isCredentialUnavailable: provider =>
|
|
1530
|
+
isSessionCredentialPinBlocking(options.modelRegistry, provider, credentialSessionId),
|
|
1465
1531
|
},
|
|
1466
1532
|
);
|
|
1467
1533
|
const defaultModel = defaultResolution.model;
|
|
@@ -4,7 +4,10 @@ export const MODEL_PROFILE_DISCOVERY_QUERY = "models.profiles.list";
|
|
|
4
4
|
export const MODEL_PROFILE_ERROR_DETAIL_MAX_BYTES = 2048;
|
|
5
5
|
const REQUESTED_PROFILE_MAX_BYTES = 256;
|
|
6
6
|
|
|
7
|
-
const LEGACY_MODEL_PROFILE_ALIASES: ReadonlyMap<string, string> = new Map([
|
|
7
|
+
const LEGACY_MODEL_PROFILE_ALIASES: ReadonlyMap<string, string> = new Map([
|
|
8
|
+
["codex-standard", "codex-medium"],
|
|
9
|
+
["codex-sol61", "codex-pro"],
|
|
10
|
+
]);
|
|
8
11
|
|
|
9
12
|
export interface ModelProfileCatalogItem {
|
|
10
13
|
id: string;
|