Skip to content

Commit 15f7bdb

Browse files
authored
Merge pull request #691 from Yumiue/codex/gateway-plan-approval-rpc
fix(context): 显式标记空 Todo 状态
2 parents b41729a + 01b4064 commit 15f7bdb

10 files changed

Lines changed: 98 additions & 13 deletions

File tree

internal/context/source_todos.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@ func (todosSource) Sections(ctx context.Context, input BuildInput) ([]promptSect
2727
return nil, err
2828
}
2929
if len(input.Todos) == 0 {
30-
return nil, nil
30+
return []promptSection{
31+
{
32+
Title: "Todo State",
33+
Content: "None",
34+
},
35+
}, nil
3136
}
3237

3338
active := make([]agentsession.TodoItem, 0, len(input.Todos))
@@ -37,7 +42,12 @@ func (todosSource) Sections(ctx context.Context, input BuildInput) ([]promptSect
3742
}
3843
}
3944
if len(active) == 0 {
40-
return nil, nil
45+
return []promptSection{
46+
{
47+
Title: "Todo State",
48+
Content: "None",
49+
},
50+
}, nil
4151
}
4252

4353
sort.SliceStable(active, func(i, j int) bool {

internal/context/source_todos_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ func TestTodosSourceSectionsBoundaries(t *testing.T) {
7474
if err != nil {
7575
t.Fatalf("Sections() error = %v", err)
7676
}
77-
if sections != nil {
78-
t.Fatalf("Sections() = %+v, want nil", sections)
77+
if len(sections) != 1 || sections[0].Content != "None" {
78+
t.Fatalf("Sections() = %+v, want single section with 'None'", sections)
7979
}
8080

8181
ctx, cancel := stdcontext.WithCancel(stdcontext.Background())
@@ -100,8 +100,8 @@ func TestTodosSourceSectionsAllTerminal(t *testing.T) {
100100
if err != nil {
101101
t.Fatalf("Sections() error = %v", err)
102102
}
103-
if sections != nil {
104-
t.Fatalf("Sections() = %+v, want nil for all terminal todos", sections)
103+
if len(sections) != 1 || sections[0].Content != "None" {
104+
t.Fatalf("Sections() = %+v, want single section with 'None' for all terminal todos", sections)
105105
}
106106
}
107107

internal/promptasset/assets_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ func TestPlanModePromptTemplates(t *testing.T) {
106106
if !strings.Contains(PlanModePrompt("build_execute"), "create current-run required todos") {
107107
t.Fatalf("expected build prompt to require direct-build todo bootstrap")
108108
}
109+
if !strings.Contains(PlanModePrompt("build_execute"), "Todo State is attached as `None`") {
110+
t.Fatalf("expected build prompt to bootstrap when Todo State is None")
111+
}
109112
if !strings.Contains(PlanModePrompt("build_execute"), "simple conversational inputs") {
110113
t.Fatalf("expected build prompt to cover simple conversational completion")
111114
}

internal/promptasset/templates/context/plan_mode_build_execute.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ You are currently in build execution.
44
- If a current plan summary is attached, use it as guidance by default.
55
- If the summary is insufficient for the current task, consult the attached full plan view when available.
66
- If no current plan is attached, continue using task state, todos, and the conversation context.
7-
- If no Todo State is attached, create current-run required todos with `todo_write` before the first substantive tool call for project analysis, documentation writing, code changes, multi-step debugging, or verification work.
7+
- If no Todo State is attached, or Todo State is attached as `None`, create current-run required todos with `todo_write` before the first substantive tool call for project analysis, documentation writing, code changes, multi-step debugging, or verification work.
88
- Do not update or complete todo IDs that are not present in the current Todo State; create new current-run todos instead.
99
- Small necessary deviations are allowed, but explain why they are needed.
1010
- Do not create or rewrite the current full plan in this stage.

internal/runtime/controlplane/phase.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ var allowedRunStateTransitions = map[RunState]map[RunState]struct{}{
4646
RunStateVerify: {
4747
RunStateVerify: {},
4848
RunStatePlan: {},
49+
RunStateExecute: {},
4950
RunStateCompacting: {},
5051
RunStateWaitingUserQuestion: {},
5152
RunStateWaitingPermission: {},

internal/runtime/controlplane/phase_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func TestValidateRunStateTransitionMainlineAndGovernanceStates(t *testing.T) {
1313
{from: RunStatePlan, to: RunStateExecute},
1414
{from: RunStateExecute, to: RunStateVerify},
1515
{from: RunStateVerify, to: RunStatePlan},
16+
{from: RunStateVerify, to: RunStateExecute},
1617
{from: RunStatePlan, to: RunStateCompacting},
1718
{from: RunStateCompacting, to: RunStatePlan},
1819
{from: RunStateExecute, to: RunStateWaitingPermission},

web/src/stores/useRuntimeInsightStore.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,21 @@ describe('useRuntimeInsightStore', () => {
139139
expect(state.todoHistory.a).toBeDefined()
140140
})
141141

142+
it('applyTodoSnapshot can clear stale conflict on reset while preserving history', () => {
143+
const store = useRuntimeInsightStore.getState()
144+
store.setTodoSnapshot({
145+
items: [{ id: 'a', content: 'task a', status: 'in_progress', required: true, revision: 1 }],
146+
})
147+
store.setTodoConflict({ action: 'todo_conflict', reason: 'todo_not_found' })
148+
149+
store.applyTodoSnapshot({ items: [] }, { clearConflict: true })
150+
151+
const state = useRuntimeInsightStore.getState()
152+
expect(state.todoSnapshot?.items).toEqual([])
153+
expect(state.todoConflict).toBeNull()
154+
expect(state.todoHistory.a).toBeDefined()
155+
})
156+
142157
it('setTodoSnapshot accumulates todoHistory across replacements', () => {
143158
const store = useRuntimeInsightStore.getState()
144159
store.setTodoSnapshot({

web/src/stores/useRuntimeInsightStore.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ interface RuntimeInsightState {
4444
failVerification: (payload: VerificationFailedPayload) => void
4545
setAcceptanceDecision: (payload: AcceptanceDecidedPayload | null) => void
4646
setTodoSnapshot: (snapshot: TodoSnapshot | null) => void
47-
applyTodoSnapshot: (snapshot: TodoSnapshot | null) => void
47+
applyTodoSnapshot: (snapshot: TodoSnapshot | null, options?: { clearConflict?: boolean }) => void
4848
addTodoEvent: (event: TodoEventPayload) => void
4949
setTodoConflict: (event: TodoEventPayload | null) => void
5050
setBudgetChecked: (payload: BudgetCheckedPayload) => void
@@ -110,13 +110,13 @@ export const useRuntimeInsightStore = create<RuntimeInsightState>((set) => ({
110110
}
111111
return { todoSnapshot, todoConflict: null, todoHistory }
112112
}),
113-
applyTodoSnapshot: (todoSnapshot) => set((s) => {
113+
applyTodoSnapshot: (todoSnapshot, options) => set((s) => {
114114
const items = todoSnapshot?.items ?? []
115115
if (!todoSnapshot) {
116-
return { todoSnapshot: null }
116+
return options?.clearConflict ? { todoSnapshot: null, todoConflict: null } : { todoSnapshot: null }
117117
}
118118
if (items.length === 0) {
119-
return { todoSnapshot }
119+
return options?.clearConflict ? { todoSnapshot, todoConflict: null } : { todoSnapshot }
120120
}
121121
const now = Date.now()
122122
const todoHistory = { ...s.todoHistory }
@@ -128,7 +128,9 @@ export const useRuntimeInsightStore = create<RuntimeInsightState>((set) => ({
128128
firstSeenAt: prev?.firstSeenAt ?? now,
129129
}
130130
}
131-
return { todoSnapshot, todoHistory }
131+
return options?.clearConflict
132+
? { todoSnapshot, todoConflict: null, todoHistory }
133+
: { todoSnapshot, todoHistory }
132134
}),
133135
addTodoEvent: (event) => set((s) => ({ todoEvents: [...s.todoEvents, event] })),
134136
setTodoConflict: (todoConflict) => set({ todoConflict }),

web/src/utils/eventBridge.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,6 +1490,56 @@ describe("eventBridge", () => {
14901490
);
14911491
});
14921492

1493+
it("TodoSnapshotUpdated reset clears stale TodoConflict", () => {
1494+
const api = createMockGatewayAPI();
1495+
handleGatewayEvent(
1496+
{
1497+
type: EventType.TodoConflict,
1498+
payload: {
1499+
payload: {
1500+
runtime_event_type: EventType.TodoConflict,
1501+
payload: { action: "update", reason: "todo_not_found" },
1502+
},
1503+
},
1504+
session_id: "sess-1",
1505+
run_id: "run-1",
1506+
},
1507+
api,
1508+
);
1509+
expect(useRuntimeInsightStore.getState().todoConflict?.reason).toBe(
1510+
"todo_not_found",
1511+
);
1512+
1513+
handleGatewayEvent(
1514+
{
1515+
type: EventType.TodoSnapshotUpdated,
1516+
payload: {
1517+
payload: {
1518+
runtime_event_type: EventType.TodoSnapshotUpdated,
1519+
payload: {
1520+
action: "reset",
1521+
reason: "new_user_run",
1522+
items: [],
1523+
summary: {
1524+
total: 0,
1525+
required_total: 0,
1526+
required_completed: 0,
1527+
required_failed: 0,
1528+
required_open: 0,
1529+
},
1530+
},
1531+
},
1532+
},
1533+
session_id: "sess-1",
1534+
run_id: "run-2",
1535+
},
1536+
api,
1537+
);
1538+
1539+
expect(useRuntimeInsightStore.getState().todoConflict).toBeNull();
1540+
expect(useRuntimeInsightStore.getState().todoSnapshot?.items).toEqual([]);
1541+
});
1542+
14931543
it("TodoUpdated clears TodoConflict", () => {
14941544
const api = createMockGatewayAPI();
14951545
// Set conflict first

web/src/utils/eventBridge.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1026,10 +1026,13 @@ export function handleGatewayEvent(
10261026
if (payload) {
10271027
insightStore.addTodoEvent(payload);
10281028
if (payload.items) {
1029+
const clearConflict =
1030+
payload.action === "reset" ||
1031+
(payload.items.length === 0 && payload.summary?.total === 0);
10291032
insightStore.applyTodoSnapshot({
10301033
items: payload.items,
10311034
summary: payload.summary,
1032-
});
1035+
}, { clearConflict });
10331036
}
10341037
}
10351038
break;

0 commit comments

Comments
 (0)