[GH-ISSUE #6619] [Bug] emitter replacement keeps a false resolved event identity #4004

Closed
opened 2026-10-31 00:45:58 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 23, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6619

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

analyze-javascript-application assigns the same resolved event identity to a listener registration and a dispatch through a binding whose emitter instance has been replaced between those operations. The fixture registers on first, replaces bus with second, and dispatches on second. The graph nevertheless uses one event node as both the registration source and dispatch target, marks both relations resolved, and records no event-family unknown.

The retained Evidence also contains a data-flow unknown stating that bus has multiple assignments. A backward-provenance query returns status: "ambiguous" but still includes both event relations as resolved through the same node. The defect is the event identity join; neither the trace nor a resolved static edge proves runtime execution.

Reproduction

Use a checkout at the source revision below with its locked dependencies and compiled runtime prepared. Preparation for a fresh checkout is npm ci && npm run build:cached; these preparation commands were not rerun for this verification. Run the following from the checkout root with Node.js 24.22.1 on PATH. The script creates every input inline and invokes the actual public launcher with isolated configuration directories. REA reads these fixtures as source text.

python3 - <<'PY'
import json, os, pathlib, subprocess, tempfile

checkout = pathlib.Path.cwd()
base = ('import { EventEmitter } from "node:events";\n'
        'const first = new EventEmitter();\n'
        'const second = new EventEmitter();\n')
cases = {
    'replacement': base + 'let bus = first;\nbus.on("ready", () => 1);\nbus = second;\nbus.emit("ready");\n',
    'stable': base + 'let bus = first;\nbus.on("ready", () => 1);\nbus.emit("ready");\n',
    'distinct': base + 'first.on("ready", () => 1);\nsecond.emit("ready");\n',
    'later-write': base + 'let bus = first;\nbus.on("ready", () => 1);\nbus.emit("ready");\nbus = second;\n',
}
with tempfile.TemporaryDirectory() as directory:
    work = pathlib.Path(directory)
    home = work / 'home'
    home.mkdir()
    environment = {
        'PATH': os.environ['PATH'], 'HOME': str(home),
        'XDG_CONFIG_HOME': str(home / 'config'),
        'XDG_CACHE_HOME': str(home / 'cache'),
        'XDG_DATA_HOME': str(home / 'data'), 'TMPDIR': str(work),
    }
    def cli(command, argument):
        result = subprocess.run(
            ['node', str(checkout / 'scripts/rea.mjs'), command,
             argument, '--format', 'json'], env=environment,
            capture_output=True, text=True, check=True)
        return json.loads(result.stdout)
    for name, source in cases.items():
        target = work / name
        target.mkdir()
        (target / 'main.mjs').write_text(source)
        evidence = cli('analyze-javascript-application', str(target))
        graph = evidence['normalized_result']['semantic_graph']
        registration = next(r for r in graph['relations']
                            if r['relation'] == 'registers-listener')
        dispatch = next(r for r in graph['relations']
                        if r['relation'] == 'dispatches-candidate')
        print(name, json.dumps({
            'events': sum(n['kind'] == 'event' for n in graph['nodes']),
            'same_event': registration['source_node_id'] == dispatch['target_node_id'],
            'registration': registration['resolution'],
            'dispatch': dispatch['resolution'],
            'event_unknowns': sum(u['family'] == 'event' for u in graph['unknowns']),
        }, sort_keys=True))
        if name == 'replacement':
            request = {'application': evidence, 'query': {
                'seed': {'kind': 'event', 'name': 'ready'},
                'direction': 'backward-provenance'}}
            trace = cli('trace-javascript-semantics', json.dumps(request))['normalized_result']
            event_relations = sorted(r['relation'] + ':' + r['resolution']
                for r in trace['relations'] if r['relation'] in
                ['registers-listener', 'dispatches-candidate'])
            print('trace', trace['status'], json.dumps(event_relations))
PY

The commands succeeded. Their summarized output was:

replacement {"dispatch": "resolved", "event_unknowns": 0, "events": 1, "registration": "resolved", "same_event": true}
trace ambiguous ["dispatches-candidate:resolved", "registers-listener:resolved"]
stable {"dispatch": "resolved", "event_unknowns": 0, "events": 1, "registration": "resolved", "same_event": true}
distinct {"dispatch": "resolved", "event_unknowns": 0, "events": 2, "registration": "resolved", "same_event": false}
later-write {"dispatch": "resolved", "event_unknowns": 0, "events": 1, "registration": "resolved", "same_event": true}

The replacement, stable, and distinct cases were also reproduced through the official published package's public launcher. A conditional intervening replacement was checked at the source baseline and likewise produced one event with both relations resolved and no event unknown.

Expected behavior

Registration and dispatch on different emitter instances must not share one proven exact-emitter event identity. Where the analyzer cannot establish the value at a receiver read, it should retain candidate identity and explicit event uncertainty. The unchanged let binding should retain one correctly shared resolved event; two distinct binding receivers should remain separate. A write after the dispatch must not retroactively make the earlier registration and dispatch refer to different emitters.

Impact and scope

This is a bounded semantic Evidence correctness defect: event provenance can connect a dispatch to a listener on a different emitter. The original source remains unchanged. The observed failure affects the inline replacement and conditional-replacement patterns; frequency in other applications and consequences for downstream consumers were not measured. No public setting that restores the graph for the original artifact was verified. Reading the source exposes the discrepancy but does not repair the retained event identity.

REA version

Source baseline: bcd1f95e68. Also reproduced with the official npm rea-agents 6.7.0 archive after verifying its registry integrity digest; the packaged runtime used the prepared dependency tree rather than a fresh package installation.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; arm64. Locked dependencies and compiled source runtime were already prepared. No native provider, browser, device, or remote target was used.

Target and artifact context

Local JavaScript module directories containing the inline main.mjs fixtures. The result is application Evidence with a semantic graph; the trace uses that unchanged returned Evidence. A separate benign Node control confirmed that dispatch after replacement invokes zero listeners, while the unchanged binding invokes its listener once.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: docs/javascript-application-workflows.md:179-184 includes EventEmitter registration/removal/dispatch extraction; :237-247 requires explicit unknowns and defines resolved static identity; :282-289 specifies one event per exact emitter/name pair. AGENTS.md:19-21 requires preservation of identity and uncertainty across boundaries.
  • Failure source: The public CLI output above compares actual relation endpoint IDs. For replacement, registration on line 5 and dispatch on line 7 share one event with emitter_key: "binding:scope:program:0:189:binding:bus"; both relations are resolved and the event-family unknown count is zero. An independent source-runtime IR probe locates those operations at lines 5 and 7 while the binding separately records a line-6 assignment and ambiguous provenance. src/domain/javascript/javascriptSemanticAsyncEffects.ts:130-170 admits the calls and :191-198 marks lexical binding identity complete; src/application/javascript/JavaScriptSemanticGraphAsyncProjection.ts:22-52 shares the event by emitter key/name and projects complete operations as resolved.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The event node has no operation source range. Its registration relation targets the line-5 listener, and its dispatch relation originates at the line-4 binding declaration. The operation positions above were independently checked in semantic IR rather than inferred from the dispatch relation's endpoint range. General data-flow and call-flow unknowns remain in the graph; the observed missing uncertainty is specific to the event identity.

Typed outcome or recovery expected

Return separate event identities for the two established receiver values, or retain explicit event ambiguity with candidate relationships where that identity cannot be established. Preserve correct sharing for the unchanged binding and correct separation for distinct bindings. Rerunning the query should reflect the corrected identity without requiring runtime execution of the target.

Analysis or suspected cause

Source-derived explanation: the event collector equates a resolved lexical binding with a complete emitter identity. The registration and dispatch therefore receive the same key despite the intervening assignment, and graph projection merges them. This explains the observed output; no proposed implementation or repaired result was tested.

Additional context

Open #3360 concerns a different independently testable signal: unrelated this receivers in two classes share one event. Its exact fixture also reproduces at this source baseline, using key this, while this replacement fixture contains no this and fails across two reads of one mutable binding. Separating callable receiver contexts does not establish value identity at those reads, and establishing the intervening write does not establish receiver separation across the two classes. The reports therefore have distinct restoration checks even though event extraction is shared.

Closed #3358 concerns overwritten timer cancellation handles; closed #3362 concerns child-process ownership after reassignment; closed #3313 concerns resource release after reassignment. Those relation-specific closure signals do not establish EventEmitter identity. Closed #2574 concerns dispatch data appearing as a listener; this fixture emits no data argument and still exhibits the identity failure. Open and closed issue searches found no matching emitter-binding replacement report.

Restoration check

For the replacement fixture, the line-5 registration and line-7 dispatch must no longer meet through one resolved exact-emitter event without event uncertainty. Preserve the unchanged let binding's single resolved event, the distinct bindings' two events, and the later-write control's sharing before the write. The conditional replacement must not assert an unqualified unique receiver identity. These are proposed post-fix checks, not claims of a tested repair; separating every operation or marking every event unknown would fail the controls.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A valid explicit emitter replacement produces a false resolved event identity in application Evidence and retains that identity in the supported provenance query; the demonstrated effect is bounded analysis correctness, with population frequency unmeasured.
  • Workaround status: unverified
  • Workaround basis: No available caller option was demonstrated to restore event identity while preserving the original artifact and requested graph. Manual inspection can identify the discrepancy but leaves the stored result incorrect.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6619 ## Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ## Area Evidence, artifacts, or reconstruction ## Observed behavior `analyze-javascript-application` assigns the same resolved event identity to a listener registration and a dispatch through a binding whose emitter instance has been replaced between those operations. The fixture registers on `first`, replaces `bus` with `second`, and dispatches on `second`. The graph nevertheless uses one event node as both the registration source and dispatch target, marks both relations resolved, and records no event-family unknown. The retained Evidence also contains a data-flow unknown stating that `bus` has multiple assignments. A backward-provenance query returns `status: "ambiguous"` but still includes both event relations as resolved through the same node. The defect is the event identity join; neither the trace nor a resolved static edge proves runtime execution. ## Reproduction Use a checkout at the source revision below with its locked dependencies and compiled runtime prepared. Preparation for a fresh checkout is `npm ci && npm run build:cached`; these preparation commands were not rerun for this verification. Run the following from the checkout root with Node.js 24.22.1 on PATH. The script creates every input inline and invokes the actual public launcher with isolated configuration directories. REA reads these fixtures as source text. ```sh python3 - <<'PY' import json, os, pathlib, subprocess, tempfile checkout = pathlib.Path.cwd() base = ('import { EventEmitter } from "node:events";\n' 'const first = new EventEmitter();\n' 'const second = new EventEmitter();\n') cases = { 'replacement': base + 'let bus = first;\nbus.on("ready", () => 1);\nbus = second;\nbus.emit("ready");\n', 'stable': base + 'let bus = first;\nbus.on("ready", () => 1);\nbus.emit("ready");\n', 'distinct': base + 'first.on("ready", () => 1);\nsecond.emit("ready");\n', 'later-write': base + 'let bus = first;\nbus.on("ready", () => 1);\nbus.emit("ready");\nbus = second;\n', } with tempfile.TemporaryDirectory() as directory: work = pathlib.Path(directory) home = work / 'home' home.mkdir() environment = { 'PATH': os.environ['PATH'], 'HOME': str(home), 'XDG_CONFIG_HOME': str(home / 'config'), 'XDG_CACHE_HOME': str(home / 'cache'), 'XDG_DATA_HOME': str(home / 'data'), 'TMPDIR': str(work), } def cli(command, argument): result = subprocess.run( ['node', str(checkout / 'scripts/rea.mjs'), command, argument, '--format', 'json'], env=environment, capture_output=True, text=True, check=True) return json.loads(result.stdout) for name, source in cases.items(): target = work / name target.mkdir() (target / 'main.mjs').write_text(source) evidence = cli('analyze-javascript-application', str(target)) graph = evidence['normalized_result']['semantic_graph'] registration = next(r for r in graph['relations'] if r['relation'] == 'registers-listener') dispatch = next(r for r in graph['relations'] if r['relation'] == 'dispatches-candidate') print(name, json.dumps({ 'events': sum(n['kind'] == 'event' for n in graph['nodes']), 'same_event': registration['source_node_id'] == dispatch['target_node_id'], 'registration': registration['resolution'], 'dispatch': dispatch['resolution'], 'event_unknowns': sum(u['family'] == 'event' for u in graph['unknowns']), }, sort_keys=True)) if name == 'replacement': request = {'application': evidence, 'query': { 'seed': {'kind': 'event', 'name': 'ready'}, 'direction': 'backward-provenance'}} trace = cli('trace-javascript-semantics', json.dumps(request))['normalized_result'] event_relations = sorted(r['relation'] + ':' + r['resolution'] for r in trace['relations'] if r['relation'] in ['registers-listener', 'dispatches-candidate']) print('trace', trace['status'], json.dumps(event_relations)) PY ``` The commands succeeded. Their summarized output was: ```text replacement {"dispatch": "resolved", "event_unknowns": 0, "events": 1, "registration": "resolved", "same_event": true} trace ambiguous ["dispatches-candidate:resolved", "registers-listener:resolved"] stable {"dispatch": "resolved", "event_unknowns": 0, "events": 1, "registration": "resolved", "same_event": true} distinct {"dispatch": "resolved", "event_unknowns": 0, "events": 2, "registration": "resolved", "same_event": false} later-write {"dispatch": "resolved", "event_unknowns": 0, "events": 1, "registration": "resolved", "same_event": true} ``` The replacement, stable, and distinct cases were also reproduced through the official published package's public launcher. A conditional intervening replacement was checked at the source baseline and likewise produced one event with both relations resolved and no event unknown. ## Expected behavior Registration and dispatch on different emitter instances must not share one proven exact-emitter event identity. Where the analyzer cannot establish the value at a receiver read, it should retain candidate identity and explicit event uncertainty. The unchanged `let` binding should retain one correctly shared resolved event; two distinct binding receivers should remain separate. A write after the dispatch must not retroactively make the earlier registration and dispatch refer to different emitters. ## Impact and scope This is a bounded semantic Evidence correctness defect: event provenance can connect a dispatch to a listener on a different emitter. The original source remains unchanged. The observed failure affects the inline replacement and conditional-replacement patterns; frequency in other applications and consequences for downstream consumers were not measured. No public setting that restores the graph for the original artifact was verified. Reading the source exposes the discrepancy but does not repair the retained event identity. ## REA version Source baseline: bcd1f95e688a2ac020a9d13f9c85e0ce321cb6b6. Also reproduced with the official npm `rea-agents` 6.7.0 archive after verifying its registry integrity digest; the packaged runtime used the prepared dependency tree rather than a fresh package installation. ## Runtime and platform Node.js v24.22.1; macOS 27.0.1; arm64. Locked dependencies and compiled source runtime were already prepared. No native provider, browser, device, or remote target was used. ## Target and artifact context Local JavaScript module directories containing the inline `main.mjs` fixtures. The result is application Evidence with a semantic graph; the trace uses that unchanged returned Evidence. A separate benign Node control confirmed that dispatch after replacement invokes zero listeners, while the unchanged binding invokes its listener once. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence - Expected source: `docs/javascript-application-workflows.md:179-184` includes EventEmitter registration/removal/dispatch extraction; `:237-247` requires explicit unknowns and defines resolved static identity; `:282-289` specifies one event per exact emitter/name pair. `AGENTS.md:19-21` requires preservation of identity and uncertainty across boundaries. - Failure source: The public CLI output above compares actual relation endpoint IDs. For replacement, registration on line 5 and dispatch on line 7 share one event with `emitter_key: "binding:scope:program:0:189:binding:bus"`; both relations are resolved and the event-family unknown count is zero. An independent source-runtime IR probe locates those operations at lines 5 and 7 while the binding separately records a line-6 assignment and ambiguous provenance. `src/domain/javascript/javascriptSemanticAsyncEffects.ts:130-170` admits the calls and `:191-198` marks lexical binding identity complete; `src/application/javascript/JavaScriptSemanticGraphAsyncProjection.ts:22-52` shares the event by emitter key/name and projects complete operations as resolved. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The event node has no operation source range. Its registration relation targets the line-5 listener, and its dispatch relation originates at the line-4 binding declaration. The operation positions above were independently checked in semantic IR rather than inferred from the dispatch relation's endpoint range. General data-flow and call-flow unknowns remain in the graph; the observed missing uncertainty is specific to the event identity. ## Typed outcome or recovery expected Return separate event identities for the two established receiver values, or retain explicit event ambiguity with candidate relationships where that identity cannot be established. Preserve correct sharing for the unchanged binding and correct separation for distinct bindings. Rerunning the query should reflect the corrected identity without requiring runtime execution of the target. ## Analysis or suspected cause Source-derived explanation: the event collector equates a resolved lexical binding with a complete emitter identity. The registration and dispatch therefore receive the same key despite the intervening assignment, and graph projection merges them. This explains the observed output; no proposed implementation or repaired result was tested. ## Additional context Open [#3360](https://github.com/morluto/rea/issues/3360) concerns a different independently testable signal: unrelated `this` receivers in two classes share one event. Its exact fixture also reproduces at this source baseline, using key `this`, while this replacement fixture contains no `this` and fails across two reads of one mutable binding. Separating callable receiver contexts does not establish value identity at those reads, and establishing the intervening write does not establish receiver separation across the two classes. The reports therefore have distinct restoration checks even though event extraction is shared. Closed [#3358](https://github.com/morluto/rea/issues/3358) concerns overwritten timer cancellation handles; closed [#3362](https://github.com/morluto/rea/issues/3362) concerns child-process ownership after reassignment; closed [#3313](https://github.com/morluto/rea/issues/3313) concerns resource release after reassignment. Those relation-specific closure signals do not establish EventEmitter identity. Closed [#2574](https://github.com/morluto/rea/issues/2574) concerns dispatch data appearing as a listener; this fixture emits no data argument and still exhibits the identity failure. Open and closed issue searches found no matching emitter-binding replacement report. ## Restoration check For the replacement fixture, the line-5 registration and line-7 dispatch must no longer meet through one resolved exact-emitter event without event uncertainty. Preserve the unchanged `let` binding's single resolved event, the distinct bindings' two events, and the later-write control's sharing before the write. The conditional replacement must not assert an unqualified unique receiver identity. These are proposed post-fix checks, not claims of a tested repair; separating every operation or marking every event unknown would fail the controls. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A valid explicit emitter replacement produces a false resolved event identity in application Evidence and retains that identity in the supported provenance query; the demonstrated effect is bounded analysis correctness, with population frequency unmeasured. - Workaround status: unverified - Workaround basis: No available caller option was demonstrated to restore event identity while preserving the original artifact and requested graph. Manual inspection can identify the discrepancy but leaves the stored result incorrect.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4004
No description provided.