@gajae-code/coding-agent 0.18.10 → 0.18.12
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 +295 -0
- package/dist/types/async/job-manager.d.ts +7 -3
- package/dist/types/async/result-formatting.d.ts +2 -0
- package/dist/types/autoresearch/git.d.ts +1 -1
- package/dist/types/capability/fs.d.ts +2 -2
- package/dist/types/cli/accounts-cli.d.ts +2 -2
- package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
- package/dist/types/cli/install-activation.d.ts +10 -4
- package/dist/types/cli/public-command-errors.d.ts +1 -0
- package/dist/types/cli/update-cli.d.ts +14 -1
- package/dist/types/cli-ordinary.d.ts +2 -2
- package/dist/types/commands/accounts.d.ts +2 -1
- package/dist/types/commands/migrate.d.ts +1 -1
- package/dist/types/commands/sdk.d.ts +5 -0
- package/dist/types/commit/changelog/index.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +30 -0
- package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
- package/dist/types/config/config-hot-reload.d.ts +39 -0
- package/dist/types/config/file-lock.d.ts +8 -1
- package/dist/types/config/model-discovery-manager.d.ts +2 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -6
- package/dist/types/config/model-registry.d.ts +56 -5
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/settings-schema.d.ts +46 -3
- package/dist/types/config/settings.d.ts +33 -1
- package/dist/types/dap/config.d.ts +8 -4
- package/dist/types/debug/system-info.d.ts +6 -1
- package/dist/types/discovery/mcp-compat.d.ts +3 -1
- package/dist/types/edit/modes/patch.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -0
- package/dist/types/exec/bash-executor.d.ts +12 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
- package/dist/types/migrate/adapters/codex.d.ts +2 -1
- package/dist/types/migrate/adapters/index.d.ts +10 -1
- package/dist/types/migrate/executor.d.ts +3 -1
- package/dist/types/migrate/types.d.ts +15 -3
- package/dist/types/modes/components/settings-selector.d.ts +6 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
- package/dist/types/runtime-mcp/manager.d.ts +7 -0
- package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
- package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
- package/dist/types/sdk/broker/broker.d.ts +7 -1
- package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
- package/dist/types/sdk/broker/ensure.d.ts +136 -7
- package/dist/types/sdk/broker/hop.d.ts +36 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
- package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
- package/dist/types/sdk/broker/session-index.d.ts +4 -1
- package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +23 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/cli/session-cli.d.ts +0 -1
- package/dist/types/sdk/host/session-runtime.d.ts +32 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
- package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
- package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
- package/dist/types/sdk/session-list.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +58 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
- package/dist/types/session/messages.d.ts +5 -1
- package/dist/types/session/session-manager.d.ts +13 -3
- package/dist/types/session/startup-auth-config.d.ts +24 -2
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/session/terminal-abort.d.ts +3 -1
- package/dist/types/session-import/service.d.ts +1 -1
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
- package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
- package/dist/types/task/executor.d.ts +19 -2
- package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
- package/dist/types/tools/atomic-file-write.d.ts +9 -7
- package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
- package/dist/types/tools/descriptors.d.ts +2 -0
- package/dist/types/tools/fetch.d.ts +1 -1
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/python.d.ts +1 -1
- package/dist/types/tools/report-issue.d.ts +41 -0
- package/dist/types/tools/subagent.d.ts +23 -0
- package/dist/types/web/scrapers/types.d.ts +8 -1
- package/dist/types/web/search/providers/insane.d.ts +7 -1
- package/dist/types/web/search/providers/searxng.d.ts +5 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +2 -1
- package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
- package/scripts/generate-docs-index.ts +8 -1
- package/scripts/generate-sdk-operation-inventory.ts +22 -4
- package/src/async/job-manager.ts +356 -56
- package/src/async/result-formatting.ts +40 -0
- package/src/autoresearch/git.ts +9 -2
- package/src/autoresearch/prompts/prompt-setup.md +1 -1
- package/src/capability/fs.ts +27 -19
- package/src/capability/index.ts +2 -2
- package/src/cli/accounts-cli.ts +176 -8
- package/src/cli/auth-broker-cli.ts +21 -11
- package/src/cli/doctor/install-repairs.ts +35 -5
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/install-activation.ts +47 -4
- package/src/cli/migrate-cli.ts +7 -5
- package/src/cli/public-command-errors.ts +4 -0
- package/src/cli/root-flags.ts +2 -2
- package/src/cli/stats-cli.ts +9 -5
- package/src/cli/update-cli.ts +224 -7
- package/src/cli-ordinary.ts +7 -2
- package/src/commands/accounts.ts +10 -5
- package/src/commands/migrate.ts +3 -3
- package/src/commands/sdk.ts +199 -42
- package/src/commands/ultragoal.ts +16 -8
- package/src/commit/changelog/index.ts +39 -1
- package/src/commit/message.ts +10 -3
- package/src/config/autorouting-tier-map.ts +30 -0
- package/src/config/config-hot-reload-trust.ts +148 -0
- package/src/config/config-hot-reload.ts +734 -0
- package/src/config/file-lock.ts +216 -56
- package/src/config/model-bindings-applier.ts +85 -47
- package/src/config/model-discovery-manager.ts +10 -0
- package/src/config/model-profile-activation.ts +716 -113
- package/src/config/model-registry.ts +1334 -158
- package/src/config/model-resolver.ts +3 -1
- package/src/config/models-config-schema.ts +6 -0
- package/src/config/settings-schema.ts +60 -3
- package/src/config/settings.ts +458 -24
- package/src/coordinator-mcp/policy.ts +26 -1
- package/src/crash/sanitize.ts +4 -0
- package/src/customization/import.ts +3 -1
- package/src/dap/config.ts +31 -11
- package/src/dap/defaults.json +1 -1
- package/src/debug/crash-diagnostics.ts +2 -1
- package/src/debug/report-bundle.ts +19 -0
- package/src/debug/system-info.ts +8 -3
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
- package/src/discovery/agents-md.ts +1 -1
- package/src/discovery/helpers.ts +1 -2
- package/src/discovery/mcp-compat.ts +120 -17
- package/src/edit/apply-patch/index.ts +12 -1
- package/src/edit/index.ts +13 -17
- package/src/edit/modes/patch.ts +187 -8
- package/src/edit/modes/replace.ts +136 -0
- package/src/eval/js/context-manager.ts +29 -3
- package/src/eval/js/worker-entry.ts +51 -18
- package/src/eval/py/executor.ts +14 -4
- package/src/eval/py/runner.py +144 -5
- package/src/exec/bash-executor.ts +81 -4
- package/src/exec/bash-shell-guardian.ts +4 -1
- package/src/exec/bash-shell-supervisor.ts +8 -4
- package/src/exec/isolated-shell.ts +5 -1
- package/src/export/html/index.ts +46 -6
- package/src/extensibility/custom-tools/wrapper.ts +3 -1
- package/src/extensibility/extensions/loader.ts +8 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
- package/src/gjc-runtime/gc-runtime.ts +33 -6
- package/src/gjc-runtime/launch-tmux.ts +7 -2
- package/src/gjc-runtime/launch-worktree.test.ts +148 -0
- package/src/gjc-runtime/launch-worktree.ts +101 -14
- package/src/gjc-runtime/managed-owner-admission.ts +46 -2
- package/src/gjc-runtime/session-resolution.ts +38 -6
- package/src/gjc-runtime/session-state-lock.ts +59 -14
- package/src/gjc-runtime/state-runtime.ts +23 -19
- package/src/hooks/native-skill-hook.ts +14 -11
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/local-protocol.ts +142 -8
- package/src/lsp/index.ts +29 -0
- package/src/lsp/workspace-path.ts +23 -0
- package/src/migrate/action-planner.ts +57 -0
- package/src/migrate/adapters/claude-code.ts +23 -4
- package/src/migrate/adapters/codex.ts +22 -4
- package/src/migrate/adapters/index.ts +22 -4
- package/src/migrate/adapters/opencode.ts +6 -1
- package/src/migrate/executor.ts +84 -7
- package/src/migrate/report.ts +19 -2
- package/src/migrate/types.ts +14 -4
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/permission-mode.ts +18 -1
- package/src/modes/components/assistant-message.ts +55 -1
- package/src/modes/components/settings-selector.ts +103 -37
- package/src/modes/controllers/command-controller.ts +38 -2
- package/src/modes/controllers/plan-mode-controller.ts +17 -7
- package/src/modes/controllers/selector-controller.ts +62 -6
- package/src/modes/interactive-mode.ts +68 -0
- package/src/modes/theme/theme.ts +15 -0
- package/src/prompts/tools/debug.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/report-issue.md +13 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/runtime-credential-selector.ts +2 -2
- package/src/runtime-mcp/content-limits.ts +7 -0
- package/src/runtime-mcp/manager-pool.test.ts +5 -4
- package/src/runtime-mcp/manager.ts +32 -0
- package/src/runtime-mcp/oauth-flow.ts +32 -6
- package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
- package/src/runtime-mcp/render.ts +16 -1
- package/src/runtime-mcp/tool-bridge.ts +126 -17
- package/src/sdk/broker/broker-exit.ts +8 -5
- package/src/sdk/broker/broker.ts +342 -82
- package/src/sdk/broker/daemon-entry.ts +338 -116
- package/src/sdk/broker/doctor-restart.ts +2 -1
- package/src/sdk/broker/ensure.ts +1143 -265
- package/src/sdk/broker/hop.ts +274 -0
- package/src/sdk/broker/lifecycle-ledger.ts +35 -11
- package/src/sdk/broker/lock-host-identity.ts +29 -0
- package/src/sdk/broker/session-index.ts +62 -12
- package/src/sdk/bus/chat-command-policy.ts +16 -3
- package/src/sdk/bus/index.ts +52 -21
- package/src/sdk/bus/reconciliation-store.ts +201 -8
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/cli/session-cli.ts +21 -6
- package/src/sdk/host/session-runtime.test.ts +118 -23
- package/src/sdk/host/session-runtime.ts +352 -47
- package/src/sdk/lifecycle/service.ts +1 -0
- package/src/sdk/prompt-deadline-flush.ts +78 -5
- package/src/sdk/prompt-deadline-manager.ts +68 -14
- package/src/sdk/protocol/adapter-validation.ts +53 -11
- package/src/sdk/protocol/operation-inventory.generated.json +101 -2
- package/src/sdk/reconciliation-extensions.ts +1 -0
- package/src/sdk/session-list.ts +1 -0
- package/src/sdk/session.ts +114 -61
- package/src/session/agent-session.ts +1043 -81
- package/src/session/internal/managed-session-scope.ts +126 -39
- package/src/session/internal/managed-session-storage.ts +14 -11
- package/src/session/internal/native-publish-outcome.ts +5 -1
- package/src/session/messages.ts +47 -6
- package/src/session/session-manager.ts +236 -14
- package/src/session/startup-auth-config.ts +116 -8
- package/src/session/streaming-output.ts +262 -4
- package/src/session/terminal-abort.ts +14 -2
- package/src/session-import/service.ts +1 -0
- package/src/skill-state/workflow-mutation-guard.ts +491 -32
- package/src/slash-commands/builtin-registry.ts +15 -3
- package/src/slash-commands/helpers/credential-switch.ts +10 -5
- package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
- package/src/task/executor.ts +103 -12
- package/src/task/index.ts +389 -175
- package/src/tools/ast-edit-write-path.ts +64 -0
- package/src/tools/ast-edit.ts +18 -1
- package/src/tools/atomic-file-write.ts +46 -15
- package/src/tools/auto-generated-guard.ts +3 -0
- package/src/tools/bash-allowed-prefixes.ts +153 -5
- package/src/tools/bash-interactive.ts +14 -2
- package/src/tools/bash.ts +14 -0
- package/src/tools/debug.ts +7 -6
- package/src/tools/descriptors.test.ts +44 -0
- package/src/tools/descriptors.ts +34 -2
- package/src/tools/fetch.ts +3 -2
- package/src/tools/index.ts +2 -0
- package/src/tools/output-meta.ts +23 -2
- package/src/tools/python.ts +1 -1
- package/src/tools/read.ts +17 -35
- package/src/tools/report-issue.ts +184 -0
- package/src/tools/subagent-render.ts +114 -12
- package/src/tools/subagent.ts +108 -1
- package/src/tools/todo-write.ts +41 -3
- package/src/tools/tool-catalog.generated.ts +31 -3
- package/src/tools/write.ts +35 -3
- package/src/utils/commit-message-generator.ts +9 -2
- package/src/utils/file-mentions.ts +29 -1
- package/src/utils/title-generator.ts +7 -2
- package/src/web/scrapers/index.ts +3 -1
- package/src/web/scrapers/types.ts +8 -0
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/insane.ts +91 -28
- package/src/web/search/providers/searxng.ts +62 -10
package/src/edit/modes/patch.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
9
10
|
import * as path from "node:path";
|
|
10
11
|
import type { AgentToolResult } from "@gajae-code/agent-core";
|
|
11
12
|
import { isEnoent } from "@gajae-code/utils";
|
|
@@ -80,6 +81,9 @@ export interface ApplyPatchOptions {
|
|
|
80
81
|
* object identity is not a durable-lock capability probe.
|
|
81
82
|
*/
|
|
82
83
|
crossProcessLock?: boolean;
|
|
84
|
+
/** Explicit artifacts `local` root for this call. The caller session owns it. */
|
|
85
|
+
localRoot?: string;
|
|
86
|
+
signal?: AbortSignal;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -117,6 +121,117 @@ function loadNativePatchBindings(): Promise<NativePatchBindings> {
|
|
|
117
121
|
return nativePatchBindingsPromise;
|
|
118
122
|
}
|
|
119
123
|
|
|
124
|
+
function explicitLocalRootForSession(session: ToolSession): string | undefined {
|
|
125
|
+
const artifactsDir = session.getArtifactsDir?.();
|
|
126
|
+
if (!artifactsDir || session.isManagedSessionDestination?.()) return undefined;
|
|
127
|
+
return path.resolve(artifactsDir, "local");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function pathIsWithin(target: string, root: string): boolean {
|
|
131
|
+
const relative = path.relative(root, target);
|
|
132
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Trust root for a delete. `localRoot` is the caller session's explicit
|
|
137
|
+
* artifacts root. `gjc-local/<id>` is recognized from the path. Else `cwd`.
|
|
138
|
+
*/
|
|
139
|
+
function boundaryForPatchDelete(cwd: string, absolutePath: string, localRoot?: string): string {
|
|
140
|
+
const resolved = path.resolve(absolutePath);
|
|
141
|
+
if (localRoot) {
|
|
142
|
+
const explicit = path.resolve(localRoot);
|
|
143
|
+
if (pathIsWithin(resolved, explicit)) return explicit;
|
|
144
|
+
}
|
|
145
|
+
const managedParent = path.join(os.tmpdir(), "gjc-local");
|
|
146
|
+
if (pathIsWithin(resolved, managedParent)) {
|
|
147
|
+
const rest = resolved.slice(managedParent.length + path.sep.length);
|
|
148
|
+
const sessionSegment = rest.split(path.sep, 1)[0] ?? "";
|
|
149
|
+
if (sessionSegment.length > 0) return path.join(managedParent, sessionSegment);
|
|
150
|
+
}
|
|
151
|
+
return path.resolve(cwd);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* `unlink` follows a directory symlink. Resolve the parent only, then delete
|
|
156
|
+
* that real parent plus the final name so a file symlink is removed as a link.
|
|
157
|
+
*/
|
|
158
|
+
async function resolvePatchDeletePath(
|
|
159
|
+
root: string,
|
|
160
|
+
absolutePath: string,
|
|
161
|
+
displayPath: string,
|
|
162
|
+
signal?: AbortSignal,
|
|
163
|
+
): Promise<string> {
|
|
164
|
+
signal?.throwIfAborted();
|
|
165
|
+
let realRoot: string;
|
|
166
|
+
try {
|
|
167
|
+
realRoot = await fs.promises.realpath(root);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (isEnoent(error)) throw new ApplyPatchError(`File not found: ${displayPath}`);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
signal?.throwIfAborted();
|
|
173
|
+
let realParent: string;
|
|
174
|
+
try {
|
|
175
|
+
realParent = await fs.promises.realpath(path.dirname(absolutePath));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (isEnoent(error)) throw new ApplyPatchError(`File not found: ${displayPath}`);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
signal?.throwIfAborted();
|
|
181
|
+
if (!pathIsWithin(realParent, realRoot)) {
|
|
182
|
+
throw new ApplyPatchError(`Delete target resolves outside its root: ${displayPath}`);
|
|
183
|
+
}
|
|
184
|
+
return path.join(realParent, path.basename(absolutePath));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* `mkdir` follows a directory symlink. Walk to the nearest existing ancestor
|
|
189
|
+
* and return its real path plus the missing suffix. A parent that is already
|
|
190
|
+
* lexically outside the workspace or session local root is unchanged.
|
|
191
|
+
*/
|
|
192
|
+
async function resolvePatchMkdirPath(
|
|
193
|
+
cwd: string,
|
|
194
|
+
absolutePath: string,
|
|
195
|
+
localRoot: string | undefined,
|
|
196
|
+
parentDir: string,
|
|
197
|
+
displayPath: string,
|
|
198
|
+
signal?: AbortSignal,
|
|
199
|
+
): Promise<string> {
|
|
200
|
+
signal?.throwIfAborted();
|
|
201
|
+
const boundary = path.resolve(boundaryForPatchDelete(cwd, absolutePath, localRoot));
|
|
202
|
+
const lexicalParent = path.resolve(parentDir);
|
|
203
|
+
if (!pathIsWithin(lexicalParent, boundary)) return parentDir;
|
|
204
|
+
// A root that does not exist yet is not an escape. Keep the missing suffix
|
|
205
|
+
// under its real ancestor, which is what mkdir did before this check.
|
|
206
|
+
const realRoot = await realpathThroughExistingAncestor(boundary, signal);
|
|
207
|
+
signal?.throwIfAborted();
|
|
208
|
+
const realParent = await realpathThroughExistingAncestor(lexicalParent, signal);
|
|
209
|
+
signal?.throwIfAborted();
|
|
210
|
+
if (!pathIsWithin(realParent, realRoot)) {
|
|
211
|
+
throw new ApplyPatchError(`Parent directory resolves outside its root: ${displayPath}`);
|
|
212
|
+
}
|
|
213
|
+
return realParent;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function realpathThroughExistingAncestor(target: string, signal?: AbortSignal): Promise<string> {
|
|
217
|
+
const resolved = path.resolve(target);
|
|
218
|
+
const missing: string[] = [];
|
|
219
|
+
let cursor = resolved;
|
|
220
|
+
for (;;) {
|
|
221
|
+
signal?.throwIfAborted();
|
|
222
|
+
try {
|
|
223
|
+
const real = await fs.promises.realpath(cursor);
|
|
224
|
+
return missing.length === 0 ? real : path.join(real, ...missing.reverse());
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (!isEnoent(error)) throw error;
|
|
227
|
+
const parent = path.dirname(cursor);
|
|
228
|
+
if (parent === cursor) return resolved;
|
|
229
|
+
missing.push(path.basename(cursor));
|
|
230
|
+
cursor = parent;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
120
235
|
async function readExistingPatchFile(fileSystem: FileSystem, absolutePath: string, path: string): Promise<string> {
|
|
121
236
|
try {
|
|
122
237
|
return await fileSystem.read(absolutePath);
|
|
@@ -160,6 +275,9 @@ export async function applyPatch(input: PatchInput, options: ApplyPatchOptions):
|
|
|
160
275
|
const destPath = resolvePath(input.rename);
|
|
161
276
|
if (destPath !== absolutePath) mutationPaths.push(destPath);
|
|
162
277
|
}
|
|
278
|
+
// The cross-process lock creates missing parent directories for `<path>.lock`.
|
|
279
|
+
// Check every locked create/move path, including a rename on create, first.
|
|
280
|
+
await preflightPatchMkdir(input, options);
|
|
163
281
|
const usingDefaultFs = options.fs === undefined || options.fs === defaultFileSystem;
|
|
164
282
|
// dryRun/preview must stay read-only: never create durable `<path>.lock` dirs
|
|
165
283
|
// (would fail on read-only parents and mutate the FS during preview) (#2900 review).
|
|
@@ -167,6 +285,36 @@ export async function applyPatch(input: PatchInput, options: ApplyPatchOptions):
|
|
|
167
285
|
return withEditPathMutation(mutationPaths, () => applyNormalizedPatch(input, options), { crossProcess });
|
|
168
286
|
}
|
|
169
287
|
|
|
288
|
+
/** Read-only mkdir check for one patch hunk. Does not create directories. */
|
|
289
|
+
export async function preflightPatchMkdir(input: PatchInput, options: ApplyPatchOptions): Promise<void> {
|
|
290
|
+
const resolvePath = (p: string): string => resolveToCwd(p, options.cwd);
|
|
291
|
+
const absolutePath = resolvePath(input.path);
|
|
292
|
+
const op = input.op ?? "update";
|
|
293
|
+
if (op === "create") {
|
|
294
|
+
await rejectPatchMkdirEscape(options.cwd, absolutePath, options.localRoot, input.path, options.signal);
|
|
295
|
+
}
|
|
296
|
+
if (input.rename) {
|
|
297
|
+
const destPath = resolvePath(input.rename);
|
|
298
|
+
if (destPath !== absolutePath) {
|
|
299
|
+
// The source is locked too. Check it before that lock can mkdir through a symlink.
|
|
300
|
+
await rejectPatchMkdirEscape(options.cwd, absolutePath, options.localRoot, input.path, options.signal);
|
|
301
|
+
await rejectPatchMkdirEscape(options.cwd, destPath, options.localRoot, input.rename, options.signal);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function rejectPatchMkdirEscape(
|
|
307
|
+
cwd: string,
|
|
308
|
+
absolutePath: string,
|
|
309
|
+
localRoot: string | undefined,
|
|
310
|
+
displayPath: string,
|
|
311
|
+
signal?: AbortSignal,
|
|
312
|
+
): Promise<void> {
|
|
313
|
+
const parentDir = path.dirname(absolutePath);
|
|
314
|
+
if (!parentDir || parentDir === ".") return;
|
|
315
|
+
await resolvePatchMkdirPath(cwd, absolutePath, localRoot, parentDir, displayPath, signal);
|
|
316
|
+
}
|
|
317
|
+
|
|
170
318
|
/**
|
|
171
319
|
* Apply a normalized patch operation to the filesystem.
|
|
172
320
|
* Caller must already hold path mutation rights when concurrent writers exist.
|
|
@@ -179,6 +327,8 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
|
|
|
179
327
|
fs = defaultFileSystem,
|
|
180
328
|
fuzzyThreshold = DEFAULT_FUZZY_THRESHOLD,
|
|
181
329
|
allowFuzzy = true,
|
|
330
|
+
localRoot,
|
|
331
|
+
signal,
|
|
182
332
|
} = options;
|
|
183
333
|
|
|
184
334
|
const resolvePath = (p: string): string => resolveToCwd(p, cwd);
|
|
@@ -201,11 +351,17 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
|
|
|
201
351
|
const normalizedContent = normalizeCreateContent(input.diff);
|
|
202
352
|
const content = normalizedContent.endsWith("\n") ? normalizedContent : `${normalizedContent}\n`;
|
|
203
353
|
|
|
354
|
+
const parentDir = path.dirname(absolutePath);
|
|
355
|
+
const mkdirPath =
|
|
356
|
+
parentDir && parentDir !== "."
|
|
357
|
+
? await resolvePatchMkdirPath(cwd, absolutePath, localRoot, parentDir, input.path, signal)
|
|
358
|
+
: undefined;
|
|
204
359
|
if (!dryRun) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
await fs.mkdir(
|
|
360
|
+
if (mkdirPath) {
|
|
361
|
+
signal?.throwIfAborted();
|
|
362
|
+
await fs.mkdir(mkdirPath);
|
|
208
363
|
}
|
|
364
|
+
signal?.throwIfAborted();
|
|
209
365
|
await fs.write(absolutePath, content);
|
|
210
366
|
}
|
|
211
367
|
|
|
@@ -220,9 +376,14 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
|
|
|
220
376
|
|
|
221
377
|
// Handle DELETE operation
|
|
222
378
|
if (op === "delete") {
|
|
223
|
-
|
|
379
|
+
signal?.throwIfAborted();
|
|
380
|
+
const boundary = boundaryForPatchDelete(cwd, absolutePath, localRoot);
|
|
381
|
+
const deletePath = await resolvePatchDeletePath(boundary, absolutePath, input.path, signal);
|
|
382
|
+
signal?.throwIfAborted();
|
|
383
|
+
const oldContent = await readExistingPatchFile(fs, deletePath, input.path);
|
|
384
|
+
signal?.throwIfAborted();
|
|
224
385
|
if (!dryRun) {
|
|
225
|
-
await fs.delete(
|
|
386
|
+
await fs.delete(deletePath);
|
|
226
387
|
}
|
|
227
388
|
|
|
228
389
|
return {
|
|
@@ -260,6 +421,20 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
|
|
|
260
421
|
const finalContent = bom + restoreLineEndings(newContent, lineEnding);
|
|
261
422
|
const destPath = input.rename ? resolvePath(input.rename) : absolutePath;
|
|
262
423
|
const isMove = Boolean(input.rename) && destPath !== absolutePath;
|
|
424
|
+
let moveParent: string | undefined;
|
|
425
|
+
if (isMove) {
|
|
426
|
+
const parentDir = path.dirname(destPath);
|
|
427
|
+
if (parentDir && parentDir !== ".") {
|
|
428
|
+
moveParent = await resolvePatchMkdirPath(
|
|
429
|
+
cwd,
|
|
430
|
+
destPath,
|
|
431
|
+
localRoot,
|
|
432
|
+
parentDir,
|
|
433
|
+
input.rename ?? input.path,
|
|
434
|
+
signal,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
263
438
|
|
|
264
439
|
if (!dryRun) {
|
|
265
440
|
// Commit-time CAS: reject if another writer mutated the file after our
|
|
@@ -269,10 +444,11 @@ async function applyNormalizedPatch(input: PatchInput, options: ApplyPatchOption
|
|
|
269
444
|
throw new ApplyPatchError(`concurrent edit conflict: file changed since read: ${input.path}`);
|
|
270
445
|
}
|
|
271
446
|
if (isMove) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
await fs.mkdir(
|
|
447
|
+
if (moveParent) {
|
|
448
|
+
signal?.throwIfAborted();
|
|
449
|
+
await fs.mkdir(moveParent);
|
|
275
450
|
}
|
|
451
|
+
signal?.throwIfAborted();
|
|
276
452
|
await fs.write(destPath, finalContent);
|
|
277
453
|
await fs.delete(absolutePath);
|
|
278
454
|
} else {
|
|
@@ -493,12 +669,15 @@ export async function executePatchSingle(
|
|
|
493
669
|
// not `defaultFileSystem` by object identity. Force durable cross-process
|
|
494
670
|
// locking so multi-process agents cannot silently race past the path mutex
|
|
495
671
|
// (#2900 review: do not infer lock capability from FileSystem identity).
|
|
672
|
+
const localRoot = explicitLocalRootForSession(session);
|
|
496
673
|
const result = await applyPatch(input, {
|
|
497
674
|
cwd: session.cwd,
|
|
498
675
|
fs: patchFileSystem,
|
|
499
676
|
fuzzyThreshold,
|
|
500
677
|
allowFuzzy,
|
|
501
678
|
crossProcessLock: true,
|
|
679
|
+
localRoot,
|
|
680
|
+
signal,
|
|
502
681
|
});
|
|
503
682
|
|
|
504
683
|
// Post-write verification: only meaningful for in-place updates where the
|
|
@@ -9,6 +9,7 @@ import { markDesignedError } from "@gajae-code/utils/error-classification";
|
|
|
9
9
|
import * as z from "zod/v4";
|
|
10
10
|
import type { WritethroughCallback, WritethroughDeferredHandle } from "../../lsp";
|
|
11
11
|
import type { ToolSession } from "../../tools";
|
|
12
|
+
import { assertEditableFile, assertEditableFileContent } from "../../tools/auto-generated-guard";
|
|
12
13
|
import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
|
|
13
14
|
import { outputMeta } from "../../tools/output-meta";
|
|
14
15
|
import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
|
|
@@ -636,6 +637,7 @@ export async function executeReplaceSingle(
|
|
|
636
637
|
}
|
|
637
638
|
|
|
638
639
|
const absolutePath = resolvePlanPath(session, path);
|
|
640
|
+
await assertEditableFile(absolutePath, path, session.settings);
|
|
639
641
|
return withEditPathMutation([absolutePath], () =>
|
|
640
642
|
executeReplaceSingleUnderLock({
|
|
641
643
|
session,
|
|
@@ -664,6 +666,7 @@ async function executeReplaceSingleUnderLock(
|
|
|
664
666
|
},
|
|
665
667
|
): Promise<AgentToolResult<EditToolDetails, typeof replaceEditEntrySchema>> {
|
|
666
668
|
const {
|
|
669
|
+
session,
|
|
667
670
|
path,
|
|
668
671
|
signal,
|
|
669
672
|
batchRequest,
|
|
@@ -678,6 +681,8 @@ async function executeReplaceSingleUnderLock(
|
|
|
678
681
|
} = options;
|
|
679
682
|
|
|
680
683
|
const rawContent = await readEditFileText(absolutePath, path);
|
|
684
|
+
assertEditableFileContent(rawContent, path, session.settings);
|
|
685
|
+
await assertEditableFile(absolutePath, path, session.settings);
|
|
681
686
|
const { bom, text: content } = stripBom(rawContent);
|
|
682
687
|
const originalEnding = detectLineEnding(content);
|
|
683
688
|
const normalizedContent = normalizeToLF(content);
|
|
@@ -749,3 +754,134 @@ async function executeReplaceSingleUnderLock(
|
|
|
749
754
|
},
|
|
750
755
|
};
|
|
751
756
|
}
|
|
757
|
+
|
|
758
|
+
export async function executeReplaceEntries(
|
|
759
|
+
options: Omit<ExecuteReplaceSingleOptions, "params"> & { edits: ReplaceEditEntry[] },
|
|
760
|
+
): Promise<AgentToolResult<EditToolDetails, typeof replaceEditSchema>> {
|
|
761
|
+
if (options.edits.length <= 1) {
|
|
762
|
+
const only = options.edits[0];
|
|
763
|
+
if (!only) {
|
|
764
|
+
throw new ToolError("edits must not be empty.");
|
|
765
|
+
}
|
|
766
|
+
return executeReplaceSingle({ ...options, params: only });
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const { session, path, edits } = options;
|
|
770
|
+
try {
|
|
771
|
+
enforcePlanModeWrite(session, path);
|
|
772
|
+
for (const entry of edits) {
|
|
773
|
+
if (entry.old_text.length === 0) {
|
|
774
|
+
throw new ToolError("old_text must not be empty.");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const absolutePath = resolvePlanPath(session, path);
|
|
778
|
+
return await withEditPathMutation([absolutePath], () =>
|
|
779
|
+
executeReplaceEntriesUnderLock({ ...options, absolutePath }),
|
|
780
|
+
);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
const errorText = err instanceof Error ? err.message : String(err);
|
|
783
|
+
return {
|
|
784
|
+
content: [{ type: "text", text: `Error editing ${path}: ${errorText}` }],
|
|
785
|
+
details: { diff: "", path },
|
|
786
|
+
isError: true,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function executeReplaceEntriesUnderLock(
|
|
792
|
+
options: Omit<ExecuteReplaceSingleOptions, "params"> & {
|
|
793
|
+
edits: ReplaceEditEntry[];
|
|
794
|
+
absolutePath: string;
|
|
795
|
+
},
|
|
796
|
+
): Promise<AgentToolResult<EditToolDetails, typeof replaceEditSchema>> {
|
|
797
|
+
const {
|
|
798
|
+
session,
|
|
799
|
+
path,
|
|
800
|
+
edits,
|
|
801
|
+
signal,
|
|
802
|
+
batchRequest,
|
|
803
|
+
allowFuzzy,
|
|
804
|
+
fuzzyThreshold,
|
|
805
|
+
writethrough,
|
|
806
|
+
beginDeferredDiagnosticsForPath,
|
|
807
|
+
absolutePath,
|
|
808
|
+
} = options;
|
|
809
|
+
|
|
810
|
+
const rawContent = await readEditFileText(absolutePath, path);
|
|
811
|
+
assertEditableFileContent(rawContent, path, session.settings);
|
|
812
|
+
await assertEditableFile(absolutePath, path, session.settings);
|
|
813
|
+
|
|
814
|
+
const { bom, text: content } = stripBom(rawContent);
|
|
815
|
+
const originalEnding = detectLineEnding(content);
|
|
816
|
+
let normalizedContent = normalizeToLF(content);
|
|
817
|
+
const originalNormalized = normalizedContent;
|
|
818
|
+
const texts: string[] = [];
|
|
819
|
+
|
|
820
|
+
for (const [index, entry] of edits.entries()) {
|
|
821
|
+
const normalizedOldText = normalizeToLF(entry.old_text);
|
|
822
|
+
const normalizedNewText = normalizeToLF(entry.new_text);
|
|
823
|
+
const replaced = await replaceText(normalizedContent, normalizedOldText, normalizedNewText, {
|
|
824
|
+
fuzzy: allowFuzzy,
|
|
825
|
+
all: entry.all ?? false,
|
|
826
|
+
threshold: fuzzyThreshold,
|
|
827
|
+
});
|
|
828
|
+
if (replaced.count === 0) {
|
|
829
|
+
const matchOutcome = await findMatch(normalizedContent, normalizedOldText, {
|
|
830
|
+
allowFuzzy,
|
|
831
|
+
threshold: fuzzyThreshold,
|
|
832
|
+
});
|
|
833
|
+
if (matchOutcome.occurrences && matchOutcome.occurrences > 1) {
|
|
834
|
+
throw new ToolError(formatOccurrenceError(path, matchOutcome));
|
|
835
|
+
}
|
|
836
|
+
throw new EditMatchError(path, normalizedOldText, matchOutcome.closest, {
|
|
837
|
+
allowFuzzy,
|
|
838
|
+
threshold: fuzzyThreshold,
|
|
839
|
+
fuzzyMatches: matchOutcome.fuzzyMatches,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
if (normalizedContent === replaced.content) {
|
|
843
|
+
throw new ToolError(`Edits to ${path} resulted in no changes being made.`);
|
|
844
|
+
}
|
|
845
|
+
normalizedContent = replaced.content;
|
|
846
|
+
if (index < edits.length - 1) {
|
|
847
|
+
assertEditableFileContent(normalizedContent, path, session.settings);
|
|
848
|
+
}
|
|
849
|
+
texts.push(
|
|
850
|
+
replaced.count > 1
|
|
851
|
+
? `Successfully replaced ${replaced.count} occurrences in ${path}.`
|
|
852
|
+
: `Successfully replaced text in ${path}.`,
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const finalContent = await serializeEditFileText(
|
|
857
|
+
absolutePath,
|
|
858
|
+
path,
|
|
859
|
+
bom + restoreLineEndings(normalizedContent, originalEnding),
|
|
860
|
+
);
|
|
861
|
+
const diagnostics = await writethrough(
|
|
862
|
+
absolutePath,
|
|
863
|
+
finalContent,
|
|
864
|
+
signal,
|
|
865
|
+
Bun.file(absolutePath),
|
|
866
|
+
batchRequest,
|
|
867
|
+
dst => (dst === absolutePath ? beginDeferredDiagnosticsForPath(absolutePath) : undefined),
|
|
868
|
+
);
|
|
869
|
+
invalidateFsScanAfterWrite(absolutePath);
|
|
870
|
+
|
|
871
|
+
const diffResult = generateDiffString(originalNormalized, normalizedContent);
|
|
872
|
+
const meta = outputMeta()
|
|
873
|
+
.diagnostics(diagnostics?.summary ?? "", diagnostics?.messages ?? [])
|
|
874
|
+
.get();
|
|
875
|
+
return {
|
|
876
|
+
content: [{ type: "text", text: texts.join("\n") }],
|
|
877
|
+
details: {
|
|
878
|
+
diff: diffResult.diff,
|
|
879
|
+
path: absolutePath,
|
|
880
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
881
|
+
diagnostics,
|
|
882
|
+
meta,
|
|
883
|
+
oldText: rawContent,
|
|
884
|
+
newText: finalContent,
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
}
|
|
@@ -523,15 +523,41 @@ async function spawnJsWorker(): Promise<WorkerHandle> {
|
|
|
523
523
|
}
|
|
524
524
|
|
|
525
525
|
function wrapBunWorker(worker: Worker): WorkerHandle {
|
|
526
|
+
// Cells share parentPort, so worker→host frames use a port transferred before
|
|
527
|
+
// the first cell. worker "message" events are accepted only until ready.
|
|
528
|
+
const channel = new MessageChannel();
|
|
529
|
+
const hostPort = channel.port2;
|
|
530
|
+
const queued: WorkerOutbound[] = [];
|
|
531
|
+
let onFrame: ((msg: WorkerOutbound) => void) | undefined;
|
|
532
|
+
let acceptWorkerEvent = true;
|
|
533
|
+
const deliver = (msg: WorkerOutbound): void => {
|
|
534
|
+
if (msg?.type === "ready" || msg?.type === "init-failed") acceptWorkerEvent = false;
|
|
535
|
+
if (onFrame) onFrame(msg);
|
|
536
|
+
else queued.push(msg);
|
|
537
|
+
};
|
|
538
|
+
const onPort = (event: Event): void => {
|
|
539
|
+
deliver((event as MessageEvent).data as WorkerOutbound);
|
|
540
|
+
};
|
|
541
|
+
const onWorker = (event: Event): void => {
|
|
542
|
+
if (!acceptWorkerEvent) return;
|
|
543
|
+
deliver((event as MessageEvent).data as WorkerOutbound);
|
|
544
|
+
};
|
|
545
|
+
hostPort.addEventListener("message", onPort);
|
|
546
|
+
worker.addEventListener("message", onWorker);
|
|
547
|
+
worker.postMessage({ type: "gjc-bind", port: channel.port1 }, { transfer: [channel.port1] });
|
|
526
548
|
return {
|
|
527
549
|
mode: "worker",
|
|
528
550
|
send(msg) {
|
|
529
551
|
worker.postMessage(msg);
|
|
530
552
|
},
|
|
531
553
|
onMessage(handler) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
return () =>
|
|
554
|
+
onFrame = handler;
|
|
555
|
+
for (const msg of queued.splice(0)) handler(msg);
|
|
556
|
+
return () => {
|
|
557
|
+
if (onFrame === handler) onFrame = undefined;
|
|
558
|
+
hostPort.removeEventListener("message", onPort);
|
|
559
|
+
worker.removeEventListener("message", onWorker);
|
|
560
|
+
};
|
|
535
561
|
},
|
|
536
562
|
async terminate() {
|
|
537
563
|
worker.terminate();
|
|
@@ -1,24 +1,57 @@
|
|
|
1
|
-
import { parentPort } from "node:worker_threads";
|
|
1
|
+
import { type MessagePort, parentPort } from "node:worker_threads";
|
|
2
2
|
import { WorkerCore } from "./worker-core";
|
|
3
3
|
import type { Transport, WorkerInbound, WorkerOutbound } from "./worker-protocol";
|
|
4
4
|
|
|
5
5
|
if (!parentPort) throw new Error("js worker-entry: missing parentPort");
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
22
|
-
};
|
|
7
|
+
const parent = parentPort;
|
|
8
|
+
|
|
9
|
+
function isHostPort(value: unknown): value is MessagePort {
|
|
10
|
+
if (!value || typeof value !== "object") return false;
|
|
11
|
+
const port = value as MessagePort;
|
|
12
|
+
return typeof port.postMessage === "function" && typeof port.on === "function";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readBindPort(data: unknown): MessagePort | undefined {
|
|
16
|
+
if (!data || typeof data !== "object") return undefined;
|
|
17
|
+
const record = data as { type?: unknown; port?: unknown };
|
|
18
|
+
if (record.type !== "gjc-bind" || !isHostPort(record.port)) return undefined;
|
|
19
|
+
return record.port;
|
|
20
|
+
}
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
function listen(port: MessagePort): void {
|
|
23
|
+
// Bound before any user cell. A later prototype patch cannot retarget this send.
|
|
24
|
+
const post = port.postMessage.bind(port);
|
|
25
|
+
const transport: Transport = {
|
|
26
|
+
send: (msg: WorkerOutbound) => {
|
|
27
|
+
post(msg);
|
|
28
|
+
},
|
|
29
|
+
onMessage: handler => {
|
|
30
|
+
const wrap = (data: unknown): void => handler(data as WorkerInbound);
|
|
31
|
+
parent.on("message", wrap);
|
|
32
|
+
return () => parent.off("message", wrap);
|
|
33
|
+
},
|
|
34
|
+
close: () => {
|
|
35
|
+
try {
|
|
36
|
+
port.close();
|
|
37
|
+
} catch {
|
|
38
|
+
// Already closed.
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
parent.close();
|
|
42
|
+
} catch {
|
|
43
|
+
// Already closed.
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
new WorkerCore(transport);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const onParent = (data: unknown): void => {
|
|
51
|
+
const port = readBindPort(data);
|
|
52
|
+
if (!port) return;
|
|
53
|
+
if (data && typeof data === "object") (data as { port?: unknown }).port = undefined;
|
|
54
|
+
parent.off("message", onParent);
|
|
55
|
+
listen(port);
|
|
56
|
+
};
|
|
57
|
+
parent.on("message", onParent);
|
package/src/eval/py/executor.ts
CHANGED
|
@@ -912,7 +912,17 @@ async function replaceSessionKernel(
|
|
|
912
912
|
const old = session.kernel;
|
|
913
913
|
const remaining = getRemainingTimeoutMs(options.deadlineMs);
|
|
914
914
|
const ownerIds = new Set(session.ownerIds);
|
|
915
|
-
|
|
915
|
+
try {
|
|
916
|
+
await shutdownOrRetainKernel(old, ownerIds, remaining === undefined ? undefined : Math.max(0, remaining));
|
|
917
|
+
} catch (error) {
|
|
918
|
+
// Suppress unconfirmed shutdown error ONLY when we have positive evidence the
|
|
919
|
+
// process actually exited (exitCode is set). Without that evidence, propagate the error
|
|
920
|
+
// so we don't start a replacement while the old subprocess is still running.
|
|
921
|
+
// See https://github.com/yeachan-heo/gajae-code/issues/6569
|
|
922
|
+
const hasConfirmedExit = old.getExitCode() !== null;
|
|
923
|
+
if (!hasConfirmedExit || !(error instanceof Error) || error.name !== "PythonKernelShutdownUnconfirmedError")
|
|
924
|
+
throw error;
|
|
925
|
+
}
|
|
916
926
|
throwIfExecutionCancelled(options);
|
|
917
927
|
if (sessions.get(session.sessionId) !== session) {
|
|
918
928
|
throw new PythonExecutionCancelledError(false);
|
|
@@ -1076,7 +1086,6 @@ export function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
|
1076
1086
|
const toShutdown: PythonSession[] = [];
|
|
1077
1087
|
const initializersToAbort: InitializingPythonSession[] = [];
|
|
1078
1088
|
for (const entry of initializing) {
|
|
1079
|
-
entry.retirementOwnerIds.add(ownerId);
|
|
1080
1089
|
let hasOwnerRequest = false;
|
|
1081
1090
|
for (const request of entry.requests) {
|
|
1082
1091
|
if (request.ownerId === ownerId && !request.signal?.aborted) {
|
|
@@ -1089,6 +1098,7 @@ export function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
|
1089
1098
|
if (entry.ownerIds.size > 0) removeKernelOwner(entry.kernel, ownerId);
|
|
1090
1099
|
}
|
|
1091
1100
|
if (![...entry.ownerIds].some(id => id !== ownerId)) {
|
|
1101
|
+
entry.retirementOwnerIds.add(ownerId);
|
|
1092
1102
|
entry.cancelled ??= new PythonExecutionCancelledError(false);
|
|
1093
1103
|
initializersToAbort.push(entry);
|
|
1094
1104
|
}
|
|
@@ -1137,10 +1147,10 @@ export function disposeKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
|
1137
1147
|
return (async () => {
|
|
1138
1148
|
await Promise.all([
|
|
1139
1149
|
...requests.map(request => request.completion),
|
|
1140
|
-
...
|
|
1150
|
+
...initializersToAbort.map(entry => entry.promise.catch(() => undefined)),
|
|
1141
1151
|
]);
|
|
1142
1152
|
const failures: unknown[] = [];
|
|
1143
|
-
for (const entry of
|
|
1153
|
+
for (const entry of initializersToAbort)
|
|
1144
1154
|
if (entry.cleanupFailed) {
|
|
1145
1155
|
if (entry.kernel) retainKernelForOwners(entry.kernel, new Set([ownerId]));
|
|
1146
1156
|
failures.push(entry.cleanupError);
|