Skip to content

feat(jobs): expose the timestamp, progress and queue-position properties on Job/AsyncJob - #171

Open
mattmillerai wants to merge 5 commits into
mainfrom
matt/be-15899-job-lifecycle-properties
Open

mattmillerai wants to merge 5 commits into
mainfrom
matt/be-15899-job-lifecycle-properties

Conversation

@mattmillerai

@mattmillerai mattmillerai commented Sep 19, 2026 •

Copy link
Copy Markdown
Member

ELI-5

A job handle already knows when the job was created, when it started, when it finished, how far along it is and where it sits in the queue — it just never told you. It handed back four things (id, status, outputs, error) and kept the rest behind a private attribute, so "how long did this run take?" meant reaching into job._model. This adds the other eight as ordinary read-only properties on both Job and AsyncJob.

What changed

created_at, started_at, completed_at, expires_at, progress, queue_position, metrics and urls are now properties on Job and on AsyncJob.

  • The timestamps come back as the timezone-aware datetimes the model already parses, not as strings — which is the whole point, since job.completed_at - job.started_at is what a caller actually wants and is the one thing they cannot do for themselves against a raw string.
  • The nullable wire fields (started_at, completed_at, progress, queue_position, metrics) reach the caller as None. created_at, expires_at and urls are non-nullable in the contract and are always set.
  • All eight are views onto the state the handle currently holds, exactly like status and outputs: nothing re-fetches implicitly, and refresh() / wait() / result() are what move them. There is a test for each half of that — reads do not poll, and a refresh() does move them.
  • urls also makes an existing README claim true: the authentication section already told readers about job.urls.self/cancel/events, which until now did not exist on the public surface. The one private-attribute access left in the test suite (job._model.urls.events, in test_sse_idle.py) now goes through the property.

Two judgment calls worth a reviewer's eye

progress returns the SDK's Progress dataclass, not the generated model. The SDK already has a public Progress — it is what job.events() yields and what the README's case Progress() as p: matches. Returning comfy_low.models.Progress here would have put two different classes called Progress on one surface, and isinstance(job.progress, comfy_sdk.Progress) would have been False. Lifting it means a snapshot read off a handle and one received live are the same type, and one case Progress() matches either.

That lift needed current_node_class, the one field of the contract's progress schema the SSE decoder was silently dropping — without it the lift would have been lossy. It is now decoded from progress frames too, and is appended to the dataclass rather than slotted in beside current_node, so the published positional order is unchanged for anyone constructing a Progress positionally. A test pins the two paths together: for the same payload, the model lift and the stream decoder must produce an equal Progress, so a field added to one and not the other fails.

urls passes the generated comfy_low.models.JobUrls through unwrapped, the same way error already passes JobError through. It is deliberately not re-exported at the comfy_sdk top level — that surface is guarded by a test and widening it was not part of this change — so a caller who wants to name the type imports it from comfy_low.models.

How this was tested

The stub server grew knobs for the nullable lifecycle fields (job_started_at, job_completed_at, job_progress, job_queue_position, job_metrics), so the populated case and the null case both come off a real response body rather than a hand-built model — which is what puts the timestamp parsing itself under test. Defaults are unchanged from the previously hardcoded values, so no existing test's fixture moved.

tests/test_job_lifecycle_properties.py covers every property in the populated and the null shape, on the sync and the async class, plus the does-not-re-fetch and the does-move-on-refresh halves of the contract.

The repo's existing tests/test_sync_async_parity.py is what guarantees the sync and async surfaces stay in step here, and it is not taken on faith: deleting AsyncJob.queue_position locally fails it with Job/AsyncJob public surface diverges: sync-only=['queue_position'], and it was restored before committing.

Provenance

  • Authored by: agent-work loop
  • Verified: ruff check .: all checks passed; ruff format --check .: 58 files already formatted; mypy src: no issues in 21 source files; pytest: 987 passed, 9 skipped; scripts/check_public_repo_hygiene.py: no internal-only references
  • Deviations: none against the plan; the acceptance gaps I could not close are under Residual below

Residual

