@gajae-code/coding-agent 0.12.0 → 0.12.2
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 +111 -0
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/config/settings-schema.d.ts +56 -2
- 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 +16 -2
- 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/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
- package/dist/types/modes/acp/acp-agent.d.ts +2 -19
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- 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 +5 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
- 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 +12 -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/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 +40 -2
- 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 +30 -4
- package/dist/types/session/session-manager.d.ts +23 -6
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +36 -7
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -0
- package/dist/types/skill-state/workflow-hud.d.ts +6 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +7 -1
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +9 -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/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +6 -0
- package/dist/types/tools/output-meta.d.ts +23 -2
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read.d.ts +0 -6
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- 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 +10 -8
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +290 -0
- package/src/async/job-manager.ts +99 -39
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/commands/team.ts +19 -1
- package/src/config/file-lock.ts +11 -2
- package/src/config/settings-schema.ts +53 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
- package/src/defaults/gjc/skills/team/SKILL.md +2 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- 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 +18 -1
- 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/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +491 -27
- package/src/gjc-runtime/team-launch.ts +8 -0
- package/src/gjc-runtime/team-runtime.ts +140 -32
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
- package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
- package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
- package/src/gjc-runtime/workflow-manifest.ts +21 -70
- package/src/internal-urls/docs-index.generated.ts +16 -16
- package/src/modes/DESIGN.md +104 -0
- package/src/modes/acp/acp-agent.ts +298 -186
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +68 -30
- package/src/modes/components/session-observer-overlay.ts +305 -86
- package/src/modes/components/settings-selector.ts +56 -5
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-status-header.ts +3 -15
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +42 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +193 -34
- package/src/modes/controllers/selector-controller.ts +160 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/runtime-init.ts +3 -0
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -0
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -4
- package/src/prompts/tools/bash.md +5 -5
- 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/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +18 -13
- 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 +7 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/index.ts +661 -126
- package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/reconciliation-store.ts +95 -16
- 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 +12 -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/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +89 -9
- package/src/session/agent-session.ts +844 -173
- 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 +200 -111
- package/src/session/internal/managed-session-storage.ts +492 -62
- package/src/session/session-manager.ts +1614 -435
- package/src/session/session-storage.ts +38 -45
- package/src/session/streaming-output.ts +238 -68
- package/src/setup/credential-import.ts +75 -13
- package/src/skill-state/active-state.ts +32 -0
- package/src/skill-state/workflow-hud.ts +17 -0
- package/src/skill-state/workflow-mutation-guard.ts +496 -2
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +280 -45
- package/src/task/output-manager.ts +24 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +34 -8
- 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 +86 -55
- 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/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +6 -0
- package/src/tools/output-meta.ts +108 -14
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +18 -0
- 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/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/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/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -106,6 +106,8 @@ export interface SessionStorageWriterOpenOptions {
|
|
|
106
106
|
export interface ManagedSessionSecurityContext {
|
|
107
107
|
readonly kind: "managed";
|
|
108
108
|
readonly agentDir: string;
|
|
109
|
+
/** Logical profile root for process-local caches; distinct from managed pathname authority. */
|
|
110
|
+
readonly profileAgentDir: string;
|
|
109
111
|
readonly sessionsRoot: string;
|
|
110
112
|
readonly sessionDir: string;
|
|
111
113
|
readonly rootAuthority: ManagedDirectoryRoot;
|
|
@@ -117,12 +119,18 @@ const managedSecurityContexts = new WeakSet<ManagedSessionSecurityContext>();
|
|
|
117
119
|
/** @internal Create the only accepted managed writer authority object. */
|
|
118
120
|
export function createManagedSessionSecurityContext(input: {
|
|
119
121
|
agentDir: string;
|
|
122
|
+
/** Optional for compatibility with existing authority-only callers. */
|
|
123
|
+
profileAgentDir?: string;
|
|
120
124
|
sessionsRoot: string;
|
|
121
125
|
sessionDir: string;
|
|
122
126
|
rootAuthority: ManagedDirectoryRoot;
|
|
123
127
|
retainedAuthority?: native.RecoveryFsRoot;
|
|
124
128
|
}): ManagedSessionSecurityContext {
|
|
125
|
-
const context = Object.freeze({
|
|
129
|
+
const context = Object.freeze({
|
|
130
|
+
kind: "managed" as const,
|
|
131
|
+
...input,
|
|
132
|
+
profileAgentDir: input.profileAgentDir ?? input.agentDir,
|
|
133
|
+
});
|
|
126
134
|
managedSecurityContexts.add(context);
|
|
127
135
|
return context;
|
|
128
136
|
}
|
|
@@ -446,7 +454,7 @@ function secureOwnerOnlyFileDescriptor(
|
|
|
446
454
|
): void {
|
|
447
455
|
if (securityContext && !isValidManagedSecurityContext(securityContext))
|
|
448
456
|
throw new Error("Invalid managed session security context");
|
|
449
|
-
if (process.platform !== "linux"
|
|
457
|
+
if (process.platform !== "linux" && process.platform !== "win32") {
|
|
450
458
|
if (operation === "apply") {
|
|
451
459
|
const applied = validateNativeSecurityResult(
|
|
452
460
|
native.applyOwnerOnlyPathSecurity(pathname, "file"),
|
|
@@ -463,7 +471,7 @@ function secureOwnerOnlyFileDescriptor(
|
|
|
463
471
|
if (!verified.ok) throw new Error(`Owner-only security rejected ${pathname}: ${verified.code}`);
|
|
464
472
|
return;
|
|
465
473
|
}
|
|
466
|
-
if (!pathIsWithin(securityContext.sessionDir, pathname))
|
|
474
|
+
if (securityContext && !pathIsWithin(securityContext.sessionDir, pathname))
|
|
467
475
|
throw new Error(`Managed writer escaped its session directory: ${pathname}`);
|
|
468
476
|
const result = validateNativeSecurityResult(
|
|
469
477
|
operation === "apply"
|
|
@@ -900,6 +908,28 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
900
908
|
}
|
|
901
909
|
|
|
902
910
|
const parentIdentity = this.#directoryIdentity(path.dirname(transcriptPath));
|
|
911
|
+
const durablyRecordArtifactPhase = (): void => {
|
|
912
|
+
if (process.platform === "win32") return;
|
|
913
|
+
let descriptor: number | undefined;
|
|
914
|
+
try {
|
|
915
|
+
descriptor = fs.openSync(
|
|
916
|
+
path.dirname(transcriptPath),
|
|
917
|
+
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
918
|
+
);
|
|
919
|
+
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
920
|
+
if (
|
|
921
|
+
!durableParent.isDirectory() ||
|
|
922
|
+
durableParent.dev !== parentIdentity.dev ||
|
|
923
|
+
durableParent.ino !== parentIdentity.ino
|
|
924
|
+
)
|
|
925
|
+
throw new Error("parent_changed");
|
|
926
|
+
fs.fsyncSync(descriptor);
|
|
927
|
+
} catch (error) {
|
|
928
|
+
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
929
|
+
} finally {
|
|
930
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
931
|
+
}
|
|
932
|
+
};
|
|
903
933
|
if (detachedArtifactsPath) {
|
|
904
934
|
if (
|
|
905
935
|
!expectedArtifactsIdentity ||
|
|
@@ -930,6 +960,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
930
960
|
"artifacts",
|
|
931
961
|
"Native artifact removal returned an unauthorized root",
|
|
932
962
|
);
|
|
963
|
+
durablyRecordArtifactPhase();
|
|
933
964
|
return {
|
|
934
965
|
kind: "cleanup_pending",
|
|
935
966
|
phase: "artifacts",
|
|
@@ -970,6 +1001,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
970
1001
|
"artifacts",
|
|
971
1002
|
"Authorized artifact removal root remains after restart",
|
|
972
1003
|
);
|
|
1004
|
+
durablyRecordArtifactPhase();
|
|
973
1005
|
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
974
1006
|
}
|
|
975
1007
|
|
|
@@ -1014,27 +1046,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1014
1046
|
`Exact artifact detach rejected: ${detach.ok ? "missing_path" : detach.code}`,
|
|
1015
1047
|
);
|
|
1016
1048
|
}
|
|
1017
|
-
if (!detach.ok
|
|
1018
|
-
let descriptor: number | undefined;
|
|
1019
|
-
try {
|
|
1020
|
-
descriptor = fs.openSync(
|
|
1021
|
-
path.dirname(transcriptPath),
|
|
1022
|
-
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
1023
|
-
);
|
|
1024
|
-
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
1025
|
-
if (
|
|
1026
|
-
!durableParent.isDirectory() ||
|
|
1027
|
-
durableParent.dev !== parentIdentity.dev ||
|
|
1028
|
-
durableParent.ino !== parentIdentity.ino
|
|
1029
|
-
)
|
|
1030
|
-
throw new Error("parent_changed");
|
|
1031
|
-
fs.fsyncSync(descriptor);
|
|
1032
|
-
} catch (error) {
|
|
1033
|
-
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
1034
|
-
} finally {
|
|
1035
|
-
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1049
|
+
if (!detach.ok) durablyRecordArtifactPhase();
|
|
1038
1050
|
if (!detach.ok) {
|
|
1039
1051
|
return {
|
|
1040
1052
|
kind: "cleanup_pending",
|
|
@@ -1058,6 +1070,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1058
1070
|
"artifacts",
|
|
1059
1071
|
"Native artifact removal returned an unauthorized root",
|
|
1060
1072
|
);
|
|
1073
|
+
durablyRecordArtifactPhase();
|
|
1061
1074
|
return {
|
|
1062
1075
|
kind: "cleanup_pending",
|
|
1063
1076
|
phase: "artifacts",
|
|
@@ -1082,27 +1095,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1082
1095
|
}
|
|
1083
1096
|
}
|
|
1084
1097
|
if (!artifactsRemoved) {
|
|
1085
|
-
|
|
1086
|
-
let descriptor: number | undefined;
|
|
1087
|
-
try {
|
|
1088
|
-
descriptor = fs.openSync(
|
|
1089
|
-
path.dirname(transcriptPath),
|
|
1090
|
-
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
1091
|
-
);
|
|
1092
|
-
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
1093
|
-
if (
|
|
1094
|
-
!durableParent.isDirectory() ||
|
|
1095
|
-
durableParent.dev !== parentIdentity.dev ||
|
|
1096
|
-
durableParent.ino !== parentIdentity.ino
|
|
1097
|
-
)
|
|
1098
|
-
throw new Error("parent_changed");
|
|
1099
|
-
fs.fsyncSync(descriptor);
|
|
1100
|
-
} catch (error) {
|
|
1101
|
-
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
1102
|
-
} finally {
|
|
1103
|
-
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1098
|
+
durablyRecordArtifactPhase();
|
|
1106
1099
|
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
1107
1100
|
}
|
|
1108
1101
|
if (hasDetachedTranscript) {
|
|
@@ -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
|
/**
|
|
@@ -913,6 +942,9 @@ export class OutputSink {
|
|
|
913
942
|
#artifactBytes = 0;
|
|
914
943
|
#artifactTruncatedBytes = 0;
|
|
915
944
|
#artifactTruncationNoticeWritten = false;
|
|
945
|
+
#artifactFailureDiagnostic?: string;
|
|
946
|
+
#publishedArtifactId?: string;
|
|
947
|
+
#artifactFinalizationFailed = false;
|
|
916
948
|
|
|
917
949
|
// Per-line column cap streaming state (persists across `push` calls so a
|
|
918
950
|
// long line split across chunks still trips the same trigger).
|
|
@@ -927,9 +959,13 @@ export class OutputSink {
|
|
|
927
959
|
};
|
|
928
960
|
|
|
929
961
|
// Raw prefix chunks not yet confirmed written to the file sink. This queue is
|
|
930
|
-
// the
|
|
962
|
+
// the pathname-backed artifact replay source; retained head/tail windows are lossy views.
|
|
931
963
|
#pendingFileWrites?: string[];
|
|
932
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;
|
|
933
969
|
#finalized = false;
|
|
934
970
|
|
|
935
971
|
#fileReady = false;
|
|
@@ -943,6 +979,7 @@ export class OutputSink {
|
|
|
943
979
|
readonly #chunkThrottleMs: number;
|
|
944
980
|
readonly #maxColumns: number;
|
|
945
981
|
readonly #artifactMaxBytes: number;
|
|
982
|
+
readonly #artifactPublisher?: TerminalArtifactPublisher;
|
|
946
983
|
readonly #coalesceSanitize: boolean;
|
|
947
984
|
#coalesceBuf = "";
|
|
948
985
|
|
|
@@ -957,6 +994,7 @@ export class OutputSink {
|
|
|
957
994
|
chunkThrottleMs = 0,
|
|
958
995
|
onRawChunk,
|
|
959
996
|
artifactMaxBytes = DEFAULT_ARTIFACT_MAX_BYTES,
|
|
997
|
+
artifactPublisher,
|
|
960
998
|
coalesceSanitize = process.env.PI_OUTPUT_SANITIZE_COALESCE === "1",
|
|
961
999
|
} = options ?? {};
|
|
962
1000
|
// Managed callers omit artifactPath at the allocation boundary; explicit callers
|
|
@@ -970,6 +1008,7 @@ export class OutputSink {
|
|
|
970
1008
|
this.#onRawChunk = onRawChunk;
|
|
971
1009
|
this.#chunkThrottleMs = chunkThrottleMs;
|
|
972
1010
|
this.#artifactMaxBytes = Math.max(0, artifactMaxBytes);
|
|
1011
|
+
this.#artifactPublisher = artifactPublisher;
|
|
973
1012
|
this.#coalesceSanitize = coalesceSanitize;
|
|
974
1013
|
}
|
|
975
1014
|
|
|
@@ -1060,11 +1099,14 @@ export class OutputSink {
|
|
|
1060
1099
|
// open, keep an independent raw replay prefix because retained head/tail
|
|
1061
1100
|
// windows are trimmed and cannot reconstruct byte-correct artifacts.
|
|
1062
1101
|
if (this.#artifactPath && this.#maxColumns === 0) this.#enqueueFileWrite(rawChunk, rawBytes);
|
|
1102
|
+
else if (this.#artifactPublisher && this.#maxColumns === 0)
|
|
1103
|
+
this.#enqueueTerminalArtifactChunk(rawChunk, rawBytes);
|
|
1063
1104
|
|
|
1064
1105
|
if (rawBytes === 0) return;
|
|
1065
1106
|
|
|
1066
1107
|
const visibleChunk = this.#maxColumns > 0 ? this.#applyColumnCap(sanitizedChunk) : sanitizedChunk;
|
|
1067
1108
|
if (this.#artifactPath && this.#maxColumns > 0) this.#enqueueFileWrite(rawChunk, rawBytes);
|
|
1109
|
+
else if (this.#artifactPublisher && this.#maxColumns > 0) this.#enqueueTerminalArtifactChunk(rawChunk, rawBytes);
|
|
1068
1110
|
if (this.#columnDroppedBytes > 0) this.#createFileSink();
|
|
1069
1111
|
const visibleBytes = Buffer.byteLength(visibleChunk, "utf-8");
|
|
1070
1112
|
if (visibleChunk.length > 0) {
|
|
@@ -1186,6 +1228,45 @@ export class OutputSink {
|
|
|
1186
1228
|
return `\n[artifact truncated after ${this.#artifactBytes} bytes; omitted at least ${droppedBytes} bytes]\n`;
|
|
1187
1229
|
}
|
|
1188
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
|
+
|
|
1189
1270
|
#capArtifactChunk(chunk: string, bytes: number): { chunk: string; bytes: number } | null {
|
|
1190
1271
|
if (bytes === 0) return null;
|
|
1191
1272
|
if (this.#artifactMaxBytes <= 0 || this.#artifactBytes >= this.#artifactMaxBytes) {
|
|
@@ -1201,18 +1282,23 @@ export class OutputSink {
|
|
|
1201
1282
|
return kept.bytes > 0 ? { chunk: kept.text, bytes: kept.bytes } : null;
|
|
1202
1283
|
}
|
|
1203
1284
|
|
|
1204
|
-
#writeArtifactTruncationNotice(): void {
|
|
1205
|
-
if (
|
|
1285
|
+
async #writeArtifactTruncationNotice(): Promise<void> {
|
|
1286
|
+
if (
|
|
1287
|
+
this.#artifactFinalizationFailed ||
|
|
1288
|
+
this.#artifactTruncatedBytes <= 0 ||
|
|
1289
|
+
this.#artifactTruncationNoticeWritten
|
|
1290
|
+
)
|
|
1291
|
+
return;
|
|
1206
1292
|
const notice = this.#artifactTruncationNotice(this.#artifactTruncatedBytes);
|
|
1207
1293
|
try {
|
|
1208
1294
|
if (this.#fileReady && this.#file) {
|
|
1209
|
-
this.#file.sink.write(notice);
|
|
1295
|
+
await this.#file.sink.write(notice);
|
|
1210
1296
|
} else {
|
|
1211
1297
|
this.#queuePendingFileWrite(notice, Buffer.byteLength(notice, "utf-8"));
|
|
1212
1298
|
}
|
|
1213
1299
|
this.#artifactTruncationNoticeWritten = true;
|
|
1214
|
-
} catch {
|
|
1215
|
-
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
this.#failArtifact("write", error, this.#file?.sink);
|
|
1216
1302
|
}
|
|
1217
1303
|
}
|
|
1218
1304
|
|
|
@@ -1224,6 +1310,7 @@ export class OutputSink {
|
|
|
1224
1310
|
}
|
|
1225
1311
|
|
|
1226
1312
|
#enqueueFileWrite(chunk: string, bytes: number): void {
|
|
1313
|
+
if (this.#artifactFinalizationFailed || this.#finalized) return;
|
|
1227
1314
|
const capped = this.#capArtifactChunk(chunk, bytes);
|
|
1228
1315
|
if (!capped) return;
|
|
1229
1316
|
this.#artifactBytes += capped.bytes;
|
|
@@ -1235,51 +1322,113 @@ export class OutputSink {
|
|
|
1235
1322
|
|
|
1236
1323
|
try {
|
|
1237
1324
|
this.#file.sink.write(capped.chunk);
|
|
1238
|
-
} catch {
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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;
|
|
1248
1342
|
}
|
|
1249
1343
|
}
|
|
1250
1344
|
|
|
1251
1345
|
#createFileSink(): boolean {
|
|
1252
|
-
if (this.#finalized) return false;
|
|
1346
|
+
if (this.#finalized || this.#artifactFinalizationFailed) return false;
|
|
1253
1347
|
|
|
1254
1348
|
if (!this.#artifactPath) return false;
|
|
1255
1349
|
if (this.#fileReady) return this.#file != null;
|
|
1350
|
+
|
|
1351
|
+
let sink: Bun.FileSink;
|
|
1256
1352
|
try {
|
|
1257
|
-
|
|
1258
|
-
|
|
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 };
|
|
1259
1359
|
|
|
1360
|
+
try {
|
|
1260
1361
|
const pending = this.#pendingFileWrites;
|
|
1261
1362
|
if (pending) {
|
|
1262
1363
|
for (const chunk of pending) sink.write(chunk);
|
|
1263
1364
|
}
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
this.#failArtifact("write", error, sink);
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1264
1369
|
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
+
}
|
|
1268
1383
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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;
|
|
1275
1404
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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;
|
|
1283
1432
|
}
|
|
1284
1433
|
}
|
|
1285
1434
|
|
|
@@ -1302,25 +1451,34 @@ export class OutputSink {
|
|
|
1302
1451
|
* minimizer rewrites the captured output after the raw bytes have already
|
|
1303
1452
|
* been streamed.
|
|
1304
1453
|
*
|
|
1305
|
-
* After this call the
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
* — which would otherwise reorder content and trip the middle-elision
|
|
1310
|
-
* 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.
|
|
1311
1458
|
*/
|
|
1312
1459
|
replace(text: string): void {
|
|
1313
1460
|
this.#coalesceBuf = "";
|
|
1314
|
-
|
|
1461
|
+
const replacementBytes = Buffer.byteLength(text, "utf-8");
|
|
1315
1462
|
this.#head = "";
|
|
1316
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);
|
|
1317
1475
|
this.#headRetentionDisabled = true;
|
|
1318
|
-
this.#totalBytes =
|
|
1319
|
-
this.#processedBytes =
|
|
1476
|
+
this.#totalBytes = replacementBytes;
|
|
1477
|
+
this.#processedBytes = replacementBytes;
|
|
1320
1478
|
this.#totalLines = countNewlines(text);
|
|
1321
1479
|
this.#processedLines = this.#totalLines;
|
|
1322
1480
|
this.#sawData = text.length > 0;
|
|
1323
|
-
this.#truncated =
|
|
1481
|
+
this.#truncated = replacementBytes > this.#headBytes + this.#bufferBytes;
|
|
1324
1482
|
this.#currentLineBytes = 0;
|
|
1325
1483
|
this.#columnEllipsisAdded = false;
|
|
1326
1484
|
this.#columnDroppedBytes = 0;
|
|
@@ -1333,18 +1491,35 @@ export class OutputSink {
|
|
|
1333
1491
|
const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
|
|
1334
1492
|
|
|
1335
1493
|
let artifactId: string | undefined;
|
|
1336
|
-
if (this.#
|
|
1337
|
-
|
|
1338
|
-
if (this.#
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
+
}
|
|
1342
1516
|
}
|
|
1343
1517
|
if (this.#finalized) {
|
|
1344
1518
|
// Terminal: the artifact is closed; replay state is no longer needed.
|
|
1345
1519
|
this.#pendingFileWrites = undefined;
|
|
1346
1520
|
this.#pendingFileWriteBytes = 0;
|
|
1347
1521
|
this.#fileReady = false;
|
|
1522
|
+
this.#pendingArtifactContent = undefined;
|
|
1348
1523
|
}
|
|
1349
1524
|
// Non-finalized dumps (no artifact sink ever opened) keep the raw replay
|
|
1350
1525
|
// queue so a later post-dump push that spills produces a CUMULATIVE
|
|
@@ -1370,24 +1545,18 @@ export class OutputSink {
|
|
|
1370
1545
|
let elidedLines: number | undefined;
|
|
1371
1546
|
|
|
1372
1547
|
if (headBytes > 0 && effectiveTotalBytes > headBytes + tailBytes) {
|
|
1373
|
-
// 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).
|
|
1374
1550
|
elidedBytes = Math.max(0, effectiveTotalBytes - headBytes - tailBytes);
|
|
1375
1551
|
elidedLines = Math.max(0, processedTotalLines - headLines - tailLines);
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
const headSep = headText.endsWith("\n") ? "" : "\n";
|
|
1385
|
-
const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
|
|
1386
|
-
body = `${headText}${headSep}${marker}${tailSep}${tailBuf}`;
|
|
1387
|
-
outputBytes = headBytes + markerBytes + tailBytes + headSep.length + tailSep.length;
|
|
1388
|
-
outputLines = headLines + 1 + tailLines;
|
|
1389
|
-
this.#truncated = true;
|
|
1390
|
-
}
|
|
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;
|
|
1391
1560
|
} else if (headBytes > 0) {
|
|
1392
1561
|
// Head + tail combine into the full buffered output (no overlap or elision).
|
|
1393
1562
|
body = `${headText}${tailBuf}`;
|
|
@@ -1412,6 +1581,7 @@ export class OutputSink {
|
|
|
1412
1581
|
columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
|
|
1413
1582
|
columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
|
|
1414
1583
|
artifactTruncatedBytes: this.#artifactTruncatedBytes > 0 ? this.#artifactTruncatedBytes : undefined,
|
|
1584
|
+
artifactFailureDiagnostic: this.#artifactFailureDiagnostic,
|
|
1415
1585
|
artifactId,
|
|
1416
1586
|
};
|
|
1417
1587
|
}
|