Skip to content

perf: cut startup cost across natives, providers, plugins and MCP (W1-W6) - #3846

Open
Yeachan-Heo wants to merge 27 commits into
devfrom
feat/jcode-research
Open

perf: cut startup cost across natives, providers, plugins and MCP (W1-W6)#3846
Yeachan-Heo wants to merge 27 commits into
devfrom
feat/jcode-research

Conversation

@Yeachan-Heo

@Yeachan-Heo Yeachan-Heo commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

Startup and steady-state memory work for the gjc CLI, delivered as plan milestones W1–W6. The theme throughout: stop paying at startup for work an invocation never performs, and give the MCP connection layer a real lifecycle boundary.

Measured on this host with the harness added in this PR, S6 deferred by contract:

Scenario Deciding stable-tree RSS (median)
S1 help ~186 MiB
S2 idle ~178 MiB
S3 session ~94 MiB
S4 ~464 MiB
S5 ~527 MiB
S6 deferred — requires W7/W8 authorization and daemon implementation
S7 ~519 MiB

S1/S2/S7 vary widely run-to-run on a loaded machine; the gate compares against a per-commit canonical baseline rather than treating any single run as authoritative.

What changed

  • SDK session runtimesdk/bus splits into a session runtime (SessionSdkHost) plus Telegram/Discord/Slack adapters behind a LazyService, so hosting a session no longer drags the adapters and their native dependency onto the startup graph.
  • MCP connection pooling — connections live in a pool behind lease-backed facades. Tools, resources, prompts, catalog refreshes and prepared slash/TUI commands all route through the owning lease. Sharing is honored for tools-only stdio and for HTTP/SSE, where one physical session and one callback stream serve every lease with per-lease demultiplexing.
  • Lazy natives — 40 importers moved from a top-level @gajae-code/natives import to an in-function require at the point of use.
  • @gajae-code/ai/core — a subpath that excludes provider construction, with ~124 coding-agent call sites migrated onto it. The root barrel is untouched for external consumers.
  • Plugin registry v2 — one execution authority for plugin tools, subskills and appendices, with digest verification at final use rather than at discovery.
  • Telemetry splitspans: false now performs zero span and attribute work while usage and cost hooks keep firing.
  • Verification harnessverify-module-trace pins which modules a scenario may load; verify-rss-checkpoints measures process-tree RSS against a per-commit baseline with floor enforcement.

Notable correctness fixes

  • computer batch path (fix(computer)) — the legacy batch adapter had drifted from the native path: thrown steps lost failureCode/failureIndex, timeoutMs was ignored, and batch dispatch skipped the pre-dispatch bounds check the single-action path applies. A batch could act on coordinates the single path rejects.
  • Shared-restart races — the pooling work surfaced a chain of them, each fixed with a regression: replayed tools/call after transport failure; peer facades left on a retired entry after restart; a replacement dropped when the receiving manager was reconnecting an unrelated server; and a second rotation landing inside the acquire-to-register window on both admission paths.
  • /notify on|off — the W1b branch dereferenced runtime.session unguarded; fixed to pass through when there is no session. Concurrent /notify on calls also remain single-flight after lazy native loading, rather than creating competing runtimes.
  • Deferred tool availability — discoverable descriptors now preserve the eager factory guards for headless asks, subagent checkpoint/rewind, IRC, GitHub, and disabled cron.
  • Canonicalization gate — the team continuation self-test now actually proves continuation dispatch precedes stale-claim reconciliation.
  • Lazy activation contracts — restored first-use syntax highlighting, clipboard hooks, joinable memory startup, exact pruning artifacts, MCP lease tests, and broker model validation without returning those modules to the startup graph.
  • Daemon and storage authority — advanced Telegram/Discord/Slack serving generations for protected lazy-native lifecycle changes and hardened long-running Linux managed-lock release with exact before/after inode checks.

Design decisions worth review

  • No replay, but recovery still runs. A failed shared tools/call surfaces typed to its calling lease and is never resent — recovery reconnects and rebinds peers, but does not retry the call. Retrying risks duplicating a server-side side effect after an uncertain response.
  • Authorization must declare a binding scope to share. An Authorization-bearing HTTP entry without a non-secret binding kind/scope is refused (MCP_AUTH_BINDING_REQUIRED) rather than sharing a transport across credentials. The secret never enters the pool key.
  • Pool-entry generations. Replacement notifications are one-shot, so a lease acquired just before a rotation would pin a retired entry. Every physical entry carries a monotonic generation that both acquire-to-register paths verify immediately before registration.
  • RSS baseline resolution is canonical-only. Omitting --baseline with --compare resolves only .gjc/rss-checkpoints/<commit>.json and otherwise fails closed. An earlier attempt allowed falling back to the same-commit .last-run.json; because a non-baseline run overwrites that file, the gate flipped between pass and fail on back-to-back identical invocations.

Verification

Final head b750886fd: bun run check:ts PASS · repository Biome PASS · repaired regression matrix 262/262 · daemon control suite 161/161 · portable descriptor/harness gates 38/38 · MCP/plugin/harness suites 98/98 · packages/agent 692/692 in a clean HOME · module traces (help/idle W1c, help W5b, literal tool and skill catalogs) PASS · Telegram/chat generation guard PASS · SDK canonicalization self-test PASS · exact bun scripts/verify-rss-checkpoints.ts --all --compare exit 0.

