Skip to content

feat: plumb time_til_autostop_notify template field#26439

Open
sreya wants to merge 1 commit into
autostop-notif/02-templatefrom
autostop-notif/03-schedule-sdk
Open

feat: plumb time_til_autostop_notify template field#26439
sreya wants to merge 1 commit into
autostop-notif/02-templatefrom
autostop-notif/03-schedule-sdk

Conversation

@sreya

@sreya sreya commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

Branch 3 of the workspace autostop reminder stack. Plumbs the per-template time_til_autostop_notify scheduling field end to end: schedule store (AGPL + enterprise), codersdk types, template create/patch handlers, and the CLI. This branch wires the configuration; it does not yet send anything (the lifecycle-executor dispatcher lands in the next branch).

What this enables

Template admins can set how long before a workspace's autostop deadline its owner is reminded (e.g. 1h). 0 (the default) disables the reminder for that template's workspaces.

Design choices

  • AGPL, not entitlement-gated. Autostop itself is an AGPL feature (workspaces stop via default_ttl / user TTL on AGPL), and the AGPL lifecycle executor already enqueues other workspace notifications (TemplateWorkspaceDormant, TemplateWorkspaceAutoUpdated). A reminder about an AGPL event should also be AGPL, so this field mirrors activity_bump: read/written by both the AGPL and enterprise schedule stores, with no advanced_template_scheduling gate.
  • Nanoseconds in the DB, milliseconds in the API. The templates.time_til_autostop_notify column is bigint nanoseconds to match every other duration column (default_ttl, activity_bump, time_til_dormant), which map directly to Go time.Duration. The API exposes time_til_autostop_notify_ms in milliseconds to match the existing *_ms convention used by activity_bump_ms, default_ttl_ms, etc., so the frontend and SDK stay consistent.
  • Validation: 0 or >= 1 minute. The future dispatcher runs on the lifecycle executor's one-minute tick, so a sub-minute reminder window could be missed entirely. Negative values are rejected separately with "Must be a positive integer." (matching the sibling fields' message).
  • UpdateTemplateMeta uses a pointer (*int64). Omitting time_til_autostop_notify_ms from a PATCH preserves the existing value (via ptr.NilToDefault), consistent with the other optional meta fields.

Context for the dispatcher (next branch)