Not fixed here, and actionable on its own:

  • The cross-SDK surface-parity check between this SDK and the TypeScript one was never run. It is the acceptance criterion that spans two repositories, and it lives outside this one — nothing in this repo can execute it, and the TypeScript side was not reachable from where this was built. What is verified is the in-repo parity test, which compares Job against AsyncJob only. So the names added here match the ones the request specified, but nobody has yet observed them matching the TypeScript side. Someone should run that check once both halves are merged; a name mismatch would be a rename on one side, not a redesign.
  • The TypeScript half is not in this PR and this change does not depend on it — the two are expected to land together, and a reviewer merging only this one leaves the parity check failing in the other direction until the counterpart lands.
  • progress has never been observed populated by a real server, only by the stub. The request notes that Comfy Cloud returns null for it on a poll today (a server-side gap tracked separately); that is taken on the request's word, not verified — this environment has no live credentials, and tests/integration/test_gateway_e2e.py skips without COMFY_BASE_URL and COMFY_API_KEY. For that reason no named-surface claim went into the README or the docstrings: they say "not every surface fills this in on a poll" rather than naming one. If the server gap is closed later, the wording is worth revisiting to say so positively.
  • Three of the eight properties have no null case to test, so the "populated and None, on both classes" criterion is met literally for the five nullable ones only. created_at, expires_at and urls are non-nullable in the contract; the tests assert them in both scenarios and say why there is no third one. If the contract ever makes one of them nullable, that test is where it has to be noticed.

Summary by CodeRabbit

  • New Features

    • Job handles now expose creation, start, completion, and expiration timestamps.
    • View current progress, queue position, metrics, and related job URLs.
    • Synchronous and asynchronous jobs provide the same lifecycle information.
    • Progress events include the currently executing node class.
  • Documentation

    • Added guidance describing job handle properties, cached state, refresh behavior, and unavailable progress values.
    • Clarified that job state updates through refresh(), wait(), or result(), and that absent progress is represented as None.

…ies on Job/AsyncJob

Job and AsyncJob both hold the full v2 job model and surfaced four fields
off it (id, status, outputs, error). created_at, started_at, completed_at,
expires_at, progress, queue_position, metrics and urls were reachable only
through the private model attribute, so a caller could not read a job's
duration without one.

All eight land as read-only properties on both classes. Timestamps come
back as the timezone-aware datetimes the model already parses, so
job.completed_at - job.started_at is the run duration; the nullable wire
fields reach the caller as None. Like the existing properties these are
views onto the state the handle holds — nothing re-fetches.

progress is lifted into the SDK's own Progress dataclass rather than the
generated model, so a snapshot read off a handle is the same type as the
progress frames job.events() yields and one 'case Progress()' matches
either. That lift needed current_node_class, which the event decoder was
dropping; it is appended to the dataclass so the published positional
order is unchanged.
@mattmillerai mattmillerai added agent-coded Authored by the agent-work loop cursor-review Request an automated Cursor review labels Sep 19, 2026
@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: Comfy-Org/comfy-python-sdk/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Team
  • Run ID: 151d7f6e-c947-4726-8ae0-6389367517f8

📥 Commits

Reviewing files that changed from the base of the PR and between 8f92101 and 78d49d1.


📒 Files selected for processing (7)
  • CHANGELOG.md
  • README.md
  • src/comfy_sdk/events.py
  • src/comfy_sdk/jobs.py
  • tests/conftest.py
  • tests/test_event_types.py
  • tests/test_job_lifecycle_properties.py

Included review availability: This review used your included allowance. 8 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 10 reviews per hour.



📝 Walkthrough

Walkthrough

The SDK adds cached lifecycle, progress, queue, metrics, and URL properties to synchronous and asynchronous job handles. Progress conversion now preserves current_node_class. Tests cover populated, nullable, refreshed, and non-refetched state.

Changes

Job state and progress accessors

Layer / File(s) Summary
Progress conversion and event fields
src/comfy_sdk/events.py, tests/test_event_types.py
Progress includes current_node_class. progress_from_model converts all LowProgress fields into an SDK Progress value.
Job state properties and refresh coverage
src/comfy_sdk/jobs.py, tests/conftest.py, tests/test_job_lifecycle_properties.py, tests/test_sse_idle.py, README.md, CHANGELOG.md
Job and AsyncJob expose cached lifecycle, progress, queue, metrics, and URL properties. Test responses provide configurable state, and tests cover refresh behavior and nullable values. Documentation describes the new properties.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Job
  participant Server
  participant progress_from_model
  Client->>Job: access properties or call refresh()
  Job->>Server: request current job state
  Server-->>Job: return timestamps, queue, metrics, URLs, and progress
  Job->>progress_from_model: convert progress snapshot
  progress_from_model-->>Job: return Progress
  Job-->>Client: expose cached properties
Loading

Suggested reviewers: wei-hai


Merge Risk: ⚪ Minimal · up to 78d49

