@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
|
@@ -400,7 +400,11 @@ import type { NotificationSessionController } from "../sdk/bus/session-control";
|
|
|
400
400
|
import { buildSyntheticModelId, syntheticNamespaceCollision } from "../sdk/model-profile-model";
|
|
401
401
|
import { sanitizePromptFailure } from "../sdk/prompt-failure";
|
|
402
402
|
import type { SecretObfuscator } from "../secrets/obfuscator";
|
|
403
|
-
import {
|
|
403
|
+
import {
|
|
404
|
+
formatNoCredentialOnboardingError,
|
|
405
|
+
formatNoModelOnboardingError,
|
|
406
|
+
NoModelSelectedError,
|
|
407
|
+
} from "../setup/model-onboarding-guidance";
|
|
404
408
|
import {
|
|
405
409
|
isCanonicalGjcWorkflowSkill,
|
|
406
410
|
isWorkflowContinuationInert,
|
|
@@ -410,6 +414,7 @@ import {
|
|
|
410
414
|
import { assertWorkflowMutationAllowed } from "../skill-state/workflow-mutation-guard";
|
|
411
415
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
412
416
|
import { buildVolatileProjectContext } from "../system-prompt";
|
|
417
|
+
import { DelegationHintController } from "../task/delegation-hint";
|
|
413
418
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
414
419
|
import {
|
|
415
420
|
buildDiscoverableToolSearchIndex,
|
|
@@ -3633,6 +3638,7 @@ export class AgentSession {
|
|
|
3633
3638
|
/** Idempotent unregister handle for this session's resource-GC registration. */
|
|
3634
3639
|
#unregisterResourceGc?: () => void;
|
|
3635
3640
|
#unregisterRuntimeStateFinalizer?: () => void;
|
|
3641
|
+
#coordinatorRuntimeStateFileOverrideForTests: string | undefined = undefined;
|
|
3636
3642
|
#unregisterBeforeMoveListener?: () => void;
|
|
3637
3643
|
#unregisterMoveAbortListener?: () => void;
|
|
3638
3644
|
#unregisterMovePublicationListener?: () => void;
|
|
@@ -3658,6 +3664,7 @@ export class AgentSession {
|
|
|
3658
3664
|
);
|
|
3659
3665
|
}
|
|
3660
3666
|
#unregisterSessionMemorySettings?: () => void;
|
|
3667
|
+
#unregisterDelegationHintSettings?: () => void;
|
|
3661
3668
|
/**
|
|
3662
3669
|
* AsyncJobManager owned by this session (top-level only). Subagents leave
|
|
3663
3670
|
* this undefined and **MUST NOT** dispose the global instance on teardown.
|
|
@@ -3830,6 +3837,7 @@ export class AgentSession {
|
|
|
3830
3837
|
|
|
3831
3838
|
// TTSR manager for time-traveling stream rules
|
|
3832
3839
|
#ttsrManager: TtsrManager | undefined = undefined;
|
|
3840
|
+
#delegationHint: DelegationHintController;
|
|
3833
3841
|
#pendingTtsrInjections: Rule[] = [];
|
|
3834
3842
|
/** Per-tool TTSR rules whose `interruptMode` opted out of aborting the stream.
|
|
3835
3843
|
* These are folded into the matched tool call's `toolResult` content as an
|
|
@@ -4138,6 +4146,8 @@ export class AgentSession {
|
|
|
4138
4146
|
|
|
4139
4147
|
#appendCoordinatorPersist(run: () => Promise<void>): Promise<void> {
|
|
4140
4148
|
const queued = this.#coordinatorPersistQueue.then(run, run);
|
|
4149
|
+
const testFailures = this.#coordinatorPersistFailuresForTests;
|
|
4150
|
+
if (testFailures) void queued.catch(error => testFailures.push(error));
|
|
4141
4151
|
this.#coordinatorPersistQueue = queued.catch(() => {});
|
|
4142
4152
|
return queued;
|
|
4143
4153
|
}
|
|
@@ -5646,10 +5656,58 @@ export class AgentSession {
|
|
|
5646
5656
|
this.#credentialStoreIdentity = config.credentialStoreIdentity;
|
|
5647
5657
|
this.#providerCacheSessionId = config.providerCacheSessionId;
|
|
5648
5658
|
this.#asyncJobProviderSessionId = config.asyncJobProviderSessionId;
|
|
5659
|
+
this.#delegationHint = new DelegationHintController({
|
|
5660
|
+
getMode: () => this.settings.get("task.delegationHint.mode"),
|
|
5661
|
+
getAutoroutingEnabled: () => this.settings.getEffectiveAutorouting().active,
|
|
5662
|
+
getAutoroutingTiers: () => this.settings.get("task.autorouting.tiers"),
|
|
5663
|
+
notify: message => this.emitNotice("info", message, "delegation-hint"),
|
|
5664
|
+
});
|
|
5665
|
+
this.#unregisterDelegationHintSettings = this.settings.onChanged(settingPath => {
|
|
5666
|
+
if (settingPath === "task.delegationHint.mode") {
|
|
5667
|
+
this.#delegationHint.setEnabled(this.settings.get("task.delegationHint.mode") === "hint");
|
|
5668
|
+
}
|
|
5669
|
+
});
|
|
5649
5670
|
// Per-tool TTSR reminders are folded into the matched tool's result via this hook.
|
|
5650
|
-
this.agent.afterToolCall = ctx => {
|
|
5671
|
+
this.agent.afterToolCall = (ctx, signal) => {
|
|
5651
5672
|
settleToolLineageRegistrationWindow(ctx.toolCall.id, this.#ownedRegistrationEndpoint());
|
|
5652
|
-
|
|
5673
|
+
const ttsrResult = this.#ttsrAfterToolCall(ctx);
|
|
5674
|
+
const delegationHintEnabled = this.settings.get("task.delegationHint.mode") === "hint";
|
|
5675
|
+
this.#delegationHint.setEnabled(delegationHintEnabled);
|
|
5676
|
+
if (delegationHintEnabled) {
|
|
5677
|
+
try {
|
|
5678
|
+
this.#delegationHint.observeAfterToolCall(
|
|
5679
|
+
{
|
|
5680
|
+
toolName: ctx.toolCall.name,
|
|
5681
|
+
args: ctx.args,
|
|
5682
|
+
isError: ctx.isError,
|
|
5683
|
+
resultError: ctx.result.isError === true,
|
|
5684
|
+
getRemainingContextRatio: () => {
|
|
5685
|
+
const percent = this.getContextUsage()?.percent;
|
|
5686
|
+
return typeof percent === "number" && Number.isFinite(percent)
|
|
5687
|
+
? Math.min(1, Math.max(0, 1 - percent / 100))
|
|
5688
|
+
: undefined;
|
|
5689
|
+
},
|
|
5690
|
+
getActivePlanStepCount: () => {
|
|
5691
|
+
// Tasks within a phase are peers; do not combine separate ordered phases.
|
|
5692
|
+
return this.getTodoPhases().reduce(
|
|
5693
|
+
(largestPhase, phase) =>
|
|
5694
|
+
Math.max(
|
|
5695
|
+
largestPhase,
|
|
5696
|
+
phase.tasks.filter(
|
|
5697
|
+
task => task.status === "pending" || task.status === "in_progress",
|
|
5698
|
+
).length,
|
|
5699
|
+
),
|
|
5700
|
+
0,
|
|
5701
|
+
);
|
|
5702
|
+
},
|
|
5703
|
+
},
|
|
5704
|
+
signal,
|
|
5705
|
+
);
|
|
5706
|
+
} catch {
|
|
5707
|
+
// Advisory inference never overrides an executed tool result.
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5710
|
+
return ttsrResult;
|
|
5653
5711
|
};
|
|
5654
5712
|
// Bind immutable lineage/attempt metadata to each tool call id before the
|
|
5655
5713
|
// tool executes. Background registrations made inside the tool (task, Bash)
|
|
@@ -6910,6 +6968,7 @@ export class AgentSession {
|
|
|
6910
6968
|
|
|
6911
6969
|
/** Serializes sidecar writes in publication order, independent of write latency. */
|
|
6912
6970
|
#coordinatorPersistQueue: Promise<void> = Promise.resolve();
|
|
6971
|
+
#coordinatorPersistFailuresForTests: unknown[] | undefined;
|
|
6913
6972
|
|
|
6914
6973
|
#recordPostPublicationOutcome(
|
|
6915
6974
|
context: CoordinatorRuntimeStatePersistContext,
|
|
@@ -6975,6 +7034,9 @@ export class AgentSession {
|
|
|
6975
7034
|
*/
|
|
6976
7035
|
#runtimeStateMarkerFile(input: { sessionId: string; cwd: string }): string | null {
|
|
6977
7036
|
if (!input.sessionId.trim()) return null;
|
|
7037
|
+
if (this.#coordinatorRuntimeStateFileOverrideForTests) {
|
|
7038
|
+
return this.#coordinatorRuntimeStateFileOverrideForTests;
|
|
7039
|
+
}
|
|
6978
7040
|
const pinned = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
6979
7041
|
if (this.taskDepth > 0) return sessionRuntimeStatePath(input.cwd, input.sessionId);
|
|
6980
7042
|
return pinned || sessionRuntimeStatePath(input.cwd, input.sessionId);
|
|
@@ -7055,6 +7117,7 @@ export class AgentSession {
|
|
|
7055
7117
|
{ event: event.type, ...(attempt > 0 ? { retries: attempt } : {}) },
|
|
7056
7118
|
);
|
|
7057
7119
|
if (propagateFailure) throw error;
|
|
7120
|
+
this.#coordinatorPersistFailuresForTests?.push(error);
|
|
7058
7121
|
return;
|
|
7059
7122
|
}
|
|
7060
7123
|
}
|
|
@@ -7080,11 +7143,18 @@ export class AgentSession {
|
|
|
7080
7143
|
async #emitSessionEvent(event: AgentSessionEvent, eventLease?: RunResourceProducerLease): Promise<void> {
|
|
7081
7144
|
const attemptScope = (event as AgentSessionEvent & { scope?: AttemptScope }).scope;
|
|
7082
7145
|
if (event.type === "turn_start") {
|
|
7146
|
+
const delegationHintEnabled = this.settings.get("task.delegationHint.mode") === "hint";
|
|
7147
|
+
this.#delegationHint.setEnabled(delegationHintEnabled);
|
|
7148
|
+
if (delegationHintEnabled) this.#delegationHint.onTurnStart();
|
|
7083
7149
|
this.#extensionTurnGeneration++;
|
|
7084
7150
|
this.#closedExtensionTurnGeneration = undefined;
|
|
7085
7151
|
} else if (event.type === "turn_end") {
|
|
7086
7152
|
this.#closedExtensionTurnGeneration = this.#extensionTurnGeneration;
|
|
7087
7153
|
}
|
|
7154
|
+
if (event.type === "message_end" && event.message.role === "user") {
|
|
7155
|
+
this.#delegationHint.setEnabled(this.settings.get("task.delegationHint.mode") === "hint");
|
|
7156
|
+
this.#delegationHint.onUserMessage();
|
|
7157
|
+
}
|
|
7088
7158
|
if (event.type === "message_update") {
|
|
7089
7159
|
// Fast path: message_update maps to no sidecar state, so we must not
|
|
7090
7160
|
// build the persistRuntimeState closure here (per-token hot path).
|
|
@@ -10152,6 +10222,20 @@ export class AgentSession {
|
|
|
10152
10222
|
this.#disposeTimeoutMs = Math.max(0, timeoutMs);
|
|
10153
10223
|
}
|
|
10154
10224
|
|
|
10225
|
+
/** Bind coordinator runtime-state persistence to this session in tests. */
|
|
10226
|
+
setCoordinatorRuntimeStateFileForTests(stateFile: string): void {
|
|
10227
|
+
if (!path.isAbsolute(stateFile)) {
|
|
10228
|
+
throw new Error("Coordinator runtime-state test path must be absolute.");
|
|
10229
|
+
}
|
|
10230
|
+
this.#coordinatorRuntimeStateFileOverrideForTests = path.resolve(stateFile);
|
|
10231
|
+
this.#registerRuntimeStateFinalizer();
|
|
10232
|
+
}
|
|
10233
|
+
|
|
10234
|
+
/** Read the test-only coordinator runtime-state path bound to this session. */
|
|
10235
|
+
getCoordinatorRuntimeStateFileForTests(): string | undefined {
|
|
10236
|
+
return this.#coordinatorRuntimeStateFileOverrideForTests;
|
|
10237
|
+
}
|
|
10238
|
+
|
|
10155
10239
|
trackPostPromptTaskForTests(task: Promise<void>): void {
|
|
10156
10240
|
this.#trackPostPromptTask(task, this.#selectionFenceGeneration);
|
|
10157
10241
|
}
|
|
@@ -10332,6 +10416,8 @@ export class AgentSession {
|
|
|
10332
10416
|
this.#unregisterResourceGc = undefined;
|
|
10333
10417
|
this.#unregisterSessionMemorySettings?.();
|
|
10334
10418
|
this.#unregisterSessionMemorySettings = undefined;
|
|
10419
|
+
this.#unregisterDelegationHintSettings?.();
|
|
10420
|
+
this.#unregisterDelegationHintSettings = undefined;
|
|
10335
10421
|
if (ownerTerminalContextFromEnvironment() === null) this.#unregisterRuntimeStateFinalizer?.();
|
|
10336
10422
|
this.#unregisterRuntimeStateFinalizer = undefined;
|
|
10337
10423
|
this.#unregisterBeforeMoveListener?.();
|
|
@@ -10443,6 +10529,8 @@ export class AgentSession {
|
|
|
10443
10529
|
this.#unregisterResourceGc = undefined;
|
|
10444
10530
|
this.#unregisterSessionMemorySettings?.();
|
|
10445
10531
|
this.#unregisterSessionMemorySettings = undefined;
|
|
10532
|
+
this.#unregisterDelegationHintSettings?.();
|
|
10533
|
+
this.#unregisterDelegationHintSettings = undefined;
|
|
10446
10534
|
const work = Promise.allSettled([
|
|
10447
10535
|
// kill:true so a forced exit also reaps spawned-app Chrome we own (headless
|
|
10448
10536
|
// always closes; connected/attached browsers only disconnect — never killed).
|
|
@@ -10594,6 +10682,11 @@ export class AgentSession {
|
|
|
10594
10682
|
});
|
|
10595
10683
|
}
|
|
10596
10684
|
|
|
10685
|
+
/** Enable sidecar write failure reporting for retry-test teardown. */
|
|
10686
|
+
trackCoordinatorRuntimeStatePersistenceFailuresForTests(): void {
|
|
10687
|
+
this.#coordinatorPersistFailuresForTests = [];
|
|
10688
|
+
}
|
|
10689
|
+
|
|
10597
10690
|
/** Test seam: await all currently admitted coordinator sidecar writes. */
|
|
10598
10691
|
async awaitCoordinatorRuntimeStatePersistenceForTests(): Promise<void> {
|
|
10599
10692
|
// A terminal abort can schedule a preserved follow-up as a fresh turn after
|
|
@@ -10616,6 +10709,11 @@ export class AgentSession {
|
|
|
10616
10709
|
}
|
|
10617
10710
|
await this.#coordinatorPersistQueue;
|
|
10618
10711
|
await this.#drainUnbarrieredCoordinatorPersists();
|
|
10712
|
+
const failures = this.#coordinatorPersistFailuresForTests;
|
|
10713
|
+
if (failures && failures.length > 0) {
|
|
10714
|
+
this.#coordinatorPersistFailuresForTests = [];
|
|
10715
|
+
throw new AggregateError(failures, "Coordinator runtime-state persistence failed during test");
|
|
10716
|
+
}
|
|
10619
10717
|
}
|
|
10620
10718
|
queueCoordinatorRuntimeStatePersistForTests(event: AgentSessionEvent, gate: Promise<void>): Promise<void> {
|
|
10621
10719
|
this.#agentEventAdmission.set(event, {
|
|
@@ -13782,7 +13880,7 @@ export class AgentSession {
|
|
|
13782
13880
|
|
|
13783
13881
|
// Validate model
|
|
13784
13882
|
if (!this.model) {
|
|
13785
|
-
throw new
|
|
13883
|
+
throw new NoModelSelectedError();
|
|
13786
13884
|
}
|
|
13787
13885
|
|
|
13788
13886
|
// Validate API key
|
|
@@ -303,7 +303,7 @@ export type ManagedCandidateListing =
|
|
|
303
303
|
scope: ManagedScope;
|
|
304
304
|
owned: readonly ManagedCandidate[];
|
|
305
305
|
foreignCount: number;
|
|
306
|
-
invalid: readonly { code: string }[];
|
|
306
|
+
invalid: readonly { code: string; sessionId?: string }[];
|
|
307
307
|
}
|
|
308
308
|
| { kind: "error"; code: "scan_failed" | "unsafe_root" | "invalid_candidate"; message: string };
|
|
309
309
|
|
|
@@ -347,6 +347,7 @@ export interface ManagedLockReleaseTestEvent {
|
|
|
347
347
|
export const ManagedSessionScopeTestHooks: {
|
|
348
348
|
beforeVerifiedDelete?: (event: ManagedVerifiedDeleteTestEvent) => void | Promise<void>;
|
|
349
349
|
beforeManagedLockRelease?: (event: ManagedLockReleaseTestEvent) => void | Promise<void>;
|
|
350
|
+
afterCandidatePreflight?: (event: { readonly sessionId?: string }) => void;
|
|
350
351
|
} = {};
|
|
351
352
|
|
|
352
353
|
async function deleteSessionVerifiedWithFence(
|
|
@@ -801,49 +802,86 @@ export function fsyncCanonicalBinding(bindingPath: string, expected: string): vo
|
|
|
801
802
|
}
|
|
802
803
|
|
|
803
804
|
type CandidatePreflight =
|
|
804
|
-
| {
|
|
805
|
+
| {
|
|
806
|
+
kind: "capture";
|
|
807
|
+
sessionId: string;
|
|
808
|
+
cwd: string;
|
|
809
|
+
diagnosticSessionId?: string;
|
|
810
|
+
identity: Pick<ManagedFileSnapshot["identity"], "dev" | "ino" | "nlink" | "size" | "mtimeNs" | "ctimeNs">;
|
|
811
|
+
}
|
|
805
812
|
| {
|
|
806
813
|
kind: "foreign";
|
|
807
814
|
}
|
|
808
815
|
| {
|
|
809
816
|
kind: "invalid";
|
|
810
817
|
code: string;
|
|
818
|
+
diagnosticSessionId?: string;
|
|
811
819
|
};
|
|
812
820
|
|
|
821
|
+
function safeManagedCandidateSessionId(value: unknown): string | undefined {
|
|
822
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value) ? value : undefined;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function invalidCandidatePreflight(filePath: string, code: string): CandidatePreflight {
|
|
826
|
+
const diagnosticSessionId = safeManagedCandidateSessionId(path.basename(filePath, ".jsonl"));
|
|
827
|
+
return {
|
|
828
|
+
kind: "invalid",
|
|
829
|
+
code,
|
|
830
|
+
...(diagnosticSessionId === undefined ? {} : { diagnosticSessionId }),
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
813
834
|
function preflightCandidate(filePath: string, scope: ManagedScope): CandidatePreflight {
|
|
814
835
|
try {
|
|
815
836
|
const snapshot = captureManagedFilePrefixNoFollow(filePath, HEADER_MAX_BYTES);
|
|
816
837
|
const lineEnd = snapshot.bytes.indexOf(0x0a);
|
|
817
|
-
if (lineEnd < 0) return
|
|
838
|
+
if (lineEnd < 0) return invalidCandidatePreflight(filePath, "invalid_header");
|
|
818
839
|
const value: unknown = JSON.parse(snapshot.bytes.subarray(0, lineEnd).toString("utf8"));
|
|
819
840
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
820
|
-
return
|
|
841
|
+
return invalidCandidatePreflight(filePath, "invalid_header");
|
|
821
842
|
const header = value as Record<string, unknown>;
|
|
822
843
|
if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string")
|
|
823
|
-
return
|
|
844
|
+
return invalidCandidatePreflight(filePath, "invalid_header");
|
|
845
|
+
const diagnosticSessionId = safeManagedCandidateSessionId(header.id);
|
|
824
846
|
const candidateIdentity = identityFor(header.cwd);
|
|
825
847
|
if (
|
|
826
848
|
candidateIdentity.ok &&
|
|
827
849
|
(candidateIdentity.platform !== scope.platform || candidateIdentity.canonicalPath !== scope.canonicalCwd)
|
|
828
850
|
)
|
|
829
851
|
return { kind: "foreign" };
|
|
830
|
-
return {
|
|
831
|
-
|
|
832
|
-
|
|
852
|
+
return {
|
|
853
|
+
kind: "capture",
|
|
854
|
+
sessionId: header.id,
|
|
855
|
+
cwd: header.cwd,
|
|
856
|
+
...(diagnosticSessionId === undefined ? {} : { diagnosticSessionId }),
|
|
857
|
+
identity: snapshot.identity,
|
|
858
|
+
};
|
|
859
|
+
} catch (error) {
|
|
860
|
+
return invalidCandidatePreflight(
|
|
861
|
+
filePath,
|
|
862
|
+
error instanceof Error && error.message === "source_changed" ? "source_changed" : "unreadable_candidate",
|
|
863
|
+
);
|
|
833
864
|
}
|
|
834
865
|
}
|
|
835
866
|
|
|
836
867
|
function matchesPreflightIdentity(candidate: ManagedCandidate, preflight: CandidatePreflight): boolean {
|
|
837
868
|
return (
|
|
838
869
|
preflight.kind === "capture" &&
|
|
870
|
+
candidate.sessionId === preflight.sessionId &&
|
|
871
|
+
candidate.cwd === preflight.cwd &&
|
|
839
872
|
candidate.identity.dev === preflight.identity.dev &&
|
|
840
873
|
candidate.identity.ino === preflight.identity.ino &&
|
|
874
|
+
candidate.identity.nlink === preflight.identity.nlink &&
|
|
841
875
|
candidate.identity.size === preflight.identity.size &&
|
|
842
|
-
candidate.identity.mtimeNs === preflight.identity.mtimeNs
|
|
876
|
+
candidate.identity.mtimeNs === preflight.identity.mtimeNs &&
|
|
877
|
+
candidate.identity.ctimeNs === preflight.identity.ctimeNs
|
|
843
878
|
);
|
|
844
879
|
}
|
|
845
880
|
|
|
846
|
-
function inspectCandidate(
|
|
881
|
+
function inspectCandidate(
|
|
882
|
+
filePath: string,
|
|
883
|
+
provenance: "v2" | "legacy",
|
|
884
|
+
): ManagedCandidate | { code: string; sessionId?: string } {
|
|
847
885
|
try {
|
|
848
886
|
const snapshot = inspectManagedFileNoFollow(filePath, HEADER_MAX_BYTES);
|
|
849
887
|
const lineEnd = snapshot.bytes.subarray(0, HEADER_MAX_BYTES).indexOf(0x0a);
|
|
@@ -905,7 +943,7 @@ function discoveredLegacyDirectoryNames(scope: ManagedScope): readonly string[]
|
|
|
905
943
|
return [...known, ...discovered];
|
|
906
944
|
}
|
|
907
945
|
|
|
908
|
-
type CandidateInspection = ManagedCandidate | { code: string } | { foreign: true };
|
|
946
|
+
type CandidateInspection = ManagedCandidate | { code: string; sessionId?: string } | { foreign: true };
|
|
909
947
|
|
|
910
948
|
function listDirectoryCandidates(
|
|
911
949
|
directory: string,
|
|
@@ -926,11 +964,29 @@ function listDirectoryCandidates(
|
|
|
926
964
|
.map(entry => {
|
|
927
965
|
const filePath = path.join(directory, entry.name);
|
|
928
966
|
const preflight = preflightCandidate(filePath, scope);
|
|
929
|
-
if (preflight.kind === "invalid")
|
|
967
|
+
if (preflight.kind === "invalid")
|
|
968
|
+
return {
|
|
969
|
+
code: preflight.code,
|
|
970
|
+
...(preflight.diagnosticSessionId === undefined ? {} : { sessionId: preflight.diagnosticSessionId }),
|
|
971
|
+
};
|
|
930
972
|
if (preflight.kind === "foreign") return { foreign: true };
|
|
973
|
+
ManagedSessionScopeTestHooks.afterCandidatePreflight?.({
|
|
974
|
+
...(preflight.diagnosticSessionId === undefined ? {} : { sessionId: preflight.diagnosticSessionId }),
|
|
975
|
+
});
|
|
931
976
|
const candidate = inspectCandidate(filePath, provenance);
|
|
932
|
-
if ("code" in candidate)
|
|
933
|
-
|
|
977
|
+
if ("code" in candidate)
|
|
978
|
+
return {
|
|
979
|
+
...candidate,
|
|
980
|
+
...(candidate.sessionId === undefined && preflight.diagnosticSessionId !== undefined
|
|
981
|
+
? { sessionId: preflight.diagnosticSessionId }
|
|
982
|
+
: {}),
|
|
983
|
+
};
|
|
984
|
+
return matchesPreflightIdentity(candidate, preflight)
|
|
985
|
+
? candidate
|
|
986
|
+
: {
|
|
987
|
+
code: "source_changed",
|
|
988
|
+
...(preflight.diagnosticSessionId === undefined ? {} : { sessionId: preflight.diagnosticSessionId }),
|
|
989
|
+
};
|
|
934
990
|
});
|
|
935
991
|
}
|
|
936
992
|
|
|
@@ -1645,7 +1701,7 @@ export function listManagedCandidates(scope: ManagedScope): ManagedCandidateList
|
|
|
1645
1701
|
if (!root.isDirectory() || root.isSymbolicLink())
|
|
1646
1702
|
return { kind: "error", code: "unsafe_root", message: "The sessions root is unsafe." };
|
|
1647
1703
|
const owned: ManagedCandidate[] = [];
|
|
1648
|
-
const invalid: { code: string }[] = [];
|
|
1704
|
+
const invalid: { code: string; sessionId?: string }[] = [];
|
|
1649
1705
|
let foreignCount = 0;
|
|
1650
1706
|
const directories: Array<{ path: string; provenance: "v2" | "legacy" }> = [
|
|
1651
1707
|
{ path: scope.directoryPath, provenance: "v2" },
|
|
@@ -1658,7 +1714,10 @@ export function listManagedCandidates(scope: ManagedScope): ManagedCandidateList
|
|
|
1658
1714
|
for (const directory of directories) {
|
|
1659
1715
|
for (const candidate of listDirectoryCandidates(directory.path, directory.provenance, scope)) {
|
|
1660
1716
|
if ("code" in candidate) {
|
|
1661
|
-
invalid.push({
|
|
1717
|
+
invalid.push({
|
|
1718
|
+
code: candidate.code,
|
|
1719
|
+
...(candidate.sessionId === undefined ? {} : { sessionId: candidate.sessionId }),
|
|
1720
|
+
});
|
|
1662
1721
|
continue;
|
|
1663
1722
|
}
|
|
1664
1723
|
if ("foreign" in candidate) {
|
|
@@ -1667,7 +1726,12 @@ export function listManagedCandidates(scope: ManagedScope): ManagedCandidateList
|
|
|
1667
1726
|
}
|
|
1668
1727
|
const candidateIdentity = identityFor(candidate.cwd);
|
|
1669
1728
|
if (!candidateIdentity.ok) {
|
|
1670
|
-
invalid.push({
|
|
1729
|
+
invalid.push({
|
|
1730
|
+
code: `cwd_${candidateIdentity.code}`,
|
|
1731
|
+
...(safeManagedCandidateSessionId(candidate.sessionId) === undefined
|
|
1732
|
+
? {}
|
|
1733
|
+
: { sessionId: candidate.sessionId }),
|
|
1734
|
+
});
|
|
1671
1735
|
continue;
|
|
1672
1736
|
}
|
|
1673
1737
|
if (
|
|
@@ -61,6 +61,12 @@ import {
|
|
|
61
61
|
Snowflake,
|
|
62
62
|
toError,
|
|
63
63
|
} from "@gajae-code/utils";
|
|
64
|
+
import {
|
|
65
|
+
InvalidModelProfileOwnershipError,
|
|
66
|
+
MODEL_PROFILE_OWNERSHIP_ENTRY,
|
|
67
|
+
type ModelProfileOwnershipMarker,
|
|
68
|
+
validateModelProfileOwnershipMarker,
|
|
69
|
+
} from "../config/model-profile-ownership";
|
|
64
70
|
import { EDIT_SNAPSHOT_EXTERNALIZED_NOTICE, editSnapshotReceipt } from "../edit/renderer";
|
|
65
71
|
import type { TtsrInjectionRecord } from "../export/ttsr";
|
|
66
72
|
import { assertSafePathComponent } from "../gjc-runtime/session-layout";
|
|
@@ -1195,20 +1201,25 @@ function isValidPersistedReducerState(value: ReducerState): boolean {
|
|
|
1195
1201
|
}
|
|
1196
1202
|
|
|
1197
1203
|
function isProviderStateEntry(entry: SessionEntry): boolean {
|
|
1198
|
-
return
|
|
1199
|
-
"
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1204
|
+
return (
|
|
1205
|
+
(entry.type === "custom" && entry.customType === MODEL_PROFILE_OWNERSHIP_ENTRY) ||
|
|
1206
|
+
[
|
|
1207
|
+
"thinking_level_change",
|
|
1208
|
+
"model_change",
|
|
1209
|
+
"configured_model_chain",
|
|
1210
|
+
"service_tier_change",
|
|
1211
|
+
"mcp_tool_selection",
|
|
1212
|
+
"discovered_builtin_tool_selection",
|
|
1213
|
+
"mode_change",
|
|
1214
|
+
"ttsr_injection",
|
|
1215
|
+
].includes(entry.type)
|
|
1216
|
+
);
|
|
1208
1217
|
}
|
|
1209
1218
|
|
|
1210
1219
|
function providerStateEntryKey(entry: SessionEntry): string | undefined {
|
|
1211
1220
|
if (!isProviderStateEntry(entry)) return undefined;
|
|
1221
|
+
if (entry.type === "custom" && entry.customType === MODEL_PROFILE_OWNERSHIP_ENTRY)
|
|
1222
|
+
return `${entry.type}:${entry.customType}`;
|
|
1212
1223
|
return entry.type === "configured_model_chain" ? `${entry.type}:${entry.role}` : entry.type;
|
|
1213
1224
|
}
|
|
1214
1225
|
/** Map a session entry to its rolling-tail record kind. */
|
|
@@ -2091,6 +2102,8 @@ export interface SessionContext {
|
|
|
2091
2102
|
models: Record<string, string>;
|
|
2092
2103
|
/** Configured fallback chains for model roles on the active branch. */
|
|
2093
2104
|
configuredModelChains: Record<string, ConfiguredModelChain>;
|
|
2105
|
+
/** Effective-branch profile owner decision; absent means the legacy inherit marker. */
|
|
2106
|
+
modelProfileOwnershipMarker?: ModelProfileOwnershipMarker;
|
|
2094
2107
|
|
|
2095
2108
|
/** Names of TTSR rules that have been injected this session */
|
|
2096
2109
|
injectedTtsrRules: string[];
|
|
@@ -2986,6 +2999,7 @@ export function buildSessionContext(
|
|
|
2986
2999
|
let serviceTier: ServiceTier | undefined;
|
|
2987
3000
|
const models: Record<string, string> = {};
|
|
2988
3001
|
const configuredModelChains: Record<string, ConfiguredModelChain> = {};
|
|
3002
|
+
let modelProfileOwnershipMarker: ModelProfileOwnershipMarker | undefined;
|
|
2989
3003
|
|
|
2990
3004
|
let compaction: CompactionEntry | null = null;
|
|
2991
3005
|
const injectedTtsrRulesSet = new Set<string>();
|
|
@@ -3030,6 +3044,10 @@ export function buildSessionContext(
|
|
|
3030
3044
|
} else if (configuredChain) {
|
|
3031
3045
|
configuredModelChains[configuredChain.role] = configuredChain;
|
|
3032
3046
|
}
|
|
3047
|
+
} else if (entry.type === "custom" && entry.customType === MODEL_PROFILE_OWNERSHIP_ENTRY) {
|
|
3048
|
+
const validated = validateModelProfileOwnershipMarker(entry.data);
|
|
3049
|
+
if (!validated) throw new InvalidModelProfileOwnershipError();
|
|
3050
|
+
modelProfileOwnershipMarker = validated;
|
|
3033
3051
|
} else if (entry.type === "service_tier_change") {
|
|
3034
3052
|
serviceTier = entry.serviceTier ?? undefined;
|
|
3035
3053
|
} else if (entry.type === "message" && entry.message.role === "assistant") {
|
|
@@ -3185,6 +3203,7 @@ export function buildSessionContext(
|
|
|
3185
3203
|
serviceTier,
|
|
3186
3204
|
models,
|
|
3187
3205
|
configuredModelChains,
|
|
3206
|
+
...(modelProfileOwnershipMarker ? { modelProfileOwnershipMarker } : {}),
|
|
3188
3207
|
|
|
3189
3208
|
injectedTtsrRules,
|
|
3190
3209
|
injectedTtsrRuleRecords: injectedTtsrRuleRecordsArray,
|
|
@@ -3206,6 +3225,9 @@ function cloneSessionContext(context: SessionContext): SessionContext {
|
|
|
3206
3225
|
...context,
|
|
3207
3226
|
messages,
|
|
3208
3227
|
models: { ...context.models },
|
|
3228
|
+
modelProfileOwnershipMarker: context.modelProfileOwnershipMarker
|
|
3229
|
+
? { ...context.modelProfileOwnershipMarker }
|
|
3230
|
+
: undefined,
|
|
3209
3231
|
configuredModelChains: Object.fromEntries(
|
|
3210
3232
|
Object.entries(context.configuredModelChains ?? {}).map(([role, chain]) => [
|
|
3211
3233
|
role,
|
|
@@ -6063,7 +6085,6 @@ async function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?:
|
|
|
6063
6085
|
changed = true;
|
|
6064
6086
|
return [];
|
|
6065
6087
|
}
|
|
6066
|
-
|
|
6067
6088
|
return [
|
|
6068
6089
|
(async () => {
|
|
6069
6090
|
if (
|
|
@@ -18385,6 +18406,42 @@ export class SessionManager {
|
|
|
18385
18406
|
return entry.id;
|
|
18386
18407
|
}
|
|
18387
18408
|
|
|
18409
|
+
appendPreparedModelProfileOwnershipMarker(
|
|
18410
|
+
prepared: PreparedNewSession,
|
|
18411
|
+
marker: ModelProfileOwnershipMarker,
|
|
18412
|
+
): string {
|
|
18413
|
+
const validated = validateModelProfileOwnershipMarker(marker);
|
|
18414
|
+
if (!validated) throw new InvalidModelProfileOwnershipError();
|
|
18415
|
+
const stage = this.#getPreparedNewSessionStage(prepared);
|
|
18416
|
+
const entry: CustomEntry<ModelProfileOwnershipMarker> = {
|
|
18417
|
+
type: "custom",
|
|
18418
|
+
customType: MODEL_PROFILE_OWNERSHIP_ENTRY,
|
|
18419
|
+
data: validated,
|
|
18420
|
+
id: this.#nextPreparedNewSessionEntryId(stage),
|
|
18421
|
+
parentId: this.#preparedNewSessionLeafId(stage),
|
|
18422
|
+
timestamp: new Date().toISOString(),
|
|
18423
|
+
};
|
|
18424
|
+
stage.fileEntries.push(entry);
|
|
18425
|
+
return entry.id;
|
|
18426
|
+
}
|
|
18427
|
+
|
|
18428
|
+
appendModelProfileOwnershipMarker(marker: ModelProfileOwnershipMarker): string {
|
|
18429
|
+
const validated = validateModelProfileOwnershipMarker(marker);
|
|
18430
|
+
if (!validated) throw new InvalidModelProfileOwnershipError();
|
|
18431
|
+
return this.appendCustomEntry(MODEL_PROFILE_OWNERSHIP_ENTRY, validated);
|
|
18432
|
+
}
|
|
18433
|
+
|
|
18434
|
+
getModelProfileOwnershipMarker(): ModelProfileOwnershipMarker | undefined {
|
|
18435
|
+
let marker: ModelProfileOwnershipMarker | undefined;
|
|
18436
|
+
for (const entry of this.getBranch()) {
|
|
18437
|
+
if (entry.type !== "custom" || entry.customType !== MODEL_PROFILE_OWNERSHIP_ENTRY) continue;
|
|
18438
|
+
const validated = validateModelProfileOwnershipMarker(entry.data);
|
|
18439
|
+
if (!validated) throw new InvalidModelProfileOwnershipError();
|
|
18440
|
+
marker = validated;
|
|
18441
|
+
}
|
|
18442
|
+
return marker;
|
|
18443
|
+
}
|
|
18444
|
+
|
|
18388
18445
|
/**
|
|
18389
18446
|
* Append a root marker that starts a fresh active branch without changing the
|
|
18390
18447
|
* session id or deleting earlier durable entries. Subsequent messages descend
|
|
@@ -18392,6 +18449,7 @@ export class SessionManager {
|
|
|
18392
18449
|
* available for diagnostics/export.
|
|
18393
18450
|
*/
|
|
18394
18451
|
appendContextClearEntry(data?: Record<string, unknown>): string {
|
|
18452
|
+
const ownershipMarker = this.getModelProfileOwnershipMarker();
|
|
18395
18453
|
const entry: CustomEntry = {
|
|
18396
18454
|
type: "custom",
|
|
18397
18455
|
customType: "context_clear",
|
|
@@ -18401,6 +18459,7 @@ export class SessionManager {
|
|
|
18401
18459
|
timestamp: new Date().toISOString(),
|
|
18402
18460
|
};
|
|
18403
18461
|
this.#appendEntry(entry);
|
|
18462
|
+
if (ownershipMarker) this.appendModelProfileOwnershipMarker(ownershipMarker);
|
|
18404
18463
|
return entry.id;
|
|
18405
18464
|
}
|
|
18406
18465
|
|
|
@@ -27,6 +27,32 @@ export function formatNoModelOnboardingError(): string {
|
|
|
27
27
|
return `No model selected.\n\n${formatModelOnboardingGuidance()}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** Control-protocol code for a prompt refused because the session has no model. */
|
|
31
|
+
export const MODEL_NOT_SELECTED_CODE = "model_not_selected";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fixed public text for {@link MODEL_NOT_SELECTED_CODE}. The onboarding
|
|
35
|
+
* guidance names providers, commands, environment variables and local setup
|
|
36
|
+
* paths, so it stays local-only; external clients get this constant instead.
|
|
37
|
+
*/
|
|
38
|
+
export const MODEL_NOT_SELECTED_PUBLIC_MESSAGE =
|
|
39
|
+
"No model is selected for this session. Select a model before submitting a prompt.";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Missing-model prompt preflight failure. The message keeps the full local
|
|
43
|
+
* onboarding guidance for the TUI and in-process callers; the `code` is what
|
|
44
|
+
* lets the SDK control surface answer with a safe, known diagnostic instead of
|
|
45
|
+
* a generic internal error.
|
|
46
|
+
*/
|
|
47
|
+
export class NoModelSelectedError extends Error {
|
|
48
|
+
readonly code = MODEL_NOT_SELECTED_CODE;
|
|
49
|
+
|
|
50
|
+
constructor() {
|
|
51
|
+
super(formatNoModelOnboardingError());
|
|
52
|
+
this.name = "NoModelSelectedError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
export function formatNoCredentialOnboardingError(providerId: string): string {
|
|
31
57
|
const lines = [
|
|
32
58
|
`No credentials found for ${providerId}.`,
|