@gajae-code/coding-agent 0.11.6 → 0.11.8
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 +92 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +70 -7
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/main.d.ts +32 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +26 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +30 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
- package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +52 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +44 -11
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +18 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +73 -7
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +93 -15
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +12 -10
- package/src/internal-urls/local-protocol.ts +64 -21
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +111 -37
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +73 -11
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +25 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +38 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +149 -63
- package/src/modes/interactive-mode.ts +57 -34
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +19 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +244 -70
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +68 -20
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +18 -3
- package/src/sdk/bus/telegram-daemon-control.ts +147 -35
- package/src/sdk/bus/telegram-daemon.ts +2463 -450
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +31 -5
- package/src/sdk/startup-capability.ts +9 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +292 -154
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +1163 -137
- package/src/session/internal/managed-session-storage.ts +397 -234
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +620 -403
- package/src/session/session-storage.ts +128 -11
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +199 -89
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +4 -0
- package/src/tools/read.ts +2 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -145,6 +145,8 @@ async function assertDirectoryNotSymlink(directoryPath: string): Promise<void> {
|
|
|
145
145
|
const LEGACY_MIGRATION_MARKER = ".gjc-local-legacy-migrated-v1";
|
|
146
146
|
const MAX_LEGACY_LOCAL_BYTES = 64 * 1024 * 1024;
|
|
147
147
|
|
|
148
|
+
type LegacyMigrationState = "complete" | "cleanup_pending";
|
|
149
|
+
|
|
148
150
|
interface LegacyEntrySnapshot {
|
|
149
151
|
readonly relativePath: string;
|
|
150
152
|
readonly dev: bigint;
|
|
@@ -169,6 +171,26 @@ async function snapshotDirectory(directoryPath: string, relativePath: string): P
|
|
|
169
171
|
return { relativePath, dev: stat.dev, ino: stat.ino, size: stat.size };
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
interface CanonicalLegacyRoot {
|
|
175
|
+
readonly root: string;
|
|
176
|
+
readonly identity: LegacyEntrySnapshot;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function canonicalLegacyRoot(artifactsDir: string): Promise<CanonicalLegacyRoot> {
|
|
180
|
+
const lexicalRoot = path.resolve(artifactsDir, "local");
|
|
181
|
+
const lexicalIdentity = await snapshotDirectory(lexicalRoot, "");
|
|
182
|
+
const [canonicalArtifactsDir, canonicalRoot] = await Promise.all([
|
|
183
|
+
fs.realpath(path.resolve(artifactsDir)),
|
|
184
|
+
fs.realpath(lexicalRoot),
|
|
185
|
+
]);
|
|
186
|
+
if (canonicalRoot === canonicalArtifactsDir) throw new Error("Unsafe legacy local:// migration source");
|
|
187
|
+
ensureWithinRoot(canonicalRoot, canonicalArtifactsDir);
|
|
188
|
+
const canonicalIdentity = await snapshotDirectory(canonicalRoot, "");
|
|
189
|
+
if (!matchesSnapshot(canonicalIdentity, lexicalIdentity))
|
|
190
|
+
throw new Error("Legacy local:// migration source changed during capture");
|
|
191
|
+
return { root: canonicalRoot, identity: lexicalIdentity };
|
|
192
|
+
}
|
|
193
|
+
|
|
172
194
|
async function snapshotRegularFile(filePath: string, relativePath: string): Promise<LegacyEntrySnapshot> {
|
|
173
195
|
const before = await fs.lstat(filePath, { bigint: true });
|
|
174
196
|
if (!before.isFile() || before.isSymbolicLink()) throw new Error("Unsafe legacy local:// migration source");
|
|
@@ -202,11 +224,16 @@ function manifestsMatch(left: readonly LegacyEntrySnapshot[], right: readonly Le
|
|
|
202
224
|
);
|
|
203
225
|
}
|
|
204
226
|
|
|
205
|
-
async function captureLegacyManifest(
|
|
227
|
+
async function captureLegacyManifest(
|
|
228
|
+
root: string,
|
|
229
|
+
expectedRootIdentity: LegacyEntrySnapshot,
|
|
230
|
+
): Promise<readonly LegacyEntrySnapshot[]> {
|
|
206
231
|
const manifest: LegacyEntrySnapshot[] = [];
|
|
207
232
|
let copiedBytes = 0n;
|
|
208
233
|
const captureDirectory = async (directoryPath: string, relativePath: string): Promise<void> => {
|
|
209
234
|
const directory = await snapshotDirectory(directoryPath, relativePath);
|
|
235
|
+
if (relativePath === "" && !matchesSnapshot(directory, expectedRootIdentity))
|
|
236
|
+
throw new Error("Legacy local:// migration source changed during capture");
|
|
210
237
|
manifest.push(directory);
|
|
211
238
|
const entries = (await fs.readdir(directoryPath, { withFileTypes: true })).sort((a, b) =>
|
|
212
239
|
a.name.localeCompare(b.name),
|
|
@@ -309,7 +336,7 @@ async function migrateManagedLegacyLocal(
|
|
|
309
336
|
}
|
|
310
337
|
const staging = path.join(scratchParent, `.gjc-local-migration-${randomUUID()}`);
|
|
311
338
|
const installed: Array<{ readonly path: string; readonly dev: bigint; readonly ino: bigint }> = [];
|
|
312
|
-
let
|
|
339
|
+
let sourceRetired = false;
|
|
313
340
|
try {
|
|
314
341
|
await fs.mkdir(staging, { mode: 0o700 });
|
|
315
342
|
for (const entry of captured.entries) {
|
|
@@ -335,18 +362,22 @@ async function migrateManagedLegacyLocal(
|
|
|
335
362
|
const identity = await fs.lstat(destination, { bigint: true });
|
|
336
363
|
installed.push({ path: destination, dev: identity.dev, ino: identity.ino });
|
|
337
364
|
}
|
|
365
|
+
try {
|
|
366
|
+
source.retire(captured.snapshot);
|
|
367
|
+
sourceRetired = true;
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (!(error instanceof Error) || error.message !== "cleanup_pending") throw error;
|
|
370
|
+
await fs.writeFile(marker, "cleanup_pending\n", { mode: 0o600, flag: "wx" });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
338
373
|
await fs.writeFile(marker, "verified\n", { mode: 0o600, flag: "wx" });
|
|
339
|
-
const markerIdentity = await fs.lstat(marker, { bigint: true });
|
|
340
|
-
installedMarker = { dev: markerIdentity.dev, ino: markerIdentity.ino };
|
|
341
|
-
source.retire(captured.snapshot);
|
|
342
374
|
} catch (error) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
await fs.rm(destination.path, { recursive: true, force: true });
|
|
375
|
+
if (!sourceRetired) {
|
|
376
|
+
for (const destination of installed.reverse()) {
|
|
377
|
+
const current = await fs.lstat(destination.path, { bigint: true }).catch(() => null);
|
|
378
|
+
if (current?.dev === destination.dev && current.ino === destination.ino)
|
|
379
|
+
await fs.rm(destination.path, { recursive: true, force: true });
|
|
380
|
+
}
|
|
350
381
|
}
|
|
351
382
|
throw error;
|
|
352
383
|
} finally {
|
|
@@ -354,15 +385,16 @@ async function migrateManagedLegacyLocal(
|
|
|
354
385
|
}
|
|
355
386
|
}
|
|
356
387
|
|
|
357
|
-
async function readMigrationMarker(marker: string): Promise<
|
|
388
|
+
async function readMigrationMarker(marker: string): Promise<LegacyMigrationState | null> {
|
|
358
389
|
try {
|
|
359
390
|
const snapshot = await snapshotRegularFile(marker, LEGACY_MIGRATION_MARKER);
|
|
360
391
|
if (snapshot.size > 32n || snapshot.bytes === undefined) throw new Error("Unsafe local:// migration marker");
|
|
361
392
|
const value = snapshot.bytes.toString("utf8");
|
|
362
|
-
if (value
|
|
363
|
-
return
|
|
393
|
+
if (value === "cleanup_pending\n") return "cleanup_pending";
|
|
394
|
+
if (value === "verified\n" || value === "absent\n") return "complete";
|
|
395
|
+
throw new Error("Unsafe local:// migration marker");
|
|
364
396
|
} catch (error) {
|
|
365
|
-
if (isEnoent(error)) return
|
|
397
|
+
if (isEnoent(error)) return null;
|
|
366
398
|
throw error;
|
|
367
399
|
}
|
|
368
400
|
}
|
|
@@ -384,10 +416,11 @@ async function migrateLegacyLocal(
|
|
|
384
416
|
await fs.writeFile(marker, "absent\n", { mode: 0o600, flag: "wx" });
|
|
385
417
|
return;
|
|
386
418
|
}
|
|
387
|
-
|
|
419
|
+
let legacySource: CanonicalLegacyRoot;
|
|
388
420
|
let manifest: readonly LegacyEntrySnapshot[];
|
|
389
421
|
try {
|
|
390
|
-
|
|
422
|
+
legacySource = await canonicalLegacyRoot(artifactsDir);
|
|
423
|
+
manifest = await captureLegacyManifest(legacySource.root, legacySource.identity);
|
|
391
424
|
} catch (error) {
|
|
392
425
|
if (isEnoent(error)) {
|
|
393
426
|
await fs.writeFile(marker, "absent\n", { mode: 0o600, flag: "wx" });
|
|
@@ -397,8 +430,8 @@ async function migrateLegacyLocal(
|
|
|
397
430
|
}
|
|
398
431
|
const staging = path.join(scratchParent, `.gjc-local-migration-${randomUUID()}`);
|
|
399
432
|
try {
|
|
400
|
-
await copyLegacyManifest(
|
|
401
|
-
const verifiedManifest = await captureLegacyManifest(
|
|
433
|
+
await copyLegacyManifest(legacySource.root, staging, manifest);
|
|
434
|
+
const verifiedManifest = await captureLegacyManifest(legacySource.root, legacySource.identity);
|
|
402
435
|
if (!manifestsMatch(verifiedManifest, manifest))
|
|
403
436
|
throw new Error("Legacy local:// migration source changed during capture");
|
|
404
437
|
for (const entry of await fs.readdir(staging)) {
|
|
@@ -410,7 +443,17 @@ async function migrateLegacyLocal(
|
|
|
410
443
|
}
|
|
411
444
|
await fs.rename(path.join(staging, entry), path.join(localRoot, entry));
|
|
412
445
|
}
|
|
413
|
-
|
|
446
|
+
try {
|
|
447
|
+
await retireLegacyTree(legacySource.root, manifest);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (
|
|
450
|
+
!(error instanceof Error) ||
|
|
451
|
+
error.message !== "Legacy local:// migration retirement failed: cleanup_pending"
|
|
452
|
+
)
|
|
453
|
+
throw error;
|
|
454
|
+
await fs.writeFile(marker, "cleanup_pending\n", { mode: 0o600, flag: "wx" });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
414
457
|
await fs.writeFile(marker, "verified\n", { mode: 0o600, flag: "wx" });
|
|
415
458
|
} finally {
|
|
416
459
|
await fs.rm(staging, { recursive: true, force: true });
|
|
@@ -2,29 +2,91 @@ import { MCPManager } from "../runtime-mcp/manager";
|
|
|
2
2
|
import type { MCPResourceReadResult } from "../runtime-mcp/types";
|
|
3
3
|
import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const MAX_MCP_RESOURCE_URI_CHARS = 16_384;
|
|
6
|
+
const MAX_MCP_URI_TEMPLATE_CHARS = 8_192;
|
|
7
|
+
const MAX_MCP_URI_TEMPLATE_EXPRESSIONS = 32;
|
|
8
|
+
|
|
9
|
+
interface CompiledUriTemplate {
|
|
10
|
+
literalSegments: string[];
|
|
11
|
+
literalChars: number;
|
|
12
|
+
expressionCount: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function containsExpressionLineTerminator(text: string, start: number, end: number): boolean {
|
|
16
|
+
for (let index = start; index < end; index++) {
|
|
17
|
+
const code = text.charCodeAt(index);
|
|
18
|
+
if (code === 0x0a || code === 0x0d || code === 0x2028 || code === 0x2029) return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compileUriTemplate(uriTemplate: string): CompiledUriTemplate | undefined {
|
|
24
|
+
if (uriTemplate.length > MAX_MCP_URI_TEMPLATE_CHARS) return undefined;
|
|
25
|
+
|
|
26
|
+
const literalSegments: string[] = [];
|
|
27
|
+
let expressionCount = 0;
|
|
28
|
+
let literalChars = 0;
|
|
29
|
+
let segmentStart = 0;
|
|
30
|
+
let scanPosition = 0;
|
|
31
|
+
while (scanPosition < uriTemplate.length) {
|
|
32
|
+
const expressionStart = uriTemplate.indexOf("{", scanPosition);
|
|
33
|
+
if (expressionStart < 0) break;
|
|
34
|
+
const expressionEnd = uriTemplate.indexOf("}", expressionStart + 1);
|
|
35
|
+
if (expressionEnd < 0) break;
|
|
36
|
+
if (expressionEnd === expressionStart + 1) {
|
|
37
|
+
scanPosition = expressionStart + 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
expressionCount++;
|
|
41
|
+
if (expressionCount > MAX_MCP_URI_TEMPLATE_EXPRESSIONS) return undefined;
|
|
42
|
+
const segment = uriTemplate.slice(segmentStart, expressionStart);
|
|
43
|
+
literalSegments.push(segment);
|
|
44
|
+
literalChars += segment.length;
|
|
45
|
+
segmentStart = expressionEnd + 1;
|
|
46
|
+
scanPosition = segmentStart;
|
|
47
|
+
}
|
|
48
|
+
const finalSegment = uriTemplate.slice(segmentStart);
|
|
49
|
+
literalSegments.push(finalSegment);
|
|
50
|
+
literalChars += finalSegment.length;
|
|
51
|
+
return { literalSegments, literalChars, expressionCount };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function matchesUriTemplate(uri: string, template: CompiledUriTemplate): boolean {
|
|
55
|
+
const { literalSegments, expressionCount } = template;
|
|
56
|
+
if (expressionCount === 0) return uri === literalSegments[0];
|
|
57
|
+
if (!uri.startsWith(literalSegments[0])) return false;
|
|
58
|
+
|
|
59
|
+
let cursor = literalSegments[0].length;
|
|
60
|
+
for (let index = 1; index < literalSegments.length - 1; index++) {
|
|
61
|
+
const segment = literalSegments[index];
|
|
62
|
+
const matchIndex = uri.indexOf(segment, cursor);
|
|
63
|
+
if (matchIndex < 0 || containsExpressionLineTerminator(uri, cursor, matchIndex)) return false;
|
|
64
|
+
cursor = matchIndex + segment.length;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const suffix = literalSegments.at(-1) ?? "";
|
|
68
|
+
const suffixStart = uri.length - suffix.length;
|
|
69
|
+
return suffixStart >= cursor && uri.endsWith(suffix) && !containsExpressionLineTerminator(uri, cursor, suffixStart);
|
|
7
70
|
}
|
|
8
71
|
|
|
9
72
|
function getUriTemplateMatchScore(
|
|
10
73
|
uri: string,
|
|
11
74
|
uriTemplate: string,
|
|
12
75
|
): { literalChars: number; expressionCount: number } | undefined {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const pattern = literalSegments.map(escapeRegex).join("(.*?)");
|
|
17
|
-
const regex = new RegExp(`^${pattern}$`);
|
|
18
|
-
if (!regex.test(uri)) return undefined;
|
|
19
|
-
const literalChars = literalSegments.reduce((total, segment) => total + segment.length, 0);
|
|
20
|
-
return { literalChars, expressionCount };
|
|
76
|
+
const template = compileUriTemplate(uriTemplate);
|
|
77
|
+
if (!template || !matchesUriTemplate(uri, template)) return undefined;
|
|
78
|
+
return { literalChars: template.literalChars, expressionCount: template.expressionCount };
|
|
21
79
|
}
|
|
22
80
|
|
|
23
81
|
function extractResourceUri(url: InternalUrl): string {
|
|
24
82
|
const host = url.rawHost || url.hostname;
|
|
25
83
|
const rawPathname = url.rawPathname ?? url.pathname;
|
|
26
84
|
const hasPath = rawPathname && rawPathname !== "/";
|
|
27
|
-
const
|
|
85
|
+
const rawUri = `${host}${hasPath ? rawPathname : ""}${url.search}${url.hash}`;
|
|
86
|
+
if (rawUri.length > MAX_MCP_RESOURCE_URI_CHARS) {
|
|
87
|
+
throw new Error("MCP resource URI exceeds the 16384-character limit.");
|
|
88
|
+
}
|
|
89
|
+
const uri = rawUri.trim();
|
|
28
90
|
if (!uri) {
|
|
29
91
|
throw new Error("mcp:// URL requires a resource URI: mcp://<resource-uri>");
|
|
30
92
|
}
|
package/src/main.ts
CHANGED
|
@@ -11,7 +11,8 @@ import * as path from "node:path";
|
|
|
11
11
|
import { createInterface } from "node:readline/promises";
|
|
12
12
|
import type { ImageContent } from "@gajae-code/ai";
|
|
13
13
|
import {
|
|
14
|
-
$
|
|
14
|
+
$pickenv,
|
|
15
|
+
getAgentDir,
|
|
15
16
|
getProjectDir,
|
|
16
17
|
logger,
|
|
17
18
|
normalizePathForComparison,
|
|
@@ -534,7 +535,16 @@ interface InteractiveModeFactoryOptions {
|
|
|
534
535
|
type CreateInteractiveMode = (options: InteractiveModeFactoryOptions) => InteractiveMode;
|
|
535
536
|
|
|
536
537
|
type ResumePickerTerminalCheck = () => boolean;
|
|
537
|
-
type ListForResumePickerReadOnly = (cwd: string, sessionDir?: string) => Promise<SessionInfo[]>;
|
|
538
|
+
type ListForResumePickerReadOnly = (cwd: string, sessionDir?: string, storage?: undefined) => Promise<SessionInfo[]>;
|
|
539
|
+
|
|
540
|
+
type ListManagedForResumePickerReadOnly = (
|
|
541
|
+
cwd: string,
|
|
542
|
+
managedAgentDir?: string,
|
|
543
|
+
storage?: undefined,
|
|
544
|
+
) => Promise<SessionInfo[]>;
|
|
545
|
+
|
|
546
|
+
type ResolveManagedAgentDirForScope = (cwd: string) => string;
|
|
547
|
+
|
|
538
548
|
type SelectResumeSession = (sessions: SessionInfo[]) => Promise<SessionSelectionResult>;
|
|
539
549
|
type OpenExistingSessionStrict = (
|
|
540
550
|
identity: ResumeSessionIdentity,
|
|
@@ -546,6 +556,11 @@ type OpenExistingSessionStrict = (
|
|
|
546
556
|
export const BARE_RESUME_CONFLICT_ERROR =
|
|
547
557
|
"--resume without a session cannot be combined with --continue, --fork, or --no-session.";
|
|
548
558
|
export const BARE_RESUME_INTERACTIVE_ERROR = "--resume requires an interactive terminal; use --resume <id>.";
|
|
559
|
+
|
|
560
|
+
/** Resolves only the managed agent root needed for pre-consent picker inventory. */
|
|
561
|
+
export function resolveManagedAgentDirForScope(_cwd: string): string {
|
|
562
|
+
return getAgentDir();
|
|
563
|
+
}
|
|
549
564
|
export const BARE_RESUME_OPEN_ERROR = "Could not open the selected session. Use --resume <id>.";
|
|
550
565
|
|
|
551
566
|
function isBareResume(parsed: Args): boolean {
|
|
@@ -741,7 +756,13 @@ export async function createSessionManager(
|
|
|
741
756
|
if (forkSource.includes("/") || forkSource.includes("\\") || forkSource.endsWith(".jsonl")) {
|
|
742
757
|
return await SessionManager.forkFrom(forkSource, cwd, sessionDestination(), undefined, migrationPolicy);
|
|
743
758
|
}
|
|
744
|
-
const match = await resolveResumableSession(
|
|
759
|
+
const match = await resolveResumableSession(
|
|
760
|
+
forkSource,
|
|
761
|
+
cwd,
|
|
762
|
+
parsed.sessionDir,
|
|
763
|
+
undefined,
|
|
764
|
+
parsed.sessionDir ? undefined : activeSettings.getAgentDir(),
|
|
765
|
+
);
|
|
745
766
|
if (!match) {
|
|
746
767
|
throw new Error(`Session "${forkSource}" not found.`);
|
|
747
768
|
}
|
|
@@ -754,14 +775,18 @@ export async function createSessionManager(
|
|
|
754
775
|
if (typeof parsed.resume === "string") {
|
|
755
776
|
const sessionArg = parsed.resume;
|
|
756
777
|
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
SessionManager.explicitDestination(path.dirname(sessionArg))
|
|
760
|
-
|
|
761
|
-
migrationPolicy,
|
|
762
|
-
);
|
|
778
|
+
const destination = parsed.sessionDir
|
|
779
|
+
? SessionManager.explicitDestination(parsed.sessionDir)
|
|
780
|
+
: SessionManager.explicitDestination(path.dirname(sessionArg));
|
|
781
|
+
return await SessionManager.open(sessionArg, destination, undefined, migrationPolicy);
|
|
763
782
|
}
|
|
764
|
-
const match = await resolveResumableSession(
|
|
783
|
+
const match = await resolveResumableSession(
|
|
784
|
+
sessionArg,
|
|
785
|
+
cwd,
|
|
786
|
+
parsed.sessionDir,
|
|
787
|
+
undefined,
|
|
788
|
+
parsed.sessionDir ? undefined : activeSettings.getAgentDir(),
|
|
789
|
+
);
|
|
765
790
|
if (!match) {
|
|
766
791
|
throw new Error(`Session "${sessionArg}" not found.`);
|
|
767
792
|
}
|
|
@@ -1058,12 +1083,62 @@ export interface RunRootCommandDependencies {
|
|
|
1058
1083
|
runPrintMode?: RunPrintMode;
|
|
1059
1084
|
isResumePickerTerminal?: ResumePickerTerminalCheck;
|
|
1060
1085
|
listForResumePickerReadOnly?: ListForResumePickerReadOnly;
|
|
1086
|
+
listManagedForResumePickerReadOnly?: ListManagedForResumePickerReadOnly;
|
|
1087
|
+
resolveManagedAgentDirForScope?: ResolveManagedAgentDirForScope;
|
|
1061
1088
|
selectResumeSession?: SelectResumeSession;
|
|
1062
1089
|
openExistingSessionStrict?: OpenExistingSessionStrict;
|
|
1063
1090
|
initializeSettings?: typeof Settings.init;
|
|
1064
1091
|
loadSettingsForScope?: typeof Settings.loadForScope;
|
|
1065
1092
|
}
|
|
1066
1093
|
|
|
1094
|
+
export interface ModelRoleOverrides {
|
|
1095
|
+
smol?: string;
|
|
1096
|
+
slow?: string;
|
|
1097
|
+
plan?: string;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/**
|
|
1101
|
+
* Resolve the ephemeral `smol`/`slow`/`plan` model-role overrides.
|
|
1102
|
+
*
|
|
1103
|
+
* Precedence per role is CLI flag > documented `GJC_*_MODEL` > legacy
|
|
1104
|
+
* `PI_*_MODEL`, matching the repo-wide GJC-first/PI-fallback convention.
|
|
1105
|
+
* Resolution reads the process environment via `$pickenv`, which trims values
|
|
1106
|
+
* and treats empty/whitespace as unset; it is deliberately kept separate from
|
|
1107
|
+
* credential env resolution (`$credentialEnv`/`$pickCredentialEnv`). The
|
|
1108
|
+
* function is pure and stateless, so it reads fresh each call and a later
|
|
1109
|
+
* invocation never inherits an earlier one's values.
|
|
1110
|
+
*/
|
|
1111
|
+
export function resolveModelRoleOverrides(parsed: Pick<Args, "smol" | "slow" | "plan">): ModelRoleOverrides {
|
|
1112
|
+
const overrides: ModelRoleOverrides = {};
|
|
1113
|
+
const smol = parsed.smol ?? $pickenv("GJC_SMOL_MODEL", "PI_SMOL_MODEL");
|
|
1114
|
+
const slow = parsed.slow ?? $pickenv("GJC_SLOW_MODEL", "PI_SLOW_MODEL");
|
|
1115
|
+
const plan = parsed.plan ?? $pickenv("GJC_PLAN_MODEL", "PI_PLAN_MODEL");
|
|
1116
|
+
if (smol) overrides.smol = smol;
|
|
1117
|
+
if (slow) overrides.slow = slow;
|
|
1118
|
+
if (plan) overrides.plan = plan;
|
|
1119
|
+
return overrides;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/**
|
|
1123
|
+
* Apply the `--no-pty` / `--no-title` terminal-control flags to the environment.
|
|
1124
|
+
*
|
|
1125
|
+
* Sets the canonical `GJC_*` name (so an explicit flag wins over a user-set
|
|
1126
|
+
* `GJC_*` value under the GJC-first resolver — CLI authority) and the legacy
|
|
1127
|
+
* `PI_*` name for backward compatibility. `--acp` mode implies `--no-title`.
|
|
1128
|
+
*/
|
|
1129
|
+
export function applyTerminalControlFlagsToEnv(
|
|
1130
|
+
parsed: Pick<Args, "noPty" | "noTitle" | "mode">,
|
|
1131
|
+
env: NodeJS.ProcessEnv = Bun.env,
|
|
1132
|
+
): void {
|
|
1133
|
+
if (parsed.noPty) {
|
|
1134
|
+
env.GJC_NO_PTY = "1";
|
|
1135
|
+
env.PI_NO_PTY = "1";
|
|
1136
|
+
}
|
|
1137
|
+
if (parsed.noTitle || parsed.mode === "acp") {
|
|
1138
|
+
env.GJC_NO_TITLE = "1";
|
|
1139
|
+
env.PI_NO_TITLE = "1";
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1067
1142
|
export async function runRootCommand(
|
|
1068
1143
|
parsed: Args,
|
|
1069
1144
|
rawArgs: string[],
|
|
@@ -1094,10 +1169,18 @@ export async function runRootCommand(
|
|
|
1094
1169
|
await logger.time("maybeAutoChdir", maybeAutoChdir, parsedArgs);
|
|
1095
1170
|
autoChdirApplied = true;
|
|
1096
1171
|
const resumeCwd = getProjectDir();
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1172
|
+
const managedAgentDir = parsedArgs.sessionDir
|
|
1173
|
+
? undefined
|
|
1174
|
+
: (deps.resolveManagedAgentDirForScope ?? resolveManagedAgentDirForScope)(resumeCwd);
|
|
1175
|
+
const sessions = parsedArgs.sessionDir
|
|
1176
|
+
? await (deps.listForResumePickerReadOnly ?? SessionManager.listForResumePickerReadOnly)(
|
|
1177
|
+
resumeCwd,
|
|
1178
|
+
parsedArgs.sessionDir,
|
|
1179
|
+
)
|
|
1180
|
+
: await (deps.listManagedForResumePickerReadOnly ?? SessionManager.listManagedForResumePickerReadOnly)(
|
|
1181
|
+
resumeCwd,
|
|
1182
|
+
managedAgentDir,
|
|
1183
|
+
);
|
|
1101
1184
|
if (sessions.length === 0) {
|
|
1102
1185
|
process.stdout.write(`${chalk.dim("No sessions found")}\n`);
|
|
1103
1186
|
return;
|
|
@@ -1108,19 +1191,16 @@ export async function runRootCommand(
|
|
|
1108
1191
|
if (selection.kind === "cancelled") {
|
|
1109
1192
|
return;
|
|
1110
1193
|
}
|
|
1194
|
+
const scopedSettings = await (deps.loadSettingsForScope ?? Settings.loadForScope)({ cwd: resumeCwd });
|
|
1111
1195
|
const resumeMigrationPolicy =
|
|
1112
|
-
(
|
|
1113
|
-
"session.directoryMigration",
|
|
1114
|
-
) === "disabled"
|
|
1115
|
-
? "disabled"
|
|
1116
|
-
: "copy-retain";
|
|
1196
|
+
scopedSettings.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
|
|
1117
1197
|
let opened: StrictSessionOpenResult;
|
|
1118
1198
|
try {
|
|
1119
1199
|
opened = await (deps.openExistingSessionStrict ?? SessionManager.openExistingStrict)(
|
|
1120
1200
|
selection.identity,
|
|
1121
1201
|
parsedArgs.sessionDir
|
|
1122
1202
|
? SessionManager.explicitDestination(parsedArgs.sessionDir)
|
|
1123
|
-
: SessionManager.managedDestination(resumeCwd,
|
|
1203
|
+
: SessionManager.managedDestination(resumeCwd, scopedSettings.getAgentDir()),
|
|
1124
1204
|
undefined,
|
|
1125
1205
|
resumeMigrationPolicy,
|
|
1126
1206
|
);
|
|
@@ -1198,12 +1278,7 @@ export async function runRootCommand(
|
|
|
1198
1278
|
applyAcpDefaultSettingOverrides(settingsInstance);
|
|
1199
1279
|
}
|
|
1200
1280
|
modelRegistry.applyConfiguredModelBindings(settingsInstance);
|
|
1201
|
-
|
|
1202
|
-
Bun.env.PI_NO_PTY = "1";
|
|
1203
|
-
}
|
|
1204
|
-
if (parsedArgs.noTitle || parsedArgs.mode === "acp") {
|
|
1205
|
-
Bun.env.PI_NO_TITLE = "1";
|
|
1206
|
-
}
|
|
1281
|
+
applyTerminalControlFlagsToEnv(parsedArgs);
|
|
1207
1282
|
const hasPreparedInput = parsedArgs.messages.length > 0 || parsedArgs.fileArgs.length > 0;
|
|
1208
1283
|
const { pipedInput, fileText, fileImages } = await logger.time("prepareInitialMessage", async () => {
|
|
1209
1284
|
const pipedInput =
|
|
@@ -1252,15 +1327,14 @@ export async function runRootCommand(
|
|
|
1252
1327
|
// Initialize discovery system with settings for provider persistence
|
|
1253
1328
|
logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
|
|
1254
1329
|
|
|
1255
|
-
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
|
|
1256
|
-
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
if (smolModel || slowModel || planModel) {
|
|
1330
|
+
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted).
|
|
1331
|
+
// Precedence per role: CLI flag > documented GJC_*_MODEL > legacy PI_*_MODEL.
|
|
1332
|
+
const roleOverrides = resolveModelRoleOverrides(parsedArgs);
|
|
1333
|
+
if (roleOverrides.smol || roleOverrides.slow || roleOverrides.plan) {
|
|
1260
1334
|
settingsInstance.overrideModelRoles({
|
|
1261
|
-
smol:
|
|
1262
|
-
slow:
|
|
1263
|
-
plan:
|
|
1335
|
+
...(roleOverrides.smol ? { smol: roleOverrides.smol } : {}),
|
|
1336
|
+
...(roleOverrides.slow ? { slow: roleOverrides.slow } : {}),
|
|
1337
|
+
...(roleOverrides.plan ? { plan: roleOverrides.plan } : {}),
|
|
1264
1338
|
});
|
|
1265
1339
|
}
|
|
1266
1340
|
|
|
@@ -1554,9 +1628,9 @@ export async function runRootCommand(
|
|
|
1554
1628
|
process.stdout.write(`${chalk.dim(`Model scope: ${modelList} ${chalk.gray("(Alt+N to cycle)")}`)}\n`);
|
|
1555
1629
|
}
|
|
1556
1630
|
|
|
1557
|
-
if ($
|
|
1631
|
+
if ($pickenv("GJC_TIMING", "PI_TIMING")) {
|
|
1558
1632
|
logger.printTimings();
|
|
1559
|
-
exitForTiming = $
|
|
1633
|
+
exitForTiming = $pickenv("GJC_TIMING", "PI_TIMING") === "x";
|
|
1560
1634
|
}
|
|
1561
1635
|
|
|
1562
1636
|
if (!exitForTiming) {
|
|
@@ -1601,7 +1675,7 @@ export async function runRootCommand(
|
|
|
1601
1675
|
initialImages,
|
|
1602
1676
|
suppressProcessExit: deps.suppressProcessExit,
|
|
1603
1677
|
});
|
|
1604
|
-
if ($
|
|
1678
|
+
if ($pickenv("GJC_TIMING", "PI_TIMING")) {
|
|
1605
1679
|
logger.printTimings();
|
|
1606
1680
|
}
|
|
1607
1681
|
} finally {
|
package/src/modes/DESIGN.md
CHANGED
|
@@ -94,6 +94,11 @@ empty space and list density for an operational setup flow.
|
|
|
94
94
|
removing line breaks. Notification secrets will use the dedicated masked
|
|
95
95
|
input from Work item 6; they must never appear in list values, descriptions,
|
|
96
96
|
previews, artifacts, or logs.
|
|
97
|
+
### Shortcut labels and binding authority
|
|
98
|
+
|
|
99
|
+
Keybinding configuration is a portable canonical grammar: textual key IDs use `ctrl`, `alt`, `shift`, and `super` plus a key name (for example, `ctrl+p` or `alt+enter`). Do not serialize or require display-only labels. Runtime UI renders those IDs through the shared formatter for its explicit platform context; macOS uses MacBook-style glyphs (`⌃`, `⌥`, `⇧`, `⌘`, `↩`, `⎋`, `⇥`, `⌫`, `⌦`, and arrow glyphs) while other platforms use textual labels. A glyph is never configuration syntax.
|
|
100
|
+
|
|
101
|
+
Static onboarding and generated documentation have authority only over shipped defaults. Keep generated tables host-independent by showing canonical textual IDs, not the capture host's labels. The runtime `KeybindingsManager` owns the effective binding set after user remaps and extensions load; `/hotkeys` and runtime hints must render that effective set with the platform context injected by their host. Do not let a static onboarding hint imply that it reflects remaps.
|
|
97
102
|
|
|
98
103
|
### Status, errors, confirmation, and disabled work
|
|
99
104
|
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
AcpSdkAdapterError,
|
|
43
43
|
} from "../../sdk/acp";
|
|
44
44
|
import { ensureBroker } from "../../sdk/broker/ensure";
|
|
45
|
-
import { readSdkBrokerDiscovery, SdkClient } from "../../sdk/client";
|
|
45
|
+
import { readSdkBrokerDiscovery, SdkClient, SdkClientError } from "../../sdk/client";
|
|
46
46
|
import { mapAgentWireEventPayloadToAcpSessionUpdates } from "./acp-event-mapper";
|
|
47
47
|
import { resolveAcpPermissionMode } from "./permission-mode";
|
|
48
48
|
import type { AcpStartupOptions } from "./startup-options";
|
|
@@ -88,6 +88,7 @@ type PendingAttachment = { epoch: number; task: Promise<void> };
|
|
|
88
88
|
type SessionRecord = {
|
|
89
89
|
cwd: string;
|
|
90
90
|
adapter: AcpSdkAdapter;
|
|
91
|
+
closeIdempotencyKey: string;
|
|
91
92
|
unsubscribe: () => void;
|
|
92
93
|
reconnectUnsubscribe: () => void;
|
|
93
94
|
/** Per-session frame work queue; callbacks never race prompt ownership. */
|
|
@@ -571,8 +572,10 @@ export class AcpAgent implements Agent {
|
|
|
571
572
|
readonly #attaching = new Map<string, PendingAttachment>();
|
|
572
573
|
readonly #resolvingExisting = new Map<string, PendingAttachment>();
|
|
573
574
|
readonly #knownSessionCwds = new Map<string, string>();
|
|
575
|
+
readonly #pendingCloseIdempotencyKeys = new Map<string, string>();
|
|
574
576
|
readonly #sessionEpochs = new Map<string, number>();
|
|
575
577
|
readonly #tearingDown = new Map<string, number>();
|
|
578
|
+
readonly #closing = new Map<string, Promise<CloseSessionResponse>>();
|
|
576
579
|
#clientCapabilities: ClientCapabilities | undefined;
|
|
577
580
|
#broker: Promise<BrokerConnection> | undefined;
|
|
578
581
|
readonly #startupOptions: AcpStartupOptions | undefined;
|
|
@@ -730,19 +733,21 @@ export class AcpAgent implements Agent {
|
|
|
730
733
|
return paginateAcpSessions(listed, params.cwd ?? undefined, this.#cursor(params.cursor));
|
|
731
734
|
}
|
|
732
735
|
|
|
733
|
-
|
|
736
|
+
closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> {
|
|
734
737
|
const record = this.#sessions.get(params.sessionId);
|
|
735
738
|
const cwd = record?.cwd ?? this.#knownSessionCwds.get(params.sessionId);
|
|
736
739
|
// ACP close has no cwd. Only connection-owned sessions may reach broker lifecycle control.
|
|
737
|
-
if (!cwd) return {};
|
|
738
|
-
this.#
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
this.#
|
|
745
|
-
}
|
|
740
|
+
if (!cwd) return Promise.resolve({});
|
|
741
|
+
const existing = this.#closing.get(params.sessionId);
|
|
742
|
+
if (existing) return existing;
|
|
743
|
+
const deferred = Promise.withResolvers<CloseSessionResponse>();
|
|
744
|
+
this.#closing.set(params.sessionId, deferred.promise);
|
|
745
|
+
void this.#closeOwnedSession(params.sessionId).then(deferred.resolve, deferred.reject);
|
|
746
|
+
const cleanup = deferred.promise.finally(() => {
|
|
747
|
+
if (this.#closing.get(params.sessionId) === deferred.promise) this.#closing.delete(params.sessionId);
|
|
748
|
+
});
|
|
749
|
+
void cleanup.catch(() => undefined);
|
|
750
|
+
return deferred.promise;
|
|
746
751
|
}
|
|
747
752
|
|
|
748
753
|
async deleteSession(params: DeleteSessionRequest): Promise<DeleteSessionResponse> {
|
|
@@ -932,6 +937,15 @@ export class AcpAgent implements Agent {
|
|
|
932
937
|
);
|
|
933
938
|
}
|
|
934
939
|
|
|
940
|
+
#isDefinitiveBrokerResponse(error: unknown): boolean {
|
|
941
|
+
// Response-derived client errors retain the broker error as details. Responses
|
|
942
|
+
// that represent ongoing or ambiguous lifecycle work must keep their key.
|
|
943
|
+
if (!(error instanceof SdkClientError)) return false;
|
|
944
|
+
const details = object(error.details);
|
|
945
|
+
if (details?.code !== error.code || details.message !== error.message) return false;
|
|
946
|
+
return !["terminal_uncertain", "cleanup_pending", "broker_restarting", "unavailable"].includes(error.code);
|
|
947
|
+
}
|
|
948
|
+
|
|
935
949
|
async #attachExisting(id: string, cwd: string): Promise<void> {
|
|
936
950
|
const epoch = this.#sessionEpoch(id);
|
|
937
951
|
const attached = this.#sessions.get(id);
|
|
@@ -1045,6 +1059,7 @@ export class AcpAgent implements Agent {
|
|
|
1045
1059
|
const record: SessionRecord = {
|
|
1046
1060
|
cwd,
|
|
1047
1061
|
adapter,
|
|
1062
|
+
closeIdempotencyKey: randomUUID(),
|
|
1048
1063
|
unsubscribe: () => {},
|
|
1049
1064
|
reconnectUnsubscribe: () => {},
|
|
1050
1065
|
frameTail: Promise.resolve(),
|
|
@@ -1059,6 +1074,7 @@ export class AcpAgent implements Agent {
|
|
|
1059
1074
|
this.#knownSessionCwds.set(id, cwd);
|
|
1060
1075
|
await applyAcpPermissionMode(adapter, this.#clientCapabilities);
|
|
1061
1076
|
this.#assertSessionEpoch(id, epoch);
|
|
1077
|
+
this.#pendingCloseIdempotencyKeys.delete(id);
|
|
1062
1078
|
} catch (error) {
|
|
1063
1079
|
if (adapter && this.#sessions.get(id)?.adapter === adapter) {
|
|
1064
1080
|
try {
|
|
@@ -1103,6 +1119,21 @@ export class AcpAgent implements Agent {
|
|
|
1103
1119
|
this.#knownSessionCwds.delete(id);
|
|
1104
1120
|
}
|
|
1105
1121
|
|
|
1122
|
+
async #closeOwnedSession(id: string): Promise<CloseSessionResponse> {
|
|
1123
|
+
this.#beginTeardown(id);
|
|
1124
|
+
try {
|
|
1125
|
+
const attaching = this.#attaching.get(id);
|
|
1126
|
+
// The record is published before permission initialization. Let a canceled
|
|
1127
|
+
// provisional attachment retire it before selecting the generation key.
|
|
1128
|
+
if (attaching) await Promise.allSettled([attaching.task]);
|
|
1129
|
+
await this.#teardownSession(id, "closed", true);
|
|
1130
|
+
this.#knownSessionCwds.delete(id);
|
|
1131
|
+
return {};
|
|
1132
|
+
} finally {
|
|
1133
|
+
this.#finishTeardown(id);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1106
1137
|
/**
|
|
1107
1138
|
* All local session disposal follows one path: remove ownership and reject a
|
|
1108
1139
|
* waiting prompt before any awaited socket or broker work. A failed close is
|
|
@@ -1130,13 +1161,13 @@ export class AcpAgent implements Agent {
|
|
|
1130
1161
|
failures.push(error);
|
|
1131
1162
|
}
|
|
1132
1163
|
if (closeRemote) {
|
|
1164
|
+
const closeIdempotencyKey =
|
|
1165
|
+
record?.closeIdempotencyKey ?? this.#pendingCloseIdempotencyKeys.get(id) ?? randomUUID();
|
|
1166
|
+
this.#pendingCloseIdempotencyKeys.set(id, closeIdempotencyKey);
|
|
1133
1167
|
try {
|
|
1134
|
-
await (await this.#brokerAdapter()).global(
|
|
1135
|
-
"session.close",
|
|
1136
|
-
{ sessionId: id },
|
|
1137
|
-
this.#lifecycleIdempotencyKey(id, "session.close"),
|
|
1138
|
-
);
|
|
1168
|
+
await (await this.#brokerAdapter()).global("session.close", { sessionId: id }, closeIdempotencyKey);
|
|
1139
1169
|
} catch (error) {
|
|
1170
|
+
if (this.#isDefinitiveBrokerResponse(error)) this.#pendingCloseIdempotencyKeys.delete(id);
|
|
1140
1171
|
if (!(ownershipBound && this.#isAlreadyGone(error))) failures.push(error);
|
|
1141
1172
|
}
|
|
1142
1173
|
}
|
|
@@ -1146,6 +1177,7 @@ export class AcpAgent implements Agent {
|
|
|
1146
1177
|
.join("; ");
|
|
1147
1178
|
throw aggregateAcpFailure("terminal_uncertain", `ACP session cleanup is uncertain: ${detail}`, failures);
|
|
1148
1179
|
}
|
|
1180
|
+
if (closeRemote) this.#pendingCloseIdempotencyKeys.delete(id);
|
|
1149
1181
|
} finally {
|
|
1150
1182
|
this.#finishTeardown(id);
|
|
1151
1183
|
}
|
|
@@ -1494,6 +1526,8 @@ export class AcpAgent implements Agent {
|
|
|
1494
1526
|
this.#attaching.clear();
|
|
1495
1527
|
this.#resolvingExisting.clear();
|
|
1496
1528
|
this.#knownSessionCwds.clear();
|
|
1529
|
+
this.#pendingCloseIdempotencyKeys.clear();
|
|
1530
|
+
if (this.#closing.size === 0) this.#closing.clear();
|
|
1497
1531
|
this.#tearingDown.clear();
|
|
1498
1532
|
if (this.#broker) {
|
|
1499
1533
|
const broker = this.#broker;
|