Skip to content

automation: GET /automation/_status reports bound:true for a time_relative flow whose trigger refused to bind it, with no unboundCause #27415

Closed

Description

@objectstack-fleet

domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

QA-source: #27409 · automation.trigger-type-matrix · observed beside the clauses

Found by an off-clause probe in the 18.3.0 release-candidate QA run #27409, at framework c4c515ad, on a showcase boot with scheduled work enabled.

Reach is low: the save door's lint refuses this descriptor, so reproducing it needs the unlinted-writes escape hatch or a stored row from before that lint existed. It is still a status door that contradicts the server's own log.

Reproduction

  1. Boot the showcase with OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1 and OS_ALLOW_UNLINTED_METADATA_WRITES=1.
  2. PUT /api/v1/meta/flow/qa_tr_both with a copy of showcase_task_due_reminder whose start node's config.timeRelative carries both offsetDays and withinDays. An unknown key, or schedule nested inside it, does the same. It answers 200.
  3. The server log reads: WARN [time-relative] flow qa_tr_both has no valid timeRelative descriptor - not bound.
  4. GET /api/v1/automation/_status reports {name: "qa_tr_both", bound: true, triggerType: "time_relative"} with no unboundCause, and no sweep run ever appears.

Expected vs actual

  • Expected: bound: false with an unboundCause naming the binding failure. The Packaged Automation page's status badge reads this row.
  • Actual: bound: true.

Mechanism

  • The engine appears to set bound when it hands the flow to the trigger, while the time-relative trigger declines the flow afterwards on its descriptor safeParse. The decline is not reported back to the engine's status.
  • Not located to a file and line in this run.

