pi-usereq 0.81.0 → 0.83.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 +8 -5
- package/package.json +1 -1
- package/src/core/debug-runtime.ts +99 -31
- package/src/core/req-reset-command.ts +209 -16
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PI-useReq/pi-usereq (0.
|
|
1
|
+
# PI-useReq/pi-usereq (0.83.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,7 +335,7 @@ 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
|
|
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>.json` (raw provider-request JSON), the provider error message text into `<timestamp>-<command>-error-<status>-response.txt`, 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.md` and `-prompt.md` 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
339
|
|
|
340
340
|
```bash
|
|
341
341
|
# Re-send the exact failing provider request (the payload file is raw request JSON)
|
|
@@ -345,14 +345,17 @@ The extension renders a status field in the pi status line showing: extension id
|
|
|
345
345
|
curl -sS https://openrouter.ai/api/v1/chat/completions \
|
|
346
346
|
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
|
|
347
347
|
-H "Content-Type: application/json" \
|
|
348
|
-
--data @/tmp/PI-useReq/20261012070707447-req-analyze-error-400
|
|
348
|
+
--data @/tmp/PI-useReq/20261012070707447-req-analyze-error-400.json
|
|
349
349
|
|
|
350
350
|
# Replay the saved prompt through pi with the same model and thinking level
|
|
351
351
|
pi --no-session --model '~deepseek/deepseek-flash-latest' --thinking high \
|
|
352
352
|
-p "$(cat /tmp/PI-useReq/20261012070647580-req-analyze-prompt)"
|
|
353
353
|
|
|
354
|
-
#
|
|
355
|
-
|
|
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"
|
|
356
359
|
```
|
|
357
360
|
|
|
358
361
|
|
package/package.json
CHANGED
|
@@ -724,32 +724,32 @@ export function formatPromptDebugFileName(
|
|
|
724
724
|
|
|
725
725
|
/**
|
|
726
726
|
* @brief Builds one command-invocation summary debug filename for one bundled prompt.
|
|
727
|
-
* @details Appends the `-request` suffix to the shared `<timestamp>-<req-command>` base so every saved command invocation summary stays attributable to the originating `/req-*` prompt. Runtime is O(1). No external state is mutated.
|
|
727
|
+
* @details Appends the `-request.md` suffix to the shared `<timestamp>-<req-command>` base so every saved command invocation summary stays attributable to the originating `/req-*` prompt, with the `.md` extension coherent with the persisted markdown summary content per REQ-435. Runtime is O(1). No external state is mutated.
|
|
728
728
|
* @param[in] promptName {PromptCommandName} Bundled prompt name.
|
|
729
729
|
* @param[in] date {Date} Write-time timestamp. Defaults to the current wall-clock time.
|
|
730
|
-
* @return {string} Prompt debug filename in the `<timestamp>-<req-command>-request` shape.
|
|
731
|
-
* @satisfies REQ-417, REQ-427
|
|
730
|
+
* @return {string} Prompt debug filename in the `<timestamp>-<req-command>-request.md` shape.
|
|
731
|
+
* @satisfies REQ-417, REQ-427, REQ-435
|
|
732
732
|
*/
|
|
733
733
|
export function formatPromptDebugRequestFileName(
|
|
734
734
|
promptName: PromptCommandName,
|
|
735
735
|
date = new Date(),
|
|
736
736
|
): string {
|
|
737
|
-
return `${formatPromptDebugFileName(promptName, date)}-request`;
|
|
737
|
+
return `${formatPromptDebugFileName(promptName, date)}-request.md`;
|
|
738
738
|
}
|
|
739
739
|
|
|
740
740
|
/**
|
|
741
741
|
* @brief Builds one dispatched-prompt content debug filename for one bundled prompt.
|
|
742
|
-
* @details Appends the `-prompt` suffix to the shared `<timestamp>-<req-command>` base so every saved initial LLM prompt stays attributable to the originating `/req-*` prompt. Runtime is O(1). No external state is mutated.
|
|
742
|
+
* @details Appends the `-prompt.md` suffix to the shared `<timestamp>-<req-command>` base so every saved initial LLM prompt stays attributable to the originating `/req-*` prompt, with the `.md` extension coherent with the persisted markdown prompt content per REQ-435. Runtime is O(1). No external state is mutated.
|
|
743
743
|
* @param[in] promptName {PromptCommandName} Bundled prompt name.
|
|
744
744
|
* @param[in] date {Date} Write-time timestamp. Defaults to the current wall-clock time.
|
|
745
|
-
* @return {string} Prompt debug filename in the `<timestamp>-<req-command>-prompt` shape.
|
|
746
|
-
* @satisfies REQ-417, REQ-418
|
|
745
|
+
* @return {string} Prompt debug filename in the `<timestamp>-<req-command>-prompt.md` shape.
|
|
746
|
+
* @satisfies REQ-417, REQ-418, REQ-435
|
|
747
747
|
*/
|
|
748
748
|
export function formatPromptDebugPromptFileName(
|
|
749
749
|
promptName: PromptCommandName,
|
|
750
750
|
date = new Date(),
|
|
751
751
|
): string {
|
|
752
|
-
return `${formatPromptDebugFileName(promptName, date)}-prompt`;
|
|
752
|
+
return `${formatPromptDebugFileName(promptName, date)}-prompt.md`;
|
|
753
753
|
}
|
|
754
754
|
|
|
755
755
|
/**
|
|
@@ -787,14 +787,14 @@ function writePromptDebugFile(
|
|
|
787
787
|
|
|
788
788
|
/**
|
|
789
789
|
* @brief Writes one command invocation summary debug file when prompt debug logging is enabled.
|
|
790
|
-
* @details Persists the exact summary text displayed on screen into one `<timestamp>-<req-command>-request` file so the `/req-*` invocation headers survive beside the dispatched prompt. Runtime is dominated by one directory creation plus one file write when enabled and O(1) otherwise. Side effects include directory creation and file creation only for enabled matching entries.
|
|
790
|
+
* @details Persists the exact summary text displayed on screen into one `<timestamp>-<req-command>-request.md` file so the `/req-*` invocation headers survive beside the dispatched prompt. Runtime is dominated by one directory creation plus one file write when enabled and O(1) otherwise. Side effects include directory creation and file creation only for enabled matching entries.
|
|
791
791
|
* @param[in] projectBase {string} Absolute original project base path.
|
|
792
792
|
* @param[in] config {UseReqConfig} Effective project configuration.
|
|
793
793
|
* @param[in] workflowState {DebugWorkflowState} Current workflow state; excluded from gating per REQ-416.
|
|
794
794
|
* @param[in] promptName {PromptCommandName} Bundled prompt name.
|
|
795
795
|
* @param[in] requestText {string} Exact command invocation summary text displayed on screen.
|
|
796
796
|
* @return {boolean} `true` when the request file is written; otherwise `false`.
|
|
797
|
-
* @satisfies REQ-416, REQ-417, REQ-427, REQ-428
|
|
797
|
+
* @satisfies REQ-416, REQ-417, REQ-427, REQ-428, REQ-435
|
|
798
798
|
*/
|
|
799
799
|
export function logDebugPromptRequest(
|
|
800
800
|
projectBase: string,
|
|
@@ -815,14 +815,14 @@ export function logDebugPromptRequest(
|
|
|
815
815
|
|
|
816
816
|
/**
|
|
817
817
|
* @brief Writes one dispatched prompt content file when prompt debug logging is enabled.
|
|
818
|
-
* @details Persists the exact rendered prompt content delivered through `sendMessage` or `sendUserMessage` into one `<timestamp>-<req-command>-prompt` file so the send can be replayed from the pi CLI without pi-usereq installed. Runtime is dominated by one directory creation plus one file write when enabled and O(1) otherwise. Side effects include directory creation and file creation only for enabled matching entries.
|
|
818
|
+
* @details Persists the exact rendered prompt content delivered through `sendMessage` or `sendUserMessage` into one `<timestamp>-<req-command>-prompt.md` file so the send can be replayed from the pi CLI without pi-usereq installed. Runtime is dominated by one directory creation plus one file write when enabled and O(1) otherwise. Side effects include directory creation and file creation only for enabled matching entries.
|
|
819
819
|
* @param[in] projectBase {string} Absolute original project base path.
|
|
820
820
|
* @param[in] config {UseReqConfig} Effective project configuration.
|
|
821
821
|
* @param[in] workflowState {DebugWorkflowState} Current workflow state; excluded from gating per REQ-416.
|
|
822
822
|
* @param[in] promptName {PromptCommandName} Bundled prompt name.
|
|
823
823
|
* @param[in] content {string} Exact rendered prompt content delivered through `sendMessage` or `sendUserMessage`.
|
|
824
824
|
* @return {boolean} `true` when the prompt content file is written; otherwise `false`.
|
|
825
|
-
* @satisfies REQ-416, REQ-417, REQ-418, REQ-428
|
|
825
|
+
* @satisfies REQ-416, REQ-417, REQ-418, REQ-428, REQ-435
|
|
826
826
|
*/
|
|
827
827
|
export function logDebugPromptContent(
|
|
828
828
|
projectBase: string,
|
|
@@ -878,7 +878,7 @@ interface CapturedPromptErrorPayload {
|
|
|
878
878
|
|
|
879
879
|
/**
|
|
880
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
|
|
881
|
+
* @details Stores the prompt name, failing status, resolved log directory, and shared extensionless error base filename of the most recent error-stage flush so a later finalized `message_end` error message can still append the `-response.txt` 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
882
|
* @satisfies REQ-429
|
|
883
883
|
*/
|
|
884
884
|
interface FlushedPromptErrorRecord {
|
|
@@ -916,12 +916,12 @@ function getProcessScopedPromptErrorCaptureStore(): ProcessScopedPromptErrorCapt
|
|
|
916
916
|
}
|
|
917
917
|
|
|
918
918
|
/**
|
|
919
|
-
* @brief Builds
|
|
920
|
-
* @details Combines the write-time `YYYYMMDDHHMMSSmmm` timestamp, the invokable `req-*` command name, and the failing provider response status into the `<timestamp>-<req-command>-error-<status>` shape so every saved error-stage prompt
|
|
919
|
+
* @brief Builds the shared extensionless provider error base filename for one bundled prompt.
|
|
920
|
+
* @details Combines the write-time `YYYYMMDDHHMMSSmmm` timestamp, the invokable `req-*` command name, and the failing provider response status into the `<timestamp>-<req-command>-error-<status>` base shape so every saved error-stage prompt artifact stays attributable to the originating `/req-*` prompt and its session error code. This extensionless base is the suffix-attachment anchor for the `-response.txt`, `-replay.json`, and `-session.jsonl` variants; the flushed raw provider-request JSON payload appends its content-coherent `.json` extension through `formatPromptDebugErrorPayloadFileName(...)`. Runtime is O(1). No external state is mutated.
|
|
921
921
|
* @param[in] promptName {PromptCommandName} Bundled prompt name.
|
|
922
922
|
* @param[in] errorCode {number} Failing provider response status, such as `400`.
|
|
923
923
|
* @param[in] date {Date} Write-time timestamp. Defaults to the current wall-clock time.
|
|
924
|
-
* @return {string}
|
|
924
|
+
* @return {string} Extensionless provider error base filename in the `<timestamp>-<req-command>-error-<status>` shape.
|
|
925
925
|
* @satisfies REQ-423
|
|
926
926
|
*/
|
|
927
927
|
export function formatPromptDebugErrorFileName(
|
|
@@ -932,21 +932,38 @@ export function formatPromptDebugErrorFileName(
|
|
|
932
932
|
return `${formatPromptDebugTimestamp(date)}-${getDebugPromptName(promptName)}-error-${errorCode}`;
|
|
933
933
|
}
|
|
934
934
|
|
|
935
|
+
/**
|
|
936
|
+
* @brief Builds one provider error payload debug filename for one bundled prompt.
|
|
937
|
+
* @details Appends the `.json` extension to the shared extensionless `<timestamp>-<req-command>-error-<status>` base so the flushed raw provider-request JSON payload keeps a filename extension coherent with its JSON content per REQ-435 while staying attributable to the exact failing request. 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 sibling error artifacts. Defaults to the current wall-clock time.
|
|
941
|
+
* @return {string} Provider error payload filename in the `<timestamp>-<req-command>-error-<status>.json` shape.
|
|
942
|
+
* @satisfies REQ-423, REQ-431, REQ-435
|
|
943
|
+
*/
|
|
944
|
+
export function formatPromptDebugErrorPayloadFileName(
|
|
945
|
+
promptName: PromptCommandName,
|
|
946
|
+
errorCode: number,
|
|
947
|
+
date = new Date(),
|
|
948
|
+
): string {
|
|
949
|
+
return `${formatPromptDebugErrorFileName(promptName, errorCode, date)}.json`;
|
|
950
|
+
}
|
|
951
|
+
|
|
935
952
|
/**
|
|
936
953
|
* @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.
|
|
954
|
+
* @details Appends the `-response.txt` suffix to the shared extensionless `<timestamp>-<req-command>-error-<status>` shape so the persisted plain-text provider error message keeps a filename extension coherent with its text content per REQ-435 and 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
955
|
* @param[in] promptName {PromptCommandName} Bundled prompt name.
|
|
939
956
|
* @param[in] errorCode {number} Failing provider response status, such as `400`.
|
|
940
957
|
* @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
|
|
958
|
+
* @return {string} Provider error response filename in the `<timestamp>-<req-command>-error-<status>-response.txt` shape.
|
|
959
|
+
* @satisfies REQ-423, REQ-429, REQ-435
|
|
943
960
|
*/
|
|
944
961
|
export function formatPromptDebugErrorResponseFileName(
|
|
945
962
|
promptName: PromptCommandName,
|
|
946
963
|
errorCode: number,
|
|
947
964
|
date = new Date(),
|
|
948
965
|
): string {
|
|
949
|
-
return `${formatPromptDebugErrorFileName(promptName, errorCode, date)}-response`;
|
|
966
|
+
return `${formatPromptDebugErrorFileName(promptName, errorCode, date)}-response.txt`;
|
|
950
967
|
}
|
|
951
968
|
|
|
952
969
|
/**
|
|
@@ -966,6 +983,23 @@ export function formatPromptDebugErrorReplayFileName(
|
|
|
966
983
|
return `${formatPromptDebugErrorFileName(promptName, errorCode, date)}-replay.json`;
|
|
967
984
|
}
|
|
968
985
|
|
|
986
|
+
/**
|
|
987
|
+
* @brief Builds one provider error session-snapshot debug filename for one bundled prompt.
|
|
988
|
+
* @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.
|
|
989
|
+
* @param[in] promptName {PromptCommandName} Bundled prompt name.
|
|
990
|
+
* @param[in] errorCode {number} Failing provider response status, such as `400`.
|
|
991
|
+
* @param[in] date {Date} Write-time timestamp shared with the payload file. Defaults to the current wall-clock time.
|
|
992
|
+
* @return {string} Provider error session-snapshot filename in the `<timestamp>-<req-command>-error-<status>-session.jsonl` shape.
|
|
993
|
+
* @satisfies REQ-423, REQ-432
|
|
994
|
+
*/
|
|
995
|
+
export function formatPromptDebugErrorSessionFileName(
|
|
996
|
+
promptName: PromptCommandName,
|
|
997
|
+
errorCode: number,
|
|
998
|
+
date = new Date(),
|
|
999
|
+
): string {
|
|
1000
|
+
return `${formatPromptDebugErrorFileName(promptName, errorCode, date)}-session.jsonl`;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
969
1003
|
/**
|
|
970
1004
|
* @brief Resolves the failing HTTP status carried by one finalized agent message.
|
|
971
1005
|
* @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.
|
|
@@ -1112,15 +1146,17 @@ function resolveProviderReplayEndpoint(facts: PromptErrorReplayFacts): string |
|
|
|
1112
1146
|
|
|
1113
1147
|
/**
|
|
1114
1148
|
* @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
|
|
1149
|
+
* @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.
|
|
1116
1150
|
* @param[in] captured {CapturedPromptErrorPayload} Consumed capture supplying replay facts and artifact paths.
|
|
1117
1151
|
* @param[in] requestFileName {string} Payload filename written with the same write-time timestamp.
|
|
1152
|
+
* @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.
|
|
1118
1153
|
* @return {string} Newline-terminated replay sidecar JSON text.
|
|
1119
|
-
* @satisfies REQ-430
|
|
1154
|
+
* @satisfies REQ-430, REQ-433
|
|
1120
1155
|
*/
|
|
1121
1156
|
function buildPromptErrorReplaySidecarText(
|
|
1122
1157
|
captured: CapturedPromptErrorPayload,
|
|
1123
1158
|
requestFileName: string,
|
|
1159
|
+
sessionSnapshotFileName?: string,
|
|
1124
1160
|
): string {
|
|
1125
1161
|
const facts = captured.replayFacts;
|
|
1126
1162
|
const requestFilePath = path.join(captured.logDirectory, requestFileName);
|
|
@@ -1133,10 +1169,13 @@ function buildPromptErrorReplaySidecarText(
|
|
|
1133
1169
|
if (captured.promptDebugFile !== undefined && facts.modelId !== undefined) {
|
|
1134
1170
|
replayCommands.piOneShot = `pi --no-session --model '${facts.modelId}' --thinking ${facts.thinkingLevel ?? "off"} -p "$(cat '${captured.promptDebugFile}')"`;
|
|
1135
1171
|
}
|
|
1136
|
-
|
|
1172
|
+
const sessionForkFile = sessionSnapshotFileName !== undefined
|
|
1173
|
+
? path.join(captured.logDirectory, sessionSnapshotFileName)
|
|
1174
|
+
: facts.executionSessionFile;
|
|
1175
|
+
if (sessionForkFile !== undefined) {
|
|
1137
1176
|
const modelFlags = facts.modelId === undefined ? "" : ` --model '${facts.modelId}'`;
|
|
1138
1177
|
const thinkingFlags = facts.thinkingLevel === undefined ? "" : ` --thinking ${facts.thinkingLevel}`;
|
|
1139
|
-
replayCommands.piFork = `pi --fork '${
|
|
1178
|
+
replayCommands.piFork = `pi --fork '${sessionForkFile}'${modelFlags}${thinkingFlags}`;
|
|
1140
1179
|
}
|
|
1141
1180
|
const sidecar = {
|
|
1142
1181
|
provider: facts.provider ?? null,
|
|
@@ -1156,11 +1195,11 @@ function buildPromptErrorReplaySidecarText(
|
|
|
1156
1195
|
|
|
1157
1196
|
/**
|
|
1158
1197
|
* @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
|
|
1198
|
+
* @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>.json` file that stays raw provider-request JSON usable with `curl --data @`, the provider error text into one identically timestamped `-response.txt` 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.txt` 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.
|
|
1160
1199
|
* @param[in] errorCode {number} Failing provider response status, such as `400`.
|
|
1161
1200
|
* @param[in] errorResponseText {string | undefined} Finalized provider error message text when already available; the `-response` file is written later when omitted.
|
|
1162
1201
|
* @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
|
|
1202
|
+
* @satisfies REQ-422, REQ-423, REQ-424, REQ-425, REQ-429, REQ-430, REQ-431, REQ-432, REQ-433, REQ-435
|
|
1164
1203
|
*/
|
|
1165
1204
|
export function flushCapturedPromptErrorPayload(errorCode: number, errorResponseText?: string): boolean {
|
|
1166
1205
|
const captureStore = getProcessScopedPromptErrorCaptureStore();
|
|
@@ -1170,7 +1209,7 @@ export function flushCapturedPromptErrorPayload(errorCode: number, errorResponse
|
|
|
1170
1209
|
return false;
|
|
1171
1210
|
}
|
|
1172
1211
|
const writeTime = new Date();
|
|
1173
|
-
const payloadFileName =
|
|
1212
|
+
const payloadFileName = formatPromptDebugErrorPayloadFileName(captured.promptName, errorCode, writeTime);
|
|
1174
1213
|
try {
|
|
1175
1214
|
fs.mkdirSync(captured.logDirectory, { recursive: true });
|
|
1176
1215
|
fs.writeFileSync(
|
|
@@ -1185,16 +1224,20 @@ export function flushCapturedPromptErrorPayload(errorCode: number, errorResponse
|
|
|
1185
1224
|
errorResponseText,
|
|
1186
1225
|
);
|
|
1187
1226
|
}
|
|
1227
|
+
const sessionSnapshotFileName = copyPromptErrorSessionSnapshotFile(
|
|
1228
|
+
captured,
|
|
1229
|
+
formatPromptDebugErrorSessionFileName(captured.promptName, errorCode, writeTime),
|
|
1230
|
+
);
|
|
1188
1231
|
fs.writeFileSync(
|
|
1189
1232
|
path.join(captured.logDirectory, formatPromptDebugErrorReplayFileName(captured.promptName, errorCode, writeTime)),
|
|
1190
|
-
buildPromptErrorReplaySidecarText(captured, payloadFileName),
|
|
1233
|
+
buildPromptErrorReplaySidecarText(captured, payloadFileName, sessionSnapshotFileName),
|
|
1191
1234
|
"utf8",
|
|
1192
1235
|
);
|
|
1193
1236
|
captureStore.lastFlushedPromptError = {
|
|
1194
1237
|
promptName: captured.promptName,
|
|
1195
1238
|
errorCode,
|
|
1196
1239
|
logDirectory: captured.logDirectory,
|
|
1197
|
-
baseFileName:
|
|
1240
|
+
baseFileName: formatPromptDebugErrorFileName(captured.promptName, errorCode, writeTime),
|
|
1198
1241
|
};
|
|
1199
1242
|
return true;
|
|
1200
1243
|
} catch {
|
|
@@ -1220,9 +1263,34 @@ function writePromptProviderErrorResponseFile(
|
|
|
1220
1263
|
fs.writeFileSync(path.join(logDirectory, fileName), `${errorResponseText}\n`, "utf8");
|
|
1221
1264
|
}
|
|
1222
1265
|
|
|
1266
|
+
/**
|
|
1267
|
+
* @brief Copies the retained execution-session file beside one flushed provider error payload.
|
|
1268
|
+
* @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.
|
|
1269
|
+
* @param[in] captured {CapturedPromptErrorPayload} Consumed capture supplying the log directory and execution-session replay facts.
|
|
1270
|
+
* @param[in] snapshotFileName {string} Session-snapshot filename sharing the payload write-time timestamp.
|
|
1271
|
+
* @return {string | undefined} Written snapshot filename when the copy succeeds; otherwise `undefined`.
|
|
1272
|
+
* @satisfies REQ-432
|
|
1273
|
+
*/
|
|
1274
|
+
function copyPromptErrorSessionSnapshotFile(
|
|
1275
|
+
captured: CapturedPromptErrorPayload,
|
|
1276
|
+
snapshotFileName: string,
|
|
1277
|
+
): string | undefined {
|
|
1278
|
+
const sessionFile = captured.replayFacts.executionSessionFile;
|
|
1279
|
+
if (sessionFile === undefined || sessionFile.length === 0) {
|
|
1280
|
+
return undefined;
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
fs.mkdirSync(captured.logDirectory, { recursive: true });
|
|
1284
|
+
fs.copyFileSync(sessionFile, path.join(captured.logDirectory, snapshotFileName));
|
|
1285
|
+
return snapshotFileName;
|
|
1286
|
+
} catch {
|
|
1287
|
+
return undefined;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1223
1291
|
/**
|
|
1224
1292
|
* @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.
|
|
1293
|
+
* @details Providers that fire `after_provider_response` before surfacing their error text flush the payload without a `-response.txt` 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
1294
|
* @param[in] errorCode {number} Failing provider response status carried by the finalized error message.
|
|
1227
1295
|
* @param[in] errorResponseText {string} Finalized provider error message text.
|
|
1228
1296
|
* @return {boolean} `true` when the response file is written; otherwise `false`.
|
|
@@ -1242,7 +1310,7 @@ export function writeFlushedPromptProviderErrorResponse(
|
|
|
1242
1310
|
try {
|
|
1243
1311
|
writePromptProviderErrorResponseFile(
|
|
1244
1312
|
record.logDirectory,
|
|
1245
|
-
`${record.baseFileName}-response`,
|
|
1313
|
+
`${record.baseFileName}-response.txt`,
|
|
1246
1314
|
errorResponseText,
|
|
1247
1315
|
);
|
|
1248
1316
|
return true;
|
|
@@ -325,7 +325,15 @@ function readReqResetSessionHeader(sessionFile: string): { cwd?: string; parentS
|
|
|
325
325
|
|
|
326
326
|
/**
|
|
327
327
|
* @brief Resolves the original base-path session file from the active session header.
|
|
328
|
-
* @details Reads the current session file exposed by the supplied command context
|
|
328
|
+
* @details Reads the current session file exposed by the supplied command context and accepts one
|
|
329
|
+
* persisted `parentSession` link as the reserved original base-path session under two conditions:
|
|
330
|
+
* when the linked file already exists on disk its own header `cwd` must remain inside the main base
|
|
331
|
+
* path, and when the linked file was never flushed to disk (pi persists session files lazily on the
|
|
332
|
+
* first entry append) the link itself proves the reserved original-session identity so the caller
|
|
333
|
+
* can materialize the base session before switching. Unpersisted links are accepted only when their
|
|
334
|
+
* parent directory already exists so recovery never writes session files into arbitrary locations.
|
|
335
|
+
* Runtime is O(n) in session-header size plus bounded filesystem probes. No external state is
|
|
336
|
+
* mutated.
|
|
329
337
|
* @param[in] activeContext {ReqResetCommandContext | undefined} Session-bound command context whose current session file links the base-path session.
|
|
330
338
|
* @param[in] basePath {string} Absolute main repository base path that must contain the parent session cwd.
|
|
331
339
|
* @return {string | undefined} Original base-path session file or `undefined` when unavailable.
|
|
@@ -346,10 +354,15 @@ function resolveReqResetParentSessionFile(
|
|
|
346
354
|
return undefined;
|
|
347
355
|
}
|
|
348
356
|
const header = readReqResetSessionHeader(path.resolve(currentSessionFile));
|
|
349
|
-
if (header?.parentSession === undefined
|
|
357
|
+
if (header?.parentSession === undefined) {
|
|
350
358
|
return undefined;
|
|
351
359
|
}
|
|
352
360
|
const resolvedParentSession = path.resolve(header.parentSession);
|
|
361
|
+
if (!fs.existsSync(resolvedParentSession)) {
|
|
362
|
+
return fs.existsSync(path.dirname(resolvedParentSession))
|
|
363
|
+
? resolvedParentSession
|
|
364
|
+
: undefined;
|
|
365
|
+
}
|
|
353
366
|
const parentHeader = readReqResetSessionHeader(resolvedParentSession);
|
|
354
367
|
if (parentHeader?.cwd === undefined || !fs.existsSync(parentHeader.cwd)) {
|
|
355
368
|
return undefined;
|
|
@@ -359,6 +372,93 @@ function resolveReqResetParentSessionFile(
|
|
|
359
372
|
: undefined;
|
|
360
373
|
}
|
|
361
374
|
|
|
375
|
+
/**
|
|
376
|
+
* @brief Guarantees one switchable persisted base-path session file for `req-reset` re-anchoring.
|
|
377
|
+
* @details Leaves existing session files untouched and, when the resolved base session file was
|
|
378
|
+
* never flushed to disk (pi persists session files lazily on the first entry append), materializes
|
|
379
|
+
* one minimal pi JSONL session header whose `cwd` equals the main base path using the same header
|
|
380
|
+
* contract as pi `SessionManager.newSession(...)`/`forkFrom(...)` and the extension-owned execution
|
|
381
|
+
* session snapshots. Materialization is restricted to existing session directories so recovery
|
|
382
|
+
* never writes into arbitrary locations, and any write failure degrades to `false` so the caller
|
|
383
|
+
* surfaces the re-anchor failure instead of silently deleting the anchoring worktree.
|
|
384
|
+
* Runtime is O(p) in path length plus one bounded filesystem probe and at most one file write.
|
|
385
|
+
* Side effects include one conditional session-file creation.
|
|
386
|
+
* @param[in] sessionFile {string} Resolved base-path session file path.
|
|
387
|
+
* @param[in] basePath {string} Absolute main repository base path recorded in the materialized header.
|
|
388
|
+
* @return {boolean} `true` when a persisted session file exists or was materialized; `false` when the session directory is missing or the write fails.
|
|
389
|
+
*/
|
|
390
|
+
function ensureReqResetBaseSessionFile(sessionFile: string, basePath: string): boolean {
|
|
391
|
+
const normalizedSessionFile = path.resolve(sessionFile);
|
|
392
|
+
if (fs.existsSync(normalizedSessionFile)) {
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
if (!fs.existsSync(path.dirname(normalizedSessionFile))) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
const sessionHeader = {
|
|
399
|
+
type: "session",
|
|
400
|
+
version: 3,
|
|
401
|
+
id: `req-reset-${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`,
|
|
402
|
+
timestamp: new Date().toISOString(),
|
|
403
|
+
cwd: path.resolve(basePath),
|
|
404
|
+
};
|
|
405
|
+
try {
|
|
406
|
+
fs.writeFileSync(normalizedSessionFile, `${JSON.stringify(sessionHeader)}\n`, "utf8");
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
return fs.existsSync(normalizedSessionFile);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* @brief Reads one live session-bound context session directory while tolerating stale contexts.
|
|
415
|
+
* @details Real pi command contexts resolve `sessionManager` through getter-only accessors whose
|
|
416
|
+
* reads throw the documented stale-extension-context error after session replacement, so the
|
|
417
|
+
* session-directory probe must degrade stale reads to `undefined`. Runtime is O(1). No external
|
|
418
|
+
* state is mutated.
|
|
419
|
+
* @param[in] activeContext {ReqResetCommandContext | undefined} Candidate session-bound context.
|
|
420
|
+
* @return {string | undefined} Live session directory when readable; `undefined` when absent or stale.
|
|
421
|
+
*/
|
|
422
|
+
function readReqResetContextSessionDir(activeContext: ReqResetCommandContext | undefined): string | undefined {
|
|
423
|
+
try {
|
|
424
|
+
const sessionDir = typeof activeContext?.sessionManager?.getSessionDir === "function"
|
|
425
|
+
? activeContext.sessionManager.getSessionDir()
|
|
426
|
+
: undefined;
|
|
427
|
+
return typeof sessionDir === "string" && sessionDir !== "" ? sessionDir : undefined;
|
|
428
|
+
} catch {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* @brief Allocates one fresh persisted base-path session file for `req-reset` re-anchoring.
|
|
435
|
+
* @details Mirrors pi `/new` semantics for recovery situations without any resolvable original or
|
|
436
|
+
* recent base session: reserves one new session file path inside the live session directory
|
|
437
|
+
* through `SessionManager.create(...)` so the caller can materialize its header with the main base
|
|
438
|
+
* path and switch the pi CLI onto it before generated worktrees are removed. Allocation is
|
|
439
|
+
* restricted to existing session directories and any failure degrades to `undefined`. Runtime is
|
|
440
|
+
* O(p) in path length plus one bounded filesystem probe. No external state is mutated beyond the
|
|
441
|
+
* pi session-manager reservation.
|
|
442
|
+
* @param[in] basePath {string} Absolute main repository base path for the reserved session.
|
|
443
|
+
* @param[in] sessionDir {string | undefined} Live session directory reused for the reserved file.
|
|
444
|
+
* @return {string | undefined} Reserved session file path or `undefined` when allocation is impossible.
|
|
445
|
+
*/
|
|
446
|
+
function allocateReqResetBaseSessionFile(
|
|
447
|
+
basePath: string,
|
|
448
|
+
sessionDir: string | undefined,
|
|
449
|
+
): string | undefined {
|
|
450
|
+
if (typeof sessionDir !== "string" || sessionDir === "" || !fs.existsSync(sessionDir)) {
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const sessionManager = SessionManager.create(path.resolve(basePath), sessionDir);
|
|
455
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
456
|
+
return typeof sessionFile === "string" && sessionFile !== "" ? path.resolve(sessionFile) : undefined;
|
|
457
|
+
} catch {
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
362
462
|
/**
|
|
363
463
|
* @brief Resolves the most recent persisted session file rooted at the main base path.
|
|
364
464
|
* @details Uses the pi SDK default session discovery for the supplied cwd and returns the persisted session file only when it already exists on disk, so the pi CLI can resume the recent base-path session instead of being created a fresh one. Runtime is O(s) in session-directory listing cost. No external state is mutated.
|
|
@@ -408,20 +508,51 @@ function reqResetExecutionPathNeedsRedirect(
|
|
|
408
508
|
});
|
|
409
509
|
}
|
|
410
510
|
|
|
511
|
+
/**
|
|
512
|
+
* @brief Reads one live session-bound context cwd while tolerating stale contexts.
|
|
513
|
+
* @details Real pi command contexts expose `cwd` as a getter-only accessor whose read throws the
|
|
514
|
+
* documented stale-extension-context error after session replacement, so every `req-reset` probe
|
|
515
|
+
* must degrade stale reads to `undefined` instead of aborting the whole recovery. Runtime is O(1).
|
|
516
|
+
* No external state is mutated.
|
|
517
|
+
* @param[in] activeContext {ReqResetCommandContext | undefined} Candidate session-bound context.
|
|
518
|
+
* @return {string | undefined} Live context cwd when readable; `undefined` when absent or stale.
|
|
519
|
+
*/
|
|
520
|
+
function readReqResetContextCwd(activeContext: ReqResetCommandContext | undefined): string | undefined {
|
|
521
|
+
try {
|
|
522
|
+
return typeof activeContext?.cwd === "string" && activeContext.cwd !== ""
|
|
523
|
+
? activeContext.cwd
|
|
524
|
+
: undefined;
|
|
525
|
+
} catch {
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
411
530
|
/**
|
|
412
531
|
* @brief Returns the pi CLI to the main base path before generated worktrees are removed.
|
|
413
|
-
* @details When the host process cwd or the supplied context cwd is still anchored inside a
|
|
532
|
+
* @details When the host process cwd or the supplied context cwd is still anchored inside a
|
|
533
|
+
* generated worktree targeted for deletion or points at a deleted path, best-effort switches the
|
|
534
|
+
* active pi session back to the original or recent base-path session, guaranteeing the resolved
|
|
535
|
+
* base session file is persisted through `ensureReqResetBaseSessionFile(...)` before switching so
|
|
536
|
+
* lazily-flushed reserved sessions still re-anchor the pi CLI onto the main base path. Re-anchors
|
|
537
|
+
* `process.cwd()`, clears the runtime worktree path state so no post-cleanup surface keeps probing
|
|
538
|
+
* the removed worktree directory, and reports any session-switch failure through the returned
|
|
539
|
+
* re-anchor diagnostic so the caller can surface it and retain anchoring worktrees instead of
|
|
540
|
+
* silently orphaning the live session at a deleted directory. Context `cwd` mirrors are probed
|
|
541
|
+
* stale-safely because pi contexts expose getter-only accessors that `Reflect.set(...)` cannot
|
|
542
|
+
* mutate. Runtime is dominated by one optional session switch plus bounded filesystem probes.
|
|
543
|
+
* Side effects include active-session replacement, host-process cwd mutation, and one conditional
|
|
544
|
+
* base-session file materialization.
|
|
414
545
|
* @param[in] basePath {string} Absolute main repository base path.
|
|
415
546
|
* @param[in,out] activeContext {ReqResetCommandContext | undefined} Mutated session context mirror and potential replacement-session context.
|
|
416
547
|
* @param[in] matchingWorktreeRoots {string[]} Absolute generated worktree roots targeted for deletion.
|
|
417
|
-
* @return {Promise<ReqResetCommandContext | undefined>}
|
|
418
|
-
* @satisfies REQ-257, REQ-307
|
|
548
|
+
* @return {Promise<{ context: ReqResetCommandContext | undefined; reanchorErrorMessage?: string }>} Re-anchored context plus one optional re-anchor failure diagnostic.
|
|
549
|
+
* @satisfies REQ-257, REQ-307, REQ-308, REQ-313
|
|
419
550
|
*/
|
|
420
551
|
async function redirectReqResetExecutionToMainBranch(
|
|
421
552
|
basePath: string,
|
|
422
553
|
activeContext: ReqResetCommandContext | undefined,
|
|
423
554
|
matchingWorktreeRoots: string[],
|
|
424
|
-
): Promise<ReqResetCommandContext | undefined> {
|
|
555
|
+
): Promise<{ context: ReqResetCommandContext | undefined; reanchorErrorMessage?: string }> {
|
|
425
556
|
const normalizedBasePath = path.resolve(basePath);
|
|
426
557
|
let processCwd: string | undefined;
|
|
427
558
|
try {
|
|
@@ -429,7 +560,7 @@ async function redirectReqResetExecutionToMainBranch(
|
|
|
429
560
|
} catch {
|
|
430
561
|
processCwd = undefined;
|
|
431
562
|
}
|
|
432
|
-
const contextCwd =
|
|
563
|
+
const contextCwd = readReqResetContextCwd(activeContext);
|
|
433
564
|
const processNeedsRedirect = reqResetExecutionPathNeedsRedirect(
|
|
434
565
|
processCwd,
|
|
435
566
|
normalizedBasePath,
|
|
@@ -441,17 +572,26 @@ async function redirectReqResetExecutionToMainBranch(
|
|
|
441
572
|
matchingWorktreeRoots,
|
|
442
573
|
);
|
|
443
574
|
if (!processNeedsRedirect && !contextNeedsRedirect) {
|
|
444
|
-
return activeContext;
|
|
575
|
+
return { context: activeContext };
|
|
445
576
|
}
|
|
577
|
+
let reanchorErrorMessage: string | undefined;
|
|
446
578
|
if (typeof activeContext?.switchSession === "function") {
|
|
447
|
-
|
|
579
|
+
let mainSessionFile = resolveReqResetParentSessionFile(activeContext, normalizedBasePath)
|
|
448
580
|
?? resolveReqResetMainSessionFile(normalizedBasePath);
|
|
449
|
-
if (mainSessionFile
|
|
581
|
+
if (mainSessionFile === undefined) {
|
|
582
|
+
mainSessionFile = allocateReqResetBaseSessionFile(
|
|
583
|
+
normalizedBasePath,
|
|
584
|
+
readReqResetContextSessionDir(activeContext),
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
if (mainSessionFile !== undefined && ensureReqResetBaseSessionFile(mainSessionFile, normalizedBasePath)) {
|
|
450
588
|
try {
|
|
451
589
|
activeContext = await switchPromptCommandSession(mainSessionFile, activeContext);
|
|
452
|
-
} catch {
|
|
453
|
-
|
|
590
|
+
} catch (error) {
|
|
591
|
+
reanchorErrorMessage = `ERROR: req-reset could not switch the pi session back to ${normalizedBasePath}: ${error instanceof Error ? error.message : String(error)}`;
|
|
454
592
|
}
|
|
593
|
+
} else {
|
|
594
|
+
reanchorErrorMessage = `ERROR: req-reset found no persisted base-path session to restore for ${normalizedBasePath}.`;
|
|
455
595
|
}
|
|
456
596
|
}
|
|
457
597
|
try {
|
|
@@ -461,7 +601,7 @@ async function redirectReqResetExecutionToMainBranch(
|
|
|
461
601
|
}
|
|
462
602
|
if (activeContext !== undefined) {
|
|
463
603
|
try {
|
|
464
|
-
if (path.resolve(activeContext
|
|
604
|
+
if (path.resolve(readReqResetContextCwd(activeContext) ?? "") !== normalizedBasePath) {
|
|
465
605
|
Reflect.set(activeContext, "cwd", normalizedBasePath);
|
|
466
606
|
}
|
|
467
607
|
} catch {
|
|
@@ -470,7 +610,38 @@ async function redirectReqResetExecutionToMainBranch(
|
|
|
470
610
|
}
|
|
471
611
|
setRuntimeContextPath(normalizedBasePath);
|
|
472
612
|
setRuntimeWorktreePathState({});
|
|
473
|
-
return activeContext;
|
|
613
|
+
return { context: activeContext, reanchorErrorMessage };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* @brief Lists matching worktree roots that still anchor the live session cwd.
|
|
618
|
+
* @details Used only after a failed session re-anchor so `req-reset` retains the worktree directory
|
|
619
|
+
* the live pi session is still checked out in, preventing the session cwd from being orphaned at a
|
|
620
|
+
* deleted directory (which renders `branch:unknown`). Stale or unreadable context cwds and paths
|
|
621
|
+
* that no longer exist yield an empty list because the session cannot be probed reliably. Runtime
|
|
622
|
+
* is O(r) in matched-worktree count plus one bounded filesystem probe. No external state is
|
|
623
|
+
* mutated.
|
|
624
|
+
* @param[in] activeContext {ReqResetCommandContext | undefined} Session-bound context whose live cwd may still anchor one matching worktree.
|
|
625
|
+
* @param[in] matchingWorktreeRoots {string[]} Absolute generated worktree roots targeted for deletion.
|
|
626
|
+
* @return {string[]} Absolute matching worktree roots that still contain the live session cwd.
|
|
627
|
+
*/
|
|
628
|
+
function resolveReqResetAnchoredWorktreeRoots(
|
|
629
|
+
activeContext: ReqResetCommandContext | undefined,
|
|
630
|
+
matchingWorktreeRoots: string[],
|
|
631
|
+
): string[] {
|
|
632
|
+
const contextCwd = readReqResetContextCwd(activeContext);
|
|
633
|
+
if (contextCwd === undefined) {
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
const normalizedContextCwd = path.resolve(contextCwd);
|
|
637
|
+
if (!fs.existsSync(normalizedContextCwd)) {
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
return matchingWorktreeRoots.filter((worktreeRootPath) => {
|
|
641
|
+
const normalizedRoot = path.resolve(worktreeRootPath);
|
|
642
|
+
return normalizedContextCwd === normalizedRoot
|
|
643
|
+
|| normalizedContextCwd.startsWith(`${normalizedRoot}${path.sep}`);
|
|
644
|
+
});
|
|
474
645
|
}
|
|
475
646
|
|
|
476
647
|
/**
|
|
@@ -547,7 +718,7 @@ export function prepareReqResetCommandExecution(
|
|
|
547
718
|
|
|
548
719
|
/**
|
|
549
720
|
* @brief Executes the specialized `req-reset` recovery and cleanup workflow.
|
|
550
|
-
* @details Preserves the execution-session transcript into the original session file when a worktree-backed prompt execution plan is still available, restores the original session-backed `base-path` through the shared prompt-command restoration helper, ensures the main repository HEAD is on the main branch so generated branch deletion cannot hit git's checked-out-worktree guard, enumerates every matching sibling worktree, returns the pi CLI execution surfaces to the main base path before any matching worktree directory is removed, force-removes every matching sibling worktree directory, force-removes every remaining matching local branch, restores deleted live cwd surfaces, and aggregates any failure diagnostics without rolling back successful cleanup steps. Runtime is dominated by session switching plus git subprocess execution. Side effects include session-file reads and writes, active-session replacement, host-process cwd mutation, branch switching, worktree deletion, branch deletion, and filesystem reads.
|
|
721
|
+
* @details Preserves the execution-session transcript into the original session file when a worktree-backed prompt execution plan is still available, restores the original session-backed `base-path` through the shared prompt-command restoration helper, ensures the main repository HEAD is on the main branch so generated branch deletion cannot hit git's checked-out-worktree guard, enumerates every matching sibling worktree, returns the pi CLI execution surfaces to the main base path (materializing the reserved base session when pi never flushed it) before any matching worktree directory is removed, force-removes every matching sibling worktree directory, force-removes every remaining matching local branch, retains the worktree plus branch that still anchor the live pi session whenever the session re-anchor fails so the session cwd is never orphaned at a deleted directory, restores deleted live cwd surfaces, and aggregates any failure diagnostics without rolling back successful cleanup steps. Runtime is dominated by session switching plus git subprocess execution. Side effects include session-file reads and writes, active-session replacement, host-process cwd mutation, branch switching, worktree deletion, branch deletion, and filesystem reads.
|
|
551
722
|
* @param[in] plan {ReqResetCommandPlan} Prepared recovery and cleanup plan.
|
|
552
723
|
* @param[in] ctx {ReqResetCommandContext | undefined} Optional session-bound command context.
|
|
553
724
|
* @return {Promise<ReqResetCommandExecutionResult>} Recovery and cleanup outcome facts.
|
|
@@ -604,14 +775,30 @@ export async function executeReqResetCommandExecution(
|
|
|
604
775
|
errorMessages.push(error instanceof Error ? error.message : String(error));
|
|
605
776
|
}
|
|
606
777
|
|
|
607
|
-
|
|
778
|
+
const redirectOutcome = await redirectReqResetExecutionToMainBranch(
|
|
608
779
|
plan.basePath,
|
|
609
780
|
activeContext,
|
|
610
781
|
matchingWorktreeRoots,
|
|
611
782
|
);
|
|
783
|
+
activeContext = redirectOutcome.context;
|
|
784
|
+
if (redirectOutcome.reanchorErrorMessage !== undefined) {
|
|
785
|
+
errorMessages.push(redirectOutcome.reanchorErrorMessage);
|
|
786
|
+
}
|
|
787
|
+
const retainedWorktreeDirs = new Set(
|
|
788
|
+
redirectOutcome.reanchorErrorMessage !== undefined
|
|
789
|
+
? resolveReqResetAnchoredWorktreeRoots(activeContext, matchingWorktreeRoots)
|
|
790
|
+
.map((worktreeRootPath) => path.basename(worktreeRootPath))
|
|
791
|
+
: [],
|
|
792
|
+
);
|
|
612
793
|
|
|
613
794
|
for (const worktreeRootPath of matchingWorktreeRoots) {
|
|
614
795
|
const worktreeDir = path.basename(worktreeRootPath);
|
|
796
|
+
if (retainedWorktreeDirs.has(worktreeDir)) {
|
|
797
|
+
errorMessages.push(
|
|
798
|
+
`ERROR: req-reset retained worktree ${worktreeDir} because the pi session could not be re-anchored to ${plan.basePath}.`,
|
|
799
|
+
);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
615
802
|
try {
|
|
616
803
|
deletePromptWorktree(plan.basePath, worktreeDir, worktreeRootPath);
|
|
617
804
|
removedWorktreeDirs.push(worktreeDir);
|
|
@@ -623,6 +810,12 @@ export async function executeReqResetCommandExecution(
|
|
|
623
810
|
try {
|
|
624
811
|
const matchingBranchNames = listReqResetMatchingBranchNames(plan.gitPath, plan.worktreeNamePattern);
|
|
625
812
|
for (const branchName of matchingBranchNames) {
|
|
813
|
+
if (retainedWorktreeDirs.has(branchName)) {
|
|
814
|
+
errorMessages.push(
|
|
815
|
+
`ERROR: req-reset retained branch ${branchName} because its worktree still anchors the pi session.`,
|
|
816
|
+
);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
626
819
|
const deleteResult = runCapture(["git", "branch", "-D", branchName], plan.gitPath);
|
|
627
820
|
if (deleteResult.error || deleteResult.status !== 0) {
|
|
628
821
|
const diagnostic = deleteResult.stderr.trim()
|