The schema branch (already merged-below) also added workspace_builds.notified_autostop_deadline, a sentinel storing the deadline value a reminder was last sent for. The dispatcher will send a reminder when the build deadline is within time_til_autostop_notify of now and notified_autostop_deadline != deadline, then stamp the sentinel to the current deadline. This gives once-per-deadline idempotence that re-arms automatically when the deadline moves (activity bump, manual extend, schedule change) and is safe across HA replicas. The dispatcher will also treat a notify-time already in the past (when the configured lead exceeds a workspace's remaining lifetime) as at-most-one reminder, not a per-tick flood.

Changes

  • coderd/database/queries/templates.sql: time_til_autostop_notify param on UpdateTemplateScheduleByID.
  • coderd/schedule/template.go + enterprise/coderd/schedule/template.go: TemplateScheduleOptions.TimeTilAutostopNotify in both stores' Get/Set.
  • codersdk/templates.go, codersdk/organizations.go: Template / UpdateTemplateMeta / CreateTemplateRequest fields (time_til_autostop_notify_ms).
  • coderd/templates.go, coderd/templates_meta_update.go: create/patch validation, convertTemplate mapping, Set wiring.
  • cli/templateedit.go: --autostop-reminder flag (preserve-current-value pattern).
  • Tests: schedule store, meta-update resolver, create/patch (valid + sub-minute/negative rejection), CLI flag. Plus regenerated swagger/apidoc/typesGenerated/CLI docs.

Stack

  1. 01-db — schema + audit (columns + notification template seed)
  2. 02-template — notification template Go wiring + goldens + docs
  3. 03-schedule-sdk (this PR)time_til_autostop_notify config plumbing
  4. 04-executor — lifecycle-executor dispatcher + eligibility/marker queries (turns the feature on)
  5. 05-ui — template schedule-settings form field

@github-actions

github-actions Bot commented Jun 16, 2026

Copy link
Copy Markdown

Docs preview

📖 View docs preview for docs/reference/api/schemas.md

@sreya

sreya commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-06-22 16:51 UTC by @sreya
Spend: $92.95 / $100.00

Review history
  • R1 (2026-06-16): 15 reviewers, 3 Nit, 2 Note, 1 P2, 3 P3, COMMENT. Review
  • R2 (2026-06-17), 3 Nit, 2 Note, 1 P2, 3 P3, COMMENT. Review
  • R3 (2026-06-17): 9 reviewers, 4 Nit, 2 Note, 1 P2, 4 P3, 2 P4, COMMENT. Review
  • R4 (2026-06-22): 4 reviewers, 4 Nit, 2 Note, 1 P2, 4 P3, 2 P4, COMMENT. Review
  • R5 (2026-06-22): 1 reviewers, 4 Nit, 2 Note, 1 P2, 4 P3, 2 P4, COMMENT. Review

deep-review v0.9.0 | Round 5 | f9b398f..2537503

Last posted: Round 5, 13 findings (1 P2, 4 P3, 2 P4, 4 Nit, 2 Note), COMMENT. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 Note Dropped by orchestrator (code moved to branch 01-db) 000525_workspace_autostop_notification.up.sql:21 notified_autostop_deadline column has no UPDATE query R1 Netero Yes
CRF-2 Note Author accepted R3 (will happen in a later PR) coderd/notifications/events.go:18 TemplateWorkspaceAutostopReminder registered but never enqueued R1 Netero Yes
CRF-3 P3 Author fixed (eb60af0) coderd/templates.go:350 No test exercises validation rejection for sub-minute values R1 Bisky P3, Chopper P3, Kite P3 Yes
CRF-4 P3 Author accepted R3 (PR description documents dispatcher behavior) coderd/templates.go:350 No upper bound; dispatcher must handle notify_time in the past R1 Hisoka Yes
CRF-5 P2 Dropped by orchestrator (code moved to branch 01-db) 000525_workspace_autostop_notification.up.sql:60 Notification body hardcodes misleading cause attribution R1 Leorio Yes
CRF-6 P3 Author fixed (0aa4537) PR description PR description is empty boilerplate R1 Leorio Yes
CRF-7 Nit Author fixed (eb60af0) cli/templateedit.go:258 CLI flag is literal DB column translation R1 Gon Yes
CRF-8 Nit Dropped by orchestrator (follows sibling style) codersdk/templates.go:234 Doc comment verbosity R1 Gon No
CRF-9 Nit Dropped by orchestrator (code moved to branch 01-db) 000525_workspace_autostop_notification.up.sql:21 notified_autostop_deadline has no SQL COMMENT R1 Gon, Leorio Yes
CRF-10 Nit Author fixed (eb60af0) coderd/templates.go:351 Validation error message does not distinguish negative from too-small R1 Kite, Ryosuke Yes
CRF-11 P3 Author fixed (2537503) enterprise/coderd/schedule/template.go:120 Enterprise schedule store TimeTilAutostopNotify plumbing has zero test coverage R3 Hisoka Yes
CRF-12 P4 Author fixed (e28f8d6) coderd/templates_test.go:1390 No integration test for disabling after enabling R3 Bisky Yes
CRF-13 P4 Author fixed (e28f8d6) coderd/templates_test.go:147 No boundary-value test at exactly 1 minute R3 Bisky Yes
CRF-14 Nit Author fixed (e28f8d6) coderd/templates_test.go:159 Create-path validation tests don't assert error Detail R3 Chopper Yes

Round log

Round 1

Panel (15 reviewers). 1 P2, 2 P3, 3 Nit, 2 Note posted. 1 Nit dropped. Reviewed against 3e68dd3..a77fdbc.

Round 2

BLOCKED. 3 addressed (CRF-3, CRF-7, CRF-10). 6 silent.

Round 3

Orchestrator override (code moved to stack). Panel (9 reviewers). 1 P3, 2 P4, 1 Nit new. All R1 findings resolved. Reviewed against d21121f..0aa4537.

Round 4

Panel (4 reviewers). CRF-12, CRF-13, CRF-14 verified fixed. CRF-11 re-raised. No new findings. Reviewed against 1c5a2f2..e28f8d6.

Round 5

CRF-11 verified fixed. Enterprise test TestTemplateScheduleTimeTilAutostopNotify covers Set/Get round-trip and no-op guard. All 14 findings resolved. No new findings. Reviewed against f9b398f..2537503.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-tasks

coder-tasks Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Documentation Check

Updates Needed

  • docs/admin/templates/managing-templates/schedule.md - Add the new Autostop reminder setting to the bullet list of template schedule settings (alongside Default autostop, Activity bump, etc.) and briefly describe how template admins can configure --autostop-reminder / time_til_autostop_notify_ms to control when a reminder notification is sent before workspace autostop. This is the primary admin-facing page for template scheduling configuration.

Already Addressed in This PR

  • docs/reference/api/templates.md - Auto-generated API reference updated
  • docs/reference/api/schemas.md - Auto-generated schema reference updated
  • docs/reference/cli/templates_edit.md - Auto-generated CLI reference updated

No Longer in This PR

  • docs/admin/monitoring/notifications/index.md - "Workspace autostop reminder" added to notification list (notification infrastructure removed from this PR)
  • docs/admin/security/audit-logs.md - Audit log table updated with new fields (audit table changes removed from this PR)

Automated review via Coder Agents

@sreya sreya force-pushed the autostop-notif/03-schedule-sdk branch from a77fdbc to 2c369cc Compare June 16, 2026 22:10

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Clean plumbing PR. The field threads correctly through all layers: DB migration, SQLC, AGPL and enterprise schedule stores (Get + Set + no-op guard), API create/patch, SDK, CLI, audit table, notification template, and frontend types. Conversion chain (nanoseconds in DB, milliseconds in API, time.Duration in Go) is consistent at every hop. Test density is strong at 47%, and the TimeTilAutostopNotifyPreservedWhenOmitted test is a good addition that catches the nil-vs-zero class of bug.

Severity summary: 1 P2, 2 P3, 3 Nit, 2 Note.

The P2 is about the notification body text living in a migration and attributing the stop to "its autostop schedule" when the deadline may come from other sources. The P3s are a missing validation-rejection test, a design note about upper-bound behavior for the future dispatcher, and the empty PR description.

"I tried to build a case against this PR and couldn't. The problem is correctly understood. The solution is proportional." (Pariston)

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/notifications/events.go
Comment thread coderd/templates.go Outdated
Comment thread coderd/templates.go Outdated
Comment thread coderd/database/migrations/000525_workspace_autostop_notification.up.sql Outdated
Comment thread coderd/templates.go Outdated
Comment thread cli/templateedit.go Outdated
Comment thread coderd/templates.go Outdated
@sreya sreya changed the base branch from main to autostop-notif/02-template June 16, 2026 22:46
@sreya sreya force-pushed the autostop-notif/03-schedule-sdk branch from 2c369cc to eb60af0 Compare June 16, 2026 23:18
@sreya

sreya commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Round 2 is blocked. The author addressed 3 findings (CRF-3, CRF-7, CRF-10), but 6 remain without substantive response. Further review is on hold until the author responds or pushes fixes for the open findings.

Findings needing response (respond, fix, or explain why no change is needed):

CRF-5 (P2): Notification body says "due to its autostop schedule" but the deadline may come from default_ttl or admin-enforced requirements. Author replied "decline" with no reasoning. The text lives in a migration, so fixing later costs another migration. What is the reasoning for keeping the current wording?

CRF-4 (P3): No upper bound on time_til_autostop_notify. The original finding explicitly said no fix is needed today, but the dispatcher PR should handle notify_time < now as a no-op. Acknowledge or note a ticket.

CRF-9 (Nit): notified_autostop_deadline has no SQL COMMENT. Author replied "decline" with no reasoning. The column name alone does not communicate that it stores the last-notified deadline value for idempotence.

CRF-1 (Note), CRF-2 (Note): These are observations about plumbing scope (no UPDATE query, notification never enqueued). Author replied "decline." If the intent is "acknowledged, no action needed because the dispatcher PR will add these," please say so.

CRF-6 (P3): PR description is empty boilerplate. Not blocking, but context on design choices would help future readers.

The panel has not yet reviewed the new diff (migration renumbered, CLI flag renamed, validation tests added, validation error message updated). The panel will review after this block is resolved.

🤖 This review was automatically generated with Coder Agents.

@sreya sreya force-pushed the autostop-notif/02-template branch from 5885637 to d21121f Compare June 17, 2026 03:59
@sreya sreya force-pushed the autostop-notif/03-schedule-sdk branch from eb60af0 to 0aa4537 Compare June 17, 2026 03:59
@sreya

sreya commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All R1 findings resolved. The PR was restructured into a 5-branch stack; migration, notification, and audit code moved to earlier branches. CRF-3 (validation tests), CRF-7 (CLI flag rename), CRF-10 (error message), CRF-6 (PR description) all fixed. The new PR description is excellent.

R3 panel (9 reviewers) found 1 P3, 2 P4, 1 Nit on the narrowed diff. The P3 is an enterprise test coverage gap. The P4s are defensive test additions. 5 of 9 reviewers reported no findings.

Commit scope coderd/schedule does not contain all changed files (changes span cli/, codersdk/, site/, enterprise/coderd/schedule/). Either omit the scope or use a broader one. (Mafu-san)

"Conversion chain: API (milliseconds) to time.Duration to int64 nanoseconds to DB bigint. Read path reverses cleanly. No-op checks in both AGPL and enterprise Set correctly include the new field. The chain is the same as round 1, but now without the migration and notification code in the way, it is easier to verify." (Melody)

🤖 This review was automatically generated with Coder Agents.


if int64(opts.DefaultTTL) == tpl.DefaultTTL &&
int64(opts.ActivityBump) == tpl.ActivityBump &&
int64(opts.TimeTilAutostopNotify) == tpl.TimeTilAutostopNotify &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-11] Enterprise schedule store's TimeTilAutostopNotify plumbing has zero test coverage.

