@gajae-code/coding-agent 0.5.0 → 0.5.2
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 +36 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +29 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +7 -0
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +72 -2
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +39 -3
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +27 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +5 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli/setup-cli.ts +138 -3
- package/src/cli.ts +1 -0
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +7 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +193 -0
- package/src/config/file-lock.ts +66 -10
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +39 -30
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +459 -3
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +457 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +232 -19
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +48 -30
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +179 -2
- package/src/gjc-runtime/tmux-common.ts +14 -0
- package/src/gjc-runtime/tmux-gc.ts +177 -0
- package/src/gjc-runtime/tmux-sessions.ts +49 -1
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1239 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +14 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/harness-control-plane/storage.ts +70 -0
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/docs-index.generated.ts +22 -12
- package/src/lsp/defaults.json +1 -0
- package/src/main.ts +18 -3
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +2 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +51 -8
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +81 -1
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +266 -34
- package/src/modes/shared/agent-wire/command-dispatch.ts +281 -261
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +32 -2
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/prompts/agents/executor.md +5 -2
- package/src/sdk.ts +29 -4
- package/src/session/agent-session.ts +99 -19
- package/src/session/blob-store.ts +59 -3
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +72 -20
- package/src/setup/credential-import.ts +429 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +16 -1
- package/src/task/render.ts +18 -7
- package/src/tools/ask.ts +59 -2
- package/src/tools/cron.ts +1 -1
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/subagent-render.ts +128 -29
- package/src/tools/subagent.ts +173 -9
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* - Events: AgentSessionEvent objects streamed as they occur
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from "node:fs/promises";
|
|
13
15
|
import * as path from "node:path";
|
|
14
|
-
import { $
|
|
16
|
+
import { $pickenv, logger, readLines, Snowflake } from "@gajae-code/utils";
|
|
15
17
|
import type {
|
|
16
18
|
ExtensionUIContext,
|
|
17
19
|
ExtensionUIDialogOptions,
|
|
@@ -23,8 +25,9 @@ import { initializeExtensions } from "../runtime-init";
|
|
|
23
25
|
import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
|
|
24
26
|
import { AgentWireFrameSequencer, toAgentWireEventFrame } from "../shared/agent-wire/event-envelope";
|
|
25
27
|
import { rpcError as error } from "../shared/agent-wire/responses";
|
|
28
|
+
import { registerRpcSession, unregisterRpcSession } from "../shared/agent-wire/session-registry";
|
|
26
29
|
import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
|
|
27
|
-
import { UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
|
|
30
|
+
import { modelSupportsTokenCostMetrics, UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
|
|
28
31
|
import { FileGateStore } from "../shared/agent-wire/workflow-gate-broker";
|
|
29
32
|
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
30
33
|
import { isRpcHostUriResult, RpcHostUriBridge } from "./host-uris";
|
|
@@ -70,13 +73,91 @@ function parseValueDialogResponse(
|
|
|
70
73
|
return undefined;
|
|
71
74
|
}
|
|
72
75
|
|
|
73
|
-
function
|
|
74
|
-
const raw = $
|
|
76
|
+
export function shouldEmitRpcTitlesForTest(): boolean {
|
|
77
|
+
const raw = $pickenv("GJC_RPC_EMIT_TITLE", "PI_RPC_EMIT_TITLE");
|
|
75
78
|
if (!raw) return false;
|
|
76
79
|
const normalized = raw.trim().toLowerCase();
|
|
77
80
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
const shouldEmitRpcTitles = shouldEmitRpcTitlesForTest;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Cancellation commands bypass the ordered serial chain because they must
|
|
87
|
+
* interrupt in-flight work — they cannot wait behind the very command they are
|
|
88
|
+
* meant to abort.
|
|
89
|
+
*/
|
|
90
|
+
export const RPC_CANCELLATION_COMMANDS: ReadonlySet<RpcCommand["type"]> = new Set<RpcCommand["type"]>([
|
|
91
|
+
"abort",
|
|
92
|
+
"abort_bash",
|
|
93
|
+
"abort_retry",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Safe read-only commands that bypass the ordered serial chain so they never
|
|
98
|
+
* head-of-line-block behind a long-running ordered command like
|
|
99
|
+
* `bash`/`compact`/`handoff`/`login` (#606, issue 13 — the partial fix only
|
|
100
|
+
* fast-laned cancellation).
|
|
101
|
+
*
|
|
102
|
+
* Every command listed here has a dispatch handler that is **fully synchronous
|
|
103
|
+
* and side-effect-free**: on the single-threaded event loop it runs to
|
|
104
|
+
* completion between the await points of any in-flight ordered command, reading
|
|
105
|
+
* live state without mutating it. Because such a read performs no causal write,
|
|
106
|
+
* jumping ahead of an earlier *queued* ordered command is observably harmless —
|
|
107
|
+
* there is no state change to reorder. Read payloads are additionally
|
|
108
|
+
* snapshotted inside the handler (e.g. `get_messages` returns a shallow copy of
|
|
109
|
+
* `session.messages`) so a fast-lane read can never serialize a half-mutated
|
|
110
|
+
* array that an ordered turn/compaction is rewriting in place.
|
|
111
|
+
*
|
|
112
|
+
* Deliberately excluded (kept ordered): every async/long command and every
|
|
113
|
+
* mutating command. In particular the control-flag setters (`set_thinking_level`,
|
|
114
|
+
* `cycle_thinking_level`, `set_steering_mode`, `set_follow_up_mode`,
|
|
115
|
+
* `set_interrupt_mode`, `set_auto_compaction`, `set_auto_retry`) stay ordered.
|
|
116
|
+
* Their handlers are synchronous, so fast-laning one ahead of an already-queued
|
|
117
|
+
* `prompt`/`bash` would apply the new mode *before* that earlier command runs —
|
|
118
|
+
* the earlier command would then observe the later setter's value, a
|
|
119
|
+
* causal-order (arrival-order) regression. Mutations therefore stay on the
|
|
120
|
+
* chain, and new command types default to ordered (fail-safe).
|
|
121
|
+
*/
|
|
122
|
+
export const RPC_SAFE_READ_CONTROL_COMMANDS: ReadonlySet<RpcCommand["type"]> = new Set<RpcCommand["type"]>([
|
|
123
|
+
// Pure synchronous reads — snapshot live state at processing time, never mutate.
|
|
124
|
+
"get_state",
|
|
125
|
+
"get_session_stats",
|
|
126
|
+
"get_available_models",
|
|
127
|
+
"get_branch_messages",
|
|
128
|
+
"get_last_assistant_text",
|
|
129
|
+
"get_messages",
|
|
130
|
+
"get_login_providers",
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
/** True when a command may bypass the ordered serial chain and run immediately. */
|
|
134
|
+
export function isFastLaneRpcCommand(type: RpcCommand["type"]): boolean {
|
|
135
|
+
return RPC_CANCELLATION_COMMANDS.has(type) || RPC_SAFE_READ_CONTROL_COMMANDS.has(type);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Schedules inbound RPC commands: fast-lane commands run immediately while
|
|
140
|
+
* everything else runs through a serial chain so causal order is preserved. The
|
|
141
|
+
* read loop never blocks, which is what lets a fast-lane command reach a
|
|
142
|
+
* long-running ordered command instead of being head-of-line-blocked behind it.
|
|
143
|
+
*/
|
|
144
|
+
export function createRpcCommandScheduler(
|
|
145
|
+
run: (command: RpcCommand) => Promise<void>,
|
|
146
|
+
track: (task: Promise<void>) => void,
|
|
147
|
+
): { dispatch: (command: RpcCommand) => void } {
|
|
148
|
+
let orderedChain: Promise<void> = Promise.resolve();
|
|
149
|
+
return {
|
|
150
|
+
dispatch(command: RpcCommand): void {
|
|
151
|
+
if (isFastLaneRpcCommand(command.type)) {
|
|
152
|
+
track(run(command));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
orderedChain = orderedChain.then(() => run(command));
|
|
156
|
+
track(orderedChain);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
80
161
|
function auditOutcomeFor(event: string): "accepted" | "rejected" | "denied" | "exceeded" | "aborted" | "info" {
|
|
81
162
|
if (event.includes("denied")) return "denied";
|
|
82
163
|
if (event.includes("exceeded")) return "exceeded";
|
|
@@ -86,6 +167,43 @@ function auditOutcomeFor(event: string): "accepted" | "rejected" | "denied" | "e
|
|
|
86
167
|
return "info";
|
|
87
168
|
}
|
|
88
169
|
|
|
170
|
+
export class RpcListenRefusedError extends Error {
|
|
171
|
+
constructor(socketPath: string) {
|
|
172
|
+
super(
|
|
173
|
+
`RPC --listen refused: a live server is already listening on ${socketPath}. ` +
|
|
174
|
+
"Stop it first or choose a different --listen path.",
|
|
175
|
+
);
|
|
176
|
+
this.name = "RpcListenRefusedError";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Probe whether a unix-domain socket path has a live server accepting
|
|
182
|
+
* connections. Returns `true` when a connection succeeds (a previous owner is
|
|
183
|
+
* still alive), and returns `false` only for known missing/stale endpoints
|
|
184
|
+
* (ENOENT / ECONNREFUSED). Unexpected probe failures fail closed as "alive" so
|
|
185
|
+
* `--listen` startup refuses to unlink a path it could not safely classify.
|
|
186
|
+
*/
|
|
187
|
+
export async function isUnixSocketAlive(socketPath: string): Promise<boolean> {
|
|
188
|
+
try {
|
|
189
|
+
const socket = await Bun.connect({
|
|
190
|
+
unix: socketPath,
|
|
191
|
+
socket: { data() {}, open() {}, error() {}, close() {} },
|
|
192
|
+
});
|
|
193
|
+
socket.end();
|
|
194
|
+
return true;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const code = err && typeof err === "object" ? (err as { code?: unknown }).code : undefined;
|
|
197
|
+
if (code === "ENOENT" || code === "ECONNREFUSED") return false;
|
|
198
|
+
logger.warn("RPC --listen socket probe failed closed", {
|
|
199
|
+
socketPath,
|
|
200
|
+
code: typeof code === "string" ? code : undefined,
|
|
201
|
+
error: err instanceof Error ? err.message : String(err),
|
|
202
|
+
});
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
89
207
|
export function requestRpcEditor(
|
|
90
208
|
pendingRequests: Map<string, PendingExtensionRequest>,
|
|
91
209
|
output: RpcOutput,
|
|
@@ -156,6 +274,7 @@ export function requestRpcEditor(
|
|
|
156
274
|
export async function runRpcMode(
|
|
157
275
|
session: AgentSession,
|
|
158
276
|
setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
|
|
277
|
+
options?: { listen?: string },
|
|
159
278
|
): Promise<never> {
|
|
160
279
|
// Signal to RPC clients that the server is ready to accept commands
|
|
161
280
|
// Suppress terminal notifications: they write \x07 (BEL) or OSC sequences directly to
|
|
@@ -164,10 +283,18 @@ export async function runRpcMode(
|
|
|
164
283
|
// may write there.
|
|
165
284
|
process.env.PI_NOTIFICATIONS = "off";
|
|
166
285
|
|
|
167
|
-
|
|
286
|
+
// Frames go to a swappable sink: stdout for stdio, the active client socket for a
|
|
287
|
+
// persistent --listen (UDS) server. Defaults to stdout, so the stdio path is unchanged.
|
|
288
|
+
let frameSink = (line: string): void => {
|
|
289
|
+
process.stdout.write(line);
|
|
290
|
+
};
|
|
168
291
|
const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
|
|
169
|
-
|
|
292
|
+
frameSink(`${JSON.stringify(obj)}\n`);
|
|
170
293
|
};
|
|
294
|
+
// stdio announces readiness immediately; the UDS server announces it per client connection.
|
|
295
|
+
if (!options?.listen) {
|
|
296
|
+
output({ type: "ready" });
|
|
297
|
+
}
|
|
171
298
|
const emitRpcTitles = shouldEmitRpcTitles();
|
|
172
299
|
const decodeError = (err: unknown): string => (err instanceof Error ? err.message : String(err));
|
|
173
300
|
|
|
@@ -216,6 +343,7 @@ export async function runRpcMode(
|
|
|
216
343
|
emitFrame: gate => output(gate),
|
|
217
344
|
store: gateStore,
|
|
218
345
|
audit: recordAudit,
|
|
346
|
+
providerSupportsTokenCostMetrics: modelSupportsTokenCostMetrics(session.model),
|
|
219
347
|
getUsageSnapshot: () => {
|
|
220
348
|
const stats = session.getSessionStats();
|
|
221
349
|
return { tokens: stats.tokens.total, costUsd: stats.cost };
|
|
@@ -231,11 +359,20 @@ export async function runRpcMode(
|
|
|
231
359
|
// Shutdown request flag (wrapped in object to allow mutation with const)
|
|
232
360
|
const shutdownState = { requested: false };
|
|
233
361
|
let shutdownStarted = false;
|
|
362
|
+
// Tracks in-flight non-blocking command handlers so shutdown can drain them.
|
|
363
|
+
const inFlightCommands = new Set<Promise<void>>();
|
|
234
364
|
async function shutdown(exitCode: number, reason: string): Promise<never> {
|
|
235
365
|
if (shutdownStarted) {
|
|
236
366
|
process.exit(exitCode);
|
|
237
367
|
}
|
|
238
368
|
shutdownStarted = true;
|
|
369
|
+
// Let in-flight non-blocking commands (bash/compact/handoff) finish and emit
|
|
370
|
+
// their responses before teardown, bounded so a never-resolving login cannot
|
|
371
|
+
// wedge shutdown (issue 13).
|
|
372
|
+
if (inFlightCommands.size > 0) {
|
|
373
|
+
await Promise.race([Promise.allSettled([...inFlightCommands]), Bun.sleep(5000)]);
|
|
374
|
+
}
|
|
375
|
+
await unregisterRpcSession(session.sessionId).catch(() => {});
|
|
239
376
|
hostToolBridge.rejectAllPending(`${reason} before host tool execution completed`);
|
|
240
377
|
hostUriBridge.clear(`${reason} before host URI request completed`);
|
|
241
378
|
try {
|
|
@@ -399,7 +536,7 @@ export async function runRpcMode(
|
|
|
399
536
|
}
|
|
400
537
|
|
|
401
538
|
setTitle(title: string): void {
|
|
402
|
-
// Title updates are low-value noise for most RPC hosts; opt in via
|
|
539
|
+
// Title updates are low-value noise for most RPC hosts; opt in via GJC_RPC_EMIT_TITLE=1.
|
|
403
540
|
if (!emitRpcTitles) return;
|
|
404
541
|
this.output({
|
|
405
542
|
type: "extension_ui_request",
|
|
@@ -514,6 +651,26 @@ export async function runRpcMode(
|
|
|
514
651
|
unattendedControlPlane,
|
|
515
652
|
});
|
|
516
653
|
|
|
654
|
+
// Fast-lane commands (cancellation + safe read/control, see
|
|
655
|
+
// isFastLaneRpcCommand) bypass the ordered serial chain and run immediately;
|
|
656
|
+
// everything else runs through a serial chain so causal order is preserved
|
|
657
|
+
// (e.g. an ordered `set_model` after `bash` still applies after the bash
|
|
658
|
+
// result) while the read loop itself never blocks — that is what lets a
|
|
659
|
+
// fast-lane command reach a long-running `bash`/`compact`/`handoff`/`login`
|
|
660
|
+
// instead of being head-of-line-blocked behind it (issue 13).
|
|
661
|
+
const runCommand = async (command: RpcCommand): Promise<void> => {
|
|
662
|
+
try {
|
|
663
|
+
output(await handleCommand(command));
|
|
664
|
+
} catch (err) {
|
|
665
|
+
output(error(command.id, command.type, decodeError(err)));
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
const trackCommand = (task: Promise<void>): void => {
|
|
669
|
+
inFlightCommands.add(task);
|
|
670
|
+
void task.finally(() => inFlightCommands.delete(task));
|
|
671
|
+
};
|
|
672
|
+
const { dispatch: dispatchCommand } = createRpcCommandScheduler(runCommand, trackCommand);
|
|
673
|
+
|
|
517
674
|
/**
|
|
518
675
|
* Check if shutdown was requested and perform shutdown if so.
|
|
519
676
|
* Called after handling each command when waiting for the next command.
|
|
@@ -523,59 +680,134 @@ export async function runRpcMode(
|
|
|
523
680
|
await shutdown(0, "RPC shutdown requested");
|
|
524
681
|
}
|
|
525
682
|
|
|
526
|
-
//
|
|
527
|
-
//
|
|
683
|
+
// Parse + route a single inbound JSONL frame. Shared by the stdio reader and the
|
|
684
|
+
// persistent UDS server so both transports use the same command surface.
|
|
528
685
|
const inputDecoder = new TextDecoder("utf-8", { fatal: false });
|
|
529
|
-
|
|
530
|
-
const text = inputDecoder.decode(line).trim();
|
|
531
|
-
if (!text) continue;
|
|
532
|
-
|
|
686
|
+
async function handleInboundLine(text: string): Promise<void> {
|
|
533
687
|
let parsed: unknown;
|
|
534
688
|
try {
|
|
535
689
|
parsed = JSON.parse(text);
|
|
536
690
|
} catch (err) {
|
|
537
691
|
output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
|
|
538
|
-
|
|
692
|
+
return;
|
|
539
693
|
}
|
|
540
|
-
|
|
541
694
|
try {
|
|
542
|
-
// Handle extension UI responses
|
|
543
695
|
if ((parsed as RpcExtensionUIResponse).type === "extension_ui_response") {
|
|
544
696
|
const response = parsed as RpcExtensionUIResponse;
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
pending.resolve(response);
|
|
548
|
-
}
|
|
549
|
-
continue;
|
|
697
|
+
pendingExtensionRequests.get(response.id)?.resolve(response);
|
|
698
|
+
return;
|
|
550
699
|
}
|
|
551
|
-
|
|
552
700
|
if (isRpcHostToolResult(parsed)) {
|
|
553
701
|
hostToolBridge.handleResult(parsed);
|
|
554
|
-
|
|
702
|
+
return;
|
|
555
703
|
}
|
|
556
|
-
|
|
557
704
|
if (isRpcHostToolUpdate(parsed)) {
|
|
558
705
|
hostToolBridge.handleUpdate(parsed);
|
|
559
|
-
|
|
706
|
+
return;
|
|
560
707
|
}
|
|
561
|
-
|
|
562
708
|
if (isRpcHostUriResult(parsed)) {
|
|
563
709
|
hostUriBridge.handleResult(parsed);
|
|
564
|
-
|
|
710
|
+
return;
|
|
565
711
|
}
|
|
566
|
-
|
|
567
|
-
//
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
output(response);
|
|
571
|
-
|
|
572
|
-
// Check for deferred shutdown request (idle between commands)
|
|
712
|
+
// Ordered commands run through a serial chain to preserve causal order; the
|
|
713
|
+
// reader never blocks, so cancellation commands stay responsive even while a
|
|
714
|
+
// long command is in flight (issue 13).
|
|
715
|
+
dispatchCommand(parsed as RpcCommand);
|
|
573
716
|
await checkShutdownRequested();
|
|
574
717
|
} catch (err) {
|
|
575
718
|
output(error(undefined, "parse", `Failed to parse command: ${decodeError(err)}`));
|
|
576
719
|
}
|
|
577
720
|
}
|
|
578
721
|
|
|
722
|
+
// Persistent UDS server (issue 09): keep the AgentSession alive across client
|
|
723
|
+
// reconnects instead of exiting on stdin EOF. Frames route to the active client
|
|
724
|
+
// socket; while no client is connected they are dropped (clients resync via
|
|
725
|
+
// get_state/get_messages on reconnect).
|
|
726
|
+
if (options?.listen) {
|
|
727
|
+
const socketPath = options.listen;
|
|
728
|
+
await fs.mkdir(path.dirname(socketPath), { recursive: true }).catch(() => {});
|
|
729
|
+
// Refuse to clobber a live previous owner: probe the path first and only
|
|
730
|
+
// unlink a stale endpoint. A second `--listen` on the same path must not
|
|
731
|
+
// remove the socket another running server is still serving (#606).
|
|
732
|
+
// Unexpected probe failures are treated as alive, so this also refuses
|
|
733
|
+
// rather than unlinking a socket path we could not safely classify.
|
|
734
|
+
if (await isUnixSocketAlive(socketPath)) {
|
|
735
|
+
throw new RpcListenRefusedError(socketPath);
|
|
736
|
+
}
|
|
737
|
+
await fs.rm(socketPath, { force: true }).catch(() => {});
|
|
738
|
+
await registerRpcSession({
|
|
739
|
+
sessionId: session.sessionId,
|
|
740
|
+
pid: process.pid,
|
|
741
|
+
transport: "socket",
|
|
742
|
+
cwd: session.sessionManager.getCwd(),
|
|
743
|
+
model: session.model?.id,
|
|
744
|
+
startedAt: new Date().toISOString(),
|
|
745
|
+
endpoint: socketPath,
|
|
746
|
+
}).catch(() => {});
|
|
747
|
+
|
|
748
|
+
const noopSink = (_line: string): void => {};
|
|
749
|
+
let currentSocket: object | undefined;
|
|
750
|
+
let buf = "";
|
|
751
|
+
Bun.listen({
|
|
752
|
+
unix: socketPath,
|
|
753
|
+
socket: {
|
|
754
|
+
open(socket) {
|
|
755
|
+
currentSocket = socket;
|
|
756
|
+
buf = "";
|
|
757
|
+
frameSink = (line: string) => {
|
|
758
|
+
socket.write(line);
|
|
759
|
+
};
|
|
760
|
+
output({ type: "ready" });
|
|
761
|
+
},
|
|
762
|
+
data(socket, data) {
|
|
763
|
+
if (socket !== currentSocket) return;
|
|
764
|
+
buf += inputDecoder.decode(data);
|
|
765
|
+
while (true) {
|
|
766
|
+
const nl = buf.indexOf("\n");
|
|
767
|
+
if (nl < 0) break;
|
|
768
|
+
const text = buf.slice(0, nl).trim();
|
|
769
|
+
buf = buf.slice(nl + 1);
|
|
770
|
+
if (text) void handleInboundLine(text);
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
close(socket) {
|
|
774
|
+
if (socket === currentSocket) {
|
|
775
|
+
currentSocket = undefined;
|
|
776
|
+
frameSink = noopSink;
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
error() {},
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const onSignal = (): void => {
|
|
784
|
+
void shutdown(0, "RPC socket server signal");
|
|
785
|
+
};
|
|
786
|
+
process.on("SIGINT", onSignal);
|
|
787
|
+
process.on("SIGTERM", onSignal);
|
|
788
|
+
// Block until an explicit shutdown (signal/extension) calls process.exit.
|
|
789
|
+
await new Promise<never>(() => {});
|
|
790
|
+
throw new Error("RPC socket server returned unexpectedly");
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Register this stdio RPC session so other processes can discover it (issue 10).
|
|
794
|
+
await registerRpcSession({
|
|
795
|
+
sessionId: session.sessionId,
|
|
796
|
+
pid: process.pid,
|
|
797
|
+
transport: "stdio",
|
|
798
|
+
cwd: session.sessionManager.getCwd(),
|
|
799
|
+
model: session.model?.id,
|
|
800
|
+
startedAt: new Date().toISOString(),
|
|
801
|
+
}).catch(() => {});
|
|
802
|
+
|
|
803
|
+
// Listen for JSONL input using Bun's stdin. Parse frame-by-frame so a malformed
|
|
804
|
+
// command reports a parse error without poisoning the whole long-lived RPC session.
|
|
805
|
+
for await (const line of readLines(Bun.stdin.stream())) {
|
|
806
|
+
const text = inputDecoder.decode(line).trim();
|
|
807
|
+
if (!text) continue;
|
|
808
|
+
await handleInboundLine(text);
|
|
809
|
+
}
|
|
810
|
+
|
|
579
811
|
// stdin closed — RPC client is gone, flush durable state and exit cleanly
|
|
580
812
|
await shutdown(0, "RPC client disconnected");
|
|
581
813
|
throw new Error("RPC shutdown returned unexpectedly");
|