@gajae-code/coding-agent 0.17.6 → 0.17.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 +34 -0
- package/dist/types/cli/public-command-errors.d.ts +2 -0
- package/dist/types/commands/sdk.d.ts +7 -1
- package/dist/types/config/model-profile-ownership.d.ts +74 -0
- package/dist/types/config/model-registry.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +32 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +4 -1
- package/dist/types/sdk/lifecycle/service.d.ts +2 -0
- package/dist/types/sdk/lifecycle-session.d.ts +5 -1
- package/dist/types/sdk/session-directory.d.ts +1 -0
- package/dist/types/sdk/session-list.d.ts +11 -0
- package/dist/types/sdk/startup-capability.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +18 -0
- package/dist/types/task/delegation-hint.d.ts +53 -0
- package/examples/README.md +3 -3
- package/examples/custom-tools/README.md +1 -1
- package/examples/hooks/README.md +1 -1
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +6 -0
- package/src/cli/config-cli.ts +2 -0
- package/src/cli/public-command-errors.ts +6 -0
- package/src/commands/sdk.ts +361 -117
- package/src/config/model-profile-ownership.ts +303 -0
- package/src/config/model-registry.ts +53 -14
- package/src/config/settings-schema.ts +81 -0
- package/src/internal-urls/docs-index.generated.ts +18 -17
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/selector-controller.ts +3 -29
- package/src/sdk/broker/broker.ts +120 -23
- package/src/sdk/broker/lifecycle.ts +89 -13
- package/src/sdk/cli/session-cli.ts +45 -9
- package/src/sdk/host/control/dispatch.ts +6 -0
- package/src/sdk/lifecycle/service.ts +186 -3
- package/src/sdk/lifecycle-session.ts +35 -8
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/protocol/operation-registry.ts +5 -1
- package/src/sdk/session-directory.ts +1 -1
- package/src/sdk/session-list.ts +47 -0
- package/src/sdk/session.ts +290 -114
- package/src/sdk/startup-capability.ts +2 -1
- package/src/session/agent-session.ts +102 -4
- package/src/session/internal/managed-session-scope.ts +81 -17
- package/src/session/session-manager.ts +70 -11
- package/src/setup/model-onboarding-guidance.ts +26 -0
- package/src/task/delegation-hint.ts +578 -0
package/src/commands/sdk.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from "../sdk/broker/ensure";
|
|
24
24
|
import { completeBrokerProcess } from "../sdk/broker/internal";
|
|
25
25
|
import {
|
|
26
|
+
LifecycleReadinessCleanupError,
|
|
26
27
|
type LifecycleTranscriptEvidence,
|
|
27
28
|
readSessionLifecycleLaunchRequest,
|
|
28
29
|
type SessionLifecycleLaunchRequest,
|
|
@@ -39,11 +40,16 @@ import { runSdkStderrDrainer } from "../sdk/broker/stderr-drainer";
|
|
|
39
40
|
import { renderSdkSearchTable, runSdkSearch, runSdkSessionCli } from "../sdk/cli";
|
|
40
41
|
import { renderSpawnTable, runSdkSpawn, SdkMasterCliError } from "../sdk/cli/master-cli";
|
|
41
42
|
import { runSdkGuidesCli } from "../sdk/guides/cli";
|
|
42
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
type CreateLifecycleAgentSessionResult,
|
|
45
|
+
createLifecycleAgentSession,
|
|
46
|
+
type SdkLifecycleStartupOwner,
|
|
47
|
+
} from "../sdk/lifecycle-session";
|
|
43
48
|
import { listManagedSessionCandidates, resolveManagedSessionScope } from "../sdk/session-directory";
|
|
44
49
|
import {
|
|
45
|
-
|
|
50
|
+
SdkStartupCapability,
|
|
46
51
|
type SdkStartupFailure,
|
|
52
|
+
type SdkStartupResult,
|
|
47
53
|
type SdkStartupRollbackResult,
|
|
48
54
|
SdkStartupRollbackTracker,
|
|
49
55
|
} from "../sdk/startup-capability";
|
|
@@ -487,12 +493,20 @@ export async function runSessionHost(
|
|
|
487
493
|
cwd?: string;
|
|
488
494
|
processIncarnation?: (pid: number) => string | undefined;
|
|
489
495
|
applyStartupModelProfiles?: typeof applyStartupModelProfiles;
|
|
496
|
+
initTheme?: typeof initTheme;
|
|
497
|
+
createLifecycleAgentSession?: typeof createLifecycleAgentSession;
|
|
498
|
+
writeSessionLifecycleReady?: typeof writeSessionLifecycleReady;
|
|
499
|
+
writeMcpConfig?: (filePath: string, contents: string) => Promise<number>;
|
|
490
500
|
} = {},
|
|
491
501
|
): Promise<void> {
|
|
492
502
|
const now = timing.now ?? Date.now;
|
|
493
503
|
const sleep = timing.sleep ?? (async ms => await Bun.sleep(ms));
|
|
494
504
|
const readIncarnation = timing.processIncarnation ?? processIncarnation;
|
|
495
505
|
const applyModelProfiles = timing.applyStartupModelProfiles ?? applyStartupModelProfiles;
|
|
506
|
+
const initializeTheme = timing.initTheme ?? initTheme;
|
|
507
|
+
const createLifecycleSession = timing.createLifecycleAgentSession ?? createLifecycleAgentSession;
|
|
508
|
+
const writeLifecycleReady = timing.writeSessionLifecycleReady ?? writeSessionLifecycleReady;
|
|
509
|
+
const writeMcpConfig = timing.writeMcpConfig ?? ((filePath, contents) => Bun.write(filePath, contents));
|
|
496
510
|
const request = readSessionLifecycleLaunchRequest(process.env.GJC_SDK_LIFECYCLE_REQUEST, now());
|
|
497
511
|
const agentDir = process.env.GJC_AGENT_DIR;
|
|
498
512
|
if (!agentDir) throw new Error("GJC_AGENT_DIR is required for sdk session-host-internal.");
|
|
@@ -571,117 +585,314 @@ export async function runSessionHost(
|
|
|
571
585
|
throw new Error("SDK startup did not complete before readiness cutoff.");
|
|
572
586
|
}
|
|
573
587
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
588
|
+
const rollback = new SdkStartupRollbackTracker();
|
|
589
|
+
const capability = new SdkStartupCapability(rollback, request.readiness ?? "immediate", effectMarker);
|
|
590
|
+
const startupOwner: SdkLifecycleStartupOwner = { capability, rollback };
|
|
591
|
+
let startupComplete = false;
|
|
592
|
+
let readinessPublicationCleanupComplete = true;
|
|
593
|
+
let revokePendingReadinessMarker: (() => Promise<boolean>) | undefined;
|
|
594
|
+
let readinessRevocation: Promise<boolean> | undefined;
|
|
595
|
+
let startupInterruption: SdkStartupFailure | undefined;
|
|
596
|
+
const interrupted = Promise.withResolvers<SdkStartupFailure>();
|
|
597
|
+
const startReadinessRevocation = (revoke: () => Promise<boolean>): void => {
|
|
598
|
+
readinessPublicationCleanupComplete = false;
|
|
599
|
+
readinessRevocation ??= Promise.resolve()
|
|
600
|
+
.then(revoke)
|
|
601
|
+
.then(
|
|
602
|
+
revoked => {
|
|
603
|
+
readinessPublicationCleanupComplete = revoked;
|
|
604
|
+
if (revoked) revokePendingReadinessMarker = undefined;
|
|
605
|
+
return revoked;
|
|
606
|
+
},
|
|
607
|
+
() => {
|
|
608
|
+
readinessPublicationCleanupComplete = false;
|
|
609
|
+
return false;
|
|
610
|
+
},
|
|
611
|
+
);
|
|
612
|
+
};
|
|
613
|
+
const interruptStartup = (failure: SdkStartupFailure): void => {
|
|
614
|
+
if (startupInterruption !== undefined) return;
|
|
615
|
+
startupInterruption = failure;
|
|
616
|
+
capability.cancel(failure);
|
|
617
|
+
if (revokePendingReadinessMarker) startReadinessRevocation(revokePendingReadinessMarker);
|
|
618
|
+
interrupted.resolve(failure);
|
|
619
|
+
};
|
|
620
|
+
const cutoffFailure = (): SdkStartupFailure => capability.normalizeFailure("startup", "pending");
|
|
621
|
+
const onStartupSignal = (): void => {
|
|
622
|
+
interruptStartup(capability.normalizeFailure("startup", "failed", "SDK lifecycle host terminated."));
|
|
623
|
+
};
|
|
624
|
+
const removeStartupSignalHandlers = (): void => {
|
|
625
|
+
process.removeListener("SIGTERM", onStartupSignal);
|
|
626
|
+
process.removeListener("SIGINT", onStartupSignal);
|
|
627
|
+
};
|
|
628
|
+
process.once("SIGTERM", onStartupSignal);
|
|
629
|
+
process.once("SIGINT", onStartupSignal);
|
|
630
|
+
void sleep(Math.max(0, request.semanticReadyDeadlineAt - now())).then(
|
|
631
|
+
() => {
|
|
632
|
+
if (!startupComplete) interruptStartup(cutoffFailure());
|
|
633
|
+
},
|
|
634
|
+
error => {
|
|
635
|
+
if (!startupComplete) interruptStartup(capability.normalizeFailure("startup", "failed", error));
|
|
636
|
+
},
|
|
637
|
+
);
|
|
638
|
+
const interruptedStageCleanupGraceMs = 100;
|
|
639
|
+
const throwIfStartupInterrupted = (): void => {
|
|
640
|
+
if (startupInterruption !== undefined) throw startupInterruption;
|
|
641
|
+
if (now() >= request.semanticReadyDeadlineAt) {
|
|
642
|
+
interruptStartup(cutoffFailure());
|
|
643
|
+
throw startupInterruption;
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
const beforeCutoff = async <T>(
|
|
647
|
+
stage: () => Promise<T>,
|
|
648
|
+
cleanupLateResult?: (value: T) => Promise<void>,
|
|
649
|
+
options: { processLocalOnly?: boolean } = {},
|
|
650
|
+
): Promise<T> => {
|
|
651
|
+
throwIfStartupInterrupted();
|
|
652
|
+
const pending = Promise.resolve()
|
|
653
|
+
.then(stage)
|
|
654
|
+
.then(
|
|
655
|
+
value => ({ value }) as const,
|
|
656
|
+
error => ({ error }) as const,
|
|
657
|
+
);
|
|
658
|
+
const result = await Promise.race([
|
|
659
|
+
pending.then(settlement => ({ settlement }) as const),
|
|
660
|
+
interrupted.promise.then(failure => ({ failure }) as const),
|
|
661
|
+
]);
|
|
662
|
+
const cleanupLateStage = async (lateStage: typeof pending): Promise<void> => {
|
|
663
|
+
if (options.processLocalOnly) return;
|
|
664
|
+
if (!cleanupLateResult) {
|
|
665
|
+
constructionCleanupComplete = false;
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const cleanup = lateStage
|
|
669
|
+
.then(async late => {
|
|
670
|
+
if ("error" in late) {
|
|
671
|
+
constructionCleanupComplete = false;
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
await cleanupLateResult(late.value);
|
|
676
|
+
} catch {
|
|
677
|
+
constructionCleanupComplete = false;
|
|
678
|
+
logger.warn(
|
|
679
|
+
"Late lifecycle stage cleanup failed after startup interruption; rollback remains incomplete.",
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
})
|
|
683
|
+
.catch(() => {
|
|
684
|
+
constructionCleanupComplete = false;
|
|
685
|
+
});
|
|
686
|
+
const cleanupCompleted = await Promise.race([
|
|
687
|
+
cleanup.then(() => true),
|
|
688
|
+
Bun.sleep(interruptedStageCleanupGraceMs).then(() => false),
|
|
689
|
+
]);
|
|
690
|
+
if (!cleanupCompleted) constructionCleanupComplete = false;
|
|
691
|
+
};
|
|
692
|
+
if ("failure" in result) {
|
|
693
|
+
await cleanupLateStage(pending);
|
|
694
|
+
throw result.failure;
|
|
695
|
+
}
|
|
696
|
+
const settlement = result.settlement;
|
|
697
|
+
if (startupInterruption !== undefined || now() >= request.semanticReadyDeadlineAt) {
|
|
698
|
+
const failure = startupInterruption ?? cutoffFailure();
|
|
699
|
+
interruptStartup(failure);
|
|
700
|
+
await cleanupLateStage(Promise.resolve(settlement));
|
|
701
|
+
throw failure;
|
|
702
|
+
}
|
|
703
|
+
if ("error" in settlement) throw settlement.error;
|
|
704
|
+
return settlement.value;
|
|
705
|
+
};
|
|
706
|
+
let constructionCleanupComplete = true;
|
|
707
|
+
const runBoundedStartupCleanup = async (cleanup: () => Promise<void>): Promise<boolean> => {
|
|
708
|
+
const completed = await Promise.race([
|
|
709
|
+
Promise.resolve()
|
|
710
|
+
.then(cleanup)
|
|
711
|
+
.then(
|
|
712
|
+
() => true,
|
|
713
|
+
() => false,
|
|
714
|
+
),
|
|
715
|
+
Bun.sleep(interruptedStageCleanupGraceMs).then(() => false),
|
|
716
|
+
]);
|
|
717
|
+
if (!completed) constructionCleanupComplete = false;
|
|
718
|
+
return completed;
|
|
719
|
+
};
|
|
720
|
+
const constructionFailure = (error: unknown): SdkStartupFailure => {
|
|
721
|
+
if (error && typeof error === "object" && "phase" in error && "reason" in error && "message" in error)
|
|
722
|
+
return error as SdkStartupFailure;
|
|
723
|
+
return capability.normalizeFailure("registration", "failed", error);
|
|
583
724
|
};
|
|
584
725
|
|
|
585
726
|
let opened: { parsed: ParsedArgs; sessionManager: SessionManager | undefined };
|
|
727
|
+
let openedSessionManager: SessionManager | undefined;
|
|
728
|
+
let sessionManagerTransferred = false;
|
|
586
729
|
let created: CreateLifecycleAgentSessionResult;
|
|
587
730
|
let mcpConfigDirectory: string | undefined;
|
|
588
731
|
try {
|
|
589
732
|
let mcpConfigPath: string | undefined;
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
733
|
+
opened = await beforeCutoff(
|
|
734
|
+
() => openLifecycleSessionManager(request, cwd, agentDir),
|
|
735
|
+
async late => {
|
|
736
|
+
try {
|
|
737
|
+
await late.sessionManager?.close();
|
|
738
|
+
} catch (error) {
|
|
739
|
+
constructionCleanupComplete = false;
|
|
740
|
+
throw error;
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
);
|
|
744
|
+
openedSessionManager = opened.sessionManager;
|
|
745
|
+
throwIfStartupInterrupted();
|
|
746
|
+
if (request.mcpServers && request.mcpServers.length > 0) {
|
|
747
|
+
const temporaryDirectory = await beforeCutoff(
|
|
748
|
+
() => fs.mkdtemp(path.join(os.tmpdir(), "gjc-acp-mcp-")),
|
|
749
|
+
async late => {
|
|
750
|
+
if (!(await runBoundedStartupCleanup(() => fs.rm(late, { recursive: true, force: true }))))
|
|
751
|
+
throw new Error("Late MCP configuration directory cleanup did not complete.");
|
|
752
|
+
},
|
|
753
|
+
);
|
|
754
|
+
mcpConfigDirectory = temporaryDirectory;
|
|
755
|
+
const canonicalDirectory = await beforeCutoff(
|
|
756
|
+
() => fs.realpath(temporaryDirectory),
|
|
757
|
+
async late => {
|
|
758
|
+
if (!(await runBoundedStartupCleanup(() => fs.rm(late, { recursive: true, force: true }))))
|
|
759
|
+
throw new Error("Late MCP configuration path cleanup did not complete.");
|
|
760
|
+
},
|
|
761
|
+
);
|
|
762
|
+
mcpConfigDirectory = canonicalDirectory;
|
|
763
|
+
const configPath = path.join(canonicalDirectory, "mcp.json");
|
|
764
|
+
mcpConfigPath = configPath;
|
|
765
|
+
const configContents = JSON.stringify({
|
|
766
|
+
mcpServers: Object.fromEntries(
|
|
767
|
+
request.mcpServers.map(server => [
|
|
768
|
+
server.name,
|
|
769
|
+
"url" in server
|
|
770
|
+
? {
|
|
771
|
+
type: server.type,
|
|
772
|
+
url: server.url,
|
|
773
|
+
...(server.headers ? { headers: server.headers } : {}),
|
|
774
|
+
timeout: ACP_MCP_REQUEST_TIMEOUT_MS,
|
|
775
|
+
}
|
|
776
|
+
: {
|
|
777
|
+
type: server.type,
|
|
778
|
+
command: server.command,
|
|
779
|
+
args: server.args,
|
|
780
|
+
...(server.env ? { env: server.env } : {}),
|
|
781
|
+
noInheritEnv: true,
|
|
782
|
+
timeout: ACP_MCP_REQUEST_TIMEOUT_MS,
|
|
783
|
+
},
|
|
784
|
+
]),
|
|
785
|
+
),
|
|
786
|
+
});
|
|
787
|
+
await beforeCutoff(
|
|
788
|
+
() => writeMcpConfig(configPath, configContents),
|
|
789
|
+
async () => {
|
|
790
|
+
if (!(await runBoundedStartupCleanup(() => fs.rm(canonicalDirectory, { recursive: true, force: true }))))
|
|
791
|
+
throw new Error("Late MCP configuration write cleanup did not complete.");
|
|
792
|
+
},
|
|
793
|
+
);
|
|
794
|
+
throwIfStartupInterrupted();
|
|
623
795
|
}
|
|
624
796
|
|
|
625
|
-
|
|
626
|
-
await initTheme(false);
|
|
627
|
-
} catch (error) {
|
|
628
|
-
throw await registrationFailure(error);
|
|
629
|
-
}
|
|
797
|
+
await beforeCutoff(() => initializeTheme(false), undefined, { processLocalOnly: true });
|
|
630
798
|
|
|
631
799
|
// The longer MCP startup ceiling is scoped to ACP lifecycle launches only:
|
|
632
800
|
// it applies when this request actually carried `mcpServers`. Ordinary
|
|
633
801
|
// CLI/SDK `mcpConfigPath` consumers keep the manager's short default.
|
|
634
802
|
//
|
|
635
|
-
// This recheck deliberately sits OUTSIDE the registration catch above.
|
|
636
|
-
// Inside it, the throw would be caught, reclassified as
|
|
637
|
-
// `registration`/`failed`, and written a second time, losing the
|
|
638
|
-
// `startup`/`pending` outcome the readiness cutoff is supposed to report.
|
|
639
803
|
// Session-manager open, MCP config write, and theme initialization already
|
|
640
|
-
// consumed part of the budget
|
|
804
|
+
// consumed part of the budget. Refuse session construction when the remaining
|
|
805
|
+
// time cannot preserve the MCP startup headroom.
|
|
641
806
|
let mcpStartupTimeoutMs: number | undefined;
|
|
642
807
|
if (mcpConfigPath !== undefined) {
|
|
643
808
|
const remaining = request.semanticReadyDeadlineAt - now() - ACP_MCP_STARTUP_HEADROOM_MS;
|
|
644
809
|
if (remaining <= 0) {
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
{
|
|
649
|
-
phase: "startup",
|
|
650
|
-
reason: "pending",
|
|
651
|
-
message: "SDK startup did not complete before readiness cutoff.",
|
|
652
|
-
},
|
|
653
|
-
absent.result,
|
|
654
|
-
);
|
|
655
|
-
throw new Error("SDK startup did not complete before readiness cutoff.");
|
|
810
|
+
const failure = cutoffFailure();
|
|
811
|
+
interruptStartup(failure);
|
|
812
|
+
throw failure;
|
|
656
813
|
}
|
|
657
814
|
mcpStartupTimeoutMs = remaining;
|
|
658
815
|
}
|
|
659
816
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
817
|
+
created = await beforeCutoff(
|
|
818
|
+
async () => {
|
|
819
|
+
sessionManagerTransferred = true;
|
|
820
|
+
const result = await createLifecycleSession(
|
|
821
|
+
{
|
|
822
|
+
cwd,
|
|
823
|
+
agentDir,
|
|
824
|
+
sessionManager: opened.sessionManager,
|
|
825
|
+
...(mcpConfigPath ? { mcpConfigPath } : {}),
|
|
826
|
+
...(mcpStartupTimeoutMs !== undefined ? { mcpStartupTimeoutMs } : {}),
|
|
827
|
+
...(request.readiness ? { readiness: request.readiness } : {}),
|
|
828
|
+
...(request.modelId ? { modelId: request.modelId } : {}),
|
|
829
|
+
lifecycleRequestId: effectMarker,
|
|
830
|
+
},
|
|
831
|
+
startupOwner,
|
|
832
|
+
);
|
|
833
|
+
if (mcpConfigDirectory) {
|
|
834
|
+
await fs.rm(mcpConfigDirectory, { recursive: true, force: true });
|
|
835
|
+
mcpConfigDirectory = undefined;
|
|
836
|
+
}
|
|
837
|
+
return result;
|
|
838
|
+
},
|
|
839
|
+
async late => {
|
|
840
|
+
if ("failure" in late) return;
|
|
841
|
+
try {
|
|
842
|
+
await late.session.dispose();
|
|
843
|
+
} catch (error) {
|
|
844
|
+
if (!isSessionDisposalIncompleteError(error)) {
|
|
845
|
+
constructionCleanupComplete = false;
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
848
|
+
await late.session.awaitDisposeCompletion();
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
removeStartupSignalHandlers();
|
|
854
|
+
if (!sessionManagerTransferred && openedSessionManager) {
|
|
855
|
+
const manager = openedSessionManager;
|
|
856
|
+
await runBoundedStartupCleanup(() => manager.close());
|
|
857
|
+
}
|
|
858
|
+
if (mcpConfigDirectory) {
|
|
859
|
+
const directory = mcpConfigDirectory;
|
|
860
|
+
mcpConfigDirectory = undefined;
|
|
861
|
+
await runBoundedStartupCleanup(() => fs.rm(directory, { recursive: true, force: true }));
|
|
673
862
|
}
|
|
674
|
-
|
|
675
|
-
|
|
863
|
+
const failure = constructionFailure(error);
|
|
864
|
+
const settled = capability.settleFailure(failure);
|
|
865
|
+
const durableFailure = settled.status === "failed" ? settled.failure : failure;
|
|
866
|
+
if (rollback.generation === undefined && constructionCleanupComplete) rollback.recordAbsent();
|
|
867
|
+
await writeFailure(durableFailure, rollback.result);
|
|
868
|
+
throw error;
|
|
676
869
|
}
|
|
677
|
-
const { parsed } = opened;
|
|
678
870
|
if ("failure" in created) {
|
|
871
|
+
removeStartupSignalHandlers();
|
|
679
872
|
created.rollback.recordAbsent();
|
|
873
|
+
capability.settleFailure(created.failure);
|
|
680
874
|
await writeFailure(created.failure, created.rollback.result);
|
|
681
|
-
|
|
682
875
|
throw created.failure;
|
|
683
876
|
}
|
|
684
|
-
|
|
877
|
+
if (created.capability !== capability || created.rollback !== rollback) {
|
|
878
|
+
removeStartupSignalHandlers();
|
|
879
|
+
try {
|
|
880
|
+
await created.session.dispose();
|
|
881
|
+
} catch (error) {
|
|
882
|
+
if (!isSessionDisposalIncompleteError(error)) throw error;
|
|
883
|
+
await created.session.awaitDisposeCompletion();
|
|
884
|
+
}
|
|
885
|
+
const failure = capability.normalizeFailure(
|
|
886
|
+
"registration",
|
|
887
|
+
"failed",
|
|
888
|
+
"Lifecycle startup owner changed during construction.",
|
|
889
|
+
);
|
|
890
|
+
if (rollback.generation === undefined) rollback.recordAbsent();
|
|
891
|
+
await writeFailure(failure, rollback.result);
|
|
892
|
+
throw failure;
|
|
893
|
+
}
|
|
894
|
+
const { parsed } = opened;
|
|
895
|
+
const { session, startDeferredMemoryBackend } = created;
|
|
685
896
|
let lifecycleTranscriptPath: string | undefined;
|
|
686
897
|
session.registerToolSessionTransitionCleanup(async () => {
|
|
687
898
|
await session.sessionManager.ensureOnDisk();
|
|
@@ -730,8 +941,15 @@ export async function runSessionHost(
|
|
|
730
941
|
let failureRollback: Promise<void> | undefined;
|
|
731
942
|
const failAfterRollback = (failure: SdkStartupFailure): Promise<void> => {
|
|
732
943
|
failureRollback ??= (async () => {
|
|
944
|
+
if (readinessRevocation) {
|
|
945
|
+
const revoked = await Promise.race([
|
|
946
|
+
readinessRevocation,
|
|
947
|
+
Bun.sleep(interruptedStageCleanupGraceMs).then(() => false),
|
|
948
|
+
]);
|
|
949
|
+
if (!revoked) readinessPublicationCleanupComplete = false;
|
|
950
|
+
}
|
|
733
951
|
const transcript = await disposeAndCapture();
|
|
734
|
-
if (rollback.generation === undefined) rollback.recordAbsent();
|
|
952
|
+
if (rollback.generation === undefined && readinessPublicationCleanupComplete) rollback.recordAbsent();
|
|
735
953
|
await writeFailure(failure, rollback.result, transcript);
|
|
736
954
|
})();
|
|
737
955
|
return failureRollback;
|
|
@@ -769,36 +987,20 @@ export async function runSessionHost(
|
|
|
769
987
|
};
|
|
770
988
|
let stopping = false;
|
|
771
989
|
const stop = (reason?: "detached_idle") => {
|
|
772
|
-
if (
|
|
990
|
+
if (startupComplete) {
|
|
773
991
|
if (stopping) return;
|
|
774
992
|
stopping = true;
|
|
775
993
|
void exitAfterSessionDisposal(reason);
|
|
776
994
|
return;
|
|
777
995
|
}
|
|
778
996
|
const failure = capability.normalizeFailure("startup", "failed", "SDK lifecycle host terminated.");
|
|
779
|
-
|
|
780
|
-
void failAfterRollback(failure).finally(() => process.exit(0));
|
|
781
|
-
};
|
|
782
|
-
const cutoffFailure = (): SdkStartupFailure => capability.normalizeFailure("startup", "pending");
|
|
783
|
-
const throwIfCutoff = (): void => {
|
|
784
|
-
if (now() >= request.semanticReadyDeadlineAt) {
|
|
785
|
-
capability.cancel();
|
|
786
|
-
throw cutoffFailure();
|
|
787
|
-
}
|
|
788
|
-
};
|
|
789
|
-
const cutoff = sleep(Math.max(0, request.semanticReadyDeadlineAt - now())).then(() => ({ cutoff: true }) as const);
|
|
790
|
-
const beforeCutoff = async <T>(stage: Promise<T>): Promise<T> => {
|
|
791
|
-
const result = await Promise.race([stage.then(value => ({ cutoff: false, value }) as const), cutoff]);
|
|
792
|
-
if (result.cutoff) {
|
|
793
|
-
capability.cancel();
|
|
794
|
-
throw cutoffFailure();
|
|
795
|
-
}
|
|
796
|
-
return result.value;
|
|
997
|
+
interruptStartup(failure);
|
|
797
998
|
};
|
|
999
|
+
const onReadySignal = (): void => stop();
|
|
798
1000
|
|
|
799
1001
|
try {
|
|
800
1002
|
const startupThinkingLevel = request.modelId ? parseModelString(request.modelId)?.thinkingLevel : undefined;
|
|
801
|
-
|
|
1003
|
+
await beforeCutoff(() =>
|
|
802
1004
|
process.env.GJC_SDK_TEST_HANG_MODEL_PROFILE === cwd
|
|
803
1005
|
? new Promise<void>(() => {})
|
|
804
1006
|
: applyModelProfiles({
|
|
@@ -809,32 +1011,76 @@ export async function runSessionHost(
|
|
|
809
1011
|
startupThinkingLevel,
|
|
810
1012
|
preferCachedModels: true,
|
|
811
1013
|
preferCachedDefaultProfile: true,
|
|
812
|
-
})
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
await beforeCutoff(
|
|
1014
|
+
}),
|
|
1015
|
+
);
|
|
1016
|
+
await beforeCutoff(() =>
|
|
816
1017
|
initializeExtensions(session, {
|
|
817
1018
|
reportSendError: () => {},
|
|
818
1019
|
reportRuntimeError: () => {},
|
|
819
1020
|
onShutdown: stop,
|
|
820
1021
|
}),
|
|
821
1022
|
);
|
|
822
|
-
|
|
1023
|
+
throwIfStartupInterrupted();
|
|
823
1024
|
if (session.sessionManager.getSessionId() !== request.sessionId)
|
|
824
1025
|
throw new Error(
|
|
825
1026
|
`Lifecycle session id mismatch: expected ${request.sessionId}, got ${session.sessionManager.getSessionId()}.`,
|
|
826
1027
|
);
|
|
827
|
-
const startup = await beforeCutoff(capability.promise);
|
|
1028
|
+
const startup = await beforeCutoff<SdkStartupResult>(() => capability.promise);
|
|
828
1029
|
if (startup.status !== "started") throw startup.failure;
|
|
829
|
-
|
|
1030
|
+
throwIfStartupInterrupted();
|
|
830
1031
|
if (process.env.GJC_SDK_TEST_FAIL_AFTER_REGISTRATION === cwd)
|
|
831
1032
|
throw new Error("Lifecycle test failure after SDK host registration.");
|
|
832
1033
|
|
|
833
|
-
await session.sessionManager.ensureOnDisk();
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
1034
|
+
await beforeCutoff(() => session.sessionManager.ensureOnDisk());
|
|
1035
|
+
throwIfStartupInterrupted();
|
|
1036
|
+
let readinessPublished = false;
|
|
1037
|
+
readinessPublicationCleanupComplete = false;
|
|
1038
|
+
const readinessOutcome = await beforeCutoff(
|
|
1039
|
+
async () => {
|
|
1040
|
+
try {
|
|
1041
|
+
await writeLifecycleReady(
|
|
1042
|
+
request.stateRoot,
|
|
1043
|
+
request.sessionId,
|
|
1044
|
+
effectMarker,
|
|
1045
|
+
() => startupInterruption === undefined && now() < request.semanticReadyDeadlineAt,
|
|
1046
|
+
revoke => {
|
|
1047
|
+
revokePendingReadinessMarker = revoke;
|
|
1048
|
+
if (startupInterruption !== undefined) startReadinessRevocation(revoke);
|
|
1049
|
+
else if (now() >= request.semanticReadyDeadlineAt) {
|
|
1050
|
+
interruptStartup(cutoffFailure());
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
() => {
|
|
1054
|
+
readinessPublished = true;
|
|
1055
|
+
},
|
|
1056
|
+
);
|
|
1057
|
+
return { published: true } as const;
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
return { published: false, error } as const;
|
|
1060
|
+
}
|
|
1061
|
+
},
|
|
1062
|
+
async outcome => {
|
|
1063
|
+
readinessPublicationCleanupComplete =
|
|
1064
|
+
!outcome.published && !(outcome.error instanceof LifecycleReadinessCleanupError);
|
|
1065
|
+
},
|
|
1066
|
+
);
|
|
1067
|
+
if (!readinessOutcome.published) {
|
|
1068
|
+
readinessPublicationCleanupComplete = !(readinessOutcome.error instanceof LifecycleReadinessCleanupError);
|
|
1069
|
+
throw readinessOutcome.error;
|
|
1070
|
+
}
|
|
1071
|
+
if (!readinessPublished) {
|
|
1072
|
+
readinessPublicationCleanupComplete = false;
|
|
1073
|
+
throw new Error("Lifecycle readiness publication completed without its commit callback.");
|
|
1074
|
+
}
|
|
1075
|
+
readinessPublicationCleanupComplete = true;
|
|
1076
|
+
startupComplete = true;
|
|
1077
|
+
revokePendingReadinessMarker = undefined;
|
|
1078
|
+
readinessRevocation = undefined;
|
|
1079
|
+
removeStartupSignalHandlers();
|
|
1080
|
+
process.once("SIGTERM", onReadySignal);
|
|
1081
|
+
process.once("SIGINT", onReadySignal);
|
|
837
1082
|
} catch (error) {
|
|
1083
|
+
removeStartupSignalHandlers();
|
|
838
1084
|
const failure =
|
|
839
1085
|
error && typeof error === "object" && "phase" in error && "reason" in error && "message" in error
|
|
840
1086
|
? (error as SdkStartupFailure)
|
|
@@ -860,8 +1106,6 @@ export async function runSessionHost(
|
|
|
860
1106
|
await Bun.sleep(5_000);
|
|
861
1107
|
}
|
|
862
1108
|
startMemoryBackendAfterReadiness(startDeferredMemoryBackend);
|
|
863
|
-
process.once("SIGTERM", () => stop());
|
|
864
|
-
process.once("SIGINT", () => stop());
|
|
865
1109
|
// Two independent bounds, either of which reaps this detached host through
|
|
866
1110
|
// the same graceful teardown a SIGTERM would take. The first covers a broker
|
|
867
1111
|
// that is gone for good; the second covers the opposite case, a perfectly
|