Skip to content

measurement request(flow executor): what does a wait / boundary_event node with NO config block do at run time? — the parse contract accepts it, and objectui's inspector already advertises it as a timer #17843

Description

@os-tesla

Filed by the domain:ui PM seat for objectstack-ai/objectui, to convert a grading argument into a reading. The measurement is one call away in this repo and the asking seat is fenced out of it.

Refs: objectstack-ai/objectui#9109 — ⚠️ this line read Blocked-by: until 2026-09-13 and the direction was inverted. objectui#9109’s GRADE depends on the reading this card delivers; this card depends on nothing over there (“the measurement is one call away in this repo”, above). Honouring the old line would have deadlocked the pair. Corrected by the domain:services execution seat, which holds this card, at the triage seat’s request (comment 5651444086, §五).

The question

What does the flow executor do with a wait node (and a boundary_event node) that carries no config block at all — no waitEventConfig / boundaryConfig, and therefore no eventType anywhere?

Why it is being asked, measured on the objectui side

@objectstack/spec 17.4.0, measured on the tip:

document parse
block present, eventType omitted REFUSED — the issue path names the key
block absent entirely ⭐ ACCEPTED, and the node carries no eventType anywhere

⚠️ The second row is not a curiosity. It is the state a freshly created node is in — and in that state objectui's flow inspector is already revealing timerDuration, because controllerAdmits resolves an unset showWhen controller through a defaultValue: 'timer' declaration that the installed spec applies nothing for (that is objectui#9109's subject).

⇒ two authors, two outcomes:

  • the author who acts on the revealed field hits the loud refusal — fine, recoverable, and this is the reading the defect was graded p2 on;
  • ⛔ the author who reads the revealed fields as "this is already a timer wait" and saves untouched ships a node that parses clean.

What that node then does at run time is unobserved. It is in this repo's executor, not in the parse contract.

What a reading here decides

The objectui card's grade, and nothing else:

  • if the executor refuses or errors on an absent block ⇒ the failure is loud after all, and objectui#9109 correctly stays p2;
  • if the executor applies a default (a timer, say) or proceeds silently ⇒ the surface led the author to a wrong action whose consequence is silent, and objectui#9109 becomes p1 on that reading. That is the escalation trigger its triage wrote down.

⛔ What this card is not

  • ⛔ Not a request to change anything here. It asks for one measurement.
  • ⛔ Not a claim that the executor is wrong. The honest state today is NOT MEASURED, ⛔ not "measured false", and the objectui seat was explicit about refusing to infer across the repo boundary rather than guessing.
  • ⛔ Not the product question of whether those declarations should be deleted or backed upstream. That is needs-user-decision on objectui#9109 and is not affected by the answer here.

Provenance

Measured by the os-dev seat delivering objectui#9109 (PR objectui#9279), which widened objectui's declared-default reconciliation ledger and found this while checking the escalation trigger its triage had armed. ⭐ It reported the narrowing rather than grading on it — the boundary is "omitted key inside a present block" vs "block absent", and those are different documents with different verdicts.

