@gajae-code/coding-agent 0.16.1 → 0.16.3
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 +11 -0
- package/dist/types/sdk/cli/rows.d.ts +7 -0
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/tools/index.d.ts +0 -2
- package/dist/types/utils/external-editor.d.ts +5 -0
- package/package.json +7 -7
- package/src/gjc-runtime/session-state-lock.ts +2 -4
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/modes/acp/acp-agent.ts +29 -14
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +1 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +7 -0
- package/src/sdk/cli/rows.ts +47 -2
- package/src/sdk/cli/session-cli.ts +38 -17
- package/src/sdk/host/events.ts +25 -2
- package/src/sdk/host/host.ts +4 -1
- package/src/sdk/host/session-runtime.test.ts +34 -23
- package/src/sdk/host/session-runtime.ts +4 -0
- package/src/sdk/router/session-router.ts +60 -37
- package/src/sdk/session.ts +0 -1
- package/src/tools/bash.ts +15 -13
- package/src/tools/index.ts +0 -2
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/utils/external-editor.ts +16 -2
|
@@ -338,6 +338,8 @@ type AttachedSession = {
|
|
|
338
338
|
replayPending: boolean;
|
|
339
339
|
replayPendingSince?: number;
|
|
340
340
|
replayPendingToken?: object;
|
|
341
|
+
drainingHeldSeq?: number;
|
|
342
|
+
drainingHeldBatch?: HeldFrame[];
|
|
341
343
|
readyTail: Promise<void>;
|
|
342
344
|
readonly publication: { promise: Promise<void>; resolve: () => void; reject: (reason?: unknown) => void };
|
|
343
345
|
dispose: () => void;
|
|
@@ -1899,44 +1901,53 @@ export class SessionRouter {
|
|
|
1899
1901
|
}
|
|
1900
1902
|
|
|
1901
1903
|
async #reinitializeAttachment(attached: AttachedSession): Promise<void> {
|
|
1902
|
-
|
|
1904
|
+
// A prior ready tail may be waiting for publication work carried by the dead
|
|
1905
|
+
// socket. Chaining the reconnect behind it prevents the replacement from even
|
|
1906
|
+
// issuing replay. Replace its hold-array identity to fence that tail, but carry
|
|
1907
|
+
// forward frames it had not started draining; the active draining sequence is
|
|
1908
|
+
// already represented by the independent frame tail.
|
|
1909
|
+
if (attached.barrier.held) {
|
|
1910
|
+
const carried = [...(attached.drainingHeldBatch ?? []), ...attached.barrier.held];
|
|
1911
|
+
attached.drainingHeldBatch = undefined;
|
|
1912
|
+
attached.barrier.held = carried
|
|
1913
|
+
.filter(entry => entry.seq !== attached.drainingHeldSeq)
|
|
1914
|
+
.filter((entry, index, entries) => entries.findIndex(candidate => candidate.seq === entry.seq) === index);
|
|
1915
|
+
}
|
|
1903
1916
|
const replayToken = {};
|
|
1904
1917
|
attached.replayPending = true;
|
|
1905
1918
|
attached.replayPendingSince = Date.now();
|
|
1906
1919
|
attached.replayPendingToken = replayToken;
|
|
1907
|
-
const current =
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
.
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
if (this.#attachmentLive(attached)) await this.#replayAttachment(attached, attached.cursor.seq);
|
|
1939
|
-
});
|
|
1920
|
+
const current = Promise.resolve().then(async () => {
|
|
1921
|
+
if (!this.#attachmentLive(attached)) return;
|
|
1922
|
+
const proven = await this.#readProvenEndpoint(attached.indexed);
|
|
1923
|
+
if (
|
|
1924
|
+
!proven ||
|
|
1925
|
+
proven.endpoint.url !== attached.endpoint.url ||
|
|
1926
|
+
proven.endpoint.token !== attached.endpoint.token ||
|
|
1927
|
+
proven.endpoint.pid !== attached.pid ||
|
|
1928
|
+
!sameEndpointIdentity(attached.endpointIdentity, proven.identity)
|
|
1929
|
+
) {
|
|
1930
|
+
await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
attached.initializingPublication = true;
|
|
1934
|
+
try {
|
|
1935
|
+
void Promise.resolve()
|
|
1936
|
+
.then(() => this.#deps.onNotificationSubscriptionReady?.(attached.notificationSubscription))
|
|
1937
|
+
.catch(error => {
|
|
1938
|
+
this.#detachNotification(attached, "cancelled");
|
|
1939
|
+
logger.warn(`SDK notification subscription reconnect hook failed locally: ${String(error)}`);
|
|
1940
|
+
});
|
|
1941
|
+
await this.#deps.onAttachmentReady?.(attached.capability);
|
|
1942
|
+
await this.#deps.onReplayReady?.();
|
|
1943
|
+
} catch {
|
|
1944
|
+
await this.#retireAttachment(attached);
|
|
1945
|
+
return;
|
|
1946
|
+
} finally {
|
|
1947
|
+
attached.initializingPublication = false;
|
|
1948
|
+
}
|
|
1949
|
+
if (this.#attachmentLive(attached)) await this.#replayAttachment(attached, attached.cursor.seq);
|
|
1950
|
+
});
|
|
1940
1951
|
attached.readyTail = current.finally(() => {
|
|
1941
1952
|
if (attached.replayPendingToken === replayToken) {
|
|
1942
1953
|
attached.replayPending = false;
|
|
@@ -2294,7 +2305,19 @@ export class SessionRouter {
|
|
|
2294
2305
|
return;
|
|
2295
2306
|
}
|
|
2296
2307
|
const batch = held.splice(0, held.length).sort((left, right) => left.seq - right.seq);
|
|
2297
|
-
|
|
2308
|
+
attached.drainingHeldBatch = batch;
|
|
2309
|
+
for (;;) {
|
|
2310
|
+
const entry = batch.shift();
|
|
2311
|
+
if (!entry) break;
|
|
2312
|
+
attached.drainingHeldSeq = entry.seq;
|
|
2313
|
+
try {
|
|
2314
|
+
await this.#enqueueFrame(attached, entry.frame, "ordered");
|
|
2315
|
+
} finally {
|
|
2316
|
+
if (attached.drainingHeldSeq === entry.seq) attached.drainingHeldSeq = undefined;
|
|
2317
|
+
}
|
|
2318
|
+
if (attached.barrier.held !== held) return;
|
|
2319
|
+
}
|
|
2320
|
+
if (attached.drainingHeldBatch === batch) attached.drainingHeldBatch = undefined;
|
|
2298
2321
|
}
|
|
2299
2322
|
}
|
|
2300
2323
|
|
|
@@ -2374,7 +2397,7 @@ export class SessionRouter {
|
|
|
2374
2397
|
this.#failBarrier(attached, "replay response contained a malformed event");
|
|
2375
2398
|
return;
|
|
2376
2399
|
}
|
|
2377
|
-
let previousSeq =
|
|
2400
|
+
let previousSeq = -1;
|
|
2378
2401
|
for (const event of events) {
|
|
2379
2402
|
if (event.type !== "event") {
|
|
2380
2403
|
this.#failBarrier(attached, "replay response contained a non-event frame");
|
|
@@ -2411,7 +2434,7 @@ export class SessionRouter {
|
|
|
2411
2434
|
}
|
|
2412
2435
|
const retained = events
|
|
2413
2436
|
.map(event => readSequence(event.seq))
|
|
2414
|
-
.find(seq => seq !== undefined && seq <= gap.toSeq);
|
|
2437
|
+
.find(seq => seq !== undefined && seq >= gap.fromSeq && seq <= gap.toSeq);
|
|
2415
2438
|
if (retained !== undefined) {
|
|
2416
2439
|
this.#failBarrier(
|
|
2417
2440
|
attached,
|
package/src/sdk/session.ts
CHANGED
|
@@ -2720,7 +2720,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2720
2720
|
hasForegroundBashBackgroundRequestHandler: () => session?.hasForegroundBashBackgroundRequestHandler() ?? false,
|
|
2721
2721
|
requestForegroundBashBackground: (reason, adapter) =>
|
|
2722
2722
|
Promise.resolve(session?.requestForegroundBashBackground(reason, adapter) ?? false),
|
|
2723
|
-
getToolInterruptPolicy: () => agent?.getToolInterruptPolicy() ?? "abort_tools",
|
|
2724
2723
|
|
|
2725
2724
|
getCredentialSessionId: () => session?.credentialSessionId ?? credentialSessionId,
|
|
2726
2725
|
getMcpManager: () => mcpManager ?? options.inheritedMcpManager,
|
package/src/tools/bash.ts
CHANGED
|
@@ -1139,20 +1139,22 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1139
1139
|
|
|
1140
1140
|
/**
|
|
1141
1141
|
* Whether a queued user steer may fold the running foreground wait. Mirrors
|
|
1142
|
-
* the loop's
|
|
1143
|
-
*
|
|
1144
|
-
*
|
|
1145
|
-
*
|
|
1146
|
-
*
|
|
1142
|
+
* the loop's steer admission: `busyPromptMode=queue` never admits a steer
|
|
1143
|
+
* into the busy run, so it never folds. `toolInterruptPolicy` is
|
|
1144
|
+
* deliberately NOT a gate: `finish_tools` means "do not kill the batch to
|
|
1145
|
+
* deliver a steer", and a fold kills nothing. It is the one way to deliver
|
|
1146
|
+
* the steer now AND let the command finish, so it applies under both
|
|
1147
|
+
* policies; the policy only decides what the loop does with sibling tools
|
|
1148
|
+
* after the fold returns. The auto-background setting is likewise not
|
|
1149
|
+
* consulted: like the chord, a steer fold is a user action.
|
|
1147
1150
|
*
|
|
1148
|
-
* Fails closed: a session that cannot
|
|
1149
|
-
*
|
|
1151
|
+
* Fails closed: a session that cannot report newly admitted steering or
|
|
1152
|
+
* accept a fold request is not steer-foldable.
|
|
1150
1153
|
*/
|
|
1151
1154
|
#steerFoldEnabled(): boolean {
|
|
1152
|
-
const { waitForUserSteering,
|
|
1153
|
-
if (!waitForUserSteering || !
|
|
1154
|
-
|
|
1155
|
-
return getToolInterruptPolicy() === "abort_tools";
|
|
1155
|
+
const { waitForUserSteering, requestForegroundBashBackground } = this.session;
|
|
1156
|
+
if (!waitForUserSteering || !requestForegroundBashBackground) return false;
|
|
1157
|
+
return this.session.settings.get("busyPromptMode") === "steer";
|
|
1156
1158
|
}
|
|
1157
1159
|
|
|
1158
1160
|
/**
|
|
@@ -1161,8 +1163,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
1161
1163
|
* when the wait starts, or one that arrives inside the grace window, never
|
|
1162
1164
|
* folds: the command finishes normally and that steer is consumed at the
|
|
1163
1165
|
* ordinary tool boundary. The watcher keeps observing so a later qualifying
|
|
1164
|
-
* steer still folds, and re-checks the
|
|
1165
|
-
* `busyPromptMode
|
|
1166
|
+
* steer still folds, and re-checks the gate at that moment so a
|
|
1167
|
+
* `busyPromptMode` change during the command is honored.
|
|
1166
1168
|
* Returns a stop function; call it when the wait settles. No-op when steer
|
|
1167
1169
|
* folding is gated off at start.
|
|
1168
1170
|
*/
|
package/src/tools/index.ts
CHANGED
|
@@ -242,8 +242,6 @@ export interface ToolSession {
|
|
|
242
242
|
hasForegroundBashBackgroundRequestHandler?: () => boolean;
|
|
243
243
|
/** Request that a foreground wait fold into a background job, if supported. `reason` names the trigger; `adapter` targets a specific wait instead of the newest registration. */
|
|
244
244
|
requestForegroundBashBackground?: (reason?: FoldReason, adapter?: FoldAdapter) => Promise<boolean>;
|
|
245
|
-
/** Current tool interrupt policy; `finish_tools` lets the running tool finish, so a steer must not fold it. */
|
|
246
|
-
getToolInterruptPolicy?: () => "abort_tools" | "finish_tools";
|
|
247
245
|
|
|
248
246
|
/** Get the session-owned or inherited async job manager. */
|
|
249
247
|
getAsyncJobManager?: () => AsyncJobManager | undefined;
|
|
@@ -2319,7 +2319,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
2319
2319
|
"job": {
|
|
2320
2320
|
"name": "job",
|
|
2321
2321
|
"label": "Job",
|
|
2322
|
-
"description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`), or automatically when the user sends a steer after the command has run for at least 2 seconds (with `busyPromptMode=steer`
|
|
2322
|
+
"description": "Inspects, waits, or cancels async jobs.\n\nBackground job results are delivered automatically when complete. Jobs that back task subagents should be controlled via the `subagent` tool when it is available; use `job` for non-subagent jobs (async bash, monitors) and as a compatibility fallback. Running job output stays quiet by default to avoid flooding the conversation; use `tail` when you explicitly want to show/reopen retained output. Reach for this tool only when you need to inspect or intervene.\n\nIn the interactive TUI, a running foreground command can be folded into a background job by pressing the fold chord twice (default `Alt+Shift+B`, plus `Cmd+B` on macOS; remappable via `app.tool.backgroundFold`), or automatically when the user sends a steer after the command has run for at least 2 seconds (with `busyPromptMode=steer` (`toolInterruptPolicy` is not a gate)). This covers managed non-PTY bash, editor/ACP client-terminal commands, and PTY-mode commands. A folded job keeps its original deadline, is never killed or restarted by folding, and its result wakes a later turn automatically; a folded PTY continues output-only and no longer accepts input. Job listings expose `foldReason` (`steer`, `chord`, `timer`, or `sdk_control`) beside `backgrounded` so you can tell why a job left the foreground. `task`/`subagent` waits are not foldable. Raw shell `Ctrl+Z`/`bg` is not the supported path inside GJC because it bypasses job ownership and output-routing contracts.\n\n# Operations\n\n## `list: true`\nUse to inspect what's running.\n\n## `tail: [id, …]`\nShow the retained output buffer for one or more background jobs without waiting.\n- Use this to reopen/tail a backgrounded long-running bash/tool output after folding it away.\n- Output is bounded by the manager retention window; stale cursors may report that only the retained tail is available.\n- Prefer `tail` over polling when you only need to peek at progress, so the conversation can continue without flooding the TUI.\n\n## `poll: [id, …]`\nBlock until the specified jobs finish or the wait window (~30 s, not configurable) elapses.\n- Use when you are genuinely blocked on a result and have no other work to do.\n- Returns the current snapshot when the timer elapses; running jobs remain running.\n- Completed jobs include their final output in the returned snapshot.\n\n## `cancel: [id, …]`\nStop running jobs.\n- Use when a job is stalled, hung, or no longer needed.\n- Returns immediately after cancelling.",
|
|
2323
2323
|
"parameters": {
|
|
2324
2324
|
"type": "object",
|
|
2325
2325
|
"properties": {
|
|
@@ -65,8 +65,22 @@ const ENABLE_ECHO_INPUT = 0x0004;
|
|
|
65
65
|
const ENABLE_WINDOW_INPUT = 0x0008;
|
|
66
66
|
const ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
interface RawModeInput {
|
|
69
|
+
setRawMode?: (mode: boolean) => unknown;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function prepareWindowsConsoleForExternalEditor(
|
|
73
|
+
platform: NodeJS.Platform = process.platform,
|
|
74
|
+
input: RawModeInput = process.stdin,
|
|
75
|
+
): void {
|
|
76
|
+
if (platform !== "win32") return;
|
|
77
|
+
// Native Windows terminals can expose stdin through a ConPTY pipe rather than
|
|
78
|
+
// a console input handle. In that case GetConsoleMode/SetConsoleMode below
|
|
79
|
+
// cannot re-arm the input stream, but the runtime's TTY implementation can.
|
|
80
|
+
try {
|
|
81
|
+
input.setRawMode?.(true);
|
|
82
|
+
} catch {}
|
|
83
|
+
|
|
70
84
|
try {
|
|
71
85
|
const kernel32 = dlopen("kernel32.dll", {
|
|
72
86
|
GetStdHandle: { args: [FFIType.i32], returns: FFIType.ptr },
|