pi-usereq 0.80.0 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -1
- package/package.json +1 -1
- package/src/core/debug-runtime.ts +273 -20
- package/src/index.ts +41 -4
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PI-useReq/pi-usereq (0.
|
|
1
|
+
# PI-useReq/pi-usereq (0.81.0)
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
4
|
<img src="https://img.shields.io/badge/python-3.11%2B-3776AB?style=flat-square&logo=python&logoColor=white" alt="Python 3.11+">
|
|
@@ -335,6 +335,26 @@ The extension renders a status field in the pi status line showing: extension id
|
|
|
335
335
|
pi -p "Rewrite this function" --append-system-prompt /tmp/PI-useReq/20261009072235123-req-implement
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
+
- **Provider error logging** — when a dispatched `req-*` run receives a provider response with status >= 400, pi-usereq writes the exact failing request payload into `<timestamp>-<command>-error-<status>` (raw provider-request JSON), the provider error message text into `<timestamp>-<command>-error-<status>-response`, and replay metadata into `<timestamp>-<command>-error-<status>-replay.json`, all beside the run's `-request` and `-prompt` files. The `-replay.json` sidecar carries the provider, base URL, model id, thinking level, context window, max output tokens, the retained execution-session file, and ready-to-use replay commands. You can re-generate the failure for immediate pi CLI debugging:
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
# Re-send the exact failing provider request (the payload file is raw request JSON)
|
|
342
|
+
curl -sS "$(jq -r '.replayCommands.provider' /tmp/PI-useReq/20261012070707447-req-analyze-error-400-replay.json)"
|
|
343
|
+
|
|
344
|
+
# Or replay it directly against the captured body
|
|
345
|
+
curl -sS https://openrouter.ai/api/v1/chat/completions \
|
|
346
|
+
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
|
|
347
|
+
-H "Content-Type: application/json" \
|
|
348
|
+
--data @/tmp/PI-useReq/20261012070707447-req-analyze-error-400
|
|
349
|
+
|
|
350
|
+
# Replay the saved prompt through pi with the same model and thinking level
|
|
351
|
+
pi --no-session --model '~deepseek/deepseek-flash-latest' --thinking high \
|
|
352
|
+
-p "$(cat /tmp/PI-useReq/20261012070647580-req-analyze-prompt)"
|
|
353
|
+
|
|
354
|
+
# Fork the retained execution session (kept on failed runs) and continue it
|
|
355
|
+
pi --fork /home/ogekuri/.pi/agent/sessions/--home-ogekuri-Git-PI-useReq--/2026-10-12T05-06-46-788Z_01a12e81-9584-72f4-92e3-19008d04cccf.jsonl
|
|
356
|
+
```
|
|
357
|
+
|
|
338
358
|
|
|
339
359
|
## Note on Git usage
|
|
340
360
|
|
package/package.json
CHANGED
|
@@ -831,45 +831,86 @@ export function logDebugPromptContent(
|
|
|
831
831
|
promptName: PromptCommandName,
|
|
832
832
|
content: string,
|
|
833
833
|
): boolean {
|
|
834
|
-
|
|
834
|
+
const fileName = formatPromptDebugPromptFileName(promptName);
|
|
835
|
+
const written = writePromptDebugFile(
|
|
835
836
|
projectBase,
|
|
836
837
|
config,
|
|
837
838
|
workflowState,
|
|
838
839
|
promptName,
|
|
839
|
-
|
|
840
|
+
fileName,
|
|
840
841
|
content,
|
|
841
842
|
);
|
|
843
|
+
if (written) {
|
|
844
|
+
getProcessScopedPromptErrorCaptureStore().dispatchedPromptDebugFiles.set(promptName, fileName);
|
|
845
|
+
}
|
|
846
|
+
return written;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* @brief Describes replay facts captured beside one provider request payload for reproduction.
|
|
851
|
+
* @details Stores the provider, API, base URL, model identifier, thinking level, context-window, max-output-token, and execution-session-file facts resolved from the live pi runtime model plus prompt execution plan so the error-stage replay sidecar can emit ready-to-use replay command lines without live runtime state. Every field stays optional so degraded runtimes and offline replays never abort the capture. The interface is compile-time only and introduces no runtime cost.
|
|
852
|
+
* @satisfies REQ-430
|
|
853
|
+
*/
|
|
854
|
+
export interface PromptErrorReplayFacts {
|
|
855
|
+
modelId: string | undefined;
|
|
856
|
+
provider: string | undefined;
|
|
857
|
+
api: string | undefined;
|
|
858
|
+
baseUrl: string | undefined;
|
|
859
|
+
thinkingLevel: string | undefined;
|
|
860
|
+
contextWindow: number | undefined;
|
|
861
|
+
maxTokens: number | undefined;
|
|
862
|
+
executionSessionFile: string | undefined;
|
|
842
863
|
}
|
|
843
864
|
|
|
844
865
|
/**
|
|
845
866
|
* @brief Describes one process-scoped captured provider request payload bound to an active prompt run.
|
|
846
|
-
* @details Pairs the bundled prompt name, the serialization of the exact `before_provider_request` payload, the original project base,
|
|
847
|
-
* @satisfies REQ-421
|
|
867
|
+
* @details Pairs the bundled prompt name, the serialization of the exact `before_provider_request` payload, the original project base, the resolved prompt log directory, the dispatched prompt debug filename of the same run, and the captured replay facts so a later `after_provider_response` or `message_end` error status can dump the failing request verbatim, its provider error text, and one replay sidecar without live controller state, cached configuration, or live provider objects. The interface is compile-time only and introduces no runtime cost.
|
|
868
|
+
* @satisfies REQ-421, REQ-429, REQ-430
|
|
848
869
|
*/
|
|
849
870
|
interface CapturedPromptErrorPayload {
|
|
850
871
|
promptName: PromptCommandName;
|
|
851
872
|
payloadText: string;
|
|
852
873
|
projectBase: string;
|
|
853
874
|
logDirectory: string;
|
|
875
|
+
promptDebugFile: string | undefined;
|
|
876
|
+
replayFacts: PromptErrorReplayFacts;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* @brief Describes the location of one already flushed provider error payload file set.
|
|
881
|
+
* @details Stores the prompt name, failing status, resolved log directory, and payload base filename of the most recent error-stage flush so a later finalized `message_end` error message can still append the `-response` file beside the already written payload when the provider fired `after_provider_response` before surfacing its error text. The interface is compile-time only and introduces no runtime cost.
|
|
882
|
+
* @satisfies REQ-429
|
|
883
|
+
*/
|
|
884
|
+
interface FlushedPromptErrorRecord {
|
|
885
|
+
promptName: PromptCommandName;
|
|
886
|
+
errorCode: number;
|
|
887
|
+
logDirectory: string;
|
|
888
|
+
baseFileName: string;
|
|
854
889
|
}
|
|
855
890
|
|
|
856
891
|
/**
|
|
857
892
|
* @brief Describes the process-scoped provider error payload capture state persisted across extension rebinds.
|
|
858
|
-
* @details Stores the latest captured prompt error payload on `globalThis` because pi rebinds extension modules for `/new`, `/resume`, `/fork`, and `/reload` between prompt dispatch and provider execution, while the hosting process persists across those operations, so the capture
|
|
893
|
+
* @details Stores the latest captured prompt error payload, the latest flushed error record, and the latest dispatched prompt debug filenames on `globalThis` because pi rebinds extension modules for `/new`, `/resume`, `/fork`, and `/reload` between prompt dispatch and provider execution, while the hosting process persists across those operations, so the capture, the late response write, and the dispatched-prompt correlation survive the forked execution-session switch. The interface is compile-time only and introduces no runtime cost.
|
|
859
894
|
*/
|
|
860
895
|
interface ProcessScopedPromptErrorCaptureStore {
|
|
861
896
|
capturedPromptErrorPayload: CapturedPromptErrorPayload | undefined;
|
|
897
|
+
lastFlushedPromptError: FlushedPromptErrorRecord | undefined;
|
|
898
|
+
dispatchedPromptDebugFiles: Map<PromptCommandName, string>;
|
|
862
899
|
}
|
|
863
900
|
|
|
864
901
|
/**
|
|
865
902
|
* @brief Returns the process-scoped provider error payload capture store.
|
|
866
|
-
* @details Lazily initializes one `globalThis` record so session rebinds never discard an already captured failing provider request. Runtime is O(1). Side effect: initializes process-scoped state on first access.
|
|
903
|
+
* @details Lazily initializes one `globalThis` record so session rebinds never discard an already captured failing provider request, its flushed error record, or its dispatched prompt debug filename correlation. Runtime is O(1). Side effect: initializes process-scoped state on first access.
|
|
867
904
|
* @return {ProcessScopedPromptErrorCaptureStore} Mutable process-scoped capture store.
|
|
868
905
|
*/
|
|
869
906
|
function getProcessScopedPromptErrorCaptureStore(): ProcessScopedPromptErrorCaptureStore {
|
|
870
907
|
const globalScope = globalThis as typeof globalThis & { __piUsereqPromptErrorCaptureStore?: ProcessScopedPromptErrorCaptureStore };
|
|
871
908
|
if (!globalScope.__piUsereqPromptErrorCaptureStore) {
|
|
872
|
-
globalScope.__piUsereqPromptErrorCaptureStore = {
|
|
909
|
+
globalScope.__piUsereqPromptErrorCaptureStore = {
|
|
910
|
+
capturedPromptErrorPayload: undefined,
|
|
911
|
+
lastFlushedPromptError: undefined,
|
|
912
|
+
dispatchedPromptDebugFiles: new Map(),
|
|
913
|
+
};
|
|
873
914
|
}
|
|
874
915
|
return globalScope.__piUsereqPromptErrorCaptureStore;
|
|
875
916
|
}
|
|
@@ -891,6 +932,40 @@ export function formatPromptDebugErrorFileName(
|
|
|
891
932
|
return `${formatPromptDebugTimestamp(date)}-${getDebugPromptName(promptName)}-error-${errorCode}`;
|
|
892
933
|
}
|
|
893
934
|
|
|
935
|
+
/**
|
|
936
|
+
* @brief Builds one provider error response debug filename for one bundled prompt.
|
|
937
|
+
* @details Appends the `-response` suffix to the shared `<timestamp>-<req-command>-error-<status>` shape so the persisted provider error text stays attributable to the exact failing request payload file written with the identical write-time timestamp. Runtime is O(1). No external state is mutated.
|
|
938
|
+
* @param[in] promptName {PromptCommandName} Bundled prompt name.
|
|
939
|
+
* @param[in] errorCode {number} Failing provider response status, such as `400`.
|
|
940
|
+
* @param[in] date {Date} Write-time timestamp shared with the payload file. Defaults to the current wall-clock time.
|
|
941
|
+
* @return {string} Provider error response filename in the `<timestamp>-<req-command>-error-<status>-response` shape.
|
|
942
|
+
* @satisfies REQ-423, REQ-429
|
|
943
|
+
*/
|
|
944
|
+
export function formatPromptDebugErrorResponseFileName(
|
|
945
|
+
promptName: PromptCommandName,
|
|
946
|
+
errorCode: number,
|
|
947
|
+
date = new Date(),
|
|
948
|
+
): string {
|
|
949
|
+
return `${formatPromptDebugErrorFileName(promptName, errorCode, date)}-response`;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* @brief Builds one provider error replay sidecar debug filename for one bundled prompt.
|
|
954
|
+
* @details Appends the `-replay.json` suffix to the shared `<timestamp>-<req-command>-error-<status>` shape so the persisted replay metadata sidecar stays attributable to the exact failing request payload file written with the identical write-time timestamp. Runtime is O(1). No external state is mutated.
|
|
955
|
+
* @param[in] promptName {PromptCommandName} Bundled prompt name.
|
|
956
|
+
* @param[in] errorCode {number} Failing provider response status, such as `400`.
|
|
957
|
+
* @param[in] date {Date} Write-time timestamp shared with the payload file. Defaults to the current wall-clock time.
|
|
958
|
+
* @return {string} Provider error replay sidecar filename in the `<timestamp>-<req-command>-error-<status>-replay.json` shape.
|
|
959
|
+
* @satisfies REQ-423, REQ-430
|
|
960
|
+
*/
|
|
961
|
+
export function formatPromptDebugErrorReplayFileName(
|
|
962
|
+
promptName: PromptCommandName,
|
|
963
|
+
errorCode: number,
|
|
964
|
+
date = new Date(),
|
|
965
|
+
): string {
|
|
966
|
+
return `${formatPromptDebugErrorFileName(promptName, errorCode, date)}-replay.json`;
|
|
967
|
+
}
|
|
968
|
+
|
|
894
969
|
/**
|
|
895
970
|
* @brief Resolves the failing HTTP status carried by one finalized agent message.
|
|
896
971
|
* @details Extracts the leading provider error status from one `message_end` assistant message whose `stopReason` equals `error`, accepting the pi `formatProviderError` display shapes (`"<status>: <body>"` and `"<prefix> (<status>): <body>"`) plus the raw SDK `error.message` shape (`"<status> <body>"`) surfaced unchanged by the agent run-failure path, so provider APIs whose SDK clients throw before emitting `after_provider_response` for non-2xx statuses (the OpenAI-compatible family, including `zai`) still flush the captured provider request payload through the message lifecycle channel. Runtime is O(n) in the inspected error-message prefix length. No external state is mutated.
|
|
@@ -946,54 +1021,230 @@ function serializePromptProviderPayload(payload: unknown): string {
|
|
|
946
1021
|
) ?? "";
|
|
947
1022
|
}
|
|
948
1023
|
|
|
1024
|
+
/**
|
|
1025
|
+
* @brief Resolves the model identifier embedded in one provider request payload.
|
|
1026
|
+
* @details Reads the string `model` field of the serialized request payload so the replay sidecar can still name the failing model when the live runtime model object was unavailable at capture time. Runtime is O(1). No external state is mutated.
|
|
1027
|
+
* @param[in] payload {unknown} Exact `before_provider_request` provider request payload.
|
|
1028
|
+
* @return {string | undefined} Payload model identifier when present as a string; otherwise `undefined`.
|
|
1029
|
+
*/
|
|
1030
|
+
function resolveProviderPayloadModelId(payload: unknown): string | undefined {
|
|
1031
|
+
if (typeof payload !== "object" || payload === null) {
|
|
1032
|
+
return undefined;
|
|
1033
|
+
}
|
|
1034
|
+
const modelId = (payload as { model?: unknown }).model;
|
|
1035
|
+
return typeof modelId === "string" && modelId.length > 0 ? modelId : undefined;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
949
1038
|
/**
|
|
950
1039
|
* @brief Captures one exact provider request payload for the active prompt run when prompt debug logging is enabled.
|
|
951
|
-
* @details Applies the global-debug plus prompt-debug gating, serializes the exact `before_provider_request` payload through the cycle-safe serializer, and stores the prompt name, payload text, original project base,
|
|
1040
|
+
* @details Applies the global-debug plus prompt-debug gating, serializes the exact `before_provider_request` payload through the cycle-safe serializer, resolves the dispatched prompt debug filename of the same run from the process-scoped correlation map, and stores the prompt name, payload text, original project base, resolved prompt log directory, replay facts, and prompt debug filename in the process-scoped capture store so the payload, its error text, and its replay sidecar survive session replacement and later error-stage writes require no live workflow state or cached configuration. Falls back to the payload `model` field when the supplied replay facts omit the model identifier. Runtime is O(n) in serialized payload size when enabled and O(1) otherwise. Side effect: mutates the process-scoped capture store for enabled matching requests.
|
|
952
1041
|
* @param[in] config {UseReqConfig} Effective project configuration.
|
|
953
1042
|
* @param[in] promptName {PromptCommandName} Active bundled prompt name.
|
|
954
1043
|
* @param[in] projectBase {string} Absolute original project base path used to resolve the prompt log directory.
|
|
955
1044
|
* @param[in] payload {unknown} Exact `before_provider_request` provider request payload.
|
|
1045
|
+
* @param[in] replayFacts {PromptErrorReplayFacts} Live runtime replay facts consumed by the error-stage sidecar. Defaults to empty facts.
|
|
956
1046
|
* @return {void} No return value.
|
|
957
|
-
* @satisfies REQ-416, REQ-421
|
|
1047
|
+
* @satisfies REQ-416, REQ-421, REQ-430
|
|
958
1048
|
*/
|
|
959
1049
|
export function capturePromptProviderRequestForDebug(
|
|
960
1050
|
config: UseReqConfig,
|
|
961
1051
|
promptName: PromptCommandName,
|
|
962
1052
|
projectBase: string,
|
|
963
1053
|
payload: unknown,
|
|
1054
|
+
replayFacts: PromptErrorReplayFacts = {
|
|
1055
|
+
modelId: undefined,
|
|
1056
|
+
provider: undefined,
|
|
1057
|
+
api: undefined,
|
|
1058
|
+
baseUrl: undefined,
|
|
1059
|
+
thinkingLevel: undefined,
|
|
1060
|
+
contextWindow: undefined,
|
|
1061
|
+
maxTokens: undefined,
|
|
1062
|
+
executionSessionFile: undefined,
|
|
1063
|
+
},
|
|
964
1064
|
): void {
|
|
965
1065
|
if (!shouldLogDebugPromptContent(config, "unknown", promptName)) {
|
|
966
1066
|
return;
|
|
967
1067
|
}
|
|
1068
|
+
const logDirectory = resolveDebugPromptsLogPath(projectBase, config);
|
|
1069
|
+
const dispatchedPromptFile = getProcessScopedPromptErrorCaptureStore().dispatchedPromptDebugFiles.get(promptName);
|
|
968
1070
|
getProcessScopedPromptErrorCaptureStore().capturedPromptErrorPayload = {
|
|
969
1071
|
promptName,
|
|
970
1072
|
payloadText: serializePromptProviderPayload(payload),
|
|
971
1073
|
projectBase,
|
|
972
|
-
logDirectory
|
|
1074
|
+
logDirectory,
|
|
1075
|
+
promptDebugFile: dispatchedPromptFile === undefined ? undefined : path.join(logDirectory, dispatchedPromptFile),
|
|
1076
|
+
replayFacts: {
|
|
1077
|
+
...replayFacts,
|
|
1078
|
+
modelId: replayFacts.modelId ?? resolveProviderPayloadModelId(payload),
|
|
1079
|
+
},
|
|
973
1080
|
};
|
|
974
1081
|
}
|
|
975
1082
|
|
|
976
1083
|
/**
|
|
977
|
-
* @brief
|
|
978
|
-
* @details
|
|
1084
|
+
* @brief Normalizes one provider name into an uppercase environment-variable token.
|
|
1085
|
+
* @details Uppercases the provider name and replaces every non-alphanumeric character with `_` so the generated replay command can reference a deterministic `<PROVIDER>_API_KEY` environment variable. Runtime is O(n) in provider-name length. No external state is mutated.
|
|
1086
|
+
* @param[in] provider {string} Provider identifier, such as `openrouter`.
|
|
1087
|
+
* @return {string} Uppercase alphanumeric environment-variable token.
|
|
1088
|
+
*/
|
|
1089
|
+
function normalizeProviderEnvToken(provider: string): string {
|
|
1090
|
+
const normalized = provider.toUpperCase().replace(/[^A-Z0-9]/gu, "_");
|
|
1091
|
+
return normalized.length > 0 ? normalized : "PROVIDER";
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* @brief Resolves the provider HTTP endpoint targeted by one captured provider request payload.
|
|
1096
|
+
* @details Appends the canonical chat-completions path for `openai-completions` APIs and the canonical messages path for `anthropic-messages` APIs, and preserves the raw base URL for every other API so the generated provider replay command never fabricates an endpoint it cannot derive. Runtime is O(p) in URL length. No external state is mutated.
|
|
1097
|
+
* @param[in] facts {PromptErrorReplayFacts} Captured replay facts supplying the API and base URL.
|
|
1098
|
+
* @return {string | undefined} Replay endpoint URL when a base URL exists; otherwise `undefined`.
|
|
1099
|
+
*/
|
|
1100
|
+
function resolveProviderReplayEndpoint(facts: PromptErrorReplayFacts): string | undefined {
|
|
1101
|
+
if (facts.baseUrl === undefined) {
|
|
1102
|
+
return undefined;
|
|
1103
|
+
}
|
|
1104
|
+
if (facts.api === "openai-completions") {
|
|
1105
|
+
return `${facts.baseUrl}/chat/completions`;
|
|
1106
|
+
}
|
|
1107
|
+
if (facts.api === "anthropic-messages") {
|
|
1108
|
+
return `${facts.baseUrl}/v1/messages`;
|
|
1109
|
+
}
|
|
1110
|
+
return facts.baseUrl;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* @brief Builds the replay sidecar JSON text for one flushed provider error payload.
|
|
1115
|
+
* @details Serializes the captured provider, base URL, API, model identifier, thinking level, context-window and max-output-token facts, the execution-session file, the dispatched prompt debug file, the flushed request payload file, and ready-to-use replay command lines: one `curl --data @` provider replay of the exact failing request, one pi one-shot replay of the dispatched prompt file, and one `pi --fork` replay of the retained execution session. Command lines are emitted only when their supporting facts exist so every emitted command is directly executable. Runtime is O(n) in serialized sidecar size. No external state is mutated.
|
|
1116
|
+
* @param[in] captured {CapturedPromptErrorPayload} Consumed capture supplying replay facts and artifact paths.
|
|
1117
|
+
* @param[in] requestFileName {string} Payload filename written with the same write-time timestamp.
|
|
1118
|
+
* @return {string} Newline-terminated replay sidecar JSON text.
|
|
1119
|
+
* @satisfies REQ-430
|
|
1120
|
+
*/
|
|
1121
|
+
function buildPromptErrorReplaySidecarText(
|
|
1122
|
+
captured: CapturedPromptErrorPayload,
|
|
1123
|
+
requestFileName: string,
|
|
1124
|
+
): string {
|
|
1125
|
+
const facts = captured.replayFacts;
|
|
1126
|
+
const requestFilePath = path.join(captured.logDirectory, requestFileName);
|
|
1127
|
+
const endpoint = resolveProviderReplayEndpoint(facts);
|
|
1128
|
+
const replayCommands: Record<string, string> = {};
|
|
1129
|
+
if (endpoint !== undefined) {
|
|
1130
|
+
const envToken = facts.provider === undefined ? "PROVIDER_API_KEY" : `${normalizeProviderEnvToken(facts.provider)}_API_KEY`;
|
|
1131
|
+
replayCommands.provider = `curl -sS '${endpoint}' -H 'Authorization: Bearer $${envToken}' -H 'Content-Type: application/json' --data @'${requestFilePath}'`;
|
|
1132
|
+
}
|
|
1133
|
+
if (captured.promptDebugFile !== undefined && facts.modelId !== undefined) {
|
|
1134
|
+
replayCommands.piOneShot = `pi --no-session --model '${facts.modelId}' --thinking ${facts.thinkingLevel ?? "off"} -p "$(cat '${captured.promptDebugFile}')"`;
|
|
1135
|
+
}
|
|
1136
|
+
if (facts.executionSessionFile !== undefined) {
|
|
1137
|
+
const modelFlags = facts.modelId === undefined ? "" : ` --model '${facts.modelId}'`;
|
|
1138
|
+
const thinkingFlags = facts.thinkingLevel === undefined ? "" : ` --thinking ${facts.thinkingLevel}`;
|
|
1139
|
+
replayCommands.piFork = `pi --fork '${facts.executionSessionFile}'${modelFlags}${thinkingFlags}`;
|
|
1140
|
+
}
|
|
1141
|
+
const sidecar = {
|
|
1142
|
+
provider: facts.provider ?? null,
|
|
1143
|
+
baseUrl: facts.baseUrl ?? null,
|
|
1144
|
+
api: facts.api ?? null,
|
|
1145
|
+
modelId: facts.modelId ?? null,
|
|
1146
|
+
thinkingLevel: facts.thinkingLevel ?? null,
|
|
1147
|
+
contextWindow: facts.contextWindow ?? null,
|
|
1148
|
+
maxTokens: facts.maxTokens ?? null,
|
|
1149
|
+
sessionFile: facts.executionSessionFile ?? null,
|
|
1150
|
+
promptFile: captured.promptDebugFile ?? null,
|
|
1151
|
+
requestFile: requestFilePath,
|
|
1152
|
+
replayCommands,
|
|
1153
|
+
};
|
|
1154
|
+
return `${JSON.stringify(sidecar, null, 2)}\n`;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* @brief Writes the captured provider request payload plus its response text and replay sidecar as error-stage prompt debug files.
|
|
1159
|
+
* @details Consumes the process-scoped capture, discards it after every write attempt so stale payloads never reach later runs, rejects failing statuses below 400, and writes the exact captured payload into one `<timestamp>-<req-command>-error-<status>` file that stays raw provider-request JSON usable with `curl --data @`, the provider error text into one identically timestamped `-response` file when supplied, and the replay sidecar into one identically timestamped `-replay.json` file inside the captured log directory beside the initial dispatched prompt artifacts. Records the flushed location so a later finalized `message_end` error message can still append the `-response` file when `after_provider_response` fired first without error text. All write inputs come from the capture itself, so the flush requires no live workflow state, cached configuration, or active controller prompt request. Runtime is dominated by one directory creation plus at most three file writes when a capture exists and O(1) otherwise. Side effects include directory creation and file creation only when a capture exists and the status is an error status.
|
|
979
1160
|
* @param[in] errorCode {number} Failing provider response status, such as `400`.
|
|
980
|
-
* @
|
|
981
|
-
* @
|
|
1161
|
+
* @param[in] errorResponseText {string | undefined} Finalized provider error message text when already available; the `-response` file is written later when omitted.
|
|
1162
|
+
* @return {boolean} `true` when the provider error payload file set is written; otherwise `false`.
|
|
1163
|
+
* @satisfies REQ-422, REQ-423, REQ-424, REQ-425, REQ-429, REQ-430, REQ-431
|
|
982
1164
|
*/
|
|
983
|
-
export function flushCapturedPromptErrorPayload(errorCode: number): boolean {
|
|
1165
|
+
export function flushCapturedPromptErrorPayload(errorCode: number, errorResponseText?: string): boolean {
|
|
984
1166
|
const captureStore = getProcessScopedPromptErrorCaptureStore();
|
|
985
1167
|
const captured = captureStore.capturedPromptErrorPayload;
|
|
986
1168
|
captureStore.capturedPromptErrorPayload = undefined;
|
|
987
1169
|
if (!captured || !Number.isFinite(errorCode) || errorCode < 400) {
|
|
988
1170
|
return false;
|
|
989
1171
|
}
|
|
1172
|
+
const writeTime = new Date();
|
|
1173
|
+
const payloadFileName = formatPromptDebugErrorFileName(captured.promptName, errorCode, writeTime);
|
|
990
1174
|
try {
|
|
991
1175
|
fs.mkdirSync(captured.logDirectory, { recursive: true });
|
|
992
1176
|
fs.writeFileSync(
|
|
993
|
-
path.join(captured.logDirectory,
|
|
1177
|
+
path.join(captured.logDirectory, payloadFileName),
|
|
994
1178
|
captured.payloadText,
|
|
995
1179
|
"utf8",
|
|
996
1180
|
);
|
|
1181
|
+
if (typeof errorResponseText === "string" && errorResponseText.length > 0) {
|
|
1182
|
+
writePromptProviderErrorResponseFile(
|
|
1183
|
+
captured.logDirectory,
|
|
1184
|
+
formatPromptDebugErrorResponseFileName(captured.promptName, errorCode, writeTime),
|
|
1185
|
+
errorResponseText,
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
fs.writeFileSync(
|
|
1189
|
+
path.join(captured.logDirectory, formatPromptDebugErrorReplayFileName(captured.promptName, errorCode, writeTime)),
|
|
1190
|
+
buildPromptErrorReplaySidecarText(captured, payloadFileName),
|
|
1191
|
+
"utf8",
|
|
1192
|
+
);
|
|
1193
|
+
captureStore.lastFlushedPromptError = {
|
|
1194
|
+
promptName: captured.promptName,
|
|
1195
|
+
errorCode,
|
|
1196
|
+
logDirectory: captured.logDirectory,
|
|
1197
|
+
baseFileName: payloadFileName,
|
|
1198
|
+
};
|
|
1199
|
+
return true;
|
|
1200
|
+
} catch {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* @brief Writes one provider error response text file under a resolved log directory.
|
|
1207
|
+
* @details Creates the parent directory and writes the finalized provider error text with one trailing newline into the requested response filename so both the flush-time and the late `message_end` response writes share one deterministic serialization. Runtime is dominated by one directory creation plus one file write. Side effects include directory creation and file creation.
|
|
1208
|
+
* @param[in] logDirectory {string} Absolute prompt log directory owning the response file.
|
|
1209
|
+
* @param[in] fileName {string} Target response filename under the log directory.
|
|
1210
|
+
* @param[in] errorResponseText {string} Finalized provider error message text.
|
|
1211
|
+
* @return {void} No return value.
|
|
1212
|
+
* @satisfies REQ-429
|
|
1213
|
+
*/
|
|
1214
|
+
function writePromptProviderErrorResponseFile(
|
|
1215
|
+
logDirectory: string,
|
|
1216
|
+
fileName: string,
|
|
1217
|
+
errorResponseText: string,
|
|
1218
|
+
): void {
|
|
1219
|
+
fs.mkdirSync(logDirectory, { recursive: true });
|
|
1220
|
+
fs.writeFileSync(path.join(logDirectory, fileName), `${errorResponseText}\n`, "utf8");
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* @brief Appends the provider error response text beside one already flushed error payload file.
|
|
1225
|
+
* @details Providers that fire `after_provider_response` before surfacing their error text flush the payload without a `-response` file, so this helper writes the finalized `message_end` error message beside the recorded flush location when the failing status matches. Runtime is dominated by one directory creation plus one file write when a matching record exists and O(1) otherwise. Side effects include directory creation and file creation only for a matching record.
|
|
1226
|
+
* @param[in] errorCode {number} Failing provider response status carried by the finalized error message.
|
|
1227
|
+
* @param[in] errorResponseText {string} Finalized provider error message text.
|
|
1228
|
+
* @return {boolean} `true` when the response file is written; otherwise `false`.
|
|
1229
|
+
* @satisfies REQ-429
|
|
1230
|
+
*/
|
|
1231
|
+
export function writeFlushedPromptProviderErrorResponse(
|
|
1232
|
+
errorCode: number,
|
|
1233
|
+
errorResponseText: string,
|
|
1234
|
+
): boolean {
|
|
1235
|
+
if (typeof errorResponseText !== "string" || errorResponseText.length === 0) {
|
|
1236
|
+
return false;
|
|
1237
|
+
}
|
|
1238
|
+
const record = getProcessScopedPromptErrorCaptureStore().lastFlushedPromptError;
|
|
1239
|
+
if (record === undefined || record.errorCode !== errorCode) {
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
try {
|
|
1243
|
+
writePromptProviderErrorResponseFile(
|
|
1244
|
+
record.logDirectory,
|
|
1245
|
+
`${record.baseFileName}-response`,
|
|
1246
|
+
errorResponseText,
|
|
1247
|
+
);
|
|
997
1248
|
return true;
|
|
998
1249
|
} catch {
|
|
999
1250
|
return false;
|
|
@@ -1001,11 +1252,13 @@ export function flushCapturedPromptErrorPayload(errorCode: number): boolean {
|
|
|
1001
1252
|
}
|
|
1002
1253
|
|
|
1003
1254
|
/**
|
|
1004
|
-
* @brief Discards any captured provider request payload for the ended prompt run.
|
|
1005
|
-
* @details Clears the process-scoped capture unconditionally so leftover payloads from a finished `req-*` prompt run never flush into later provider errors raised outside extension-owned prompt orchestration. Runtime is O(1). Side effect: mutates the process-scoped capture store.
|
|
1255
|
+
* @brief Discards any captured provider request payload and flushed error record for the ended prompt run.
|
|
1256
|
+
* @details Clears the process-scoped capture and the latest flushed error record unconditionally so leftover payloads and flush locations from a finished `req-*` prompt run never flush into later provider errors raised outside extension-owned prompt orchestration. Runtime is O(1). Side effect: mutates the process-scoped capture store.
|
|
1006
1257
|
* @return {void} No return value.
|
|
1007
|
-
* @satisfies REQ-421, REQ-425
|
|
1258
|
+
* @satisfies REQ-421, REQ-425, REQ-429
|
|
1008
1259
|
*/
|
|
1009
1260
|
export function discardCapturedPromptErrorPayload(): void {
|
|
1010
|
-
getProcessScopedPromptErrorCaptureStore()
|
|
1261
|
+
const captureStore = getProcessScopedPromptErrorCaptureStore();
|
|
1262
|
+
captureStore.capturedPromptErrorPayload = undefined;
|
|
1263
|
+
captureStore.lastFlushedPromptError = undefined;
|
|
1011
1264
|
}
|
package/src/index.ts
CHANGED
|
@@ -153,8 +153,10 @@ import {
|
|
|
153
153
|
resolveProviderErrorFlushStatusFromMessage,
|
|
154
154
|
setRuntimeDebugEnabled,
|
|
155
155
|
shouldLogDebugPromptWorkflowState,
|
|
156
|
+
writeFlushedPromptProviderErrorResponse,
|
|
156
157
|
type DebugLogOnStatus,
|
|
157
158
|
type DebugWorkflowState,
|
|
159
|
+
type PromptErrorReplayFacts,
|
|
158
160
|
} from "./core/debug-runtime.js";
|
|
159
161
|
import { PROMPT_COMMAND_NAMES } from "./core/prompt-command-catalog.js";
|
|
160
162
|
import { resolveRuntimeGitPath } from "./core/runtime-project-paths.js";
|
|
@@ -305,6 +307,31 @@ function loadPromptRunBaseConfig(projectBase: string): UseReqConfig {
|
|
|
305
307
|
return normalizeConfigPaths(projectBase, loadConfig(projectBase));
|
|
306
308
|
}
|
|
307
309
|
|
|
310
|
+
/**
|
|
311
|
+
* @brief Builds the replay facts captured beside one provider request payload.
|
|
312
|
+
* @details Reads the provider, API, base URL, model identifier, thinking level, context-window, and max-output-token facts from the live extension-context model plus the execution-session file from the live prompt execution plan so the error-stage replay sidecar can emit ready-to-use `curl` and `pi` replay command lines without live runtime state at flush time. Degrades every fact to `undefined` when the runtime model is unavailable so the capture never aborts. Runtime is O(1). No external state is mutated.
|
|
313
|
+
* @param[in] ctx {ExtensionContext} Active extension context supplying the live model and thinking level.
|
|
314
|
+
* @param[in] executionSessionFile {string | undefined} Persisted execution-session file of the active prompt run retained per REQ-209.
|
|
315
|
+
* @return {PromptErrorReplayFacts} Replay facts consumed by the error-stage sidecar writer.
|
|
316
|
+
* @satisfies REQ-430
|
|
317
|
+
*/
|
|
318
|
+
function buildPromptProviderReplayFacts(
|
|
319
|
+
ctx: ExtensionContext,
|
|
320
|
+
executionSessionFile: string | undefined,
|
|
321
|
+
): PromptErrorReplayFacts {
|
|
322
|
+
const model = ctx.model;
|
|
323
|
+
return {
|
|
324
|
+
modelId: model?.id,
|
|
325
|
+
provider: model?.provider,
|
|
326
|
+
api: model?.api,
|
|
327
|
+
baseUrl: model?.baseUrl,
|
|
328
|
+
thinkingLevel: ctx.thinkingLevel,
|
|
329
|
+
contextWindow: model?.contextWindow,
|
|
330
|
+
maxTokens: model?.maxTokens,
|
|
331
|
+
executionSessionFile,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
308
335
|
/**
|
|
309
336
|
* @brief Loads project configuration for the extension runtime.
|
|
310
337
|
* @details Resolves the project base, loads persisted config, and normalizes configured directory paths without reading or persisting runtime-derived `base-path` or `git-path` metadata. Runtime is dominated by config I/O. Side effects are limited to filesystem reads.
|
|
@@ -1545,11 +1572,15 @@ async function handleExtensionStatusEvent(
|
|
|
1545
1572
|
livePromptRequest.promptName,
|
|
1546
1573
|
livePromptRequest.basePath,
|
|
1547
1574
|
providerRequestEvent.payload,
|
|
1575
|
+
buildPromptProviderReplayFacts(ctx, livePromptRequest.executionSessionFile),
|
|
1548
1576
|
);
|
|
1549
1577
|
}
|
|
1550
1578
|
if (hookName === "after_provider_response") {
|
|
1551
1579
|
const providerResponseEvent = event as { status?: unknown };
|
|
1552
1580
|
if (typeof providerResponseEvent.status === "number" && providerResponseEvent.status >= 400) {
|
|
1581
|
+
// The response event carries status plus headers only, so the provider error text is
|
|
1582
|
+
// appended later by the finalized `message_end` error message through the recorded
|
|
1583
|
+
// flush location.
|
|
1553
1584
|
flushCapturedPromptErrorPayload(providerResponseEvent.status);
|
|
1554
1585
|
}
|
|
1555
1586
|
}
|
|
@@ -1560,11 +1591,17 @@ async function handleExtensionStatusEvent(
|
|
|
1560
1591
|
// `message_end` for every failed provider request with the HTTP status embedded in
|
|
1561
1592
|
// `errorMessage` by `formatProviderError`.
|
|
1562
1593
|
if (hookName === "message_end") {
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
);
|
|
1594
|
+
const finalizedMessage = (event as { message?: unknown }).message as { errorMessage?: unknown } | undefined;
|
|
1595
|
+
const providerErrorStatus = resolveProviderErrorFlushStatusFromMessage(finalizedMessage);
|
|
1566
1596
|
if (providerErrorStatus !== undefined) {
|
|
1567
|
-
|
|
1597
|
+
const providerErrorText = typeof finalizedMessage?.errorMessage === "string"
|
|
1598
|
+
? finalizedMessage.errorMessage
|
|
1599
|
+
: undefined;
|
|
1600
|
+
if (!flushCapturedPromptErrorPayload(providerErrorStatus, providerErrorText)) {
|
|
1601
|
+
// The capture was already flushed by `after_provider_response`, so the finalized error
|
|
1602
|
+
// text is appended beside the recorded payload location.
|
|
1603
|
+
writeFlushedPromptProviderErrorResponse(providerErrorStatus, providerErrorText ?? "");
|
|
1604
|
+
}
|
|
1568
1605
|
}
|
|
1569
1606
|
}
|
|
1570
1607
|
if (hookName === "agent_end" || hookName === "agent_settled") {
|