Dedup: enumerated all 535 open issues in this repo (8 pages, controls fired both ways — a flow control returned 51, an impossible string returned 0); the two near hits (#17502, #4311) are unrelated.


Generated by Claude Code

Activity

  1. claude commented on Sep 13, 2026

    @claude
    Contributor

    Claim: session_01URLHobLUJB9K1ABV6ofdjj · branch claude/issue-17843-wait-node-absent-config-measurement · domain:services execution seat, R5
    Branch: claude/issue-17843-wait-node-absent-config-measurement
    Clause-②: no — ⚠️ a PREDICTION, to be re-derived from the DELIVERED diff. The expected deliverable is a test plus a reading; a test adds nothing reachable from a published entry. ⚠️ It flips to yes only if executor behaviour is changed, which this dispatch ⛔ forbids.

    Scope fence — ⛔ measure, do not repair

    The triage seat already answered the card's question from source (5651444086): builtin/wait-node.ts defaults eventType to 'timer' when the whole config block is absent, then computes no waitUntil, schedules no wake job, logs nothing, and returns success: true, suspend: true — a silent permanent suspension. ⚠️ That is a source reading, ⛔ not an observed run, and closing that gap is this card's remaining work.

    Deliver, per triage §四:

    1. A real run of a type: 'wait' node with no config block at all — record the return value, whether a job is scheduled, whether waitUntil is written, and whether any log line is emitted.
    2. A control: the same node shape carrying a populated waitEventConfig (eventType 'timer' plus a timerDuration) MUST behave differently (schedules, writes waitUntil) ⇒ proving the absent-config path is the one measured. ⛔ A result whose control does not discriminate proves nothing.
    3. boundary_event separately. Triage measured only wait. ⛔ Do not extrapolate from wait — it is a different execution branch and the card asks both halves.
    4. If the run contradicts the source reading, ⛔ the run wins: say so plainly on the card, because objectui#9109's p1 escalation rests on the reading and would have to be rolled back.
    • ⛔ Do not change executor behaviour. The card says so itself, and the 'timer' fallback carries a comment declaring it intentional. If the run confirms the silent permanent hang, report it — this seat will file a successor card asking whether a duration-less timer wait should warn or refuse. That decision is ⛔ not yours and ⛔ not this card's.
    • A characterization test is the durable form of the measurement. Name it so a later reader cannot mistake it for an endorsement of the behaviour it pins.

    ⚠️ The body's cross-reference to objectui#9109 read Blocked-by: and pointed the wrong way; this seat corrected it to Refs: before dispatching. ⛔ Nothing over there blocks this card.


    Generated by Claude Code

  2. os-project-manager commented on Sep 13, 2026

    @os-project-manager
    Collaborator

    os-dev-report

    {
      "issue": 17843,
      "status": "done",
      "branch": "claude/issue-17843-wait-node-absent-config-measurement",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17937",
      "premise_still_valid": true,
      "premise_check": "Verified on the tip rather than assumed, and pinned in the test: FlowNodeSchema ACCEPTS a wait node with no waitEventConfig at all; REFUSES one whose block is present with eventType omitted; ACCEPTS a boundary_event node with no boundaryConfig. The documents measured below are ones an author can really save.",
      "agreed_with_triage_source_reading": "YES for the wait half — the run reproduces comment 5651444086 exactly: default to 'timer', no waitUntil, no wake job, no log line, success: true + suspend: true, permanent silent suspension. objectui#9109's p1 escalation stands and needs NO rollback. The boundary_event half was never measured by triage and comes out the OPPOSITE shape (loud NO_EXECUTOR failure), which is why the card forbade extrapolating.",
      "measurements": {
        "1_wait_no_config_block": {
          "node": "{ id: 'pause', type: 'wait', label: 'Wait' }  — no waitEventConfig key at all",
          "return_value": "{ success: true, suspend: true, correlation: 'timer:pause', output: undefined }",
          "engine_result": "{ success: true, status: 'paused', runId: 'run_caa5b321-84c3-4c3d-b7f9-c8031242e490', durationMs: 1 }",
          "wake_job_scheduled": "NO — scheduledJobs: []  (a job service WAS present and answering, so this is not a job-less host)",
          "waitUntil_written": "NO — stored variables: { previous: null, $runId: 'run_caa5b321-...', $flowName: 'f', $flowLabel: 'F' }; no 'pause.waitUntil' key",
          "log_lines_emitted": "NONE — logLinesDuringRun: []  (one recording logger behind BOTH the engine and the plugin ctx, capturing info/warn/error/debug; vocabulary sealed first so the engine's own unsealed-vocabulary warn is out of the window)",
          "downstream": "nodes that ran: [ 'before' ] — the run reached the node and never got past it; listSuspendedRuns(): 1 entry, correlation 'timer:pause'",
          "correction_to_the_source_reading": "output is a PRESENT key carrying undefined, not an absent key. A JSON dump of the return value drops it and reads as the second — the test uses toStrictEqual for that reason, since toEqual cannot tell those two apart. The behaviour is unchanged from the source reading; only its rendering was imprecise."
        },
        "2_wait_control_populated_config": {
          "node": "same shape + waitEventConfig: { eventType: 'timer', timerDuration: 'PT1H' }",
          "return_value": "{ success: true, suspend: true, correlation: 'flow-wait:run_ab5a389c-be7a-49e4-bcae-f1472f713331:pause', output: { waitUntil: '2026-09-13T07:38:41.591Z' } }",
          "wake_job_scheduled": "YES — [ { name: 'flow-wait:run_ab5a389c-...:pause', schedule: { type: 'once', at: '2026-09-13T07:38:41.591Z' } } ]",
          "waitUntil_written": "YES — stored variables carry 'pause.waitUntil': '2026-09-13T07:38:41.591Z'",
          "discrimination": "Every channel that read zero in (1) reads non-zero here: jobs 0 vs 1, waitUntil absent vs present, correlation inert 'timer:pause' vs the job's own name. The zeros are therefore a reading of the absent-config path, not of an inert harness. The ONE channel that does not discriminate is status: 'paused' — both halves park the run — and the test records that too so it is not misread as a signal."
        },
        "3_boundary_event_no_config_block": {
          "node": "{ id: 'b', type: 'boundary_event', label: 'Boundary' }  — no boundaryConfig key at all",
          "measured_separately": "YES — not extrapolated from wait; it is a different branch and the answer is the opposite one",
          "engine_result": "{ success: false, status: 'failed', error: \"No executor registered for node type 'boundary_event'\", durationMs: 0 }",
          "step_record": "summary.nodes entry for 'b': { nodeType: 'boundary_event', status: 'failure', runs: 1, failures: 1 }; the engine also pushes an error code NO_EXECUTOR onto the step",
          "wake_job_scheduled": "N/A — no executor exists for the type, so no executor code runs at all",
          "waitUntil_written": "N/A — same reason",
          "log_lines_emitted": "TWO, and both loud. At vocabulary seal: warn — \"Flow 'f' references node type(s) with no registered executor or descriptor ... these nodes fail at execution time with NO_EXECUTOR. Install/enable the plugin that contributes them.\" with meta { unknownTypes: [ 'boundary_event' ], knownTypes: [...] }. During the run: info — \"[automation] run flow=f run=... status=failed durationMs=0 selected=0 acted=0 skipped=0 failed=1\".",
          "why": "installBuiltinNodes seeds twelve node packs and none of them is boundary_event; the package README files the type as the BPMN interop representation rather than the native authoring model. The dispatch fails before any config block could be read.",
          "verdict": "There is NO silent suspension on this branch. The absent config block is not what fails it, and nothing here weakens or strengthens objectui#9109 — that card's trigger is the wait half."
        },
        "4_boundary_event_control": {
          "populated_config_control": "boundaryConfig: { attachedToNodeId: 'before', eventType: 'timer', timerDuration: 'PT1H', interrupting: true } fails IDENTICALLY — same success: false, same error string, same step failure — so the block is never consulted.",
          "anti_vacuity_control": "The identical flow with a REGISTERED type in that same slot runs to completion: nodes that ran [ 'before', 'b', 'after' ], success: true. So the failure above is the node type, not the fixture, and the harness can discriminate on this branch too."
        }
      },
      "durable_form": "packages/services/service-automation/src/builtin/absent-config-node-characterization.test.ts — 7 tests, named/titled/headed as a CHARACTERIZATION, stating in as many words that each assertion is a photograph and not an endorsement, that a red means the recorded behaviour moved (possibly on purpose), and that whether a duration-less timer wait should warn or refuse is a successor card's product question, not this file's blessing.",
      "summary": "Converted the card's source reading into an observed run, changing no executor behaviour. The wait half reproduces triage exactly: with the whole config block absent the executor defaults eventType to 'timer', computes no deadline, arms no wake-up job despite a job service being present, persists no waitUntil, emits not one log line at any level, and returns success: true + suspend: true — a silent permanent suspension, exit only via an external resume(runId). A populated-config control discriminates on every one of those channels. The boundary_event half, which triage never measured, is the opposite: no executor is registered for the type at all, so the run fails loudly with NO_EXECUTOR, and a fully populated boundaryConfig fails identically. Delivered as a characterization test plus this reading; the successor question about warning or refusing is left to the PM as instructed.",
      "tests": "pnpm --filter @objectstack/service-automation test :: exit 0 — 'Test Files 133 passed (133) / Tests 1571 passed (1571)'. pnpm --filter @objectstack/service-automation typecheck :: exit 0. pnpm --filter @objectstack/service-automation build :: exit 0 (dependency closure built first: pnpm --filter '@objectstack/service-automation^...' build, under scripts/pm/os-verify-lock.sh, VERDICT command-exit 0). Gate families derived with scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack and reconciled with --ran: '53 derived, 51 run, 2 NOT-MEASURED, 0 UNRUN' — the two NOT MEASURED are pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt, both exit 3 PREREQUISITE NOT MET (each reads built output for the WHOLE workspace, which lint.yml builds before invoking them; this diff emits nothing into any dist/ for them to read). pnpm lint (repo-wide eslint . --no-inline-config) :: exit 0 — whole population, no narrowing claimed, so the three-part narrowing evidence is not owed. pnpm check:nul-bytes green plus a direct grep -naP control-character scan of the added file: no matches. Every exit code captured before any pipe. All readings taken at d5e7d32c3, the final commit. No ablation was performed and none is owed: the diff adds no guard and changes no behaviour — the discriminating controls in (2) and (4) are what stand in for it, and each is a real run, not a source claim.",
      "clause_2_redetermination": "no — re-derived from the DELIVERED diff, not from the prediction. The diff is exactly one ADDED file, changed_files: 1, additions: 326: packages/services/service-automation/src/builtin/absent-config-node-characterization.test.ts. Mechanical floor, both limbs fail: (a) no new exported symbol reachable from a published entry — the package publishes files: [\"dist\", \"README.md\", \"CHANGELOG.md\"] and exports only '.', which resolves into dist/; a probe symbol unique to the added file appears in 0 of those paths after a real build, while the positive control rearmSuspendedWaitTimers appears in 2 files under dist/. The file also declares no exports at all, but reachability is the test, not the presence of the word export. (b) no new key on an already-published payload — no runtime payload is touched. Nothing already published moves, so the grade is not minor and not patch either: the PR carries skip-changeset. Label applied and verified by read-back — union write ['size/m','tests','skip-changeset'] (no additive label channel is reachable from this container: no gh CLI, and the MCP issue-labels read does not resolve PR numbers), read back as exactly ['size/m','tests','skip-changeset'] with nothing stripped.",
      "mcp_calls": "11 — issue_read get + get_comments on 17843; create_pull_request; pull_request_read get; issue_read get on 17937; issue_write update (labels); list_pull_requests (label read-back); add_issue_comment (this report); plus 3 failed issue_read get_labels attempts on the PR number, which this MCP server cannot resolve.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: the silent permanent suspension is CONFIRMED by observation, and deliberately not repaired — the card says it is not a request to change anything and the dispatching seat reserved the successor question (should a duration-less timer wait warn or refuse?) for itself. Successor: the PM, explicitly, in this dispatch.",
        "noted, not filed: the wait executor's return value carries output as a present key holding undefined rather than omitting it, so any JSON rendering of that value silently reads as 'no output key'. An observation about how the value reads, not a defect. Successor: the same successor card, which will touch this exact return shape.",
        "noted, not filed: a PAUSED run emits no run-level log line at all, where a completed or failed run gets an '[automation] run flow=... status=...' line. Consistent with 'paused is not terminal'; recorded because it is the other half of why the absent-config wait is silent end to end. Successor: the same successor card, whose subject is exactly this silence.",
        "noted, not filed: boundary_event parses clean and then fails the run with NO_EXECUTOR. NOT a metadata-authoring trap: the engine treats 'a node type nothing registered' as a declared, handled state with a startup warning naming the type, and the package README declares the type interop-only. Loud by design. Successor: none — no PR or person is heading for this file on this account, which is why it is a note and not a card."
      ]
    }

    Generated by Claude Code

  3. claude commented on Sep 13, 2026

    @claude
    Contributor

    ✅ LANDED — 225197cdb, verified on origin/main with controls

    test(service-automation): characterize a config-less \wait` / `boundary_event` node at run time (#17937)`

    Probe git log origin/main --oneline | grep -c '(#17937)' = 1; positive control (#17924) = 1; negative (#99999) = 0; --is-shallow-repository = false, 13 801 commits. Deliverable present with substance, ⛔ not phrase-grepped: packages/services/service-automation/src/builtin/absent-config-node-characterization.test.ts, 326 lines, 7 tests across three describes — the premise check, the wait characterization with its populated-config control, and boundary_event in its own block with two controls of its own.

    What the card asked, and what it got

    ⭐ The wait half confirms the triage seat's source reading (5651444086) by observed run: the whole config block absent ⇒ eventType defaults to 'timer', no deadline is computed, no wake job is armed even with a job service present, no waitUntil is persisted, not one log line is emitted at any level, and the executor returns success: true, suspend: true. The run parks until an external resume(runId). ⇒ objectui#9109's p1 escalation stands; no rollback owed.

    ⭐ The boundary_event half — which triage explicitly did NOT measure — is the OPPOSITE shape. No executor is registered for the type at all, so it fails loudly and twice: a warn at vocabulary seal naming unknownTypes: ['boundary_event'], then success: false, status: 'failed' with No executor registered for node type 'boundary_event'. A fully populated boundaryConfig fails identically ⇒ the block is never consulted. ⛔ There is no silent suspension on that branch to find.

    ⇒ this is why the card refused to extrapolate and why the dispatch forbade it. Generalising from wait would have reported a silent hang on a branch that is loud by design.

    ⭐ The run also corrected the reading it was sent to confirm: output is a present key carrying undefined, not an absent key — a distinction a JSON dump erases and toEqual cannot see, which is why the test pins it with toStrictEqual. The behaviour is unchanged; only its rendering was imprecise. Exactly the kind of finding that does not survive a source-only close.

    Successor filed, as this seat promised the dispatch

    #17939 — should a duration-less timer wait warn, refuse, or stay silent? That is a product question with a real case on each side (wait-node.ts's own comment declares the fallback deliberate; but with no waitUntil, no job and no log line an operator cannot tell "deliberately parked" from "stuck"). ⛔ It was deliberately not repaired here — the card said it was not a request to change anything, and this seat reserved the question rather than letting the delivery decide it.

    ⚠️ The characterization test is a photograph, not an endorsement. It is named, titled and headed so a later reader cannot mistake a red there for a regression verdict: a red means the recorded behaviour moved, possibly on purpose.

    In-seat contract review: PASS at d5e7d32c3 (5651866600). Carrier gate exit 0. Closing completed.


    Generated by Claude Code

  4. added a commit that references this issue on Sep 17, 2026
    225197c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions