Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/logs/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,6 @@ PR #1113 is merged to `main` as the #1106 rebase baseline.

Issue #1106 documents mixed-version callback dispatch fencing, bootstrap-only
crash recovery provenance, and eventual pre-claim contention progress.

Recent entry: Issue #1009 additive server task lifecycle projection and native
macOS controls/reconciliation (pending final repository regression at time of entry).
68 changes: 68 additions & 0 deletions docs/logs/engineering-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -4329,3 +4329,71 @@ Skipped creating separate issues for Op/EventMsg protocol (already covered by SS
`./scripts/test-regression.sh` rerun passed normal, race, and coverage at
85.5% with zero uncovered functions. This is not PTY or native-GUI proof;
those remain the #1000 convergence matrix.

## 2026-08-04 (Issue #1009 — scheduled-task lifecycle and macOS controls)

- Change: `GET /v1/tasks` now projects optional, server-authored cron and
callback lifecycle fields: conversation linkage, cron next/last timestamps,
most-recent execution state/run/error, callback due time, and update time.
Existing type-specific routes remain the sole mutation authority.
- Native app: `TaskInfo` now has typed forward-compatible kind, state, and
action values; unknown server values decode without making the Activity page
unusable. HarnessKit adds scoped pause/resume/delete/cancel requests.
Activity displays lifecycle detail and accessible controls, asks before cron
deletion, and always reloads server state after an action succeeds or fails.
- TDD: the first Go test failed because `Task` had no lifecycle fields; the
first Swift test failed because task values were raw strings and control APIs
were absent. A full Swift run then caught a global URLProtocol-stub race in
the new tests; the task tests now use their own isolated protocol class.
- Verification: focused task lifecycle tests, `go test ./internal/server
-count=1`, `go test ./internal/server -race -count=1`, and the full Swift
suite (`256` tests) passed. The direct full repository gate, run with its own
temporary cache and coverage profile after rebase to `f7b6c70`, passed normal,
race, and coverage phases at `85.5%` total coverage with zero uncovered
functions. This makes the implementation ready for review; it is not the
separate #1010 API/TUI/native full-conversation proof.
- Review repair: cron Activity actions now carry optional `expected_updated_at`
only when the row provides it; server actions preserve empty legacy bodies,
require active-to-pause and paused-to-resume state, and map stale/invalid
mutations to 409 without changing the job. Callback `updated_at` is read
from the durable row on every list/get/returning path and projects into the
task row. The native "Open linked run" control opens the durable conversation
and lets only non-terminal run-event reducer evidence establish live control
ownership; terminal and missing links cannot manufacture controls. Repair
regressions cover stale/current actions, persisted callback freshness, JSON
request shape, and active/terminal/missing navigation.
- Repair verification: complete affected server/tool normal and race suites
passed; the full native suite passed 259 tests. The first full repository
attempt ran concurrently with another coverage regression and hit two
unrelated `cmd/harnessd` three-second startup timeouts, so it was not
accepted. After that load completed, the serial rerun with fresh cache/profile
passed normal, race, and coverage at 85.5% total with zero uncovered
functions.

## 2026-08-04 (Issue #1009 review repair — opaque cron task version)

- Cause: HarnessKit decoded task `updated_at` into `Date`, then encoded it
with a new ISO-8601 formatter for `expected_updated_at`. That conversion can
discard server-issued nanoseconds, making an otherwise fresh Activity row
fail its cron CAS action with 409.
- Fix: `TaskInfo.updatedAtVersion` retains the raw optional `updated_at`
string through ProjectSession and `TaskActionVersion`; standard `Encodable`
now emits the token unchanged. Missing versions still use the existing empty
request body for older additive task payloads.
- Regression: Swift asserts a `.123456789Z` task token and the exact JSON
action field; Go lists a nanosecond cron token, proves `.123Z` returns 409
without mutation, then proves the exact listed token pauses it.

## 2026-08-04 (Issue #1009 review repair — no-store callback terminals)

- Cause: no-store callback terminal rows stay in `m.callbacks` but leave the
active `byConv` index. `ListAllCallbacks` incorrectly walked that active
index, making canceled/fired/shutdown callbacks vanish from `/v1/tasks`.
- Fix: all-state listing snapshots and safely projects `m.callbacks`; legacy
conversation `List`/`ListCallbacks` remain active-only through `byConv`.
Legacy cancel, fire, and shutdown cancellation now stamp `UpdatedAt` from
the manager clock. The durable cancel/list branches are unchanged.
- Regression: deterministic manager coverage proves terminal timestamps and
all-state retention while agent-facing lists exclude terminals; server
coverage proves cancel then `GET /v1/tasks` returns one canceled read-only
row with nonzero `updated_at`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Issue #1009 — Cross-Surface Impact Map

## Task

- Task / issue: #1009 macOS scheduled-task lifecycle and controls.
- Plan link: `2026-08-04-issue-1009-macapp-task-lifecycle-plan.md`.
- Owner: Codex implementation worktree.
- Status: in implementation.

## Current Ownership, Callers, and Data Flow

- Entry points: `internal/server/http_tasks.go` creates `Task`; existing
`http_cron.go` and callback action handler own mutation; macOS
`HarnessKit/ClientTasks.swift` decodes and `ActivityView.swift` renders.
- Source of truth: cron client/status/executions and callback manager state;
the server calculates task action availability. `ProjectSession.refreshActivity`
owns macOS reconciliation.
- Search evidence: `rg -n "TaskInfo|/v1/tasks|TaskAction|Activity" internal cmd macapp`.
- Conclusion: extend the common task DTO rather than duplicate a parallel
scheduled-task listing; task-specific fields remain optional.

## Config, API, CLI, and Tools

- No configuration, CLI command, tool schema, or provider routing change.
- Additive `/v1/tasks` fields: schedule timing, latest execution state/run/error
and callback due time; existing cron/callback endpoints remain action targets.
- Mac client sends optional row `updated_at` as its opaque raw server string in
scoped cron action JSON while preserving empty legacy action bodies; server maps stale/invalid action
state to 409. Errors are retained as `HarnessError` and no client-side
permission inference is trusted.

## Persistence and Compatibility

- No migration. Cron values originate from persisted cron storage; callback
`updated_at` is now selected/scanned from its existing durable column for
each lifecycle transition.
- Older server payloads omit all new optionals; unknown kind/state/action raw
values preserve display rather than causing decode failures.
- Mixed-version clients use existing coarse row fields and show no absent action.
- No-store callback managers retain terminal map rows for the all-state task
API, but their `byConv` compatibility index remains active-only so capacity
accounting and agent-facing conversation lists do not regress.

## Lifecycle, Security, and Reliability

- No new goroutines/timers. Activity polling remains bounded to the visible view.
- Existing runs:read/runs:write and tenant checks enforce authority. UI refreshes
after either action success or failure so stale action sets are not retained.
Cron pause/resume require the matching current state and an optional CAS
version; linked run controls require reducer-admitted live event evidence.
- Server-safe errors are surfaced; delete needs confirmation; cancel/pause/resume
depend only on advertised actions.

## Product and Integration Surfaces

- Server/runtime: task union gets lifecycle projection only.
- macOS: typed models, action client, task detail/controls, VoiceOver labels,
linked-run navigation/selection where available.
- TUI/web and external systems: none; existing API remains backward compatible.
- Provider/model/tool catalog: none; rendering never fabricates assistant output.

## Deployment and Operations

- Deploy server before clients (additive reads); rollback clients preserves
read-only task display. Roll back server fields without breaking clients.
- API task rows provide operator diagnostics; no new metrics or secrets.

## Regression Tests

- Red: task lifecycle projection and typed Swift action request/decode tests.
- New tests: cron active/paused lifecycle, exact nine-digit `updated_at`
list-to-action preservation plus stale truncated-token CAS rejection,
stale/current CAS action state,
no-store callback cancel/fire/shutdown terminal timestamp and all-state task
visibility with legacy active-list exclusion,
callback durable update-time projection/transition, versioned/legacy action
bodies, active/terminal/missing linked-run navigation, reconciliation after
success/failure, unknown task values, accessible labels.
- Exact commands: `go test ./internal/server -run 'TestTasks|TestCron' -count=1`,
race equivalent, `swift test --package-path macapp`, and full regression.

## Documentation and Handoff

- Update plan/log indexes and engineering log once implemented. No public
assertion of full native proof until #1010 uses an exact current artifact.
79 changes: 79 additions & 0 deletions docs/plans/2026-08-04-issue-1009-macapp-task-lifecycle-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Issue #1009 — macOS scheduled-task lifecycle and controls

## Context

- Governing GitHub issue: #1009 (child of #1000).
- Problem: `/v1/tasks` currently exposes only a generic task row and the macOS
Activity page renders it read-only. Cron jobs and callbacks therefore lack
visible timing/result/run linkage and cannot be controlled from the app.
- User impact: a deployment watcher must be observable, controllable, and
reconciled to the server rather than represented by a model text claim.
- Constraints: additive wire contract; server remains authoritative for every
action; do not alter scheduler or retry behavior; preserve generic task rows.

## Scope

- In scope: additive cron/callback lifecycle fields in task rows; typed,
forward-compatible Swift task values; client requests for pause/resume/delete
and cancel; Activity detail/action UX with accessibility; reconciliation and
regression coverage.
- Out of scope: scheduler persistence/retry algorithm, a general notification
system, TUI changes, and final #1010 live proof.

## Documentation Contract

- Feature status: in implementation.
- Public docs affected: native macOS and API behavior documentation only after
implementation is tested.
- Implementation notes: engineering log and plan indexes record the actual
additive contract.

## Test Plan (TDD)

- First red: server task-union contract asserts cron next/last execution/run
fields, callback due time, and authoritative actions; Swift client tests
assert typed unknown decoding and exact control requests.
- Existing tests updated: task API and Activity session tests.
- Review repair red: stale cron pause/resume/delete requests must return 409
without mutation; versioned Swift requests must preserve an empty body for
older rows; active versus terminal linked runs must not share controls.
- Timestamp-wire repair red: a nanosecond `updated_at` token must decode and
return to the action endpoint byte-for-byte; a client-truncated token must
receive 409 without mutating the cron job.
- Regression: focused Go server normal/race, focused Swift package tests, full
`./scripts/test-regression.sh`, and `swift test --package-path macapp`.

## Cross-Surface Impact Map

See `2026-08-04-issue-1009-macapp-task-lifecycle-impact-map.md`.

## Implementation Checklist

- [x] Verify issue contract and architecture/search evidence.
- [x] Create plan and impact map.
- [x] Capture red task serialization/client-action tests.
- [x] Implement additive server and typed client contract.
- [x] Add accessible Activity actions and server reconciliation.
- [x] Update logs/indexes; issue evidence follows final regression.
- [x] Pass focused and full regression gates.
- [x] Preserve opaque cron action version tokens through final review repair.
- [x] Keep no-store callback terminal rows and their lifecycle timestamps in
the all-state task inventory without changing legacy active-only lists.

## Risks and Mitigations

- Stale UI actions could claim success: each action refreshes `/v1/tasks` in a
`defer` path and surfaces the server error. Cron actions additionally send
optional `expected_updated_at` as the opaque server string (rather than a
reformatted `Date`); stale versions and invalid state transitions return 409
without mutation.
- Mixed server versions omit fields/actions: optional fields decode as absent,
unknown enum values remain displayable, and controls remain hidden.
- Destructive deletion: confirmation is required in the Activity UI.
- Linked run authority: opening a task loads its durable conversation first;
only a non-terminal event accepted by the run reducer can make its linked
run a live control target.
- No-store callback terminal visibility: `ListAllCallbacks` snapshots retained
manager rows for the task API, while `List` and `ListCallbacks` remain backed
by the active `byConv` index; every legacy cancel/fire/shutdown terminal
transition advances `updated_at`.
3 changes: 3 additions & 0 deletions docs/plans/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@

- `2026-08-03-issue-1115-workflow-subscriber-plan.md` — Issue #1115 deterministic full-buffer workflow subscriber terminal-close regression plan.
- `2026-08-03-issue-1115-workflow-subscriber-impact-map.md` — Cross-surface impact map for Issue #1115's test-only subscription scheduling repair.

- `2026-08-04-issue-1009-macapp-task-lifecycle-plan.md` — Issue #1009 plan for additive scheduled-task lifecycle visibility and controls in the macOS app.
- `2026-08-04-issue-1009-macapp-task-lifecycle-impact-map.md` — Cross-surface impact map for #1009 server task projection and macOS reconciliation.
- `2026-08-01-issue-1083-approval-publication-race-plan.md` — Issue #1083 plan for making approval-required publication an immediate-resolution readiness guarantee.
- `2026-08-01-issue-1083-approval-publication-race-impact-map.md` — Cross-surface impact map for Issue #1083 broker registration ordering.

Expand Down
16 changes: 10 additions & 6 deletions internal/harness/tools/delayed_callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ type CallbackInfo struct {
State CallbackState `json:"state"`
FiresAt time.Time `json:"fires_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// TenantID and AgentID capture the originating run's scope so the fired
// follow-up run is started on the same tenant + agent. Both may be empty
// for the default/unscoped case. Omitted from JSON when empty to preserve
Expand Down Expand Up @@ -323,6 +324,7 @@ func (m *CallbackManager) Set(req SetRequest) (CallbackInfo, error) {
State: CallbackStatePending,
FiresAt: now.Add(delay),
CreatedAt: now,
UpdatedAt: now,
TenantID: req.TenantID,
AgentID: req.AgentID,
Model: strings.TrimSpace(req.Model),
Expand Down Expand Up @@ -400,6 +402,7 @@ func (m *CallbackManager) Cancel(id string) (CallbackInfo, error) {

info := cb.info
info.State = CallbackStateCanceled
info.UpdatedAt = m.now().UTC()
if m.store != nil {
if err := m.store.Update(context.Background(), info); err != nil {
m.mu.Unlock()
Expand Down Expand Up @@ -494,13 +497,12 @@ func (m *CallbackManager) ListAllCallbacks(ctx context.Context) ([]CallbackInfo,
m.mu.Lock()
defer m.mu.Unlock()

// The all-state API inventory includes retained terminal callbacks. The
// per-conversation active index intentionally omits those entries to free
// scheduling capacity, so it must not be used here.
result := make([]CallbackInfo, 0, len(m.callbacks))
for _, ids := range m.byConv {
for _, id := range ids {
if cb, ok := m.callbacks[id]; ok {
result = append(result, cb.info)
}
}
for _, cb := range m.callbacks {
result = append(result, publicCallbackInfo(cb.info))
}
return result, nil
}
Expand All @@ -521,6 +523,7 @@ func (m *CallbackManager) Shutdown() {
// Preserve the historical in-memory manager contract. Durable
// managers retain pending rows for restart recovery instead.
cb.info.State = CallbackStateCanceled
cb.info.UpdatedAt = m.now().UTC()
m.removeFromByConv(cb.info.ConversationID, cb.info.ID)
canceled = append(canceled, cb.info)
}
Expand Down Expand Up @@ -574,6 +577,7 @@ func (m *CallbackManager) fire(id string) {
func (m *CallbackManager) fireLegacyLocked(id string, cb *pendingCallback) {
info := cb.info
info.State = CallbackStateFired
info.UpdatedAt = m.now().UTC()
cb.info = info
convID := info.ConversationID
prompt := info.Prompt
Expand Down
Loading
Loading