pi-usereq 0.80.0 → 0.82.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 +24 -1
- package/package.json +1 -1
- package/src/core/debug-runtime.ts +324 -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.82.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,29 @@ 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`, a byte-for-byte snapshot of the retained execution session into `<timestamp>-<command>-error-<status>-session.jsonl`, 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; `replayCommands.piFork` targets the log-local `-session.jsonl` snapshot, which freezes the transcript exactly at the failing provider request (before any context edit or compaction). 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
|
+
# Resume the exact failing state using only log files, then re-trigger the failing request:
|
|
355
|
+
# the -session.jsonl snapshot is the execution session frozen at the 400, so any
|
|
356
|
+
# continuation message re-sends the same oversized request and reproduces the error
|
|
357
|
+
pi --fork /tmp/PI-useReq/20261012091841114-req-analyze-error-400-session.jsonl \
|
|
358
|
+
--model '~deepseek/deepseek-flash-latest' --thinking high "continue"
|
|
359
|
+
```
|
|
360
|
+
|
|
338
361
|
|
|
339
362
|
## Note on Git usage
|
|
340
363
|
|
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,57 @@ 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
|
+
|
|
969
|
+
/**
|
|
970
|
+
* @brief Builds one provider error session-snapshot debug filename for one bundled prompt.
|
|
971
|
+
* @details Appends the `-session.jsonl` suffix to the shared `<timestamp>-<req-command>-error-<status>` shape so the byte-for-byte execution-session copy stays attributable to the exact failing request payload file written with the identical write-time timestamp and remains directly consumable by `pi --fork` without files outside the prompt log directory. Runtime is O(1). No external state is mutated.
|
|
972
|
+
* @param[in] promptName {PromptCommandName} Bundled prompt name.
|
|
973
|
+
* @param[in] errorCode {number} Failing provider response status, such as `400`.
|
|
974
|
+
* @param[in] date {Date} Write-time timestamp shared with the payload file. Defaults to the current wall-clock time.
|
|
975
|
+
* @return {string} Provider error session-snapshot filename in the `<timestamp>-<req-command>-error-<status>-session.jsonl` shape.
|
|
976
|
+
* @satisfies REQ-423, REQ-432
|
|
977
|
+
*/
|
|
978
|
+
export function formatPromptDebugErrorSessionFileName(
|
|
979
|
+
promptName: PromptCommandName,
|
|
980
|
+
errorCode: number,
|
|
981
|
+
date = new Date(),
|
|
982
|
+
): string {
|
|
983
|
+
return `${formatPromptDebugErrorFileName(promptName, errorCode, date)}-session.jsonl`;
|
|
984
|
+
}
|
|
985
|
+
|
|
894
986
|
/**
|
|
895
987
|
* @brief Resolves the failing HTTP status carried by one finalized agent message.
|
|
896
988
|
* @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 +1038,190 @@ function serializePromptProviderPayload(payload: unknown): string {
|
|
|
946
1038
|
) ?? "";
|
|
947
1039
|
}
|
|
948
1040
|
|
|
1041
|
+
/**
|
|
1042
|
+
* @brief Resolves the model identifier embedded in one provider request payload.
|
|
1043
|
+
* @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.
|
|
1044
|
+
* @param[in] payload {unknown} Exact `before_provider_request` provider request payload.
|
|
1045
|
+
* @return {string | undefined} Payload model identifier when present as a string; otherwise `undefined`.
|
|
1046
|
+
*/
|
|
1047
|
+
function resolveProviderPayloadModelId(payload: unknown): string | undefined {
|
|
1048
|
+
if (typeof payload !== "object" || payload === null) {
|
|
1049
|
+
return undefined;
|
|
1050
|
+
}
|
|
1051
|
+
const modelId = (payload as { model?: unknown }).model;
|
|
1052
|
+
return typeof modelId === "string" && modelId.length > 0 ? modelId : undefined;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
949
1055
|
/**
|
|
950
1056
|
* @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,
|
|
1057
|
+
* @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
1058
|
* @param[in] config {UseReqConfig} Effective project configuration.
|
|
953
1059
|
* @param[in] promptName {PromptCommandName} Active bundled prompt name.
|
|
954
1060
|
* @param[in] projectBase {string} Absolute original project base path used to resolve the prompt log directory.
|
|
955
1061
|
* @param[in] payload {unknown} Exact `before_provider_request` provider request payload.
|
|
1062
|
+
* @param[in] replayFacts {PromptErrorReplayFacts} Live runtime replay facts consumed by the error-stage sidecar. Defaults to empty facts.
|
|
956
1063
|
* @return {void} No return value.
|
|
957
|
-
* @satisfies REQ-416, REQ-421
|
|
1064
|
+
* @satisfies REQ-416, REQ-421, REQ-430
|
|
958
1065
|
*/
|
|
959
1066
|
export function capturePromptProviderRequestForDebug(
|
|
960
1067
|
config: UseReqConfig,
|
|
961
1068
|
promptName: PromptCommandName,
|
|
962
1069
|
projectBase: string,
|
|
963
1070
|
payload: unknown,
|
|
1071
|
+
replayFacts: PromptErrorReplayFacts = {
|
|
1072
|
+
modelId: undefined,
|
|
1073
|
+
provider: undefined,
|
|
1074
|
+
api: undefined,
|
|
1075
|
+
baseUrl: undefined,
|
|
1076
|
+
thinkingLevel: undefined,
|
|
1077
|
+
contextWindow: undefined,
|
|
1078
|
+
maxTokens: undefined,
|
|
1079
|
+
executionSessionFile: undefined,
|
|
1080
|
+
},
|
|
964
1081
|
): void {
|
|
965
1082
|
if (!shouldLogDebugPromptContent(config, "unknown", promptName)) {
|
|
966
1083
|
return;
|
|
967
1084
|
}
|
|
1085
|
+
const logDirectory = resolveDebugPromptsLogPath(projectBase, config);
|
|
1086
|
+
const dispatchedPromptFile = getProcessScopedPromptErrorCaptureStore().dispatchedPromptDebugFiles.get(promptName);
|
|
968
1087
|
getProcessScopedPromptErrorCaptureStore().capturedPromptErrorPayload = {
|
|
969
1088
|
promptName,
|
|
970
1089
|
payloadText: serializePromptProviderPayload(payload),
|
|
971
1090
|
projectBase,
|
|
972
|
-
logDirectory
|
|
1091
|
+
logDirectory,
|
|
1092
|
+
promptDebugFile: dispatchedPromptFile === undefined ? undefined : path.join(logDirectory, dispatchedPromptFile),
|
|
1093
|
+
replayFacts: {
|
|
1094
|
+
...replayFacts,
|
|
1095
|
+
modelId: replayFacts.modelId ?? resolveProviderPayloadModelId(payload),
|
|
1096
|
+
},
|
|
973
1097
|
};
|
|
974
1098
|
}
|
|
975
1099
|
|
|
976
1100
|
/**
|
|
977
|
-
* @brief
|
|
978
|
-
* @details
|
|
1101
|
+
* @brief Normalizes one provider name into an uppercase environment-variable token.
|
|
1102
|
+
* @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.
|
|
1103
|
+
* @param[in] provider {string} Provider identifier, such as `openrouter`.
|
|
1104
|
+
* @return {string} Uppercase alphanumeric environment-variable token.
|
|
1105
|
+
*/
|
|
1106
|
+
function normalizeProviderEnvToken(provider: string): string {
|
|
1107
|
+
const normalized = provider.toUpperCase().replace(/[^A-Z0-9]/gu, "_");
|
|
1108
|
+
return normalized.length > 0 ? normalized : "PROVIDER";
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* @brief Resolves the provider HTTP endpoint targeted by one captured provider request payload.
|
|
1113
|
+
* @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.
|
|
1114
|
+
* @param[in] facts {PromptErrorReplayFacts} Captured replay facts supplying the API and base URL.
|
|
1115
|
+
* @return {string | undefined} Replay endpoint URL when a base URL exists; otherwise `undefined`.
|
|
1116
|
+
*/
|
|
1117
|
+
function resolveProviderReplayEndpoint(facts: PromptErrorReplayFacts): string | undefined {
|
|
1118
|
+
if (facts.baseUrl === undefined) {
|
|
1119
|
+
return undefined;
|
|
1120
|
+
}
|
|
1121
|
+
if (facts.api === "openai-completions") {
|
|
1122
|
+
return `${facts.baseUrl}/chat/completions`;
|
|
1123
|
+
}
|
|
1124
|
+
if (facts.api === "anthropic-messages") {
|
|
1125
|
+
return `${facts.baseUrl}/v1/messages`;
|
|
1126
|
+
}
|
|
1127
|
+
return facts.baseUrl;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* @brief Builds the replay sidecar JSON text for one flushed provider error payload.
|
|
1132
|
+
* @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 that targets the log-local `-session.jsonl` snapshot when supplied and falls back to the retained execution session otherwise. 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.
|
|
1133
|
+
* @param[in] captured {CapturedPromptErrorPayload} Consumed capture supplying replay facts and artifact paths.
|
|
1134
|
+
* @param[in] requestFileName {string} Payload filename written with the same write-time timestamp.
|
|
1135
|
+
* @param[in] sessionSnapshotFileName {string | undefined} Log-local `-session.jsonl` snapshot filename written with the same write-time timestamp; `undefined` falls back to the retained execution-session path.
|
|
1136
|
+
* @return {string} Newline-terminated replay sidecar JSON text.
|
|
1137
|
+
* @satisfies REQ-430, REQ-433
|
|
1138
|
+
*/
|
|
1139
|
+
function buildPromptErrorReplaySidecarText(
|
|
1140
|
+
captured: CapturedPromptErrorPayload,
|
|
1141
|
+
requestFileName: string,
|
|
1142
|
+
sessionSnapshotFileName?: string,
|
|
1143
|
+
): string {
|
|
1144
|
+
const facts = captured.replayFacts;
|
|
1145
|
+
const requestFilePath = path.join(captured.logDirectory, requestFileName);
|
|
1146
|
+
const endpoint = resolveProviderReplayEndpoint(facts);
|
|
1147
|
+
const replayCommands: Record<string, string> = {};
|
|
1148
|
+
if (endpoint !== undefined) {
|
|
1149
|
+
const envToken = facts.provider === undefined ? "PROVIDER_API_KEY" : `${normalizeProviderEnvToken(facts.provider)}_API_KEY`;
|
|
1150
|
+
replayCommands.provider = `curl -sS '${endpoint}' -H 'Authorization: Bearer $${envToken}' -H 'Content-Type: application/json' --data @'${requestFilePath}'`;
|
|
1151
|
+
}
|
|
1152
|
+
if (captured.promptDebugFile !== undefined && facts.modelId !== undefined) {
|
|
1153
|
+
replayCommands.piOneShot = `pi --no-session --model '${facts.modelId}' --thinking ${facts.thinkingLevel ?? "off"} -p "$(cat '${captured.promptDebugFile}')"`;
|
|
1154
|
+
}
|
|
1155
|
+
const sessionForkFile = sessionSnapshotFileName !== undefined
|
|
1156
|
+
? path.join(captured.logDirectory, sessionSnapshotFileName)
|
|
1157
|
+
: facts.executionSessionFile;
|
|
1158
|
+
if (sessionForkFile !== undefined) {
|
|
1159
|
+
const modelFlags = facts.modelId === undefined ? "" : ` --model '${facts.modelId}'`;
|
|
1160
|
+
const thinkingFlags = facts.thinkingLevel === undefined ? "" : ` --thinking ${facts.thinkingLevel}`;
|
|
1161
|
+
replayCommands.piFork = `pi --fork '${sessionForkFile}'${modelFlags}${thinkingFlags}`;
|
|
1162
|
+
}
|
|
1163
|
+
const sidecar = {
|
|
1164
|
+
provider: facts.provider ?? null,
|
|
1165
|
+
baseUrl: facts.baseUrl ?? null,
|
|
1166
|
+
api: facts.api ?? null,
|
|
1167
|
+
modelId: facts.modelId ?? null,
|
|
1168
|
+
thinkingLevel: facts.thinkingLevel ?? null,
|
|
1169
|
+
contextWindow: facts.contextWindow ?? null,
|
|
1170
|
+
maxTokens: facts.maxTokens ?? null,
|
|
1171
|
+
sessionFile: facts.executionSessionFile ?? null,
|
|
1172
|
+
promptFile: captured.promptDebugFile ?? null,
|
|
1173
|
+
requestFile: requestFilePath,
|
|
1174
|
+
replayCommands,
|
|
1175
|
+
};
|
|
1176
|
+
return `${JSON.stringify(sidecar, null, 2)}\n`;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* @brief Writes the captured provider request payload plus its response text and replay sidecar as error-stage prompt debug files.
|
|
1181
|
+
* @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, one identically timestamped byte-for-byte `-session.jsonl` copy of the retained execution session so `pi --fork` can resume the exact failing state from the prompt log directory alone, 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 four 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
1182
|
* @param[in] errorCode {number} Failing provider response status, such as `400`.
|
|
980
|
-
* @
|
|
981
|
-
* @
|
|
1183
|
+
* @param[in] errorResponseText {string | undefined} Finalized provider error message text when already available; the `-response` file is written later when omitted.
|
|
1184
|
+
* @return {boolean} `true` when the provider error payload file set is written; otherwise `false`.
|
|
1185
|
+
* @satisfies REQ-422, REQ-423, REQ-424, REQ-425, REQ-429, REQ-430, REQ-431, REQ-432, REQ-433
|
|
982
1186
|
*/
|
|
983
|
-
export function flushCapturedPromptErrorPayload(errorCode: number): boolean {
|
|
1187
|
+
export function flushCapturedPromptErrorPayload(errorCode: number, errorResponseText?: string): boolean {
|
|
984
1188
|
const captureStore = getProcessScopedPromptErrorCaptureStore();
|
|
985
1189
|
const captured = captureStore.capturedPromptErrorPayload;
|
|
986
1190
|
captureStore.capturedPromptErrorPayload = undefined;
|
|
987
1191
|
if (!captured || !Number.isFinite(errorCode) || errorCode < 400) {
|
|
988
1192
|
return false;
|
|
989
1193
|
}
|
|
1194
|
+
const writeTime = new Date();
|
|
1195
|
+
const payloadFileName = formatPromptDebugErrorFileName(captured.promptName, errorCode, writeTime);
|
|
990
1196
|
try {
|
|
991
1197
|
fs.mkdirSync(captured.logDirectory, { recursive: true });
|
|
992
1198
|
fs.writeFileSync(
|
|
993
|
-
path.join(captured.logDirectory,
|
|
1199
|
+
path.join(captured.logDirectory, payloadFileName),
|
|
994
1200
|
captured.payloadText,
|
|
995
1201
|
"utf8",
|
|
996
1202
|
);
|
|
1203
|
+
if (typeof errorResponseText === "string" && errorResponseText.length > 0) {
|
|
1204
|
+
writePromptProviderErrorResponseFile(
|
|
1205
|
+
captured.logDirectory,
|
|
1206
|
+
formatPromptDebugErrorResponseFileName(captured.promptName, errorCode, writeTime),
|
|
1207
|
+
errorResponseText,
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
const sessionSnapshotFileName = copyPromptErrorSessionSnapshotFile(
|
|
1211
|
+
captured,
|
|
1212
|
+
formatPromptDebugErrorSessionFileName(captured.promptName, errorCode, writeTime),
|
|
1213
|
+
);
|
|
1214
|
+
fs.writeFileSync(
|
|
1215
|
+
path.join(captured.logDirectory, formatPromptDebugErrorReplayFileName(captured.promptName, errorCode, writeTime)),
|
|
1216
|
+
buildPromptErrorReplaySidecarText(captured, payloadFileName, sessionSnapshotFileName),
|
|
1217
|
+
"utf8",
|
|
1218
|
+
);
|
|
1219
|
+
captureStore.lastFlushedPromptError = {
|
|
1220
|
+
promptName: captured.promptName,
|
|
1221
|
+
errorCode,
|
|
1222
|
+
logDirectory: captured.logDirectory,
|
|
1223
|
+
baseFileName: payloadFileName,
|
|
1224
|
+
};
|
|
997
1225
|
return true;
|
|
998
1226
|
} catch {
|
|
999
1227
|
return false;
|
|
@@ -1001,11 +1229,87 @@ export function flushCapturedPromptErrorPayload(errorCode: number): boolean {
|
|
|
1001
1229
|
}
|
|
1002
1230
|
|
|
1003
1231
|
/**
|
|
1004
|
-
* @brief
|
|
1005
|
-
* @details
|
|
1232
|
+
* @brief Writes one provider error response text file under a resolved log directory.
|
|
1233
|
+
* @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.
|
|
1234
|
+
* @param[in] logDirectory {string} Absolute prompt log directory owning the response file.
|
|
1235
|
+
* @param[in] fileName {string} Target response filename under the log directory.
|
|
1236
|
+
* @param[in] errorResponseText {string} Finalized provider error message text.
|
|
1006
1237
|
* @return {void} No return value.
|
|
1007
|
-
* @satisfies REQ-
|
|
1238
|
+
* @satisfies REQ-429
|
|
1239
|
+
*/
|
|
1240
|
+
function writePromptProviderErrorResponseFile(
|
|
1241
|
+
logDirectory: string,
|
|
1242
|
+
fileName: string,
|
|
1243
|
+
errorResponseText: string,
|
|
1244
|
+
): void {
|
|
1245
|
+
fs.mkdirSync(logDirectory, { recursive: true });
|
|
1246
|
+
fs.writeFileSync(path.join(logDirectory, fileName), `${errorResponseText}\n`, "utf8");
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* @brief Copies the retained execution-session file beside one flushed provider error payload.
|
|
1251
|
+
* @details Duplicates the captured execution-session file byte-for-byte into the captured log directory under the requested `-session.jsonl` filename synchronously during the error-stage flush, so the snapshot freezes the transcript exactly at the state that produced the failing provider request before pi's `context_edit` or compaction appends further entries and stays directly consumable by `pi --fork` without any file outside the prompt log directory. Degrades to `undefined` on any copy failure so the replay sidecar falls back to the retained execution-session path. Runtime is dominated by one directory creation plus one file copy. Side effects include directory creation and file creation.
|
|
1252
|
+
* @param[in] captured {CapturedPromptErrorPayload} Consumed capture supplying the log directory and execution-session replay facts.
|
|
1253
|
+
* @param[in] snapshotFileName {string} Session-snapshot filename sharing the payload write-time timestamp.
|
|
1254
|
+
* @return {string | undefined} Written snapshot filename when the copy succeeds; otherwise `undefined`.
|
|
1255
|
+
* @satisfies REQ-432
|
|
1256
|
+
*/
|
|
1257
|
+
function copyPromptErrorSessionSnapshotFile(
|
|
1258
|
+
captured: CapturedPromptErrorPayload,
|
|
1259
|
+
snapshotFileName: string,
|
|
1260
|
+
): string | undefined {
|
|
1261
|
+
const sessionFile = captured.replayFacts.executionSessionFile;
|
|
1262
|
+
if (sessionFile === undefined || sessionFile.length === 0) {
|
|
1263
|
+
return undefined;
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
fs.mkdirSync(captured.logDirectory, { recursive: true });
|
|
1267
|
+
fs.copyFileSync(sessionFile, path.join(captured.logDirectory, snapshotFileName));
|
|
1268
|
+
return snapshotFileName;
|
|
1269
|
+
} catch {
|
|
1270
|
+
return undefined;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
/**
|
|
1275
|
+
* @brief Appends the provider error response text beside one already flushed error payload file.
|
|
1276
|
+
* @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.
|
|
1277
|
+
* @param[in] errorCode {number} Failing provider response status carried by the finalized error message.
|
|
1278
|
+
* @param[in] errorResponseText {string} Finalized provider error message text.
|
|
1279
|
+
* @return {boolean} `true` when the response file is written; otherwise `false`.
|
|
1280
|
+
* @satisfies REQ-429
|
|
1281
|
+
*/
|
|
1282
|
+
export function writeFlushedPromptProviderErrorResponse(
|
|
1283
|
+
errorCode: number,
|
|
1284
|
+
errorResponseText: string,
|
|
1285
|
+
): boolean {
|
|
1286
|
+
if (typeof errorResponseText !== "string" || errorResponseText.length === 0) {
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
const record = getProcessScopedPromptErrorCaptureStore().lastFlushedPromptError;
|
|
1290
|
+
if (record === undefined || record.errorCode !== errorCode) {
|
|
1291
|
+
return false;
|
|
1292
|
+
}
|
|
1293
|
+
try {
|
|
1294
|
+
writePromptProviderErrorResponseFile(
|
|
1295
|
+
record.logDirectory,
|
|
1296
|
+
`${record.baseFileName}-response`,
|
|
1297
|
+
errorResponseText,
|
|
1298
|
+
);
|
|
1299
|
+
return true;
|
|
1300
|
+
} catch {
|
|
1301
|
+
return false;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* @brief Discards any captured provider request payload and flushed error record for the ended prompt run.
|
|
1307
|
+
* @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.
|
|
1308
|
+
* @return {void} No return value.
|
|
1309
|
+
* @satisfies REQ-421, REQ-425, REQ-429
|
|
1008
1310
|
*/
|
|
1009
1311
|
export function discardCapturedPromptErrorPayload(): void {
|
|
1010
|
-
getProcessScopedPromptErrorCaptureStore()
|
|
1312
|
+
const captureStore = getProcessScopedPromptErrorCaptureStore();
|
|
1313
|
+
captureStore.capturedPromptErrorPayload = undefined;
|
|
1314
|
+
captureStore.lastFlushedPromptError = undefined;
|
|
1011
1315
|
}
|
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") {
|