@gajae-code/coding-agent 0.5.3 → 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 +9 -0
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +12 -0
- package/dist/types/session/streaming-output.d.ts +7 -0
- package/dist/types/web/search/providers/codex.d.ts +4 -4
- package/package.json +7 -7
- package/src/async/job-manager.ts +30 -6
- package/src/config/model-profile-activation.ts +71 -3
- package/src/config/model-profiles.ts +39 -14
- 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/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/ultragoal-runtime.ts +8 -4
- package/src/modes/controllers/input-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/types.ts +1 -0
- package/src/prompts/agents/executor.md +1 -1
- package/src/runtime-mcp/manager.ts +2 -2
- package/src/session/agent-session.ts +103 -3
- package/src/session/streaming-output.ts +41 -0
- package/src/setup/model-onboarding-guidance.ts +10 -3
- package/src/skill-state/active-state.ts +79 -7
- package/src/tools/browser/registry.ts +17 -1
- package/src/tools/cron.ts +2 -6
- package/src/web/search/providers/codex.ts +6 -5
|
@@ -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
|
|
|
@@ -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") {
|
package/src/tools/cron.ts
CHANGED
|
@@ -700,13 +700,9 @@ export class CronDeleteTool implements AgentTool<typeof cronDeleteSchema, CronDe
|
|
|
700
700
|
): Promise<AgentToolResult<CronDeleteToolDetails>> {
|
|
701
701
|
const ownerId = this.session.getAgentId?.() ?? undefined;
|
|
702
702
|
const deleted = deleteRecord(ownerId, params.id);
|
|
703
|
+
const text = deleted ? `Cancelled ${params.id}` : `No scheduled task '${params.id}' found; nothing to cancel.`;
|
|
703
704
|
return {
|
|
704
|
-
content: [
|
|
705
|
-
{
|
|
706
|
-
type: "text",
|
|
707
|
-
text: deleted ? `Cancelled ${params.id}` : `Failed to remove scheduled task '${params.id}'`,
|
|
708
|
-
},
|
|
709
|
-
],
|
|
705
|
+
content: [{ type: "text", text }],
|
|
710
706
|
details: { id: params.id, deleted },
|
|
711
707
|
};
|
|
712
708
|
}
|
|
@@ -19,8 +19,9 @@ import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
|
19
19
|
|
|
20
20
|
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
21
21
|
const CODEX_RESPONSES_PATH = "/codex/responses";
|
|
22
|
-
const FALLBACK_MODEL = "gpt-5.
|
|
22
|
+
const FALLBACK_MODEL = "gpt-5.5";
|
|
23
23
|
const DEFAULT_MODEL_PREFERENCES = [
|
|
24
|
+
"gpt-5.5",
|
|
24
25
|
"gpt-5.4",
|
|
25
26
|
"gpt-5-codex",
|
|
26
27
|
"gpt-5",
|
|
@@ -453,12 +454,12 @@ async function callCodexSearch(
|
|
|
453
454
|
* Executes a web search using OpenAI code provider's built-in web search tool.
|
|
454
455
|
*
|
|
455
456
|
* Default-model behavior:
|
|
456
|
-
* - If `
|
|
457
|
+
* - If `PI_CODEX_WEB_SEARCH_MODEL` is set, use it exactly once and surface any
|
|
457
458
|
* upstream error verbatim.
|
|
458
|
-
* - Otherwise prefer ChatGPT-account-safe bundled defaults (GPT-5.
|
|
459
|
-
*
|
|
459
|
+
* - Otherwise prefer ChatGPT-account-safe bundled defaults (GPT-5.5, GPT-5.4,
|
|
460
|
+
* GPT-5 code backend, …) and retry the next candidate only when OpenAI code backend returns the
|
|
460
461
|
* known 400 "model is not supported" family. This avoids selecting
|
|
461
|
-
* `gpt-5-
|
|
462
|
+
* `gpt-5-codex-mini` first on ChatGPT accounts, which OpenAI rejects.
|
|
462
463
|
*/
|
|
463
464
|
export async function searchCodex(params: SearchParams): Promise<SearchResponse> {
|
|
464
465
|
const auth = await findCodexAuth(params.authStorage, params.sessionId, params.signal);
|