The lifecycle properties appear ready to merge after normal checks; no concrete issue requiring a fix was identified.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 46.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 50 functions across 6 files. (2 skipped: … 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 clearly identifies the primary change: exposing timestamp, progress, and queue-position properties on both Job and AsyncJob. It is concise and directly related to the changeset, although…
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.

Full details: Docstring Coverage

Explanation

Docstring coverage is 46.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 50 functions across 6 files. (2 skipped: 2 unsupported.)



  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR



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

@mattmillerai
mattmillerai marked this pull request as ready for review September 19, 2026 07:49
@mattmillerai
mattmillerai requested review from a team as code owners September 19, 2026 07:49
coderabbitai[bot]
coderabbitai Bot previously approved these changes Sep 19, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

⚠️ Panel did not produce any findings.

Every reviewer in the matrix failed to contribute — see the panel summary for which cells errored, and the run logs for the underlying cause.

Panel: 0/6 reviewers contributed findings.

Reviewers that did not contribute: claude-opus-5-thinking-max:adversarial (error), gpt-5.6-sol-max:adversarial (error), kimi-k3-high:adversarial (error), claude-opus-5-thinking-max:edge-case (error), gpt-5.6-sol-max:edge-case (error), kimi-k3-high:edge-case (error)

@mattmillerai mattmillerai added the full-autonomy Approved AI-brownfield: merges on machine gates alone, no human approver. Design doc + flag req'd. label Sep 20, 2026
…ecycle-properties

# Conflicts:
#	CHANGELOG.md
The main-branch spec sync (86d8f2c) declared X-Comfy-Credits-Used on
runRouterModel's 200 after credits_used was already lifted from it,
tripping the reconciliation tripwire on merge. Move credits_used into
_CONTRACT_HEADER_LIFTS per the tripwire's own instructions, and give
its presence-check a valid decimal (the generic "x" sentinel parses to
None same as absent, since credits_used goes through Decimal parsing).
robinjhuang
robinjhuang previously approved these changes Sep 20, 2026

@robinjhuang robinjhuang left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Auto-approved under the full-autonomy policy.

Gates verified at 16a5c8eb0cd0059b7d89acece861fb285fd1a9af:

  • full-autonomy label present
  • assigned to, or review requested from, @robinjhuang
  • not a draft
  • 8 required check(s) green — none failing, none pending

Issued by full-autonomy-approve.yml (run). This approval attests
that the machine gates above passed at this commit. It does not attest that a
human read the diff.

@guill guill left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This needs to be designed along with Developer Platform.

vqt123 added 2 commits October 9, 2026 15:38
…ecycle-properties

# Conflicts:
#	CHANGELOG.md
#	src/comfy_sdk/jobs.py
#	tests/conftest.py
#	tests/test_router_spec_contract.py
Editing the dict or JobUrls a read returned rewrote the handle's own model,
whose urls refresh(), cancel() and events() follow. Copy them, as metadata
already does and as the TypeScript SDK's accessors do.
@vqt123

vqt123 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

@guill on your review (designed along with Developer Platform): I'm on Developer Platform and reviewed this together with its TypeScript twin, Comfy-Org/comfy-typescript-sdk#162, which merged today. The two now ship the same surface: the same eight properties (created_at/createdAt and so on) with the same nullability from the Job schema, progress returning the SDK's own Progress type that events() yields (now carrying current_node_class on both sides), and metrics/urls handed back as copies so editing one does not change the handle (fixed here in 78d49d1; metadata from #204 already worked that way). Also merged main in (dd6be49). If you had a different design in mind for these accessors, say what and I'll change it here; otherwise could you take another look?

@vqt123
vqt123 requested a review from guill October 9, 2026 19:39
@vqt123

vqt123 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Approving at 78d49d1c0e.

Merged main in (dd6be49): conflicts in jobs.py, CHANGELOG, conftest.py and test_router_spec_contract.py. Kept both the job metadata properties from #204 and the lifecycle properties here, made the stub job body take both the metadata and the lifecycle knobs, and took main's version of the credits_used contract test, which already reconciles the same header.

Fixed in 78d49d1:

  • metrics and urls returned the handle's own dict and JobUrls, so editing what a read gave back rewrote the links refresh(), cancel() and events() follow. They now return copies, as metadata does and as the TypeScript twin (comfy-typescript-sdk#162) does; tests on Job and AsyncJob, README line added.

Clean: the nullable vs required split against the Job schema in spec/openapi.yaml, progress_from_model field for field against the generated Progress (and equal to the stream decoder), current_node_class appended so positional order holds, refresh moving the view and reads never polling, sync/async parity, and parity with comfy-typescript-sdk#162 (same eight properties, same nullability, same events Progress type).

Checked: pytest (1248 passed on the merge), ruff, ruff format, mypy, codegen drift and the hygiene script locally; the copy tests fail with the fix reverted.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-coded Authored by the agent-work loop cursor-review Request an automated Cursor review full-autonomy Approved AI-brownfield: merges on machine gates alone, no human approver. Design doc + flag req'd.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants