aki-pro-max 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/.env.example +34 -0
  2. package/AICOWORKER-NATIVE-TOOLS.md +60 -0
  3. package/CLAUDE-LIVE-TOOLS-EVIDENCE.json +94 -0
  4. package/FULL-TRACE-EVIDENCE.json +109 -0
  5. package/ISSUE-1-REMOTE.json +1 -0
  6. package/ISSUE-2-POSTREVIEW.json +1 -0
  7. package/ISSUE-2-REMOTE.json +1 -0
  8. package/KEY-ROTATION-EVIDENCE.json +9 -0
  9. package/LICENSE +21 -0
  10. package/PMN-9ROUTER-FINAL.md +13 -0
  11. package/RAPID-BASIL-RECONCILIATION.json +10 -0
  12. package/README.md +232 -0
  13. package/RELEASE-ARCHIVE.json +15 -0
  14. package/SECURITY-RECONCILIATION.json +30 -0
  15. package/SECURITY.md +16 -0
  16. package/TEST-ISSUES12-FINAL.txt +0 -0
  17. package/TEST-ISSUES12-HARNESS.txt +0 -0
  18. package/VERIFICATION-REPORT.md +58 -0
  19. package/VERIFIER-ISSUES12-FINAL.txt +0 -0
  20. package/VERIFY-RELEASE-ISSUES12-FINAL.txt +0 -0
  21. package/VERIFY-RELEASE-ISSUES12-HARNESS.txt +0 -0
  22. package/bin/aki-pro-max.js +98 -0
  23. package/docs/ADMIN-GUI-CONTRACT.md +29 -0
  24. package/docs/ARCHITECTURE.md +109 -0
  25. package/docs/CAPABILITY-MATRIX.md +44 -0
  26. package/docs/CORRELATION-DESIGN.md +226 -0
  27. package/docs/FAIL-CLOSED-ISSUE-HARNESS.md +21 -0
  28. package/docs/WEB-SESSION-TRANSPORT-DESIGN.md +423 -0
  29. package/docs/assets/control-plane.jpg +0 -0
  30. package/gitleaks-report-all.json +1 -0
  31. package/gitleaks-report-latest.json +1 -0
  32. package/gitleaks-report.json +1 -0
  33. package/package.json +33 -0
  34. package/scripts/eventual-tool-loop.mjs +55 -0
  35. package/scripts/install-local.ps1 +35 -0
  36. package/scripts/live-eventual-multitool.mjs +18 -0
  37. package/scripts/upgrade-admin-v232.mjs +33 -0
  38. package/scripts/verify-issue-closure.mjs +81 -0
  39. package/scripts/verify-release.mjs +31 -0
  40. package/src/admin-auth.mjs +94 -0
  41. package/src/admin.mjs +133 -0
  42. package/src/canonical.mjs +23 -0
  43. package/src/config.mjs +88 -0
  44. package/src/correlation-store.mjs +120 -0
  45. package/src/errors.mjs +18 -0
  46. package/src/index.mjs +4 -0
  47. package/src/openai-response.mjs +72 -0
  48. package/src/openai.mjs +104 -0
  49. package/src/postman-events.mjs +43 -0
  50. package/src/postman-request.mjs +49 -0
  51. package/src/schema.mjs +35 -0
  52. package/src/server.mjs +73 -0
  53. package/src/session-store.mjs +48 -0
  54. package/src/sse.mjs +13 -0
  55. package/src/transport.mjs +90 -0
  56. package/src/web-session-events.mjs +358 -0
  57. package/src/web-session-request.mjs +280 -0
  58. package/test/9router-executor.integration.test.mjs +207 -0
  59. package/test/admin-auth.test.mjs +47 -0
  60. package/test/admin.test.mjs +68 -0
  61. package/test/config.test.mjs +14 -0
  62. package/test/contract.test.mjs +14 -0
  63. package/test/correlation-store.test.mjs +19 -0
  64. package/test/correlation.integration.test.mjs +48 -0
  65. package/test/eventual-tool-loop.test.mjs +42 -0
  66. package/test/fixtures/text.json +8 -0
  67. package/test/fixtures/tool.json +7 -0
  68. package/test/fixtures/web-session-observed-done.json +12 -0
  69. package/test/fixtures/web-session-tool-fragments.json +14 -0
  70. package/test/full-ingress/alias-loader.mjs +22 -0
  71. package/test/full-ingress/run-full-ingress.mjs +207 -0
  72. package/test/full-ingress/seed-9router.mjs +36 -0
  73. package/test/helpers.mjs +9 -0
  74. package/test/issue-closure-harness.test.mjs +49 -0
  75. package/test/model-thinking.test.mjs +20 -0
  76. package/test/protocol.test.mjs +16 -0
  77. package/test/request.test.mjs +10 -0
  78. package/test/session-store.test.mjs +20 -0
  79. package/test/web-session-builder.test.mjs +124 -0
  80. package/test/web-session-events.test.mjs +241 -0
  81. package/test/web-session-integration.test.mjs +103 -0
  82. package/test/web-session-tools.test.mjs +52 -0
  83. package/version.json +8 -0
