Skip to content

Commit 7471132

Browse files
committed
fix(autofix-pr): extractBetween 支持 latest tag 截断时回溯到更早完整对
如果远端 agent 重试时写了完整 <autofix-result> 后又开了一个被截断的 第二个 tag, 旧实现只看 lastIndexOf(open) 然后找不到 close 就返回 null, 导致前面那个完整结果被丢弃。改为从尾向首遍历所有 open tag, 返回第一个 能配对的 open/close 对。 附带: - docs/features/remote-agent-completion-analysis.md: 9 处裸 fenced block 补 language tag (text/http), 修复 markdownlint MD040 警告 - 同文件: 两处"三选项" → "三个选项" 符合中文量词习惯
1 parent c3de3b0 commit 7471132

3 files changed

Lines changed: 34 additions & 16 deletions

File tree

docs/features/remote-agent-completion-analysis.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ type TaskStatus =
7171
7272
转换规则(terminal 三态不可再变,由 `isTerminalTaskStatus()` 守卫):
7373
74-
```
74+
```text
7575
pendingrunning : registerTask 推入后,或 teleport 成功
7676
runningcompleted: sessionStatus === 'archived' / completionChecker 返回非 null / result.subtype === 'success'
7777
runningfailed : result.subtype !== 'success' / 超时 / session 404
@@ -88,7 +88,7 @@ running → killed : 显式调用 task.kill()
8888
8989
完成回流路径:
9090
91-
```
91+
```text
9292
远端 session 完成
9393
poller 检测 (sessionStatus='archived'result 出现 或 completionChecker 返回值)
9494
enqueuePendingNotification({ value: xmlMessage, mode: 'task-notification' })
@@ -180,7 +180,7 @@ export async function archiveRemoteSession(sessionId: string, timeout = 10_000):
180180

181181
官方推荐 **cursor-based HTTP Polling**
182182

183-
```
183+
```http
184184
GET /v1/sessions/{id}/events?after_id={cursor}
185185
186186
Response:
@@ -331,7 +331,7 @@ usage?: {
331331

332332
### 2.1 状态机
333333

334-
```
334+
```text
335335
registerRemoteAgentTask()
336336
337337
@@ -524,15 +524,15 @@ setAppState(prev => ({
524524
// → REPL 检测到 ultraplanPendingChoice 后挂载 UltraplanChoiceDialog
525525
```
526526

527-
**用户交互**`UltraplanChoiceDialog` 三选项
527+
**用户交互**`UltraplanChoiceDialog` 三个选项
528528

529529
| 选项 | 行为 |
530530
|---|---|
531531
| Implement here | 注入 `<ultraplan>{plan}</ultraplan>` 到消息流(`UltraplanChoiceDialog.tsx:110-121`|
532532
| Start new session | clear conversation + plan 作为新 promptmode: `'prompt'`line 148-151|
533533
| Cancel | 写到 `{date}-ultraplan.md`line 154-159|
534534

535-
三选项共同收尾`UltraplanChoiceDialog.tsx:165-177`):
535+
三个选项共同收尾`UltraplanChoiceDialog.tsx:165-177`):
536536

537537
```typescript
538538
updateTaskState(taskId, setAppState, t => ({ ...t, status: 'completed' }))
@@ -742,7 +742,7 @@ enqueuePendingNotification({
742742

743743
**影响**
744744

745-
```
745+
```text
746746
用户跑 /autofix-pr 12 → singleton lock 被设
747747
CCR session 自然 archive → enqueueRemoteNotification 发完成通知
748748
监控状态在 monitorState 中仍记着 active monitor
@@ -901,7 +901,7 @@ clearActiveMonitor(frameworkTaskId)
901901
文件:`src/commands/autofix-pr/launchAutofixPr.ts` + `src/commands/autofix-pr/monitorState.ts`
902902

903903
修改前 `callAutofixPr` 流程:
904-
```
904+
```text
905905
1. createAutofixTeammate() 生成 teammate UUID
906906
2. setActiveMonitor({ taskId: teammateUUID, ... })
907907
3. teleportToRemote(...)
@@ -910,7 +910,7 @@ clearActiveMonitor(frameworkTaskId)
910910
```
911911

912912
修改后流程:
913-
```
913+
```text
914914
1. createAutofixTeammate() 生成 teammate UUID
915915
2. setActiveMonitor({ taskId: teammateUUID, ... })
916916
3. teleportToRemote(...)
@@ -1031,7 +1031,7 @@ if (result || sessionDone || reviewTimedOut) {
10311031

10321032
`initialMessage` 中加入指令:
10331033

1034-
```
1034+
```text
10351035
When you complete the autofix work,output the following XML tag as your final message:
10361036
10371037
<autofix-result>
@@ -1396,7 +1396,7 @@ ultrareview 的回流次重(内容回流但无 dialog),autofix-pr 的回流最
13961396

13971397
当前实现使用的 CCR API beta header:
13981398

1399-
```
1399+
```http
14001400
anthropic-beta: ccr-byoc-2025-07-29
14011401
```
14021402

src/commands/autofix-pr/__tests__/extractAutofixResult.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ describe('extractAutofixResultFromLog', () => {
8989
expect(extractAutofixResultFromLog(log)).toBeNull()
9090
})
9191

92+
test('returns earlier complete tag when latest open tag is truncated within the same block', () => {
93+
// Retry scenario: a full result was emitted, then a second result tag
94+
// started but got cut off. We should surface the earlier complete pair
95+
// rather than dropping the whole block.
96+
const complete = sampleTag('earlier complete result')
97+
const truncated = `<${AUTOFIX_RESULT_TAG}>\n<summary>truncated retry...`
98+
const log = [assistantTextMessage(`${complete}\n${truncated}`)]
99+
expect(extractAutofixResultFromLog(log)).toBe(complete)
100+
})
101+
92102
test('walks backwards so hook stdout from later in log wins over earlier assistant text', () => {
93103
const earlier = sampleTag('via assistant first')
94104
const later = sampleTag('via hook later')

src/commands/autofix-pr/extractAutofixResult.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,22 @@ export function extractAutofixResultFromLog(log: SDKMessage[]): string | null {
7171
return null
7272
}
7373

74+
// Walks open tags from latest to earliest, returning the first complete
75+
// open/close pair. Guards against a truncated final tag shadowing an
76+
// earlier complete pair within the same text block (e.g., a retry wrote a
77+
// full result, then the model started a second tag that got cut off).
7478
function extractBetween(
7579
text: string,
7680
open: string,
7781
close: string,
7882
): string | null {
79-
const start = text.lastIndexOf(open)
80-
if (start === -1) return null
81-
const end = text.indexOf(close, start + open.length)
82-
if (end === -1) return null
83-
return text.slice(start, end + close.length)
83+
let searchFrom = text.length
84+
while (searchFrom >= 0) {
85+
const start = text.lastIndexOf(open, searchFrom)
86+
if (start === -1) return null
87+
const end = text.indexOf(close, start + open.length)
88+
if (end !== -1) return text.slice(start, end + close.length)
89+
searchFrom = start - 1
90+
}
91+
return null
8492
}

0 commit comments

Comments
 (0)