@gajae-code/coding-agent 0.12.12 → 0.12.13
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 +30 -0
- package/dist/types/cli/marketplace-hint.d.ts +28 -0
- package/dist/types/cli.d.ts +10 -0
- package/dist/types/config/file-lock-gc.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/models-config-schema.d.ts +99 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +14 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +11 -0
- package/dist/types/modes/acp/acp-agent.d.ts +24 -1
- package/dist/types/sdk/bus/html-format.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +15 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +10 -0
- package/dist/types/session/agent-session.d.ts +0 -2
- package/dist/types/session/session-manager.d.ts +17 -1
- package/dist/types/tools/index.d.ts +7 -0
- package/package.json +8 -8
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/marketplace-hint.ts +39 -0
- package/src/cli/plugin-cli.ts +10 -0
- package/src/cli.ts +23 -0
- package/src/config/file-lock-gc.ts +47 -26
- package/src/config/model-profiles.ts +18 -18
- package/src/config/model-registry.ts +17 -5
- package/src/config/models-config-schema.ts +9 -4
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/gjc-runtime/deep-interview-recorder.ts +9 -5
- package/src/gjc-runtime/deep-interview-state.ts +27 -3
- package/src/gjc-runtime/gc-render.ts +8 -1
- package/src/gjc-runtime/gc-runtime.ts +15 -1
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/main.ts +11 -3
- package/src/modes/acp/acp-agent.ts +123 -7
- package/src/modes/components/model-selector.ts +52 -5
- package/src/modes/print-mode.ts +10 -0
- package/src/prompts/system/project-prompt.md +19 -0
- package/src/sdk/bus/html-format.ts +19 -3
- package/src/sdk/bus/index.ts +21 -12
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -2
- package/src/sdk/bus/telegram-daemon.ts +50 -16
- package/src/sdk/bus/telegram-reference.ts +72 -9
- package/src/sdk/session.ts +5 -1
- package/src/session/agent-session.ts +0 -4
- package/src/session/internal/managed-session-storage.ts +24 -2
- package/src/session/session-manager.ts +77 -22
- package/src/setup/provider-presets.json +3 -3
- package/src/thinking-metadata.ts +2 -1
- package/src/tools/ask.ts +10 -0
- package/src/tools/index.ts +7 -0
- package/src/tools/todo-write.ts +4 -4
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
|
|
17
17
|
import * as crypto from "node:crypto";
|
|
18
18
|
import * as fs from "node:fs";
|
|
19
|
+
import { logger } from "@gajae-code/utils";
|
|
19
20
|
import {
|
|
20
21
|
bold,
|
|
21
22
|
buildCompactChoiceGrid,
|
|
22
23
|
escapeHtml,
|
|
23
24
|
numberedOptionList,
|
|
25
|
+
renumberableOptionText,
|
|
24
26
|
splitTelegramHtml,
|
|
25
27
|
TELEGRAM_PARSE_MODE,
|
|
26
28
|
} from "./html-format";
|
|
@@ -34,6 +36,44 @@ const REFERENCE_CLIENT_HELLO = {
|
|
|
34
36
|
capabilities: ["ask_controls_v1"],
|
|
35
37
|
} as const;
|
|
36
38
|
|
|
39
|
+
const MAX_DIAGNOSTIC_CAPABILITIES = 4;
|
|
40
|
+
const MAX_DIAGNOSTIC_CAPABILITY_CODE_POINTS = 64;
|
|
41
|
+
const MAX_DIAGNOSTIC_ERROR_NAME_CODE_POINTS = 64;
|
|
42
|
+
const MAX_DIAGNOSTIC_ERROR_MESSAGE_CODE_POINTS = 128;
|
|
43
|
+
|
|
44
|
+
function boundedDiagnosticText(value: string, maxCodePoints: number): string {
|
|
45
|
+
let result = "";
|
|
46
|
+
let codePoints = 0;
|
|
47
|
+
for (const character of value) {
|
|
48
|
+
const codePoint = character.codePointAt(0)!;
|
|
49
|
+
if (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)) continue;
|
|
50
|
+
result += character;
|
|
51
|
+
codePoints++;
|
|
52
|
+
if (codePoints === maxCodePoints) break;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function boundedDiagnosticCallbackError(error: unknown): Readonly<{ errorName: string; errorMessage: string }> {
|
|
58
|
+
let errorName = "UnknownError";
|
|
59
|
+
let errorMessage = "Diagnostic callback threw";
|
|
60
|
+
try {
|
|
61
|
+
if (error instanceof Error) {
|
|
62
|
+
errorName = boundedDiagnosticText(error.name, MAX_DIAGNOSTIC_ERROR_NAME_CODE_POINTS) || "Error";
|
|
63
|
+
errorMessage =
|
|
64
|
+
boundedDiagnosticText(error.message, MAX_DIAGNOSTIC_ERROR_MESSAGE_CODE_POINTS) ||
|
|
65
|
+
"Diagnostic callback threw";
|
|
66
|
+
} else {
|
|
67
|
+
errorMessage =
|
|
68
|
+
boundedDiagnosticText(String(error), MAX_DIAGNOSTIC_ERROR_MESSAGE_CODE_POINTS) ||
|
|
69
|
+
"Diagnostic callback threw";
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Hostile thrown values must not escape the diagnostic failure boundary.
|
|
73
|
+
}
|
|
74
|
+
return Object.freeze({ errorName, errorMessage });
|
|
75
|
+
}
|
|
76
|
+
|
|
37
77
|
/** One inline-keyboard button. */
|
|
38
78
|
export interface InlineButton {
|
|
39
79
|
text: string;
|
|
@@ -254,7 +294,7 @@ export function buildActionMarkdown(action: {
|
|
|
254
294
|
const options = action.options ?? [];
|
|
255
295
|
const displayOptions = withRecommendedOptionLabel(options, action.recommendedIndex);
|
|
256
296
|
if (options.length === 0) return `${heading}\n\n(reply with text)`;
|
|
257
|
-
const list = displayOptions.map((label, i) => `${i + 1}. ${label
|
|
297
|
+
const list = displayOptions.map((label, i) => `${i + 1}. ${renumberableOptionText(label)}`).join("\n");
|
|
258
298
|
return `${heading}\n\n${list}`;
|
|
259
299
|
}
|
|
260
300
|
|
|
@@ -406,6 +446,12 @@ export function readEndpoint(path: string): { url: string; token: string; pid?:
|
|
|
406
446
|
};
|
|
407
447
|
}
|
|
408
448
|
|
|
449
|
+
/** A bounded, diagnostic-only notification mirrored to reference-client embedders. */
|
|
450
|
+
export type TelegramReferenceDiagnostic = Readonly<{
|
|
451
|
+
code: "action_unavailable";
|
|
452
|
+
requiredCapabilities: readonly string[];
|
|
453
|
+
}>;
|
|
454
|
+
|
|
409
455
|
/** Options for {@link runTelegramReferenceClient}. */
|
|
410
456
|
export interface TelegramReferenceOptions {
|
|
411
457
|
botToken: string;
|
|
@@ -414,6 +460,11 @@ export interface TelegramReferenceOptions {
|
|
|
414
460
|
apiBase?: string;
|
|
415
461
|
fetchImpl?: typeof fetch;
|
|
416
462
|
sound?: TelegramNotificationSound;
|
|
463
|
+
/**
|
|
464
|
+
* Additive mirror of bounded logger diagnostics. It never receives tokens,
|
|
465
|
+
* identifiers, raw frames, Telegram content, or WebSocket replies.
|
|
466
|
+
*/
|
|
467
|
+
onDiagnostic?: (diagnostic: TelegramReferenceDiagnostic) => void;
|
|
417
468
|
}
|
|
418
469
|
|
|
419
470
|
/**
|
|
@@ -470,15 +521,27 @@ export async function runTelegramReferenceClient(opts: TelegramReferenceOptions)
|
|
|
470
521
|
msg.kind ?? "ask",
|
|
471
522
|
);
|
|
472
523
|
} else if (msg.type === "action_unavailable") {
|
|
473
|
-
const requiredCapabilities =
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
`Telegram reference client: server withheld a controlled ask because this client lacks requiredCapabilities=[${requiredCapabilities.join(", ") || "unspecified"}].`,
|
|
524
|
+
const requiredCapabilities = Object.freeze(
|
|
525
|
+
Array.isArray(msg.requiredCapabilities)
|
|
526
|
+
? msg.requiredCapabilities
|
|
527
|
+
.filter((capability): capability is string => typeof capability === "string")
|
|
528
|
+
.slice(0, MAX_DIAGNOSTIC_CAPABILITIES)
|
|
529
|
+
.map(capability => boundedDiagnosticText(capability, MAX_DIAGNOSTIC_CAPABILITY_CODE_POINTS))
|
|
530
|
+
: [],
|
|
481
531
|
);
|
|
532
|
+
const diagnostic: TelegramReferenceDiagnostic = Object.freeze({
|
|
533
|
+
code: "action_unavailable",
|
|
534
|
+
requiredCapabilities,
|
|
535
|
+
});
|
|
536
|
+
logger.warn("Telegram reference client action unavailable", diagnostic);
|
|
537
|
+
try {
|
|
538
|
+
opts.onDiagnostic?.(diagnostic);
|
|
539
|
+
} catch (error) {
|
|
540
|
+
logger.warn("Telegram reference client diagnostic callback failed", {
|
|
541
|
+
code: "diagnostic_callback_failed",
|
|
542
|
+
...boundedDiagnosticCallbackError(error),
|
|
543
|
+
});
|
|
544
|
+
}
|
|
482
545
|
// Diagnostic only: never turn it into a Telegram prompt or option buttons.
|
|
483
546
|
return;
|
|
484
547
|
} else if (msg.type === "action_resolved" && msg.id === latestPendingAskId) {
|
package/src/sdk/session.ts
CHANGED
|
@@ -1253,7 +1253,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1253
1253
|
return SessionManager.create(cwd, SessionManager.managedDestination(cwd, agentDir));
|
|
1254
1254
|
}));
|
|
1255
1255
|
const logicalSessionId = sessionManager.getSessionId();
|
|
1256
|
-
|
|
1256
|
+
// Fork-context seeds carry conversation content only, never provider identity:
|
|
1257
|
+
// a shared continuity id would make concurrent subagents present the same
|
|
1258
|
+
// session_id upstream, where session-owning transports reject the extra
|
|
1259
|
+
// downstreams (owner_busy) and degrade those turns to uncached HTTP.
|
|
1260
|
+
const providerSessionId = options.providerSessionId ?? logicalSessionId;
|
|
1257
1261
|
const modelApiKeyAvailability = new Map<string, boolean>();
|
|
1258
1262
|
const getModelAvailabilityKey = (candidate: Model): string =>
|
|
1259
1263
|
`${candidate.provider}\u0000${candidate.baseUrl ?? ""}`;
|
|
@@ -159,7 +159,6 @@ export interface ForkContextSeed {
|
|
|
159
159
|
messages: Message[];
|
|
160
160
|
agentMessages: AgentMessage[];
|
|
161
161
|
metadata: ForkContextSeedMetadata;
|
|
162
|
-
cacheIdentity?: string;
|
|
163
162
|
appendOnlyPrefixSnapshot?: StablePrefixSnapshot;
|
|
164
163
|
}
|
|
165
164
|
|
|
@@ -167,7 +166,6 @@ export interface ForkContextSeedOptions {
|
|
|
167
166
|
maxMessages: number;
|
|
168
167
|
maxTokens: number;
|
|
169
168
|
preserveLatestUser?: boolean;
|
|
170
|
-
cacheIdentity?: string;
|
|
171
169
|
signal?: AbortSignal;
|
|
172
170
|
}
|
|
173
171
|
|
|
@@ -3118,7 +3116,6 @@ export class AgentSession {
|
|
|
3118
3116
|
maxTokens,
|
|
3119
3117
|
skippedReasons: {},
|
|
3120
3118
|
},
|
|
3121
|
-
cacheIdentity: options.cacheIdentity ?? this.sessionId,
|
|
3122
3119
|
};
|
|
3123
3120
|
}
|
|
3124
3121
|
const transformedMessages = await this.#transformContext([...this.messages], options.signal);
|
|
@@ -3318,7 +3315,6 @@ export class AgentSession {
|
|
|
3318
3315
|
maxTokens,
|
|
3319
3316
|
skippedReasons,
|
|
3320
3317
|
},
|
|
3321
|
-
cacheIdentity: options.cacheIdentity ?? this.sessionId,
|
|
3322
3318
|
appendOnlyPrefixSnapshot,
|
|
3323
3319
|
};
|
|
3324
3320
|
}
|
|
@@ -943,6 +943,13 @@ export class ManagedSessionDescendantStore {
|
|
|
943
943
|
detachOnly: true,
|
|
944
944
|
quarantineName,
|
|
945
945
|
});
|
|
946
|
+
if (
|
|
947
|
+
!detached.ok &&
|
|
948
|
+
detached.code === "not_found" &&
|
|
949
|
+
detached.retainedSuccessorPath === undefined &&
|
|
950
|
+
detached.retainedUnknownPath === undefined
|
|
951
|
+
)
|
|
952
|
+
return;
|
|
946
953
|
if (
|
|
947
954
|
(!detached.ok && detached.code !== "cleanup_pending") ||
|
|
948
955
|
detached.detachedPath !== path.join(this.#baseDir, quarantineName) ||
|
|
@@ -956,7 +963,13 @@ export class ManagedSessionDescendantStore {
|
|
|
956
963
|
#reconcileLegacyReplacementCleanupReceipt(receiptPath: string, name: string): void {
|
|
957
964
|
const binding = legacyReplacementCleanupReceiptBinding(name);
|
|
958
965
|
if (!binding) throw new Error("managed_replace_cleanup_receipt_invalid");
|
|
959
|
-
|
|
966
|
+
let receipt: ManagedFileSnapshot;
|
|
967
|
+
try {
|
|
968
|
+
receipt = captureManagedFileNoFollow(receiptPath);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
971
|
+
throw error;
|
|
972
|
+
}
|
|
960
973
|
const parsed = parseLegacyReplacementCleanupReceipt(receipt.bytes);
|
|
961
974
|
const expectedPredecessor = path.join(
|
|
962
975
|
this.#baseDir,
|
|
@@ -985,7 +998,16 @@ export class ManagedSessionDescendantStore {
|
|
|
985
998
|
if (!exactUnlinkCompleted(retired) && retired.code !== "not_found")
|
|
986
999
|
throw new Error(`managed_replace_cleanup_pending:${retired.code ?? "unknown"}`);
|
|
987
1000
|
|
|
988
|
-
|
|
1001
|
+
let currentReceipt: ManagedFileSnapshot;
|
|
1002
|
+
try {
|
|
1003
|
+
currentReceipt = captureManagedFileNoFollow(receiptPath);
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
1006
|
+
fsyncDirectory(this.#baseDir);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
throw error;
|
|
1010
|
+
}
|
|
989
1011
|
if (
|
|
990
1012
|
!sameIdentity(currentReceipt.identity, receipt.identity) ||
|
|
991
1013
|
currentReceipt.identity.sha256 !== receipt.identity.sha256
|
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
assertManagedDirectoryRoot,
|
|
77
77
|
captureManagedFileNoFollow,
|
|
78
78
|
fsyncManagedArtifactTree,
|
|
79
|
+
MANAGED_ARTIFACT_MAX_FILE_BYTES,
|
|
79
80
|
type ManagedDirectoryRoot,
|
|
80
81
|
type ManagedFileSnapshot,
|
|
81
82
|
ManagedSessionDescendantStore,
|
|
@@ -915,12 +916,25 @@ export interface ResumeTailTerminal {
|
|
|
915
916
|
|
|
916
917
|
export interface ResumeTailError {
|
|
917
918
|
kind: "error";
|
|
918
|
-
reason: "missing" | "malformed" | "unstable" | "read-failed" | "legacy_migration_disabled";
|
|
919
|
+
reason: "missing" | "malformed" | "unstable" | "read-failed" | "legacy_migration_disabled" | "oversized";
|
|
920
|
+
size?: number;
|
|
919
921
|
}
|
|
920
922
|
|
|
921
923
|
export type SessionDirectoryMigrationPolicy = ManagedMigrationPolicy;
|
|
922
924
|
export type SessionAppendPersistenceFailurePhase = "current_append" | "prior_failure";
|
|
923
925
|
|
|
926
|
+
/**
|
|
927
|
+
* Safety bound for resuming a single transcript file. Matches the managed-storage
|
|
928
|
+
* per-file artifact limit (`MANAGED_ARTIFACT_MAX_FILE_BYTES`): transcripts at or above
|
|
929
|
+
* this size are not loaded into memory wholesale because the decode + per-line JSON
|
|
930
|
+
* parse + identity hash would OOM or stall the process (#3851). The bound is a
|
|
931
|
+
* load-time guard only; it does not raise the on-disk managed-storage limit.
|
|
932
|
+
*/
|
|
933
|
+
export const RESUME_TRANSCRIPT_MAX_BYTES = MANAGED_ARTIFACT_MAX_FILE_BYTES;
|
|
934
|
+
|
|
935
|
+
export const SESSION_OVERSIZED_RECOVERY_MESSAGE =
|
|
936
|
+
"The selected session transcript is too large to resume safely. Use `gjc export <session-file>` to export its content into a new session, or remove/archive it after confirming its content is no longer needed.";
|
|
937
|
+
|
|
924
938
|
export class SessionAppendPersistenceError extends Error {
|
|
925
939
|
readonly phase: SessionAppendPersistenceFailurePhase;
|
|
926
940
|
readonly entryId: string;
|
|
@@ -964,6 +978,17 @@ export class SessionArtifactCapacityError extends Error {
|
|
|
964
978
|
}
|
|
965
979
|
}
|
|
966
980
|
|
|
981
|
+
export class SessionTranscriptOversizedError extends Error {
|
|
982
|
+
readonly code = "oversized";
|
|
983
|
+
readonly size: number;
|
|
984
|
+
|
|
985
|
+
constructor(size: number) {
|
|
986
|
+
super(SESSION_OVERSIZED_RECOVERY_MESSAGE);
|
|
987
|
+
this.name = "SessionTranscriptOversizedError";
|
|
988
|
+
this.size = size;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
967
992
|
export type ResumeTailInspection = ResumeTailResumable | ResumeTailTerminal | ResumeTailError;
|
|
968
993
|
export interface StrictSessionOpenSuccess {
|
|
969
994
|
kind: "opened";
|
|
@@ -979,6 +1004,7 @@ export interface StrictSessionOpenFailure {
|
|
|
979
1004
|
| "artifact_capacity_exceeded"
|
|
980
1005
|
| "migration_busy";
|
|
981
1006
|
message?: string;
|
|
1007
|
+
size?: number;
|
|
982
1008
|
}
|
|
983
1009
|
|
|
984
1010
|
/** Exact transcript bytes and authority captured from one descriptor-bound read. */
|
|
@@ -2346,6 +2372,12 @@ function inspectResumeSessionFile(
|
|
|
2346
2372
|
if (!before.isFile || !storage.readSnapshotSync) {
|
|
2347
2373
|
return { kind: "error", reason: "read-failed" };
|
|
2348
2374
|
}
|
|
2375
|
+
// Fail closed on oversized transcripts before the full read/decode/parse
|
|
2376
|
+
// path that would otherwise OOM or stall the process (#3851). The stat is
|
|
2377
|
+
// already in hand; this bound matches the managed-storage per-file limit.
|
|
2378
|
+
if (before.size > RESUME_TRANSCRIPT_MAX_BYTES) {
|
|
2379
|
+
return { kind: "error", reason: "oversized", size: before.size };
|
|
2380
|
+
}
|
|
2349
2381
|
|
|
2350
2382
|
let bytes: Uint8Array;
|
|
2351
2383
|
let snapshot: SessionStorageStat;
|
|
@@ -4378,12 +4410,6 @@ function extractTextFromContent(content: Message["content"]): string {
|
|
|
4378
4410
|
|
|
4379
4411
|
const SESSION_LIST_PREFIX_BYTES = 4096;
|
|
4380
4412
|
const SESSION_LIST_TRAILING_PATCH_BYTES = 4096;
|
|
4381
|
-
// Hard cap on how many trailing bytes the listers may scan for header patches.
|
|
4382
|
-
// moveTo folds cwd patches into line 1 via a full atomic rewrite, so only a
|
|
4383
|
-
// recent title patch can live unfolded near EOF; anything buried deeper is
|
|
4384
|
-
// folded on the next natural full rewrite. Keeping this bounded preserves the
|
|
4385
|
-
// O(SESSION_LIST_PREFIX_BYTES) listing cost that SESSION_LIST_PREFIX_BYTES exists for.
|
|
4386
|
-
const SESSION_LIST_TRAILING_PATCH_SCAN_CAP = SESSION_LIST_TRAILING_PATCH_BYTES * 4;
|
|
4387
4413
|
const SESSION_NAME_MAX_CHARS = 1_000;
|
|
4388
4414
|
const SESSION_LIST_PARALLEL_THRESHOLD = 64;
|
|
4389
4415
|
const SESSION_LIST_MAX_WORKERS = 16;
|
|
@@ -4419,12 +4445,17 @@ async function readSessionListTrailingPatches(
|
|
|
4419
4445
|
if (size <= SESSION_LIST_PREFIX_BYTES) return [];
|
|
4420
4446
|
const latest: HeaderPatchRecord["patch"] = {};
|
|
4421
4447
|
let position = size;
|
|
4422
|
-
|
|
4448
|
+
// Reverse-scan from EOF in fixed chunks. Stop once both cwd and title
|
|
4449
|
+
// resolve so recent patches stay cheap. Continue past the historical 16 KiB
|
|
4450
|
+
// window when a field is still missing so a buried but canonically valid
|
|
4451
|
+
// header_patch remains listable without a full sequential JSONL parse (#3633).
|
|
4452
|
+
// Chunks that cannot contain a header_patch marker skip JSON parsing.
|
|
4423
4453
|
let trailingFragment = Buffer.alloc(0);
|
|
4424
4454
|
const chunkSize = Math.min(buffer.byteLength, SESSION_LIST_TRAILING_PATCH_BYTES);
|
|
4455
|
+
const headerPatchMarker = Buffer.from("header_patch");
|
|
4425
4456
|
const handle = await fs.promises.open(file, "r");
|
|
4426
4457
|
try {
|
|
4427
|
-
while (position >
|
|
4458
|
+
while (position > 0 && (latest.cwd === undefined || latest.title === undefined)) {
|
|
4428
4459
|
const start = Math.max(0, position - chunkSize);
|
|
4429
4460
|
const length = position - start;
|
|
4430
4461
|
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
@@ -4440,18 +4471,26 @@ async function readSessionListTrailingPatches(
|
|
|
4440
4471
|
trailingFragment = combined.subarray(0, firstNewline);
|
|
4441
4472
|
complete = combined.subarray(firstNewline + 1);
|
|
4442
4473
|
}
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4474
|
+
// Marker may straddle the discarded partial first line; still parse when
|
|
4475
|
+
// the raw chunk or carried fragment could hold a header_patch record.
|
|
4476
|
+
const mayContainPatch =
|
|
4477
|
+
complete.includes(headerPatchMarker) ||
|
|
4478
|
+
buffer.subarray(0, bytesRead).includes(headerPatchMarker) ||
|
|
4479
|
+
trailingFragment.includes(headerPatchMarker);
|
|
4480
|
+
if (mayContainPatch) {
|
|
4481
|
+
const lines = complete.toString("utf8").split("\n");
|
|
4482
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
4483
|
+
const line = lines[index];
|
|
4484
|
+
if (!line?.includes("header_patch")) continue;
|
|
4485
|
+
try {
|
|
4486
|
+
const record = JSON.parse(line) as SessionPatchRecord;
|
|
4487
|
+
if (!isHeaderPatchRecord(record)) continue;
|
|
4488
|
+
if (latest.cwd === undefined && typeof record.patch.cwd === "string") latest.cwd = record.patch.cwd;
|
|
4489
|
+
if (latest.title === undefined && typeof record.patch.title === "string")
|
|
4490
|
+
latest.title = record.patch.title;
|
|
4491
|
+
} catch {
|
|
4492
|
+
// Ignore malformed or partial records exactly as the canonical loader does.
|
|
4493
|
+
}
|
|
4455
4494
|
}
|
|
4456
4495
|
}
|
|
4457
4496
|
position = start;
|
|
@@ -9938,6 +9977,16 @@ export class SessionManager {
|
|
|
9938
9977
|
// owner-only and reparse guards accept.
|
|
9939
9978
|
if (storage instanceof FileSessionStorage) filePath = canonicalizeTrustedPath(filePath);
|
|
9940
9979
|
if (destination.kind === "explicit" || !(storage instanceof FileSessionStorage)) {
|
|
9980
|
+
// Fail closed on oversized transcripts before the full read in the explicit path too (#3851).
|
|
9981
|
+
// A missing file falls through to loadEntriesFromFile, which returns [] and creates
|
|
9982
|
+
// a fresh session — matching the pre-existing behavior.
|
|
9983
|
+
try {
|
|
9984
|
+
const preStat = storage.statSync(filePath);
|
|
9985
|
+
if (preStat.size > RESUME_TRANSCRIPT_MAX_BYTES) throw new SessionTranscriptOversizedError(preStat.size);
|
|
9986
|
+
} catch (error) {
|
|
9987
|
+
if (error instanceof SessionTranscriptOversizedError) throw error;
|
|
9988
|
+
if (!isEnoent(error)) throw error;
|
|
9989
|
+
}
|
|
9941
9990
|
const entries = await loadEntriesFromFile(filePath, storage);
|
|
9942
9991
|
const header = entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
9943
9992
|
const manager = new SessionManager(
|
|
@@ -9958,6 +10007,7 @@ export class SessionManager {
|
|
|
9958
10007
|
await manager.#initSessionFile(filePath);
|
|
9959
10008
|
return manager;
|
|
9960
10009
|
}
|
|
10010
|
+
if (inspected.reason === "oversized") throw new SessionTranscriptOversizedError(inspected.size ?? 0);
|
|
9961
10011
|
throw new Error(`Could not open session: ${inspected.reason}`);
|
|
9962
10012
|
}
|
|
9963
10013
|
const opened = await SessionManager.openExistingStrict(inspected.identity, destination, storage, migrationPolicy);
|
|
@@ -9967,6 +10017,7 @@ export class SessionManager {
|
|
|
9967
10017
|
throw new SessionArtifactCapacityError(
|
|
9968
10018
|
opened.message ?? "Session artifacts exceed the migration capacity.",
|
|
9969
10019
|
);
|
|
10020
|
+
if (opened.reason === "oversized") throw new SessionTranscriptOversizedError(opened.size ?? 0);
|
|
9970
10021
|
if (opened.reason === "migration_busy") throw new SessionMigrationBusyError();
|
|
9971
10022
|
throw new Error(`Could not open session: ${opened.reason}`);
|
|
9972
10023
|
}
|
|
@@ -10705,7 +10756,10 @@ export class SessionManager {
|
|
|
10705
10756
|
const dir = destination.directory;
|
|
10706
10757
|
const openSelectedStrictly = async (selectedPath: string): Promise<SessionManager | undefined> => {
|
|
10707
10758
|
const inspected = await SessionManager.inspectSessionTailReadOnly(selectedPath, storage);
|
|
10708
|
-
if (inspected.kind === "error")
|
|
10759
|
+
if (inspected.kind === "error") {
|
|
10760
|
+
if (inspected.reason === "oversized") throw new SessionTranscriptOversizedError(inspected.size ?? 0);
|
|
10761
|
+
return undefined;
|
|
10762
|
+
}
|
|
10709
10763
|
const opened = await SessionManager.openExistingStrict(
|
|
10710
10764
|
inspected.identity,
|
|
10711
10765
|
destination,
|
|
@@ -10718,6 +10772,7 @@ export class SessionManager {
|
|
|
10718
10772
|
throw new SessionArtifactCapacityError(
|
|
10719
10773
|
opened.message ?? "Session artifacts exceed the migration capacity.",
|
|
10720
10774
|
);
|
|
10775
|
+
if (opened.reason === "oversized") throw new SessionTranscriptOversizedError(opened.size ?? 0);
|
|
10721
10776
|
if (opened.reason === "migration_busy") throw new SessionMigrationBusyError();
|
|
10722
10777
|
return undefined;
|
|
10723
10778
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[
|
|
2
|
-
{"id":"minimax","aliases":["minimax-code"],"name":"MiniMax Coding Plan","description":"OpenAI-compatible MiniMax Coding Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"minimax-code","baseUrl":"https://api.minimax.io/v1","apiKeyEnv":"MINIMAX_CODE_API_KEY","models":["
|
|
3
|
-
{"id":"minimax-cn","aliases":["minimax-code-cn","minimaxi"],"name":"MiniMax Coding Plan (China)","description":"OpenAI-compatible MiniMax China endpoint","compatibility":"openai","api":"openai-completions","providerId":"minimax-code-cn","baseUrl":"https://api.minimaxi.com/v1","apiKeyEnv":"MINIMAX_CODE_CN_API_KEY","models":["
|
|
2
|
+
{"id":"minimax","aliases":["minimax-code"],"name":"MiniMax Coding Plan","description":"OpenAI-compatible MiniMax Coding Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"minimax-code","baseUrl":"https://api.minimax.io/v1","apiKeyEnv":"MINIMAX_CODE_API_KEY","models":["MiniMax-M3"],"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}},
|
|
3
|
+
{"id":"minimax-cn","aliases":["minimax-code-cn","minimaxi"],"name":"MiniMax Coding Plan (China)","description":"OpenAI-compatible MiniMax China endpoint","compatibility":"openai","api":"openai-completions","providerId":"minimax-code-cn","baseUrl":"https://api.minimaxi.com/v1","apiKeyEnv":"MINIMAX_CODE_CN_API_KEY","models":["MiniMax-M3"],"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}},
|
|
4
4
|
{"id":"glm","aliases":["zai","z-ai","bigmodel"],"name":"GLM / zAI","description":"OpenAI-compatible GLM endpoint from zAI/BigModel","compatibility":"openai","api":"openai-completions","providerId":"glm-proxy","baseUrl":"https://api.z.ai/api/paas/v4","apiKeyEnv":"ZAI_API_KEY","models":["glm-4.6"],"compat":{"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","reasoningContentField":"reasoning_content"}},
|
|
5
|
-
{"id":"alibaba-token-plan","aliases":["alibaba","token-plan"],"name":"Alibaba Token Plan","description":"OpenAI-compatible Alibaba Token Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"alibaba-token-plan","baseUrl":"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1","apiKeyEnv":"ALIBABA_TOKEN_PLAN_API_KEY","models":["qwen3.8-max-preview","
|
|
5
|
+
{"id":"alibaba-token-plan","aliases":["alibaba","token-plan"],"name":"Alibaba Token Plan","description":"OpenAI-compatible Alibaba Token Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"alibaba-token-plan","baseUrl":"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1","apiKeyEnv":"ALIBABA_TOKEN_PLAN_API_KEY","models":["qwen3.8-max-preview","qwen3.8-max","glm-5.2","deepseek-v4-pro"],"modelApi":{"qwen3.8-max-preview":"openai-responses","qwen3.8-max":"openai-responses","glm-5.2":"openai-completions","deepseek-v4-pro":"openai-completions"},"compat":{"supportsDeveloperRole":false}}
|
|
6
6
|
]
|
package/src/thinking-metadata.ts
CHANGED
|
@@ -39,7 +39,8 @@ const THINKING_LEVEL_METADATA: Record<ThinkingLevelValue, ThinkingLevelMetadata>
|
|
|
39
39
|
max: {
|
|
40
40
|
value: "max",
|
|
41
41
|
label: "max",
|
|
42
|
-
|
|
42
|
+
// Provider-neutral: max is not Anthropic/Opus-specific (OpenAI, Luna, etc.).
|
|
43
|
+
description: "Absolute maximum reasoning",
|
|
43
44
|
},
|
|
44
45
|
};
|
|
45
46
|
|
package/src/tools/ask.ts
CHANGED
|
@@ -763,6 +763,8 @@ interface AskSingleQuestionOptions {
|
|
|
763
763
|
onRemoteState?: (state: {
|
|
764
764
|
interaction: "selector" | "custom_editor" | "clarification_editor";
|
|
765
765
|
selectedCount: number;
|
|
766
|
+
/** Currently selected option labels, so a remote transport can render the selection. */
|
|
767
|
+
selectedOptions: readonly string[];
|
|
766
768
|
hasNonWhitespaceCustom: boolean;
|
|
767
769
|
}) => void;
|
|
768
770
|
}
|
|
@@ -900,6 +902,7 @@ async function askSingleQuestion(
|
|
|
900
902
|
options.onRemoteState?.({
|
|
901
903
|
interaction: "custom_editor",
|
|
902
904
|
selectedCount: selectedOptions.length,
|
|
905
|
+
selectedOptions: [...selectedOptions],
|
|
903
906
|
hasNonWhitespaceCustom: (customInput?.trim().length ?? 0) > 0,
|
|
904
907
|
});
|
|
905
908
|
return ui.editor("Enter your response:", undefined, dialogOptions, { promptStyle: true });
|
|
@@ -913,6 +916,7 @@ async function askSingleQuestion(
|
|
|
913
916
|
options.onRemoteState?.({
|
|
914
917
|
interaction: "clarification_editor",
|
|
915
918
|
selectedCount: selectedOptions.length,
|
|
919
|
+
selectedOptions: [...selectedOptions],
|
|
916
920
|
hasNonWhitespaceCustom: false,
|
|
917
921
|
});
|
|
918
922
|
return ui.editor("Ask a clarification question:", undefined, dialogOptions, { promptStyle: true });
|
|
@@ -949,6 +953,7 @@ async function askSingleQuestion(
|
|
|
949
953
|
options.onRemoteState?.({
|
|
950
954
|
interaction: "selector",
|
|
951
955
|
selectedCount: selected.size,
|
|
956
|
+
selectedOptions: [...selected],
|
|
952
957
|
hasNonWhitespaceCustom: (customInput?.trim().length ?? 0) > 0,
|
|
953
958
|
});
|
|
954
959
|
const prefix = selected.size > 0 ? `(${selected.size} selected) ` : "";
|
|
@@ -1054,6 +1059,7 @@ async function askSingleQuestion(
|
|
|
1054
1059
|
options.onRemoteState?.({
|
|
1055
1060
|
interaction: "selector",
|
|
1056
1061
|
selectedCount: selectedOptions.length,
|
|
1062
|
+
selectedOptions: [...selectedOptions],
|
|
1057
1063
|
hasNonWhitespaceCustom: (customInput?.trim().length ?? 0) > 0,
|
|
1058
1064
|
});
|
|
1059
1065
|
const {
|
|
@@ -1591,6 +1597,8 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1591
1597
|
options: remoteSelectorOptions,
|
|
1592
1598
|
interaction: "selector",
|
|
1593
1599
|
...(recommendedIndex === undefined ? {} : { recommendedIndex }),
|
|
1600
|
+
multi: q.multi === true,
|
|
1601
|
+
selectedOptions: [...(initialSelection?.selectedOptions ?? [])],
|
|
1594
1602
|
controls: askRemoteControls({
|
|
1595
1603
|
multi: q.multi === true,
|
|
1596
1604
|
questionIndex,
|
|
@@ -1625,6 +1633,8 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1625
1633
|
...(state.interaction === "selector" && recommendedIndex !== undefined
|
|
1626
1634
|
? { recommendedIndex }
|
|
1627
1635
|
: {}),
|
|
1636
|
+
multi: q.multi === true,
|
|
1637
|
+
selectedOptions: [...state.selectedOptions],
|
|
1628
1638
|
controls:
|
|
1629
1639
|
state.interaction === "selector"
|
|
1630
1640
|
? askRemoteControls({
|
package/src/tools/index.ts
CHANGED
|
@@ -139,6 +139,13 @@ export interface AskAnswerRequest {
|
|
|
139
139
|
controls: readonly AskRemoteControl[];
|
|
140
140
|
/** Optional zero-based recommendation into the authoritative raw options. */
|
|
141
141
|
recommendedIndex?: number;
|
|
142
|
+
/** True while the question accepts several options before the navigation control commits. */
|
|
143
|
+
multi?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Options currently selected in the multi-select loop, as authoritative option
|
|
146
|
+
* labels. Remote transports render the selection state so a toggle is visible.
|
|
147
|
+
*/
|
|
148
|
+
selectedOptions?: readonly string[];
|
|
142
149
|
}
|
|
143
150
|
|
|
144
151
|
export type AskRemoteInteraction =
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -73,14 +73,14 @@ function hasUnknownKeys(value: object, allowed: Set<string>): boolean {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
function validateRawTodoArguments(arguments_: Record<string, unknown>): RawArgumentValidationResult {
|
|
76
|
-
if (hasUnknownKeys(arguments_, TODO_WRITE_KEYS)) return { outcome: "reject" };
|
|
76
|
+
if (hasUnknownKeys(arguments_, TODO_WRITE_KEYS)) return { outcome: "reject", code: "todo-write-unknown-root-key" };
|
|
77
77
|
if (!Array.isArray(arguments_.ops)) return { outcome: "passthrough" };
|
|
78
78
|
for (const entry of arguments_.ops) {
|
|
79
79
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue;
|
|
80
|
-
if (hasUnknownKeys(entry, TODO_OP_KEYS)) return { outcome: "reject" };
|
|
80
|
+
if (hasUnknownKeys(entry, TODO_OP_KEYS)) return { outcome: "reject", code: "todo-write-unknown-op-entry-key" };
|
|
81
81
|
const record = entry as Record<string, unknown>;
|
|
82
82
|
if ((record.op === "done" || record.op === "drop") && !record.task && !record.phase) {
|
|
83
|
-
return { outcome: "reject" };
|
|
83
|
+
return { outcome: "reject", code: "todo-write-done-drop-requires-target" };
|
|
84
84
|
}
|
|
85
85
|
const list = record.list;
|
|
86
86
|
if (!Array.isArray(list)) continue;
|
|
@@ -91,7 +91,7 @@ function validateRawTodoArguments(arguments_: Record<string, unknown>): RawArgum
|
|
|
91
91
|
!Array.isArray(item) &&
|
|
92
92
|
hasUnknownKeys(item, TODO_INIT_ENTRY_KEYS)
|
|
93
93
|
)
|
|
94
|
-
return { outcome: "reject" };
|
|
94
|
+
return { outcome: "reject", code: "todo-write-unknown-init-entry-key" };
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
return { outcome: "passthrough" };
|