@gajae-code/coding-agent 0.17.2 → 0.17.4
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 +194 -0
- package/dist/types/cli/daemon-cli.d.ts +3 -1
- package/dist/types/cli/github-release.d.ts +17 -0
- package/dist/types/cli/public-command-entry.d.ts +56 -0
- package/dist/types/cli/public-command-errors.d.ts +126 -0
- package/dist/types/cli/public-command-evidence.d.ts +99 -0
- package/dist/types/cli/public-command-help.d.ts +65 -0
- package/dist/types/cli/public-command-registry.d.ts +69 -0
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -0
- package/dist/types/cli/skills-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +4 -50
- package/dist/types/commands/sdk.d.ts +20 -37
- package/dist/types/commands/setup.d.ts +3 -0
- package/dist/types/commands/skills.d.ts +9 -0
- package/dist/types/config/autorouting-tier-map.d.ts +18 -0
- package/dist/types/config/file-lock.d.ts +9 -1
- package/dist/types/config/model-profile-activation.d.ts +45 -1
- package/dist/types/config/model-registry.d.ts +11 -2
- package/dist/types/config/settings-schema.d.ts +11 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
- package/dist/types/daemon/operator-contract.d.ts +5 -1
- package/dist/types/eval/js/worker-protocol.d.ts +2 -0
- package/dist/types/extensibility/extensions/types.d.ts +12 -0
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
- package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/parse.d.ts +9 -1
- package/dist/types/main.d.ts +22 -0
- package/dist/types/modes/acp/acp-agent.d.ts +55 -1
- package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
- package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
- package/dist/types/modes/components/session-selector.d.ts +5 -2
- package/dist/types/modes/components/user-message-selector.d.ts +5 -2
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/runtime/process-lifecycle.d.ts +2 -0
- package/dist/types/runtime-mcp/manager.d.ts +56 -0
- package/dist/types/runtime-mcp/pool.d.ts +17 -0
- package/dist/types/runtime-mcp/redaction.d.ts +20 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
- package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
- package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
- package/dist/types/sdk/broker/broker.d.ts +13 -2
- package/dist/types/sdk/broker/identity.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
- package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
- package/dist/types/sdk/broker/runtime.d.ts +1 -1
- package/dist/types/sdk/broker/session-index.d.ts +22 -3
- package/dist/types/sdk/broker/spawn-authority.d.ts +4 -0
- package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
- package/dist/types/sdk/bus/index.d.ts +19 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
- package/dist/types/sdk/cli/master-cli.d.ts +6 -10
- package/dist/types/sdk/cli/rows.d.ts +19 -1
- package/dist/types/sdk/cli/session-cli.d.ts +22 -5
- package/dist/types/sdk/client/client.d.ts +2 -0
- package/dist/types/sdk/guides/cli.d.ts +3 -4
- package/dist/types/sdk/guides/verify.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +9 -0
- package/dist/types/sdk/host/session-runtime.d.ts +31 -0
- package/dist/types/sdk/host/types.d.ts +10 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/prompt-deadline-flush.d.ts +83 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +37 -0
- package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +48 -4
- package/dist/types/sdk/transport/stdio.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +168 -21
- package/dist/types/session/fallback-chain-controller.d.ts +6 -6
- package/dist/types/session/fold-coordinator.d.ts +17 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
- package/dist/types/session/session-manager.d.ts +64 -1
- package/dist/types/session/session-work-lease.d.ts +14 -0
- package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
- package/dist/types/setup/provider-onboarding.d.ts +81 -1
- package/dist/types/tools/bash.d.ts +37 -11
- package/dist/types/tools/job.d.ts +1 -1
- package/dist/types/tools/steer-fold.d.ts +31 -0
- package/dist/types/utils/git.d.ts +28 -2
- package/dist/types/utils/mupdf-embedded.d.ts +2 -0
- package/dist/types/utils/mupdf.d.ts +4 -0
- package/dist/types/utils/session-title-generation.d.ts +6 -0
- package/dist/types/utils/title-generator.d.ts +11 -2
- package/dist/types/web/scrapers/types.d.ts +3 -0
- package/package.json +18 -10
- package/scripts/build-binary.ts +9 -3
- package/scripts/compile-args.ts +0 -9
- package/scripts/embed-mupdf.ts +52 -0
- package/scripts/generate-sdk-operation-inventory.ts +31 -1
- package/scripts/vendor-markit.ts +196 -0
- package/src/async/job-manager.ts +22 -1
- package/src/cli/daemon-cli.ts +53 -18
- package/src/cli/github-release.ts +87 -5
- package/src/cli/mcp-cli.ts +38 -4
- package/src/cli/public-command-entry.ts +450 -0
- package/src/cli/public-command-errors.ts +581 -0
- package/src/cli/public-command-evidence.ts +717 -0
- package/src/cli/public-command-help.ts +360 -0
- package/src/cli/public-command-registry.ts +535 -0
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +9 -1
- package/src/cli/skills-cli.ts +46 -2
- package/src/cli.ts +61 -1
- package/src/commands/daemon.ts +65 -69
- package/src/commands/sdk.ts +175 -117
- package/src/commands/setup.ts +2 -0
- package/src/commands/skills.ts +11 -0
- package/src/config/autorouting-tier-map.ts +14 -0
- package/src/config/file-lock.ts +147 -51
- package/src/config/model-profile-activation.ts +334 -43
- package/src/config/model-registry.ts +183 -25
- package/src/config/settings-schema.ts +26 -2
- package/src/coordinator-mcp/policy.ts +10 -5
- package/src/coordinator-mcp/question-gate-codec.ts +17 -0
- package/src/coordinator-mcp/question-state.ts +175 -8
- package/src/coordinator-mcp/server.ts +507 -81
- package/src/coordinator-mcp/session-reaper.ts +6 -4
- package/src/crash/record-loader.ts +6 -2
- package/src/daemon/operator-contract.ts +26 -12
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
- package/src/eval/js/context-manager.ts +8 -8
- package/src/eval/js/worker-core.ts +9 -4
- package/src/eval/js/worker-protocol.ts +2 -0
- package/src/extensibility/extensions/runner.ts +52 -4
- package/src/extensibility/extensions/types.ts +10 -0
- package/src/extensibility/gjc-plugins/registry.ts +196 -37
- package/src/extensibility/runtime-skill-discovery.ts +61 -3
- package/src/extensibility/shared-events.ts +3 -1
- package/src/gjc-runtime/session-state-lock.ts +165 -24
- package/src/gjc-runtime/ultragoal-succession.ts +31 -3
- package/src/internal-urls/docs-index.generated.ts +20 -19
- package/src/internal-urls/embedded-protocol.ts +83 -0
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/parse.ts +21 -1
- package/src/internal-urls/router.ts +4 -2
- package/src/main.ts +123 -55
- package/src/modes/acp/acp-agent.ts +408 -35
- package/src/modes/acp/startup-provenance.ts +59 -0
- package/src/modes/components/custom-provider-wizard.ts +308 -7
- package/src/modes/components/oauth-selector.ts +9 -0
- package/src/modes/components/session-selector.ts +47 -4
- package/src/modes/components/user-message-selector.ts +57 -15
- package/src/modes/controllers/command-controller.ts +58 -32
- package/src/modes/controllers/extension-ui-controller.ts +33 -7
- package/src/modes/controllers/input-controller.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +161 -15
- package/src/modes/interactive-mode.ts +21 -7
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
- package/src/modes/types.ts +2 -2
- package/src/prompts/agent-fragments/browser-aside.md +1 -0
- package/src/prompts/system/title-system.md +1 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/job.md +1 -1
- package/src/prompts/tools/read.md +2 -0
- package/src/runtime/network-prewarm-service.test.ts +36 -1
- package/src/runtime/network-prewarm-service.ts +28 -3
- package/src/runtime/process-lifecycle.ts +22 -11
- package/src/runtime-mcp/config.ts +16 -1
- package/src/runtime-mcp/manager.ts +689 -55
- package/src/runtime-mcp/pool.test.ts +31 -0
- package/src/runtime-mcp/pool.ts +304 -28
- package/src/runtime-mcp/redaction.ts +42 -0
- package/src/runtime-mcp/startup-policy.ts +7 -0
- package/src/runtime-mcp/tool-bridge.ts +71 -0
- package/src/runtime-mcp/transports/http.ts +108 -25
- package/src/sdk/acp/adapter.ts +27 -8
- package/src/sdk/broker/broker.ts +378 -52
- package/src/sdk/broker/identity.ts +9 -1
- package/src/sdk/broker/lifecycle-ledger.ts +95 -31
- package/src/sdk/broker/lifecycle.ts +690 -141
- package/src/sdk/broker/runtime.ts +1 -1
- package/src/sdk/broker/session-index.ts +97 -4
- package/src/sdk/broker/spawn-authority.ts +35 -7
- package/src/sdk/broker/stderr-drainer.ts +59 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/index.ts +690 -80
- package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
- package/src/sdk/bus/telegram-daemon.ts +10 -3
- package/src/sdk/cli/master-cli.ts +47 -36
- package/src/sdk/cli/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +687 -184
- package/src/sdk/client/client.ts +18 -0
- package/src/sdk/guides/catalog.ts +2 -2
- package/src/sdk/guides/cli.ts +20 -62
- package/src/sdk/guides/verify.ts +14 -0
- package/src/sdk/host/host.ts +49 -5
- package/src/sdk/host/session-runtime.test.ts +138 -2
- package/src/sdk/host/session-runtime.ts +311 -97
- package/src/sdk/host/types.ts +12 -1
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
- package/src/sdk/host/websocket-transport.ts +40 -6
- package/src/sdk/index.ts +11 -0
- package/src/sdk/prompt-deadline-flush.ts +334 -0
- package/src/sdk/prompt-deadline-manager.ts +133 -1
- package/src/sdk/prompt-tool-boundary.ts +121 -0
- package/src/sdk/protocol/operation-inventory.generated.json +106 -0
- package/src/sdk/router/session-router.ts +9 -1
- package/src/sdk/session.ts +182 -7
- package/src/sdk/startup-capability.ts +1 -1
- package/src/sdk/transport/relay.ts +71 -6
- package/src/sdk/transport/serve-cli.ts +397 -42
- package/src/sdk/transport/socket.ts +12 -11
- package/src/sdk/transport/stdio.ts +7 -3
- package/src/session/agent-session.ts +2437 -400
- package/src/session/fallback-chain-controller.ts +6 -7
- package/src/session/fold-coordinator.ts +24 -5
- package/src/session/internal/managed-session-scope.ts +58 -18
- package/src/session/internal/managed-session-storage.ts +304 -11
- package/src/session/session-manager.ts +382 -44
- package/src/session/session-work-lease.ts +34 -0
- package/src/setup/model-onboarding-guidance.ts +1 -1
- package/src/setup/provider-onboarding.ts +397 -24
- package/src/slash-commands/builtin-registry.ts +258 -14
- package/src/task/index.ts +14 -2
- package/src/tools/bash.ts +98 -78
- package/src/tools/fetch.ts +119 -20
- package/src/tools/job.ts +151 -25
- package/src/tools/output-meta.ts +15 -2
- package/src/tools/path-utils.ts +27 -3
- package/src/tools/read.ts +3 -3
- package/src/tools/steer-fold.ts +107 -0
- package/src/tools/tool-catalog.generated.ts +29 -2
- package/src/tools/tool-errors.ts +4 -0
- package/src/utils/git.ts +53 -3
- package/src/utils/markit.ts +55 -41
- package/src/utils/mupdf-assets.d.ts +4 -0
- package/src/utils/mupdf-embedded.ts +4 -0
- package/src/utils/mupdf.ts +129 -0
- package/src/utils/session-title-generation.ts +29 -0
- package/src/utils/title-generator.ts +46 -2
- package/src/web/scrapers/types.ts +24 -3
- package/vendor/markit-ai/LICENSE +21 -0
- package/vendor/markit-ai/MANIFEST.json +412 -0
- package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/config.js +139 -0
- package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
- package/vendor/markit-ai/dist/commands/convert.js +113 -0
- package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/formats.js +92 -0
- package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/init.js +29 -0
- package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
- package/vendor/markit-ai/dist/commands/onboard.js +61 -0
- package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
- package/vendor/markit-ai/dist/commands/plugin.js +58 -0
- package/vendor/markit-ai/dist/config.d.ts +26 -0
- package/vendor/markit-ai/dist/config.js +42 -0
- package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/audio.js +104 -0
- package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
- package/vendor/markit-ai/dist/converters/csv.js +83 -0
- package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/docx.js +57 -0
- package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/epub.js +128 -0
- package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
- package/vendor/markit-ai/dist/converters/github.js +148 -0
- package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/html.js +30 -0
- package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/image.js +124 -0
- package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
- package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
- package/vendor/markit-ai/dist/converters/iwork.js +391 -0
- package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/json.js +21 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
- package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
- package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
- package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
- package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
- package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
- package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
- package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
- package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
- package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
- package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
- package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/pptx.js +289 -0
- package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
- package/vendor/markit-ai/dist/converters/rss.js +138 -0
- package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
- package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
- package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/xml.js +18 -0
- package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
- package/vendor/markit-ai/dist/converters/yaml.js +17 -0
- package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
- package/vendor/markit-ai/dist/converters/zip.js +57 -0
- package/vendor/markit-ai/dist/index.d.ts +30 -0
- package/vendor/markit-ai/dist/index.js +26 -0
- package/vendor/markit-ai/dist/main.d.ts +2 -0
- package/vendor/markit-ai/dist/main.js +191 -0
- package/vendor/markit-ai/dist/markit.d.ts +45 -0
- package/vendor/markit-ai/dist/markit.js +267 -0
- package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
- package/vendor/markit-ai/dist/plugins/api.js +42 -0
- package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
- package/vendor/markit-ai/dist/plugins/index.js +3 -0
- package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/installer.js +176 -0
- package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
- package/vendor/markit-ai/dist/plugins/loader.js +61 -0
- package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
- package/vendor/markit-ai/dist/plugins/types.js +1 -0
- package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
- package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
- package/vendor/markit-ai/dist/providers/index.js +56 -0
- package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
- package/vendor/markit-ai/dist/providers/openai.js +76 -0
- package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
- package/vendor/markit-ai/dist/providers/types.js +1 -0
- package/vendor/markit-ai/dist/types.d.ts +36 -0
- package/vendor/markit-ai/dist/types.js +1 -0
- package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
- package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
- package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
- package/vendor/markit-ai/dist/utils/output.js +31 -0
- package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
- package/vendor/markit-ai/dist/utils/turndown.js +70 -0
- package/vendor/markit-ai/package.json +74 -0
- package/vendor/markit-ai.patch +49 -0
|
@@ -3,7 +3,9 @@ import * as fsSync from "node:fs";
|
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { replaceTabs, truncateToWidth } from "@gajae-code/tui";
|
|
6
|
-
import { getAgentDir } from "@gajae-code/utils";
|
|
6
|
+
import { getAgentDir, logger, sanitizeDisplayLine } from "@gajae-code/utils";
|
|
7
|
+
import { PublicCommandFailure, type PublicEffectProof, type PublicFailureKind } from "../../cli/public-command-errors";
|
|
8
|
+
import type { EvidenceReference } from "../../cli/public-command-evidence";
|
|
7
9
|
import { repo as resolveGitRepository } from "../../utils/git";
|
|
8
10
|
import { ensureBroker } from "../broker/ensure";
|
|
9
11
|
import { resolveSessionLocator } from "../broker/session-index";
|
|
@@ -20,6 +22,7 @@ import { createBrokerSessionLifecycleService } from "../lifecycle/broker-client"
|
|
|
20
22
|
import type {
|
|
21
23
|
SessionLifecycleMutationRequest,
|
|
22
24
|
SessionLifecycleOperation,
|
|
25
|
+
SessionLifecycleResult,
|
|
23
26
|
SessionLifecycleSavedSession,
|
|
24
27
|
SessionLifecycleSavedSessionIdentity,
|
|
25
28
|
SessionLifecycleService,
|
|
@@ -58,12 +61,12 @@ export type SdkSessionCliAction =
|
|
|
58
61
|
| "send"
|
|
59
62
|
| "status"
|
|
60
63
|
| "tail"
|
|
64
|
+
| "close"
|
|
61
65
|
| "retire"
|
|
62
66
|
| "global"
|
|
63
67
|
| "raw"
|
|
64
68
|
| "control"
|
|
65
|
-
| "query"
|
|
66
|
-
| "global";
|
|
69
|
+
| "query";
|
|
67
70
|
export type SdkSessionListScope = "repo" | "cwd" | "worktree" | "all";
|
|
68
71
|
export type SdkSessionCliRawKind = "control" | "query" | "global";
|
|
69
72
|
|
|
@@ -86,6 +89,8 @@ export interface SdkSessionCliArgs {
|
|
|
86
89
|
strict?: boolean;
|
|
87
90
|
untilIdle?: boolean;
|
|
88
91
|
allEvents?: boolean;
|
|
92
|
+
/** tail --cursor: transcript row id the caller has already processed; rows up to and including it are omitted. */
|
|
93
|
+
afterTranscriptId?: string;
|
|
89
94
|
page?: boolean;
|
|
90
95
|
repo?: string;
|
|
91
96
|
scope?: string;
|
|
@@ -95,9 +100,14 @@ export interface SdkSessionCliArgs {
|
|
|
95
100
|
agentDir?: string;
|
|
96
101
|
}
|
|
97
102
|
|
|
103
|
+
export interface SdkSessionCliDependencies {
|
|
104
|
+
readonly lifecycleService?: SessionLifecycleService;
|
|
105
|
+
}
|
|
106
|
+
|
|
98
107
|
type JsonRecord = Record<string, unknown>;
|
|
99
108
|
type LifecycleMutationOperation = Exclude<SessionLifecycleOperation, "session.lookup" | "session.list">;
|
|
100
|
-
|
|
109
|
+
/** Why a tail call returned: the turn went idle, the session closed, or the caller's wait window closed (non-terminal). */
|
|
110
|
+
type TailExitReason = "idle" | "close" | "timeout";
|
|
101
111
|
export interface RetainedTranscriptTailReader {
|
|
102
112
|
readonly size: number;
|
|
103
113
|
readRange(start: number, end: number): Promise<Uint8Array>;
|
|
@@ -117,6 +127,7 @@ const transcriptDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
|
117
127
|
const SEARCH_PROBE_TIMEOUT_MS = 2_000;
|
|
118
128
|
const SEARCH_PROBE_MAX_ROWS = 100;
|
|
119
129
|
const SEARCH_TEXT_WIDTH = 80;
|
|
130
|
+
export const SDK_JSON_INPUT_FILE_MAX_BYTES = 4 * 1024 * 1024;
|
|
120
131
|
|
|
121
132
|
const TERMINAL_TURN_KINDS = new Set(["turn_end", "agent_end"]);
|
|
122
133
|
const START_TURN_KINDS = new Set(["turn_start", "agent_start"]);
|
|
@@ -144,9 +155,109 @@ class SdkSessionCliError extends Error {
|
|
|
144
155
|
}
|
|
145
156
|
}
|
|
146
157
|
|
|
158
|
+
/** Translate only SDK-owned codes and explicitly allowlisted reconciliation fields. */
|
|
159
|
+
export function sdkPublicFailure(code: string, details?: unknown, proof?: PublicEffectProof): PublicCommandFailure {
|
|
160
|
+
const kinds: Record<string, PublicFailureKind> = {
|
|
161
|
+
usage: "usage",
|
|
162
|
+
invalid_input: "usage",
|
|
163
|
+
invalid_json: "invalid_json",
|
|
164
|
+
broker_unavailable: "broker_unavailable",
|
|
165
|
+
session_unavailable: "endpoint_stale",
|
|
166
|
+
endpoint_stale: "endpoint_stale",
|
|
167
|
+
broker_restarting: "broker_restarting",
|
|
168
|
+
unavailable: "unavailable",
|
|
169
|
+
timeout: "timeout",
|
|
170
|
+
tail_timeout: "timeout",
|
|
171
|
+
wait_timeout: "wait_timeout",
|
|
172
|
+
uncertain_after_send: "uncertain_after_send",
|
|
173
|
+
authorization_denied: "authorization_denied",
|
|
174
|
+
unauthorized: "authorization_denied",
|
|
175
|
+
forbidden: "authorization_denied",
|
|
176
|
+
master_context_required: "authorization_denied",
|
|
177
|
+
adapter_operation_prohibited: "authorization_denied",
|
|
178
|
+
endpoint_credential_forbidden: "authorization_denied",
|
|
179
|
+
};
|
|
180
|
+
const references: EvidenceReference[] = [];
|
|
181
|
+
const source = object(details);
|
|
182
|
+
for (const [field, kind] of [
|
|
183
|
+
["sessionId", "sessionId"],
|
|
184
|
+
["operationRef", "operationRef"],
|
|
185
|
+
["clientRef", "operationRef"],
|
|
186
|
+
["idempotencyKey", "idempotencyKey"],
|
|
187
|
+
["claimId", "claimId"],
|
|
188
|
+
["commandId", "commandId"],
|
|
189
|
+
["turnId", "turnId"],
|
|
190
|
+
] as const) {
|
|
191
|
+
if (typeof source?.[field] === "string") references.push({ kind, value: source[field] as string });
|
|
192
|
+
}
|
|
193
|
+
return new PublicCommandFailure({
|
|
194
|
+
kind: Object.hasOwn(kinds, code) ? kinds[code]! : "operation_failed",
|
|
195
|
+
proof: code === "wait_timeout" ? "accepted" : proof,
|
|
196
|
+
references,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeSessionFailure(error: unknown, args: SdkSessionCliArgs): PublicCommandFailure {
|
|
201
|
+
if (error instanceof PublicCommandFailure)
|
|
202
|
+
return new PublicCommandFailure({
|
|
203
|
+
...error.input,
|
|
204
|
+
references: [
|
|
205
|
+
...(error.input.references ?? []),
|
|
206
|
+
...(sdkPublicFailure("operation_failed", {
|
|
207
|
+
sessionId: args.sessionId,
|
|
208
|
+
operationRef: args.opRef,
|
|
209
|
+
idempotencyKey: args.idempotencyKey,
|
|
210
|
+
}).input.references ?? []),
|
|
211
|
+
],
|
|
212
|
+
});
|
|
213
|
+
const context = { sessionId: args.sessionId, operationRef: args.opRef, idempotencyKey: args.idempotencyKey };
|
|
214
|
+
if (error instanceof SdkSessionCliError)
|
|
215
|
+
return sdkPublicFailure(
|
|
216
|
+
error.exitCode === 2 && error.code !== "invalid_json" ? "usage" : error.code,
|
|
217
|
+
{ ...context, ...object(error.details) },
|
|
218
|
+
error.exitCode === 2 ? "pre-effect" : undefined,
|
|
219
|
+
);
|
|
220
|
+
if (error instanceof SdkClientError)
|
|
221
|
+
return sdkPublicFailure(
|
|
222
|
+
error.code,
|
|
223
|
+
{ ...context, ...object(error.details) },
|
|
224
|
+
object(error.details)?.requestSent === false
|
|
225
|
+
? "pre-send"
|
|
226
|
+
: object(error.details)?.requestSent === true
|
|
227
|
+
? "sent"
|
|
228
|
+
: undefined,
|
|
229
|
+
);
|
|
230
|
+
if (error instanceof SessionRouterError)
|
|
231
|
+
return sdkPublicFailure(
|
|
232
|
+
error.phase === "pre_send" ? "endpoint_stale" : "uncertain_after_send",
|
|
233
|
+
context,
|
|
234
|
+
error.phase === "pre_send" ? "pre-send" : "sent",
|
|
235
|
+
);
|
|
236
|
+
return sdkPublicFailure("operation_failed", context);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Lifecycle terminality/retryability is not, by itself, proof of an effect outcome. */
|
|
240
|
+
export function lifecyclePublicFailure(outcome: Extract<SessionLifecycleResult, { ok: false }>): PublicCommandFailure {
|
|
241
|
+
const failure = sdkPublicFailure(outcome.error.code);
|
|
242
|
+
// The service emits retryable protocol_error only when requestSent is explicitly false.
|
|
243
|
+
const proof: PublicEffectProof =
|
|
244
|
+
outcome.certainty === "retryable" && outcome.error.code === "protocol_error" ? "pre-send" : "unknown";
|
|
245
|
+
return new PublicCommandFailure({
|
|
246
|
+
...failure.input,
|
|
247
|
+
// Generic lifecycle codes cannot override the authoritative, conservative proof.
|
|
248
|
+
kind:
|
|
249
|
+
failure.input.kind === "usage" || failure.input.kind === "invalid_json"
|
|
250
|
+
? "operation_failed"
|
|
251
|
+
: failure.input.kind === "wait_timeout"
|
|
252
|
+
? "timeout"
|
|
253
|
+
: failure.input.kind,
|
|
254
|
+
proof,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
147
258
|
class RetainedTranscriptTailError extends Error {
|
|
148
259
|
constructor(
|
|
149
|
-
readonly reason: "unavailable" | "corrupt" | "line_limit" | "scan_limit" | "changed",
|
|
260
|
+
readonly reason: "unavailable" | "corrupt" | "line_limit" | "scan_limit" | "changed" | "boundary_out_of_window",
|
|
150
261
|
message: string,
|
|
151
262
|
) {
|
|
152
263
|
super(message);
|
|
@@ -186,6 +297,128 @@ function parseInput(raw: string | undefined, source: string): JsonRecord {
|
|
|
186
297
|
}
|
|
187
298
|
}
|
|
188
299
|
|
|
300
|
+
function matchesSecurePathIdentity(before: fsSync.BigIntStats, after: fsSync.BigIntStats): boolean {
|
|
301
|
+
return (
|
|
302
|
+
before.dev === after.dev &&
|
|
303
|
+
before.ino === after.ino &&
|
|
304
|
+
before.nlink === after.nlink &&
|
|
305
|
+
before.mode === after.mode &&
|
|
306
|
+
before.size === after.size &&
|
|
307
|
+
before.mtimeNs === after.mtimeNs &&
|
|
308
|
+
before.ctimeNs === after.ctimeNs
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function matchesSecureInputIdentity(before: fsSync.BigIntStats, after: fsSync.BigIntStats): boolean {
|
|
313
|
+
return before.isFile() && after.isFile() && matchesSecurePathIdentity(before, after);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
type SecureInputPathSnapshot = {
|
|
317
|
+
resolvedPath: string;
|
|
318
|
+
ancestors: readonly { path: string; stat: fsSync.BigIntStats }[];
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
async function secureInputPath(filePath: string): Promise<SecureInputPathSnapshot> {
|
|
322
|
+
const resolved = path.resolve(filePath);
|
|
323
|
+
const canonical = await fs.realpath(resolved);
|
|
324
|
+
if (canonical !== resolved)
|
|
325
|
+
throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
|
|
326
|
+
const ancestorPaths: string[] = [];
|
|
327
|
+
for (let current = path.dirname(resolved); ; ) {
|
|
328
|
+
ancestorPaths.unshift(current);
|
|
329
|
+
const parent = path.dirname(current);
|
|
330
|
+
if (parent === current) break;
|
|
331
|
+
current = parent;
|
|
332
|
+
}
|
|
333
|
+
const ancestors = [] as { path: string; stat: fsSync.BigIntStats }[];
|
|
334
|
+
for (const ancestor of ancestorPaths) {
|
|
335
|
+
const stat = await fs.lstat(ancestor, { bigint: true });
|
|
336
|
+
if (!stat.isDirectory())
|
|
337
|
+
throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
|
|
338
|
+
ancestors.push({ path: ancestor, stat });
|
|
339
|
+
}
|
|
340
|
+
return { resolvedPath: resolved, ancestors };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function sameSecureInputAncestors(
|
|
344
|
+
before: readonly { path: string; stat: fsSync.BigIntStats }[],
|
|
345
|
+
after: readonly { path: string; stat: fsSync.BigIntStats }[],
|
|
346
|
+
): boolean {
|
|
347
|
+
return (
|
|
348
|
+
before.length === after.length &&
|
|
349
|
+
before.every(
|
|
350
|
+
(entry, index) =>
|
|
351
|
+
after[index]?.path === entry.path &&
|
|
352
|
+
after[index] !== undefined &&
|
|
353
|
+
matchesSecurePathIdentity(entry.stat, after[index]!.stat),
|
|
354
|
+
)
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Reads a 0600 JSON input through one descriptor, never through a replaceable pathname. */
|
|
359
|
+
export async function readSecureJsonInputFile(filePath: string): Promise<string> {
|
|
360
|
+
let descriptor: fs.FileHandle | undefined;
|
|
361
|
+
try {
|
|
362
|
+
const beforePath = await secureInputPath(filePath);
|
|
363
|
+
const resolvedPath = beforePath.resolvedPath;
|
|
364
|
+
const noFollow = process.platform === "win32" ? 0 : fsSync.constants.O_NOFOLLOW;
|
|
365
|
+
descriptor = await fs.open(resolvedPath, fsSync.constants.O_RDONLY | noFollow);
|
|
366
|
+
const before = await descriptor.stat({ bigint: true });
|
|
367
|
+
const uid = process.getuid?.();
|
|
368
|
+
if (
|
|
369
|
+
!before.isFile() ||
|
|
370
|
+
(before.mode & 0o7777n) !== 0o600n ||
|
|
371
|
+
(process.platform !== "win32" && uid !== undefined && before.uid !== BigInt(uid))
|
|
372
|
+
)
|
|
373
|
+
throw new SdkSessionCliError(
|
|
374
|
+
"input_file_permissions",
|
|
375
|
+
"--json-input-file must be a regular file with 0600 permissions.",
|
|
376
|
+
2,
|
|
377
|
+
);
|
|
378
|
+
if (before.size > BigInt(SDK_JSON_INPUT_FILE_MAX_BYTES))
|
|
379
|
+
throw new SdkSessionCliError(
|
|
380
|
+
"usage",
|
|
381
|
+
`--json-input-file must be at most ${SDK_JSON_INPUT_FILE_MAX_BYTES} bytes.`,
|
|
382
|
+
2,
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const pathIdentity = await fs.lstat(resolvedPath, { bigint: true });
|
|
386
|
+
const openedPath = await secureInputPath(resolvedPath);
|
|
387
|
+
if (
|
|
388
|
+
!matchesSecureInputIdentity(before, pathIdentity) ||
|
|
389
|
+
!sameSecureInputAncestors(beforePath.ancestors, openedPath.ancestors)
|
|
390
|
+
)
|
|
391
|
+
throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
|
|
392
|
+
|
|
393
|
+
const size = Number(before.size);
|
|
394
|
+
const bytes = Buffer.alloc(size);
|
|
395
|
+
let offset = 0;
|
|
396
|
+
while (offset < size) {
|
|
397
|
+
const read = await descriptor.read(bytes, offset, size - offset, offset);
|
|
398
|
+
if (read.bytesRead === 0) break;
|
|
399
|
+
offset += read.bytesRead;
|
|
400
|
+
}
|
|
401
|
+
if (offset !== size)
|
|
402
|
+
throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
|
|
403
|
+
|
|
404
|
+
const after = await descriptor.stat({ bigint: true });
|
|
405
|
+
const finalPath = await secureInputPath(resolvedPath);
|
|
406
|
+
const finalPathIdentity = await fs.lstat(resolvedPath, { bigint: true });
|
|
407
|
+
if (
|
|
408
|
+
!matchesSecureInputIdentity(before, after) ||
|
|
409
|
+
!matchesSecureInputIdentity(before, finalPathIdentity) ||
|
|
410
|
+
!sameSecureInputAncestors(beforePath.ancestors, finalPath.ancestors)
|
|
411
|
+
)
|
|
412
|
+
throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
|
|
413
|
+
return bytes.toString("utf8");
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (error instanceof SdkSessionCliError) throw error;
|
|
416
|
+
throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
|
|
417
|
+
} finally {
|
|
418
|
+
if (descriptor !== undefined) await descriptor.close().catch(() => {});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
189
422
|
function containsSecretField(value: unknown): boolean {
|
|
190
423
|
if (Array.isArray(value)) return value.some(containsSecretField);
|
|
191
424
|
if (!isRecord(value)) return false;
|
|
@@ -211,14 +444,7 @@ async function inputFromArgs(args: SdkSessionCliArgs): Promise<JsonRecord> {
|
|
|
211
444
|
}
|
|
212
445
|
if (args.jsonInputFile !== undefined) {
|
|
213
446
|
try {
|
|
214
|
-
|
|
215
|
-
if (!stat.isFile() || (stat.mode & 0o077) !== 0)
|
|
216
|
-
throw new SdkSessionCliError(
|
|
217
|
-
"input_file_permissions",
|
|
218
|
-
"--json-input-file must be a regular file with 0600 permissions.",
|
|
219
|
-
2,
|
|
220
|
-
);
|
|
221
|
-
return parseInput(await Bun.file(args.jsonInputFile).text(), "--json-input-file");
|
|
447
|
+
return parseInput(await readSecureJsonInputFile(args.jsonInputFile), "--json-input-file");
|
|
222
448
|
} catch (error) {
|
|
223
449
|
if (error instanceof SdkSessionCliError) throw error;
|
|
224
450
|
throw new SdkSessionCliError("input_file_unavailable", "Unable to read --json-input-file.", 2);
|
|
@@ -274,9 +500,8 @@ async function bounded<T>(promise: Promise<T>, timeoutMs: number, message: strin
|
|
|
274
500
|
}
|
|
275
501
|
}
|
|
276
502
|
|
|
277
|
-
function reportRouterCleanupFailure(
|
|
278
|
-
|
|
279
|
-
process.stderr.write(`SDK session Router cleanup failed: ${message}\n`);
|
|
503
|
+
function reportRouterCleanupFailure(): void {
|
|
504
|
+
logger.warn("SDK session Router cleanup failed");
|
|
280
505
|
}
|
|
281
506
|
|
|
282
507
|
/**
|
|
@@ -314,8 +539,14 @@ async function withRouter<T>(
|
|
|
314
539
|
}
|
|
315
540
|
try {
|
|
316
541
|
await bounded(router.stop(), ROUTER_STOP_TIMEOUT_MS, "SDK session Router shutdown timed out.");
|
|
317
|
-
} catch
|
|
318
|
-
|
|
542
|
+
} catch {
|
|
543
|
+
if (actionFailed) {
|
|
544
|
+
const failure = normalizeSessionFailure(actionError, {});
|
|
545
|
+
actionError = new PublicCommandFailure({
|
|
546
|
+
...failure.input,
|
|
547
|
+
diagnostics: [...(failure.input.diagnostics ?? []), "router_cleanup_failed"],
|
|
548
|
+
});
|
|
549
|
+
} else reportRouterCleanupFailure();
|
|
319
550
|
}
|
|
320
551
|
if (actionFailed) throw actionError;
|
|
321
552
|
return result;
|
|
@@ -336,12 +567,17 @@ function throwResponseFailure(response: unknown): void {
|
|
|
336
567
|
const record = object(response);
|
|
337
568
|
if (record?.ok !== false) return;
|
|
338
569
|
const failure = object(record.error);
|
|
339
|
-
|
|
340
|
-
typeof failure?.code === "string" ? failure.code : "
|
|
341
|
-
typeof failure?.message === "string" ? failure.message : "SDK request failed.",
|
|
342
|
-
1,
|
|
570
|
+
const failureInput = sdkPublicFailure(
|
|
571
|
+
typeof failure?.code === "string" ? failure.code : "operation_failed",
|
|
343
572
|
failure,
|
|
344
573
|
);
|
|
574
|
+
throw new PublicCommandFailure({
|
|
575
|
+
...failureInput.input,
|
|
576
|
+
references: [
|
|
577
|
+
...(failureInput.input.references ?? []),
|
|
578
|
+
...(sdkPublicFailure("operation_failed", record.result).input.references ?? []),
|
|
579
|
+
],
|
|
580
|
+
});
|
|
345
581
|
}
|
|
346
582
|
|
|
347
583
|
async function paginatedSessionList(
|
|
@@ -488,9 +724,10 @@ async function probeSearchRows(agentDir: string, result: SdkSearchResultV1): Pro
|
|
|
488
724
|
} catch {
|
|
489
725
|
return {
|
|
490
726
|
...result,
|
|
727
|
+
warnings: [...result.warnings, "probe_unavailable"],
|
|
491
728
|
rows: mergeProbedSearchRows(
|
|
492
729
|
result.rows,
|
|
493
|
-
rows.map(row => ({ ...row
|
|
730
|
+
rows.map(row => (row.live ? { ...row } : { ...row, probe: "stale" as const })),
|
|
494
731
|
),
|
|
495
732
|
};
|
|
496
733
|
}
|
|
@@ -501,54 +738,32 @@ export async function runSdkSearch(
|
|
|
501
738
|
args: Pick<SdkSessionCliArgs, "agentDir" | "repo" | "scope" | "limit" | "cursor">,
|
|
502
739
|
createService: (agentDir: string) => SessionLifecycleService = createBrokerSessionLifecycleService,
|
|
503
740
|
probe: (agentDir: string, result: SdkSearchResultV1) => Promise<SdkSearchResultV1> = probeSearchRows,
|
|
504
|
-
): Promise<{ result: SdkSearchResultV1; exitCode: 0
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
"usage",
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
741
|
+
): Promise<{ result: SdkSearchResultV1; exitCode: 0 }> {
|
|
742
|
+
try {
|
|
743
|
+
const agentDir = path.resolve(args.agentDir ?? getAgentDir());
|
|
744
|
+
const locator = await resolveSessionLocator(args.repo ?? process.cwd(), agentDir);
|
|
745
|
+
const scope: ScopeNameV1 =
|
|
746
|
+
args.scope === undefined || args.scope === "repo" || args.scope === "pwd" || args.scope === "global"
|
|
747
|
+
? ((args.scope ?? "repo") as ScopeNameV1)
|
|
748
|
+
: (() => {
|
|
749
|
+
throw sdkPublicFailure("usage", undefined, "pre-effect");
|
|
750
|
+
})();
|
|
751
|
+
const request = searchScopeRequest(scope, locator);
|
|
752
|
+
await resolveScopeRequest(request);
|
|
753
|
+
const outcome = await createService(agentDir).scopedList(request, args.limit, args.cursor);
|
|
754
|
+
if (outcome.ok) {
|
|
755
|
+
if ("rows" in outcome.result) {
|
|
756
|
+
if (outcome.result.status === "not-in-git-worktree") return { result: outcome.result, exitCode: 0 };
|
|
757
|
+
if (outcome.result.status === "unavailable")
|
|
758
|
+
throw sdkPublicFailure(outcome.result.error?.code ?? "unavailable");
|
|
759
|
+
return { result: await probe(agentDir, outcome.result), exitCode: 0 };
|
|
760
|
+
}
|
|
761
|
+
throw sdkPublicFailure("operation_failed");
|
|
525
762
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
scope: resolved,
|
|
530
|
-
status: "unavailable",
|
|
531
|
-
observedAt: new Date().toISOString(),
|
|
532
|
-
rows: [],
|
|
533
|
-
warnings: [],
|
|
534
|
-
error: { code: "malformed_response", message: "broker search returned an unscoped result" },
|
|
535
|
-
},
|
|
536
|
-
exitCode: 1,
|
|
537
|
-
};
|
|
763
|
+
throw lifecyclePublicFailure(outcome);
|
|
764
|
+
} catch (error) {
|
|
765
|
+
throw normalizeSessionFailure(error, args);
|
|
538
766
|
}
|
|
539
|
-
if (!outcome.ok && outcome.result?.status === "unavailable") return { result: outcome.result, exitCode: 1 };
|
|
540
|
-
return {
|
|
541
|
-
result: {
|
|
542
|
-
version: 1,
|
|
543
|
-
scope: resolved,
|
|
544
|
-
status: "unavailable",
|
|
545
|
-
observedAt: new Date().toISOString(),
|
|
546
|
-
rows: [],
|
|
547
|
-
warnings: [],
|
|
548
|
-
error: { code: outcome.error.code, message: outcome.error.message },
|
|
549
|
-
},
|
|
550
|
-
exitCode: 1,
|
|
551
|
-
};
|
|
552
767
|
}
|
|
553
768
|
|
|
554
769
|
function searchScopeLabel(result: SdkSearchResultV1): string {
|
|
@@ -560,7 +775,10 @@ function searchScopeLabel(result: SdkSearchResultV1): string {
|
|
|
560
775
|
}
|
|
561
776
|
|
|
562
777
|
function safeSearchText(value: string): string {
|
|
563
|
-
return truncateToWidth(
|
|
778
|
+
return truncateToWidth(
|
|
779
|
+
replaceTabs(sanitizeDisplayLine(value).replaceAll(/[\u2028\u2029]/gu, " ")),
|
|
780
|
+
SEARCH_TEXT_WIDTH,
|
|
781
|
+
);
|
|
564
782
|
}
|
|
565
783
|
|
|
566
784
|
/** Renders a credential-free scope/status preamble and bounded search table. */
|
|
@@ -568,15 +786,15 @@ export function renderSdkSearchTable(result: SdkSearchResultV1): string {
|
|
|
568
786
|
const lines = [
|
|
569
787
|
`Scope requested: ${result.scope.requested}`,
|
|
570
788
|
`Scope resolved: ${safeSearchText(searchScopeLabel(result))}`,
|
|
571
|
-
`Status: ${result.status}`,
|
|
572
|
-
`Observed at: ${result.observedAt}`,
|
|
789
|
+
`Status: ${safeSearchText(result.status)}`,
|
|
790
|
+
`Observed at: ${safeSearchText(result.observedAt)}`,
|
|
573
791
|
...(result.cursor === undefined ? [] : [`Continuation cursor: ${safeSearchText(result.cursor)}`]),
|
|
574
792
|
];
|
|
575
793
|
if (result.rows.length === 0) return lines.join("\n");
|
|
576
794
|
lines.push("ID PROBE LIVE CWD");
|
|
577
795
|
for (const row of result.rows)
|
|
578
796
|
lines.push(
|
|
579
|
-
`${safeSearchText(row.id).padEnd(20)} ${(row.probe ?? "-").padEnd(11)} ${String(row.live).padEnd(4)} ${safeSearchText(row.locator.cwd)}`,
|
|
797
|
+
`${safeSearchText(row.id).padEnd(20)} ${safeSearchText(row.probe ?? "-").padEnd(11)} ${String(row.live).padEnd(4)} ${safeSearchText(row.locator.cwd)}`,
|
|
580
798
|
);
|
|
581
799
|
return lines.join("\n");
|
|
582
800
|
}
|
|
@@ -844,22 +1062,35 @@ async function requestBrokerOperatorAbort(
|
|
|
844
1062
|
timeoutMs,
|
|
845
1063
|
reconnectAttempts: 0,
|
|
846
1064
|
});
|
|
1065
|
+
let actionFailed = false;
|
|
1066
|
+
let actionError: unknown;
|
|
1067
|
+
let result!: JsonRecord;
|
|
847
1068
|
try {
|
|
848
1069
|
const response = await client.global("session.control", request, {
|
|
849
1070
|
idempotencyKey,
|
|
850
1071
|
timeoutMs,
|
|
851
1072
|
});
|
|
852
|
-
const
|
|
853
|
-
if (!
|
|
854
|
-
throwResponseFailure(
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1073
|
+
const record = object(response);
|
|
1074
|
+
if (!record) throw new SdkClientError("protocol_error", "SDK broker returned a malformed control response.");
|
|
1075
|
+
throwResponseFailure(record);
|
|
1076
|
+
result = record;
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
actionFailed = true;
|
|
1079
|
+
actionError = error;
|
|
1080
|
+
}
|
|
1081
|
+
try {
|
|
1082
|
+
await client.close();
|
|
1083
|
+
} catch {
|
|
1084
|
+
if (actionFailed) {
|
|
1085
|
+
const failure = normalizeSessionFailure(actionError, args);
|
|
1086
|
+
actionError = new PublicCommandFailure({
|
|
1087
|
+
...failure.input,
|
|
1088
|
+
diagnostics: [...(failure.input.diagnostics ?? []), "broker_cleanup_failed"],
|
|
1089
|
+
});
|
|
1090
|
+
} else logger.warn("SDK broker client cleanup failed");
|
|
862
1091
|
}
|
|
1092
|
+
if (actionFailed) throw actionError;
|
|
1093
|
+
return result;
|
|
863
1094
|
}
|
|
864
1095
|
|
|
865
1096
|
async function requestQuery(
|
|
@@ -939,6 +1170,8 @@ export async function waitForTerminalStatus(
|
|
|
939
1170
|
* reports when a request that was already on the wire is abandoned.
|
|
940
1171
|
*/
|
|
941
1172
|
function isWaitWindowFailure(error: unknown): boolean {
|
|
1173
|
+
if (error instanceof PublicCommandFailure)
|
|
1174
|
+
return error.input.kind === "timeout" || error.input.kind === "uncertain_after_send";
|
|
942
1175
|
if (error instanceof SdkClientError) return error.code === "timeout" || error.code === "uncertain_after_send";
|
|
943
1176
|
return error instanceof SdkSessionCliError && error.code === "timeout";
|
|
944
1177
|
}
|
|
@@ -958,30 +1191,49 @@ async function runSend(agentDir: string, sessionId: string, args: SdkSessionCliA
|
|
|
958
1191
|
if (inputRef === undefined) promptInput.clientRef = clientRef;
|
|
959
1192
|
const invalid = validateAdapterControl("turn.prompt", promptInput);
|
|
960
1193
|
if (invalid) throw new SdkSessionCliError(invalid.code, invalid.message, 2);
|
|
961
|
-
|
|
1194
|
+
let accepted = false;
|
|
1195
|
+
try {
|
|
1196
|
+
await ensureBroker({ agentDir });
|
|
962
1197
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1198
|
+
return await withRouter(agentDir, [sessionId], async router => {
|
|
1199
|
+
const response = await requestControl(router, sessionId, "turn.prompt", promptInput, args);
|
|
1200
|
+
accepted = true;
|
|
1201
|
+
const result: JsonRecord = {
|
|
1202
|
+
version: SESSION_ROWS_VERSION,
|
|
1203
|
+
operationRef: clientRef,
|
|
1204
|
+
status: "accepted",
|
|
1205
|
+
receipt: resultObject(response) ?? response,
|
|
1206
|
+
};
|
|
1207
|
+
if (args.wait === true) {
|
|
1208
|
+
const outcome = await waitForTerminalStatus(router, sessionId, clientRef, args.timeoutMs ?? 30_000);
|
|
1209
|
+
if (!outcome.terminal)
|
|
1210
|
+
throw new SdkSessionCliError(
|
|
1211
|
+
"wait_timeout",
|
|
1212
|
+
`Prompt ${clientRef} did not reach a terminal state within the wait window.`,
|
|
1213
|
+
1,
|
|
1214
|
+
{ operationRef: clientRef, status: outcome.status },
|
|
1215
|
+
);
|
|
1216
|
+
result.status = outcome.status;
|
|
1217
|
+
result.statusDetail = outcome.detail;
|
|
1218
|
+
}
|
|
1219
|
+
return { ok: true, result };
|
|
1220
|
+
});
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
const failure = normalizeSessionFailure(error, { ...args, sessionId, opRef: clientRef });
|
|
1223
|
+
throw new PublicCommandFailure({
|
|
1224
|
+
...failure.input,
|
|
1225
|
+
...(accepted
|
|
1226
|
+
? {
|
|
1227
|
+
proof: "accepted" as const,
|
|
1228
|
+
kind:
|
|
1229
|
+
failure.input.kind === "uncertain_after_send" || failure.input.kind === "timeout"
|
|
1230
|
+
? ("wait_timeout" as const)
|
|
1231
|
+
: failure.input.kind,
|
|
1232
|
+
}
|
|
1233
|
+
: {}),
|
|
1234
|
+
references: failure.input.references,
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
985
1237
|
}
|
|
986
1238
|
|
|
987
1239
|
async function runStatus(
|
|
@@ -1266,14 +1518,23 @@ function retainedTranscriptCorrupt(): RetainedTranscriptTailError {
|
|
|
1266
1518
|
);
|
|
1267
1519
|
}
|
|
1268
1520
|
|
|
1269
|
-
export async function scanRetainedTranscriptTail(
|
|
1521
|
+
export async function scanRetainedTranscriptTail(
|
|
1522
|
+
reader: RetainedTranscriptTailReader,
|
|
1523
|
+
options: { boundaryId?: string } = {},
|
|
1524
|
+
): Promise<unknown[]> {
|
|
1270
1525
|
if (!Number.isSafeInteger(reader.size) || reader.size < 0) throw retainedTranscriptUnavailable();
|
|
1271
1526
|
const entries: unknown[] = [];
|
|
1527
|
+
let rowsAfterBoundary = 0;
|
|
1528
|
+
let boundaryFound = false;
|
|
1272
1529
|
let position = reader.size;
|
|
1273
1530
|
let scannedBytes = 0;
|
|
1274
1531
|
let scannedLines = 0;
|
|
1275
1532
|
let trailingFragment = new Uint8Array();
|
|
1276
|
-
while (
|
|
1533
|
+
while (
|
|
1534
|
+
position > 0 &&
|
|
1535
|
+
(entries.length < TAIL_OFFLINE_MAX_ENTRIES || (options.boundaryId !== undefined && !boundaryFound)) &&
|
|
1536
|
+
(options.boundaryId === undefined || !boundaryFound)
|
|
1537
|
+
) {
|
|
1277
1538
|
const remainingBytes = TAIL_OFFLINE_MAX_SCAN_BYTES - scannedBytes;
|
|
1278
1539
|
if (remainingBytes <= 0)
|
|
1279
1540
|
throw new RetainedTranscriptTailError(
|
|
@@ -1313,7 +1574,11 @@ export async function scanRetainedTranscriptTail(reader: RetainedTranscriptTailR
|
|
|
1313
1574
|
}
|
|
1314
1575
|
|
|
1315
1576
|
let lineEnd = complete.byteLength;
|
|
1316
|
-
while (
|
|
1577
|
+
while (
|
|
1578
|
+
lineEnd > 0 &&
|
|
1579
|
+
(entries.length < TAIL_OFFLINE_MAX_ENTRIES || (options.boundaryId !== undefined && !boundaryFound)) &&
|
|
1580
|
+
(options.boundaryId === undefined || !boundaryFound)
|
|
1581
|
+
) {
|
|
1317
1582
|
const newline = complete.lastIndexOf(0x0a, lineEnd - 1);
|
|
1318
1583
|
const lineStart = newline < 0 ? 0 : newline + 1;
|
|
1319
1584
|
const line = complete.subarray(lineStart, lineEnd);
|
|
@@ -1331,12 +1596,24 @@ export async function scanRetainedTranscriptTail(reader: RetainedTranscriptTailR
|
|
|
1331
1596
|
"Retained transcript history exceeds the bounded tail replay limit.",
|
|
1332
1597
|
);
|
|
1333
1598
|
try {
|
|
1334
|
-
|
|
1335
|
-
|
|
1599
|
+
const entry: unknown = JSON.parse(transcriptDecoder.decode(line));
|
|
1600
|
+
if (options.boundaryId !== undefined && isRecord(entry) && entry.id === options.boundaryId) {
|
|
1601
|
+
if (rowsAfterBoundary > TAIL_OFFLINE_MAX_ENTRIES)
|
|
1602
|
+
throw new RetainedTranscriptTailError(
|
|
1603
|
+
"boundary_out_of_window",
|
|
1604
|
+
"The requested transcript boundary is older than the bounded offline tail window.",
|
|
1605
|
+
);
|
|
1606
|
+
boundaryFound = true;
|
|
1607
|
+
} else if (options.boundaryId !== undefined && !boundaryFound) {
|
|
1608
|
+
rowsAfterBoundary++;
|
|
1609
|
+
}
|
|
1610
|
+
if (entries.length < TAIL_OFFLINE_MAX_ENTRIES) entries.push(entry);
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
if (error instanceof RetainedTranscriptTailError) throw error;
|
|
1336
1613
|
throw retainedTranscriptCorrupt();
|
|
1337
1614
|
}
|
|
1338
1615
|
}
|
|
1339
|
-
if (entries.length === TAIL_OFFLINE_MAX_ENTRIES) break;
|
|
1616
|
+
if (entries.length === TAIL_OFFLINE_MAX_ENTRIES && (options.boundaryId === undefined || boundaryFound)) break;
|
|
1340
1617
|
if (start > 0) {
|
|
1341
1618
|
if (partial.byteLength > TAIL_OFFLINE_MAX_LINE_BYTES)
|
|
1342
1619
|
throw new RetainedTranscriptTailError(
|
|
@@ -1350,7 +1627,10 @@ export async function scanRetainedTranscriptTail(reader: RetainedTranscriptTailR
|
|
|
1350
1627
|
return entries.reverse();
|
|
1351
1628
|
}
|
|
1352
1629
|
|
|
1353
|
-
async function readRetainedTranscriptTail(
|
|
1630
|
+
async function readRetainedTranscriptTail(
|
|
1631
|
+
savedSession: SessionLifecycleSavedSession,
|
|
1632
|
+
afterTranscriptId?: string,
|
|
1633
|
+
): Promise<unknown[]> {
|
|
1354
1634
|
let descriptor: fs.FileHandle | undefined;
|
|
1355
1635
|
try {
|
|
1356
1636
|
descriptor = await fs.open(savedSession.path, retainedTranscriptOpenFlags());
|
|
@@ -1360,10 +1640,13 @@ async function readRetainedTranscriptTail(savedSession: SessionLifecycleSavedSes
|
|
|
1360
1640
|
// Bind every range read to the opened descriptor so a later pathname replacement
|
|
1361
1641
|
// cannot change the retained history selected by the lifecycle lookup.
|
|
1362
1642
|
const file = Bun.file(descriptor.fd);
|
|
1363
|
-
const entries = await scanRetainedTranscriptTail(
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1643
|
+
const entries = await scanRetainedTranscriptTail(
|
|
1644
|
+
{
|
|
1645
|
+
size: Number(before.size),
|
|
1646
|
+
readRange: async (start, end) => new Uint8Array(await file.slice(start, end).arrayBuffer()),
|
|
1647
|
+
},
|
|
1648
|
+
{ boundaryId: afterTranscriptId },
|
|
1649
|
+
);
|
|
1367
1650
|
const after = await descriptor.stat({ bigint: true });
|
|
1368
1651
|
if (
|
|
1369
1652
|
!matchesRetainedTranscriptIdentity(savedSession.identity, after) ||
|
|
@@ -1391,6 +1674,7 @@ async function offlineTailReplay(
|
|
|
1391
1674
|
agentDir: string,
|
|
1392
1675
|
sessionId: string,
|
|
1393
1676
|
row: SdkSessionRowV1,
|
|
1677
|
+
afterTranscriptId?: string,
|
|
1394
1678
|
): Promise<unknown> {
|
|
1395
1679
|
const lifecycle = createBrokerSessionLifecycleService(agentDir);
|
|
1396
1680
|
const outcome = await lifecycle.list({
|
|
@@ -1398,8 +1682,7 @@ async function offlineTailReplay(
|
|
|
1398
1682
|
capability: "session.list",
|
|
1399
1683
|
target: { cwd: repo, resolveSessionId: sessionId },
|
|
1400
1684
|
});
|
|
1401
|
-
if (!outcome.ok)
|
|
1402
|
-
throw new SdkSessionCliError(outcome.error.code, outcome.error.message, 1, { certainty: outcome.certainty });
|
|
1685
|
+
if (!outcome.ok) throw lifecyclePublicFailure(outcome);
|
|
1403
1686
|
if ("rows" in outcome.result)
|
|
1404
1687
|
throw new SdkSessionCliError(
|
|
1405
1688
|
"malformed_response",
|
|
@@ -1415,7 +1698,7 @@ async function offlineTailReplay(
|
|
|
1415
1698
|
);
|
|
1416
1699
|
let entries: unknown[];
|
|
1417
1700
|
try {
|
|
1418
|
-
entries = await readRetainedTranscriptTail(savedSession);
|
|
1701
|
+
entries = await readRetainedTranscriptTail(savedSession, afterTranscriptId);
|
|
1419
1702
|
} catch (error) {
|
|
1420
1703
|
const retained = error instanceof RetainedTranscriptTailError ? error : retainedTranscriptUnavailable();
|
|
1421
1704
|
throw new SdkSessionCliError("retention_gap", retained.message, 1, {
|
|
@@ -1423,15 +1706,21 @@ async function offlineTailReplay(
|
|
|
1423
1706
|
reason: retained.reason,
|
|
1424
1707
|
});
|
|
1425
1708
|
}
|
|
1709
|
+
const items = entries.map((entry, index) =>
|
|
1710
|
+
toTailItemV1(entry, { kind: "transcript", revision: index + 1, seq: index }),
|
|
1711
|
+
);
|
|
1712
|
+
// The same contract as a live resume: rows up to and including the id the
|
|
1713
|
+
// caller already has are omitted, so a session that stopped between two
|
|
1714
|
+
// polls does not replay processed history on the next one. Unknown
|
|
1715
|
+
// boundary keeps everything (a duplicate is recoverable; a missing row is not).
|
|
1716
|
+
const boundary = afterTranscriptId === undefined ? -1 : items.findIndex(item => item.id === afterTranscriptId);
|
|
1426
1717
|
return {
|
|
1427
1718
|
ok: true,
|
|
1428
1719
|
result: {
|
|
1429
1720
|
version: SESSION_ROWS_VERSION,
|
|
1430
1721
|
source: "offline",
|
|
1431
1722
|
session: row,
|
|
1432
|
-
items:
|
|
1433
|
-
toTailItemV1(entry, { kind: "transcript", revision: index + 1, seq: index }),
|
|
1434
|
-
),
|
|
1723
|
+
items: boundary >= 0 ? items.slice(boundary + 1) : items,
|
|
1435
1724
|
terminal: true,
|
|
1436
1725
|
},
|
|
1437
1726
|
};
|
|
@@ -1460,6 +1749,8 @@ async function runLiveTail(
|
|
|
1460
1749
|
const seenEvents = new Set<string>();
|
|
1461
1750
|
const liveRevisionBuffer = new TailRevisionBuffer();
|
|
1462
1751
|
let checkpoint: SdkCheckpointRecordV1 | undefined;
|
|
1752
|
+
let transcriptCheckpoint: SdkCheckpointRecordV1 | undefined;
|
|
1753
|
+
let eventReplayCheckpoint: SdkCheckpointRecordV1 | undefined;
|
|
1463
1754
|
let gap: SdkRetentionGapV1 | undefined;
|
|
1464
1755
|
let liveReason: TailExitReason | undefined;
|
|
1465
1756
|
let resolveLive: ((reason: TailExitReason) => void) | undefined;
|
|
@@ -1473,6 +1764,7 @@ async function runLiveTail(
|
|
|
1473
1764
|
let turnIdle = false;
|
|
1474
1765
|
let turnStateKey: TailSeqKey | undefined;
|
|
1475
1766
|
let closed = false;
|
|
1767
|
+
let acceptingLiveFrames = true;
|
|
1476
1768
|
// Lifecycle kind already claimed at each canonical position. A same-kind
|
|
1477
1769
|
// duplicate never reaches here (dedupe keys include kind), so any entry that
|
|
1478
1770
|
// disagrees is a host stating two different lifecycle kinds at one position.
|
|
@@ -1537,6 +1829,7 @@ async function runLiveTail(
|
|
|
1537
1829
|
};
|
|
1538
1830
|
|
|
1539
1831
|
const recordLiveFrame = (attachment: SessionAttachment, frame: SessionRouterFrame): void => {
|
|
1832
|
+
if (!acceptingLiveFrames) return;
|
|
1540
1833
|
if (attachment.sessionId !== sessionId) return;
|
|
1541
1834
|
const item = tailItemFromRouterFrame(frame);
|
|
1542
1835
|
if (!item) return;
|
|
@@ -1572,6 +1865,8 @@ async function runLiveTail(
|
|
|
1572
1865
|
throwResponseFailure(checkpointResponse);
|
|
1573
1866
|
const extraction = extractCheckpoint(checkpointResponse);
|
|
1574
1867
|
checkpoint = extraction.record;
|
|
1868
|
+
transcriptCheckpoint = checkpoint;
|
|
1869
|
+
eventReplayCheckpoint = checkpoint;
|
|
1575
1870
|
gap = extraction.gap;
|
|
1576
1871
|
if (checkpoint !== undefined)
|
|
1577
1872
|
applyLifecycle(
|
|
@@ -1585,7 +1880,87 @@ async function runLiveTail(
|
|
|
1585
1880
|
gap,
|
|
1586
1881
|
);
|
|
1587
1882
|
|
|
1883
|
+
// Which snapshot to page. A fresh tail pages the checkpoint it was just
|
|
1884
|
+
// handed. A resumed tail (`--cursor`) must NOT page the exchanged cursor:
|
|
1885
|
+
// that one is pinned to the OLD snapshot at offset 0, so it would replay
|
|
1886
|
+
// every row the caller already processed. It pages a fresh snapshot
|
|
1887
|
+
// instead and drops rows up to the caller's `--after-transcript-id`, so
|
|
1888
|
+
// the result is exactly the transcript delta since the last tail - the
|
|
1889
|
+
// rows that carry tool calls and interim assistant text.
|
|
1588
1890
|
let cursor = extraction.cursor;
|
|
1891
|
+
if (args.cursor !== undefined) {
|
|
1892
|
+
if (cursor === undefined)
|
|
1893
|
+
throw new SdkSessionCliError(
|
|
1894
|
+
"unavailable",
|
|
1895
|
+
"The host did not return the exchanged checkpoint needed to resume this tail.",
|
|
1896
|
+
1,
|
|
1897
|
+
);
|
|
1898
|
+
const exchangedCursor = cursor;
|
|
1899
|
+
let freshPagingCursor: string | undefined;
|
|
1900
|
+
let freshPagingCheckpoint: SdkCheckpointRecordV1 | undefined;
|
|
1901
|
+
try {
|
|
1902
|
+
const fresh = await router.request(
|
|
1903
|
+
sessionId,
|
|
1904
|
+
{ type: "query_request", query: "session.checkpoint", input: {} },
|
|
1905
|
+
attachment.generation,
|
|
1906
|
+
attachment,
|
|
1907
|
+
args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
|
|
1908
|
+
);
|
|
1909
|
+
throwResponseFailure(fresh);
|
|
1910
|
+
const freshExtraction = extractCheckpoint(fresh);
|
|
1911
|
+
freshPagingCursor = freshExtraction.cursor;
|
|
1912
|
+
freshPagingCheckpoint = freshExtraction.record;
|
|
1913
|
+
if (freshPagingCursor === undefined || freshPagingCheckpoint === undefined)
|
|
1914
|
+
throw new Error("The host returned an incomplete transcript checkpoint.");
|
|
1915
|
+
} catch {
|
|
1916
|
+
// Keep the exchanged cursor as a complete fallback. Returning only live
|
|
1917
|
+
// frames after a failed fresh checkpoint would silently discard the
|
|
1918
|
+
// transcript delta the caller is resuming for.
|
|
1919
|
+
freshPagingCursor = undefined;
|
|
1920
|
+
freshPagingCheckpoint = undefined;
|
|
1921
|
+
}
|
|
1922
|
+
if (freshPagingCursor !== undefined && freshPagingCheckpoint !== undefined) {
|
|
1923
|
+
const pagingCursor = freshPagingCursor;
|
|
1924
|
+
const pagingCheckpoint = freshPagingCheckpoint;
|
|
1925
|
+
// The exchanged cursor is pinned to the old snapshot. Release it only
|
|
1926
|
+
// after the fresh snapshot has been acquired so a failed mint can still
|
|
1927
|
+
// page the exchanged snapshot instead of returning partial success.
|
|
1928
|
+
let drain: string | undefined = exchangedCursor;
|
|
1929
|
+
while (drain !== undefined) {
|
|
1930
|
+
try {
|
|
1931
|
+
const page = extractTranscriptPage(
|
|
1932
|
+
await router.request(
|
|
1933
|
+
sessionId,
|
|
1934
|
+
{ type: "query_request", query: "transcript.list", input: {}, cursor: drain },
|
|
1935
|
+
attachment.generation,
|
|
1936
|
+
attachment,
|
|
1937
|
+
args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
|
|
1938
|
+
),
|
|
1939
|
+
);
|
|
1940
|
+
drain = page.complete ? undefined : page.cursor;
|
|
1941
|
+
} catch {
|
|
1942
|
+
// Releasing is best-effort; a pin that cannot be drained expires with its TTL.
|
|
1943
|
+
drain = undefined;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
cursor = pagingCursor;
|
|
1947
|
+
transcriptCheckpoint = pagingCheckpoint;
|
|
1948
|
+
eventReplayCheckpoint = pagingCheckpoint;
|
|
1949
|
+
applyLifecycle(
|
|
1950
|
+
mergeEventTailItems(
|
|
1951
|
+
eventItems,
|
|
1952
|
+
seenEvents,
|
|
1953
|
+
liveRevisionBuffer.resolve(pagingCheckpoint.revision),
|
|
1954
|
+
include,
|
|
1955
|
+
),
|
|
1956
|
+
);
|
|
1957
|
+
} else {
|
|
1958
|
+
cursor = exchangedCursor;
|
|
1959
|
+
transcriptCheckpoint = checkpoint;
|
|
1960
|
+
eventReplayCheckpoint = checkpoint;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
const transcriptStart = transcriptItems.length;
|
|
1589
1964
|
while (cursor !== undefined) {
|
|
1590
1965
|
const response = await router.request(
|
|
1591
1966
|
sessionId,
|
|
@@ -1596,7 +1971,7 @@ async function runLiveTail(
|
|
|
1596
1971
|
);
|
|
1597
1972
|
throwResponseFailure(response);
|
|
1598
1973
|
const page = extractTranscriptPage(response);
|
|
1599
|
-
const rev =
|
|
1974
|
+
const rev = transcriptCheckpoint?.revision;
|
|
1600
1975
|
let nextSeq = transcriptItems.length;
|
|
1601
1976
|
mergeTailItems(
|
|
1602
1977
|
transcriptItems,
|
|
@@ -1604,7 +1979,7 @@ async function runLiveTail(
|
|
|
1604
1979
|
page.items.map(item => {
|
|
1605
1980
|
const it = toTailItemV1(item, { kind: "transcript" });
|
|
1606
1981
|
if (it.revision === undefined && rev !== undefined) it.revision = rev;
|
|
1607
|
-
if (it.generation === undefined) it.generation =
|
|
1982
|
+
if (it.generation === undefined) it.generation = transcriptCheckpoint?.generation ?? 0;
|
|
1608
1983
|
if (it.seq === undefined) it.seq = nextSeq++;
|
|
1609
1984
|
return it;
|
|
1610
1985
|
}),
|
|
@@ -1613,14 +1988,23 @@ async function runLiveTail(
|
|
|
1613
1988
|
if (page.complete || page.cursor === undefined) break;
|
|
1614
1989
|
cursor = page.cursor;
|
|
1615
1990
|
}
|
|
1991
|
+
if (args.cursor !== undefined && args.afterTranscriptId !== undefined) {
|
|
1992
|
+
// Drop the rows the caller already has. Unknown boundary (row rotated
|
|
1993
|
+
// out, or a different session) → keep everything: a duplicate is
|
|
1994
|
+
// recoverable downstream, a silently missing row is not.
|
|
1995
|
+
const boundary = transcriptItems.findIndex(
|
|
1996
|
+
(item, index) => index >= transcriptStart && item.id === args.afterTranscriptId,
|
|
1997
|
+
);
|
|
1998
|
+
if (boundary >= 0) transcriptItems.splice(transcriptStart, boundary + 1 - transcriptStart);
|
|
1999
|
+
}
|
|
1616
2000
|
|
|
1617
2001
|
const replayResponse = await router.request(
|
|
1618
2002
|
sessionId,
|
|
1619
2003
|
{
|
|
1620
2004
|
type: "event_replay",
|
|
1621
|
-
...(
|
|
2005
|
+
...(eventReplayCheckpoint === undefined
|
|
1622
2006
|
? {}
|
|
1623
|
-
: { sinceGeneration:
|
|
2007
|
+
: { sinceGeneration: eventReplayCheckpoint.generation, sinceSeq: eventReplayCheckpoint.seq }),
|
|
1624
2008
|
},
|
|
1625
2009
|
attachment.generation,
|
|
1626
2010
|
attachment,
|
|
@@ -1628,7 +2012,7 @@ async function runLiveTail(
|
|
|
1628
2012
|
);
|
|
1629
2013
|
throwResponseFailure(replayResponse);
|
|
1630
2014
|
const replay = object(replayResponse) ?? {};
|
|
1631
|
-
const replayGap = eventGapToRetentionGap(replay.gap, replay,
|
|
2015
|
+
const replayGap = eventGapToRetentionGap(replay.gap, replay, eventReplayCheckpoint);
|
|
1632
2016
|
if (replayGap !== undefined) {
|
|
1633
2017
|
gap = replayGap;
|
|
1634
2018
|
if (args.strict === true)
|
|
@@ -1655,20 +2039,21 @@ async function runLiveTail(
|
|
|
1655
2039
|
}
|
|
1656
2040
|
const replayItems = rawEvents.map(event => {
|
|
1657
2041
|
const it = toTailItemV1(event, { kind: "event" });
|
|
1658
|
-
if (it.revision === undefined &&
|
|
2042
|
+
if (it.revision === undefined && eventReplayCheckpoint?.revision !== undefined)
|
|
2043
|
+
it.revision = eventReplayCheckpoint.revision;
|
|
1659
2044
|
return it;
|
|
1660
2045
|
});
|
|
1661
2046
|
applyLifecycle(mergeEventTailItems(eventItems, seenEvents, replayItems, include));
|
|
1662
2047
|
if (malformed !== undefined) throw malformed;
|
|
1663
|
-
if (
|
|
2048
|
+
if (eventReplayCheckpoint !== undefined) {
|
|
1664
2049
|
const checkpointKey = {
|
|
1665
|
-
revision:
|
|
1666
|
-
generation:
|
|
1667
|
-
seq:
|
|
2050
|
+
revision: eventReplayCheckpoint.revision,
|
|
2051
|
+
generation: eventReplayCheckpoint.generation,
|
|
2052
|
+
seq: eventReplayCheckpoint.seq,
|
|
1668
2053
|
};
|
|
1669
2054
|
if (turnStateKey === undefined || compareTailSeqKeys(checkpointKey, turnStateKey) >= 0) {
|
|
1670
2055
|
turnStateKey = checkpointKey;
|
|
1671
|
-
turnIdle =
|
|
2056
|
+
turnIdle = eventReplayCheckpoint.idle;
|
|
1672
2057
|
}
|
|
1673
2058
|
}
|
|
1674
2059
|
if (liveReason === undefined && args.untilIdle === true && turnIdle) liveReason = "idle";
|
|
@@ -1677,18 +2062,14 @@ async function runLiveTail(
|
|
|
1677
2062
|
resolveLive = completion.resolve;
|
|
1678
2063
|
rejectLive = completion.reject;
|
|
1679
2064
|
const timeoutMs = args.timeoutMs ?? 10_000;
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
),
|
|
1689
|
-
),
|
|
1690
|
-
timeoutMs,
|
|
1691
|
-
);
|
|
2065
|
+
// The wait window closing is not a failure: it is the caller's bound on
|
|
2066
|
+
// one tail call, and everything collected inside it (transcript rows,
|
|
2067
|
+
// replayed and live events) is a valid, non-terminal observation.
|
|
2068
|
+
// Throwing here discarded that observation, so a poller watching a
|
|
2069
|
+
// running turn with `--until-idle` saw nothing until the turn ended -
|
|
2070
|
+
// every mid-turn tool call and interim line was lost, and the poll
|
|
2071
|
+
// itself blocked for the whole window. `terminal: false` says the rest.
|
|
2072
|
+
const timer = setTimeout(() => completion.resolve("timeout"), timeoutMs);
|
|
1692
2073
|
try {
|
|
1693
2074
|
liveReason = await completion.promise;
|
|
1694
2075
|
} finally {
|
|
@@ -1697,6 +2078,48 @@ async function runLiveTail(
|
|
|
1697
2078
|
rejectLive = undefined;
|
|
1698
2079
|
}
|
|
1699
2080
|
}
|
|
2081
|
+
// Freeze collection before minting the caller's continuation. A host can
|
|
2082
|
+
// capture the checkpoint watermark and publish a frame before the response
|
|
2083
|
+
// reaches us; that frame belongs to the next poll, not behind this cursor.
|
|
2084
|
+
acceptingLiveFrames = false;
|
|
2085
|
+
// Mint the caller's continuation only after replay and live follow have
|
|
2086
|
+
// reached their exit condition. A checkpoint minted earlier can point
|
|
2087
|
+
// behind events already included in this response, causing the next poll
|
|
2088
|
+
// to replay observations the caller already processed.
|
|
2089
|
+
let resumeCursor: string | undefined;
|
|
2090
|
+
try {
|
|
2091
|
+
const fresh = await router.request(
|
|
2092
|
+
sessionId,
|
|
2093
|
+
{ type: "query_request", query: "session.checkpoint", input: {} },
|
|
2094
|
+
attachment.generation,
|
|
2095
|
+
attachment,
|
|
2096
|
+
args.timeoutMs === undefined ? undefined : { timeoutMs: args.timeoutMs },
|
|
2097
|
+
);
|
|
2098
|
+
throwResponseFailure(fresh);
|
|
2099
|
+
resumeCursor = extractCheckpoint(fresh).cursor;
|
|
2100
|
+
} catch (error) {
|
|
2101
|
+
// A closed session has no next poll that could resume, so preserve the
|
|
2102
|
+
// complete terminal observation even when the host tears down the
|
|
2103
|
+
// connection before it can mint a final cursor. Active and timed-out
|
|
2104
|
+
// tails still fail closed: returning them without a continuation would
|
|
2105
|
+
// let the caller advance past observations it cannot resume.
|
|
2106
|
+
if (liveReason !== "close") {
|
|
2107
|
+
if (error instanceof SdkSessionCliError) throw error;
|
|
2108
|
+
throw new SdkSessionCliError(
|
|
2109
|
+
"unavailable",
|
|
2110
|
+
"The host did not return a continuation checkpoint for this tail.",
|
|
2111
|
+
1,
|
|
2112
|
+
{ sessionId },
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
if (resumeCursor === undefined && liveReason !== "close")
|
|
2117
|
+
throw new SdkSessionCliError(
|
|
2118
|
+
"unavailable",
|
|
2119
|
+
"The host did not return a continuation cursor for this tail.",
|
|
2120
|
+
1,
|
|
2121
|
+
{ sessionId },
|
|
2122
|
+
);
|
|
1700
2123
|
return {
|
|
1701
2124
|
ok: true,
|
|
1702
2125
|
result: {
|
|
@@ -1704,6 +2127,13 @@ async function runLiveTail(
|
|
|
1704
2127
|
source: "session",
|
|
1705
2128
|
session: row,
|
|
1706
2129
|
...(checkpoint === undefined ? {} : { checkpoint }),
|
|
2130
|
+
// A signed, UNCONSUMED checkpoint cursor to resume from. Carried as
|
|
2131
|
+
// `cursor`, not `checkpointToken`: output passes through
|
|
2132
|
+
// stripSecretFields, whose /token/i matches the latter by name, so a
|
|
2133
|
+
// caller never saw it and no tail could ever be resumed (every poll
|
|
2134
|
+
// replayed the whole session). It is an opaque per-grant cursor, not a
|
|
2135
|
+
// credential - the same shape `list`/`transcript` already return.
|
|
2136
|
+
...(resumeCursor === undefined ? {} : { cursor: resumeCursor }),
|
|
1707
2137
|
...(gap === undefined ? {} : { gap }),
|
|
1708
2138
|
items: tailItems(),
|
|
1709
2139
|
terminal: liveReason === "idle" || liveReason === "close",
|
|
@@ -1720,6 +2150,12 @@ export async function runTail(
|
|
|
1720
2150
|
sessionId: string,
|
|
1721
2151
|
args: SdkSessionCliArgs,
|
|
1722
2152
|
): Promise<unknown> {
|
|
2153
|
+
if (args.cursor !== undefined && args.afterTranscriptId === undefined)
|
|
2154
|
+
throw new SdkSessionCliError(
|
|
2155
|
+
"usage",
|
|
2156
|
+
"--after-transcript-id is required when resuming a session tail with --cursor.",
|
|
2157
|
+
2,
|
|
2158
|
+
);
|
|
1723
2159
|
const row = (await sessionRows(agentDir, { resolveSessionId: sessionId })).sessions.find(
|
|
1724
2160
|
candidate => candidate.sessionId === sessionId,
|
|
1725
2161
|
);
|
|
@@ -1727,7 +2163,10 @@ export async function runTail(
|
|
|
1727
2163
|
throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} is not indexed by the broker.`, 1);
|
|
1728
2164
|
if (row.deleted)
|
|
1729
2165
|
throw new SdkSessionCliError("session_deleted", `Session ${sessionId} was deleted and has no tail.`, 1);
|
|
1730
|
-
if (
|
|
2166
|
+
if (row.live && row.terminalUncertain !== true && args.afterTranscriptId !== undefined && args.cursor === undefined)
|
|
2167
|
+
throw new SdkSessionCliError("usage", "--after-transcript-id requires --cursor for a live session tail.", 2);
|
|
2168
|
+
if (!row.live || row.terminalUncertain === true)
|
|
2169
|
+
return await offlineTailReplay(repo, agentDir, sessionId, row, args.afterTranscriptId);
|
|
1731
2170
|
return await runLiveTail(agentDir, sessionId, row, args);
|
|
1732
2171
|
}
|
|
1733
2172
|
|
|
@@ -1803,11 +2242,44 @@ function lifecycleMutationRequest(
|
|
|
1803
2242
|
return { ...base, operation, capability: "session.delete", target };
|
|
1804
2243
|
}
|
|
1805
2244
|
|
|
2245
|
+
async function resolveCloseAuthority(
|
|
2246
|
+
lifecycle: SessionLifecycleService,
|
|
2247
|
+
sessionId: string,
|
|
2248
|
+
): Promise<{ endpointGeneration: number; endpointIncarnation: string }> {
|
|
2249
|
+
const listed = await lifecycle.list({
|
|
2250
|
+
actor: SDK_SESSION_CLI_LIFECYCLE_ACTOR,
|
|
2251
|
+
capability: "session.list",
|
|
2252
|
+
target: { resolveSessionId: sessionId },
|
|
2253
|
+
});
|
|
2254
|
+
if (!listed.ok) throw new SdkSessionCliError(listed.error.code, listed.error.message, 1);
|
|
2255
|
+
if (!("sessions" in listed.result))
|
|
2256
|
+
throw new SdkSessionCliError("malformed_response", "broker returned a scoped result for session close", 1);
|
|
2257
|
+
const row = listed.result.sessions.find(session => session.sessionId === sessionId);
|
|
2258
|
+
if (
|
|
2259
|
+
row === undefined ||
|
|
2260
|
+
typeof row.endpointGeneration !== "number" ||
|
|
2261
|
+
!Number.isSafeInteger(row.endpointGeneration) ||
|
|
2262
|
+
row.endpointGeneration <= 0 ||
|
|
2263
|
+
typeof row.endpointIncarnation !== "string" ||
|
|
2264
|
+
!/^[a-f0-9]{64}$/u.test(row.endpointIncarnation)
|
|
2265
|
+
)
|
|
2266
|
+
throw new SdkSessionCliError("session_unavailable", `Session ${sessionId} has no live endpoint authority.`, 1);
|
|
2267
|
+
return { endpointGeneration: row.endpointGeneration, endpointIncarnation: row.endpointIncarnation };
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function sessionCloseRequestKey(
|
|
2271
|
+
sessionId: string,
|
|
2272
|
+
authority: { endpointGeneration: number; endpointIncarnation: string },
|
|
2273
|
+
): string {
|
|
2274
|
+
return `${SDK_SESSION_CLI_LIFECYCLE_ACTOR.namespace}:session.close:${sessionId}:${authority.endpointGeneration}:${authority.endpointIncarnation}`;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
1806
2277
|
async function runRawGlobal(
|
|
1807
2278
|
agentDir: string,
|
|
1808
2279
|
operation: string,
|
|
1809
2280
|
input: JsonRecord,
|
|
1810
2281
|
args: SdkSessionCliArgs,
|
|
2282
|
+
lifecycleOverride?: SessionLifecycleService,
|
|
1811
2283
|
): Promise<unknown> {
|
|
1812
2284
|
if (args.page && operation !== "session.list")
|
|
1813
2285
|
throw new SdkSessionCliError("usage", "--page is only available for raw global session.list.", 2);
|
|
@@ -1834,7 +2306,7 @@ async function runRawGlobal(
|
|
|
1834
2306
|
if (operation === "session.lookup") {
|
|
1835
2307
|
if (!args.idempotencyKey)
|
|
1836
2308
|
throw new SdkSessionCliError("invalid_input", "--idempotency-key is required for session lookup.", 2);
|
|
1837
|
-
const lifecycle = createBrokerSessionLifecycleService(agentDir);
|
|
2309
|
+
const lifecycle = lifecycleOverride ?? createBrokerSessionLifecycleService(agentDir);
|
|
1838
2310
|
return await lifecycle.lookup({
|
|
1839
2311
|
actor: SDK_SESSION_CLI_LIFECYCLE_ACTOR,
|
|
1840
2312
|
capability: "session.lookup",
|
|
@@ -1847,10 +2319,17 @@ async function runRawGlobal(
|
|
|
1847
2319
|
throw new SdkSessionCliError("unknown_operation", `Unknown global operation: ${operation}`, 1);
|
|
1848
2320
|
if (!args.idempotencyKey)
|
|
1849
2321
|
throw new SdkSessionCliError("invalid_input", "--idempotency-key is required for lifecycle operations.", 2);
|
|
1850
|
-
const lifecycle = createBrokerSessionLifecycleService(agentDir);
|
|
2322
|
+
const lifecycle = lifecycleOverride ?? createBrokerSessionLifecycleService(agentDir);
|
|
1851
2323
|
const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
|
|
1852
2324
|
const response = await lifecycle.execute(lifecycleMutationRequest(operation, input, args.idempotencyKey, timeoutMs));
|
|
1853
|
-
|
|
2325
|
+
try {
|
|
2326
|
+
if (!response.ok) throw lifecyclePublicFailure(response);
|
|
2327
|
+
} catch (error) {
|
|
2328
|
+
throw normalizeSessionFailure(error, {
|
|
2329
|
+
...args,
|
|
2330
|
+
sessionId: typeof input.sessionId === "string" ? input.sessionId : args.sessionId,
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
1854
2333
|
return response;
|
|
1855
2334
|
}
|
|
1856
2335
|
|
|
@@ -1869,6 +2348,7 @@ export async function runSdkSessionCli(
|
|
|
1869
2348
|
setExitCode: (exitCode: 1 | 2) => void = exitCode => {
|
|
1870
2349
|
process.exitCode = exitCode;
|
|
1871
2350
|
},
|
|
2351
|
+
deps: SdkSessionCliDependencies = {},
|
|
1872
2352
|
): Promise<void> {
|
|
1873
2353
|
try {
|
|
1874
2354
|
const action = args.action;
|
|
@@ -1879,6 +2359,7 @@ export async function runSdkSessionCli(
|
|
|
1879
2359
|
action !== "send" &&
|
|
1880
2360
|
action !== "status" &&
|
|
1881
2361
|
action !== "tail" &&
|
|
2362
|
+
action !== "close" &&
|
|
1882
2363
|
action !== "retire" &&
|
|
1883
2364
|
action !== "raw" &&
|
|
1884
2365
|
action !== "control" &&
|
|
@@ -1887,7 +2368,7 @@ export async function runSdkSessionCli(
|
|
|
1887
2368
|
)
|
|
1888
2369
|
throw new SdkSessionCliError(
|
|
1889
2370
|
"usage",
|
|
1890
|
-
"Expected one of: list, inspect, send, status, tail, retire, raw (control|query|global).",
|
|
2371
|
+
"Expected one of: list, inspect, send, status, tail, close, retire, raw (control|query|global).",
|
|
1891
2372
|
2,
|
|
1892
2373
|
);
|
|
1893
2374
|
const agentDir = path.resolve(args.agentDir ?? getAgentDir());
|
|
@@ -1898,7 +2379,6 @@ export async function runSdkSessionCli(
|
|
|
1898
2379
|
if (action === "search") {
|
|
1899
2380
|
const search = await runSdkSearch(args);
|
|
1900
2381
|
writeOutput(args.json === true ? search.result : renderSdkSearchTable(search.result));
|
|
1901
|
-
if (search.exitCode !== 0) setExitCode(search.exitCode);
|
|
1902
2382
|
return;
|
|
1903
2383
|
}
|
|
1904
2384
|
if (action === "inspect") {
|
|
@@ -1930,6 +2410,57 @@ export async function runSdkSessionCli(
|
|
|
1930
2410
|
);
|
|
1931
2411
|
return;
|
|
1932
2412
|
}
|
|
2413
|
+
if (action === "close") {
|
|
2414
|
+
const sessionId = requireValue(args.sessionId, "<sessionId>");
|
|
2415
|
+
const input = await inputFromArgs(args);
|
|
2416
|
+
if (input.sessionId !== undefined && input.sessionId !== sessionId)
|
|
2417
|
+
throw new SdkSessionCliError("invalid_input", "Close sessionId does not match the selected session.", 2);
|
|
2418
|
+
const secretError = validateAdapterSecretFields("session.close", input);
|
|
2419
|
+
if (secretError) throw new SdkSessionCliError(secretError.code, secretError.message, 2);
|
|
2420
|
+
const lifecycle = deps.lifecycleService ?? createBrokerSessionLifecycleService(agentDir);
|
|
2421
|
+
const hasExplicitAuthority =
|
|
2422
|
+
Object.hasOwn(input, "endpointGeneration") || Object.hasOwn(input, "endpointIncarnation");
|
|
2423
|
+
const authority = hasExplicitAuthority
|
|
2424
|
+
? typeof input.endpointGeneration === "number" &&
|
|
2425
|
+
Number.isSafeInteger(input.endpointGeneration) &&
|
|
2426
|
+
input.endpointGeneration > 0 &&
|
|
2427
|
+
typeof input.endpointIncarnation === "string" &&
|
|
2428
|
+
/^[a-f0-9]{64}$/u.test(input.endpointIncarnation)
|
|
2429
|
+
? { endpointGeneration: input.endpointGeneration, endpointIncarnation: input.endpointIncarnation }
|
|
2430
|
+
: undefined
|
|
2431
|
+
: await resolveCloseAuthority(lifecycle, sessionId);
|
|
2432
|
+
if (hasExplicitAuthority && authority === undefined)
|
|
2433
|
+
throw new SdkSessionCliError(
|
|
2434
|
+
"invalid_input",
|
|
2435
|
+
"endpointGeneration and endpointIncarnation must identify a valid endpoint authority.",
|
|
2436
|
+
2,
|
|
2437
|
+
);
|
|
2438
|
+
const target = { ...input, sessionId, ...(authority ?? {}) };
|
|
2439
|
+
const requestKey = args.idempotencyKey ?? sessionCloseRequestKey(sessionId, authority!);
|
|
2440
|
+
// Closing never attaches: a Router attachment registers this process as a
|
|
2441
|
+
// live client and renews the host's abandonment window, which is the
|
|
2442
|
+
// opposite of what a close is for. The Broker answers the lifecycle
|
|
2443
|
+
// mutation over its own client.
|
|
2444
|
+
//
|
|
2445
|
+
// The default request key is scoped to the current endpoint generation and
|
|
2446
|
+
// incarnation. A retry for one live host replays, while a resumed host with
|
|
2447
|
+
// the same session id receives a fresh lifecycle identity.
|
|
2448
|
+
writeOutput(
|
|
2449
|
+
stripSecretFields(
|
|
2450
|
+
await runRawGlobal(
|
|
2451
|
+
agentDir,
|
|
2452
|
+
"session.close",
|
|
2453
|
+
target,
|
|
2454
|
+
{
|
|
2455
|
+
...args,
|
|
2456
|
+
idempotencyKey: requestKey,
|
|
2457
|
+
},
|
|
2458
|
+
lifecycle,
|
|
2459
|
+
),
|
|
2460
|
+
),
|
|
2461
|
+
);
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
1933
2464
|
if (action === "retire") {
|
|
1934
2465
|
const sessionId = requireValue(args.sessionId, "<sessionId>");
|
|
1935
2466
|
const input = await inputFromArgs(args);
|
|
@@ -1953,19 +2484,11 @@ export async function runSdkSessionCli(
|
|
|
1953
2484
|
if (!kind) throw new SdkSessionCliError("usage", "raw requires one of: control, query, global.", 2);
|
|
1954
2485
|
const operation = kind === "query" ? requireValue(args.query, "--query") : requireValue(args.operation, "--op");
|
|
1955
2486
|
if (isRawSpawnOperation(kind, operation))
|
|
1956
|
-
throw
|
|
1957
|
-
"adapter_operation_prohibited",
|
|
1958
|
-
"session.spawn is unavailable through the SDK session CLI.",
|
|
1959
|
-
1,
|
|
1960
|
-
);
|
|
2487
|
+
throw sdkPublicFailure("adapter_operation_prohibited", undefined, "pre-effect");
|
|
1961
2488
|
const dispositionError = cliOperationError(kind, operation);
|
|
1962
|
-
if (dispositionError) throw
|
|
2489
|
+
if (dispositionError) throw sdkPublicFailure(dispositionError.code, undefined, "pre-effect");
|
|
1963
2490
|
if (isEndpointOperation(operation))
|
|
1964
|
-
throw
|
|
1965
|
-
"endpoint_credential_forbidden",
|
|
1966
|
-
"session.get_endpoint is not available through the SDK session CLI.",
|
|
1967
|
-
1,
|
|
1968
|
-
);
|
|
2491
|
+
throw sdkPublicFailure("endpoint_credential_forbidden", undefined, "pre-effect");
|
|
1969
2492
|
const input = await inputFromArgs(args);
|
|
1970
2493
|
const secretError = validateAdapterSecretFields(operation, input);
|
|
1971
2494
|
if (secretError) throw new SdkSessionCliError(secretError.code, secretError.message, 2);
|
|
@@ -1984,26 +2507,6 @@ export async function runSdkSessionCli(
|
|
|
1984
2507
|
),
|
|
1985
2508
|
);
|
|
1986
2509
|
} catch (error) {
|
|
1987
|
-
|
|
1988
|
-
error instanceof SdkSessionCliError
|
|
1989
|
-
? error
|
|
1990
|
-
: error instanceof SessionRouterError
|
|
1991
|
-
? new SdkSessionCliError(error.phase, error.message, 1)
|
|
1992
|
-
: error instanceof SdkClientError
|
|
1993
|
-
? new SdkSessionCliError(error.code, error.message, 1, error.details)
|
|
1994
|
-
: new SdkSessionCliError(
|
|
1995
|
-
"operation_failed",
|
|
1996
|
-
error instanceof Error ? error.message : "SDK operation failed.",
|
|
1997
|
-
1,
|
|
1998
|
-
);
|
|
1999
|
-
writeOutput({
|
|
2000
|
-
ok: false,
|
|
2001
|
-
error: {
|
|
2002
|
-
code: cliError.code,
|
|
2003
|
-
message: cliError.message,
|
|
2004
|
-
...(cliError.details === undefined ? {} : { details: stripSecretFields(cliError.details) }),
|
|
2005
|
-
},
|
|
2006
|
-
});
|
|
2007
|
-
setExitCode(cliError.exitCode);
|
|
2510
|
+
throw normalizeSessionFailure(error, args);
|
|
2008
2511
|
}
|
|
2009
2512
|
}
|