@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
|
@@ -8,6 +8,13 @@ function sanitizeOutputChunk(rawChunk: string): string {
|
|
|
8
8
|
return sanitizeWithOptionalSixelPassthrough(rawChunk, sanitizeText);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Flush threshold for the opt-in sanitize-coalescing path (F21). When coalescing is enabled, raw
|
|
13
|
+
* chunks accumulate until they reach this many chars, then are sanitized + delivered as one batch,
|
|
14
|
+
* so many-small-chunk output pays one sanitize pass per batch instead of one per tiny chunk.
|
|
15
|
+
*/
|
|
16
|
+
const COALESCE_FLUSH_CHARS = 64 * 1024;
|
|
17
|
+
|
|
11
18
|
// =============================================================================
|
|
12
19
|
// Constants
|
|
13
20
|
// =============================================================================
|
|
@@ -15,6 +22,7 @@ function sanitizeOutputChunk(rawChunk: string): string {
|
|
|
15
22
|
export const DEFAULT_MAX_LINES = 3000;
|
|
16
23
|
export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
|
17
24
|
export const DEFAULT_MAX_COLUMN = 1024; // Max chars per grep match line
|
|
25
|
+
export const DEFAULT_ARTIFACT_MAX_BYTES = 10 * 1024 * 1024; // 10MB
|
|
18
26
|
|
|
19
27
|
const NL = "\n";
|
|
20
28
|
|
|
@@ -41,6 +49,8 @@ export interface OutputSummary {
|
|
|
41
49
|
columnTruncatedLines?: number;
|
|
42
50
|
/** Artifact ID for internal URL access (artifact://<id>) when truncated */
|
|
43
51
|
artifactId?: string;
|
|
52
|
+
/** Bytes omitted from artifact storage after the artifact hard cap was reached. */
|
|
53
|
+
artifactTruncatedBytes?: number;
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
export interface OutputSinkOptions {
|
|
@@ -61,6 +71,8 @@ export interface OutputSinkOptions {
|
|
|
61
71
|
* writes still respect the budget. Default 0 = no per-line cap.
|
|
62
72
|
*/
|
|
63
73
|
maxColumns?: number;
|
|
74
|
+
/** Hard cap for artifact writes/pending replay. Default DEFAULT_ARTIFACT_MAX_BYTES. */
|
|
75
|
+
artifactMaxBytes?: number;
|
|
64
76
|
onChunk?: (chunk: string) => void;
|
|
65
77
|
/** Minimum ms between onChunk calls. 0 = every chunk (default). */
|
|
66
78
|
chunkThrottleMs?: number;
|
|
@@ -75,6 +87,13 @@ export interface OutputSinkOptions {
|
|
|
75
87
|
* relative to the sink (the sink does not catch errors from this callback).
|
|
76
88
|
*/
|
|
77
89
|
onRawChunk?: (chunk: string) => void;
|
|
90
|
+
/**
|
|
91
|
+
* Opt-in (F21): when true, sanitization + live callback delivery + retention are coalesced over
|
|
92
|
+
* batched raw chunks instead of run per chunk, bounding sync CPU for many-small-chunk output. The
|
|
93
|
+
* raw artifact mirror stays byte-correct. Defaults to the PI_OUTPUT_SANITIZE_COALESCE env flag
|
|
94
|
+
* (default OFF — the per-chunk path is byte-identical to historical behavior).
|
|
95
|
+
*/
|
|
96
|
+
coalesceSanitize?: boolean;
|
|
78
97
|
}
|
|
79
98
|
|
|
80
99
|
export interface TruncationResult {
|
|
@@ -668,6 +687,9 @@ export class OutputSink {
|
|
|
668
687
|
#sawData = false;
|
|
669
688
|
#truncated = false;
|
|
670
689
|
#lastChunkTime = 0;
|
|
690
|
+
#artifactBytes = 0;
|
|
691
|
+
#artifactTruncatedBytes = 0;
|
|
692
|
+
#artifactTruncationNoticeWritten = false;
|
|
671
693
|
|
|
672
694
|
// Per-line column cap streaming state (persists across `push` calls so a
|
|
673
695
|
// long line split across chunks still trips the same trigger).
|
|
@@ -697,6 +719,9 @@ export class OutputSink {
|
|
|
697
719
|
readonly #onRawChunk?: (chunk: string) => void;
|
|
698
720
|
readonly #chunkThrottleMs: number;
|
|
699
721
|
readonly #maxColumns: number;
|
|
722
|
+
readonly #artifactMaxBytes: number;
|
|
723
|
+
readonly #coalesceSanitize: boolean;
|
|
724
|
+
#coalesceBuf = "";
|
|
700
725
|
|
|
701
726
|
constructor(options?: OutputSinkOptions) {
|
|
702
727
|
const {
|
|
@@ -708,6 +733,8 @@ export class OutputSink {
|
|
|
708
733
|
onChunk,
|
|
709
734
|
chunkThrottleMs = 0,
|
|
710
735
|
onRawChunk,
|
|
736
|
+
artifactMaxBytes = DEFAULT_ARTIFACT_MAX_BYTES,
|
|
737
|
+
coalesceSanitize = process.env.PI_OUTPUT_SANITIZE_COALESCE === "1",
|
|
711
738
|
} = options ?? {};
|
|
712
739
|
this.#artifactPath = artifactPath;
|
|
713
740
|
this.#artifactId = artifactId;
|
|
@@ -717,6 +744,8 @@ export class OutputSink {
|
|
|
717
744
|
this.#onChunk = onChunk;
|
|
718
745
|
this.#onRawChunk = onRawChunk;
|
|
719
746
|
this.#chunkThrottleMs = chunkThrottleMs;
|
|
747
|
+
this.#artifactMaxBytes = Math.max(0, artifactMaxBytes);
|
|
748
|
+
this.#coalesceSanitize = coalesceSanitize;
|
|
720
749
|
}
|
|
721
750
|
|
|
722
751
|
#headText(): string {
|
|
@@ -754,7 +783,28 @@ export class OutputSink {
|
|
|
754
783
|
* visible retention windows are selected from the sanitized/column-capped
|
|
755
784
|
* stream so production-default display matches the historical processed view.
|
|
756
785
|
*/
|
|
786
|
+
// F21: with coalescing enabled, accumulate raw chunks and process them in batches; the default
|
|
787
|
+
// (disabled) path calls #ingest directly and is byte-identical to the historical per-chunk path.
|
|
757
788
|
push(chunk: string): void {
|
|
789
|
+
if (!this.#coalesceSanitize) {
|
|
790
|
+
this.#ingest(chunk);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
this.#coalesceBuf += chunk;
|
|
794
|
+
if (this.#coalesceBuf.length >= COALESCE_FLUSH_CHARS) {
|
|
795
|
+
this.#flushCoalesced();
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** Process any buffered coalesced chunks as a single batch (F21). */
|
|
800
|
+
#flushCoalesced(): void {
|
|
801
|
+
if (this.#coalesceBuf.length === 0) return;
|
|
802
|
+
const batch = this.#coalesceBuf;
|
|
803
|
+
this.#coalesceBuf = "";
|
|
804
|
+
this.#ingest(batch);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
#ingest(chunk: string): void {
|
|
758
808
|
const rawChunk = chunk;
|
|
759
809
|
|
|
760
810
|
// Live callbacks historically observe sanitized, uncapped chunks. The same
|
|
@@ -907,6 +957,40 @@ export class OutputSink {
|
|
|
907
957
|
}
|
|
908
958
|
}
|
|
909
959
|
|
|
960
|
+
#artifactTruncationNotice(droppedBytes: number): string {
|
|
961
|
+
return `\n[artifact truncated after ${this.#artifactBytes} bytes; omitted at least ${droppedBytes} bytes]\n`;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
#capArtifactChunk(chunk: string, bytes: number): { chunk: string; bytes: number } | null {
|
|
965
|
+
if (bytes === 0) return null;
|
|
966
|
+
if (this.#artifactMaxBytes <= 0 || this.#artifactBytes >= this.#artifactMaxBytes) {
|
|
967
|
+
this.#artifactTruncatedBytes += bytes;
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
const room = this.#artifactMaxBytes - this.#artifactBytes;
|
|
971
|
+
if (bytes <= room) {
|
|
972
|
+
return { chunk, bytes };
|
|
973
|
+
}
|
|
974
|
+
const kept = truncateHeadBytes(chunk, room);
|
|
975
|
+
this.#artifactTruncatedBytes += bytes - kept.bytes;
|
|
976
|
+
return kept.bytes > 0 ? { chunk: kept.text, bytes: kept.bytes } : null;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
#writeArtifactTruncationNotice(): void {
|
|
980
|
+
if (this.#artifactTruncatedBytes <= 0 || this.#artifactTruncationNoticeWritten) return;
|
|
981
|
+
const notice = this.#artifactTruncationNotice(this.#artifactTruncatedBytes);
|
|
982
|
+
try {
|
|
983
|
+
if (this.#fileReady && this.#file) {
|
|
984
|
+
this.#file.sink.write(notice);
|
|
985
|
+
} else {
|
|
986
|
+
this.#queuePendingFileWrite(notice, Buffer.byteLength(notice, "utf-8"));
|
|
987
|
+
}
|
|
988
|
+
this.#artifactTruncationNoticeWritten = true;
|
|
989
|
+
} catch {
|
|
990
|
+
/* ignore */
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
910
994
|
#queuePendingFileWrite(chunk: string, bytes = Buffer.byteLength(chunk, "utf-8")): void {
|
|
911
995
|
if (!this.#pendingFileWrites) this.#pendingFileWrites = [chunk];
|
|
912
996
|
else this.#pendingFileWrites.push(chunk);
|
|
@@ -915,14 +999,17 @@ export class OutputSink {
|
|
|
915
999
|
}
|
|
916
1000
|
|
|
917
1001
|
#enqueueFileWrite(chunk: string, bytes: number): void {
|
|
1002
|
+
const capped = this.#capArtifactChunk(chunk, bytes);
|
|
1003
|
+
if (!capped) return;
|
|
1004
|
+
this.#artifactBytes += capped.bytes;
|
|
918
1005
|
if (!this.#fileReady || !this.#file) {
|
|
919
|
-
this.#queuePendingFileWrite(chunk, bytes);
|
|
1006
|
+
this.#queuePendingFileWrite(capped.chunk, capped.bytes);
|
|
920
1007
|
if (this.#willOverflow(bytes) || this.#pendingFileWriteBytes > this.#spillThreshold) this.#createFileSink();
|
|
921
1008
|
return;
|
|
922
1009
|
}
|
|
923
1010
|
|
|
924
1011
|
try {
|
|
925
|
-
this.#file.sink.write(chunk);
|
|
1012
|
+
this.#file.sink.write(capped.chunk);
|
|
926
1013
|
} catch {
|
|
927
1014
|
try {
|
|
928
1015
|
void this.#file.sink.end();
|
|
@@ -931,7 +1018,7 @@ export class OutputSink {
|
|
|
931
1018
|
}
|
|
932
1019
|
this.#file = undefined;
|
|
933
1020
|
this.#fileReady = false;
|
|
934
|
-
this.#queuePendingFileWrite(chunk, bytes);
|
|
1021
|
+
this.#queuePendingFileWrite(capped.chunk, capped.bytes);
|
|
935
1022
|
this.#createFileSink();
|
|
936
1023
|
}
|
|
937
1024
|
}
|
|
@@ -998,6 +1085,7 @@ export class OutputSink {
|
|
|
998
1085
|
* branch in `dump()` against stale totals.
|
|
999
1086
|
*/
|
|
1000
1087
|
replace(text: string): void {
|
|
1088
|
+
this.#coalesceBuf = "";
|
|
1001
1089
|
this.#setTail(text);
|
|
1002
1090
|
this.#head = "";
|
|
1003
1091
|
this.#headBytes = 0;
|
|
@@ -1015,10 +1103,13 @@ export class OutputSink {
|
|
|
1015
1103
|
}
|
|
1016
1104
|
|
|
1017
1105
|
async dump(notice?: string): Promise<OutputSummary> {
|
|
1106
|
+
this.#flushCoalesced();
|
|
1018
1107
|
const noticeLine = notice ? `[${notice}]\n` : "";
|
|
1019
1108
|
const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
|
|
1020
1109
|
|
|
1021
1110
|
let artifactId: string | undefined;
|
|
1111
|
+
if (this.#artifactTruncatedBytes > 0) this.#createFileSink();
|
|
1112
|
+
this.#writeArtifactTruncationNotice();
|
|
1022
1113
|
if (this.#file) {
|
|
1023
1114
|
artifactId = this.#file.artifactId;
|
|
1024
1115
|
await this.#file.sink.end();
|
|
@@ -1095,6 +1186,7 @@ export class OutputSink {
|
|
|
1095
1186
|
elidedLines,
|
|
1096
1187
|
columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
|
|
1097
1188
|
columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
|
|
1189
|
+
artifactTruncatedBytes: this.#artifactTruncatedBytes > 0 ? this.#artifactTruncatedBytes : undefined,
|
|
1098
1190
|
artifactId,
|
|
1099
1191
|
};
|
|
1100
1192
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { formatProviderCredentialHint } from "@gajae-code/ai/stream";
|
|
2
|
+
|
|
1
3
|
export const MODEL_ONBOARDING_API_PROVIDER_COMMAND =
|
|
2
4
|
"/provider add --compat <openai|anthropic> --provider <id> --base-url <url> --api-key-env <ENV> --model <model>";
|
|
3
5
|
export const MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND = "/provider add --preset <minimax|minimax-cn|glm>";
|
|
@@ -26,14 +28,19 @@ export function formatNoModelOnboardingError(): string {
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function formatNoCredentialOnboardingError(providerId: string): string {
|
|
29
|
-
|
|
31
|
+
const lines = [
|
|
30
32
|
`No credentials found for ${providerId}.`,
|
|
31
33
|
"",
|
|
32
34
|
`For MiniMax/GLM presets, configure credentials with ${MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND} --preset <preset>).`,
|
|
33
35
|
`For custom API-compatible providers, use ${MODEL_ONBOARDING_API_PROVIDER_COMMAND}.`,
|
|
34
|
-
`For OAuth/subscription providers, use ${MODEL_ONBOARDING_OAUTH_COMMAND}.`,
|
|
36
|
+
`For OAuth/subscription providers, use ${MODEL_ONBOARDING_OAUTH_COMMAND} (interactive; not available in headless/print mode).`,
|
|
37
|
+
];
|
|
38
|
+
const headlessHint = formatProviderCredentialHint(providerId);
|
|
39
|
+
if (headlessHint) lines.push(headlessHint);
|
|
40
|
+
lines.push(
|
|
35
41
|
"Then run /model to select a configured model or assign it to DEFAULT, EXECUTOR, ARCHITECT, PLANNER, or CRITIC.",
|
|
36
|
-
|
|
42
|
+
);
|
|
43
|
+
return lines.join("\n");
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
export function formatNoModelsAvailableFallback(): string {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import {
|
|
3
3
|
type ActiveSessionScope,
|
|
4
|
+
readActiveEntries,
|
|
4
5
|
rebuildActiveSnapshot,
|
|
5
6
|
removeActiveEntry,
|
|
6
7
|
writeActiveEntry,
|
|
@@ -416,6 +417,62 @@ function rawActiveEntries(state: SkillActiveState | null): SkillActiveEntry[] {
|
|
|
416
417
|
return out;
|
|
417
418
|
}
|
|
418
419
|
|
|
420
|
+
async function readModeStatePhase(
|
|
421
|
+
cwd: string,
|
|
422
|
+
sessionId: string | undefined,
|
|
423
|
+
skill: CanonicalGjcWorkflowSkill,
|
|
424
|
+
): Promise<string | undefined> {
|
|
425
|
+
const stateDir = path.join(cwd, ".gjc", "state");
|
|
426
|
+
const normalizedSessionId = safeString(sessionId).trim();
|
|
427
|
+
const filePath = normalizedSessionId
|
|
428
|
+
? path.join(stateDir, "sessions", encodePathSegment(normalizedSessionId), `${skill}-state.json`)
|
|
429
|
+
: path.join(stateDir, `${skill}-state.json`);
|
|
430
|
+
try {
|
|
431
|
+
const parsed = JSON.parse(await Bun.file(filePath).text());
|
|
432
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
|
|
433
|
+
const record = parsed as Record<string, unknown>;
|
|
434
|
+
const phase = safeString(record.current_phase).trim();
|
|
435
|
+
if (!phase) return undefined;
|
|
436
|
+
if (record.active === false && !RALPLAN_CANONICAL_PHASE_OVERRIDES.has(phase)) return undefined;
|
|
437
|
+
return phase;
|
|
438
|
+
} catch {
|
|
439
|
+
return undefined;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const RALPLAN_CANONICAL_PHASE_OVERRIDES = new Set([
|
|
444
|
+
"final",
|
|
445
|
+
"handoff",
|
|
446
|
+
"complete",
|
|
447
|
+
"completed",
|
|
448
|
+
"failed",
|
|
449
|
+
"cancelled",
|
|
450
|
+
"canceled",
|
|
451
|
+
"inactive",
|
|
452
|
+
]);
|
|
453
|
+
|
|
454
|
+
function withCanonicalRalplanPhase(entry: SkillActiveEntry, canonicalPhase: string | undefined): SkillActiveEntry {
|
|
455
|
+
if (
|
|
456
|
+
entry.skill !== "ralplan" ||
|
|
457
|
+
!canonicalPhase ||
|
|
458
|
+
!RALPLAN_CANONICAL_PHASE_OVERRIDES.has(canonicalPhase) ||
|
|
459
|
+
entry.phase === canonicalPhase
|
|
460
|
+
) {
|
|
461
|
+
return entry;
|
|
462
|
+
}
|
|
463
|
+
const hud = entry.hud
|
|
464
|
+
? {
|
|
465
|
+
...entry.hud,
|
|
466
|
+
chips: entry.hud.chips?.map(chip => (chip.label === "stage" ? { ...chip, value: canonicalPhase } : chip)),
|
|
467
|
+
}
|
|
468
|
+
: undefined;
|
|
469
|
+
return {
|
|
470
|
+
...entry,
|
|
471
|
+
phase: canonicalPhase,
|
|
472
|
+
...(hud ? { hud } : {}),
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
419
476
|
function filterRootEntriesForSession(entries: SkillActiveEntry[], sessionId?: string): SkillActiveEntry[] {
|
|
420
477
|
const normalizedSessionId = safeString(sessionId).trim();
|
|
421
478
|
if (!normalizedSessionId) return entries;
|
|
@@ -538,19 +595,32 @@ export function collapsePlanningPipeline(entries: readonly SkillActiveEntry[]):
|
|
|
538
595
|
return entries.filter(entry => !PLANNING_PIPELINE_SKILLS.has(entry.skill) || entry === current);
|
|
539
596
|
}
|
|
540
597
|
|
|
541
|
-
function mergeVisibleEntries(
|
|
598
|
+
async function mergeVisibleEntries(
|
|
599
|
+
cwd: string,
|
|
542
600
|
sessionState: SkillActiveState | null,
|
|
543
601
|
rootState: SkillActiveState | null,
|
|
544
602
|
sessionId?: string,
|
|
545
|
-
): SkillActiveEntry[] {
|
|
603
|
+
): Promise<SkillActiveEntry[]> {
|
|
546
604
|
// Use the raw (active + inactive) rows so a handoff demotion stays visible
|
|
547
605
|
// long enough to supersede a stale same-skill row before the active filter.
|
|
548
|
-
|
|
606
|
+
// Per-skill files in active/<skill>.json are authoritative and are merged
|
|
607
|
+
// after the derived snapshot cache, so a stale skill-active-state.json row
|
|
608
|
+
// cannot override the latest entry file.
|
|
609
|
+
const rootEntries = filterRootEntriesForSession(
|
|
610
|
+
[...rawActiveEntries(rootState), ...(await readActiveEntries(cwd))],
|
|
611
|
+
sessionId,
|
|
612
|
+
);
|
|
549
613
|
const merged = new Map(rootEntries.map(entry => [entryKey(entry), entry]));
|
|
550
|
-
|
|
614
|
+
const sessionEntries = sessionId
|
|
615
|
+
? [...rawActiveEntries(sessionState), ...(await readActiveEntries(cwd, { sessionId }))]
|
|
616
|
+
: rawActiveEntries(sessionState);
|
|
617
|
+
for (const entry of sessionEntries) {
|
|
551
618
|
merged.set(entryKey(entry), entry);
|
|
552
619
|
}
|
|
553
|
-
|
|
620
|
+
const canonicalRalplanPhase = await readModeStatePhase(cwd, sessionId, "ralplan");
|
|
621
|
+
return dedupeVisibleBySkill([...merged.values()], sessionId)
|
|
622
|
+
.filter(entry => entry.active !== false)
|
|
623
|
+
.map(entry => withCanonicalRalplanPhase(entry, canonicalRalplanPhase));
|
|
554
624
|
}
|
|
555
625
|
|
|
556
626
|
export async function readVisibleSkillActiveState(cwd: string, sessionId?: string): Promise<SkillActiveState | null> {
|
|
@@ -559,7 +629,7 @@ export async function readVisibleSkillActiveState(cwd: string, sessionId?: strin
|
|
|
559
629
|
readRawActiveStateForHandoff(rootPath, false),
|
|
560
630
|
sessionPath ? readRawActiveStateForHandoff(sessionPath, false) : Promise.resolve(null),
|
|
561
631
|
]);
|
|
562
|
-
const activeSkills = mergeVisibleEntries(sessionState, rootState, sessionId);
|
|
632
|
+
const activeSkills = await mergeVisibleEntries(cwd, sessionState, rootState, sessionId);
|
|
563
633
|
if (activeSkills.length === 0) return null;
|
|
564
634
|
const primary = activeSkills[0];
|
|
565
635
|
return {
|
|
@@ -622,7 +692,9 @@ async function activeSubskillsForExistingEntry(
|
|
|
622
692
|
readRawActiveStateForHandoff(rootPath, false),
|
|
623
693
|
sessionPath ? readRawActiveStateForHandoff(sessionPath, false) : Promise.resolve(null),
|
|
624
694
|
]);
|
|
625
|
-
const existing = mergeVisibleEntries(sessionState, rootState, sessionId).find(
|
|
695
|
+
const existing = (await mergeVisibleEntries(cwd, sessionState, rootState, sessionId)).find(
|
|
696
|
+
entry => entry.skill === skill,
|
|
697
|
+
);
|
|
626
698
|
return existing?.active_subskills;
|
|
627
699
|
}
|
|
628
700
|
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { ThinkingLevel } from "@gajae-code/agent-core";
|
|
4
4
|
import { type Model, modelsAreEqual } from "@gajae-code/ai";
|
|
5
5
|
import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
|
|
6
|
+
import { Spacer, Text } from "@gajae-code/tui";
|
|
6
7
|
import { setProjectDir } from "@gajae-code/utils";
|
|
7
8
|
import { jobElapsedMs } from "../async";
|
|
8
9
|
import {
|
|
@@ -13,6 +14,8 @@ import {
|
|
|
13
14
|
import { extractExplicitThinkingSelector, formatModelSelectorValue, parseModelPattern } from "../config/model-resolver";
|
|
14
15
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../discovery/helpers.js";
|
|
15
16
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
17
|
+
import { DynamicBorder } from "../modes/components/dynamic-border";
|
|
18
|
+
import { theme } from "../modes/theme/theme";
|
|
16
19
|
import type { InteractiveModeContext } from "../modes/types";
|
|
17
20
|
import { formatModelOnboardingGuidance } from "../setup/model-onboarding-guidance";
|
|
18
21
|
import {
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
} from "../setup/provider-onboarding";
|
|
24
27
|
import { parseThinkingLevel } from "../thinking";
|
|
25
28
|
import { buildContextReportText } from "./helpers/context-report";
|
|
29
|
+
import { buildFastStatusReport } from "./helpers/fast-status-report";
|
|
26
30
|
import { formatDuration } from "./helpers/format";
|
|
27
31
|
import { commandConsumed, errorMessage, parseSlashCommand, usage } from "./helpers/parse";
|
|
28
32
|
import { handleSshAcp } from "./helpers/ssh";
|
|
@@ -41,6 +45,14 @@ export type { BuiltinSlashCommand, SubcommandDef } from "./types";
|
|
|
41
45
|
/** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
|
|
42
46
|
export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
|
|
43
47
|
|
|
48
|
+
function fastStatusRoleTargets(): Array<{ id: GjcModelAssignmentTargetId; label: string; isSubagentRole: boolean }> {
|
|
49
|
+
return GJC_MODEL_ASSIGNMENT_TARGET_IDS.map(id => ({
|
|
50
|
+
id,
|
|
51
|
+
label: GJC_MODEL_ASSIGNMENT_TARGETS[id].tag ?? id.toUpperCase(),
|
|
52
|
+
isSubagentRole: GJC_MODEL_ASSIGNMENT_TARGETS[id].settingsPath === "task.agentModelOverrides",
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
function parseProviderSetupSlashArgs(args: string): {
|
|
45
57
|
preset?: string;
|
|
46
58
|
compat?: string;
|
|
@@ -357,7 +369,13 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
357
369
|
return commandConsumed();
|
|
358
370
|
}
|
|
359
371
|
if (arg === "status") {
|
|
360
|
-
await runtime.output(
|
|
372
|
+
await runtime.output(
|
|
373
|
+
buildFastStatusReport({
|
|
374
|
+
session: runtime.session,
|
|
375
|
+
roleTargets: fastStatusRoleTargets(),
|
|
376
|
+
iconFast: theme.icon.fast,
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
361
379
|
return commandConsumed();
|
|
362
380
|
}
|
|
363
381
|
return usage("Usage: /fast [on|off|status]", runtime);
|
|
@@ -386,8 +404,17 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
386
404
|
return;
|
|
387
405
|
}
|
|
388
406
|
if (arg === "status") {
|
|
389
|
-
const
|
|
390
|
-
|
|
407
|
+
const report = buildFastStatusReport({
|
|
408
|
+
session: runtime.ctx.session,
|
|
409
|
+
roleTargets: fastStatusRoleTargets(),
|
|
410
|
+
iconFast: theme.icon.fast,
|
|
411
|
+
formatInactive: text => theme.fg("dim", text),
|
|
412
|
+
});
|
|
413
|
+
runtime.ctx.chatContainer.addChild(new Spacer(1));
|
|
414
|
+
runtime.ctx.chatContainer.addChild(new DynamicBorder());
|
|
415
|
+
runtime.ctx.chatContainer.addChild(new Text(report, 1, 0));
|
|
416
|
+
runtime.ctx.chatContainer.addChild(new DynamicBorder());
|
|
417
|
+
runtime.ctx.ui.requestRender();
|
|
391
418
|
runtime.ctx.editor.setText("");
|
|
392
419
|
return;
|
|
393
420
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Model } from "@gajae-code/ai";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A single line in the `/fast status` report: a labelled model and whether fast
|
|
5
|
+
* mode is effective for it. The `fast` flag is resolved by the caller
|
|
6
|
+
* (`buildFastStatusReport`) so each row can use the correct service tier — the
|
|
7
|
+
* main session tier for the current model / `modelRoles` roles, or the subagent
|
|
8
|
+
* tier (`task.serviceTier`) for `task.agentModelOverrides` roles.
|
|
9
|
+
*/
|
|
10
|
+
export interface FastStatusRow {
|
|
11
|
+
/** Display label, e.g. "현재 모델", "DEFAULT", "EXECUTOR". */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Resolved model for this row, if any. */
|
|
14
|
+
model?: Model;
|
|
15
|
+
/** Whether fast mode is effective for this row's model. */
|
|
16
|
+
fast: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FormatFastStatusReportArgs {
|
|
20
|
+
rows: FastStatusRow[];
|
|
21
|
+
/** The active theme's fast icon token (`theme.icon.fast`). */
|
|
22
|
+
iconFast: string;
|
|
23
|
+
/** Optional decorator for inactive ("off") text, e.g. theme dim in the TUI. */
|
|
24
|
+
formatInactive?: (text: string) => string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Title line of the `/fast status` report. */
|
|
28
|
+
export const FAST_STATUS_TITLE = "Fast 모드 상태";
|
|
29
|
+
|
|
30
|
+
/** The inactive marker shown for rows where fast mode does not apply. */
|
|
31
|
+
export const FAST_STATUS_OFF = "off";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Format a multiline `/fast status` report. Pure and shared by the CLI
|
|
35
|
+
* (`handle`) and TUI (`handleTui`) command branches so the two never drift.
|
|
36
|
+
* Each row's fast/off state is decided by the caller (see
|
|
37
|
+
* {@link buildFastStatusReport}) so per-row service-tier differences are honored.
|
|
38
|
+
*/
|
|
39
|
+
export function formatFastStatusReport(args: FormatFastStatusReportArgs): string {
|
|
40
|
+
const { rows, iconFast } = args;
|
|
41
|
+
const formatInactive = args.formatInactive ?? ((text: string) => text);
|
|
42
|
+
const lines: string[] = [FAST_STATUS_TITLE];
|
|
43
|
+
for (const row of rows) {
|
|
44
|
+
if (!row.model) {
|
|
45
|
+
lines.push(`${row.label}: ${formatInactive(FAST_STATUS_OFF)}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const ref = `${row.model.provider}/${row.model.id}`;
|
|
49
|
+
lines.push(`${row.label}: ${ref} ${row.fast ? iconFast : formatInactive(FAST_STATUS_OFF)}`);
|
|
50
|
+
}
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Minimal session surface needed to build the `/fast status` report. */
|
|
55
|
+
export interface FastStatusSessionLike {
|
|
56
|
+
readonly model?: Model;
|
|
57
|
+
/** Fast predicate against the main session tier (current model + `modelRoles`). */
|
|
58
|
+
isFastForProvider(provider?: string): boolean;
|
|
59
|
+
/** Fast predicate against the effective subagent tier (`task.agentModelOverrides` roles). */
|
|
60
|
+
isFastForSubagentProvider(provider?: string): boolean;
|
|
61
|
+
resolveRoleModelWithThinking(role: string): { model?: Model };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A role to enumerate in the report, with the tier source its subagent runs under. */
|
|
65
|
+
export interface FastStatusRoleTarget {
|
|
66
|
+
id: string;
|
|
67
|
+
label: string;
|
|
68
|
+
/**
|
|
69
|
+
* True for `task.agentModelOverrides` roles (executor/architect/planner/critic)
|
|
70
|
+
* that run under `task.serviceTier`; false for `modelRoles` roles (default)
|
|
71
|
+
* that run under the main session tier.
|
|
72
|
+
*/
|
|
73
|
+
isSubagentRole: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface BuildFastStatusReportArgs {
|
|
77
|
+
session: FastStatusSessionLike;
|
|
78
|
+
/** Role targets to enumerate, in display order. */
|
|
79
|
+
roleTargets: ReadonlyArray<FastStatusRoleTarget>;
|
|
80
|
+
/** The active theme's fast icon token (`theme.icon.fast`). */
|
|
81
|
+
iconFast: string;
|
|
82
|
+
/** Optional decorator for inactive ("off") text, e.g. theme dim in the TUI. */
|
|
83
|
+
formatInactive?: (text: string) => string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build the `/fast status` report from a live session: the active/current model
|
|
88
|
+
* followed by each assigned role (subagent) model. Unassigned roles are skipped
|
|
89
|
+
* so the report mirrors the `/model` selector, which only badges assigned roles.
|
|
90
|
+
*
|
|
91
|
+
* Subagent roles (`task.agentModelOverrides`) are evaluated against the
|
|
92
|
+
* effective subagent tier (`task.serviceTier`), while the current model and
|
|
93
|
+
* `modelRoles` roles use the main session tier — matching where each model
|
|
94
|
+
* actually runs.
|
|
95
|
+
*/
|
|
96
|
+
export function buildFastStatusReport(args: BuildFastStatusReportArgs): string {
|
|
97
|
+
const { session, roleTargets, iconFast, formatInactive } = args;
|
|
98
|
+
const rows: FastStatusRow[] = [
|
|
99
|
+
{ label: "현재 모델", model: session.model, fast: session.isFastForProvider(session.model?.provider) },
|
|
100
|
+
];
|
|
101
|
+
for (const target of roleTargets) {
|
|
102
|
+
const resolved = session.resolveRoleModelWithThinking(target.id);
|
|
103
|
+
if (resolved.model) {
|
|
104
|
+
const fast = target.isSubagentRole
|
|
105
|
+
? session.isFastForSubagentProvider(resolved.model.provider)
|
|
106
|
+
: session.isFastForProvider(resolved.model.provider);
|
|
107
|
+
rows.push({ label: target.label, model: resolved.model, fast });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return formatFastStatusReport({ rows, iconFast, formatInactive });
|
|
111
|
+
}
|
|
@@ -315,7 +315,16 @@ export async function openArchive(filePath: string): Promise<ArchiveReader> {
|
|
|
315
315
|
throw new ToolError(`Unsupported archive format: ${filePath}`);
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
// F20: cap the compressed archive read so opening a multi-GB archive cannot buffer it
|
|
319
|
+
// whole into memory. (Zip-bomb expanded-size bounding would need a streaming inflate.)
|
|
320
|
+
const MAX_ARCHIVE_BYTES = 256 * 1024 * 1024;
|
|
321
|
+
const file = Bun.file(filePath);
|
|
322
|
+
if (file.size > MAX_ARCHIVE_BYTES) {
|
|
323
|
+
throw new ToolError(
|
|
324
|
+
`Archive too large to open: ${filePath} is ${file.size} bytes (limit ${MAX_ARCHIVE_BYTES}). Extract a subset with a dedicated tool.`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const bytes = await file.bytes();
|
|
319
328
|
const entries = format === "zip" ? await readZipEntries(bytes) : await readTarEntries(bytes);
|
|
320
329
|
return new ArchiveReader(format, entries);
|
|
321
330
|
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -37,8 +37,13 @@ export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
|
37
37
|
const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
38
38
|
const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
|
|
39
39
|
|
|
40
|
-
async function
|
|
40
|
+
export async function saveBashOriginalArtifactForTests(
|
|
41
|
+
session: ToolSession,
|
|
42
|
+
originalText: string,
|
|
43
|
+
): Promise<string | undefined> {
|
|
41
44
|
try {
|
|
45
|
+
const manager = session.getArtifactManager?.();
|
|
46
|
+
if (manager) return await manager.save(originalText, "bash-original");
|
|
42
47
|
const alloc = await session.allocateOutputArtifact?.("bash-original");
|
|
43
48
|
if (!alloc?.path || !alloc.id) return undefined;
|
|
44
49
|
await Bun.write(alloc.path, originalText);
|
|
@@ -375,6 +380,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
375
380
|
env: options.resolvedEnv,
|
|
376
381
|
artifactPath,
|
|
377
382
|
artifactId,
|
|
383
|
+
oneShot: true,
|
|
378
384
|
onChunk: chunk => {
|
|
379
385
|
tailBuffer.append(chunk);
|
|
380
386
|
latestText = tailBuffer.text();
|
|
@@ -387,7 +393,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
387
393
|
// path above.
|
|
388
394
|
manager.appendOutput(jobId, chunk);
|
|
389
395
|
},
|
|
390
|
-
onMinimizedSave: originalText =>
|
|
396
|
+
onMinimizedSave: originalText => saveBashOriginalArtifactForTests(this.session, originalText),
|
|
391
397
|
});
|
|
392
398
|
const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
|
|
393
399
|
requestedTimeoutSec: options.requestedTimeoutSec,
|
|
@@ -675,6 +681,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
675
681
|
env: prepared.resolvedEnv,
|
|
676
682
|
artifactPath,
|
|
677
683
|
artifactId,
|
|
684
|
+
oneShot: true,
|
|
678
685
|
onChunk: chunk => {
|
|
679
686
|
tailBuffer.append(chunk);
|
|
680
687
|
void reportProgress(tailBuffer.text(), {
|
|
@@ -688,7 +695,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
688
695
|
cursorOffset = slice.nextOffset;
|
|
689
696
|
dispatchLines(slice.text);
|
|
690
697
|
},
|
|
691
|
-
onMinimizedSave: originalText =>
|
|
698
|
+
onMinimizedSave: originalText => saveBashOriginalArtifactForTests(this.session, originalText),
|
|
692
699
|
});
|
|
693
700
|
flushTrailingLine();
|
|
694
701
|
this.#buildResultText(result, prepared.timeoutSec, result.output || "(no output)");
|
|
@@ -996,7 +1003,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
996
1003
|
artifactPath,
|
|
997
1004
|
artifactId,
|
|
998
1005
|
onChunk: streamTailUpdates(tailBuffer, onUpdate),
|
|
999
|
-
onMinimizedSave: originalText =>
|
|
1006
|
+
onMinimizedSave: originalText => saveBashOriginalArtifactForTests(this.session, originalText),
|
|
1000
1007
|
});
|
|
1001
1008
|
if (result.cancelled) {
|
|
1002
1009
|
if (signal?.aborted) {
|
|
@@ -26,6 +26,13 @@ export interface BrowserHandle {
|
|
|
26
26
|
|
|
27
27
|
const browsers = new Map<string, BrowserHandle>();
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Upper bound on the CDP `browser.close()` round-trip during a forced (signal-path)
|
|
31
|
+
* teardown before we fall back to killing the Chrome process tree. Only applies when
|
|
32
|
+
* `kill` is set; graceful release still awaits close() unbounded.
|
|
33
|
+
*/
|
|
34
|
+
const HEADLESS_FORCE_CLOSE_GRACE_MS = 1_500;
|
|
35
|
+
|
|
29
36
|
function browserKey(kind: BrowserKind): string {
|
|
30
37
|
switch (kind.kind) {
|
|
31
38
|
case "headless":
|
|
@@ -164,13 +171,22 @@ export async function releaseBrowser(handle: BrowserHandle, opts: { kill: boolea
|
|
|
164
171
|
|
|
165
172
|
async function disposeBrowserHandle(handle: BrowserHandle, opts: { kill: boolean }): Promise<void> {
|
|
166
173
|
if (handle.kind.kind === "headless") {
|
|
174
|
+
// Capture the launched Chrome process before close() so a forced (signal-path)
|
|
175
|
+
// teardown can SIGTERM/SIGKILL the tree even if the CDP close hangs on a wedged
|
|
176
|
+
// renderer. Otherwise the headless Chrome reparents to PID 1 (#698).
|
|
177
|
+
const proc = handle.browser.process();
|
|
167
178
|
if (handle.browser.connected) {
|
|
168
179
|
try {
|
|
169
|
-
|
|
180
|
+
const closing = handle.browser.close();
|
|
181
|
+
// Graceful release waits for close() to finish (it also removes the
|
|
182
|
+
// puppeteer_dev_chrome_profile-* temp dir). Forced release bounds it so
|
|
183
|
+
// the kill fallback below still runs within the signal handler's budget.
|
|
184
|
+
await (opts.kill ? Promise.race([closing, Bun.sleep(HEADLESS_FORCE_CLOSE_GRACE_MS)]) : closing);
|
|
170
185
|
} catch (err) {
|
|
171
186
|
logger.debug("Failed to close headless browser", { error: (err as Error).message });
|
|
172
187
|
}
|
|
173
188
|
}
|
|
189
|
+
if (opts.kill && proc?.pid !== undefined) await gracefulKillTreeOnce(proc.pid);
|
|
174
190
|
return;
|
|
175
191
|
}
|
|
176
192
|
if (handle.kind.kind === "connected") {
|