pi-usereq 0.79.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 +59 -6
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";
|
|
@@ -294,6 +296,42 @@ function syncContextCwdMirror(ctx: { cwd?: string }, cwd: string): void {
|
|
|
294
296
|
}
|
|
295
297
|
}
|
|
296
298
|
|
|
299
|
+
/**
|
|
300
|
+
* @brief Loads the effective configuration anchored at one exact prompt-run project base.
|
|
301
|
+
* @details Reads the split local and global configuration scoped to the supplied absolute `projectBase` without consulting the shared runtime path state, because during worktree-backed prompt runs the runtime `base-path` resolves to the execution checkout whose untracked `.pi-usereq.json` is absent while the prompt debug gating configuration lives at the original project base that also produced the `-request` and `-prompt` debug files. Runtime is dominated by config I/O. Side effects are limited to filesystem reads.
|
|
302
|
+
* @param[in] projectBase {string} Absolute original project base path recorded in the prompt execution plan.
|
|
303
|
+
* @return {UseReqConfig} Effective configuration anchored at the exact supplied project base.
|
|
304
|
+
* @satisfies REQ-416, REQ-421
|
|
305
|
+
*/
|
|
306
|
+
function loadPromptRunBaseConfig(projectBase: string): UseReqConfig {
|
|
307
|
+
return normalizeConfigPaths(projectBase, loadConfig(projectBase));
|
|
308
|
+
}
|
|
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
|
+
|
|
297
335
|
/**
|
|
298
336
|
* @brief Loads project configuration for the extension runtime.
|
|
299
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.
|
|
@@ -1449,7 +1487,7 @@ async function finalizeMatchedPromptSuccess(
|
|
|
1449
1487
|
|
|
1450
1488
|
/**
|
|
1451
1489
|
* @brief Handles one intercepted pi lifecycle hook for pi-usereq status updates.
|
|
1452
|
-
* @details Applies session-start-specific resource validation, project-config refresh, idle-time shared-tokenizer and context-file measurement pre-warm, startup-tool enablement, and selected debug-tool logging before forwarding the originating hook name and payload into the shared `updateExtensionStatus(...)` pipeline. After the status update, captures the exact `before_provider_request` provider payload for the live active or pending prompt run
|
|
1490
|
+
* @details Applies session-start-specific resource validation, project-config refresh, idle-time shared-tokenizer and context-file measurement pre-warm, startup-tool enablement, and selected debug-tool logging before forwarding the originating hook name and payload into the shared `updateExtensionStatus(...)` pipeline. After the status update, captures the exact `before_provider_request` provider payload for the live active or pending prompt run by reloading the prompt debug gating configuration from the prompt run's original `base-path` (worktree-backed checkouts do not contain the untracked `.pi-usereq.json`, so the cwd-derived cached config would silently disable capture), flushes the process-scoped captured payload as one `<timestamp>-<req-command>-error-<status>` file whenever `after_provider_response` reports a status >= 400 or whenever one finalized `message_end` assistant error message carries a leading HTTP status >= 400 (the provider-independent fallback for provider APIs whose SDK clients throw before emitting `after_provider_response` for non-2xx statuses) without requiring live workflow state, and discards the capture at prompt-run end so stale payloads never reach later runs. Before `agent_start`, re-verifies any prepared prompt execution session switch. On `agent_end`, dispatches configured command-notify, sound, and prompt-specific Pushover effects, logs dedicated workflow-closure diagnostics, classifies the prompt outcome, and for every matched successful worktree-backed completion defers the restore switch, stash-assisted merge, and worktree deletion to `agent_settled` when the running pi host supports that 0.80.4+ event (because its `switchSession` awaits the active agent run to become idle and would deadlock inside `agent_end`), or executes the finalization directly at `agent_end` when the host does not emit `agent_settled`. On `agent_settled`, reuses persisted replacement-session command contexts when event contexts omit `switchSession()`, executes the deferred stash-assisted merge-and-delete finalization path, emits a warning-only notification when restored `base-path` changes are reapplied after merge, tolerates stale replacement-session notification contexts after session replacement, retains the worktree plus notifies closure failure for interrupted or failed outcomes, logs selected prompt workflow transitions, and transitions workflow state through `merging`, `error`, and `idle` as required. On `session_shutdown`, captures pre-update prompt snapshots so workflow-shutdown diagnostics and same-runtime command continuation preserve the active prompt workflow state across switch-triggered rebinding, then disposes the shared controller. Runtime is dominated by configuration loading during `session_start` and git finalization during matched successful closure handling; all other hooks are O(1). Side effects include resource checks, active-tool mutation, active-session replacement, status updates, live-ticker disposal on shutdown, optional child-process spawning, outbound HTTPS requests, branch merges, worktree deletion, and optional debug-log writes.
|
|
1453
1491
|
* @param[in] pi {ExtensionAPI} Active extension API instance.
|
|
1454
1492
|
* @param[in,out] statusController {PiUsereqStatusController} Mutable status controller.
|
|
1455
1493
|
* @param[in] hookName {PiUsereqStatusHookName} Intercepted hook name.
|
|
@@ -1524,16 +1562,25 @@ async function handleExtensionStatusEvent(
|
|
|
1524
1562
|
?? statusController.state.pendingPromptRequest;
|
|
1525
1563
|
if (hookName === "before_provider_request" && livePromptRequest !== undefined) {
|
|
1526
1564
|
const providerRequestEvent = event as { payload?: unknown };
|
|
1565
|
+
// The capture gate MUST resolve its gating configuration from the prompt run's ORIGINAL
|
|
1566
|
+
// project base: worktree-backed runs execute in a checkout whose untracked
|
|
1567
|
+
// `.pi-usereq.json` is absent, so the cwd-derived cached config silently reverts to
|
|
1568
|
+
// `DEBUG_PROMPTS_ENABLED=disable` defaults and would skip the REQ-421 capture even though
|
|
1569
|
+
// the base configuration already wrote the `-request` and `-prompt` debug files.
|
|
1527
1570
|
capturePromptProviderRequestForDebug(
|
|
1528
|
-
|
|
1571
|
+
loadPromptRunBaseConfig(livePromptRequest.basePath),
|
|
1529
1572
|
livePromptRequest.promptName,
|
|
1530
1573
|
livePromptRequest.basePath,
|
|
1531
1574
|
providerRequestEvent.payload,
|
|
1575
|
+
buildPromptProviderReplayFacts(ctx, livePromptRequest.executionSessionFile),
|
|
1532
1576
|
);
|
|
1533
1577
|
}
|
|
1534
1578
|
if (hookName === "after_provider_response") {
|
|
1535
1579
|
const providerResponseEvent = event as { status?: unknown };
|
|
1536
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.
|
|
1537
1584
|
flushCapturedPromptErrorPayload(providerResponseEvent.status);
|
|
1538
1585
|
}
|
|
1539
1586
|
}
|
|
@@ -1544,11 +1591,17 @@ async function handleExtensionStatusEvent(
|
|
|
1544
1591
|
// `message_end` for every failed provider request with the HTTP status embedded in
|
|
1545
1592
|
// `errorMessage` by `formatProviderError`.
|
|
1546
1593
|
if (hookName === "message_end") {
|
|
1547
|
-
const
|
|
1548
|
-
|
|
1549
|
-
);
|
|
1594
|
+
const finalizedMessage = (event as { message?: unknown }).message as { errorMessage?: unknown } | undefined;
|
|
1595
|
+
const providerErrorStatus = resolveProviderErrorFlushStatusFromMessage(finalizedMessage);
|
|
1550
1596
|
if (providerErrorStatus !== undefined) {
|
|
1551
|
-
|
|
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
|
+
}
|
|
1552
1605
|
}
|
|
1553
1606
|
}
|
|
1554
1607
|
if (hookName === "agent_end" || hookName === "agent_settled") {
|