Three code paths changed in this file: Get (line 91), the no-op short-circuit (line 120), and Set params (line 158). No enterprise test exercises any of them. The no-op comparison is the risky one: if this line had a copy-paste error (e.g., compared against tpl.ActivityBump instead of tpl.TimeTilAutostopNotify), updates to TimeTilAutostopNotify on enterprise deployments would be silently swallowed. The AGPL store has the same pattern and is exercised through integration tests, but the enterprise store has its own Set implementation with different conditional logic.

(Hisoka)

🤖

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-11] Still open, no author response across two rounds.

enterprise/coderd/schedule/template_test.go has zero references to TimeTilAutostopNotify. Three production lines in the enterprise store are untested: Get return (line 91), no-change guard (line 120), Set params (line 158). The AGPL integration tests exercise the AGPL store, not this one.

Mitigating: the enterprise code is character-for-character identical to the AGPL store's handling, and ActivityBump has the same pre-existing gap. But a copy-paste error in the no-change comparison (e.g., comparing against tpl.ActivityBump instead of tpl.TimeTilAutostopNotify) would silently swallow updates on enterprise deployments.

What response is needed: add a test row to TestTemplateTTL or another enterprise schedule test that verifies TimeTilAutostopNotify round-trips through the enterprise store, or explain why enterprise test coverage is intentionally deferred.

