@gajae-code/coding-agent 0.12.8 → 0.12.11
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 +36 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +23 -0
- package/dist/types/discovery/agents-md.d.ts +11 -1
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +23 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +20 -2
- package/dist/types/session/internal/native-publish-outcome.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +17 -0
- package/dist/types/session/session-storage.d.ts +1 -0
- package/dist/types/system-prompt.d.ts +12 -1
- package/dist/types/tools/composer-bash-policy.d.ts +9 -1
- package/dist/types/tools/computer.d.ts +35 -2
- package/dist/types/tools/todo-write.d.ts +2 -0
- package/package.json +8 -8
- package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
- package/src/cli/notify-cli.ts +12 -6
- package/src/commands/sdk.ts +54 -0
- package/src/config/model-profiles.ts +8 -0
- package/src/config/model-registry.ts +4 -1
- package/src/discovery/agents-md.ts +76 -19
- package/src/gjc-runtime/psmux-detect.ts +12 -16
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/modes/components/extensions/inspector-panel.ts +9 -5
- package/src/modes/controllers/selector-controller.ts +61 -43
- package/src/modes/print-mode.ts +3 -0
- package/src/modes/types.ts +6 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/computer.md +6 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/notification-service.ts +140 -7
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -3
- package/src/sdk/session.ts +24 -7
- package/src/session/agent-session.ts +176 -34
- package/src/session/internal/managed-session-storage.ts +280 -248
- package/src/session/internal/native-publish-outcome.ts +12 -2
- package/src/session/session-manager.ts +140 -6
- package/src/session/session-storage.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +35 -11
- package/src/system-prompt.ts +28 -15
- package/src/tools/bash.ts +3 -1
- package/src/tools/composer-bash-policy.ts +21 -5
- package/src/tools/computer.ts +322 -45
- package/src/tools/todo-write.ts +29 -11
- package/src/utils/command-args.ts +19 -30
- package/src/utils/lang-from-path.ts +2 -4
|
@@ -109,6 +109,7 @@ import {
|
|
|
109
109
|
streamSimple,
|
|
110
110
|
} from "@gajae-code/ai";
|
|
111
111
|
import {
|
|
112
|
+
type AuthDisposition,
|
|
112
113
|
beginAttempt,
|
|
113
114
|
classifyFallbackTrigger,
|
|
114
115
|
type FallbackAttemptToken,
|
|
@@ -426,6 +427,7 @@ import {
|
|
|
426
427
|
getLatestCompactionEntry,
|
|
427
428
|
getSessionMessageEntryId,
|
|
428
429
|
getSessionMessageObservationId,
|
|
430
|
+
SessionAppendPersistenceError,
|
|
429
431
|
transferSessionMessageIdentity,
|
|
430
432
|
} from "./session-manager";
|
|
431
433
|
import { getEntriesForInternalRead, getSessionContextForInternalRead } from "./session-manager-internal";
|
|
@@ -3898,14 +3900,19 @@ export class AgentSession {
|
|
|
3898
3900
|
) {
|
|
3899
3901
|
const isRosterReminder = event.message.role === "custom" && event.message.customType === "irc-peer-roster";
|
|
3900
3902
|
if (!isRosterReminder) {
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3903
|
+
try {
|
|
3904
|
+
this.#appendCustomMessageEntry(
|
|
3905
|
+
event.message.customType,
|
|
3906
|
+
event.message.content,
|
|
3907
|
+
event.message.display,
|
|
3908
|
+
event.message.details,
|
|
3909
|
+
event.message.attribution ?? "agent",
|
|
3910
|
+
getSessionMessageObservationId(event.message),
|
|
3911
|
+
);
|
|
3912
|
+
} catch (error) {
|
|
3913
|
+
this.agent.abort();
|
|
3914
|
+
throw error;
|
|
3915
|
+
}
|
|
3909
3916
|
}
|
|
3910
3917
|
} else if (
|
|
3911
3918
|
event.message.role === "user" ||
|
|
@@ -3914,7 +3921,73 @@ export class AgentSession {
|
|
|
3914
3921
|
event.message.role === "toolResult" ||
|
|
3915
3922
|
event.message.role === "fileMention"
|
|
3916
3923
|
) {
|
|
3917
|
-
|
|
3924
|
+
try {
|
|
3925
|
+
this.sessionManager.appendMessage(event.message);
|
|
3926
|
+
} catch (error) {
|
|
3927
|
+
if (
|
|
3928
|
+
event.message.role !== "toolResult" ||
|
|
3929
|
+
event.message.toolName !== "todo_write" ||
|
|
3930
|
+
!(error instanceof SessionAppendPersistenceError) ||
|
|
3931
|
+
error.phase !== "current_append"
|
|
3932
|
+
) {
|
|
3933
|
+
this.agent.abort();
|
|
3934
|
+
throw error;
|
|
3935
|
+
}
|
|
3936
|
+
this.agent.abort();
|
|
3937
|
+
|
|
3938
|
+
const failure = error.persistenceError.message;
|
|
3939
|
+
const failedEntryId = error.entryId;
|
|
3940
|
+
this.#syncTodoPhasesFromBranch();
|
|
3941
|
+
event.message.isError = true;
|
|
3942
|
+
event.message.content = [
|
|
3943
|
+
{
|
|
3944
|
+
type: "text",
|
|
3945
|
+
text: `Todo state persistence failed: ${failure}\nDo not change the payload solely because of this failure. The durable outcome is unknown; reconcile the session state before retrying or continuing.`,
|
|
3946
|
+
},
|
|
3947
|
+
];
|
|
3948
|
+
event.message.details = {
|
|
3949
|
+
...(event.message.details && typeof event.message.details === "object" ? event.message.details : {}),
|
|
3950
|
+
phases: this.getTodoPhases(),
|
|
3951
|
+
failureKind: "persistence",
|
|
3952
|
+
};
|
|
3953
|
+
this.agent.touchContext();
|
|
3954
|
+
let recovered = false;
|
|
3955
|
+
try {
|
|
3956
|
+
await this.sessionManager.recoverPersistenceFailure();
|
|
3957
|
+
recovered = true;
|
|
3958
|
+
} catch (recoveryError) {
|
|
3959
|
+
logger.warn("Todo persistence recovery failed", {
|
|
3960
|
+
error: recoveryError instanceof Error ? recoveryError.message : String(recoveryError),
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
if (recovered) {
|
|
3964
|
+
const durableTodoResult = this.sessionManager
|
|
3965
|
+
.getBranch()
|
|
3966
|
+
.find(
|
|
3967
|
+
entry =>
|
|
3968
|
+
entry.id === failedEntryId &&
|
|
3969
|
+
entry.type === "message" &&
|
|
3970
|
+
entry.message.role === "toolResult" &&
|
|
3971
|
+
entry.message.toolName === "todo_write",
|
|
3972
|
+
);
|
|
3973
|
+
this.#syncTodoPhasesFromBranch();
|
|
3974
|
+
if (durableTodoResult?.type === "message" && durableTodoResult.message.role === "toolResult") {
|
|
3975
|
+
event.message.content = durableTodoResult.message.content;
|
|
3976
|
+
event.message.details = durableTodoResult.message.details;
|
|
3977
|
+
event.message.isError = durableTodoResult.message.isError;
|
|
3978
|
+
} else {
|
|
3979
|
+
event.message.details = {
|
|
3980
|
+
...(event.message.details && typeof event.message.details === "object"
|
|
3981
|
+
? event.message.details
|
|
3982
|
+
: {}),
|
|
3983
|
+
phases: this.getTodoPhases(),
|
|
3984
|
+
failureKind: "persistence",
|
|
3985
|
+
};
|
|
3986
|
+
this.sessionManager.appendMessage(event.message);
|
|
3987
|
+
this.agent.touchContext();
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3918
3991
|
}
|
|
3919
3992
|
}
|
|
3920
3993
|
|
|
@@ -4188,7 +4261,13 @@ export class AgentSession {
|
|
|
4188
4261
|
if (event.message.role === "toolResult") {
|
|
4189
4262
|
const { toolName, details, isError, content } = event.message as {
|
|
4190
4263
|
toolName?: string;
|
|
4191
|
-
details?: {
|
|
4264
|
+
details?: {
|
|
4265
|
+
path?: string;
|
|
4266
|
+
phases?: TodoPhase[];
|
|
4267
|
+
report?: string;
|
|
4268
|
+
startedAt?: string;
|
|
4269
|
+
failureKind?: "payload_rejected" | "argument_validation" | "execution" | "persistence";
|
|
4270
|
+
};
|
|
4192
4271
|
isError?: boolean;
|
|
4193
4272
|
content?: Array<TextContent | ImageContent>;
|
|
4194
4273
|
};
|
|
@@ -4201,22 +4280,34 @@ export class AgentSession {
|
|
|
4201
4280
|
}
|
|
4202
4281
|
if (toolName === "todo_write" && isError) {
|
|
4203
4282
|
const errorText = content?.find(part => part.type === "text")?.text;
|
|
4283
|
+
const payloadRejected =
|
|
4284
|
+
details?.failureKind === "payload_rejected" || details?.failureKind === "argument_validation";
|
|
4204
4285
|
const reminderText = [
|
|
4205
4286
|
"<system-reminder>",
|
|
4206
|
-
|
|
4287
|
+
payloadRejected
|
|
4288
|
+
? "todo_write rejected its payload. The requested todo update was not applied."
|
|
4289
|
+
: "todo_write did not complete because the turn aborted during runtime execution or persistence.",
|
|
4207
4290
|
errorText ? `Failure: ${errorText}` : "Failure: todo_write returned an error.",
|
|
4208
|
-
|
|
4291
|
+
payloadRejected
|
|
4292
|
+
? "Correct the payload and call todo_write again before continuing."
|
|
4293
|
+
: "Do not change the payload solely because of this failure. The durable outcome is unknown; reconcile the session state before retrying or continuing.",
|
|
4209
4294
|
"</system-reminder>",
|
|
4210
4295
|
].join("\n");
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4296
|
+
try {
|
|
4297
|
+
await this.sendCustomMessage(
|
|
4298
|
+
{
|
|
4299
|
+
customType: "todo-write-error-reminder",
|
|
4300
|
+
content: reminderText,
|
|
4301
|
+
display: false,
|
|
4302
|
+
details: { toolName, errorText },
|
|
4303
|
+
},
|
|
4304
|
+
{ deliverAs: "nextTurn" },
|
|
4305
|
+
);
|
|
4306
|
+
} catch (error) {
|
|
4307
|
+
logger.warn("Todo write error reminder persistence failed", {
|
|
4308
|
+
error: error instanceof Error ? error.message : String(error),
|
|
4309
|
+
});
|
|
4310
|
+
}
|
|
4220
4311
|
}
|
|
4221
4312
|
if (toolName === "checkpoint" && !isError) {
|
|
4222
4313
|
const checkpointEntryId = this.sessionManager.getLeafId();
|
|
@@ -14228,6 +14319,12 @@ export class AgentSession {
|
|
|
14228
14319
|
);
|
|
14229
14320
|
}
|
|
14230
14321
|
const trigger = classifyFallbackTrigger(transportFailure ?? { status: message.errorStatus });
|
|
14322
|
+
// A plain `forbidden` is terminal. Without this, the blanket
|
|
14323
|
+
// `transportFailure` admission below re-admits a committed forbidden
|
|
14324
|
+
// failure into retry policy: credential mutation is vetoed downstream, but
|
|
14325
|
+
// the chain would still retry and advance models on a request the caller is
|
|
14326
|
+
// simply not authorized to make.
|
|
14327
|
+
if (trigger.class === "auth" && trigger.authDisposition === "forbidden") return false;
|
|
14231
14328
|
if (transportFailure) return true;
|
|
14232
14329
|
if (
|
|
14233
14330
|
trigger.class === "rate_limit" ||
|
|
@@ -14616,7 +14713,10 @@ export class AgentSession {
|
|
|
14616
14713
|
message: AssistantMessage,
|
|
14617
14714
|
allowLegacyUsageLimit: boolean,
|
|
14618
14715
|
transportFailure?: TransportFailureFacts,
|
|
14619
|
-
|
|
14716
|
+
// `authDisposition` is carried through from `classifyFallbackTrigger`, whose
|
|
14717
|
+
// result is returned verbatim below. Narrowing it away here would silently
|
|
14718
|
+
// disable the terminal-403 guard in `#markFailedCredential`.
|
|
14719
|
+
): { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition } | undefined {
|
|
14620
14720
|
if (classifyContextOverflow(message, transportFailure, this.model?.contextWindow ?? 0)) return undefined;
|
|
14621
14721
|
const transport = classifyFallbackTrigger(transportFailure ?? { status: message.errorStatus });
|
|
14622
14722
|
if (transport.class !== "other") return transport;
|
|
@@ -14723,25 +14823,60 @@ export class AgentSession {
|
|
|
14723
14823
|
return `Model fallback chain exhausted; models tried: ${tried}; models skipped: ${skipped}`;
|
|
14724
14824
|
}
|
|
14725
14825
|
|
|
14726
|
-
|
|
14826
|
+
/**
|
|
14827
|
+
* Marks the credential that just failed and reports whether the session
|
|
14828
|
+
* actually moved to a DIFFERENT stored credential.
|
|
14829
|
+
*
|
|
14830
|
+
* Three invariants this enforces, in order:
|
|
14831
|
+
*
|
|
14832
|
+
* 1. **Pin guard, first and for every trigger class.** A pinned credential
|
|
14833
|
+
* (`--api-key` or `--credential`) must never be mutated or rotated away
|
|
14834
|
+
* from. Both overrides are consulted: they live in separate maps, so
|
|
14835
|
+
* checking only `hasRuntimeApiKey` would silently rotate a `--credential`
|
|
14836
|
+
* pin. Previously the guard existed only on the quota path, so the `auth`
|
|
14837
|
+
* path invalidated pinned credentials outright.
|
|
14838
|
+
* 2. **A terminal `forbidden` never mutates credential state.** Rotation
|
|
14839
|
+
* would hide an authorization defect and cycle through healthy rows.
|
|
14840
|
+
* 3. **Distinct-row proof in BOTH branches.** `invalidateCredentialMatching`
|
|
14841
|
+
* reports "I matched and blocked a row", which is not the same as "the
|
|
14842
|
+
* session now uses a different credential" — with a single-row pool it is
|
|
14843
|
+
* true while nothing rotated. Both branches therefore re-resolve and
|
|
14844
|
+
* require the active key to have actually changed.
|
|
14845
|
+
*/
|
|
14846
|
+
async #markFailedCredential(trigger: {
|
|
14847
|
+
class: FallbackTriggerClass;
|
|
14848
|
+
retryAfterMs?: number;
|
|
14849
|
+
authDisposition?: AuthDisposition;
|
|
14850
|
+
}): Promise<boolean> {
|
|
14727
14851
|
if (!this.model || (trigger.class !== "auth" && trigger.class !== "quota" && trigger.class !== "rate_limit")) {
|
|
14728
14852
|
return false;
|
|
14729
14853
|
}
|
|
14854
|
+
// (2) Terminal forbidden: no credential state may change.
|
|
14855
|
+
if (trigger.class === "auth" && trigger.authDisposition === "forbidden") return false;
|
|
14856
|
+
|
|
14730
14857
|
const authStorage = this.#modelRegistry.authStorage;
|
|
14858
|
+
const provider = this.model.provider;
|
|
14859
|
+
// (1) Pin guard, before any mutation and for every branch.
|
|
14860
|
+
if (authStorage.hasRuntimeApiKey(provider) || authStorage.hasRuntimeCredentialSelector(provider)) return false;
|
|
14861
|
+
|
|
14731
14862
|
const providerAffinitySessionId = this.agent.providerSessionId ?? this.agent.sessionId ?? this.sessionId;
|
|
14863
|
+
const activeApiKey = await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId);
|
|
14864
|
+
|
|
14865
|
+
let mutated: boolean;
|
|
14732
14866
|
if (trigger.class === "auth") {
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
return authStorage.invalidateCredentialMatching(this.model.provider, apiKey, {
|
|
14867
|
+
if (!isAuthenticated(activeApiKey)) return false;
|
|
14868
|
+
mutated = await authStorage.invalidateCredentialMatching(provider, activeApiKey, {
|
|
14736
14869
|
sessionId: providerAffinitySessionId,
|
|
14737
14870
|
});
|
|
14871
|
+
} else {
|
|
14872
|
+
mutated = await authStorage.markUsageLimitReached(provider, providerAffinitySessionId, {
|
|
14873
|
+
retryAfterMs: trigger.retryAfterMs,
|
|
14874
|
+
});
|
|
14738
14875
|
}
|
|
14739
|
-
if (
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
});
|
|
14744
|
-
return rotated && (await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId)) !== activeApiKey;
|
|
14876
|
+
if (!mutated) return false;
|
|
14877
|
+
|
|
14878
|
+
// (3) Distinct-row proof.
|
|
14879
|
+
return (await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId)) !== activeApiKey;
|
|
14745
14880
|
}
|
|
14746
14881
|
|
|
14747
14882
|
/** Handle retryable errors with exponential backoff. */
|
|
@@ -14836,8 +14971,15 @@ export class AgentSession {
|
|
|
14836
14971
|
credentialRotated = await this.#markFailedCredential(trigger);
|
|
14837
14972
|
}
|
|
14838
14973
|
if (credentialRotated) {
|
|
14839
|
-
if
|
|
14840
|
-
|
|
14974
|
+
// A rotation only becomes a same-model retry if the controller can
|
|
14975
|
+
// actually be rewound. `restorePreviousEntryForRetry()` refuses once an
|
|
14976
|
+
// entry's restore budget is consumed or attempts are exhausted; ignoring
|
|
14977
|
+
// that would force `outcome = "retry"` while `activeIndex` stays on the
|
|
14978
|
+
// next entry and `this.model` stays on the previous one — splitting
|
|
14979
|
+
// attempt attribution, exhaustion, and sticky selection across two models.
|
|
14980
|
+
if (!managedFallback || controller.restorePreviousEntryForRetry()) {
|
|
14981
|
+
outcome = "retry";
|
|
14982
|
+
}
|
|
14841
14983
|
}
|
|
14842
14984
|
if (outcome === "exhausted") {
|
|
14843
14985
|
if (managedFallback) {
|