Skip to content

Commit 882fad7

Browse files
authored
Merge pull request #631 from Yumiue/codex/fix-sqlite-session-concurrency
fix(session): 串行化 SQLite 会话写入
2 parents 678f1f6 + e2a18d4 commit 882fad7

11 files changed

Lines changed: 1082 additions & 41 deletions

internal/cli/gateway_runtime_bridge.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,26 @@ type runtimeSessionCreator interface {
4444
CreateSession(ctx context.Context, id string) (agentsession.Session, error)
4545
}
4646

47+
type runtimeSessionDeleter interface {
48+
SupportsSessionMutationBoundary() bool
49+
DeleteSession(ctx context.Context, sessionID string) error
50+
}
51+
52+
type runtimeSessionRenamer interface {
53+
SupportsSessionMutationBoundary() bool
54+
RenameSession(ctx context.Context, sessionID string, title string) error
55+
}
56+
57+
type runtimeSessionModelUpdater interface {
58+
SupportsSessionMutationBoundary() bool
59+
UpdateSessionModel(ctx context.Context, sessionID string, providerID string, modelID string) error
60+
}
61+
62+
type runtimeSessionsProviderModelSyncer interface {
63+
SupportsSessionMutationBoundary() bool
64+
SyncSessionsProviderModel(ctx context.Context, providerID string, modelID string) error
65+
}
66+
4767
type runtimeTodoLister interface {
4868
ListTodos(ctx context.Context, sessionID string) (agentruntime.TodoSnapshot, error)
4969
}
@@ -648,6 +668,12 @@ func (b *gatewayRuntimePortBridge) DeleteSession(ctx context.Context, input gate
648668
if sessionID == "" {
649669
return false, gateway.ErrRuntimeResourceNotFound
650670
}
671+
if deleter, ok := b.runtime.(runtimeSessionDeleter); ok {
672+
if err := deleter.DeleteSession(ctx, sessionID); err != nil {
673+
return false, err
674+
}
675+
return true, nil
676+
}
651677
if b.sessionStore == nil {
652678
return false, fmt.Errorf("gateway runtime bridge: session store is unavailable")
653679
}
@@ -670,6 +696,9 @@ func (b *gatewayRuntimePortBridge) RenameSession(ctx context.Context, input gate
670696
if title == "" {
671697
return fmt.Errorf("gateway runtime bridge: title is required for rename")
672698
}
699+
if renamer, ok := b.runtime.(runtimeSessionRenamer); ok {
700+
return renamer.RenameSession(ctx, sessionID, title)
701+
}
673702
if b.sessionStore == nil {
674703
return fmt.Errorf("gateway runtime bridge: session store is unavailable")
675704
}
@@ -919,6 +948,9 @@ func (b *gatewayRuntimePortBridge) SetSessionModel(ctx context.Context, input ga
919948
if err != nil {
920949
return err
921950
}
951+
if updater, ok := b.runtime.(runtimeSessionModelUpdater); ok {
952+
return updater.UpdateSessionModel(ctx, session.ID, providerID, modelID)
953+
}
922954
head := session.HeadSnapshot()
923955
head.Provider = providerID
924956
head.Model = modelID
@@ -1999,6 +2031,9 @@ func (b *gatewayRuntimePortBridge) SyncSessionsProviderModel(
19992031
if providerID == "" || modelID == "" {
20002032
return nil
20012033
}
2034+
if syncer, ok := b.runtime.(runtimeSessionsProviderModelSyncer); ok {
2035+
return syncer.SyncSessionsProviderModel(ctx, providerID, modelID)
2036+
}
20022037

20032038
summaries, err := b.runtime.ListSessions(ctx)
20042039
if err != nil {

internal/cli/gateway_runtime_bridge_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,46 @@ func (s *bridgeSessionStoreStub) UpdateSessionState(ctx context.Context, input a
367367
return nil
368368
}
369369

370+
type boundaryRuntimeStub struct {
371+
*runtimeStub
372+
deletedSessionID string
373+
renamedSessionID string
374+
renamedTitle string
375+
modelSessionID string
376+
modelProviderID string
377+
modelID string
378+
syncProviderID string
379+
syncModelID string
380+
}
381+
382+
func (s *boundaryRuntimeStub) SupportsSessionMutationBoundary() bool {
383+
return true
384+
}
385+
386+
func (s *boundaryRuntimeStub) DeleteSession(_ context.Context, sessionID string) error {
387+
s.deletedSessionID = sessionID
388+
return nil
389+
}
390+
391+
func (s *boundaryRuntimeStub) RenameSession(_ context.Context, sessionID string, title string) error {
392+
s.renamedSessionID = sessionID
393+
s.renamedTitle = title
394+
return nil
395+
}
396+
397+
func (s *boundaryRuntimeStub) UpdateSessionModel(_ context.Context, sessionID string, providerID string, modelID string) error {
398+
s.modelSessionID = sessionID
399+
s.modelProviderID = providerID
400+
s.modelID = modelID
401+
return nil
402+
}
403+
404+
func (s *boundaryRuntimeStub) SyncSessionsProviderModel(_ context.Context, providerID string, modelID string) error {
405+
s.syncProviderID = providerID
406+
s.syncModelID = modelID
407+
return nil
408+
}
409+
370410
func TestGatewayRuntimePortBridgeCheckpointOperations(t *testing.T) {
371411
stub := &runtimeStub{
372412
listCheckpointsResult: []agentsession.CheckpointRecord{
@@ -1496,6 +1536,32 @@ func TestGatewayRuntimePortBridgeDeleteSession(t *testing.T) {
14961536
})
14971537
}
14981538

1539+
func TestGatewayRuntimePortBridgeDeleteSessionUsesRuntimeBoundary(t *testing.T) {
1540+
store := &bridgeSessionStoreStub{
1541+
deleteFn: func(_ context.Context, _ string) error {
1542+
t.Fatalf("DeleteSession should use runtime boundary instead of direct store write")
1543+
return nil
1544+
},
1545+
}
1546+
stub := &boundaryRuntimeStub{runtimeStub: &runtimeStub{eventsCh: make(chan agentruntime.RuntimeEvent, 1)}}
1547+
bridge, err := newGatewayRuntimePortBridge(context.Background(), stub, store)
1548+
if err != nil {
1549+
t.Fatalf("new bridge: %v", err)
1550+
}
1551+
defer bridge.Close()
1552+
1553+
deleted, err := bridge.DeleteSession(context.Background(), gateway.DeleteSessionInput{
1554+
SubjectID: testBridgeSubjectID,
1555+
SessionID: " session-1 ",
1556+
})
1557+
if err != nil {
1558+
t.Fatalf("DeleteSession() error = %v", err)
1559+
}
1560+
if !deleted || stub.deletedSessionID != "session-1" {
1561+
t.Fatalf("deleted=%v runtimeID=%q", deleted, stub.deletedSessionID)
1562+
}
1563+
}
1564+
14991565
func TestGatewayRuntimePortBridgeRenameSession(t *testing.T) {
15001566
t.Run("success", func(t *testing.T) {
15011567
store := &bridgeSessionStoreStub{
@@ -1564,6 +1630,32 @@ func TestGatewayRuntimePortBridgeRenameSession(t *testing.T) {
15641630
})
15651631
}
15661632

1633+
func TestGatewayRuntimePortBridgeRenameSessionUsesRuntimeBoundary(t *testing.T) {
1634+
store := &bridgeSessionStoreStub{
1635+
updateFn: func(_ context.Context, _ agentsession.UpdateSessionStateInput) error {
1636+
t.Fatalf("RenameSession should use runtime boundary instead of direct store write")
1637+
return nil
1638+
},
1639+
}
1640+
stub := &boundaryRuntimeStub{runtimeStub: &runtimeStub{eventsCh: make(chan agentruntime.RuntimeEvent, 1)}}
1641+
bridge, err := newGatewayRuntimePortBridge(context.Background(), stub, store)
1642+
if err != nil {
1643+
t.Fatalf("new bridge: %v", err)
1644+
}
1645+
defer bridge.Close()
1646+
1647+
if err := bridge.RenameSession(context.Background(), gateway.RenameSessionInput{
1648+
SubjectID: testBridgeSubjectID,
1649+
SessionID: " session-1 ",
1650+
Title: " New Title ",
1651+
}); err != nil {
1652+
t.Fatalf("RenameSession() error = %v", err)
1653+
}
1654+
if stub.renamedSessionID != "session-1" || stub.renamedTitle != "New Title" {
1655+
t.Fatalf("runtime rename = (%q, %q)", stub.renamedSessionID, stub.renamedTitle)
1656+
}
1657+
}
1658+
15671659
// ---- providerSelection stub ----
15681660

15691661
type providerSelectionStub struct {
@@ -2099,6 +2191,58 @@ func TestGatewayRuntimePortBridgeSetSessionModelProviderInference(t *testing.T)
20992191
}
21002192
}
21012193

2194+
func TestGatewayRuntimePortBridgeSetSessionModelUsesRuntimeBoundary(t *testing.T) {
2195+
store := &bridgeSessionStoreWithLoader{
2196+
bridgeSessionStoreStub: bridgeSessionStoreStub{
2197+
updateFn: func(_ context.Context, _ agentsession.UpdateSessionStateInput) error {
2198+
t.Fatalf("SetSessionModel should use runtime boundary instead of direct store write")
2199+
return nil
2200+
},
2201+
},
2202+
session: agentsession.Session{ID: "s-1", Provider: "", Model: ""},
2203+
}
2204+
ps := &providerSelectionStub{
2205+
listOptions: []configstate.ProviderOption{
2206+
{ID: "openai", Models: []providertypes.ModelDescriptor{{ID: "gpt-4", Name: "GPT-4"}}},
2207+
},
2208+
}
2209+
stub := &boundaryRuntimeStub{runtimeStub: &runtimeStub{eventsCh: make(chan agentruntime.RuntimeEvent, 1)}}
2210+
bridge, _ := newGatewayRuntimePortBridge(context.Background(), stub, store, nil, ps)
2211+
defer bridge.Close()
2212+
2213+
if err := bridge.SetSessionModel(context.Background(), gateway.SetSessionModelInput{
2214+
SubjectID: testBridgeSubjectID,
2215+
SessionID: "s-1",
2216+
ModelID: "gpt-4",
2217+
}); err != nil {
2218+
t.Fatalf("SetSessionModel() error = %v", err)
2219+
}
2220+
if stub.modelSessionID != "s-1" || stub.modelProviderID != "openai" || stub.modelID != "gpt-4" {
2221+
t.Fatalf("runtime model update = (%q, %q, %q)", stub.modelSessionID, stub.modelProviderID, stub.modelID)
2222+
}
2223+
}
2224+
2225+
func TestGatewayRuntimePortBridgeSyncSessionsProviderModelUsesRuntimeBoundary(t *testing.T) {
2226+
store := &bridgeSessionStoreWithLoader{
2227+
bridgeSessionStoreStub: bridgeSessionStoreStub{
2228+
updateFn: func(_ context.Context, _ agentsession.UpdateSessionStateInput) error {
2229+
t.Fatalf("SyncSessionsProviderModel should use runtime boundary instead of direct store write")
2230+
return nil
2231+
},
2232+
},
2233+
}
2234+
stub := &boundaryRuntimeStub{runtimeStub: &runtimeStub{eventsCh: make(chan agentruntime.RuntimeEvent, 1)}}
2235+
bridge, _ := newGatewayRuntimePortBridge(context.Background(), stub, store)
2236+
defer bridge.Close()
2237+
2238+
if err := bridge.SyncSessionsProviderModel(context.Background(), " openai ", " gpt-4 "); err != nil {
2239+
t.Fatalf("SyncSessionsProviderModel() error = %v", err)
2240+
}
2241+
if stub.syncProviderID != "openai" || stub.syncModelID != "gpt-4" {
2242+
t.Fatalf("runtime sync = (%q, %q)", stub.syncProviderID, stub.syncModelID)
2243+
}
2244+
}
2245+
21022246
func TestGatewayRuntimePortBridgeSetSessionModelNotFound(t *testing.T) {
21032247
store := &bridgeSessionStoreWithLoader{
21042248
session: agentsession.Session{ID: "s-1", Provider: "openai", Model: ""},

internal/repository/git.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -441,14 +441,14 @@ func isAmbiguousGitStatusOutsideRepo(workdir string, output string, err error) b
441441
func hasGitMetadataAncestor(workdir string) bool {
442442
current := filepath.Clean(workdir)
443443
for {
444-
gitPath := filepath.Join(current, ".git")
445-
if _, err := os.Stat(gitPath); err == nil {
446-
return true
447-
}
448444
parent := filepath.Dir(current)
449445
if parent == current {
450446
return false
451447
}
448+
gitPath := filepath.Join(current, ".git")
449+
if _, err := os.Stat(gitPath); err == nil {
450+
return true
451+
}
452452
current = parent
453453
}
454454
}

internal/runner/capability.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func resolvePath(target string, workdir string) string {
9797
if trimmed == "" {
9898
return ""
9999
}
100-
if filepath.IsAbs(trimmed) {
100+
if filepath.IsAbs(trimmed) || strings.HasPrefix(filepath.ToSlash(trimmed), "/") {
101101
return trimmed
102102
}
103103
base := strings.TrimSpace(workdir)

0 commit comments

Comments
 (0)