(Bisky, Mafuuu, Hisoka)

🤖

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified fixed in 2537503fb48c. TestTemplateScheduleTimeTilAutostopNotify covers:

  1. Set/Get round-trip (1h persists through enterprise store)
  2. No-op guard: changes only TimeTilAutostopNotify from 1h to 2h with all other options identical, confirming the comparison at line 120 references the correct field

The test comment explicitly documents the CRF-11 scenario. Well done.

🤖

Comment thread coderd/templates_test.go
assert.Equal(t, template.AllowUserAutostop, updated.AllowUserAutostop)
})

t.Run("TimeTilAutostopNotifyPreservedWhenOmitted", func(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P4 [CRF-12] No integration test for disabling autostop notify after enabling it (patch to ptr.Ref(int64(0)) after creating with a non-zero value).

This is a real user flow: "I enabled reminders, now I want to disable them." The resolver test (TimeTilAutostopNotifyMillisZeroExplicit) covers the logic, but no API-level test round-trips the disable path through the handler. If the handler had a bug in zero-value plumbing (e.g., treating ptr.Ref(int64(0)) the same as nil), this flow would break silently.

(Bisky)

🤖

Comment thread coderd/templates_test.go
require.Contains(t, err.Error(), "default_ttl_ms: Must be a positive integer")
})

