@gajae-code/coding-agent 0.5.2 → 0.5.4
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 +23 -0
- package/dist/types/async/job-manager.d.ts +6 -0
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/dap/client.d.ts +2 -1
- package/dist/types/edit/read-file.d.ts +6 -0
- package/dist/types/eval/js/context-manager.d.ts +3 -0
- package/dist/types/eval/js/executor.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime/process-lifecycle.d.ts +108 -0
- package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +29 -1
- package/dist/types/session/artifacts.d.ts +4 -1
- package/dist/types/session/streaming-output.d.ts +12 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
- package/dist/types/tools/sqlite-reader.d.ts +2 -1
- package/dist/types/web/search/providers/codex.d.ts +4 -4
- package/package.json +7 -7
- package/src/async/job-manager.ts +181 -43
- package/src/config/file-lock.ts +9 -1
- package/src/config/model-profile-activation.ts +71 -3
- package/src/config/model-profiles.ts +39 -14
- package/src/dap/client.ts +105 -64
- package/src/dap/session.ts +44 -7
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
- package/src/edit/read-file.ts +19 -1
- package/src/eval/js/context-manager.ts +228 -65
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/js/index.ts +1 -0
- package/src/eval/js/worker-core.ts +10 -6
- package/src/eval/py/executor.ts +68 -19
- package/src/eval/py/kernel.ts +46 -22
- package/src/eval/py/runner.py +68 -14
- package/src/exec/bash-executor.ts +49 -13
- package/src/gjc-runtime/deep-interview-runtime.ts +14 -13
- package/src/gjc-runtime/ralplan-runtime.ts +10 -0
- package/src/gjc-runtime/state-runtime.ts +73 -0
- package/src/gjc-runtime/tmux-gc.ts +86 -37
- package/src/gjc-runtime/tmux-sessions.ts +44 -6
- package/src/gjc-runtime/ultragoal-runtime.ts +8 -4
- package/src/internal-urls/artifact-protocol.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/client.ts +64 -26
- package/src/lsp/index.ts +2 -1
- package/src/lsp/lspmux.ts +33 -9
- package/src/lsp/types.ts +2 -0
- package/src/modes/bridge/bridge-mode.ts +21 -0
- package/src/modes/components/assistant-message.ts +10 -2
- package/src/modes/components/bash-execution.ts +5 -1
- package/src/modes/components/eval-execution.ts +5 -1
- package/src/modes/components/model-selector.ts +34 -2
- package/src/modes/components/oauth-selector.ts +5 -0
- package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
- package/src/modes/components/skill-message.ts +24 -16
- package/src/modes/components/tool-execution.ts +6 -0
- package/src/modes/controllers/extension-ui-controller.ts +33 -6
- package/src/modes/controllers/input-controller.ts +19 -0
- package/src/modes/controllers/selector-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +5 -2
- package/src/prompts/agents/executor.md +1 -1
- package/src/runtime/process-lifecycle.ts +400 -0
- package/src/runtime-mcp/manager.ts +164 -50
- package/src/runtime-mcp/transports/http.ts +12 -11
- package/src/runtime-mcp/transports/stdio.ts +64 -38
- package/src/runtime-mcp/types.ts +3 -0
- package/src/sdk.ts +27 -0
- package/src/session/agent-session.ts +271 -25
- package/src/session/artifacts.ts +17 -2
- package/src/session/blob-store.ts +36 -2
- package/src/session/session-manager.ts +29 -13
- package/src/session/streaming-output.ts +95 -3
- package/src/setup/model-onboarding-guidance.ts +10 -3
- package/src/skill-state/active-state.ts +79 -7
- package/src/slash-commands/builtin-registry.ts +30 -3
- package/src/slash-commands/helpers/fast-status-report.ts +111 -0
- package/src/tools/archive-reader.ts +10 -1
- package/src/tools/bash.ts +11 -4
- package/src/tools/browser/registry.ts +17 -1
- package/src/tools/browser/tab-supervisor.ts +22 -0
- package/src/tools/browser.ts +38 -4
- package/src/tools/cron.ts +2 -6
- package/src/tools/read.ts +11 -12
- package/src/tools/sqlite-reader.ts +19 -5
- package/src/web/search/providers/codex.ts +6 -5
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/coding-agent",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.4",
|
|
5
5
|
"description": "Gajae Code CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
52
52
|
"@babel/parser": "^7.29.3",
|
|
53
53
|
"@mozilla/readability": "^0.6.0",
|
|
54
|
-
"@gajae-code/stats": "0.5.
|
|
55
|
-
"@gajae-code/agent-core": "0.5.
|
|
56
|
-
"@gajae-code/ai": "0.5.
|
|
57
|
-
"@gajae-code/natives": "0.5.
|
|
58
|
-
"@gajae-code/tui": "0.5.
|
|
59
|
-
"@gajae-code/utils": "0.5.
|
|
54
|
+
"@gajae-code/stats": "0.5.4",
|
|
55
|
+
"@gajae-code/agent-core": "0.5.4",
|
|
56
|
+
"@gajae-code/ai": "0.5.4",
|
|
57
|
+
"@gajae-code/natives": "0.5.4",
|
|
58
|
+
"@gajae-code/tui": "0.5.4",
|
|
59
|
+
"@gajae-code/utils": "0.5.4",
|
|
60
60
|
"@puppeteer/browsers": "^2.13.0",
|
|
61
61
|
"@types/turndown": "5.0.6",
|
|
62
62
|
"@xterm/headless": "^6.0.0",
|
package/src/async/job-manager.ts
CHANGED
|
@@ -7,6 +7,11 @@ const DELIVERY_RETRY_JITTER_MS = 200;
|
|
|
7
7
|
const DEFAULT_RETENTION_MS = 5 * 60 * 1000;
|
|
8
8
|
const DEFAULT_MAX_RUNNING_JOBS = 15;
|
|
9
9
|
const MONITOR_TOMBSTONE_TTL_MS = 5 * 60_000;
|
|
10
|
+
const DEFAULT_MAX_DELIVERY_QUEUE = 100;
|
|
11
|
+
const DELIVERY_MAX_TEXT_BYTES = 64 * 1024;
|
|
12
|
+
const DELIVERY_PREVIEW_HEAD_BYTES = 32 * 1024;
|
|
13
|
+
const DELIVERY_PREVIEW_TAIL_BYTES = 32 * 1024;
|
|
14
|
+
const DELIVERY_MAX_ATTEMPTS = 3;
|
|
10
15
|
|
|
11
16
|
export interface AsyncJob {
|
|
12
17
|
id: string;
|
|
@@ -113,6 +118,12 @@ export interface ResumeDescriptor {
|
|
|
113
118
|
data: unknown;
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
function sessionFileFromResumeDescriptorData(data: unknown): string | null {
|
|
122
|
+
if (typeof data !== "object" || data === null) return null;
|
|
123
|
+
const sessionFile = (data as { sessionFile?: unknown }).sessionFile;
|
|
124
|
+
return typeof sessionFile === "string" && sessionFile.trim().length > 0 ? sessionFile : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
116
127
|
/** A pending resume awaiting a free concurrency slot. */
|
|
117
128
|
interface ResumeQueueEntry {
|
|
118
129
|
subagentId: string;
|
|
@@ -128,9 +139,16 @@ export interface AsyncJobManagerOptions {
|
|
|
128
139
|
retentionMs?: number;
|
|
129
140
|
}
|
|
130
141
|
|
|
142
|
+
export interface AsyncJobDisposeDiagnostics {
|
|
143
|
+
stuckJobIds: string[];
|
|
144
|
+
deliveriesDrained: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
131
147
|
interface AsyncJobDelivery {
|
|
132
148
|
jobId: string;
|
|
133
149
|
text: string;
|
|
150
|
+
originalBytes?: number;
|
|
151
|
+
truncated?: boolean;
|
|
134
152
|
attempt: number;
|
|
135
153
|
nextAttemptAt: number;
|
|
136
154
|
lastError?: string;
|
|
@@ -143,6 +161,7 @@ export interface AsyncJobDeliveryState {
|
|
|
143
161
|
delivering: boolean;
|
|
144
162
|
nextRetryAt?: number;
|
|
145
163
|
pendingJobIds: string[];
|
|
164
|
+
deadLettered: number;
|
|
146
165
|
}
|
|
147
166
|
|
|
148
167
|
export interface AsyncJobLifecycleCleanup {
|
|
@@ -198,6 +217,32 @@ function sliceTextFromUtf8ByteOffset(text: string, offsetBytes: number): string
|
|
|
198
217
|
return text.slice(codeUnitIndex);
|
|
199
218
|
}
|
|
200
219
|
|
|
220
|
+
function sliceTextAfterUtf8ByteOffset(text: string, offsetBytes: number): string {
|
|
221
|
+
if (offsetBytes <= 0) return text;
|
|
222
|
+
let consumedBytes = 0;
|
|
223
|
+
let codeUnitIndex = 0;
|
|
224
|
+
for (const char of text) {
|
|
225
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
226
|
+
consumedBytes += charBytes;
|
|
227
|
+
codeUnitIndex += char.length;
|
|
228
|
+
if (consumedBytes >= offsetBytes) break;
|
|
229
|
+
}
|
|
230
|
+
return text.slice(codeUnitIndex);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function sliceTextToUtf8ByteLength(text: string, maxBytes: number): string {
|
|
234
|
+
if (maxBytes <= 0) return "";
|
|
235
|
+
let consumedBytes = 0;
|
|
236
|
+
let codeUnitIndex = 0;
|
|
237
|
+
for (const char of text) {
|
|
238
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
239
|
+
if (consumedBytes + charBytes > maxBytes) break;
|
|
240
|
+
consumedBytes += charBytes;
|
|
241
|
+
codeUnitIndex += char.length;
|
|
242
|
+
}
|
|
243
|
+
return text.slice(0, codeUnitIndex);
|
|
244
|
+
}
|
|
245
|
+
|
|
201
246
|
/**
|
|
202
247
|
* A slice of process-stream output for a background job, as recorded by
|
|
203
248
|
* `appendOutput` / read by `readOutputSince`.
|
|
@@ -277,6 +322,8 @@ export class AsyncJobManager {
|
|
|
277
322
|
#resumeSeq = 0;
|
|
278
323
|
#resumeRunner?: (subagentId: string, message?: string, descriptor?: ResumeDescriptor) => string | undefined;
|
|
279
324
|
readonly #resumeDescriptors = new Map<string, ResumeDescriptor>();
|
|
325
|
+
readonly #deadLetteredDeliveries = new Map<string, AsyncJobDelivery>();
|
|
326
|
+
#lastDisposeDiagnostics: AsyncJobDisposeDiagnostics = { stuckJobIds: [], deliveriesDrained: true };
|
|
280
327
|
/**
|
|
281
328
|
* Change listeners notified on any mutation that can alter the live job set
|
|
282
329
|
* (register, terminal/eviction transitions, dispose). Used by the status-line
|
|
@@ -381,7 +428,7 @@ export class AsyncJobManager {
|
|
|
381
428
|
|
|
382
429
|
if (job.status === "cancelled") {
|
|
383
430
|
job.resultText = outcome.kind === "completed" ? outcome.text : outcome.note;
|
|
384
|
-
this.#runLifecycle(id, "terminal");
|
|
431
|
+
this.#runLifecycle(id, "terminal", job);
|
|
385
432
|
this.#scheduleEviction(id);
|
|
386
433
|
this.#markRecordTerminal(id, "cancelled");
|
|
387
434
|
this.#drainResumeQueue();
|
|
@@ -403,20 +450,20 @@ export class AsyncJobManager {
|
|
|
403
450
|
this.#freezeEndTime(job);
|
|
404
451
|
job.resultText = outcome.text;
|
|
405
452
|
this.#enqueueDelivery(id, outcome.text);
|
|
406
|
-
this.#runLifecycle(id, "terminal");
|
|
453
|
+
this.#runLifecycle(id, "terminal", job);
|
|
407
454
|
this.#scheduleEviction(id);
|
|
408
455
|
this.#markRecordTerminal(id, "completed");
|
|
409
456
|
this.#drainResumeQueue();
|
|
410
457
|
} catch (error) {
|
|
411
458
|
if (job.status === "cancelled") {
|
|
412
459
|
job.errorText = error instanceof Error ? error.message : String(error);
|
|
413
|
-
this.#runLifecycle(id, "terminal");
|
|
460
|
+
this.#runLifecycle(id, "terminal", job);
|
|
414
461
|
this.#scheduleEviction(id);
|
|
415
462
|
this.#markRecordTerminal(id, "cancelled");
|
|
416
463
|
this.#drainResumeQueue();
|
|
417
464
|
return;
|
|
418
465
|
}
|
|
419
|
-
this.#runLifecycle(id, "terminal");
|
|
466
|
+
this.#runLifecycle(id, "terminal", job);
|
|
420
467
|
const errorText = error instanceof Error ? error.message : String(error);
|
|
421
468
|
job.status = "failed";
|
|
422
469
|
this.#freezeEndTime(job);
|
|
@@ -471,14 +518,14 @@ export class AsyncJobManager {
|
|
|
471
518
|
job.endTime ??= Date.now();
|
|
472
519
|
}
|
|
473
520
|
|
|
474
|
-
#runLifecycle(jobId: string, phase: "cancel" | "terminal" | "evict"): void {
|
|
521
|
+
#runLifecycle(jobId: string, phase: "cancel" | "terminal" | "evict", jobOverride?: AsyncJob): void {
|
|
522
|
+
const lifecycle = this.#lifecycles.get(jobId);
|
|
523
|
+
const job = jobOverride ?? this.#jobs.get(jobId);
|
|
524
|
+
if (!lifecycle || !job) return;
|
|
475
525
|
const fired = this.#lifecyclePhases.get(jobId) ?? new Set<"cancel" | "terminal" | "evict">();
|
|
476
526
|
if (fired.has(phase)) return;
|
|
477
527
|
fired.add(phase);
|
|
478
528
|
this.#lifecyclePhases.set(jobId, fired);
|
|
479
|
-
const lifecycle = this.#lifecycles.get(jobId);
|
|
480
|
-
const job = this.#jobs.get(jobId);
|
|
481
|
-
if (!lifecycle || !job) return;
|
|
482
529
|
try {
|
|
483
530
|
if (phase === "cancel") lifecycle.onCancel?.(job);
|
|
484
531
|
else if (phase === "terminal") lifecycle.onTerminal?.(job);
|
|
@@ -554,11 +601,31 @@ export class AsyncJobManager {
|
|
|
554
601
|
record.modelFellBack = model.modelFellBack;
|
|
555
602
|
}
|
|
556
603
|
|
|
604
|
+
#recordFromResumeDescriptor(subagentId: string, filter?: AsyncJobFilter): SubagentRecord | undefined {
|
|
605
|
+
const descriptor = this.getResumeDescriptor(subagentId, filter);
|
|
606
|
+
if (!descriptor) return undefined;
|
|
607
|
+
const sessionFile = sessionFileFromResumeDescriptorData(descriptor.data);
|
|
608
|
+
const record: SubagentRecord = {
|
|
609
|
+
subagentId: descriptor.subagentId,
|
|
610
|
+
ownerId: descriptor.ownerId,
|
|
611
|
+
currentJobId: null,
|
|
612
|
+
historicalJobIds: [],
|
|
613
|
+
status: "completed",
|
|
614
|
+
sessionFile,
|
|
615
|
+
resumable: sessionFile !== null,
|
|
616
|
+
};
|
|
617
|
+
this.#subagentRecords.set(record.subagentId, record);
|
|
618
|
+
return record;
|
|
619
|
+
}
|
|
620
|
+
|
|
557
621
|
getSubagentRecord(subagentId: string, filter?: AsyncJobFilter): SubagentRecord | undefined {
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
if (
|
|
561
|
-
|
|
622
|
+
const trimmed = subagentId.trim();
|
|
623
|
+
const rec = this.#subagentRecords.get(trimmed);
|
|
624
|
+
if (rec) {
|
|
625
|
+
if (filter?.ownerId && rec.ownerId !== filter.ownerId) return undefined;
|
|
626
|
+
return rec;
|
|
627
|
+
}
|
|
628
|
+
return this.#recordFromResumeDescriptor(trimmed, filter);
|
|
562
629
|
}
|
|
563
630
|
|
|
564
631
|
getSubagentRecords(filter?: AsyncJobFilter): SubagentRecord[] {
|
|
@@ -649,6 +716,14 @@ export class AsyncJobManager {
|
|
|
649
716
|
}
|
|
650
717
|
}
|
|
651
718
|
|
|
719
|
+
#purgeTerminalSubagentStateForJob(jobId: string): void {
|
|
720
|
+
const rec = this.#recordByJobId(jobId);
|
|
721
|
+
if (!rec) return;
|
|
722
|
+
if (rec.status === "paused" || rec.status === "queued") return;
|
|
723
|
+
this.#liveHandles.delete(rec.subagentId);
|
|
724
|
+
this.#subagentProgress.delete(rec.subagentId);
|
|
725
|
+
}
|
|
726
|
+
|
|
652
727
|
#markRecordTerminal(jobId: string, status: "completed" | "failed" | "cancelled"): void {
|
|
653
728
|
const rec = this.#recordByJobId(jobId);
|
|
654
729
|
if (!rec) return;
|
|
@@ -967,6 +1042,10 @@ export class AsyncJobManager {
|
|
|
967
1042
|
getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState {
|
|
968
1043
|
const deliveries = this.#filterDeliveries(filter);
|
|
969
1044
|
const inFlightDeliveries = this.#filterInFlightDeliveries(filter);
|
|
1045
|
+
const ownerId = filter?.ownerId;
|
|
1046
|
+
const deadLettered = Array.from(this.#deadLetteredDeliveries.values()).filter(
|
|
1047
|
+
delivery => !ownerId || delivery.ownerId === ownerId,
|
|
1048
|
+
).length;
|
|
970
1049
|
const nextRetryAt = deliveries.reduce<number | undefined>((next, delivery) => {
|
|
971
1050
|
if (next === undefined) return delivery.nextAttemptAt;
|
|
972
1051
|
return Math.min(next, delivery.nextAttemptAt);
|
|
@@ -977,6 +1056,7 @@ export class AsyncJobManager {
|
|
|
977
1056
|
delivering: inFlightDeliveries.length > 0 || (this.#deliveryLoop !== undefined && deliveries.length > 0),
|
|
978
1057
|
nextRetryAt,
|
|
979
1058
|
pendingJobIds: deliveries.concat(inFlightDeliveries).map(delivery => delivery.jobId),
|
|
1059
|
+
deadLettered,
|
|
980
1060
|
};
|
|
981
1061
|
}
|
|
982
1062
|
|
|
@@ -1035,6 +1115,29 @@ export class AsyncJobManager {
|
|
|
1035
1115
|
}
|
|
1036
1116
|
}
|
|
1037
1117
|
|
|
1118
|
+
getLastDisposeDiagnostics(): AsyncJobDisposeDiagnostics {
|
|
1119
|
+
return { ...this.#lastDisposeDiagnostics, stuckJobIds: [...this.#lastDisposeDiagnostics.stuckJobIds] };
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
async #waitForAllWithDeadline(timeoutMs: number): Promise<{ completed: boolean; stuckJobIds: string[] }> {
|
|
1123
|
+
const jobs = Array.from(this.#jobs.values());
|
|
1124
|
+
if (jobs.length === 0) return { completed: true, stuckJobIds: [] };
|
|
1125
|
+
let timedOut = false;
|
|
1126
|
+
await Promise.race([
|
|
1127
|
+
Promise.allSettled(jobs.map(job => job.promise)),
|
|
1128
|
+
Bun.sleep(Math.max(0, timeoutMs)).then(() => {
|
|
1129
|
+
timedOut = true;
|
|
1130
|
+
}),
|
|
1131
|
+
]);
|
|
1132
|
+
if (!timedOut) return { completed: true, stuckJobIds: [] };
|
|
1133
|
+
return {
|
|
1134
|
+
completed: false,
|
|
1135
|
+
stuckJobIds: Array.from(this.#jobs.values())
|
|
1136
|
+
.filter(job => job.status === "running" || job.status === "cancelled")
|
|
1137
|
+
.map(job => job.id),
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1038
1141
|
async waitForAll(): Promise<void> {
|
|
1039
1142
|
await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
|
|
1040
1143
|
}
|
|
@@ -1102,12 +1205,18 @@ export class AsyncJobManager {
|
|
|
1102
1205
|
}
|
|
1103
1206
|
}
|
|
1104
1207
|
this.#monitorTombstones.clear();
|
|
1105
|
-
|
|
1106
|
-
const
|
|
1208
|
+
const timeoutMs = options?.timeoutMs ?? 3_000;
|
|
1209
|
+
const waitResult = await this.#waitForAllWithDeadline(timeoutMs);
|
|
1210
|
+
const drained = waitResult.completed ? await this.drainDeliveries({ timeoutMs }) : false;
|
|
1211
|
+
this.#lastDisposeDiagnostics = { stuckJobIds: waitResult.stuckJobIds, deliveriesDrained: drained };
|
|
1212
|
+
if (waitResult.stuckJobIds.length > 0) {
|
|
1213
|
+
logger.warn("Async job manager dispose timed out waiting for jobs", { stuckJobIds: waitResult.stuckJobIds });
|
|
1214
|
+
}
|
|
1107
1215
|
this.#clearEvictionTimers();
|
|
1108
1216
|
this.#jobs.clear();
|
|
1109
1217
|
this.#deliveries.length = 0;
|
|
1110
1218
|
this.#inFlightDeliveries.length = 0;
|
|
1219
|
+
this.#deadLetteredDeliveries.clear();
|
|
1111
1220
|
this.#suppressedDeliveries.clear();
|
|
1112
1221
|
this.#watchedJobs.clear();
|
|
1113
1222
|
this.#outputState.clear();
|
|
@@ -1119,7 +1228,7 @@ export class AsyncJobManager {
|
|
|
1119
1228
|
this.#resumeQueue.length = 0;
|
|
1120
1229
|
this.#notifyChange();
|
|
1121
1230
|
this.#changeListeners.clear();
|
|
1122
|
-
return drained;
|
|
1231
|
+
return drained && waitResult.completed;
|
|
1123
1232
|
}
|
|
1124
1233
|
|
|
1125
1234
|
#resolveJobId(preferredId?: string): string {
|
|
@@ -1148,16 +1257,10 @@ export class AsyncJobManager {
|
|
|
1148
1257
|
}
|
|
1149
1258
|
|
|
1150
1259
|
#scheduleEviction(jobId: string): void {
|
|
1260
|
+
if (this.#disposed) return;
|
|
1151
1261
|
this.#notifyChange();
|
|
1152
1262
|
if (this.#retentionMs <= 0) {
|
|
1153
|
-
this.#
|
|
1154
|
-
this.#runLifecycle(jobId, "evict");
|
|
1155
|
-
this.#jobs.delete(jobId);
|
|
1156
|
-
this.#lifecycles.delete(jobId);
|
|
1157
|
-
this.#lifecyclePhases.delete(jobId);
|
|
1158
|
-
this.#suppressedDeliveries.delete(jobId);
|
|
1159
|
-
this.#watchedJobs.delete(jobId);
|
|
1160
|
-
this.#outputState.delete(jobId);
|
|
1263
|
+
this.#evictJob(jobId);
|
|
1161
1264
|
return;
|
|
1162
1265
|
}
|
|
1163
1266
|
const existing = this.#evictionTimers.get(jobId);
|
|
@@ -1166,20 +1269,25 @@ export class AsyncJobManager {
|
|
|
1166
1269
|
}
|
|
1167
1270
|
const timer = setTimeout(() => {
|
|
1168
1271
|
this.#evictionTimers.delete(jobId);
|
|
1169
|
-
this.#
|
|
1170
|
-
this.#runLifecycle(jobId, "evict");
|
|
1171
|
-
this.#jobs.delete(jobId);
|
|
1172
|
-
this.#lifecycles.delete(jobId);
|
|
1173
|
-
this.#lifecyclePhases.delete(jobId);
|
|
1174
|
-
this.#suppressedDeliveries.delete(jobId);
|
|
1175
|
-
this.#watchedJobs.delete(jobId);
|
|
1176
|
-
this.#outputState.delete(jobId);
|
|
1272
|
+
this.#evictJob(jobId);
|
|
1177
1273
|
this.#notifyChange();
|
|
1178
1274
|
}, this.#retentionMs);
|
|
1179
1275
|
timer.unref();
|
|
1180
1276
|
this.#evictionTimers.set(jobId, timer);
|
|
1181
1277
|
}
|
|
1182
1278
|
|
|
1279
|
+
#evictJob(jobId: string): void {
|
|
1280
|
+
this.#recordMonitorTombstone(jobId);
|
|
1281
|
+
this.#runLifecycle(jobId, "evict");
|
|
1282
|
+
this.#purgeTerminalSubagentStateForJob(jobId);
|
|
1283
|
+
this.#jobs.delete(jobId);
|
|
1284
|
+
this.#lifecycles.delete(jobId);
|
|
1285
|
+
this.#lifecyclePhases.delete(jobId);
|
|
1286
|
+
this.#suppressedDeliveries.delete(jobId);
|
|
1287
|
+
this.#watchedJobs.delete(jobId);
|
|
1288
|
+
this.#outputState.delete(jobId);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1183
1291
|
#clearEvictionTimers(): void {
|
|
1184
1292
|
for (const timer of this.#evictionTimers.values()) {
|
|
1185
1293
|
clearTimeout(timer);
|
|
@@ -1245,17 +1353,38 @@ export class AsyncJobManager {
|
|
|
1245
1353
|
if (this.isDeliverySuppressed(jobId)) {
|
|
1246
1354
|
return;
|
|
1247
1355
|
}
|
|
1356
|
+
const deliveryText = this.#boundedDeliveryText(text);
|
|
1248
1357
|
this.#deliveries.push({
|
|
1249
1358
|
jobId,
|
|
1250
|
-
text,
|
|
1359
|
+
text: deliveryText.text,
|
|
1360
|
+
originalBytes: deliveryText.originalBytes,
|
|
1361
|
+
truncated: deliveryText.truncated,
|
|
1251
1362
|
attempt: 0,
|
|
1252
1363
|
nextAttemptAt: Date.now(),
|
|
1253
1364
|
ownerId: this.#jobs.get(jobId)?.ownerId,
|
|
1254
1365
|
});
|
|
1366
|
+
while (this.#deliveries.length > DEFAULT_MAX_DELIVERY_QUEUE) {
|
|
1367
|
+
const dropped = this.#deliveries.shift();
|
|
1368
|
+
if (dropped) this.#deadLetteredDeliveries.set(dropped.jobId, dropped);
|
|
1369
|
+
}
|
|
1255
1370
|
this.#ensureDeliveryLoop();
|
|
1256
1371
|
}
|
|
1257
1372
|
|
|
1373
|
+
#boundedDeliveryText(text: string): { text: string; originalBytes?: number; truncated?: boolean } {
|
|
1374
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
1375
|
+
if (bytes <= DELIVERY_MAX_TEXT_BYTES) return { text };
|
|
1376
|
+
const head = sliceTextToUtf8ByteLength(text, DELIVERY_PREVIEW_HEAD_BYTES);
|
|
1377
|
+
const tailStart = Math.max(0, bytes - DELIVERY_PREVIEW_TAIL_BYTES);
|
|
1378
|
+
const tail = sliceTextAfterUtf8ByteOffset(text, tailStart);
|
|
1379
|
+
return {
|
|
1380
|
+
text: `${head}\n\n[async delivery output truncated from ${bytes} bytes]\n\n${tail}`,
|
|
1381
|
+
originalBytes: bytes,
|
|
1382
|
+
truncated: true,
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1258
1386
|
#ensureDeliveryLoop(): void {
|
|
1387
|
+
if (this.#disposed) return;
|
|
1259
1388
|
if (this.#deliveryLoop) {
|
|
1260
1389
|
return;
|
|
1261
1390
|
}
|
|
@@ -1266,7 +1395,7 @@ export class AsyncJobManager {
|
|
|
1266
1395
|
})
|
|
1267
1396
|
.finally(() => {
|
|
1268
1397
|
this.#deliveryLoop = undefined;
|
|
1269
|
-
if (this.#deliveries.length > 0) {
|
|
1398
|
+
if (!this.#disposed && this.#deliveries.length > 0) {
|
|
1270
1399
|
this.#ensureDeliveryLoop();
|
|
1271
1400
|
}
|
|
1272
1401
|
});
|
|
@@ -1304,20 +1433,29 @@ export class AsyncJobManager {
|
|
|
1304
1433
|
} catch (error) {
|
|
1305
1434
|
delivery.attempt += 1;
|
|
1306
1435
|
delivery.lastError = error instanceof Error ? error.message : String(error);
|
|
1307
|
-
delivery.
|
|
1308
|
-
|
|
1309
|
-
|
|
1436
|
+
if (delivery.attempt >= DELIVERY_MAX_ATTEMPTS) {
|
|
1437
|
+
this.#deadLetteredDeliveries.set(delivery.jobId, delivery);
|
|
1438
|
+
logger.warn("Async job completion delivery reached retry cap", {
|
|
1439
|
+
jobId: delivery.jobId,
|
|
1440
|
+
attempt: delivery.attempt,
|
|
1441
|
+
error: delivery.lastError,
|
|
1442
|
+
});
|
|
1443
|
+
} else {
|
|
1444
|
+
delivery.nextAttemptAt = Date.now() + this.#getRetryDelay(delivery.attempt);
|
|
1445
|
+
if (!this.isDeliverySuppressed(delivery.jobId)) {
|
|
1446
|
+
this.#deliveries.push(delivery);
|
|
1447
|
+
}
|
|
1448
|
+
logger.warn("Async job completion delivery failed", {
|
|
1449
|
+
jobId: delivery.jobId,
|
|
1450
|
+
attempt: delivery.attempt,
|
|
1451
|
+
nextRetryAt: delivery.nextAttemptAt,
|
|
1452
|
+
error: delivery.lastError,
|
|
1453
|
+
});
|
|
1310
1454
|
}
|
|
1311
|
-
logger.warn("Async job completion delivery failed", {
|
|
1312
|
-
jobId: delivery.jobId,
|
|
1313
|
-
attempt: delivery.attempt,
|
|
1314
|
-
nextRetryAt: delivery.nextAttemptAt,
|
|
1315
|
-
error: delivery.lastError,
|
|
1316
|
-
});
|
|
1317
1455
|
} finally {
|
|
1318
1456
|
const index = this.#inFlightDeliveries.indexOf(delivery);
|
|
1319
1457
|
if (index !== -1) this.#inFlightDeliveries.splice(index, 1);
|
|
1320
|
-
if (this.#deliveries.length > 0) this.#ensureDeliveryLoop();
|
|
1458
|
+
if (!this.#disposed && this.#deliveries.length > 0) this.#ensureDeliveryLoop();
|
|
1321
1459
|
}
|
|
1322
1460
|
})();
|
|
1323
1461
|
delivery.promise = promise;
|
package/src/config/file-lock.ts
CHANGED
|
@@ -101,7 +101,15 @@ function ownerLiveness(pid: number): OwnerLiveness {
|
|
|
101
101
|
|
|
102
102
|
async function isLockStale(lockPath: string, staleMs: number): Promise<boolean> {
|
|
103
103
|
const info = await readLockInfo(lockPath);
|
|
104
|
-
if (!info)
|
|
104
|
+
if (!info) {
|
|
105
|
+
try {
|
|
106
|
+
const stats = await fs.stat(lockPath);
|
|
107
|
+
return Date.now() - stats.mtimeMs > staleMs;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (isEnoent(err)) return false;
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
105
113
|
|
|
106
114
|
// Never reap a live owner by elapsed time: a long legitimate critical section must
|
|
107
115
|
// not have its lock stolen (#652). Reclaim a dead owner immediately. Only when owner
|
|
@@ -61,6 +61,49 @@ function resolveModelProfileName(profileName: string, profiles: ReadonlyMap<stri
|
|
|
61
61
|
return replacement && profiles.has(replacement) ? replacement : profileName;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Rewrite a selector only within the selector provider's own alternative group.
|
|
66
|
+
* Strict providers are never rewritten, and authenticated alternative providers
|
|
67
|
+
* keep their original selectors.
|
|
68
|
+
*/
|
|
69
|
+
function rewriteSelectorProvider(
|
|
70
|
+
selector: string,
|
|
71
|
+
authenticatedProviders: ReadonlySet<string>,
|
|
72
|
+
alternativeGroups: readonly (readonly string[])[],
|
|
73
|
+
): string {
|
|
74
|
+
const slash = selector.indexOf("/");
|
|
75
|
+
if (slash < 0) return selector;
|
|
76
|
+
|
|
77
|
+
const provider = selector.substring(0, slash);
|
|
78
|
+
if (authenticatedProviders.has(provider)) return selector;
|
|
79
|
+
|
|
80
|
+
const group = alternativeGroups.find(candidates => candidates.includes(provider));
|
|
81
|
+
if (!group) return selector;
|
|
82
|
+
|
|
83
|
+
const replacement = group.find(candidate => authenticatedProviders.has(candidate));
|
|
84
|
+
if (!replacement) return selector;
|
|
85
|
+
|
|
86
|
+
return replacement + selector.substring(slash);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function rewriteBindingsProviders(
|
|
90
|
+
bindings: { defaultSelector?: string; agentModelOverrides: Record<string, string> },
|
|
91
|
+
authenticatedProviders: ReadonlySet<string>,
|
|
92
|
+
alternativeGroups: readonly (readonly string[])[],
|
|
93
|
+
): { defaultSelector?: string; agentModelOverrides: Record<string, string> } {
|
|
94
|
+
return {
|
|
95
|
+
defaultSelector: bindings.defaultSelector
|
|
96
|
+
? rewriteSelectorProvider(bindings.defaultSelector, authenticatedProviders, alternativeGroups)
|
|
97
|
+
: undefined,
|
|
98
|
+
agentModelOverrides: Object.fromEntries(
|
|
99
|
+
Object.entries(bindings.agentModelOverrides).map(([role, sel]) => [
|
|
100
|
+
role,
|
|
101
|
+
rewriteSelectorProvider(sel, authenticatedProviders, alternativeGroups),
|
|
102
|
+
]),
|
|
103
|
+
),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
64
107
|
export async function prepareModelProfileActivation(
|
|
65
108
|
options: PrepareModelProfileActivationOptions,
|
|
66
109
|
): Promise<PreparedModelProfileActivation> {
|
|
@@ -72,19 +115,44 @@ export async function prepareModelProfileActivation(
|
|
|
72
115
|
throw new Error(`Unknown model profile "${options.profileName}". Available profiles: ${available}`);
|
|
73
116
|
}
|
|
74
117
|
|
|
118
|
+
const allProviders = aggregateModelProfileRequiredProviders(profile.requiredProviders, profile);
|
|
119
|
+
const alternativeGroups = profile.alternativeProviderGroups ?? [];
|
|
120
|
+
const alternativeSet = new Set(alternativeGroups.flat());
|
|
121
|
+
|
|
75
122
|
const missingProviders: string[] = [];
|
|
76
|
-
|
|
123
|
+
const authenticatedProviders: string[] = [];
|
|
124
|
+
for (const provider of allProviders) {
|
|
77
125
|
const apiKey = await options.modelRegistry.getApiKeyForProvider(provider, options.session.sessionId);
|
|
78
126
|
if (!isAuthenticated(apiKey)) {
|
|
79
127
|
missingProviders.push(provider);
|
|
128
|
+
} else {
|
|
129
|
+
authenticatedProviders.push(provider);
|
|
80
130
|
}
|
|
81
131
|
}
|
|
82
|
-
|
|
132
|
+
|
|
133
|
+
// Check strict (non-alternative) providers — all must be authenticated.
|
|
134
|
+
const strictMissing = missingProviders.filter(p => !alternativeSet.has(p));
|
|
135
|
+
if (strictMissing.length > 0) {
|
|
136
|
+
throw new Error(formatModelProfileCredentialError(options.profileName, strictMissing));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check alternative groups — at least one provider per group must be authenticated.
|
|
140
|
+
for (const group of alternativeGroups) {
|
|
141
|
+
const groupAuthenticated = group.some(p => authenticatedProviders.includes(p));
|
|
142
|
+
if (!groupAuthenticated) {
|
|
143
|
+
throw new Error(formatModelProfileCredentialError(options.profileName, [...group]));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (authenticatedProviders.length === 0) {
|
|
83
148
|
throw new Error(formatModelProfileCredentialError(options.profileName, missingProviders));
|
|
84
149
|
}
|
|
85
150
|
|
|
86
151
|
const availableModels = options.modelRegistry.getAll();
|
|
87
|
-
|
|
152
|
+
let bindings = resolveProfileBindings(profile);
|
|
153
|
+
if (missingProviders.length > 0 && alternativeGroups.length > 0) {
|
|
154
|
+
bindings = rewriteBindingsProviders(bindings, new Set(authenticatedProviders), alternativeGroups);
|
|
155
|
+
}
|
|
88
156
|
const resolvedDefault = bindings.defaultSelector
|
|
89
157
|
? resolveModelRoleValue(bindings.defaultSelector, availableModels, {
|
|
90
158
|
settings: options.settings as Settings,
|
|
@@ -6,6 +6,16 @@ export type ModelProfileRole = GjcModelAssignmentTargetId;
|
|
|
6
6
|
export interface ModelProfileDefinition {
|
|
7
7
|
name: string;
|
|
8
8
|
requiredProviders: string[];
|
|
9
|
+
/**
|
|
10
|
+
* Optional groups of providers that are interchangeable fallbacks.
|
|
11
|
+
* Each group is an array of provider ids where at least one must be
|
|
12
|
+
* authenticated. Providers NOT in any group are treated as strict
|
|
13
|
+
* requirements (all must be authenticated).
|
|
14
|
+
*
|
|
15
|
+
* Example: `[["xiaomi", "xiaomi-token-plan-sgp", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn"]]`
|
|
16
|
+
* means any single xiaomi credential satisfies the group.
|
|
17
|
+
*/
|
|
18
|
+
alternativeProviderGroups?: readonly (readonly string[])[];
|
|
9
19
|
modelMapping: Partial<Record<ModelProfileRole, string>>;
|
|
10
20
|
source: "builtin" | "user";
|
|
11
21
|
}
|
|
@@ -46,9 +56,11 @@ const profile = (
|
|
|
46
56
|
name: string,
|
|
47
57
|
requiredProviders: string[],
|
|
48
58
|
modelMapping: Record<ModelProfileRole, string>,
|
|
59
|
+
alternativeProviderGroups?: readonly (readonly string[])[],
|
|
49
60
|
): ModelProfileDefinition => ({
|
|
50
61
|
name,
|
|
51
62
|
requiredProviders: aggregateModelProfileRequiredProviders(requiredProviders, { modelMapping }),
|
|
63
|
+
alternativeProviderGroups,
|
|
52
64
|
modelMapping,
|
|
53
65
|
source: "builtin",
|
|
54
66
|
});
|
|
@@ -138,20 +150,30 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
138
150
|
critic: "xiaomi/mimo-v2.5-pro:medium",
|
|
139
151
|
architect: "xiaomi/mimo-v2.5-pro:high",
|
|
140
152
|
}),
|
|
141
|
-
profile(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
profile(
|
|
154
|
+
"mimo-medium",
|
|
155
|
+
["xiaomi", "xiaomi-token-plan-sgp", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn"],
|
|
156
|
+
{
|
|
157
|
+
default: "xiaomi/mimo-v2.5-pro:medium",
|
|
158
|
+
executor: "xiaomi/mimo-v2.5-pro:low",
|
|
159
|
+
planner: "xiaomi/mimo-v2.5-pro:medium",
|
|
160
|
+
critic: "xiaomi/mimo-v2.5-pro:high",
|
|
161
|
+
architect: "xiaomi/mimo-v2.5-pro:xhigh",
|
|
162
|
+
},
|
|
163
|
+
[["xiaomi", "xiaomi-token-plan-sgp", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn"]],
|
|
164
|
+
),
|
|
165
|
+
profile(
|
|
166
|
+
"mimo-pro",
|
|
167
|
+
["xiaomi", "xiaomi-token-plan-sgp", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn"],
|
|
168
|
+
{
|
|
169
|
+
default: "xiaomi/mimo-v2.5-pro:xhigh",
|
|
170
|
+
executor: "xiaomi/mimo-v2.5-pro:medium",
|
|
171
|
+
planner: "xiaomi/mimo-v2.5-pro:high",
|
|
172
|
+
critic: "xiaomi/mimo-v2.5-pro:xhigh",
|
|
173
|
+
architect: "xiaomi/mimo-v2.5-pro:xhigh",
|
|
174
|
+
},
|
|
175
|
+
[["xiaomi", "xiaomi-token-plan-sgp", "xiaomi-token-plan-ams", "xiaomi-token-plan-cn"]],
|
|
176
|
+
),
|
|
155
177
|
profile("grok-eco", ["xai"], {
|
|
156
178
|
default: "xai/grok-4.3:low",
|
|
157
179
|
executor: "xai/grok-4.3:minimal",
|
|
@@ -292,6 +314,9 @@ const PROFILE_RECOMMENDATIONS: Record<string, string> = {
|
|
|
292
314
|
zai: "glm-medium",
|
|
293
315
|
"kimi-code": "kimi-coding-plan-medium",
|
|
294
316
|
xiaomi: "mimo-medium",
|
|
317
|
+
"xiaomi-token-plan-sgp": "mimo-medium",
|
|
318
|
+
"xiaomi-token-plan-ams": "mimo-medium",
|
|
319
|
+
"xiaomi-token-plan-cn": "mimo-medium",
|
|
295
320
|
xai: "grok-medium",
|
|
296
321
|
"grok-build": "grok-build-pro",
|
|
297
322
|
cursor: "cursor-medium",
|