@gajae-code/coding-agent 0.18.12 → 0.19.0
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 +467 -0
- package/dist/types/capability/rule.d.ts +7 -2
- package/dist/types/cli/append-system-prompt.d.ts +6 -0
- package/dist/types/cli/args.d.ts +7 -2
- package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
- package/dist/types/cli/customize-doctor.d.ts +4 -3
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/root-flags.d.ts +0 -12
- package/dist/types/cli/update-cli.d.ts +7 -2
- package/dist/types/cli-main.d.ts +0 -6
- package/dist/types/commands/launch.d.ts +0 -6
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +8 -0
- package/dist/types/commit/map-reduce/index.d.ts +4 -4
- package/dist/types/config/autorouting-tier-map.d.ts +23 -1
- package/dist/types/config/config-hot-reload.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +44 -2
- package/dist/types/config/model-profile-activation.d.ts +19 -5
- package/dist/types/config/model-registry.d.ts +16 -3
- package/dist/types/config/model-resolver.d.ts +5 -4
- package/dist/types/config/settings-schema.d.ts +11 -1
- package/dist/types/coordinator-mcp/server.d.ts +4 -0
- package/dist/types/customization/import.d.ts +2 -0
- package/dist/types/customization/types.d.ts +1 -1
- package/dist/types/dap/session.d.ts +11 -2
- package/dist/types/discovery/helpers.d.ts +7 -2
- package/dist/types/edit/index.d.ts +5 -0
- package/dist/types/edit/modes/patch.d.ts +9 -0
- package/dist/types/eval/py/runtime.d.ts +9 -0
- package/dist/types/export/ttsr.d.ts +19 -0
- package/dist/types/extensibility/extensions/types.d.ts +5 -1
- package/dist/types/extensibility/slash-commands.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
- package/dist/types/hindsight/content.d.ts +5 -6
- package/dist/types/internal-urls/types.d.ts +2 -0
- package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
- package/dist/types/main.d.ts +23 -18
- package/dist/types/migrate/action-planner.d.ts +10 -1
- package/dist/types/migrate/adapters/codex.d.ts +1 -1
- package/dist/types/migrate/adapters/index.d.ts +35 -3
- package/dist/types/migrate/hook-module.d.ts +23 -0
- package/dist/types/migrate/report.d.ts +2 -1
- package/dist/types/migrate/types.d.ts +69 -2
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +43 -0
- package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +15 -11
- package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
- package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
- package/dist/types/sdk/broker/broker.d.ts +22 -1
- package/dist/types/sdk/broker/ensure.d.ts +5 -2
- package/dist/types/sdk/broker/hop.d.ts +9 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
- package/dist/types/sdk/broker/session-index.d.ts +2 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/index.d.ts +21 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
- package/dist/types/sdk/host/control/operations.d.ts +5 -1
- package/dist/types/sdk/host/events.d.ts +4 -0
- package/dist/types/sdk/host/host.d.ts +2 -1
- package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
- package/dist/types/sdk/host/session-runtime.d.ts +8 -8
- package/dist/types/sdk/host/surface-policy.d.ts +2 -1
- package/dist/types/sdk/lifecycle/service.d.ts +12 -0
- package/dist/types/sdk/prompt-status.d.ts +11 -4
- package/dist/types/session/agent-session.d.ts +50 -8
- package/dist/types/session/session-manager.d.ts +16 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
- package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
- package/dist/types/ssh/ssh-executor.d.ts +8 -1
- package/dist/types/system-prompt.d.ts +3 -2
- package/dist/types/task/role-body.d.ts +11 -0
- package/dist/types/tools/awaiting-input.d.ts +28 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +4 -0
- package/dist/types/tools/bash.d.ts +7 -0
- package/dist/types/tools/descriptors.d.ts +13 -2
- package/dist/types/tools/fetch.d.ts +6 -0
- package/dist/types/tools/irc.d.ts +5 -3
- package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/file-mentions.d.ts +5 -0
- package/dist/types/utils/image-limits.d.ts +7 -0
- package/dist/types/utils/image-loading.d.ts +0 -1
- package/dist/types/utils/open.d.ts +0 -1
- package/dist/types/utils/pasted-image-loading.d.ts +1 -5
- package/dist/types/utils/prompt-suggestion.d.ts +2 -2
- package/dist/types/utils/shell-snapshot.d.ts +2 -0
- package/dist/types/utils/title-generator.d.ts +13 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -7
- package/package.json +9 -8
- package/scripts/acp-conformance-agent.ts +12 -2
- package/scripts/generate-sdk-operation-inventory.ts +16 -0
- package/src/capability/fs.ts +31 -3
- package/src/capability/rule.ts +11 -2
- package/src/cli/append-system-prompt.ts +12 -0
- package/src/cli/args.ts +11 -7
- package/src/cli/customize-doctor-precedence.ts +31 -0
- package/src/cli/customize-doctor.ts +49 -39
- package/src/cli/fast-help.ts +0 -2
- package/src/cli/migrate-cli.ts +14 -3
- package/src/cli/public-command-errors.ts +2 -0
- package/src/cli/root-flags.ts +0 -2
- package/src/cli/update-cli.ts +247 -6
- package/src/commands/harness.ts +25 -15
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +81 -11
- package/src/commit/agentic/agent.ts +6 -6
- package/src/commit/analysis/conventional.ts +14 -5
- package/src/commit/map-reduce/index.ts +7 -7
- package/src/commit/pipeline.ts +12 -12
- package/src/config/autorouting-tier-map.ts +20 -1
- package/src/config/config-file.ts +106 -3
- package/src/config/config-hot-reload.ts +8 -0
- package/src/config/file-lock.ts +1431 -304
- package/src/config/keybindings.ts +154 -7
- package/src/config/model-profile-activation.ts +177 -39
- package/src/config/model-registry.ts +380 -55
- package/src/config/model-resolver.ts +49 -21
- package/src/config/settings-schema.ts +13 -1
- package/src/config/settings.ts +2 -0
- package/src/coordinator-mcp/server.ts +37 -2
- package/src/crash/sanitize.ts +3 -1
- package/src/customization/import.ts +276 -7
- package/src/customization/inventory.ts +24 -8
- package/src/customization/types.ts +1 -0
- package/src/dap/client.ts +3 -2
- package/src/dap/session.ts +161 -36
- package/src/debug/crash-diagnostics.ts +87 -5
- package/src/debug/report-bundle.ts +71 -2
- package/src/discovery/builtin.ts +4 -3
- package/src/discovery/helpers.ts +24 -14
- package/src/edit/index.ts +191 -5
- package/src/edit/modes/patch.ts +27 -0
- package/src/eval/py/runtime.ts +44 -4
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/html/index.ts +60 -20
- package/src/export/ttsr.ts +176 -1
- package/src/extensibility/extensions/loader.ts +2 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
- package/src/extensibility/extensions/types.ts +5 -1
- package/src/extensibility/hooks/loader.ts +10 -2
- package/src/extensibility/plugins/marketplace/manager.ts +36 -10
- package/src/extensibility/slash-commands.ts +5 -0
- package/src/gjc-runtime/gc-runtime.ts +4 -1
- package/src/gjc-runtime/launch-tmux.ts +23 -13
- package/src/gjc-runtime/ralplan-current-final.ts +294 -0
- package/src/gjc-runtime/ralplan-runtime.ts +95 -1
- package/src/gjc-runtime/state-writer.ts +109 -5
- package/src/gjc-runtime/tmux-sessions.ts +93 -13
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
- package/src/gjc-runtime/ultragoal-guard.ts +9 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
- package/src/harness-control-plane/finalize.ts +11 -0
- package/src/harness-control-plane/receipt-spool.ts +105 -28
- package/src/harness-control-plane/storage.ts +124 -6
- package/src/hindsight/content.ts +116 -16
- package/src/hindsight/mental-models.ts +22 -10
- package/src/hindsight/state.ts +7 -4
- package/src/hooks/native-skill-hook.ts +3 -1
- package/src/internal-urls/artifact-protocol.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +14 -14
- package/src/internal-urls/local-protocol.ts +28 -3
- package/src/internal-urls/types.ts +2 -0
- package/src/lsp/client.ts +2 -1
- package/src/lsp/clients/biome-client.ts +2 -0
- package/src/lsp/clients/swiftlint-client.ts +2 -0
- package/src/main.ts +330 -92
- package/src/memories/index.ts +25 -2
- package/src/migrate/action-planner.ts +517 -2
- package/src/migrate/adapters/claude-code.ts +141 -3
- package/src/migrate/adapters/codex.ts +57 -5
- package/src/migrate/adapters/index.ts +240 -5
- package/src/migrate/adapters/opencode.ts +4 -0
- package/src/migrate/executor.ts +141 -3
- package/src/migrate/hook-module.ts +193 -0
- package/src/migrate/report.ts +45 -1
- package/src/migrate/types.ts +73 -3
- package/src/modes/acp/acp-agent.ts +10 -1
- package/src/modes/components/agent-dashboard.ts +20 -4
- package/src/modes/components/customization/customization-dashboard.ts +7 -0
- package/src/modes/components/customization/import-wizard.ts +29 -3
- package/src/modes/components/model-selector.ts +157 -103
- package/src/modes/components/welcome.ts +3 -1
- package/src/modes/controllers/command-controller.ts +271 -16
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +31 -1
- package/src/modes/controllers/goal-mode-controller.ts +79 -10
- package/src/modes/controllers/input-controller.ts +2 -0
- package/src/modes/controllers/plan-mode-controller.ts +49 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
- package/src/modes/controllers/selector-controller.ts +14 -1
- package/src/modes/interactive-mode.ts +55 -4
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +5 -1
- package/src/modes/utils/host-status.ts +21 -10
- package/src/prompts/agent-fragments/browser-aside.md +2 -2
- package/src/prompts/system/irc-incoming.md +3 -1
- package/src/prompts/tools/irc.md +1 -1
- package/src/runtime-mcp/manager.ts +8 -3
- package/src/runtime-mcp/project-host-values.ts +12 -8
- package/src/runtime-mcp/tool-bridge.ts +68 -4
- package/src/runtime-mcp/transports/stdio.ts +2 -1
- package/src/sdk/broker/broker-exit.ts +2 -0
- package/src/sdk/broker/broker.ts +143 -4
- package/src/sdk/broker/ensure.ts +74 -6
- package/src/sdk/broker/hop.ts +13 -1
- package/src/sdk/broker/lifecycle-ledger.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +102 -6
- package/src/sdk/broker/session-index.ts +29 -4
- package/src/sdk/broker/transport.ts +17 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +408 -14
- package/src/sdk/bus/session-control.ts +2 -0
- package/src/sdk/bus/slack-daemon.ts +105 -9
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -2
- package/src/sdk/bus/threaded-render.ts +20 -1
- package/src/sdk/cli/session-cli.ts +2 -0
- package/src/sdk/host/control/dispatch.ts +13 -1
- package/src/sdk/host/control/operations.ts +10 -1
- package/src/sdk/host/events.ts +67 -4
- package/src/sdk/host/host.ts +8 -2
- package/src/sdk/host/prompt-image-upload.ts +438 -0
- package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
- package/src/sdk/host/session-runtime.test.ts +1537 -24
- package/src/sdk/host/session-runtime.ts +332 -36
- package/src/sdk/host/surface-policy.ts +4 -0
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
- package/src/sdk/host/websocket-transport.ts +19 -7
- package/src/sdk/lifecycle/service.ts +14 -1
- package/src/sdk/prompt-status.ts +6 -4
- package/src/sdk/protocol/adapter-validation.ts +28 -1
- package/src/sdk/protocol/operation-inventory.generated.json +160 -0
- package/src/sdk/protocol/operation-registry.ts +33 -3
- package/src/sdk/session.ts +123 -36
- package/src/session/agent-session.ts +1614 -329
- package/src/session/history-storage.ts +182 -12
- package/src/session/internal/managed-session-scope.ts +47 -18
- package/src/session/internal/managed-session-storage.ts +71 -1
- package/src/session/session-manager.ts +90 -5
- package/src/session-import/redact.ts +53 -2
- package/src/skill-state/workflow-mutation-guard.ts +1498 -18
- package/src/slash-commands/builtin-registry.ts +6 -3
- package/src/slash-commands/helpers/mcp.ts +11 -3
- package/src/ssh/ssh-executor.ts +9 -2
- package/src/stt/recorder.ts +26 -9
- package/src/system-prompt.ts +61 -16
- package/src/task/executor.ts +12 -8
- package/src/task/role-body.ts +84 -0
- package/src/task/token-log-mode.test.ts +162 -0
- package/src/task/token-log.ts +91 -2
- package/src/tools/archive-reader.ts +650 -37
- package/src/tools/ask.ts +1 -0
- package/src/tools/ast-grep.ts +7 -2
- package/src/tools/auto-generated-guard.ts +12 -3
- package/src/tools/awaiting-input.ts +114 -0
- package/src/tools/bash-allowed-prefixes.ts +61 -1
- package/src/tools/bash-skill-urls.ts +58 -5
- package/src/tools/bash.ts +99 -26
- package/src/tools/browser/tab-worker.ts +39 -13
- package/src/tools/browser.ts +17 -7
- package/src/tools/cron.ts +10 -1
- package/src/tools/debug.ts +9 -2
- package/src/tools/descriptors.test.ts +4 -4
- package/src/tools/descriptors.ts +16 -4
- package/src/tools/eval.ts +15 -0
- package/src/tools/fetch.ts +90 -2
- package/src/tools/gh.ts +27 -12
- package/src/tools/image-gen.ts +20 -6
- package/src/tools/irc.ts +5 -3
- package/src/tools/monitor.ts +9 -1
- package/src/tools/output-meta.ts +51 -3
- package/src/tools/read.ts +9 -2
- package/src/tools/recipe/runner.ts +23 -12
- package/src/tools/render-utils.ts +2 -2
- package/src/tools/search.ts +3 -2
- package/src/tools/ssh.ts +274 -8
- package/src/tools/subagent.ts +10 -4
- package/src/tools/tmux-self-injection-guard.ts +3301 -122
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/tools/ultragoal-ask-guard.ts +13 -1
- package/src/tools/write.ts +7 -0
- package/src/tui/tree-list.ts +15 -1
- package/src/utils/commit-message-generator.ts +3 -2
- package/src/utils/external-editor.ts +21 -7
- package/src/utils/file-mentions.ts +46 -6
- package/src/utils/git.ts +38 -7
- package/src/utils/image-limits.ts +8 -0
- package/src/utils/image-loading.ts +1 -1
- package/src/utils/open.ts +73 -4
- package/src/utils/pasted-image-loading.ts +9 -8
- package/src/utils/prompt-suggestion.ts +5 -4
- package/src/utils/shell-snapshot.ts +9 -1
- package/src/utils/title-generator.ts +65 -20
- package/src/web/search/index.ts +2 -1
- package/src/web/search/providers/codex.ts +29 -32
|
@@ -237,8 +237,8 @@ function assertManagedParentSync(localRoot: string): void {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
240
|
+
* Keep a pinned root unless a changed inode is validated as a settled
|
|
241
|
+
* recreation. A root created by this call may be rebound after migration.
|
|
242
242
|
*/
|
|
243
243
|
function acceptManagedRootForInit(
|
|
244
244
|
localRoot: string,
|
|
@@ -250,6 +250,7 @@ function acceptManagedRootForInit(
|
|
|
250
250
|
if (sameManagedRoot(rootStat, recorded)) return;
|
|
251
251
|
const pending = pendingCreatedRoots.get(localRoot);
|
|
252
252
|
if (pending && sameManagedRoot(rootStat, pending)) return;
|
|
253
|
+
if (isSettledRecreatedManagedRoot(localRoot, rootStat)) return;
|
|
253
254
|
throw new Error("Unsafe local:// root");
|
|
254
255
|
}
|
|
255
256
|
if (recorded || adoptedExisting) {
|
|
@@ -259,6 +260,28 @@ function acceptManagedRootForInit(
|
|
|
259
260
|
if (isUnsafeDirectory(rootStat)) throw new Error("Unsafe local:// root");
|
|
260
261
|
}
|
|
261
262
|
|
|
263
|
+
/**
|
|
264
|
+
* A pinned root whose inode changed is still ours when a gjc initializer
|
|
265
|
+
* completed it: the temp sweeper (or `gjc gc`) removed the directory and
|
|
266
|
+
* another process for the same session recreated it, writing a settled
|
|
267
|
+
* migration marker. POSIX owner and write-bit checks on the root and parent
|
|
268
|
+
* protect this exception. Windows changed-inode adoption stays fail-closed
|
|
269
|
+
* until this path validates the replacement's identity and ACLs.
|
|
270
|
+
*/
|
|
271
|
+
function isSettledRecreatedManagedRoot(localRoot: string, rootStat: DirectoryIdentityStat): boolean {
|
|
272
|
+
if (process.platform === "win32") return false;
|
|
273
|
+
try {
|
|
274
|
+
assertAdoptedManagedRoot(rootStat);
|
|
275
|
+
assertManagedParentSync(localRoot);
|
|
276
|
+
const marker = path.join(localRoot, LEGACY_MIGRATION_MARKER);
|
|
277
|
+
const markerStat = fsSync.lstatSync(marker);
|
|
278
|
+
if (!markerStat.isFile() || markerStat.isSymbolicLink()) return false;
|
|
279
|
+
return isSettledMigrationMarkerValue(fsSync.readFileSync(marker, "utf8"));
|
|
280
|
+
} catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
262
285
|
const LEGACY_MIGRATION_MARKER = ".gjc-local-legacy-migrated-v1";
|
|
263
286
|
const MAX_LEGACY_LOCAL_BYTES = 64 * 1024 * 1024;
|
|
264
287
|
|
|
@@ -714,7 +737,9 @@ function initializeLocalRootSyncWhenLegacyAbsent(options: LocalProtocolOptions,
|
|
|
714
737
|
return;
|
|
715
738
|
}
|
|
716
739
|
const pending = pendingCreatedRoots.get(localRoot);
|
|
717
|
-
if (!(pending && sameManagedRoot(present, pending))
|
|
740
|
+
if (!(pending && sameManagedRoot(present, pending)) && !isSettledRecreatedManagedRoot(localRoot, present)) {
|
|
741
|
+
throw new Error("Unsafe local:// root");
|
|
742
|
+
}
|
|
718
743
|
}
|
|
719
744
|
}
|
|
720
745
|
const scratchParent = path.dirname(localRoot);
|
|
@@ -21,6 +21,8 @@ export interface InternalResource {
|
|
|
21
21
|
size?: number;
|
|
22
22
|
/** Underlying filesystem path (for debugging, not exposed to agent) */
|
|
23
23
|
sourcePath?: string;
|
|
24
|
+
/** True when model-facing reads must wrap the selected content as untrusted. */
|
|
25
|
+
untrusted?: boolean;
|
|
24
26
|
/** Additional notes about resolution */
|
|
25
27
|
notes?: string[];
|
|
26
28
|
/**
|
package/src/lsp/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isEnoent, isKnownSinkPeerClosedError, logger, untilAborted } from "@gajae-code/utils";
|
|
2
|
+
import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
|
|
2
3
|
import { formatCrashDiagnosticNotice, writeCrashReport } from "../debug/crash-diagnostics";
|
|
3
4
|
import { registerResourceOwner, spawnOwnedProcess } from "../runtime/process-lifecycle";
|
|
4
5
|
import { ToolAbortError, throwIfAborted } from "../tools/tool-errors";
|
|
@@ -538,7 +539,7 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
538
539
|
const owner = spawnOwnedProcess([command, ...args], {
|
|
539
540
|
cwd,
|
|
540
541
|
stdin: "pipe",
|
|
541
|
-
env:
|
|
542
|
+
env: { ...projectSafeSpawnEnv(), ...env },
|
|
542
543
|
name: `lsp:${config.command}`,
|
|
543
544
|
});
|
|
544
545
|
const proc = owner.child;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Uses Biome's CLI with JSON output instead of LSP (which has stale diagnostics issues).
|
|
4
4
|
*/
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
|
|
6
7
|
import type { Diagnostic, DiagnosticSeverity, LinterClient, ServerConfig } from "../../lsp/types";
|
|
7
8
|
|
|
8
9
|
// =============================================================================
|
|
@@ -84,6 +85,7 @@ async function runBiome(
|
|
|
84
85
|
try {
|
|
85
86
|
const proc = Bun.spawn([command, ...args], {
|
|
86
87
|
cwd,
|
|
88
|
+
env: projectSafeSpawnEnv(),
|
|
87
89
|
stdout: "pipe",
|
|
88
90
|
stderr: "pipe",
|
|
89
91
|
windowsHide: true,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* SwiftLint CLI-based linter client.
|
|
3
3
|
* Parses SwiftLint's JSON reporter output into LSP Diagnostic format.
|
|
4
4
|
*/
|
|
5
|
+
import { projectSafeSpawnEnv } from "@gajae-code/utils/shell-config";
|
|
5
6
|
import type { Diagnostic, DiagnosticSeverity, LinterClient, ServerConfig } from "../../lsp/types";
|
|
6
7
|
|
|
7
8
|
/** Shape of a single violation from `swiftlint lint --reporter json`. */
|
|
@@ -36,6 +37,7 @@ async function runSwiftLint(
|
|
|
36
37
|
try {
|
|
37
38
|
const proc = Bun.spawn([command, ...args], {
|
|
38
39
|
cwd,
|
|
40
|
+
env: projectSafeSpawnEnv(),
|
|
39
41
|
stdout: "pipe",
|
|
40
42
|
stderr: "pipe",
|
|
41
43
|
windowsHide: true,
|
package/src/main.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* createAgentSession() options. The SDK does the heavy lifting.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
8
9
|
import * as fs from "node:fs/promises";
|
|
9
10
|
import * as os from "node:os";
|
|
10
11
|
import * as path from "node:path";
|
|
@@ -21,14 +22,16 @@ import {
|
|
|
21
22
|
VERSION,
|
|
22
23
|
} from "@gajae-code/utils";
|
|
23
24
|
import chalk from "chalk";
|
|
24
|
-
import
|
|
25
|
+
import { selectAppendSystemPromptFile } from "./cli/append-system-prompt";
|
|
26
|
+
import { type Args, resolvePlanModelOverride } from "./cli/args";
|
|
25
27
|
import { processFileArguments } from "./cli/file-processor";
|
|
26
28
|
import { fetchGithubChannelRelease } from "./cli/github-release";
|
|
27
29
|
import { buildInitialMessage } from "./cli/initial-message";
|
|
28
30
|
import { resolveLaunchDisposition } from "./cli/launch-disposition";
|
|
29
31
|
import { runListModelsCommand } from "./cli/list-models";
|
|
30
32
|
import { selectSession } from "./cli/session-picker";
|
|
31
|
-
import {
|
|
33
|
+
import { getConfigDirs } from "./config";
|
|
34
|
+
import { FileLockAcquireError, FileLockStagingCleanupError } from "./config/file-lock";
|
|
32
35
|
import {
|
|
33
36
|
activateModelProfile,
|
|
34
37
|
applyModelProfileRuntimeBindings,
|
|
@@ -74,7 +77,12 @@ import {
|
|
|
74
77
|
discoverAuthStorage,
|
|
75
78
|
} from "./sdk";
|
|
76
79
|
import { processIncarnation } from "./sdk/broker/process-incarnation";
|
|
77
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
newMasterAttestationEpoch,
|
|
82
|
+
resolveSessionLocator,
|
|
83
|
+
SessionIndex,
|
|
84
|
+
type SessionLocatorV2,
|
|
85
|
+
} from "./sdk/broker/session-index";
|
|
78
86
|
import type { AgentSession } from "./session/agent-session";
|
|
79
87
|
import { ManagedAppendIdentityMismatchError } from "./session/internal/managed-session-storage";
|
|
80
88
|
import { SessionMigrationBusyError } from "./session/internal/session-open-errors";
|
|
@@ -249,8 +257,6 @@ export function resolveAcpStartupOptions(
|
|
|
249
257
|
| "resume"
|
|
250
258
|
| "sessionDir"
|
|
251
259
|
| "skills"
|
|
252
|
-
| "slow"
|
|
253
|
-
| "smol"
|
|
254
260
|
| "plan"
|
|
255
261
|
| "systemPrompt"
|
|
256
262
|
| "thinking"
|
|
@@ -291,8 +297,6 @@ export function resolveAcpStartupOptions(
|
|
|
291
297
|
...(parsed.resume ? ["--resume"] : []),
|
|
292
298
|
...(parsed.sessionDir ? ["--session-dir"] : []),
|
|
293
299
|
...(parsed.skills !== undefined ? ["--skills"] : []),
|
|
294
|
-
...(parsed.slow ? ["--slow"] : []),
|
|
295
|
-
...(parsed.smol ? ["--smol"] : []),
|
|
296
300
|
...(parsed.plan ? ["--plan"] : []),
|
|
297
301
|
...(parsed.systemPrompt ? ["--system-prompt"] : []),
|
|
298
302
|
...(parsed.tmux ? ["--tmux"] : []),
|
|
@@ -1446,17 +1450,13 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
|
|
|
1446
1450
|
}
|
|
1447
1451
|
}
|
|
1448
1452
|
|
|
1449
|
-
/**
|
|
1453
|
+
/** User APPEND_SYSTEM.md only. Repository copies are not appended. */
|
|
1450
1454
|
function discoverAppendSystemPromptFile(): string | undefined {
|
|
1451
|
-
const
|
|
1452
|
-
|
|
1453
|
-
return
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
if (globalPath) {
|
|
1457
|
-
return globalPath;
|
|
1458
|
-
}
|
|
1459
|
-
return undefined;
|
|
1455
|
+
const candidates = getConfigDirs("", { existingOnly: false }).flatMap(entry => {
|
|
1456
|
+
const filePath = path.join(entry.path, "APPEND_SYSTEM.md");
|
|
1457
|
+
return existsSync(filePath) ? [{ level: entry.level, path: filePath }] : [];
|
|
1458
|
+
});
|
|
1459
|
+
return selectAppendSystemPromptFile(candidates);
|
|
1460
1460
|
}
|
|
1461
1461
|
|
|
1462
1462
|
async function buildSessionOptions(
|
|
@@ -1617,6 +1617,7 @@ type RunPrintMode = (session: AgentSession, options: PrintModeOptions) => Promis
|
|
|
1617
1617
|
export interface RunRootCommandDependencies {
|
|
1618
1618
|
createAgentSession?: typeof createAgentSession;
|
|
1619
1619
|
createSessionManager?: typeof createSessionManager;
|
|
1620
|
+
registerDirectSession?: typeof registerDirectSession;
|
|
1620
1621
|
discoverAuthStorage?: typeof discoverAuthStorage;
|
|
1621
1622
|
resolveStartupAuthConfig?: typeof resolveStartupAuthConfig;
|
|
1622
1623
|
runAcpMode?: (options?: { agentDir?: string }) => Promise<void>;
|
|
@@ -1663,84 +1664,278 @@ interface DirectSessionRegistrationDependencies {
|
|
|
1663
1664
|
masterRole?: { ownerSessionId: string; attestationEpoch: string };
|
|
1664
1665
|
}
|
|
1665
1666
|
|
|
1667
|
+
/**
|
|
1668
|
+
* Handle for a direct CLI session's broker-index registration.
|
|
1669
|
+
*
|
|
1670
|
+
* A direct session registers once at startup, but `/new`, `/resume` and forks
|
|
1671
|
+
* replace the manager's session id inside the same process. The index row is
|
|
1672
|
+
* keyed on that id, so without a follow-up the successor is never indexed and the
|
|
1673
|
+
* predecessor stays live for as long as this pid is alive (the broker keeps
|
|
1674
|
+
* heartbeating a live pid). `syncIdentity` moves the registration to whichever
|
|
1675
|
+
* id the manager the session has adopted currently reports.
|
|
1676
|
+
*/
|
|
1677
|
+
export interface DirectSessionRegistration {
|
|
1678
|
+
/**
|
|
1679
|
+
* Register the current manager's session id and release the one it replaced.
|
|
1680
|
+
*
|
|
1681
|
+
* `adoptedManager` rebinds the registration to the manager the session now
|
|
1682
|
+
* owns: a bounded cold-sidecar fork adopts its own forked manager before it
|
|
1683
|
+
* emits `session_switch`, so the wired handler must hand that manager over or
|
|
1684
|
+
* the sync keeps reading the startup manager's predecessor id.
|
|
1685
|
+
*/
|
|
1686
|
+
syncIdentity(adoptedManager?: DirectSessionManager): Promise<void>;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
const NOOP_DIRECT_SESSION_REGISTRATION: DirectSessionRegistration = { syncIdentity: async () => {} };
|
|
1690
|
+
|
|
1691
|
+
/**
|
|
1692
|
+
* Appends allowed for one successor registration before the switch gives up and
|
|
1693
|
+
* keeps the predecessor registered. `SessionIndex.append` takes the shared index
|
|
1694
|
+
* lock independently per append, so a peer holding it is a transient failure.
|
|
1695
|
+
*/
|
|
1696
|
+
const DIRECT_SESSION_REGISTRATION_ATTEMPTS = 3;
|
|
1697
|
+
|
|
1666
1698
|
/** Register a durable direct CLI session while leaving ephemeral sessions lock-free. */
|
|
1667
1699
|
export async function registerDirectSession(
|
|
1668
1700
|
sessionManager: DirectSessionManager | undefined,
|
|
1669
1701
|
agentDir: string,
|
|
1670
1702
|
lifecycleRequestId: string | undefined,
|
|
1671
1703
|
dependencies: DirectSessionRegistrationDependencies = {},
|
|
1672
|
-
): Promise<
|
|
1673
|
-
const
|
|
1674
|
-
if (!
|
|
1704
|
+
): Promise<DirectSessionRegistration> {
|
|
1705
|
+
const initialSessionId = directSessionRegistrationId(sessionManager, lifecycleRequestId);
|
|
1706
|
+
if (!initialSessionId || !sessionManager) return NOOP_DIRECT_SESSION_REGISTRATION;
|
|
1675
1707
|
const sessionIndex = dependencies.createIndex?.(agentDir) ?? new SessionIndex(agentDir);
|
|
1676
|
-
const locator = await resolveSessionLocator(sessionManager.getCwd(), agentDir);
|
|
1677
1708
|
const directSessionIncarnation = (dependencies.processIncarnation ?? processIncarnation)(process.pid);
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
});
|
|
1698
|
-
if (locator.worktreeRoot !== null) {
|
|
1709
|
+
// The session can adopt a different manager mid-process (bounded cold-sidecar
|
|
1710
|
+
// fork), so every read goes through the manager that is current now.
|
|
1711
|
+
let activeManager = sessionManager;
|
|
1712
|
+
let registeredLocator: SessionLocatorV2 | undefined;
|
|
1713
|
+
let registeredSessionId: string | undefined;
|
|
1714
|
+
// Predecessors whose `host_unregistered` append has not succeeded yet. Once the
|
|
1715
|
+
// successor is registered the old id is no longer `registeredSessionId`, so a
|
|
1716
|
+
// release that failed here would otherwise be lost and keep the predecessor live
|
|
1717
|
+
// in the index for the rest of the process. An entry is dropped as soon as its id
|
|
1718
|
+
// is current again: replaying it would retire a session this process reactivated.
|
|
1719
|
+
const pendingPredecessorReleases: Array<{ sessionId: string; locator: SessionLocatorV2 | undefined }> = [];
|
|
1720
|
+
const forgetPendingRelease = (sessionId: string): void => {
|
|
1721
|
+
for (let index = pendingPredecessorReleases.length - 1; index >= 0; index--) {
|
|
1722
|
+
if (pendingPredecessorReleases[index]?.sessionId === sessionId) pendingPredecessorReleases.splice(index, 1);
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
|
|
1726
|
+
const register = async (sessionId: string): Promise<void> => {
|
|
1727
|
+
const locator = await resolveSessionLocator(activeManager.getCwd(), agentDir);
|
|
1699
1728
|
try {
|
|
1700
|
-
await
|
|
1729
|
+
await sessionIndex.append({
|
|
1730
|
+
type: "host_registered",
|
|
1731
|
+
sessionId,
|
|
1732
|
+
locator,
|
|
1733
|
+
endpointGeneration: 0,
|
|
1734
|
+
pid: process.pid,
|
|
1735
|
+
...(directSessionIncarnation ? { processIncarnation: directSessionIncarnation } : {}),
|
|
1736
|
+
...(dependencies.masterRole && directSessionIncarnation
|
|
1737
|
+
? {
|
|
1738
|
+
masterRole: {
|
|
1739
|
+
version: 2 as const,
|
|
1740
|
+
ownerSessionId: dependencies.masterRole.ownerSessionId,
|
|
1741
|
+
launchPid: process.pid,
|
|
1742
|
+
launchProcessIncarnation: directSessionIncarnation,
|
|
1743
|
+
role: "master" as const,
|
|
1744
|
+
attestationEpoch: dependencies.masterRole.attestationEpoch,
|
|
1745
|
+
},
|
|
1746
|
+
}
|
|
1747
|
+
: {}),
|
|
1748
|
+
});
|
|
1701
1749
|
} catch (error) {
|
|
1702
|
-
|
|
1750
|
+
// A lock-release refusal after the row is durable is a real startup failure:
|
|
1751
|
+
// the next process would observe a registered session whose lock generation
|
|
1752
|
+
// this process can no longer prove. Surface it instead of adopting the row.
|
|
1753
|
+
if (error instanceof Error && /^Failed to release file lock:/u.test(error.message)) throw error;
|
|
1754
|
+
// `SessionIndex.append` can reject after its row is already durable (awaited
|
|
1755
|
+
// post-append maintenance), so a rejection does not prove the registration is
|
|
1756
|
+
// missing. Adopt the id when this process already owns a live row for it, so a
|
|
1757
|
+
// later switch still releases it; otherwise the switch keeps failing and the
|
|
1758
|
+
// previous identity stays registered.
|
|
1759
|
+
if (!isRegisteredLive(sessionId)) throw error;
|
|
1760
|
+
logger.warn("Direct session registration append failed after its row was durable", {
|
|
1703
1761
|
error: error instanceof Error ? error.message : String(error),
|
|
1704
1762
|
});
|
|
1705
1763
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1764
|
+
// This id is current again: any queued release for it is stale.
|
|
1765
|
+
forgetPendingRelease(sessionId);
|
|
1766
|
+
registeredLocator = locator;
|
|
1767
|
+
registeredSessionId = sessionId;
|
|
1768
|
+
};
|
|
1769
|
+
const isRegisteredLive = (sessionId: string): boolean => {
|
|
1770
|
+
try {
|
|
1771
|
+
return sessionIndex
|
|
1772
|
+
.listSessions()
|
|
1773
|
+
.sessions.some(row => row.sessionId === sessionId && row.pid === process.pid && row.live === true);
|
|
1774
|
+
} catch {
|
|
1775
|
+
return false;
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
const unregister = async (sessionId: string, locator: SessionLocatorV2 | undefined): Promise<void> => {
|
|
1779
|
+
if (!locator) return;
|
|
1708
1780
|
await sessionIndex.append({
|
|
1709
1781
|
type: "host_unregistered",
|
|
1710
|
-
sessionId
|
|
1782
|
+
sessionId,
|
|
1711
1783
|
locator,
|
|
1712
1784
|
endpointGeneration: 0,
|
|
1713
1785
|
pid: process.pid,
|
|
1714
1786
|
});
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
/**
|
|
1790
|
+
* Release queued predecessors in order. A failed append keeps its entry, so the
|
|
1791
|
+
* next sync or the teardown handler retries it; the failure itself is a warning
|
|
1792
|
+
* rather than a failed identity switch, matching the best-effort release of the
|
|
1793
|
+
* launch worktree reservation below.
|
|
1794
|
+
*/
|
|
1795
|
+
const releasePendingPredecessors = async (): Promise<void> => {
|
|
1796
|
+
while (pendingPredecessorReleases.length > 0) {
|
|
1797
|
+
const predecessor = pendingPredecessorReleases[0]!;
|
|
1798
|
+
// Defence in depth for the reactivation order: never retire the id this
|
|
1799
|
+
// process currently publishes as its own.
|
|
1800
|
+
if (predecessor.sessionId === registeredSessionId) {
|
|
1801
|
+
pendingPredecessorReleases.shift();
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
try {
|
|
1805
|
+
await unregister(predecessor.sessionId, predecessor.locator);
|
|
1806
|
+
} catch (error) {
|
|
1807
|
+
logger.warn("Failed to release a superseded session registration", {
|
|
1808
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1809
|
+
});
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
pendingPredecessorReleases.shift();
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
|
|
1816
|
+
/**
|
|
1817
|
+
* Register `sessionId`, retrying a bounded number of times so a transient
|
|
1818
|
+
* index-lock loss self-heals instead of leaving the successor unregistered
|
|
1819
|
+
* until the next identity change.
|
|
1820
|
+
*/
|
|
1821
|
+
const registerWithRetry = async (sessionId: string): Promise<void> => {
|
|
1822
|
+
for (let attempt = 1; ; attempt++) {
|
|
1823
|
+
try {
|
|
1824
|
+
await register(sessionId);
|
|
1825
|
+
return;
|
|
1826
|
+
} catch (error) {
|
|
1827
|
+
// Only lock contention self-heals. A refused release is an identity
|
|
1828
|
+
// verdict; retrying it would mask a committed registration whose lock
|
|
1829
|
+
// generation no longer matches.
|
|
1830
|
+
const code = (error as NodeJS.ErrnoException | undefined)?.code;
|
|
1831
|
+
const transient =
|
|
1832
|
+
code === "sharing_violation" ||
|
|
1833
|
+
code === "access_denied" ||
|
|
1834
|
+
code === "EBUSY" ||
|
|
1835
|
+
code === "EPERM" ||
|
|
1836
|
+
code === "EACCES" ||
|
|
1837
|
+
code === "ENOTEMPTY" ||
|
|
1838
|
+
code === "removal_record_busy";
|
|
1839
|
+
if (!transient || attempt >= DIRECT_SESSION_REGISTRATION_ATTEMPTS) throw error;
|
|
1840
|
+
await Bun.sleep(50 * attempt);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
};
|
|
1844
|
+
|
|
1845
|
+
/**
|
|
1846
|
+
* Move the registration to the id `activeManager` currently reports.
|
|
1847
|
+
*
|
|
1848
|
+
* A successor append that cannot land after its bounded retries keeps the
|
|
1849
|
+
* predecessor registered and the successor unregistered, so every later call
|
|
1850
|
+
* (any session event, or teardown) retries the successor instead of leaving it
|
|
1851
|
+
* unindexed for the life of the process.
|
|
1852
|
+
*/
|
|
1853
|
+
const syncToActiveManager = async (): Promise<void> => {
|
|
1854
|
+
const currentId = activeManager.getSessionFile() === undefined ? undefined : activeManager.getSessionId();
|
|
1855
|
+
// A predecessor left over from a failed release is retried here even when
|
|
1856
|
+
// the identity itself did not change.
|
|
1857
|
+
if (!currentId || currentId === registeredSessionId) {
|
|
1858
|
+
await releasePendingPredecessors();
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
// The predecessor is released under the locator it was registered with, and
|
|
1862
|
+
// only after the successor's own append succeeded.
|
|
1863
|
+
const predecessorId = registeredSessionId;
|
|
1864
|
+
const predecessorLocator = registeredLocator;
|
|
1865
|
+
await registerWithRetry(currentId);
|
|
1866
|
+
if (predecessorId) {
|
|
1867
|
+
pendingPredecessorReleases.push({ sessionId: predecessorId, locator: predecessorLocator });
|
|
1868
|
+
await releasePendingPredecessors();
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
|
|
1872
|
+
await register(initialSessionId);
|
|
1873
|
+
if (registeredLocator?.worktreeRoot != null) {
|
|
1874
|
+
try {
|
|
1875
|
+
await releaseLaunchWorktreeReservationAfterRegistration(agentDir, registeredLocator.worktreeRoot);
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
logger.warn("Failed to release launch worktree reservation after host registration", {
|
|
1878
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
// Teardown registers the id the session is on now — a post-switch append that
|
|
1883
|
+
// failed would otherwise leave the active transcript unindexed — then releases
|
|
1884
|
+
// everything this process still holds.
|
|
1885
|
+
(dependencies.registerPostmortem ?? postmortem.register)("direct-session-index", async _reason => {
|
|
1886
|
+
try {
|
|
1887
|
+
await syncToActiveManager();
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
logger.warn("Failed to register the active direct session during teardown", {
|
|
1890
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
if (registeredSessionId) await unregister(registeredSessionId, registeredLocator);
|
|
1894
|
+
await releasePendingPredecessors();
|
|
1715
1895
|
});
|
|
1716
|
-
}
|
|
1717
1896
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1897
|
+
let syncing: Promise<void> = Promise.resolve();
|
|
1898
|
+
return {
|
|
1899
|
+
syncIdentity: adoptedManager => {
|
|
1900
|
+
// Serialized: a rapid /new /new must register in order and never interleave.
|
|
1901
|
+
const run = syncing.then(async () => {
|
|
1902
|
+
// An ephemeral manager owns nothing the index could fence, so the tracked
|
|
1903
|
+
// manager survives an adoption that has no durable transcript.
|
|
1904
|
+
if (adoptedManager?.getSessionFile() !== undefined) activeManager = adoptedManager;
|
|
1905
|
+
await syncToActiveManager();
|
|
1906
|
+
});
|
|
1907
|
+
syncing = run.catch(() => {});
|
|
1908
|
+
return run;
|
|
1909
|
+
},
|
|
1910
|
+
};
|
|
1722
1911
|
}
|
|
1723
1912
|
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
return
|
|
1913
|
+
function directSessionIndexLockFailureMessage(error: unknown, agentDir: string): string | undefined {
|
|
1914
|
+
if (error instanceof FileLockStagingCleanupError) {
|
|
1915
|
+
return (
|
|
1916
|
+
"Session index lock staging cleanup failed; acquisition stopped instead of retrying with another staging directory.\n" +
|
|
1917
|
+
`Lock: ${error.lockPath}\nStaging: ${error.stagingPath}\nCode: ${error.code ?? "unknown"}\n${error.message}`
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
if (error instanceof FileLockAcquireError) {
|
|
1921
|
+
const code = error.publicationFailureCode ?? error.code;
|
|
1922
|
+
if (error.publicationFailureCode === "sharing_violation" || error.holderIsLive)
|
|
1923
|
+
return (
|
|
1924
|
+
"Session index is busy (validated lock-publication contention). Retry in a few seconds.\n" +
|
|
1925
|
+
`Lock: ${error.lockPath}\nCode: ${code}\n${error.message}`
|
|
1926
|
+
);
|
|
1927
|
+
return (
|
|
1928
|
+
"Session index lock acquisition failed; no session was registered.\n" +
|
|
1929
|
+
`Lock: ${error.lockPath}\nCode: ${code}\nHolder: ${error.holder}`
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
if (!(error instanceof Error) || !/^Failed to publish file lock:/u.test(error.message)) return undefined;
|
|
1933
|
+
const code =
|
|
1934
|
+
(error as NodeJS.ErrnoException).code ??
|
|
1935
|
+
error.message.match(/^Failed to publish file lock: ([a-z0-9_]+)/u)?.[1] ??
|
|
1936
|
+
"unknown";
|
|
1937
|
+
const lockPath = path.join(agentDir, "sdk", "sessions", "index.jsonl.lock");
|
|
1938
|
+
return `Session index lock publication failed; no session was registered.\nLock: ${lockPath}\nCode: ${code}\n${error.message}`;
|
|
1744
1939
|
}
|
|
1745
1940
|
|
|
1746
1941
|
/**
|
|
@@ -2002,19 +2197,12 @@ export async function runRootCommand(
|
|
|
2002
2197
|
// Initialize discovery system with settings for provider persistence
|
|
2003
2198
|
logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
|
|
2004
2199
|
|
|
2005
|
-
//
|
|
2006
|
-
//
|
|
2007
|
-
const
|
|
2008
|
-
if (
|
|
2009
|
-
settingsInstance.overrideModelRoles({
|
|
2010
|
-
...(roleOverrides.smol ? { smol: roleOverrides.smol } : {}),
|
|
2011
|
-
...(roleOverrides.slow ? { slow: roleOverrides.slow } : {}),
|
|
2012
|
-
...(roleOverrides.plan ? { plan: roleOverrides.plan } : {}),
|
|
2013
|
-
});
|
|
2014
|
-
}
|
|
2200
|
+
// Plan mode switches to the `plan` model role; --plan / GJC_PLAN_MODEL override it
|
|
2201
|
+
// for this run only (ephemeral, not persisted).
|
|
2202
|
+
const planModelOverride = resolvePlanModelOverride(parsedArgs);
|
|
2203
|
+
if (planModelOverride) settingsInstance.overrideModelRoles({ plan: planModelOverride });
|
|
2015
2204
|
// Apply --clipboard-transport / --clipboard-ssh-host as ephemeral runtime
|
|
2016
|
-
// overrides (CLI > persisted config > "auto" default). Not persisted
|
|
2017
|
-
// mirrors the role-override precedence above.
|
|
2205
|
+
// overrides (CLI > persisted config > "auto" default). Not persisted.
|
|
2018
2206
|
if (parsedArgs.clipboardTransport) {
|
|
2019
2207
|
settingsInstance.override("clipboard.transport", parsedArgs.clipboardTransport);
|
|
2020
2208
|
}
|
|
@@ -2302,15 +2490,64 @@ export async function runRootCommand(
|
|
|
2302
2490
|
// Register a resumed direct session before constructing the agent: GC holds the
|
|
2303
2491
|
// same index lock while deleting artifacts, so startup and deletion are fenced.
|
|
2304
2492
|
// Ephemeral sessions have no durable artifacts or liveness authority to fence.
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2493
|
+
const sessionAgentDir = settingsInstance.getAgentDir();
|
|
2494
|
+
let directSessionRegistration: DirectSessionRegistration;
|
|
2495
|
+
try {
|
|
2496
|
+
directSessionRegistration = await (deps.registerDirectSession ?? registerDirectSession)(
|
|
2497
|
+
sessionManager,
|
|
2498
|
+
sessionAgentDir,
|
|
2499
|
+
process.env.GJC_LIFECYCLE_REQUEST_ID,
|
|
2500
|
+
{
|
|
2501
|
+
masterRole: masterModeContext
|
|
2502
|
+
? {
|
|
2503
|
+
ownerSessionId: masterModeContext.ownerSessionId,
|
|
2504
|
+
attestationEpoch: masterModeContext.attestationEpoch,
|
|
2505
|
+
}
|
|
2506
|
+
: undefined,
|
|
2507
|
+
},
|
|
2508
|
+
);
|
|
2509
|
+
} catch (error) {
|
|
2510
|
+
const message = directSessionIndexLockFailureMessage(error, sessionAgentDir);
|
|
2511
|
+
if (message === undefined) throw error;
|
|
2512
|
+
process.stderr.write(`${message}\n`);
|
|
2513
|
+
if (!deps.suppressProcessExit) process.exitCode = 1;
|
|
2514
|
+
await sessionManager?.close().catch(closeError => {
|
|
2515
|
+
logger.warn("Failed to close session manager after session-index registration failure", {
|
|
2516
|
+
error: closeError instanceof Error ? closeError.message : String(closeError),
|
|
2517
|
+
});
|
|
2518
|
+
});
|
|
2519
|
+
authStorage.close();
|
|
2520
|
+
stopThemeWatcher();
|
|
2521
|
+
await postmortem.cleanup();
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
// /new, /resume and forks replace the manager's session id in this same process.
|
|
2525
|
+
// Follow it, or the broker index keeps listing the predecessor as live forever.
|
|
2526
|
+
//
|
|
2527
|
+
// The extension context is not a usable source here: `ExtensionRunner` is
|
|
2528
|
+
// constructed with the startup manager and `createContext()` keeps handing that
|
|
2529
|
+
// facade back, while a bounded cold-sidecar fork replaces only the session's own
|
|
2530
|
+
// manager. Read the live session at dispatch time instead.
|
|
2531
|
+
let liveSession: AgentSession | undefined;
|
|
2532
|
+
sessionOptions.extensions = [
|
|
2533
|
+
...(sessionOptions.extensions ?? []),
|
|
2534
|
+
pi => {
|
|
2535
|
+
const sync = async (): Promise<void> => {
|
|
2536
|
+
try {
|
|
2537
|
+
await directSessionRegistration.syncIdentity(liveSession?.sessionManager);
|
|
2538
|
+
} catch (error) {
|
|
2539
|
+
logger.warn("Failed to move direct session registration to the new session id", {
|
|
2540
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2541
|
+
});
|
|
2310
2542
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2543
|
+
};
|
|
2544
|
+
pi.on("session_switch", sync);
|
|
2545
|
+
pi.on("session_branch", sync);
|
|
2546
|
+
// A transition whose append failed is retried at the next turn boundary, not
|
|
2547
|
+
// only on another identity change.
|
|
2548
|
+
pi.on("agent_end", sync);
|
|
2549
|
+
},
|
|
2550
|
+
];
|
|
2314
2551
|
const createAgentSessionImpl = deps.createAgentSession ?? createAgentSession;
|
|
2315
2552
|
const createSession: CreateSessionForMain = async (options, context): Promise<CreateAgentSessionResult> => {
|
|
2316
2553
|
const result = await logger.time("createAgentSession", createAgentSessionImpl, options);
|
|
@@ -2346,6 +2583,7 @@ export async function runRootCommand(
|
|
|
2346
2583
|
startDeferredMemoryBackend,
|
|
2347
2584
|
eventBus,
|
|
2348
2585
|
} = await createSession(sessionOptions, { skipPostCreateModelRefresh: hasRootStartupProfile });
|
|
2586
|
+
liveSession = session;
|
|
2349
2587
|
applyCliRuntimeApiKeyOverride(authStorage, parsedArgs.apiKey, session.model);
|
|
2350
2588
|
// Herdr integration: report gjc lifecycle state when running in a Herdr pane.
|
|
2351
2589
|
installHerdrReporter(listener => session.subscribe(listener));
|