Not in scope

W7–W9 (daemon, attach, compatibility-default flips) are deliberately absent. workspaceTree.mode stays eager, startup.networkPrewarm stays true, and MCP sharing defaults to per-session, so this PR changes no user-visible defaults. S6 stays deferred until that work is authorized.

Known pre-existing failures

test/session-storage.test.ts (34) and test/session-manager-resume-readonly.test.ts (4) fail identically on a clean worktree at the merge base — established by git worktree add at pristine upstream and diffing failing case names, not just totals. Neither file is touched here.

@Yeachan-Heo
Yeachan-Heo marked this pull request as draft August 5, 2026 07:17

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38a28670d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

export function readBundledContentSync(entry: BundledGjcSkillCatalogEntry): string {
const sourcePath = sourcePathForBundledEntry(entry);
try {
return readFileSync(sourcePath, "utf8");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid fs reads for bundled skill content

When this runs from a compiled Bun executable, sourcePath resolves under /$bunfs/...; literal text imports are available there, but readFileSync/Bun.file against that path returns ENOENT. Because content on the exported bundled default definitions and embedded skills now calls this getter, any compiled-binary caller that still reads definition.content/skill.content instead of the new async loadContent() loses the bundled workflow skill body. Keep the text behind the generated import loaders/cache rather than reading the source path synchronously.

AGENTS.md reference: AGENTS.md:L112-L122

Useful? React with 👍 / 👎.

}
if (isManagedOwnerSupervisorArgv(argv)) {
if (argv.length === 1 && argv[0] === MANAGED_OWNER_SUPERVISOR_ARG) {
const { runManagedOwnerSupervisor } = await import("./gjc-runtime/managed-owner-supervisor");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Install the supervisor SIGTERM guard before lazy import

For the internal --internal-managed-owner-supervisor invocation, this lazy import leaves a startup window before managed-owner-supervisor.ts's module-scope SIGTERM capture is installed. If the pane close flow sends SIGTERM while the CLI is still loading or before this branch imports the module, the supervisor exits via the default handler instead of preserving bootstrapSigtermPending and relaying/observing the exact child termination, so managed-owner recovery loses its trusted terminal evidence. Keep a tiny early handler or load the supervisor guard before normal CLI initialization.

AGENTS.md reference: AGENTS.md:L101-L110

Useful? React with 👍 / 👎.

@Yeachan-Heo
Yeachan-Heo marked this pull request as ready for review August 6, 2026 01:03

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38a28670d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +693 to +694
options.onNotification?.(method, params);
this.emit(entry, { type: "notification", method, params });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Dispatch each pooled notification only once

When a shared MCP entry receives a notification, this calls the first acquirer's onNotification directly and then emits the same event to every lease, including that first lease. Its manager therefore processes tools/list_changed, resources/list_changed, and similar events twice, causing duplicate refresh requests and duplicate consumer callbacks whenever it owns the physical connection; route notifications exclusively through the lease fan-out.

Useful? React with 👍 / 👎.

const plan = planToolOutputPrune(branchEntries, {
...DEFAULT_PRUNE_CONFIG,
minimumSavings: overThreshold ? 0 : DEFAULT_PRUNE_CONFIG.minimumSavings,
});
const artifactManager = this.sessionManager.getArtifactManager();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Establish artifacts before pruning ephemeral sessions

For a non-persistent/SDK session with no session file and no previously saved artifact, getArtifactManager() returns null; consequently nothing is added to published, the committed plan is empty, and stale tool outputs are never pruned. Long ephemeral sessions therefore fall through to compaction or context overflow even though ensureArtifactManager() can create the supported temporary artifact store; establish that store before publishing prune candidates.

Useful? React with 👍 / 👎.

Comment on lines +172 to +174
this.#connectionDisposer?.();
this.#capabilitiesDisposer?.();
this.#malformedDisposer?.();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reinstall SDK transport handlers after restart

When the same SessionSdkSessionRuntime is stopped and then started again, these one-time disposers permanently remove the connection-close, capability-negotiation, and malformed-frame handlers, while start() never registers replacements. The restarted runtime consequently retains reverse leases after disconnects, ignores negotiated capabilities, and stops returning structured malformed-frame errors; either make the runtime explicitly terminal after stop or recreate these subscriptions on every start.

Useful? React with 👍 / 👎.

@Yeachan-Heo
Yeachan-Heo force-pushed the feat/jcode-research branch from 38a2867 to 2f2e32c Compare August 6, 2026 08:32

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f2e32c536

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +370 to +373
if (
!/\.(?:optional|nullable)\s*\(\s*\)\s*$/.test(field[2]!) &&
!/\.Optional\s*\(\s*\)\s*$/.test(field[2]!) &&
!/(?:Type\.)?Optional\s*\(/.test(field[2]!)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep nullable plugin parameters required

For plugin tools whose schema is inferred from source, a declaration such as z.string().nullable() is classified as optional here. Nullable fields may contain null, but their presence is still required, so the published schema permits the model to omit a value that the plugin's runtime validator rejects; the static expression parser also reduces this chained expression to the underlying string schema and therefore rejects the valid null value. Treat only optional wrappers as removing a property from required, while preserving nullability in the property schema.

Useful? React with 👍 / 👎.

Comment on lines +8372 to +8373
const runtime = await this.#workspaceTreeService.get("first-turn-barrier");
this.#cachedWorkspaceTree = firstWorkspaceTree ? runtime.snapshot : await runtime.refresh();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve best-effort workspace scanning in lazy mode

With workspaceTree.mode="lazy", any native scan error or filesystem failure from the service now escapes this first-turn barrier and rejects prompt() instead of omitting the workspace tree as the fallback branch and the previous implementation do. Because LazyService retains the failed state, subsequent prompts keep rejecting as well, making an optional context scan permanently disable the session; catch scan/service failures here and continue with an undefined tree.

Useful? React with 👍 / 👎.

Comment on lines +132 to +134
const endpoint = await this.transport.start();
this.#transportStarted = true;
return endpoint;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent transport startup from winning after shutdown

If stop() runs while transport.start() is pending, the transport shutdown path waits for that start and then closes the newly created server, but this continuation still sets #transportStarted to true and returns its endpoint. The start caller can therefore receive a URL whose endpoint file/server has already been removed, while the runtime incorrectly reports the transport as started; coordinate start and stop with a generation or shutdown flag so an in-flight start cannot publish success after shutdown begins.

Useful? React with 👍 / 👎.

@Yeachan-Heo
Yeachan-Heo force-pushed the feat/jcode-research branch from 2f2e32c to ad2110b Compare August 6, 2026 09:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ad2110b2fb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

export const localBackend: MemoryBackend = {
id: "local",
async start(options) {
return (await import("./local-backend")).localBackend.start(options);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace inline backend imports with approved lazy loaders

Replace this await import() path, along with the other method-level imports in these backend facades, with the repository's approved top-level/static loading pattern. The repository contract explicitly prohibits inline imports, and these calls make the memory backend's dependency and compiled-binary loading behavior depend on runtime method execution rather than the statically inspectable module graph.

AGENTS.md reference: AGENTS.md:L103-L106

Useful? React with 👍 / 👎.

Comment on lines +11732 to +11735
const plan = planToolOutputPrune(branchEntries, {
...DEFAULT_PRUNE_CONFIG,
minimumSavings: overThreshold ? 0 : DEFAULT_PRUNE_CONFIG.minimumSavings,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Account for artifact references when admitting a prune

When below-threshold pruning is near minimumSavings, this plan is built without artifactRefMaxChars, so admission assumes the replacement has no artifact URI; the method then appends the published URI and can commit even after actual savings fall below the configured minimum, because the optional commit gate checks only cache-reset cost. This can trigger a history rewrite and provider-cache reset for a prune that the documented minimum-savings gate should have rejected; build the committed plan with the same artifact-reference budget used by the preflight estimate or recheck the final savings against the minimum.

Useful? React with 👍 / 👎.

@Yeachan-Heo
Yeachan-Heo force-pushed the feat/jcode-research branch from ad2110b to 5933677 Compare August 6, 2026 11:31

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5933677db4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1061 to +1063
active = { runtime, revisions, cursors, reconciliation, pending, disposeGate };
try {
await runtime.start();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register SDK-only sessions with the broker

When notifications are disabled—the default for ordinary top-level sessions—createAgentSession selects this SDK-only extension, but startup ends after runtime.start() and never calls the available runtime.registerWithBroker(...) path. Unlike the previous notification-host path, no host_registered record reaches SessionIndex, so broker-backed session.list, SDK attach, and relay commands cannot discover these otherwise-live endpoints.

Useful? React with 👍 / 👎.

Comment on lines +199 to +202
await filesystem.writeFile(
endpointFile,
JSON.stringify({ version: 1, url, token: input.token, pid: process.pid }),
"utf8",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include the session identity in SDK endpoint records

For sessions using the new WebSocket transport, this endpoint record omits sessionId. Broker endpoint resolution explicitly rejects records unless endpoint.sessionId === record.sessionId (sdk/broker/broker.ts:784-791), and lifecycle readiness applies the same check, so even after the SDK-only host is indexed, broker attach/readiness treats its endpoint as stale. Publish input.sessionId in the discovery JSON, matching the existing endpoint contract.

Useful? React with 👍 / 👎.

Comment on lines +2462 to +2465
const bindings = nativeThemeBindings;
loadNativeThemeBindings();
const validLang = bindings && lang && bindings.supportsLanguage(lang) ? lang : undefined;
if (!bindings) return code.split("\n").map(line => theme.fg("mdCodeBlock", line));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load syntax bindings before one-shot rendering

On non-macOS hosts and in non-interactive/print mode, theme initialization does not preload the native bindings. The first code block therefore captures undefined, starts an asynchronous load, and immediately returns unhighlighted text; print mode has no subsequent render, so syntax highlighting remains absent despite syntaxHighlighting.enabled defaulting to true. Await the binding load before one-shot rendering or trigger a rerender when it completes.

Useful? React with 👍 / 👎.

@yazzang-homelab

Copy link
Copy Markdown
Contributor

경고 — 이 PR의 현재 head가 CHANGELOG 전체를 삭제한다

머지하면 안 된다. 확인된 사실:

$ git cat-file -s <이 PR head>:<해당 CHANGELOG 경로>
1

1바이트 — 개행 하나만 남았다. dev의 같은 파일은 312,259 bytes(coding-agent) / 244,785 bytes(ai) / 45,275 bytes(agent)다. 릴리스 이력 전체가 사라진 상태다.

원인은 내 쪽이다

#3932(11:25:32Z 머지)가 .gitattributes에서 packages/*/CHANGELOG.md merge=union을 제거했다. 제거 자체는 근거가 있었다 — union은 충돌을 내지 않고 양쪽을 이어붙여서 이미 릴리스된 섹션에 항목을 조용히 밀어넣고 있었다(#3929, 실측 35건).

그런데 그 결과 리베이스에서 CHANGELOG가 처음으로 진짜 충돌을 내기 시작했고, 그 충돌을 해소하는 과정에서 파일이 비워졌다. 시간대가 명확하다:

시각 (UTC) 사건
11:25:32 #3932 머지 (union 제거)
11:29:29 ~ 11:35:02 #3920 #3697 #3870 #3908 #3887 #3864 #3729 #3869 #3866 #3873작성자 6명, 10개 PR이 전부 1바이트 CHANGELOG로 갱신됨

전환 비용을 예고하지 못한 건 내 잘못이다. 미안하다.

복구

git fetch origin
git checkout origin/dev -- packages/coding-agent/CHANGELOG.md   # 해당 패키지 경로로
# 그 다음 ## [Unreleased] 아래에 이 PR의 항목만 다시 추가
git add packages/coding-agent/CHANGELOG.md
git commit --amend --no-edit    # 또는 새 커밋

앞으로 리베이스에서 CHANGELOG 충돌이 나면 양쪽 항목을 모두 ## [Unreleased] 아래에 남기는 것이 올바른 해소다. 이미 릴리스된 ## [X.Y.Z] 섹션은 손대지 않는다. CONTRIBUTING.md의 "Rebasing onto dev" 절에 적어두었다.

푸시 전에 다음으로 자가 점검할 수 있다:

git cat-file -s HEAD:packages/coding-agent/CHANGELOG.md   # 30만 바이트 근처여야 정상

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3b5de5cbe7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +86 to +87
if (range !== null) {
content = await file.slice(start, boundedEnd).text();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cap explicit artifact ranges before reading

For an artifact larger than 16 MiB, an explicit open-ended or wide query such as ?range=0- enters this branch and materializes the entire file, completely bypassing the ceiling applied to bare reads. Large spilled artifacts can therefore allocate hundreds of MiB or more and crash the session; clamp every requested range to a bounded maximum before calling text().

Useful? React with 👍 / 👎.

Comment on lines +76 to +77
const stopResult = current.stop(true);
await Promise.race([stopResult, new Promise<void>(resolve => setTimeout(resolve, 250))]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel the SDK shutdown timeout after a fast stop

When current.stop(true) resolves before 250 ms, Promise.race drops the timeout promise but does not cancel its timer. Bun keeps that timer alive, so an otherwise-complete SDK shutdown can hold process exit open for the remainder of the 250 ms window; use a cancellable timeout and clear it when the stop settles.

AGENTS.md reference: AGENTS.md:L108-L109

Useful? React with 👍 / 👎.

Comment on lines +717 to +719
void this.#ensureHistoryStorage().then(storage => {
if (storage && this.#history.length > 0) this.#navigateHistory(direction);
this.invalidate();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Discard stale asynchronous history navigation

When history has not loaded yet, pressing Up starts this asynchronous load; if the user types or otherwise changes the draft before it resolves, the continuation still calls #navigateHistory(direction) and replaces the newer draft with an old prompt. Capture the editor/document state at the initial keypress and only navigate if it is still unchanged when loading completes.

Useful? React with 👍 / 👎.

@Yeachan-Heo
Yeachan-Heo force-pushed the feat/jcode-research branch from 3b5de5c to 9549e2f Compare August 6, 2026 13:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9549e2fe72

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
return maxDepth < 0 || (session.taskDepth ?? 0) < maxDepth;
}
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude unavailable Telegram tools from discovery

When tools.discoveryMode uses its default all value and Telegram notifications are disabled, telegram_send falls through to this unconditional true, so createTools() publishes a deferred LazyAgentTool without running TelegramSendTool.createIf(). The model can then discover and activate the tool, but its first execution loads a null implementation and fails with the generic “failed to load” error. Mirror the provider-enabled predicate from tools/telegram-send.ts in descriptor availability so the discovery catalog does not advertise an unavailable capability.

Useful? React with 👍 / 👎.

Yeachan-Heo pushed a commit that referenced this pull request Aug 6, 2026
PR #3846 altered protected Telegram lifecycle symbols (defaultProcessReference,
exactUnlinkNotificationFile, process-incarnation authority). The daemon
generation guard requires a strict generation bump for these changes.

Generation 54 covers the startup-cost refactoring that moved these symbols
off the eager import path.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 099918b3cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"initTheme:final",
deps.initTheme ?? initTheme,
isInteractive,
isInteractive && settingsInstance.get("theme.watchFiles"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Decouple theme file watching from appearance detection

When an interactive user sets theme.watchFiles=false, this passes false as initTheme's general enableWatcher argument. That argument also gates startSigwinchListener() and the macOS appearance observer in theme.ts, so disabling custom-theme file reloads unexpectedly disables automatic dark/light theme detection and subsequent terminal appearance updates. Keep appearance monitoring enabled for interactive sessions and gate only the file watcher with this setting.

Useful? React with 👍 / 👎.

Yeachan-Heo added 11 commits August 6, 2026 22:40
The RSS harness writes per-commit baselines and per-run reports under
.gjc/rss-checkpoints/. These are host-specific measurements (hundreds of
KB of sampled process-tree RSS) that differ on every machine and every
run, so they are runtime state rather than source.

Lore-id: a7c31e08
Constraint: the harness must keep resolving .gjc/rss-checkpoints/<commit>.json -- ignore the directory, do not relocate it
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: git check-ignore resolves the canonical baseline and the last-run report
Not-tested: CI runners that may expect a committed baseline
…ation adapters

Hosting an SDK session pulled the Telegram/Discord/Slack adapters, and
through them a native dependency, onto the startup module graph even when
notifications were inactive. Splitting the bus into a session runtime
(control/query/replay via SessionSdkHost) plus adapters behind a
LazyService keeps the adapters off the graph until an eligibility check
actually activates one.

Lore-id: 4f19b2ac
Constraint: SDK control/query/replay contract must stay byte-identical -- surface parity is manifest-pinned
Constraint: adapters must still activate under every currently-activating condition with identical event ordering
Rejected: leaving adapters statically imported and gating only at call time | the native import already happened by then
Rejected: package-name dynamic import for the adapters | crashes bun --compile; only statically-traceable in-function require() survives
Confidence: high
Scope-risk: wide
Reversibility: reversible
Directive: keep adapter access behind LazyService; a bare top-level import silently re-adds the native to startup
Tested: module-trace deny of the adapter modules and their native on an idle session; surface parity manifest; hostile-error paths
Not-tested: live delivery against real Telegram/Discord/Slack endpoints
…across facades

Every session opened its own child per MCP server, and the manager talked
to the physical connection directly, so lifecycle, retry, restart and
credential scope had no boundary to live on. This introduces a connection
pool with lease-backed facades: tools, resources, prompts, catalog
refreshes and prepared slash/TUI commands all route through the owning
lease and fail their live-state check once it is released.

Sharing is honored for tools-only stdio and for HTTP/SSE, where one
physical MCP session and one callback stream serve every lease with
per-lease demultiplexing. Endpoint identity follows C5 exactly: raw path
and query are preserved (trailing slash, reserved and unreserved percent
encoding, parameter order, duplicate keys, empty vs absent), only
universally equivalent forms collapse, and an Authorization-bearing entry
must declare a non-secret binding kind and scope before it may share.

Restart is owned by the pool: exactly one facade reconnects, every
surviving lease is rebound, and each physical entry carries a monotonic
generation that both acquire-to-register paths verify immediately before
registration so a retired entry can never be published.

Lore-id: 91d4e7b3
Constraint: never replay an in-flight tools/call -- recovery may reconnect, but the failed call must surface typed to its calling lease
Constraint: exactly one restart owner per shared entry; peers rebind rather than racing a second reconnect
Constraint: never hash an Authorization value into the pool key
Rejected: retrying the shared call after recovery | duplicates server-side side effects on an uncertain response
Rejected: best-effort replacement events without a durable generation | a lease acquired just before a rotation has no listener and pins a retired entry
Rejected: sharing unbound Authorization headers | second facade silently reuses the first transport's credential across tenants
Confidence: high
Scope-risk: wide
Reversibility: reversible
Directive: any future acquire-to-register path must re-check pool.isCurrentLease() immediately before registration
Tested: two facades in one process over one child; release/idle reap; -32601 sampling and elicitation confinement; crash isolation; roots union add/remove on the wire; endpoint partition and equivalence; second rotation inside the acquire-to-register window on both paths
Not-tested: two top-level AgentSessions sharing a pool -- runtime-scope isolation is a later milestone and stays unauthorized
Forty modules imported @gajae-code/natives at the top level, so the addon
was resolved and mapped during startup even for `gjc --help`. Each call
site now takes the binding through an in-function require at the point of
use, which keeps the addon off the startup graph without changing any
behavior once a native call actually happens.

Lore-id: 6b820fd1
Constraint: the require specifier must be a literal string in the function body -- createRequire and package-name import() both crash bun --compile
Rejected: a single lazy wrapper module re-exported everywhere | the wrapper itself becomes a static edge and the addon loads again
Confidence: high
Scope-risk: wide
Reversibility: easy
Directive: never add a top-level `import ... from "@gajae-code/natives"`; the help/idle trace gate denies it
Tested: module-trace help scenario with @gajae-code/natives denied; compiled binary --version and --smoke-test
Not-tested: non-darwin-arm64 native variants
The package root barrel pulled every provider module, so importing a
single type dragged the whole provider set onto the graph. A /core
subpath exposes the runtime-neutral surface (descriptors, auth storage,
shared types) while providers stay reachable only through their own
subpaths and the root barrel, which external consumers still depend on.

Lore-id: 2e5aa93c
Constraint: keep the root barrel intact -- external consumers import from it and must not break
Constraint: subpath requires must work under bun --compile, which is why ./* and ./providers/* carry require conditions
Rejected: pruning the root barrel to force everyone onto /core | silently breaks published consumers
Confidence: high
Scope-risk: moderate
Reversibility: reversible
Tested: packages/ai suite env-scrubbed; dual-package identity checked absent; providers denied on the help and idle traces
Not-tested: downstream repos consuming the published tarball
…kage root

Consumers inside coding-agent imported from @gajae-code/ai, which meant
every one of them re-attached the provider set to the module graph. These
call sites now use @gajae-code/ai/core, leaving provider construction to
the paths that genuinely build a provider.

Lore-id: 8c40df67
Constraint: packages/coding-agent/src/** must not import bare @gajae-code/ai -- enforced by a biome noRestrictedImports override and an import-gate test
Confidence: high
Scope-risk: wide
Reversibility: easy
Directive: new coding-agent code imports @gajae-code/ai/core; the gate test fails the build otherwise
Tested: ai-core-import-gate test; help and idle module traces with packages/ai/src/providers/** denied
Not-tested: nothing material -- this is a mechanical specifier change
AgentTelemetryConfig conflated span emission with usage and cost
accounting, so a spans-disabled run still resolved span attributes on
every step. Splitting them means resolveAttributes is never called and
attributes stay undefined under spans:false, while usage and cost hooks
keep firing and the run collector still reports real summaries with
exactly-once onRunEnd across success, failure and tool-bearing runs.

Lore-id: d5e37a10
Constraint: usage and cost hooks must keep firing with spans off -- billing depends on them
Constraint: onRunEnd fires exactly once per run, span or no span
Rejected: keeping one flag and skipping only the exporter | attribute resolution was the actual cost
Confidence: high
Scope-risk: moderate
Reversibility: easy
Tested: OTEL suite asserting attributes undefined under spans:false, plus exactly-once success/error/tool coverage; packages/agent suite
Not-tested: a live OTLP collector
…thority

Plugin tools, subskills and prompt appendices were resolved from more
than one place, so a path validated by the registry could be replaced
before it was used. Registry v2 becomes the sole authority: it checks
identity, scope and digest, confines paths by realpath under the plugin
root, honors disabled and quarantined state, and rejects path-only
persisted records. Legacy roots migrate only inside the locked registry
transaction, and auto-migration never executes plugin code.

Every final use is now verified at the moment of use. Tool and subskill
imports re-check their exact reference immediately before importing,
injection carries the verified bytes rather than re-reading the file, and
prompt appendices hash their body -- file-backed and inline alike --
before it can reach the prompt, failing closed with a typed
runtime_mismatch on drift.

Lore-id: 7a1c9e54
Constraint: one runtime path per plugin entry -- a failed v2 load must never fall back to v1
Constraint: the legacy loader must be unreachable from the published package, not merely absent from the barrel
Rejected: validating once at registry load and trusting the path later | leaves a mutation window before every import and render
Rejected: dropping the loader from the barrel only | the ./extensibility/* export wildcard still resolved its subpath
Confidence: high
Scope-risk: wide
Reversibility: migration-needed
Directive: any new plugin-supplied content must be digest-verified at final use, not at discovery
Tested: mutation-between-validation-and-use races for tools, subskills, system and agent appendices; forged delimiter and tag injection; packed-consumer resolution failure for both legacy loader spellings; preview/apply classification matrix
Not-tested: third-party plugins in the wild that depend on v1 record shapes
…alidation

The legacy batch adapter had drifted from the native path: thrown steps
lost their failureCode and failureIndex, timeoutMs was ignored so a
non-terminating action ran unbounded, and batch dispatch reached the
native seam without the pre-dispatch bounds check that the single-action
path applies. A batch could therefore act on coordinates the single path
would reject.

Lore-id: 3fb6c082
Constraint: batch and single dispatch must agree on failure shape, deadline and coordinate validation
Rejected: treating the divergence as legacy-path tolerance | it silently widened what a batch may do
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: batch step failure mapping, deadline cancellation, and COMPUTER_COORD_INVALID on the batch path; the seven-case enforcement suite
Not-tested: real display hardware -- the controller is driven through its test seam
The tool index imported every implementation to expose descriptors, so
listing tools pulled their whole dependency set. Descriptors now live
apart from implementations behind a generated catalog, letting discovery
read metadata without constructing anything.

Lore-id: 5d92ae3b
Constraint: the generated catalog must stay in sync with its generator -- the literal-catalog trace gate enforces it
Confidence: high
Scope-risk: moderate
Reversibility: easy
Directive: change the generator, then regenerate; do not hand-edit tool-catalog.generated.ts
Tested: literal catalog gates for tools and skills; tool-discovery initial-tools suite
Not-tested: nothing material beyond the catalog gates
Session construction eagerly built artifact and history storage even for
runs that never touched either. Both are now created on first use, which
keeps their dependencies off the startup path.

Lore-id: c6e14b09
Constraint: storage identity must stay stable across the deferral -- a session that later writes must land in the same place it would have
Confidence: high
Scope-risk: moderate
Reversibility: easy
Tested: agent-session suites covering concurrency, handoff, mid-run maintenance, detached bash, todos and replay
Not-tested: resume from sessions written by older builds
Yeachan-Heo added 16 commits August 6, 2026 22:40
…p path

Follow-through for the laziness milestones across the surfaces the earlier
commits exposed: MCP capability and CLI entry points, skill discovery and
defaults, eval executors, and the settings and schema surfaces that
describe them. Each moves construction behind first use or narrows an
import so the startup graph stops paying for work a given invocation never
performs.

Lore-id: 0b7fd253
Constraint: compatibility defaults must not shift -- workspaceTree.mode stays eager and startup.networkPrewarm stays true in this change
Rejected: flipping the defaults here to bank the win | that is a separate authorized milestone and would change behavior without review
Confidence: medium
Scope-risk: wide
Reversibility: reversible
Tested: coding-agent source suites; help full-deny, help+idle bun:sqlite and provider deny, and literal catalog trace gates; check:schemas
Not-tested: every downstream consumer of the touched settings shapes
…ubpath

Two boundaries needed manifest and lint enforcement rather than
convention. Biome now rejects bare @gajae-code/ai imports inside
packages/coding-agent/src, and the coding-agent export map null-blocks the
legacy GJC plugin loader subpath -- both spellings, ordered before the
recursive ./extensibility/* wildcard that would otherwise resolve it.
packages/ai carries require conditions on ./* and ./providers/* so subpath
requires resolve under bun --compile.

Lore-id: e94a5b71
Constraint: null export entries must precede the wildcard; after it they are dead
Constraint: the root barrel stays intact for external consumers
Rejected: relying on review to catch bare @gajae-code/ai imports | the graph regression is invisible without a gate
Confidence: high
Scope-risk: moderate
Reversibility: easy
Tested: child-process package-subpath resolution failure for both loader spellings; packed SDK smoke at root 381 / sdk 39; restricted-import lint
Not-tested: consumers already importing the legacy loader subpath -- they break by design
The laziness work needed measurable gates rather than reviewer judgement.
verify-module-trace asserts which modules a scenario may load and pins the
literal tool and skill catalogs; verify-rss-checkpoints measures
whole-process-tree RSS at an explicit barrier and compares against a
per-commit baseline with floor enforcement and schema-bound re-scope
records.

Omitting --baseline with --compare resolves only the canonical
.gjc/rss-checkpoints/<commit>.json and otherwise fails closed with a typed
BaselineDefaultMissing naming that path. S6 is emitted as deferred with
its authorization reason instead of being measured or fabricated.

Lore-id: b30c8fd4
Constraint: never fall back to <commit>.last-run.json -- a non-baseline run overwrites it, so comparing against it makes the gate oscillate between pass and fail on identical invocations
Constraint: no implicit --allow-baseline-drift; drift stays an explicit opt-in
Rejected: same-commit last-run fallback | observed flipping exit 0 and FAIL across back-to-back identical runs
Rejected: fabricating an S6 measurement to fill the table | the scenario needs a daemon that does not exist yet
Confidence: high
Scope-risk: moderate
Reversibility: easy
Directive: stable-tree RSS at the barrier is the deciding metric; sampled peak stays diagnostic
Tested: harness-gates covering canonical resolution, refusal of same-commit and foreign last-run fallbacks, malformed baselines, deferred S6 shape, and explicit --baseline precedence
Not-tested: CI hardware -- S1 and S7 vary widely under load on this host
The session-runtime extraction also flipped the broker's no-cleanup
session.delete fallback from an idempotent ok to invalid_input, which
breaks the manifest-pinned adapter disposition contract (AD-*-G07) for
every machine adapter. Deleting an unknown session stays a successful
no-op.

Lore-id: 7c41a9e2
Confidence: high
Scope-risk: narrow
Reversibility: reversible
Tested: bun test packages/coding-agent/test/sdk-adapter-dispositions.test.ts (576 pass)
src/tools/tool-catalog.test.ts imported ../../scripts/generate-tool-catalog,
so the publish type check emitted a stray scripts/generate-tool-catalog.d.ts
into the package and the next `biome check .` failed on it. The test belongs
with the other tool tests, where importing scripts/ is already normal.

Lore-id: b83d5510
Confidence: high
Scope-risk: narrow
Reversibility: reversible
Tested: bun test packages/coding-agent/test/tools/tool-catalog.test.ts (3 pass)
chat-daemon-control.ts moved its native process and unlink access behind
lazy bindings, so the semantic declaration digests the guard pins had to be
regenerated with --write-manifest.

Lore-id: 5a2f77c1
Confidence: high
Scope-risk: narrow
Reversibility: reversible
Tested: bun scripts/telegram-daemon-generation-guard.ts --validate-current-tree
`biome check .` was clean on dev and reported 225 errors on this branch:
unformatted files, unsorted imports, and refactor residue — dead
`import type * as native` aliases, an unused DiffQueryError copy in
sdk/bus, an orphaned #ensureDir, unused native type aliases, and a
cancelPendingEntry parameter no caller needs. findParametersExpression
kept a while-assign loop that could only ever run once, and
safeIsInstanceOf shadowed the global `constructor`.

Lore-id: 9d2c04b7
Confidence: high
Scope-risk: wide
Reversibility: reversible
Tested: biome check . (clean)
Not-tested: no behavior change intended beyond the dead-code removals
Deferring @gajae-code/natives behind an async accessor added a microtask
yield inside startSession before it registers in sessionStartPromises, so
two concurrent `/notify on` calls each built a runtime and the loser threw
"Lifecycle SDK startup was cancelled". require() is synchronous, so the
accessor does not need to be async to stay lazy.

Lore-id: 3e6fb18d
Constraint: the native must stay off the startup module graph -- keep the in-function require
Confidence: high
Scope-risk: narrow
Reversibility: reversible
Tested: bun test packages/coding-agent/test/sdk-host-wiring.test.ts (78 pass)
Deferring discoverable tools advertises them from the descriptor alone, so
every factory guard that used to drop a tool at creation had to move into
availableFor. Without it a headless session advertised `ask` and only
failed at call time; the same held for `checkpoint`/`rewind` in subagents,
`irc` without an agent registry, `github` without the gh CLI, and `cron`
under CLAUDE_CODE_DISABLE_CRON.

fetch's html-to-markdown accessor cached the bound export rather than the
module, freezing the first-seen implementation for the process.

Lore-id: 1f7ad64c
Constraint: availability must stay cheap -- no heavy imports on the descriptor path
Rejected: eager materialization for conditional tools | reloads exactly what the deferral removed
Confidence: high
Scope-risk: medium
Reversibility: reversible
Tested: bun test packages/coding-agent/test/tools packages/coding-agent/src/tools (1719 pass)
Not-tested: telegram_send availability still resolves at load; its guard needs the notification snapshot
The self-test expected moving continueStalledGjcTeamWorkers after stale-claim
reconciliation to fail, but exactTeamRuntimeSendKeysRanges validated only the
continuation function and ignored its monitor call site. Pin one continuation
call before one reconciliation call inside monitorGjcTeam, and keep the direct
Bun.spawnSync adversarial fixture syntactically valid.

Lore-id: c61a9df4
Confidence: high
Scope-risk: narrow
Reversibility: reversible
Tested: bun packages/coding-agent/scripts/verify-gjc-sdk-canonicalization.ts --self-test
The PR changes public package surfaces and runtime behavior across the AI,
agent, coding-agent, TUI, and utils workspaces. Record the core entrypoint,
telemetry fast path, lazy native loading, MCP/plugin/runtime changes, and the
correctness fixes under each package's Unreleased section.

Lore-id: 6f7430ad
Confidence: high
Scope-risk: narrow
Reversibility: reversible
Tested: git diff --check
The RSS harness test read three ignored `.gjc/rss-checkpoints` files that
existed only in the author's worktree, so CI could never run M6. Commit the
minimal immutable W1c identity evidence under scripts/fixtures and read it
there instead.

The descriptor availability matrix also assumed a Darwin-arm64 computer
backend and an unset cron-disable variable. Derive those expected exclusions
from the same platform and environment predicates as the descriptor.

Lore-id: e3f8256a
Confidence: high
Scope-risk: narrow
Reversibility: reversible
Tested: CLAUDE_CODE_DISABLE_CRON=1 bun test packages/coding-agent/src/tools/descriptors.test.ts
Tested: bun test scripts/harness-gates.test.ts
Deferred modules changed first-use timing and captured several exports too early,
breaking syntax highlighting, clipboard spies, memory startup joins, pruning, MCP
leases, broker validation, and test-only inspection of lazy tools. Keep startup
graphs lazy while restoring the observable contracts at feature use.

Lore-id: 7c51fd9a
Confidence: high
Scope-risk: wide
Reversibility: reversible
Tested: focused interactive, SDK, broker, pruning, memory, and workflow-gate suites (262 pass)
Tested: W1c and W5b module traces
Tested: bun run check:ts
Cached native bindings bypassed live identity adapters, while long-lived lock
descriptors could retain stale bytes or report a transient Linux release mismatch.
Resolve bindings at feature use, make descriptor writes complete and truncating,
and allow only an independently verified exact-identity release fallback.

Lore-id: 98bf5a24
Confidence: medium
Scope-risk: wide
Reversibility: reversible
Tested: SDK broker lifecycle suites (75 pass)
Not-tested: Linux 9,999-artifact migration locally; CI runner is authoritative
Lazy exact-unlink and process-incarnation bindings change protected lifecycle
authority, so older resident daemons must not continue serving as current. Advance
the generation and refresh the guarded manifest and assertions.

Lore-id: 6a7d3e11
Confidence: high
Scope-risk: narrow
Reversibility: reversible
Tested: Telegram generation guard current-tree validation
Tested: bun run check:ts
Discord and Slack share the exact unlink and process-incarnation lifecycle symbols
moved behind lazy native bindings in this branch. Advance both serving generations
and refresh the protected manifest so resident older daemons are replaced.

Lore-id: 31cb749e
Confidence: high
Scope-risk: narrow
Reversibility: reversible
Tested: bun test packages/coding-agent/test/daemon-control.test.ts
Tested: bun scripts/telegram-daemon-generation-guard.ts --validate-current-tree
@Yeachan-Heo
Yeachan-Heo force-pushed the feat/jcode-research branch from 099918b to b750886 Compare August 6, 2026 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants