Skip to content

feat: add task attempt lifecycle protocol#41

Merged
glittercowboy merged 1 commit intomainfrom
codex/turn-lifecycle-protocol
Apr 30, 2026
Merged

feat: add task attempt lifecycle protocol#41
glittercowboy merged 1 commit intomainfrom
codex/turn-lifecycle-protocol

Conversation

@glittercowboy
Copy link
Copy Markdown
Contributor

@glittercowboy glittercowboy commented Apr 30, 2026

Summary by CodeRabbit

  • New Features
    • Added attempt-scoped task execution tracking with unique identifiers and lease expiry.
    • Introduced task lifecycle diagnostics with detailed phase and status reporting.
    • Enhanced deadline management with configurable timing profiles.
    • Added turn classification to distinguish user-visible operations from control operations.
    • Improved error information with failure codes, retry indicators, and user-facing messages.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

📝 Walkthrough

Walkthrough

The protocol is extended to support attempt-scoped task execution and structured lifecycle diagnostics. New message types, deadline configuration, and attempt metadata are introduced across task-related messages and a new taskLifecycle frame type for comprehensive task attempt tracking.

Changes

Cohort / File(s) Summary
Protocol Documentation
PROTOCOL.md
Extended task request payload with attempt metadata (attemptId, attemptNumber, leaseExpiresAt), deadline configuration (deadlineProfile), and turn classification (turnKind). New taskLifecycle frame documented with attempt lifecycle phases, status enumerations, failure details, and timing metadata.
Message Type Registration
envelope.go
Added DecodePayload() method to convert Envelope.Payload into concrete message structs based on type, with explicit json.RawMessage handling and type-based error wrapping. Registered new MsgTypeTaskLifecycle message type for envelope parsing.
Protocol Message Types & Structs
messages.go
Added new types: TurnKind, TaskDeadlines, TaskLifecyclePhase, TaskAttemptStatus, and TaskLifecycle. Extended existing structs (Task, PlanCapability, Stream, TaskStarted, TaskComplete, TaskError, TaskCancelled, PermissionRequest, Question) with optional attempt identifiers, deadline profiles, and failure metadata.
Message Roundtrip Tests
messages_test.go
Added two test cases: one validating Envelope.DecodePayload() with marshaled TaskLifecycle, the other verifying JSON roundtrip for Task with full attempt metadata, deadline profile, and associated PlanCapability.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Hops with glee through lifecycle phases!
Attempts tracked from first to last,
Deadlines set, diagnostics gleam,
Task execution, crystal-clean! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding task attempt lifecycle protocol support. It directly reflects the core functionality introduced across the changed files (protocol definitions, envelope decoding, and lifecycle message types).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/turn-lifecycle-protocol

Review rate limit: 9/10 reviews remaining, refill in 6 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@messages_test.go`:
- Around line 1838-1926: Add tests to messages_test.go that exercise
ParseEnvelope/Envelope.DecodePayload for negative and compatibility cases: (1)
add a ParseEnvelope test for MsgTypeTaskLifecycle that ensures decoding succeeds
for the new wire shape (use TaskLifecycle and
Envelope.DecodePayload/ParseEnvelope), (2) add invalid-payload tests that feed
JSON where fields have wrong types (e.g., numeric PID as string or ObservedAt as
number) and assert DecodePayload/ParseEnvelope returns an error, (3) add a test
that includes unknown extra fields in the TaskLifecycle/Task JSON and asserts
decoding tolerates them, and (4) add a compatibility test that decodes a legacy
Task JSON without AttemptID/DeadlineProfile and verifies Task unmarshals with
zero/nil AttemptID and a nil/zero DeadlineProfile (use Task and TaskDeadlines
validation), ensuring all new tests reference the existing symbols
TaskLifecycle, Task, Envelope.DecodePayload, ParseEnvelope, and TaskDeadlines so
they fail/validate the protocol change.

In `@messages.go`:
- Around line 253-257: Field DeadlineProfile in messages.go is currently a
value-typed TaskDeadlines which cannot distinguish "omitted" vs "present but
empty" when marshaling JSON (it will serialize as {}), so change the type to a
pointer (*TaskDeadlines) on the struct that contains DeadlineProfile (or
implement custom JSON marshal/unmarshal for that field) so omitempty will
actually omit absent values; update any construction sites that populate
DeadlineProfile to use &TaskDeadlines or nil accordingly and adjust consumers to
handle a nil DeadlineProfile.

In `@PROTOCOL.md`:
- Around line 247-250: The protocol docs currently state that attempt metadata
is echoed beyond taskLifecycle but the message tables for stream, taskStarted,
taskComplete, taskError, taskCancelled, permissionRequest, and question still
show the old shapes; update PROTOCOL.md to include the new attempt fields
(attemptId, attemptNumber, leaseExpiresAt, deadlineProfile, turnKind) on each of
those task-adjacent frame descriptions and message tables (also apply the same
fix at the other occurrence around lines 297-300), and ensure the prose
clarifies that these fields are present on every echoed frame so it matches the
Go types and tests before bumping consumers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8eb9c4c1-616c-4111-9733-c92e25a56b54

📥 Commits

Reviewing files that changed from the base of the PR and between 1812fca and 70d1b0d.

📒 Files selected for processing (4)
  • PROTOCOL.md
  • envelope.go
  • messages.go
  • messages_test.go

Comment thread messages_test.go
Comment on lines +1838 to +1926
func TestTaskLifecycleRoundTrip(t *testing.T) {
observedAt := time.Date(2026, 4, 30, 12, 30, 55, 0, time.UTC)
deadlineAt := observedAt.Add(90 * time.Second)
msg := &TaskLifecycle{
Type: MsgTypeTaskLifecycle,
TaskID: "task-1",
AttemptID: "attempt-1",
AttemptNumber: 1,
SessionID: "session-1",
ChannelID: "channel-1",
Phase: TaskLifecyclePhasePromptWritten,
Status: TaskAttemptStatusPromptWritten,
ObservedAt: observedAt,
DeadlineAt: &deadlineAt,
PID: 12345,
Provider: "claude-cli",
Model: "claude-sonnet-4-6",
RequestID: "request-1",
Traceparent: "00-b1a91ab1f89d5141c7280de8bd272d73-1111111111111111-01",
}

raw, err := json.Marshal(msg)
if err != nil {
t.Fatalf("marshal: %v", err)
}

env := Envelope{Type: MsgTypeTaskLifecycle, Payload: raw}
decoded, err := env.DecodePayload()
if err != nil {
t.Fatalf("decode: %v", err)
}
got, ok := decoded.(*TaskLifecycle)
if !ok {
t.Fatalf("decoded type = %T", decoded)
}
if got.AttemptID != "attempt-1" || got.Phase != TaskLifecyclePhasePromptWritten {
t.Fatalf("lifecycle fields missing: %#v", got)
}
}

func TestTaskAttemptFieldsRoundTrip(t *testing.T) {
task := Task{
Type: MsgTypeTask,
TaskID: "task-1",
SessionID: "session-1",
ChannelID: "channel-1",
Prompt: "hello",
Model: "claude-sonnet-4-6",
Effort: "max",
PermissionMode: "acceptEdits",
CWD: "/tmp/project",
AttemptID: "attempt-1",
AttemptNumber: 2,
LeaseExpiresAt: "2026-04-30T12:32:55Z",
TurnKind: TurnKindUser,
DeadlineProfile: TaskDeadlines{
ProcessStartMs: 15000,
PromptWriteMs: 5000,
FirstEventMs: 90000,
FirstVisibleEventMs: 120000,
StreamIdleMs: 120000,
ToolIdleMs: 300000,
UserInputMs: 600000,
CleanupTermMs: 2000,
},
PlanCapability: &PlanCapability{
ID: "cap-1",
AttemptID: "attempt-1",
Token: "opaque",
APIBaseURL: "https://app.gsd.build",
ExpiresAt: "2026-04-30T12:45:55Z",
},
}

raw, err := json.Marshal(task)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var got Task
if err := json.Unmarshal(raw, &got); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if got.AttemptID != "attempt-1" || got.DeadlineProfile.FirstEventMs != 90000 {
t.Fatalf("attempt/deadline fields missing: %#v", got)
}
if got.PlanCapability == nil || got.PlanCapability.AttemptID != "attempt-1" {
t.Fatalf("plan capability attempt missing: %#v", got.PlanCapability)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add compatibility and invalid-payload coverage for the new wire shapes.

These tests only cover the happy path. For this protocol change, we still need ParseEnvelope coverage for taskLifecycle, invalid field-type cases, unknown-field tolerance, and a compatibility case where legacy task payloads omit the new attempt/deadline fields.

As per coding guidelines: "Tests must verify parsing, serialization, unknown/invalid payload handling, and protocol compatibility behavior".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages_test.go` around lines 1838 - 1926, Add tests to messages_test.go
that exercise ParseEnvelope/Envelope.DecodePayload for negative and
compatibility cases: (1) add a ParseEnvelope test for MsgTypeTaskLifecycle that
ensures decoding succeeds for the new wire shape (use TaskLifecycle and
Envelope.DecodePayload/ParseEnvelope), (2) add invalid-payload tests that feed
JSON where fields have wrong types (e.g., numeric PID as string or ObservedAt as
number) and assert DecodePayload/ParseEnvelope returns an error, (3) add a test
that includes unknown extra fields in the TaskLifecycle/Task JSON and asserts
decoding tolerates them, and (4) add a compatibility test that decodes a legacy
Task JSON without AttemptID/DeadlineProfile and verifies Task unmarshals with
zero/nil AttemptID and a nil/zero DeadlineProfile (use Task and TaskDeadlines
validation), ensuring all new tests reference the existing symbols
TaskLifecycle, Task, Envelope.DecodePayload, ParseEnvelope, and TaskDeadlines so
they fail/validate the protocol change.

Comment thread messages.go
Comment on lines +253 to +257
AttemptID string `json:"attemptId,omitempty"`
AttemptNumber int `json:"attemptNumber,omitempty"`
LeaseExpiresAt string `json:"leaseExpiresAt,omitempty"`
DeadlineProfile TaskDeadlines `json:"deadlineProfile,omitempty"`
TurnKind TurnKind `json:"turnKind,omitempty"`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve omission for deadlineProfile.

deadlineProfile is documented as optional, but this value-typed field can't preserve “omitted” versus “present but empty” across JSON round-trips. That makes legacy task payloads easy to re-emit in a different shape. Prefer a pointer here (or equivalent custom marshal logic).

Suggested change
-	DeadlineProfile    TaskDeadlines   `json:"deadlineProfile,omitempty"`
+	DeadlineProfile    *TaskDeadlines  `json:"deadlineProfile,omitempty"`
In Go's `encoding/json`, does `omitempty` omit a zero-value non-pointer struct field, or will it still serialize as `{}`?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages.go` around lines 253 - 257, Field DeadlineProfile in messages.go is
currently a value-typed TaskDeadlines which cannot distinguish "omitted" vs
"present but empty" when marshaling JSON (it will serialize as {}), so change
the type to a pointer (*TaskDeadlines) on the struct that contains
DeadlineProfile (or implement custom JSON marshal/unmarshal for that field) so
omitempty will actually omit absent values; update any construction sites that
populate DeadlineProfile to use &TaskDeadlines or nil accordingly and adjust
consumers to handle a nil DeadlineProfile.

Comment thread PROTOCOL.md
Comment on lines +247 to +250
The relay owns task attempts. A dispatched `task` includes the active
`attemptId`, `attemptNumber`, `leaseExpiresAt`, `deadlineProfile`, and
`turnKind`. Daemons echo attempt metadata on task-adjacent frames so the relay
can associate runtime events with the active attempt.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Document the new attempt fields on the existing task-adjacent frames too.

This section says attempt metadata is echoed beyond taskLifecycle, but the message tables for stream, taskStarted, taskComplete, taskError, taskCancelled, permissionRequest, and question still describe the pre-attempt shapes. That leaves the authoritative spec behind the Go types.

Based on learnings: "Treat message shape changes as cross-repo work: update PROTOCOL.md, Go types, and tests here first before bumping consumers".

Also applies to: 297-300

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PROTOCOL.md` around lines 247 - 250, The protocol docs currently state that
attempt metadata is echoed beyond taskLifecycle but the message tables for
stream, taskStarted, taskComplete, taskError, taskCancelled, permissionRequest,
and question still show the old shapes; update PROTOCOL.md to include the new
attempt fields (attemptId, attemptNumber, leaseExpiresAt, deadlineProfile,
turnKind) on each of those task-adjacent frame descriptions and message tables
(also apply the same fix at the other occurrence around lines 297-300), and
ensure the prose clarifies that these fields are present on every echoed frame
so it matches the Go types and tests before bumping consumers.

@glittercowboy glittercowboy merged commit a4328b6 into main Apr 30, 2026
2 checks passed
@glittercowboy glittercowboy deleted the codex/turn-lifecycle-protocol branch April 30, 2026 17:40
@coderabbitai coderabbitai Bot mentioned this pull request May 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant