@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.
@@ -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
- const rootEntries = filterRootEntriesForSession(rawActiveEntries(rootState), sessionId);
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
- for (const entry of rawActiveEntries(sessionState)) {
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
- return dedupeVisibleBySkill([...merged.values()], sessionId).filter(entry => entry.active !== false);
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(entry => entry.skill === skill);
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
- await handle.browser.close();
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.4";
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 `PI_OPENAI_CODE_WEB_SEARCH_MODEL` is set, use it exactly once and surface any
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.4, GPT-5
459
- * OpenAI code backend, GPT-5, …) and retry the next candidate only when OpenAI code backend returns the
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-OpenAI code backend-mini` first on ChatGPT accounts, which OpenAI rejects.
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);