@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
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -117,7 +117,7 @@ import {
|
|
|
117
117
|
} from "./managed-task-dag";
|
|
118
118
|
import { reconcileRunningManagedVerification, verifyManagedTaskAttempt } from "./managed-task-verification";
|
|
119
119
|
import { createMasterCapabilityVerifier, readEndpoint } from "./master-capability";
|
|
120
|
-
import { isProcessIncarnation } from "./process-incarnation";
|
|
120
|
+
import { isProcessIncarnation, observeProcessIncarnation } from "./process-incarnation";
|
|
121
121
|
import { sdkInternalRuntimeImage } from "./runtime";
|
|
122
122
|
import {
|
|
123
123
|
type IndexedSession,
|
|
@@ -165,6 +165,21 @@ export interface BrokerSettings {
|
|
|
165
165
|
masterCapabilityVerifier?: MasterCapabilityVerifier;
|
|
166
166
|
/** Grace before an orphaned spawn child closes; schema-bounded in production. */
|
|
167
167
|
masterOrphanGraceMs?: number;
|
|
168
|
+
/**
|
|
169
|
+
* Grace before a broker nobody is left to use exits on its own. The clock only
|
|
170
|
+
* runs while the index proves no live session host and no open spawn authority,
|
|
171
|
+
* so a broker whose client died without closing exits instead of holding its
|
|
172
|
+
* port, its publication, and its lock for the life of the machine.
|
|
173
|
+
*/
|
|
174
|
+
abandonedGraceMs?: number;
|
|
175
|
+
/**
|
|
176
|
+
* Whether this broker may end itself once nobody is left to use it. Only the
|
|
177
|
+
* detached autostart entry point enables this: an operator-supervised
|
|
178
|
+
* `gjc sdk broker run` service owns its own lifetime, and its supervisor reads an
|
|
179
|
+
* owned-root completion as a clean exit, so self-reaping would quietly retire a
|
|
180
|
+
* service that was meant to keep serving.
|
|
181
|
+
*/
|
|
182
|
+
reapWhenAbandoned?: boolean;
|
|
168
183
|
/** Host model resolver override for lifecycle tests and embedders. */
|
|
169
184
|
resolveModelPin?: SdkHostModelResolver;
|
|
170
185
|
/**
|
|
@@ -202,6 +217,8 @@ type ResolvedBrokerSettings = {
|
|
|
202
217
|
spawnSubstrateProvider?: SpawnSubstrateProvider;
|
|
203
218
|
spawnPromptLayer?: SpawnPromptLayer;
|
|
204
219
|
masterOrphanGraceMs: number;
|
|
220
|
+
abandonedGraceMs: number;
|
|
221
|
+
reapWhenAbandoned: boolean;
|
|
205
222
|
restartRequestId?: string;
|
|
206
223
|
};
|
|
207
224
|
export function resolveBrokerPackageGeneration(): string {
|
|
@@ -227,6 +244,7 @@ function modelResolutionCwd(input: Record<string, unknown>): string | undefined
|
|
|
227
244
|
|
|
228
245
|
export type BrokerErrorCode =
|
|
229
246
|
| "idempotency_conflict"
|
|
247
|
+
| "legacy_lifecycle_ambiguous"
|
|
230
248
|
| "terminal_uncertain"
|
|
231
249
|
| "broker_restarting"
|
|
232
250
|
| "unavailable"
|
|
@@ -623,6 +641,7 @@ const SPAWN_HOST_REGISTRATION_TIMEOUT_MS = 10_000;
|
|
|
623
641
|
const SPAWN_HOST_REGISTRATION_POLL_MS = 50;
|
|
624
642
|
const SPAWN_PROMPT_EXCHANGE_TIMEOUT_MS = 10_000;
|
|
625
643
|
const MASTER_ORPHAN_GRACE_DEFAULT_MS = 120_000;
|
|
644
|
+
const BROKER_ABANDONED_GRACE_DEFAULT_MS = 120_000;
|
|
626
645
|
const MANAGED_CLOSE_WAIT_MS = 10_000;
|
|
627
646
|
|
|
628
647
|
function settlesWithin(promise: Promise<unknown>, timeoutMs: number): Promise<boolean> {
|
|
@@ -1602,6 +1621,7 @@ const brokerLockReclaimHooksForTest = new WeakMap<
|
|
|
1602
1621
|
{ beforeQuarantine?: () => Promise<void>; onContended?: () => void }
|
|
1603
1622
|
>();
|
|
1604
1623
|
const livenessGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
1624
|
+
const abandonedGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
1605
1625
|
const heartbeatStallOverridesForTest = new WeakMap<Broker, PromiseWithResolvers<void>>();
|
|
1606
1626
|
const managedCloseWaitOverridesForTest = new WeakMap<Broker, number>();
|
|
1607
1627
|
|
|
@@ -1644,6 +1664,8 @@ export class Broker {
|
|
|
1644
1664
|
#publishedAt: bigint | null = null;
|
|
1645
1665
|
#startedAt: bigint | null = null;
|
|
1646
1666
|
#watchInFlight = false;
|
|
1667
|
+
#abandonedAt: number | null = null;
|
|
1668
|
+
#abandonedReapInFlight = false;
|
|
1647
1669
|
#checkpointInFlight = false;
|
|
1648
1670
|
#stopping = false;
|
|
1649
1671
|
#transport: BrokerTransport | null = null;
|
|
@@ -1683,6 +1705,8 @@ export class Broker {
|
|
|
1683
1705
|
spawnSubstrateProvider: settings.spawnSubstrateProvider,
|
|
1684
1706
|
spawnPromptLayer: settings.spawnPromptLayer,
|
|
1685
1707
|
masterOrphanGraceMs: settings.masterOrphanGraceMs ?? MASTER_ORPHAN_GRACE_DEFAULT_MS,
|
|
1708
|
+
abandonedGraceMs: settings.abandonedGraceMs ?? BROKER_ABANDONED_GRACE_DEFAULT_MS,
|
|
1709
|
+
reapWhenAbandoned: settings.reapWhenAbandoned === true,
|
|
1686
1710
|
...(settings.restartRequestId === undefined ? {} : { restartRequestId: settings.restartRequestId }),
|
|
1687
1711
|
};
|
|
1688
1712
|
this.index = new SessionIndex(settings.agentDir);
|
|
@@ -3656,6 +3680,96 @@ export class Broker {
|
|
|
3656
3680
|
await this.#reapSpawnOrphans();
|
|
3657
3681
|
}
|
|
3658
3682
|
|
|
3683
|
+
/**
|
|
3684
|
+
* Grace before an idle broker exits. The clock starts only after the index has
|
|
3685
|
+
* already stopped proving any host live, so this grace is time on top of that
|
|
3686
|
+
* freshness window, never a substitute for it.
|
|
3687
|
+
*/
|
|
3688
|
+
#abandonedGraceMs(): number {
|
|
3689
|
+
return abandonedGraceOverridesForTest.get(this) ?? this.settings.abandonedGraceMs;
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
/**
|
|
3693
|
+
* What the index proves about the hosts using this agent directory. Liveness is
|
|
3694
|
+
* judged on read -- fresh checkpoint, live pid, matching incarnation -- so a host
|
|
3695
|
+
* that died without closing stops proving it without help. A row that is not live
|
|
3696
|
+
* is absence only once the OS confirms the process is gone: an inconclusive
|
|
3697
|
+
* reading is never death proof on any platform, and a pid that still exists but
|
|
3698
|
+
* cannot be matched to the recorded incarnation leaves the host undecided.
|
|
3699
|
+
*/
|
|
3700
|
+
async #sessionHostLiveness(): Promise<"live" | "absent" | "unknown"> {
|
|
3701
|
+
try {
|
|
3702
|
+
await this.index.refreshIfChanged();
|
|
3703
|
+
} catch (error) {
|
|
3704
|
+
logger.warn(`sdk broker: abandonment probe could not read the session index: ${String(error)}`);
|
|
3705
|
+
return "unknown";
|
|
3706
|
+
}
|
|
3707
|
+
for (const row of this.index.listSessionIdentities()) {
|
|
3708
|
+
if (row.endpointGeneration <= 0) continue;
|
|
3709
|
+
if (row.live) return "live";
|
|
3710
|
+
// The tri-state observer keeps positive absence separate from every
|
|
3711
|
+
// inconclusive outcome, which is the difference between reaping a broker and
|
|
3712
|
+
// killing one out from under a host this platform simply cannot confirm.
|
|
3713
|
+
const observed = observeProcessIncarnation(row.pid);
|
|
3714
|
+
if (observed.status === "unknown") return "unknown";
|
|
3715
|
+
if (observed.status === "absent") continue;
|
|
3716
|
+
const recorded = row.hostIncarnation ?? row.processIncarnation;
|
|
3717
|
+
if (recorded === undefined) return "unknown";
|
|
3718
|
+
if (observed.incarnation === recorded) return "live";
|
|
3719
|
+
// The pid names another process now: the host that registered it is gone.
|
|
3720
|
+
}
|
|
3721
|
+
return "absent";
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
/** Whether any client currently holds a transport connection to this broker. */
|
|
3725
|
+
#hasAttachedClient(): boolean {
|
|
3726
|
+
return (this.#transport?.clientCount ?? 0) > 0;
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
/** Whether this broker still owns a spawn authority it must keep serving. */
|
|
3730
|
+
#hasOpenSpawnAuthority(): boolean {
|
|
3731
|
+
const store = this.#spawnAuthority;
|
|
3732
|
+
if (!store) return false;
|
|
3733
|
+
for (const claim of store.claims()) {
|
|
3734
|
+
const authority = store.authority(claim.lifecycleIdentity);
|
|
3735
|
+
if (authority && (authority.closeState === "active" || authority.closeState === "close_requested"))
|
|
3736
|
+
return true;
|
|
3737
|
+
}
|
|
3738
|
+
return false;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
/**
|
|
3742
|
+
* Ends a broker nobody is left to use. A client that dies without closing leaves
|
|
3743
|
+
* no live session host behind, and nothing else reaps this process: without this
|
|
3744
|
+
* pass it would hold its port, its publication, and its lock until the machine
|
|
3745
|
+
* reboots. The clock only runs while absence is confirmed and no client holds a
|
|
3746
|
+
* connection, and any live host, attached client, open authority, or undecided
|
|
3747
|
+
* reading disarms it, so a slow reattach never races the grace. Brokers an
|
|
3748
|
+
* operator supervises never reap themselves at all.
|
|
3749
|
+
*/
|
|
3750
|
+
async #reapSelfIfAbandoned(): Promise<void> {
|
|
3751
|
+
if (!this.settings.reapWhenAbandoned) return;
|
|
3752
|
+
if (this.#stopping || this.#completionTask !== null || this.#fenceReason !== null) return;
|
|
3753
|
+
if (this.#abandonedReapInFlight) return;
|
|
3754
|
+
this.#abandonedReapInFlight = true;
|
|
3755
|
+
try {
|
|
3756
|
+
const liveness = await this.#sessionHostLiveness();
|
|
3757
|
+
const now = Date.now();
|
|
3758
|
+
if (liveness !== "absent" || this.#hasAttachedClient() || this.#hasOpenSpawnAuthority()) {
|
|
3759
|
+
this.#abandonedAt = null;
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
if (this.#abandonedAt === null) {
|
|
3763
|
+
this.#abandonedAt = now;
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
if (now - this.#abandonedAt < this.#abandonedGraceMs()) return;
|
|
3767
|
+
await this.#complete(this.#provenOwnedRoot() ? "owned-root" : "lost-root", "abandoned");
|
|
3768
|
+
} finally {
|
|
3769
|
+
this.#abandonedReapInFlight = false;
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3659
3773
|
async #reapSpawnOrphans(): Promise<void> {
|
|
3660
3774
|
if (this.#spawnReapInFlight || this.#stopping) return;
|
|
3661
3775
|
this.#spawnReapInFlight = true;
|
|
@@ -4005,6 +4119,11 @@ export class Broker {
|
|
|
4005
4119
|
this.#ownedLockIncarnation = undefined;
|
|
4006
4120
|
this.#watchInFlight = false;
|
|
4007
4121
|
this.#checkpointInFlight = false;
|
|
4122
|
+
// A restarted instance is a new generation: it holds none of the state the
|
|
4123
|
+
// previous one stopped with, so the abandonment clock it armed (and any reap
|
|
4124
|
+
// still unwinding) must not survive into the generation clients reconnect to.
|
|
4125
|
+
this.#abandonedAt = null;
|
|
4126
|
+
this.#abandonedReapInFlight = false;
|
|
4008
4127
|
this.#throwIfStartupAborted();
|
|
4009
4128
|
await Promise.all([this.ledger.assertSupportedStateVersions(), readBrokerDiscovery(this.settings.agentDir)]);
|
|
4010
4129
|
this.#throwIfStartupAborted();
|
|
@@ -4178,6 +4297,7 @@ export class Broker {
|
|
|
4178
4297
|
this.#heartbeatTimer = setInterval(() => {
|
|
4179
4298
|
void this.#watchPublication();
|
|
4180
4299
|
void this.#reapSpawnOrphans();
|
|
4300
|
+
void this.#reapSelfIfAbandoned();
|
|
4181
4301
|
}, cadenceMs);
|
|
4182
4302
|
if (this.#startupPostPublicationDelayMs > 0) await Bun.sleep(this.#startupPostPublicationDelayMs);
|
|
4183
4303
|
// This process is now the independently verified successor: its own
|
|
@@ -4227,6 +4347,10 @@ export class Broker {
|
|
|
4227
4347
|
this.discovery?.ownerId === this.#owner
|
|
4228
4348
|
);
|
|
4229
4349
|
}
|
|
4350
|
+
/** Whether this broker completed its initial retained discovery publication. */
|
|
4351
|
+
get hasPublishedDiscovery(): boolean {
|
|
4352
|
+
return this.#publishedAt !== null;
|
|
4353
|
+
}
|
|
4230
4354
|
get completion(): Promise<void> {
|
|
4231
4355
|
return this.#completion;
|
|
4232
4356
|
}
|
|
@@ -5348,7 +5472,7 @@ export class Broker {
|
|
|
5348
5472
|
},
|
|
5349
5473
|
)
|
|
5350
5474
|
) {
|
|
5351
|
-
return error("
|
|
5475
|
+
return error("legacy_lifecycle_ambiguous", "legacy lifecycle request has an ambiguous target");
|
|
5352
5476
|
}
|
|
5353
5477
|
}
|
|
5354
5478
|
}
|
|
@@ -5424,6 +5548,9 @@ export class Broker {
|
|
|
5424
5548
|
const begun = await this.ledger.begin(identity, requestHash, metadata);
|
|
5425
5549
|
if (begun.kind === "replay") {
|
|
5426
5550
|
const replay = begun.entry.response as BrokerResponse;
|
|
5551
|
+
// Proven errors are immutable even when their response carries cleanup
|
|
5552
|
+
// metadata. Only unproven cleanup outcomes may execute again.
|
|
5553
|
+
if (begun.entry.state === "terminal_error") return replay;
|
|
5427
5554
|
const cleanup = cleanupFromResponse(replay) ?? reconstructedDeleteCleanup;
|
|
5428
5555
|
if (!cleanup) {
|
|
5429
5556
|
if (
|
|
@@ -5468,7 +5595,7 @@ export class Broker {
|
|
|
5468
5595
|
const outcome = await executeLifecycle(this, operation, input, identity, cleanup);
|
|
5469
5596
|
const response = outcome.response;
|
|
5470
5597
|
const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
|
|
5471
|
-
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
5598
|
+
const recorded = await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
5472
5599
|
...(operation === "session.delete" &&
|
|
5473
5600
|
typeof input.sessionId === "string" &&
|
|
5474
5601
|
lifecycleResponseState(response) === "terminal_uncertain" &&
|
|
@@ -5480,6 +5607,10 @@ export class Broker {
|
|
|
5480
5607
|
...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
|
|
5481
5608
|
...(outcome.startupFailure ? { startupFailure: outcome.startupFailure } : {}),
|
|
5482
5609
|
});
|
|
5610
|
+
// A late transition may have proven a terminal result already. The ledger
|
|
5611
|
+
// preserves that row; return it instead of the freshly computed response.
|
|
5612
|
+
if (recorded.state === "terminal_error" && recorded.response !== undefined)
|
|
5613
|
+
return recorded.response as BrokerResponse;
|
|
5483
5614
|
return response;
|
|
5484
5615
|
}
|
|
5485
5616
|
if (begun.kind === "idempotency_conflict")
|
|
@@ -5492,7 +5623,7 @@ export class Broker {
|
|
|
5492
5623
|
const outcome = await executeLifecycle(this, operation, input, identity, cleanup);
|
|
5493
5624
|
const response = outcome.response;
|
|
5494
5625
|
const storedResponse = credentialFreeLifecycleResponse(response) as BrokerResponse;
|
|
5495
|
-
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
5626
|
+
const recorded = await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
5496
5627
|
...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
|
|
5497
5628
|
...(operation === "session.delete" &&
|
|
5498
5629
|
typeof input.sessionId === "string" &&
|
|
@@ -5505,6 +5636,8 @@ export class Broker {
|
|
|
5505
5636
|
...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
|
|
5506
5637
|
...(outcome.startupFailure ? { startupFailure: outcome.startupFailure } : {}),
|
|
5507
5638
|
});
|
|
5639
|
+
if (recorded.state === "terminal_error" && recorded.response !== undefined)
|
|
5640
|
+
return recorded.response as BrokerResponse;
|
|
5508
5641
|
return response;
|
|
5509
5642
|
}
|
|
5510
5643
|
if (begun.kind === "in_progress") return error("broker_restarting", "lifecycle operation is in progress");
|
|
@@ -5601,6 +5734,12 @@ export function setBrokerLockReclaimHooksForTest(
|
|
|
5601
5734
|
else brokerLockReclaimHooksForTest.set(broker, hooks);
|
|
5602
5735
|
}
|
|
5603
5736
|
|
|
5737
|
+
/** Test-only hook for shortening the bounded abandonment grace. */
|
|
5738
|
+
export function setAbandonedGraceForTest(broker: Broker, graceMs: number | undefined): void {
|
|
5739
|
+
if (graceMs === undefined) abandonedGraceOverridesForTest.delete(broker);
|
|
5740
|
+
else abandonedGraceOverridesForTest.set(broker, graceMs);
|
|
5741
|
+
}
|
|
5742
|
+
|
|
5604
5743
|
/** Test-only hook for shortening the bounded liveness deadline. */
|
|
5605
5744
|
export function setLivenessGraceForTest(broker: Broker, graceMs: number | undefined): void {
|
|
5606
5745
|
if (graceMs === undefined) livenessGraceOverridesForTest.delete(broker);
|
package/src/sdk/broker/ensure.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { FileHandle } from "node:fs/promises";
|
|
|
4
4
|
import * as fs from "node:fs/promises";
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { type ProjectEnvSnapshot, projectEnvSnapshot } from "@gajae-code/utils/env-file";
|
|
7
8
|
import { nativeProcessBindings } from "@gajae-code/utils/native-process";
|
|
8
9
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
9
10
|
import {
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
type FileLockOptions,
|
|
13
14
|
withFileLock,
|
|
14
15
|
} from "../../config/file-lock";
|
|
16
|
+
import { trustedCoordinatorEnv } from "../../gjc-runtime/sidecar-bootstrap-env";
|
|
15
17
|
import { SdkClient, SdkClientError } from "../client/client";
|
|
16
18
|
import { type BrokerStartupExitRecord, clearBrokerStartupExitRecord, readBrokerStartupExitRecord } from "./broker-exit";
|
|
17
19
|
import {
|
|
@@ -726,13 +728,64 @@ export interface BrokerStartupLockOptions {
|
|
|
726
728
|
onContended?: () => void;
|
|
727
729
|
}
|
|
728
730
|
|
|
731
|
+
const BROKER_TEST_SIGNAL_DIR_ENV = "GJC_SDK_TEST_BROKER_SIGNAL_DIR";
|
|
732
|
+
/** Bound the project dotenv read. A FIFO or a larger file is incomplete and rejected. */
|
|
733
|
+
const BROKER_SIGNAL_DOTENV_MAX_BYTES = 64 * 1024;
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Dotenv from the cwd that first loaded this module. `launch --worktree` changes
|
|
737
|
+
* cwd after Bun has already copied that file into `process.env`.
|
|
738
|
+
*/
|
|
739
|
+
const startupBrokerSignalSnapshot = projectEnvSnapshot(process.cwd(), {
|
|
740
|
+
maxFileBytes: BROKER_SIGNAL_DOTENV_MAX_BYTES,
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
function brokerSignalSnapshots(override?: readonly ProjectEnvSnapshot[]): readonly ProjectEnvSnapshot[] {
|
|
744
|
+
if (override) return override;
|
|
745
|
+
return [
|
|
746
|
+
projectEnvSnapshot(process.cwd(), { maxFileBytes: BROKER_SIGNAL_DOTENV_MAX_BYTES }),
|
|
747
|
+
startupBrokerSignalSnapshot,
|
|
748
|
+
];
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Operator environment only. A directory declared by the project dotenv is
|
|
753
|
+
* ignored so startup cannot create a path the repository chose.
|
|
754
|
+
* No signal value means no later dotenv read, so an unused seam cannot stall startup.
|
|
755
|
+
* An incomplete startup or cwd snapshot rejects the value.
|
|
756
|
+
*/
|
|
757
|
+
function trustedBrokerTestSignalDirectory(
|
|
758
|
+
env: NodeJS.ProcessEnv,
|
|
759
|
+
snapshots?: readonly ProjectEnvSnapshot[],
|
|
760
|
+
): string | undefined {
|
|
761
|
+
const raw = env[BROKER_TEST_SIGNAL_DIR_ENV];
|
|
762
|
+
if (!raw) return undefined;
|
|
763
|
+
let accepted: string | undefined;
|
|
764
|
+
for (const snapshot of brokerSignalSnapshots(snapshots)) {
|
|
765
|
+
if (snapshot.incomplete) return undefined;
|
|
766
|
+
const trusted = trustedCoordinatorEnv(BROKER_TEST_SIGNAL_DIR_ENV, env, snapshot);
|
|
767
|
+
if (trusted === undefined) return undefined;
|
|
768
|
+
accepted = trusted;
|
|
769
|
+
}
|
|
770
|
+
return accepted;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Drop a project-dotenv signal directory. Keep a trusted directory as the value that was accepted. */
|
|
774
|
+
function scrubUntrustedBrokerTestSignalDirectory(environment: NodeJS.ProcessEnv): void {
|
|
775
|
+
const trusted = trustedBrokerTestSignalDirectory(environment);
|
|
776
|
+
for (const name of Object.keys(environment)) {
|
|
777
|
+
if (name.toUpperCase() === BROKER_TEST_SIGNAL_DIR_ENV) delete environment[name];
|
|
778
|
+
}
|
|
779
|
+
if (trusted !== undefined) environment[BROKER_TEST_SIGNAL_DIR_ENV] = trusted;
|
|
780
|
+
}
|
|
781
|
+
|
|
729
782
|
/**
|
|
730
783
|
* Test-only phase observation for detached broker startup regressions. The
|
|
731
|
-
*
|
|
732
|
-
* ownership or error handling when the
|
|
784
|
+
* directory is opt-in through a trusted operator environment and never affects
|
|
785
|
+
* broker ownership or error handling when the seam is absent.
|
|
733
786
|
*/
|
|
734
787
|
export async function emitBrokerStartupTestSignal(signal: string): Promise<void> {
|
|
735
|
-
const directory = process.env
|
|
788
|
+
const directory = trustedBrokerTestSignalDirectory(process.env);
|
|
736
789
|
if (!directory || !/^[a-z0-9-]+$/.test(signal)) return;
|
|
737
790
|
try {
|
|
738
791
|
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
@@ -1203,11 +1256,11 @@ function registerBrokerOwner(
|
|
|
1203
1256
|
handoff = undefined;
|
|
1204
1257
|
} else if (publishedDiscovery) {
|
|
1205
1258
|
try {
|
|
1206
|
-
await
|
|
1207
|
-
agentDir,
|
|
1259
|
+
await shutdownBrokerViaRpc(
|
|
1208
1260
|
publishedDiscovery,
|
|
1209
1261
|
Date.now() +
|
|
1210
1262
|
Math.max(BROKER_DISCOVERY_BUDGET.staleRetirementMs, timing.gracefulMs + timing.killVerifyMs),
|
|
1263
|
+
agentDir,
|
|
1211
1264
|
);
|
|
1212
1265
|
} catch (error) {
|
|
1213
1266
|
if (process.platform === "darwin" && expectedBrokerPid !== child.pid) throw error;
|
|
@@ -1270,6 +1323,7 @@ function brokerSpawnEnvironment(command: SdkInternalSpawnCommand, override?: Nod
|
|
|
1270
1323
|
delete environment.PI_COMPILED;
|
|
1271
1324
|
delete environment.GJC_COMPILED;
|
|
1272
1325
|
}
|
|
1326
|
+
scrubUntrustedBrokerTestSignalDirectory(environment);
|
|
1273
1327
|
return environment;
|
|
1274
1328
|
}
|
|
1275
1329
|
|
|
@@ -1343,9 +1397,15 @@ async function shutdownPublishedBrokerViaRpc(
|
|
|
1343
1397
|
else await waitForBrokerIdentityGone(discovery, deadline);
|
|
1344
1398
|
return;
|
|
1345
1399
|
}
|
|
1400
|
+
await shutdownBrokerViaRpc(current, deadline, agentDir);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/** Stop a broker whose authenticated discovery was retained by its local owner. */
|
|
1404
|
+
async function shutdownBrokerViaRpc(discovery: BrokerDiscovery, deadline: number, agentDir: string): Promise<void> {
|
|
1405
|
+
if (brokerIdentityIsGone(discovery)) return;
|
|
1346
1406
|
const remainingMs = deadline - ensureBrokerTiming.now();
|
|
1347
1407
|
if (remainingMs <= 0) throw new Error("SDK broker shutdown deadline elapsed before RPC connection.");
|
|
1348
|
-
const client = await SdkClient.connect(
|
|
1408
|
+
const client = await SdkClient.connect(discovery.url, discovery.token, {
|
|
1349
1409
|
timeoutMs: Math.max(1, Math.min(2_000, remainingMs)),
|
|
1350
1410
|
});
|
|
1351
1411
|
try {
|
|
@@ -1954,6 +2014,14 @@ export function signalDetachedBrokerProcessForTest(
|
|
|
1954
2014
|
): boolean {
|
|
1955
2015
|
return signalDetachedBrokerProcess(pid, brokerIncarnation, signal, platform);
|
|
1956
2016
|
}
|
|
2017
|
+
/** Test hook: the signal directory accepted from this environment and snapshot. */
|
|
2018
|
+
export function trustedBrokerSignalDirectoryForTest(
|
|
2019
|
+
env: NodeJS.ProcessEnv,
|
|
2020
|
+
snapshot: ProjectEnvSnapshot | readonly ProjectEnvSnapshot[],
|
|
2021
|
+
): string | undefined {
|
|
2022
|
+
const snapshots = Array.isArray(snapshot) ? snapshot : [snapshot];
|
|
2023
|
+
return trustedBrokerTestSignalDirectory(env, snapshots);
|
|
2024
|
+
}
|
|
1957
2025
|
/** Test hook: resolves the complete broker environment without spawning. */
|
|
1958
2026
|
export function brokerSpawnEnvironmentForTest(
|
|
1959
2027
|
command: SdkInternalSpawnCommand,
|
package/src/sdk/broker/hop.ts
CHANGED
|
@@ -7,9 +7,18 @@ import type { Process } from "@gajae-code/natives";
|
|
|
7
7
|
import { nativeProcessBindings } from "@gajae-code/utils/native-process";
|
|
8
8
|
import type { BrokerHopMessage } from "./ensure";
|
|
9
9
|
|
|
10
|
+
/** Handoff protocol frames written to the trampoline's stdin. */
|
|
10
11
|
export const BROKER_HANDOFF_ACKNOWLEDGEMENT = "GJC_BROKER_HANDOFF_ACK\n";
|
|
11
12
|
export const BROKER_HANDOFF_COMMIT = "GJC_BROKER_HANDOFF_COMMIT\n";
|
|
12
13
|
export const BROKER_HANDOFF_ABORT = "GJC_BROKER_HANDOFF_ABORT\n";
|
|
14
|
+
/**
|
|
15
|
+
* Backstop for a retained trampoline whose parent never sends a decision. It must
|
|
16
|
+
* outlive the parent's own broker discovery deadline (which is derived from the
|
|
17
|
+
* publication budget and can double across a guarded retry), otherwise the
|
|
18
|
+
* trampoline reaps the broker first and the parent misreports its own discovery
|
|
19
|
+
* timeout as an "exited before discovery" startup failure.
|
|
20
|
+
*/
|
|
21
|
+
export const BROKER_HANDOFF_DECISION_TIMEOUT_MS = 120_000;
|
|
13
22
|
|
|
14
23
|
/**
|
|
15
24
|
* Windows broker hop: spawns the real broker with detached:true and reports its pid.
|
|
@@ -165,7 +174,10 @@ export function waitForBrokerHandoffAcknowledgement(input: stream.Readable): Pro
|
|
|
165
174
|
}
|
|
166
175
|
|
|
167
176
|
/** Wait for a validated identity ACK followed by the parent's publication decision. */
|
|
168
|
-
export function waitForBrokerHandoffDecision(
|
|
177
|
+
export function waitForBrokerHandoffDecision(
|
|
178
|
+
input: stream.Readable,
|
|
179
|
+
timeoutMs = BROKER_HANDOFF_DECISION_TIMEOUT_MS,
|
|
180
|
+
): Promise<"commit" | "abort"> {
|
|
169
181
|
const decision = Promise.withResolvers<"commit" | "abort">();
|
|
170
182
|
let received = "";
|
|
171
183
|
let settled = false;
|
|
@@ -968,6 +968,9 @@ export class LifecycleLedger {
|
|
|
968
968
|
return this.#mutate(async () => {
|
|
969
969
|
const previous = this.#byIdentity.get(identity);
|
|
970
970
|
if (!previous) throw new Error("Unknown lifecycle identity");
|
|
971
|
+
// Match replay validation: proven terminal records admit no successor.
|
|
972
|
+
// A late failure must not append a row that will be quarantined on reopen.
|
|
973
|
+
if (terminal(previous.state)) return previous;
|
|
971
974
|
const next = {
|
|
972
975
|
...previous,
|
|
973
976
|
...fields,
|
|
@@ -951,6 +951,14 @@ type SessionLaunch = {
|
|
|
951
951
|
modelId?: string;
|
|
952
952
|
modelPreset?: string;
|
|
953
953
|
mcpServers?: SessionLifecycleMcpServer[];
|
|
954
|
+
/**
|
|
955
|
+
* Per-session environment for the hosted session (#6735). Applied to the
|
|
956
|
+
* session host's spawn environment (and therefore inherited by its Bash
|
|
957
|
+
* executor) on top of the broker's own environment, after `unsetEnv` has
|
|
958
|
+
* removed inherited names. Broker-reserved `GJC_*` names are rejected.
|
|
959
|
+
*/
|
|
960
|
+
env?: Record<string, string>;
|
|
961
|
+
unsetEnv?: string[];
|
|
954
962
|
/** Coordinator namespace dir; broker computes the state file path from launch.id (#2549). */
|
|
955
963
|
coordinatorStateDir?: string;
|
|
956
964
|
coordinatorSessionId?: string;
|
|
@@ -963,6 +971,85 @@ type SessionLaunch = {
|
|
|
963
971
|
};
|
|
964
972
|
|
|
965
973
|
const CREDENTIAL_NAME_PATTERN = /(?:token|secret|password|credential|api[_-]?key|auth|cookie)/iu;
|
|
974
|
+
const SESSION_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
975
|
+
const SESSION_ENV_MAX_ENTRIES = 256;
|
|
976
|
+
const SESSION_ENV_MAX_VALUE_LENGTH = 131_072;
|
|
977
|
+
|
|
978
|
+
function isSessionEnvName(name: unknown): name is string {
|
|
979
|
+
return (
|
|
980
|
+
typeof name === "string" &&
|
|
981
|
+
name.length <= 256 &&
|
|
982
|
+
SESSION_ENV_NAME_PATTERN.test(name) &&
|
|
983
|
+
// GJC_* carries broker-issued launch authority (request, effect marker,
|
|
984
|
+
// state root, coordinator correlation); a caller must not shadow it.
|
|
985
|
+
!name.toUpperCase().startsWith("GJC_")
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const sessionEnvKey = (name: string): string => (process.platform === "win32" ? name.toUpperCase() : name);
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Validates the optional per-session `env`/`unsetEnv` pair (#6735). Returns
|
|
993
|
+
* null when either is malformed so the caller fails the launch closed.
|
|
994
|
+
*/
|
|
995
|
+
export function lifecycleSessionEnv(input: {
|
|
996
|
+
env?: unknown;
|
|
997
|
+
unsetEnv?: unknown;
|
|
998
|
+
}): { env?: Record<string, string>; unsetEnv?: string[] } | null {
|
|
999
|
+
let env: Record<string, string> | undefined;
|
|
1000
|
+
if (input.env !== undefined) {
|
|
1001
|
+
if (input.env === null || typeof input.env !== "object" || Array.isArray(input.env)) return null;
|
|
1002
|
+
const entries = Object.entries(input.env as Record<string, unknown>);
|
|
1003
|
+
if (entries.length > SESSION_ENV_MAX_ENTRIES) return null;
|
|
1004
|
+
const seen = new Set<string>();
|
|
1005
|
+
for (const [name, value] of entries) {
|
|
1006
|
+
if (
|
|
1007
|
+
!isSessionEnvName(name) ||
|
|
1008
|
+
typeof value !== "string" ||
|
|
1009
|
+
value.length > SESSION_ENV_MAX_VALUE_LENGTH ||
|
|
1010
|
+
value.includes("\0") ||
|
|
1011
|
+
seen.has(sessionEnvKey(name))
|
|
1012
|
+
)
|
|
1013
|
+
return null;
|
|
1014
|
+
seen.add(sessionEnvKey(name));
|
|
1015
|
+
}
|
|
1016
|
+
env = Object.fromEntries(entries) as Record<string, string>;
|
|
1017
|
+
}
|
|
1018
|
+
let unsetEnv: string[] | undefined;
|
|
1019
|
+
if (input.unsetEnv !== undefined) {
|
|
1020
|
+
if (!Array.isArray(input.unsetEnv) || input.unsetEnv.length > SESSION_ENV_MAX_ENTRIES) return null;
|
|
1021
|
+
const keys = new Set<string>();
|
|
1022
|
+
for (const name of input.unsetEnv) {
|
|
1023
|
+
if (!isSessionEnvName(name) || keys.has(sessionEnvKey(name))) return null;
|
|
1024
|
+
keys.add(sessionEnvKey(name));
|
|
1025
|
+
}
|
|
1026
|
+
// Setting and unsetting the same name is contradictory, not a precedence question.
|
|
1027
|
+
if (env && Object.keys(env).some(name => keys.has(sessionEnvKey(name)))) return null;
|
|
1028
|
+
unsetEnv = [...(input.unsetEnv as string[])];
|
|
1029
|
+
}
|
|
1030
|
+
return { ...(env ? { env } : {}), ...(unsetEnv ? { unsetEnv } : {}) };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Composes the inherited part of a lifecycle session host's environment:
|
|
1035
|
+
* drops the process-local master capability, every `unsetEnv` name, and any
|
|
1036
|
+
* inherited name an explicit per-session `env` entry replaces (case-folded on
|
|
1037
|
+
* Windows so `Path` and `PATH` never both reach the child), then layers the
|
|
1038
|
+
* per-session `env` on top. Broker-issued `GJC_*` keys are applied after this.
|
|
1039
|
+
*/
|
|
1040
|
+
export function lifecycleSessionHostBaseEnv(
|
|
1041
|
+
inherited: NodeJS.ProcessEnv | Readonly<Record<string, string | undefined>>,
|
|
1042
|
+
launch: { env?: Readonly<Record<string, string>>; unsetEnv?: readonly string[] },
|
|
1043
|
+
): Record<string, string> {
|
|
1044
|
+
const removed = new Set([...(launch.unsetEnv ?? []), ...Object.keys(launch.env ?? {})].map(sessionEnvKey));
|
|
1045
|
+
// Valid names like `__proto__` must remain own environment entries.
|
|
1046
|
+
const base = Object.create(null) as Record<string, string>;
|
|
1047
|
+
for (const [name, value] of Object.entries(inherited)) {
|
|
1048
|
+
if (name === "GJC_MASTER_CAPABILITY" || typeof value !== "string" || removed.has(sessionEnvKey(name))) continue;
|
|
1049
|
+
base[name] = value;
|
|
1050
|
+
}
|
|
1051
|
+
return { ...base, ...launch.env };
|
|
1052
|
+
}
|
|
966
1053
|
|
|
967
1054
|
function lifecycleLaunchKnownSecrets(launch: SessionLaunch): string[] {
|
|
968
1055
|
const secrets = [
|
|
@@ -985,6 +1072,9 @@ function lifecycleLaunchKnownSecrets(launch: SessionLaunch): string[] {
|
|
|
985
1072
|
.filter(([name, value]) => value && CREDENTIAL_NAME_PATTERN.test(name))
|
|
986
1073
|
.map(([, value]) => value);
|
|
987
1074
|
}),
|
|
1075
|
+
...Object.entries(launch.env ?? {})
|
|
1076
|
+
.filter(([name, value]) => value && CREDENTIAL_NAME_PATTERN.test(name))
|
|
1077
|
+
.map(([, value]) => value),
|
|
988
1078
|
];
|
|
989
1079
|
return [...new Set(secrets.filter(secret => typeof secret === "string" && secret.length > 0))];
|
|
990
1080
|
}
|
|
@@ -5998,6 +6088,12 @@ async function launchInput(
|
|
|
5998
6088
|
if (input.mcpServers !== undefined && !isSessionLifecycleMcpServers(input.mcpServers))
|
|
5999
6089
|
return fail("invalid_input", "mcpServers must contain unique valid stdio, HTTP, or SSE server definitions.");
|
|
6000
6090
|
const mcpServers = input.mcpServers as SessionLifecycleMcpServer[] | undefined;
|
|
6091
|
+
const sessionEnv = lifecycleSessionEnv(input);
|
|
6092
|
+
if (!sessionEnv)
|
|
6093
|
+
return fail(
|
|
6094
|
+
"invalid_input",
|
|
6095
|
+
"env must map valid non-GJC_ variable names to strings, and unsetEnv must list distinct valid names not also set in env.",
|
|
6096
|
+
);
|
|
6001
6097
|
|
|
6002
6098
|
/**
|
|
6003
6099
|
* Prepared readiness is an explicit creation-only intent. Only the two exact
|
|
@@ -6047,6 +6143,7 @@ async function launchInput(
|
|
|
6047
6143
|
modelId,
|
|
6048
6144
|
modelPreset,
|
|
6049
6145
|
mcpServers,
|
|
6146
|
+
...sessionEnv,
|
|
6050
6147
|
worktree,
|
|
6051
6148
|
worktreePlan,
|
|
6052
6149
|
...(readiness ? { readiness } : {}),
|
|
@@ -6070,6 +6167,7 @@ async function launchInput(
|
|
|
6070
6167
|
sessionIdentity: saved.identity,
|
|
6071
6168
|
modelPreset,
|
|
6072
6169
|
mcpServers,
|
|
6170
|
+
...sessionEnv,
|
|
6073
6171
|
worktree,
|
|
6074
6172
|
worktreePlan,
|
|
6075
6173
|
...(coordinatorStateDir ? { coordinatorStateDir } : {}),
|
|
@@ -6104,6 +6202,7 @@ async function launchInput(
|
|
|
6104
6202
|
modelId,
|
|
6105
6203
|
modelPreset,
|
|
6106
6204
|
mcpServers,
|
|
6205
|
+
...sessionEnv,
|
|
6107
6206
|
worktree,
|
|
6108
6207
|
worktreePlan,
|
|
6109
6208
|
...(coordinatorStateDir ? { coordinatorStateDir } : {}),
|
|
@@ -7160,12 +7259,9 @@ async function executeLifecycleResponse(
|
|
|
7160
7259
|
stdio: ["ignore", "ignore", childStderrSink?.drainer.stdin ?? "ignore"],
|
|
7161
7260
|
env: {
|
|
7162
7261
|
// Master capability is process-local to direct Bash children and must
|
|
7163
|
-
// never cross a broker lifecycle launch boundary.
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
([key]) => key !== "GJC_MASTER_CAPABILITY",
|
|
7167
|
-
),
|
|
7168
|
-
),
|
|
7262
|
+
// never cross a broker lifecycle launch boundary. Per-session env
|
|
7263
|
+
// (#6735) is layered here, below the broker-issued GJC_* keys.
|
|
7264
|
+
...lifecycleSessionHostBaseEnv("kind" in cmd ? cmd.env : process.env, launch),
|
|
7169
7265
|
GJC_AGENT_DIR: broker.settings.agentDir,
|
|
7170
7266
|
GJC_CODING_AGENT_DIR: broker.settings.agentDir,
|
|
7171
7267
|
GJC_SESSION_ID: launch.id,
|