codex-agent-view 0.2.0 → 0.2.1
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/.codex-plugin/plugin.json +1 -1
- package/README.md +78 -28
- package/bin/codex-agent-view.mjs +183 -9
- package/hooks/hooks.json +44 -0
- package/package.json +1 -1
- package/public/app.js +86 -7
- package/public/index.html +9 -4
- package/public/styles.css +72 -0
- package/src/core/monitor-store.mjs +91 -2
- package/src/core/normalize-hook-payload.mjs +11 -3
package/README.md
CHANGED
|
@@ -8,23 +8,27 @@ Codex Agent View는 공식 Codex 앱을 그대로 사용하면서 부모 task와
|
|
|
8
8
|
|
|
9
9
|
### 현재 상태
|
|
10
10
|
|
|
11
|
-
현재 source version은 `0.2.
|
|
11
|
+
현재 source version은 `0.2.1`이다. 다음 구성은 구현되어 있다.
|
|
12
12
|
|
|
13
13
|
- `.codex-plugin/plugin.json`, local marketplace catalog, genuine Codex skill
|
|
14
|
-
-
|
|
14
|
+
- 부모 task용 `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `Stop`과 subagent/tool/permission hook wiring
|
|
15
15
|
- privacy-minimized hook sender와 bounded in-memory reducer
|
|
16
16
|
- `127.0.0.1` 전용 token-authenticated local HTTP runtime
|
|
17
17
|
- 부모 task/session, subagent, 최근 활동, permission wait 상태를 표시하는 local UI
|
|
18
18
|
- `start`, `status`, `doctor`, `install`, `uninstall` CLI
|
|
19
19
|
- 명시적 설치·hook trust·제거 경로
|
|
20
20
|
|
|
21
|
-
Homebrew Codex CLI와 공식 앱에 포함된 embedded Codex executable
|
|
21
|
+
Homebrew Codex CLI와 공식 앱에 포함된 embedded Codex executable에서는 plugin 설치와 lifecycle payload를 검증했다. 그러나 `0.2.0`을 실행 중인 공식 앱 process에 설치·enable한 실사용 재현에서는 실제 subagent 2개를 실행해도 monitor가 event를 0건 수신했다. Monitor, plugin 등록, enable, 설치 bundle은 정상이었지만 앱 log에는 sender 실행 흔적이 없었다. 같은 app process가 설치 전의 `hooks/list` snapshot을 유지한 정황이 있으며, CLI JSON으로 exact hook trust 상태를 확인할 수 없어 config snapshot과 trust 중 어느 경계에서 skip됐는지는 확정하지 않았다.
|
|
22
|
+
|
|
23
|
+
`0.2.1`은 부모 task lifecycle hook을 추가하고, `status`, `doctor`, 빈 UI가 “monitor 정상”과 “hook event 수신”을 구분하도록 개선한 patch다. 공식 Codex 앱 `26.727.40816`(`build 6067`)을 재시작하고 설치·활성화된 plugin `0.2.1`을 사용한 실제 E2E에서 task ID를 사전 등록하지 않아도 parent session 3개와 subagent 3개가 UI에 자동으로 나타났다. 실제 hook의 `SessionStart`, `UserPromptSubmit`, `Stop`, `SubagentStart`, `SubagentStop`, `PreToolUse`, `PostToolUse`, `PermissionRequest`가 sender → loopback monitor → UI 경로에 반영됐다. `SessionEnd` wiring은 포함돼 있지만 실제 공식 앱 event는 아직 관찰하지 않았으므로 호환 확인을 주장하지 않는다.
|
|
22
24
|
|
|
23
25
|
Maintainer npm 2FA는 `auth-and-writes` mode로 활성화됐고 `codex-agent-view@0.2.0`은 public npm registry에서 사용할 수 있다. npm publish와 별개인 Universal Plugins Directory 제출은 아직 완료되지 않아 directory 검색에는 나타나지 않는다.
|
|
24
26
|
|
|
27
|
+
검증된 `0.2.0` 릴리스: npm `gitHead`와 annotated `v0.2.0` tag는 commit `00b62af56698ac875e39c7d1386905c157c3a7e8`로 일치하고, registry SRI/signature 및 tag source와 registry artifact의 21개 package file byte 일치를 확인했다. [GitHub Release v0.2.0](https://github.com/JunhoYoon95/codex-agent-view/releases/tag/v0.2.0)은 공개 상태다. 별도 npm provenance attestation은 선택 사항이며 이 릴리스에는 없다.
|
|
28
|
+
|
|
25
29
|
### 제품 경계
|
|
26
30
|
|
|
27
|
-
Codex Agent View는 historical audit이나 session replay 제품이 아니라 현재 활동을 보여주는 live companion이다. Bounded in-memory state와 monitor 재시작 시 reset은 privacy와 단순한 failure boundary를 위한 의도된
|
|
31
|
+
Codex Agent View는 historical audit이나 session replay 제품이 아니라 현재 활동을 보여주는 live companion이다. Bounded in-memory state와 monitor 재시작 시 reset은 privacy와 단순한 failure boundary를 위한 의도된 완성 설계다. SQLite/영구 history는 누락된 요구사항이 아니다. 실제 사용자 요구가 입증될 때에만 retention, migration, deletion, privacy 비용을 별도 검토하는 명시적 opt-in 기능 후보로 취급한다.
|
|
28
32
|
|
|
29
33
|
- live 상태의 source of truth는 hook event다.
|
|
30
34
|
- Live 상태는 설계대로 monitor process의 bounded memory에만 있고 재시작하면 새 관찰 window가 시작된다.
|
|
@@ -33,6 +37,14 @@ Codex Agent View는 historical audit이나 session replay 제품이 아니라
|
|
|
33
37
|
- task/subagent 중지·재시작, message 전송, permission 자동 승인·거절 기능이 없다.
|
|
34
38
|
- App Server는 향후 계층 metadata 보강 후보일 뿐이며 공식 앱 process와 memory를 공유한다고 가정하지 않는다.
|
|
35
39
|
|
|
40
|
+
별도로 실행한 Codex `0.146` App Server의 `thread/list` fallback도 실제 확인했지만 현재 root/subagent가 모두 `notLoaded`로 나타나 공식 앱의 live running/completed 상태를 공유하지 않았다. Persisted parent ID, alias, depth 보강은 가능하나 live 판별은 불가능했고 state DB read, privacy, 복잡도만 늘어나므로 `0.2.1`에는 채택하지 않았다.
|
|
41
|
+
|
|
42
|
+
### npm, local browser UI, Plugins Directory가 각각 필요한 이유
|
|
43
|
+
|
|
44
|
+
- npm은 `codex-agent-view` local executable, hook sender, runtime, static UI를 사용자 machine에 배포하는 경로다.
|
|
45
|
+
- Browser는 `127.0.0.1`에서만 열리는 현재 read-only companion UI다. 외부 website나 telemetry dashboard가 아니다.
|
|
46
|
+
- Universal Plugins Directory는 npm의 대체재가 아니다. 공개 directory의 in-app custom UI 경로는 public HTTPS MCP server와 domain verification이 필요해 local-only/no-external-server 원칙과 충돌한다. 현재는 별도의 listing/skills 제출 가능성만 검토하며, 심사·publish 전에는 Codex plugin 검색으로 설치할 수 있다고 안내하지 않는다.
|
|
47
|
+
|
|
36
48
|
Hook event가 누락·중복·역순으로 올 수 있으므로 UI의 `unknown`, `stopped_without_start`, 빈 상태는 그대로 해석해야 한다. 빈 session 목록은 “이 monitor가 event를 관찰하지 못함”이며 “실행 중인 task가 없음”의 증거가 아니다.
|
|
37
49
|
|
|
38
50
|
### 요구사항과 검증 범위
|
|
@@ -45,7 +57,7 @@ Hook event가 누락·중복·역순으로 올 수 있으므로 UI의 `unknown`,
|
|
|
45
57
|
|
|
46
58
|
| Runtime | 확인된 버전 | 확인 범위 |
|
|
47
59
|
| --- | --- | --- |
|
|
48
|
-
| 공식 Codex 앱 | `26.727.40816` (`build 6067`) |
|
|
60
|
+
| 공식 Codex 앱 | `26.727.40816` (`build 6067`) | 재시작한 현재 조합에서 plugin `0.2.1`의 parent 3개·subagent 3개 자동 표시 및 실제 hook 8종 E2E 확인; `SessionEnd` 미관찰 |
|
|
49
61
|
| 앱 embedded Codex | `0.146.0-alpha.9.2` | isolated plugin install/runtime 및 lifecycle probe |
|
|
50
62
|
| Homebrew Codex CLI | `0.146.0` | isolated plugin install/runtime probe |
|
|
51
63
|
|
|
@@ -89,7 +101,8 @@ node bin/codex-agent-view.mjs install
|
|
|
89
101
|
2. 공식 앱의 Plugins Directory 또는 CLI `/plugins`에서 plugin이 설치·활성화됐는지 확인한다.
|
|
90
102
|
3. CLI TUI composer의 `/hooks` 또는 공식 앱의 해당 hook review UI에서 `hooks/hooks.json`과 `node "${PLUGIN_ROOT}/scripts/send-hook.mjs"` command를 검토한다.
|
|
91
103
|
4. 현재 hook definition의 exact hash를 사용자가 직접 trust한다.
|
|
92
|
-
5. 공식
|
|
104
|
+
5. Plugin 설치 전부터 공식 앱이 열려 있었다면 앱을 완전히 종료·재실행한다.
|
|
105
|
+
6. 반드시 plugin 활성화와 hook trust 이후에 **새 task**를 만든다. 설치·trust 전에 발생한 event는 재생되지 않는다.
|
|
93
106
|
|
|
94
107
|
`/hooks`는 CLI TUI command이며 `codex /hooks`라는 shell command가 아니다. Hook definition이 바뀌면 hash도 바뀌므로 다시 검토한다. 일반 설치에서 trust-bypass option을 사용하지 않는다.
|
|
95
108
|
|
|
@@ -117,17 +130,20 @@ node bin/codex-agent-view.mjs doctor --json
|
|
|
117
130
|
|
|
118
131
|
- `status`는 실행 중 monitor가 관찰한 task/session과 subagent 수를 읽는다.
|
|
119
132
|
- `status --json`은 hook 기반 snapshot과 bounded diagnostics를 반환한다.
|
|
120
|
-
- `doctor`는 Codex CLI, plugin
|
|
133
|
+
- `doctor`는 Codex CLI, plugin 설치·enable, 설치된 hook bundle, monitor event 수신 여부, runtime directory를 진단한다.
|
|
134
|
+
- `doctor`의 hook trust는 `unknown`일 수 있다. `codex plugin list --json`은 persisted exact-hook trust를 노출하지 않으므로 interactive Codex CLI의 `/hooks`에서 직접 확인한다.
|
|
121
135
|
- `Ctrl+C`는 monitor를 종료하며 in-memory state와 정상 종료된 runtime file을 정리한다.
|
|
122
136
|
|
|
123
137
|
Monitor가 꺼져 있어도 hook sender는 fail-open으로 끝나 Codex task를 막지 않는다. Monitor를 나중에 켜면 꺼져 있던 동안의 event가 복구되지는 않는다.
|
|
124
138
|
|
|
139
|
+
Monitor가 실행 중이고 plugin enable/trust가 끝난 뒤 생성되거나 재개되는 task는 hook이 도착하면 task ID를 미리 등록하지 않아도 자동으로 목록에 나타난다. UI 검색은 이렇게 자동 수신된 목록을 거르는 선택적 filter일 뿐이며, task 추적을 시작하거나 ID를 등록하는 기능이 아니다. Plugin 설치·trust 전이나 monitor downtime에 이미 지나간 event는 재생되지 않는다.
|
|
140
|
+
|
|
125
141
|
### npm에서 설치
|
|
126
142
|
|
|
127
|
-
권장 설치 방법은
|
|
143
|
+
`0.2.1` release의 권장 설치 방법은 exact version을 global로 설치하는 것이다. 이 문서 작성 시점에 public registry에서 확인된 버전은 아직 `0.2.0`이므로, `0.2.1` publish가 확인되기 전에는 이 repository source에서 검증한다.
|
|
128
144
|
|
|
129
145
|
```bash
|
|
130
|
-
npm install --global codex-agent-view@0.2.
|
|
146
|
+
npm install --global codex-agent-view@0.2.1
|
|
131
147
|
codex-agent-view doctor
|
|
132
148
|
codex-agent-view install
|
|
133
149
|
codex-agent-view start
|
|
@@ -136,11 +152,13 @@ codex-agent-view start
|
|
|
136
152
|
Global install 없이 exact version을 일회성으로 실행할 수도 있다.
|
|
137
153
|
|
|
138
154
|
```bash
|
|
139
|
-
npx --yes codex-agent-view@0.2.
|
|
140
|
-
npx --yes codex-agent-view@0.2.
|
|
141
|
-
npx --yes codex-agent-view@0.2.
|
|
155
|
+
npx --yes codex-agent-view@0.2.1 doctor
|
|
156
|
+
npx --yes codex-agent-view@0.2.1 install
|
|
157
|
+
npx --yes codex-agent-view@0.2.1 start
|
|
142
158
|
```
|
|
143
159
|
|
|
160
|
+
위 public artifact 검증 기록은 `0.2.0`에 대한 것이다. Isolated global install과 exact-version `npx` 양쪽에서 CLI lifecycle을 통과했고 fixture event는 status/UI에 반영됐지만, 이후 실행 중이던 실제 공식 앱 process에서는 event 0건이 재현됐다. 별도의 `0.2.1` source/package QA와 재시작한 공식 앱 E2E에서는 실제 hook 8종과 task ID 등록 없는 자동 표시를 확인했다. 다만 `0.2.1` registry artifact, digest, tag·GitHub Release는 publish 뒤 별도로 검증해야 하며, 실제 `SessionEnd`도 아직 미관찰이다. Registry evidence와 검증 경계는 [docs/distribution.md](docs/distribution.md)에 기록한다.
|
|
161
|
+
|
|
144
162
|
npm install 자체는 Codex 설정을 자동 변경하지 않는다. `install` command는 사용자가 명시적으로 실행하며 hook trust도 사용자 검토로 남긴다. npm publish와 Universal Plugins Directory 제출은 서로 별도 절차다. 자세한 배포 경계는 [docs/distribution.md](docs/distribution.md), directory 제출 상태는 [docs/plugin-submission.md](docs/plugin-submission.md)를 참고한다.
|
|
145
163
|
|
|
146
164
|
### Privacy와 opt-in diagnostic capture
|
|
@@ -182,15 +200,19 @@ Monitor가 실행 중인지, stale runtime file인지, runtime directory가 예
|
|
|
182
200
|
|
|
183
201
|
#### UI에 task/subagent가 없음
|
|
184
202
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
- plugin
|
|
188
|
-
|
|
189
|
-
|
|
203
|
+
다음 순서대로 확인한다.
|
|
204
|
+
|
|
205
|
+
1. `codex-agent-view doctor --json`에서 plugin `installed`, `enabled`, hook bundle `wiring_ok`, monitor `ok`를 확인한다.
|
|
206
|
+
2. `monitor.events_received`가 `false`라면 monitor 연결 성공과 hook 전달 성공을 혼동하지 않는다.
|
|
207
|
+
3. Interactive Codex CLI의 `/hooks`에서 현재 `send-hook.mjs` definition의 exact hash를 검토하고 trust한다.
|
|
208
|
+
4. Plugin 설치 전에 열려 있던 공식 앱은 완전히 종료·재실행한다.
|
|
209
|
+
5. Plugin enable/trust 뒤 만든 새 task에서 parent prompt와 subagent를 실행한다.
|
|
210
|
+
|
|
211
|
+
빈 상태는 “이 monitor observation window에 event가 도착하지 않음”을 뜻한다. Codex에 실행 중인 task가 없다는 뜻도, 반대로 GUI가 hook을 절대 지원하지 않는다는 뜻도 아니다. 위 절차 뒤에도 `events_received: false`이면 Codex 앱 version, plugin version, 앱/CLI 구분을 포함해 issue로 보고한다.
|
|
190
212
|
|
|
191
213
|
#### `PermissionRequest`가 표시되지 않음
|
|
192
214
|
|
|
193
|
-
Approval이 실제 필요한 동작에서만 발생할 수 있다.
|
|
215
|
+
Approval이 실제 필요한 동작에서만 발생할 수 있다. 공식 앱 `26.727.40816`(`build 6067`) E2E에서는 실제 `PermissionRequest` hook이 sender → loopback monitor → UI에 도착해 read-only waiting 상태로 표시되는 것을 확인했다. 다른 환경에서 표시되지 않으면 schema 문제와 “event 자체가 발생하지 않음”을 분리해 조사한다. Monitor는 approval을 자동 처리하지 않는다.
|
|
194
216
|
|
|
195
217
|
### 문서와 지원
|
|
196
218
|
|
|
@@ -213,15 +235,19 @@ Codex Agent View is a lightweight, read-only companion monitor for the official
|
|
|
213
235
|
|
|
214
236
|
### Status
|
|
215
237
|
|
|
216
|
-
The current source version is `0.2.
|
|
238
|
+
The current source version is `0.2.1`. It includes the plugin and marketplace manifests, a genuine Codex skill, privacy-minimized parent-task, subagent, tool, and permission hooks, a bounded in-memory reducer, a token-authenticated `127.0.0.1` runtime, a local dashboard, and `start`, `status`, `doctor`, `install`, and `uninstall` commands.
|
|
239
|
+
|
|
240
|
+
Plugin installation and lifecycle payloads were verified with Homebrew Codex CLI and the Codex executable embedded in the official app. However, a real-use attempt that installed and enabled `0.2.0` in an already-running official app process delivered zero events while two subagents ran. The monitor, registration, enablement, and installed bundle were healthy, while app logs showed no sender invocation. Evidence indicates that the same process retained a pre-install `hooks/list` snapshot; persisted exact-hook trust is not exposed through CLI JSON, so the precise skip boundary remains unconfirmed.
|
|
217
241
|
|
|
218
|
-
|
|
242
|
+
`0.2.1` adds `SessionStart`, `SessionEnd`, `UserPromptSubmit`, and `Stop` for parent-task lifecycle visibility and makes `status`, `doctor`, and the empty UI distinguish monitor health from hook delivery. In a real E2E after restarting official Codex app `26.727.40816` (`build 6067`) with plugin `0.2.1` installed and enabled, three parent sessions and three subagents appeared automatically without pre-registering task IDs. Real `SessionStart`, `UserPromptSubmit`, `Stop`, `SubagentStart`, `SubagentStop`, `PreToolUse`, `PostToolUse`, and `PermissionRequest` hooks reached the sender, loopback monitor, and UI. `SessionEnd` is wired but has not yet been observed from the real official app, so compatibility for that event is not claimed.
|
|
219
243
|
|
|
220
244
|
Maintainer npm 2FA is enabled in `auth-and-writes` mode, and `codex-agent-view@0.2.0` is available from the public npm registry. npm publication remains separate from Universal Plugins Directory submission; the plugin is not directory-searchable.
|
|
221
245
|
|
|
246
|
+
Verified `0.2.0` release: npm `gitHead` and the annotated `v0.2.0` tag both resolve to commit `00b62af56698ac875e39c7d1386905c157c3a7e8`; the registry SRI/signature and all 21 package files against the tagged source were verified. [GitHub Release v0.2.0](https://github.com/JunhoYoon95/codex-agent-view/releases/tag/v0.2.0) is public. A separate npm provenance attestation is optional and was not published for this release.
|
|
247
|
+
|
|
222
248
|
### Boundaries
|
|
223
249
|
|
|
224
|
-
Codex Agent View is a live companion, not a historical audit or session-replay product. Bounded in-memory state and reset-on-restart semantics are
|
|
250
|
+
Codex Agent View is a live companion, not a historical audit or session-replay product. Bounded in-memory state and reset-on-restart semantics are intentional: they keep privacy and failure boundaries small. SQLite or persistent history is not a missing requirement. Consider it only as a separate explicit opt-in feature if demonstrated user demand justifies retention, migration, deletion, and privacy costs.
|
|
225
251
|
|
|
226
252
|
- Hooks are the source of truth for live state.
|
|
227
253
|
- Operational state exists by design only in bounded process memory; restart begins a new observation window.
|
|
@@ -230,6 +256,14 @@ Codex Agent View is a live companion, not a historical audit or session-replay p
|
|
|
230
256
|
- The product cannot stop or restart tasks/subagents, send messages, or approve/deny permissions.
|
|
231
257
|
- Missing, duplicated, or out-of-order events remain visible as empty, unknown, or degraded state instead of being guessed away.
|
|
232
258
|
|
|
259
|
+
A separately launched Codex `0.146` App Server `thread/list` fallback was also tested. It reported both the current root and subagents as `notLoaded`, so it did not share the official app's live running/completed state. Persisted parent IDs, aliases, and depth could enrich metadata, but the fallback could not determine live status and would add state-database reads, privacy surface, and complexity. It is therefore not included in `0.2.1`.
|
|
260
|
+
|
|
261
|
+
### Why npm, a local browser UI, and the Plugins Directory are different
|
|
262
|
+
|
|
263
|
+
- npm distributes the local `codex-agent-view` executable, hook sender, runtime, and static UI to the user's machine.
|
|
264
|
+
- The browser renders the current read-only companion UI on `127.0.0.1`; it is not an external website or telemetry dashboard.
|
|
265
|
+
- The Universal Plugins Directory does not replace npm. A public in-app custom UI path requires a public HTTPS MCP server and domain verification, which conflicts with this project's local-only, no-external-server boundary. Only a separate listing/skills submission remains under consideration; do not expect Directory search installation until review and publication actually finish.
|
|
266
|
+
|
|
233
267
|
### Requirements and tested versions
|
|
234
268
|
|
|
235
269
|
- Node.js `>=18`
|
|
@@ -238,7 +272,7 @@ Codex Agent View is a live companion, not a historical audit or session-replay p
|
|
|
238
272
|
|
|
239
273
|
| Runtime | Tested version | Scope |
|
|
240
274
|
| --- | --- | --- |
|
|
241
|
-
| Official Codex app | `26.727.40816` (`build 6067`) |
|
|
275
|
+
| Official Codex app | `26.727.40816` (`build 6067`) | real E2E after restart with plugin `0.2.1`: three parents and three subagents appeared automatically and eight real hook types reached the UI; `SessionEnd` unobserved |
|
|
242
276
|
| App-embedded Codex | `0.146.0-alpha.9.2` | isolated install/runtime and lifecycle probe |
|
|
243
277
|
| Homebrew Codex CLI | `0.146.0` | isolated install/runtime probe |
|
|
244
278
|
|
|
@@ -258,7 +292,7 @@ node bin/codex-agent-view.mjs install
|
|
|
258
292
|
|
|
259
293
|
There are no production dependencies; the runtime uses Node.js built-ins. `install` explicitly copies the package into a local marketplace under the runtime directory and registers `codex-agent-view@codex-agent-view`. No `postinstall` script changes Codex settings.
|
|
260
294
|
|
|
261
|
-
Review the installed plugin and `hooks/hooks.json`, inspect the `node "${PLUGIN_ROOT}/scripts/send-hook.mjs"` command, explicitly trust the current hook hash
|
|
295
|
+
Review the installed plugin and `hooks/hooks.json`, inspect the `node "${PLUGIN_ROOT}/scripts/send-hook.mjs"` command, and explicitly trust the current hook hash. If the app was open before installation, quit it completely and reopen it. Create the test task only after enablement and trust; earlier events are not replayed.
|
|
262
296
|
|
|
263
297
|
Start the foreground monitor:
|
|
264
298
|
|
|
@@ -273,14 +307,18 @@ node bin/codex-agent-view.mjs status --json
|
|
|
273
307
|
node bin/codex-agent-view.mjs doctor --json
|
|
274
308
|
```
|
|
275
309
|
|
|
310
|
+
`doctor` checks installation, enablement, the installed hook bundle, monitor health, and whether any hook event reached the monitor. Hook trust can remain `unknown`: `codex plugin list --json` does not expose persisted exact-hook trust, so inspect it interactively in Codex CLI `/hooks`.
|
|
311
|
+
|
|
276
312
|
An empty session list means that this monitor observed no events. It does not prove that Codex has no running task. Stopping or restarting the monitor discards its in-memory state, and downtime events are not replayed.
|
|
277
313
|
|
|
314
|
+
Once the monitor is running and plugin enablement/trust is complete, hooks from newly created or resumed tasks appear automatically without pre-registering a task ID. Search is only an optional filter over that automatically received list; it does not start tracking or register a task. Events that occurred before installation/trust or while the monitor was down are not replayed.
|
|
315
|
+
|
|
278
316
|
### Install from npm
|
|
279
317
|
|
|
280
|
-
|
|
318
|
+
The recommended installation for the `0.2.1` release is the exact global version below. At the time of writing, the public registry version confirmed by this document is still `0.2.0`; validate from this repository source until the `0.2.1` publication is confirmed.
|
|
281
319
|
|
|
282
320
|
```bash
|
|
283
|
-
npm install --global codex-agent-view@0.2.
|
|
321
|
+
npm install --global codex-agent-view@0.2.1
|
|
284
322
|
codex-agent-view doctor
|
|
285
323
|
codex-agent-view install
|
|
286
324
|
codex-agent-view start
|
|
@@ -289,13 +327,25 @@ codex-agent-view start
|
|
|
289
327
|
Or run the exact version without a global install:
|
|
290
328
|
|
|
291
329
|
```bash
|
|
292
|
-
npx --yes codex-agent-view@0.2.
|
|
293
|
-
npx --yes codex-agent-view@0.2.
|
|
294
|
-
npx --yes codex-agent-view@0.2.
|
|
330
|
+
npx --yes codex-agent-view@0.2.1 doctor
|
|
331
|
+
npx --yes codex-agent-view@0.2.1 install
|
|
332
|
+
npx --yes codex-agent-view@0.2.1 start
|
|
295
333
|
```
|
|
296
334
|
|
|
335
|
+
That public-artifact evidence applies to `0.2.0`: isolated global and exact-version `npx` CLI lifecycles passed, and fixture events reached status/UI. A later already-running official-app process reproduced zero delivered events. Separate `0.2.1` source/package QA and a restarted official-app E2E confirmed eight real hook types and automatic listing without task-ID registration. The `0.2.1` registry artifact, digest, tag, and GitHub Release still require separate post-publication verification, and a real `SessionEnd` remains unobserved. See [Distribution](docs/distribution.md) for the registry evidence and verification boundary.
|
|
336
|
+
|
|
297
337
|
npm installation does not modify Codex settings automatically. The explicit `install` command performs local plugin registration and leaves hook trust to the user. npm publication and Universal Plugins Directory submission are separate. See [Distribution](docs/distribution.md) and [Plugin submission](docs/plugin-submission.md).
|
|
298
338
|
|
|
339
|
+
### Troubleshooting an empty monitor
|
|
340
|
+
|
|
341
|
+
1. Run `codex-agent-view doctor --json` and check plugin `installed`, `enabled`, hook `wiring_ok`, and monitor `ok`.
|
|
342
|
+
2. If `monitor.events_received` is `false`, do not confuse monitor connectivity with successful hook delivery.
|
|
343
|
+
3. In interactive Codex CLI `/hooks`, review and trust the exact current `send-hook.mjs` definition.
|
|
344
|
+
4. Fully quit and reopen an official app process that was running before plugin installation.
|
|
345
|
+
5. Create a new task after enablement/trust, then run a parent prompt and a subagent.
|
|
346
|
+
|
|
347
|
+
If events are still absent, report the Codex app/CLI version, plugin version, app-versus-CLI runtime, and redacted `doctor` diagnostic codes. Never share the runtime token or a raw payload.
|
|
348
|
+
|
|
299
349
|
### Privacy
|
|
300
350
|
|
|
301
351
|
The normal hook path uses `scripts/send-hook.mjs`. It minimizes the local Codex payload before loopback delivery, and the reducer retains only a narrower state schema in memory. The normal monitor does not write an event JSONL history.
|
package/bin/codex-agent-view.mjs
CHANGED
|
@@ -172,32 +172,172 @@ async function status(args) {
|
|
|
172
172
|
0,
|
|
173
173
|
);
|
|
174
174
|
process.stdout.write(`${sessions.length} task(s), ${agents} subagent(s) observed.\n`);
|
|
175
|
+
if (!Number.isFinite(snapshot.updated_at_ms) || snapshot.updated_at_ms <= 0) {
|
|
176
|
+
process.stdout.write(
|
|
177
|
+
"No hook event has reached this monitor yet. The monitor can be healthy while Codex skips an untrusted or not-yet-loaded hook. Run `codex-agent-view doctor`, review `/hooks` in Codex CLI, then use a new task.\n",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
175
180
|
for (const session of sessions) {
|
|
176
181
|
process.stdout.write(`- ${session.session_id}: ${session.status} (${session.agents.length} agents)\n`);
|
|
177
182
|
}
|
|
178
183
|
}
|
|
179
184
|
|
|
185
|
+
function parsePluginList(result) {
|
|
186
|
+
if (result.code !== 0) return null;
|
|
187
|
+
try {
|
|
188
|
+
const parsed = JSON.parse(result.stdout);
|
|
189
|
+
return Array.isArray(parsed.installed) ? parsed.installed : [];
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function inspectInstalledHookBundle(pluginEntry) {
|
|
196
|
+
const sourcePath = pluginEntry?.source?.path;
|
|
197
|
+
if (typeof sourcePath !== "string" || sourcePath.length === 0) {
|
|
198
|
+
return {
|
|
199
|
+
hooks_file: false,
|
|
200
|
+
sender_file: false,
|
|
201
|
+
source_path: null,
|
|
202
|
+
wiring_ok: false,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const hooksPath = join(sourcePath, "hooks", "hooks.json");
|
|
206
|
+
const senderPath = join(sourcePath, "scripts", "send-hook.mjs");
|
|
207
|
+
const hooks = await readJsonRegularFile(hooksPath);
|
|
208
|
+
const sender = await pathExists(senderPath);
|
|
209
|
+
const hookGroups = hooks?.hooks;
|
|
210
|
+
const expectedEvents = [
|
|
211
|
+
"SessionStart",
|
|
212
|
+
"SessionEnd",
|
|
213
|
+
"UserPromptSubmit",
|
|
214
|
+
"Stop",
|
|
215
|
+
"SubagentStart",
|
|
216
|
+
"SubagentStop",
|
|
217
|
+
"PreToolUse",
|
|
218
|
+
"PostToolUse",
|
|
219
|
+
"PermissionRequest",
|
|
220
|
+
];
|
|
221
|
+
const hooksFile = hookGroups && typeof hookGroups === "object";
|
|
222
|
+
const declaredEvents = hooksFile
|
|
223
|
+
? expectedEvents.filter((event) => Array.isArray(hookGroups[event]))
|
|
224
|
+
: [];
|
|
225
|
+
return {
|
|
226
|
+
declared_events: declaredEvents,
|
|
227
|
+
hooks_file: Boolean(hooksFile),
|
|
228
|
+
sender_file: Boolean(sender?.isFile() && !sender.isSymbolicLink()),
|
|
229
|
+
source_path: sourcePath,
|
|
230
|
+
wiring_ok:
|
|
231
|
+
Boolean(hooksFile) &&
|
|
232
|
+
declaredEvents.length === expectedEvents.length &&
|
|
233
|
+
Boolean(sender?.isFile() && !sender.isSymbolicLink()),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
180
237
|
async function doctor(args) {
|
|
181
238
|
const codex = await run("codex", ["--version"], { allowFailure: true });
|
|
182
239
|
let monitor = { ok: false, message: "not running" };
|
|
183
240
|
try {
|
|
184
241
|
const snapshot = await fetchState();
|
|
185
|
-
monitor = {
|
|
242
|
+
monitor = {
|
|
243
|
+
events_received:
|
|
244
|
+
Number.isFinite(snapshot.updated_at_ms) && snapshot.updated_at_ms > 0,
|
|
245
|
+
ok: true,
|
|
246
|
+
sessions: snapshot.sessions?.length || 0,
|
|
247
|
+
updated_at_ms:
|
|
248
|
+
Number.isFinite(snapshot.updated_at_ms) && snapshot.updated_at_ms > 0
|
|
249
|
+
? snapshot.updated_at_ms
|
|
250
|
+
: null,
|
|
251
|
+
};
|
|
186
252
|
} catch (error) {
|
|
187
253
|
monitor = { ok: false, message: error.message };
|
|
188
254
|
}
|
|
189
255
|
const plugins = await run("codex", ["plugin", "list", "--json"], {
|
|
190
256
|
allowFailure: true,
|
|
191
257
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
258
|
+
const pluginList = parsePluginList(plugins);
|
|
259
|
+
const pluginEntry = pluginList?.find((entry) => entry.pluginId === PLUGIN_ID) || null;
|
|
260
|
+
const hookBundle = await inspectInstalledHookBundle(pluginEntry);
|
|
261
|
+
const diagnostics = [];
|
|
262
|
+
if (codex.code !== 0) {
|
|
263
|
+
diagnostics.push({
|
|
264
|
+
action: "Install or repair Codex CLI so `codex --version` succeeds.",
|
|
265
|
+
code: "codex_cli_unavailable",
|
|
266
|
+
severity: "error",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (pluginList === null) {
|
|
270
|
+
diagnostics.push({
|
|
271
|
+
action: "Check `codex plugin list --json` and the installed Codex CLI version.",
|
|
272
|
+
code: "plugin_list_unavailable",
|
|
273
|
+
severity: "error",
|
|
274
|
+
});
|
|
275
|
+
} else if (!pluginEntry) {
|
|
276
|
+
diagnostics.push({
|
|
277
|
+
action: "Run `codex-agent-view install`.",
|
|
278
|
+
code: "plugin_not_installed",
|
|
279
|
+
severity: "error",
|
|
280
|
+
});
|
|
281
|
+
} else if (pluginEntry.enabled !== true) {
|
|
282
|
+
diagnostics.push({
|
|
283
|
+
action: "Enable Codex Agent View in the Codex plugin browser, then start a new task.",
|
|
284
|
+
code: "plugin_disabled",
|
|
285
|
+
severity: "error",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (pluginEntry && !hookBundle.wiring_ok) {
|
|
289
|
+
diagnostics.push({
|
|
290
|
+
action: "Run `codex-agent-view install` again to restore the owned plugin bundle.",
|
|
291
|
+
code: "hook_bundle_invalid",
|
|
292
|
+
severity: "error",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (pluginEntry && pluginEntry.version !== (await packageVersion())) {
|
|
296
|
+
diagnostics.push({
|
|
297
|
+
action: "Run `codex-agent-view install` to align the installed plugin with this CLI package.",
|
|
298
|
+
code: "plugin_version_mismatch",
|
|
299
|
+
severity: "warning",
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
if (!monitor.ok) {
|
|
303
|
+
diagnostics.push({
|
|
304
|
+
action: "Run `codex-agent-view start` before expecting live task events.",
|
|
305
|
+
code: "monitor_not_running",
|
|
306
|
+
severity: "warning",
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (pluginEntry) {
|
|
310
|
+
diagnostics.push({
|
|
311
|
+
action: "Review the current plugin hook definition in interactive Codex CLI `/hooks`.",
|
|
312
|
+
code: "hook_trust_unverified",
|
|
313
|
+
severity: "info",
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (monitor.ok && monitor.events_received === false) {
|
|
317
|
+
diagnostics.push({
|
|
318
|
+
action:
|
|
319
|
+
"Open Codex CLI, review and trust the current definition in `/hooks`, then start a new task. Restart a Codex app process that was already open during installation.",
|
|
320
|
+
code: "no_hook_events_observed",
|
|
321
|
+
severity: "warning",
|
|
322
|
+
});
|
|
323
|
+
}
|
|
197
324
|
const report = {
|
|
198
325
|
codex: { ok: codex.code === 0, version: codex.stdout.trim() || null },
|
|
326
|
+
diagnostics,
|
|
327
|
+
hook: {
|
|
328
|
+
...hookBundle,
|
|
329
|
+
trust: pluginEntry ? "unknown" : "not_applicable",
|
|
330
|
+
trust_note: pluginEntry
|
|
331
|
+
? "Codex CLI does not expose persisted hook trust through `plugin list --json`; review `/hooks` interactively."
|
|
332
|
+
: null,
|
|
333
|
+
},
|
|
199
334
|
monitor,
|
|
200
|
-
plugin: {
|
|
335
|
+
plugin: {
|
|
336
|
+
enabled: pluginEntry?.enabled === true,
|
|
337
|
+
installed: Boolean(pluginEntry),
|
|
338
|
+
source_path: pluginEntry?.source?.path || null,
|
|
339
|
+
version: pluginEntry?.version || null,
|
|
340
|
+
},
|
|
201
341
|
runtime_directory: runtimeDirectory(),
|
|
202
342
|
};
|
|
203
343
|
if (args.includes("--json")) {
|
|
@@ -205,8 +345,20 @@ async function doctor(args) {
|
|
|
205
345
|
return;
|
|
206
346
|
}
|
|
207
347
|
process.stdout.write(`Codex CLI: ${report.codex.ok ? report.codex.version : "not available"}\n`);
|
|
208
|
-
process.stdout.write(
|
|
348
|
+
process.stdout.write(
|
|
349
|
+
`Plugin: ${report.plugin.installed ? `installed (${report.plugin.enabled ? "enabled" : "disabled"})` : "not installed"}\n`,
|
|
350
|
+
);
|
|
351
|
+
if (report.plugin.installed) {
|
|
352
|
+
process.stdout.write(`Hook bundle: ${report.hook.wiring_ok ? "valid" : "invalid"}\n`);
|
|
353
|
+
process.stdout.write("Hook trust: unknown (Codex exposes review through interactive `/hooks`)\n");
|
|
354
|
+
}
|
|
209
355
|
process.stdout.write(`Monitor: ${monitor.ok ? "running" : monitor.message}\n`);
|
|
356
|
+
if (monitor.ok) {
|
|
357
|
+
process.stdout.write(`Hook events: ${monitor.events_received ? "observed" : "none observed"}\n`);
|
|
358
|
+
}
|
|
359
|
+
for (const diagnostic of diagnostics) {
|
|
360
|
+
process.stdout.write(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.action}\n`);
|
|
361
|
+
}
|
|
210
362
|
}
|
|
211
363
|
|
|
212
364
|
async function pathExists(path) {
|
|
@@ -362,8 +514,30 @@ async function install() {
|
|
|
362
514
|
await run("codex", ["plugin", "marketplace", "add", destination, "--json"]);
|
|
363
515
|
}
|
|
364
516
|
await run("codex", ["plugin", "add", PLUGIN_ID, "--json"]);
|
|
517
|
+
const verification = await run("codex", ["plugin", "list", "--json"], {
|
|
518
|
+
allowFailure: true,
|
|
519
|
+
});
|
|
520
|
+
const installed = parsePluginList(verification)?.find(
|
|
521
|
+
(entry) => entry.pluginId === PLUGIN_ID,
|
|
522
|
+
);
|
|
523
|
+
if (!installed) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
"Codex did not report the plugin as installed after registration; run `codex-agent-view doctor` for details",
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (installed.enabled !== true) {
|
|
529
|
+
throw new Error(
|
|
530
|
+
"the plugin was registered but is disabled; enable it in the Codex plugin browser, then run `codex-agent-view doctor`",
|
|
531
|
+
);
|
|
532
|
+
}
|
|
365
533
|
process.stdout.write(`Installed ${PLUGIN_ID} from ${destination}.\n`);
|
|
366
|
-
process.stdout.write("
|
|
534
|
+
process.stdout.write("Registration verified: installed and enabled.\n");
|
|
535
|
+
process.stdout.write(
|
|
536
|
+
"Hook trust cannot be granted or inspected non-interactively. Review and trust this plugin's current hook definition in Codex CLI `/hooks`.\n",
|
|
537
|
+
);
|
|
538
|
+
process.stdout.write(
|
|
539
|
+
"If Codex was already open during installation, restart it completely, then create a new task.\n",
|
|
540
|
+
);
|
|
367
541
|
}
|
|
368
542
|
|
|
369
543
|
function isBroadRuntimeRoot(root) {
|
package/hooks/hooks.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"description": "Capture privacy-minimized Codex lifecycle payloads for the local companion monitor.",
|
|
3
3
|
"hooks": {
|
|
4
|
+
"SessionStart": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/send-hook.mjs\"",
|
|
10
|
+
"timeout": 5
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"SessionEnd": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/send-hook.mjs\"",
|
|
21
|
+
"timeout": 3
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"UserPromptSubmit": [
|
|
27
|
+
{
|
|
28
|
+
"hooks": [
|
|
29
|
+
{
|
|
30
|
+
"type": "command",
|
|
31
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/send-hook.mjs\"",
|
|
32
|
+
"timeout": 5
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"Stop": [
|
|
38
|
+
{
|
|
39
|
+
"hooks": [
|
|
40
|
+
{
|
|
41
|
+
"type": "command",
|
|
42
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/send-hook.mjs\"",
|
|
43
|
+
"timeout": 5
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
4
48
|
"SubagentStart": [
|
|
5
49
|
{
|
|
6
50
|
"hooks": [
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -65,6 +65,7 @@ const elements = Object.freeze({
|
|
|
65
65
|
const viewState = {
|
|
66
66
|
updatedAtMs: null,
|
|
67
67
|
sessions: [],
|
|
68
|
+
diagnostics: [],
|
|
68
69
|
hasLoaded: false,
|
|
69
70
|
errorMessage: "",
|
|
70
71
|
canRetry: true,
|
|
@@ -130,10 +131,22 @@ function normalizeActivity(value) {
|
|
|
130
131
|
};
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
function normalizeDiagnostic(value) {
|
|
135
|
+
const diagnostic = isRecord(value) ? value : {};
|
|
136
|
+
return {
|
|
137
|
+
code: safeString(diagnostic.code, "unknown_diagnostic"),
|
|
138
|
+
diagnosedAtMs: safeTimestamp(diagnostic.diagnosed_at_ms),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
133
142
|
function deriveSessionStatus(session, agents, recentActivities) {
|
|
134
143
|
if (session.permission?.status === "waiting_for_user") {
|
|
135
144
|
return "waiting";
|
|
136
145
|
}
|
|
146
|
+
const reportedStatus = normalizeCoreStatus(session.status);
|
|
147
|
+
if (reportedStatus === "running" || reportedStatus === "completed") {
|
|
148
|
+
return reportedStatus;
|
|
149
|
+
}
|
|
137
150
|
if (
|
|
138
151
|
agents.some((agent) => agent.status === "running") ||
|
|
139
152
|
recentActivities[0]?.status === "running"
|
|
@@ -166,7 +179,8 @@ function normalizeState(value) {
|
|
|
166
179
|
!isRecord(value) ||
|
|
167
180
|
value.schema_version !== 1 ||
|
|
168
181
|
value.source_of_truth !== "hook" ||
|
|
169
|
-
!Array.isArray(value.sessions)
|
|
182
|
+
!Array.isArray(value.sessions) ||
|
|
183
|
+
!Array.isArray(value.diagnostics)
|
|
170
184
|
) {
|
|
171
185
|
throw new TypeError("상태 응답 형식이 올바르지 않습니다.");
|
|
172
186
|
}
|
|
@@ -174,6 +188,7 @@ function normalizeState(value) {
|
|
|
174
188
|
return {
|
|
175
189
|
updatedAtMs: safeTimestamp(value.updated_at_ms),
|
|
176
190
|
sessions: value.sessions.map(normalizeSession),
|
|
191
|
+
diagnostics: value.diagnostics.map(normalizeDiagnostic),
|
|
177
192
|
};
|
|
178
193
|
}
|
|
179
194
|
|
|
@@ -462,7 +477,7 @@ function renderMetrics() {
|
|
|
462
477
|
elements.metricWaiting.textContent = String(waitingCount);
|
|
463
478
|
elements.metricCompleted.textContent = String(countStatus("completed"));
|
|
464
479
|
elements.lastUpdated.textContent = !viewState.updatedAtMs
|
|
465
|
-
? "
|
|
480
|
+
? "수신된 hook 없음"
|
|
466
481
|
: formatDateTime(viewState.updatedAtMs);
|
|
467
482
|
}
|
|
468
483
|
|
|
@@ -486,10 +501,77 @@ function setStateMessage(kind, title, description, includeRetry = false) {
|
|
|
486
501
|
}
|
|
487
502
|
}
|
|
488
503
|
|
|
504
|
+
function setEmptyObservationMessage() {
|
|
505
|
+
elements.stateMessage.className = "state-message state-empty state-empty-observation";
|
|
506
|
+
elements.stateMessage.replaceChildren();
|
|
507
|
+
|
|
508
|
+
const heading = document.createElement("strong");
|
|
509
|
+
const copy = document.createElement("span");
|
|
510
|
+
|
|
511
|
+
if (viewState.diagnostics.length) {
|
|
512
|
+
heading.textContent = "표시 가능한 task가 없습니다.";
|
|
513
|
+
copy.textContent = `Monitor가 hook 입력 ${viewState.diagnostics.length}건을 받았지만 유효한 session으로 적용하지 않았습니다.`;
|
|
514
|
+
} else {
|
|
515
|
+
heading.textContent = "이 관찰 창에서 수신된 hook event가 0건입니다.";
|
|
516
|
+
copy.textContent = "로컬 monitor 연결은 정상입니다. 이 결과만으로 Codex에 실행 중인 task나 agent가 없다고 판단할 수 없습니다.";
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const guidance = document.createElement("div");
|
|
520
|
+
guidance.className = "empty-guidance";
|
|
521
|
+
|
|
522
|
+
const guidanceTitle = document.createElement("h3");
|
|
523
|
+
guidanceTitle.textContent = "표시되지 않을 때 확인 순서";
|
|
524
|
+
|
|
525
|
+
const automaticTracking = document.createElement("p");
|
|
526
|
+
automaticTracking.className = "automatic-tracking";
|
|
527
|
+
automaticTracking.textContent = "task ID를 입력하거나 task별로 등록할 필요가 없습니다. Trusted hook event가 자동으로 이 목록에 추가됩니다.";
|
|
528
|
+
|
|
529
|
+
const steps = document.createElement("ol");
|
|
530
|
+
for (const step of [
|
|
531
|
+
"Codex Agent View를 실행한 뒤 새 Codex task를 시작합니다.",
|
|
532
|
+
"설치된 plugin의 현재 hook command를 검토하고 직접 trust했는지 확인합니다.",
|
|
533
|
+
"plugin 설치 또는 hook trust 변경 후 공식 Codex 앱을 완전히 재시작하고 새 task에서 subagent를 실행합니다.",
|
|
534
|
+
]) {
|
|
535
|
+
const item = document.createElement("li");
|
|
536
|
+
item.textContent = step;
|
|
537
|
+
steps.append(item);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const boundary = document.createElement("p");
|
|
541
|
+
boundary.className = "observation-boundary";
|
|
542
|
+
boundary.textContent = "Plugin 설치·trust 전에 이미 지나간 event와 monitor가 꺼져 있던 동안의 event는 재생되지 않습니다.";
|
|
543
|
+
|
|
544
|
+
guidance.append(guidanceTitle, automaticTracking, steps, boundary);
|
|
545
|
+
|
|
546
|
+
if (viewState.diagnostics.length) {
|
|
547
|
+
const diagnostics = document.createElement("details");
|
|
548
|
+
diagnostics.className = "diagnostic-details";
|
|
549
|
+
const summary = document.createElement("summary");
|
|
550
|
+
summary.textContent = `검증 diagnostic ${viewState.diagnostics.length}건`;
|
|
551
|
+
const codes = document.createElement("ul");
|
|
552
|
+
const diagnosticCounts = new Map();
|
|
553
|
+
for (const { code } of viewState.diagnostics) {
|
|
554
|
+
diagnosticCounts.set(code, (diagnosticCounts.get(code) ?? 0) + 1);
|
|
555
|
+
}
|
|
556
|
+
for (const [diagnosticCode, count] of diagnosticCounts) {
|
|
557
|
+
const item = document.createElement("li");
|
|
558
|
+
const code = document.createElement("code");
|
|
559
|
+
code.textContent = diagnosticCode;
|
|
560
|
+
item.append(code, ` · ${count}건`);
|
|
561
|
+
codes.append(item);
|
|
562
|
+
}
|
|
563
|
+
diagnostics.append(summary, codes);
|
|
564
|
+
guidance.append(diagnostics);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
elements.stateMessage.append(heading, copy, guidance);
|
|
568
|
+
}
|
|
569
|
+
|
|
489
570
|
function renderSessions() {
|
|
490
571
|
const visibleSessions = filteredSessions();
|
|
491
572
|
const hasFilters = elements.search.value.trim() || elements.statusFilter.value !== "all";
|
|
492
573
|
|
|
574
|
+
elements.toolbar.hidden = viewState.sessions.length === 0;
|
|
493
575
|
elements.stateMessage.hidden = false;
|
|
494
576
|
|
|
495
577
|
elements.sessionList.replaceChildren();
|
|
@@ -523,11 +605,7 @@ function renderSessions() {
|
|
|
523
605
|
|
|
524
606
|
if (!viewState.sessions.length) {
|
|
525
607
|
elements.sessionList.hidden = true;
|
|
526
|
-
|
|
527
|
-
"empty",
|
|
528
|
-
"아직 관찰된 task가 없습니다.",
|
|
529
|
-
"Codex에서 task나 subagent가 시작되면 이곳에 나타납니다.",
|
|
530
|
-
);
|
|
608
|
+
setEmptyObservationMessage();
|
|
531
609
|
return;
|
|
532
610
|
}
|
|
533
611
|
|
|
@@ -597,6 +675,7 @@ async function refreshState() {
|
|
|
597
675
|
const nextState = normalizeState(await response.json());
|
|
598
676
|
viewState.updatedAtMs = nextState.updatedAtMs;
|
|
599
677
|
viewState.sessions = nextState.sessions;
|
|
678
|
+
viewState.diagnostics = nextState.diagnostics;
|
|
600
679
|
viewState.hasLoaded = true;
|
|
601
680
|
viewState.errorMessage = "";
|
|
602
681
|
viewState.canRetry = true;
|
package/public/index.html
CHANGED
|
@@ -95,20 +95,25 @@
|
|
|
95
95
|
</p>
|
|
96
96
|
</div>
|
|
97
97
|
|
|
98
|
-
<form
|
|
98
|
+
<form
|
|
99
|
+
class="toolbar"
|
|
100
|
+
role="search"
|
|
101
|
+
aria-label="자동 수신된 task와 subagent 목록 필터"
|
|
102
|
+
hidden
|
|
103
|
+
>
|
|
99
104
|
<div class="field field-search">
|
|
100
|
-
<label for="session-search"
|
|
105
|
+
<label for="session-search">목록 필터 (선택)</label>
|
|
101
106
|
<input
|
|
102
107
|
id="session-search"
|
|
103
108
|
name="query"
|
|
104
109
|
type="search"
|
|
105
|
-
placeholder="
|
|
110
|
+
placeholder="자동 수신된 task·agent 목록에서 찾기"
|
|
106
111
|
autocomplete="off"
|
|
107
112
|
spellcheck="false"
|
|
108
113
|
>
|
|
109
114
|
</div>
|
|
110
115
|
<div class="field">
|
|
111
|
-
<label for="status-filter"
|
|
116
|
+
<label for="status-filter">상태 필터 (선택)</label>
|
|
112
117
|
<select id="status-filter" name="status">
|
|
113
118
|
<option value="all">모든 상태</option>
|
|
114
119
|
<option value="running">실행 중</option>
|
package/public/styles.css
CHANGED
|
@@ -362,6 +362,10 @@ h1 {
|
|
|
362
362
|
gap: var(--space-3);
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
.toolbar[hidden] {
|
|
366
|
+
display: none;
|
|
367
|
+
}
|
|
368
|
+
|
|
365
369
|
.field {
|
|
366
370
|
display: grid;
|
|
367
371
|
gap: var(--space-2);
|
|
@@ -425,6 +429,74 @@ h1 {
|
|
|
425
429
|
color: var(--text-secondary);
|
|
426
430
|
}
|
|
427
431
|
|
|
432
|
+
.state-empty-observation {
|
|
433
|
+
justify-items: stretch;
|
|
434
|
+
text-align: left;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.state-empty-observation > strong,
|
|
438
|
+
.state-empty-observation > span {
|
|
439
|
+
justify-self: center;
|
|
440
|
+
text-align: center;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.empty-guidance {
|
|
444
|
+
width: min(42rem, 100%);
|
|
445
|
+
margin-top: var(--space-5);
|
|
446
|
+
padding: var(--space-4);
|
|
447
|
+
background: var(--surface-raised);
|
|
448
|
+
border: 1px solid var(--border-default);
|
|
449
|
+
border-radius: var(--radius-sm);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.empty-guidance h3 {
|
|
453
|
+
margin-bottom: var(--space-3);
|
|
454
|
+
font-size: var(--text-sm);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.automatic-tracking {
|
|
458
|
+
margin-bottom: var(--space-3);
|
|
459
|
+
color: var(--text-secondary);
|
|
460
|
+
font-size: var(--text-sm);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.empty-guidance ol {
|
|
464
|
+
margin: 0;
|
|
465
|
+
padding-left: 1.25rem;
|
|
466
|
+
color: var(--text-secondary);
|
|
467
|
+
font-size: var(--text-sm);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.empty-guidance li + li {
|
|
471
|
+
margin-top: var(--space-2);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.observation-boundary {
|
|
475
|
+
margin: var(--space-4) 0 0;
|
|
476
|
+
padding-top: var(--space-3);
|
|
477
|
+
color: var(--text-muted);
|
|
478
|
+
border-top: 1px solid var(--border-default);
|
|
479
|
+
font-size: var(--text-xs);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.diagnostic-details {
|
|
483
|
+
margin-top: var(--space-3);
|
|
484
|
+
color: var(--text-secondary);
|
|
485
|
+
font-size: var(--text-sm);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.diagnostic-details summary {
|
|
489
|
+
min-height: 2.75rem;
|
|
490
|
+
padding: var(--space-2) 0;
|
|
491
|
+
cursor: pointer;
|
|
492
|
+
font-weight: 700;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.diagnostic-details ul {
|
|
496
|
+
margin: 0;
|
|
497
|
+
padding-left: 1.25rem;
|
|
498
|
+
}
|
|
499
|
+
|
|
428
500
|
.state-message.state-error {
|
|
429
501
|
background: var(--danger-soft);
|
|
430
502
|
border-color: color-mix(in srgb, var(--danger) 40%, transparent);
|
|
@@ -21,6 +21,20 @@ function createSession(event) {
|
|
|
21
21
|
last_seen_at_ms: event.received_at_ms,
|
|
22
22
|
agents: new Map(),
|
|
23
23
|
tools: new Map(),
|
|
24
|
+
lifecycle: {
|
|
25
|
+
start_observed: false,
|
|
26
|
+
end_observed: false,
|
|
27
|
+
started_at_ms: null,
|
|
28
|
+
ended_at_ms: null,
|
|
29
|
+
has_out_of_order_events: false,
|
|
30
|
+
},
|
|
31
|
+
root_turn: {
|
|
32
|
+
status: "idle",
|
|
33
|
+
turn_id: null,
|
|
34
|
+
started_at_ms: null,
|
|
35
|
+
stopped_at_ms: null,
|
|
36
|
+
has_out_of_order_events: false,
|
|
37
|
+
},
|
|
24
38
|
permission: { status: "idle" },
|
|
25
39
|
recent_activities: [],
|
|
26
40
|
};
|
|
@@ -31,11 +45,15 @@ function deriveSessionStatus(session) {
|
|
|
31
45
|
return "waiting_for_user";
|
|
32
46
|
}
|
|
33
47
|
if (
|
|
48
|
+
session.root_turn.status === "running" ||
|
|
34
49
|
[...session.agents.values()].some(({ status }) => status === "running") ||
|
|
35
50
|
[...session.tools.values()].some(({ status }) => status === "running")
|
|
36
51
|
) {
|
|
37
52
|
return "running";
|
|
38
53
|
}
|
|
54
|
+
if (session.lifecycle.end_observed) {
|
|
55
|
+
return "completed";
|
|
56
|
+
}
|
|
39
57
|
return "observed";
|
|
40
58
|
}
|
|
41
59
|
|
|
@@ -54,11 +72,10 @@ function addActivity(session, event, status, limit) {
|
|
|
54
72
|
const activity = {
|
|
55
73
|
type: event.type,
|
|
56
74
|
status,
|
|
57
|
-
turn_id: event.turn_id,
|
|
58
75
|
received_at_ms: event.received_at_ms,
|
|
59
76
|
};
|
|
60
77
|
|
|
61
|
-
for (const field of ["agent_id", "agent_type", "tool_name", "tool_use_id"]) {
|
|
78
|
+
for (const field of ["turn_id", "agent_id", "agent_type", "tool_name", "tool_use_id"]) {
|
|
62
79
|
if (field in event) {
|
|
63
80
|
activity[field] = event[field];
|
|
64
81
|
}
|
|
@@ -70,6 +87,71 @@ function addActivity(session, event, status, limit) {
|
|
|
70
87
|
}
|
|
71
88
|
}
|
|
72
89
|
|
|
90
|
+
function applySessionEvent(session, event, limits) {
|
|
91
|
+
const lifecycle = session.lifecycle;
|
|
92
|
+
if (event.type === "session_started") {
|
|
93
|
+
const resumedAfterEnd = lifecycle.end_observed;
|
|
94
|
+
lifecycle.start_observed = true;
|
|
95
|
+
lifecycle.started_at_ms ??= event.received_at_ms;
|
|
96
|
+
lifecycle.end_observed = false;
|
|
97
|
+
lifecycle.ended_at_ms = null;
|
|
98
|
+
lifecycle.has_out_of_order_events ||= resumedAfterEnd;
|
|
99
|
+
addActivity(session, event, "observed", limits.maxActivitiesPerSession);
|
|
100
|
+
return "applied";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (lifecycle.end_observed) {
|
|
104
|
+
return "duplicate";
|
|
105
|
+
}
|
|
106
|
+
lifecycle.end_observed = true;
|
|
107
|
+
lifecycle.ended_at_ms = event.received_at_ms;
|
|
108
|
+
lifecycle.has_out_of_order_events = !lifecycle.start_observed;
|
|
109
|
+
session.root_turn.status = "completed";
|
|
110
|
+
session.root_turn.stopped_at_ms ??= event.received_at_ms;
|
|
111
|
+
session.permission = { status: "idle" };
|
|
112
|
+
addActivity(session, event, "completed", limits.maxActivitiesPerSession);
|
|
113
|
+
return "applied";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function applyTurnEvent(session, event, limits) {
|
|
117
|
+
const turn = session.root_turn;
|
|
118
|
+
if (event.type === "turn_started") {
|
|
119
|
+
if (turn.turn_id === event.turn_id && turn.status === "running") {
|
|
120
|
+
return "duplicate";
|
|
121
|
+
}
|
|
122
|
+
session.root_turn = {
|
|
123
|
+
status: "running",
|
|
124
|
+
turn_id: event.turn_id,
|
|
125
|
+
started_at_ms: event.received_at_ms,
|
|
126
|
+
stopped_at_ms: null,
|
|
127
|
+
has_out_of_order_events: false,
|
|
128
|
+
};
|
|
129
|
+
session.permission = { status: "idle" };
|
|
130
|
+
addActivity(session, event, "running", limits.maxActivitiesPerSession);
|
|
131
|
+
return "applied";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (turn.turn_id === event.turn_id && turn.status === "completed") {
|
|
135
|
+
return "duplicate";
|
|
136
|
+
}
|
|
137
|
+
const startObserved = turn.turn_id === event.turn_id && turn.started_at_ms !== null;
|
|
138
|
+
session.root_turn = {
|
|
139
|
+
status: "completed",
|
|
140
|
+
turn_id: event.turn_id,
|
|
141
|
+
started_at_ms: startObserved ? turn.started_at_ms : null,
|
|
142
|
+
stopped_at_ms: event.received_at_ms,
|
|
143
|
+
has_out_of_order_events: !startObserved,
|
|
144
|
+
};
|
|
145
|
+
session.permission = { status: "idle" };
|
|
146
|
+
addActivity(
|
|
147
|
+
session,
|
|
148
|
+
event,
|
|
149
|
+
startObserved ? "completed" : "completed_without_start",
|
|
150
|
+
limits.maxActivitiesPerSession,
|
|
151
|
+
);
|
|
152
|
+
return "applied";
|
|
153
|
+
}
|
|
154
|
+
|
|
73
155
|
function applySubagentEvent(session, event, limits) {
|
|
74
156
|
let agent = session.agents.get(event.agent_id);
|
|
75
157
|
if (!agent) {
|
|
@@ -212,6 +294,12 @@ function applyPermissionEvent(session, event, limits) {
|
|
|
212
294
|
}
|
|
213
295
|
|
|
214
296
|
function applyEvent(session, event, limits) {
|
|
297
|
+
if (event.type === "session_started" || event.type === "session_ended") {
|
|
298
|
+
return applySessionEvent(session, event, limits);
|
|
299
|
+
}
|
|
300
|
+
if (event.type === "turn_started" || event.type === "turn_stopped") {
|
|
301
|
+
return applyTurnEvent(session, event, limits);
|
|
302
|
+
}
|
|
215
303
|
if (event.type === "subagent_started" || event.type === "subagent_stopped") {
|
|
216
304
|
return applySubagentEvent(session, event, limits);
|
|
217
305
|
}
|
|
@@ -230,6 +318,7 @@ function snapshotSession(session) {
|
|
|
230
318
|
agents: [...session.agents.values()]
|
|
231
319
|
.map(({ start_observed, stop_observed, ...agent }) => ({ ...agent }))
|
|
232
320
|
.sort((left, right) => right.last_seen_at_ms - left.last_seen_at_ms),
|
|
321
|
+
root_turn: { ...session.root_turn },
|
|
233
322
|
recent_activities: session.recent_activities.map((activity) => ({
|
|
234
323
|
...activity,
|
|
235
324
|
})),
|
|
@@ -2,10 +2,16 @@ const NORMALIZED_EVENT_TYPES = Object.freeze({
|
|
|
2
2
|
PermissionRequest: "permission_requested",
|
|
3
3
|
PostToolUse: "tool_completed",
|
|
4
4
|
PreToolUse: "tool_started",
|
|
5
|
+
SessionEnd: "session_ended",
|
|
6
|
+
SessionStart: "session_started",
|
|
7
|
+
Stop: "turn_stopped",
|
|
5
8
|
SubagentStart: "subagent_started",
|
|
6
9
|
SubagentStop: "subagent_stopped",
|
|
10
|
+
UserPromptSubmit: "turn_started",
|
|
7
11
|
});
|
|
8
12
|
|
|
13
|
+
const SESSION_EVENT_TYPES = new Set(["session_started", "session_ended"]);
|
|
14
|
+
|
|
9
15
|
const MAX_IDENTIFIER_LENGTH = 512;
|
|
10
16
|
const MAX_LABEL_LENGTH = 256;
|
|
11
17
|
|
|
@@ -58,8 +64,8 @@ function commonEvent(payload, type, receivedAtMs) {
|
|
|
58
64
|
source: "hook",
|
|
59
65
|
type,
|
|
60
66
|
session_id: payload.session_id,
|
|
61
|
-
turn_id: payload.turn_id,
|
|
62
67
|
received_at_ms: receivedAtMs,
|
|
68
|
+
...(typeof payload.turn_id === "string" ? { turn_id: payload.turn_id } : {}),
|
|
63
69
|
};
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -88,7 +94,10 @@ export function normalizeHookPayload(payload, options = {}) {
|
|
|
88
94
|
return ignored("unsupported_hook_event", receivedAtMs, "hook_event_name");
|
|
89
95
|
}
|
|
90
96
|
|
|
91
|
-
|
|
97
|
+
const commonFields = SESSION_EVENT_TYPES.has(type)
|
|
98
|
+
? ["session_id"]
|
|
99
|
+
: ["session_id", "turn_id"];
|
|
100
|
+
for (const field of commonFields) {
|
|
92
101
|
const error = requiredString(payload, field, receivedAtMs);
|
|
93
102
|
if (error) {
|
|
94
103
|
return error;
|
|
@@ -143,4 +152,3 @@ export function normalizeHookPayload(payload, options = {}) {
|
|
|
143
152
|
|
|
144
153
|
return { status: "accepted", event };
|
|
145
154
|
}
|
|
146
|
-
|