t.Run("TimeTilAutostopNotifyTooLow", func(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P4 [CRF-13] No boundary-value test at exactly 1 minute (60,000ms). Validation accepts 0 and >= 1 minute, rejects 0 < x < 1 minute. Tests exercise 30s (rejected) and 5m (accepted) but never the exact boundary. If someone changed < time.Minute to <= time.Minute, no existing test would catch it.

(Bisky)

🤖

Comment thread coderd/templates_test.go
VersionID: version.ID,
TimeTilAutostopNotifyMillis: ptr.Ref(int64(30_000)),
})
var apiErr *codersdk.Error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit [CRF-14] TimeTilAutostopNotifyTooLow and TimeTilAutostopNotifyNegative assert apiErr.Validations[0].Field but not apiErr.Validations[0].Detail. The two tests send different invalid values (30s vs -1) to exercise distinct validation branches that produce different error messages, but both only check the field name. If someone accidentally swapped the messages, both tests would still pass.

(Chopper)

🤖

@sreya sreya force-pushed the autostop-notif/03-schedule-sdk branch from 0aa4537 to 987590b Compare June 17, 2026 05:43
@sreya sreya force-pushed the autostop-notif/02-template branch from d21121f to 1c5a2f2 Compare June 22, 2026 15:01
@sreya sreya force-pushed the autostop-notif/03-schedule-sdk branch from 987590b to e28f8d6 Compare June 22, 2026 15:01
@sreya

sreya commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for adding the boundary test, the disable-after-enable test, and the Detail assertions. CRF-12, CRF-13, CRF-14 all verified fixed by the panel.

One finding remains from R3: 1 P3 across 1 inline comment. CRF-11 (enterprise test coverage) has had no response across two rounds.

🤖 This review was automatically generated with Coder Agents.

@sreya sreya force-pushed the autostop-notif/03-schedule-sdk branch from e28f8d6 to f98783e Compare June 22, 2026 16:48
Add the per-template time_til_autostop_notify duration (ns; 0=disabled) through the schedule store (AGPL + enterprise), codersdk Template/CreateTemplateRequest/UpdateTemplateMeta, the template create/patch handlers, and the CLI --autostop-reminder flag. Validation rejects negatives and sub-minute values. Not entitlement-gated, mirrors activity_bump.
@sreya sreya force-pushed the autostop-notif/02-template branch from 1c5a2f2 to f9b398f Compare June 22, 2026 16:50
@sreya sreya force-pushed the autostop-notif/03-schedule-sdk branch from f98783e to 2537503 Compare June 22, 2026 16:50
@sreya

sreya commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CRF-11 verified fixed. TestTemplateScheduleTimeTilAutostopNotify exercises the enterprise store's Set/Get round-trip and, critically, the no-op short-circuit guard: it changes only TimeTilAutostopNotify while keeping all other options identical, confirming the comparison uses the correct field. This is the exact scenario the finding described.

All 14 findings across 5 rounds are resolved. Test density is now 74%. The plumbing is clean and thoroughly tested across both AGPL and enterprise stores.

🤖 This review was automatically generated with Coder Agents.

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