@@ -0,0 +1,226 @@
1
+ # Native tool-result correlation design (offline, source-derived)
2
+
3
+ Status: design only; no implementation or live validation.
4
+
5
+ ## 1. Evidence and boundary
6
+
7
+ ### The current HTTP 400 is emitted by this facade
8
+
9
+ The pinned 9router executor forwards the OpenAI request to `<facade>/v1/chat/completions` with its configured bearer key. `test/9router-executor.integration.test.mjs` then observes the second request returning HTTP 400. The rejecting branch is local: `src/postman-request.mjs:5-12` checks `request.toolResults?.length` and throws `invalid(..., 'unsupported_tool_result_continuation')` before `fetchImpl` is called. The integration assertion `captures.length === 1` proves the rejected second request did not reach the Postman-facing fetch boundary. This design therefore changes the facade; it does not attribute the 400 to 9router and does not require a 9router session header.
10
+
11
+ ### Immutable ZIP native request shape
12
+
13
+ Reviewed archive: `C:\Users\ADMIN\aicoworker\openclaw\media\outbound\6f139e01-fd99-4f9b-a8a4-2e2777fad843.zip`, SHA-256 `807E29A51F6EE068D9BEFE0112855DF2B244486502DF721D5AF3BC5CD071BDF6`.
14
+
15
+ `proxypostman-main/src/postman.mjs:113-124` establishes:
16
+
17
+ - Initial request: `input={chatType:'USER_QUERY', query, conversationId, product:'workspace_v12', toolCallGroupId:null, toolResponses:[]}`.
18
+ - Native continuation starts from saved `conversationId`, then uses `chatType:'TOOL_RESPONSE'`, `query:''`, `useCase:null`.
19
+ - Grouped call: `toolCallGroupId=<upstream group id>` and `toolResponses=[{toolCallId,content,toolResponseSummary,toolResponseStatus}, ...]` (the local `groupId` property is removed).
20
+ - Singular ungrouped call: `toolCallId`, `toolResponse`, `toolResponseSummary`.
21
+ - Multiple calls without one common non-empty group ID are rejected by that source transport rather than submitted as a native batch.
22
+
23
+ `proxypostman-main/src/bridge.mjs:163-182,329-372` establishes that continuation depends on saved history, `conversationId`, pending calls, and `correlateToolResults`; non-correlatable/transcript cases reset native conversation state. `src/tool-results.mjs` requires exactly one client result for every pending call, no duplicate result IDs, and no unexpected IDs.
24
+
25
+ ### Existing facade limits retained
26
+
27
+ - Request body: default 1 MiB, configurable 1 KiB–8 MiB (`MAX_BODY_BYTES`).
28
+ - Tools: default 64, configurable 0–256 (`MAX_TOOLS`).
29
+ - One accumulated upstream tool argument value: default 256 KiB, configurable 256 B–4 MiB (`MAX_TOOL_ARGUMENT_BYTES`).
30
+ - Upstream output: default 4 MiB, configurable 1 KiB–16 MiB.
31
+ - Tool names: `[A-Za-z0-9_-]{1,64}`; registered names are namespaced/hashed to at most 64 characters.
32
+ - Supported schema vocabulary is deliberately bounded to `type`, `properties`, `required`, boolean `additionalProperties`, `items`, `enum`, `const`, `anyOf`, `oneOf`, `allOf`, descriptive fields, numeric/string/array bounds, `$schema`, and `$id`. Formats, regex patterns, refs, tuple items, numeric `additionalProperties`, and other keywords remain rejected.
33
+ - Tool result content remains text-only at the OpenAI facade boundary and is still constrained by the overall request body limit. A separate `MAX_TOOL_RESULT_BYTES` should bound each result and the aggregate native continuation body below `MAX_BODY_BYTES`.
34
+
35
+ ## 2. Scope and lifecycle
36
+
37
+ This is not general conversation persistence. Every request without a valid pending facade-issued tool call is stateless and becomes a fresh upstream `USER_QUERY`. State begins only after an upstream response has supplied a conversation ID and one or more complete, schema-valid tool calls. State ends after their native result continuation is definitively consumed, explicitly expires before dispatch, or reaches a terminal uncertain/cancelled record retained only for replay rejection/audit TTL.
38
+
39
+ A normal no-tool response stores nothing. A caller cannot opt into state using a header or caller-created ID. No 9router session behavior is assumed.
40
+
41
+ ## 3. Authenticated principal and isolation
42
+
43
+ Define `principalId = HMAC(serverPrincipalKey, credentialSlotId)` where `credentialSlotId` is the server-side identity of the matched configured ingress API key, not the plaintext key and not `body.user`.
44
+
45
+ The present single `PROVIDER_API_KEY` configuration creates exactly one principal. It isolates holders of that key from holders of other configured keys only after multi-key configuration exists; it does **not** distinguish fictional end users who share the same key. `body.user`, forwarded IP, and arbitrary headers are not authentication boundaries.
46
+
47
+ Deployment requirement for mutually untrusted clients: configure a distinct ingress key/credential slot per client. A later implementation may support an explicit key map, with each slot optionally restricted to a fixed workspace/model allowlist. Until then, shared-key clients must be treated as one trust domain.
48
+
49
+ Lookup is always `(principalId, publicToolCallId)`. An ID that exists for a different principal returns the same generic `unknown_tool_call` response as an absent/forged ID; it must not reveal cross-principal existence.
50
+
51
+ ## 4. Public IDs and internal records
52
+
53
+ ### Public tool call IDs
54
+
55
+ Never expose the upstream `toolCallId` or `toolCallGroupId` as the OpenAI `assistant.tool_calls[].id`.
56
+
57
+ Generate each public ID with at least 128 bits from a CSPRNG, for example `call_pmn_` plus base64url of 24 random bytes (192 bits). IDs are facade-issued, unstructured, non-sequential, and carry no tenant, workspace, group, or upstream identifier. Enforce uniqueness in the active + terminal-retention indexes; regenerate on collision.
58
+
59
+ The response adapter replaces each upstream call ID with its public ID. The private mapping remains server-side.
60
+
61
+ ### Group record
62
+
63
+ One record per upstream group (or one synthetic group per singular ungrouped call):
64
+
65
+ ```text
66
+ GroupRecord {
67
+ principalId
68
+ workspaceId
69
+ model
70
+ conversationId
71
+ upstreamGroupId: string|null
72
+ groupKey: random internal identifier
73
+ state: PENDING|INFLIGHT|CONSUMED|EXPIRED|CANCELLED|UNCERTAIN
74
+ issuedAt, pendingExpiresAt, terminalExpiresAt
75
+ transcriptHash
76
+ transcriptPrefixLength
77
+ toolsHash
78
+ calls: ordered CallRecord[]
79
+ acceptedResultsHash: string|null
80
+ dispatchAttemptId: random|null
81
+ terminalResponseDigest: string|null
82
+ }
83
+ CallRecord {
84
+ publicToolCallId
85
+ upstreamToolCallId
86
+ upstreamGroupId: string|null
87
+ index
88
+ originalName
89
+ argumentsHash
90
+ argumentsJson
91
+ schemaHash
92
+ }
93
+ ```
94
+
95
+ `workspaceId` comes only from server configuration selected for the authenticated credential. `conversationId`, upstream call/group IDs, tool name, arguments, and order come only from validated upstream events. `model` is the exact model used for the initial request.
96
+
97
+ ### Canonical hashes
98
+
99
+ Use a versioned deterministic serializer (sorted object keys, preserved array order, UTF-8, explicit nulls; no Unicode normalization) and SHA-256 or stronger.
100
+
101
+ - `transcriptHash`: hash of the exact normalized OpenAI message prefix returned/accepted for the tool-call turn: prior normalized messages plus the facade-produced assistant message containing public IDs, names, and exact argument strings. Exclude the new `role=tool` messages. Store `transcriptPrefixLength` and require exact equality of that prefix on continuation.
102
+ - `toolsHash`: hash of normalized caller tool definitions, original names, schemas, descriptions, and effective `tool_choice` in order.
103
+ - Each `argumentsHash`: exact upstream argument string plus original name and schema hash.
104
+ - `acceptedResultsHash`: ordered canonical tuple of every `(publicToolCallId, content)` accepted for the group.
105
+
106
+ The hash is an integrity attribute, not a lookup key. The primary lookup remains `(principalId, publicToolCallId)`; after lookup, every binding is checked.
107
+
108
+ ## 5. Continuation validation
109
+
110
+ A request containing any `role=tool` message takes the continuation path and must never fall back to `USER_QUERY`.
111
+
112
+ 1. Authenticate and derive `principalId`.
113
+ 2. Normalize the OpenAI request under existing body/tool/schema limits.
114
+ 3. Resolve every distinct `tool_call_id` by `(principalId, publicToolCallId)`.
115
+ 4. Require all IDs to resolve to one `GroupRecord`; reject mixed groups.
116
+ 5. Require exactly one result for every call in the record, no duplicates and no extras. Grouped native continuation is all-results-only. A singular ungrouped record requires exactly one result.
117
+ 6. Require the messages before the result block to equal the stored normalized transcript prefix byte-for-byte after the versioned canonical serialization. Any removed, inserted, reordered, or changed earlier message, tool call, name, arguments, public ID, or assistant content fails `continuation_history_mismatch`.
118
+ 7. Require results to form the final contiguous message suffix. No new user/assistant/system/developer message may accompany a pending native result submission. This deliberately avoids ambiguity about whether extra context belongs in `TOOL_RESPONSE`.
119
+ 8. Require `request.model === record.model`; otherwise return `continuation_model_mismatch`. Do not route silently to the old or new model.
120
+ 9. Require server-selected `workspaceId === record.workspaceId` and current tool definitions/choice hash to equal `toolsHash`; otherwise reject explicitly. Omitting `tools` on continuation is permitted only if normalization substitutes the stored definitions before hashing; the simpler first implementation should require exact resubmission.
121
+ 10. Enforce state and expiration transitions atomically, then build native `TOOL_RESPONSE` exclusively from private upstream IDs.
122
+
123
+ Unknown, hallucinated, expired, replayed, cross-principal, cross-workspace, mixed-group, partial-group, duplicate, model-changed, tool-changed, and history-mismatched submissions are explicit failures. None becomes a new query.
124
+
125
+ ## 6. Native output construction
126
+
127
+ For a common non-empty upstream group ID:
128
+
129
+ ```json
130
+ {
131
+ "chatType": "TOOL_RESPONSE",
132
+ "query": "",
133
+ "conversationId": "<stored upstream conversationId>",
134
+ "product": "workspace_v12",
135
+ "useCase": null,
136
+ "toolCallGroupId": "<stored upstream group id>",
137
+ "toolResponses": [
138
+ {
139
+ "toolCallId": "<stored upstream call id>",
140
+ "content": "<OpenAI tool message text>",
141
+ "toolResponseSummary": "Result for <original tool name>",
142
+ "toolResponseStatus": "SUCCESS"
143
+ }
144
+ ]
145
+ }
146
+ ```
147
+
148
+ For one ungrouped upstream call, omit `toolCallGroupId`/`toolResponses` and set stored `toolCallId`, `toolResponse`, and `toolResponseSummary`. Do not infer or invent group IDs. Result-status mapping is not expressed by standard OpenAI `role=tool`; the bounded first implementation therefore uses `SUCCESS` for accepted text results. Adding an error-status extension requires a separately documented request contract and tests.
149
+
150
+ ## 7. Atomic state machine
151
+
152
+ All transitions below occur under a per-group mutex/CAS. Multi-ID lookup first resolves records, then locks the one group and revalidates state and expiration.
153
+
154
+ | Current state | Event | Next state | Required result |
155
+ |---|---|---|---|
156
+ | PENDING | Valid complete result set before expiry | INFLIGHT | Store `acceptedResultsHash` and random `dispatchAttemptId` atomically, then perform exactly one upstream dispatch. |
157
+ | PENDING | Partial/duplicate/extra/mixed result set | PENDING | HTTP 400, no mutation. |
158
+ | PENDING | History/model/tools/workspace mismatch | PENDING | HTTP 409 `continuation_*_mismatch`, no mutation. |
159
+ | PENDING | Unknown/cross-principal public ID | PENDING/none | HTTP 404 `unknown_tool_call`, no information leak. |
160
+ | PENDING | Deadline observed before dispatch claim | EXPIRED | HTTP 410 `tool_call_expired`; retain tombstone. |
161
+ | PENDING | Capacity pressure | PENDING | Never evict; reject admission of newer tool groups instead. |
162
+ | PENDING | Explicit server shutdown/cancel before dispatch | CANCELLED | HTTP 409/503 on later submission; retain tombstone. |
163
+ | INFLIGHT | Same result set retry | INFLIGHT | HTTP 409 `continuation_inflight` plus stable request ID; never dispatch again. |
164
+ | INFLIGHT | Different result set retry | INFLIGHT | HTTP 409 `continuation_conflict`; never dispatch. |
165
+ | INFLIGHT | Upstream `[DONE]` and response committed | CONSUMED | Cache terminal response digest/body within strict size bound for idempotent replay policy. |
166
+ | INFLIGHT | Definite pre-send failure (no bytes/request accepted) | PENDING | Clear dispatch claim only when transport proves no upstream request could have been accepted. |
167
+ | INFLIGHT | Timeout, disconnect, abort, or ambiguous post-send failure | UNCERTAIN | Never automatically retry TOOL_RESPONSE; return `continuation_outcome_uncertain`. |
168
+ | CONSUMED | Same result set retry | CONSUMED | Return cached response if safely retained, else HTTP 409 `tool_call_already_consumed`; never dispatch. |
169
+ | CONSUMED | Different result set retry | CONSUMED | HTTP 409 `continuation_conflict`. |
170
+ | EXPIRED | Any submission | EXPIRED | HTTP 410 `tool_call_expired`. |
171
+ | CANCELLED | Any submission | CANCELLED | HTTP 409 `tool_call_cancelled`. |
172
+ | UNCERTAIN | Any submission | UNCERTAIN | HTTP 409/502 `continuation_outcome_uncertain`; operator reconciliation required, no duplicate side effect. |
173
+ | terminal | Tombstone TTL elapsed | removed | Later lookup returns generic 404 `unknown_tool_call`; no live work is affected. |
174
+
175
+ `CONSUMED` means the native result submission is consumed, not that subsequent tool-call groups cannot be issued. If the upstream continuation response emits another complete tool group and conversation ID, create a new independent PENDING group with new public IDs and bindings after consuming the prior group.
176
+
177
+ ## 8. TTL and bounded capacity
178
+
179
+ Suggested configurable starting values (implementation approval required): pending TTL 15 minutes, inflight hard deadline equal to request timeout plus 30 seconds, terminal/tombstone TTL 15 minutes, maximum 1,000 live groups, 4,096 live calls, and a bounded cached terminal response budget.
180
+
181
+ Rules:
182
+
183
+ - Expiration is checked on lookup, admission, and periodic sweep.
184
+ - A sweep may transition an overdue PENDING record to EXPIRED and later remove terminal tombstones. It may not silently delete a non-expired PENDING record or any INFLIGHT record.
185
+ - INFLIGHT timeout becomes UNCERTAIN, not EXPIRED/PENDING.
186
+ - When live-group/call/memory capacity is full, reject the **new initial request's tool-call response before exposing unusable public IDs** if possible. For streaming, because headers/chunks may already be emitted, reserve capacity when the first upstream conversation/tool call appears; on reservation failure terminate with explicit `correlation_capacity_exceeded` and do not pretend the calls are continuable.
187
+ - Do not LRU-evict pending groups. Capacity recovery comes from explicit expiry/terminal cleanup or operator action.
188
+ - Record and result byte accounting must be calculated before admission/transition, using configured hard maxima.
189
+
190
+ ## 9. Cancellation and side-effect safety
191
+
192
+ Client disconnect during the initial tool-call response does not erase an admitted PENDING group; the client may have received IDs before disconnect. It expires normally.
193
+
194
+ Client disconnect/cancel before the continuation wins the atomic PENDING→INFLIGHT claim leaves PENDING or transitions to CANCELLED only through an explicit server decision. After the claim and possible upstream send, cancellation is not proof the upstream ignored the TOOL_RESPONSE. Any ambiguous interruption becomes UNCERTAIN. The facade must not reset to PENDING and must not automatically submit the same native result again, because continuation can cause the model to request new side-effecting tools and duplicate dispatch would produce divergent actions.
195
+
196
+ Within one process, the group CAS plus `dispatchAttemptId` guarantees at most one dispatch. This design is explicitly single-process/in-memory; multiple workers or restart durability require a shared transactional store and are not claimed.
197
+
198
+ ## 10. Error contract
199
+
200
+ Suggested stable codes:
201
+
202
+ - 404 `unknown_tool_call` — absent, forged, or cross-principal ID.
203
+ - 400 `tool_result_group_mismatch` — mixed groups, duplicate/extra IDs, malformed suffix.
204
+ - 400 `tool_result_incomplete` — not every group call has exactly one result.
205
+ - 409 `continuation_history_mismatch`.
206
+ - 409 `continuation_model_mismatch`.
207
+ - 409 `continuation_tools_mismatch`.
208
+ - 409 `continuation_workspace_mismatch` (generic message; normally impossible under fixed per-key config).
209
+ - 409 `continuation_inflight`.
210
+ - 409 `tool_call_already_consumed` / `continuation_conflict`.
211
+ - 410 `tool_call_expired`.
212
+ - 409 `tool_call_cancelled`.
213
+ - 502 `continuation_outcome_uncertain`.
214
+ - 429/503 `correlation_capacity_exceeded` for new group admission.
215
+ - Retain current 400 `unsupported_tool_result_continuation` only when correlation support is disabled by configuration; never use it for a resolvable but invalid state.
216
+
217
+ Error bodies and logs must not contain upstream conversation, group, or call IDs, bearer tokens, result contents, or transcript text. Structured logs may include request ID, principal credential-slot identifier/hash, public group fingerprint, state transition, byte counts, and error code.
218
+
219
+ ## 11. Implementation seams (future, not performed)
220
+
221
+ - Authentication returns a principal/credential configuration rather than only boolean success.
222
+ - Response adaptation allocates group capacity and replaces upstream call IDs before OpenAI output.
223
+ - A dedicated `CorrelationStore` owns records, indexes, locks/CAS, timers, byte accounting, and tombstones.
224
+ - Request normalization separates the stored prefix from the contiguous final tool-result suffix and validates exact group completeness.
225
+ - `buildPostmanRequest` receives a validated continuation object and constructs either source-derived `USER_QUERY` or `TOOL_RESPONSE`; it never decides correlation from untrusted message IDs alone.
226
+ - Tests use source-derived synthetic frames and direct assertions, not claims that fixtures are captured live payloads.
@@ -0,0 +1,21 @@
1
+ # Fail-Closed Issue-Closure Harness
2
+
3
+ `node scripts/verify-issue-closure.mjs` is the deterministic closure gate for the Claude tool-loop and PMN attribution evidence. It never retries, chooses a fallback, repairs evidence, or treats missing fields as success.
4
+
5
+ ## Enumerated rejection branches
6
+
7
+ | Boundary | Rejected branch |
8
+ |---|---|
9
+ | Freshness | Prior current-probe attempts are non-zero |
10
+ | Upstream policy | Retry or failover is enabled |
11
+ | Claude selection | Public/selected model differs from the accepted pair |
12
+ | Claude initial phase | Non-200, zero/multiple tool calls, invalid call |
13
+ | Claude arguments | Not an object, non-empty object, parse error, or invalid schema |
14
+ | Claude continuation | Non-200, missing exact marker, non-terminal match, branch error |
15
+ | Cleanup | Probe server cleanup is not proven |
16
+ | Provider mapping | Dedicated PMN connection missing, renamed, or inactive |
17
+ | Negative control | CPX was inactive or no surrounding CPX control row exists |
18
+ | Attribution | Zero/multiple PMN rows, wrong connection/model/status |
19
+ | Marker correlation | Completion-token count differs from the exact four-token marker |
20
+
21
+ Tests seed each major bad branch and assert rejection in `test/issue-closure-harness.test.mjs`. This prevents a future documentation-only or happy-path-only claim from silently reopening issues #1/#2.