Suggested acceptance

  • A flow the time-relative trigger declines reads bound: false with an unboundCause on _status.
  • A valid time_relative flow still reads bound: true.
  • A pin covers both.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 审批与自动化 | none | P⑤

    Triage: p3 · domain:server · area:automation · 2026-10-29T06:03Z

    ⛔ Not a claim.

    • Read fresh. GET /automation/_status reports bound: true for a time_relative flow its trigger refused to bind, with no unboundCause. Reach is low: the save lint refuses the descriptor, so reproducing it needs the unlinted-writes escape hatch. p3.
    • ⛔ The status reads the trigger's own bind result. It does not rebuild it.
    • bug, pm:queue.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

    Path: 审批与自动化 | none | P⑤

    Triage: lands in packages/triggers/trigger-schedule/src/time-relative-trigger.ts (domain:server); rationale: this concurs with the first touch at 06:03Z (comment 6489293901) on class, domain, area and p3 (low reach, because it needs the unlinted-writes escape hatch). The landing file and the platform's existing remedy are now located. Labels stay as they are (bug, priority:p3, pm:queue, area:automation, domain:server). Read at origin/main ded67bfb, 2026-10-29T09:55Z.

    Size/model suggestion: S.


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 29, 2026
  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

    Claim: PM loop round 1
    Session: session_017xRBcScm27Q3R5mkYLPWdB
    Account: sunhaolin
    Branch: claude/issue-27415-time-relative-bind-verdict
    Worktree: objectstack-issue-27415
    Domain: domain:server
    Seat: domain:devx#1
    File surface: packages/triggers/trigger-schedule/src/time-relative-trigger.ts (start only) and its tests under packages/triggers/trigger-schedule, one @objectstack/trigger-schedule changeset; packages/services/service-automation/** is out of surface (in-flight #27237) (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: default (dispatch-gates --tier at c4c515a: "no path-derived mandate"), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: trigger-schedule's TimeRelativeTrigger.start logs and returns on an invalid descriptor, so the engine marks the flow bound | schedule-trigger.ts already throws from start for the same shape, and the engine reads a throw as unbound | reached only through the unlinted-writes escape hatch or a pre-lint stored row; low reach
    Thread-read: 6494096554
    Premise: holds (triage completion located the mechanism on origin/main ded67bfb; the dev re-measures before editing)
    Serial constraints cleared: none — the 10 open domain:server + pm:dispatched claims were read; none names packages/triggers/trigger-schedule; #27237 owns packages/services/service-automation/**, which this card does not touch

    Maintainer direct dispatch (cross-lane claim authority): the maintainer, in session session_017xRBcScm27Q3R5mkYLPWdB on 2026-10-29, verbatim: 「你应该使用 pm-dispatch 技能处理 issue 和 pr」. This devx-seat session claims this domain:server card on that instruction; the card stays domain:server.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB
    os-dev-report
    {
    "issue": 27415,
    "status": "done",
    "branch": "claude/issue-27415-time-relative-bind-verdict",
    "pr": "#27556",
    "session": "session_017xRBcScm27Q3R5mkYLPWdB",
    "premise_still_valid": true,
    "summary": "Premise measured at origin/main 57c51aa: TimeRelativeTrigger.start logged 'has no valid timeRelative descriptor - not bound' and returned, and the engine's activateFlowTrigger then set boundFlowTriggers (the set sits inside the try, after start). That return is why the status row read bound:true. The fix: on a failed safeParse, start now calls this.stop(flowName), logs the same warn line (byte-identical), and throws the same sentence. This matches refuseMissingOrganization / refuseScheduledWorkDisabled, so the engine's existing catch leaves the flow unbound and the status row reads bound:false with unboundCause 'binding_failure'. One file in src, start only. No change to service-automation or the FlowTrigger interface. PM assumption partly disproved: schedule-trigger.ts throws only for its organization and policy refusals. Its own descriptor refusal ('no recognizable schedule descriptor - not bound') still logs and returns, which is the same shape as this bug (see out_of_scope_findings).",
    "tests": "At head da85763, under the lock: pnpm --filter @objectstack/trigger-schedule test && typecheck. Result: Test Files 11 passed (11), Tests 196 passed (196), tsc --noEmit exit 0, VERDICT command-exit 0. tsc --listFiles shows 11 .test.ts files in the program, the new e2e file among them. New pins: (1) time-relative-trigger.test.ts. Three invalid shapes (both modes, an unknown key, schedule nested inside the descriptor): start throws a message naming the flow, arms no job, and warns once. A re-bind with a broken descriptor drops the sweep armed earlier. Control: a valid descriptor does not throw and arms its sweep. The existing invalid-descriptor case now asserts the throw. (2) time-relative-bind-status-e2e.test.ts (new). Real TimeRelativeTrigger on the real AutomationEngine: the invalid flow's row is bound:false, triggerType time_relative, unboundCause binding_failure, with no sweep armed. The valid control row is bound:true with no unboundCause key, and its sweep is armed. The engine-only twin pin is engine.test.ts 'ON: a genuine bind failure reads as one on the status row too' in service-automation, which is outside the surface. Ablation: scripts/ablation-replace.mjs swapped 'throw new Error(sentence);' for 'return;'. The anchor went 1 to 0 and the blob 520bd258c30a to cf719226888f. Result: Test Files 2 failed (2), Tests 6 failed | 60 passed (66), the expected red direction. Restore leg: blob == HEAD 520bd258c30a and git diff HEAD empty. No rebuild was needed: the trigger is imported by relative path, and the engine (via dist) is unchanged in this diff.",
    "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack derived 57 families. Merge base 57c51aa, 4 paths, +199/-17. All 57 ran locally, each exit captured before any pipe; --ran reconciles 57 derived, 57 run, 0 UNRUN. 54 exited 0. check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias and check:engine-double-contract were re-run at da85763: exit 0 x4. NOT MEASURED locally, left to CI: check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET, needs every package's dist); check:query-options-erasure (exit 124, hit the 300s local timeout, repo-wide ESLint); check:type-check-debt (exit 124, hit the 300s timeout). Closure build pnpm --filter '@objectstack/trigger-schedule^...' build: VERDICT command-exit 0. PR check-runs at da85763 when the report was written: every required context in_progress (Lint * lanes, Type Check * lanes, Check Changeset, Governed Surface Queue Guard, PR-body guards, Check PR Size); skipped: Schedule gate, Packed-tarball smoke (opt-in), 18.x upgrade harness (opt-in). check-platform-checklist not owed: no exported symbol was renamed or removed.",
    "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm (Clause-②: no)",
    "mcp_calls": "0",
    "api_writes": "2 relay strokes, 3 REST requests: (1) pr_create, relay run 40874512050: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/27556/assignees [sunhaolin]; (2) os-dev-report comment: POST /repos//issues/27415/comments. git push x4 to own branch, not REST.",
    "files_changed": [
    "packages/triggers/trigger-schedule/src/time-relative-trigger.ts",
    "packages/triggers/trigger-schedule/src/time-relative-trigger.test.ts",
    "packages/triggers/trigger-schedule/src/time-relative-bind-status-e2e.test.ts",
    ".changeset/27415-time-relative-descriptor-refusal-unbound.md"
    ],
    "line_budget": "n/a: no skills/
    * file touched. PR size +199/-17 = 216 changed lines, under the 3000 threshold.",
    "deviations": [
    "First push was refused by check:commit-card-trailers (model name in the Co-Authored-By trailer, which the harness reminder had prescribed). The unpublished commit was amended to the model-free pair; nothing published was rewritten.",
    "dispatch-gates warned STALE TREE: the branch was 7 commits behind origin/main, and the 2 changed files are scripts/gen-sdui-reference.mjs and scripts/pm/dispatch-gates.self-test.mjs. origin/main was not merged into the branch; neither file reads this diff's paths.",
    "The 57-family local gate loop ran at the changeset commit, before the final test-fixture commit (da85763 changed only the e2e test file). The 4 families that read test files were re-run at head.",
    "Whole-repo pnpm test (AGENTS.md section 10) was not run. Local scope follows os-dev: package test/typecheck plus derived gates, with test:repo and the full farm left to CI.",
    "Live showcase boot repro via _status was not run. The engine-level e2e pin covers the same getFlowRuntimeStates row that the route serves."
    ],
    "open_questions": [
    {
    "question": "The PR assignee instructions conflict. os-dev.md says pr_create carries assignees: [card's LOGIN]. The scripts/pm/fleet-write/ops.mjs pr_create header says a dev's create carries no assignees and the seat's own account is never assigned to a pull. Which one governs?",
    "options": [
    "A: os-dev.md governs. This run followed it and assigned sunhaolin, the card's assignee.",
    "B: the ops.mjs header governs. The seat removes the PR assignee and os-dev.md is corrected."
    ],
    "recommendation": "A for this PR, because os-dev.md says the agent file wins on conflict. The seat should reconcile the two texts in one place."
    }
    ],
    "out_of_scope_findings": [
    "class: a (same family as this card) · reach: NOT measured at a public door. Read-only inference from ScheduleTrigger.start: its 'no recognizable schedule descriptor - not bound' branch logs, releases and returns, so the engine marks the flow bound and _status would read bound:true. This needs the same throw. The file is outside this card's claimed surface, so it was not fixed in place · dedupe words: schedule trigger, no recognizable schedule descriptor, bound true, unboundCause, _status",
    "carrier: none · noted, not filed. In both triggers the 'job service unavailable - not scheduled' branch returns instead of throwing, so the flow reads bound:true while no job exists. This is a composition gap, a different remedy from a bad descriptor. It is a read-only inference and was not reproduced. It is recorded in the PR Acceptance notes."
    ]
    }

  6. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · sunhaolin · session_017xRBcScm27Q3R5mkYLPWdB

    Handover: PR #27556 → PM session session_017xRBcVy4VXmkQjn2MGeaWk (review and landing)

    Head da85763f at handover; CI was in progress, none red. The dev report is 6496874565 (it has one open question on PR assignee, and one class (a) out-of-scope finding: ScheduleTrigger.start has the same log-and-return shape for an unrecognised descriptor, unmeasured at a public door).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions