claude-spotter 1.5.10 → 1.5.12
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.
- package/CHANGELOG.md +21 -0
- package/README.ja.md +4 -2
- package/README.md +6 -3
- package/docs/11_dashboard-operations.md +40 -7
- package/ops/dashboard/windows/install-dashboard-tasks.ps1 +2 -2
- package/package.json +1 -1
- package/src/core/evaluation-report.mjs +3 -1
- package/src/core/evaluation-store.mjs +8 -2
- package/src/dashboard/render.mjs +2 -2
- package/src/hooks/pre-tool-use.mjs +4 -3
- package/src/hooks/user-prompt.mjs +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
各節はそのversion公開時点の変更記録であり、後続versionにより置換された仕様を含む。
|
|
4
4
|
現行runtime契約は[`docs/00_overview.md`](docs/00_overview.md)から辿る。
|
|
5
5
|
|
|
6
|
+
## 1.5.12 — 2026-08-18
|
|
7
|
+
|
|
8
|
+
- **Stop未観測の評価turnを次prompt時に採点して閉じる。** 実測でoutcome_missing 2,091件の94%が
|
|
9
|
+
「Stop未観測のまま次promptが来てincomplete廃棄」だった(steering・キュー済みprompt・Esc中断で
|
|
10
|
+
prompt:StopはClaude 3:1 / Codex 24:1)。`recordTurn`はopenな前turnを白紙で捨てず、収集済み
|
|
11
|
+
`used_tool_ids`で`adopted` / `not_adopted`へ確定する。`usage_status=incomplete`の印が付いたturn
|
|
12
|
+
だけが従来どおり`outcome_missing`に残る。
|
|
13
|
+
- **A/C率の表示を「提案適合率(上限)」へ改名。** 同一turn内の利用は提案の文脈適合の上限しか
|
|
14
|
+
示さず、Spotter起因の成果を証明しない。CLI報告は`tool fit rate (upper bound)`、dashboardは
|
|
15
|
+
「提案適合率(上限)」と表示する。内部schema(`toolAdoptionRate`キー)と集計式は変更しない。
|
|
16
|
+
- 過去に保存済みの`outcome_missing`itemは遡って再採点しない(closeの証拠が残っていないため)。
|
|
17
|
+
|
|
18
|
+
## 1.5.11 — 2026-08-15
|
|
19
|
+
|
|
20
|
+
- **Windows dashboard taskのconsole windowを隠す。** device serverとreverse tunnelの
|
|
21
|
+
PowerShell actionへ`-NonInteractive -WindowStyle Hidden`を指定し、ログオン時に黒い窓を表示しない。
|
|
22
|
+
- **既存のユーザー実行契約を維持する。** Task Schedulerの`Interactive` principal、APPDATA上の
|
|
23
|
+
npm shim、SSH鍵とknown_hostsのユーザープロファイルは変更せず、起動表示だけを修正する。
|
|
24
|
+
- **非Claude Hookの無出力契約をNode 24でも満たす。** UserPromptSubmitとPreToolUseはunsupported判定後に
|
|
25
|
+
評価SQLiteを遅延読込し、negative fixtureへ`ExperimentalWarning`を出さない。
|
|
26
|
+
|
|
6
27
|
## 1.5.10 — 2026-08-14
|
|
7
28
|
|
|
8
29
|
- **非ClaudeのHook入力を副作用前に無視する。** 非空の`sessionId`と`hookEventName`を持ち、
|
package/README.ja.md
CHANGED
|
@@ -224,7 +224,7 @@ spotter diagnostics factory
|
|
|
224
224
|
spotter diagnostics runtime-errors
|
|
225
225
|
# opt-in端末内runtime error集計をread-only表示(network送信なし)
|
|
226
226
|
spotter evaluation report
|
|
227
|
-
# 端末内DBからproject
|
|
227
|
+
# 端末内DBからproject横断の提案率・提案適合率(上限)を集計
|
|
228
228
|
spotter evaluation cases --outcome not-adopted
|
|
229
229
|
# 提案されたが同じturnで使われなかったtool itemを一覧
|
|
230
230
|
spotter evaluation case <observation-id>
|
|
@@ -253,13 +253,15 @@ spotter uninstall # hook 登録を解除 (~/.spotter は残す)
|
|
|
253
253
|
|
|
254
254
|
dashboardはlocal-firstで動く。各端末が自身の`~/.spotter/evaluation.db`を読み、hubは固定の
|
|
255
255
|
端末・upstream対応だけを持つ。評価データをcloud DBへ複製しない。端末画面では
|
|
256
|
-
|
|
256
|
+
対象ターン、ツール提案あり、提案ツール数、利用判定済み、実際に使用、判定不能、提案率、提案適合率(上限。同一ターン内の利用はSpotter起因を証明しない)、
|
|
257
257
|
project/tool内訳、非採用case、監査対象request、任意の提案時Throughline証拠を確認できる。
|
|
258
258
|
health確認は端末一覧request時だけなので、端末がofflineでもbackground監視や
|
|
259
259
|
retry queueを作らず、その端末だけを切り離せる。
|
|
260
260
|
|
|
261
261
|
4端末のservice、reverse tunnel、Caddy/Cloudflare構成は
|
|
262
262
|
[docs/11_dashboard-operations.md](docs/11_dashboard-operations.md)を参照。
|
|
263
|
+
Windows同梱のTask Scheduler installerはnpm・SSH用の対話ユーザープロファイルを維持しつつ、
|
|
264
|
+
dashboardの2つのPowerShell actionを非対話・console非表示で起動する。
|
|
263
265
|
|
|
264
266
|
Codex risk dispatch を daemon から非同期に流す場合:
|
|
265
267
|
|
package/README.md
CHANGED
|
@@ -232,7 +232,7 @@ spotter diagnostics factory
|
|
|
232
232
|
spotter diagnostics runtime-errors
|
|
233
233
|
# print the local allow-listed runtime-error aggregate snapshot (no network)
|
|
234
234
|
spotter evaluation report
|
|
235
|
-
# show cross-project proposal
|
|
235
|
+
# show cross-project proposal counts and fit (upper-bound) rates from the local DB
|
|
236
236
|
spotter evaluation cases --outcome not-adopted
|
|
237
237
|
# list proposed tools that were not used in the same turn
|
|
238
238
|
spotter evaluation case <observation-id>
|
|
@@ -261,14 +261,17 @@ spotter uninstall # remove hooks from this project (leaves ~/.spotter int
|
|
|
261
261
|
|
|
262
262
|
The dashboard is local-first. Every terminal reads its own `~/.spotter/evaluation.db`; the hub
|
|
263
263
|
keeps only a static device-to-upstream map and does not copy evaluation data into a cloud database.
|
|
264
|
-
The device view shows Japanese labels for every evaluation metric, proposal and
|
|
265
|
-
|
|
264
|
+
The device view shows Japanese labels for every evaluation metric, the proposal rate and the
|
|
265
|
+
fit rate (an upper bound: same-turn usage does not prove Spotter caused it) with their
|
|
266
|
+
numerator and denominator, project/tool breakdowns, non-adopted cases, the request
|
|
266
267
|
audited by Spotter, and optional proposal-time Throughline evidence. The hub checks health only
|
|
267
268
|
when the device list is requested, so an offline terminal is isolated without a background monitor
|
|
268
269
|
or retry queue.
|
|
269
270
|
|
|
270
271
|
The reference four-terminal service, reverse-tunnel, and Caddy/Cloudflare layout is documented in
|
|
271
272
|
[docs/11_dashboard-operations.md](docs/11_dashboard-operations.md).
|
|
273
|
+
On Windows, the bundled Task Scheduler installer keeps the interactive user's profile for npm and
|
|
274
|
+
SSH while starting both dashboard PowerShell actions non-interactively with hidden console windows.
|
|
272
275
|
|
|
273
276
|
Optional async Codex risk dispatch:
|
|
274
277
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Spotter評価dashboard運用
|
|
2
2
|
|
|
3
|
-
現行npm配布版: **v1.5.
|
|
4
|
-
dashboard
|
|
3
|
+
現行npm配布版: **v1.5.12**(2026-08-18)。v1.5.12は評価の採点方式と表示名
|
|
4
|
+
(提案適合率(上限))を変更し、dashboardのrouting構成は変更していない。
|
|
5
5
|
|
|
6
6
|
この文書はservice設定の正本であり、各端末に現在installされているnpm versionの台帳ではない。
|
|
7
7
|
端末versionは対象端末で`spotter --version`を実行して確認する。
|
|
@@ -86,20 +86,53 @@ systemctl --user enable --now spotter-dashboard-device.service spotter-dashboard
|
|
|
86
86
|
|
|
87
87
|
## FOX Windows native
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
main-serverのSSH host名またはIP
|
|
89
|
+
同梱された`ops/dashboard/windows/`のPowerShellを固定pathへ配置する。npm global版から
|
|
90
|
+
`~/.spotter/dashboard/`へ反映し、Windows側から解決できるmain-serverのSSH host名またはIPを
|
|
91
|
+
明示してinstallerを実行する。npm更新後も同じ手順で配布scriptとtask定義を更新する。
|
|
91
92
|
|
|
92
93
|
```powershell
|
|
93
|
-
|
|
94
|
+
$source = Join-Path (npm root -g) 'claude-spotter\ops\dashboard\windows'
|
|
95
|
+
$target = Join-Path $HOME '.spotter\dashboard'
|
|
96
|
+
New-Item -ItemType Directory -Path $target -Force | Out-Null
|
|
97
|
+
Copy-Item -Path (Join-Path $source '*.ps1') -Destination $target -Force
|
|
98
|
+
& (Join-Path $target 'install-dashboard-tasks.ps1') -MainServer '<main-server-host-or-ip>'
|
|
94
99
|
```
|
|
95
100
|
|
|
96
|
-
|
|
101
|
+
新規taskまたは現在ユーザーが所有するtaskなら管理者権限は不要。既存taskが
|
|
102
|
+
`BUILTIN\Administrators`所有で`Register-ScheduledTask`が`Access is denied`になった場合だけ、
|
|
103
|
+
同じinstallerを管理者PowerShellから再実行する。principalは`Interactive` / `Limited`のままで、
|
|
104
|
+
APPDATAのnpm shim、SSH鍵、known_hostsを現在ユーザープロファイルから読む。
|
|
105
|
+
|
|
106
|
+
登録される2 taskはログオン時にdevice serverとreverse tunnelをhidden起動し、console windowを
|
|
107
|
+
表示しない。task actionに`-NonInteractive -WindowStyle Hidden`が含まれることとdevice healthを確認する。
|
|
97
108
|
|
|
98
109
|
```powershell
|
|
99
110
|
Invoke-RestMethod http://127.0.0.1:53944/_spotter/health
|
|
100
|
-
Get-ScheduledTask -TaskName 'Spotter dashboard *' |
|
|
111
|
+
Get-ScheduledTask -TaskName 'Spotter dashboard *' |
|
|
112
|
+
Select-Object TaskName, State, @{Name='Arguments'; Expression={$_.Actions.Arguments}}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
task更新時に旧deviceの子processだけが残ると、healthは旧processから返る一方、新taskはport競合で
|
|
116
|
+
`Ready` / result `2`になる。この組合せを観測した場合だけ、53944 listenerのcommand lineを先に確認する。
|
|
117
|
+
|
|
118
|
+
```powershell
|
|
119
|
+
$connection = Get-NetTCPConnection -LocalAddress 127.0.0.1 -LocalPort 53944 -State Listen
|
|
120
|
+
$owner = Get-CimInstance Win32_Process -Filter "ProcessId = $($connection.OwningProcess)"
|
|
121
|
+
$owner | Select-Object ProcessId, ParentProcessId, Name, CommandLine
|
|
101
122
|
```
|
|
102
123
|
|
|
124
|
+
`CommandLine`が`claude-spotter`の`dashboard device`かつtaskが`Ready`であることを確認できた時だけ、
|
|
125
|
+
その旧PIDを停止してtaskを起動し直す。名前だけで全Node processを停止しない。
|
|
126
|
+
|
|
127
|
+
```powershell
|
|
128
|
+
Stop-Process -Id $owner.ProcessId
|
|
129
|
+
Start-ScheduledTask -TaskName 'Spotter dashboard device'
|
|
130
|
+
Invoke-RestMethod http://127.0.0.1:53944/_spotter/health
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
v1.5.11のFOX Windows native実測は
|
|
134
|
+
[`evidence/dashboard-windows-hidden-v1.5.11.md`](evidence/dashboard-windows-hidden-v1.5.11.md)に記録する。
|
|
135
|
+
|
|
103
136
|
## 公開経路
|
|
104
137
|
|
|
105
138
|
Caddyへ次を追加する。`spotter.kitepon.dev`はcase詳細に会話文脈を含むため、Cloudflare側では
|
|
@@ -6,10 +6,10 @@ $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interac
|
|
|
6
6
|
$settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)
|
|
7
7
|
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
|
|
8
8
|
|
|
9
|
-
$deviceAction = New-ScheduledTaskAction -Execute $powershell -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ops\spotter-dashboard-device.ps1`""
|
|
9
|
+
$deviceAction = New-ScheduledTaskAction -Execute $powershell -Argument "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$ops\spotter-dashboard-device.ps1`""
|
|
10
10
|
Register-ScheduledTask -TaskName 'Spotter dashboard device' -Action $deviceAction -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
|
|
11
11
|
|
|
12
|
-
$tunnelAction = New-ScheduledTaskAction -Execute $powershell -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ops\spotter-dashboard-tunnel.ps1`" -MainServer `"$MainServer`""
|
|
12
|
+
$tunnelAction = New-ScheduledTaskAction -Execute $powershell -Argument "-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$ops\spotter-dashboard-tunnel.ps1`" -MainServer `"$MainServer`""
|
|
13
13
|
Register-ScheduledTask -TaskName 'Spotter dashboard tunnel' -Action $tunnelAction -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null
|
|
14
14
|
|
|
15
15
|
Start-ScheduledTask -TaskName 'Spotter dashboard device'
|
package/package.json
CHANGED
|
@@ -68,7 +68,9 @@ function appendSummary(lines, label, summary, prefix = '') {
|
|
|
68
68
|
lines.push(`${prefix}${label}`);
|
|
69
69
|
lines.push(`${prefix} S=${summary.S} P=${summary.P} I=${summary.I} C=${summary.C} A=${summary.A} M=${summary.M}`);
|
|
70
70
|
lines.push(`${prefix} proposal rate: ${fraction(summary.P, summary.S)}`);
|
|
71
|
-
|
|
71
|
+
// Same-turn usage proves at most that a proposal fit the context; it cannot
|
|
72
|
+
// attribute the usage to Spotter. Label the A/C ratio as an upper bound.
|
|
73
|
+
lines.push(`${prefix} tool fit rate (upper bound): ${fraction(summary.A, summary.C)}`);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
function fraction(numerator, denominator) {
|
|
@@ -50,12 +50,18 @@ export class EvaluationStore {
|
|
|
50
50
|
recordTurn(input) {
|
|
51
51
|
const turn = normalizeTurn(input);
|
|
52
52
|
return this.#transaction(() => {
|
|
53
|
+
// A still-open previous turn means its Stop was never observed (steering /
|
|
54
|
+
// queued prompt / Esc interrupt). Grade it with the usage evidence collected
|
|
55
|
+
// so far instead of discarding it: only a turn already marked incomplete
|
|
56
|
+
// keeps outcome_missing.
|
|
53
57
|
const previous = this.database.prepare(`
|
|
54
|
-
SELECT observation_id FROM evaluation_turns
|
|
58
|
+
SELECT observation_id, used_tool_ids, usage_status FROM evaluation_turns
|
|
55
59
|
WHERE session_id = ? AND completed_at_ms IS NULL
|
|
56
60
|
ORDER BY recorded_at_ms DESC LIMIT 1
|
|
57
61
|
`).get(turn.sessionId);
|
|
58
|
-
if (previous)
|
|
62
|
+
if (previous) {
|
|
63
|
+
this.#close(previous.observation_id, parseToolIds(previous.used_tool_ids), previous.usage_status, turn.recordedAtMs);
|
|
64
|
+
}
|
|
59
65
|
|
|
60
66
|
this.database.prepare(`
|
|
61
67
|
INSERT INTO evaluation_turns (
|
package/src/dashboard/render.mjs
CHANGED
|
@@ -127,13 +127,13 @@ function renderFilters(filters, action) {
|
|
|
127
127
|
function renderRates(summary) {
|
|
128
128
|
const proposal = rate(metricNumber(summary, 'P'), metricNumber(summary, 'S'));
|
|
129
129
|
const adoption = rate(metricNumber(summary, 'A'), metricNumber(summary, 'C'));
|
|
130
|
-
return `<dl class="rates"><div><dt>提案率<small>ツール提案あり ÷ 対象ターン</small></dt><dd>${escapeHtml(proposal)}</dd></div><div><dt
|
|
130
|
+
return `<dl class="rates"><div><dt>提案率<small>ツール提案あり ÷ 対象ターン</small></dt><dd>${escapeHtml(proposal)}</dd></div><div><dt>提案適合率(上限)<small>実際に使用 ÷ 利用判定済み</small></dt><dd>${escapeHtml(adoption)}</dd></div></dl>`;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
function renderBreakdown(id, title, rows) {
|
|
134
134
|
return `<section aria-labelledby="${id}">
|
|
135
135
|
<h2 id="${id}">${title}</h2>
|
|
136
|
-
${rows.length === 0 ? '<p class="empty">該当するデータはありません。</p>' : `<div class="table-wrap"><table><thead><tr><th scope="col">項目</th>${METRIC_KEYS.map((key) => `<th scope="col">${METRIC_LABELS[key]}</th>`).join('')}<th scope="col">提案率</th><th scope="col"
|
|
136
|
+
${rows.length === 0 ? '<p class="empty">該当するデータはありません。</p>' : `<div class="table-wrap"><table><thead><tr><th scope="col">項目</th>${METRIC_KEYS.map((key) => `<th scope="col">${METRIC_LABELS[key]}</th>`).join('')}<th scope="col">提案率</th><th scope="col">提案適合率(上限)</th></tr></thead><tbody>${rows.map(([label, summary]) => `<tr><th scope="row">${escapeHtml(label)}</th>${METRIC_KEYS.map((key) => `<td>${escapeHtml(metric(summary, key))}</td>`).join('')}<td>${escapeHtml(rate(metricNumber(summary, 'P'), metricNumber(summary, 'S')))}</td><td>${escapeHtml(rate(metricNumber(summary, 'A'), metricNumber(summary, 'C')))}</td></tr>`).join('')}</tbody></table></div>`}
|
|
137
137
|
</section>`;
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
} from './lib.mjs';
|
|
18
18
|
import { sendRequest } from '../daemon/transport.mjs';
|
|
19
19
|
import { canonicalizeToolId } from '../core/evaluation-tool-id.mjs';
|
|
20
|
-
import { createEvaluationStore } from '../core/evaluation-store.mjs';
|
|
21
20
|
|
|
22
21
|
const TIMEOUT_MS = 1_000;
|
|
23
22
|
|
|
@@ -25,7 +24,7 @@ export async function runPreToolUse({
|
|
|
25
24
|
readInput = readStdinJson,
|
|
26
25
|
sendRequestFn = sendRequest,
|
|
27
26
|
recordHookEventFn = recordClaudeHookEvent,
|
|
28
|
-
createEvaluationStoreFn =
|
|
27
|
+
createEvaluationStoreFn = null,
|
|
29
28
|
writeError = (text) => process.stderr.write(text),
|
|
30
29
|
} = {}) {
|
|
31
30
|
if (isChildCall()) return;
|
|
@@ -33,6 +32,8 @@ export async function runPreToolUse({
|
|
|
33
32
|
if (isUnsupportedNonClaudeEnvelope(input)) return;
|
|
34
33
|
if (isSubagentCall(input)) return;
|
|
35
34
|
if (isOutsideSpotterProject(input)) return;
|
|
35
|
+
const evaluationStoreFactory = createEvaluationStoreFn
|
|
36
|
+
?? (await import('../core/evaluation-store.mjs')).createEvaluationStore;
|
|
36
37
|
|
|
37
38
|
const sessionId = requireString(input, 'session_id');
|
|
38
39
|
const toolName = requireString(input, 'tool_name');
|
|
@@ -45,7 +46,7 @@ export async function runPreToolUse({
|
|
|
45
46
|
const invalidateEvaluation = () => {
|
|
46
47
|
if (!evaluationObserved || evaluationInvalidated) return;
|
|
47
48
|
evaluationInvalidated = true;
|
|
48
|
-
closeEvaluationAsIncomplete({ sessionId, createEvaluationStoreFn, writeError });
|
|
49
|
+
closeEvaluationAsIncomplete({ sessionId, createEvaluationStoreFn: evaluationStoreFactory, writeError });
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
try {
|
|
@@ -27,7 +27,6 @@ import { spawnDaemonAndWaitReady } from './spawn-daemon.mjs';
|
|
|
27
27
|
import { discardLegacyPending } from './pending-context.mjs';
|
|
28
28
|
import { projectBackendFailure, projectParentAdvice, projectToolIds } from './parent-output-projector.mjs';
|
|
29
29
|
import { randomUUID } from 'node:crypto';
|
|
30
|
-
import { createEvaluationStore } from '../core/evaluation-store.mjs';
|
|
31
30
|
import { loadEvaluationContext } from '../core/evaluation-context.mjs';
|
|
32
31
|
import { version } from '../version.mjs';
|
|
33
32
|
|
|
@@ -39,7 +38,7 @@ export async function runUserPrompt({
|
|
|
39
38
|
spawnDaemonAndWaitReadyFn = spawnDaemonAndWaitReady,
|
|
40
39
|
discardLegacyPendingFn = discardLegacyPending,
|
|
41
40
|
recordHookEventFn = recordClaudeHookEvent,
|
|
42
|
-
createEvaluationStoreFn =
|
|
41
|
+
createEvaluationStoreFn = null,
|
|
43
42
|
loadEvaluationContextFn = loadEvaluationContext,
|
|
44
43
|
randomUUIDFn = randomUUID,
|
|
45
44
|
spotterVersion = version,
|
|
@@ -52,6 +51,8 @@ export async function runUserPrompt({
|
|
|
52
51
|
if (isUnsupportedNonClaudeEnvelope(input)) return;
|
|
53
52
|
if (isSubagentCall(input)) return;
|
|
54
53
|
if (isOutsideSpotterProject(input)) return;
|
|
54
|
+
const evaluationStoreFactory = createEvaluationStoreFn
|
|
55
|
+
?? (await import('../core/evaluation-store.mjs')).createEvaluationStore;
|
|
55
56
|
|
|
56
57
|
const sessionId = requireString(input, 'session_id');
|
|
57
58
|
const prompt = requireString(input, 'prompt');
|
|
@@ -76,7 +77,7 @@ export async function runUserPrompt({
|
|
|
76
77
|
recordedAtMs: proposalRecordedAtMs,
|
|
77
78
|
})
|
|
78
79
|
: { status: 'not_requested', snapshot: null };
|
|
79
|
-
const store =
|
|
80
|
+
const store = evaluationStoreFactory();
|
|
80
81
|
try {
|
|
81
82
|
store.recordTurn({
|
|
82
83
|
observationId,
|