@gajae-code/coding-agent 0.11.11 → 0.12.1
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 +131 -4
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +83 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +38 -4
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +10 -19
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
- package/dist/types/modes/components/settings-selector.d.ts +11 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +44 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +82 -12
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +48 -19
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +96 -12
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -4
- package/dist/types/skill-state/workflow-hud.d.ts +9 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/output-meta.d.ts +44 -3
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -7
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +11 -13
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/async/job-manager.ts +78 -20
- package/src/capability/index.ts +8 -0
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/team.ts +20 -2
- package/src/config/file-lock.ts +11 -2
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +74 -1
- package/src/config/settings.ts +246 -108
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
- package/src/defaults/gjc/skills/team/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/eval/py/executor.ts +7 -2
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +46 -5
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +42 -5
- package/src/gjc-runtime/team-runtime.ts +1085 -40
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
- package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
- package/src/gjc-runtime/workflow-manifest.ts +38 -247
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +21 -22
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +98 -0
- package/src/modes/acp/acp-agent.ts +339 -191
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +96 -28
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +457 -83
- package/src/modes/components/settings-selector.ts +116 -23
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +3 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +26 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +127 -36
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +22 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -5
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/read.md +4 -2
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +984 -146
- package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +303 -0
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +101 -10
- package/src/session/agent-session.ts +1286 -220
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2364 -438
- package/src/session/session-storage.ts +9 -1
- package/src/session/streaming-output.ts +500 -111
- package/src/setup/credential-import.ts +75 -13
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +32 -10
- package/src/skill-state/workflow-hud.ts +42 -2
- package/src/skill-state/workflow-mutation-guard.ts +560 -8
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +169 -118
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/output-meta.ts +281 -20
- package/src/tools/path-utils.ts +8 -3
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1560 -310
- package/src/tools/resource-gc.ts +182 -18
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/tools/tool-result.ts +14 -1
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/codex.ts +1 -1
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -28,6 +28,27 @@ const NL = "\n";
|
|
|
28
28
|
|
|
29
29
|
const ELLIPSIS = "…";
|
|
30
30
|
|
|
31
|
+
const ARTIFACT_WRITER_DIAGNOSTIC_MAX_BYTES = 256;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Terminal publisher used when managed artifact storage exposes an ID but no
|
|
35
|
+
* writable pathname. The sink supplies raw content retained up to its artifact
|
|
36
|
+
* hard cap; `omittedBytes` is the source-byte count already omitted by that cap.
|
|
37
|
+
* A successful publisher may report additional omitted bytes (for example, a
|
|
38
|
+
* downstream storage cap); those counts are added to the sink summary. Failed
|
|
39
|
+
* and unavailable results are surfaced through the bounded artifact diagnostic
|
|
40
|
+
* without exposing an unresolvable artifact URL.
|
|
41
|
+
*/
|
|
42
|
+
export type TerminalArtifactPublishResult =
|
|
43
|
+
| { status: "published"; artifactId: string; omittedBytes?: number }
|
|
44
|
+
| { status: "unavailable" }
|
|
45
|
+
| { status: "failed"; diagnostic: string };
|
|
46
|
+
|
|
47
|
+
export type TerminalArtifactPublisher = (
|
|
48
|
+
content: string,
|
|
49
|
+
info: { totalBytes: number; omittedBytes: number },
|
|
50
|
+
) => Promise<TerminalArtifactPublishResult>;
|
|
51
|
+
|
|
31
52
|
// =============================================================================
|
|
32
53
|
// Interfaces
|
|
33
54
|
// =============================================================================
|
|
@@ -47,10 +68,12 @@ export interface OutputSummary {
|
|
|
47
68
|
columnDroppedBytes?: number;
|
|
48
69
|
/** Number of distinct lines that hit the per-line column cap. */
|
|
49
70
|
columnTruncatedLines?: number;
|
|
50
|
-
/** Artifact ID for internal URL access (artifact://<id>) when
|
|
71
|
+
/** Artifact ID for internal URL access (artifact://<id>) when output was persisted. */
|
|
51
72
|
artifactId?: string;
|
|
52
73
|
/** Bytes omitted from artifact storage after the artifact hard cap was reached. */
|
|
53
74
|
artifactTruncatedBytes?: number;
|
|
75
|
+
/** Bounded diagnostic when artifact writer or terminal publisher creation, write, finalization, or publication failed. */
|
|
76
|
+
artifactFailureDiagnostic?: string;
|
|
54
77
|
}
|
|
55
78
|
|
|
56
79
|
export interface OutputSinkOptions {
|
|
@@ -60,6 +83,12 @@ export interface OutputSinkOptions {
|
|
|
60
83
|
*/
|
|
61
84
|
artifactPath?: string;
|
|
62
85
|
artifactId?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Optional terminal publisher for managed artifact stores that do not expose a
|
|
88
|
+
* writable path. It is invoked only when visible spill/truncation requires an
|
|
89
|
+
* artifact, and receives raw content bounded by `artifactMaxBytes`.
|
|
90
|
+
*/
|
|
91
|
+
artifactPublisher?: TerminalArtifactPublisher;
|
|
63
92
|
/** Tail buffer budget (bytes). Default DEFAULT_MAX_BYTES. */
|
|
64
93
|
spillThreshold?: number;
|
|
65
94
|
/**
|
|
@@ -114,8 +143,12 @@ export interface TruncationResult {
|
|
|
114
143
|
elidedLines?: number;
|
|
115
144
|
lastLinePartial?: boolean;
|
|
116
145
|
firstLineExceedsLimit?: boolean;
|
|
146
|
+
lastLineExceedsLimit?: boolean;
|
|
117
147
|
}
|
|
118
148
|
|
|
149
|
+
/** Direction vocabulary used by callers that select which end of content to retain. */
|
|
150
|
+
export type TruncationDirection = "head" | "last" | "both";
|
|
151
|
+
|
|
119
152
|
export interface TruncationOptions {
|
|
120
153
|
/** Maximum number of lines (default: 3000) */
|
|
121
154
|
maxLines?: number;
|
|
@@ -131,6 +164,8 @@ export interface TruncationOptions {
|
|
|
131
164
|
* window receives `maxLines - maxHeadLines`. Default `floor(maxLines/2)`.
|
|
132
165
|
*/
|
|
133
166
|
maxHeadLines?: number;
|
|
167
|
+
/** Direction to dispatch through truncateContent (head, last, or both). */
|
|
168
|
+
direction?: TruncationDirection;
|
|
134
169
|
}
|
|
135
170
|
|
|
136
171
|
/** Result from byte-level truncation helpers. */
|
|
@@ -452,6 +487,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
452
487
|
outputBytes: tail.bytes,
|
|
453
488
|
lastLinePartial: true,
|
|
454
489
|
firstLineExceedsLimit: false,
|
|
490
|
+
lastLineExceedsLimit: true,
|
|
455
491
|
};
|
|
456
492
|
}
|
|
457
493
|
break;
|
|
@@ -474,6 +510,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|
|
474
510
|
outputBytes: tail.bytes,
|
|
475
511
|
lastLinePartial: true,
|
|
476
512
|
firstLineExceedsLimit: false,
|
|
513
|
+
lastLineExceedsLimit: true,
|
|
477
514
|
};
|
|
478
515
|
}
|
|
479
516
|
break;
|
|
@@ -516,14 +553,129 @@ export function formatMiddleElisionMarker(elidedLines: number, elidedBytes: numb
|
|
|
516
553
|
}
|
|
517
554
|
|
|
518
555
|
/**
|
|
519
|
-
*
|
|
556
|
+
* A retained source segment returned by {@link truncateMiddleWindows}.
|
|
557
|
+
* Line coordinates are 1-indexed and inclusive.
|
|
558
|
+
*/
|
|
559
|
+
export type ReadSegment =
|
|
560
|
+
| {
|
|
561
|
+
kind: "lines";
|
|
562
|
+
content: string;
|
|
563
|
+
lines: number;
|
|
564
|
+
bytes: number;
|
|
565
|
+
origin: { startLine: number; endLine: number };
|
|
566
|
+
lastLinePartial: false;
|
|
567
|
+
}
|
|
568
|
+
| {
|
|
569
|
+
kind: "partial-line";
|
|
570
|
+
content: string;
|
|
571
|
+
lines: 1;
|
|
572
|
+
bytes: number;
|
|
573
|
+
origin: { startLine: number; endLine: number };
|
|
574
|
+
sourceLineBytes: number;
|
|
575
|
+
lastLinePartial: true;
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
/** The actual windows retained by middle truncation. */
|
|
579
|
+
export interface ReadWindow {
|
|
580
|
+
kind: "full" | "head-only" | "tail-only" | "middle";
|
|
581
|
+
head?: ReadSegment;
|
|
582
|
+
tail?: ReadSegment;
|
|
583
|
+
// With normalized head/tail line budgets, an overlap cannot occur: a
|
|
584
|
+
// source that can fit both windows returns `full`, otherwise their kept
|
|
585
|
+
// line counts sum to less than the source. Retain this defensive value for
|
|
586
|
+
// custom/future budgets and wire compatibility.
|
|
587
|
+
overlap: "disjoint" | "adjacent" | "overlapping";
|
|
588
|
+
elidedLines: number;
|
|
589
|
+
elidedBytes: number;
|
|
590
|
+
totalLines: number;
|
|
591
|
+
totalBytes: number;
|
|
592
|
+
/** @deprecated Non-enumerable compatibility reason for older consumers. */
|
|
593
|
+
truncatedBy?: "lines" | "bytes" | "middle";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function outputLineCount(result: TruncationResult): number {
|
|
597
|
+
return result.outputLines ?? (result.content.length === 0 ? 0 : countNewlines(result.content) + 1);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function outputByteCount(result: TruncationResult): number {
|
|
601
|
+
return result.outputBytes ?? Buffer.byteLength(result.content, "utf-8");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function lastSourceLineBytes(content: string): number {
|
|
605
|
+
const lineStart = content.lastIndexOf(NL) + 1;
|
|
606
|
+
return Buffer.byteLength(content.slice(lineStart), "utf-8");
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function makeReadSegment(
|
|
610
|
+
result: TruncationResult,
|
|
611
|
+
startLine: number,
|
|
612
|
+
totalLines: number,
|
|
613
|
+
sourceContent: string = result.content,
|
|
614
|
+
): ReadSegment | undefined {
|
|
615
|
+
const lines = outputLineCount(result);
|
|
616
|
+
if (lines <= 0) return undefined;
|
|
617
|
+
const bytes = outputByteCount(result);
|
|
618
|
+
const endLine = Math.min(totalLines, startLine + lines - 1);
|
|
619
|
+
if (result.lastLinePartial) {
|
|
620
|
+
const partialLine = endLine;
|
|
621
|
+
return {
|
|
622
|
+
kind: "partial-line",
|
|
623
|
+
content: result.content,
|
|
624
|
+
lines: 1,
|
|
625
|
+
bytes,
|
|
626
|
+
origin: { startLine: partialLine, endLine: partialLine },
|
|
627
|
+
sourceLineBytes: lastSourceLineBytes(sourceContent),
|
|
628
|
+
lastLinePartial: true,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
kind: "lines",
|
|
633
|
+
content: result.content,
|
|
634
|
+
lines,
|
|
635
|
+
bytes,
|
|
636
|
+
origin: { startLine, endLine },
|
|
637
|
+
lastLinePartial: false,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function withMiddleWindowReason(windows: ReadWindow, truncatedBy: "lines" | "bytes" | "middle"): ReadWindow {
|
|
642
|
+
Object.defineProperty(windows, "truncatedBy", { value: truncatedBy, enumerable: false });
|
|
643
|
+
return windows;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function fullReadWindow(
|
|
647
|
+
content: string,
|
|
648
|
+
totalLines: number,
|
|
649
|
+
totalBytes: number,
|
|
650
|
+
overlap: ReadWindow["overlap"] = "disjoint",
|
|
651
|
+
): ReadWindow {
|
|
652
|
+
return {
|
|
653
|
+
kind: "full",
|
|
654
|
+
head: {
|
|
655
|
+
kind: "lines",
|
|
656
|
+
content,
|
|
657
|
+
lines: totalLines,
|
|
658
|
+
bytes: totalBytes,
|
|
659
|
+
origin: { startLine: 1, endLine: totalLines },
|
|
660
|
+
lastLinePartial: false,
|
|
661
|
+
},
|
|
662
|
+
overlap,
|
|
663
|
+
elidedLines: 0,
|
|
664
|
+
elidedBytes: 0,
|
|
665
|
+
totalLines,
|
|
666
|
+
totalBytes,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Truncate content while exposing the exact retained head/tail windows.
|
|
520
672
|
*
|
|
521
|
-
* The
|
|
522
|
-
*
|
|
523
|
-
*
|
|
524
|
-
*
|
|
673
|
+
* The budget split and fallback ordering intentionally mirror truncateMiddle.
|
|
674
|
+
* The overlap classification is performed before resolving an overlap to a
|
|
675
|
+
* full window, so callers can distinguish disjoint, adjacent and overlapping
|
|
676
|
+
* candidate windows even when no marker is needed.
|
|
525
677
|
*/
|
|
526
|
-
export function
|
|
678
|
+
export function truncateMiddleWindows(content: string, options: TruncationOptions = {}): ReadWindow {
|
|
527
679
|
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
528
680
|
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
529
681
|
const headBytes = options.maxHeadBytes ?? Math.floor(maxBytes / 2);
|
|
@@ -533,57 +685,153 @@ export function truncateMiddle(content: string, options: TruncationOptions = {})
|
|
|
533
685
|
|
|
534
686
|
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
535
687
|
const totalLines = countNewlines(content) + 1;
|
|
688
|
+
if (totalBytes <= maxBytes && totalLines <= maxLines) return fullReadWindow(content, totalLines, totalBytes);
|
|
689
|
+
|
|
690
|
+
const oneSided = (kind: "head-only" | "tail-only", result: TruncationResult, startLine: number): ReadWindow => {
|
|
691
|
+
const lines = outputLineCount(result);
|
|
692
|
+
const bytes = outputByteCount(result);
|
|
693
|
+
const segment = makeReadSegment(result, startLine, totalLines, content);
|
|
694
|
+
|
|
695
|
+
const windows: ReadWindow = {
|
|
696
|
+
kind,
|
|
697
|
+
...(kind === "head-only" ? (segment ? { head: segment } : {}) : segment ? { tail: segment } : {}),
|
|
698
|
+
overlap: "disjoint",
|
|
699
|
+
elidedLines: Math.max(0, totalLines - lines),
|
|
700
|
+
elidedBytes: Math.max(0, totalBytes - bytes),
|
|
701
|
+
totalLines,
|
|
702
|
+
totalBytes,
|
|
703
|
+
};
|
|
704
|
+
return withMiddleWindowReason(windows, result.truncatedBy === "lines" ? "lines" : "bytes");
|
|
705
|
+
};
|
|
536
706
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Degenerate budgets → fall back to one-sided truncation.
|
|
707
|
+
// Keep this order identical to truncateMiddle: degenerate budgets are
|
|
708
|
+
// resolved before either one-sided truncator is called.
|
|
542
709
|
if (headBytes <= 0 || headLines <= 0) {
|
|
543
|
-
|
|
710
|
+
const tail = truncateTail(content, { maxBytes: tailBytes || maxBytes, maxLines: tailLines || maxLines });
|
|
711
|
+
return oneSided("tail-only", tail, totalLines - outputLineCount(tail) + 1);
|
|
544
712
|
}
|
|
545
713
|
if (tailBytes <= 0 || tailLines <= 0) {
|
|
546
|
-
|
|
714
|
+
const head = truncateHead(content, { maxBytes: headBytes, maxLines: headLines });
|
|
715
|
+
return oneSided("head-only", head, 1);
|
|
547
716
|
}
|
|
548
717
|
|
|
549
718
|
const head = truncateHead(content, { maxBytes: headBytes, maxLines: headLines });
|
|
550
719
|
const tail = truncateTail(content, { maxBytes: tailBytes, maxLines: tailLines });
|
|
551
|
-
|
|
552
|
-
const
|
|
553
|
-
const
|
|
554
|
-
const
|
|
555
|
-
const tailBytesKept = tail.outputBytes ?? Buffer.byteLength(tail.content, "utf-8");
|
|
720
|
+
const headLinesKept = outputLineCount(head);
|
|
721
|
+
const tailLinesKept = outputLineCount(tail);
|
|
722
|
+
const headBytesKept = outputByteCount(head);
|
|
723
|
+
const tailBytesKept = outputByteCount(tail);
|
|
556
724
|
|
|
557
725
|
// Head unusable (first line exceeds budget) → tail-only.
|
|
558
|
-
if (headLinesKept === 0 || head.firstLineExceedsLimit)
|
|
726
|
+
if (headLinesKept === 0 || head.firstLineExceedsLimit) {
|
|
727
|
+
return oneSided("tail-only", tail, totalLines - tailLinesKept + 1);
|
|
728
|
+
}
|
|
559
729
|
// Tail unusable → head-only.
|
|
560
|
-
if (tailLinesKept === 0) return head;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
730
|
+
if (tailLinesKept === 0) return oneSided("head-only", head, 1);
|
|
731
|
+
|
|
732
|
+
const headEnd = headLinesKept;
|
|
733
|
+
const tailStart = totalLines - tailLinesKept + 1;
|
|
734
|
+
const overlap: ReadWindow["overlap"] =
|
|
735
|
+
headEnd + 1 < tailStart ? "disjoint" : headEnd + 1 === tailStart ? "adjacent" : "overlapping";
|
|
736
|
+
const tailPartial = tail.lastLinePartial === true;
|
|
737
|
+
|
|
738
|
+
// Adjacent/overlapping complete windows cover the whole source. Return one
|
|
739
|
+
// full segment so renderers never insert a marker for an overlap.
|
|
740
|
+
if (headLinesKept + tailLinesKept >= totalLines && !tailPartial) {
|
|
741
|
+
return fullReadWindow(content, totalLines, totalBytes, overlap);
|
|
564
742
|
}
|
|
565
743
|
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
const
|
|
571
|
-
const
|
|
572
|
-
|
|
744
|
+
const headWindow = makeReadSegment(head, 1, totalLines, content);
|
|
745
|
+
const tailWindow = makeReadSegment(tail, tailStart, totalLines, content);
|
|
746
|
+
if (!headWindow || !tailWindow) return fullReadWindow(content, totalLines, totalBytes, overlap);
|
|
747
|
+
|
|
748
|
+
const partialSourceBytes = tailWindow.kind === "partial-line" ? tailWindow.sourceLineBytes : tailWindow.bytes;
|
|
749
|
+
const elidedLines = tailPartial
|
|
750
|
+
? Math.max(0, tailStart - headEnd - 1)
|
|
751
|
+
: Math.max(0, totalLines - headLinesKept - tailLinesKept);
|
|
752
|
+
const elidedBytes = tailPartial
|
|
753
|
+
? Math.max(0, totalBytes - headBytesKept - partialSourceBytes)
|
|
754
|
+
: Math.max(0, totalBytes - headBytesKept - tailBytesKept);
|
|
755
|
+
return withMiddleWindowReason(
|
|
756
|
+
{
|
|
757
|
+
kind: "middle",
|
|
758
|
+
head: headWindow,
|
|
759
|
+
tail: tailWindow,
|
|
760
|
+
overlap,
|
|
761
|
+
elidedLines,
|
|
762
|
+
elidedBytes,
|
|
763
|
+
totalLines,
|
|
764
|
+
totalBytes,
|
|
765
|
+
},
|
|
766
|
+
"middle",
|
|
767
|
+
);
|
|
768
|
+
}
|
|
573
769
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
770
|
+
/**
|
|
771
|
+
* Truncate content keeping a head window and a tail window, eliding the middle.
|
|
772
|
+
* The composed return shape remains field-for-field compatible with the
|
|
773
|
+
* historical implementation; callers that need coordinates use
|
|
774
|
+
* truncateMiddleWindows directly.
|
|
775
|
+
*/
|
|
776
|
+
export function truncateMiddle(content: string, options: TruncationOptions = {}): TruncationResult {
|
|
777
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
778
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
779
|
+
const headBytes = options.maxHeadBytes ?? Math.floor(maxBytes / 2);
|
|
780
|
+
const tailBytes = Math.max(0, maxBytes - headBytes);
|
|
781
|
+
const headLines = options.maxHeadLines ?? Math.max(1, Math.floor(maxLines / 2));
|
|
782
|
+
const tailLines = Math.max(0, maxLines - headLines);
|
|
783
|
+
const windows = truncateMiddleWindows(content, options);
|
|
784
|
+
|
|
785
|
+
switch (windows.kind) {
|
|
786
|
+
case "full":
|
|
787
|
+
return noTruncResult(content, windows.totalLines, windows.totalBytes);
|
|
788
|
+
case "tail-only":
|
|
789
|
+
return truncateTail(content, { maxBytes: tailBytes || maxBytes, maxLines: tailLines || maxLines });
|
|
790
|
+
case "head-only":
|
|
791
|
+
return truncateHead(content, { maxBytes: headBytes, maxLines: headLines });
|
|
792
|
+
case "middle": {
|
|
793
|
+
const head = windows.head;
|
|
794
|
+
const tail = windows.tail;
|
|
795
|
+
if (!head || !tail) return noTruncResult(content, windows.totalLines, windows.totalBytes);
|
|
796
|
+
if (head.lines + tail.lines >= windows.totalLines)
|
|
797
|
+
return noTruncResult(content, windows.totalLines, windows.totalBytes);
|
|
798
|
+
const legacyElidedLines = Math.max(0, windows.totalLines - head.lines - tail.lines);
|
|
799
|
+
const legacyElidedBytes =
|
|
800
|
+
tail.kind === "partial-line"
|
|
801
|
+
? Math.max(0, windows.totalBytes - head.bytes - tail.bytes)
|
|
802
|
+
: windows.elidedBytes;
|
|
803
|
+
const marker = formatMiddleElisionMarker(legacyElidedLines, legacyElidedBytes);
|
|
804
|
+
const markerBytes = Buffer.byteLength(marker, "utf-8");
|
|
805
|
+
return {
|
|
806
|
+
content: `${head.content}\n${marker}\n${tail.content}`,
|
|
807
|
+
truncated: true,
|
|
808
|
+
truncatedBy: "middle",
|
|
809
|
+
totalLines: windows.totalLines,
|
|
810
|
+
totalBytes: windows.totalBytes,
|
|
811
|
+
outputLines: head.lines + tail.lines + 1,
|
|
812
|
+
outputBytes: head.bytes + tail.bytes + markerBytes + 2,
|
|
813
|
+
elidedLines: legacyElidedLines,
|
|
814
|
+
elidedBytes: legacyElidedBytes,
|
|
815
|
+
lastLinePartial: tail.lastLinePartial,
|
|
816
|
+
firstLineExceedsLimit: false,
|
|
817
|
+
...(tail.lastLinePartial ? { lastLineExceedsLimit: true } : {}),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Dispatch truncation using the caller-facing direction vocabulary.
|
|
825
|
+
*/
|
|
826
|
+
export function truncateContent(content: string, options: TruncationOptions = {}): TruncationResult {
|
|
827
|
+
switch (options.direction) {
|
|
828
|
+
case "last":
|
|
829
|
+
return truncateTail(content, options);
|
|
830
|
+
case "both":
|
|
831
|
+
return truncateMiddle(content, options);
|
|
832
|
+
default:
|
|
833
|
+
return truncateHead(content, options);
|
|
834
|
+
}
|
|
587
835
|
}
|
|
588
836
|
|
|
589
837
|
// =============================================================================
|
|
@@ -694,6 +942,9 @@ export class OutputSink {
|
|
|
694
942
|
#artifactBytes = 0;
|
|
695
943
|
#artifactTruncatedBytes = 0;
|
|
696
944
|
#artifactTruncationNoticeWritten = false;
|
|
945
|
+
#artifactFailureDiagnostic?: string;
|
|
946
|
+
#publishedArtifactId?: string;
|
|
947
|
+
#artifactFinalizationFailed = false;
|
|
697
948
|
|
|
698
949
|
// Per-line column cap streaming state (persists across `push` calls so a
|
|
699
950
|
// long line split across chunks still trips the same trigger).
|
|
@@ -708,9 +959,13 @@ export class OutputSink {
|
|
|
708
959
|
};
|
|
709
960
|
|
|
710
961
|
// Raw prefix chunks not yet confirmed written to the file sink. This queue is
|
|
711
|
-
// the
|
|
962
|
+
// the pathname-backed artifact replay source; retained head/tail windows are lossy views.
|
|
712
963
|
#pendingFileWrites?: string[];
|
|
713
964
|
#pendingFileWriteBytes = 0;
|
|
965
|
+
// Raw chunks retained for a terminal publisher. This queue is capped by
|
|
966
|
+
// `#artifactMaxBytes` and is never used by pathname-backed sinks.
|
|
967
|
+
#pendingArtifactContent?: string[];
|
|
968
|
+
#artifactPublishAttempted = false;
|
|
714
969
|
#finalized = false;
|
|
715
970
|
|
|
716
971
|
#fileReady = false;
|
|
@@ -724,6 +979,7 @@ export class OutputSink {
|
|
|
724
979
|
readonly #chunkThrottleMs: number;
|
|
725
980
|
readonly #maxColumns: number;
|
|
726
981
|
readonly #artifactMaxBytes: number;
|
|
982
|
+
readonly #artifactPublisher?: TerminalArtifactPublisher;
|
|
727
983
|
readonly #coalesceSanitize: boolean;
|
|
728
984
|
#coalesceBuf = "";
|
|
729
985
|
|
|
@@ -738,6 +994,7 @@ export class OutputSink {
|
|
|
738
994
|
chunkThrottleMs = 0,
|
|
739
995
|
onRawChunk,
|
|
740
996
|
artifactMaxBytes = DEFAULT_ARTIFACT_MAX_BYTES,
|
|
997
|
+
artifactPublisher,
|
|
741
998
|
coalesceSanitize = process.env.PI_OUTPUT_SANITIZE_COALESCE === "1",
|
|
742
999
|
} = options ?? {};
|
|
743
1000
|
// Managed callers omit artifactPath at the allocation boundary; explicit callers
|
|
@@ -751,6 +1008,7 @@ export class OutputSink {
|
|
|
751
1008
|
this.#onRawChunk = onRawChunk;
|
|
752
1009
|
this.#chunkThrottleMs = chunkThrottleMs;
|
|
753
1010
|
this.#artifactMaxBytes = Math.max(0, artifactMaxBytes);
|
|
1011
|
+
this.#artifactPublisher = artifactPublisher;
|
|
754
1012
|
this.#coalesceSanitize = coalesceSanitize;
|
|
755
1013
|
}
|
|
756
1014
|
|
|
@@ -841,11 +1099,14 @@ export class OutputSink {
|
|
|
841
1099
|
// open, keep an independent raw replay prefix because retained head/tail
|
|
842
1100
|
// windows are trimmed and cannot reconstruct byte-correct artifacts.
|
|
843
1101
|
if (this.#artifactPath && this.#maxColumns === 0) this.#enqueueFileWrite(rawChunk, rawBytes);
|
|
1102
|
+
else if (this.#artifactPublisher && this.#maxColumns === 0)
|
|
1103
|
+
this.#enqueueTerminalArtifactChunk(rawChunk, rawBytes);
|
|
844
1104
|
|
|
845
1105
|
if (rawBytes === 0) return;
|
|
846
1106
|
|
|
847
1107
|
const visibleChunk = this.#maxColumns > 0 ? this.#applyColumnCap(sanitizedChunk) : sanitizedChunk;
|
|
848
1108
|
if (this.#artifactPath && this.#maxColumns > 0) this.#enqueueFileWrite(rawChunk, rawBytes);
|
|
1109
|
+
else if (this.#artifactPublisher && this.#maxColumns > 0) this.#enqueueTerminalArtifactChunk(rawChunk, rawBytes);
|
|
849
1110
|
if (this.#columnDroppedBytes > 0) this.#createFileSink();
|
|
850
1111
|
const visibleBytes = Buffer.byteLength(visibleChunk, "utf-8");
|
|
851
1112
|
if (visibleChunk.length > 0) {
|
|
@@ -967,6 +1228,45 @@ export class OutputSink {
|
|
|
967
1228
|
return `\n[artifact truncated after ${this.#artifactBytes} bytes; omitted at least ${droppedBytes} bytes]\n`;
|
|
968
1229
|
}
|
|
969
1230
|
|
|
1231
|
+
#recordArtifactFailure(phase: "create" | "write" | "end", error: unknown): void {
|
|
1232
|
+
if (this.#artifactFailureDiagnostic) return;
|
|
1233
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim();
|
|
1234
|
+
const normalized = message || "unknown storage error";
|
|
1235
|
+
this.#artifactFailureDiagnostic = truncateHeadBytes(
|
|
1236
|
+
`${phase}: ${normalized}`,
|
|
1237
|
+
ARTIFACT_WRITER_DIAGNOSTIC_MAX_BYTES,
|
|
1238
|
+
).text;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
#recordTerminalArtifactDiagnostic(kind: "unavailable" | "failed", error?: unknown): void {
|
|
1242
|
+
if (this.#artifactFailureDiagnostic) return;
|
|
1243
|
+
const message =
|
|
1244
|
+
error === undefined
|
|
1245
|
+
? "terminal artifact publisher unavailable"
|
|
1246
|
+
: (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim() ||
|
|
1247
|
+
"unknown publication error";
|
|
1248
|
+
this.#artifactFailureDiagnostic = truncateHeadBytes(
|
|
1249
|
+
`${kind}: ${message}`,
|
|
1250
|
+
ARTIFACT_WRITER_DIAGNOSTIC_MAX_BYTES,
|
|
1251
|
+
).text;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// A failed write may have committed only a prefix, so the replay queue is no longer trustworthy.
|
|
1255
|
+
#failArtifact(phase: "create" | "write" | "end", error: unknown, sink?: Bun.FileSink): void {
|
|
1256
|
+
this.#recordArtifactFailure(phase, error);
|
|
1257
|
+
this.#artifactFinalizationFailed = true;
|
|
1258
|
+
this.#file = undefined;
|
|
1259
|
+
this.#fileReady = false;
|
|
1260
|
+
this.#pendingFileWrites = undefined;
|
|
1261
|
+
this.#pendingFileWriteBytes = 0;
|
|
1262
|
+
if (!sink) return;
|
|
1263
|
+
try {
|
|
1264
|
+
void Promise.resolve(sink.end()).catch(endError => this.#recordArtifactFailure("end", endError));
|
|
1265
|
+
} catch (endError) {
|
|
1266
|
+
this.#recordArtifactFailure("end", endError);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
970
1270
|
#capArtifactChunk(chunk: string, bytes: number): { chunk: string; bytes: number } | null {
|
|
971
1271
|
if (bytes === 0) return null;
|
|
972
1272
|
if (this.#artifactMaxBytes <= 0 || this.#artifactBytes >= this.#artifactMaxBytes) {
|
|
@@ -982,18 +1282,23 @@ export class OutputSink {
|
|
|
982
1282
|
return kept.bytes > 0 ? { chunk: kept.text, bytes: kept.bytes } : null;
|
|
983
1283
|
}
|
|
984
1284
|
|
|
985
|
-
#writeArtifactTruncationNotice(): void {
|
|
986
|
-
if (
|
|
1285
|
+
async #writeArtifactTruncationNotice(): Promise<void> {
|
|
1286
|
+
if (
|
|
1287
|
+
this.#artifactFinalizationFailed ||
|
|
1288
|
+
this.#artifactTruncatedBytes <= 0 ||
|
|
1289
|
+
this.#artifactTruncationNoticeWritten
|
|
1290
|
+
)
|
|
1291
|
+
return;
|
|
987
1292
|
const notice = this.#artifactTruncationNotice(this.#artifactTruncatedBytes);
|
|
988
1293
|
try {
|
|
989
1294
|
if (this.#fileReady && this.#file) {
|
|
990
|
-
this.#file.sink.write(notice);
|
|
1295
|
+
await this.#file.sink.write(notice);
|
|
991
1296
|
} else {
|
|
992
1297
|
this.#queuePendingFileWrite(notice, Buffer.byteLength(notice, "utf-8"));
|
|
993
1298
|
}
|
|
994
1299
|
this.#artifactTruncationNoticeWritten = true;
|
|
995
|
-
} catch {
|
|
996
|
-
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
this.#failArtifact("write", error, this.#file?.sink);
|
|
997
1302
|
}
|
|
998
1303
|
}
|
|
999
1304
|
|
|
@@ -1005,6 +1310,7 @@ export class OutputSink {
|
|
|
1005
1310
|
}
|
|
1006
1311
|
|
|
1007
1312
|
#enqueueFileWrite(chunk: string, bytes: number): void {
|
|
1313
|
+
if (this.#artifactFinalizationFailed || this.#finalized) return;
|
|
1008
1314
|
const capped = this.#capArtifactChunk(chunk, bytes);
|
|
1009
1315
|
if (!capped) return;
|
|
1010
1316
|
this.#artifactBytes += capped.bytes;
|
|
@@ -1016,51 +1322,113 @@ export class OutputSink {
|
|
|
1016
1322
|
|
|
1017
1323
|
try {
|
|
1018
1324
|
this.#file.sink.write(capped.chunk);
|
|
1019
|
-
} catch {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
this.#failArtifact("write", error, this.#file.sink);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
#enqueueTerminalArtifactChunk(chunk: string, bytes: number): void {
|
|
1331
|
+
if (this.#artifactFinalizationFailed || this.#finalized) return;
|
|
1332
|
+
const capped = this.#capArtifactChunk(chunk, bytes);
|
|
1333
|
+
if (!capped) return;
|
|
1334
|
+
this.#artifactBytes += capped.bytes;
|
|
1335
|
+
if (!this.#pendingArtifactContent) this.#pendingArtifactContent = [capped.chunk];
|
|
1336
|
+
else this.#pendingArtifactContent.push(capped.chunk);
|
|
1337
|
+
const pending = this.#pendingArtifactContent;
|
|
1338
|
+
if (!pending) return;
|
|
1339
|
+
if (pending.length > MAX_PENDING) {
|
|
1340
|
+
pending[0] = pending.join("");
|
|
1341
|
+
pending.length = 1;
|
|
1029
1342
|
}
|
|
1030
1343
|
}
|
|
1031
1344
|
|
|
1032
1345
|
#createFileSink(): boolean {
|
|
1033
|
-
if (this.#finalized) return false;
|
|
1346
|
+
if (this.#finalized || this.#artifactFinalizationFailed) return false;
|
|
1034
1347
|
|
|
1035
1348
|
if (!this.#artifactPath) return false;
|
|
1036
1349
|
if (this.#fileReady) return this.#file != null;
|
|
1350
|
+
|
|
1351
|
+
let sink: Bun.FileSink;
|
|
1037
1352
|
try {
|
|
1038
|
-
|
|
1039
|
-
|
|
1353
|
+
sink = Bun.file(this.#artifactPath).writer();
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
this.#recordArtifactFailure("create", error);
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
this.#file = { path: this.#artifactPath, artifactId: this.#artifactId, sink };
|
|
1040
1359
|
|
|
1360
|
+
try {
|
|
1041
1361
|
const pending = this.#pendingFileWrites;
|
|
1042
1362
|
if (pending) {
|
|
1043
1363
|
for (const chunk of pending) sink.write(chunk);
|
|
1044
1364
|
}
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
this.#failArtifact("write", error, sink);
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1045
1369
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1370
|
+
this.#fileReady = true;
|
|
1371
|
+
this.#pendingFileWrites = undefined;
|
|
1372
|
+
this.#pendingFileWriteBytes = 0;
|
|
1373
|
+
return true;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
#shouldPublishTerminalArtifact(): boolean {
|
|
1377
|
+
return (
|
|
1378
|
+
this.#artifactPublisher !== undefined &&
|
|
1379
|
+
this.#artifactPath === undefined &&
|
|
1380
|
+
(this.#truncated || this.#columnDroppedBytes > 0 || this.#artifactTruncatedBytes > 0)
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1049
1383
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1384
|
+
async #publishTerminalArtifact(): Promise<string | undefined> {
|
|
1385
|
+
if (
|
|
1386
|
+
!this.#shouldPublishTerminalArtifact() ||
|
|
1387
|
+
this.#artifactPublishAttempted ||
|
|
1388
|
+
this.#artifactFinalizationFailed
|
|
1389
|
+
) {
|
|
1390
|
+
return this.#publishedArtifactId;
|
|
1391
|
+
}
|
|
1392
|
+
this.#artifactPublishAttempted = true;
|
|
1393
|
+
const content = this.#pendingArtifactContent?.join("") ?? "";
|
|
1394
|
+
const omittedBytes = this.#artifactTruncatedBytes;
|
|
1395
|
+
try {
|
|
1396
|
+
const published = await this.#artifactPublisher!(content, {
|
|
1397
|
+
totalBytes: this.#totalBytes,
|
|
1398
|
+
omittedBytes,
|
|
1399
|
+
});
|
|
1400
|
+
if (published.status === "unavailable") {
|
|
1401
|
+
this.#recordTerminalArtifactDiagnostic("unavailable");
|
|
1402
|
+
this.#artifactFinalizationFailed = true;
|
|
1403
|
+
return undefined;
|
|
1056
1404
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1405
|
+
if (published.status === "failed") {
|
|
1406
|
+
this.#recordTerminalArtifactDiagnostic("failed", published.diagnostic);
|
|
1407
|
+
this.#artifactFinalizationFailed = true;
|
|
1408
|
+
return undefined;
|
|
1409
|
+
}
|
|
1410
|
+
if (published.artifactId.length === 0) {
|
|
1411
|
+
this.#recordTerminalArtifactDiagnostic("failed", "storage returned no artifact id");
|
|
1412
|
+
this.#artifactFinalizationFailed = true;
|
|
1413
|
+
return undefined;
|
|
1414
|
+
}
|
|
1415
|
+
if (published.omittedBytes !== undefined) {
|
|
1416
|
+
if (!Number.isSafeInteger(published.omittedBytes) || published.omittedBytes < 0) {
|
|
1417
|
+
this.#recordTerminalArtifactDiagnostic("failed", "publisher returned invalid omitted-byte accounting");
|
|
1418
|
+
this.#artifactFinalizationFailed = true;
|
|
1419
|
+
return undefined;
|
|
1420
|
+
}
|
|
1421
|
+
this.#artifactTruncatedBytes += published.omittedBytes;
|
|
1422
|
+
}
|
|
1423
|
+
this.#publishedArtifactId = published.artifactId;
|
|
1424
|
+
this.#finalized = true;
|
|
1425
|
+
return this.#publishedArtifactId;
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
this.#recordTerminalArtifactDiagnostic("failed", error);
|
|
1428
|
+
this.#artifactFinalizationFailed = true;
|
|
1429
|
+
return undefined;
|
|
1430
|
+
} finally {
|
|
1431
|
+
this.#pendingArtifactContent = undefined;
|
|
1064
1432
|
}
|
|
1065
1433
|
}
|
|
1066
1434
|
|
|
@@ -1083,25 +1451,34 @@ export class OutputSink {
|
|
|
1083
1451
|
* minimizer rewrites the captured output after the raw bytes have already
|
|
1084
1452
|
* been streamed.
|
|
1085
1453
|
*
|
|
1086
|
-
* After this call the
|
|
1087
|
-
*
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1090
|
-
* — which would otherwise reorder content and trip the middle-elision
|
|
1091
|
-
* branch in `dump()` against stale totals.
|
|
1454
|
+
* After this call the replacement is authoritative: counters reflect its full
|
|
1455
|
+
* size, the configured head+tail byte windows are retained with UTF-8-safe
|
|
1456
|
+
* boundaries, and future head accumulation is disabled so later `push()` calls
|
|
1457
|
+
* append directly to the tail without reordering the replacement.
|
|
1092
1458
|
*/
|
|
1093
1459
|
replace(text: string): void {
|
|
1094
1460
|
this.#coalesceBuf = "";
|
|
1095
|
-
|
|
1461
|
+
const replacementBytes = Buffer.byteLength(text, "utf-8");
|
|
1096
1462
|
this.#head = "";
|
|
1097
1463
|
this.#headBytes = 0;
|
|
1464
|
+
this.#setTail("");
|
|
1465
|
+
|
|
1466
|
+
if (this.#headLimit > 0) {
|
|
1467
|
+
const head = truncateHeadBytes(text, this.#headLimit);
|
|
1468
|
+
this.#appendHead(head.text, head.bytes);
|
|
1469
|
+
const tailText = text.substring(head.text.length);
|
|
1470
|
+
this.#setTail(tailText, replacementBytes - head.bytes);
|
|
1471
|
+
} else {
|
|
1472
|
+
this.#setTail(text, replacementBytes);
|
|
1473
|
+
}
|
|
1474
|
+
this.#trimTailTo(this.#spillThreshold);
|
|
1098
1475
|
this.#headRetentionDisabled = true;
|
|
1099
|
-
this.#totalBytes =
|
|
1100
|
-
this.#processedBytes =
|
|
1476
|
+
this.#totalBytes = replacementBytes;
|
|
1477
|
+
this.#processedBytes = replacementBytes;
|
|
1101
1478
|
this.#totalLines = countNewlines(text);
|
|
1102
1479
|
this.#processedLines = this.#totalLines;
|
|
1103
1480
|
this.#sawData = text.length > 0;
|
|
1104
|
-
this.#truncated =
|
|
1481
|
+
this.#truncated = replacementBytes > this.#headBytes + this.#bufferBytes;
|
|
1105
1482
|
this.#currentLineBytes = 0;
|
|
1106
1483
|
this.#columnEllipsisAdded = false;
|
|
1107
1484
|
this.#columnDroppedBytes = 0;
|
|
@@ -1114,18 +1491,35 @@ export class OutputSink {
|
|
|
1114
1491
|
const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
|
|
1115
1492
|
|
|
1116
1493
|
let artifactId: string | undefined;
|
|
1117
|
-
if (this.#
|
|
1118
|
-
|
|
1119
|
-
if (this.#
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1494
|
+
if (this.#finalized) {
|
|
1495
|
+
artifactId = this.#publishedArtifactId ?? this.#artifactId;
|
|
1496
|
+
} else if (!this.#artifactFinalizationFailed) {
|
|
1497
|
+
if (this.#artifactPublisher && !this.#artifactPath) {
|
|
1498
|
+
artifactId = await this.#publishTerminalArtifact();
|
|
1499
|
+
}
|
|
1500
|
+
if (!this.#artifactFinalizationFailed && this.#artifactPath) {
|
|
1501
|
+
if (this.#artifactTruncatedBytes > 0) this.#createFileSink();
|
|
1502
|
+
await this.#writeArtifactTruncationNotice();
|
|
1503
|
+
if (this.#file) {
|
|
1504
|
+
try {
|
|
1505
|
+
await this.#file.sink.end();
|
|
1506
|
+
artifactId = this.#file.artifactId;
|
|
1507
|
+
this.#finalized = true;
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
this.#recordArtifactFailure("end", error);
|
|
1510
|
+
this.#artifactFinalizationFailed = true;
|
|
1511
|
+
this.#file = undefined;
|
|
1512
|
+
this.#fileReady = false;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1123
1516
|
}
|
|
1124
1517
|
if (this.#finalized) {
|
|
1125
1518
|
// Terminal: the artifact is closed; replay state is no longer needed.
|
|
1126
1519
|
this.#pendingFileWrites = undefined;
|
|
1127
1520
|
this.#pendingFileWriteBytes = 0;
|
|
1128
1521
|
this.#fileReady = false;
|
|
1522
|
+
this.#pendingArtifactContent = undefined;
|
|
1129
1523
|
}
|
|
1130
1524
|
// Non-finalized dumps (no artifact sink ever opened) keep the raw replay
|
|
1131
1525
|
// queue so a later post-dump push that spills produces a CUMULATIVE
|
|
@@ -1151,24 +1545,18 @@ export class OutputSink {
|
|
|
1151
1545
|
let elidedLines: number | undefined;
|
|
1152
1546
|
|
|
1153
1547
|
if (headBytes > 0 && effectiveTotalBytes > headBytes + tailBytes) {
|
|
1154
|
-
// Middle was elided. Emit
|
|
1548
|
+
// Middle was elided. Emit both explicit windows, including when they are
|
|
1549
|
+
// fragments of one source line (the marker still carries byte evidence).
|
|
1155
1550
|
elidedBytes = Math.max(0, effectiveTotalBytes - headBytes - tailBytes);
|
|
1156
1551
|
elidedLines = Math.max(0, processedTotalLines - headLines - tailLines);
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
const headSep = headText.endsWith("\n") ? "" : "\n";
|
|
1166
|
-
const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
|
|
1167
|
-
body = `${headText}${headSep}${marker}${tailSep}${tailBuf}`;
|
|
1168
|
-
outputBytes = headBytes + markerBytes + tailBytes + headSep.length + tailSep.length;
|
|
1169
|
-
outputLines = headLines + 1 + tailLines;
|
|
1170
|
-
this.#truncated = true;
|
|
1171
|
-
}
|
|
1552
|
+
const marker = formatMiddleElisionMarker(elidedLines, elidedBytes);
|
|
1553
|
+
const markerBytes = Buffer.byteLength(marker, "utf-8");
|
|
1554
|
+
const headSep = headText.endsWith("\n") ? "" : "\n";
|
|
1555
|
+
const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
|
|
1556
|
+
body = `${headText}${headSep}${marker}${tailSep}${tailBuf}`;
|
|
1557
|
+
outputBytes = headBytes + markerBytes + tailBytes + headSep.length + tailSep.length;
|
|
1558
|
+
outputLines = headLines + 1 + tailLines;
|
|
1559
|
+
this.#truncated = true;
|
|
1172
1560
|
} else if (headBytes > 0) {
|
|
1173
1561
|
// Head + tail combine into the full buffered output (no overlap or elision).
|
|
1174
1562
|
body = `${headText}${tailBuf}`;
|
|
@@ -1193,6 +1581,7 @@ export class OutputSink {
|
|
|
1193
1581
|
columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
|
|
1194
1582
|
columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
|
|
1195
1583
|
artifactTruncatedBytes: this.#artifactTruncatedBytes > 0 ? this.#artifactTruncatedBytes : undefined,
|
|
1584
|
+
artifactFailureDiagnostic: this.#artifactFailureDiagnostic,
|
|
1196
1585
|
artifactId,
|
|
1197
1586
|
};
|
|
1198
1587
|
}
|