Skip to content

Commit 09ff73c

Browse files
committed
refactor: remove activity source tracking (#62)
1 parent a34094e commit 09ff73c

23 files changed

Lines changed: 71 additions & 149 deletions

backend/internal/cdc/cdc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func seedSession(t *testing.T, s *sqlite.Store) domain.SessionRecord {
3232
}
3333
r, err := s.CreateSession(ctx, domain.SessionRecord{
3434
ProjectID: "mer", Kind: domain.KindWorker,
35-
Activity: domain.ActivitySubstate{State: domain.ActivityActive, LastActivityAt: now, Source: domain.SourceNative},
35+
Activity: domain.Activity{State: domain.ActivityActive, LastActivityAt: now},
3636
CreatedAt: now, UpdatedAt: now,
3737
})
3838
if err != nil {

backend/internal/daemon/wiring_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ func TestWiring_WriteFlowsToBroadcaster(t *testing.T) {
4242
}
4343
rec, err := store.CreateSession(ctx, domain.SessionRecord{
4444
ProjectID: "mer", Kind: domain.KindWorker,
45-
Activity: domain.ActivitySubstate{State: domain.ActivityIdle, LastActivityAt: time.Now(), Source: domain.SourceNone},
45+
Activity: domain.Activity{State: domain.ActivityIdle, LastActivityAt: time.Now()},
4646
})
4747
if err != nil {
4848
t.Fatal(err)
4949
}
5050
// A real transition through the engine, which writes the row and fires the
5151
// activity_state/is_terminated CDC trigger.
52-
if err := lcm.ApplyActivitySignal(ctx, rec.ID, ports.ActivitySignal{Valid: true, State: domain.ActivityActive, Timestamp: time.Now(), Source: domain.SourceNative}); err != nil {
52+
if err := lcm.ApplyActivitySignal(ctx, rec.ID, ports.ActivitySignal{Valid: true, State: domain.ActivityActive, Timestamp: time.Now()}); err != nil {
5353
t.Fatal(err)
5454
}
5555

backend/internal/domain/activity.go

Lines changed: 7 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,23 @@ import "time"
55
// ActivityState is how busy the agent is, derived from its output/JSONL.
66
type ActivityState string
77

8-
// Activity states. WaitingInput and Blocked are sticky (see IsSticky).
8+
// Activity states. WaitingInput is sticky (see IsSticky).
99
const (
1010
ActivityActive ActivityState = "active"
1111
ActivityIdle ActivityState = "idle"
1212
ActivityWaitingInput ActivityState = "waiting_input"
13-
ActivityBlocked ActivityState = "blocked"
1413
ActivityExited ActivityState = "exited"
1514
)
1615

1716
// IsSticky reports whether an activity state must NOT be aged/demoted by the
1817
// passage of time (a paused agent is still paused until a new signal says so).
1918
func (a ActivityState) IsSticky() bool {
20-
return a == ActivityWaitingInput || a == ActivityBlocked
19+
return a == ActivityWaitingInput
2120
}
2221

23-
// ActivitySource records where an activity reading came from, so a weaker
24-
// source can't override a stronger one.
25-
type ActivitySource string
26-
27-
// Activity signal sources, strongest first.
28-
const (
29-
SourceNative ActivitySource = "native"
30-
SourceTerminal ActivitySource = "terminal"
31-
SourceHook ActivitySource = "hook"
32-
SourceRuntime ActivitySource = "runtime"
33-
SourceNone ActivitySource = "none"
34-
)
35-
36-
// CanOverride reports whether a reading from source a may replace a current
37-
// reading from source current. Unknown sources are treated as weakest.
38-
func (a ActivitySource) CanOverride(current ActivitySource) bool {
39-
return activitySourceRank(a) <= activitySourceRank(current)
40-
}
41-
42-
func activitySourceRank(s ActivitySource) int {
43-
switch s {
44-
case SourceNative:
45-
return 0
46-
case SourceTerminal:
47-
return 1
48-
case SourceHook:
49-
return 2
50-
case SourceRuntime:
51-
return 3
52-
default:
53-
return 4
54-
}
55-
}
56-
57-
// ActivitySubstate is the persisted activity reading: the state, when it was
58-
// last observed, and which source reported it.
59-
type ActivitySubstate struct {
60-
State ActivityState `json:"state"`
61-
LastActivityAt time.Time `json:"lastActivityAt"`
62-
Source ActivitySource `json:"source"`
22+
// Activity captures the persisted activity reading: the state and when it was
23+
// last observed.
24+
type Activity struct {
25+
State ActivityState `json:"state"`
26+
LastActivityAt time.Time `json:"lastActivityAt"`
6327
}

backend/internal/domain/session.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,16 @@ type SessionMetadata struct {
3636
// facts: identity, agent harness, activity_state, is_terminated, and operational
3737
// metadata. The user-facing Status is derived from these facts plus PR facts.
3838
type SessionRecord struct {
39-
ID SessionID `json:"id"`
40-
ProjectID ProjectID `json:"projectId"`
41-
IssueID IssueID `json:"issueId,omitempty"`
42-
Kind SessionKind `json:"kind"`
43-
Harness AgentHarness `json:"harness,omitempty"`
44-
Activity ActivitySubstate `json:"activity"`
45-
IsTerminated bool `json:"isTerminated"`
46-
Metadata SessionMetadata `json:"-"`
47-
CreatedAt time.Time `json:"createdAt"`
48-
UpdatedAt time.Time `json:"updatedAt"`
39+
ID SessionID `json:"id"`
40+
ProjectID ProjectID `json:"projectId"`
41+
IssueID IssueID `json:"issueId,omitempty"`
42+
Kind SessionKind `json:"kind"`
43+
Harness AgentHarness `json:"harness,omitempty"`
44+
Activity Activity `json:"activity"`
45+
IsTerminated bool `json:"isTerminated"`
46+
Metadata SessionMetadata `json:"-"`
47+
CreatedAt time.Time `json:"createdAt"`
48+
UpdatedAt time.Time `json:"updatedAt"`
4949
}
5050

5151
// Session is the read-model returned across the API boundary: a SessionRecord

backend/internal/domain/status.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const (
1616
StatusMergeable SessionStatus = "mergeable"
1717
StatusMerged SessionStatus = "merged"
1818
StatusNeedsInput SessionStatus = "needs_input"
19-
StatusStuck SessionStatus = "stuck"
2019
StatusIdle SessionStatus = "idle"
2120
StatusTerminated SessionStatus = "terminated"
2221
)
@@ -32,11 +31,8 @@ func DeriveStatus(rec SessionRecord, pr *PRFacts) SessionStatus {
3231
return StatusTerminated
3332
}
3433

35-
switch rec.Activity.State {
36-
case ActivityWaitingInput:
34+
if rec.Activity.State == ActivityWaitingInput {
3735
return StatusNeedsInput
38-
case ActivityBlocked:
39-
return StatusStuck
4036
}
4137

4238
if pr != nil {

backend/internal/domain/status_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package domain
33
import "testing"
44

55
func rec(activity ActivityState, terminated bool) SessionRecord {
6-
return SessionRecord{Activity: ActivitySubstate{State: activity}, IsTerminated: terminated}
6+
return SessionRecord{Activity: Activity{State: activity}, IsTerminated: terminated}
77
}
88

99
func pr(facts PRFacts) *PRFacts { return &facts }
@@ -18,7 +18,6 @@ func TestDeriveStatusFromSessionFactsAndPR(t *testing.T) {
1818
{"terminated", rec(ActivityExited, true), nil, StatusTerminated},
1919
{"merged-pr", rec(ActivityIdle, true), pr(PRFacts{Merged: true}), StatusMerged},
2020
{"needs-input", rec(ActivityWaitingInput, false), pr(PRFacts{CI: CIFailing}), StatusNeedsInput},
21-
{"blocked", rec(ActivityBlocked, false), pr(PRFacts{CI: CIFailing}), StatusStuck},
2221
{"ci-failed", rec(ActivityIdle, false), pr(PRFacts{CI: CIFailing}), StatusCIFailed},
2322
{"draft", rec(ActivityIdle, false), pr(PRFacts{Draft: true}), StatusDraft},
2423
{"changes-requested", rec(ActivityIdle, false), pr(PRFacts{Review: ReviewChangesRequest}), StatusChangesRequested},

backend/internal/lifecycle/manager.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func (m *Manager) ApplyRuntimeObservation(ctx context.Context, id domain.Session
6565
}
6666
next := cur
6767
next.IsTerminated = true
68-
next.Activity = domain.ActivitySubstate{State: domain.ActivityExited, LastActivityAt: timeOr(f.ObservedAt, now), Source: domain.SourceRuntime}
68+
next.Activity = domain.Activity{State: domain.ActivityExited, LastActivityAt: timeOr(f.ObservedAt, now)}
6969
return next, true
7070
})
7171
}
@@ -79,11 +79,8 @@ func (m *Manager) ApplyActivitySignal(ctx context.Context, id domain.SessionID,
7979
if cur.IsTerminated {
8080
return cur, false
8181
}
82-
if !s.Source.CanOverride(cur.Activity.Source) {
83-
return cur, false
84-
}
8582
next := cur
86-
act := domain.ActivitySubstate{State: s.State, LastActivityAt: timeOr(s.Timestamp, now), Source: s.Source}
83+
act := domain.Activity{State: s.State, LastActivityAt: timeOr(s.Timestamp, now)}
8784
if sameActivity(cur.Activity, act) {
8885
return cur, false
8986
}
@@ -108,7 +105,7 @@ func (m *Manager) MarkSpawned(ctx context.Context, id domain.SessionID, metadata
108105
}
109106
now := m.clock()
110107
rec.IsTerminated = false
111-
rec.Activity = domain.ActivitySubstate{State: domain.ActivityIdle, LastActivityAt: now, Source: domain.SourceRuntime}
108+
rec.Activity = domain.Activity{State: domain.ActivityIdle, LastActivityAt: now}
112109
rec.Metadata = mergeMetadata(rec.Metadata, metadata)
113110
rec.UpdatedAt = now
114111
return m.store.UpdateSession(ctx, rec)
@@ -121,13 +118,13 @@ func (m *Manager) MarkTerminated(ctx context.Context, id domain.SessionID) error
121118
return cur, false
122119
}
123120
cur.IsTerminated = true
124-
cur.Activity = domain.ActivitySubstate{State: domain.ActivityExited, LastActivityAt: now, Source: domain.SourceRuntime}
121+
cur.Activity = domain.Activity{State: domain.ActivityExited, LastActivityAt: now}
125122
return cur, true
126123
})
127124
}
128125

129-
func sameActivity(a, b domain.ActivitySubstate) bool {
130-
return a.State == b.State && a.Source == b.Source && a.LastActivityAt.Equal(b.LastActivityAt)
126+
func sameActivity(a, b domain.Activity) bool {
127+
return a.State == b.State && a.LastActivityAt.Equal(b.LastActivityAt)
131128
}
132129

133130
func mergeMetadata(base, in domain.SessionMetadata) domain.SessionMetadata {

backend/internal/lifecycle/manager_test.go

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func newManager() (*Manager, *fakeStore, *fakeMessenger) {
5151
}
5252

5353
func working(id domain.SessionID) domain.SessionRecord {
54-
return domain.SessionRecord{ID: id, ProjectID: "mer", Activity: domain.ActivitySubstate{State: domain.ActivityActive, LastActivityAt: time.Now(), Source: domain.SourceNative}}
54+
return domain.SessionRecord{ID: id, ProjectID: "mer", Activity: domain.Activity{State: domain.ActivityActive, LastActivityAt: time.Now()}}
5555
}
5656

5757
func TestRuntimeObservation_InferredDeathSetsTerminated(t *testing.T) {
@@ -92,30 +92,6 @@ func TestActivity_InvalidIsIgnored(t *testing.T) {
9292
}
9393
}
9494

95-
func TestActivity_WeakerSourceDoesNotOverrideStronger(t *testing.T) {
96-
m, st, _ := newManager()
97-
st.sessions["mer-1"] = working("mer-1")
98-
before := st.sessions["mer-1"]
99-
if err := m.ApplyActivitySignal(ctx, "mer-1", ports.ActivitySignal{Valid: true, State: domain.ActivityIdle, Source: domain.SourceRuntime}); err != nil {
100-
t.Fatal(err)
101-
}
102-
if st.sessions["mer-1"] != before {
103-
t.Fatalf("weaker runtime signal should not override native activity, got %+v", st.sessions["mer-1"])
104-
}
105-
}
106-
107-
func TestActivity_StrongerSourceOverridesWeaker(t *testing.T) {
108-
m, st, _ := newManager()
109-
st.sessions["mer-1"] = domain.SessionRecord{ID: "mer-1", ProjectID: "mer", Activity: domain.ActivitySubstate{State: domain.ActivityIdle, LastActivityAt: time.Now(), Source: domain.SourceRuntime}}
110-
if err := m.ApplyActivitySignal(ctx, "mer-1", ports.ActivitySignal{Valid: true, State: domain.ActivityActive, Source: domain.SourceNative}); err != nil {
111-
t.Fatal(err)
112-
}
113-
got := st.sessions["mer-1"].Activity
114-
if got.State != domain.ActivityActive || got.Source != domain.SourceNative {
115-
t.Fatalf("stronger native signal should override runtime, got %+v", got)
116-
}
117-
}
118-
11995
func TestMarkTerminated(t *testing.T) {
12096
m, st, _ := newManager()
12197
st.sessions["mer-1"] = working("mer-1")

backend/internal/lifecycle/reactions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func (m *Manager) ApplyPRObservation(ctx context.Context, id domain.SessionID, o
3939
if err != nil || !ok {
4040
return err
4141
}
42-
if rec.IsTerminated || rec.Activity.State == domain.ActivityBlocked || rec.Activity.State == domain.ActivityWaitingInput {
42+
if rec.IsTerminated || rec.Activity.State == domain.ActivityWaitingInput {
4343
return nil
4444
}
4545
if o.CI == domain.CIFailing {

backend/internal/lifecycle/runtime.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99

1010
const defaultRecentActivityWindow = 60 * time.Second
1111

12-
func hasRecentActivity(a domain.ActivitySubstate, now time.Time, window time.Duration) bool {
12+
func hasRecentActivity(a domain.Activity, now time.Time, window time.Duration) bool {
1313
switch {
1414
case a.State == domain.ActivityExited:
1515
return false
@@ -22,7 +22,7 @@ func hasRecentActivity(a domain.ActivitySubstate, now time.Time, window time.Dur
2222
}
2323
}
2424

25-
func runtimeClearlyDead(f ports.RuntimeFacts, activity domain.ActivitySubstate, now time.Time, window time.Duration) bool {
25+
func runtimeClearlyDead(f ports.RuntimeFacts, activity domain.Activity, now time.Time, window time.Duration) bool {
2626
observedAt := timeOr(f.ObservedAt, now)
2727
return f.Probe == ports.ProbeDead && !hasRecentActivity(activity, observedAt, window)
2828
}

0 commit comments

Comments
 (0)