@gajae-code/coding-agent 0.17.4 → 0.17.6
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 +82 -0
- package/dist/types/config/autorouting-tier-map.d.ts +1031 -3
- package/dist/types/config/file-lock.d.ts +4 -2
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/model-resolver.d.ts +1 -1
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
- package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
- package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
- package/dist/types/harness-control-plane/finalize.d.ts +14 -3
- package/dist/types/main.d.ts +1 -1
- package/dist/types/runtime-mcp/loader.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +13 -6
- package/dist/types/sdk/broker/broker.d.ts +4 -2
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/identity.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +11 -3
- package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
- package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +8 -0
- package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -1
- package/dist/types/sdk/prompt-deadline-flush.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +3 -3
- package/dist/types/session/agent-session.d.ts +7 -2
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tools/bash-interactive.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +10 -1
- package/dist/types/tools/subagent.d.ts +8 -1
- package/package.json +7 -7
- package/src/cli/public-command-registry.ts +8 -2
- package/src/cli/setup-cli.ts +1 -0
- package/src/cli/stats-cli.ts +11 -1
- package/src/config/autorouting-tier-map.ts +529 -3
- package/src/config/file-lock.ts +59 -14
- package/src/config/model-discovery-manager.ts +4 -4
- package/src/config/model-profile-activation.ts +83 -23
- package/src/config/model-registry.ts +72 -12
- package/src/config/model-resolver.ts +6 -1
- package/src/coordinator-mcp/question-state.ts +4 -1
- package/src/coordinator-mcp/server.ts +14 -6
- package/src/exec/bash-executor.ts +3 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
- package/src/gjc-runtime/state-writer.ts +317 -6
- package/src/harness-control-plane/control-endpoint.ts +24 -6
- package/src/harness-control-plane/finalize.ts +312 -25
- package/src/harness-control-plane/owner.ts +323 -89
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/main.ts +69 -19
- package/src/modes/acp/acp-agent.ts +581 -37
- package/src/modes/components/gjc-bundle-settings.ts +21 -1
- package/src/modes/components/tool-status-header.ts +72 -6
- package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
- package/src/runtime-mcp/loader.ts +6 -3
- package/src/runtime-mcp/manager.ts +177 -75
- package/src/sdk/broker/broker.ts +1058 -150
- package/src/sdk/broker/ensure.ts +65 -8
- package/src/sdk/broker/identity.ts +13 -0
- package/src/sdk/broker/lifecycle-ledger.ts +16 -4
- package/src/sdk/broker/managed-task-dag.ts +1927 -0
- package/src/sdk/broker/managed-task-verification.ts +642 -0
- package/src/sdk/broker/spawn-authority.ts +140 -7
- package/src/sdk/broker/startup-failure.ts +80 -1
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +22 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +5 -1
- package/src/sdk/cli/session-cli.ts +18 -0
- package/src/sdk/host/session-runtime.test.ts +46 -0
- package/src/sdk/host/session-runtime.ts +13 -6
- package/src/sdk/lifecycle/service.ts +2 -0
- package/src/sdk/prompt-deadline-flush.ts +26 -4
- package/src/sdk/session.ts +587 -115
- package/src/session/agent-session.ts +60 -14
- package/src/session/internal/managed-session-scope.ts +10 -0
- package/src/session/internal/managed-session-storage.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/task/executor.ts +46 -17
- package/src/task/types.ts +2 -0
- package/src/tools/bash-interactive.ts +2 -0
- package/src/tools/bash.ts +20 -0
- package/src/tools/fetch.ts +21 -5
- package/src/tools/subagent-render.ts +24 -1
- package/src/tools/subagent.ts +27 -1
- package/src/utils/mupdf.ts +11 -4
package/src/config/file-lock.ts
CHANGED
|
@@ -44,12 +44,14 @@ export type FileLockAcquireReason = "acquire_timeout" | "orphan_transition";
|
|
|
44
44
|
* generation); this records the cause that exhaustion must report.
|
|
45
45
|
*/
|
|
46
46
|
export interface FileLockStaleRemovalFailure {
|
|
47
|
-
/** Guarded-removal outcome
|
|
48
|
-
outcome: "cleanup_failed" | "error";
|
|
47
|
+
/** Guarded-removal outcome; refusals are surfaced only after exhaustion revalidates the dead generation. */
|
|
48
|
+
outcome: "cleanup_failed" | "owner_changed" | "missing" | "error";
|
|
49
49
|
/** Native/errno code carried by the refusal, when one was present. */
|
|
50
50
|
code?: string;
|
|
51
51
|
/** Human-readable cause surfaced to the operator at exhaustion. */
|
|
52
52
|
message: string;
|
|
53
|
+
/** Platform-specific manual cleanup command, set only when the same dead owner persists at exhaustion. */
|
|
54
|
+
manualCleanupCommand?: string;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export class FileLockAcquireError extends Error {
|
|
@@ -70,10 +72,12 @@ export class FileLockAcquireError extends Error {
|
|
|
70
72
|
removalFailure.code ? ` [${removalFailure.code}]` : ""
|
|
71
73
|
})`
|
|
72
74
|
: "";
|
|
75
|
+
const cleanupDetail = removalFailure?.manualCleanupCommand
|
|
76
|
+
? `; manual cleanup is appropriate only after independently verifying this exact directory still belongs to the dead owner and no successor has taken it: ${removalFailure.manualCleanupCommand}`
|
|
77
|
+
: "";
|
|
73
78
|
super(
|
|
74
79
|
`Failed to acquire lock for ${filePath} after ${attempts} attempts: ${detail}${removalDetail} (${lockPath}); ` +
|
|
75
|
-
`a live owner is never displaced — if this is an SDK broker (gjc sdk session list), it must finish or be stopped before retrying
|
|
76
|
-
`the lock is a directory, remove it only by deleting the directory (${lockPath}) once no live owner remains`,
|
|
80
|
+
`a live owner is never displaced — if this is an SDK broker (gjc sdk session list), it must finish or be stopped before retrying${cleanupDetail}`,
|
|
77
81
|
);
|
|
78
82
|
this.code = reason;
|
|
79
83
|
this.name = "FileLockAcquireError";
|
|
@@ -1069,6 +1073,10 @@ function ownerIncarnationChanged(owner: FileLockOwnerToken, startTimeCache?: Map
|
|
|
1069
1073
|
return currentStartTime !== null && currentStartTime !== owner.start_time;
|
|
1070
1074
|
}
|
|
1071
1075
|
|
|
1076
|
+
function ownerGenerationIsDead(owner: FileLockOwnerToken): boolean {
|
|
1077
|
+
return ownerLiveness(owner.pid) === "dead" || ownerIncarnationChanged(owner);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1072
1080
|
/** Outcome of a guarded lock-dir removal attempt (`removeFileLockDirForGc`). */
|
|
1073
1081
|
export type FileLockGcRemoval = "removed" | "owner_changed" | "missing" | "cleanup_failed";
|
|
1074
1082
|
|
|
@@ -1550,13 +1558,17 @@ async function staleLockSnapshot(
|
|
|
1550
1558
|
|
|
1551
1559
|
type StaleLockRemovalAttempt = { removed: true } | { removed: false; failure?: FileLockStaleRemovalFailure };
|
|
1552
1560
|
|
|
1553
|
-
type RecordedStaleRemovalFailure = {
|
|
1561
|
+
type RecordedStaleRemovalFailure = {
|
|
1562
|
+
owner: FileLockOwnerToken;
|
|
1563
|
+
identity: GenericFileLockDirIdentity;
|
|
1564
|
+
failure: FileLockStaleRemovalFailure;
|
|
1565
|
+
};
|
|
1554
1566
|
|
|
1555
1567
|
/**
|
|
1556
|
-
* A recorded refusal describes exactly one dead owner generation
|
|
1557
|
-
* exhaustion and report it only while
|
|
1558
|
-
*
|
|
1559
|
-
*
|
|
1568
|
+
* A recorded refusal describes exactly one dead owner generation and directory identity.
|
|
1569
|
+
* Re-read the pathname at exhaustion and report it only while both still match, so a refusal
|
|
1570
|
+
* recorded for a dead generation is never pinned onto a successor that took the pathname over
|
|
1571
|
+
* before the budget ran out, even if its legacy owner bytes are identical.
|
|
1560
1572
|
*/
|
|
1561
1573
|
async function staleRemovalFailureForCurrentGeneration(
|
|
1562
1574
|
lockPath: string,
|
|
@@ -1564,13 +1576,31 @@ async function staleRemovalFailureForCurrentGeneration(
|
|
|
1564
1576
|
): Promise<FileLockStaleRemovalFailure | undefined> {
|
|
1565
1577
|
if (!recorded) return undefined;
|
|
1566
1578
|
try {
|
|
1567
|
-
const
|
|
1568
|
-
|
|
1579
|
+
const observation = await readLockInfoObservation(lockPath);
|
|
1580
|
+
if (!observation) return undefined;
|
|
1581
|
+
const current = parseLockInfoBytes(observation.bytes);
|
|
1582
|
+
if (!current || !sameFileLockOwnerToken(current, recorded.owner) || !ownerGenerationIsDead(current))
|
|
1583
|
+
return undefined;
|
|
1584
|
+
const currentIdentity = fileLockDirIdentityFromPathState(observation.state, observation.bytes);
|
|
1585
|
+
if (!sameGenericFileLockDirIdentity(recorded.identity, currentIdentity)) return undefined;
|
|
1586
|
+
return {
|
|
1587
|
+
...recorded.failure,
|
|
1588
|
+
manualCleanupCommand: manualLockCleanupCommand(lockPath),
|
|
1589
|
+
};
|
|
1569
1590
|
} catch {
|
|
1570
1591
|
return undefined;
|
|
1571
1592
|
}
|
|
1572
1593
|
}
|
|
1573
1594
|
|
|
1595
|
+
function manualLockCleanupCommand(lockPath: string): string {
|
|
1596
|
+
if (process.platform === "win32") {
|
|
1597
|
+
const quotedPath = lockPath.replace(/'/g, "''");
|
|
1598
|
+
return `Remove-Item -LiteralPath '${quotedPath}' -Recurse -Force`;
|
|
1599
|
+
}
|
|
1600
|
+
const quotedPath = lockPath.replace(/'/g, "'\\''");
|
|
1601
|
+
return `rm -rf -- '${quotedPath}'`;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1574
1604
|
async function removeStaleLockForAcquire(
|
|
1575
1605
|
lockPath: string,
|
|
1576
1606
|
snapshot: LockStaleSnapshot,
|
|
@@ -1579,8 +1609,21 @@ async function removeStaleLockForAcquire(
|
|
|
1579
1609
|
try {
|
|
1580
1610
|
const outcome = await removeFileLockDirForGc(lockPath, snapshot.owner, snapshot.identity);
|
|
1581
1611
|
if (outcome === "removed") return { removed: true };
|
|
1582
|
-
//
|
|
1583
|
-
//
|
|
1612
|
+
// Either refusal can mean a successor or an incomplete publication owns the path;
|
|
1613
|
+
// keep it only as a diagnostic candidate and expose it at exhaustion if the same
|
|
1614
|
+
// dead owner still persists. Never fall back to path-only deletion.
|
|
1615
|
+
if (outcome === "owner_changed" || outcome === "missing") {
|
|
1616
|
+
return {
|
|
1617
|
+
removed: false,
|
|
1618
|
+
failure: {
|
|
1619
|
+
outcome,
|
|
1620
|
+
message:
|
|
1621
|
+
outcome === "owner_changed"
|
|
1622
|
+
? "the identity-bound removal guard returned owner_changed; no path-only fallback was attempted"
|
|
1623
|
+
: "the guarded removal could not read the owner record; no path-only fallback was attempted",
|
|
1624
|
+
},
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1584
1627
|
if (outcome !== "cleanup_failed") return { removed: false };
|
|
1585
1628
|
return {
|
|
1586
1629
|
removed: false,
|
|
@@ -2207,7 +2250,9 @@ export async function acquireFileLock(filePath: string, options: FileLockOptions
|
|
|
2207
2250
|
continue;
|
|
2208
2251
|
}
|
|
2209
2252
|
staleRemovalFailure =
|
|
2210
|
-
stale.stale && staleRemoval.failure
|
|
2253
|
+
stale.stale && staleRemoval.failure
|
|
2254
|
+
? { owner: stale.owner, identity: stale.identity, failure: staleRemoval.failure }
|
|
2255
|
+
: undefined;
|
|
2211
2256
|
if (!opts.signal) {
|
|
2212
2257
|
await Bun.sleep(opts.retryDelayMs);
|
|
2213
2258
|
continue;
|
|
@@ -222,11 +222,11 @@ export class ModelDiscoveryManager<TProvider extends DiscoveryProvider> {
|
|
|
222
222
|
? result.stale
|
|
223
223
|
? "cached"
|
|
224
224
|
: "ok"
|
|
225
|
-
: result.fetched
|
|
225
|
+
: result.fetched ||
|
|
226
|
+
(result.cacheFresh && result.cacheAuthoritative && result.dynamicModelIds?.length === 0)
|
|
226
227
|
? "empty"
|
|
227
|
-
: // No fetch happened
|
|
228
|
-
//
|
|
229
|
-
// provider is unvalidated, not authoritatively empty.
|
|
228
|
+
: // No fetch happened and no fresh authoritative empty row
|
|
229
|
+
// attested to this result, so the provider is unvalidated.
|
|
230
230
|
"unavailable";
|
|
231
231
|
const state: ProviderDiscoveryState = {
|
|
232
232
|
provider: provider.provider,
|
|
@@ -1528,15 +1528,58 @@ export async function prepareModelProfileActivation(
|
|
|
1528
1528
|
previousDefaultFallbackRuntimeState: options.session.getDefaultFallbackRuntimeState?.(),
|
|
1529
1529
|
};
|
|
1530
1530
|
} catch (error) {
|
|
1531
|
-
|
|
1531
|
+
try {
|
|
1532
|
+
restoreCanonicalVariant(options.modelRegistry, options.session.sessionId, previousCanonicalVariant);
|
|
1533
|
+
} catch (rollbackError) {
|
|
1534
|
+
throw incompleteModelProfileRollbackError("preparation", "profile preflight", error, [
|
|
1535
|
+
{ stage: "restore canonical model variant", error: rollbackError },
|
|
1536
|
+
]);
|
|
1537
|
+
}
|
|
1532
1538
|
throw error;
|
|
1533
1539
|
}
|
|
1534
1540
|
}
|
|
1535
1541
|
|
|
1542
|
+
function modelProfileFailureReason(error: unknown, stage: string): string {
|
|
1543
|
+
try {
|
|
1544
|
+
if (error instanceof ModelProfileCredentialError) return "required credentials unavailable";
|
|
1545
|
+
if (error instanceof Error && error.message.startsWith("No API key for ")) return "credentials unavailable";
|
|
1546
|
+
} catch {
|
|
1547
|
+
// An exotic error getter must not replace the activation failure.
|
|
1548
|
+
}
|
|
1549
|
+
return stage.includes("settings") || stage.startsWith("persist ") || stage.endsWith(" setting")
|
|
1550
|
+
? "settings write failed"
|
|
1551
|
+
: "operation failed";
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
class ModelProfileActivationStageError extends Error {
|
|
1555
|
+
readonly stage: string;
|
|
1556
|
+
|
|
1557
|
+
constructor(stage: string, error: unknown) {
|
|
1558
|
+
super(`${stage} (${modelProfileFailureReason(error, stage)})`, { cause: error });
|
|
1559
|
+
this.name = "ModelProfileActivationStageError";
|
|
1560
|
+
this.stage = stage;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
function incompleteModelProfileRollbackError(
|
|
1565
|
+
phase: "preparation" | "activation",
|
|
1566
|
+
stage: string,
|
|
1567
|
+
error: unknown,
|
|
1568
|
+
rollbackErrors: readonly { stage: string; error: unknown }[],
|
|
1569
|
+
): AggregateError {
|
|
1570
|
+
const primary = new ModelProfileActivationStageError(stage, error);
|
|
1571
|
+
const incomplete = rollbackErrors.map(failure => new ModelProfileActivationStageError(failure.stage, failure.error));
|
|
1572
|
+
return new AggregateError(
|
|
1573
|
+
[primary, ...incomplete],
|
|
1574
|
+
`Model profile ${phase} failed at ${primary.message}; rollback incomplete: ${incomplete.map(failure => failure.message).join("; ")}. Prior state may be unverified.`,
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1536
1578
|
export async function applyPreparedModelProfileActivation(
|
|
1537
1579
|
prepared: PreparedModelProfileActivation,
|
|
1538
1580
|
options: ApplyModelProfileActivationOptions = {},
|
|
1539
1581
|
): Promise<void> {
|
|
1582
|
+
let activationStage = "default chain";
|
|
1540
1583
|
let modelMutationStarted = false;
|
|
1541
1584
|
let overridesChanged = false;
|
|
1542
1585
|
let modelRolesChanged = false;
|
|
@@ -1567,15 +1610,21 @@ export async function applyPreparedModelProfileActivation(
|
|
|
1567
1610
|
}
|
|
1568
1611
|
}
|
|
1569
1612
|
if (prepared.defaultModel) {
|
|
1570
|
-
|
|
1613
|
+
activationStage = "model selection";
|
|
1571
1614
|
await prepared.session.setModelTemporary(
|
|
1572
1615
|
prepared.defaultModel,
|
|
1573
1616
|
options.thinkingLevelOverride ?? prepared.defaultThinkingLevel,
|
|
1574
|
-
{
|
|
1617
|
+
{
|
|
1618
|
+
cause: "profile-activation",
|
|
1619
|
+
onMutationStarted: () => {
|
|
1620
|
+
modelMutationStarted = true;
|
|
1621
|
+
},
|
|
1622
|
+
},
|
|
1575
1623
|
);
|
|
1576
1624
|
}
|
|
1577
1625
|
// Always reinstall the model role layer from the durable base plus the
|
|
1578
1626
|
// new profile's roles so omitted roles from the previous profile are dropped.
|
|
1627
|
+
activationStage = "model role overrides";
|
|
1579
1628
|
prepared.settings.override("modelRoles", {
|
|
1580
1629
|
...prepared.baseModelRoles,
|
|
1581
1630
|
...prepared.modelRoles,
|
|
@@ -1584,6 +1633,7 @@ export async function applyPreparedModelProfileActivation(
|
|
|
1584
1633
|
// Always reinstall the agent role layer from the durable base plus the
|
|
1585
1634
|
// new profile's roles: a default-only or role-free successor must drop
|
|
1586
1635
|
// the previous profile's role-agent mappings rather than inheriting them.
|
|
1636
|
+
activationStage = "agent role overrides";
|
|
1587
1637
|
prepared.settings.override("task.agentModelOverrides", {
|
|
1588
1638
|
...prepared.baseAgentModelOverrides,
|
|
1589
1639
|
...prepared.agentModelOverrides,
|
|
@@ -1591,65 +1641,74 @@ export async function applyPreparedModelProfileActivation(
|
|
|
1591
1641
|
overridesChanged = true;
|
|
1592
1642
|
if (options.persistDefault) {
|
|
1593
1643
|
persistentMutationStarted = true;
|
|
1644
|
+
activationStage = "persist model roles";
|
|
1594
1645
|
prepared.settings.set("modelRoles", {});
|
|
1646
|
+
activationStage = "persist agent roles";
|
|
1595
1647
|
prepared.settings.set("task.agentModelOverrides", {});
|
|
1596
1648
|
if (prepared.defaultThinkingLevel !== undefined && prepared.defaultThinkingLevel !== ThinkingLevel.Inherit) {
|
|
1649
|
+
activationStage = "persist thinking level";
|
|
1597
1650
|
prepared.settings.set("defaultThinkingLevel", prepared.defaultThinkingLevel);
|
|
1598
1651
|
}
|
|
1652
|
+
activationStage = "persist default profile";
|
|
1599
1653
|
prepared.settings.set("modelProfile.default", prepared.profileName);
|
|
1654
|
+
activationStage = "forward settings flush";
|
|
1600
1655
|
await prepared.settings.flushOrThrow();
|
|
1601
1656
|
}
|
|
1657
|
+
activationStage = "active profile marker";
|
|
1602
1658
|
prepared.session.setActiveModelProfile?.(prepared.profileName);
|
|
1603
1659
|
if (prepared.defaultModel) {
|
|
1660
|
+
activationStage = "canonical model variant";
|
|
1604
1661
|
prepared.modelRegistry.seedCanonicalVariant?.(prepared.session.sessionId, prepared.defaultModel);
|
|
1605
1662
|
resumeDefaultChanged = true;
|
|
1663
|
+
activationStage = "resume default model";
|
|
1606
1664
|
prepared.session.recordResumeDefaultModel?.(`${prepared.defaultModel.provider}/${prepared.defaultModel.id}`);
|
|
1607
1665
|
}
|
|
1666
|
+
activationStage = "installed role tracking";
|
|
1608
1667
|
prepared.session.noteProfileInstalledOverrides?.(
|
|
1609
1668
|
Object.keys(prepared.modelRoles),
|
|
1610
1669
|
Object.keys(prepared.agentModelOverrides),
|
|
1611
1670
|
prepared.previousModel,
|
|
1612
1671
|
);
|
|
1613
1672
|
} catch (error) {
|
|
1614
|
-
const rollbackErrors: unknown
|
|
1615
|
-
const restore = (action: () => void): void => {
|
|
1673
|
+
const rollbackErrors: Array<{ stage: string; error: unknown }> = [];
|
|
1674
|
+
const restore = (stage: string, action: () => void): void => {
|
|
1616
1675
|
try {
|
|
1617
1676
|
action();
|
|
1618
1677
|
} catch (rollbackError) {
|
|
1619
|
-
rollbackErrors.push(rollbackError);
|
|
1678
|
+
rollbackErrors.push({ stage, error: rollbackError });
|
|
1620
1679
|
}
|
|
1621
1680
|
};
|
|
1622
1681
|
if (persistentMutationStarted) {
|
|
1623
|
-
restore(() =>
|
|
1682
|
+
restore("restore default profile setting", () =>
|
|
1624
1683
|
prepared.previousPersistedDefaultProfile === undefined
|
|
1625
1684
|
? prepared.settings.unset("modelProfile.default")
|
|
1626
1685
|
: prepared.settings.set("modelProfile.default", prepared.previousPersistedDefaultProfile),
|
|
1627
1686
|
);
|
|
1628
|
-
restore(() =>
|
|
1687
|
+
restore("restore model role setting", () =>
|
|
1629
1688
|
prepared.previousPersistedModelRoles === undefined
|
|
1630
1689
|
? prepared.settings.unset("modelRoles")
|
|
1631
1690
|
: prepared.settings.set("modelRoles", prepared.previousPersistedModelRoles),
|
|
1632
1691
|
);
|
|
1633
|
-
restore(() =>
|
|
1692
|
+
restore("restore agent role setting", () =>
|
|
1634
1693
|
prepared.previousPersistedAgentModelOverrides === undefined
|
|
1635
1694
|
? prepared.settings.unset("task.agentModelOverrides")
|
|
1636
1695
|
: prepared.settings.set("task.agentModelOverrides", prepared.previousPersistedAgentModelOverrides),
|
|
1637
1696
|
);
|
|
1638
|
-
restore(() =>
|
|
1697
|
+
restore("restore thinking level setting", () =>
|
|
1639
1698
|
prepared.previousPersistedDefaultThinkingLevel === undefined
|
|
1640
1699
|
? prepared.settings.unset("defaultThinkingLevel")
|
|
1641
1700
|
: prepared.settings.set("defaultThinkingLevel", prepared.previousPersistedDefaultThinkingLevel),
|
|
1642
1701
|
);
|
|
1643
1702
|
}
|
|
1644
1703
|
if (modelRolesChanged) {
|
|
1645
|
-
restore(() =>
|
|
1704
|
+
restore("restore model role overrides", () =>
|
|
1646
1705
|
prepared.previousModelRolesOverride === undefined
|
|
1647
1706
|
? prepared.settings.clearOverride("modelRoles")
|
|
1648
1707
|
: prepared.settings.override("modelRoles", prepared.previousModelRolesOverride),
|
|
1649
1708
|
);
|
|
1650
1709
|
}
|
|
1651
1710
|
if (overridesChanged) {
|
|
1652
|
-
restore(() =>
|
|
1711
|
+
restore("restore agent role overrides", () =>
|
|
1653
1712
|
prepared.previousAgentModelOverridesOverride === undefined
|
|
1654
1713
|
? prepared.settings.clearOverride("task.agentModelOverrides")
|
|
1655
1714
|
: prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverridesOverride),
|
|
@@ -1670,15 +1729,17 @@ export async function applyPreparedModelProfileActivation(
|
|
|
1670
1729
|
throw new Error("Model-less profile activation rollback is unavailable");
|
|
1671
1730
|
}
|
|
1672
1731
|
} catch (rollbackError) {
|
|
1673
|
-
rollbackErrors.push(rollbackError);
|
|
1732
|
+
rollbackErrors.push({ stage: "restore live model", error: rollbackError });
|
|
1674
1733
|
}
|
|
1675
1734
|
}
|
|
1676
1735
|
if (resumeDefaultChanged) {
|
|
1677
|
-
restore(() =>
|
|
1736
|
+
restore("restore resume default", () =>
|
|
1737
|
+
prepared.session.recordResumeDefaultModel?.(prepared.previousSessionDefaultModel),
|
|
1738
|
+
);
|
|
1678
1739
|
}
|
|
1679
1740
|
if (defaultChainChanged) {
|
|
1680
1741
|
const previousChain = prepared.previousDefaultChainState;
|
|
1681
|
-
restore(() =>
|
|
1742
|
+
restore("restore default chain", () =>
|
|
1682
1743
|
prepared.session.setConfiguredModelChain(
|
|
1683
1744
|
"default",
|
|
1684
1745
|
previousChain?.entries ??
|
|
@@ -1691,26 +1752,25 @@ export async function applyPreparedModelProfileActivation(
|
|
|
1691
1752
|
);
|
|
1692
1753
|
}
|
|
1693
1754
|
if (prepared.previousDefaultFallbackRuntimeState) {
|
|
1694
|
-
restore(() =>
|
|
1755
|
+
restore("restore fallback runtime", () =>
|
|
1695
1756
|
prepared.session.restoreDefaultFallbackRuntimeState?.(prepared.previousDefaultFallbackRuntimeState!),
|
|
1696
1757
|
);
|
|
1697
1758
|
}
|
|
1698
|
-
restore(() =>
|
|
1699
|
-
|
|
1759
|
+
restore("restore active profile", () =>
|
|
1760
|
+
prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile),
|
|
1761
|
+
);
|
|
1762
|
+
restore("restore canonical model variant", () =>
|
|
1700
1763
|
restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant),
|
|
1701
1764
|
);
|
|
1702
1765
|
if (persistentMutationStarted) {
|
|
1703
1766
|
try {
|
|
1704
1767
|
await prepared.settings.flushOrThrow();
|
|
1705
1768
|
} catch (rollbackError) {
|
|
1706
|
-
rollbackErrors.push(rollbackError);
|
|
1769
|
+
rollbackErrors.push({ stage: "restore settings flush", error: rollbackError });
|
|
1707
1770
|
}
|
|
1708
1771
|
}
|
|
1709
1772
|
if (rollbackErrors.length > 0) {
|
|
1710
|
-
throw
|
|
1711
|
-
[error, ...rollbackErrors],
|
|
1712
|
-
"Model profile activation failed and rollback was incomplete",
|
|
1713
|
-
);
|
|
1773
|
+
throw incompleteModelProfileRollbackError("activation", activationStage, error, rollbackErrors);
|
|
1714
1774
|
}
|
|
1715
1775
|
throw error;
|
|
1716
1776
|
}
|
|
@@ -83,6 +83,8 @@ import { ModelDiscoveryManager, type ProviderDiscoveryState } from "./model-disc
|
|
|
83
83
|
import {
|
|
84
84
|
loadAcceptedModelPresetProfiles,
|
|
85
85
|
type ModelPresetRegistryDependencies,
|
|
86
|
+
type ModelPresetRegistryRefreshResult,
|
|
87
|
+
refreshModelPresetRegistry,
|
|
86
88
|
refreshModelPresetRegistryInBackground,
|
|
87
89
|
} from "./model-preset-registry";
|
|
88
90
|
|
|
@@ -1902,6 +1904,17 @@ export class ModelRegistry {
|
|
|
1902
1904
|
});
|
|
1903
1905
|
}
|
|
1904
1906
|
|
|
1907
|
+
/** Refresh the signed profile catalog online, then publish its accepted snapshot. */
|
|
1908
|
+
async refreshModelPresetProfilesFromRegistry(): Promise<ModelPresetRegistryRefreshResult> {
|
|
1909
|
+
const result = await refreshModelPresetRegistry({
|
|
1910
|
+
...this.#modelPresetRegistryDependencies,
|
|
1911
|
+
agentDir: this.#modelPresetRegistryAgentDir,
|
|
1912
|
+
knownManifestSha256: this.#loadedModelPresetRegistryManifestSha256,
|
|
1913
|
+
});
|
|
1914
|
+
await this.refreshStatic();
|
|
1915
|
+
return result;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1905
1918
|
/**
|
|
1906
1919
|
* Reload models from disk and refresh runtime provider discovery state.
|
|
1907
1920
|
*/
|
|
@@ -2547,12 +2560,20 @@ export class ModelRegistry {
|
|
|
2547
2560
|
for (const providerConfig of this.#discoveryManager.providers) {
|
|
2548
2561
|
if (providerId && providerConfig.provider !== providerId) continue;
|
|
2549
2562
|
let expectedProvenance: string | undefined;
|
|
2563
|
+
let effectiveConfig: DiscoveryProviderConfig | undefined;
|
|
2564
|
+
let expectedAuthGeneration: string | undefined;
|
|
2565
|
+
let expectedEndpoint: string | undefined;
|
|
2550
2566
|
try {
|
|
2551
|
-
|
|
2567
|
+
effectiveConfig = this.#effectiveDiscoveryProviderConfig(providerConfig);
|
|
2568
|
+
const effectiveAuthGeneration =
|
|
2569
|
+
authEvidence ?? this.#getProviderEvidenceGeneration(effectiveConfig.provider);
|
|
2570
|
+
const effectiveEndpoint = this.#normalizeDiscoveryEvidenceEndpoint(effectiveConfig.baseUrl ?? "");
|
|
2571
|
+
expectedAuthGeneration = effectiveAuthGeneration;
|
|
2572
|
+
expectedEndpoint = effectiveEndpoint;
|
|
2552
2573
|
expectedProvenance = fingerprintConfiguredDiscoveryRequestShape(
|
|
2553
2574
|
effectiveConfig,
|
|
2554
|
-
|
|
2555
|
-
|
|
2575
|
+
effectiveAuthGeneration,
|
|
2576
|
+
effectiveEndpoint,
|
|
2556
2577
|
);
|
|
2557
2578
|
} catch {
|
|
2558
2579
|
// A context that cannot be fully derived cannot vouch for a cache row.
|
|
@@ -2561,6 +2582,30 @@ export class ModelRegistry {
|
|
|
2561
2582
|
const models = publishDiscoveryState
|
|
2562
2583
|
? this.#discoveryManager.loadCached(providerConfig, this.#cacheDbPath, expectedProvenance)
|
|
2563
2584
|
: this.#readCachedDiscoverableModels(providerConfig, expectedProvenance);
|
|
2585
|
+
if (publishDiscoveryState) {
|
|
2586
|
+
const cache =
|
|
2587
|
+
effectiveConfig !== undefined
|
|
2588
|
+
? readModelCache<Api>(effectiveConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath)
|
|
2589
|
+
: null;
|
|
2590
|
+
if (
|
|
2591
|
+
effectiveConfig !== undefined &&
|
|
2592
|
+
expectedAuthGeneration !== undefined &&
|
|
2593
|
+
expectedEndpoint !== undefined &&
|
|
2594
|
+
expectedProvenance !== undefined &&
|
|
2595
|
+
cache?.fresh === true &&
|
|
2596
|
+
cache.authoritative === true &&
|
|
2597
|
+
cache.dynamicModelIds !== undefined &&
|
|
2598
|
+
cache.dynamicModelProvenance === expectedProvenance
|
|
2599
|
+
) {
|
|
2600
|
+
this.#configuredDiscoveryEvidence.set(effectiveConfig.provider, {
|
|
2601
|
+
authGeneration: expectedAuthGeneration,
|
|
2602
|
+
endpoint: expectedEndpoint,
|
|
2603
|
+
modelIds: new Set(cache.dynamicModelIds),
|
|
2604
|
+
});
|
|
2605
|
+
} else {
|
|
2606
|
+
this.#configuredDiscoveryEvidence.delete(providerConfig.provider);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2564
2609
|
// Cache rows persist sanitized transport metadata (no headers), so a
|
|
2565
2610
|
// rebooted registry re-derives the provider transport override from the
|
|
2566
2611
|
// same source the live publish path uses — mirroring the cached
|
|
@@ -3293,16 +3338,24 @@ export class ModelRegistry {
|
|
|
3293
3338
|
const currentEndpoint = this.#normalizeDiscoveryEvidenceEndpoint(
|
|
3294
3339
|
this.#effectiveDiscoveryProviderConfig(provider).baseUrl ?? "",
|
|
3295
3340
|
);
|
|
3341
|
+
const authoritativeState =
|
|
3342
|
+
state !== undefined &&
|
|
3343
|
+
state.error === undefined &&
|
|
3344
|
+
!state.stale &&
|
|
3345
|
+
((state.status === "ok" && discovery.fetched) ||
|
|
3346
|
+
state.status === "empty" ||
|
|
3347
|
+
(state.status === "ok" && !discovery.fetched));
|
|
3296
3348
|
if (
|
|
3297
3349
|
evidence !== undefined &&
|
|
3298
|
-
|
|
3299
|
-
discovery.fetched &&
|
|
3350
|
+
authoritativeState &&
|
|
3300
3351
|
currentAuthGeneration === evidence.authGeneration &&
|
|
3301
3352
|
currentEndpoint === evidence.endpoint
|
|
3302
3353
|
) {
|
|
3303
3354
|
this.#configuredDiscoveryEvidence.set(provider.provider, evidence);
|
|
3304
3355
|
} else if (
|
|
3305
|
-
(state?.status !== "cached" &&
|
|
3356
|
+
(state?.status !== "cached" &&
|
|
3357
|
+
state?.status !== "empty" &&
|
|
3358
|
+
!(state?.status === "ok" && !discovery.fetched)) ||
|
|
3306
3359
|
state.error !== undefined ||
|
|
3307
3360
|
this.#configuredDiscoveryEvidence.get(provider.provider)?.authGeneration !== currentAuthGeneration ||
|
|
3308
3361
|
this.#configuredDiscoveryEvidence.get(provider.provider)?.endpoint !== currentEndpoint
|
|
@@ -5425,6 +5478,10 @@ export class ModelRegistry {
|
|
|
5425
5478
|
*/
|
|
5426
5479
|
getAvailableForProfileActivation(): Model<Api>[] {
|
|
5427
5480
|
const bundledIdsByProvider = new Map<string, Set<string>>();
|
|
5481
|
+
// Evidence staleness depends only on the provider, and resolving it scans the
|
|
5482
|
+
// catalog for the provider base URL. Decide it once per provider per call so a
|
|
5483
|
+
// multi-thousand-model catalog is not rescanned for every model.
|
|
5484
|
+
const staleEvidenceByProvider = new Map<string, boolean>();
|
|
5428
5485
|
return this.getAvailable().filter(model => {
|
|
5429
5486
|
const evidence = this.#descriptorDiscoveryEvidence.get(model.provider);
|
|
5430
5487
|
if (!evidence?.profileFresh || evidence.profileModelIds === undefined) return true;
|
|
@@ -5439,12 +5496,15 @@ export class ModelRegistry {
|
|
|
5439
5496
|
bundledIdsByProvider.set(model.provider, bundledModelIds);
|
|
5440
5497
|
}
|
|
5441
5498
|
if (!bundledModelIds.has(model.id)) return true;
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
this.#
|
|
5446
|
-
|
|
5447
|
-
|
|
5499
|
+
let staleEvidence = staleEvidenceByProvider.get(model.provider);
|
|
5500
|
+
if (staleEvidence === undefined) {
|
|
5501
|
+
staleEvidence =
|
|
5502
|
+
evidence.authGeneration !== this.#getProviderEvidenceGeneration(model.provider) ||
|
|
5503
|
+
evidence.profileEndpoint !==
|
|
5504
|
+
this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(model.provider) ?? "");
|
|
5505
|
+
staleEvidenceByProvider.set(model.provider, staleEvidence);
|
|
5506
|
+
}
|
|
5507
|
+
if (staleEvidence) return true;
|
|
5448
5508
|
return evidence.profileModelIds.has(model.id);
|
|
5449
5509
|
});
|
|
5450
5510
|
}
|
|
@@ -87,6 +87,7 @@ export function resolveStartupModelRefreshSelectors(
|
|
|
87
87
|
export async function refreshMissingQualifiedModelProviders(
|
|
88
88
|
selectors: ModelSelectorValue | undefined,
|
|
89
89
|
modelRegistry: Pick<ModelRegistry, "getAvailable" | "getRefreshableProviders" | "refreshProvider">,
|
|
90
|
+
credentialSessionId?: string,
|
|
90
91
|
): Promise<boolean> {
|
|
91
92
|
const refreshedProviders = new Set<string>();
|
|
92
93
|
for (const selector of normalizeModelSelectorValue(selectors)) {
|
|
@@ -104,7 +105,11 @@ export async function refreshMissingQualifiedModelProviders(
|
|
|
104
105
|
)
|
|
105
106
|
continue;
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
if (credentialSessionId === undefined) {
|
|
109
|
+
await modelRegistry.refreshProvider(provider, "online-if-uncached");
|
|
110
|
+
} else {
|
|
111
|
+
await modelRegistry.refreshProvider(provider, "online-if-uncached", credentialSessionId);
|
|
112
|
+
}
|
|
108
113
|
refreshedProviders.add(provider);
|
|
109
114
|
}
|
|
110
115
|
return refreshedProviders.size > 0;
|
|
@@ -1879,7 +1879,10 @@ export async function bindCreationRequest(
|
|
|
1879
1879
|
)
|
|
1880
1880
|
)
|
|
1881
1881
|
throw new Error("session_closing");
|
|
1882
|
-
if (
|
|
1882
|
+
if (
|
|
1883
|
+
request.canonical_create_intent &&
|
|
1884
|
+
creationIntentDigest(request.canonical_create_intent) !== creationIntentDigest(intent)
|
|
1885
|
+
)
|
|
1883
1886
|
throw new Error("idempotency_conflict");
|
|
1884
1887
|
request.canonical_create_intent ??= intent;
|
|
1885
1888
|
request.session_id = session.session_id;
|
|
@@ -10995,16 +10995,24 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
10995
10995
|
}
|
|
10996
10996
|
if (status !== "accepted")
|
|
10997
10997
|
throw new SdkClientError("terminal_uncertain", "Workflow gate answer was not accepted.");
|
|
10998
|
-
const
|
|
10999
|
-
typeof resolution?.resolved_at === "string"
|
|
10998
|
+
const reportedResolvedAt =
|
|
10999
|
+
typeof resolution?.resolved_at === "string" &&
|
|
11000
|
+
Number.isFinite(Date.parse(resolution.resolved_at))
|
|
11001
|
+
? resolution.resolved_at
|
|
11002
|
+
: undefined;
|
|
11003
|
+
// The gate timestamp is remote metadata; retention must age the local receipt from
|
|
11004
|
+
// the coordinator's own persistence time so a skewed or replayed response cannot
|
|
11005
|
+
// delete a fresh answer.
|
|
11006
|
+
const persistedAt = new Date().toISOString();
|
|
11007
|
+
const resolvedAt = reportedResolvedAt ?? persistedAt;
|
|
11000
11008
|
await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
|
|
11001
11009
|
const question = transaction.canonical.questions[questionId];
|
|
11002
11010
|
if (!question || question.claim_fence_epoch !== (claimed as { fence: number }).fence)
|
|
11003
11011
|
throw new Error("terminal_uncertain");
|
|
11004
11012
|
question.status = "answered";
|
|
11005
|
-
question.answered_at =
|
|
11006
|
-
question.updated_at =
|
|
11007
|
-
question.history.push({ at:
|
|
11013
|
+
question.answered_at = persistedAt;
|
|
11014
|
+
question.updated_at = persistedAt;
|
|
11015
|
+
question.history.push({ at: persistedAt, status: "answered", reason: null });
|
|
11008
11016
|
const authority = transaction.canonical.gate_authorities[question.authority_id];
|
|
11009
11017
|
if (authority)
|
|
11010
11018
|
authority.outcome = { state: "answered", turn_id: turnId, question_id: questionId };
|
|
@@ -11022,7 +11030,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
11022
11030
|
resolved_at: resolvedAt,
|
|
11023
11031
|
};
|
|
11024
11032
|
request.phase = "completed";
|
|
11025
|
-
request.updated_at =
|
|
11033
|
+
request.updated_at = persistedAt;
|
|
11026
11034
|
});
|
|
11027
11035
|
return {
|
|
11028
11036
|
ok: true,
|
|
@@ -319,6 +319,8 @@ export interface BashExecutorOptions {
|
|
|
319
319
|
sessionKey?: string;
|
|
320
320
|
/** Additional environment variables to inject */
|
|
321
321
|
env?: Record<string, string>;
|
|
322
|
+
/** Environment names to hide from this command's inherited environment. */
|
|
323
|
+
unsetEnv?: string[];
|
|
322
324
|
/** Artifact path/id for full output storage */
|
|
323
325
|
artifactPath?: string;
|
|
324
326
|
artifactId?: string;
|
|
@@ -578,6 +580,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
|
|
|
578
580
|
command: finalCommand,
|
|
579
581
|
cwd: commandCwd,
|
|
580
582
|
env: commandEnv,
|
|
583
|
+
unsetEnv: options?.unsetEnv,
|
|
581
584
|
timeoutMs: executionTimeoutMs,
|
|
582
585
|
signal: runAbortController.signal,
|
|
583
586
|
};
|