dialai 1.2.0 → 1.3.0

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.
@@ -0,0 +1,211 @@
1
+ # Strategy Result Operational Metrics
2
+
3
+ ## Executive Summary
4
+
5
+ Extend `ProposerStrategyResult` with optional operational fields (`costUSD`, `latencyMsec`, `numInputTokens`, `numOutputTokens`) so that when `submitProposal` invokes a strategy internally, the resulting proposal carries the same cost and latency data it would carry if the caller had submitted explicitly.
6
+
7
+ ## Objective
8
+
9
+ Close the data gap between the two `submitProposal` paths so both produce proposals with full operational metrics.
10
+
11
+ ## In Scope
12
+
13
+ - Extend `ProposerStrategyResult` in `types.ts` with four optional fields
14
+ - Update `executeProposerLlm` in `llm.ts` to capture timing and token usage from `callLlm`
15
+ - Update `invokeProposerStrategy` in `api.ts` to use `ProposerStrategyResult` as its return type
16
+ - Update `submitProposal` in `api.ts` to merge strategy-returned metrics into the proposal
17
+ - Unit tests for the new behavior
18
+ - Verify all existing tests still pass
19
+
20
+ ## Out of Scope
21
+
22
+ - Changing `callLlm`'s return type or signature
23
+ - Changing `ArbiterStrategyResult`
24
+ - Adding `costUSD` calculation to `executeProposerLlm`
25
+ - Webhook proposer timing (`executeProposerWebhook`)
26
+ - Changes to `executeContextWebhookProposer` (it delegates to `executeProposerLlm`; metrics cascade automatically)
27
+
28
+ ## Assumptions and Constraints
29
+
30
+ - `callLlm` returns `{ content: string; usage?: { prompt_tokens?: number; completion_tokens?: number } }` — confirmed at `llm.ts:110-115`
31
+ - `executeProposerLlm` currently discards `usage` and does not measure wall-clock time
32
+ - `submitProposal` opts values (`costUSD`, `latencyMsec`, etc.) take precedence over strategy-returned values when both present
33
+ - `ProposerStrategyResult` is the return type for `strategyFn`, `executeProposerLlm`, `executeProposerWebhook`, and `executeContextWebhookProposer`
34
+ - Custom `strategyFn` implementations are not required to return the new fields (they are optional)
35
+ - `invokeProposerStrategy` is a private function in `api.ts` with explicit return type that must be updated
36
+
37
+ ## Files to Modify
38
+
39
+ | File | Action |
40
+ |------|--------|
41
+ | `src/dialai/types.ts` | Add 4 optional fields to `ProposerStrategyResult` |
42
+ | `src/dialai/llm.ts` | Update `executeProposerLlm` to capture timing + usage |
43
+ | `src/dialai/api.ts` | Update `invokeProposerStrategy` return type; update `submitProposal` to merge metrics |
44
+ | `tests/unit/submit-proposal.test.ts` | Add 3 new tests |
45
+ | `src/dialai/llm.test.ts` | Add 1 new test |
46
+
47
+ ## Files to Read (do not modify)
48
+
49
+ | File | Why |
50
+ |------|-----|
51
+ | `src/dialai/llm-audit.test.ts` | Mock patterns for `callLlm` |
52
+ | `src/dialai/store.ts` | Understand `Proposal` storage |
53
+
54
+ ## Implementation Plan
55
+
56
+ ### Phase 1: Extend the type
57
+
58
+ In `src/dialai/types.ts`, add four optional fields to `ProposerStrategyResult` (after `reasoning`):
59
+
60
+ ```typescript
61
+ export interface ProposerStrategyResult {
62
+ transitionName: string;
63
+ toState: string;
64
+ reasoning: string;
65
+ /** Cost in USD to produce this result, if known */
66
+ costUSD?: number;
67
+ /** Wall-clock time in milliseconds, if measured */
68
+ latencyMsec?: number;
69
+ /** Input tokens consumed, if known */
70
+ numInputTokens?: number;
71
+ /** Output tokens consumed, if known */
72
+ numOutputTokens?: number;
73
+ }
74
+ ```
75
+
76
+ **Validate:** Run `npm run typecheck`. This is backward-compatible; expect no errors.
77
+
78
+ ### Phase 2: Update `executeProposerLlm`
79
+
80
+ In `src/dialai/llm.ts`, the function currently (lines 238-259) calls `callLlm`, parses the JSON, and returns the parsed result directly. Update it to:
81
+
82
+ 1. Record `Date.now()` before calling `callLlm`
83
+ 2. Compute `latencyMsec` after `callLlm` returns
84
+ 3. Extract `usage.prompt_tokens` and `usage.completion_tokens` from the result
85
+ 4. Return an explicit object with the three existing fields plus the three new measurement fields
86
+
87
+ The try/catch structure must be preserved. The `as ProposerStrategyResult` cast on the parsed JSON should be removed in favor of explicit field extraction (to avoid casting an object that lacks the metric fields into the type that now declares them).
88
+
89
+ **Validate:** Run `npm test`. All existing tests must pass.
90
+
91
+ ### Phase 3: Update `invokeProposerStrategy` and `submitProposal`
92
+
93
+ **Step 3a:** In `src/dialai/api.ts`, change the return type of `invokeProposerStrategy` (line 569) from `Promise<{ transitionName: string; toState: string; reasoning: string }>` to `Promise<ProposerStrategyResult>`. Import `ProposerStrategyResult` if not already imported. No other changes to this function are needed — it already delegates to functions that return `ProposerStrategyResult`.
94
+
95
+ **Step 3b:** In `submitProposal`, update the strategy-invocation branch. Before the `if (!finalTransitionName)` block, declare:
96
+
97
+ ```typescript
98
+ let finalCostUSD = costUSD;
99
+ let finalLatencyMsec = latencyMsec;
100
+ let finalNumInputTokens = numInputTokens;
101
+ let finalNumOutputTokens = numOutputTokens;
102
+ ```
103
+
104
+ Inside the block, after setting `finalReasoning`, add:
105
+
106
+ ```typescript
107
+ finalCostUSD = finalCostUSD ?? result.costUSD;
108
+ finalLatencyMsec = finalLatencyMsec ?? result.latencyMsec;
109
+ finalNumInputTokens = finalNumInputTokens ?? result.numInputTokens;
110
+ finalNumOutputTokens = finalNumOutputTokens ?? result.numOutputTokens;
111
+ ```
112
+
113
+ Then use the `final*` variables in the proposal construction object instead of the raw `costUSD`, `latencyMsec`, `numInputTokens`, `numOutputTokens`.
114
+
115
+ **Validate:** Run `npm run typecheck && npm test`. All must pass.
116
+
117
+ ### Phase 4: Tests
118
+
119
+ Add these tests:
120
+
121
+ **In `src/dialai/llm.test.ts`:**
122
+
123
+ 1. **`executeProposerLlm` returns metrics.** Mock `callLlm` to return `{ content: '{"transitionName":"close","toState":"closed","reasoning":"test"}', usage: { prompt_tokens: 100, completion_tokens: 50 } }`. Assert the result includes `latencyMsec` (a number > 0), `numInputTokens` (100), `numOutputTokens` (50).
124
+
125
+ **In `tests/unit/submit-proposal.test.ts`:**
126
+
127
+ 2. **Strategy metrics flow to proposal.** Register a proposer with a `strategyFn` that returns `{ transitionName: "t", toState: "s", reasoning: "r", latencyMsec: 500, numInputTokens: 200, numOutputTokens: 80 }`. Call `submitProposal({ sessionId, specialistId })` without `transitionName`. Assert the stored proposal has `latencyMsec: 500`, `numInputTokens: 200`, `numOutputTokens: 80`.
128
+
129
+ 3. **Opts override strategy metrics.** Same setup as test 2, but call `submitProposal({ sessionId, specialistId, latencyMsec: 999 })`. Assert proposal has `latencyMsec: 999` (opts wins), `numInputTokens: 200` (strategy fills gap).
130
+
131
+ 4. **Backward compat — no metrics from strategy.** Register a proposer with a `strategyFn` that returns `{ transitionName: "t", toState: "s", reasoning: "r" }` (no metric fields). Call `submitProposal` without `transitionName`. Assert the proposal is created with `latencyMsec: undefined`, `numInputTokens: undefined`. No crash.
132
+
133
+ **Validate:** Run `npm run ci`. All must pass (typecheck + lint + test + build).
134
+
135
+ ## Acceptance Criteria
136
+
137
+ ### Functional
138
+
139
+ - `ProposerStrategyResult` has optional `costUSD`, `latencyMsec`, `numInputTokens`, `numOutputTokens` fields
140
+ - `executeProposerLlm` returns `latencyMsec` and token counts from the underlying `callLlm` response
141
+ - `invokeProposerStrategy` return type is `ProposerStrategyResult`
142
+ - `submitProposal` populates proposal metrics from strategy result when caller does not provide them via opts
143
+ - Caller-provided opts values always take precedence over strategy-returned values
144
+
145
+ ### Quality
146
+
147
+ - Existing `strategyFn` implementations that do not return the new fields continue to work
148
+ - All existing tests pass without modification
149
+ - New tests cover: metrics flow, opts precedence, backward compat
150
+
151
+ ### Operational
152
+
153
+ - `npm run ci` passes (typecheck + lint + test + build)
154
+
155
+ ## Failure and Recovery Rules
156
+
157
+ 1. Run `npm run typecheck` after Phase 1. Run `npm test` after Phases 2 and 3. Run `npm run ci` after Phase 4.
158
+ 2. If adding fields to `ProposerStrategyResult` causes type errors, verify they are marked optional. If a destructure or spread assumes the exact shape, update it.
159
+ 3. If `executeProposerLlm` tests fail because `callLlm` is hard to mock, follow the pattern in `src/dialai/llm-audit.test.ts` (mock `globalThis.fetch`, set `OPENROUTER_API_TOKEN`).
160
+ 4. If changing `invokeProposerStrategy` return type causes downstream type errors, check that `submitProposal` correctly accesses the new optional fields with nullish coalescing.
161
+ 5. Do not declare completion while any acceptance criterion is unmet.
162
+
163
+ ## Completion Signal
164
+
165
+ Output exactly `COMPLETE` only when:
166
+ - All acceptance criteria are met
167
+ - `npm run ci` passes
168
+ - No blocking errors remain
169
+
170
+ ## Ralph Prompt Draft
171
+
172
+ ```
173
+ Implement operational metrics on ProposerStrategyResult.
174
+
175
+ Spec location: .claude/specs/proposal-metadata-update.md
176
+
177
+ Constraints:
178
+ - Do not change callLlm's return type or signature
179
+ - Do not change ArbiterStrategyResult
180
+ - Do not modify executeContextWebhookProposer (metrics cascade from executeProposerLlm)
181
+ - All new fields on ProposerStrategyResult must be optional
182
+
183
+ Files to modify:
184
+ - src/dialai/types.ts (ProposerStrategyResult)
185
+ - src/dialai/llm.ts (executeProposerLlm)
186
+ - src/dialai/api.ts (invokeProposerStrategy return type, submitProposal merge logic)
187
+ - tests/unit/submit-proposal.test.ts (3 new tests)
188
+ - src/dialai/llm.test.ts (1 new test)
189
+
190
+ Required deliverables:
191
+ - ProposerStrategyResult has optional costUSD, latencyMsec, numInputTokens, numOutputTokens
192
+ - executeProposerLlm returns latencyMsec and token counts from callLlm
193
+ - invokeProposerStrategy return type updated to ProposerStrategyResult
194
+ - submitProposal merges strategy metrics into proposal (opts take precedence)
195
+ - 4 new tests covering metrics flow, opts precedence, and backward compat
196
+
197
+ Acceptance criteria:
198
+ - npm run ci passes (typecheck + lint + test + build)
199
+ - All existing tests pass without modification
200
+ - New tests verify: (1) executeProposerLlm returns metrics, (2) strategy metrics flow to proposal, (3) opts override strategy metrics, (4) missing metrics don't crash
201
+
202
+ Execution rules:
203
+ 1. Work in phase order: types → llm.ts → api.ts → tests
204
+ 2. Run npm run typecheck after Phase 1
205
+ 3. Run npm test after Phases 2 and 3
206
+ 4. Run npm run ci after Phase 4
207
+ 5. If tests fail, inspect and fix the root cause before continuing
208
+ 6. If blocked after 3 attempts on the same issue, report the blocker
209
+
210
+ Output exactly COMPLETE when all criteria are met.
211
+ ```
@@ -0,0 +1,319 @@
1
+ # submitProposal: Remove `final*` Prefix Pattern
2
+
3
+ ## Executive Summary
4
+
5
+ Refactor `submitProposal` in `api.ts` to drop the `final*` variable prefix introduced in commit `0f273a9`. Instead of destructuring all opts fields as `const` and then creating parallel `let final*` variables, destructure the immutable fields as `const` and declare the mutable ones as `let` directly. Also bring `metaJson` into the same pattern so strategy-returned `metaJson` flows to the proposal.
6
+
7
+ ## Objective
8
+
9
+ Simplify `submitProposal` so the "opts wins, strategy fills gaps" merging logic reads naturally without the `final*` indirection, and add `metaJson` to the merge pattern so it works identically to the other fields.
10
+
11
+ ## In Scope
12
+
13
+ - Refactor variable declarations in `submitProposal`
14
+ - Bring `metaJson` into the same `let` / `??=` pattern as `costUSD`, `latencyMsec`, etc.
15
+ - Update existing tests if assertions reference the old behavior
16
+ - Add `metaJson` to `ProposerStrategyResult` (if not already there from the enriched-transitions spec)
17
+
18
+ ## Out of Scope
19
+
20
+ - Changes to `submitArbitration` (it doesn't have the `final*` pattern)
21
+ - Changes to any other function
22
+
23
+ ## Assumptions and Constraints
24
+
25
+ - `ProposerStrategyResult` must have an optional `metaJson` field. If the enriched-transitions spec runs first, it will already exist. If this spec runs first, add it.
26
+ - The merge semantics are: caller-provided opts value wins; strategy-returned value fills the gap.
27
+ - `transitionName`, `toState`, and `reasoning` also follow the same pattern — they just don't use the `final*` prefix inconsistently (they already do use it). Clean those up too.
28
+
29
+ ## Files to Modify
30
+
31
+ | File | Action |
32
+ |------|--------|
33
+ | `src/dialai/api.ts` | Refactor `submitProposal` variable declarations |
34
+ | `src/dialai/types.ts` | Add `metaJson` to `ProposerStrategyResult` if not already present |
35
+ | `tests/unit/submit-proposal.test.ts` | Add tests for metaJson merging |
36
+ | `website/docs/api/types.md` | Add `metaJson` field to `ProposerStrategyResult` docs |
37
+ | `.claude/skills/dial-machine/references/api-reference.md` | Add `metaJson` field to `ProposerStrategyResult` docs |
38
+
39
+ ## Implementation Plan
40
+
41
+ ### Phase 1: Refactor `submitProposal`
42
+
43
+ **Before (current code, lines 639-717):**
44
+
45
+ ```typescript
46
+ export async function submitProposal(
47
+ opts: SubmitProposalOptions
48
+ ): Promise<Proposal> {
49
+ const {
50
+ sessionId,
51
+ specialistId,
52
+ roundId,
53
+ transitionName,
54
+ reasoning,
55
+ metaJson,
56
+ costUSD,
57
+ latencyMsec,
58
+ numInputTokens,
59
+ numOutputTokens,
60
+ } = opts;
61
+ // ...
62
+ let finalTransitionName = transitionName;
63
+ let finalToState: string | undefined;
64
+ let finalReasoning = reasoning;
65
+ let finalCostUSD = costUSD;
66
+ let finalLatencyMsec = latencyMsec;
67
+ let finalNumInputTokens = numInputTokens;
68
+ let finalNumOutputTokens = numOutputTokens;
69
+
70
+ if (!finalTransitionName) {
71
+ // ...
72
+ finalTransitionName = result.transitionName;
73
+ finalToState = result.toState;
74
+ finalReasoning = finalReasoning ?? result.reasoning;
75
+ finalCostUSD = finalCostUSD ?? result.costUSD;
76
+ // ...
77
+ }
78
+
79
+ const proposal: Proposal = {
80
+ // ...
81
+ transitionName: finalTransitionName,
82
+ toState: finalToState,
83
+ reasoning: finalReasoning ?? "",
84
+ metaJson, // <-- BUG: ignores strategy metaJson
85
+ costUSD: finalCostUSD,
86
+ // ...
87
+ };
88
+ }
89
+ ```
90
+
91
+ **After:**
92
+
93
+ ```typescript
94
+ export async function submitProposal(
95
+ opts: SubmitProposalOptions
96
+ ): Promise<Proposal> {
97
+ const { sessionId, specialistId, roundId } = opts;
98
+
99
+ let transitionName = opts.transitionName;
100
+ let toState: string | undefined;
101
+ let reasoning = opts.reasoning;
102
+ let metaJson = opts.metaJson;
103
+ let costUSD = opts.costUSD;
104
+ let latencyMsec = opts.latencyMsec;
105
+ let numInputTokens = opts.numInputTokens;
106
+ let numOutputTokens = opts.numOutputTokens;
107
+
108
+ const session = await getSession(sessionId);
109
+ const specialist = await getStore().getSpecialist(specialistId);
110
+
111
+ if (!specialist) {
112
+ throw new Error(`Specialist not found: ${specialistId}`);
113
+ }
114
+
115
+ if (specialist.role !== "proposer") {
116
+ throw new Error(`Specialist ${specialistId} is not a proposer`);
117
+ }
118
+
119
+ const proposer = specialist;
120
+ const effectiveRoundId = roundId ?? session.currentRoundId;
121
+ const isHuman = proposer.isHuman ?? false;
122
+
123
+ if (!transitionName) {
124
+ const ctx = buildProposerContext(session);
125
+ const result = await invokeProposerStrategy(proposer, ctx);
126
+ transitionName = result.transitionName;
127
+ toState = result.toState;
128
+ reasoning = reasoning ?? result.reasoning;
129
+ metaJson = metaJson ?? result.metaJson;
130
+ costUSD = costUSD ?? result.costUSD;
131
+ latencyMsec = latencyMsec ?? result.latencyMsec;
132
+ numInputTokens = numInputTokens ?? result.numInputTokens;
133
+ numOutputTokens = numOutputTokens ?? result.numOutputTokens;
134
+ } else {
135
+ const currentStateDef = session.machine.states[session.currentState];
136
+ if (!currentStateDef?.transitions?.[transitionName]) {
137
+ throw new Error(
138
+ `Invalid transition "${transitionName}" from state "${session.currentState}"`
139
+ );
140
+ }
141
+ toState = currentStateDef.transitions[transitionName];
142
+ }
143
+
144
+ const proposal: Proposal = {
145
+ proposalId: generateUUID(),
146
+ sessionId,
147
+ roundId: effectiveRoundId,
148
+ specialistId,
149
+ isHuman,
150
+ transitionName,
151
+ toState,
152
+ reasoning: reasoning ?? "",
153
+ metaJson,
154
+ costUSD,
155
+ latencyMsec,
156
+ numInputTokens,
157
+ numOutputTokens,
158
+ createdAt: new Date(),
159
+ };
160
+
161
+ await getStore().setProposal(proposal);
162
+ return proposal;
163
+ }
164
+ ```
165
+
166
+ Key changes:
167
+ 1. `const` destructuring only for truly immutable fields: `sessionId`, `specialistId`, `roundId`
168
+ 2. All mutable fields declared as `let` from `opts.*`
169
+ 3. No `final*` prefix anywhere
170
+ 4. `metaJson` follows the same `??=` pattern as the metrics fields
171
+ 5. `toState` replaces `finalToState` (was already the only one using a different name)
172
+
173
+ ### Phase 2: Add `metaJson` to `ProposerStrategyResult`
174
+
175
+ In `types.ts`, add to `ProposerStrategyResult` if not already present:
176
+
177
+ ```typescript
178
+ /** Structured metadata from the strategy (e.g., tool arguments) */
179
+ metaJson?: Record<string, unknown>;
180
+ ```
181
+
182
+ **Validate:** `npm run typecheck`
183
+
184
+ ### Phase 3: Tests
185
+
186
+ **In `tests/unit/submit-proposal.test.ts`:**
187
+
188
+ **Test 1: strategy-returned metaJson flows to proposal**
189
+
190
+ ```
191
+ Setup: strategyFn returns { transitionName: "approve", toState: "approved", reasoning: "r", metaJson: { key: "from-strategy" } }
192
+ Call: submitProposal({ sessionId, specialistId }) — no metaJson in opts
193
+ Assert: proposal.metaJson deep-equals { key: "from-strategy" }
194
+ ```
195
+
196
+ **Test 2: caller-provided metaJson takes precedence over strategy metaJson**
197
+
198
+ ```
199
+ Setup: same strategyFn returning metaJson: { key: "from-strategy" }
200
+ Call: submitProposal({ sessionId, specialistId, metaJson: { key: "from-caller" } })
201
+ Assert: proposal.metaJson deep-equals { key: "from-caller" }
202
+ ```
203
+
204
+ **Test 3: no metaJson from either source**
205
+
206
+ ```
207
+ Setup: strategyFn returns { transitionName: "approve", toState: "approved", reasoning: "r" } (no metaJson)
208
+ Call: submitProposal({ sessionId, specialistId })
209
+ Assert: proposal.metaJson is undefined
210
+ Assert: no crash
211
+ ```
212
+
213
+ Existing tests (`strategy metrics flow to proposal`, `opts override strategy metrics`, `backward compat — no metrics from strategy`) must continue to pass without modification.
214
+
215
+ **Validate:** `npm run ci`
216
+
217
+ ### Phase 4: Update Documentation
218
+
219
+ **`website/docs/api/types.md` — `ProposerStrategyResult` section:**
220
+
221
+ Add `metaJson` to the interface listing. Currently shows:
222
+
223
+ ```typescript
224
+ interface ProposerStrategyResult {
225
+ transitionName: string;
226
+ toState: string;
227
+ reasoning: string;
228
+ costUSD?: number;
229
+ latencyMsec?: number;
230
+ numInputTokens?: number;
231
+ numOutputTokens?: number;
232
+ }
233
+ ```
234
+
235
+ Add after `reasoning`:
236
+
237
+ ```typescript
238
+ metaJson?: Record<string, unknown>; // Structured metadata (e.g., tool arguments)
239
+ ```
240
+
241
+ Update the prose above the code block to mention metaJson: "The optional metric and metadata fields are merged into the resulting `Proposal` when the strategy is invoked via `submitProposal`. Values passed via `SubmitProposalOptions` take precedence over strategy-returned values."
242
+
243
+ **`.claude/skills/dial-machine/references/api-reference.md` — same change:**
244
+
245
+ Add `metaJson` to `ProposerStrategyResult` in the same position.
246
+
247
+ ## Acceptance Criteria
248
+
249
+ ### Functional
250
+
251
+ - `submitProposal` has no variables with the `final` prefix
252
+ - Immutable fields (`sessionId`, `specialistId`, `roundId`) are `const` destructured
253
+ - Mutable fields (`transitionName`, `reasoning`, `metaJson`, `costUSD`, `latencyMsec`, `numInputTokens`, `numOutputTokens`, `toState`) are `let` declarations
254
+ - `metaJson` merging follows the same `opts ?? strategy` pattern as the metrics fields
255
+ - Strategy-returned `metaJson` flows to the proposal when caller doesn't provide it
256
+ - Caller-provided `metaJson` takes precedence over strategy-returned `metaJson`
257
+
258
+ ### Quality
259
+
260
+ - All existing `submit-proposal.test.ts` tests pass unchanged
261
+ - All existing tests across the project pass
262
+ - `npm run typecheck` passes
263
+ - `npm run lint` passes
264
+
265
+ ### Operational
266
+
267
+ - `npm run ci` passes
268
+
269
+ ## Failure and Recovery Rules
270
+
271
+ 1. If `npm run lint` complains about variable shadowing (the `let` declarations shadow the destructured names), that means the `const` destructuring still includes those fields. Remove them from the destructuring.
272
+ 2. If existing tests break, check that the proposal construction object uses the same field names (no `final*` prefix).
273
+ 3. The `toState` line in the `else` branch (`toState = currentStateDef.transitions[transitionName]`) will need updating if the enriched-transitions spec has already run (it would return a `TransitionDefinition` instead of a string). If so, use `typeof` check.
274
+
275
+ ## Completion Signal
276
+
277
+ Output exactly `COMPLETE` only when:
278
+ - All acceptance criteria are met
279
+ - `npm run ci` passes
280
+ - No `final*` variables remain in `submitProposal`
281
+
282
+ ## Ralph Prompt Draft
283
+
284
+ ```
285
+ Refactor submitProposal in src/dialai/api.ts to remove the final* variable prefix.
286
+
287
+ Spec location: .claude/specs/submit-proposal-cleanup.md
288
+
289
+ The current code destructures all opts fields as const, then creates parallel
290
+ let final* variables for the mutable ones. This is noisy and metaJson was
291
+ missed (line 708 uses raw opts.metaJson, so strategy-returned metaJson never
292
+ reaches the proposal).
293
+
294
+ Required changes:
295
+ - const destructure only: sessionId, specialistId, roundId
296
+ - let declarations for: transitionName, toState, reasoning, metaJson, costUSD,
297
+ latencyMsec, numInputTokens, numOutputTokens (initialized from opts.*)
298
+ - Strategy merge block: field = field ?? result.field (no final prefix)
299
+ - metaJson follows the same pattern as the metrics fields
300
+ - Proposal construction uses the let variables directly (no final prefix)
301
+ - Add metaJson to ProposerStrategyResult in types.ts if not already present
302
+
303
+ Tests to add in tests/unit/submit-proposal.test.ts:
304
+ 1. Strategy-returned metaJson flows to proposal when caller omits it
305
+ 2. Caller-provided metaJson wins over strategy-returned metaJson
306
+ 3. No metaJson from either source — proposal.metaJson is undefined
307
+
308
+ Docs to update:
309
+ - website/docs/api/types.md — add metaJson to ProposerStrategyResult
310
+ - .claude/skills/dial-machine/references/api-reference.md — same
311
+
312
+ Acceptance criteria:
313
+ - No final* variables in submitProposal
314
+ - metaJson merges with same opts-wins semantics as costUSD etc.
315
+ - All existing tests pass unchanged
316
+ - npm run ci passes
317
+
318
+ Output exactly COMPLETE when all criteria are met.
319
+ ```
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/dialai/api.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/dialai/api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAaH,OAAO,KAAK,EACV,iBAAiB,EACjB,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,OAAO,EACP,UAAU,EACV,uBAAuB,EACvB,sBAAsB,EACtB,eAAe,EACf,sBAAsB,EACtB,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,eAAe,EAGf,qBAAqB,EACrB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAMpB;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,OAAO,CAAC,OAAO,CAAC,CAgDlB;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAMpE;AAED;;;;GAIG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAEtD;AAyID;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,uBAAuB,GAC5B,OAAO,CAAC,QAAQ,CAAC,CA4BnB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAwBlB;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,UAAU,GAAG,OAAO,CAAC,GAAG,SAAS,CAAC,CAErG;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAG3E;AAED;;GAEG;AACH,wBAAsB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAGlF;AAMD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO1E;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3E;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAGlF;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAIzF;AAMD;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAchF;AAED;;;GAGG;AACH,wBAAsB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAgBvF;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAevF;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAazF;AA2CD;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,GACjB,CAAC,CAAC,GAAG,EAAE,eAAe,KAAK,OAAO,CAAC,sBAAsB,CAAC,CAAC,GAAG,IAAI,CAUpE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,GACf,CAAC,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC,GAAG,IAAI,CAU5D;AAyED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,QAAQ,CAAC,CAqEnB;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,QAAQ,EAAE,CAAC,CAErB;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,eAAe,CAAC,CA6B1B;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,cAAc,EAAE,MAAM,EACtB,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,OAAO,EAChB,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,uBAAuB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,EAC3D,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,GACnB,eAAe,CA2BjB;AA+CD;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,wBAAwB,GAC7B,OAAO,CAAC,iBAAiB,CAAC,CAkM5B;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAwClB"}
@@ -480,7 +480,15 @@ async function invokeArbiterStrategy(arbiter, ctx) {
480
480
  * If transitionName is omitted, invokes the specialist's registered strategy.
481
481
  */
482
482
  export async function submitProposal(opts) {
483
- const { sessionId, specialistId, roundId, transitionName, reasoning, metaJson, costUSD, latencyMsec, numInputTokens, numOutputTokens, } = opts;
483
+ const { sessionId, specialistId, roundId } = opts;
484
+ let transitionName = opts.transitionName;
485
+ let toState;
486
+ let reasoning = opts.reasoning;
487
+ let metaJson = opts.metaJson;
488
+ let costUSD = opts.costUSD;
489
+ let latencyMsec = opts.latencyMsec;
490
+ let numInputTokens = opts.numInputTokens;
491
+ let numOutputTokens = opts.numOutputTokens;
484
492
  const session = await getSession(sessionId);
485
493
  const specialist = await getStore().getSpecialist(specialistId);
486
494
  if (!specialist) {
@@ -492,32 +500,26 @@ export async function submitProposal(opts) {
492
500
  const proposer = specialist;
493
501
  const effectiveRoundId = roundId ?? session.currentRoundId;
494
502
  const isHuman = proposer.isHuman ?? false;
495
- let finalTransitionName = transitionName;
496
- let finalToState;
497
- let finalReasoning = reasoning;
498
- let finalCostUSD = costUSD;
499
- let finalLatencyMsec = latencyMsec;
500
- let finalNumInputTokens = numInputTokens;
501
- let finalNumOutputTokens = numOutputTokens;
502
503
  // If transitionName not provided, invoke strategy
503
- if (!finalTransitionName) {
504
+ if (!transitionName) {
504
505
  const ctx = buildProposerContext(session);
505
506
  const result = await invokeProposerStrategy(proposer, ctx);
506
- finalTransitionName = result.transitionName;
507
- finalToState = result.toState;
508
- finalReasoning = finalReasoning ?? result.reasoning;
509
- finalCostUSD = finalCostUSD ?? result.costUSD;
510
- finalLatencyMsec = finalLatencyMsec ?? result.latencyMsec;
511
- finalNumInputTokens = finalNumInputTokens ?? result.numInputTokens;
512
- finalNumOutputTokens = finalNumOutputTokens ?? result.numOutputTokens;
507
+ transitionName = result.transitionName;
508
+ toState = result.toState;
509
+ reasoning = reasoning ?? result.reasoning;
510
+ metaJson = metaJson ?? result.metaJson;
511
+ costUSD = costUSD ?? result.costUSD;
512
+ latencyMsec = latencyMsec ?? result.latencyMsec;
513
+ numInputTokens = numInputTokens ?? result.numInputTokens;
514
+ numOutputTokens = numOutputTokens ?? result.numOutputTokens;
513
515
  }
514
516
  else {
515
517
  // Validate the transition
516
518
  const currentStateDef = session.machine.states[session.currentState];
517
- if (!currentStateDef?.transitions?.[finalTransitionName]) {
518
- throw new Error(`Invalid transition "${finalTransitionName}" from state "${session.currentState}"`);
519
+ if (!currentStateDef?.transitions?.[transitionName]) {
520
+ throw new Error(`Invalid transition "${transitionName}" from state "${session.currentState}"`);
519
521
  }
520
- finalToState = currentStateDef.transitions[finalTransitionName];
522
+ toState = currentStateDef.transitions[transitionName];
521
523
  }
522
524
  const proposal = {
523
525
  proposalId: generateUUID(),
@@ -525,14 +527,14 @@ export async function submitProposal(opts) {
525
527
  roundId: effectiveRoundId,
526
528
  specialistId,
527
529
  isHuman,
528
- transitionName: finalTransitionName,
529
- toState: finalToState,
530
- reasoning: finalReasoning ?? "",
530
+ transitionName,
531
+ toState,
532
+ reasoning: reasoning ?? "",
531
533
  metaJson,
532
- costUSD: finalCostUSD,
533
- latencyMsec: finalLatencyMsec,
534
- numInputTokens: finalNumInputTokens,
535
- numOutputTokens: finalNumOutputTokens,
534
+ costUSD,
535
+ latencyMsec,
536
+ numInputTokens,
537
+ numOutputTokens,
536
538
  createdAt: new Date(),
537
539
  };
538
540
  await getStore().setProposal(proposal);